mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
chore: single selection field
This commit is contained in:
parent
d2080a6c03
commit
55b888e364
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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];
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user