Ver Fonte

chore: cell highlight

appflowy há 3 anos atrás
pai
commit
1abf0b565f

+ 2 - 1
frontend/app_flowy/assets/translations/en.json

@@ -185,7 +185,8 @@
       "aquaColor": "Aqua",
       "blueColor": "Blue",
       "deleteTag": "Delete tag",
-      "colorPannelTitle": "Colors"
+      "colorPannelTitle": "Colors",
+      "pannelTitle": "Select an option"
     }
   }
 }

+ 73 - 22
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart

@@ -1,9 +1,23 @@
-import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
 import 'package:flowy_infra/theme.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:provider/provider.dart';
+
+import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
+
+class CellStateNotifier extends ChangeNotifier {
+  bool _isFocus = false;
+
+  set isFocus(bool value) {
+    if (_isFocus != value) {
+      _isFocus = value;
+      notifyListeners();
+    }
+  }
+
+  bool get isFocus => _isFocus;
+}
 
-class CellContainer extends StatefulWidget {
+class CellContainer extends StatelessWidget {
   final Widget child;
   final double width;
   const CellContainer({
@@ -12,28 +26,65 @@ class CellContainer extends StatefulWidget {
     required this.width,
   }) : super(key: key);
 
-  @override
-  State<CellContainer> createState() => _CellContainerState();
-}
-
-class _CellContainerState extends State<CellContainer> {
   @override
   Widget build(BuildContext context) {
-    final theme = context.watch<AppTheme>();
-    final borderSide = BorderSide(color: theme.shader4, width: 0.4);
-    return GestureDetector(
-      behavior: HitTestBehavior.translucent,
-      onTap: () {},
-      child: Container(
-        constraints: BoxConstraints(
-          maxWidth: widget.width,
-        ),
-        decoration: BoxDecoration(
-          border: Border(right: borderSide, bottom: borderSide),
-        ),
-        padding: GridSize.cellContentInsets,
-        child: Center(child: IntrinsicHeight(child: widget.child)),
+    return ChangeNotifierProvider(
+      create: (_) => CellStateNotifier(),
+      child: Consumer<CellStateNotifier>(
+        builder: (context, state, _) {
+          return Container(
+            constraints: BoxConstraints(
+              maxWidth: width,
+            ),
+            decoration: _makeBoxDecoration(context, state),
+            padding: GridSize.cellContentInsets,
+            child: Center(child: IntrinsicHeight(child: child)),
+          );
+        },
       ),
     );
   }
+
+  BoxDecoration _makeBoxDecoration(BuildContext context, CellStateNotifier state) {
+    final theme = context.watch<AppTheme>();
+    if (state.isFocus) {
+      final borderSide = BorderSide(color: theme.main1, width: 1.0);
+      return BoxDecoration(border: Border.fromBorderSide(borderSide));
+    } else {
+      final borderSide = BorderSide(color: theme.shader4, width: 0.4);
+      return BoxDecoration(border: Border(right: borderSide, bottom: borderSide));
+    }
+  }
+}
+
+abstract class GridCell extends StatefulWidget {
+  const GridCell({Key? key}) : super(key: key);
+
+  void setFocus(BuildContext context, bool value) {
+    Provider.of<CellStateNotifier>(context, listen: false).isFocus = value;
+  }
+}
+
+class CellFocusNode extends FocusNode {
+  VoidCallback? focusCallback;
+
+  void addCallback(BuildContext context, VoidCallback callback) {
+    if (focusCallback != null) {
+      removeListener(focusCallback!);
+    }
+    focusCallback = () {
+      Provider.of<CellStateNotifier>(context, listen: false).isFocus = hasFocus;
+      callback();
+    };
+
+    addListener(focusCallback!);
+  }
+
+  @override
+  void dispose() {
+    if (focusCallback != null) {
+      removeListener(focusCallback!);
+    }
+    super.dispose();
+  }
 }

+ 95 - 17
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection.dart → frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/extension.dart

@@ -5,23 +5,7 @@ import 'package:flutter/material.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
-
-class SelectionBadge extends StatelessWidget {
-  final SelectOption option;
-  const SelectionBadge({required this.option, Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return Container(
-      decoration: BoxDecoration(
-        color: option.color.make(context),
-        shape: BoxShape.rectangle,
-        borderRadius: BorderRadius.circular(6.0),
-      ),
-      child: FlowyText.medium(option.name, fontSize: 12),
-    );
-  }
-}
+import 'package:textfield_tags/textfield_tags.dart';
 
 extension SelectOptionColorExtension on SelectOptionColor {
   Color make(BuildContext context) {
@@ -75,3 +59,97 @@ extension SelectOptionColorExtension on SelectOptionColor {
     }
   }
 }
+
+class SelectOptionTextField extends StatelessWidget {
+  final TextEditingController _controller;
+  final FocusNode _focusNode;
+
+  SelectOptionTextField({
+    TextEditingController? controller,
+    FocusNode? focusNode,
+    Key? key,
+  })  : _controller = controller ?? TextEditingController(),
+        _focusNode = focusNode ?? FocusNode(),
+        super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return TextFieldTags(
+      textEditingController: _controller,
+      initialTags: ["abc", "bdf"],
+      focusNode: _focusNode,
+      textSeparators: const [' ', ','],
+      inputfieldBuilder: (
+        BuildContext context,
+        TextEditingController editController,
+        FocusNode focusNode,
+        String? error,
+        void Function(String)? onChanged,
+        void Function(String)? onSubmitted,
+      ) {
+        return ((context, sc, tags, onTagDelegate) {
+          return TextField(
+            controller: editController,
+            focusNode: focusNode,
+            onChanged: (value) {},
+            onEditingComplete: () => focusNode.unfocus(),
+            maxLines: 1,
+            style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
+            decoration: InputDecoration(
+              contentPadding: EdgeInsets.zero,
+              border: InputBorder.none,
+              isDense: true,
+              prefixIcon: _renderTags(tags, sc),
+            ),
+          );
+        });
+      },
+    );
+  }
+
+  Widget? _renderTags(List<String> tags, ScrollController sc) {
+    if (tags.isEmpty) {
+      return null;
+    }
+
+    return SingleChildScrollView(
+      controller: sc,
+      scrollDirection: Axis.horizontal,
+      child: Row(children: [
+        Container(
+          decoration: BoxDecoration(
+            color: Color.fromARGB(255, 74, 137, 92),
+            shape: BoxShape.rectangle,
+            borderRadius: BorderRadius.circular(6.0),
+          ),
+          child: FlowyText.medium("efc", fontSize: 12),
+        ),
+        Container(
+          decoration: BoxDecoration(
+            color: Color.fromARGB(255, 74, 137, 92),
+            shape: BoxShape.rectangle,
+            borderRadius: BorderRadius.circular(6.0),
+          ),
+          child: FlowyText.medium("abc", fontSize: 12),
+        )
+      ]),
+    );
+  }
+}
+
+class SelectionBadge extends StatelessWidget {
+  final SelectOption option;
+  const SelectionBadge({required this.option, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      decoration: BoxDecoration(
+        color: option.color.make(context),
+        shape: BoxShape.rectangle,
+        borderRadius: BorderRadius.circular(6.0),
+      ),
+      child: FlowyText.medium(option.name, fontSize: 12),
+    );
+  }
+}

+ 13 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_cell.dart

@@ -1,7 +1,10 @@
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/grid/prelude.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/cell_container.dart';
 import 'package:flutter/material.dart';
 
+import 'extension.dart';
+
 class SingleSelectCell extends StatefulWidget {
   final FutureCellData cellData;
 
@@ -15,22 +18,31 @@ class SingleSelectCell extends StatefulWidget {
 }
 
 class _SingleSelectCellState extends State<SingleSelectCell> {
+  late CellFocusNode _focusNode;
   late SelectionCellBloc _cellBloc;
+  late TextEditingController _controller;
 
   @override
   void initState() {
     _cellBloc = getIt<SelectionCellBloc>(param1: widget.cellData);
+    _controller = TextEditingController();
+    _focusNode = CellFocusNode();
     super.initState();
   }
 
   @override
   Widget build(BuildContext context) {
-    return Container();
+    _focusNode.addCallback(context, () {});
+    return SelectOptionTextField(
+      focusNode: _focusNode,
+      controller: _controller,
+    );
   }
 
   @override
   Future<void> dispose() async {
     _cellBloc.close();
+    _focusNode.dispose();
     super.dispose();
   }
 }

+ 91 - 3
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection_editor.dart

@@ -1,37 +1,108 @@
 import 'package:app_flowy/workspace/application/grid/cell_bloc/selection_editor_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:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flowy_infra_ui/style_widget/hover.dart';
+import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:app_flowy/generated/locale_keys.g.dart';
 
-import 'selection.dart';
+import 'extension.dart';
 
 class SelectionEditor extends StatelessWidget {
   final GridCellData cellData;
   const SelectionEditor({required this.cellData, Key? key}) : super(key: key);
 
+  void show(BuildContext context) {
+    FlowyOverlay.of(context).insertWithAnchor(
+      widget: OverlayContainer(
+        child: this,
+        constraints: BoxConstraints.loose(const Size(240, 200)),
+      ),
+      identifier: toString(),
+      anchorContext: context,
+      anchorDirection: AnchorDirection.bottomWithLeftAligned,
+    );
+  }
+
   @override
   Widget build(BuildContext context) {
     return BlocProvider(
       create: (context) => SelectionEditorBloc(gridId: cellData.gridId, field: cellData.field),
       child: BlocBuilder<SelectionEditorBloc, SelectionEditorState>(
         builder: (context, state) {
-          return Container();
+          return Column(
+            children: const [
+              _Title(),
+              VSpace(10),
+              _OptionList(),
+            ],
+          );
         },
       ),
     );
   }
 }
 
+class _OptionList extends StatelessWidget {
+  const _OptionList({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<SelectionEditorBloc, SelectionEditorState>(
+      builder: (context, state) {
+        final cells = state.options.map((option) => _SelectionCell(option)).toList();
+        final list = ListView.separated(
+          shrinkWrap: true,
+          controller: ScrollController(),
+          itemCount: cells.length,
+          separatorBuilder: (context, index) {
+            return VSpace(GridSize.typeOptionSeparatorHeight);
+          },
+          physics: StyledScrollPhysics(),
+          itemBuilder: (BuildContext context, int index) {
+            return cells[index];
+          },
+        );
+        return list;
+      },
+    );
+  }
+}
+
+class _Title extends StatelessWidget {
+  const _Title({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: GridSize.typeOptionItemHeight,
+      child: FlowyText.medium(LocaleKeys.grid_selectOption_pannelTitle.tr(), fontSize: 12),
+    );
+  }
+}
+
 class _SelectionCell extends StatelessWidget {
   final SelectOption option;
-  const _SelectionCell({required this.option, Key? key}) : super(key: key);
+  const _SelectionCell(this.option, {Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
+
+    // return FlowyButton(
+    //   text: FlowyText.medium(fieldType.title(), fontSize: 12),
+    //   hoverColor: theme.hover,
+    //   onTap: () => onSelectField(fieldType),
+    //   leftIcon: svgWidget(fieldType.iconName(), color: theme.iconColor),
+    // );
+
     return InkWell(
       onTap: () {},
       child: FlowyHover(
@@ -43,3 +114,20 @@ class _SelectionCell extends StatelessWidget {
     );
   }
 }
+
+class SelectionBadge extends StatelessWidget {
+  final SelectOption option;
+  const SelectionBadge({required this.option, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      decoration: BoxDecoration(
+        color: option.color.make(context),
+        shape: BoxShape.rectangle,
+        borderRadius: BorderRadius.circular(6.0),
+      ),
+      child: FlowyText.medium(option.name, fontSize: 12),
+    );
+  }
+}

+ 18 - 26
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart

@@ -1,13 +1,11 @@
 import 'dart:async';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/grid/prelude.dart';
-import 'package:flowy_sdk/log.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
+import 'cell_container.dart';
 
-/// The interface of base cell.
-
-class GridTextCell extends StatefulWidget {
+class GridTextCell extends GridCell {
   final FutureCellData cellData;
   const GridTextCell({
     required this.cellData,
@@ -19,21 +17,23 @@ class GridTextCell extends StatefulWidget {
 }
 
 class _GridTextCellState extends State<GridTextCell> {
+  late TextCellBloc _cellBloc;
   late TextEditingController _controller;
+  late CellFocusNode _focusNode;
   Timer? _delayOperation;
-  final _focusNode = FocusNode();
-  late TextCellBloc _cellBloc;
 
   @override
   void initState() {
     _cellBloc = getIt<TextCellBloc>(param1: widget.cellData);
     _controller = TextEditingController(text: _cellBloc.state.content);
-    _focusNode.addListener(save);
+    _focusNode = CellFocusNode();
     super.initState();
   }
 
   @override
   Widget build(BuildContext context) {
+    _focusNode.addCallback(context, focusChanged);
+
     return BlocProvider.value(
       value: _cellBloc,
       child: BlocConsumer<TextCellBloc, TextCellState>(
@@ -46,16 +46,8 @@ class _GridTextCellState extends State<GridTextCell> {
           return TextField(
             controller: _controller,
             focusNode: _focusNode,
-            onChanged: (value) {
-              Log.info("On change");
-              save();
-            },
-            onEditingComplete: () {
-              Log.info("On complete");
-            },
-            onSubmitted: (value) {
-              Log.info("On submit");
-            },
+            onChanged: (value) => focusChanged(),
+            onEditingComplete: () => _focusNode.unfocus(),
             maxLines: 1,
             style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
             decoration: const InputDecoration(
@@ -72,18 +64,18 @@ class _GridTextCellState extends State<GridTextCell> {
   @override
   Future<void> dispose() async {
     _cellBloc.close();
-    _focusNode.removeListener(save);
     _focusNode.dispose();
     super.dispose();
   }
 
-  Future<void> save() async {
-    _delayOperation?.cancel();
-    _delayOperation = Timer(const Duration(milliseconds: 300), () {
-      if (_cellBloc.isClosed == false) {
-        _cellBloc.add(TextCellEvent.updateText(_controller.text));
-      }
-    });
-    // and later, before the timer goes off...
+  Future<void> focusChanged() async {
+    if (mounted) {
+      _delayOperation?.cancel();
+      _delayOperation = Timer(const Duration(milliseconds: 300), () {
+        if (_cellBloc.isClosed == false) {
+          _cellBloc.add(TextCellEvent.updateText(_controller.text));
+        }
+      });
+    }
   }
 }

+ 16 - 21
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/grid_header.dart

@@ -46,7 +46,7 @@ class _GridHeaderDelegate extends SliverPersistentHeaderDelegate {
 
   @override
   Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
-    return _GridHeaderWidget(gridId: gridId, fields: fields, key: ObjectKey(fields));
+    return _GridHeaderWidget(gridId: gridId, fields: fields);
   }
 
   @override
@@ -73,27 +73,22 @@ class _GridHeaderWidget extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
-    return BlocBuilder<GridHeaderBloc, GridHeaderState>(
-      builder: (context, state) {
-        final cells = state.fields.map(
-          (field) => GridFieldCell(
-            GridFieldCellContext(gridId: gridId, field: field),
-          ),
-        );
-
-        final row = Row(
-          crossAxisAlignment: CrossAxisAlignment.stretch,
-          children: [
-            const _HeaderLeading(),
-            ...cells,
-            _HeaderTrailing(gridId: gridId),
-          ],
-          key: UniqueKey(),
-        );
-
-        return Container(height: GridSize.headerHeight, color: theme.surface, child: row);
-      },
+    final cells = fields.map(
+      (field) => GridFieldCell(
+        GridFieldCellContext(gridId: gridId, field: field),
+      ),
     );
+
+    final row = Row(
+      crossAxisAlignment: CrossAxisAlignment.stretch,
+      children: [
+        const _HeaderLeading(),
+        ...cells,
+        _HeaderTrailing(gridId: gridId),
+      ],
+    );
+
+    return Container(height: GridSize.headerHeight, color: theme.surface, child: row);
   }
 }
 

+ 2 - 2
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/edit_option_pannel.dart

@@ -1,6 +1,6 @@
 import 'package:app_flowy/workspace/application/grid/field/type_option/edit_option_bloc.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
-import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/selection.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/selection_cell/extension.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/type_option/widget.dart';
 import 'package:flowy_infra/image.dart';
 import 'package:flowy_infra/theme.dart';
@@ -164,7 +164,7 @@ class _SelectOptionColorCell extends StatelessWidget {
       dimension: 16,
       child: Container(
         decoration: BoxDecoration(
-          color: color.color(context),
+          color: color.make(context),
           shape: BoxShape.circle,
         ),
       ),

+ 7 - 0
frontend/app_flowy/pubspec.lock

@@ -1148,6 +1148,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "0.4.11"
+  textfield_tags:
+    dependency: "direct main"
+    description:
+      name: textfield_tags
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
   textstyle_extensions:
     dependency: transitive
     description:

+ 1 - 1
frontend/app_flowy/pubspec.yaml

@@ -67,7 +67,7 @@ dependencies:
   clipboard: ^0.1.3
   connectivity_plus: 2.2.0
   easy_localization: ^3.0.0
-
+  textfield_tags: ^2.0.0
   # The following adds the Cupertino Icons font to your application.
   # Use with the CupertinoIcons class for iOS style icons.
   cupertino_icons: ^1.0.2

+ 2 - 2
frontend/rust-lib/flowy-grid/src/services/grid_editor.rs

@@ -303,12 +303,12 @@ impl ClientGridEditor {
         };
 
         let field_metas = self.pad.read().await.get_field_metas(field_orders)?;
-        debug_assert!(field_metas.len() == expected_len);
-        if field_metas.len() != expected_len {
+        if expected_len != 0 && field_metas.len() != expected_len {
             tracing::error!(
                 "This is a bug. The len of the field_metas should equal to {}",
                 expected_len
             );
+            debug_assert!(field_metas.len() == expected_len);
         }
         // field_metas.retain(|field_meta| field_meta.visibility);
         Ok(field_metas)