瀏覽代碼

chore: support open url with https scheme

appflowy 2 年之前
父節點
當前提交
4a9627b31d
共有 20 個文件被更改,包括 388 次插入184 次删除
  1. 23 8
      frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart
  2. 1 1
      frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart
  3. 22 21
      frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart
  4. 8 2
      frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart
  5. 2 2
      frontend/app_flowy/lib/workspace/application/grid/cell/number_cell_bloc.dart
  6. 1 1
      frontend/app_flowy/lib/workspace/application/grid/cell/select_option_cell_bloc.dart
  7. 1 1
      frontend/app_flowy/lib/workspace/application/grid/cell/text_cell_bloc.dart
  8. 5 7
      frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_bloc.dart
  9. 73 0
      frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_editor_bloc.dart
  10. 4 5
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart
  11. 0 127
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell.dart
  12. 96 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/cell_editor.dart
  13. 131 0
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/url_cell.dart
  14. 3 2
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart
  15. 1 1
      frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart
  16. 1 0
      frontend/rust-lib/Cargo.lock
  17. 1 0
      frontend/rust-lib/flowy-grid/Cargo.toml
  18. 0 2
      frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option.rs
  19. 15 3
      frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option.rs
  20. 0 1
      frontend/rust-lib/flowy-grid/src/services/row/cell_data_operation.rs

+ 23 - 8
frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart

@@ -108,7 +108,8 @@ class _GridCellContext<T, D> extends Equatable {
   late final ValueNotifier<T?> _cellDataNotifier;
   bool isListening = false;
   VoidCallback? _onFieldChangedFn;
-  Timer? _delayOperation;
+  Timer? _loadDataOperation;
+  Timer? _saveDataOperation;
 
   _GridCellContext({
     required this.gridCell,
@@ -138,7 +139,7 @@ class _GridCellContext<T, D> 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;
@@ -162,7 +163,7 @@ class _GridCellContext<T, D> extends Equatable {
     }
 
     onCellChangedFn() {
-      onCellChanged(_cellDataNotifier.value as T);
+      onCellChanged(_cellDataNotifier.value);
 
       if (cellDataLoader.config.reloadOnCellChanged) {
         _loadData();
@@ -189,13 +190,26 @@ class _GridCellContext<T, D> extends Equatable {
     return _fieldService.getFieldTypeOptionData(fieldType: fieldType);
   }
 
-  Future<Option<FlowyError>> saveCellData(D data) {
-    return cellDataPersistence.save(data);
+  void saveCellData(D data, {bool deduplicate = false, void Function(Option<FlowyError>)? 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));
@@ -204,7 +218,8 @@ class _GridCellContext<T, D> extends Equatable {
   }
 
   void dispose() {
-    _delayOperation?.cancel();
+    _loadDataOperation?.cancel();
+    _saveDataOperation?.cancel();
 
     if (_onFieldChangedFn != null) {
       cellCache.removeFieldListener(_cacheKey, _onFieldChangedFn!);

+ 1 - 1
frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart

@@ -58,7 +58,7 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
 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

+ 22 - 21
frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart

@@ -37,7 +37,7 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
           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<DateCalEvent, DateCalState> {
       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

+ 8 - 2
frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart

@@ -16,7 +16,13 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
       (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<DateCellEvent, DateCellState> {
 @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;
 }
 

+ 2 - 2
frontend/app_flowy/lib/workspace/application/grid/cell/number_cell_bloc.dart

@@ -19,7 +19,7 @@ class NumberCellBloc extends Bloc<NumberCellEvent, NumberCellState> {
             _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<NumberCellEvent, NumberCellState> {
 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

+ 1 - 1
frontend/app_flowy/lib/workspace/application/grid/cell/select_option_cell_bloc.dart

@@ -44,7 +44,7 @@ class SelectOptionCellBloc extends Bloc<SelectOptionCellEvent, SelectOptionCellS
       onCellChanged: ((selectOptionContext) {
         if (!isClosed) {
           add(SelectOptionCellEvent.didReceiveOptions(
-            selectOptionContext.selectOptions,
+            selectOptionContext?.selectOptions ?? [],
           ));
         }
       }),

+ 1 - 1
frontend/app_flowy/lib/workspace/application/grid/cell/text_cell_bloc.dart

@@ -43,7 +43,7 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
     _onCellChangedFn = cellContext.startListening(
       onCellChanged: ((cellContent) {
         if (!isClosed) {
-          add(TextCellEvent.didReceiveCellUpdate(cellContent));
+          add(TextCellEvent.didReceiveCellUpdate(cellContent ?? ""));
         }
       }),
     );

+ 5 - 7
frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_bloc.dart

@@ -18,12 +18,11 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
           initial: () {
             _startListening();
           },
-          updateText: (text) {
-            cellContext.saveCellData(text);
-            emit(state.copyWith(content: text));
-          },
           didReceiveCellUpdate: (cellData) {
-            emit(state.copyWith(content: cellData.content, url: cellData.url));
+            emit(state.copyWith(
+              content: cellData?.content ?? "",
+              url: cellData?.url ?? "",
+            ));
           },
         );
       },
@@ -54,8 +53,7 @@ class URLCellBloc extends Bloc<URLCellEvent, URLCellState> {
 @freezed
 class URLCellEvent with _$URLCellEvent {
   const factory URLCellEvent.initial() = _InitialCell;
-  const factory URLCellEvent.didReceiveCellUpdate(URLCellData cell) = _DidReceiveCellUpdate;
-  const factory URLCellEvent.updateText(String text) = _UpdateText;
+  const factory URLCellEvent.didReceiveCellUpdate(URLCellData? cell) = _DidReceiveCellUpdate;
 }
 
 @freezed

+ 73 - 0
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<URLCellEditorEvent, URLCellEditorState> {
+  final GridURLCellContext cellContext;
+  void Function()? _onCellChangedFn;
+  URLCellEditorBloc({
+    required this.cellContext,
+  }) : super(URLCellEditorState.initial(cellContext)) {
+    on<URLCellEditorEvent>(
+      (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<void> 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 ?? "",
+    );
+  }
+}

+ 4 - 5
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart

@@ -13,7 +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.dart';
+import 'url_cell/url_cell.dart';
 
 GridCellWidget buildGridCellWidget(GridCell gridCell, GridCellCache cellCache, {GridCellStyle? style}) {
   final key = ValueKey(gridCell.cellId());
@@ -35,7 +35,6 @@ GridCellWidget buildGridCellWidget(GridCell gridCell, GridCellCache cellCache, {
       return GridTextCell(cellContextBuilder: cellContextBuilder, style: style, key: key);
     case FieldType.URL:
       return GridURLCell(cellContextBuilder: cellContextBuilder, style: style, key: key);
-      
   }
   throw UnimplementedError;
 }
@@ -151,7 +150,7 @@ class CellContainer extends StatelessWidget {
           });
 
           if (expander != null) {
-            container = _CellEnterRegion(child: container, expander: expander!);
+            container = CellEnterRegion(child: container, expander: expander!);
           }
 
           return GestureDetector(
@@ -181,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) {

+ 0 - 127
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell.dart

@@ -1,127 +0,0 @@
-import 'dart:async';
-import 'package:app_flowy/workspace/application/grid/cell/url_cell_bloc.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:app_flowy/workspace/application/grid/prelude.dart';
-import 'cell_builder.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<GridURLCell> createState() => _GridURLCellState();
-}
-
-class _GridURLCellState extends State<GridURLCell> {
-  late URLCellBloc _cellBloc;
-  late TextEditingController _controller;
-  late CellSingleFocusNode _focusNode;
-  Timer? _delayOperation;
-
-  @override
-  void initState() {
-    final cellContext = widget.cellContextBuilder.build() as GridURLCellContext;
-    _cellBloc = URLCellBloc(cellContext: cellContext);
-    _cellBloc.add(const URLCellEvent.initial());
-    _controller = TextEditingController(text: _cellBloc.state.content);
-    _focusNode = CellSingleFocusNode();
-
-    _listenFocusNode();
-    _listenRequestFocus(context);
-    super.initState();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return BlocProvider.value(
-      value: _cellBloc,
-      child: BlocListener<URLCellBloc, URLCellState>(
-        listener: (context, state) {
-          if (_controller.text != state.content) {
-            _controller.text = state.content;
-          }
-        },
-        child: TextField(
-          controller: _controller,
-          focusNode: _focusNode,
-          onChanged: (value) => focusChanged(),
-          onEditingComplete: () => _focusNode.unfocus(),
-          maxLines: null,
-          style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
-          decoration: InputDecoration(
-            contentPadding: EdgeInsets.zero,
-            border: InputBorder.none,
-            hintText: widget.cellStyle?.placeholder,
-            isDense: true,
-          ),
-        ),
-      ),
-    );
-  }
-
-  @override
-  Future<void> dispose() async {
-    widget.requestFocus.removeAllListener();
-    _delayOperation?.cancel();
-    _cellBloc.close();
-    _focusNode.removeSingleListener();
-    _focusNode.dispose();
-
-    super.dispose();
-  }
-
-  @override
-  void didUpdateWidget(covariant GridURLCell oldWidget) {
-    if (oldWidget != widget) {
-      _listenFocusNode();
-    }
-    super.didUpdateWidget(oldWidget);
-  }
-
-  void _listenFocusNode() {
-    widget.onFocus.value = _focusNode.hasFocus;
-    _focusNode.setSingleListener(() {
-      widget.onFocus.value = _focusNode.hasFocus;
-      focusChanged();
-    });
-  }
-
-  void _listenRequestFocus(BuildContext context) {
-    widget.requestFocus.addListener(() {
-      if (_focusNode.hasFocus == false && _focusNode.canRequestFocus) {
-        FocusScope.of(context).requestFocus(_focusNode);
-      }
-    });
-  }
-
-  Future<void> focusChanged() async {
-    if (mounted) {
-      _delayOperation?.cancel();
-      _delayOperation = Timer(const Duration(milliseconds: 300), () {
-        if (_cellBloc.isClosed == false && _controller.text != _cellBloc.state.content) {
-          _cellBloc.add(URLCellEvent.updateText(_controller.text));
-        }
-      });
-    }
-  }
-}

+ 96 - 0
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<URLCellEditor> 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<URLCellEditor> {
+  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<URLCellEditorBloc, URLCellEditorState>(
+        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<void> dispose() async {
+    _cellBloc.close();
+
+    super.dispose();
+  }
+
+  Future<void> focusChanged() async {
+    if (mounted) {
+      if (_cellBloc.isClosed == false && _controller.text != _cellBloc.state.content) {
+        _cellBloc.add(URLCellEditorEvent.updateText(_controller.text));
+      }
+    }
+  }
+}

+ 131 - 0
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<GridURLCell> createState() => _GridURLCellState();
+}
+
+class _GridURLCellState extends State<GridURLCell> {
+  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<AppTheme>();
+    return BlocProvider.value(
+      value: _cellBloc,
+      child: BlocBuilder<URLCellBloc, URLCellState>(
+        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<void> dispose() async {
+    widget.requestFocus.removeAllListener();
+    _cellBloc.close();
+    super.dispose();
+  }
+
+  TapGestureRecognizer _tapGesture(BuildContext context) {
+    final gesture = TapGestureRecognizer();
+    gesture.onTap = () async {
+      final url = context.read<URLCellBloc>().state.url;
+      await _openUrlOrEdit(url);
+    };
+    return gesture;
+  }
+
+  Future<void> _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<AppTheme>();
+    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),
+    );
+  }
+}

+ 3 - 2
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),
       ),
     );

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart

@@ -4,7 +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.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';

+ 1 - 0
frontend/rust-lib/Cargo.lock

@@ -954,6 +954,7 @@ dependencies = [
  "strum_macros",
  "tokio",
  "tracing",
+ "url",
 ]
 
 [[package]]

+ 1 - 0
frontend/rust-lib/flowy-grid/Cargo.toml

@@ -36,6 +36,7 @@ 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" }

+ 0 - 2
frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option.rs

@@ -207,8 +207,6 @@ impl CellDataOperation<String, String> 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()

+ 15 - 3
frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option.rs

@@ -62,7 +62,19 @@ impl CellDataOperation<EncodedCellData<URLCellData>, String> for URLTypeOption {
         };
 
         if let Ok(Some(m)) = URL_REGEX.find(&changeset) {
-            cell_data.url = m.as_str().to_string();
+            // 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()
@@ -132,7 +144,7 @@ mod tests {
             &field_type,
             &field_meta,
             "AppFlowy website - https://www.appflowy.io",
-            "https://www.appflowy.io",
+            "https://www.appflowy.io/",
         );
 
         assert_changeset(
@@ -141,7 +153,7 @@ mod tests {
             &field_type,
             &field_meta,
             "AppFlowy website appflowy.io",
-            "appflowy.io",
+            "https://appflowy.io",
         );
     }
 

+ 0 - 1
frontend/rust-lib/flowy-grid/src/services/row/cell_data_operation.rs

@@ -167,7 +167,6 @@ pub fn decode_cell_data<T: Into<String>>(
     field_meta: &FieldMeta,
 ) -> FlowyResult<DecodedCellData> {
     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