mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge remote-tracking branch 'origin/main' into fix/click-selection-menu-item-delete-text
This commit is contained in:
commit
d1d5b37c14
@ -181,7 +181,7 @@
|
||||
"includeTime": " Include time",
|
||||
"dateFormatFriendly": "Month Day,Year",
|
||||
"dateFormatISO": "Year-Month-Day",
|
||||
"dateFormatLocal": "Year/Month/Day",
|
||||
"dateFormatLocal": "Month/Day/Year",
|
||||
"dateFormatUS": "Year/Month/Day",
|
||||
"timeFormat": " Time format",
|
||||
"invalidTimeFormat": "Invalid format",
|
||||
|
@ -172,7 +172,7 @@
|
||||
"includeTime": " Incluir tiempo",
|
||||
"dateFormatFriendly": "Mes Día, Año",
|
||||
"dateFormatISO": "Año-Mes-Día",
|
||||
"dateFormatLocal": "Año/Mes/Día",
|
||||
"dateFormatLocal": "Mes/Día/Año",
|
||||
"dateFormatUS": "Año/Mes/Día",
|
||||
"timeFormat": " Time format",
|
||||
"invalidTimeFormat": "Formato de tiempo",
|
||||
|
@ -170,7 +170,7 @@
|
||||
"includeTime": " Inclure l'heure",
|
||||
"dateFormatFriendly": "Mois Jour, Année",
|
||||
"dateFormatISO": "Année-Mois-Jour",
|
||||
"dateFormatLocal": "Année/Mois/Jour",
|
||||
"dateFormatLocal": "Mois/Jour/Année",
|
||||
"dateFormatUS": "Année/Mois/Jour",
|
||||
"timeFormat": " Format du temps",
|
||||
"invalidTimeFormat": "Format invalide",
|
||||
|
@ -173,7 +173,7 @@
|
||||
"includeTime": " Sertakan waktu",
|
||||
"dateFormatFriendly": "Bulan Hari,Tahun",
|
||||
"dateFormatISO": "Tahun-Bulan-Hari",
|
||||
"dateFormatLocal": "Tahun/Bulan/Hari",
|
||||
"dateFormatLocal": "Bulan/Hari/Tahun",
|
||||
"dateFormatUS": "Tahun/Bulan/Hari",
|
||||
"timeFormat": " Format waktu",
|
||||
"invalidTimeFormat": "Format yang tidak valid",
|
||||
|
@ -165,7 +165,7 @@
|
||||
"includeTime": " 時刻を含める",
|
||||
"dateFormatFriendly": "月 日,年",
|
||||
"dateFormatISO": "年-月-日",
|
||||
"dateFormatLocal": "年/月/日",
|
||||
"dateFormatLocal": "月/日/年",
|
||||
"dateFormatUS": "年/月/日",
|
||||
"timeFormat": " 時刻書式",
|
||||
"timeFormatTwelveHour": "12 時間表記",
|
||||
|
@ -63,13 +63,13 @@
|
||||
"deletePermanent": "Apagar permanentemente"
|
||||
},
|
||||
"dialogCreatePageNameHint": "Nome da página",
|
||||
"questionBubble": {
|
||||
"questionBubble": {
|
||||
"whatsNew": "O que há de novo?",
|
||||
"help": "Ajuda e Suporte",
|
||||
"debug": {
|
||||
"name": "Informação de depuração",
|
||||
"success": "Informação de depuração copiada para a área de transferência!",
|
||||
"fail": "Falha ao copiar a informação de depuração para a área de transferência"
|
||||
"name": "Informação de depuração",
|
||||
"success": "Informação de depuração copiada para a área de transferência!",
|
||||
"fail": "Falha ao copiar a informação de depuração para a área de transferência"
|
||||
}
|
||||
},
|
||||
"menuAppHeader": {
|
||||
@ -148,7 +148,7 @@
|
||||
"menu": {
|
||||
"appearance": "Aparência",
|
||||
"language": "Idioma",
|
||||
"user":"Usuário",
|
||||
"user": "Usuário",
|
||||
"open": "Abrir as Configurações"
|
||||
},
|
||||
"appearance": {
|
||||
@ -181,7 +181,7 @@
|
||||
"includeTime": "Incluir horário",
|
||||
"dateFormatFriendly": "Mês/Dia/Ano",
|
||||
"dateFormatISO": "Ano/Mês/Dia",
|
||||
"dateFormatLocal": "Ano/Mês/Dia",
|
||||
"dateFormatLocal": "Mês/Dia/Ano",
|
||||
"dateFormatUS": "Ano/Mês/Dia",
|
||||
"timeFormat": "Formato de hora",
|
||||
"invalidTimeFormat": "Formato Inválido",
|
||||
@ -231,4 +231,4 @@
|
||||
"create_new_card": "Novo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -94,9 +94,9 @@
|
||||
},
|
||||
"tooltip": {
|
||||
"darkMode": "Переключиться в тёмную тему",
|
||||
"openAsPage": "Открыть как страницу",
|
||||
"addNewRow": "Добавить новую строку",
|
||||
"openMenu": "Открыть меню"
|
||||
"openAsPage": "Открыть как страницу",
|
||||
"addNewRow": "Добавить новую строку",
|
||||
"openMenu": "Открыть меню"
|
||||
},
|
||||
"sideBar": {
|
||||
"closeSidebar": "Закрыть боковое меню",
|
||||
@ -180,7 +180,7 @@
|
||||
"includeTime": " Время",
|
||||
"dateFormatFriendly": "День Месяц, Год",
|
||||
"dateFormatISO": "Год-Месяц-День",
|
||||
"dateFormatLocal": "Год/Месяц/День",
|
||||
"dateFormatLocal": "Месяц/День/Год",
|
||||
"dateFormatUS": "Год/Месяц/День",
|
||||
"timeFormat": " Форматировать время",
|
||||
"invalidTimeFormat": "Неверный формат",
|
||||
|
@ -177,7 +177,7 @@
|
||||
"includeTime": " 包含时间",
|
||||
"dateFormatFriendly": "月 日,年",
|
||||
"dateFormatISO": "年-月-日",
|
||||
"dateFormatLocal": "年/月/日",
|
||||
"dateFormatLocal": "月/日/年",
|
||||
"dateFormatUS": "年/月/日",
|
||||
"timeFormat": " 时间格式",
|
||||
"invalidTimeFormat": "时间格式错误",
|
||||
|
@ -173,7 +173,7 @@
|
||||
"includeTime": " 包含時間",
|
||||
"dateFormatFriendly": "月 日,年",
|
||||
"dateFormatISO": "年-月-日",
|
||||
"dateFormatLocal": "年/月/日",
|
||||
"dateFormatLocal": "月/日/年",
|
||||
"dateFormatUS": "年/月/日",
|
||||
"timeFormat": " 時間格式",
|
||||
"invalidTimeFormat": "格式無效",
|
||||
|
@ -83,7 +83,9 @@ class _BoardCardState extends State<BoardCard> {
|
||||
builder: (context, state) {
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
constraints: BoxConstraints.loose(const Size(140, 200)),
|
||||
margin: const EdgeInsets.all(6),
|
||||
direction: PopoverDirection.rightWithCenterAligned,
|
||||
popupBuilder: (popoverContext) => _handlePopoverBuilder(
|
||||
context,
|
||||
@ -132,7 +134,8 @@ class _BoardCardState extends State<BoardCard> {
|
||||
throw UnimplementedError();
|
||||
case AccessoryType.more:
|
||||
return GridRowActionSheet(
|
||||
rowData: context.read<BoardCardBloc>().rowInfo());
|
||||
rowData: context.read<BoardCardBloc>().rowInfo(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -195,9 +195,6 @@ class ShareActions with ActionList<ShareActionWrapper>, FlowyOverlayDelegate {
|
||||
|
||||
ShareActions({required this.onSelected});
|
||||
|
||||
@override
|
||||
double get maxWidth => 130;
|
||||
|
||||
@override
|
||||
double get itemHeight => 22;
|
||||
|
||||
@ -233,7 +230,7 @@ class ShareActionWrapper extends ActionItem {
|
||||
ShareActionWrapper(this.inner);
|
||||
|
||||
@override
|
||||
Widget? get icon => null;
|
||||
Widget? icon(Color iconColor) => null;
|
||||
|
||||
@override
|
||||
String get name => inner.name;
|
||||
|
@ -149,18 +149,13 @@ class IGridCellController<T, D> extends Equatable {
|
||||
_cellDataPersistence = cellDataPersistence,
|
||||
_fieldNotifier = fieldNotifier,
|
||||
_fieldService = FieldService(
|
||||
gridId: cellId.gridId, fieldId: cellId.fieldContext.id),
|
||||
gridId: cellId.gridId,
|
||||
fieldId: cellId.fieldContext.id,
|
||||
),
|
||||
_cacheKey = GridCellCacheKey(
|
||||
rowId: cellId.rowId, fieldId: cellId.fieldContext.id);
|
||||
|
||||
IGridCellController<T, D> clone() {
|
||||
return IGridCellController(
|
||||
cellId: cellId,
|
||||
cellDataLoader: _cellDataLoader,
|
||||
cellCache: _cellsCache,
|
||||
fieldNotifier: _fieldNotifier,
|
||||
cellDataPersistence: _cellDataPersistence);
|
||||
}
|
||||
rowId: cellId.rowId,
|
||||
fieldId: cellId.fieldContext.id,
|
||||
);
|
||||
|
||||
String get gridId => cellId.gridId;
|
||||
|
||||
@ -172,9 +167,10 @@ class IGridCellController<T, D> extends Equatable {
|
||||
|
||||
FieldType get fieldType => cellId.fieldContext.fieldType;
|
||||
|
||||
VoidCallback? startListening(
|
||||
{required void Function(T?) onCellChanged,
|
||||
VoidCallback? onCellFieldChanged}) {
|
||||
VoidCallback? startListening({
|
||||
required void Function(T?) onCellChanged,
|
||||
VoidCallback? onCellFieldChanged,
|
||||
}) {
|
||||
if (isListening) {
|
||||
Log.error("Already started. It seems like you should call clone first");
|
||||
return null;
|
||||
@ -283,7 +279,12 @@ class IGridCellController<T, D> extends Equatable {
|
||||
_loadDataOperation?.cancel();
|
||||
_loadDataOperation = Timer(const Duration(milliseconds: 10), () {
|
||||
_cellDataLoader.loadData().then((data) {
|
||||
_cellsCache.insert(_cacheKey, GridCell(object: data));
|
||||
if (data != null) {
|
||||
_cellsCache.insert(_cacheKey, GridCell(object: data));
|
||||
} else {
|
||||
_cellsCache.remove(_cacheKey);
|
||||
}
|
||||
|
||||
_cellDataNotifier?.value = data;
|
||||
});
|
||||
});
|
||||
|
@ -54,6 +54,9 @@ class SelectOptionCellEditorBloc
|
||||
selectOption: (_SelectOption value) {
|
||||
_onSelectOption(value.optionId);
|
||||
},
|
||||
trySelectOption: (_TrySelectOption value) {
|
||||
_trySelectOption(value.optionName, emit);
|
||||
},
|
||||
filterOption: (_SelectOptionFilter value) {
|
||||
_filterOption(value.optionName, emit);
|
||||
},
|
||||
@ -100,6 +103,36 @@ class SelectOptionCellEditorBloc
|
||||
}
|
||||
}
|
||||
|
||||
void _trySelectOption(
|
||||
String optionName, Emitter<SelectOptionEditorState> emit) async {
|
||||
SelectOptionPB? matchingOption;
|
||||
bool optionExistsButSelected = false;
|
||||
|
||||
for (final option in state.options) {
|
||||
if (option.name.toLowerCase() == optionName.toLowerCase()) {
|
||||
if (!state.selectedOptions.contains(option)) {
|
||||
matchingOption = option;
|
||||
break;
|
||||
} else {
|
||||
optionExistsButSelected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if there isn't a matching option at all, then create it
|
||||
if (matchingOption == null && !optionExistsButSelected) {
|
||||
_createOption(optionName);
|
||||
}
|
||||
|
||||
// if there is an unselected matching option, select it
|
||||
if (matchingOption != null) {
|
||||
_selectOptionService.select(optionId: matchingOption.id);
|
||||
}
|
||||
|
||||
// clear the filter
|
||||
emit(state.copyWith(filter: none()));
|
||||
}
|
||||
|
||||
void _filterOption(String optionName, Emitter<SelectOptionEditorState> emit) {
|
||||
final _MakeOptionResult result =
|
||||
_makeOptions(Some(optionName), state.allOptions);
|
||||
@ -187,6 +220,8 @@ class SelectOptionEditorEvent with _$SelectOptionEditorEvent {
|
||||
_DeleteOption;
|
||||
const factory SelectOptionEditorEvent.filterOption(String optionName) =
|
||||
_SelectOptionFilter;
|
||||
const factory SelectOptionEditorEvent.trySelectOption(String optionName) =
|
||||
_TrySelectOption;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
@ -65,30 +65,24 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
||||
builder: (context, state) {
|
||||
return AppFlowyPopover(
|
||||
controller: _popover,
|
||||
offset: const Offset(0, 20),
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
constraints: BoxConstraints.loose(const Size(320, 500)),
|
||||
margin: EdgeInsets.zero,
|
||||
child: SizedBox.expand(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => _showCalendar(context),
|
||||
child: MouseRegion(
|
||||
opaque: false,
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Align(
|
||||
alignment: alignment,
|
||||
child: FlowyText.medium(
|
||||
state.dateStr,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
onTap: () => _popover.show(),
|
||||
child: Align(
|
||||
alignment: alignment,
|
||||
child: FlowyText.medium(state.dateStr, fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContent) {
|
||||
final bloc = context.read<DateCellBloc>();
|
||||
return DateCellEditor(
|
||||
cellController: bloc.cellController.clone(),
|
||||
cellController: widget.cellControllerBuilder.build()
|
||||
as GridDateCellController,
|
||||
onDismissed: () => widget.onCellEditing.value = false,
|
||||
);
|
||||
},
|
||||
@ -101,10 +95,6 @@ class _DateCellState extends GridCellState<GridDateCell> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showCalendar(BuildContext context) {
|
||||
_popover.show();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_cellBloc.close();
|
||||
|
@ -2,6 +2,7 @@ import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/cell/date_cal_bloc.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:dartz/dartz.dart' show Either;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
@ -11,6 +12,7 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pbserver.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@ -39,39 +41,42 @@ class DateCellEditor extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DateCellEditor extends State<DateCellEditor> {
|
||||
DateTypeOptionPB? _dateTypeOptionPB;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchData();
|
||||
}
|
||||
|
||||
_fetchData() async {
|
||||
final result = await widget.cellController
|
||||
.getFieldTypeOption(DateTypeOptionDataParser());
|
||||
|
||||
result.fold((dateTypeOptionPB) {
|
||||
setState(() {
|
||||
_dateTypeOptionPB = dateTypeOptionPB;
|
||||
});
|
||||
}, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_dateTypeOptionPB == null) {
|
||||
return Container();
|
||||
}
|
||||
return FutureBuilder<Either<dynamic, FlowyError>>(
|
||||
future: widget.cellController.getFieldTypeOption(
|
||||
DateTypeOptionDataParser(),
|
||||
),
|
||||
builder: (BuildContext context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return _buildWidget(snapshot);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return _CellCalendarWidget(
|
||||
cellContext: widget.cellController,
|
||||
dateTypeOptionPB: _dateTypeOptionPB!,
|
||||
Widget _buildWidget(AsyncSnapshot<Either<dynamic, FlowyError>> snapshot) {
|
||||
return snapshot.data!.fold(
|
||||
(dateTypeOptionPB) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: _CellCalendarWidget(
|
||||
cellContext: widget.cellController,
|
||||
dateTypeOptionPB: dateTypeOptionPB,
|
||||
),
|
||||
);
|
||||
},
|
||||
(err) {
|
||||
Log.error(err);
|
||||
return const SizedBox();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CellCalendarWidget extends StatelessWidget {
|
||||
class _CellCalendarWidget extends StatefulWidget {
|
||||
final GridDateCellController cellContext;
|
||||
final DateTypeOptionPB dateTypeOptionPB;
|
||||
|
||||
@ -81,26 +86,43 @@ class _CellCalendarWidget extends StatelessWidget {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_CellCalendarWidget> createState() => _CellCalendarWidgetState();
|
||||
}
|
||||
|
||||
class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
|
||||
late PopoverMutex popoverMutex;
|
||||
late DateCalBloc bloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
popoverMutex = PopoverMutex();
|
||||
|
||||
bloc = DateCalBloc(
|
||||
dateTypeOptionPB: widget.dateTypeOptionPB,
|
||||
cellData: widget.cellContext.getCellData(),
|
||||
cellController: widget.cellContext,
|
||||
)..add(const DateCalEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return BlocProvider(
|
||||
create: (context) {
|
||||
return DateCalBloc(
|
||||
dateTypeOptionPB: dateTypeOptionPB,
|
||||
cellData: cellContext.getCellData(),
|
||||
cellController: cellContext,
|
||||
)..add(const DateCalEvent.initial());
|
||||
},
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
child: BlocBuilder<DateCalBloc, DateCalState>(
|
||||
buildWhen: (p, c) => false,
|
||||
builder: (context, state) {
|
||||
List<Widget> children = [
|
||||
_buildCalendar(theme, context),
|
||||
_TimeTextField(bloc: context.read<DateCalBloc>()),
|
||||
_TimeTextField(
|
||||
bloc: context.read<DateCalBloc>(),
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
Divider(height: 1, color: theme.shader5),
|
||||
const _IncludeTimeButton(),
|
||||
const _DateTypeOptionButton()
|
||||
_DateTypeOptionButton(popoverMutex: popoverMutex)
|
||||
];
|
||||
|
||||
return ListView.separated(
|
||||
@ -119,6 +141,13 @@ class _CellCalendarWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
bloc.close();
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildCalendar(AppTheme theme, BuildContext context) {
|
||||
return BlocBuilder<DateCalBloc, DateCalState>(
|
||||
builder: (context, state) {
|
||||
@ -218,8 +247,10 @@ class _IncludeTimeButton extends StatelessWidget {
|
||||
|
||||
class _TimeTextField extends StatefulWidget {
|
||||
final DateCalBloc bloc;
|
||||
final PopoverMutex popoverMutex;
|
||||
const _TimeTextField({
|
||||
required this.bloc,
|
||||
required this.popoverMutex,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -240,9 +271,18 @@ class _TimeTextFieldState extends State<_TimeTextField> {
|
||||
if (mounted) {
|
||||
widget.bloc.add(DateCalEvent.setTime(_controller.text));
|
||||
}
|
||||
|
||||
if (_focusNode.hasFocus) {
|
||||
widget.popoverMutex.close();
|
||||
}
|
||||
});
|
||||
|
||||
widget.popoverMutex.listenOnPopoverChanged(() {
|
||||
if (_focusNode.hasFocus) {
|
||||
_focusNode.unfocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -290,7 +330,11 @@ class _TimeTextFieldState extends State<_TimeTextField> {
|
||||
}
|
||||
|
||||
class _DateTypeOptionButton extends StatelessWidget {
|
||||
const _DateTypeOptionButton({Key? key}) : super(key: key);
|
||||
final PopoverMutex popoverMutex;
|
||||
const _DateTypeOptionButton({
|
||||
required this.popoverMutex,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -301,6 +345,7 @@ class _DateTypeOptionButton extends StatelessWidget {
|
||||
selector: (state) => state.dateTypeOptionPB,
|
||||
builder: (context, dateTypeOptionPB) {
|
||||
return AppFlowyPopover(
|
||||
mutex: popoverMutex,
|
||||
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
|
||||
offset: const Offset(20, 0),
|
||||
constraints: BoxConstraints.loose(const Size(140, 100)),
|
||||
@ -313,7 +358,10 @@ class _DateTypeOptionButton extends StatelessWidget {
|
||||
popupBuilder: (BuildContext popContext) {
|
||||
return _CalDateTimeSetting(
|
||||
dateTypeOptionPB: dateTypeOptionPB,
|
||||
onEvent: (event) => context.read<DateCalBloc>().add(event),
|
||||
onEvent: (event) {
|
||||
context.read<DateCalBloc>().add(event);
|
||||
popoverMutex.close();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -325,46 +373,49 @@ class _DateTypeOptionButton extends StatelessWidget {
|
||||
class _CalDateTimeSetting extends StatefulWidget {
|
||||
final DateTypeOptionPB dateTypeOptionPB;
|
||||
final Function(DateCalEvent) onEvent;
|
||||
const _CalDateTimeSetting(
|
||||
{required this.dateTypeOptionPB, required this.onEvent, Key? key})
|
||||
: super(key: key);
|
||||
const _CalDateTimeSetting({
|
||||
required this.dateTypeOptionPB,
|
||||
required this.onEvent,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_CalDateTimeSetting> createState() => _CalDateTimeSettingState();
|
||||
}
|
||||
|
||||
class _CalDateTimeSettingState extends State<_CalDateTimeSetting> {
|
||||
final timeSettingPopoverMutex = PopoverMutex();
|
||||
String? overlayIdentifier;
|
||||
final _popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> children = [
|
||||
AppFlowyPopover(
|
||||
mutex: _popoverMutex,
|
||||
asBarrier: true,
|
||||
mutex: timeSettingPopoverMutex,
|
||||
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
|
||||
offset: const Offset(20, 0),
|
||||
popupBuilder: (BuildContext context) {
|
||||
return DateFormatList(
|
||||
selectedFormat: widget.dateTypeOptionPB.dateFormat,
|
||||
onSelected: (format) =>
|
||||
widget.onEvent(DateCalEvent.setDateFormat(format)),
|
||||
onSelected: (format) {
|
||||
widget.onEvent(DateCalEvent.setDateFormat(format));
|
||||
timeSettingPopoverMutex.close();
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const DateFormatButton(),
|
||||
),
|
||||
AppFlowyPopover(
|
||||
mutex: _popoverMutex,
|
||||
asBarrier: true,
|
||||
mutex: timeSettingPopoverMutex,
|
||||
triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
|
||||
offset: const Offset(20, 0),
|
||||
popupBuilder: (BuildContext context) {
|
||||
return TimeFormatList(
|
||||
selectedFormat: widget.dateTypeOptionPB.timeFormat,
|
||||
onSelected: (format) =>
|
||||
widget.onEvent(DateCalEvent.setTimeFormat(format)),
|
||||
);
|
||||
selectedFormat: widget.dateTypeOptionPB.timeFormat,
|
||||
onSelected: (format) {
|
||||
widget.onEvent(DateCalEvent.setTimeFormat(format));
|
||||
timeSettingPopoverMutex.close();
|
||||
});
|
||||
},
|
||||
child: TimeFormatButton(timeFormat: widget.dateTypeOptionPB.timeFormat),
|
||||
),
|
||||
|
@ -154,10 +154,10 @@ class _TextField extends StatelessWidget {
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.filterOption(text));
|
||||
},
|
||||
onNewTag: (tagName) {
|
||||
onSubmitted: (tagName) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.newOption(tagName));
|
||||
.add(SelectOptionEditorEvent.trySelectOption(tagName));
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -17,7 +17,7 @@ class SelectOptionTextField extends StatefulWidget {
|
||||
final LinkedHashMap<String, SelectOptionPB> selectedOptionMap;
|
||||
final double distanceToText;
|
||||
|
||||
final Function(String) onNewTag;
|
||||
final Function(String) onSubmitted;
|
||||
final Function(String) newText;
|
||||
final VoidCallback? onClick;
|
||||
|
||||
@ -26,7 +26,7 @@ class SelectOptionTextField extends StatefulWidget {
|
||||
required this.selectedOptionMap,
|
||||
required this.distanceToText,
|
||||
required this.tagController,
|
||||
required this.onNewTag,
|
||||
required this.onSubmitted,
|
||||
required this.newText,
|
||||
this.onClick,
|
||||
TextEditingController? textController,
|
||||
@ -88,7 +88,7 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
}
|
||||
|
||||
if (text.isNotEmpty) {
|
||||
widget.onNewTag(text);
|
||||
widget.onSubmitted(text);
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
},
|
||||
|
@ -54,13 +54,11 @@ class GridURLCell extends GridCellWidget {
|
||||
GridURLCellAccessoryType ty, GridCellAccessoryBuildContext buildContext) {
|
||||
switch (ty) {
|
||||
case GridURLCellAccessoryType.edit:
|
||||
final cellController =
|
||||
cellControllerBuilder.build() as GridURLCellController;
|
||||
return GridCellAccessoryBuilder(
|
||||
builder: (Key key) => _EditURLAccessory(
|
||||
key: key,
|
||||
cellContext: cellController,
|
||||
anchorContext: buildContext.anchorContext,
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
),
|
||||
);
|
||||
|
||||
@ -191,10 +189,10 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
|
||||
}
|
||||
|
||||
class _EditURLAccessory extends StatefulWidget {
|
||||
final GridURLCellController cellContext;
|
||||
final GridCellControllerBuilder cellControllerBuilder;
|
||||
final BuildContext anchorContext;
|
||||
const _EditURLAccessory({
|
||||
required this.cellContext,
|
||||
required this.cellControllerBuilder,
|
||||
required this.anchorContext,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
@ -224,7 +222,8 @@ class _EditURLAccessoryState extends State<_EditURLAccessory>
|
||||
child: svgWidget("editor/edit", color: theme.iconColor),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return URLEditorPopover(
|
||||
cellController: widget.cellContext.clone(),
|
||||
cellController:
|
||||
widget.cellControllerBuilder.build() as GridURLCellController,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -162,6 +162,7 @@ class FieldCellButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return FlowyButton(
|
||||
radius: BorderRadius.zero,
|
||||
hoverColor: theme.shader6,
|
||||
onTap: onTap,
|
||||
leftIcon: svgWidget(field.fieldType.iconName(), color: theme.iconColor),
|
||||
|
@ -93,9 +93,9 @@ class _EditFieldButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _FieldOperationList extends StatelessWidget {
|
||||
final GridFieldCellContext fieldData;
|
||||
final GridFieldCellContext fieldContext;
|
||||
final VoidCallback onDismissed;
|
||||
const _FieldOperationList(this.fieldData, this.onDismissed, {Key? key})
|
||||
const _FieldOperationList(this.fieldContext, this.onDismissed, {Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@ -118,14 +118,14 @@ class _FieldOperationList extends StatelessWidget {
|
||||
bool enable = true;
|
||||
switch (action) {
|
||||
case FieldAction.delete:
|
||||
enable = !fieldData.field.isPrimary;
|
||||
enable = !fieldContext.field.isPrimary;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return FieldActionCell(
|
||||
fieldId: fieldData.field.id,
|
||||
fieldContext: fieldContext,
|
||||
action: action,
|
||||
onTap: onDismissed,
|
||||
enable: enable,
|
||||
@ -136,13 +136,13 @@ class _FieldOperationList extends StatelessWidget {
|
||||
}
|
||||
|
||||
class FieldActionCell extends StatelessWidget {
|
||||
final String fieldId;
|
||||
final GridFieldCellContext fieldContext;
|
||||
final VoidCallback onTap;
|
||||
final FieldAction action;
|
||||
final bool enable;
|
||||
|
||||
const FieldActionCell({
|
||||
required this.fieldId,
|
||||
required this.fieldContext,
|
||||
required this.action,
|
||||
required this.onTap,
|
||||
required this.enable,
|
||||
@ -161,7 +161,7 @@ class FieldActionCell extends StatelessWidget {
|
||||
hoverColor: theme.hover,
|
||||
onTap: () {
|
||||
if (enable) {
|
||||
action.run(context);
|
||||
action.run(context, fieldContext);
|
||||
onTap();
|
||||
}
|
||||
},
|
||||
@ -202,7 +202,7 @@ extension _FieldActionExtension on FieldAction {
|
||||
}
|
||||
}
|
||||
|
||||
void run(BuildContext context) {
|
||||
void run(BuildContext context, GridFieldCellContext fieldContext) {
|
||||
switch (this) {
|
||||
case FieldAction.hide:
|
||||
context
|
||||
@ -210,18 +210,24 @@ extension _FieldActionExtension on FieldAction {
|
||||
.add(const FieldActionSheetEvent.hideField());
|
||||
break;
|
||||
case FieldAction.duplicate:
|
||||
context
|
||||
.read<FieldActionSheetBloc>()
|
||||
.add(const FieldActionSheetEvent.duplicateField());
|
||||
PopoverContainer.of(context).close();
|
||||
|
||||
FieldService(
|
||||
gridId: fieldContext.gridId,
|
||||
fieldId: fieldContext.field.id,
|
||||
).duplicateField();
|
||||
|
||||
break;
|
||||
case FieldAction.delete:
|
||||
PopoverContainer.of(context).close();
|
||||
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||
confirm: () {
|
||||
context
|
||||
.read<FieldActionSheetBloc>()
|
||||
.add(const FieldActionSheetEvent.deleteField());
|
||||
FieldService(
|
||||
gridId: fieldContext.gridId,
|
||||
fieldId: fieldContext.field.id,
|
||||
).deleteField();
|
||||
},
|
||||
).show(context);
|
||||
|
||||
|
@ -139,7 +139,6 @@ class _FieldNameTextField extends StatefulWidget {
|
||||
|
||||
class _FieldNameTextFieldState extends State<_FieldNameTextField> {
|
||||
FocusNode focusNode = FocusNode();
|
||||
VoidCallback? _popoverCallback;
|
||||
late TextEditingController controller;
|
||||
|
||||
@override
|
||||
@ -151,6 +150,12 @@ class _FieldNameTextFieldState extends State<_FieldNameTextField> {
|
||||
}
|
||||
});
|
||||
|
||||
widget.popoverMutex.listenOnPopoverChanged(() {
|
||||
if (focusNode.hasFocus) {
|
||||
focusNode.unfocus();
|
||||
}
|
||||
});
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -176,8 +181,6 @@ class _FieldNameTextFieldState extends State<_FieldNameTextField> {
|
||||
buildWhen: (previous, current) =>
|
||||
previous.errorText != current.errorText,
|
||||
builder: (context, state) {
|
||||
listenOnPopoverChanged(context);
|
||||
|
||||
return RoundedInputField(
|
||||
height: 36,
|
||||
focusNode: focusNode,
|
||||
@ -198,18 +201,6 @@ class _FieldNameTextFieldState extends State<_FieldNameTextField> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void listenOnPopoverChanged(BuildContext context) {
|
||||
if (_popoverCallback != null) {
|
||||
widget.popoverMutex.removePopoverListener(_popoverCallback!);
|
||||
}
|
||||
_popoverCallback = widget.popoverMutex.listenOnPopoverChanged(() {
|
||||
if (focusNode.hasFocus) {
|
||||
final node = FocusScope.of(context);
|
||||
node.unfocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _DeleteFieldButton extends StatelessWidget {
|
||||
@ -236,9 +227,10 @@ class _DeleteFieldButton extends StatelessWidget {
|
||||
color: enable ? null : theme.shader4,
|
||||
),
|
||||
onTap: () => onDeleted?.call(),
|
||||
hoverColor: theme.hover,
|
||||
onHover: (_) => popoverMutex.close(),
|
||||
);
|
||||
// if (enable) button = button;
|
||||
return button;
|
||||
return SizedBox(height: 36, child: button);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -180,6 +180,7 @@ class CreateFieldButton extends StatelessWidget {
|
||||
asBarrier: true,
|
||||
constraints: BoxConstraints.loose(const Size(240, 600)),
|
||||
child: FlowyButton(
|
||||
radius: BorderRadius.zero,
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_field_newColumn.tr(),
|
||||
fontSize: 12,
|
||||
|
@ -109,6 +109,7 @@ class _RowLeadingState extends State<_RowLeading> {
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
constraints: BoxConstraints.loose(const Size(140, 200)),
|
||||
direction: PopoverDirection.rightWithCenterAligned,
|
||||
margin: const EdgeInsets.all(6),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return GridRowActionSheet(
|
||||
rowData: context.read<RowBloc>().state.rowInfo);
|
||||
|
@ -280,6 +280,7 @@ class _RowDetailCellState extends State<_RowDetailCell> {
|
||||
AppFlowyPopover(
|
||||
controller: popover,
|
||||
constraints: BoxConstraints.loose(const Size(240, 600)),
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
popupBuilder: (popoverContext) => buildFieldEditor(),
|
||||
child: SizedBox(
|
||||
width: 150,
|
||||
|
@ -55,6 +55,7 @@ class _SettingButton extends StatelessWidget {
|
||||
return AppFlowyPopover(
|
||||
constraints: BoxConstraints.loose(const Size(260, 400)),
|
||||
offset: const Offset(0, 10),
|
||||
margin: const EdgeInsets.all(6),
|
||||
child: FlowyIconButton(
|
||||
width: 22,
|
||||
hoverColor: theme.hover,
|
||||
|
@ -61,8 +61,7 @@ class ActionList {
|
||||
itemBuilder: (context, index) => items[index],
|
||||
anchorContext: anchorContext,
|
||||
anchorDirection: AnchorDirection.bottomRight,
|
||||
width: 120,
|
||||
height: 80,
|
||||
constraints: BoxConstraints.tight(const Size(120, 80)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -86,7 +86,8 @@ class MenuAppHeader extends StatelessWidget {
|
||||
?.toggle(),
|
||||
onSecondaryTap: () {
|
||||
final actionList = AppDisclosureActionSheet(
|
||||
onSelected: (action) => _handleAction(context, action));
|
||||
onSelected: (action) => _handleAction(context, action),
|
||||
);
|
||||
actionList.show(
|
||||
context,
|
||||
anchorDirection: AnchorDirection.bottomWithCenterAligned,
|
||||
@ -158,12 +159,12 @@ extension AppDisclosureExtension on AppDisclosureAction {
|
||||
}
|
||||
}
|
||||
|
||||
Widget get icon {
|
||||
Widget icon(Color iconColor) {
|
||||
switch (this) {
|
||||
case AppDisclosureAction.rename:
|
||||
return svgWidget('editor/edit', color: const Color(0xffe5e5e5));
|
||||
return svgWidget('editor/edit', color: iconColor);
|
||||
case AppDisclosureAction.delete:
|
||||
return svgWidget('editor/delete', color: const Color(0xffe5e5e5));
|
||||
return svgWidget('editor/delete', color: iconColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,12 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'header.dart';
|
||||
|
||||
class AppDisclosureActionSheet with ActionList<DisclosureActionWrapper>, FlowyOverlayDelegate {
|
||||
class AppDisclosureActionSheet
|
||||
with ActionList<DisclosureActionWrapper>, FlowyOverlayDelegate {
|
||||
final Function(dartz.Option<AppDisclosureAction>) onSelected;
|
||||
final _items = AppDisclosureAction.values.map((action) => DisclosureActionWrapper(action)).toList();
|
||||
final _items = AppDisclosureAction.values
|
||||
.map((action) => DisclosureActionWrapper(action))
|
||||
.toList();
|
||||
|
||||
AppDisclosureActionSheet({
|
||||
required this.onSelected,
|
||||
@ -17,7 +20,8 @@ class AppDisclosureActionSheet with ActionList<DisclosureActionWrapper>, FlowyOv
|
||||
List<DisclosureActionWrapper> get items => _items;
|
||||
|
||||
@override
|
||||
void Function(dartz.Option<DisclosureActionWrapper> p1) get selectCallback => (result) {
|
||||
void Function(dartz.Option<DisclosureActionWrapper> p1) get selectCallback =>
|
||||
(result) {
|
||||
result.fold(
|
||||
() => onSelected(dartz.none()),
|
||||
(wrapper) => onSelected(
|
||||
@ -40,7 +44,7 @@ class DisclosureActionWrapper extends ActionItem {
|
||||
|
||||
DisclosureActionWrapper(this.inner);
|
||||
@override
|
||||
Widget? get icon => inner.icon;
|
||||
Widget? icon(Color iconColor) => inner.icon(iconColor);
|
||||
|
||||
@override
|
||||
String get name => inner.name;
|
||||
|
@ -80,7 +80,7 @@ class ViewDisclosureRegion extends StatelessWidget
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Listener(
|
||||
onPointerDown: (event) => {_handleClick(event, context)},
|
||||
onPointerDown: (event) => _handleClick(event, context),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
@ -123,7 +123,7 @@ class ViewDisclosureActionWrapper extends ActionItem {
|
||||
|
||||
ViewDisclosureActionWrapper(this.inner);
|
||||
@override
|
||||
Widget? get icon => inner.icon;
|
||||
Widget? icon(Color iconColor) => inner.icon(iconColor);
|
||||
|
||||
@override
|
||||
String get name => inner.name;
|
||||
|
@ -147,14 +147,14 @@ extension ViewDisclosureExtension on ViewDisclosureAction {
|
||||
}
|
||||
}
|
||||
|
||||
Widget get icon {
|
||||
Widget icon(Color iconColor) {
|
||||
switch (this) {
|
||||
case ViewDisclosureAction.rename:
|
||||
return svgWidget('editor/edit', color: const Color(0xff999999));
|
||||
return svgWidget('editor/edit', color: iconColor);
|
||||
case ViewDisclosureAction.delete:
|
||||
return svgWidget('editor/delete', color: const Color(0xff999999));
|
||||
return svgWidget('editor/delete', color: iconColor);
|
||||
case ViewDisclosureAction.duplicate:
|
||||
return svgWidget('editor/copy', color: const Color(0xff999999));
|
||||
return svgWidget('editor/copy', color: iconColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -101,17 +101,16 @@ class _DebugToast {
|
||||
}
|
||||
}
|
||||
|
||||
class QuestionBubbleActionSheet with ActionList<BubbleActionWrapper>, FlowyOverlayDelegate {
|
||||
class QuestionBubbleActionSheet
|
||||
with ActionList<BubbleActionWrapper>, FlowyOverlayDelegate {
|
||||
final Function(dartz.Option<BubbleAction>) onSelected;
|
||||
final _items = BubbleAction.values.map((action) => BubbleActionWrapper(action)).toList();
|
||||
final _items =
|
||||
BubbleAction.values.map((action) => BubbleActionWrapper(action)).toList();
|
||||
|
||||
QuestionBubbleActionSheet({
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
double get maxWidth => 170;
|
||||
|
||||
@override
|
||||
double get itemHeight => 22;
|
||||
|
||||
@ -119,7 +118,8 @@ class QuestionBubbleActionSheet with ActionList<BubbleActionWrapper>, FlowyOverl
|
||||
List<BubbleActionWrapper> get items => _items;
|
||||
|
||||
@override
|
||||
void Function(dartz.Option<BubbleActionWrapper> p1) get selectCallback => (result) {
|
||||
void Function(dartz.Option<BubbleActionWrapper> p1) get selectCallback =>
|
||||
(result) {
|
||||
result.fold(
|
||||
() => onSelected(dartz.none()),
|
||||
(wrapper) => onSelected(
|
||||
@ -139,7 +139,7 @@ class QuestionBubbleActionSheet with ActionList<BubbleActionWrapper>, FlowyOverl
|
||||
@override
|
||||
ListOverlayFooter? get footer => ListOverlayFooter(
|
||||
widget: const FlowyVersionDescription(),
|
||||
height: 30,
|
||||
height: 40,
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
);
|
||||
}
|
||||
@ -156,7 +156,8 @@ class FlowyVersionDescription extends StatelessWidget {
|
||||
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.hasError) {
|
||||
return FlowyText("Error: ${snapshot.error}", fontSize: 12, color: theme.shader4);
|
||||
return FlowyText("Error: ${snapshot.error}",
|
||||
fontSize: 12, color: theme.shader4);
|
||||
}
|
||||
|
||||
PackageInfo packageInfo = snapshot.data;
|
||||
@ -170,7 +171,11 @@ class FlowyVersionDescription extends StatelessWidget {
|
||||
children: [
|
||||
Divider(height: 1, color: theme.shader6, thickness: 1.0),
|
||||
const VSpace(6),
|
||||
FlowyText("$appName $version.$buildNumber", fontSize: 12, color: theme.shader4),
|
||||
FlowyText(
|
||||
"$appName $version.$buildNumber",
|
||||
fontSize: 12,
|
||||
color: theme.shader4,
|
||||
),
|
||||
],
|
||||
).padding(
|
||||
horizontal: ActionListSizes.itemHPadding + ActionListSizes.padding,
|
||||
@ -190,7 +195,7 @@ class BubbleActionWrapper extends ActionItem {
|
||||
|
||||
BubbleActionWrapper(this.inner);
|
||||
@override
|
||||
Widget? get icon => inner.emoji;
|
||||
Widget? icon(Color iconColor) => inner.emoji;
|
||||
|
||||
@override
|
||||
String get name => inner.name;
|
||||
|
@ -13,7 +13,9 @@ abstract class ActionList<T extends ActionItem> {
|
||||
|
||||
String get identifier => toString();
|
||||
|
||||
double get maxWidth => 162;
|
||||
double get maxWidth => 300;
|
||||
|
||||
double get minWidth => 120;
|
||||
|
||||
double get itemHeight => ActionListSizes.itemHeight;
|
||||
|
||||
@ -29,28 +31,29 @@ abstract class ActionList<T extends ActionItem> {
|
||||
AnchorDirection anchorDirection = AnchorDirection.bottomRight,
|
||||
Offset? anchorOffset,
|
||||
}) {
|
||||
final widgets = items
|
||||
.map(
|
||||
(action) => ActionCell<T>(
|
||||
action: action,
|
||||
itemHeight: itemHeight,
|
||||
onSelected: (action) {
|
||||
FlowyOverlay.of(buildContext).remove(identifier);
|
||||
selectCallback(dartz.some(action));
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
ListOverlay.showWithAnchor(
|
||||
buildContext,
|
||||
identifier: identifier,
|
||||
itemCount: widgets.length,
|
||||
itemBuilder: (context, index) => widgets[index],
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final action = items[index];
|
||||
return ActionCell<T>(
|
||||
action: action,
|
||||
itemHeight: itemHeight,
|
||||
onSelected: (action) {
|
||||
FlowyOverlay.of(buildContext).remove(identifier);
|
||||
selectCallback(dartz.some(action));
|
||||
},
|
||||
);
|
||||
},
|
||||
anchorContext: anchorContext ?? buildContext,
|
||||
anchorDirection: anchorDirection,
|
||||
width: maxWidth,
|
||||
height: widgets.length * (itemHeight + ActionListSizes.padding * 2),
|
||||
constraints: BoxConstraints(
|
||||
minHeight: items.length * (itemHeight + ActionListSizes.padding * 2),
|
||||
maxHeight: items.length * (itemHeight + ActionListSizes.padding * 2),
|
||||
maxWidth: maxWidth,
|
||||
minWidth: minWidth,
|
||||
),
|
||||
delegate: delegate,
|
||||
anchorOffset: anchorOffset,
|
||||
footer: footer,
|
||||
@ -59,7 +62,7 @@ abstract class ActionList<T extends ActionItem> {
|
||||
}
|
||||
|
||||
abstract class ActionItem {
|
||||
Widget? get icon;
|
||||
Widget? icon(Color iconColor);
|
||||
String get name;
|
||||
}
|
||||
|
||||
@ -83,6 +86,7 @@ class ActionCell<T extends ActionItem> extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
final icon = action.icon(theme.iconColor);
|
||||
|
||||
return FlowyHover(
|
||||
style: HoverStyle(hoverColor: theme.hover),
|
||||
@ -92,14 +96,11 @@ class ActionCell<T extends ActionItem> extends StatelessWidget {
|
||||
child: SizedBox(
|
||||
height: itemHeight,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (action.icon != null) action.icon!,
|
||||
if (icon != null) icon,
|
||||
HSpace(ActionListSizes.itemHPadding),
|
||||
FlowyText.medium(
|
||||
action.name,
|
||||
fontSize: 12,
|
||||
),
|
||||
FlowyText.medium(action.name, fontSize: 12),
|
||||
],
|
||||
),
|
||||
).padding(
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"projects": {
|
||||
"default": "appflowy-editor"
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
{
|
||||
"hosting": {
|
||||
"public": "build/web",
|
||||
"ignore": [
|
||||
"firebase.json",
|
||||
"**/.*",
|
||||
"**/node_modules/**"
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "**",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
],
|
||||
"headers": [ {
|
||||
"source": "**/*.@(png|jpg|jpeg|gif)",
|
||||
"headers": [ {
|
||||
"key": "Access-Control-Allow-Origin",
|
||||
"value": "*"
|
||||
} ]
|
||||
} ]
|
||||
}
|
||||
}
|
@ -1,13 +1,16 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:example/plugin/underscore_to_italic.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:example/plugin/underscore_to_italic.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
@ -112,6 +115,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
child: AppFlowyEditor(
|
||||
editorState: _editorState!,
|
||||
editorStyle: _editorStyle,
|
||||
editable: true,
|
||||
shortcutEvents: [
|
||||
underscoreToItalic,
|
||||
],
|
||||
@ -148,7 +152,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
),
|
||||
ActionButton(
|
||||
icon: const Icon(Icons.import_export),
|
||||
onPressed: () => _importDocument(),
|
||||
onPressed: () async => await _importDocument(),
|
||||
),
|
||||
ActionButton(
|
||||
icon: const Icon(Icons.color_lens),
|
||||
@ -167,28 +171,53 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
void _exportDocument(EditorState editorState) async {
|
||||
final document = editorState.document.toJson();
|
||||
final json = jsonEncode(document);
|
||||
final directory = await getTemporaryDirectory();
|
||||
final path = directory.path;
|
||||
final file = File('$path/editor.json');
|
||||
await file.writeAsString(json);
|
||||
if (kIsWeb) {
|
||||
final blob = html.Blob([json], 'text/plain', 'native');
|
||||
html.AnchorElement(
|
||||
href: html.Url.createObjectUrlFromBlob(blob).toString(),
|
||||
)
|
||||
..setAttribute('download', 'editor.json')
|
||||
..click();
|
||||
} else {
|
||||
final directory = await getTemporaryDirectory();
|
||||
final path = directory.path;
|
||||
final file = File('$path/editor.json');
|
||||
await file.writeAsString(json);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('The document is saved to the ${file.path}'),
|
||||
),
|
||||
);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('The document is saved to the ${file.path}'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _importDocument() async {
|
||||
final directory = await getTemporaryDirectory();
|
||||
final path = directory.path;
|
||||
final file = File('$path/editor.json');
|
||||
setState(() {
|
||||
_editorState = null;
|
||||
_jsonString = file.readAsString();
|
||||
});
|
||||
Future<void> _importDocument() async {
|
||||
if (kIsWeb) {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
allowMultiple: false,
|
||||
allowedExtensions: ['json'],
|
||||
type: FileType.custom,
|
||||
);
|
||||
final bytes = result?.files.first.bytes;
|
||||
if (bytes != null) {
|
||||
final jsonString = const Utf8Decoder().convert(bytes);
|
||||
setState(() {
|
||||
_editorState = null;
|
||||
_jsonString = Future.value(jsonString);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
final directory = await getTemporaryDirectory();
|
||||
final path = '${directory.path}/editor.json';
|
||||
final file = File(path);
|
||||
setState(() {
|
||||
_editorState = null;
|
||||
_jsonString = file.readAsString();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _switchToPage(int pageIndex) {
|
||||
|
@ -1,165 +0,0 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 1. define your custom type in example.json
|
||||
/// For example I need to define an image plugin, then I define type equals
|
||||
/// "image", and add "image_src" into "attributes".
|
||||
/// {
|
||||
/// "type": "image",
|
||||
/// "attributes", { "image_src": "https://s1.ax1x.com/2022/07/28/vCgz1x.png" }
|
||||
/// }
|
||||
/// 2. create a class extends [NodeWidgetBuilder]
|
||||
/// 3. override the function `Widget build(NodeWidgetContext<Node> context)`
|
||||
/// and return a widget to render. The returned widget should be
|
||||
/// a StatefulWidget and mixin with [SelectableMixin].
|
||||
///
|
||||
/// 4. override the getter `nodeValidator`
|
||||
/// to verify the data structure in [Node].
|
||||
/// 5. register the plugin with `type` to `AppFlowyEditor` in `main.dart`.
|
||||
/// 6. Congratulations!
|
||||
|
||||
class ImageNodeBuilder extends NodeWidgetBuilder<Node> {
|
||||
@override
|
||||
Widget build(NodeWidgetContext<Node> context) {
|
||||
return ImageNodeWidget(
|
||||
key: context.node.key,
|
||||
node: context.node,
|
||||
editorState: context.editorState,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
NodeValidator<Node> get nodeValidator => ((node) {
|
||||
return node.type == 'image';
|
||||
});
|
||||
}
|
||||
|
||||
const double placeholderHeight = 132;
|
||||
|
||||
class ImageNodeWidget extends StatefulWidget {
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
const ImageNodeWidget({
|
||||
Key? key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ImageNodeWidget> createState() => _ImageNodeWidgetState();
|
||||
}
|
||||
|
||||
class _ImageNodeWidgetState extends State<ImageNodeWidget>
|
||||
with SelectableMixin {
|
||||
bool isHovered = false;
|
||||
Node get node => widget.node;
|
||||
EditorState get editorState => widget.editorState;
|
||||
String get src => widget.node.attributes['image_src'] as String;
|
||||
|
||||
@override
|
||||
Position end() {
|
||||
return Position(path: node.path, offset: 0);
|
||||
}
|
||||
|
||||
@override
|
||||
Position start() {
|
||||
return Position(path: node.path, offset: 0);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Rect> getRectsInSelection(Selection selection) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
Selection getSelectionInRange(Offset start, Offset end) {
|
||||
return Selection.collapsed(Position(path: node.path, offset: 0));
|
||||
}
|
||||
|
||||
@override
|
||||
Offset localToGlobal(Offset offset) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Position getPositionInOffset(Offset start) {
|
||||
return Position(path: node.path, offset: 0);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _build(context);
|
||||
}
|
||||
|
||||
Widget _loadingBuilder(
|
||||
BuildContext context, Widget widget, ImageChunkEvent? evt) {
|
||||
if (evt == null) {
|
||||
return widget;
|
||||
}
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
height: placeholderHeight,
|
||||
child: const Text("Loading..."),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _errorBuilder(
|
||||
BuildContext context, Object obj, StackTrace? stackTrace) {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
height: placeholderHeight,
|
||||
child: const Text("Error..."),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _frameBuilder(
|
||||
BuildContext context,
|
||||
Widget child,
|
||||
int? frame,
|
||||
bool wasSynchronouslyLoaded,
|
||||
) {
|
||||
if (frame == null) {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
height: placeholderHeight,
|
||||
child: const Text("Loading..."),
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
Widget _build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
MouseRegion(
|
||||
onEnter: (event) {
|
||||
setState(() {
|
||||
isHovered = true;
|
||||
});
|
||||
},
|
||||
onExit: (event) {
|
||||
setState(() {
|
||||
isHovered = false;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isHovered ? Colors.blue : Colors.grey,
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20))),
|
||||
child: Image.network(
|
||||
src,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
frameBuilder: _frameBuilder,
|
||||
loadingBuilder: _loadingBuilder,
|
||||
errorBuilder: _errorBuilder,
|
||||
),
|
||||
)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pod_player/pod_player.dart';
|
||||
|
||||
class YouTubeLinkNodeBuilder extends NodeWidgetBuilder<Node> {
|
||||
@override
|
||||
Widget build(NodeWidgetContext<Node> context) {
|
||||
return LinkNodeWidget(
|
||||
key: context.node.key,
|
||||
node: context.node,
|
||||
editorState: context.editorState,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
NodeValidator<Node> get nodeValidator => ((node) {
|
||||
return node.type == 'youtube_link';
|
||||
});
|
||||
}
|
||||
|
||||
class LinkNodeWidget extends StatefulWidget {
|
||||
final Node node;
|
||||
final EditorState editorState;
|
||||
|
||||
const LinkNodeWidget({
|
||||
Key? key,
|
||||
required this.node,
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<LinkNodeWidget> createState() => _YouTubeLinkNodeWidgetState();
|
||||
}
|
||||
|
||||
class _YouTubeLinkNodeWidgetState extends State<LinkNodeWidget>
|
||||
with SelectableMixin {
|
||||
Node get node => widget.node;
|
||||
EditorState get editorState => widget.editorState;
|
||||
String get src => widget.node.attributes['youtube_link'] as String;
|
||||
|
||||
@override
|
||||
Position end() {
|
||||
// TODO: implement end
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Position start() {
|
||||
// TODO: implement start
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
List<Rect> getRectsInSelection(Selection selection) {
|
||||
// TODO: implement getRectsInSelection
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Selection getSelectionInRange(Offset start, Offset end) {
|
||||
// TODO: implement getSelectionInRange
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Offset localToGlobal(Offset offset) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Position getPositionInOffset(Offset start) {
|
||||
// TODO: implement getPositionInOffset
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _build(context);
|
||||
}
|
||||
|
||||
late final PodPlayerController controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
controller = PodPlayerController(
|
||||
playVideoFrom: PlayVideoFrom.network(
|
||||
src,
|
||||
),
|
||||
)..initialise();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Widget _build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
PodVideoPlayer(controller: controller),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -8,11 +8,9 @@ import Foundation
|
||||
import path_provider_macos
|
||||
import rich_clipboard_macos
|
||||
import url_launcher_macos
|
||||
import wakelock_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
RichClipboardPlugin.register(with: registry.registrar(forPlugin: "RichClipboardPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin"))
|
||||
}
|
||||
|
@ -6,15 +6,12 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- wakelock_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
- path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
|
||||
- rich_clipboard_macos (from `Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
FlutterMacOS:
|
||||
@ -25,15 +22,12 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/rich_clipboard_macos/macos
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
wakelock_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
|
||||
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
|
||||
rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
|
||||
url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
|
||||
wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9
|
||||
|
||||
PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
|
||||
|
||||
|
@ -37,12 +37,12 @@ dependencies:
|
||||
path: ../
|
||||
provider: ^6.0.3
|
||||
url_launcher: ^6.1.5
|
||||
video_player: ^2.4.5
|
||||
pod_player: 0.0.8
|
||||
path_provider: ^2.0.11
|
||||
google_fonts: ^3.0.1
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
file_picker: ^5.0.1
|
||||
universal_html: ^2.0.8
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -193,16 +193,24 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||
return parent!._path([index, ...previous]);
|
||||
}
|
||||
|
||||
Node deepClone() {
|
||||
final newNode = Node(
|
||||
type: type, children: LinkedList<Node>(), attributes: {...attributes});
|
||||
|
||||
for (final node in children) {
|
||||
final newNode = node.deepClone();
|
||||
newNode.parent = this;
|
||||
newNode.children.add(newNode);
|
||||
Node copyWith({
|
||||
String? type,
|
||||
LinkedList<Node>? children,
|
||||
Attributes? attributes,
|
||||
}) {
|
||||
final node = Node(
|
||||
type: type ?? this.type,
|
||||
attributes: attributes ?? {..._attributes},
|
||||
children: children ?? LinkedList(),
|
||||
);
|
||||
if (children == null && this.children.isNotEmpty) {
|
||||
for (final child in this.children) {
|
||||
node.children.add(
|
||||
child.copyWith()..parent = node,
|
||||
);
|
||||
}
|
||||
}
|
||||
return newNode;
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,7 +223,10 @@ class TextNode extends Node {
|
||||
LinkedList<Node>? children,
|
||||
Attributes? attributes,
|
||||
}) : _delta = delta,
|
||||
super(children: children ?? LinkedList(), attributes: attributes ?? {});
|
||||
super(
|
||||
children: children ?? LinkedList(),
|
||||
attributes: attributes ?? {},
|
||||
);
|
||||
|
||||
TextNode.empty({Attributes? attributes})
|
||||
: _delta = Delta([TextInsert('')]),
|
||||
@ -241,33 +252,27 @@ class TextNode extends Node {
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
TextNode copyWith({
|
||||
String? type,
|
||||
LinkedList<Node>? children,
|
||||
Attributes? attributes,
|
||||
Delta? delta,
|
||||
}) =>
|
||||
TextNode(
|
||||
type: type ?? this.type,
|
||||
children: children ?? this.children,
|
||||
attributes: attributes ?? _attributes,
|
||||
delta: delta ?? this.delta,
|
||||
);
|
||||
|
||||
@override
|
||||
TextNode deepClone() {
|
||||
final newNode = TextNode(
|
||||
type: type,
|
||||
children: LinkedList<Node>(),
|
||||
delta: delta.slice(0),
|
||||
attributes: {...attributes});
|
||||
|
||||
for (final node in children) {
|
||||
final newNode = node.deepClone();
|
||||
newNode.parent = this;
|
||||
newNode.children.add(newNode);
|
||||
}) {
|
||||
final textNode = TextNode(
|
||||
type: type ?? this.type,
|
||||
children: children,
|
||||
attributes: attributes ?? _attributes,
|
||||
delta: delta ?? this.delta,
|
||||
);
|
||||
if (children == null && this.children.isNotEmpty) {
|
||||
for (final child in this.children) {
|
||||
textNode.children.add(
|
||||
child.copyWith()..parent = textNode,
|
||||
);
|
||||
}
|
||||
}
|
||||
return newNode;
|
||||
return textNode;
|
||||
}
|
||||
|
||||
String toRawString() => _delta.toRawString();
|
||||
|
@ -40,11 +40,9 @@ class Selection {
|
||||
bool get isCollapsed => start == end;
|
||||
bool get isSingle => pathEquals(start.path, end.path);
|
||||
bool get isForward =>
|
||||
(start.path >= end.path && !pathEquals(start.path, end.path)) ||
|
||||
(isSingle && start.offset > end.offset);
|
||||
(start.path > end.path) || (isSingle && start.offset > end.offset);
|
||||
bool get isBackward =>
|
||||
(start.path <= end.path && !pathEquals(start.path, end.path)) ||
|
||||
(isSingle && start.offset < end.offset);
|
||||
(start.path < end.path) || (isSingle && start.offset < end.offset);
|
||||
|
||||
Selection get normalize {
|
||||
if (isForward) {
|
||||
|
@ -4,22 +4,52 @@ import 'dart:math';
|
||||
|
||||
extension PathExtensions on Path {
|
||||
bool operator >=(Path other) {
|
||||
if (pathEquals(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return this > other;
|
||||
}
|
||||
|
||||
bool operator >(Path other) {
|
||||
if (pathEquals(this, other)) {
|
||||
return false;
|
||||
}
|
||||
final length = min(this.length, other.length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (this[i] < other[i]) {
|
||||
return false;
|
||||
} else if (this[i] > other[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (this.length < other.length) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool operator <=(Path other) {
|
||||
if (pathEquals(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return this < other;
|
||||
}
|
||||
|
||||
bool operator <(Path other) {
|
||||
if (pathEquals(this, other)) {
|
||||
return false;
|
||||
}
|
||||
final length = min(this.length, other.length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (this[i] > other[i]) {
|
||||
return false;
|
||||
} else if (this[i] < other[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (this.length > other.length) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,46 @@
|
||||
import 'package:appflowy_editor/src/document/node.dart';
|
||||
|
||||
class Infra {
|
||||
// find the forward nearest text node
|
||||
static TextNode? forwardNearestTextNode(Node node) {
|
||||
var previous = node.previous;
|
||||
while (previous != null) {
|
||||
final lastTextNode = findLastTextNode(previous);
|
||||
if (lastTextNode != null) {
|
||||
return lastTextNode;
|
||||
}
|
||||
if (previous is TextNode) {
|
||||
return previous;
|
||||
}
|
||||
previous = previous.previous;
|
||||
}
|
||||
final parent = node.parent;
|
||||
if (parent != null) {
|
||||
if (parent is TextNode) {
|
||||
return parent;
|
||||
}
|
||||
return forwardNearestTextNode(parent);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// find the last text node
|
||||
static TextNode? findLastTextNode(Node node) {
|
||||
final children = node.children.toList(growable: false).reversed;
|
||||
for (final child in children) {
|
||||
if (child.children.isNotEmpty) {
|
||||
final result = findLastTextNode(child);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (child is TextNode) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
if (node is TextNode) {
|
||||
return node;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -36,7 +36,7 @@ class TransactionBuilder {
|
||||
/// Inserts a sequence of nodes at the position of path.
|
||||
insertNodes(Path path, List<Node> nodes) {
|
||||
beforeSelection = state.cursorSelection;
|
||||
add(InsertOperation(path, nodes.map((node) => node.deepClone()).toList()));
|
||||
add(InsertOperation(path, nodes.map((node) => node.copyWith()).toList()));
|
||||
}
|
||||
|
||||
/// Updates the attributes of nodes.
|
||||
@ -75,7 +75,7 @@ class TransactionBuilder {
|
||||
nodes.add(node);
|
||||
}
|
||||
|
||||
add(DeleteOperation(path, nodes.map((node) => node.deepClone()).toList()));
|
||||
add(DeleteOperation(path, nodes.map((node) => node.copyWith()).toList()));
|
||||
}
|
||||
|
||||
textEdit(TextNode node, Delta Function() f) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
abstract class BuiltInTextWidget extends StatefulWidget {
|
||||
@ -59,3 +60,58 @@ mixin BuiltInStyleMixin<T extends BuiltInTextWidget> on State<T> {
|
||||
return const EdgeInsets.all(0);
|
||||
}
|
||||
}
|
||||
|
||||
mixin BuiltInTextWidgetMixin<T extends BuiltInTextWidget> on State<T>
|
||||
implements DefaultSelectable {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.textNode.children.isEmpty) {
|
||||
return buildWithSingle(context);
|
||||
} else {
|
||||
return buildWithChildren(context);
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildWithSingle(BuildContext context);
|
||||
|
||||
Widget buildWithChildren(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildWithSingle(context),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// TODO: customize
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: widget.textNode.children
|
||||
.map(
|
||||
(child) => widget.editorState.service.renderPluginService
|
||||
.buildPluginWidget(
|
||||
child is TextNode
|
||||
? NodeWidgetContext<TextNode>(
|
||||
context: context,
|
||||
node: child,
|
||||
editorState: widget.editorState,
|
||||
)
|
||||
: NodeWidgetContext<Node>(
|
||||
context: context,
|
||||
node: child,
|
||||
editorState: widget.editorState,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,11 @@ class BulletedListTextNodeWidget extends BuiltInTextWidget {
|
||||
// customize
|
||||
|
||||
class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
|
||||
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
|
||||
with
|
||||
SelectableMixin,
|
||||
DefaultSelectable,
|
||||
BuiltInStyleMixin,
|
||||
BuiltInTextWidgetMixin {
|
||||
@override
|
||||
final iconKey = GlobalKey();
|
||||
|
||||
@ -61,7 +65,7 @@ class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget buildWithSingle(BuildContext context) {
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: Row(
|
||||
|
@ -46,7 +46,11 @@ class CheckboxNodeWidget extends BuiltInTextWidget {
|
||||
}
|
||||
|
||||
class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
|
||||
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
|
||||
with
|
||||
SelectableMixin,
|
||||
DefaultSelectable,
|
||||
BuiltInStyleMixin,
|
||||
BuiltInTextWidgetMixin {
|
||||
@override
|
||||
final iconKey = GlobalKey();
|
||||
|
||||
@ -62,15 +66,7 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.textNode.children.isEmpty) {
|
||||
return _buildWithSingle(context);
|
||||
} else {
|
||||
return _buildWithChildren(context);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildWithSingle(BuildContext context) {
|
||||
Widget buildWithSingle(BuildContext context) {
|
||||
final check = widget.textNode.attributes.check;
|
||||
return Padding(
|
||||
padding: padding,
|
||||
@ -106,40 +102,4 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWithChildren(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildWithSingle(context),
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
Column(
|
||||
children: widget.textNode.children
|
||||
.map(
|
||||
(child) => widget.editorState.service.renderPluginService
|
||||
.buildPluginWidget(
|
||||
child is TextNode
|
||||
? NodeWidgetContext<TextNode>(
|
||||
context: context,
|
||||
node: child,
|
||||
editorState: widget.editorState,
|
||||
)
|
||||
: NodeWidgetContext<Node>(
|
||||
context: context,
|
||||
node: child,
|
||||
editorState: widget.editorState,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,11 @@ class RichTextNodeWidget extends BuiltInTextWidget {
|
||||
// customize
|
||||
|
||||
class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
|
||||
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
|
||||
with
|
||||
SelectableMixin,
|
||||
DefaultSelectable,
|
||||
BuiltInStyleMixin,
|
||||
BuiltInTextWidgetMixin {
|
||||
@override
|
||||
GlobalKey? get iconKey => null;
|
||||
|
||||
@ -59,7 +63,7 @@ class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget buildWithSingle(BuildContext context) {
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: FlowyRichText(
|
||||
|
@ -38,6 +38,7 @@ class AppFlowyEditor extends StatefulWidget {
|
||||
this.customBuilders = const {},
|
||||
this.shortcutEvents = const [],
|
||||
this.selectionMenuItems = const [],
|
||||
this.editable = true,
|
||||
required this.editorStyle,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -53,6 +54,8 @@ class AppFlowyEditor extends StatefulWidget {
|
||||
|
||||
final EditorStyle editorStyle;
|
||||
|
||||
final bool editable;
|
||||
|
||||
@override
|
||||
State<AppFlowyEditor> createState() => _AppFlowyEditorState();
|
||||
}
|
||||
@ -106,11 +109,14 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
||||
cursorColor: widget.editorStyle.cursorColor,
|
||||
selectionColor: widget.editorStyle.selectionColor,
|
||||
editorState: editorState,
|
||||
editable: widget.editable,
|
||||
child: AppFlowyInput(
|
||||
key: editorState.service.inputServiceKey,
|
||||
editorState: editorState,
|
||||
editable: widget.editable,
|
||||
child: AppFlowyKeyboard(
|
||||
key: editorState.service.keyboardServiceKey,
|
||||
editable: widget.editable,
|
||||
shortcutEvents: [
|
||||
...builtInShortcutEvents,
|
||||
...widget.shortcutEvents,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:appflowy_editor/src/infra/log.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@ -43,11 +44,13 @@ abstract class AppFlowyInputService {
|
||||
class AppFlowyInput extends StatefulWidget {
|
||||
const AppFlowyInput({
|
||||
Key? key,
|
||||
this.editable = true,
|
||||
required this.editorState,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
final EditorState editorState;
|
||||
final bool editable;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
@ -61,26 +64,39 @@ class _AppFlowyInputState extends State<AppFlowyInput>
|
||||
|
||||
EditorState get _editorState => widget.editorState;
|
||||
|
||||
// Disable space shortcut on the Web platform.
|
||||
final Map<ShortcutActivator, Intent> _shortcuts = kIsWeb
|
||||
? {
|
||||
LogicalKeySet(LogicalKeyboardKey.space):
|
||||
DoNothingAndStopPropagationIntent(),
|
||||
}
|
||||
: {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_editorState.service.selectionService.currentSelection
|
||||
.addListener(_onSelectionChange);
|
||||
if (widget.editable) {
|
||||
_editorState.service.selectionService.currentSelection
|
||||
.addListener(_onSelectionChange);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
close();
|
||||
_editorState.service.selectionService.currentSelection
|
||||
.removeListener(_onSelectionChange);
|
||||
if (widget.editable) {
|
||||
close();
|
||||
_editorState.service.selectionService.currentSelection
|
||||
.removeListener(_onSelectionChange);
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
return Shortcuts(
|
||||
shortcuts: _shortcuts,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import 'package:appflowy_editor/src/infra/infra.dart';
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/extensions/path_extensions.dart';
|
||||
|
||||
// Handle delete text.
|
||||
ShortcutEventHandler deleteTextHandler = (editorState, event) {
|
||||
@ -121,32 +122,46 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
|
||||
}
|
||||
|
||||
KeyEventResult _backDeleteToPreviousTextNode(
|
||||
EditorState editorState,
|
||||
TextNode textNode,
|
||||
TransactionBuilder transactionBuilder,
|
||||
List<Node> nonTextNodes,
|
||||
Selection selection) {
|
||||
var previous = textNode.previous;
|
||||
bool prevIsNumberList = false;
|
||||
while (previous != null) {
|
||||
if (previous is TextNode) {
|
||||
if (previous.subtype == BuiltInAttributeKey.numberList) {
|
||||
prevIsNumberList = true;
|
||||
}
|
||||
EditorState editorState,
|
||||
TextNode textNode,
|
||||
TransactionBuilder transactionBuilder,
|
||||
List<Node> nonTextNodes,
|
||||
Selection selection,
|
||||
) {
|
||||
if (textNode.next == null &&
|
||||
textNode.children.isEmpty &&
|
||||
textNode.parent?.parent != null) {
|
||||
transactionBuilder
|
||||
..deleteNode(textNode)
|
||||
..insertNode(textNode.parent!.path.next, textNode)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(path: textNode.parent!.path.next, offset: 0),
|
||||
)
|
||||
..commit();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
transactionBuilder
|
||||
..mergeText(previous, textNode)
|
||||
..deleteNode(textNode)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: previous.path,
|
||||
offset: previous.toRawString().length,
|
||||
),
|
||||
);
|
||||
break;
|
||||
} else {
|
||||
previous = previous.previous;
|
||||
bool prevIsNumberList = false;
|
||||
final previousTextNode = Infra.forwardNearestTextNode(textNode);
|
||||
if (previousTextNode != null) {
|
||||
if (previousTextNode.subtype == BuiltInAttributeKey.numberList) {
|
||||
prevIsNumberList = true;
|
||||
}
|
||||
|
||||
transactionBuilder.mergeText(previousTextNode, textNode);
|
||||
if (textNode.children.isNotEmpty) {
|
||||
transactionBuilder.insertNodes(
|
||||
previousTextNode.path.next,
|
||||
textNode.children.toList(growable: false),
|
||||
);
|
||||
}
|
||||
transactionBuilder.deleteNode(textNode);
|
||||
transactionBuilder.afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: previousTextNode.path,
|
||||
offset: previousTextNode.toRawString().length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (transactionBuilder.operations.isNotEmpty) {
|
||||
@ -157,8 +172,8 @@ KeyEventResult _backDeleteToPreviousTextNode(
|
||||
}
|
||||
|
||||
if (prevIsNumberList) {
|
||||
makeFollowingNodesIncremental(
|
||||
editorState, previous!.path, transactionBuilder.afterSelection!);
|
||||
makeFollowingNodesIncremental(editorState, previousTextNode!.path,
|
||||
transactionBuilder.afterSelection!);
|
||||
}
|
||||
|
||||
return KeyEventResult.handled;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy_editor/src/extensions/path_extensions.dart';
|
||||
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
|
||||
import './number_list_helper.dart';
|
||||
|
||||
/// Handle some cases where enter is pressed and shift is not pressed.
|
||||
@ -16,10 +16,6 @@ import './number_list_helper.dart';
|
||||
/// 2.2 or insert a empty text node before.
|
||||
ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
||||
(editorState, event) {
|
||||
if (event.logicalKey != LogicalKeyboardKey.enter || event.isShiftPressed) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
var selection = editorState.service.selectionService.currentSelection.value;
|
||||
var nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
if (selection == null) {
|
||||
@ -124,7 +120,10 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
||||
TransactionBuilder(editorState)
|
||||
..insertNode(
|
||||
textNode.path,
|
||||
TextNode.empty(),
|
||||
textNode.copyWith(
|
||||
children: LinkedList(),
|
||||
delta: Delta(),
|
||||
),
|
||||
)
|
||||
..afterSelection = afterSelection
|
||||
..commit();
|
||||
@ -142,21 +141,25 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
||||
Position(path: nextPath, offset: 0),
|
||||
);
|
||||
|
||||
TransactionBuilder(editorState)
|
||||
..insertNode(
|
||||
textNode.path.next,
|
||||
textNode.copyWith(
|
||||
attributes: attributes,
|
||||
delta: textNode.delta.slice(selection.end.offset),
|
||||
),
|
||||
)
|
||||
..deleteText(
|
||||
textNode,
|
||||
selection.start.offset,
|
||||
textNode.toRawString().length - selection.start.offset,
|
||||
)
|
||||
..afterSelection = afterSelection
|
||||
..commit();
|
||||
final transactionBuilder = TransactionBuilder(editorState);
|
||||
transactionBuilder.insertNode(
|
||||
textNode.path.next,
|
||||
textNode.copyWith(
|
||||
attributes: attributes,
|
||||
delta: textNode.delta.slice(selection.end.offset),
|
||||
),
|
||||
);
|
||||
transactionBuilder.deleteText(
|
||||
textNode,
|
||||
selection.start.offset,
|
||||
textNode.toRawString().length - selection.start.offset,
|
||||
);
|
||||
if (textNode.children.isNotEmpty) {
|
||||
final children = textNode.children.toList(growable: false);
|
||||
transactionBuilder.deleteNodes(children);
|
||||
}
|
||||
transactionBuilder.afterSelection = afterSelection;
|
||||
transactionBuilder.commit();
|
||||
|
||||
// If the new type of a text node is number list,
|
||||
// the numbers of the following nodes should be incremental.
|
||||
|
@ -0,0 +1,34 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
ShortcutEventHandler tabHandler = (editorState, event) {
|
||||
// Only Supports BulletedList For Now.
|
||||
|
||||
final selection = editorState.service.selectionService.currentSelection.value;
|
||||
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
||||
.whereType<TextNode>();
|
||||
if (textNodes.length != 1 || selection == null || !selection.isSingle) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final textNode = textNodes.first;
|
||||
final previous = textNode.previous;
|
||||
if (textNode.subtype != BuiltInAttributeKey.bulletedList ||
|
||||
previous == null ||
|
||||
previous.subtype != BuiltInAttributeKey.bulletedList) {
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
final path = previous.path + [previous.children.length];
|
||||
final afterSelection = Selection(
|
||||
start: selection.start.copyWith(path: path),
|
||||
end: selection.end.copyWith(path: path),
|
||||
);
|
||||
TransactionBuilder(editorState)
|
||||
..deleteNode(textNode)
|
||||
..insertNode(path, textNode)
|
||||
..setAfterSelection(afterSelection)
|
||||
..commit();
|
||||
|
||||
return KeyEventResult.handled;
|
||||
};
|
@ -42,6 +42,7 @@ abstract class AppFlowyKeyboardService {
|
||||
class AppFlowyKeyboard extends StatefulWidget {
|
||||
const AppFlowyKeyboard({
|
||||
Key? key,
|
||||
this.editable = true,
|
||||
required this.shortcutEvents,
|
||||
required this.editorState,
|
||||
required this.child,
|
||||
@ -50,6 +51,7 @@ class AppFlowyKeyboard extends StatefulWidget {
|
||||
final EditorState editorState;
|
||||
final Widget child;
|
||||
final List<ShortcutEvent> shortcutEvents;
|
||||
final bool editable;
|
||||
|
||||
@override
|
||||
State<AppFlowyKeyboard> createState() => _AppFlowyKeyboardState();
|
||||
@ -62,7 +64,6 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
|
||||
bool isFocus = true;
|
||||
|
||||
@override
|
||||
// TODO: implement shortcutEvents
|
||||
List<ShortcutEvent> get shortcutEvents => widget.shortcutEvents;
|
||||
|
||||
@override
|
||||
@ -91,8 +92,12 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
|
||||
|
||||
@override
|
||||
void enable() {
|
||||
isFocus = true;
|
||||
_focusNode.requestFocus();
|
||||
if (widget.editable) {
|
||||
isFocus = true;
|
||||
_focusNode.requestFocus();
|
||||
} else {
|
||||
disable();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -84,6 +84,7 @@ class AppFlowySelection extends StatefulWidget {
|
||||
Key? key,
|
||||
this.cursorColor = const Color(0xFF00BCF0),
|
||||
this.selectionColor = const Color.fromARGB(53, 111, 201, 231),
|
||||
this.editable = true,
|
||||
required this.editorState,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
@ -92,6 +93,7 @@ class AppFlowySelection extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Color cursorColor;
|
||||
final Color selectionColor;
|
||||
final bool editable;
|
||||
|
||||
@override
|
||||
State<AppFlowySelection> createState() => _AppFlowySelectionState();
|
||||
@ -144,15 +146,21 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SelectionGestureDetector(
|
||||
onPanStart: _onPanStart,
|
||||
onPanUpdate: _onPanUpdate,
|
||||
onPanEnd: _onPanEnd,
|
||||
onTapDown: _onTapDown,
|
||||
onDoubleTapDown: _onDoubleTapDown,
|
||||
onTripleTapDown: _onTripleTapDown,
|
||||
child: widget.child,
|
||||
);
|
||||
if (!widget.editable) {
|
||||
return Container(
|
||||
child: widget.child,
|
||||
);
|
||||
} else {
|
||||
return SelectionGestureDetector(
|
||||
onPanStart: _onPanStart,
|
||||
onPanUpdate: _onPanUpdate,
|
||||
onPanEnd: _onPanEnd,
|
||||
onTapDown: _onTapDown,
|
||||
onDoubleTapDown: _onDoubleTapDown,
|
||||
onTripleTapDown: _onTripleTapDown,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -184,6 +192,10 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
||||
|
||||
@override
|
||||
void updateSelection(Selection? selection) {
|
||||
if (!widget.editable) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectionRects.clear();
|
||||
clearSelection();
|
||||
|
||||
@ -323,6 +335,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
||||
|
||||
// compute the selection in range.
|
||||
if (first != null && last != null) {
|
||||
Log.selection.debug('first = $first, last = $last');
|
||||
final start =
|
||||
first.getSelectionInRange(panStartOffset, panEndOffset).start;
|
||||
final end = last.getSelectionInRange(panStartOffset, panEndOffset).end;
|
||||
@ -353,6 +366,8 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
|
||||
final normalizedSelection = selection.normalize;
|
||||
assert(normalizedSelection.isBackward);
|
||||
|
||||
Log.selection.debug('update selection areas, $normalizedSelection');
|
||||
|
||||
for (var i = 0; i < backwardNodes.length; i++) {
|
||||
final node = backwardNodes[i];
|
||||
final selectable = node.selectable;
|
||||
|
@ -9,6 +9,7 @@ import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_und
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/select_all_handler.dart';
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/slash_handler.dart';
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/format_style_handler.dart';
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/tab_handler.dart';
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart';
|
||||
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event.dart';
|
||||
|
||||
@ -243,4 +244,9 @@ List<ShortcutEvent> builtInShortcutEvents = [
|
||||
command: 'page down',
|
||||
handler: pageDownHandler,
|
||||
),
|
||||
ShortcutEvent(
|
||||
key: 'Tab',
|
||||
command: 'tab',
|
||||
handler: tabHandler,
|
||||
),
|
||||
];
|
||||
|
@ -2,6 +2,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:appflowy_editor/src/service/shortcut_event/keybinding.dart';
|
||||
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Defines the implementation of shortcut event.
|
||||
class ShortcutEvent {
|
||||
@ -56,7 +57,10 @@ class ShortcutEvent {
|
||||
String? linuxCommand,
|
||||
}) {
|
||||
var matched = false;
|
||||
if (Platform.isWindows &&
|
||||
if (kIsWeb && command != null && command.isNotEmpty) {
|
||||
this.command = command;
|
||||
matched = true;
|
||||
} else if (Platform.isWindows &&
|
||||
windowsCommand != null &&
|
||||
windowsCommand.isNotEmpty) {
|
||||
this.command = windowsCommand;
|
||||
|
@ -0,0 +1,153 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() async {
|
||||
setUpAll(() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
|
||||
group('node.dart', () {
|
||||
test('test node copyWith', () {
|
||||
final node = Node(
|
||||
type: 'example',
|
||||
children: LinkedList(),
|
||||
attributes: {
|
||||
'example': 'example',
|
||||
},
|
||||
);
|
||||
expect(node.toJson(), {
|
||||
'type': 'example',
|
||||
'attributes': {
|
||||
'example': 'example',
|
||||
},
|
||||
});
|
||||
expect(
|
||||
node.copyWith().toJson(),
|
||||
node.toJson(),
|
||||
);
|
||||
|
||||
final nodeWithChildren = Node(
|
||||
type: 'example',
|
||||
children: LinkedList()..add(node),
|
||||
attributes: {
|
||||
'example': 'example',
|
||||
},
|
||||
);
|
||||
expect(nodeWithChildren.toJson(), {
|
||||
'type': 'example',
|
||||
'attributes': {
|
||||
'example': 'example',
|
||||
},
|
||||
'children': [
|
||||
{
|
||||
'type': 'example',
|
||||
'attributes': {
|
||||
'example': 'example',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(
|
||||
nodeWithChildren.copyWith().toJson(),
|
||||
nodeWithChildren.toJson(),
|
||||
);
|
||||
});
|
||||
|
||||
test('test textNode copyWith', () {
|
||||
final textNode = TextNode(
|
||||
type: 'example',
|
||||
children: LinkedList(),
|
||||
attributes: {
|
||||
'example': 'example',
|
||||
},
|
||||
delta: Delta()..insert('AppFlowy'),
|
||||
);
|
||||
expect(textNode.toJson(), {
|
||||
'type': 'example',
|
||||
'attributes': {
|
||||
'example': 'example',
|
||||
},
|
||||
'delta': [
|
||||
{'insert': 'AppFlowy'},
|
||||
],
|
||||
});
|
||||
expect(
|
||||
textNode.copyWith().toJson(),
|
||||
textNode.toJson(),
|
||||
);
|
||||
|
||||
final textNodeWithChildren = TextNode(
|
||||
type: 'example',
|
||||
children: LinkedList()..add(textNode),
|
||||
attributes: {
|
||||
'example': 'example',
|
||||
},
|
||||
delta: Delta()..insert('AppFlowy'),
|
||||
);
|
||||
expect(textNodeWithChildren.toJson(), {
|
||||
'type': 'example',
|
||||
'attributes': {
|
||||
'example': 'example',
|
||||
},
|
||||
'delta': [
|
||||
{'insert': 'AppFlowy'},
|
||||
],
|
||||
'children': [
|
||||
{
|
||||
'type': 'example',
|
||||
'attributes': {
|
||||
'example': 'example',
|
||||
},
|
||||
'delta': [
|
||||
{'insert': 'AppFlowy'},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(
|
||||
textNodeWithChildren.copyWith().toJson(),
|
||||
textNodeWithChildren.toJson(),
|
||||
);
|
||||
});
|
||||
|
||||
test('test node path', () {
|
||||
Node previous = Node(
|
||||
type: 'example',
|
||||
attributes: {},
|
||||
children: LinkedList(),
|
||||
);
|
||||
const len = 10;
|
||||
for (var i = 0; i < len; i++) {
|
||||
final node = Node(
|
||||
type: 'example_$i',
|
||||
attributes: {},
|
||||
children: LinkedList(),
|
||||
);
|
||||
previous.children.add(node..parent = previous);
|
||||
previous = node;
|
||||
}
|
||||
expect(previous.path, List.filled(len, 0));
|
||||
});
|
||||
|
||||
test('test copy with', () {
|
||||
final child = Node(
|
||||
type: 'child',
|
||||
attributes: {},
|
||||
children: LinkedList(),
|
||||
);
|
||||
final base = Node(
|
||||
type: 'base',
|
||||
attributes: {},
|
||||
children: LinkedList()..add(child),
|
||||
);
|
||||
final node = base.copyWith(
|
||||
type: 'node',
|
||||
);
|
||||
expect(identical(node.attributes, base.attributes), false);
|
||||
expect(identical(node.children, base.children), false);
|
||||
expect(identical(node.children.first, base.children.first), false);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:appflowy_editor/src/extensions/path_extensions.dart';
|
||||
|
||||
void main() async {
|
||||
setUpAll(() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
|
||||
group('path_extensions.dart', () {
|
||||
test('test path equality', () {
|
||||
var p1 = [0, 0];
|
||||
var p2 = [0];
|
||||
|
||||
expect(p1 > p2, true);
|
||||
expect(p1 >= p2, true);
|
||||
expect(p1 < p2, false);
|
||||
expect(p1 <= p2, false);
|
||||
|
||||
p1 = [1, 1, 2];
|
||||
p2 = [1, 1, 3];
|
||||
|
||||
expect(p2 > p1, true);
|
||||
expect(p2 >= p1, true);
|
||||
expect(p2 < p1, false);
|
||||
expect(p2 <= p1, false);
|
||||
|
||||
p1 = [2, 0, 1];
|
||||
p2 = [2, 0, 1];
|
||||
|
||||
expect(p2 > p1, false);
|
||||
expect(p1 > p2, false);
|
||||
expect(p2 >= p1, true);
|
||||
expect(p2 <= p1, true);
|
||||
expect(pathEquals(p1, p2), true);
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/infra/infra.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() async {
|
||||
group('infra.dart', () {
|
||||
test('find the last text node', () {
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
TextNode textNode() {
|
||||
return TextNode(
|
||||
type: 'text',
|
||||
delta: Delta()..insert(text),
|
||||
);
|
||||
}
|
||||
|
||||
final node110 = textNode();
|
||||
final node111 = textNode();
|
||||
final node11 = textNode()
|
||||
..insert(node110)
|
||||
..insert(node111);
|
||||
final node10 = textNode();
|
||||
final node1 = textNode()
|
||||
..insert(node10)
|
||||
..insert(node11);
|
||||
final node0 = textNode();
|
||||
final node = textNode()
|
||||
..insert(node0)
|
||||
..insert(node1);
|
||||
|
||||
expect(Infra.findLastTextNode(node)?.path, [1, 1, 1]);
|
||||
expect(Infra.findLastTextNode(node0)?.path, [0]);
|
||||
expect(Infra.findLastTextNode(node1)?.path, [1, 1, 1]);
|
||||
expect(Infra.findLastTextNode(node10)?.path, [1, 0]);
|
||||
expect(Infra.findLastTextNode(node11)?.path, [1, 1, 1]);
|
||||
|
||||
expect(Infra.forwardNearestTextNode(node111)?.path, [1, 1, 0]);
|
||||
expect(Infra.forwardNearestTextNode(node110)?.path, [1, 1]);
|
||||
expect(Infra.forwardNearestTextNode(node11)?.path, [1, 0]);
|
||||
expect(Infra.forwardNearestTextNode(node10)?.path, [1]);
|
||||
expect(Infra.forwardNearestTextNode(node1)?.path, [0]);
|
||||
expect(Infra.forwardNearestTextNode(node0)?.path, []);
|
||||
});
|
||||
});
|
||||
}
|
@ -19,6 +19,7 @@ class EditorWidgetTester {
|
||||
EditorState get editorState => _editorState;
|
||||
Node get root => _editorState.document.root;
|
||||
|
||||
StateTree get document => _editorState.document;
|
||||
int get documentLength => _editorState.document.root.children.length;
|
||||
Selection? get documentSelection =>
|
||||
_editorState.service.selectionService.currentSelection.value;
|
||||
|
@ -1,49 +0,0 @@
|
||||
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_item_widget.dart';
|
||||
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart';
|
||||
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '../../infra/test_editor.dart';
|
||||
|
||||
void main() async {
|
||||
setUpAll(() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
|
||||
group('selection_menu_item_widget.dart', () {
|
||||
testWidgets('test selection menu item widget', (tester) async {
|
||||
bool flag = false;
|
||||
final editorState = tester.editor.editorState;
|
||||
final menuService = _TestSelectionMenuService();
|
||||
const icon = Icon(Icons.abc);
|
||||
final item = SelectionMenuItem(
|
||||
name: () => 'example',
|
||||
icon: icon,
|
||||
keywords: ['example A', 'example B'],
|
||||
handler: (editorState, menuService, context) {
|
||||
flag = true;
|
||||
},
|
||||
);
|
||||
final widget = SelectionMenuItemWidget(
|
||||
editorState: editorState,
|
||||
menuService: menuService,
|
||||
item: item,
|
||||
isSelected: true,
|
||||
);
|
||||
await tester.pumpWidget(MaterialApp(home: widget));
|
||||
await tester.tap(find.byType(SelectionMenuItemWidget));
|
||||
expect(flag, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _TestSelectionMenuService implements SelectionMenuService {
|
||||
@override
|
||||
void dismiss() {}
|
||||
|
||||
@override
|
||||
void show() {}
|
||||
|
||||
@override
|
||||
Offset get topLeft => throw UnimplementedError();
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
|
||||
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_item_widget.dart';
|
||||
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart';
|
||||
import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
|
||||
|
@ -1,10 +1,12 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/render/image/image_node_widget.dart';
|
||||
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:network_image_mock/network_image_mock.dart';
|
||||
import '../../infra/test_editor.dart';
|
||||
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
|
||||
|
||||
void main() async {
|
||||
setUpAll(() {
|
||||
@ -267,6 +269,140 @@ void main() async {
|
||||
BuiltInAttributeKey.h1,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Delete the nested bulleted list', (tester) async {
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
final node = TextNode(
|
||||
type: 'text',
|
||||
delta: Delta()..insert(text),
|
||||
attributes: {
|
||||
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
|
||||
},
|
||||
);
|
||||
node.insert(
|
||||
node.copyWith()
|
||||
..insert(
|
||||
node.copyWith(),
|
||||
),
|
||||
);
|
||||
|
||||
final editor = tester.editor..insert(node);
|
||||
await editor.startTesting();
|
||||
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// Welcome to Appflowy 😁
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [0, 0, 0], startOffset: 0),
|
||||
);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
|
||||
expect(editor.nodeAtPath([0, 0, 0])?.subtype, null);
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [0, 0, 0], startOffset: 0),
|
||||
);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
|
||||
expect(editor.nodeAtPath([0, 1]) != null, true);
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [0, 1], startOffset: 0),
|
||||
);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
|
||||
expect(editor.nodeAtPath([1]) != null, true);
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [1], startOffset: 0),
|
||||
);
|
||||
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁Welcome to Appflowy 😁
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [0, 0], startOffset: text.length),
|
||||
);
|
||||
expect((editor.nodeAtPath([0, 0]) as TextNode).toRawString(), text * 2);
|
||||
});
|
||||
|
||||
testWidgets('Delete the complicated nested bulleted list', (tester) async {
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
final node = TextNode(
|
||||
type: 'text',
|
||||
delta: Delta()..insert(text),
|
||||
attributes: {
|
||||
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
|
||||
},
|
||||
);
|
||||
|
||||
node
|
||||
..insert(
|
||||
node.copyWith(children: LinkedList()),
|
||||
)
|
||||
..insert(
|
||||
node.copyWith(children: LinkedList())
|
||||
..insert(
|
||||
node.copyWith(children: LinkedList()),
|
||||
)
|
||||
..insert(
|
||||
node.copyWith(children: LinkedList()),
|
||||
),
|
||||
);
|
||||
|
||||
final editor = tester.editor..insert(node);
|
||||
await editor.startTesting();
|
||||
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [0, 1], startOffset: 0),
|
||||
);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
|
||||
expect(
|
||||
editor.nodeAtPath([0, 1])!.subtype != BuiltInAttributeKey.bulletedList,
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
editor.nodeAtPath([0, 1, 0])!.subtype,
|
||||
BuiltInAttributeKey.bulletedList,
|
||||
);
|
||||
expect(
|
||||
editor.nodeAtPath([0, 1, 1])!.subtype,
|
||||
BuiltInAttributeKey.bulletedList,
|
||||
);
|
||||
expect(find.byType(FlowyRichText), findsNWidgets(5));
|
||||
|
||||
// Before
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// After
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
|
||||
expect(
|
||||
editor.nodeAtPath([0, 0])!.subtype == BuiltInAttributeKey.bulletedList,
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
(editor.nodeAtPath([0, 0]) as TextNode).toRawString() == text * 2,
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
editor.nodeAtPath([0, 1])!.subtype == BuiltInAttributeKey.bulletedList,
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
editor.nodeAtPath([0, 2])!.subtype == BuiltInAttributeKey.bulletedList,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _deleteFirstImage(WidgetTester tester, bool isBackward) async {
|
||||
|
@ -0,0 +1,151 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '../../infra/test_editor.dart';
|
||||
|
||||
void main() async {
|
||||
setUpAll(() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
|
||||
group('tab_handler.dart', () {
|
||||
testWidgets('press tab in plain text', (tester) async {
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
final editor = tester.editor
|
||||
..insertTextNode(text)
|
||||
..insertTextNode(text);
|
||||
await editor.startTesting();
|
||||
final document = editor.document;
|
||||
|
||||
var selection = Selection.single(path: [0], startOffset: 0);
|
||||
await editor.updateSelection(selection);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||
|
||||
// nothing happens
|
||||
expect(editor.documentSelection, selection);
|
||||
expect(editor.document.toJson(), document.toJson());
|
||||
|
||||
selection = Selection.single(path: [1], startOffset: 0);
|
||||
await editor.updateSelection(selection);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||
|
||||
// nothing happens
|
||||
expect(editor.documentSelection, selection);
|
||||
expect(editor.document.toJson(), document.toJson());
|
||||
});
|
||||
|
||||
testWidgets('press tab in bulleted list', (tester) async {
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
final editor = tester.editor
|
||||
..insertTextNode(
|
||||
text,
|
||||
attributes: {
|
||||
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList
|
||||
},
|
||||
)
|
||||
..insertTextNode(
|
||||
text,
|
||||
attributes: {
|
||||
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList
|
||||
},
|
||||
)
|
||||
..insertTextNode(
|
||||
text,
|
||||
attributes: {
|
||||
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList
|
||||
},
|
||||
);
|
||||
await editor.startTesting();
|
||||
var document = editor.document;
|
||||
|
||||
var selection = Selection.single(path: [0], startOffset: 0);
|
||||
await editor.updateSelection(selection);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||
|
||||
// nothing happens
|
||||
expect(editor.documentSelection, selection);
|
||||
expect(editor.document.toJson(), document.toJson());
|
||||
|
||||
// Before
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// After
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
|
||||
selection = Selection.single(path: [1], startOffset: 0);
|
||||
await editor.updateSelection(selection);
|
||||
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [0, 0], startOffset: 0),
|
||||
);
|
||||
expect(editor.nodeAtPath([0])!.subtype, BuiltInAttributeKey.bulletedList);
|
||||
expect(editor.nodeAtPath([1])!.subtype, BuiltInAttributeKey.bulletedList);
|
||||
expect(editor.nodeAtPath([2]), null);
|
||||
expect(
|
||||
editor.nodeAtPath([0, 0])!.subtype, BuiltInAttributeKey.bulletedList);
|
||||
|
||||
selection = Selection.single(path: [1], startOffset: 0);
|
||||
await editor.updateSelection(selection);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [0, 1], startOffset: 0),
|
||||
);
|
||||
expect(editor.nodeAtPath([0])!.subtype, BuiltInAttributeKey.bulletedList);
|
||||
expect(editor.nodeAtPath([1]), null);
|
||||
expect(editor.nodeAtPath([2]), null);
|
||||
expect(
|
||||
editor.nodeAtPath([0, 0])!.subtype, BuiltInAttributeKey.bulletedList);
|
||||
expect(
|
||||
editor.nodeAtPath([0, 1])!.subtype, BuiltInAttributeKey.bulletedList);
|
||||
|
||||
// Before
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// After
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
// * Welcome to Appflowy 😁
|
||||
document = editor.document;
|
||||
selection = Selection.single(path: [0, 0], startOffset: 0);
|
||||
await editor.updateSelection(selection);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [0, 0], startOffset: 0),
|
||||
);
|
||||
expect(editor.document.toJson(), document.toJson());
|
||||
|
||||
selection = Selection.single(path: [0, 1], startOffset: 0);
|
||||
await editor.updateSelection(selection);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [0, 0, 0], startOffset: 0),
|
||||
);
|
||||
expect(
|
||||
editor.nodeAtPath([0])!.subtype,
|
||||
BuiltInAttributeKey.bulletedList,
|
||||
);
|
||||
expect(
|
||||
editor.nodeAtPath([0, 0])!.subtype,
|
||||
BuiltInAttributeKey.bulletedList,
|
||||
);
|
||||
expect(editor.nodeAtPath([0, 1]), null);
|
||||
expect(
|
||||
editor.nodeAtPath([0, 0, 0])!.subtype,
|
||||
BuiltInAttributeKey.bulletedList,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
@ -136,7 +136,6 @@ class PopoverState extends State<Popover> {
|
||||
|
||||
return Stack(children: children);
|
||||
});
|
||||
|
||||
_rootEntry.addEntry(context, this, newEntry, widget.asBarrier);
|
||||
}
|
||||
|
||||
@ -243,7 +242,7 @@ class PopoverContainerState extends State<PopoverContainer> {
|
||||
);
|
||||
}
|
||||
|
||||
close() => widget.onClose();
|
||||
void close() => widget.onClose();
|
||||
|
||||
closeAll() => widget.onCloseAll();
|
||||
void closeAll() => widget.onCloseAll();
|
||||
}
|
||||
|
@ -104,7 +104,7 @@ class AppTheme {
|
||||
..tint6 = const Color(0xfff5ffdc)
|
||||
..tint7 = const Color(0xffddffd6)
|
||||
..tint8 = const Color(0xffdefff1)
|
||||
..tint9 = const Color(0xffdefff1)
|
||||
..tint9 = const Color(0xffe1fbff)
|
||||
..main1 = const Color(0xff00bcf0)
|
||||
..main2 = const Color(0xff00b7ea)
|
||||
..textColor = _black
|
||||
@ -152,7 +152,8 @@ class AppTheme {
|
||||
ThemeData get themeData {
|
||||
var t = ThemeData(
|
||||
textTheme: TextTheme(bodyText2: TextStyle(color: textColor)),
|
||||
textSelectionTheme: TextSelectionThemeData(cursorColor: main2, selectionHandleColor: main2),
|
||||
textSelectionTheme: TextSelectionThemeData(
|
||||
cursorColor: main2, selectionHandleColor: main2),
|
||||
primaryIconTheme: IconThemeData(color: hover),
|
||||
iconTheme: IconThemeData(color: shader1),
|
||||
canvasColor: shader6,
|
||||
@ -179,7 +180,8 @@ class AppTheme {
|
||||
toggleableActiveColor: main1);
|
||||
}
|
||||
|
||||
Color shift(Color c, double d) => ColorUtils.shiftHsl(c, d * (isDark ? -1 : 1));
|
||||
Color shift(Color c, double d) =>
|
||||
ColorUtils.shiftHsl(c, d * (isDark ? -1 : 1));
|
||||
}
|
||||
|
||||
class ColorUtils {
|
||||
@ -188,14 +190,18 @@ class ColorUtils {
|
||||
return hslc.withLightness((hslc.lightness + amt).clamp(0.0, 1.0)).toColor();
|
||||
}
|
||||
|
||||
static Color parseHex(String value) => Color(int.parse(value.substring(1, 7), radix: 16) + 0xFF000000);
|
||||
static Color parseHex(String value) =>
|
||||
Color(int.parse(value.substring(1, 7), radix: 16) + 0xFF000000);
|
||||
|
||||
static Color blend(Color dst, Color src, double opacity) {
|
||||
return Color.fromARGB(
|
||||
255,
|
||||
(dst.red.toDouble() * (1.0 - opacity) + src.red.toDouble() * opacity).toInt(),
|
||||
(dst.green.toDouble() * (1.0 - opacity) + src.green.toDouble() * opacity).toInt(),
|
||||
(dst.blue.toDouble() * (1.0 - opacity) + src.blue.toDouble() * opacity).toInt(),
|
||||
(dst.red.toDouble() * (1.0 - opacity) + src.red.toDouble() * opacity)
|
||||
.toInt(),
|
||||
(dst.green.toDouble() * (1.0 - opacity) + src.green.toDouble() * opacity)
|
||||
.toInt(),
|
||||
(dst.blue.toDouble() * (1.0 - opacity) + src.blue.toDouble() * opacity)
|
||||
.toInt(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -218,8 +218,8 @@ class OverlayScreen extends StatelessWidget {
|
||||
overlapBehaviour: providerContext
|
||||
.read<OverlayDemoConfiguration>()
|
||||
.overlapBehaviour,
|
||||
width: 200.0,
|
||||
height: 200.0,
|
||||
constraints:
|
||||
BoxConstraints.tight(const Size(200, 200)),
|
||||
);
|
||||
},
|
||||
child: const Text('Show List Overlay'),
|
||||
|
@ -9,4 +9,4 @@ export 'src/flowy_overlay/flowy_overlay.dart';
|
||||
export 'src/flowy_overlay/list_overlay.dart';
|
||||
export 'src/flowy_overlay/option_overlay.dart';
|
||||
export 'src/flowy_overlay/flowy_dialog.dart';
|
||||
export 'src/flowy_overlay/appflowy_stype_popover.dart';
|
||||
export 'src/flowy_overlay/appflowy_popover.dart';
|
||||
|
@ -1,7 +1,10 @@
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/decoration.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AppFlowyPopover extends StatelessWidget {
|
||||
final Widget child;
|
||||
final PopoverController? controller;
|
||||
@ -13,6 +16,7 @@ class AppFlowyPopover extends StatelessWidget {
|
||||
final PopoverMutex? mutex;
|
||||
final Offset? offset;
|
||||
final bool asBarrier;
|
||||
final EdgeInsets margin;
|
||||
|
||||
const AppFlowyPopover({
|
||||
Key? key,
|
||||
@ -26,6 +30,7 @@ class AppFlowyPopover extends StatelessWidget {
|
||||
this.offset,
|
||||
this.controller,
|
||||
this.asBarrier = false,
|
||||
this.margin = const EdgeInsets.all(12),
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -39,13 +44,44 @@ class AppFlowyPopover extends StatelessWidget {
|
||||
triggerActions: triggerActions,
|
||||
popupBuilder: (context) {
|
||||
final child = popupBuilder(context);
|
||||
debugPrint('$child popover');
|
||||
return OverlayContainer(
|
||||
debugPrint('Show $child popover');
|
||||
return _PopoverContainer(
|
||||
constraints: constraints,
|
||||
child: popupBuilder(context),
|
||||
margin: margin,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PopoverContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
final BoxConstraints? constraints;
|
||||
final EdgeInsets margin;
|
||||
const _PopoverContainer({
|
||||
required this.child,
|
||||
required this.margin,
|
||||
this.constraints,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
final decoration = FlowyDecoration.decoration(
|
||||
theme.surface,
|
||||
theme.shadowColor.withOpacity(0.15),
|
||||
);
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Container(
|
||||
padding: margin,
|
||||
decoration: decoration,
|
||||
constraints: constraints,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ import 'dart:ui';
|
||||
import 'package:flowy_infra_ui/src/flowy_overlay/layout.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
export './overlay_container.dart';
|
||||
|
||||
/// Specifies how overlay are anchored to the SourceWidget
|
||||
enum AnchorDirection {
|
||||
|
@ -1,5 +1,10 @@
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/decoration.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ListOverlayFooter {
|
||||
Widget widget;
|
||||
@ -16,46 +21,55 @@ class ListOverlay extends StatelessWidget {
|
||||
const ListOverlay({
|
||||
Key? key,
|
||||
required this.itemBuilder,
|
||||
this.itemCount,
|
||||
this.itemCount = 0,
|
||||
this.controller,
|
||||
this.width = double.infinity,
|
||||
this.height = double.infinity,
|
||||
this.constraints = const BoxConstraints(),
|
||||
this.footer,
|
||||
}) : super(key: key);
|
||||
|
||||
final IndexedWidgetBuilder itemBuilder;
|
||||
final int? itemCount;
|
||||
final int itemCount;
|
||||
final ScrollController? controller;
|
||||
final double width;
|
||||
final double height;
|
||||
final BoxConstraints constraints;
|
||||
final ListOverlayFooter? footer;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const padding = EdgeInsets.symmetric(horizontal: 6, vertical: 6);
|
||||
double totalHeight = height + padding.vertical;
|
||||
double totalHeight = constraints.minHeight + padding.vertical;
|
||||
if (footer != null) {
|
||||
totalHeight = totalHeight + footer!.height + footer!.padding.vertical;
|
||||
}
|
||||
|
||||
final innerConstraints = BoxConstraints(
|
||||
minHeight: totalHeight,
|
||||
maxHeight: max(constraints.maxHeight, totalHeight),
|
||||
minWidth: constraints.minWidth,
|
||||
maxWidth: constraints.maxWidth,
|
||||
);
|
||||
|
||||
List<Widget> children = [];
|
||||
for (var i = 0; i < itemCount; i++) {
|
||||
children.add(itemBuilder(context, i));
|
||||
}
|
||||
|
||||
return OverlayContainer(
|
||||
constraints: BoxConstraints.tight(Size(width, totalHeight)),
|
||||
constraints: innerConstraints,
|
||||
padding: padding,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemBuilder: itemBuilder,
|
||||
itemCount: itemCount,
|
||||
controller: controller,
|
||||
),
|
||||
if (footer != null)
|
||||
Padding(
|
||||
padding: footer!.padding,
|
||||
child: footer!.widget,
|
||||
),
|
||||
],
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: IntrinsicWidth(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...children,
|
||||
if (footer != null)
|
||||
Padding(
|
||||
padding: footer!.padding,
|
||||
child: footer!.widget,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -65,10 +79,9 @@ class ListOverlay extends StatelessWidget {
|
||||
BuildContext context, {
|
||||
required String identifier,
|
||||
required IndexedWidgetBuilder itemBuilder,
|
||||
int? itemCount,
|
||||
int itemCount = 0,
|
||||
ScrollController? controller,
|
||||
double width = double.infinity,
|
||||
double height = double.infinity,
|
||||
BoxConstraints constraints = const BoxConstraints(),
|
||||
required BuildContext anchorContext,
|
||||
AnchorDirection? anchorDirection,
|
||||
FlowyOverlayDelegate? delegate,
|
||||
@ -82,8 +95,7 @@ class ListOverlay extends StatelessWidget {
|
||||
itemBuilder: itemBuilder,
|
||||
itemCount: itemCount,
|
||||
controller: controller,
|
||||
width: width,
|
||||
height: height,
|
||||
constraints: constraints,
|
||||
footer: footer,
|
||||
),
|
||||
identifier: identifier,
|
||||
@ -96,3 +108,35 @@ class ListOverlay extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const overlayContainerPadding = EdgeInsets.all(12);
|
||||
|
||||
class OverlayContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
final BoxConstraints? constraints;
|
||||
final EdgeInsets padding;
|
||||
const OverlayContainer({
|
||||
required this.child,
|
||||
this.constraints,
|
||||
this.padding = overlayContainerPadding,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme =
|
||||
context.watch<AppTheme?>() ?? AppTheme.fromType(ThemeType.light);
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Container(
|
||||
padding: padding,
|
||||
decoration: FlowyDecoration.decoration(
|
||||
theme.surface,
|
||||
theme.shadowColor.withOpacity(0.15),
|
||||
),
|
||||
constraints: constraints,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,34 +0,0 @@
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/decoration.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
const overlayContainerPadding = EdgeInsets.all(12);
|
||||
|
||||
class OverlayContainer extends StatelessWidget {
|
||||
final Widget child;
|
||||
final BoxConstraints? constraints;
|
||||
final EdgeInsets padding;
|
||||
const OverlayContainer({
|
||||
required this.child,
|
||||
this.constraints,
|
||||
this.padding = overlayContainerPadding,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme =
|
||||
context.watch<AppTheme?>() ?? AppTheme.fromType(ThemeType.light);
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Container(
|
||||
padding: padding,
|
||||
decoration: FlowyDecoration.decoration(
|
||||
theme.surface, theme.shadowColor.withOpacity(0.15)),
|
||||
constraints: constraints,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -12,6 +12,8 @@ class FlowyButton extends StatelessWidget {
|
||||
final Widget? rightIcon;
|
||||
final Color hoverColor;
|
||||
final bool isSelected;
|
||||
final BorderRadius radius;
|
||||
|
||||
const FlowyButton({
|
||||
Key? key,
|
||||
required this.text,
|
||||
@ -22,6 +24,7 @@ class FlowyButton extends StatelessWidget {
|
||||
this.rightIcon,
|
||||
this.hoverColor = Colors.transparent,
|
||||
this.isSelected = false,
|
||||
this.radius = const BorderRadius.all(Radius.circular(6)),
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -29,8 +32,10 @@ class FlowyButton extends StatelessWidget {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: FlowyHover(
|
||||
style:
|
||||
HoverStyle(borderRadius: BorderRadius.zero, hoverColor: hoverColor),
|
||||
style: HoverStyle(
|
||||
borderRadius: radius,
|
||||
hoverColor: hoverColor,
|
||||
),
|
||||
onHover: onHover,
|
||||
setSelected: () => isSelected,
|
||||
builder: (context, onHover) => _render(),
|
||||
|
@ -221,8 +221,9 @@ impl DefaultFolderBuilder {
|
||||
initial_quill_delta_string()
|
||||
};
|
||||
let _ = view_controller.set_latest_view(&view.id);
|
||||
let layout_type = ViewLayoutTypePB::from(view.layout.clone());
|
||||
let _ = view_controller
|
||||
.create_view(&view.id, ViewDataTypePB::Text, Bytes::from(view_data))
|
||||
.create_view(&view.id, ViewDataTypePB::Text, layout_type, Bytes::from(view_data))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
@ -249,11 +250,17 @@ impl FolderManager {
|
||||
pub trait ViewDataProcessor {
|
||||
fn initialize(&self) -> FutureResult<(), FlowyError>;
|
||||
|
||||
fn create_container(&self, user_id: &str, view_id: &str, delta_data: Bytes) -> FutureResult<(), FlowyError>;
|
||||
fn create_container(
|
||||
&self,
|
||||
user_id: &str,
|
||||
view_id: &str,
|
||||
layout: ViewLayoutTypePB,
|
||||
delta_data: Bytes,
|
||||
) -> FutureResult<(), FlowyError>;
|
||||
|
||||
fn close_container(&self, view_id: &str) -> FutureResult<(), FlowyError>;
|
||||
|
||||
fn get_delta_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError>;
|
||||
fn get_view_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError>;
|
||||
|
||||
fn create_default_view(
|
||||
&self,
|
||||
@ -267,6 +274,7 @@ pub trait ViewDataProcessor {
|
||||
user_id: &str,
|
||||
view_id: &str,
|
||||
data: Vec<u8>,
|
||||
layout: ViewLayoutTypePB,
|
||||
) -> FutureResult<Bytes, FlowyError>;
|
||||
|
||||
fn data_type(&self) -> ViewDataTypePB;
|
||||
|
@ -1,5 +1,5 @@
|
||||
pub use crate::entities::view::ViewDataTypePB;
|
||||
use crate::entities::ViewInfoPB;
|
||||
use crate::entities::{ViewInfoPB, ViewLayoutTypePB};
|
||||
use crate::manager::{ViewDataProcessor, ViewDataProcessorMap};
|
||||
use crate::{
|
||||
dart_notification::{send_dart_notification, FolderNotification},
|
||||
@ -61,16 +61,28 @@ impl ViewController {
|
||||
let processor = self.get_data_processor(params.data_type.clone())?;
|
||||
let user_id = self.user.user_id()?;
|
||||
if params.view_content_data.is_empty() {
|
||||
tracing::trace!("Create view with build-in data");
|
||||
let view_data = processor
|
||||
.create_default_view(&user_id, ¶ms.view_id, params.layout.clone())
|
||||
.await?;
|
||||
params.view_content_data = view_data.to_vec();
|
||||
} else {
|
||||
tracing::trace!("Create view with view data");
|
||||
let delta_data = processor
|
||||
.create_view_from_delta_data(&user_id, ¶ms.view_id, params.view_content_data.clone())
|
||||
.create_view_from_delta_data(
|
||||
&user_id,
|
||||
¶ms.view_id,
|
||||
params.view_content_data.clone(),
|
||||
params.layout.clone(),
|
||||
)
|
||||
.await?;
|
||||
let _ = self
|
||||
.create_view(¶ms.view_id, params.data_type.clone(), delta_data)
|
||||
.create_view(
|
||||
¶ms.view_id,
|
||||
params.data_type.clone(),
|
||||
params.layout.clone(),
|
||||
delta_data,
|
||||
)
|
||||
.await?;
|
||||
};
|
||||
|
||||
@ -84,6 +96,7 @@ impl ViewController {
|
||||
&self,
|
||||
view_id: &str,
|
||||
data_type: ViewDataTypePB,
|
||||
layout_type: ViewLayoutTypePB,
|
||||
delta_data: Bytes,
|
||||
) -> Result<(), FlowyError> {
|
||||
if delta_data.is_empty() {
|
||||
@ -91,7 +104,9 @@ impl ViewController {
|
||||
}
|
||||
let user_id = self.user.user_id()?;
|
||||
let processor = self.get_data_processor(data_type)?;
|
||||
let _ = processor.create_container(&user_id, view_id, delta_data).await?;
|
||||
let _ = processor
|
||||
.create_container(&user_id, view_id, layout_type, delta_data)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -218,7 +233,7 @@ impl ViewController {
|
||||
.await?;
|
||||
|
||||
let processor = self.get_data_processor(view_rev.data_type.clone())?;
|
||||
let delta_bytes = processor.get_delta_data(view_id).await?;
|
||||
let view_data = processor.get_view_data(view_id).await?;
|
||||
let duplicate_params = CreateViewParams {
|
||||
belong_to_id: view_rev.app_id.clone(),
|
||||
name: format!("{} (copy)", &view_rev.name),
|
||||
@ -226,7 +241,7 @@ impl ViewController {
|
||||
thumbnail: view_rev.thumbnail,
|
||||
data_type: view_rev.data_type.into(),
|
||||
layout: view_rev.layout.into(),
|
||||
view_content_data: delta_bytes.to_vec(),
|
||||
view_content_data: view_data.to_vec(),
|
||||
view_id: gen_view_id(),
|
||||
};
|
||||
|
||||
|
@ -33,8 +33,3 @@ impl std::convert::From<GridNotification> for i32 {
|
||||
pub fn send_dart_notification(id: &str, ty: GridNotification) -> DartNotifyBuilder {
|
||||
DartNotifyBuilder::new(id, ty, OBSERVABLE_CATEGORY)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace")]
|
||||
pub fn send_anonymous_dart_notification(ty: GridNotification) -> DartNotifyBuilder {
|
||||
DartNotifyBuilder::new("", ty, OBSERVABLE_CATEGORY)
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ pub(crate) async fn update_grid_setting_handler(
|
||||
|
||||
let editor = manager.get_grid_editor(¶ms.grid_id)?;
|
||||
if let Some(insert_params) = params.insert_group {
|
||||
let _ = editor.create_group(insert_params).await?;
|
||||
let _ = editor.insert_group(insert_params).await?;
|
||||
}
|
||||
|
||||
if let Some(delete_params) = params.delete_group {
|
||||
|
@ -73,7 +73,12 @@ macro_rules! impl_type_option {
|
||||
match serde_json::from_str(s) {
|
||||
Ok(obj) => obj,
|
||||
Err(err) => {
|
||||
tracing::error!("{} convert from any data failed, {:?}", stringify!($target), err);
|
||||
tracing::error!(
|
||||
"{} type option deserialize from {} failed, {:?}",
|
||||
stringify!($target),
|
||||
s,
|
||||
err
|
||||
);
|
||||
$target::default()
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
use crate::entities::GridLayout;
|
||||
use crate::services::block_editor::GridBlockRevisionCompactor;
|
||||
use crate::services::grid_editor::{GridRevisionCompactor, GridRevisionEditor};
|
||||
use crate::services::grid_view_manager::make_grid_view_rev_manager;
|
||||
@ -178,10 +179,18 @@ impl GridManager {
|
||||
pub async fn make_grid_view_data(
|
||||
user_id: &str,
|
||||
view_id: &str,
|
||||
layout: GridLayout,
|
||||
grid_manager: Arc<GridManager>,
|
||||
build_context: BuildGridContext,
|
||||
) -> FlowyResult<Bytes> {
|
||||
for block_meta_data in &build_context.blocks {
|
||||
let BuildGridContext {
|
||||
field_revs,
|
||||
block_metas,
|
||||
blocks,
|
||||
grid_view_revision_data,
|
||||
} = build_context;
|
||||
|
||||
for block_meta_data in &blocks {
|
||||
let block_id = &block_meta_data.block_id;
|
||||
// Indexing the block's rows
|
||||
block_meta_data.rows.iter().for_each(|row| {
|
||||
@ -198,7 +207,7 @@ pub async fn make_grid_view_data(
|
||||
|
||||
// Will replace the grid_id with the value returned by the gen_grid_id()
|
||||
let grid_id = view_id.to_owned();
|
||||
let grid_rev = GridRevision::from_build_context(&grid_id, build_context);
|
||||
let grid_rev = GridRevision::from_build_context(&grid_id, field_revs, block_metas);
|
||||
|
||||
// Create grid
|
||||
let grid_rev_delta = make_grid_delta(&grid_rev);
|
||||
@ -208,7 +217,11 @@ pub async fn make_grid_view_data(
|
||||
let _ = grid_manager.create_grid(&grid_id, repeated_revision).await?;
|
||||
|
||||
// Create grid view
|
||||
let grid_view = GridViewRevision::new(grid_id, view_id.to_owned());
|
||||
let grid_view = if grid_view_revision_data.is_empty() {
|
||||
GridViewRevision::new(grid_id, view_id.to_owned(), layout.into())
|
||||
} else {
|
||||
GridViewRevision::from_json(grid_view_revision_data)?
|
||||
};
|
||||
let grid_view_delta = make_grid_view_delta(&grid_view);
|
||||
let grid_view_delta_bytes = grid_view_delta.json_bytes();
|
||||
let repeated_revision: RepeatedRevision =
|
||||
|
@ -19,7 +19,10 @@ impl std::str::FromStr for AnyCellData {
|
||||
type Err = FlowyError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let type_option_cell_data: AnyCellData = serde_json::from_str(s)?;
|
||||
let type_option_cell_data: AnyCellData = serde_json::from_str(s).map_err(|err| {
|
||||
let msg = format!("Deserialize {} to any cell data failed. Serde error: {}", s, err);
|
||||
FlowyError::internal().context(msg)
|
||||
})?;
|
||||
Ok(type_option_cell_data)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::entities::FieldType;
|
||||
use crate::services::cell::{AnyCellData, CellBytes};
|
||||
use crate::services::field::*;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, FieldTypeRevision};
|
||||
@ -73,20 +74,28 @@ pub fn apply_cell_data_changeset<C: ToString, T: AsRef<FieldRevision>>(
|
||||
Ok(AnyCellData::new(s, field_type).json())
|
||||
}
|
||||
|
||||
pub fn decode_any_cell_data<T: TryInto<AnyCellData>>(data: T, field_rev: &FieldRevision) -> CellBytes {
|
||||
if let Ok(any_cell_data) = data.try_into() {
|
||||
let AnyCellData { data, field_type } = any_cell_data;
|
||||
let to_field_type = field_rev.ty.into();
|
||||
match try_decode_cell_data(data.into(), field_rev, &field_type, &to_field_type) {
|
||||
Ok(cell_bytes) => cell_bytes,
|
||||
Err(e) => {
|
||||
tracing::error!("Decode cell data failed, {:?}", e);
|
||||
CellBytes::default()
|
||||
pub fn decode_any_cell_data<T: TryInto<AnyCellData, Error = FlowyError> + Debug>(
|
||||
data: T,
|
||||
field_rev: &FieldRevision,
|
||||
) -> CellBytes {
|
||||
match data.try_into() {
|
||||
Ok(any_cell_data) => {
|
||||
let AnyCellData { data, field_type } = any_cell_data;
|
||||
let to_field_type = field_rev.ty.into();
|
||||
match try_decode_cell_data(data.into(), field_rev, &field_type, &to_field_type) {
|
||||
Ok(cell_bytes) => cell_bytes,
|
||||
Err(e) => {
|
||||
tracing::error!("Decode cell data failed, {:?}", e);
|
||||
CellBytes::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::error!("Decode type option data failed");
|
||||
CellBytes::default()
|
||||
Err(_err) => {
|
||||
// It's okay to ignore this error, because it's okay that the current cell can't
|
||||
// display the existing cell data. For example, the UI of the text cell will be blank if
|
||||
// the type of the data of cell is Number.
|
||||
CellBytes::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ mod tests {
|
||||
assert_date(&type_option, 1647251762, None, "2022-03-14", &field_rev);
|
||||
}
|
||||
DateFormat::Local => {
|
||||
assert_date(&type_option, 1647251762, None, "2022/03/14", &field_rev);
|
||||
assert_date(&type_option, 1647251762, None, "03/14/2022", &field_rev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -153,7 +153,7 @@ impl DateFormat {
|
||||
// https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html
|
||||
pub fn format_str(&self) -> &'static str {
|
||||
match self {
|
||||
DateFormat::Local => "%Y/%m/%d",
|
||||
DateFormat::Local => "%m/%d/%Y",
|
||||
DateFormat::US => "%Y/%m/%d",
|
||||
DateFormat::ISO => "%Y-%m-%d",
|
||||
DateFormat::Friendly => "%b %d,%Y",
|
||||
|
@ -179,23 +179,20 @@ impl GridRevisionEditor {
|
||||
None => Err(ErrorCode::FieldDoesNotExist.into()),
|
||||
Some(field_type) => {
|
||||
let _ = self.update_field_rev(params, field_type).await?;
|
||||
match self.view_manager.did_update_field(&field_id).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => tracing::error!("View manager update field failed: {:?}", e),
|
||||
}
|
||||
let _ = self.notify_did_update_grid_field(&field_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces the field revision with new field revision.
|
||||
pub async fn replace_field(&self, field_rev: Arc<FieldRevision>) -> FlowyResult<()> {
|
||||
let field_id = field_rev.id.clone();
|
||||
let _ = self
|
||||
.modify(|grid_pad| Ok(grid_pad.replace_field_rev(field_rev.clone())?))
|
||||
.await?;
|
||||
|
||||
match self.view_manager.did_update_field(&field_rev.id).await {
|
||||
match self.view_manager.did_update_field(&field_rev.id, false).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => tracing::error!("View manager update field failed: {:?}", e),
|
||||
}
|
||||
@ -279,6 +276,7 @@ impl GridRevisionEditor {
|
||||
}
|
||||
|
||||
async fn update_field_rev(&self, params: FieldChangesetParams, field_type: FieldType) -> FlowyResult<()> {
|
||||
let mut is_type_option_changed = false;
|
||||
let _ = self
|
||||
.modify(|grid| {
|
||||
let deserializer = TypeOptionJsonDeserializer(field_type);
|
||||
@ -319,6 +317,7 @@ impl GridRevisionEditor {
|
||||
Ok(json_str) => {
|
||||
let field_type = field.ty;
|
||||
field.insert_type_option_str(&field_type, json_str);
|
||||
is_type_option_changed = true;
|
||||
is_changed = Some(())
|
||||
}
|
||||
Err(err) => {
|
||||
@ -333,7 +332,11 @@ impl GridRevisionEditor {
|
||||
})
|
||||
.await?;
|
||||
|
||||
match self.view_manager.did_update_field(¶ms.field_id).await {
|
||||
match self
|
||||
.view_manager
|
||||
.did_update_field(¶ms.field_id, is_type_option_changed)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(e) => tracing::error!("View manager update field failed: {:?}", e),
|
||||
}
|
||||
@ -537,7 +540,7 @@ impl GridRevisionEditor {
|
||||
self.view_manager.get_filters().await
|
||||
}
|
||||
|
||||
pub async fn create_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
|
||||
pub async fn insert_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
|
||||
self.view_manager.insert_or_update_group(params).await
|
||||
}
|
||||
|
||||
@ -673,6 +676,7 @@ impl GridRevisionEditor {
|
||||
|
||||
pub async fn duplicate_grid(&self) -> FlowyResult<BuildGridContext> {
|
||||
let grid_pad = self.grid_pad.read().await;
|
||||
let grid_view_revision_data = self.view_manager.duplicate_grid_view().await?;
|
||||
let original_blocks = grid_pad.get_block_meta_revs();
|
||||
let (duplicated_fields, duplicated_blocks) = grid_pad.duplicate_grid_block_meta().await;
|
||||
|
||||
@ -698,6 +702,7 @@ impl GridRevisionEditor {
|
||||
field_revs: duplicated_fields.into_iter().map(Arc::new).collect(),
|
||||
block_metas: duplicated_blocks,
|
||||
blocks: blocks_meta_data,
|
||||
grid_view_revision_data,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -74,6 +74,11 @@ impl GridViewRevisionEditor {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn duplicate_view_data(&self) -> FlowyResult<String> {
|
||||
let json_str = self.pad.read().await.json_str()?;
|
||||
Ok(json_str)
|
||||
}
|
||||
|
||||
pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
|
||||
if params.group_id.is_none() {
|
||||
return;
|
||||
@ -277,6 +282,7 @@ impl GridViewRevisionEditor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all, err)]
|
||||
pub(crate) async fn group_by_field(&self, field_id: &str) -> FlowyResult<()> {
|
||||
if let Some(field_rev) = self.field_delegate.get_field_rev(field_id).await {
|
||||
let new_group_controller = new_group_controller_with_field_rev(
|
||||
@ -374,13 +380,14 @@ impl GridViewRevisionEditor {
|
||||
async fn new_group_controller(
|
||||
user_id: String,
|
||||
view_id: String,
|
||||
pad: Arc<RwLock<GridViewRevisionPad>>,
|
||||
view_rev_pad: Arc<RwLock<GridViewRevisionPad>>,
|
||||
rev_manager: Arc<RevisionManager>,
|
||||
field_delegate: Arc<dyn GridViewFieldDelegate>,
|
||||
row_delegate: Arc<dyn GridViewRowDelegate>,
|
||||
) -> FlowyResult<Box<dyn GroupController>> {
|
||||
let configuration_reader = GroupConfigurationReaderImpl(pad.clone());
|
||||
let configuration_reader = GroupConfigurationReaderImpl(view_rev_pad.clone());
|
||||
let field_revs = field_delegate.get_field_revs().await;
|
||||
let layout = view_rev_pad.read().await.layout();
|
||||
// Read the group field or find a new group field
|
||||
let field_rev = configuration_reader
|
||||
.get_configuration()
|
||||
@ -391,24 +398,24 @@ async fn new_group_controller(
|
||||
.find(|field_rev| field_rev.id == configuration.field_id)
|
||||
.cloned()
|
||||
})
|
||||
.unwrap_or_else(|| find_group_field(&field_revs).unwrap());
|
||||
.unwrap_or_else(|| find_group_field(&field_revs, &layout).unwrap());
|
||||
|
||||
new_group_controller_with_field_rev(user_id, view_id, pad, rev_manager, field_rev, row_delegate).await
|
||||
new_group_controller_with_field_rev(user_id, view_id, view_rev_pad, rev_manager, field_rev, row_delegate).await
|
||||
}
|
||||
|
||||
async fn new_group_controller_with_field_rev(
|
||||
user_id: String,
|
||||
view_id: String,
|
||||
pad: Arc<RwLock<GridViewRevisionPad>>,
|
||||
view_rev_pad: Arc<RwLock<GridViewRevisionPad>>,
|
||||
rev_manager: Arc<RevisionManager>,
|
||||
field_rev: Arc<FieldRevision>,
|
||||
row_delegate: Arc<dyn GridViewRowDelegate>,
|
||||
) -> FlowyResult<Box<dyn GroupController>> {
|
||||
let configuration_reader = GroupConfigurationReaderImpl(pad.clone());
|
||||
let configuration_reader = GroupConfigurationReaderImpl(view_rev_pad.clone());
|
||||
let configuration_writer = GroupConfigurationWriterImpl {
|
||||
user_id,
|
||||
rev_manager,
|
||||
view_pad: pad,
|
||||
view_pad: view_rev_pad,
|
||||
};
|
||||
let row_revs = row_delegate.gv_row_revs().await;
|
||||
make_group_controller(view_id, field_rev, row_revs, configuration_reader, configuration_writer).await
|
||||
|
@ -56,6 +56,12 @@ impl GridViewManager {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn duplicate_grid_view(&self) -> FlowyResult<String> {
|
||||
let editor = self.get_default_view_editor().await?;
|
||||
let view_data = editor.duplicate_view_data().await?;
|
||||
Ok(view_data)
|
||||
}
|
||||
|
||||
/// When the row was created, we may need to modify the [RowRevision] according to the [CreateRowParams].
|
||||
pub(crate) async fn will_create_row(&self, row_rev: &mut RowRevision, params: &CreateRowParams) {
|
||||
for view_editor in self.view_editors.iter() {
|
||||
@ -169,9 +175,15 @@ impl GridViewManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn did_update_field(&self, field_id: &str) -> FlowyResult<()> {
|
||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||
pub(crate) async fn did_update_field(&self, field_id: &str, is_type_option_changed: bool) -> FlowyResult<()> {
|
||||
let view_editor = self.get_default_view_editor().await?;
|
||||
let _ = view_editor.did_update_field(field_id).await?;
|
||||
if is_type_option_changed {
|
||||
let _ = view_editor.group_by_field(field_id).await?;
|
||||
} else {
|
||||
let _ = view_editor.did_update_field(field_id).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -14,5 +14,6 @@ pub trait GroupAction: Send + Sync {
|
||||
fn can_group(&self, content: &str, cell_data: &Self::CellDataType) -> bool;
|
||||
fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB>;
|
||||
fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB>;
|
||||
// Move row from one group to another
|
||||
fn move_row(&mut self, cell_data: &Self::CellDataType, context: MoveGroupRowContext) -> Vec<GroupChangesetPB>;
|
||||
}
|
||||
|
@ -86,8 +86,7 @@ where
|
||||
G: GroupGenerator<Context = GroupContext<C>, TypeOptionType = T>,
|
||||
{
|
||||
pub async fn new(field_rev: &Arc<FieldRevision>, mut configuration: GroupContext<C>) -> FlowyResult<Self> {
|
||||
let field_type_rev = field_rev.ty;
|
||||
let type_option = field_rev.get_type_option::<T>(field_type_rev);
|
||||
let type_option = field_rev.get_type_option::<T>(field_rev.ty);
|
||||
let groups = G::generate_groups(&field_rev.id, &configuration, &type_option);
|
||||
let _ = configuration.init_groups(groups, true)?;
|
||||
|
||||
@ -278,12 +277,19 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all, err)]
|
||||
fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult<Vec<GroupChangesetPB>> {
|
||||
if let Some(cell_rev) = context.row_rev.cells.get(&self.field_id) {
|
||||
let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), context.field_rev);
|
||||
let cell_rev = match context.row_rev.cells.get(&self.field_id) {
|
||||
Some(cell_rev) => Some(cell_rev.clone()),
|
||||
None => self.default_cell_rev(),
|
||||
};
|
||||
|
||||
if let Some(cell_rev) = cell_rev {
|
||||
let cell_bytes = decode_any_cell_data(cell_rev.data, context.field_rev);
|
||||
let cell_data = cell_bytes.parser::<P>()?;
|
||||
Ok(self.move_row(&cell_data, context))
|
||||
} else {
|
||||
tracing::warn!("Unexpected moving group row, changes should not be empty");
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ impl GroupControllerSharedOperation for DefaultGroupController {
|
||||
_row_rev: &RowRevision,
|
||||
_field_rev: &FieldRevision,
|
||||
) -> FlowyResult<Vec<GroupChangesetPB>> {
|
||||
todo!()
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn did_delete_row(
|
||||
@ -63,7 +63,7 @@ impl GroupControllerSharedOperation for DefaultGroupController {
|
||||
_row_rev: &RowRevision,
|
||||
_field_rev: &FieldRevision,
|
||||
) -> FlowyResult<Vec<GroupChangesetPB>> {
|
||||
todo!()
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn move_group_row(&mut self, _context: MoveGroupRowContext) -> FlowyResult<Vec<GroupChangesetPB>> {
|
||||
|
@ -1,11 +1,12 @@
|
||||
use crate::entities::{GroupChangesetPB, InsertedRowPB, RowPB};
|
||||
use crate::services::cell::insert_select_option_cell;
|
||||
use crate::services::field::{SelectOptionCellDataPB, SelectOptionPB};
|
||||
use crate::entities::{FieldType, GroupChangesetPB, InsertedRowPB, RowPB};
|
||||
use crate::services::cell::{insert_checkbox_cell, insert_select_option_cell};
|
||||
use crate::services::field::{SelectOptionCellDataPB, SelectOptionPB, CHECK};
|
||||
use crate::services::group::configuration::GroupContext;
|
||||
use crate::services::group::{GeneratedGroup, Group};
|
||||
|
||||
use crate::services::group::controller::MoveGroupRowContext;
|
||||
use flowy_grid_data_model::revision::{GroupRevision, RowRevision, SelectOptionGroupConfigurationRevision};
|
||||
use crate::services::group::{GeneratedGroup, Group};
|
||||
use flowy_grid_data_model::revision::{
|
||||
CellRevision, FieldRevision, GroupRevision, RowRevision, SelectOptionGroupConfigurationRevision,
|
||||
};
|
||||
|
||||
pub type SelectOptionGroupContext = GroupContext<SelectOptionGroupConfigurationRevision>;
|
||||
|
||||
@ -109,10 +110,16 @@ pub fn move_group_row(group: &mut Group, context: &mut MoveGroupRowContext) -> O
|
||||
|
||||
// Update the corresponding row's cell content.
|
||||
if from_index.is_none() {
|
||||
tracing::debug!("Mark row:{} belong to group:{}", row_rev.id, group.id);
|
||||
let cell_rev = insert_select_option_cell(group.id.clone(), field_rev);
|
||||
row_changeset.cell_by_field_id.insert(field_rev.id.clone(), cell_rev);
|
||||
changeset.updated_rows.push(RowPB::from(*row_rev));
|
||||
let cell_rev = make_inserted_cell_rev(&group.id, field_rev);
|
||||
if let Some(cell_rev) = cell_rev {
|
||||
tracing::debug!(
|
||||
"Update content of the cell in the row:{} to group:{}",
|
||||
row_rev.id,
|
||||
group.id
|
||||
);
|
||||
row_changeset.cell_by_field_id.insert(field_rev.id.clone(), cell_rev);
|
||||
changeset.updated_rows.push(RowPB::from(*row_rev));
|
||||
}
|
||||
}
|
||||
}
|
||||
if changeset.is_empty() {
|
||||
@ -122,6 +129,27 @@ pub fn move_group_row(group: &mut Group, context: &mut MoveGroupRowContext) -> O
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_inserted_cell_rev(group_id: &str, field_rev: &FieldRevision) -> Option<CellRevision> {
|
||||
let field_type: FieldType = field_rev.ty.into();
|
||||
match field_type {
|
||||
FieldType::SingleSelect => {
|
||||
let cell_rev = insert_select_option_cell(group_id.to_owned(), field_rev);
|
||||
Some(cell_rev)
|
||||
}
|
||||
FieldType::MultiSelect => {
|
||||
let cell_rev = insert_select_option_cell(group_id.to_owned(), field_rev);
|
||||
Some(cell_rev)
|
||||
}
|
||||
FieldType::Checkbox => {
|
||||
let cell_rev = insert_checkbox_cell(group_id == CHECK, field_rev);
|
||||
Some(cell_rev)
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("Unknown field type: {:?}", field_type);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn generate_select_option_groups(
|
||||
_field_id: &str,
|
||||
_group_ctx: &SelectOptionGroupContext,
|
||||
|
@ -8,7 +8,7 @@ use crate::services::group::{
|
||||
use flowy_error::FlowyResult;
|
||||
use flowy_grid_data_model::revision::{
|
||||
CheckboxGroupConfigurationRevision, DateGroupConfigurationRevision, FieldRevision, GroupConfigurationRevision,
|
||||
NumberGroupConfigurationRevision, RowRevision, SelectOptionGroupConfigurationRevision,
|
||||
LayoutRevision, NumberGroupConfigurationRevision, RowRevision, SelectOptionGroupConfigurationRevision,
|
||||
TextGroupConfigurationRevision, UrlGroupConfigurationRevision,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
@ -62,15 +62,17 @@ where
|
||||
Ok(group_controller)
|
||||
}
|
||||
|
||||
pub fn find_group_field(field_revs: &[Arc<FieldRevision>]) -> Option<Arc<FieldRevision>> {
|
||||
let field_rev = field_revs
|
||||
.iter()
|
||||
.find(|field_rev| {
|
||||
let field_type: FieldType = field_rev.ty.into();
|
||||
field_type.can_be_group()
|
||||
})
|
||||
.cloned();
|
||||
field_rev
|
||||
pub fn find_group_field(field_revs: &[Arc<FieldRevision>], layout: &LayoutRevision) -> Option<Arc<FieldRevision>> {
|
||||
match layout {
|
||||
LayoutRevision::Table => field_revs.iter().find(|field_rev| field_rev.is_primary).cloned(),
|
||||
LayoutRevision::Board => field_revs
|
||||
.iter()
|
||||
.find(|field_rev| {
|
||||
let field_type: FieldType = field_rev.ty.into();
|
||||
field_type.can_be_group()
|
||||
})
|
||||
.cloned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_group_configuration(field_rev: &FieldRevision) -> GroupConfigurationRevision {
|
||||
|
@ -127,6 +127,7 @@ fn make_test_grid() -> BuildGridContext {
|
||||
let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default())
|
||||
.name("Name")
|
||||
.visibility(true)
|
||||
.primary(true)
|
||||
.build();
|
||||
grid_builder.add_field(text_field);
|
||||
}
|
||||
|
@ -382,11 +382,8 @@ async fn group_insert_single_select_option_test() {
|
||||
AssertGroupCount(5),
|
||||
];
|
||||
test.run_scripts(scripts).await;
|
||||
|
||||
// the group at index 4 is the default_group, so the new insert group will be the
|
||||
// index 3.
|
||||
let group_3 = test.group_at_index(3).await;
|
||||
assert_eq!(group_3.desc, new_option_name);
|
||||
let new_group = test.group_at_index(0).await;
|
||||
assert_eq!(new_group.desc, new_option_name);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -7,6 +7,7 @@ use flowy_folder::{
|
||||
event_map::{FolderCouldServiceV1, WorkspaceDatabase, WorkspaceUser},
|
||||
manager::FolderManager,
|
||||
};
|
||||
use flowy_grid::entities::GridLayout;
|
||||
use flowy_grid::manager::{make_grid_view_data, GridManager};
|
||||
use flowy_grid::util::{make_default_board, make_default_grid};
|
||||
use flowy_grid_data_model::revision::BuildGridContext;
|
||||
@ -142,7 +143,15 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
|
||||
FutureResult::new(async move { manager.init() })
|
||||
}
|
||||
|
||||
fn create_container(&self, user_id: &str, view_id: &str, delta_data: Bytes) -> FutureResult<(), FlowyError> {
|
||||
fn create_container(
|
||||
&self,
|
||||
user_id: &str,
|
||||
view_id: &str,
|
||||
layout: ViewLayoutTypePB,
|
||||
delta_data: Bytes,
|
||||
) -> FutureResult<(), FlowyError> {
|
||||
// Only accept Document type
|
||||
debug_assert_eq!(layout, ViewLayoutTypePB::Document);
|
||||
let repeated_revision: RepeatedRevision = Revision::initial_revision(user_id, view_id, delta_data).into();
|
||||
let view_id = view_id.to_string();
|
||||
let manager = self.0.clone();
|
||||
@ -161,7 +170,7 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
|
||||
})
|
||||
}
|
||||
|
||||
fn get_delta_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
|
||||
fn get_view_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
|
||||
let view_id = view_id.to_string();
|
||||
let manager = self.0.clone();
|
||||
FutureResult::new(async move {
|
||||
@ -196,7 +205,9 @@ impl ViewDataProcessor for TextBlockViewDataProcessor {
|
||||
_user_id: &str,
|
||||
_view_id: &str,
|
||||
data: Vec<u8>,
|
||||
layout: ViewLayoutTypePB,
|
||||
) -> FutureResult<Bytes, FlowyError> {
|
||||
debug_assert_eq!(layout, ViewLayoutTypePB::Document);
|
||||
FutureResult::new(async move { Ok(Bytes::from(data)) })
|
||||
}
|
||||
|
||||
@ -211,7 +222,13 @@ impl ViewDataProcessor for GridViewDataProcessor {
|
||||
FutureResult::new(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn create_container(&self, user_id: &str, view_id: &str, delta_data: Bytes) -> FutureResult<(), FlowyError> {
|
||||
fn create_container(
|
||||
&self,
|
||||
user_id: &str,
|
||||
view_id: &str,
|
||||
_layout: ViewLayoutTypePB,
|
||||
delta_data: Bytes,
|
||||
) -> FutureResult<(), FlowyError> {
|
||||
let repeated_revision: RepeatedRevision = Revision::initial_revision(user_id, view_id, delta_data).into();
|
||||
let view_id = view_id.to_string();
|
||||
let grid_manager = self.0.clone();
|
||||
@ -230,7 +247,7 @@ impl ViewDataProcessor for GridViewDataProcessor {
|
||||
})
|
||||
}
|
||||
|
||||
fn get_delta_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
|
||||
fn get_view_data(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> {
|
||||
let view_id = view_id.to_string();
|
||||
let grid_manager = self.0.clone();
|
||||
FutureResult::new(async move {
|
||||
@ -246,19 +263,22 @@ impl ViewDataProcessor for GridViewDataProcessor {
|
||||
view_id: &str,
|
||||
layout: ViewLayoutTypePB,
|
||||
) -> FutureResult<Bytes, FlowyError> {
|
||||
let build_context = match layout {
|
||||
ViewLayoutTypePB::Grid => make_default_grid(),
|
||||
ViewLayoutTypePB::Board => make_default_board(),
|
||||
let (build_context, layout) = match layout {
|
||||
ViewLayoutTypePB::Grid => (make_default_grid(), GridLayout::Table),
|
||||
ViewLayoutTypePB::Board => (make_default_board(), GridLayout::Board),
|
||||
ViewLayoutTypePB::Document => {
|
||||
return FutureResult::new(async move {
|
||||
Err(FlowyError::internal().context(format!("Can't handle {:?} layout type", layout)))
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let user_id = user_id.to_string();
|
||||
let view_id = view_id.to_string();
|
||||
let grid_manager = self.0.clone();
|
||||
FutureResult::new(async move { make_grid_view_data(&user_id, &view_id, grid_manager, build_context).await })
|
||||
FutureResult::new(
|
||||
async move { make_grid_view_data(&user_id, &view_id, layout, grid_manager, build_context).await },
|
||||
)
|
||||
}
|
||||
|
||||
fn create_view_from_delta_data(
|
||||
@ -266,15 +286,26 @@ impl ViewDataProcessor for GridViewDataProcessor {
|
||||
user_id: &str,
|
||||
view_id: &str,
|
||||
data: Vec<u8>,
|
||||
layout: ViewLayoutTypePB,
|
||||
) -> FutureResult<Bytes, FlowyError> {
|
||||
let user_id = user_id.to_string();
|
||||
let view_id = view_id.to_string();
|
||||
let grid_manager = self.0.clone();
|
||||
|
||||
let layout = match layout {
|
||||
ViewLayoutTypePB::Grid => GridLayout::Table,
|
||||
ViewLayoutTypePB::Board => GridLayout::Board,
|
||||
ViewLayoutTypePB::Document => {
|
||||
return FutureResult::new(async move {
|
||||
Err(FlowyError::internal().context(format!("Can't handle {:?} layout type", layout)))
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
FutureResult::new(async move {
|
||||
let bytes = Bytes::from(data);
|
||||
let build_context = BuildGridContext::try_from(bytes)?;
|
||||
make_grid_view_data(&user_id, &view_id, grid_manager, build_context).await
|
||||
make_grid_view_data(&user_id, &view_id, layout, grid_manager, build_context).await
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -34,8 +34,6 @@ else
|
||||
printMessage "Skipping Rust installation."
|
||||
fi
|
||||
|
||||
abvc
|
||||
|
||||
# Install sqllite
|
||||
printMessage "Installing sqlLite3."
|
||||
brew install sqlite3
|
||||
|
@ -34,11 +34,15 @@ impl GridRevision {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_build_context(grid_id: &str, context: BuildGridContext) -> Self {
|
||||
pub fn from_build_context(
|
||||
grid_id: &str,
|
||||
field_revs: Vec<Arc<FieldRevision>>,
|
||||
block_metas: Vec<GridBlockMetaRevision>,
|
||||
) -> Self {
|
||||
Self {
|
||||
grid_id: grid_id.to_owned(),
|
||||
fields: context.field_revs,
|
||||
blocks: context.block_metas.into_iter().map(Arc::new).collect(),
|
||||
fields: field_revs,
|
||||
blocks: block_metas.into_iter().map(Arc::new).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -188,6 +192,9 @@ pub struct BuildGridContext {
|
||||
pub field_revs: Vec<Arc<FieldRevision>>,
|
||||
pub block_metas: Vec<GridBlockMetaRevision>,
|
||||
pub blocks: Vec<GridBlockRevision>,
|
||||
|
||||
// String in JSON format. It can be deserialized into [GridViewRevision]
|
||||
pub grid_view_revision_data: String,
|
||||
}
|
||||
|
||||
impl BuildGridContext {
|
||||
|
@ -48,16 +48,20 @@ pub struct GridViewRevision {
|
||||
}
|
||||
|
||||
impl GridViewRevision {
|
||||
pub fn new(grid_id: String, view_id: String) -> Self {
|
||||
pub fn new(grid_id: String, view_id: String, layout: LayoutRevision) -> Self {
|
||||
GridViewRevision {
|
||||
view_id,
|
||||
grid_id,
|
||||
layout: Default::default(),
|
||||
layout,
|
||||
filters: Default::default(),
|
||||
groups: Default::default(),
|
||||
// row_orders: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_json(json: String) -> Result<Self, serde_json::Error> {
|
||||
serde_json::from_str(&json)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
|
@ -103,8 +103,12 @@ impl GridRevisionPad {
|
||||
|grid_meta| match grid_meta.fields.iter().position(|field| field.id == field_id) {
|
||||
None => Ok(None),
|
||||
Some(index) => {
|
||||
grid_meta.fields.remove(index);
|
||||
Ok(Some(()))
|
||||
if grid_meta.fields[index].is_primary {
|
||||
Err(CollaborateError::can_not_delete_primary_field())
|
||||
} else {
|
||||
grid_meta.fields.remove(index);
|
||||
Ok(Some(()))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user