Skip to content
代码片段 群组 项目
提交 f8df35e6 编辑于 作者: miaolu's avatar miaolu 提交者: ling zhang
浏览文件

fix: #247 创建议题页面样式问题

上级 77dd7e6e
No related branches found
No related tags found
无相关合并请求
显示 112 个添加118 个删除
assets/images/create_issuue_add_icon.png

278 字节

assets/images/photo_icon.png

1.7 KB

......@@ -28,7 +28,7 @@ class _InputFieldWithTitleState extends State<InputFieldWithTitle> {
alignment: Alignment.center,
child: TextField(
controller: widget.controller,
style: textTheme.bodySmall,
style: const TextStyle(color: Color(0xFF03162F)),
maxLines: widget.maxLines,
cursorColor: const Color(0xFFFC6D26),
decoration: InputDecoration(hintText: widget.placeHolder, border: const OutlineInputBorder(borderSide: BorderSide.none), contentPadding: const EdgeInsets.only(left: 8))))
......
......@@ -52,6 +52,7 @@ class _MarkdownInputBoxState extends State<MarkdownInputBox> with TickerProvider
alignment: Alignment.centerLeft,
constraints: const BoxConstraints(maxWidth: 150),
height: 40,
transform: Matrix4.translationValues(-16, 0, 0),
child: TabBar(
isScrollable: false,
padding: EdgeInsets.zero,
......@@ -61,7 +62,7 @@ class _MarkdownInputBoxState extends State<MarkdownInputBox> with TickerProvider
labelColor: Colors.black,
labelStyle: textTheme.titleLarge,
unselectedLabelColor: Colors.black54,
labelPadding: EdgeInsets.zero,
labelPadding: const EdgeInsets.only(bottom: 8, top: 8),
splashFactory: NoSplash.splashFactory,
tabs: [for (String tab in MarkdownInputBox._tabs) Text(tab)],
onTap: (index) {
......@@ -72,29 +73,15 @@ class _MarkdownInputBoxState extends State<MarkdownInputBox> with TickerProvider
)),
const Flexible(fit: FlexFit.tight, child: SizedBox()),
InkWell(
onTap: () => _onAtButtonPressed(),
child: Container(
decoration: const BoxDecoration(color: Colors.white),
alignment: Alignment.centerRight,
height: 40,
width: 50,
child: const Icon(
Icons.alternate_email,
color: Colors.black26,
),
),
),
onTap: () => _onAtButtonPressed(),
child: Container(alignment: Alignment.centerRight, height: 40, width: 50, child: const Icon(Icons.alternate_email, color: Color(0xFF87878C), size: 19))),
InkWell(
onTap: () => _onImageButtonPressed(),
child: Container(
decoration: const BoxDecoration(color: Colors.white),
alignment: Alignment.centerRight,
height: 40,
width: 50,
child: const Icon(
Icons.photo_outlined,
color: Colors.black26,
),
width: 30,
child: Image.asset("assets/images/photo_icon.png", height: 16, width: 16),
),
),
],
......
import 'package:jihu_gitlab_app/core/settings/settings_provider.dart';
class DescriptionContent {
String content;
String pathWithNamespace;
DescriptionContent._(this.content, this.pathWithNamespace);
factory DescriptionContent.init(String content, String pathWithNamespace) {
return DescriptionContent._(content, pathWithNamespace);
}
String get value {
return content.replaceAll("](/uploads/", "](${SettingsProvider().baseUrl}/$pathWithNamespace/uploads/").replaceAll('<br>', '\n').replaceAll('<br/>', '\n');
}
}
import 'package:jihu_gitlab_app/core/settings/settings_provider.dart';
import 'package:jihu_gitlab_app/core/time.dart';
import 'package:jihu_gitlab_app/domain/description_content.dart';
import 'assignee.dart';
......@@ -24,11 +24,7 @@ class IssueDetail {
}
factory IssueDetail.fromJson(Map<String, dynamic> issueJson, Map<String, dynamic> projectJson) {
var description = (issueJson['description'] ?? '')
.toString()
.replaceAll("[image](/uploads/", "[image](${SettingsProvider().baseUrl}/${projectJson['path_with_namespace']}/uploads/")
.replaceAll('<br>', '\n')
.replaceAll('<br/>', '\n');
String description = DescriptionContent.init(issueJson['description'] ?? '', projectJson['path_with_namespace']).value;
return IssueDetail._(issueJson['id'] ?? 0, issueJson['iid'] ?? 0, issueJson['project_id'] ?? 0, issueJson['title'] ?? '', description, issueJson['created_at'] ?? '', projectJson['name'] ?? '',
Assignee.fromJson(issueJson['author']));
}
......
import 'package:jihu_gitlab_app/core/settings/settings_provider.dart';
import 'package:jihu_gitlab_app/core/time.dart';
import 'package:jihu_gitlab_app/domain/description_content.dart';
import 'assignee.dart';
......@@ -14,7 +14,7 @@ class Note {
Note._(this.id, this.createdAt, this.body, this.system, this.author, this.type);
factory Note.fromJson(Map<String, dynamic> json, String pathWithNamespace) {
var replaceAll = json['body'].toString().replaceAll("[image](/uploads/", "[image](${SettingsProvider().baseUrl}/$pathWithNamespace/uploads/");
return Note._(json['id'], Time.init(json['created_at']), replaceAll, json['system'], Assignee.fromJson(json['author']), json['type'] ?? "");
var description = DescriptionContent.init(json['body'], pathWithNamespace).value;
return Note._(json['id'], Time.init(json['created_at']), description, json['system'], Assignee.fromJson(json['author']), json['type'] ?? "");
}
}
......@@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:core';
import 'package:flutter/material.dart';
import 'package:jihu_gitlab_app/core/load_state.dart';
import 'package:jihu_gitlab_app/core/loader.dart';
import 'package:jihu_gitlab_app/core/token_provider.dart';
import 'package:jihu_gitlab_app/core/widgets/avatar_and_name.dart';
......@@ -90,35 +89,40 @@ class _IssueCreationPageState extends State<IssueCreationPage> {
Navigator.pop(context, true);
}
}),
actions: [
Container(
transform: Matrix4.translationValues(-12, 2, 0),
child: TextButton(
onPressed: () {
var title = _issueTitleController.text;
var description = _issueDescriptionController.text;
var labels = _model.selectedLabels.map((e) => e.name).toList().join(",");
Loader.showProgress(context);
_model.create(widget.projectId, title, description: description, labels: labels).then((value) {
Loader.hideProgress(context);
if (value) {
Toast.success(context, 'Created successfully');
if (widget.from == 'group') {
Navigator.pop(context);
Navigator.pop(context, true);
} else {
Navigator.pop(context, true);
}
} else {
Toast.error(context, 'Failed to create');
}
});
},
child: Text('Create', style: TextStyle(color: Theme.of(context).primaryColor))),
),
],
),
body: SafeArea(child: Consumer(
builder: (context, tokenProvider, child) {
if (!TokenProvider.authorized) {
return const UnauthorizedView();
}
return IssueCreationView(
projectId: widget.projectId,
model: _model,
issueTitleController: _issueTitleController,
issueDescriptionController: _issueDescriptionController,
onCreateIssueTapped: ({required String title, String? description, String? labels}) async {
Loader.showProgress(context);
_model.create(widget.projectId, title, description: description, labels: labels).then((value) {
Loader.hideProgress(context);
if (value) {
Toast.success(context, 'Created successfully');
if (widget.from == 'group') {
Navigator.pop(context);
Navigator.pop(context, true);
} else {
Navigator.pop(context, true);
}
} else {
Toast.error(context, 'Failed to create');
}
});
},
);
return IssueCreationView(projectId: widget.projectId, model: _model, issueTitleController: _issueTitleController, issueDescriptionController: _issueDescriptionController);
},
))));
}
......
......@@ -3,11 +3,10 @@ part of 'issue_creation_page.dart';
typedef CreateIssueCallback = void Function({required String title, String? description, String? labels});
class IssueCreationView extends StatefulWidget {
const IssueCreationView({super.key, required this.projectId, required this.model, required this.onCreateIssueTapped, required this.issueTitleController, required this.issueDescriptionController});
const IssueCreationView({super.key, required this.projectId, required this.model, required this.issueTitleController, required this.issueDescriptionController});
final int projectId;
final IssueCreationModel model;
final CreateIssueCallback onCreateIssueTapped;
final TextEditingController issueTitleController;
final TextEditingController issueDescriptionController;
......@@ -20,7 +19,6 @@ class _IssueCreationViewState extends State<IssueCreationView> {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Stack(children: [
Container(
padding: EdgeInsets.only(bottom: _bottomActionBoxHeight),
......@@ -28,60 +26,24 @@ class _IssueCreationViewState extends State<IssueCreationView> {
physics: const ScrollPhysics(),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
InputFieldWithTitle(title: 'Title (required)', placeHolder: 'Write a issue title', controller: widget.issueTitleController),
_buildTitle("Description"),
_buildTitle("Template"),
_buildDescriptionTemplatesSelector(),
_buildDescriptionTitle("Description"),
MarkdownInputBox(projectId: widget.projectId, controller: widget.issueDescriptionController),
// _buildTitle("Assignees"),
// _buildAssigneeSelector(),
_buildAssigneeSelector(),
_buildTitle("Labels"),
_buildSelectLabelView(),
]))),
Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
width: double.infinity,
height: _bottomActionBoxHeight,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Spacer(flex: 1),
Expanded(
flex: 10,
child: InkWell(
onTap: () {
Navigator.pop(context);
},
child: Container(
decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(4))),
alignment: Alignment.center,
child: Text('Cancel', style: TextStyle(color: colorScheme.secondary, fontSize: 14, fontWeight: FontWeight.w600)),
),
)),
const Spacer(flex: 2),
Expanded(
flex: 14,
child: widget.model.createIssueState == LoadState.loadingState
? const Center(child: CircularProgressIndicator())
: InkWell(
onTap: () {
var title = widget.issueTitleController.text;
var description = widget.issueDescriptionController.text;
var labels = widget.model.selectedLabels.map((e) => e.name).toList().join(",");
widget.onCreateIssueTapped(title: title, description: description, labels: labels);
},
child: Container(
alignment: Alignment.center,
decoration: const BoxDecoration(color: Color(0xFFFC6D26), borderRadius: BorderRadius.all(Radius.circular(4))),
child: const Text('Create issue', style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600)),
),
)),
const Spacer(flex: 1)
],
)))
]);
}
Padding _buildDescriptionTitle(String title) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Text(title, style: const TextStyle(color: Color(0XFF03162F), fontSize: 16, fontWeight: FontWeight.w600)),
);
}
Padding _buildTitle(String title) {
return Padding(
padding: const EdgeInsets.all(16.0),
......@@ -101,7 +63,7 @@ class _IssueCreationViewState extends State<IssueCreationView> {
});
}
},
icon: const Icon(Icons.add));
icon: const Icon(Icons.add, size: 22, color: Color(0xFF87878C)));
final List<Widget> children = [];
if (widget.model.selectedLabels.isEmpty) {
children.add(const Text("Select label(s)", style: TextStyle(color: Color(0XFF66696D), fontSize: 14, fontWeight: FontWeight.w400)));
......@@ -110,7 +72,7 @@ class _IssueCreationViewState extends State<IssueCreationView> {
children.add(_buildLabelView(e));
}
}
children.add(iconButton);
children.add(Container(transform: Matrix4.translationValues(12, 0, 0), child: iconButton));
return Container(
width: double.infinity,
decoration: const BoxDecoration(border: Border.fromBorderSide(BorderSide(color: Color(0XFFEAEAEA), width: 1)), borderRadius: BorderRadius.all(Radius.circular(4)), color: Colors.white),
......@@ -179,10 +141,10 @@ class _IssueCreationViewState extends State<IssueCreationView> {
widget.issueDescriptionController.text = content;
setState(() {});
},
icon: const Icon(Icons.add));
icon: const Icon(Icons.add, size: 22, color: Color(0xFF87878C)));
final List<Widget> children = [];
children.add(Text(widget.model.templateName ?? 'Choose a template', style: const TextStyle(color: Color(0XFF66696D), fontSize: 14, fontWeight: FontWeight.w400)));
children.add(iconButton);
children.add(Container(transform: Matrix4.translationValues(12, 0, 0), child: iconButton));
return Container(
width: double.infinity,
decoration: const BoxDecoration(border: Border.fromBorderSide(BorderSide(color: Color(0XFFEAEAEA), width: 1)), borderRadius: BorderRadius.all(Radius.circular(4)), color: Colors.white),
......
......@@ -70,7 +70,7 @@ class _SelectableState<T> extends State<Selectable<T>> {
_onSelectedCallback();
}
},
icon: const Icon(Icons.add));
icon: const Icon(Icons.add, size: 22, color: Color(0xFF87878C)));
final List<Widget> children = [];
if (hasNoData) {
children.add(const Text("Select assignee(s)", style: TextStyle(color: Color(0XFF66696D), fontSize: 14, fontWeight: FontWeight.w400)));
......@@ -79,7 +79,7 @@ class _SelectableState<T> extends State<Selectable<T>> {
children.add(_buildSelectedItem(e));
}
}
children.add(iconButton);
children.add(Container(transform: Matrix4.translationValues(12, 0, 0), child: iconButton));
return Container(
width: double.infinity,
decoration: const BoxDecoration(border: Border.fromBorderSide(BorderSide(color: Color(0XFFEAEAEA), width: 1)), borderRadius: BorderRadius.all(Radius.circular(4)), color: Colors.white),
......
import 'package:flutter_test/flutter_test.dart';
import 'package:jihu_gitlab_app/domain/description_content.dart';
void main() {
test("Include uploads no image", () {
String content = "![创建议题页面样式问题](/uploads/ab1ce029b7a1f057e80105d04ef7e0ae/创建议题页面样式问题.png)";
var descriptionContent = DescriptionContent.init(content, "123");
expect(descriptionContent.value, "![创建议题页面样式问题](/123/uploads/ab1ce029b7a1f057e80105d04ef7e0ae/创建议题页面样式问题.png)");
});
test("Include uploads has image", () {
String content = "![image](/uploads/ab1ce029b7a1f057e80105d04ef7e0ae/创建议题页面样式问题.png)";
var descriptionContent = DescriptionContent.init(content, "123");
expect(descriptionContent.value, "![image](/123/uploads/ab1ce029b7a1f057e80105d04ef7e0ae/创建议题页面样式问题.png)");
});
test("Include uploads content", () {
String content = "/uploads";
var descriptionContent = DescriptionContent.init(content, "123");
expect(descriptionContent.value, "/uploads");
});
test("Include uploads with https", () {
String content = "![image](https://jihulab.com/123/uploads/ab1ce029b7a1f057e80105d04ef7e0ae/创建议题页面样式问题.png)";
var descriptionContent = DescriptionContent.init(content, "123");
expect(descriptionContent.value, "![image](https://jihulab.com/123/uploads/ab1ce029b7a1f057e80105d04ef7e0ae/创建议题页面样式问题.png)");
});
}
......@@ -72,9 +72,8 @@ void main() {
await tester.tap(find.text('旗舰版演示 / Demo / Go Demo'));
await tester.pumpAndSettle();
expect(find.byType(IssueCreationPage), findsOneWidget);
expect(find.text('Create issue'), findsNWidgets(2));
expect(find.text('Create'), findsOneWidget);
expect(find.text('Title (required)'), findsOneWidget);
expect(find.text('Cancel'), findsOneWidget);
// 用户填入了一些信息
await tester.enterText(
......@@ -89,7 +88,7 @@ void main() {
'Demo description');
// 用户点击了创建issue按钮
await tester.tap(find.widgetWithText(Container, 'Create issue'));
await tester.tap(find.text('Create'));
await tester.pumpAndSettle(const Duration(seconds: 1));
// 创建成功,返回了上一个页面
......@@ -131,9 +130,8 @@ void main() {
await tester.tap(find.text('旗舰版演示 / Demo / Go Demo'));
await tester.pumpAndSettle();
expect(find.byType(IssueCreationPage), findsOneWidget);
expect(find.text('Create issue'), findsNWidgets(2));
expect(find.text('Create'), findsOneWidget);
expect(find.text('Title (required)'), findsOneWidget);
expect(find.text('Cancel'), findsOneWidget);
// 用户填入了一些信息
await tester.enterText(
......@@ -183,9 +181,8 @@ void main() {
await tester.tap(find.text('旗舰版演示 / Demo / Go Demo'));
await tester.pumpAndSettle();
expect(find.byType(IssueCreationPage), findsOneWidget);
expect(find.text('Create issue'), findsNWidgets(2));
expect(find.text('Create'), findsOneWidget);
expect(find.text('Title (required)'), findsOneWidget);
expect(find.text('Cancel'), findsOneWidget);
// 用户添加description的template
await tester.tap(find.byKey(const Key("goto-description-selector")));
......
......@@ -57,10 +57,9 @@ void main() {
),
));
await tester.pumpAndSettle();
expect(find.text('Create issue'), findsNWidgets(2));
expect(find.text('Create'), findsOneWidget);
expect(find.text('Title (required)'), findsOneWidget);
expect(find.text('Write a issue title'), findsOneWidget);
expect(find.text('Cancel'), findsOneWidget);
expect(find.byType(MarkdownInputBox), findsOneWidget);
UserProvider().fullReset();
......@@ -70,6 +69,8 @@ void main() {
testWidgets('Should create issue with title and description success', (WidgetTester tester) async {
await TokenProvider().reset(Tester.token());
UserProvider().reset(Tester.user());
when(client.get<Map<String, dynamic>>("/api/v4/user")).thenAnswer((_) => Future(() => Response.of<Map<String, dynamic>>({'id': 5882})));
HttpClient.setInstance(client);
await tester.pumpWidget(const MaterialApp(
home: Scaffold(
body: IssueCreationPage(projectId: 10000, from: 'group', groupId: 0),
......@@ -90,8 +91,11 @@ void main() {
(widget) => widget is TextField && widget.decoration?.hintText == 'Write a comment here...',
),
issueDescription);
await tester.tap(find.text('Create issue').at(1));
await tester.pumpAndSettle();
await tester.tap(find.text('Create'));
for (int i = 0; i < 5; i++) {
await tester.pumpAndSettle(const Duration(seconds: 1));
}
UserProvider().fullReset();
TokenProvider().fullReset();
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册