Browse Source

chore: single selection field

appflowy 3 years ago
parent
commit
55b888e364

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

@@ -163,7 +163,10 @@
       "dateFormatUS": "Month/Month/Day",
       "timeFormat": " Time format",
       "timeFormatTwelveHour": "12 hour",
-      "timeFormatTwentyFourHour": "24 hour"
+      "timeFormatTwentyFourHour": "24 hour",
+      "addSelectOption": "Add an option",
+      "optionTitle": "Options",
+      "addOption": "Add option"
     }
   }
 }

+ 56 - 0
frontend/app_flowy/lib/workspace/application/grid/field/type_option/option_pannel_bloc.dart

@@ -0,0 +1,56 @@
+import 'dart:typed_data';
+
+import 'package:flowy_sdk/log.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+import 'package:dartz/dartz.dart';
+
+part 'option_pannel_bloc.freezed.dart';
+
+class OptionPannelBloc extends Bloc<OptionPannelEvent, OptionPannelState> {
+  OptionPannelBloc({required List<SelectOption> options}) : super(OptionPannelState.initial(options)) {
+    on<OptionPannelEvent>(
+      (event, emit) async {
+        await event.map(
+          createOption: (_CreateOption value) async {
+            emit(state.copyWith(isAddingOption: false));
+          },
+          beginAddingOption: (_BeginAddingOption value) {
+            emit(state.copyWith(isAddingOption: true));
+          },
+          endAddingOption: (_EndAddingOption value) {
+            emit(state.copyWith(isAddingOption: false));
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    return super.close();
+  }
+}
+
+@freezed
+class OptionPannelEvent with _$OptionPannelEvent {
+  const factory OptionPannelEvent.createOption(String optionName) = _CreateOption;
+  const factory OptionPannelEvent.beginAddingOption() = _BeginAddingOption;
+  const factory OptionPannelEvent.endAddingOption() = _EndAddingOption;
+}
+
+@freezed
+class OptionPannelState with _$OptionPannelState {
+  const factory OptionPannelState({
+    required List<SelectOption> options,
+    required bool isAddingOption,
+  }) = _OptionPannelState;
+
+  factory OptionPannelState.initial(List<SelectOption> options) => OptionPannelState(
+        options: options,
+        isAddingOption: false,
+      );
+}

+ 20 - 39
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/content/grid_row.dart

@@ -30,47 +30,28 @@ class _GridRowWidgetState extends State<GridRowWidget> {
   Widget build(BuildContext context) {
     return BlocProvider.value(
       value: _rowBloc,
-      child: BlocBuilder<RowBloc, RowState>(
-        buildWhen: (p, c) => p.rowHeight != c.rowHeight,
-        builder: (context, state) {
-          return SizedBox(
-            height: _rowBloc.state.rowHeight,
-            child: Row(
-              crossAxisAlignment: CrossAxisAlignment.stretch,
-              children: const [
-                _RowLeading(),
-                _RowCells(),
-                _RowTrailing(),
-              ],
-            ),
-          );
-        },
+      child: MouseRegion(
+        cursor: SystemMouseCursors.click,
+        onEnter: (p) => _rowBloc.add(const RowEvent.activeRow()),
+        onExit: (p) => _rowBloc.add(const RowEvent.disactiveRow()),
+        child: BlocBuilder<RowBloc, RowState>(
+          buildWhen: (p, c) => p.rowHeight != c.rowHeight,
+          builder: (context, state) {
+            return SizedBox(
+              height: _rowBloc.state.rowHeight,
+              child: Row(
+                crossAxisAlignment: CrossAxisAlignment.stretch,
+                children: const [
+                  _RowLeading(),
+                  _RowCells(),
+                  _RowTrailing(),
+                ],
+              ),
+            );
+          },
+        ),
       ),
     );
-    // return BlocProvider.value(
-    //   value: _rowBloc,
-    //   child: MouseRegion(
-    //     cursor: SystemMouseCursors.click,
-    //     onEnter: (p) => _rowBloc.add(const RowEvent.activeRow()),
-    //     onExit: (p) => _rowBloc.add(const RowEvent.disactiveRow()),
-    //     child: BlocBuilder<RowBloc, RowState>(
-    //       buildWhen: (p, c) => p.rowHeight != c.rowHeight,
-    //       builder: (context, state) {
-    //         return SizedBox(
-    //           height: _rowBloc.state.rowHeight,
-    //           child: Row(
-    //             crossAxisAlignment: CrossAxisAlignment.stretch,
-    //             children: const [
-    //               _RowLeading(),
-    //               _RowCells(),
-    //               _RowTrailing(),
-    //             ],
-    //           ),
-    //         );
-    //       },
-    //     ),
-    //   ),
-    // );
   }
 
   @override

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/create_field_pannel.dart

@@ -20,7 +20,7 @@ class CreateFieldPannel extends FlowyOverlayDelegate {
     FlowyOverlay.of(context).insertWithAnchor(
       widget: OverlayContainer(
         child: _CreateFieldPannelWidget(_createFieldBloc),
-        constraints: BoxConstraints.loose(const Size(220, 400)),
+        constraints: BoxConstraints.loose(const Size(220, 500)),
       ),
       identifier: identifier(),
       anchorContext: context,

+ 14 - 11
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_type_list.dart

@@ -49,17 +49,20 @@ class FieldTypeList extends StatelessWidget {
       );
     }).toList();
 
-    return ListView.separated(
-      shrinkWrap: true,
-      controller: ScrollController(),
-      itemCount: cells.length,
-      separatorBuilder: (context, index) {
-        return const VSpace(10);
-      },
-      physics: StyledScrollPhysics(),
-      itemBuilder: (BuildContext context, int index) {
-        return cells[index];
-      },
+    return SizedBox(
+      width: 140,
+      child: ListView.separated(
+        shrinkWrap: true,
+        controller: ScrollController(),
+        itemCount: cells.length,
+        separatorBuilder: (context, index) {
+          return const VSpace(10);
+        },
+        physics: StyledScrollPhysics(),
+        itemBuilder: (BuildContext context, int index) {
+          return cells[index];
+        },
+      ),
     );
   }
 

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

@@ -111,7 +111,7 @@ class NumberFormatItem extends StatelessWidget {
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
     return SizedBox(
-      height: 26,
+      height: GridSize.typeOptionItemHeight,
       child: FlowyButton(
         text: FlowyText.medium(format.title(), fontSize: 12),
         hoverColor: theme.hover,

+ 166 - 6
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/selection.dart

@@ -1,9 +1,20 @@
 import 'package:app_flowy/startup/startup.dart';
+import 'package:app_flowy/workspace/application/grid/field/type_option/option_pannel_bloc.dart';
 import 'package:app_flowy/workspace/application/grid/field/type_option/selection_bloc.dart';
+import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
 import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_tyep_switcher.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.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 'widget.dart';
 
 class SingleSelectTypeOptionBuilder extends TypeOptionBuilder {
   SingleSelectTypeOption typeOption;
@@ -12,17 +23,18 @@ class SingleSelectTypeOptionBuilder extends TypeOptionBuilder {
       : typeOption = SingleSelectTypeOption.fromBuffer(typeOptionData);
 
   @override
-  Widget? get customWidget => const SingleSelectTypeOptionWidget();
+  Widget? get customWidget => SingleSelectTypeOptionWidget(typeOption);
 }
 
 class SingleSelectTypeOptionWidget extends TypeOptionWidget {
-  const SingleSelectTypeOptionWidget({Key? key}) : super(key: key);
+  final SingleSelectTypeOption typeOption;
+  const SingleSelectTypeOptionWidget(this.typeOption, {Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return BlocProvider(
       create: (context) => getIt<SelectionTypeOptionBloc>(),
-      child: Container(height: 100, color: Colors.yellow),
+      child: OptionPannel(options: typeOption.options),
     );
   }
 }
@@ -34,17 +46,165 @@ class MultiSelectTypeOptionBuilder extends TypeOptionBuilder {
       : typeOption = MultiSelectTypeOption.fromBuffer(typeOptionData);
 
   @override
-  Widget? get customWidget => const MultiSelectTypeOptionWidget();
+  Widget? get customWidget => MultiSelectTypeOptionWidget(typeOption);
 }
 
 class MultiSelectTypeOptionWidget extends TypeOptionWidget {
-  const MultiSelectTypeOptionWidget({Key? key}) : super(key: key);
+  final MultiSelectTypeOption typeOption;
+  const MultiSelectTypeOptionWidget(this.typeOption, {Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return BlocProvider(
       create: (context) => getIt<SelectionTypeOptionBloc>(),
-      child: Container(height: 100, color: Colors.blue),
+      child: OptionPannel(options: typeOption.options),
+    );
+  }
+}
+
+class OptionPannel extends StatelessWidget {
+  final List<SelectOption> options;
+  const OptionPannel({required this.options, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (context) => OptionPannelBloc(options: options),
+      child: BlocBuilder<OptionPannelBloc, OptionPannelState>(
+        builder: (context, state) {
+          List<Widget> children = [const OptionTitle()];
+          if (state.isAddingOption) {
+            children.add(const _AddOptionTextField());
+          }
+
+          if (state.options.isEmpty && !state.isAddingOption) {
+            children.add(const _AddOptionButton());
+          }
+
+          if (state.options.isNotEmpty) {
+            children.add(const _OptionList());
+          }
+
+          return Column(children: children);
+        },
+      ),
+    );
+  }
+}
+
+class OptionTitle extends StatelessWidget {
+  const OptionTitle({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+
+    return BlocBuilder<OptionPannelBloc, OptionPannelState>(
+      buildWhen: (previous, current) => previous.options.length != current.options.length,
+      builder: (context, state) {
+        List<Widget> children = [FlowyText.medium(LocaleKeys.grid_field_optionTitle.tr(), fontSize: 12)];
+
+        if (state.options.isNotEmpty && state.isAddingOption == false) {
+          children.add(FlowyButton(
+            text: FlowyText.medium(LocaleKeys.grid_field_addOption.tr(), fontSize: 12),
+            hoverColor: theme.hover,
+            onTap: () {
+              context.read<OptionPannelBloc>().add(const OptionPannelEvent.beginAddingOption());
+            },
+            rightIcon: svg("grid/more", color: theme.iconColor),
+          ));
+        }
+
+        return SizedBox(
+          height: GridSize.typeOptionItemHeight,
+          child: Row(children: children),
+        );
+      },
+    );
+  }
+}
+
+class _OptionList extends StatelessWidget {
+  const _OptionList({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<OptionPannelBloc, OptionPannelState>(
+      builder: (context, state) {
+        final optionItems = state.options.map((option) {
+          return _OptionItem(option: option);
+        }).toList();
+
+        return SizedBox(
+          width: 120,
+          child: ListView.separated(
+            shrinkWrap: true,
+            controller: ScrollController(),
+            separatorBuilder: (context, index) {
+              return VSpace(GridSize.typeOptionSeparatorHeight);
+            },
+            itemCount: optionItems.length,
+            itemBuilder: (BuildContext context, int index) {
+              return optionItems[index];
+            },
+          ),
+        );
+      },
     );
   }
 }
+
+class _OptionItem extends StatelessWidget {
+  final SelectOption option;
+  const _OptionItem({required this.option, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return SizedBox(
+      height: GridSize.typeOptionItemHeight,
+      child: FlowyButton(
+        text: FlowyText.medium(option.name, fontSize: 12),
+        hoverColor: theme.hover,
+        onTap: () {},
+        rightIcon: svg("grid/more", color: theme.iconColor),
+      ),
+    );
+  }
+}
+
+class _AddOptionButton extends StatelessWidget {
+  const _AddOptionButton({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return SizedBox(
+      height: GridSize.typeOptionItemHeight,
+      child: FlowyButton(
+        text: FlowyText.medium(LocaleKeys.grid_field_addSelectOption.tr(), fontSize: 12),
+        hoverColor: theme.hover,
+        onTap: () {
+          context.read<OptionPannelBloc>().add(const OptionPannelEvent.beginAddingOption());
+        },
+        leftIcon: svg("home/add", color: theme.iconColor),
+      ),
+    );
+  }
+}
+
+class _AddOptionTextField extends StatelessWidget {
+  const _AddOptionTextField({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return NameTextField(
+        name: "",
+        onCanceled: () {
+          context.read<OptionPannelBloc>().add(const OptionPannelEvent.endAddingOption());
+        },
+        onDone: (optionName) {
+          context.read<OptionPannelBloc>().add(OptionPannelEvent.createOption(optionName));
+        });
+  }
+}

+ 65 - 0
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/widget.dart

@@ -0,0 +1,65 @@
+import 'package:flowy_infra/theme.dart';
+import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class NameTextField extends StatefulWidget {
+  final void Function(String) onDone;
+  final void Function() onCanceled;
+  final String name;
+
+  const NameTextField({
+    required this.name,
+    required this.onDone,
+    required this.onCanceled,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<NameTextField> createState() => _NameTextFieldState();
+}
+
+class _NameTextFieldState extends State<NameTextField> {
+  late FocusNode _focusNode;
+  late TextEditingController _controller;
+
+  @override
+  void initState() {
+    _focusNode = FocusNode();
+    _controller = TextEditingController(text: widget.name);
+
+    _focusNode.addListener(notifyDidEndEditing);
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = context.watch<AppTheme>();
+    return RoundedInputField(
+        controller: _controller,
+        focusNode: _focusNode,
+        height: 36,
+        style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
+        normalBorderColor: theme.shader4,
+        errorBorderColor: theme.red,
+        focusBorderColor: theme.main1,
+        cursorColor: theme.main1,
+        onChanged: (text) {
+          print(text);
+        });
+  }
+
+  @override
+  void dispose() {
+    _focusNode.removeListener(notifyDidEndEditing);
+    super.dispose();
+  }
+
+  void notifyDidEndEditing() {
+    if (_controller.text.isEmpty) {
+      // widget.onCanceled();
+    } else {
+      widget.onDone(_controller.text);
+    }
+  }
+}

+ 6 - 0
frontend/app_flowy/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart

@@ -20,6 +20,8 @@ class RoundedInputField extends StatefulWidget {
   final EdgeInsets padding;
   final EdgeInsets contentPadding;
   final double height;
+  final FocusNode? focusNode;
+  final TextEditingController? controller;
 
   const RoundedInputField({
     Key? key,
@@ -39,6 +41,8 @@ class RoundedInputField extends StatefulWidget {
     this.padding = EdgeInsets.zero,
     this.contentPadding = const EdgeInsets.symmetric(horizontal: 10),
     this.height = 48,
+    this.focusNode,
+    this.controller,
   }) : super(key: key);
 
   @override
@@ -71,7 +75,9 @@ class _RoundedInputFieldState extends State<RoundedInputField> {
         padding: widget.padding,
         height: widget.height,
         child: TextFormField(
+          controller: widget.controller,
           initialValue: widget.initialValue,
+          focusNode: widget.focusNode,
           onChanged: (value) {
             inputText = value;
             if (widget.onChanged != null) {