mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-24 22:57:12 -04:00
fix: filter UI bugs (#1489)
* chore: remove the add filter button if there is no filters can not be added * fix: update field info after filter was changed * chore: update filter choicechip ui * chore: insert and delete one by one to keep the delete/insert index is right * chore: show filter after creating the default filter * chore: update textfield_tags version to calm the warnings * chore: try to fix potential fails on backend test Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
parent
149c2a2725
commit
182bfae5ad
23 changed files with 251 additions and 136 deletions
|
@ -175,7 +175,14 @@
|
||||||
"is": "Is",
|
"is": "Is",
|
||||||
"isNot": "Is not",
|
"isNot": "Is not",
|
||||||
"isEmpty": "Is empty",
|
"isEmpty": "Is empty",
|
||||||
"isNotEmpty": "Is not empty"
|
"isNotEmpty": "Is not empty",
|
||||||
|
"choicechipPrefix": {
|
||||||
|
"isNot": "Not",
|
||||||
|
"startWith": "Starts with",
|
||||||
|
"endWith": "Ends with",
|
||||||
|
"isEmpty": "is empty",
|
||||||
|
"isNotEmpty": "is not empty"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"field": {
|
"field": {
|
||||||
"hide": "Hide",
|
"hide": "Hide",
|
||||||
|
|
|
@ -142,6 +142,9 @@ class GridFieldController {
|
||||||
filters.retainWhere(
|
filters.retainWhere(
|
||||||
(element) => !deleteFilterIds.contains(element.filter.id),
|
(element) => !deleteFilterIds.contains(element.filter.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_filterPBByFieldId.removeWhere(
|
||||||
|
(key, value) => deleteFilterIds.contains(value.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inserts the new filter if it's not exist
|
// Inserts the new filter if it's not exist
|
||||||
|
@ -151,6 +154,7 @@ class GridFieldController {
|
||||||
if (filterIndex == -1) {
|
if (filterIndex == -1) {
|
||||||
final fieldInfo = _findFieldInfoForFilter(fieldInfos, newFilter);
|
final fieldInfo = _findFieldInfoForFilter(fieldInfos, newFilter);
|
||||||
if (fieldInfo != null) {
|
if (fieldInfo != null) {
|
||||||
|
_filterPBByFieldId[fieldInfo.id] = newFilter;
|
||||||
filters.add(FilterInfo(gridId, newFilter, fieldInfo));
|
filters.add(FilterInfo(gridId, newFilter, fieldInfo));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -187,10 +191,9 @@ class GridFieldController {
|
||||||
}
|
}
|
||||||
_filterPBByFieldId[fieldInfo.id] = updatedFilter.filter;
|
_filterPBByFieldId[fieldInfo.id] = updatedFilter.filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateFieldInfos();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_updateFieldInfos();
|
||||||
_filterNotifier?.filters = filters;
|
_filterNotifier?.filters = filters;
|
||||||
},
|
},
|
||||||
(err) => Log.error(err),
|
(err) => Log.error(err),
|
||||||
|
@ -345,7 +348,6 @@ class GridFieldController {
|
||||||
}
|
}
|
||||||
|
|
||||||
_filterCallbacks[onFilters] = callback;
|
_filterCallbacks[onFilters] = callback;
|
||||||
callback();
|
|
||||||
_filterNotifier?.addListener(callback);
|
_filterNotifier?.addListener(callback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,7 @@ class GridCreateFilterBloc
|
||||||
|
|
||||||
void _startListening() {
|
void _startListening() {
|
||||||
_onFieldFn = (fields) {
|
_onFieldFn = (fields) {
|
||||||
fields.retainWhere((field) => field.hasFilter == false);
|
fields.retainWhere((field) => field.canCreateFilter);
|
||||||
add(GridCreateFilterEvent.didReceiveFields(fields));
|
add(GridCreateFilterEvent.didReceiveFields(fields));
|
||||||
};
|
};
|
||||||
fieldController.addListener(onFields: _onFieldFn);
|
fieldController.addListener(onFields: _onFieldFn);
|
||||||
|
|
|
@ -11,6 +11,7 @@ class GridFilterMenuBloc
|
||||||
final String viewId;
|
final String viewId;
|
||||||
final GridFieldController fieldController;
|
final GridFieldController fieldController;
|
||||||
void Function(List<FilterInfo>)? _onFilterFn;
|
void Function(List<FilterInfo>)? _onFilterFn;
|
||||||
|
void Function(List<FieldInfo>)? _onFieldFn;
|
||||||
|
|
||||||
GridFilterMenuBloc({required this.viewId, required this.fieldController})
|
GridFilterMenuBloc({required this.viewId, required this.fieldController})
|
||||||
: super(GridFilterMenuState.initial(
|
: super(GridFilterMenuState.initial(
|
||||||
|
@ -32,7 +33,12 @@ class GridFilterMenuBloc
|
||||||
emit(state.copyWith(isVisible: isVisible));
|
emit(state.copyWith(isVisible: isVisible));
|
||||||
},
|
},
|
||||||
didReceiveFields: (List<FieldInfo> fields) {
|
didReceiveFields: (List<FieldInfo> fields) {
|
||||||
emit(state.copyWith(fields: fields));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
fields: fields,
|
||||||
|
creatableFields: getCreatableFilter(fields),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -44,9 +50,18 @@ class GridFilterMenuBloc
|
||||||
add(GridFilterMenuEvent.didReceiveFilters(filters));
|
add(GridFilterMenuEvent.didReceiveFilters(filters));
|
||||||
};
|
};
|
||||||
|
|
||||||
fieldController.addListener(onFilters: (filters) {
|
_onFieldFn = (fields) {
|
||||||
_onFilterFn?.call(filters);
|
add(GridFilterMenuEvent.didReceiveFields(fields));
|
||||||
});
|
};
|
||||||
|
|
||||||
|
fieldController.addListener(
|
||||||
|
onFilters: (filters) {
|
||||||
|
_onFilterFn?.call(filters);
|
||||||
|
},
|
||||||
|
onFields: (fields) {
|
||||||
|
_onFieldFn?.call(fields);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -55,6 +70,10 @@ class GridFilterMenuBloc
|
||||||
fieldController.removeListener(onFiltersListener: _onFilterFn!);
|
fieldController.removeListener(onFiltersListener: _onFilterFn!);
|
||||||
_onFilterFn = null;
|
_onFilterFn = null;
|
||||||
}
|
}
|
||||||
|
if (_onFieldFn != null) {
|
||||||
|
fieldController.removeListener(onFieldsListener: _onFieldFn!);
|
||||||
|
_onFieldFn = null;
|
||||||
|
}
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,6 +94,7 @@ class GridFilterMenuState with _$GridFilterMenuState {
|
||||||
required String viewId,
|
required String viewId,
|
||||||
required List<FilterInfo> filters,
|
required List<FilterInfo> filters,
|
||||||
required List<FieldInfo> fields,
|
required List<FieldInfo> fields,
|
||||||
|
required List<FieldInfo> creatableFields,
|
||||||
required bool isVisible,
|
required bool isVisible,
|
||||||
}) = _GridFilterMenuState;
|
}) = _GridFilterMenuState;
|
||||||
|
|
||||||
|
@ -87,6 +107,13 @@ class GridFilterMenuState with _$GridFilterMenuState {
|
||||||
viewId: viewId,
|
viewId: viewId,
|
||||||
filters: filterInfos,
|
filters: filterInfos,
|
||||||
fields: fields,
|
fields: fields,
|
||||||
|
creatableFields: getCreatableFilter(fields),
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<FieldInfo> getCreatableFilter(List<FieldInfo> fieldInfos) {
|
||||||
|
final List<FieldInfo> creatableFields = List.from(fieldInfos);
|
||||||
|
creatableFields.retainWhere((element) => element.canCreateFilter);
|
||||||
|
return creatableFields;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
|
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
|
||||||
import 'package:flowy_sdk/log.dart';
|
|
||||||
import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pbserver.dart';
|
import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pbserver.dart';
|
||||||
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
|
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
@ -30,26 +29,20 @@ class TextFilterEditorBloc
|
||||||
_startListening();
|
_startListening();
|
||||||
},
|
},
|
||||||
updateCondition: (TextFilterCondition condition) {
|
updateCondition: (TextFilterCondition condition) {
|
||||||
final textFilter = filterInfo.textFilter()!;
|
|
||||||
_ffiService.insertTextFilter(
|
_ffiService.insertTextFilter(
|
||||||
filterId: filterInfo.filter.id,
|
filterId: filterInfo.filter.id,
|
||||||
fieldId: filterInfo.field.id,
|
fieldId: filterInfo.field.id,
|
||||||
condition: condition,
|
condition: condition,
|
||||||
content: textFilter.content,
|
content: state.filter.content,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
updateContent: (content) {
|
updateContent: (content) {
|
||||||
final textFilter = filterInfo.textFilter();
|
_ffiService.insertTextFilter(
|
||||||
if (textFilter != null) {
|
filterId: filterInfo.filter.id,
|
||||||
_ffiService.insertTextFilter(
|
fieldId: filterInfo.field.id,
|
||||||
filterId: filterInfo.filter.id,
|
condition: state.filter.condition,
|
||||||
fieldId: filterInfo.field.id,
|
content: content,
|
||||||
condition: textFilter.condition,
|
);
|
||||||
content: content,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Log.error("Invalid text filter");
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
delete: () {
|
delete: () {
|
||||||
_ffiService.deleteFilter(
|
_ffiService.deleteFilter(
|
||||||
|
@ -60,7 +53,11 @@ class TextFilterEditorBloc
|
||||||
},
|
},
|
||||||
didReceiveFilter: (FilterPB filter) {
|
didReceiveFilter: (FilterPB filter) {
|
||||||
final filterInfo = state.filterInfo.copyWith(filter: filter);
|
final filterInfo = state.filterInfo.copyWith(filter: filter);
|
||||||
emit(state.copyWith(filterInfo: filterInfo));
|
final textFilter = filterInfo.textFilter()!;
|
||||||
|
emit(state.copyWith(
|
||||||
|
filterInfo: filterInfo,
|
||||||
|
filter: textFilter,
|
||||||
|
));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -99,12 +96,15 @@ class TextFilterEditorEvent with _$TextFilterEditorEvent {
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class TextFilterEditorState with _$TextFilterEditorState {
|
class TextFilterEditorState with _$TextFilterEditorState {
|
||||||
const factory TextFilterEditorState({required FilterInfo filterInfo}) =
|
const factory TextFilterEditorState({
|
||||||
_GridFilterState;
|
required FilterInfo filterInfo,
|
||||||
|
required TextFilterPB filter,
|
||||||
|
}) = _GridFilterState;
|
||||||
|
|
||||||
factory TextFilterEditorState.initial(FilterInfo filterInfo) {
|
factory TextFilterEditorState.initial(FilterInfo filterInfo) {
|
||||||
return TextFilterEditorState(
|
return TextFilterEditorState(
|
||||||
filterInfo: filterInfo,
|
filterInfo: filterInfo,
|
||||||
|
filter: filterInfo.textFilter()!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,22 +78,23 @@ class GridRowCache {
|
||||||
_showRows(changeset.visibleRows);
|
_showRows(changeset.visibleRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _deleteRows(List<String> deletedRows) {
|
void _deleteRows(List<String> deletedRowIds) {
|
||||||
if (deletedRows.isEmpty) return;
|
for (final rowId in deletedRowIds) {
|
||||||
|
final deletedRow = _rowList.remove(rowId);
|
||||||
final deletedIndex = _rowList.removeRows(deletedRows);
|
if (deletedRow != null) {
|
||||||
if (deletedIndex.isNotEmpty) {
|
_rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedRow));
|
||||||
_rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedIndex));
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _insertRows(List<InsertedRowPB> insertRows) {
|
void _insertRows(List<InsertedRowPB> insertRows) {
|
||||||
if (insertRows.isEmpty) return;
|
for (final insertedRow in insertRows) {
|
||||||
|
final insertedIndex =
|
||||||
InsertedIndexs insertIndexs =
|
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.row));
|
||||||
_rowList.insertRows(insertRows, (rowPB) => buildGridRow(rowPB));
|
if (insertedIndex != null) {
|
||||||
if (insertIndexs.isNotEmpty) {
|
_rowChangeReasonNotifier
|
||||||
_rowChangeReasonNotifier.receive(RowsChangedReason.insert(insertIndexs));
|
.receive(RowsChangedReason.insert(insertedIndex));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,21 +109,22 @@ class GridRowCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _hideRows(List<String> invisibleRows) {
|
void _hideRows(List<String> invisibleRows) {
|
||||||
if (invisibleRows.isEmpty) return;
|
for (final rowId in invisibleRows) {
|
||||||
|
final deletedRow = _rowList.remove(rowId);
|
||||||
final List<DeletedIndex> deletedRows = _rowList.removeRows(invisibleRows);
|
if (deletedRow != null) {
|
||||||
if (deletedRows.isNotEmpty) {
|
_rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedRow));
|
||||||
_rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedRows));
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showRows(List<InsertedRowPB> visibleRows) {
|
void _showRows(List<InsertedRowPB> visibleRows) {
|
||||||
if (visibleRows.isEmpty) return;
|
for (final insertedRow in visibleRows) {
|
||||||
|
final insertedIndex =
|
||||||
final List<InsertedIndex> insertedRows =
|
_rowList.insert(insertedRow.index, buildGridRow(insertedRow.row));
|
||||||
_rowList.insertRows(visibleRows, (rowPB) => buildGridRow(rowPB));
|
if (insertedIndex != null) {
|
||||||
if (insertedRows.isNotEmpty) {
|
_rowChangeReasonNotifier
|
||||||
_rowChangeReasonNotifier.receive(RowsChangedReason.insert(insertedRows));
|
.receive(RowsChangedReason.insert(insertedIndex));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,8 +276,8 @@ typedef UpdatedIndexMap = LinkedHashMap<String, UpdatedIndex>;
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class RowsChangedReason with _$RowsChangedReason {
|
class RowsChangedReason with _$RowsChangedReason {
|
||||||
const factory RowsChangedReason.insert(InsertedIndexs items) = _Insert;
|
const factory RowsChangedReason.insert(InsertedIndex item) = _Insert;
|
||||||
const factory RowsChangedReason.delete(DeletedIndexs items) = _Delete;
|
const factory RowsChangedReason.delete(DeletedIndex item) = _Delete;
|
||||||
const factory RowsChangedReason.update(UpdatedIndexMap indexs) = _Update;
|
const factory RowsChangedReason.update(UpdatedIndexMap indexs) = _Update;
|
||||||
const factory RowsChangedReason.fieldDidChange() = _FieldDidChange;
|
const factory RowsChangedReason.fieldDidChange() = _FieldDidChange;
|
||||||
const factory RowsChangedReason.initial() = InitialListState;
|
const factory RowsChangedReason.initial() = InitialListState;
|
||||||
|
|
|
@ -39,10 +39,10 @@ class RowList {
|
||||||
_rowInfoByRowId[rowId] = rowInfo;
|
_rowInfoByRowId[rowId] = rowInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
void insert(int index, RowInfo rowInfo) {
|
InsertedIndex? insert(int index, RowInfo rowInfo) {
|
||||||
final rowId = rowInfo.rowPB.id;
|
final rowId = rowInfo.rowPB.id;
|
||||||
var insertedIndex = index;
|
var insertedIndex = index;
|
||||||
if (_rowInfos.length < insertedIndex) {
|
if (_rowInfos.length <= insertedIndex) {
|
||||||
insertedIndex = _rowInfos.length;
|
insertedIndex = _rowInfos.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,13 +50,16 @@ class RowList {
|
||||||
if (oldRowInfo != null) {
|
if (oldRowInfo != null) {
|
||||||
_rowInfos.insert(insertedIndex, rowInfo);
|
_rowInfos.insert(insertedIndex, rowInfo);
|
||||||
_rowInfos.remove(oldRowInfo);
|
_rowInfos.remove(oldRowInfo);
|
||||||
|
_rowInfoByRowId[rowId] = rowInfo;
|
||||||
|
return null;
|
||||||
} else {
|
} else {
|
||||||
_rowInfos.insert(insertedIndex, rowInfo);
|
_rowInfos.insert(insertedIndex, rowInfo);
|
||||||
|
_rowInfoByRowId[rowId] = rowInfo;
|
||||||
|
return InsertedIndex(index: insertedIndex, rowId: rowId);
|
||||||
}
|
}
|
||||||
_rowInfoByRowId[rowId] = rowInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RowInfo? remove(String rowId) {
|
DeletedIndex? remove(String rowId) {
|
||||||
final rowInfo = _rowInfoByRowId[rowId];
|
final rowInfo = _rowInfoByRowId[rowId];
|
||||||
if (rowInfo != null) {
|
if (rowInfo != null) {
|
||||||
final index = _rowInfos.indexOf(rowInfo);
|
final index = _rowInfos.indexOf(rowInfo);
|
||||||
|
@ -64,8 +67,10 @@ class RowList {
|
||||||
_rowInfoByRowId.remove(rowInfo.rowPB.id);
|
_rowInfoByRowId.remove(rowInfo.rowPB.id);
|
||||||
_rowInfos.remove(rowInfo);
|
_rowInfos.remove(rowInfo);
|
||||||
}
|
}
|
||||||
|
return DeletedIndex(index: index, rowInfo: rowInfo);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return rowInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
InsertedIndexs insertRows(
|
InsertedIndexs insertRows(
|
||||||
|
|
|
@ -207,20 +207,16 @@ class _GridRowsState extends State<_GridRows> {
|
||||||
return BlocConsumer<GridBloc, GridState>(
|
return BlocConsumer<GridBloc, GridState>(
|
||||||
listenWhen: (previous, current) => previous.reason != current.reason,
|
listenWhen: (previous, current) => previous.reason != current.reason,
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
state.reason.mapOrNull(
|
state.reason.whenOrNull(
|
||||||
insert: (value) {
|
insert: (item) {
|
||||||
for (final item in value.items) {
|
_key.currentState?.insertItem(item.index);
|
||||||
_key.currentState?.insertItem(item.index);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
delete: (value) {
|
delete: (item) {
|
||||||
for (final item in value.items) {
|
_key.currentState?.removeItem(
|
||||||
_key.currentState?.removeItem(
|
item.index,
|
||||||
item.index,
|
(context, animation) =>
|
||||||
(context, animation) =>
|
_renderRow(context, item.rowInfo, animation),
|
||||||
_renderRow(context, item.rowInfo, animation),
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,19 +10,17 @@ import 'dart:math' as math;
|
||||||
class ChoiceChipButton extends StatelessWidget {
|
class ChoiceChipButton extends StatelessWidget {
|
||||||
final FilterInfo filterInfo;
|
final FilterInfo filterInfo;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
final String filterDesc;
|
||||||
|
|
||||||
const ChoiceChipButton({
|
const ChoiceChipButton({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.filterInfo,
|
required this.filterInfo,
|
||||||
|
this.filterDesc = '',
|
||||||
this.onTap,
|
this.onTap,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final arrow = Transform.rotate(
|
|
||||||
angle: -math.pi / 2,
|
|
||||||
child: svgWidget("home/arrow_left"),
|
|
||||||
);
|
|
||||||
final borderSide = BorderSide(
|
final borderSide = BorderSide(
|
||||||
color: AFThemeExtension.of(context).toggleOffFill,
|
color: AFThemeExtension.of(context).toggleOffFill,
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
|
@ -46,10 +44,33 @@ class ChoiceChipButton extends StatelessWidget {
|
||||||
filterInfo.field.fieldType.iconName(),
|
filterInfo.field.fieldType.iconName(),
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
rightIcon: arrow,
|
rightIcon: _ChoicechipFilterDesc(filterDesc: filterDesc),
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ChoicechipFilterDesc extends StatelessWidget {
|
||||||
|
final String filterDesc;
|
||||||
|
const _ChoicechipFilterDesc({this.filterDesc = '', Key? key})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final arrow = Transform.rotate(
|
||||||
|
angle: -math.pi / 2,
|
||||||
|
child: svgWidget("home/arrow_left"),
|
||||||
|
);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (filterDesc.isNotEmpty) FlowyText(': $filterDesc'),
|
||||||
|
arrow,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -26,27 +26,62 @@ class TextFilterChoicechip extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TextFilterChoicechipState extends State<TextFilterChoicechip> {
|
class _TextFilterChoicechipState extends State<TextFilterChoicechip> {
|
||||||
|
late TextFilterEditorBloc bloc;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
bloc = TextFilterEditorBloc(filterInfo: widget.filterInfo)
|
||||||
|
..add(const TextFilterEditorEvent.initial());
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
bloc.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppFlowyPopover(
|
return BlocProvider.value(
|
||||||
controller: PopoverController(),
|
value: bloc,
|
||||||
constraints: BoxConstraints.loose(const Size(200, 76)),
|
child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
|
||||||
direction: PopoverDirection.bottomWithCenterAligned,
|
builder: (blocContext, state) {
|
||||||
popupBuilder: (BuildContext context) {
|
return AppFlowyPopover(
|
||||||
return TextFilterEditor(filterInfo: widget.filterInfo);
|
controller: PopoverController(),
|
||||||
},
|
constraints: BoxConstraints.loose(const Size(200, 76)),
|
||||||
child: ChoiceChipButton(
|
direction: PopoverDirection.bottomWithCenterAligned,
|
||||||
filterInfo: widget.filterInfo,
|
popupBuilder: (BuildContext context) {
|
||||||
onTap: () {},
|
return TextFilterEditor(bloc: bloc);
|
||||||
|
},
|
||||||
|
child: ChoiceChipButton(
|
||||||
|
filterInfo: widget.filterInfo,
|
||||||
|
filterDesc: _makeFilterDesc(state),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _makeFilterDesc(TextFilterEditorState state) {
|
||||||
|
String filterDesc = state.filter.condition.choicechipPrefix;
|
||||||
|
if (state.filter.condition == TextFilterCondition.TextIsEmpty ||
|
||||||
|
state.filter.condition == TextFilterCondition.TextIsNotEmpty) {
|
||||||
|
return filterDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.filter.content.isNotEmpty) {
|
||||||
|
filterDesc += " ${state.filter.content}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterDesc;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TextFilterEditor extends StatefulWidget {
|
class TextFilterEditor extends StatefulWidget {
|
||||||
final FilterInfo filterInfo;
|
final TextFilterEditorBloc bloc;
|
||||||
const TextFilterEditor({required this.filterInfo, Key? key})
|
const TextFilterEditor({required this.bloc, Key? key}) : super(key: key);
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TextFilterEditor> createState() => _TextFilterEditorState();
|
State<TextFilterEditor> createState() => _TextFilterEditorState();
|
||||||
|
@ -57,20 +92,23 @@ class _TextFilterEditorState extends State<TextFilterEditor> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider.value(
|
||||||
create: (context) => TextFilterEditorBloc(filterInfo: widget.filterInfo)
|
value: widget.bloc,
|
||||||
..add(const TextFilterEditorEvent.initial()),
|
|
||||||
child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
|
child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
final List<Widget> children = [
|
||||||
|
_buildFilterPannel(context, state),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (state.filter.condition != TextFilterCondition.TextIsEmpty &&
|
||||||
|
state.filter.condition != TextFilterCondition.TextIsNotEmpty) {
|
||||||
|
children.add(const VSpace(4));
|
||||||
|
children.add(_buildFilterTextField(context, state));
|
||||||
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||||
child: Column(
|
child: IntrinsicHeight(child: Column(children: children)),
|
||||||
children: [
|
|
||||||
_buildFilterPannel(context, state),
|
|
||||||
const VSpace(4),
|
|
||||||
_buildFilterTextField(context, state),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -113,9 +151,8 @@ class _TextFilterEditorState extends State<TextFilterEditor> {
|
||||||
|
|
||||||
Widget _buildFilterTextField(
|
Widget _buildFilterTextField(
|
||||||
BuildContext context, TextFilterEditorState state) {
|
BuildContext context, TextFilterEditorState state) {
|
||||||
final textFilter = state.filterInfo.textFilter()!;
|
|
||||||
return FilterTextField(
|
return FilterTextField(
|
||||||
text: textFilter.content,
|
text: state.filter.content,
|
||||||
hintText: LocaleKeys.grid_settings_typeAValue.tr(),
|
hintText: LocaleKeys.grid_settings_typeAValue.tr(),
|
||||||
autoFucous: false,
|
autoFucous: false,
|
||||||
onSubmitted: (text) {
|
onSubmitted: (text) {
|
||||||
|
@ -209,4 +246,23 @@ extension TextFilterConditionExtension on TextFilterCondition {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get choicechipPrefix {
|
||||||
|
switch (this) {
|
||||||
|
case TextFilterCondition.DoesNotContain:
|
||||||
|
return LocaleKeys.grid_textFilter_choicechipPrefix_isNot.tr();
|
||||||
|
case TextFilterCondition.EndsWith:
|
||||||
|
return LocaleKeys.grid_textFilter_choicechipPrefix_endWith.tr();
|
||||||
|
case TextFilterCondition.IsNot:
|
||||||
|
return LocaleKeys.grid_textFilter_choicechipPrefix_isNot.tr();
|
||||||
|
case TextFilterCondition.StartsWith:
|
||||||
|
return LocaleKeys.grid_textFilter_choicechipPrefix_startWith.tr();
|
||||||
|
case TextFilterCondition.TextIsEmpty:
|
||||||
|
return LocaleKeys.grid_textFilter_choicechipPrefix_isEmpty.tr();
|
||||||
|
case TextFilterCondition.TextIsNotEmpty:
|
||||||
|
return LocaleKeys.grid_textFilter_choicechipPrefix_isNotEmpty.tr();
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,11 +17,13 @@ class GridCreateFilterList extends StatefulWidget {
|
||||||
final String viewId;
|
final String viewId;
|
||||||
final GridFieldController fieldController;
|
final GridFieldController fieldController;
|
||||||
final VoidCallback onClosed;
|
final VoidCallback onClosed;
|
||||||
|
final VoidCallback? onCreateFilter;
|
||||||
|
|
||||||
const GridCreateFilterList({
|
const GridCreateFilterList({
|
||||||
required this.viewId,
|
required this.viewId,
|
||||||
required this.fieldController,
|
required this.fieldController,
|
||||||
required this.onClosed,
|
required this.onClosed,
|
||||||
|
this.onCreateFilter,
|
||||||
Key? key,
|
Key? key,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@ -102,6 +104,7 @@ class _GridCreateFilterListState extends State<GridCreateFilterList> {
|
||||||
|
|
||||||
void createFilter(FieldInfo field) {
|
void createFilter(FieldInfo field) {
|
||||||
editBloc.add(GridCreateFilterEvent.createDefaultFilter(field));
|
editBloc.add(GridCreateFilterEvent.createDefaultFilter(field));
|
||||||
|
widget.onCreateFilter?.call();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import 'create_filter_list.dart';
|
import 'create_filter_list.dart';
|
||||||
import 'filter_info.dart';
|
|
||||||
import 'menu_item.dart';
|
import 'menu_item.dart';
|
||||||
|
|
||||||
class GridFilterMenu extends StatelessWidget {
|
class GridFilterMenu extends StatelessWidget {
|
||||||
|
@ -28,7 +27,7 @@ class GridFilterMenu extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
buildDivider(context),
|
buildDivider(context),
|
||||||
const VSpace(6),
|
const VSpace(6),
|
||||||
buildFilterItems(state.viewId, state.filters),
|
buildFilterItems(state.viewId, state),
|
||||||
],
|
],
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
|
@ -55,8 +54,8 @@ class GridFilterMenu extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildFilterItems(String viewId, List<FilterInfo> filters) {
|
Widget buildFilterItems(String viewId, GridFilterMenuState state) {
|
||||||
final List<Widget> children = filters
|
final List<Widget> children = state.filters
|
||||||
.map((filterInfo) => FilterMenuItem(filterInfo: filterInfo))
|
.map((filterInfo) => FilterMenuItem(filterInfo: filterInfo))
|
||||||
.toList();
|
.toList();
|
||||||
return Row(
|
return Row(
|
||||||
|
@ -70,7 +69,7 @@ class GridFilterMenu extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const HSpace(4),
|
const HSpace(4),
|
||||||
AddFilterButton(viewId: viewId),
|
if (state.creatableFields.isNotEmpty) AddFilterButton(viewId: viewId),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -110,9 +109,7 @@ class _AddFilterButtonState extends State<AddFilterButton> {
|
||||||
"home/add",
|
"home/add",
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () => popoverController.show(),
|
||||||
popoverController.show();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -69,6 +69,11 @@ class _FilterButtonState extends State<FilterButton> {
|
||||||
viewId: bloc.viewId,
|
viewId: bloc.viewId,
|
||||||
fieldController: bloc.fieldController,
|
fieldController: bloc.fieldController,
|
||||||
onClosed: () => _popoverController.close(),
|
onClosed: () => _popoverController.close(),
|
||||||
|
onCreateFilter: () {
|
||||||
|
if (!bloc.state.isVisible) {
|
||||||
|
bloc.add(const GridFilterMenuEvent.toggleMenu());
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -63,8 +63,7 @@ class FlowyButton extends StatelessWidget {
|
||||||
children.add(Expanded(child: text));
|
children.add(Expanded(child: text));
|
||||||
|
|
||||||
if (rightIcon != null) {
|
if (rightIcon != null) {
|
||||||
children.add(
|
children.add(rightIcon!);
|
||||||
SizedBox.fromSize(size: const Size.square(16), child: rightIcon!));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget child = Row(
|
Widget child = Row(
|
||||||
|
|
|
@ -1209,7 +1209,7 @@ packages:
|
||||||
name: textfield_tags
|
name: textfield_tags
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0+1"
|
version: "2.0.2"
|
||||||
textstyle_extensions:
|
textstyle_extensions:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -70,7 +70,7 @@ dependencies:
|
||||||
connectivity_plus: ^2.3.6+1
|
connectivity_plus: ^2.3.6+1
|
||||||
connectivity_plus_platform_interface: ^1.2.2
|
connectivity_plus_platform_interface: ^1.2.2
|
||||||
easy_localization: ^3.0.0
|
easy_localization: ^3.0.0
|
||||||
textfield_tags: ^2.0.0
|
textfield_tags: ^2.0.2
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.2
|
cupertino_icons: ^1.0.2
|
||||||
|
|
|
@ -24,7 +24,7 @@ void main() {
|
||||||
assert(bloc.state.app.name == 'Hello world');
|
assert(bloc.state.app.name == 'Hello world');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('delete ap test', () async {
|
test('delete app test', () async {
|
||||||
final app = await testContext.createTestApp();
|
final app = await testContext.createTestApp();
|
||||||
final bloc = AppBloc(app: app)..add(const AppEvent.initial());
|
final bloc = AppBloc(app: app)..add(const AppEvent.initial());
|
||||||
await blocResponseFuture();
|
await blocResponseFuture();
|
||||||
|
@ -64,9 +64,11 @@ void main() {
|
||||||
await blocResponseFuture();
|
await blocResponseFuture();
|
||||||
bloc.add(AppEvent.createView("3", DocumentPluginBuilder()));
|
bloc.add(AppEvent.createView("3", DocumentPluginBuilder()));
|
||||||
await blocResponseFuture();
|
await blocResponseFuture();
|
||||||
|
assert(bloc.state.views.length == 3);
|
||||||
|
|
||||||
final appViewData = AppViewDataContext(appId: app.id);
|
final appViewData = AppViewDataContext(appId: app.id);
|
||||||
appViewData.views = bloc.state.views;
|
appViewData.views = bloc.state.views;
|
||||||
|
|
||||||
final viewSectionBloc = ViewSectionBloc(
|
final viewSectionBloc = ViewSectionBloc(
|
||||||
appViewData: appViewData,
|
appViewData: appViewData,
|
||||||
)..add(const ViewSectionEvent.initial());
|
)..add(const ViewSectionEvent.initial());
|
||||||
|
|
|
@ -39,8 +39,8 @@ impl DocumentMigration {
|
||||||
}
|
}
|
||||||
|
|
||||||
let document_id = revisions.first().unwrap().object_id.clone();
|
let document_id = revisions.first().unwrap().object_id.clone();
|
||||||
match make_operations_from_revisions(revisions) {
|
if let Ok(delta) = make_operations_from_revisions(revisions) {
|
||||||
Ok(delta) => match DeltaRevisionMigration::run(delta) {
|
match DeltaRevisionMigration::run(delta) {
|
||||||
Ok(transaction) => {
|
Ok(transaction) => {
|
||||||
let bytes = Bytes::from(transaction.to_bytes()?);
|
let bytes = Bytes::from(transaction.to_bytes()?);
|
||||||
let md5 = format!("{:x}", md5::compute(&bytes));
|
let md5 = format!("{:x}", md5::compute(&bytes));
|
||||||
|
@ -59,9 +59,6 @@ impl DocumentMigration {
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("[Document migration]: Make delta from revisions failed: {:?}", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -212,19 +212,18 @@ impl FilterController {
|
||||||
filter_id = new_filter.as_ref().map(|filter| filter.id.clone());
|
filter_id = new_filter.as_ref().map(|filter| filter.id.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the cached filter
|
// Update the corresponding filter in the cache
|
||||||
if let Some(filter_rev) = self.delegate.get_filter_rev(updated_filter_type.new.clone()).await {
|
if let Some(filter_rev) = self.delegate.get_filter_rev(updated_filter_type.new.clone()).await {
|
||||||
let _ = self.cache_filters(vec![filter_rev]).await;
|
let _ = self.cache_filters(vec![filter_rev]).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(filter_id) = filter_id {
|
if let Some(filter_id) = filter_id {
|
||||||
let updated_filter = UpdatedFilter {
|
|
||||||
filter_id,
|
|
||||||
filter: new_filter,
|
|
||||||
};
|
|
||||||
notification = Some(FilterChangesetNotificationPB::from_update(
|
notification = Some(FilterChangesetNotificationPB::from_update(
|
||||||
&self.view_id,
|
&self.view_id,
|
||||||
vec![updated_filter],
|
vec![UpdatedFilter {
|
||||||
|
filter_id,
|
||||||
|
filter: new_filter,
|
||||||
|
}],
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -331,7 +330,7 @@ fn filter_row(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
Some((row_rev.id.clone(), true))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns None if there is no change in this cell after applying the filter
|
// Returns None if there is no change in this cell after applying the filter
|
||||||
|
|
|
@ -99,7 +99,7 @@ pub fn move_group_row(group: &mut Group, context: &mut MoveGroupRowContext) -> O
|
||||||
inserted_row.index = Some(to_index as i32);
|
inserted_row.index = Some(to_index as i32);
|
||||||
group.insert_row(to_index, row_pb);
|
group.insert_row(to_index, row_pb);
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!("Mote to index: {} is out of bounds", to_index);
|
tracing::warn!("Move to index: {} is out of bounds", to_index);
|
||||||
tracing::debug!("Group:{} append row:{}", group.id, row_rev.id);
|
tracing::debug!("Group:{} append row:{}", group.id, row_rev.id);
|
||||||
group.add_row(row_pb);
|
group.add_row(row_pb);
|
||||||
}
|
}
|
||||||
|
|
|
@ -359,6 +359,7 @@ impl GridViewRevisionEditor {
|
||||||
.await
|
.await
|
||||||
.did_receive_filter_changed(FilterChangeset::from_delete(filter_type.clone()))
|
.did_receive_filter_changed(FilterChangeset::from_delete(filter_type.clone()))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let _ = self
|
let _ = self
|
||||||
.modify(|pad| {
|
.modify(|pad| {
|
||||||
let changeset = pad.delete_filter(¶ms.filter_id, &filter_type.field_id, &field_type_rev)?;
|
let changeset = pad.delete_filter(¶ms.filter_id, &filter_type.field_id, &field_type_rev)?;
|
||||||
|
|
|
@ -206,8 +206,8 @@ impl GridFilterTest {
|
||||||
let mut receiver = self.editor.subscribe_view_changed(&self.grid_id).await.unwrap();
|
let mut receiver = self.editor.subscribe_view_changed(&self.grid_id).await.unwrap();
|
||||||
match tokio::time::timeout(Duration::from_secs(2), receiver.recv()).await {
|
match tokio::time::timeout(Duration::from_secs(2), receiver.recv()).await {
|
||||||
Ok(changed) => match changed.unwrap() { GridViewChanged::DidReceiveFilterResult(changed) => {
|
Ok(changed) => match changed.unwrap() { GridViewChanged::DidReceiveFilterResult(changed) => {
|
||||||
assert_eq!(changed.visible_rows.len(), visible_row_len);
|
assert_eq!(changed.visible_rows.len(), visible_row_len, "visible rows not match");
|
||||||
assert_eq!(changed.invisible_rows.len(), hide_row_len);
|
assert_eq!(changed.invisible_rows.len(), hide_row_len, "invisible rows not match");
|
||||||
} },
|
} },
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
panic!("Process task timeout: {:?}", e);
|
panic!("Process task timeout: {:?}", e);
|
||||||
|
|
|
@ -98,20 +98,16 @@ async fn grid_filter_single_select_is_test2() {
|
||||||
row_index: 1,
|
row_index: 1,
|
||||||
option_id: option.id.clone(),
|
option_id: option.id.clone(),
|
||||||
},
|
},
|
||||||
AssertFilterChanged {
|
|
||||||
visible_row_len: 1,
|
|
||||||
hide_row_len: 0,
|
|
||||||
},
|
|
||||||
AssertNumberOfVisibleRows { expected: 3 },
|
AssertNumberOfVisibleRows { expected: 3 },
|
||||||
UpdateSingleSelectCell {
|
UpdateSingleSelectCell {
|
||||||
row_index: 1,
|
row_index: 1,
|
||||||
option_id: "".to_string(),
|
option_id: "".to_string(),
|
||||||
},
|
},
|
||||||
// AssertFilterChanged {
|
AssertFilterChanged {
|
||||||
// visible_row_len: 0,
|
visible_row_len: 0,
|
||||||
// hide_row_len: 1,
|
hide_row_len: 1,
|
||||||
// },
|
},
|
||||||
// AssertNumberOfVisibleRows { expected: 2 },
|
AssertNumberOfVisibleRows { expected: 2 },
|
||||||
];
|
];
|
||||||
test.run_scripts(scripts).await;
|
test.run_scripts(scripts).await;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue