mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
fix: bottom sheet updates apply immediately (#4220)
* fix: mobile field editor * fix: immediately update select option * fix: insert left and right field flow * fix: create row behavior
This commit is contained in:
parent
ca7b186325
commit
ce58737ec5
@ -2,7 +2,9 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
|||||||
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
|
import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
|
||||||
import 'package:appflowy/mobile/presentation/database/field/mobile_field_type_option_editor.dart';
|
import 'package:appflowy/mobile/presentation/database/field/mobile_field_type_option_editor.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/field/field_backend_service.dart';
|
import 'package:appflowy/plugins/database_view/application/field/field_backend_service.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/setting/field_visibility_extension.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -20,7 +22,7 @@ class MobileEditPropertyScreen extends StatefulWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
final String viewId;
|
final String viewId;
|
||||||
final FieldPB field;
|
final FieldInfo field;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MobileEditPropertyScreen> createState() =>
|
State<MobileEditPropertyScreen> createState() =>
|
||||||
@ -28,12 +30,17 @@ class MobileEditPropertyScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MobileEditPropertyScreenState extends State<MobileEditPropertyScreen> {
|
class _MobileEditPropertyScreenState extends State<MobileEditPropertyScreen> {
|
||||||
late FieldOptionValues optionValues;
|
late final FieldBackendService fieldService;
|
||||||
|
late FieldOptionValues field;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
optionValues = FieldOptionValues.fromField(field: widget.field);
|
field = FieldOptionValues.fromField(field: widget.field.field);
|
||||||
|
fieldService = FieldBackendService(
|
||||||
|
viewId: widget.viewId,
|
||||||
|
fieldId: widget.field.id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -47,24 +54,40 @@ class _MobileEditPropertyScreenState extends State<MobileEditPropertyScreen> {
|
|||||||
title: FlowyText.medium(
|
title: FlowyText.medium(
|
||||||
LocaleKeys.grid_field_editProperty.tr(),
|
LocaleKeys.grid_field_editProperty.tr(),
|
||||||
),
|
),
|
||||||
leading: AppBarCancelButton(
|
leading: AppBarBackButton(
|
||||||
onTap: () => context.pop(),
|
onTap: () => context.pop(),
|
||||||
),
|
),
|
||||||
leadingWidth: 120,
|
|
||||||
actions: [
|
|
||||||
_SaveButton(
|
|
||||||
onSave: () {
|
|
||||||
context.pop(optionValues);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
body: FieldOptionEditor(
|
body: FieldOptionEditor(
|
||||||
mode: FieldOptionMode.edit,
|
mode: FieldOptionMode.edit,
|
||||||
isPrimary: widget.field.isPrimary,
|
isPrimary: widget.field.isPrimary,
|
||||||
defaultValues: optionValues,
|
defaultValues: field,
|
||||||
onOptionValuesChanged: (optionValues) {
|
actions: [
|
||||||
this.optionValues = optionValues;
|
if (widget.field.fieldSettings?.visibility.isVisibleState() ?? true)
|
||||||
|
FieldOptionAction.hide
|
||||||
|
else
|
||||||
|
FieldOptionAction.show,
|
||||||
|
FieldOptionAction.duplicate,
|
||||||
|
FieldOptionAction.delete,
|
||||||
|
],
|
||||||
|
onOptionValuesChanged: (newField) async {
|
||||||
|
if (newField.name != field.name) {
|
||||||
|
await fieldService.updateField(name: newField.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newField.type != field.type) {
|
||||||
|
await fieldService.updateFieldType(fieldType: newField.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = newField.getTypeOptionData();
|
||||||
|
if (data != null) {
|
||||||
|
await FieldBackendService.updateFieldTypeOption(
|
||||||
|
viewId: viewId,
|
||||||
|
fieldId: widget.field.id,
|
||||||
|
typeOptionData: data,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// setState(() => field = newField);
|
||||||
},
|
},
|
||||||
onAction: (action) {
|
onAction: (action) {
|
||||||
final service = FieldServices(
|
final service = FieldServices(
|
||||||
@ -81,6 +104,9 @@ class _MobileEditPropertyScreenState extends State<MobileEditPropertyScreen> {
|
|||||||
case FieldOptionAction.hide:
|
case FieldOptionAction.hide:
|
||||||
service.hide();
|
service.hide();
|
||||||
break;
|
break;
|
||||||
|
case FieldOptionAction.show:
|
||||||
|
service.show();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
context.pop();
|
context.pop();
|
||||||
},
|
},
|
||||||
@ -88,28 +114,3 @@ class _MobileEditPropertyScreenState extends State<MobileEditPropertyScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SaveButton extends StatelessWidget {
|
|
||||||
const _SaveButton({
|
|
||||||
required this.onSave,
|
|
||||||
});
|
|
||||||
|
|
||||||
final VoidCallback onSave;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 16.0),
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: onSave,
|
|
||||||
child: FlowyText.medium(
|
|
||||||
LocaleKeys.button_save.tr(),
|
|
||||||
color: const Color(0xFF00ADDC),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
import 'package:appflowy/plugins/database_view/application/field/field_controller.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/field/field_service.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
@ -12,7 +12,11 @@ import 'mobile_field_type_grid.dart';
|
|||||||
import 'mobile_field_type_option_editor.dart';
|
import 'mobile_field_type_option_editor.dart';
|
||||||
import 'mobile_quick_field_editor.dart';
|
import 'mobile_quick_field_editor.dart';
|
||||||
|
|
||||||
void showCreateFieldBottomSheet(BuildContext context, String viewId) {
|
void showCreateFieldBottomSheet(
|
||||||
|
BuildContext context,
|
||||||
|
String viewId, {
|
||||||
|
OrderObjectPositionPB? position,
|
||||||
|
}) {
|
||||||
showMobileBottomSheet(
|
showMobileBottomSheet(
|
||||||
context,
|
context,
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
@ -36,7 +40,7 @@ void showCreateFieldBottomSheet(BuildContext context, String viewId) {
|
|||||||
).toString(),
|
).toString(),
|
||||||
);
|
);
|
||||||
if (optionValues != null) {
|
if (optionValues != null) {
|
||||||
await optionValues.create(viewId: viewId);
|
await optionValues.create(viewId: viewId, position: position);
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
context.pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
@ -57,32 +61,9 @@ Future<FieldOptionValues?> showEditFieldScreen(
|
|||||||
MobileEditPropertyScreen.routeName,
|
MobileEditPropertyScreen.routeName,
|
||||||
extra: {
|
extra: {
|
||||||
MobileEditPropertyScreen.argViewId: viewId,
|
MobileEditPropertyScreen.argViewId: viewId,
|
||||||
MobileEditPropertyScreen.argField: field.field,
|
MobileEditPropertyScreen.argField: field,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (optionValues != null) {
|
|
||||||
final service = FieldBackendService(
|
|
||||||
viewId: viewId,
|
|
||||||
fieldId: field.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (optionValues.name != field.name) {
|
|
||||||
await service.updateField(name: optionValues.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (optionValues.type != field.fieldType) {
|
|
||||||
await service.updateFieldType(fieldType: optionValues.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
final data = optionValues.toTypeOptionBuffer();
|
|
||||||
if (data != null) {
|
|
||||||
await FieldBackendService.updateFieldTypeOption(
|
|
||||||
viewId: viewId,
|
|
||||||
fieldId: field.id,
|
|
||||||
typeOptionData: data,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return optionValues;
|
return optionValues;
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,7 @@ class _Header extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
onPressed: () => context.pop(newFieldId),
|
onPressed: () => context.pop(newFieldId),
|
||||||
child: FlowyText.medium(
|
child: FlowyText.medium(
|
||||||
LocaleKeys.button_save.tr(),
|
LocaleKeys.button_done.tr(),
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
@ -55,16 +55,18 @@ class FieldOptionValues {
|
|||||||
|
|
||||||
Future<void> create({
|
Future<void> create({
|
||||||
required String viewId,
|
required String viewId,
|
||||||
|
OrderObjectPositionPB? position,
|
||||||
}) async {
|
}) async {
|
||||||
await FieldBackendService.createField(
|
await FieldBackendService.createField(
|
||||||
viewId: viewId,
|
viewId: viewId,
|
||||||
fieldType: type,
|
fieldType: type,
|
||||||
fieldName: name,
|
fieldName: name,
|
||||||
typeOptionData: toTypeOptionBuffer(),
|
typeOptionData: getTypeOptionData(),
|
||||||
|
position: position,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Uint8List? toTypeOptionBuffer() {
|
Uint8List? getTypeOptionData() {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case FieldType.RichText:
|
case FieldType.RichText:
|
||||||
case FieldType.URL:
|
case FieldType.URL:
|
||||||
@ -124,6 +126,7 @@ class FieldOptionValues {
|
|||||||
|
|
||||||
enum FieldOptionAction {
|
enum FieldOptionAction {
|
||||||
hide,
|
hide,
|
||||||
|
show,
|
||||||
duplicate,
|
duplicate,
|
||||||
delete,
|
delete,
|
||||||
}
|
}
|
||||||
@ -134,6 +137,7 @@ class FieldOptionEditor extends StatefulWidget {
|
|||||||
required this.mode,
|
required this.mode,
|
||||||
required this.defaultValues,
|
required this.defaultValues,
|
||||||
required this.onOptionValuesChanged,
|
required this.onOptionValuesChanged,
|
||||||
|
this.actions = const [],
|
||||||
this.onAction,
|
this.onAction,
|
||||||
this.isPrimary = false,
|
this.isPrimary = false,
|
||||||
});
|
});
|
||||||
@ -143,6 +147,7 @@ class FieldOptionEditor extends StatefulWidget {
|
|||||||
final void Function(FieldOptionValues values) onOptionValuesChanged;
|
final void Function(FieldOptionValues values) onOptionValuesChanged;
|
||||||
|
|
||||||
// only used in edit mode
|
// only used in edit mode
|
||||||
|
final List<FieldOptionAction> actions;
|
||||||
final void Function(FieldOptionAction action)? onAction;
|
final void Function(FieldOptionAction action)? onAction;
|
||||||
|
|
||||||
// the primary field can't be deleted, duplicated, and changed type
|
// the primary field can't be deleted, duplicated, and changed type
|
||||||
@ -273,34 +278,44 @@ class _FieldOptionEditorState extends State<FieldOptionEditor> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildOptionActions() {
|
List<Widget> _buildOptionActions() {
|
||||||
return switch (widget.mode) {
|
if (widget.mode == FieldOptionMode.add || widget.actions.isEmpty) {
|
||||||
FieldOptionMode.add => [],
|
return [];
|
||||||
FieldOptionMode.edit => [
|
}
|
||||||
FlowyOptionTile.text(
|
|
||||||
text: LocaleKeys.grid_field_hide.tr(),
|
return [
|
||||||
leftIcon: const FlowySvg(FlowySvgs.hide_s),
|
if (widget.actions.contains(FieldOptionAction.hide))
|
||||||
onTap: () => widget.onAction?.call(FieldOptionAction.hide),
|
FlowyOptionTile.text(
|
||||||
|
text: LocaleKeys.grid_field_hide.tr(),
|
||||||
|
leftIcon: const FlowySvg(FlowySvgs.hide_s),
|
||||||
|
onTap: () => widget.onAction?.call(FieldOptionAction.hide),
|
||||||
|
),
|
||||||
|
if (widget.actions.contains(FieldOptionAction.show))
|
||||||
|
FlowyOptionTile.text(
|
||||||
|
text: LocaleKeys.grid_field_show.tr(),
|
||||||
|
leftIcon: const FlowySvg(FlowySvgs.show_m, size: Size.square(16)),
|
||||||
|
onTap: () => widget.onAction?.call(FieldOptionAction.show),
|
||||||
|
),
|
||||||
|
if (widget.actions.contains(FieldOptionAction.duplicate) &&
|
||||||
|
!widget.isPrimary)
|
||||||
|
FlowyOptionTile.text(
|
||||||
|
showTopBorder: false,
|
||||||
|
text: LocaleKeys.button_duplicate.tr(),
|
||||||
|
leftIcon: const FlowySvg(FlowySvgs.copy_s),
|
||||||
|
onTap: () => widget.onAction?.call(FieldOptionAction.duplicate),
|
||||||
|
),
|
||||||
|
if (widget.actions.contains(FieldOptionAction.delete) &&
|
||||||
|
!widget.isPrimary)
|
||||||
|
FlowyOptionTile.text(
|
||||||
|
showTopBorder: false,
|
||||||
|
text: LocaleKeys.button_delete.tr(),
|
||||||
|
textColor: Theme.of(context).colorScheme.error,
|
||||||
|
leftIcon: FlowySvg(
|
||||||
|
FlowySvgs.delete_s,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
),
|
),
|
||||||
if (!widget.isPrimary) ...[
|
onTap: () => widget.onAction?.call(FieldOptionAction.delete),
|
||||||
FlowyOptionTile.text(
|
),
|
||||||
showTopBorder: false,
|
];
|
||||||
text: LocaleKeys.button_duplicate.tr(),
|
|
||||||
leftIcon: const FlowySvg(FlowySvgs.copy_s),
|
|
||||||
onTap: () => widget.onAction?.call(FieldOptionAction.duplicate),
|
|
||||||
),
|
|
||||||
FlowyOptionTile.text(
|
|
||||||
showTopBorder: false,
|
|
||||||
text: LocaleKeys.button_delete.tr(),
|
|
||||||
textColor: Theme.of(context).colorScheme.error,
|
|
||||||
leftIcon: FlowySvg(
|
|
||||||
FlowySvgs.delete_s,
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
onTap: () => widget.onAction?.call(FieldOptionAction.delete),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateOptionValues({
|
void _updateOptionValues({
|
||||||
|
@ -6,11 +6,13 @@ import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_
|
|||||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/field/field_backend_service.dart';
|
import 'package:appflowy/plugins/database_view/application/field/field_backend_service.dart';
|
||||||
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
|
||||||
|
import 'package:appflowy/plugins/database_view/widgets/setting/field_visibility_extension.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:protobuf/protobuf.dart' hide FieldInfo;
|
||||||
|
|
||||||
class QuickEditField extends StatefulWidget {
|
class QuickEditField extends StatefulWidget {
|
||||||
const QuickEditField({
|
const QuickEditField({
|
||||||
@ -35,12 +37,15 @@ class _QuickEditFieldState extends State<QuickEditField> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
late FieldType fieldType;
|
late FieldType fieldType;
|
||||||
|
late FieldVisibility fieldVisibility;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
fieldType = widget.fieldInfo.fieldType;
|
fieldType = widget.fieldInfo.fieldType;
|
||||||
|
fieldVisibility = widget.fieldInfo.fieldSettings?.visibility ??
|
||||||
|
FieldVisibility.AlwaysShown;
|
||||||
controller.text = widget.fieldInfo.field.name;
|
controller.text = widget.fieldInfo.field.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,10 +74,14 @@ class _QuickEditFieldState extends State<QuickEditField> {
|
|||||||
text: LocaleKeys.grid_field_editProperty.tr(),
|
text: LocaleKeys.grid_field_editProperty.tr(),
|
||||||
leftIcon: const FlowySvg(FlowySvgs.edit_s),
|
leftIcon: const FlowySvg(FlowySvgs.edit_s),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
|
widget.fieldInfo.field.freeze();
|
||||||
|
final field = widget.fieldInfo.field.rebuild((field) {
|
||||||
|
field.name = controller.text;
|
||||||
|
});
|
||||||
final optionValues = await showEditFieldScreen(
|
final optionValues = await showEditFieldScreen(
|
||||||
context,
|
context,
|
||||||
widget.viewId,
|
widget.viewId,
|
||||||
widget.fieldInfo,
|
widget.fieldInfo.copyWith(field: field),
|
||||||
);
|
);
|
||||||
if (optionValues != null) {
|
if (optionValues != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -85,11 +94,17 @@ class _QuickEditFieldState extends State<QuickEditField> {
|
|||||||
if (!widget.fieldInfo.isPrimary)
|
if (!widget.fieldInfo.isPrimary)
|
||||||
FlowyOptionTile.text(
|
FlowyOptionTile.text(
|
||||||
showTopBorder: false,
|
showTopBorder: false,
|
||||||
text: LocaleKeys.grid_field_hide.tr(),
|
text: fieldVisibility.isVisibleState()
|
||||||
|
? LocaleKeys.grid_field_hide.tr()
|
||||||
|
: LocaleKeys.grid_field_show.tr(),
|
||||||
leftIcon: const FlowySvg(FlowySvgs.hide_s),
|
leftIcon: const FlowySvg(FlowySvgs.hide_s),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
context.pop();
|
context.pop();
|
||||||
await service.hide();
|
if (fieldVisibility.isVisibleState()) {
|
||||||
|
await service.hide();
|
||||||
|
} else {
|
||||||
|
await service.hide();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (!widget.fieldInfo.isPrimary)
|
if (!widget.fieldInfo.isPrimary)
|
||||||
@ -99,7 +114,14 @@ class _QuickEditFieldState extends State<QuickEditField> {
|
|||||||
leftIcon: const FlowySvg(FlowySvgs.insert_left_s),
|
leftIcon: const FlowySvg(FlowySvgs.insert_left_s),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
context.pop();
|
context.pop();
|
||||||
await service.insertLeft();
|
showCreateFieldBottomSheet(
|
||||||
|
context,
|
||||||
|
widget.viewId,
|
||||||
|
position: OrderObjectPositionPB(
|
||||||
|
position: OrderObjectPositionTypePB.Before,
|
||||||
|
objectId: widget.fieldInfo.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
FlowyOptionTile.text(
|
FlowyOptionTile.text(
|
||||||
@ -108,7 +130,14 @@ class _QuickEditFieldState extends State<QuickEditField> {
|
|||||||
leftIcon: const FlowySvg(FlowySvgs.insert_right_s),
|
leftIcon: const FlowySvg(FlowySvgs.insert_right_s),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
context.pop();
|
context.pop();
|
||||||
await service.insertRight();
|
showCreateFieldBottomSheet(
|
||||||
|
context,
|
||||||
|
widget.viewId,
|
||||||
|
position: OrderObjectPositionPB(
|
||||||
|
position: OrderObjectPositionTypePB.After,
|
||||||
|
objectId: widget.fieldInfo.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (!widget.fieldInfo.isPrimary) ...[
|
if (!widget.fieldInfo.isPrimary) ...[
|
||||||
|
@ -110,7 +110,19 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
|
|||||||
)..add(const DatabasePropertyEvent.initial()),
|
)..add(const DatabasePropertyEvent.initial()),
|
||||||
child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>(
|
child: BlocBuilder<DatabasePropertyBloc, DatabasePropertyState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final cells = state.fieldContexts
|
if (state.fieldContexts.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
final fields = [...state.fieldContexts];
|
||||||
|
final firstField = fields.removeAt(0);
|
||||||
|
final firstCell = DatabaseFieldListTile(
|
||||||
|
key: ValueKey(firstField.id),
|
||||||
|
viewId: view.id,
|
||||||
|
fieldController: databaseController.fieldController,
|
||||||
|
fieldInfo: firstField,
|
||||||
|
showTopBorder: true,
|
||||||
|
);
|
||||||
|
final cells = fields
|
||||||
.mapIndexed(
|
.mapIndexed(
|
||||||
(index, field) => DatabaseFieldListTile(
|
(index, field) => DatabaseFieldListTile(
|
||||||
key: ValueKey(field.id),
|
key: ValueKey(field.id),
|
||||||
@ -118,14 +130,14 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
|
|||||||
fieldController: databaseController.fieldController,
|
fieldController: databaseController.fieldController,
|
||||||
fieldInfo: field,
|
fieldInfo: field,
|
||||||
index: index,
|
index: index,
|
||||||
showTopBorder: index == 0,
|
showTopBorder: false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
return ReorderableListView.builder(
|
return ReorderableListView.builder(
|
||||||
proxyDecorator: (_, index, anim) {
|
proxyDecorator: (_, index, anim) {
|
||||||
final field = state.fieldContexts[index];
|
final field = fields[index];
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: anim,
|
animation: anim,
|
||||||
builder: (BuildContext context, Widget? child) {
|
builder: (BuildContext context, Widget? child) {
|
||||||
@ -151,10 +163,13 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
|
|||||||
buildDefaultDragHandles: true,
|
buildDefaultDragHandles: true,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
onReorder: (from, to) {
|
onReorder: (from, to) {
|
||||||
|
from++;
|
||||||
|
to++;
|
||||||
context
|
context
|
||||||
.read<DatabasePropertyBloc>()
|
.read<DatabasePropertyBloc>()
|
||||||
.add(DatabasePropertyEvent.moveField(from, to));
|
.add(DatabasePropertyEvent.moveField(from, to));
|
||||||
},
|
},
|
||||||
|
header: firstCell,
|
||||||
footer: Column(
|
footer: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@ -176,14 +191,14 @@ class _MobileDatabaseFieldListBody extends StatelessWidget {
|
|||||||
class DatabaseFieldListTile extends StatelessWidget {
|
class DatabaseFieldListTile extends StatelessWidget {
|
||||||
const DatabaseFieldListTile({
|
const DatabaseFieldListTile({
|
||||||
super.key,
|
super.key,
|
||||||
required this.index,
|
this.index,
|
||||||
required this.fieldInfo,
|
required this.fieldInfo,
|
||||||
required this.viewId,
|
required this.viewId,
|
||||||
required this.fieldController,
|
required this.fieldController,
|
||||||
required this.showTopBorder,
|
required this.showTopBorder,
|
||||||
});
|
});
|
||||||
|
|
||||||
final int index;
|
final int? index;
|
||||||
final FieldInfo fieldInfo;
|
final FieldInfo fieldInfo;
|
||||||
final String viewId;
|
final String viewId;
|
||||||
final FieldController fieldController;
|
final FieldController fieldController;
|
||||||
|
@ -36,6 +36,13 @@ class FieldServices {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> show() async {
|
||||||
|
await fieldSettingsService.updateFieldSettings(
|
||||||
|
fieldId: fieldId,
|
||||||
|
fieldVisibility: FieldVisibility.AlwaysShown,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> delete() async {
|
Future<void> delete() async {
|
||||||
await fieldBackendService.delete();
|
await fieldBackendService.delete();
|
||||||
}
|
}
|
||||||
|
@ -27,15 +27,20 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
|||||||
_startListening();
|
_startListening();
|
||||||
await _openGrid(emit);
|
await _openGrid(emit);
|
||||||
},
|
},
|
||||||
createRow: () async {
|
createRow: (openRowDetail) async {
|
||||||
final result = await RowBackendService.createRow(viewId: viewId);
|
final result = await RowBackendService.createRow(viewId: viewId);
|
||||||
result.fold(
|
result.fold(
|
||||||
(createdRow) => emit(state.copyWith(createdRow: createdRow)),
|
(createdRow) => emit(
|
||||||
|
state.copyWith(
|
||||||
|
createdRow: createdRow,
|
||||||
|
openRowDetail: openRowDetail ?? false,
|
||||||
|
),
|
||||||
|
),
|
||||||
(err) => Log.error(err),
|
(err) => Log.error(err),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
resetCreatedRow: () {
|
resetCreatedRow: () {
|
||||||
emit(state.copyWith(createdRow: null));
|
emit(state.copyWith(createdRow: null, openRowDetail: false));
|
||||||
},
|
},
|
||||||
deleteRow: (rowInfo) async {
|
deleteRow: (rowInfo) async {
|
||||||
await RowBackendService.deleteRow(rowInfo.viewId, rowInfo.rowId);
|
await RowBackendService.deleteRow(rowInfo.viewId, rowInfo.rowId);
|
||||||
@ -151,7 +156,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
|||||||
@freezed
|
@freezed
|
||||||
class GridEvent with _$GridEvent {
|
class GridEvent with _$GridEvent {
|
||||||
const factory GridEvent.initial() = InitialGrid;
|
const factory GridEvent.initial() = InitialGrid;
|
||||||
const factory GridEvent.createRow() = _CreateRow;
|
const factory GridEvent.createRow({bool? openRowDetail}) = _CreateRow;
|
||||||
const factory GridEvent.resetCreatedRow() = _ResetCreatedRow;
|
const factory GridEvent.resetCreatedRow() = _ResetCreatedRow;
|
||||||
const factory GridEvent.deleteRow(RowInfo rowInfo) = _DeleteRow;
|
const factory GridEvent.deleteRow(RowInfo rowInfo) = _DeleteRow;
|
||||||
const factory GridEvent.moveRow(int from, int to) = _MoveRow;
|
const factory GridEvent.moveRow(int from, int to) = _MoveRow;
|
||||||
@ -187,6 +192,7 @@ class GridState with _$GridState {
|
|||||||
required ChangedReason reason,
|
required ChangedReason reason,
|
||||||
required List<SortInfo> sorts,
|
required List<SortInfo> sorts,
|
||||||
required List<FilterInfo> filters,
|
required List<FilterInfo> filters,
|
||||||
|
required bool openRowDetail,
|
||||||
}) = _GridState;
|
}) = _GridState;
|
||||||
|
|
||||||
factory GridState.initial(String viewId) => GridState(
|
factory GridState.initial(String viewId) => GridState(
|
||||||
@ -201,5 +207,6 @@ class GridState with _$GridState {
|
|||||||
reason: const InitialListState(),
|
reason: const InitialListState(),
|
||||||
filters: [],
|
filters: [],
|
||||||
sorts: [],
|
sorts: [],
|
||||||
|
openRowDetail: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -139,7 +139,7 @@ class _GridPageContentState extends State<GridPageContent> {
|
|||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) =>
|
||||||
previous.createdRow != current.createdRow,
|
previous.createdRow != current.createdRow,
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.createdRow == null) {
|
if (state.createdRow == null || !state.openRowDetail) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final bloc = context.read<GridBloc>();
|
final bloc = context.read<GridBloc>();
|
||||||
|
@ -39,7 +39,9 @@ Widget getGridFabs(BuildContext context) {
|
|||||||
backgroundColor: Theme.of(context).primaryColor,
|
backgroundColor: Theme.of(context).primaryColor,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.read<GridBloc>().add(const GridEvent.createRow());
|
context
|
||||||
|
.read<GridBloc>()
|
||||||
|
.add(const GridEvent.createRow(openRowDetail: true));
|
||||||
},
|
},
|
||||||
overlayColor: const MaterialStatePropertyAll<Color>(Color(0xFF009FD1)),
|
overlayColor: const MaterialStatePropertyAll<Color>(Color(0xFF009FD1)),
|
||||||
boxShadow: const BoxShadow(
|
boxShadow: const BoxShadow(
|
||||||
|
@ -10,7 +10,6 @@ import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_c
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/size.dart';
|
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
@ -64,14 +63,8 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
|||||||
const DragHandler(),
|
const DragHandler(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
child: _buildHeader(
|
child: _buildHeader(context),
|
||||||
context,
|
|
||||||
showSaveButton: state.createOption
|
|
||||||
.fold(() => false, (a) => a.isNotEmpty) ||
|
|
||||||
showMoreOptions,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Divider(),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
@ -88,7 +81,7 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeader(BuildContext context, {required bool showSaveButton}) {
|
Widget _buildHeader(BuildContext context) {
|
||||||
const iconWidth = 36.0;
|
const iconWidth = 36.0;
|
||||||
const height = 44.0;
|
const height = 44.0;
|
||||||
return Stack(
|
return Stack(
|
||||||
@ -96,9 +89,9 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
|||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: FlowyIconButton(
|
child: FlowyIconButton(
|
||||||
icon: const FlowySvg(
|
icon: FlowySvg(
|
||||||
FlowySvgs.close_s,
|
showMoreOptions ? FlowySvgs.arrow_left_s : FlowySvgs.close_s,
|
||||||
size: Size.square(iconWidth),
|
size: const Size.square(iconWidth),
|
||||||
),
|
),
|
||||||
width: iconWidth,
|
width: iconWidth,
|
||||||
iconPadding: EdgeInsets.zero,
|
iconPadding: EdgeInsets.zero,
|
||||||
@ -115,54 +108,6 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: !showSaveButton
|
|
||||||
? const HSpace(iconWidth)
|
|
||||||
: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 2.0,
|
|
||||||
horizontal: 8.0,
|
|
||||||
),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Color(0xFF00bcf0),
|
|
||||||
borderRadius: Corners.s10Border,
|
|
||||||
),
|
|
||||||
child: FlowyButton(
|
|
||||||
text: FlowyText(
|
|
||||||
LocaleKeys.button_save.tr(),
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
useIntrinsicWidth: true,
|
|
||||||
onTap: () {
|
|
||||||
if (showMoreOptions) {
|
|
||||||
final option = this.option;
|
|
||||||
if (option == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
option.freeze();
|
|
||||||
context.read<SelectOptionCellEditorBloc>().add(
|
|
||||||
SelectOptionEditorEvent.updateOption(
|
|
||||||
option.rebuild((p0) {
|
|
||||||
if (p0.name != renameController.text) {
|
|
||||||
p0.name = renameController.text;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_popOrBack();
|
|
||||||
} else if (typingOption.isNotEmpty) {
|
|
||||||
context.read<SelectOptionCellEditorBloc>().add(
|
|
||||||
SelectOptionEditorEvent.trySelectOption(
|
|
||||||
typingOption,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
searchController.clear();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
].map((e) => SizedBox(height: height, child: e)).toList(),
|
].map((e) => SizedBox(height: height, child: e)).toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -170,13 +115,13 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
|||||||
Widget _buildBody(BuildContext context) {
|
Widget _buildBody(BuildContext context) {
|
||||||
if (showMoreOptions && option != null) {
|
if (showMoreOptions && option != null) {
|
||||||
return _MoreOptions(
|
return _MoreOptions(
|
||||||
option: option!,
|
initialOption: option!,
|
||||||
controller: renameController..text = option!.name,
|
controller: renameController,
|
||||||
onDelete: () {
|
onDelete: () {
|
||||||
context
|
context
|
||||||
.read<SelectOptionCellEditorBloc>()
|
.read<SelectOptionCellEditorBloc>()
|
||||||
.add(SelectOptionEditorEvent.deleteOption(option!));
|
.add(SelectOptionEditorEvent.deleteOption(option!));
|
||||||
context.pop();
|
_popOrBack();
|
||||||
},
|
},
|
||||||
onUpdate: (name, color) {
|
onUpdate: (name, color) {
|
||||||
final option = this.option;
|
final option = this.option;
|
||||||
@ -196,7 +141,6 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_popOrBack();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -244,6 +188,7 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
|||||||
onMoreOptions: (option) {
|
onMoreOptions: (option) {
|
||||||
setState(() {
|
setState(() {
|
||||||
this.option = option;
|
this.option = option;
|
||||||
|
renameController.text = option.name;
|
||||||
showMoreOptions = true;
|
showMoreOptions = true;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -461,19 +406,26 @@ class _CreateOptionCell extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MoreOptions extends StatelessWidget {
|
class _MoreOptions extends StatefulWidget {
|
||||||
const _MoreOptions({
|
const _MoreOptions({
|
||||||
required this.option,
|
required this.initialOption,
|
||||||
required this.onDelete,
|
required this.onDelete,
|
||||||
required this.onUpdate,
|
required this.onUpdate,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
});
|
});
|
||||||
|
|
||||||
final SelectOptionPB option;
|
final SelectOptionPB initialOption;
|
||||||
final VoidCallback onDelete;
|
final VoidCallback onDelete;
|
||||||
final void Function(String? name, SelectOptionColorPB? color) onUpdate;
|
final void Function(String? name, SelectOptionColorPB? color) onUpdate;
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_MoreOptions> createState() => _MoreOptionsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MoreOptionsState extends State<_MoreOptions> {
|
||||||
|
late SelectOptionPB option = widget.initialOption;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final color = Theme.of(context).colorScheme.secondaryContainer;
|
final color = Theme.of(context).colorScheme.secondaryContainer;
|
||||||
@ -481,7 +433,6 @@ class _MoreOptions extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const VSpace(8.0),
|
|
||||||
_buildRenameTextField(context),
|
_buildRenameTextField(context),
|
||||||
const VSpace(16.0),
|
const VSpace(16.0),
|
||||||
_buildDeleteButton(context),
|
_buildDeleteButton(context),
|
||||||
@ -491,8 +442,9 @@ class _MoreOptions extends StatelessWidget {
|
|||||||
child: ColoredBox(
|
child: ColoredBox(
|
||||||
color: color,
|
color: color,
|
||||||
child: FlowyText(
|
child: FlowyText(
|
||||||
LocaleKeys.grid_field_optionTitle.tr(),
|
LocaleKeys.grid_selectOption_colorPanelTitle.tr().toUpperCase(),
|
||||||
color: Theme.of(context).hintColor,
|
color: Theme.of(context).hintColor,
|
||||||
|
fontSize: 13,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -508,7 +460,13 @@ class _MoreOptions extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: OptionColorList(
|
child: OptionColorList(
|
||||||
selectedColor: option.color,
|
selectedColor: option.color,
|
||||||
onSelectedColor: (color) => onUpdate(null, color),
|
onSelectedColor: (color) {
|
||||||
|
widget.onUpdate(null, color);
|
||||||
|
setState(() {
|
||||||
|
option.freeze();
|
||||||
|
option = option.rebuild((option) => option.color = color);
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -521,7 +479,8 @@ class _MoreOptions extends StatelessWidget {
|
|||||||
return ConstrainedBox(
|
return ConstrainedBox(
|
||||||
constraints: const BoxConstraints.tightFor(height: 52.0),
|
constraints: const BoxConstraints.tightFor(height: 52.0),
|
||||||
child: FlowyOptionTile.textField(
|
child: FlowyOptionTile.textField(
|
||||||
controller: controller,
|
onTextChanged: (name) => widget.onUpdate(name, null),
|
||||||
|
controller: widget.controller,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -530,7 +489,7 @@ class _MoreOptions extends StatelessWidget {
|
|||||||
return FlowyOptionTile.text(
|
return FlowyOptionTile.text(
|
||||||
text: LocaleKeys.button_delete.tr(),
|
text: LocaleKeys.button_delete.tr(),
|
||||||
leftIcon: const FlowySvg(FlowySvgs.delete_s),
|
leftIcon: const FlowySvg(FlowySvgs.delete_s),
|
||||||
onTap: onDelete,
|
onTap: widget.onDelete,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -620,7 +620,7 @@
|
|||||||
"aquaColor": "Aqua",
|
"aquaColor": "Aqua",
|
||||||
"blueColor": "Blue",
|
"blueColor": "Blue",
|
||||||
"deleteTag": "Delete tag",
|
"deleteTag": "Delete tag",
|
||||||
"colorPanelTitle": "Colors",
|
"colorPanelTitle": "Color",
|
||||||
"panelTitle": "Select an option or create one",
|
"panelTitle": "Select an option or create one",
|
||||||
"searchOption": "Search for an option",
|
"searchOption": "Search for an option",
|
||||||
"searchOrCreateOption": "Search or create an option...",
|
"searchOrCreateOption": "Search or create an option...",
|
||||||
@ -778,7 +778,6 @@
|
|||||||
"textBlock": {
|
"textBlock": {
|
||||||
"placeholder": "Type '/' for commands"
|
"placeholder": "Type '/' for commands"
|
||||||
},
|
},
|
||||||
|
|
||||||
"title": {
|
"title": {
|
||||||
"placeholder": "Untitled"
|
"placeholder": "Untitled"
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user