Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03ed2065ea | ||
|
|
4529c61c71 | ||
|
|
3e6c3c9240 | ||
|
|
68fdb58836 | ||
|
|
cc4e46f312 | ||
|
|
01dbc68d4c | ||
|
|
b090aae280 | ||
|
|
f86e9b25e8 | ||
|
|
9f845adc74 | ||
|
|
79cb3ea25c | ||
|
|
fd07a87f45 | ||
|
|
e037e11504 | ||
|
|
fa7390066c | ||
|
|
d770003ed0 | ||
|
|
e03024f22c | ||
|
|
043de6d03a | ||
|
|
61727ace59 | ||
|
|
db7845722a | ||
|
|
e93bf3ddc2 | ||
|
|
2fdc5e25f6 | ||
|
|
1b600055fd | ||
|
|
5275bb67c2 | ||
|
|
f65558116f | ||
|
|
529ede5596 | ||
|
|
a7dd6c6944 | ||
|
|
3c77899a34 |
No files matched your search
@@ -129,3 +129,54 @@ jobs:
|
||||
asset_path: ${{ env.MACOS_APP_RELEASE_PATH }}/${{ env.MACOS_X86_ZIP_NAME }}
|
||||
asset_name: ${{ env.MACOS_X86_ZIP_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
build-windows-x86_64:
|
||||
runs-on: windows-latest
|
||||
needs: create-release
|
||||
env:
|
||||
WINDOWS_APP_RELEASE_PATH: frontend\app_flowy\product\${{ github.ref_name }}\windows
|
||||
WINDOWS_ZIP_NAME: AppFlowy-windows-x86_64.zip
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup environment - Rust and Cargo
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 'stable-2022-01-20'
|
||||
|
||||
- name: Setup environment - Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: '3.0.5'
|
||||
|
||||
- name: Pre build
|
||||
working-directory: frontend
|
||||
run: |
|
||||
vcpkg integrate install
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
cargo make flowy_dev
|
||||
|
||||
- name: Build Windows app
|
||||
working-directory: frontend
|
||||
run: |
|
||||
flutter config --enable-windows-desktop
|
||||
cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-windows-x86 appflowy
|
||||
|
||||
- uses: vimtor/action-zip@v1
|
||||
with:
|
||||
files: ${{ env.WINDOWS_APP_RELEASE_PATH }}\
|
||||
dest: ${{ env.WINDOWS_APP_RELEASE_PATH }}\${{ env.WINDOWS_ZIP_NAME }}
|
||||
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: ${{ env.WINDOWS_APP_RELEASE_PATH }}\${{ env.WINDOWS_ZIP_NAME }}
|
||||
asset_name: ${{ env.WINDOWS_ZIP_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
@@ -1,5 +1,27 @@
|
||||
# Release Notes
|
||||
|
||||
## Version 0.0.5.1 - 09/14/2022
|
||||
|
||||
New features
|
||||
- Enable deleting a field in board
|
||||
- Fix some bugs
|
||||
|
||||
|
||||
## Version 0.0.5 - 09/08/2022
|
||||
New Features - Kanban Board like Notion and Trello beta
|
||||
Boards are the best way to manage projects & tasks. Use them to group your databases by select, multiselect, and checkbox.
|
||||
|
||||
<p align="left"><img src="https://user-images.githubusercontent.com/12026239/190055984-6efa2d7a-ee38-4551-859e-ee56388e1859.gif" width="1000px" /></p>
|
||||
|
||||
- Set up columns that represent a specific phase of the project cycle and use cards to represent each project / task
|
||||
- Drag and drop a card from one phase / column to another phase / column
|
||||
- Update database properties in the Board view by clicking on a property and making edits on the card
|
||||
|
||||
### Other Features & Improvements
|
||||
- Settings allow users to change avatars
|
||||
- Click and drag the right edge to resize your sidebar
|
||||
- And many user interface improvements (link)
|
||||
|
||||
## Version 0.0.5 - beta.2 - beta.1 - 09/01/2022
|
||||
|
||||
New features
|
||||
|
||||
@@ -22,7 +22,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
CURRENT_APP_VERSION = "0.0.5"
|
||||
CURRENT_APP_VERSION = "0.0.5.2"
|
||||
FEATURES = "flutter"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
|
||||
@@ -191,13 +191,15 @@
|
||||
"optionTitle": "Options",
|
||||
"addOption": "Add option",
|
||||
"editProperty": "Edit property",
|
||||
"newColumn": "New column"
|
||||
"newColumn": "New column",
|
||||
"deleteFieldPromptMessage": "Are you sure? This property will be deleted"
|
||||
},
|
||||
"row": {
|
||||
"duplicate": "Duplicate",
|
||||
"delete": "Delete",
|
||||
"textPlaceholder": "Empty",
|
||||
"copyProperty": "Copied property to clipboard"
|
||||
"copyProperty": "Copied property to clipboard",
|
||||
"count": "Count"
|
||||
},
|
||||
"selectOption": {
|
||||
"create": "Create",
|
||||
|
||||
@@ -21,6 +21,9 @@ class BoardCheckboxCellBloc
|
||||
didReceiveCellUpdate: (cellData) {
|
||||
emit(state.copyWith(isSelected: _isSelected(cellData)));
|
||||
},
|
||||
select: () async {
|
||||
cellController.saveCellData(!state.isSelected ? "Yes" : "No");
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -50,6 +53,7 @@ class BoardCheckboxCellBloc
|
||||
@freezed
|
||||
class BoardCheckboxCellEvent with _$BoardCheckboxCellEvent {
|
||||
const factory BoardCheckboxCellEvent.initial() = _InitialCell;
|
||||
const factory BoardCheckboxCellEvent.select() = _Selected;
|
||||
const factory BoardCheckboxCellEvent.didReceiveCellUpdate(
|
||||
String cellContent) = _DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/cell_builder.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/row/row_detail.dart';
|
||||
import 'package:app_flowy/workspace/application/appearance.dart';
|
||||
import 'package:appflowy_board/appflowy_board.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
@@ -20,6 +21,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../grid/application/row/row_cache.dart';
|
||||
import '../application/board_bloc.dart';
|
||||
import 'card/card.dart';
|
||||
@@ -87,34 +89,37 @@ class _BoardContentState extends State<BoardContent> {
|
||||
children: [const _ToolbarBlocAdaptor(), _buildBoard(context)],
|
||||
);
|
||||
|
||||
return Container(
|
||||
color: context.read<AppTheme>().surface,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: column,
|
||||
),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: column,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Expanded _buildBoard(BuildContext context) {
|
||||
return Expanded(
|
||||
child: AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
scrollController: ScrollController(),
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
headerBuilder: _buildHeader,
|
||||
footerBuilder: _buildFooter,
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context,
|
||||
column,
|
||||
columnItem,
|
||||
),
|
||||
groupConstraints: const BoxConstraints.tightFor(width: 300),
|
||||
config: AppFlowyBoardConfig(
|
||||
groupBackgroundColor: HexColor.fromHex('#F7F8FC'),
|
||||
Widget _buildBoard(BuildContext context) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: Provider.of<AppearanceSettingModel>(context, listen: true),
|
||||
child: Selector<AppearanceSettingModel, AppTheme>(
|
||||
selector: (ctx, notifier) => notifier.theme,
|
||||
builder: (ctx, theme, child) => Expanded(
|
||||
child: AppFlowyBoard(
|
||||
boardScrollController: scrollManager,
|
||||
scrollController: ScrollController(),
|
||||
controller: context.read<BoardBloc>().boardController,
|
||||
headerBuilder: _buildHeader,
|
||||
footerBuilder: _buildFooter,
|
||||
cardBuilder: (_, column, columnItem) => _buildCard(
|
||||
context,
|
||||
column,
|
||||
columnItem,
|
||||
),
|
||||
groupConstraints: const BoxConstraints.tightFor(width: 300),
|
||||
config: AppFlowyBoardConfig(
|
||||
groupBackgroundColor: theme.bg1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -181,40 +186,36 @@ class _BoardContentState extends State<BoardContent> {
|
||||
}
|
||||
|
||||
Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) {
|
||||
final boardCustomData = columnData.customData as BoardCustomData;
|
||||
final group = boardCustomData.group;
|
||||
// final boardCustomData = columnData.customData as BoardCustomData;
|
||||
// final group = boardCustomData.group;
|
||||
|
||||
if (group.isDefault) {
|
||||
return const SizedBox();
|
||||
} else {
|
||||
return AppFlowyGroupFooter(
|
||||
icon: SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: svgWidget(
|
||||
"home/add",
|
||||
color: context.read<AppTheme>().iconColor,
|
||||
),
|
||||
return AppFlowyGroupFooter(
|
||||
icon: SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: svgWidget(
|
||||
"home/add",
|
||||
color: context.read<AppTheme>().iconColor,
|
||||
),
|
||||
title: FlowyText.medium(
|
||||
LocaleKeys.board_column_create_new_card.tr(),
|
||||
fontSize: 14,
|
||||
color: context.read<AppTheme>().textColor,
|
||||
),
|
||||
height: 50,
|
||||
margin: config.footerPadding,
|
||||
onAddButtonClick: () {
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.createBottomRow(columnData.id),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
),
|
||||
title: FlowyText.medium(
|
||||
LocaleKeys.board_column_create_new_card.tr(),
|
||||
fontSize: 14,
|
||||
color: context.read<AppTheme>().textColor,
|
||||
),
|
||||
height: 50,
|
||||
margin: config.footerPadding,
|
||||
onAddButtonClick: () {
|
||||
context.read<BoardBloc>().add(
|
||||
BoardEvent.createBottomRow(columnData.id),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCard(
|
||||
BuildContext context,
|
||||
AppFlowyGroupData column,
|
||||
AppFlowyGroupData group,
|
||||
AppFlowyGroupItem columnItem,
|
||||
) {
|
||||
final boardColumnItem = columnItem as BoardColumnItem;
|
||||
@@ -241,10 +242,11 @@ class _BoardContentState extends State<BoardContent> {
|
||||
},
|
||||
);
|
||||
|
||||
ValueKey? key = cardKeysCache[columnItem.id];
|
||||
final groupItemId = columnItem.id + group.id;
|
||||
ValueKey? key = cardKeysCache[groupItemId];
|
||||
if (key == null) {
|
||||
final newKey = ValueKey(columnItem.id);
|
||||
cardKeysCache[columnItem.id] = newKey;
|
||||
final newKey = ValueKey(groupItemId);
|
||||
cardKeysCache[groupItemId] = newKey;
|
||||
key = newKey;
|
||||
}
|
||||
|
||||
@@ -254,7 +256,7 @@ class _BoardContentState extends State<BoardContent> {
|
||||
decoration: _makeBoxDecoration(context),
|
||||
child: BoardCard(
|
||||
gridId: gridId,
|
||||
groupId: column.id,
|
||||
groupId: group.id,
|
||||
fieldId: boardColumnItem.fieldContext.id,
|
||||
isEditing: isEditing,
|
||||
cellBuilder: cellBuilder,
|
||||
@@ -280,8 +282,13 @@ class _BoardContentState extends State<BoardContent> {
|
||||
);
|
||||
}
|
||||
|
||||
void _openCard(String gridId, GridFieldController fieldController,
|
||||
RowPB rowPB, GridRowCache rowCache, BuildContext context) {
|
||||
void _openCard(
|
||||
String gridId,
|
||||
GridFieldController fieldController,
|
||||
RowPB rowPB,
|
||||
GridRowCache rowCache,
|
||||
BuildContext context,
|
||||
) {
|
||||
final rowInfo = RowInfo(
|
||||
gridId: gridId,
|
||||
fields: UnmodifiableListView(fieldController.fieldContexts),
|
||||
@@ -314,7 +321,15 @@ class _ToolbarBlocAdaptor extends StatelessWidget {
|
||||
fieldController: bloc.fieldController,
|
||||
);
|
||||
|
||||
return BoardToolbar(toolbarContext: toolbarContext);
|
||||
return ChangeNotifierProvider.value(
|
||||
value: Provider.of<AppearanceSettingModel>(context, listen: true),
|
||||
child: Selector<AppearanceSettingModel, AppTheme>(
|
||||
selector: (ctx, notifier) => notifier.theme,
|
||||
builder: (ctx, theme, child) {
|
||||
return BoardToolbar(toolbarContext: toolbarContext);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,9 @@ class _BoardCheckboxCellState extends State<BoardCheckboxCell> {
|
||||
iconPadding: EdgeInsets.zero,
|
||||
icon: icon,
|
||||
width: 20,
|
||||
onPressed: () => context
|
||||
.read<BoardCheckboxCellBloc>()
|
||||
.add(const BoardCheckboxCellEvent.select()),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -87,24 +87,3 @@ class _BoardSelectOptionCellState extends State<BoardSelectOptionCell> {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectOptionDialog extends StatelessWidget {
|
||||
final GridSelectOptionCellController _controller;
|
||||
const _SelectOptionDialog({
|
||||
Key? key,
|
||||
required IGridCellController controller,
|
||||
}) : _controller = controller as GridSelectOptionCellController,
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
child: InkWell(onTap: () {
|
||||
SelectOptionCellEditor.show(
|
||||
context: context,
|
||||
cellController: _controller,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ class _SettingButton extends StatelessWidget {
|
||||
onPressed: () => BoardSettingList.show(context, settingContext),
|
||||
icon: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 3.0),
|
||||
child: svgWidget("grid/setting/setting"),
|
||||
child: svgWidget("grid/setting/setting", color: theme.iconColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
|
||||
_startListening();
|
||||
},
|
||||
select: () async {
|
||||
_updateCellData();
|
||||
cellController.saveCellData(!state.isSelected ? "Yes" : "No");
|
||||
},
|
||||
didReceiveCellUpdate: (cellData) {
|
||||
emit(state.copyWith(isSelected: _isSelected(cellData)));
|
||||
@@ -49,10 +49,6 @@ class CheckboxCellBloc extends Bloc<CheckboxCellEvent, CheckboxCellState> {
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
void _updateCellData() {
|
||||
cellController.saveCellData(!state.isSelected ? "Yes" : "No");
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
+13
-5
@@ -103,11 +103,19 @@ class SelectOptionCellEditorBloc
|
||||
void _filterOption(String optionName, Emitter<SelectOptionEditorState> emit) {
|
||||
final _MakeOptionResult result =
|
||||
_makeOptions(Some(optionName), state.allOptions);
|
||||
emit(state.copyWith(
|
||||
filter: Some(optionName),
|
||||
options: result.options,
|
||||
createOption: result.createOption,
|
||||
));
|
||||
if (optionName.isEmpty) {
|
||||
emit(state.copyWith(
|
||||
filter: Some(optionName),
|
||||
options: result.options,
|
||||
createOption: none(),
|
||||
));
|
||||
} else {
|
||||
emit(state.copyWith(
|
||||
filter: Some(optionName),
|
||||
options: result.options,
|
||||
createOption: result.createOption,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void _loadOptions() {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'dart:async';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'field_service.dart';
|
||||
import 'type_option/type_option_context.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
@@ -15,10 +16,11 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
|
||||
FieldEditorBloc({
|
||||
required String gridId,
|
||||
required String fieldName,
|
||||
required bool isGroupField,
|
||||
required IFieldTypeOptionLoader loader,
|
||||
}) : dataController =
|
||||
TypeOptionDataController(gridId: gridId, loader: loader),
|
||||
super(FieldEditorState.initial(gridId, fieldName)) {
|
||||
super(FieldEditorState.initial(gridId, fieldName, isGroupField)) {
|
||||
on<FieldEditorEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
@@ -35,7 +37,23 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
|
||||
emit(state.copyWith(name: name));
|
||||
},
|
||||
didReceiveFieldChanged: (FieldPB field) {
|
||||
emit(state.copyWith(field: Some(field)));
|
||||
emit(state.copyWith(
|
||||
field: Some(field),
|
||||
name: field.name,
|
||||
canDelete: field.isPrimary,
|
||||
));
|
||||
},
|
||||
deleteField: () {
|
||||
state.field.fold(
|
||||
() => null,
|
||||
(field) {
|
||||
final fieldService = FieldService(
|
||||
gridId: gridId,
|
||||
fieldId: field.id,
|
||||
);
|
||||
fieldService.deleteField();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -52,6 +70,7 @@ class FieldEditorBloc extends Bloc<FieldEditorEvent, FieldEditorState> {
|
||||
class FieldEditorEvent with _$FieldEditorEvent {
|
||||
const factory FieldEditorEvent.initial() = _InitialField;
|
||||
const factory FieldEditorEvent.updateName(String name) = _UpdateName;
|
||||
const factory FieldEditorEvent.deleteField() = _DeleteField;
|
||||
const factory FieldEditorEvent.didReceiveFieldChanged(FieldPB field) =
|
||||
_DidReceiveFieldChanged;
|
||||
}
|
||||
@@ -63,16 +82,21 @@ class FieldEditorState with _$FieldEditorState {
|
||||
required String errorText,
|
||||
required String name,
|
||||
required Option<FieldPB> field,
|
||||
required bool canDelete,
|
||||
required bool isGroupField,
|
||||
}) = _FieldEditorState;
|
||||
|
||||
factory FieldEditorState.initial(
|
||||
String gridId,
|
||||
String fieldName,
|
||||
bool isGroupField,
|
||||
) =>
|
||||
FieldEditorState(
|
||||
gridId: gridId,
|
||||
errorText: '',
|
||||
field: none(),
|
||||
canDelete: false,
|
||||
name: fieldName,
|
||||
isGroupField: isGroupField,
|
||||
);
|
||||
}
|
||||
@@ -41,6 +41,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
didReceiveRowUpdate: (newRowInfos, reason) {
|
||||
emit(state.copyWith(
|
||||
rowInfos: newRowInfos,
|
||||
rowCount: newRowInfos.length,
|
||||
reason: reason,
|
||||
));
|
||||
},
|
||||
@@ -117,6 +118,7 @@ class GridState with _$GridState {
|
||||
required Option<GridPB> grid,
|
||||
required GridFieldEquatable fields,
|
||||
required List<RowInfo> rowInfos,
|
||||
required int rowCount,
|
||||
required GridLoadingState loadingState,
|
||||
required RowsChangedReason reason,
|
||||
}) = _GridState;
|
||||
@@ -124,6 +126,7 @@ class GridState with _$GridState {
|
||||
factory GridState.initial(String gridId) => GridState(
|
||||
fields: GridFieldEquatable(UnmodifiableListView([])),
|
||||
rowInfos: [],
|
||||
rowCount: 0,
|
||||
grid: none(),
|
||||
gridId: gridId,
|
||||
loadingState: const _Loading(),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
|
||||
@@ -118,6 +120,7 @@ class _FlowyGridState extends State<FlowyGrid> {
|
||||
const _GridToolbarAdaptor(),
|
||||
_gridHeader(context, state.gridId),
|
||||
Flexible(child: child),
|
||||
const RowCountBadge(),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -302,48 +305,46 @@ class _GridFooter extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final rowCount = context.watch<GridBloc>().state.rowInfos.length;
|
||||
final theme = context.watch<AppTheme>();
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.only(bottom: 200),
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: GridSize.footerHeight,
|
||||
child: Padding(
|
||||
padding: GridSize.headerContentInsets,
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(width: GridSize.leadingHeaderPadding),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(width: 120, child: GridAddRowButton()),
|
||||
const SizedBox(height: 30),
|
||||
_rowCountTextWidget(theme: theme, count: rowCount)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: GridSize.footerContentInsets,
|
||||
child: const SizedBox(height: 40, child: GridAddRowButton()),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _rowCountTextWidget({required AppTheme theme, required int count}) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText.regular(
|
||||
'Count : ',
|
||||
fontSize: 13,
|
||||
color: theme.shader3,
|
||||
),
|
||||
FlowyText.regular(
|
||||
count.toString(),
|
||||
fontSize: 13,
|
||||
),
|
||||
],
|
||||
class RowCountBadge extends StatelessWidget {
|
||||
const RowCountBadge({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
|
||||
return BlocSelector<GridBloc, GridState, int>(
|
||||
selector: (state) => state.rowCount,
|
||||
builder: (context, rowCount) {
|
||||
return Padding(
|
||||
padding: GridSize.footerContentInsets,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText.regular(
|
||||
'${LocaleKeys.grid_row_count.tr()} : ',
|
||||
fontSize: 13,
|
||||
color: theme.shader3,
|
||||
),
|
||||
FlowyText.regular(rowCount.toString(), fontSize: 13),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ class GridSize {
|
||||
|
||||
static double get scrollBarSize => 12 * scale;
|
||||
static double get headerHeight => 40 * scale;
|
||||
static double get footerHeight => 100 * scale;
|
||||
static double get footerHeight => 40 * scale;
|
||||
static double get leadingHeaderPadding => 50 * scale;
|
||||
static double get trailHeaderPadding => 140 * scale;
|
||||
static double get headerContainerPadding => 0 * scale;
|
||||
@@ -35,7 +35,7 @@ class GridSize {
|
||||
);
|
||||
|
||||
static EdgeInsets get footerContentInsets => EdgeInsets.fromLTRB(
|
||||
0,
|
||||
GridSize.leadingHeaderPadding,
|
||||
GridSize.headerContainerPadding,
|
||||
GridSize.headerContainerPadding,
|
||||
GridSize.headerContainerPadding,
|
||||
|
||||
+1
-5
@@ -100,11 +100,7 @@ class SelectOptionTag extends StatelessWidget {
|
||||
backgroundColor: color,
|
||||
labelPadding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
selected: true,
|
||||
onSelected: (_) {
|
||||
if (onSelected != null) {
|
||||
onSelected!();
|
||||
}
|
||||
},
|
||||
onSelected: (_) => onSelected?.call(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -22,7 +22,7 @@ import '../../header/type_option/select_option_editor.dart';
|
||||
import 'extension.dart';
|
||||
import 'text_field.dart';
|
||||
|
||||
const double _editorPannelWidth = 300;
|
||||
const double _editorPanelWidth = 300;
|
||||
|
||||
class SelectOptionCellEditor extends StatelessWidget with FlowyOverlayDelegate {
|
||||
final GridSelectOptionCellController cellController;
|
||||
@@ -72,8 +72,8 @@ class SelectOptionCellEditor extends StatelessWidget with FlowyOverlayDelegate {
|
||||
//
|
||||
FlowyOverlay.of(context).insertWithAnchor(
|
||||
widget: OverlayContainer(
|
||||
constraints: BoxConstraints.loose(const Size(_editorPannelWidth, 300)),
|
||||
child: SizedBox(width: _editorPannelWidth, child: editor),
|
||||
constraints: BoxConstraints.loose(const Size(_editorPanelWidth, 300)),
|
||||
child: SizedBox(width: _editorPanelWidth, child: editor),
|
||||
),
|
||||
identifier: SelectOptionCellEditor.identifier(),
|
||||
anchorContext: context,
|
||||
@@ -158,7 +158,7 @@ class _TextField extends StatelessWidget {
|
||||
child: SelectOptionTextField(
|
||||
options: state.options,
|
||||
selectedOptionMap: optionMap,
|
||||
distanceToText: _editorPannelWidth * 0.7,
|
||||
distanceToText: _editorPanelWidth * 0.7,
|
||||
tagController: _tagController,
|
||||
onClick: () => FlowyOverlay.of(context)
|
||||
.remove(SelectOptionTypeOptionEditor.identifier),
|
||||
|
||||
+4
-3
@@ -32,10 +32,10 @@ class SelectOptionTextField extends StatelessWidget {
|
||||
required this.onNewTag,
|
||||
required this.newText,
|
||||
this.onClick,
|
||||
TextEditingController? controller,
|
||||
TextEditingController? textController,
|
||||
FocusNode? focusNode,
|
||||
Key? key,
|
||||
}) : _controller = controller ?? TextEditingController(),
|
||||
}) : _controller = textController ?? TextEditingController(),
|
||||
_focusNode = focusNode ?? FocusNode(),
|
||||
super(key: key);
|
||||
|
||||
@@ -48,7 +48,7 @@ class SelectOptionTextField extends StatelessWidget {
|
||||
textfieldTagsController: tagController,
|
||||
initialTags: selectedOptionMap.keys.toList(),
|
||||
focusNode: _focusNode,
|
||||
textSeparators: const [' ', ','],
|
||||
textSeparators: const [','],
|
||||
inputfieldBuilder: (BuildContext context, editController, focusNode,
|
||||
error, onChanged, onSubmitted) {
|
||||
return ((context, sc, tags, onTagDelegate) {
|
||||
@@ -70,6 +70,7 @@ class SelectOptionTextField extends StatelessWidget {
|
||||
|
||||
if (text.isNotEmpty) {
|
||||
onNewTag(text);
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
},
|
||||
maxLines: 1,
|
||||
|
||||
@@ -16,7 +16,7 @@ class GridAddRowButton extends StatelessWidget {
|
||||
text: const FlowyText.medium('New row', fontSize: 12),
|
||||
hoverColor: theme.shader6,
|
||||
onTap: () => context.read<GridBloc>().add(const GridEvent.createRow()),
|
||||
leftIcon: svgWidget("home/add"),
|
||||
leftIcon: svgWidget("home/add", color: theme.iconColor),
|
||||
);
|
||||
}
|
||||
}
|
||||
+10
-3
@@ -1,5 +1,6 @@
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/prelude.dart';
|
||||
import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -206,9 +207,15 @@ extension _FieldActionExtension on FieldAction {
|
||||
.add(const FieldActionSheetEvent.duplicateField());
|
||||
break;
|
||||
case FieldAction.delete:
|
||||
context
|
||||
.read<FieldActionSheetBloc>()
|
||||
.add(const FieldActionSheetEvent.deleteField());
|
||||
FlowyAlertDialog(
|
||||
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||
confirm: () {
|
||||
context
|
||||
.read<FieldActionSheetBloc>()
|
||||
.add(const FieldActionSheetEvent.deleteField());
|
||||
},
|
||||
).show(context);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
|
||||
import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.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:flutter/material.dart';
|
||||
@@ -13,12 +18,16 @@ import 'field_type_option_editor.dart';
|
||||
class FieldEditor extends StatelessWidget with FlowyOverlayDelegate {
|
||||
final String gridId;
|
||||
final String fieldName;
|
||||
final bool isGroupField;
|
||||
final VoidCallback? onRemoved;
|
||||
|
||||
final IFieldTypeOptionLoader typeOptionLoader;
|
||||
const FieldEditor({
|
||||
required this.gridId,
|
||||
required this.fieldName,
|
||||
this.fieldName = "",
|
||||
required this.typeOptionLoader,
|
||||
this.isGroupField = false,
|
||||
this.onRemoved,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -28,6 +37,7 @@ class FieldEditor extends StatelessWidget with FlowyOverlayDelegate {
|
||||
create: (context) => FieldEditorBloc(
|
||||
gridId: gridId,
|
||||
fieldName: fieldName,
|
||||
isGroupField: isGroupField,
|
||||
loader: typeOptionLoader,
|
||||
)..add(const FieldEditorEvent.initial()),
|
||||
child: BlocBuilder<FieldEditorBloc, FieldEditorState>(
|
||||
@@ -41,6 +51,8 @@ class FieldEditor extends StatelessWidget with FlowyOverlayDelegate {
|
||||
const VSpace(10),
|
||||
const _FieldNameCell(),
|
||||
const VSpace(10),
|
||||
const _DeleteFieldButton(),
|
||||
const VSpace(10),
|
||||
const _FieldTypeOptionCell(),
|
||||
],
|
||||
);
|
||||
@@ -73,6 +85,9 @@ class FieldEditor extends StatelessWidget with FlowyOverlayDelegate {
|
||||
|
||||
@override
|
||||
bool asBarrier() => true;
|
||||
|
||||
@override
|
||||
void didRemove() => onRemoved?.call();
|
||||
}
|
||||
|
||||
class _FieldTypeOptionCell extends StatelessWidget {
|
||||
@@ -116,3 +131,47 @@ class _FieldNameCell extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DeleteFieldButton extends StatelessWidget {
|
||||
const _DeleteFieldButton({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return BlocBuilder<FieldEditorBloc, FieldEditorState>(
|
||||
builder: (context, state) {
|
||||
final enable = !state.canDelete && !state.isGroupField;
|
||||
return SizedBox(
|
||||
height: GridSize.typeOptionItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_field_delete.tr(),
|
||||
fontSize: 12,
|
||||
color: enable ? null : theme.shader4,
|
||||
),
|
||||
hoverColor: theme.hover,
|
||||
onTap: () {
|
||||
if (enable) {
|
||||
FlowyAlertDialog(
|
||||
title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
|
||||
cancel: () {
|
||||
FlowyOverlay.of(context).remove(FieldEditor.identifier());
|
||||
},
|
||||
confirm: () {
|
||||
context
|
||||
.read<FieldEditorBloc>()
|
||||
.add(const FieldEditorEvent.deleteField());
|
||||
FlowyOverlay.of(context).remove(FieldEditor.identifier());
|
||||
},
|
||||
).show(context);
|
||||
}
|
||||
},
|
||||
leftIcon: svgWidget('grid/delete', color: theme.iconColor),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void so() {}
|
||||
}
|
||||
+3
-1
@@ -33,6 +33,7 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return RoundedInputField(
|
||||
height: 36,
|
||||
autoFocus: true,
|
||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
|
||||
controller: controller,
|
||||
normalBorderColor: theme.shader4,
|
||||
@@ -47,7 +48,8 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
|
||||
@override
|
||||
void didUpdateWidget(covariant FieldNameTextField oldWidget) {
|
||||
controller.text = widget.name;
|
||||
controller.selection = TextSelection.fromPosition(TextPosition(offset: controller.text.length));
|
||||
controller.selection = TextSelection.fromPosition(
|
||||
TextPosition(offset: controller.text.length));
|
||||
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@@ -153,6 +153,7 @@ extension _RowActionExtension on _RowAction {
|
||||
context
|
||||
.read<RowActionSheetBloc>()
|
||||
.add(const RowActionSheetEvent.deleteRow());
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +130,7 @@ class _PropertyList extends StatelessWidget {
|
||||
axis: Axis.vertical,
|
||||
controller: _scrollController,
|
||||
barSize: GridSize.scrollBarSize,
|
||||
autoHideScrollbar: false,
|
||||
child: ListView.separated(
|
||||
controller: _scrollController,
|
||||
itemCount: state.gridCells.length,
|
||||
@@ -145,7 +146,26 @@ class _PropertyList extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
_CreateFieldButton(viewId: viewId),
|
||||
const VSpace(10),
|
||||
_CreateFieldButton(
|
||||
viewId: viewId,
|
||||
onCreated: () {
|
||||
// _scrollController.
|
||||
FieldEditor(
|
||||
gridId: viewId,
|
||||
typeOptionLoader: NewFieldTypeOptionLoader(gridId: viewId),
|
||||
onRemoved: () {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollController.animateTo(
|
||||
_scrollController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.ease,
|
||||
);
|
||||
});
|
||||
},
|
||||
).show(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
@@ -155,29 +175,40 @@ class _PropertyList extends StatelessWidget {
|
||||
|
||||
class _CreateFieldButton extends StatelessWidget {
|
||||
final String viewId;
|
||||
const _CreateFieldButton({required this.viewId, Key? key}) : super(key: key);
|
||||
final VoidCallback onCreated;
|
||||
const _CreateFieldButton({
|
||||
required this.viewId,
|
||||
required this.onCreated,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.read<AppTheme>();
|
||||
|
||||
return SizedBox(
|
||||
return Container(
|
||||
height: 40,
|
||||
decoration: _makeBoxDecoration(context),
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_field_newColumn.tr(),
|
||||
fontSize: 12,
|
||||
),
|
||||
hoverColor: theme.shader6,
|
||||
onTap: () => FieldEditor(
|
||||
gridId: viewId,
|
||||
fieldName: "",
|
||||
typeOptionLoader: NewFieldTypeOptionLoader(gridId: viewId),
|
||||
).show(context),
|
||||
onTap: () => onCreated(),
|
||||
leftIcon: svgWidget("home/add"),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration _makeBoxDecoration(BuildContext context) {
|
||||
final theme = context.read<AppTheme>();
|
||||
final borderSide = BorderSide(color: theme.shader6, width: 1.0);
|
||||
return BoxDecoration(
|
||||
color: theme.surface,
|
||||
border: Border(top: borderSide),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RowDetailCell extends StatelessWidget {
|
||||
@@ -231,6 +262,7 @@ class _RowDetailCell extends StatelessWidget {
|
||||
FieldEditor(
|
||||
gridId: cellId.gridId,
|
||||
fieldName: cellId.fieldContext.name,
|
||||
isGroupField: cellId.fieldContext.isGroupField,
|
||||
typeOptionLoader: FieldTypeOptionLoader(
|
||||
gridId: cellId.gridId,
|
||||
field: cellId.fieldContext.field,
|
||||
|
||||
@@ -46,6 +46,7 @@ class InitAppWidgetTask extends LaunchTask {
|
||||
],
|
||||
path: 'assets/translations',
|
||||
fallbackLocale: const Locale('en'),
|
||||
useFallbackTranslations: true,
|
||||
saveLocale: false,
|
||||
child: app,
|
||||
),
|
||||
|
||||
@@ -56,7 +56,8 @@ class _CreateTextFieldDialog extends State<TextFieldDialog> {
|
||||
FlowyFormTextInput(
|
||||
hintText: LocaleKeys.dialogCreatePageNameHint.tr(),
|
||||
initialValue: widget.value,
|
||||
textStyle: const TextStyle(fontSize: 24, fontWeight: FontWeight.w400),
|
||||
textStyle:
|
||||
const TextStyle(fontSize: 24, fontWeight: FontWeight.w400),
|
||||
autoFocus: true,
|
||||
onChanged: (text) {
|
||||
newValue = text;
|
||||
@@ -120,7 +121,7 @@ class _CreateFlowyAlertDialog extends State<FlowyAlertDialog> {
|
||||
const VSpace(20),
|
||||
OkCancelButton(
|
||||
onOkPressed: widget.confirm!,
|
||||
onCancelPressed: widget.confirm,
|
||||
onCancelPressed: widget.cancel,
|
||||
)
|
||||
]
|
||||
],
|
||||
@@ -158,7 +159,7 @@ class OkCancelDialog extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
if (title != null) ...[
|
||||
Text(title!.toUpperCase(), style: TextStyles.T1.textColor(theme.shader1)),
|
||||
FlowyText.medium(title!.toUpperCase(), color: theme.shader1),
|
||||
VSpace(Insets.sm * 1.5),
|
||||
Container(color: theme.bg1, height: 1),
|
||||
VSpace(Insets.m * 1.5),
|
||||
@@ -185,7 +186,12 @@ class OkCancelButton extends StatelessWidget {
|
||||
final double? minHeight;
|
||||
|
||||
const OkCancelButton(
|
||||
{Key? key, this.onOkPressed, this.onCancelPressed, this.okTitle, this.cancelTitle, this.minHeight})
|
||||
{Key? key,
|
||||
this.onOkPressed,
|
||||
this.onCancelPressed,
|
||||
this.okTitle,
|
||||
this.cancelTitle,
|
||||
this.minHeight})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
@@ -200,7 +206,7 @@ class OkCancelButton extends StatelessWidget {
|
||||
cancelTitle ?? LocaleKeys.button_Cancel.tr(),
|
||||
onPressed: () {
|
||||
onCancelPressed!();
|
||||
AppGlobals.nav.pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
bigMode: true,
|
||||
),
|
||||
@@ -210,7 +216,7 @@ class OkCancelButton extends StatelessWidget {
|
||||
okTitle ?? LocaleKeys.button_OK.tr(),
|
||||
onPressed: () {
|
||||
onOkPressed!();
|
||||
AppGlobals.nav.pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
bigMode: true,
|
||||
),
|
||||
|
||||
+60
-40
@@ -14,12 +14,9 @@ class StyledScrollbar extends StatefulWidget {
|
||||
final ScrollController controller;
|
||||
final Function(double)? onDrag;
|
||||
final bool showTrack;
|
||||
final bool autoHideScrollbar;
|
||||
final Color? handleColor;
|
||||
final Color? trackColor;
|
||||
|
||||
// ignore: todo
|
||||
// TODO: Remove contentHeight if we can fix this issue
|
||||
// https://stackoverflow.com/questions/60855712/flutter-how-to-force-scrollcontroller-to-recalculate-position-maxextents
|
||||
final double? contentSize;
|
||||
|
||||
const StyledScrollbar(
|
||||
@@ -30,6 +27,7 @@ class StyledScrollbar extends StatefulWidget {
|
||||
this.onDrag,
|
||||
this.contentSize,
|
||||
this.showTrack = false,
|
||||
this.autoHideScrollbar = true,
|
||||
this.handleColor,
|
||||
this.trackColor})
|
||||
: super(key: key);
|
||||
@@ -46,28 +44,7 @@ class ScrollbarState extends State<StyledScrollbar> {
|
||||
@override
|
||||
void initState() {
|
||||
widget.controller.addListener(() => setState(() {}));
|
||||
widget.controller.position.isScrollingNotifier.addListener(
|
||||
() {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
_hideScrollbarOperation?.cancel();
|
||||
if (!widget.controller.position.isScrollingNotifier.value) {
|
||||
// scroll is stopped
|
||||
_hideScrollbarOperation = CancelableOperation.fromFuture(
|
||||
Future.delayed(const Duration(seconds: 2), () {}),
|
||||
).then((_) {
|
||||
// Opti: hide with animation
|
||||
hideHandler = true;
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
hideHandler = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
_listenOnScrollPositionChanged();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -78,6 +55,7 @@ class ScrollbarState extends State<StyledScrollbar> {
|
||||
|
||||
@override
|
||||
void didUpdateWidget(StyledScrollbar oldWidget) {
|
||||
_listenOnScrollPositionChanged();
|
||||
if (oldWidget.contentSize != widget.contentSize) setState(() {});
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
@@ -103,7 +81,6 @@ class ScrollbarState extends State<StyledScrollbar> {
|
||||
break;
|
||||
case Axis.horizontal:
|
||||
// Use supplied contentSize if we have it, otherwise just fallback to maxScrollExtents
|
||||
|
||||
if (contentSize != null && contentSize > 0) {
|
||||
maxExtent = contentSize - constraints.maxWidth;
|
||||
} else {
|
||||
@@ -118,7 +95,8 @@ class ScrollbarState extends State<StyledScrollbar> {
|
||||
// Calculate the alignment for the handle, this is a value between 0 and 1,
|
||||
// it automatically takes the handle size into acct
|
||||
// ignore: omit_local_variable_types
|
||||
double handleAlignment = maxExtent == 0 ? 0 : widget.controller.offset / maxExtent;
|
||||
double handleAlignment =
|
||||
maxExtent == 0 ? 0 : widget.controller.offset / maxExtent;
|
||||
|
||||
// Convert handle alignment from [0, 1] to [-1, 1]
|
||||
handleAlignment *= 2.0;
|
||||
@@ -127,19 +105,25 @@ class ScrollbarState extends State<StyledScrollbar> {
|
||||
// Calculate handleSize by comparing the total content size to our viewport
|
||||
var handleExtent = _viewExtent;
|
||||
if (contentExtent > _viewExtent) {
|
||||
//Make sure handle is never small than the minSize
|
||||
// Make sure handle is never small than the minSize
|
||||
handleExtent = max(60, _viewExtent * _viewExtent / contentExtent);
|
||||
}
|
||||
|
||||
// Hide the handle if content is < the viewExtent
|
||||
var showHandle = contentExtent > _viewExtent && contentExtent > 0;
|
||||
|
||||
if (hideHandler) {
|
||||
showHandle = false;
|
||||
}
|
||||
|
||||
// Handle color
|
||||
var handleColor = widget.handleColor ?? (theme.isDark ? theme.bg2.withOpacity(.2) : theme.bg2);
|
||||
var handleColor = widget.handleColor ??
|
||||
(theme.isDark ? theme.bg2.withOpacity(.2) : theme.bg2);
|
||||
// Track color
|
||||
var trackColor = widget.trackColor ?? (theme.isDark ? theme.bg2.withOpacity(.1) : theme.bg2.withOpacity(.3));
|
||||
var trackColor = widget.trackColor ??
|
||||
(theme.isDark
|
||||
? theme.bg2.withOpacity(.1)
|
||||
: theme.bg2.withOpacity(.3));
|
||||
|
||||
//Layout the stack, it just contains a child, and
|
||||
return Stack(children: <Widget>[
|
||||
@@ -149,8 +133,12 @@ class ScrollbarState extends State<StyledScrollbar> {
|
||||
alignment: const Alignment(1, 1),
|
||||
child: Container(
|
||||
color: trackColor,
|
||||
width: widget.axis == Axis.vertical ? widget.size : double.infinity,
|
||||
height: widget.axis == Axis.horizontal ? widget.size : double.infinity,
|
||||
width: widget.axis == Axis.vertical
|
||||
? widget.size
|
||||
: double.infinity,
|
||||
height: widget.axis == Axis.horizontal
|
||||
? widget.size
|
||||
: double.infinity,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -167,10 +155,14 @@ class ScrollbarState extends State<StyledScrollbar> {
|
||||
// HANDLE SHAPE
|
||||
child: MouseHoverBuilder(
|
||||
builder: (_, isHovered) => Container(
|
||||
width: widget.axis == Axis.vertical ? widget.size : handleExtent,
|
||||
height: widget.axis == Axis.horizontal ? widget.size : handleExtent,
|
||||
width:
|
||||
widget.axis == Axis.vertical ? widget.size : handleExtent,
|
||||
height: widget.axis == Axis.horizontal
|
||||
? widget.size
|
||||
: handleExtent,
|
||||
decoration: BoxDecoration(
|
||||
color: handleColor.withOpacity(isHovered ? 1 : .85), borderRadius: Corners.s3Border),
|
||||
color: handleColor.withOpacity(isHovered ? 1 : .85),
|
||||
borderRadius: Corners.s3Border),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -182,17 +174,42 @@ class ScrollbarState extends State<StyledScrollbar> {
|
||||
|
||||
void _handleHorizontalDrag(DragUpdateDetails details) {
|
||||
var pos = widget.controller.offset;
|
||||
var pxRatio = (widget.controller.position.maxScrollExtent + _viewExtent) / _viewExtent;
|
||||
widget.controller.jumpTo((pos + details.delta.dx * pxRatio).clamp(0.0, widget.controller.position.maxScrollExtent));
|
||||
var pxRatio = (widget.controller.position.maxScrollExtent + _viewExtent) /
|
||||
_viewExtent;
|
||||
widget.controller.jumpTo((pos + details.delta.dx * pxRatio)
|
||||
.clamp(0.0, widget.controller.position.maxScrollExtent));
|
||||
widget.onDrag?.call(details.delta.dx);
|
||||
}
|
||||
|
||||
void _handleVerticalDrag(DragUpdateDetails details) {
|
||||
var pos = widget.controller.offset;
|
||||
var pxRatio = (widget.controller.position.maxScrollExtent + _viewExtent) / _viewExtent;
|
||||
widget.controller.jumpTo((pos + details.delta.dy * pxRatio).clamp(0.0, widget.controller.position.maxScrollExtent));
|
||||
var pxRatio = (widget.controller.position.maxScrollExtent + _viewExtent) /
|
||||
_viewExtent;
|
||||
widget.controller.jumpTo((pos + details.delta.dy * pxRatio)
|
||||
.clamp(0.0, widget.controller.position.maxScrollExtent));
|
||||
widget.onDrag?.call(details.delta.dy);
|
||||
}
|
||||
|
||||
void _listenOnScrollPositionChanged() {
|
||||
widget.controller.position.isScrollingNotifier.addListener(
|
||||
() {
|
||||
if (!mounted) return;
|
||||
if (!widget.autoHideScrollbar) return;
|
||||
|
||||
_hideScrollbarOperation?.cancel();
|
||||
if (!widget.controller.position.isScrollingNotifier.value) {
|
||||
_hideScrollbarOperation = CancelableOperation.fromFuture(
|
||||
Future.delayed(const Duration(seconds: 2), () {}),
|
||||
).then((_) {
|
||||
hideHandler = true;
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
} else {
|
||||
hideHandler = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ScrollbarListStack extends StatelessWidget {
|
||||
@@ -204,6 +221,7 @@ class ScrollbarListStack extends StatelessWidget {
|
||||
final EdgeInsets? scrollbarPadding;
|
||||
final Color? handleColor;
|
||||
final Color? trackColor;
|
||||
final bool autoHideScrollbar;
|
||||
|
||||
const ScrollbarListStack(
|
||||
{Key? key,
|
||||
@@ -214,6 +232,7 @@ class ScrollbarListStack extends StatelessWidget {
|
||||
this.contentSize,
|
||||
this.scrollbarPadding,
|
||||
this.handleColor,
|
||||
this.autoHideScrollbar = true,
|
||||
this.trackColor})
|
||||
: super(key: key);
|
||||
|
||||
@@ -238,6 +257,7 @@ class ScrollbarListStack extends StatelessWidget {
|
||||
contentSize: contentSize,
|
||||
trackColor: trackColor,
|
||||
handleColor: handleColor,
|
||||
autoHideScrollbar: autoHideScrollbar,
|
||||
),
|
||||
)
|
||||
// The animate will be used by the children that using styled_widget.
|
||||
|
||||
+6
-1
@@ -43,7 +43,9 @@ class StyledSingleChildScrollViewState
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// scrollController.dispose();
|
||||
if (widget.controller == null) {
|
||||
scrollController.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -109,6 +111,9 @@ class StyledCustomScrollViewState extends State<StyledCustomScrollView> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (widget.verticalController == null) {
|
||||
controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/text_style.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'base_styled_button.dart';
|
||||
import 'package:textstyle_extensions/textstyle_extensions.dart';
|
||||
|
||||
class PrimaryTextButton extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
final bool bigMode;
|
||||
|
||||
const PrimaryTextButton(this.label, {Key? key, this.onPressed, this.bigMode = false}) : super(key: key);
|
||||
const PrimaryTextButton(this.label,
|
||||
{Key? key, this.onPressed, this.bigMode = false})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
TextStyle txtStyle = TextStyles.Btn.textColor(Colors.white);
|
||||
return PrimaryButton(bigMode: bigMode, onPressed: onPressed, child: Text(label, style: txtStyle));
|
||||
final theme = context.watch<AppTheme>();
|
||||
return PrimaryButton(
|
||||
bigMode: bigMode,
|
||||
onPressed: onPressed,
|
||||
child: FlowyText.regular(
|
||||
label,
|
||||
color: theme.surface,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,14 +33,16 @@ class PrimaryButton extends StatelessWidget {
|
||||
final VoidCallback? onPressed;
|
||||
final bool bigMode;
|
||||
|
||||
const PrimaryButton({Key? key, required this.child, this.onPressed, this.bigMode = false}) : super(key: key);
|
||||
const PrimaryButton(
|
||||
{Key? key, required this.child, this.onPressed, this.bigMode = false})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return BaseStyledButton(
|
||||
minWidth: bigMode ? 170 : 78,
|
||||
minHeight: bigMode ? 48 : 28,
|
||||
minWidth: bigMode ? 100 : 80,
|
||||
minHeight: bigMode ? 40 : 38,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
bgColor: theme.main1,
|
||||
hoverColor: theme.main1,
|
||||
|
||||
+17
-8
@@ -1,9 +1,8 @@
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
// ignore: import_of_legacy_library_into_null_safe
|
||||
import 'package:textstyle_extensions/textstyle_extensions.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/text_style.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'base_styled_button.dart';
|
||||
|
||||
@@ -12,13 +11,21 @@ class SecondaryTextButton extends StatelessWidget {
|
||||
final VoidCallback? onPressed;
|
||||
final bool bigMode;
|
||||
|
||||
const SecondaryTextButton(this.label, {Key? key, this.onPressed, this.bigMode = false}) : super(key: key);
|
||||
const SecondaryTextButton(this.label,
|
||||
{Key? key, this.onPressed, this.bigMode = false})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
TextStyle txtStyle = TextStyles.Btn.textColor(theme.main1);
|
||||
return SecondaryButton(bigMode: bigMode, onPressed: onPressed, child: Text(label, style: txtStyle));
|
||||
return SecondaryButton(
|
||||
bigMode: bigMode,
|
||||
onPressed: onPressed,
|
||||
child: FlowyText.regular(
|
||||
label,
|
||||
color: theme.main1,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,14 +34,16 @@ class SecondaryButton extends StatelessWidget {
|
||||
final VoidCallback? onPressed;
|
||||
final bool bigMode;
|
||||
|
||||
const SecondaryButton({Key? key, required this.child, this.onPressed, this.bigMode = false}) : super(key: key);
|
||||
const SecondaryButton(
|
||||
{Key? key, required this.child, this.onPressed, this.bigMode = false})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
return BaseStyledButton(
|
||||
minWidth: bigMode ? 170 : 78,
|
||||
minHeight: bigMode ? 48 : 28,
|
||||
minWidth: bigMode ? 100 : 80,
|
||||
minHeight: bigMode ? 40 : 38,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
bgColor: theme.shader7,
|
||||
hoverColor: theme.hover,
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
class DialogSize {
|
||||
static double get minDialogWidth => 480;
|
||||
static double get minDialogWidth => 400;
|
||||
}
|
||||
@@ -133,7 +133,9 @@ class StyledDialogRoute<T> extends PopupRoute<T> {
|
||||
super(settings: settings, filter: barrier.filter);
|
||||
|
||||
@override
|
||||
bool get barrierDismissible => barrier.dismissible;
|
||||
bool get barrierDismissible {
|
||||
return barrier.dismissible;
|
||||
}
|
||||
|
||||
@override
|
||||
String get barrierLabel => barrier.label;
|
||||
|
||||
@@ -78,7 +78,7 @@ impl FolderMigration {
|
||||
|
||||
let folder = FolderPad::new(workspaces, trash)?;
|
||||
KV::set_bool(&key, true);
|
||||
tracing::trace!("Run folder v1 migration");
|
||||
tracing::info!("Run folder v1 migration");
|
||||
Ok(Some(folder))
|
||||
}
|
||||
|
||||
@@ -89,11 +89,10 @@ impl FolderMigration {
|
||||
}
|
||||
let _ = self.migration_folder_rev_struct(folder_id).await?;
|
||||
KV::set_bool(&key, true);
|
||||
tracing::trace!("Run folder v2 migration");
|
||||
// tracing::info!("Run folder v2 migration");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn run_v3_migration(&self, folder_id: &FolderId) -> FlowyResult<()> {
|
||||
let key = migration_flag_key(&self.user_id, V3_MIGRATION);
|
||||
if KV::get_bool(&key) {
|
||||
@@ -101,7 +100,7 @@ impl FolderMigration {
|
||||
}
|
||||
let _ = self.migration_folder_rev_struct(folder_id).await?;
|
||||
KV::set_bool(&key, true);
|
||||
tracing::trace!("Run folder v3 migration");
|
||||
tracing::info!("Run folder v3 migration");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -101,8 +101,7 @@ impl FolderPersistence {
|
||||
}
|
||||
|
||||
let _ = migrations.run_v2_migration(folder_id).await?;
|
||||
|
||||
// let _ = migrations.run_v3_migration(folder_id).await?;
|
||||
let _ = migrations.run_v3_migration(folder_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ impl GridBlockManager {
|
||||
Ok(self.get_block_editor(&block_id).await?)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self, start_row_id), err)]
|
||||
pub(crate) async fn create_row(&self, row_rev: RowRevision, start_row_id: Option<String>) -> FlowyResult<i32> {
|
||||
let block_id = row_rev.block_id.clone();
|
||||
let _ = self.persistence.insert(&row_rev.block_id, &row_rev.id)?;
|
||||
|
||||
+1
@@ -29,6 +29,7 @@ impl TypeOptionBuilder for RichTextTypeOptionBuilder {
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, ProtoBuf)]
|
||||
pub struct RichTextTypeOptionPB {
|
||||
#[pb(index = 1)]
|
||||
#[serde(default)]
|
||||
data: String, //It's not used yet
|
||||
}
|
||||
impl_type_option!(RichTextTypeOptionPB, FieldType::RichText);
|
||||
|
||||
@@ -420,6 +420,7 @@ impl GridRevisionEditor {
|
||||
|
||||
pub async fn delete_row(&self, row_id: &str) -> FlowyResult<()> {
|
||||
let row_rev = self.block_manager.delete_row(row_id).await?;
|
||||
tracing::trace!("Did delete row:{:?}", row_rev);
|
||||
if let Some(row_rev) = row_rev {
|
||||
self.view_manager.did_delete_row(row_rev).await;
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ impl GridViewRevisionEditor {
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub(crate) async fn did_delete_row(&self, row_rev: &RowRevision) {
|
||||
// Send the group notification if the current view has groups;
|
||||
let changesets = self
|
||||
@@ -116,6 +117,7 @@ impl GridViewRevisionEditor {
|
||||
.await;
|
||||
|
||||
if let Some(changesets) = changesets {
|
||||
tracing::trace!("{:?}", changesets);
|
||||
for changeset in changesets {
|
||||
self.notify_did_update_group(changeset).await;
|
||||
}
|
||||
|
||||
@@ -92,17 +92,33 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the groups without the default group
|
||||
pub(crate) fn concrete_groups(&self) -> Vec<&Group> {
|
||||
self.groups_map.values().collect()
|
||||
}
|
||||
|
||||
pub(crate) fn default_group(&self) -> &Group {
|
||||
pub(crate) fn get_default_group(&self) -> &Group {
|
||||
&self.default_group
|
||||
}
|
||||
|
||||
pub(crate) fn get_mut_default_group(&mut self) -> &mut Group {
|
||||
&mut self.default_group
|
||||
}
|
||||
|
||||
/// Returns the groups without the default group
|
||||
pub(crate) fn groups(&self) -> Vec<&Group> {
|
||||
self.groups_map.values().collect()
|
||||
}
|
||||
|
||||
pub(crate) fn get_mut_group(&mut self, group_id: &str) -> Option<&mut Group> {
|
||||
self.groups_map.get_mut(group_id)
|
||||
}
|
||||
|
||||
// Returns the index and group specified by the group_id
|
||||
pub(crate) fn get_group(&self, group_id: &str) -> Option<(usize, &Group)> {
|
||||
match (self.groups_map.get_index_of(group_id), self.groups_map.get(group_id)) {
|
||||
(Some(index), Some(group)) => Some((index, group)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate mut the groups. The default group will be the last one that get mutated.
|
||||
pub(crate) fn iter_mut_groups(&mut self, mut each: impl FnMut(&mut Group)) {
|
||||
pub(crate) fn iter_mut_all_groups(&mut self, mut each: impl FnMut(&mut Group)) {
|
||||
self.groups_map.iter_mut().for_each(|(_, group)| {
|
||||
each(group);
|
||||
});
|
||||
@@ -253,22 +269,6 @@ where
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn get_mut_default_group(&mut self) -> &mut Group {
|
||||
&mut self.default_group
|
||||
}
|
||||
|
||||
pub(crate) fn get_mut_group(&mut self, group_id: &str) -> Option<&mut Group> {
|
||||
self.groups_map.get_mut(group_id)
|
||||
}
|
||||
|
||||
// Returns the index and group specified by the group_id
|
||||
pub(crate) fn get_group(&self, group_id: &str) -> Option<(usize, &Group)> {
|
||||
match (self.groups_map.get_index_of(group_id), self.groups_map.get(group_id)) {
|
||||
(Some(index), Some(group)) => Some((index, group)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_configuration(&self) -> FlowyResult<()> {
|
||||
let configuration = (&*self.configuration).clone();
|
||||
let writer = self.writer.clone();
|
||||
|
||||
@@ -183,11 +183,11 @@ where
|
||||
|
||||
fn groups(&self) -> Vec<Group> {
|
||||
if self.use_default_group() {
|
||||
let mut groups: Vec<Group> = self.group_ctx.concrete_groups().into_iter().cloned().collect();
|
||||
groups.push(self.group_ctx.default_group().clone());
|
||||
let mut groups: Vec<Group> = self.group_ctx.groups().into_iter().cloned().collect();
|
||||
groups.push(self.group_ctx.get_default_group().clone());
|
||||
groups
|
||||
} else {
|
||||
self.group_ctx.concrete_groups().into_iter().cloned().collect()
|
||||
self.group_ctx.groups().into_iter().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ where
|
||||
let mut grouped_rows: Vec<GroupedRow> = vec![];
|
||||
let cell_bytes = decode_any_cell_data(cell_rev.data, field_rev);
|
||||
let cell_data = cell_bytes.parser::<P>()?;
|
||||
for group in self.group_ctx.concrete_groups() {
|
||||
for group in self.group_ctx.groups() {
|
||||
if self.can_group(&group.filter_content, &cell_data) {
|
||||
grouped_rows.push(GroupedRow {
|
||||
row: row_rev.into(),
|
||||
@@ -264,12 +264,17 @@ where
|
||||
row_rev: &RowRevision,
|
||||
field_rev: &FieldRevision,
|
||||
) -> FlowyResult<Vec<GroupChangesetPB>> {
|
||||
// if the cell_rev is none, then the row must be crated from the default group.
|
||||
if let Some(cell_rev) = row_rev.cells.get(&self.field_id) {
|
||||
let cell_bytes = decode_any_cell_data(cell_rev.data.clone(), field_rev);
|
||||
let cell_data = cell_bytes.parser::<P>()?;
|
||||
Ok(self.remove_row_if_match(row_rev, &cell_data))
|
||||
} else {
|
||||
Ok(vec![])
|
||||
let group = self.group_ctx.get_default_group();
|
||||
Ok(vec![GroupChangesetPB::delete(
|
||||
group.id.clone(),
|
||||
vec![row_rev.id.clone()],
|
||||
)])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -41,7 +41,7 @@ impl GroupAction for CheckboxGroupController {
|
||||
|
||||
fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
|
||||
let mut changesets = vec![];
|
||||
self.group_ctx.iter_mut_groups(|group| {
|
||||
self.group_ctx.iter_mut_all_groups(|group| {
|
||||
let mut changeset = GroupChangesetPB::new(group.id.clone());
|
||||
let is_contained = group.contains_row(&row_rev.id);
|
||||
if group.id == CHECK && cell_data.is_check() {
|
||||
@@ -63,7 +63,7 @@ impl GroupAction for CheckboxGroupController {
|
||||
|
||||
fn remove_row_if_match(&mut self, row_rev: &RowRevision, _cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
|
||||
let mut changesets = vec![];
|
||||
self.group_ctx.iter_mut_groups(|group| {
|
||||
self.group_ctx.iter_mut_all_groups(|group| {
|
||||
let mut changeset = GroupChangesetPB::new(group.id.clone());
|
||||
if group.contains_row(&row_rev.id) {
|
||||
changeset.deleted_rows.push(row_rev.id.clone());
|
||||
@@ -79,7 +79,7 @@ impl GroupAction for CheckboxGroupController {
|
||||
|
||||
fn move_row(&mut self, _cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec<GroupChangesetPB> {
|
||||
let mut group_changeset = vec![];
|
||||
self.group_ctx.iter_mut_groups(|group| {
|
||||
self.group_ctx.iter_mut_all_groups(|group| {
|
||||
if let Some(changeset) = move_group_row(group, &mut context) {
|
||||
group_changeset.push(changeset);
|
||||
}
|
||||
|
||||
+3
-3
@@ -28,7 +28,7 @@ impl GroupAction for MultiSelectGroupController {
|
||||
|
||||
fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
|
||||
let mut changesets = vec![];
|
||||
self.group_ctx.iter_mut_groups(|group| {
|
||||
self.group_ctx.iter_mut_all_groups(|group| {
|
||||
if let Some(changeset) = add_select_option_row(group, cell_data, row_rev) {
|
||||
changesets.push(changeset);
|
||||
}
|
||||
@@ -38,7 +38,7 @@ impl GroupAction for MultiSelectGroupController {
|
||||
|
||||
fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
|
||||
let mut changesets = vec![];
|
||||
self.group_ctx.iter_mut_groups(|group| {
|
||||
self.group_ctx.iter_mut_all_groups(|group| {
|
||||
if let Some(changeset) = remove_select_option_row(group, cell_data, row_rev) {
|
||||
changesets.push(changeset);
|
||||
}
|
||||
@@ -48,7 +48,7 @@ impl GroupAction for MultiSelectGroupController {
|
||||
|
||||
fn move_row(&mut self, _cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec<GroupChangesetPB> {
|
||||
let mut group_changeset = vec![];
|
||||
self.group_ctx.iter_mut_groups(|group| {
|
||||
self.group_ctx.iter_mut_all_groups(|group| {
|
||||
if let Some(changeset) = move_group_row(group, &mut context) {
|
||||
group_changeset.push(changeset);
|
||||
}
|
||||
|
||||
+3
-3
@@ -28,7 +28,7 @@ impl GroupAction for SingleSelectGroupController {
|
||||
|
||||
fn add_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
|
||||
let mut changesets = vec![];
|
||||
self.group_ctx.iter_mut_groups(|group| {
|
||||
self.group_ctx.iter_mut_all_groups(|group| {
|
||||
if let Some(changeset) = add_select_option_row(group, cell_data, row_rev) {
|
||||
changesets.push(changeset);
|
||||
}
|
||||
@@ -38,7 +38,7 @@ impl GroupAction for SingleSelectGroupController {
|
||||
|
||||
fn remove_row_if_match(&mut self, row_rev: &RowRevision, cell_data: &Self::CellDataType) -> Vec<GroupChangesetPB> {
|
||||
let mut changesets = vec![];
|
||||
self.group_ctx.iter_mut_groups(|group| {
|
||||
self.group_ctx.iter_mut_all_groups(|group| {
|
||||
if let Some(changeset) = remove_select_option_row(group, cell_data, row_rev) {
|
||||
changesets.push(changeset);
|
||||
}
|
||||
@@ -48,7 +48,7 @@ impl GroupAction for SingleSelectGroupController {
|
||||
|
||||
fn move_row(&mut self, _cell_data: &Self::CellDataType, mut context: MoveGroupRowContext) -> Vec<GroupChangesetPB> {
|
||||
let mut group_changeset = vec![];
|
||||
self.group_ctx.iter_mut_groups(|group| {
|
||||
self.group_ctx.iter_mut_all_groups(|group| {
|
||||
if let Some(changeset) = move_group_row(group, &mut context) {
|
||||
group_changeset.push(changeset);
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ pub fn make_default_board() -> BuildGridContext {
|
||||
let done_option = SelectOptionPB::with_color("Done", SelectOptionColorPB::Yellow);
|
||||
let single_select_type_option = SingleSelectTypeOptionBuilder::default()
|
||||
.add_option(to_do_option.clone())
|
||||
.add_option(doing_option.clone())
|
||||
.add_option(done_option.clone());
|
||||
.add_option(doing_option)
|
||||
.add_option(done_option);
|
||||
let single_select_field = FieldBuilder::new(single_select_type_option)
|
||||
.name("Status")
|
||||
.visibility(true)
|
||||
|
||||
@@ -186,7 +186,7 @@ script = [
|
||||
"""
|
||||
cd ./app_flowy/
|
||||
exec cmd.exe /c flutter clean
|
||||
exec cmd.exe /c flutter pub get
|
||||
exec cmd.exe /c flutter packages pub get
|
||||
exec cmd.exe /c flutter packages pub run easy_localization:generate -S assets/translations/ -f keys -o locale_keys.g.dart -S assets/translations -s en.json
|
||||
exec cmd.exe /c flutter packages pub run build_runner build --delete-conflicting-outputs
|
||||
"""
|
||||
|
||||
@@ -29,6 +29,9 @@ end
|
||||
ret = which protoc-gen-dart
|
||||
if is_empty ${ret}
|
||||
exec cmd.exe /c dart pub global activate protoc_plugin
|
||||
end
|
||||
ret = which protoc-gen-dart
|
||||
if is_empty ${ret}
|
||||
home_dir = get_home_dir
|
||||
echo Please add '${home_dir}\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\bin' into PATH env var
|
||||
exit -1
|
||||
|
||||
@@ -17,10 +17,13 @@ pub struct AppRevision {
|
||||
|
||||
pub belongings: Vec<ViewRevision>,
|
||||
|
||||
#[serde(default)]
|
||||
pub version: i64,
|
||||
|
||||
#[serde(default)]
|
||||
pub modified_time: i64,
|
||||
|
||||
#[serde(default)]
|
||||
pub create_time: i64,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,76 @@
|
||||
use crate::revision::{TrashRevision, WorkspaceRevision};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::de::{MapAccess, Visitor};
|
||||
use serde::{de, Deserialize, Deserializer, Serialize};
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, Clone, Eq, PartialEq)]
|
||||
#[derive(Debug, Default, Serialize, Clone, Eq, PartialEq)]
|
||||
pub struct FolderRevision {
|
||||
pub workspaces: Vec<Arc<WorkspaceRevision>>,
|
||||
pub trash: Vec<Arc<TrashRevision>>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for FolderRevision {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct FolderVisitor<'a>(&'a mut Option<FolderRevision>);
|
||||
impl<'de, 'a> Visitor<'de> for FolderVisitor<'a> {
|
||||
type Value = ();
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("Expect struct FolderRevision")
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: MapAccess<'de>,
|
||||
{
|
||||
let f = |map: &mut A,
|
||||
workspaces: &mut Option<Vec<WorkspaceRevision>>,
|
||||
trash: &mut Option<Vec<TrashRevision>>| match map.next_key::<String>()
|
||||
{
|
||||
Ok(Some(key)) => {
|
||||
if key == "workspaces" && workspaces.is_none() {
|
||||
*workspaces = Some(map.next_value::<Vec<WorkspaceRevision>>().ok()?);
|
||||
}
|
||||
if key == "trash" && trash.is_none() {
|
||||
*trash = Some(map.next_value::<Vec<TrashRevision>>().ok()?);
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
Ok(None) => None,
|
||||
Err(_e) => None,
|
||||
};
|
||||
|
||||
let mut workspaces: Option<Vec<WorkspaceRevision>> = None;
|
||||
let mut trash: Option<Vec<TrashRevision>> = None;
|
||||
while f(&mut map, &mut workspaces, &mut trash).is_some() {
|
||||
if workspaces.is_some() && trash.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
*self.0 = Some(FolderRevision {
|
||||
workspaces: workspaces.unwrap_or_default().into_iter().map(Arc::new).collect(),
|
||||
trash: trash.unwrap_or_default().into_iter().map(Arc::new).collect(),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let mut folder_rev: Option<FolderRevision> = None;
|
||||
const FIELDS: &[&str] = &["workspaces", "trash"];
|
||||
let _ = serde::Deserializer::deserialize_struct(
|
||||
deserializer,
|
||||
"FolderRevision",
|
||||
FIELDS,
|
||||
FolderVisitor(&mut folder_rev),
|
||||
);
|
||||
|
||||
match folder_rev {
|
||||
None => Err(de::Error::missing_field("workspaces or trash")),
|
||||
Some(folder_rev) => Ok(folder_rev),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,10 @@ pub struct TrashRevision {
|
||||
|
||||
pub name: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub modified_time: i64,
|
||||
|
||||
#[serde(default)]
|
||||
pub create_time: i64,
|
||||
|
||||
pub ty: TrashTypeRevision,
|
||||
|
||||
@@ -9,7 +9,6 @@ pub fn gen_view_id() -> String {
|
||||
pub struct ViewRevision {
|
||||
pub id: String,
|
||||
|
||||
// Maybe app_id or vi
|
||||
#[serde(rename = "belong_to_id")]
|
||||
pub app_id: String,
|
||||
|
||||
@@ -24,8 +23,10 @@ pub struct ViewRevision {
|
||||
|
||||
pub belongings: Vec<ViewRevision>,
|
||||
|
||||
#[serde(default)]
|
||||
pub modified_time: i64,
|
||||
|
||||
#[serde(default)]
|
||||
pub create_time: i64,
|
||||
|
||||
#[serde(default)]
|
||||
|
||||
@@ -14,7 +14,9 @@ pub struct WorkspaceRevision {
|
||||
|
||||
pub apps: Vec<AppRevision>,
|
||||
|
||||
#[serde(default)]
|
||||
pub modified_time: i64,
|
||||
|
||||
#[serde(default)]
|
||||
pub create_time: i64,
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use flowy_folder_data_model::revision::{AppRevision, FolderRevision, TrashRevisi
|
||||
use lib_infra::util::move_vec_element;
|
||||
use lib_ot::core::*;
|
||||
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
@@ -44,7 +45,9 @@ impl FolderPad {
|
||||
pub fn from_delta(delta: FolderDelta) -> CollaborateResult<Self> {
|
||||
// TODO: Reconvert from history if delta.to_str() failed.
|
||||
let content = delta.content()?;
|
||||
let folder_rev: FolderRevision = serde_json::from_str(&content).map_err(|e| {
|
||||
let mut deserializer = serde_json::Deserializer::from_reader(content.as_bytes());
|
||||
|
||||
let folder_rev = FolderRevision::deserialize(&mut deserializer).map_err(|e| {
|
||||
tracing::error!("Deserialize folder from {} failed", content);
|
||||
return CollaborateError::internal().context(format!("Deserialize delta to folder failed: {}", e));
|
||||
})?;
|
||||
@@ -457,6 +460,7 @@ mod tests {
|
||||
#![allow(clippy::all)]
|
||||
use crate::{client_folder::folder_pad::FolderPad, entities::folder::FolderDelta};
|
||||
use chrono::Utc;
|
||||
use serde::Deserialize;
|
||||
|
||||
use flowy_folder_data_model::revision::{
|
||||
AppRevision, FolderRevision, TrashRevision, ViewRevision, WorkspaceRevision,
|
||||
@@ -480,6 +484,20 @@ mod tests {
|
||||
assert_eq!(folder, folder_from_delta);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folder_deserialize_invalid_json_test() {
|
||||
for json in vec![
|
||||
// No timestamp
|
||||
r#"{"workspaces":[{"id":"1","name":"first workspace","desc":"","apps":[]}],"trash":[]}"#,
|
||||
// Trailing characters
|
||||
r#"{"workspaces":[{"id":"1","name":"first workspace","desc":"","apps":[]}],"trash":[]}123"#,
|
||||
] {
|
||||
let mut deserializer = serde_json::Deserializer::from_reader(json.as_bytes());
|
||||
let folder_rev = FolderRevision::deserialize(&mut deserializer).unwrap();
|
||||
assert_eq!(folder_rev.workspaces.first().as_ref().unwrap().name, "first workspace");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn folder_update_workspace() {
|
||||
let (mut folder, initial_delta, workspace) = test_folder();
|
||||
|
||||
@@ -23,6 +23,8 @@ impl std::ops::Deref for GridViewRevisionPad {
|
||||
}
|
||||
|
||||
impl GridViewRevisionPad {
|
||||
// For the moment, the view_id is equal to grid_id. The grid_id represents the database id.
|
||||
// A database can be referenced by multiple views.
|
||||
pub fn new(grid_id: String, view_id: String) -> Self {
|
||||
let view = Arc::new(GridViewRevision::new(grid_id, view_id));
|
||||
let json = serde_json::to_string(&view).unwrap();
|
||||
@@ -30,11 +32,14 @@ impl GridViewRevisionPad {
|
||||
Self { view, delta }
|
||||
}
|
||||
|
||||
pub fn from_delta(delta: TextDelta) -> CollaborateResult<Self> {
|
||||
pub fn from_delta(view_id: &str, delta: TextDelta) -> CollaborateResult<Self> {
|
||||
if delta.is_empty() {
|
||||
return Ok(GridViewRevisionPad::new(view_id.to_owned(), view_id.to_owned()));
|
||||
}
|
||||
let s = delta.content()?;
|
||||
let view: GridViewRevision = serde_json::from_str(&s).map_err(|e| {
|
||||
let msg = format!("Deserialize delta to GridViewRevision failed: {}", e);
|
||||
tracing::error!("{}", s);
|
||||
tracing::error!("parsing json: {}", s);
|
||||
CollaborateError::internal().context(msg)
|
||||
})?;
|
||||
Ok(Self {
|
||||
@@ -43,9 +48,9 @@ impl GridViewRevisionPad {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_revisions(_grid_id: &str, revisions: Vec<Revision>) -> CollaborateResult<Self> {
|
||||
pub fn from_revisions(view_id: &str, revisions: Vec<Revision>) -> CollaborateResult<Self> {
|
||||
let delta: TextDelta = make_text_delta_from_revisions(revisions)?;
|
||||
Self::from_delta(delta)
|
||||
Self::from_delta(view_id, delta)
|
||||
}
|
||||
|
||||
pub fn get_groups_by_field_revs(&self, field_revs: &[Arc<FieldRevision>]) -> Option<GroupConfigurationsByFieldId> {
|
||||
|
||||
Reference in new issue
Block a user