chore: single selection field

This commit is contained in:
appflowy 2022-03-29 11:29:21 +08:00
parent d2080a6c03
commit 55b888e364
9 changed files with 333 additions and 59 deletions

View File

@ -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"
}
}
}

View File

@ -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,
);
}

View File

@ -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

View File

@ -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,

View File

@ -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];
},
),
);
}

View File

@ -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,

View File

@ -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));
});
}
}

View File

@ -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);
}
}
}

View File

@ -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) {