diff --git a/frontend/app_flowy/assets/images/grid/field/url.svg b/frontend/app_flowy/assets/images/grid/field/url.svg new file mode 100644 index 0000000000..f00f5c7aa2 --- /dev/null +++ b/frontend/app_flowy/assets/images/grid/field/url.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index 8fc9e90581..4e6c8f3420 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/frontend/app_flowy/assets/translations/en.json @@ -160,6 +160,7 @@ "numberFieldName": "Numbers", "singleSelectFieldName": "Select", "multiSelectFieldName": "Multiselect", + "urlFieldName": "URL", "numberFormat": " Number format", "dateFormat": " Date format", "includeTime": " Include time", diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart index 09b8bef584..68f8eada78 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart @@ -10,9 +10,9 @@ import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/cell_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; - import 'package:app_flowy/workspace/application/grid/cell/cell_listener.dart'; import 'package:app_flowy/workspace/application/grid/cell/select_option_service.dart'; import 'package:app_flowy/workspace/application/grid/field/field_service.dart'; diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart index aad8137f2e..e4141c3e16 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart @@ -3,6 +3,7 @@ part of 'cell_service.dart'; typedef GridCellContext = _GridCellContext; typedef GridSelectOptionCellContext = _GridCellContext; typedef GridDateCellContext = _GridCellContext; +typedef GridURLCellContext = _GridCellContext; class GridCellContextBuilder { final GridCellCache _cellCache; @@ -75,12 +76,25 @@ class GridCellContextBuilder { cellDataLoader: cellDataLoader, cellDataPersistence: CellDataPersistence(gridCell: _gridCell), ); - default: - throw UnimplementedError; + + case FieldType.URL: + final cellDataLoader = GridCellDataLoader( + gridCell: _gridCell, + parser: URLCellDataParser(), + ); + return GridURLCellContext( + gridCell: _gridCell, + cellCache: _cellCache, + cellDataLoader: cellDataLoader, + cellDataPersistence: CellDataPersistence(gridCell: _gridCell), + ); } + throw UnimplementedError; } } +// T: the type of the CellData +// D: the type of the data that will be save to disk // ignore: must_be_immutable class _GridCellContext extends Equatable { final GridCell gridCell; @@ -94,7 +108,8 @@ class _GridCellContext extends Equatable { late final ValueNotifier _cellDataNotifier; bool isListening = false; VoidCallback? _onFieldChangedFn; - Timer? _delayOperation; + Timer? _loadDataOperation; + Timer? _saveDataOperation; _GridCellContext({ required this.gridCell, @@ -124,7 +139,7 @@ class _GridCellContext extends Equatable { FieldType get fieldType => gridCell.field.fieldType; - VoidCallback? startListening({required void Function(T) onCellChanged}) { + VoidCallback? startListening({required void Function(T?) onCellChanged}) { if (isListening) { Log.error("Already started. It seems like you should call clone first"); return null; @@ -148,7 +163,7 @@ class _GridCellContext extends Equatable { } onCellChangedFn() { - onCellChanged(_cellDataNotifier.value as T); + onCellChanged(_cellDataNotifier.value); if (cellDataLoader.config.reloadOnCellChanged) { _loadData(); @@ -175,13 +190,26 @@ class _GridCellContext extends Equatable { return _fieldService.getFieldTypeOptionData(fieldType: fieldType); } - Future> saveCellData(D data) { - return cellDataPersistence.save(data); + void saveCellData(D data, {bool deduplicate = false, void Function(Option)? resultCallback}) async { + if (deduplicate) { + _loadDataOperation?.cancel(); + _loadDataOperation = Timer(const Duration(milliseconds: 300), () async { + final result = await cellDataPersistence.save(data); + if (resultCallback != null) { + resultCallback(result); + } + }); + } else { + final result = await cellDataPersistence.save(data); + if (resultCallback != null) { + resultCallback(result); + } + } } void _loadData() { - _delayOperation?.cancel(); - _delayOperation = Timer(const Duration(milliseconds: 10), () { + _loadDataOperation?.cancel(); + _loadDataOperation = Timer(const Duration(milliseconds: 10), () { cellDataLoader.loadData().then((data) { _cellDataNotifier.value = data; cellCache.insert(GridCellCacheData(key: _cacheKey, object: data)); @@ -190,7 +218,8 @@ class _GridCellContext extends Equatable { } void dispose() { - _delayOperation?.cancel(); + _loadDataOperation?.cancel(); + _saveDataOperation?.cancel(); if (_onFieldChangedFn != null) { cellCache.removeFieldListener(_cacheKey, _onFieldChangedFn!); diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/data_loader.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/data_loader.dart index 4b66c08224..92caedc4e9 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/data_loader.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/data_loader.dart @@ -58,7 +58,11 @@ class GridCellDataLoader extends IGridCellDataLoader { return fut.then( (result) => result.fold((Cell cell) { try { - return parser.parserData(cell.data); + if (cell.data.isEmpty) { + return null; + } else { + return parser.parserData(cell.data); + } } catch (e, s) { Log.error('$parser parser cellData failed, $e'); Log.error('Stack trace \n $s'); @@ -105,9 +109,6 @@ class StringCellDataParser implements ICellDataParser { class DateCellDataParser implements ICellDataParser { @override DateCellData? parserData(List data) { - if (data.isEmpty) { - return null; - } return DateCellData.fromBuffer(data); } } @@ -115,9 +116,13 @@ class DateCellDataParser implements ICellDataParser { class SelectOptionCellDataParser implements ICellDataParser { @override SelectOptionCellData? parserData(List data) { - if (data.isEmpty) { - return null; - } return SelectOptionCellData.fromBuffer(data); } } + +class URLCellDataParser implements ICellDataParser { + @override + URLCellData? parserData(List data) { + return URLCellData.fromBuffer(data); + } +} diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart index 514ae2ce4e..b8e2b13bbc 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart @@ -58,7 +58,7 @@ class CheckboxCellBloc extends Bloc { class CheckboxCellEvent with _$CheckboxCellEvent { const factory CheckboxCellEvent.initial() = _Initial; const factory CheckboxCellEvent.select() = _Selected; - const factory CheckboxCellEvent.didReceiveCellUpdate(String cellData) = _DidReceiveCellUpdate; + const factory CheckboxCellEvent.didReceiveCellUpdate(String? cellData) = _DidReceiveCellUpdate; } @freezed diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart index ff001eaa75..15f18707f8 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart @@ -37,7 +37,7 @@ class DateCalBloc extends Bloc { setFocusedDay: (focusedDay) { emit(state.copyWith(focusedDay: focusedDay)); }, - didReceiveCellUpdate: (DateCellData cellData) { + didReceiveCellUpdate: (DateCellData? cellData) { final dateData = dateDataFromCellData(cellData); final time = dateData.foldRight("", (dateData, previous) => dateData.time); emit(state.copyWith(dateData: dateData, time: time)); @@ -83,25 +83,26 @@ class DateCalBloc extends Bloc { return; } - final result = await cellContext.saveCellData(newDateData); - result.fold( - () => emit(state.copyWith( - dateData: Some(newDateData), - timeFormatError: none(), - )), - (err) { - switch (ErrorCode.valueOf(err.code)!) { - case ErrorCode.InvalidDateTimeFormat: - emit(state.copyWith( - dateData: Some(newDateData), - timeFormatError: Some(timeFormatPrompt(err)), - )); - break; - default: - Log.error(err); - } - }, - ); + cellContext.saveCellData(newDateData, resultCallback: (result) { + result.fold( + () => emit(state.copyWith( + dateData: Some(newDateData), + timeFormatError: none(), + )), + (err) { + switch (ErrorCode.valueOf(err.code)!) { + case ErrorCode.InvalidDateTimeFormat: + emit(state.copyWith( + dateData: Some(newDateData), + timeFormatError: Some(timeFormatPrompt(err)), + )); + break; + default: + Log.error(err); + } + }, + ); + }); } String timeFormatPrompt(FlowyError error) { @@ -183,7 +184,7 @@ class DateCalEvent with _$DateCalEvent { const factory DateCalEvent.setDateFormat(DateFormat dateFormat) = _DateFormat; const factory DateCalEvent.setIncludeTime(bool includeTime) = _IncludeTime; const factory DateCalEvent.setTime(String time) = _Time; - const factory DateCalEvent.didReceiveCellUpdate(DateCellData data) = _DidReceiveCellUpdate; + const factory DateCalEvent.didReceiveCellUpdate(DateCellData? data) = _DidReceiveCellUpdate; } @freezed diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart index 4b068dd289..b06a3d60b3 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart @@ -16,7 +16,13 @@ class DateCellBloc extends Bloc { (event, emit) async { event.when( initial: () => _startListening(), - didReceiveCellUpdate: (DateCellData value) => emit(state.copyWith(data: Some(value))), + didReceiveCellUpdate: (DateCellData? cellData) { + if (cellData != null) { + emit(state.copyWith(data: Some(cellData))); + } else { + emit(state.copyWith(data: none())); + } + }, didReceiveFieldUpdate: (Field value) => emit(state.copyWith(field: value)), ); }, @@ -47,7 +53,7 @@ class DateCellBloc extends Bloc { @freezed class DateCellEvent with _$DateCellEvent { const factory DateCellEvent.initial() = _InitialCell; - const factory DateCellEvent.didReceiveCellUpdate(DateCellData data) = _DidReceiveCellUpdate; + const factory DateCellEvent.didReceiveCellUpdate(DateCellData? data) = _DidReceiveCellUpdate; const factory DateCellEvent.didReceiveFieldUpdate(Field field) = _DidReceiveFieldUpdate; } diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/number_cell_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/number_cell_bloc.dart index ceb89bc201..8157f6a3f2 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/number_cell_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/number_cell_bloc.dart @@ -19,7 +19,7 @@ class NumberCellBloc extends Bloc { _startListening(); }, didReceiveCellUpdate: (_DidReceiveCellUpdate value) { - emit(state.copyWith(content: value.cellContent)); + emit(state.copyWith(content: value.cellContent ?? "")); }, updateCell: (_UpdateCell value) async { await _updateCellValue(value, emit); @@ -58,7 +58,7 @@ class NumberCellBloc extends Bloc { class NumberCellEvent with _$NumberCellEvent { const factory NumberCellEvent.initial() = _Initial; const factory NumberCellEvent.updateCell(String text) = _UpdateCell; - const factory NumberCellEvent.didReceiveCellUpdate(String cellContent) = _DidReceiveCellUpdate; + const factory NumberCellEvent.didReceiveCellUpdate(String? cellContent) = _DidReceiveCellUpdate; } @freezed diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_cell_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_cell_bloc.dart index 0b6b1fd4ab..c6393e4831 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_cell_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_cell_bloc.dart @@ -44,7 +44,7 @@ class SelectOptionCellBloc extends Bloc { _onCellChangedFn = cellContext.startListening( onCellChanged: ((cellContent) { if (!isClosed) { - add(TextCellEvent.didReceiveCellUpdate(cellContent)); + add(TextCellEvent.didReceiveCellUpdate(cellContent ?? "")); } }), ); diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_bloc.dart new file mode 100644 index 0000000000..609c625001 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_bloc.dart @@ -0,0 +1,73 @@ +import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; +import 'cell_service/cell_service.dart'; + +part 'url_cell_bloc.freezed.dart'; + +class URLCellBloc extends Bloc { + final GridURLCellContext cellContext; + void Function()? _onCellChangedFn; + URLCellBloc({ + required this.cellContext, + }) : super(URLCellState.initial(cellContext)) { + on( + (event, emit) async { + event.when( + initial: () { + _startListening(); + }, + didReceiveCellUpdate: (cellData) { + emit(state.copyWith( + content: cellData?.content ?? "", + url: cellData?.url ?? "", + )); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellContext.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + cellContext.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellContext.startListening( + onCellChanged: ((cellData) { + if (!isClosed) { + add(URLCellEvent.didReceiveCellUpdate(cellData)); + } + }), + ); + } +} + +@freezed +class URLCellEvent with _$URLCellEvent { + const factory URLCellEvent.initial() = _InitialCell; + const factory URLCellEvent.didReceiveCellUpdate(URLCellData? cell) = _DidReceiveCellUpdate; +} + +@freezed +class URLCellState with _$URLCellState { + const factory URLCellState({ + required String content, + required String url, + }) = _URLCellState; + + factory URLCellState.initial(GridURLCellContext context) { + final cellData = context.getCellData(); + return URLCellState( + content: cellData?.content ?? "", + url: cellData?.url ?? "", + ); + } +} diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_editor_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_editor_bloc.dart new file mode 100644 index 0000000000..6e4990943f --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_editor_bloc.dart @@ -0,0 +1,73 @@ +import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; +import 'cell_service/cell_service.dart'; + +part 'url_cell_editor_bloc.freezed.dart'; + +class URLCellEditorBloc extends Bloc { + final GridURLCellContext cellContext; + void Function()? _onCellChangedFn; + URLCellEditorBloc({ + required this.cellContext, + }) : super(URLCellEditorState.initial(cellContext)) { + on( + (event, emit) async { + event.when( + initial: () { + _startListening(); + }, + updateText: (text) { + cellContext.saveCellData(text, deduplicate: true); + emit(state.copyWith(content: text)); + }, + didReceiveCellUpdate: (cellData) { + emit(state.copyWith(content: cellData?.content ?? "")); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellContext.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + cellContext.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellContext.startListening( + onCellChanged: ((cellData) { + if (!isClosed) { + add(URLCellEditorEvent.didReceiveCellUpdate(cellData)); + } + }), + ); + } +} + +@freezed +class URLCellEditorEvent with _$URLCellEditorEvent { + const factory URLCellEditorEvent.initial() = _InitialCell; + const factory URLCellEditorEvent.didReceiveCellUpdate(URLCellData? cell) = _DidReceiveCellUpdate; + const factory URLCellEditorEvent.updateText(String text) = _UpdateText; +} + +@freezed +class URLCellEditorState with _$URLCellEditorState { + const factory URLCellEditorState({ + required String content, + }) = _URLCellEditorState; + + factory URLCellEditorState.initial(GridURLCellContext context) { + final cellData = context.getCellData(); + return URLCellEditorState( + content: cellData?.content ?? "", + ); + } +} diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart index 6f74d23ad0..f8189e7f02 100755 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart @@ -13,6 +13,7 @@ import 'date_cell/date_cell.dart'; import 'number_cell.dart'; import 'select_option_cell/select_option_cell.dart'; import 'text_cell.dart'; +import 'url_cell/url_cell.dart'; GridCellWidget buildGridCellWidget(GridCell gridCell, GridCellCache cellCache, {GridCellStyle? style}) { final key = ValueKey(gridCell.cellId()); @@ -32,10 +33,10 @@ GridCellWidget buildGridCellWidget(GridCell gridCell, GridCellCache cellCache, { return NumberCell(cellContextBuilder: cellContextBuilder, key: key); case FieldType.RichText: return GridTextCell(cellContextBuilder: cellContextBuilder, style: style, key: key); - - default: - throw UnimplementedError; + case FieldType.URL: + return GridURLCell(cellContextBuilder: cellContextBuilder, style: style, key: key); } + throw UnimplementedError; } class BlankCell extends StatelessWidget { @@ -149,7 +150,7 @@ class CellContainer extends StatelessWidget { }); if (expander != null) { - container = _CellEnterRegion(child: container, expander: expander!); + container = CellEnterRegion(child: container, expander: expander!); } return GestureDetector( @@ -179,10 +180,10 @@ class CellContainer extends StatelessWidget { } } -class _CellEnterRegion extends StatelessWidget { +class CellEnterRegion extends StatelessWidget { final Widget child; final Widget expander; - const _CellEnterRegion({required this.child, required this.expander, Key? key}) : super(key: key); + const CellEnterRegion({required this.child, required this.expander, Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/cell_editor.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/cell_editor.dart new file mode 100644 index 0000000000..055a4947c8 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/cell_editor.dart @@ -0,0 +1,96 @@ +import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart'; +import 'package:app_flowy/workspace/application/grid/cell/url_cell_editor_bloc.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:flutter_bloc/flutter_bloc.dart'; + +class URLCellEditor extends StatefulWidget { + final GridURLCellContext cellContext; + const URLCellEditor({required this.cellContext, Key? key}) : super(key: key); + + @override + State createState() => _URLCellEditorState(); + + static void show( + BuildContext context, + GridURLCellContext cellContext, + ) { + FlowyOverlay.of(context).remove(identifier()); + final editor = URLCellEditor( + cellContext: cellContext, + ); + + // + FlowyOverlay.of(context).insertWithAnchor( + widget: OverlayContainer( + child: SizedBox(width: 200, child: editor), + constraints: BoxConstraints.loose(const Size(300, 160)), + ), + identifier: URLCellEditor.identifier(), + anchorContext: context, + anchorDirection: AnchorDirection.bottomWithCenterAligned, + ); + } + + static String identifier() { + return (URLCellEditor).toString(); + } +} + +class _URLCellEditorState extends State { + late URLCellEditorBloc _cellBloc; + late TextEditingController _controller; + + @override + void initState() { + _cellBloc = URLCellEditorBloc(cellContext: widget.cellContext); + _cellBloc.add(const URLCellEditorEvent.initial()); + _controller = TextEditingController(text: _cellBloc.state.content); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocListener( + listener: (context, state) { + if (_controller.text != state.content) { + _controller.text = state.content; + } + }, + child: TextField( + autofocus: true, + controller: _controller, + onChanged: (value) => focusChanged(), + maxLines: null, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + decoration: const InputDecoration( + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + hintText: "", + isDense: true, + ), + ), + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + + super.dispose(); + } + + Future focusChanged() async { + if (mounted) { + if (_cellBloc.isClosed == false && _controller.text != _cellBloc.state.content) { + _cellBloc.add(URLCellEditorEvent.updateText(_controller.text)); + } + } + } +} diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/url_cell.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/url_cell.dart new file mode 100644 index 0000000000..db0dcade79 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/url_cell.dart @@ -0,0 +1,131 @@ +import 'dart:async'; +import 'package:app_flowy/workspace/application/grid/cell/url_cell_bloc.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:app_flowy/workspace/application/grid/prelude.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../cell_builder.dart'; +import 'cell_editor.dart'; + +class GridURLCellStyle extends GridCellStyle { + String? placeholder; + + GridURLCellStyle({ + this.placeholder, + }); +} + +class GridURLCell extends StatefulWidget with GridCellWidget { + final GridCellContextBuilder cellContextBuilder; + late final GridURLCellStyle? cellStyle; + GridURLCell({ + required this.cellContextBuilder, + GridCellStyle? style, + Key? key, + }) : super(key: key) { + if (style != null) { + cellStyle = (style as GridURLCellStyle); + } else { + cellStyle = null; + } + } + + @override + State createState() => _GridURLCellState(); +} + +class _GridURLCellState extends State { + late URLCellBloc _cellBloc; + + @override + void initState() { + final cellContext = widget.cellContextBuilder.build() as GridURLCellContext; + _cellBloc = URLCellBloc(cellContext: cellContext); + _cellBloc.add(const URLCellEvent.initial()); + _listenRequestFocus(context); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final theme = context.watch(); + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + final richText = RichText( + textAlign: TextAlign.left, + text: TextSpan( + text: state.content, + style: TextStyle( + color: theme.main2, + fontSize: 14, + decoration: TextDecoration.underline, + ), + recognizer: _tapGesture(context), + ), + ); + + return CellEnterRegion( + child: Align(alignment: Alignment.centerLeft, child: richText), + expander: _EditCellIndicator(onTap: () {}), + ); + }, + ), + ); + } + + @override + Future dispose() async { + widget.requestFocus.removeAllListener(); + _cellBloc.close(); + super.dispose(); + } + + TapGestureRecognizer _tapGesture(BuildContext context) { + final gesture = TapGestureRecognizer(); + gesture.onTap = () async { + final url = context.read().state.url; + await _openUrlOrEdit(url); + }; + return gesture; + } + + Future _openUrlOrEdit(String url) async { + final uri = Uri.parse(url); + if (url.isNotEmpty && await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + final cellContext = widget.cellContextBuilder.build() as GridURLCellContext; + URLCellEditor.show(context, cellContext); + } + } + + void _listenRequestFocus(BuildContext context) { + widget.requestFocus.addListener(() { + _openUrlOrEdit(_cellBloc.state.url); + }); + } +} + +class _EditCellIndicator extends StatelessWidget { + final VoidCallback onTap; + const _EditCellIndicator({required this.onTap, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = context.watch(); + return FlowyIconButton( + width: 26, + onPressed: onTap, + hoverColor: theme.hover, + radius: BorderRadius.circular(4), + iconPadding: const EdgeInsets.all(5), + icon: svgWidget("editor/edit", color: theme.iconColor), + ); + } +} diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart index eb42267445..63b790b02c 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart @@ -22,6 +22,7 @@ import 'type_option/multi_select.dart'; import 'type_option/number.dart'; import 'type_option/rich_text.dart'; import 'type_option/single_select.dart'; +import 'type_option/url.dart'; typedef UpdateFieldCallback = void Function(Field, Uint8List); typedef SwitchToFieldCallback = Future> Function( @@ -168,9 +169,12 @@ TypeOptionBuilder _makeTypeOptionBuild({ typeOptionContext as RichTextTypeOptionContext, ); - default: - throw UnimplementedError; + case FieldType.URL: + return URLTypeOptionBuilder( + typeOptionContext as URLTypeOptionContext, + ); } + throw UnimplementedError; } TypeOptionContext _makeTypeOptionContext(GridFieldContext fieldContext) { @@ -205,9 +209,15 @@ TypeOptionContext _makeTypeOptionContext(GridFieldContext fieldContext) { fieldContext: fieldContext, dataBuilder: SingleSelectTypeOptionDataBuilder(), ); - default: - throw UnimplementedError(); + + case FieldType.URL: + return URLTypeOptionContext( + fieldContext: fieldContext, + dataBuilder: URLTypeOptionDataBuilder(), + ); } + + throw UnimplementedError(); } abstract class TypeOptionWidget extends StatelessWidget { diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_type_extension.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_type_extension.dart index a4da8fa1b9..035d101544 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_type_extension.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_type_extension.dart @@ -17,9 +17,10 @@ extension FieldTypeListExtension on FieldType { return "grid/field/text"; case FieldType.SingleSelect: return "grid/field/single_select"; - default: - throw UnimplementedError; + case FieldType.URL: + return "grid/field/url"; } + throw UnimplementedError; } String title() { @@ -36,8 +37,9 @@ extension FieldTypeListExtension on FieldType { return LocaleKeys.grid_field_textFieldName.tr(); case FieldType.SingleSelect: return LocaleKeys.grid_field_singleSelectFieldName.tr(); - default: - throw UnimplementedError; + case FieldType.URL: + return LocaleKeys.grid_field_urlFieldName.tr(); } + throw UnimplementedError; } } diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/url.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/url.dart new file mode 100644 index 0000000000..f4e73f7fdc --- /dev/null +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/url.dart @@ -0,0 +1,20 @@ +import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart'; +import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; +import 'package:flutter/material.dart'; + +typedef URLTypeOptionContext = TypeOptionContext; + +class URLTypeOptionDataBuilder extends TypeOptionDataBuilder { + @override + URLTypeOption fromBuffer(List buffer) { + return URLTypeOption.fromBuffer(buffer); + } +} + +class URLTypeOptionBuilder extends TypeOptionBuilder { + URLTypeOptionBuilder(URLTypeOptionContext typeOptionContext); + + @override + Widget? get customWidget => null; +} diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart index 200a079d55..a643b58928 100755 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart @@ -209,9 +209,10 @@ class _CellExpander extends StatelessWidget { return FittedBox( fit: BoxFit.contain, child: FlowyIconButton( + width: 26, onPressed: onExpand, - iconPadding: const EdgeInsets.fromLTRB(6, 6, 6, 6), - fillColor: theme.surface, + iconPadding: const EdgeInsets.all(5), + radius: BorderRadius.circular(4), icon: svgWidget("grid/expander", color: theme.main1), ), ); diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart index f2b93e5018..0900039b1f 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart @@ -4,6 +4,7 @@ import 'package:app_flowy/workspace/application/grid/row/row_detail_bloc.dart'; import 'package:app_flowy/workspace/application/grid/row/row_service.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/prelude.dart'; +import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/url_cell.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_editor.dart'; import 'package:flowy_infra/image.dart'; @@ -212,7 +213,11 @@ GridCellStyle? _buildCellStyle(AppTheme theme, FieldType fieldType) { return SelectOptionCellStyle( placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), ); - default: - return null; + + case FieldType.URL: + return GridURLCellStyle( + placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + ); } + return null; } diff --git a/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbenum.dart b/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbenum.dart index e6cb17314b..78331a46e5 100644 --- a/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbenum.dart +++ b/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbenum.dart @@ -31,6 +31,7 @@ class FieldType extends $pb.ProtobufEnum { static const FieldType SingleSelect = FieldType._(3, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'SingleSelect'); static const FieldType MultiSelect = FieldType._(4, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'MultiSelect'); static const FieldType Checkbox = FieldType._(5, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'Checkbox'); + static const FieldType URL = FieldType._(6, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'URL'); static const $core.List values = [ RichText, @@ -39,6 +40,7 @@ class FieldType extends $pb.ProtobufEnum { SingleSelect, MultiSelect, Checkbox, + URL, ]; static final $core.Map<$core.int, FieldType> _byValue = $pb.ProtobufEnum.initByValue(values); diff --git a/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbjson.dart b/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbjson.dart index 68c1569c9c..7c28fa1ceb 100644 --- a/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbjson.dart +++ b/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid-data-model/grid.pbjson.dart @@ -29,11 +29,12 @@ const FieldType$json = const { const {'1': 'SingleSelect', '2': 3}, const {'1': 'MultiSelect', '2': 4}, const {'1': 'Checkbox', '2': 5}, + const {'1': 'URL', '2': 6}, ], }; /// Descriptor for `FieldType`. Decode as a `google.protobuf.EnumDescriptorProto`. -final $typed_data.Uint8List fieldTypeDescriptor = $convert.base64Decode('CglGaWVsZFR5cGUSDAoIUmljaFRleHQQABIKCgZOdW1iZXIQARIMCghEYXRlVGltZRACEhAKDFNpbmdsZVNlbGVjdBADEg8KC011bHRpU2VsZWN0EAQSDAoIQ2hlY2tib3gQBQ=='); +final $typed_data.Uint8List fieldTypeDescriptor = $convert.base64Decode('CglGaWVsZFR5cGUSDAoIUmljaFRleHQQABIKCgZOdW1iZXIQARIMCghEYXRlVGltZRACEhAKDFNpbmdsZVNlbGVjdBADEg8KC011bHRpU2VsZWN0EAQSDAoIQ2hlY2tib3gQBRIHCgNVUkwQBg=='); @$core.Deprecated('Use gridDescriptor instead') const Grid$json = const { '1': 'Grid', diff --git a/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/protobuf.dart b/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/protobuf.dart index af6583c106..c056e2799a 100644 --- a/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/protobuf.dart +++ b/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/protobuf.dart @@ -5,6 +5,7 @@ export './dart_notification.pb.dart'; export './selection_type_option.pb.dart'; export './row_entities.pb.dart'; export './cell_entities.pb.dart'; +export './url_type_option.pb.dart'; export './checkbox_type_option.pb.dart'; export './event_map.pb.dart'; export './text_type_option.pb.dart'; diff --git a/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/text_type_option.pb.dart b/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/text_type_option.pb.dart index c30f2eb6e1..a38a68be36 100644 --- a/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/text_type_option.pb.dart +++ b/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/text_type_option.pb.dart @@ -11,17 +11,17 @@ import 'package:protobuf/protobuf.dart' as $pb; class RichTextTypeOption extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'RichTextTypeOption', createEmptyInstance: create) - ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'format') + ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'data') ..hasRequiredFields = false ; RichTextTypeOption._() : super(); factory RichTextTypeOption({ - $core.String? format, + $core.String? data, }) { final _result = create(); - if (format != null) { - _result.format = format; + if (data != null) { + _result.data = data; } return _result; } @@ -47,12 +47,12 @@ class RichTextTypeOption extends $pb.GeneratedMessage { static RichTextTypeOption? _defaultInstance; @$pb.TagNumber(1) - $core.String get format => $_getSZ(0); + $core.String get data => $_getSZ(0); @$pb.TagNumber(1) - set format($core.String v) { $_setString(0, v); } + set data($core.String v) { $_setString(0, v); } @$pb.TagNumber(1) - $core.bool hasFormat() => $_has(0); + $core.bool hasData() => $_has(0); @$pb.TagNumber(1) - void clearFormat() => clearField(1); + void clearData() => clearField(1); } diff --git a/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/text_type_option.pbjson.dart b/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/text_type_option.pbjson.dart index e4ba6956ee..5999ce87e0 100644 --- a/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/text_type_option.pbjson.dart +++ b/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/text_type_option.pbjson.dart @@ -12,9 +12,9 @@ import 'dart:typed_data' as $typed_data; const RichTextTypeOption$json = const { '1': 'RichTextTypeOption', '2': const [ - const {'1': 'format', '3': 1, '4': 1, '5': 9, '10': 'format'}, + const {'1': 'data', '3': 1, '4': 1, '5': 9, '10': 'data'}, ], }; /// Descriptor for `RichTextTypeOption`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List richTextTypeOptionDescriptor = $convert.base64Decode('ChJSaWNoVGV4dFR5cGVPcHRpb24SFgoGZm9ybWF0GAEgASgJUgZmb3JtYXQ='); +final $typed_data.Uint8List richTextTypeOptionDescriptor = $convert.base64Decode('ChJSaWNoVGV4dFR5cGVPcHRpb24SEgoEZGF0YRgBIAEoCVIEZGF0YQ=='); diff --git a/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/url_type_option.pb.dart b/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/url_type_option.pb.dart new file mode 100644 index 0000000000..c43474a92a --- /dev/null +++ b/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/url_type_option.pb.dart @@ -0,0 +1,119 @@ +/// +// Generated code. Do not modify. +// source: url_type_option.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + +import 'dart:core' as $core; + +import 'package:protobuf/protobuf.dart' as $pb; + +class URLTypeOption extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'URLTypeOption', createEmptyInstance: create) + ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'data') + ..hasRequiredFields = false + ; + + URLTypeOption._() : super(); + factory URLTypeOption({ + $core.String? data, + }) { + final _result = create(); + if (data != null) { + _result.data = data; + } + return _result; + } + factory URLTypeOption.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory URLTypeOption.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + URLTypeOption clone() => URLTypeOption()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + URLTypeOption copyWith(void Function(URLTypeOption) updates) => super.copyWith((message) => updates(message as URLTypeOption)) as URLTypeOption; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static URLTypeOption create() => URLTypeOption._(); + URLTypeOption createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static URLTypeOption getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static URLTypeOption? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get data => $_getSZ(0); + @$pb.TagNumber(1) + set data($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasData() => $_has(0); + @$pb.TagNumber(1) + void clearData() => clearField(1); +} + +class URLCellData extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'URLCellData', createEmptyInstance: create) + ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'url') + ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'content') + ..hasRequiredFields = false + ; + + URLCellData._() : super(); + factory URLCellData({ + $core.String? url, + $core.String? content, + }) { + final _result = create(); + if (url != null) { + _result.url = url; + } + if (content != null) { + _result.content = content; + } + return _result; + } + factory URLCellData.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory URLCellData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + URLCellData clone() => URLCellData()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + URLCellData copyWith(void Function(URLCellData) updates) => super.copyWith((message) => updates(message as URLCellData)) as URLCellData; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static URLCellData create() => URLCellData._(); + URLCellData createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static URLCellData getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static URLCellData? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get url => $_getSZ(0); + @$pb.TagNumber(1) + set url($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasUrl() => $_has(0); + @$pb.TagNumber(1) + void clearUrl() => clearField(1); + + @$pb.TagNumber(2) + $core.String get content => $_getSZ(1); + @$pb.TagNumber(2) + set content($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasContent() => $_has(1); + @$pb.TagNumber(2) + void clearContent() => clearField(2); +} + diff --git a/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/url_type_option.pbenum.dart b/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/url_type_option.pbenum.dart new file mode 100644 index 0000000000..de8793d432 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/url_type_option.pbenum.dart @@ -0,0 +1,7 @@ +/// +// Generated code. Do not modify. +// source: url_type_option.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields + diff --git a/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/url_type_option.pbjson.dart b/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/url_type_option.pbjson.dart new file mode 100644 index 0000000000..30ac81dfb2 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/url_type_option.pbjson.dart @@ -0,0 +1,31 @@ +/// +// Generated code. Do not modify. +// source: url_type_option.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package + +import 'dart:core' as $core; +import 'dart:convert' as $convert; +import 'dart:typed_data' as $typed_data; +@$core.Deprecated('Use uRLTypeOptionDescriptor instead') +const URLTypeOption$json = const { + '1': 'URLTypeOption', + '2': const [ + const {'1': 'data', '3': 1, '4': 1, '5': 9, '10': 'data'}, + ], +}; + +/// Descriptor for `URLTypeOption`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List uRLTypeOptionDescriptor = $convert.base64Decode('Cg1VUkxUeXBlT3B0aW9uEhIKBGRhdGEYASABKAlSBGRhdGE='); +@$core.Deprecated('Use uRLCellDataDescriptor instead') +const URLCellData$json = const { + '1': 'URLCellData', + '2': const [ + const {'1': 'url', '3': 1, '4': 1, '5': 9, '10': 'url'}, + const {'1': 'content', '3': 2, '4': 1, '5': 9, '10': 'content'}, + ], +}; + +/// Descriptor for `URLCellData`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List uRLCellDataDescriptor = $convert.base64Decode('CgtVUkxDZWxsRGF0YRIQCgN1cmwYASABKAlSA3VybBIYCgdjb250ZW50GAIgASgJUgdjb250ZW50'); diff --git a/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/url_type_option.pbserver.dart b/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/url_type_option.pbserver.dart new file mode 100644 index 0000000000..6889e31393 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-grid/url_type_option.pbserver.dart @@ -0,0 +1,9 @@ +/// +// Generated code. Do not modify. +// source: url_type_option.proto +// +// @dart = 2.12 +// ignore_for_file: annotate_overrides,camel_case_types,unnecessary_const,non_constant_identifier_names,library_prefixes,unused_import,unused_shown_name,return_of_invalid_type,unnecessary_this,prefer_final_fields,deprecated_member_use_from_same_package + +export 'url_type_option.pb.dart'; + diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 554def4acd..393a02ac73 100755 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -928,6 +928,7 @@ dependencies = [ "dart-notify", "dashmap", "diesel", + "fancy-regex", "flowy-database", "flowy-derive", "flowy-error", @@ -953,6 +954,7 @@ dependencies = [ "strum_macros", "tokio", "tracing", + "url", ] [[package]] diff --git a/frontend/rust-lib/flowy-grid/Cargo.toml b/frontend/rust-lib/flowy-grid/Cargo.toml index 1691b3eee7..43b0cbf69f 100644 --- a/frontend/rust-lib/flowy-grid/Cargo.toml +++ b/frontend/rust-lib/flowy-grid/Cargo.toml @@ -35,6 +35,8 @@ serde = { version = "1.0", features = ["derive"] } serde_json = {version = "1.0"} serde_repr = "0.1" indexmap = {version = "1.8.1", features = ["serde"]} +fancy-regex = "0.10.0" +url = { version = "2"} [dev-dependencies] flowy-test = { path = "../flowy-test" } diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index edc5082ba8..7374515793 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -329,7 +329,7 @@ pub(crate) async fn get_select_option_handler( let editor = manager.get_grid_editor(¶ms.grid_id)?; match editor.get_field_meta(¶ms.field_id).await { None => { - tracing::error!("Can't find the corresponding field with id: {}", params.field_id); + tracing::error!("Can't find the select option field with id: {}", params.field_id); data_result(SelectOptionCellData::default()) } Some(field_meta) => { diff --git a/frontend/rust-lib/flowy-grid/src/protobuf/model/mod.rs b/frontend/rust-lib/flowy-grid/src/protobuf/model/mod.rs index 99d0ecd1b6..c0f74e1e9c 100644 --- a/frontend/rust-lib/flowy-grid/src/protobuf/model/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/protobuf/model/mod.rs @@ -19,6 +19,9 @@ pub use row_entities::*; mod cell_entities; pub use cell_entities::*; +mod url_type_option; +pub use url_type_option::*; + mod checkbox_type_option; pub use checkbox_type_option::*; diff --git a/frontend/rust-lib/flowy-grid/src/protobuf/model/text_type_option.rs b/frontend/rust-lib/flowy-grid/src/protobuf/model/text_type_option.rs index febc180e03..b6bb5e55ab 100644 --- a/frontend/rust-lib/flowy-grid/src/protobuf/model/text_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/protobuf/model/text_type_option.rs @@ -26,7 +26,7 @@ #[derive(PartialEq,Clone,Default)] pub struct RichTextTypeOption { // message fields - pub format: ::std::string::String, + pub data: ::std::string::String, // special fields pub unknown_fields: ::protobuf::UnknownFields, pub cached_size: ::protobuf::CachedSize, @@ -43,30 +43,30 @@ impl RichTextTypeOption { ::std::default::Default::default() } - // string format = 1; + // string data = 1; - pub fn get_format(&self) -> &str { - &self.format + pub fn get_data(&self) -> &str { + &self.data } - pub fn clear_format(&mut self) { - self.format.clear(); + pub fn clear_data(&mut self) { + self.data.clear(); } // Param is passed by value, moved - pub fn set_format(&mut self, v: ::std::string::String) { - self.format = v; + pub fn set_data(&mut self, v: ::std::string::String) { + self.data = v; } // Mutable pointer to the field. // If field is not initialized, it is initialized with default value first. - pub fn mut_format(&mut self) -> &mut ::std::string::String { - &mut self.format + pub fn mut_data(&mut self) -> &mut ::std::string::String { + &mut self.data } // Take field - pub fn take_format(&mut self) -> ::std::string::String { - ::std::mem::replace(&mut self.format, ::std::string::String::new()) + pub fn take_data(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.data, ::std::string::String::new()) } } @@ -80,7 +80,7 @@ impl ::protobuf::Message for RichTextTypeOption { let (field_number, wire_type) = is.read_tag_unpack()?; match field_number { 1 => { - ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.format)?; + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.data)?; }, _ => { ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?; @@ -94,8 +94,8 @@ impl ::protobuf::Message for RichTextTypeOption { #[allow(unused_variables)] fn compute_size(&self) -> u32 { let mut my_size = 0; - if !self.format.is_empty() { - my_size += ::protobuf::rt::string_size(1, &self.format); + if !self.data.is_empty() { + my_size += ::protobuf::rt::string_size(1, &self.data); } my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); self.cached_size.set(my_size); @@ -103,8 +103,8 @@ impl ::protobuf::Message for RichTextTypeOption { } fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { - if !self.format.is_empty() { - os.write_string(1, &self.format)?; + if !self.data.is_empty() { + os.write_string(1, &self.data)?; } os.write_unknown_fields(self.get_unknown_fields())?; ::std::result::Result::Ok(()) @@ -145,9 +145,9 @@ impl ::protobuf::Message for RichTextTypeOption { descriptor.get(|| { let mut fields = ::std::vec::Vec::new(); fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( - "format", - |m: &RichTextTypeOption| { &m.format }, - |m: &mut RichTextTypeOption| { &mut m.format }, + "data", + |m: &RichTextTypeOption| { &m.data }, + |m: &mut RichTextTypeOption| { &mut m.data }, )); ::protobuf::reflect::MessageDescriptor::new_pb_name::( "RichTextTypeOption", @@ -165,7 +165,7 @@ impl ::protobuf::Message for RichTextTypeOption { impl ::protobuf::Clear for RichTextTypeOption { fn clear(&mut self) { - self.format.clear(); + self.data.clear(); self.unknown_fields.clear(); } } @@ -183,8 +183,8 @@ impl ::protobuf::reflect::ProtobufValue for RichTextTypeOption { } static file_descriptor_proto_data: &'static [u8] = b"\ - \n\x16text_type_option.proto\",\n\x12RichTextTypeOption\x12\x16\n\x06for\ - mat\x18\x01\x20\x01(\tR\x06formatb\x06proto3\ + \n\x16text_type_option.proto\"(\n\x12RichTextTypeOption\x12\x12\n\x04dat\ + a\x18\x01\x20\x01(\tR\x04datab\x06proto3\ "; static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT; diff --git a/frontend/rust-lib/flowy-grid/src/protobuf/model/url_type_option.rs b/frontend/rust-lib/flowy-grid/src/protobuf/model/url_type_option.rs new file mode 100644 index 0000000000..fe83999fd3 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/protobuf/model/url_type_option.rs @@ -0,0 +1,403 @@ +// This file is generated by rust-protobuf 2.25.2. Do not edit +// @generated + +// https://github.com/rust-lang/rust-clippy/issues/702 +#![allow(unknown_lints)] +#![allow(clippy::all)] + +#![allow(unused_attributes)] +#![cfg_attr(rustfmt, rustfmt::skip)] + +#![allow(box_pointers)] +#![allow(dead_code)] +#![allow(missing_docs)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(trivial_casts)] +#![allow(unused_imports)] +#![allow(unused_results)] +//! Generated file from `url_type_option.proto` + +/// Generated files are compatible only with the same version +/// of protobuf runtime. +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_25_2; + +#[derive(PartialEq,Clone,Default)] +pub struct URLTypeOption { + // message fields + pub data: ::std::string::String, + // special fields + pub unknown_fields: ::protobuf::UnknownFields, + pub cached_size: ::protobuf::CachedSize, +} + +impl<'a> ::std::default::Default for &'a URLTypeOption { + fn default() -> &'a URLTypeOption { + ::default_instance() + } +} + +impl URLTypeOption { + pub fn new() -> URLTypeOption { + ::std::default::Default::default() + } + + // string data = 1; + + + pub fn get_data(&self) -> &str { + &self.data + } + pub fn clear_data(&mut self) { + self.data.clear(); + } + + // Param is passed by value, moved + pub fn set_data(&mut self, v: ::std::string::String) { + self.data = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_data(&mut self) -> &mut ::std::string::String { + &mut self.data + } + + // Take field + pub fn take_data(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.data, ::std::string::String::new()) + } +} + +impl ::protobuf::Message for URLTypeOption { + fn is_initialized(&self) -> bool { + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> { + while !is.eof()? { + let (field_number, wire_type) = is.read_tag_unpack()?; + match field_number { + 1 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.data)?; + }, + _ => { + ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u32 { + let mut my_size = 0; + if !self.data.is_empty() { + my_size += ::protobuf::rt::string_size(1, &self.data); + } + my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); + self.cached_size.set(my_size); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { + if !self.data.is_empty() { + os.write_string(1, &self.data)?; + } + os.write_unknown_fields(self.get_unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn get_cached_size(&self) -> u32 { + self.cached_size.get() + } + + fn get_unknown_fields(&self) -> &::protobuf::UnknownFields { + &self.unknown_fields + } + + fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields { + &mut self.unknown_fields + } + + fn as_any(&self) -> &dyn (::std::any::Any) { + self as &dyn (::std::any::Any) + } + fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) { + self as &mut dyn (::std::any::Any) + } + fn into_any(self: ::std::boxed::Box) -> ::std::boxed::Box { + self + } + + fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor { + Self::descriptor_static() + } + + fn new() -> URLTypeOption { + URLTypeOption::new() + } + + fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor { + static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT; + descriptor.get(|| { + let mut fields = ::std::vec::Vec::new(); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "data", + |m: &URLTypeOption| { &m.data }, + |m: &mut URLTypeOption| { &mut m.data }, + )); + ::protobuf::reflect::MessageDescriptor::new_pb_name::( + "URLTypeOption", + fields, + file_descriptor_proto() + ) + }) + } + + fn default_instance() -> &'static URLTypeOption { + static instance: ::protobuf::rt::LazyV2 = ::protobuf::rt::LazyV2::INIT; + instance.get(URLTypeOption::new) + } +} + +impl ::protobuf::Clear for URLTypeOption { + fn clear(&mut self) { + self.data.clear(); + self.unknown_fields.clear(); + } +} + +impl ::std::fmt::Debug for URLTypeOption { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::protobuf::text_format::fmt(self, f) + } +} + +impl ::protobuf::reflect::ProtobufValue for URLTypeOption { + fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef { + ::protobuf::reflect::ReflectValueRef::Message(self) + } +} + +#[derive(PartialEq,Clone,Default)] +pub struct URLCellData { + // message fields + pub url: ::std::string::String, + pub content: ::std::string::String, + // special fields + pub unknown_fields: ::protobuf::UnknownFields, + pub cached_size: ::protobuf::CachedSize, +} + +impl<'a> ::std::default::Default for &'a URLCellData { + fn default() -> &'a URLCellData { + ::default_instance() + } +} + +impl URLCellData { + pub fn new() -> URLCellData { + ::std::default::Default::default() + } + + // string url = 1; + + + pub fn get_url(&self) -> &str { + &self.url + } + pub fn clear_url(&mut self) { + self.url.clear(); + } + + // Param is passed by value, moved + pub fn set_url(&mut self, v: ::std::string::String) { + self.url = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_url(&mut self) -> &mut ::std::string::String { + &mut self.url + } + + // Take field + pub fn take_url(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.url, ::std::string::String::new()) + } + + // string content = 2; + + + pub fn get_content(&self) -> &str { + &self.content + } + pub fn clear_content(&mut self) { + self.content.clear(); + } + + // Param is passed by value, moved + pub fn set_content(&mut self, v: ::std::string::String) { + self.content = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_content(&mut self) -> &mut ::std::string::String { + &mut self.content + } + + // Take field + pub fn take_content(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.content, ::std::string::String::new()) + } +} + +impl ::protobuf::Message for URLCellData { + fn is_initialized(&self) -> bool { + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> { + while !is.eof()? { + let (field_number, wire_type) = is.read_tag_unpack()?; + match field_number { + 1 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.url)?; + }, + 2 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.content)?; + }, + _ => { + ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u32 { + let mut my_size = 0; + if !self.url.is_empty() { + my_size += ::protobuf::rt::string_size(1, &self.url); + } + if !self.content.is_empty() { + my_size += ::protobuf::rt::string_size(2, &self.content); + } + my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); + self.cached_size.set(my_size); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { + if !self.url.is_empty() { + os.write_string(1, &self.url)?; + } + if !self.content.is_empty() { + os.write_string(2, &self.content)?; + } + os.write_unknown_fields(self.get_unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn get_cached_size(&self) -> u32 { + self.cached_size.get() + } + + fn get_unknown_fields(&self) -> &::protobuf::UnknownFields { + &self.unknown_fields + } + + fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields { + &mut self.unknown_fields + } + + fn as_any(&self) -> &dyn (::std::any::Any) { + self as &dyn (::std::any::Any) + } + fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) { + self as &mut dyn (::std::any::Any) + } + fn into_any(self: ::std::boxed::Box) -> ::std::boxed::Box { + self + } + + fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor { + Self::descriptor_static() + } + + fn new() -> URLCellData { + URLCellData::new() + } + + fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor { + static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT; + descriptor.get(|| { + let mut fields = ::std::vec::Vec::new(); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "url", + |m: &URLCellData| { &m.url }, + |m: &mut URLCellData| { &mut m.url }, + )); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "content", + |m: &URLCellData| { &m.content }, + |m: &mut URLCellData| { &mut m.content }, + )); + ::protobuf::reflect::MessageDescriptor::new_pb_name::( + "URLCellData", + fields, + file_descriptor_proto() + ) + }) + } + + fn default_instance() -> &'static URLCellData { + static instance: ::protobuf::rt::LazyV2 = ::protobuf::rt::LazyV2::INIT; + instance.get(URLCellData::new) + } +} + +impl ::protobuf::Clear for URLCellData { + fn clear(&mut self) { + self.url.clear(); + self.content.clear(); + self.unknown_fields.clear(); + } +} + +impl ::std::fmt::Debug for URLCellData { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::protobuf::text_format::fmt(self, f) + } +} + +impl ::protobuf::reflect::ProtobufValue for URLCellData { + fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef { + ::protobuf::reflect::ReflectValueRef::Message(self) + } +} + +static file_descriptor_proto_data: &'static [u8] = b"\ + \n\x15url_type_option.proto\"#\n\rURLTypeOption\x12\x12\n\x04data\x18\ + \x01\x20\x01(\tR\x04data\"9\n\x0bURLCellData\x12\x10\n\x03url\x18\x01\ + \x20\x01(\tR\x03url\x12\x18\n\x07content\x18\x02\x20\x01(\tR\x07contentb\ + \x06proto3\ +"; + +static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT; + +fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto { + ::protobuf::Message::parse_from_bytes(file_descriptor_proto_data).unwrap() +} + +pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto { + file_descriptor_proto_lazy.get(|| { + parse_descriptor_proto() + }) +} diff --git a/frontend/rust-lib/flowy-grid/src/protobuf/proto/text_type_option.proto b/frontend/rust-lib/flowy-grid/src/protobuf/proto/text_type_option.proto index 67cfb438ea..827c569a74 100644 --- a/frontend/rust-lib/flowy-grid/src/protobuf/proto/text_type_option.proto +++ b/frontend/rust-lib/flowy-grid/src/protobuf/proto/text_type_option.proto @@ -1,5 +1,5 @@ syntax = "proto3"; message RichTextTypeOption { - string format = 1; + string data = 1; } diff --git a/frontend/rust-lib/flowy-grid/src/protobuf/proto/url_type_option.proto b/frontend/rust-lib/flowy-grid/src/protobuf/proto/url_type_option.proto new file mode 100644 index 0000000000..edf1c7e341 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/protobuf/proto/url_type_option.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +message URLTypeOption { + string data = 1; +} +message URLCellData { + string url = 1; + string content = 2; +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs b/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs index 5eaabb0294..7978323be1 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs @@ -94,6 +94,7 @@ pub fn default_type_option_builder_from_type(field_type: &FieldType) -> Box SingleSelectTypeOption::default().into(), FieldType::MultiSelect => MultiSelectTypeOption::default().into(), FieldType::Checkbox => CheckboxTypeOption::default().into(), + FieldType::URL => URLTypeOption::default().into(), }; type_option_builder_from_json_str(&s, field_type) @@ -107,6 +108,7 @@ pub fn type_option_builder_from_json_str(s: &str, field_type: &FieldType) -> Box FieldType::SingleSelect => Box::new(SingleSelectTypeOptionBuilder::from_json_str(s)), FieldType::MultiSelect => Box::new(MultiSelectTypeOptionBuilder::from_json_str(s)), FieldType::Checkbox => Box::new(CheckboxTypeOptionBuilder::from_json_str(s)), + FieldType::URL => Box::new(URLTypeOptionBuilder::from_json_str(s)), } } @@ -119,5 +121,6 @@ pub fn type_option_builder_from_bytes>(bytes: T, field_type: &Fie FieldType::SingleSelect => Box::new(SingleSelectTypeOptionBuilder::from_protobuf_bytes(bytes)), FieldType::MultiSelect => Box::new(MultiSelectTypeOptionBuilder::from_protobuf_bytes(bytes)), FieldType::Checkbox => Box::new(CheckboxTypeOptionBuilder::from_protobuf_bytes(bytes)), + FieldType::URL => Box::new(URLTypeOptionBuilder::from_protobuf_bytes(bytes)), } } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option.rs index 25a16a0515..c96166272a 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option.rs @@ -627,7 +627,7 @@ mod tests { field_meta: &FieldMeta, ) -> String { type_option - .decode_cell_data(encoded_data, &FieldType::DateTime, &field_meta) + .decode_cell_data(encoded_data, &FieldType::DateTime, field_meta) .unwrap() .parse::() .unwrap() diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/mod.rs index 2c74b2097b..3cfe390b38 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/mod.rs @@ -3,6 +3,7 @@ mod date_type_option; mod number_type_option; mod selection_type_option; mod text_type_option; +mod url_type_option; mod util; pub use checkbox_type_option::*; @@ -10,3 +11,4 @@ pub use date_type_option::*; pub use number_type_option::*; pub use selection_type_option::*; pub use text_type_option::*; +pub use url_type_option::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option.rs index b889c1c2c0..cf4bd22e3d 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option.rs @@ -736,14 +736,10 @@ mod tests { ) { assert_eq!( type_option - .decode_cell_data(data(cell_data), field_type, field_meta) + .decode_cell_data(cell_data, field_type, field_meta) .unwrap() .to_string(), expected_str.to_owned() ); } - - fn data(s: &str) -> String { - s.to_owned() - } } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option.rs index 1e857e9fd3..81a7ff5c04 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option.rs @@ -207,8 +207,6 @@ impl CellDataOperation for MultiSelectTypeOption { return Ok(DecodedCellData::default()); } - tracing::info!("😁{}", self.options.len()); - let encoded_data = encoded_data.into(); let select_options = select_option_ids(encoded_data) .into_iter() @@ -537,7 +535,7 @@ mod tests { let data = SelectOptionCellContentChangeset::from_insert(&google_option.id).to_str(); let cell_data = type_option.apply_changeset(data, None).unwrap(); - assert_single_select_options(cell_data, &type_option, &field_meta, vec![google_option.clone()]); + assert_single_select_options(cell_data, &type_option, &field_meta, vec![google_option]); // Invalid option id let cell_data = type_option @@ -580,12 +578,12 @@ mod tests { cell_data, &type_option, &field_meta, - vec![google_option.clone(), facebook_option.clone()], + vec![google_option.clone(), facebook_option], ); let data = SelectOptionCellContentChangeset::from_insert(&google_option.id).to_str(); let cell_data = type_option.apply_changeset(data, None).unwrap(); - assert_multi_select_options(cell_data, &type_option, &field_meta, vec![google_option.clone()]); + assert_multi_select_options(cell_data, &type_option, &field_meta, vec![google_option]); // Invalid option id let cell_data = type_option @@ -612,7 +610,7 @@ mod tests { assert_eq!( expected, type_option - .decode_cell_data(cell_data, &field_meta.field_type, &field_meta) + .decode_cell_data(cell_data, &field_meta.field_type, field_meta) .unwrap() .parse::() .unwrap() @@ -629,7 +627,7 @@ mod tests { assert_eq!( expected, type_option - .decode_cell_data(cell_data, &field_meta.field_type, &field_meta) + .decode_cell_data(cell_data, &field_meta.field_type, field_meta) .unwrap() .parse::() .unwrap() diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option.rs index 94c55e3664..3acdfd97c5 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option.rs @@ -27,7 +27,7 @@ impl TypeOptionBuilder for RichTextTypeOptionBuilder { #[derive(Debug, Clone, Default, Serialize, Deserialize, ProtoBuf)] pub struct RichTextTypeOption { #[pb(index = 1)] - pub format: String, + data: String, //It's not used. } impl_type_option!(RichTextTypeOption, FieldType::RichText); diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option.rs new file mode 100644 index 0000000000..7299b1babd --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option.rs @@ -0,0 +1,186 @@ +use crate::impl_type_option; +use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; +use crate::services::row::{CellContentChangeset, CellDataOperation, DecodedCellData, EncodedCellData}; +use bytes::Bytes; +use fancy_regex::Regex; +use flowy_derive::ProtoBuf; +use flowy_error::{internal_error, FlowyError, FlowyResult}; +use flowy_grid_data_model::entities::{ + CellMeta, FieldMeta, FieldType, TypeOptionDataDeserializer, TypeOptionDataEntry, +}; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +#[derive(Default)] +pub struct URLTypeOptionBuilder(URLTypeOption); +impl_into_box_type_option_builder!(URLTypeOptionBuilder); +impl_builder_from_json_str_and_from_bytes!(URLTypeOptionBuilder, URLTypeOption); + +impl TypeOptionBuilder for URLTypeOptionBuilder { + fn field_type(&self) -> FieldType { + self.0.field_type() + } + + fn entry(&self) -> &dyn TypeOptionDataEntry { + &self.0 + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)] +pub struct URLTypeOption { + #[pb(index = 1)] + data: String, //It's not used. +} +impl_type_option!(URLTypeOption, FieldType::URL); + +impl CellDataOperation, String> for URLTypeOption { + fn decode_cell_data( + &self, + encoded_data: T, + decoded_field_type: &FieldType, + _field_meta: &FieldMeta, + ) -> FlowyResult + where + T: Into>, + { + if !decoded_field_type.is_url() { + return Ok(DecodedCellData::default()); + } + let cell_data = encoded_data.into().try_into_inner()?; + DecodedCellData::try_from_bytes(cell_data) + } + + fn apply_changeset(&self, changeset: C, _cell_meta: Option) -> Result + where + C: Into, + { + let changeset = changeset.into(); + let mut cell_data = URLCellData { + url: "".to_string(), + content: changeset.to_string(), + }; + + if let Ok(Some(m)) = URL_REGEX.find(&changeset) { + // Only support https scheme by now + match url::Url::parse(m.as_str()) { + Ok(url) => { + if url.scheme() == "https" { + cell_data.url = url.into(); + } else { + cell_data.url = format!("https://{}", m.as_str()); + } + } + Err(_) => { + cell_data.url = format!("https://{}", m.as_str()); + } + } + } + + cell_data.to_json() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] +pub struct URLCellData { + #[pb(index = 1)] + pub url: String, + + #[pb(index = 2)] + pub content: String, +} + +impl URLCellData { + pub fn new(s: &str) -> Self { + Self { + url: "".to_string(), + content: s.to_string(), + } + } + + fn to_json(&self) -> FlowyResult { + serde_json::to_string(self).map_err(internal_error) + } +} + +impl FromStr for URLCellData { + type Err = FlowyError; + + fn from_str(s: &str) -> Result { + serde_json::from_str::(s).map_err(internal_error) + } +} + +lazy_static! { + static ref URL_REGEX: Regex = Regex::new( + "[(http(s)?):\\/\\/(www\\.)?a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)" + ) + .unwrap(); +} + +#[cfg(test)] +mod tests { + use crate::services::field::FieldBuilder; + use crate::services::field::{URLCellData, URLTypeOption}; + use crate::services::row::{CellDataOperation, EncodedCellData}; + use flowy_grid_data_model::entities::{FieldMeta, FieldType}; + + #[test] + fn url_type_option_test_no_url() { + let type_option = URLTypeOption::default(); + let field_type = FieldType::URL; + let field_meta = FieldBuilder::from_field_type(&field_type).build(); + assert_changeset(&type_option, "123", &field_type, &field_meta, "123", ""); + } + + #[test] + fn url_type_option_test_contains_url() { + let type_option = URLTypeOption::default(); + let field_type = FieldType::URL; + let field_meta = FieldBuilder::from_field_type(&field_type).build(); + assert_changeset( + &type_option, + "AppFlowy website - https://www.appflowy.io", + &field_type, + &field_meta, + "AppFlowy website - https://www.appflowy.io", + "https://www.appflowy.io/", + ); + + assert_changeset( + &type_option, + "AppFlowy website appflowy.io", + &field_type, + &field_meta, + "AppFlowy website appflowy.io", + "https://appflowy.io", + ); + } + + fn assert_changeset( + type_option: &URLTypeOption, + cell_data: &str, + field_type: &FieldType, + field_meta: &FieldMeta, + expected: &str, + expected_url: &str, + ) { + let encoded_data = type_option.apply_changeset(cell_data, None).unwrap(); + let decode_cell_data = decode_cell_data(encoded_data, type_option, field_meta, field_type); + assert_eq!(expected.to_owned(), decode_cell_data.content); + assert_eq!(expected_url.to_owned(), decode_cell_data.url); + } + + fn decode_cell_data>>( + encoded_data: T, + type_option: &URLTypeOption, + field_meta: &FieldMeta, + field_type: &FieldType, + ) -> URLCellData { + type_option + .decode_cell_data(encoded_data, field_type, field_meta) + .unwrap() + .parse::() + .unwrap() + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/row/cell_data_operation.rs b/frontend/rust-lib/flowy-grid/src/services/row/cell_data_operation.rs index 0ecc9198dd..d93f84244a 100644 --- a/frontend/rust-lib/flowy-grid/src/services/row/cell_data_operation.rs +++ b/frontend/rust-lib/flowy-grid/src/services/row/cell_data_operation.rs @@ -134,6 +134,7 @@ pub fn apply_cell_data_changeset>( FieldType::SingleSelect => SingleSelectTypeOption::from(field_meta).apply_changeset(changeset, cell_meta), FieldType::MultiSelect => MultiSelectTypeOption::from(field_meta).apply_changeset(changeset, cell_meta), FieldType::Checkbox => CheckboxTypeOption::from(field_meta).apply_changeset(changeset, cell_meta), + FieldType::URL => URLTypeOption::from(field_meta).apply_changeset(changeset, cell_meta), }?; Ok(TypeOptionCellData::new(s, field_meta.field_type.clone()).json()) @@ -166,7 +167,6 @@ pub fn decode_cell_data>( field_meta: &FieldMeta, ) -> FlowyResult { let encoded_data = encoded_data.into(); - tracing::info!("😁{:?}", field_meta.type_options); let get_cell_data = || { let data = match t_field_type { FieldType::RichText => field_meta @@ -187,6 +187,9 @@ pub fn decode_cell_data>( FieldType::Checkbox => field_meta .get_type_option_entry::(t_field_type)? .decode_cell_data(encoded_data, s_field_type, field_meta), + FieldType::URL => field_meta + .get_type_option_entry::(t_field_type)? + .decode_cell_data(encoded_data, s_field_type, field_meta), }; Some(data) }; diff --git a/frontend/rust-lib/flowy-grid/tests/grid/grid_test.rs b/frontend/rust-lib/flowy-grid/tests/grid/grid_test.rs index afe89d4623..7b210d4f95 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/grid_test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/grid_test.rs @@ -262,6 +262,9 @@ async fn grid_row_add_cells_test() { FieldType::Checkbox => { builder.add_cell(&field.id, "false".to_string()).unwrap(); } + FieldType::URL => { + builder.add_cell(&field.id, "1".to_string()).unwrap(); + } } } let context = builder.build(); @@ -328,6 +331,7 @@ async fn grid_cell_update() { SelectOptionCellContentChangeset::from_insert(&type_option.options.first().unwrap().id).to_str() } FieldType::Checkbox => "1".to_string(), + FieldType::URL => "1".to_string(), }; scripts.push(UpdateCell { @@ -349,6 +353,7 @@ async fn grid_cell_update() { FieldType::SingleSelect => (SelectOptionCellContentChangeset::from_insert("abc").to_str(), false), FieldType::MultiSelect => (SelectOptionCellContentChangeset::from_insert("abc").to_str(), false), FieldType::Checkbox => ("2".to_string(), false), + FieldType::URL => ("2".to_string(), false), }; scripts.push(UpdateCell { diff --git a/frontend/rust-lib/flowy-grid/tests/grid/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/script.rs index 49b6ee7c90..37c3b736d4 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/script.rs @@ -354,6 +354,10 @@ fn make_template_1_grid() -> BuildGridContext { let checkbox = CheckboxTypeOptionBuilder::default(); let checkbox_field = FieldBuilder::new(checkbox).name("is done").visibility(true).build(); + // URL + let url = URLTypeOptionBuilder::default(); + let url_field = FieldBuilder::new(url).name("link").visibility(true).build(); + GridBuilder::default() .add_field(text_field) .add_field(single_select_field) @@ -361,6 +365,7 @@ fn make_template_1_grid() -> BuildGridContext { .add_field(number_field) .add_field(date_field) .add_field(checkbox_field) + .add_field(url_field) .add_empty_row() .add_empty_row() .add_empty_row() diff --git a/shared-lib/flowy-grid-data-model/src/entities/grid.rs b/shared-lib/flowy-grid-data-model/src/entities/grid.rs index 944f52a81f..5889e1093b 100644 --- a/shared-lib/flowy-grid-data-model/src/entities/grid.rs +++ b/shared-lib/flowy-grid-data-model/src/entities/grid.rs @@ -875,6 +875,7 @@ pub enum FieldType { SingleSelect = 3, MultiSelect = 4, Checkbox = 5, + URL = 6, } impl std::default::Default for FieldType { @@ -932,6 +933,10 @@ impl FieldType { self == &FieldType::MultiSelect } + pub fn is_url(&self) -> bool { + self == &FieldType::URL + } + pub fn is_select_option(&self) -> bool { self == &FieldType::MultiSelect || self == &FieldType::SingleSelect } diff --git a/shared-lib/flowy-grid-data-model/src/protobuf/model/grid.rs b/shared-lib/flowy-grid-data-model/src/protobuf/model/grid.rs index a063df9d04..c087cc5ea3 100644 --- a/shared-lib/flowy-grid-data-model/src/protobuf/model/grid.rs +++ b/shared-lib/flowy-grid-data-model/src/protobuf/model/grid.rs @@ -8189,6 +8189,7 @@ pub enum FieldType { SingleSelect = 3, MultiSelect = 4, Checkbox = 5, + URL = 6, } impl ::protobuf::ProtobufEnum for FieldType { @@ -8204,6 +8205,7 @@ impl ::protobuf::ProtobufEnum for FieldType { 3 => ::std::option::Option::Some(FieldType::SingleSelect), 4 => ::std::option::Option::Some(FieldType::MultiSelect), 5 => ::std::option::Option::Some(FieldType::Checkbox), + 6 => ::std::option::Option::Some(FieldType::URL), _ => ::std::option::Option::None } } @@ -8216,6 +8218,7 @@ impl ::protobuf::ProtobufEnum for FieldType { FieldType::SingleSelect, FieldType::MultiSelect, FieldType::Checkbox, + FieldType::URL, ]; values } @@ -8337,10 +8340,10 @@ static file_descriptor_proto_data: &'static [u8] = b"\ wId\x12\x19\n\x08field_id\x18\x03\x20\x01(\tR\x07fieldId\x126\n\x16cell_\ content_changeset\x18\x04\x20\x01(\tH\0R\x14cellContentChangesetB\x1f\n\ \x1done_of_cell_content_changeset**\n\x0cMoveItemType\x12\r\n\tMoveField\ - \x10\0\x12\x0b\n\x07MoveRow\x10\x01*d\n\tFieldType\x12\x0c\n\x08RichText\ + \x10\0\x12\x0b\n\x07MoveRow\x10\x01*m\n\tFieldType\x12\x0c\n\x08RichText\ \x10\0\x12\n\n\x06Number\x10\x01\x12\x0c\n\x08DateTime\x10\x02\x12\x10\n\ \x0cSingleSelect\x10\x03\x12\x0f\n\x0bMultiSelect\x10\x04\x12\x0c\n\x08C\ - heckbox\x10\x05b\x06proto3\ + heckbox\x10\x05\x12\x07\n\x03URL\x10\x06b\x06proto3\ "; static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT; diff --git a/shared-lib/flowy-grid-data-model/src/protobuf/proto/grid.proto b/shared-lib/flowy-grid-data-model/src/protobuf/proto/grid.proto index 18abbd8e54..f06d84e5b8 100644 --- a/shared-lib/flowy-grid-data-model/src/protobuf/proto/grid.proto +++ b/shared-lib/flowy-grid-data-model/src/protobuf/proto/grid.proto @@ -167,4 +167,5 @@ enum FieldType { SingleSelect = 3; MultiSelect = 4; Checkbox = 5; + URL = 6; }