diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/select_option_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/select_option_bd_svc.ts
index f5a15669e3..068ae60797 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/select_option_bd_svc.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/select_option_bd_svc.ts
@@ -51,19 +51,19 @@ export class SelectOptionCellBackendService {
const payload = RepeatedSelectOptionPayload.fromObject({
view_id: this.cellIdentifier.viewId,
field_id: this.cellIdentifier.fieldId,
- row_id: this.cellIdentifier.rowId,
+ items: [option],
});
- payload.items.push(option);
+
return DatabaseEventInsertOrUpdateSelectOption(payload);
};
- updateOption = (option: SelectOptionPB) => {
+ updateOption = async (option: SelectOptionPB) => {
const payload = RepeatedSelectOptionPayload.fromObject({
view_id: this.cellIdentifier.viewId,
field_id: this.cellIdentifier.fieldId,
- row_id: this.cellIdentifier.rowId,
+ items: [option],
});
- payload.items.push(option);
+
return DatabaseEventInsertOrUpdateSelectOption(payload);
};
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts
index e90e4fac82..7a03c82beb 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_bd_svc.ts
@@ -11,13 +11,17 @@ import {
DatabaseEventMoveGroup,
DatabaseEventMoveGroupRow,
DatabaseEventMoveRow,
+ DatabaseEventUpdateField,
DatabaseGroupIdPB,
+ FieldChangesetPB,
MoveFieldPayloadPB,
MoveGroupPayloadPB,
MoveGroupRowPayloadPB,
MoveRowPayloadPB,
RowIdPB,
DatabaseEventUpdateDatabaseSetting,
+ DuplicateFieldPayloadPB,
+ DatabaseEventDuplicateField,
} from '@/services/backend/events/flowy-database2';
import {
GetFieldPayloadPB,
@@ -28,6 +32,8 @@ import {
ViewIdPB,
} from '@/services/backend';
import { FolderEventCloseView } from '@/services/backend/events/flowy-folder2';
+import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
+import { None } from 'ts-results';
/// A service that wraps the backend service
export class DatabaseBackendService {
@@ -84,6 +90,11 @@ export class DatabaseBackendService {
return DatabaseEventDeleteRow(payload);
};
+ moveRow = async (fromRowId: string, toRowId: string) => {
+ const payload = MoveRowPayloadPB.fromObject({ view_id: this.viewId, from_row_id: fromRowId, to_row_id: toRowId });
+ return DatabaseEventMoveRow(payload);
+ };
+
/// Move the row from one group to another group
/// [toRowId] is used to locate the moving row location.
moveGroupRow = (fromRowId: string, toGroupId: string, toRowId?: string) => {
@@ -139,6 +150,24 @@ export class DatabaseBackendService {
return DatabaseEventMoveField(payload);
};
+ changeWidth = (params: { fieldId: string; width: number }) => {
+ const payload = FieldChangesetPB.fromObject({ view_id: this.viewId, field_id: params.fieldId, width: params.width });
+
+ return DatabaseEventUpdateField(payload);
+ };
+
+ duplicateField = (fieldId: string) => {
+ const payload = DuplicateFieldPayloadPB.fromObject({ view_id: this.viewId, field_id: fieldId });
+
+ return DatabaseEventDuplicateField(payload);
+ };
+
+ createField = async () => {
+ const fieldController = new TypeOptionController(this.viewId, None);
+
+ await fieldController.initialize();
+ };
+
/// Get all groups in database
/// It should only call once after the board open
loadGroups = () => {
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts
index dd98b41747..f4e94f4790 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/database_controller.ts
@@ -8,11 +8,17 @@ import { DatabaseGroupController } from './group/group_controller';
import { BehaviorSubject } from 'rxjs';
import { DatabaseGroupObserver } from './group/group_observer';
import { Log } from '$app/utils/log';
+import { FilterController } from '$app/stores/effects/database/filter/filter_controller';
+import { FilterParsed } from '$app/stores/effects/database/filter/filter_bd_svc';
+import { SortController } from '$app/stores/effects/database/sort/sort_controller';
+import { IDatabaseSort } from '$app_reducers/database/slice';
export type DatabaseSubscriberCallbacks = {
onViewChanged?: (data: DatabasePB) => void;
onRowsChanged?: (rowInfos: readonly RowInfo[], reason: RowChangedReason) => void;
onFieldsChanged?: (fieldInfos: readonly FieldInfo[]) => void;
+ onFiltersChanged?: (filters: readonly FilterParsed[]) => void;
+ onSortChanged?: (sorts: readonly IDatabaseSort[]) => void;
onGroupByField?: (groups: GroupPB[]) => void;
onNumOfGroupChanged?: {
@@ -25,6 +31,8 @@ export type DatabaseSubscriberCallbacks = {
export class DatabaseController {
private readonly backendService: DatabaseBackendService;
fieldController: FieldController;
+ sortController: SortController;
+ filterController: FilterController;
databaseViewCache: DatabaseViewCache;
private _callback?: DatabaseSubscriberCallbacks;
public groups: BehaviorSubject
;
@@ -33,6 +41,8 @@ export class DatabaseController {
constructor(public readonly viewId: string) {
this.backendService = new DatabaseBackendService(viewId);
this.fieldController = new FieldController(viewId);
+ this.filterController = new FilterController(viewId);
+ this.sortController = new SortController(viewId);
this.databaseViewCache = new DatabaseViewCache(viewId, this.fieldController);
this.groups = new BehaviorSubject([]);
this.groupsObserver = new DatabaseGroupObserver(viewId);
@@ -41,6 +51,8 @@ export class DatabaseController {
subscribe = (callbacks: DatabaseSubscriberCallbacks) => {
this._callback = callbacks;
this.fieldController.subscribe({ onNumOfFieldsChanged: callbacks.onFieldsChanged });
+ this.filterController.subscribe({ onFiltersChanged: callbacks.onFiltersChanged });
+ this.sortController.subscribe({ onSortChanged: callbacks.onSortChanged });
this.databaseViewCache.getRowCache().subscribe({
onRowsChanged: (reason) => {
this._callback?.onRowsChanged?.(this.databaseViewCache.rowInfos, reason);
@@ -50,10 +62,14 @@ export class DatabaseController {
open = async () => {
const openDatabaseResult = await this.backendService.openDatabase();
+
if (openDatabaseResult.ok) {
const database: DatabasePB = openDatabaseResult.val;
+
await this.databaseViewCache.initialize();
await this.fieldController.initialize();
+ await this.filterController.initialize();
+ await this.sortController.initialize();
// subscriptions
await this.subscribeOnGroupsChanged();
@@ -73,12 +89,15 @@ export class DatabaseController {
getGroupByFieldId = async () => {
const settingsResult = await this.backendService.getSettings();
+
if (settingsResult.ok) {
const settings = settingsResult.val;
const groupConfig = settings.group_settings.items;
+
if (groupConfig.length === 0) {
return Err(new FlowyError({ msg: 'this database has no groups' }));
}
+
return Ok(settings.group_settings.items[0].field_id);
} else {
return Err(settingsResult.val);
@@ -89,6 +108,10 @@ export class DatabaseController {
return this.backendService.createRow();
};
+ createRowAfter = (rowId: string) => {
+ return this.backendService.createRow({ rowId });
+ };
+
duplicateRow = async (rowId: string) => {
return this.backendService.duplicateRow(rowId);
};
@@ -97,6 +120,10 @@ export class DatabaseController {
return this.backendService.deleteRow(rowId);
};
+ moveRow = (fromRowId: string, toRowId: string) => {
+ return this.backendService.moveRow(fromRowId, toRowId);
+ };
+
moveGroupRow = (rowId: string, groupId: string) => {
return this.backendService.moveGroupRow(rowId, groupId);
};
@@ -114,12 +141,51 @@ export class DatabaseController {
return this.backendService.moveField(params);
};
+ changeWidth = (params: { fieldId: string; width: number }) => {
+ return this.backendService.changeWidth(params);
+ };
+
+ duplicateField = (fieldId: string) => {
+ return this.backendService.duplicateField(fieldId);
+ };
+
+ addFieldToLeft = async (fieldId: string) => {
+ const index = this.fieldController.fieldInfos.findIndex((fieldInfo) => fieldInfo.field.id === fieldId);
+
+ await this.backendService.createField();
+
+ const newFieldId = this.fieldController.fieldInfos[this.fieldController.fieldInfos.length - 1].field.id;
+
+ await this.moveField({
+ fieldId: newFieldId,
+ fromIndex: this.fieldController.fieldInfos.length - 1,
+ toIndex: index,
+ });
+ };
+
+ addFieldToRight = async (fieldId: string) => {
+ const index = this.fieldController.fieldInfos.findIndex((fieldInfo) => fieldInfo.field.id === fieldId);
+
+ await this.backendService.createField();
+
+ const newFieldId = this.fieldController.fieldInfos[this.fieldController.fieldInfos.length - 1].field.id;
+
+ await this.moveField({
+ fieldId: newFieldId,
+ fromIndex: this.fieldController.fieldInfos.length - 1,
+ toIndex: index + 1,
+ });
+ };
+
private loadGroup = async () => {
const result = await this.backendService.loadGroups();
+
if (result.ok) {
const groups = result.val.items;
+
await this.initialGroups(groups);
}
+
return result;
};
@@ -129,11 +195,14 @@ export class DatabaseController {
});
const controllers: DatabaseGroupController[] = [];
+
for (const groupPB of groups) {
const controller = new DatabaseGroupController(groupPB, this.backendService);
+
await controller.initialize();
controllers.push(controller);
}
+
this.groups.next(controllers);
this.groups.value;
};
@@ -150,14 +219,17 @@ export class DatabaseController {
Log.error(result.val);
return;
}
+
const changeset = result.val;
let existControllers = [...this.groups.getValue()];
+
for (const deleteId of changeset.deleted_groups) {
existControllers = existControllers.filter((c) => c.groupId !== deleteId);
}
for (const update of changeset.update_groups) {
const index = existControllers.findIndex((c) => c.groupId === update.group_id);
+
if (index !== -1) {
existControllers[index].updateGroup(update);
}
@@ -165,12 +237,14 @@ export class DatabaseController {
for (const insert of changeset.inserted_groups) {
const controller = new DatabaseGroupController(insert.group, this.backendService);
+
if (insert.index > existControllers.length) {
existControllers.push(controller);
} else {
existControllers.splice(insert.index, 0, controller);
}
}
+
this.groups.next(existControllers);
},
});
@@ -183,6 +257,7 @@ export class DatabaseController {
await this.groupsObserver.unsubscribe();
await this.backendService.closeDatabase();
await this.fieldController.dispose();
+ this.filterController.dispose();
await this.databaseViewCache.dispose();
};
}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_controller.ts
index b44b6fa12f..a13196a236 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_controller.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/field/type_option/type_option_controller.ts
@@ -89,6 +89,14 @@ export class TypeOptionController {
}
};
+ changeWidth = async (width: number) => {
+ if (this.fieldBackendSvc) {
+ void this.fieldBackendSvc.updateField({ width: width });
+ } else {
+ throw Error('Unexpected empty field backend service');
+ }
+ };
+
saveTypeOption = async (data: Uint8Array) => {
if (this.typeOptionData.some) {
this.typeOptionData.val.type_option_data = data;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/filter/filter_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/filter/filter_bd_svc.ts
new file mode 100644
index 0000000000..3fa6982933
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/filter/filter_bd_svc.ts
@@ -0,0 +1,127 @@
+import {
+ CheckboxFilterPB,
+ DatabaseSettingChangesetPB,
+ DatabaseViewIdPB,
+ DeleteFilterPayloadPB,
+ FieldType,
+ FilterPB,
+ FlowyError,
+ SelectOptionFilterPB,
+ TextFilterPB,
+ UpdateFilterPayloadPB,
+} from '@/services/backend';
+import {
+ DatabaseEventGetAllFilters,
+ DatabaseEventUpdateDatabaseSetting,
+} from '@/services/backend/events/flowy-database2';
+import { Err, Ok, Result } from 'ts-results';
+import { nanoid } from 'nanoid';
+
+export class FilterBackendService {
+ constructor(public readonly viewId: string) {}
+
+ getFilters = async (): Promise> => {
+ const payload = DatabaseViewIdPB.fromObject({
+ value: this.viewId,
+ });
+
+ const res = await DatabaseEventGetAllFilters(payload);
+
+ if (res.ok) {
+ return Ok(res.val.items.map((f) => new FilterParsed(this.viewId, f)));
+ } else {
+ return Err(res.val);
+ }
+ };
+
+ addFilter = async (
+ fieldId: string,
+ fieldType: FieldType,
+ filter: TextFilterPB | SelectOptionFilterPB | CheckboxFilterPB
+ ) => {
+ const data = filter.serializeBinary();
+ const id = nanoid(4);
+
+ await DatabaseEventUpdateDatabaseSetting(
+ DatabaseSettingChangesetPB.fromObject({
+ view_id: this.viewId,
+ update_filter: UpdateFilterPayloadPB.fromObject({
+ filter_id: id,
+ view_id: this.viewId,
+ field_id: fieldId,
+ field_type: fieldType,
+ data,
+ }),
+ })
+ );
+ return id;
+ };
+
+ updateFilter = (
+ filterId: string,
+ fieldId: string,
+ fieldType: FieldType,
+ filter: TextFilterPB | SelectOptionFilterPB | CheckboxFilterPB
+ ) => {
+ const data = filter.serializeBinary();
+
+ return DatabaseEventUpdateDatabaseSetting(
+ DatabaseSettingChangesetPB.fromObject({
+ view_id: this.viewId,
+ update_filter: UpdateFilterPayloadPB.fromObject({
+ view_id: this.viewId,
+ field_id: fieldId,
+ field_type: fieldType,
+ filter_id: filterId,
+ data,
+ }),
+ })
+ );
+ };
+
+ removeFilter = (fieldId: string, fieldType: FieldType, filterId: string) => {
+ return DatabaseEventUpdateDatabaseSetting(
+ DatabaseSettingChangesetPB.fromObject({
+ view_id: this.viewId,
+ delete_filter: DeleteFilterPayloadPB.fromObject({
+ view_id: this.viewId,
+ field_id: fieldId,
+ field_type: fieldType,
+ filter_id: filterId,
+ }),
+ })
+ );
+ };
+}
+
+export class FilterParsed {
+ view_id: string;
+ id: string;
+ field_id: string;
+ field_type: FieldType;
+ data: TextFilterPB | SelectOptionFilterPB | CheckboxFilterPB | Uint8Array;
+
+ constructor(view_id: string, filter: FilterPB) {
+ this.view_id = view_id;
+
+ this.id = filter.id;
+ this.field_id = filter.field_id;
+ this.field_type = filter.field_type;
+
+ switch (filter.field_type) {
+ case FieldType.RichText:
+ this.data = TextFilterPB.deserializeBinary(filter.data);
+ break;
+ case FieldType.SingleSelect:
+ case FieldType.MultiSelect:
+ this.data = SelectOptionFilterPB.deserializeBinary(filter.data);
+ break;
+ case FieldType.Checkbox:
+ this.data = CheckboxFilterPB.deserializeBinary(filter.data);
+ break;
+ default:
+ this.data = filter.data;
+ break;
+ }
+ }
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/filter/filter_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/filter/filter_controller.ts
new file mode 100644
index 0000000000..b56d1e8a58
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/filter/filter_controller.ts
@@ -0,0 +1,80 @@
+import { FilterBackendService, FilterParsed } from '$app/stores/effects/database/filter/filter_bd_svc';
+import { CheckboxFilterPB, FieldType, SelectOptionFilterPB, TextFilterPB } from '@/services/backend';
+import { ChangeNotifier } from '$app/utils/change_notifier';
+
+export class FilterController {
+ filterService: FilterBackendService;
+ notifier: FilterNotifier;
+
+ constructor(public readonly viewId: string) {
+ this.filterService = new FilterBackendService(viewId);
+ this.notifier = new FilterNotifier();
+ }
+
+ initialize = async () => {
+ await this.readFilters();
+ };
+
+ readFilters = async () => {
+ const result = await this.filterService.getFilters();
+
+ if (result.ok) {
+ this.notifier.filters = result.val;
+ }
+ };
+
+ addFilter = async (
+ fieldId: string,
+ fieldType: FieldType,
+ filter: TextFilterPB | SelectOptionFilterPB | CheckboxFilterPB
+ ) => {
+ const id = await this.filterService.addFilter(fieldId, fieldType, filter);
+
+ await this.readFilters();
+ return id;
+ };
+
+ updateFilter = async (
+ filterId: string,
+ fieldId: string,
+ fieldType: FieldType,
+ filter: TextFilterPB | SelectOptionFilterPB | CheckboxFilterPB
+ ) => {
+ const result = await this.filterService.updateFilter(filterId, fieldId, fieldType, filter);
+
+ if (result.ok) {
+ await this.readFilters();
+ }
+ };
+
+ removeFilter = async (fieldId: string, fieldType: FieldType, filterId: string) => {
+ const result = await this.filterService.removeFilter(fieldId, fieldType, filterId);
+
+ if (result.ok) {
+ await this.readFilters();
+ }
+ };
+
+ subscribe = (callbacks: { onFiltersChanged?: (filters: FilterParsed[]) => void }) => {
+ if (callbacks.onFiltersChanged) {
+ this.notifier.observer?.subscribe(callbacks.onFiltersChanged);
+ }
+ };
+
+ dispose = () => {
+ this.notifier.unsubscribe();
+ };
+}
+
+class FilterNotifier extends ChangeNotifier {
+ private _filters: FilterParsed[] = [];
+
+ get filters(): FilterParsed[] {
+ return this._filters;
+ }
+
+ set filters(value: FilterParsed[]) {
+ this._filters = value;
+ this.notify(value);
+ }
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/sort/sort_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/sort/sort_bd_svc.ts
new file mode 100644
index 0000000000..1eabf080d2
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/sort/sort_bd_svc.ts
@@ -0,0 +1,85 @@
+import {
+ DatabaseSettingChangesetPB,
+ DatabaseViewIdPB,
+ DeleteSortPayloadPB,
+ FieldType,
+ FlowyError,
+ SortConditionPB,
+ UpdateSortPayloadPB,
+} from '@/services/backend';
+import { DatabaseEventGetAllSorts, DatabaseEventUpdateDatabaseSetting } from '@/services/backend/events/flowy-database2';
+import { Err, Ok, Result } from 'ts-results';
+import { nanoid } from 'nanoid';
+import type { IDatabaseSort } from '$app_reducers/database/slice';
+
+export class SortBackendService {
+ constructor(public readonly viewId: string) {}
+
+ getSorts = async (): Promise> => {
+ const payload = DatabaseViewIdPB.fromObject({
+ value: this.viewId,
+ });
+
+ const res = await DatabaseEventGetAllSorts(payload);
+
+ if (res.ok) {
+ return Ok(
+ res.val.items.map((o) => ({
+ id: o.id,
+ fieldId: o.field_id,
+ fieldType: o.field_type,
+ order: o.condition,
+ }))
+ );
+ } else {
+ return Err(res.val);
+ }
+ };
+
+ addSort = async (fieldId: string, fieldType: FieldType, order: SortConditionPB) => {
+ const id = nanoid(4);
+
+ await DatabaseEventUpdateDatabaseSetting(
+ DatabaseSettingChangesetPB.fromObject({
+ view_id: this.viewId,
+ update_sort: UpdateSortPayloadPB.fromObject({
+ view_id: this.viewId,
+ field_id: fieldId,
+ field_type: fieldType,
+ sort_id: id,
+ condition: order,
+ }),
+ })
+ );
+ return id;
+ };
+
+ updateSort = (sortId: string, fieldId: string, fieldType: FieldType, order: SortConditionPB) => {
+ return DatabaseEventUpdateDatabaseSetting(
+ DatabaseSettingChangesetPB.fromObject({
+ view_id: this.viewId,
+ update_sort: UpdateSortPayloadPB.fromObject({
+ view_id: this.viewId,
+ field_id: fieldId,
+ field_type: fieldType,
+ sort_id: sortId,
+ condition: order,
+ }),
+ })
+ );
+ };
+
+ removeSort = (fieldId: string, fieldType: FieldType, sortId: string) => {
+ return DatabaseEventUpdateDatabaseSetting(
+ DatabaseSettingChangesetPB.fromObject({
+ view_id: this.viewId,
+ delete_sort: DeleteSortPayloadPB.fromObject({
+ view_id: this.viewId,
+ field_id: fieldId,
+ field_type: fieldType,
+ sort_id: sortId,
+ }),
+ })
+ );
+ };
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/sort/sort_controller.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/sort/sort_controller.ts
new file mode 100644
index 0000000000..2bf7dcde90
--- /dev/null
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/sort/sort_controller.ts
@@ -0,0 +1,72 @@
+import { FieldType, SortConditionPB } from '@/services/backend';
+import { ChangeNotifier } from '$app/utils/change_notifier';
+import { IDatabaseSort } from '$app_reducers/database/slice';
+import { SortBackendService } from '$app/stores/effects/database/sort/sort_bd_svc';
+
+export class SortController {
+ sortService: SortBackendService;
+ notifier: SortNotifier;
+
+ constructor(public readonly viewId: string) {
+ this.sortService = new SortBackendService(viewId);
+ this.notifier = new SortNotifier();
+ }
+
+ initialize = async () => {
+ await this.readSorts();
+ };
+
+ readSorts = async () => {
+ const result = await this.sortService.getSorts();
+
+ if (result.ok) {
+ this.notifier.sorts = result.val;
+ }
+ };
+
+ addSort = async (fieldId: string, fieldType: FieldType, sort: SortConditionPB) => {
+ const id = await this.sortService.addSort(fieldId, fieldType, sort);
+
+ await this.readSorts();
+ return id;
+ };
+
+ updateSort = async (sortId: string, fieldId: string, fieldType: FieldType, sort: SortConditionPB) => {
+ const result = await this.sortService.updateSort(sortId, fieldId, fieldType, sort);
+
+ if (result.ok) {
+ await this.readSorts();
+ }
+ };
+
+ removeSort = async (fieldId: string, fieldType: FieldType, sortId: string) => {
+ const result = await this.sortService.removeSort(fieldId, fieldType, sortId);
+
+ if (result.ok) {
+ await this.readSorts();
+ }
+ };
+
+ subscribe = (callbacks: { onSortChanged?: (sorts: IDatabaseSort[]) => void }) => {
+ if (callbacks.onSortChanged) {
+ this.notifier.observer?.subscribe(callbacks.onSortChanged);
+ }
+ };
+
+ dispose = () => {
+ this.notifier.unsubscribe();
+ };
+}
+
+class SortNotifier extends ChangeNotifier {
+ private _sorts: IDatabaseSort[] = [];
+
+ get sorts(): IDatabaseSort[] {
+ return this._sorts;
+ }
+
+ set sorts(value: IDatabaseSort[]) {
+ this._sorts = value;
+ this.notify(value);
+ }
+}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/database/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/database/slice.ts
index f3592eabce..26525d23b0 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/database/slice.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/database/slice.ts
@@ -1,11 +1,11 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { FieldType } from '@/services/backend/models/flowy-database2/field_entities';
-import { DateFormatPB, NumberFormatPB, SelectOptionColorPB, TimeFormatPB } from '@/services/backend';
+import { DateFormatPB, NumberFormatPB, SelectOptionColorPB, SortConditionPB, TimeFormatPB } from '@/services/backend';
export interface ISelectOption {
selectOptionId: string;
title: string;
- color?: SelectOptionColorPB;
+ color: SelectOptionColorPB;
}
export interface ISelectOptionType {
@@ -26,6 +26,7 @@ export interface IDatabaseField {
fieldId: string;
title: string;
visible: boolean;
+ width: number;
fieldType: FieldType;
fieldOptions?: ISelectOptionType | IDateType | INumberType;
}
@@ -42,11 +43,51 @@ export interface IDatabaseRow {
export type DatabaseFieldMap = { [keys: string]: IDatabaseField };
+export type TDatabaseOperators =
+ | 'contains'
+ | 'doesNotContain'
+ | 'endsWith'
+ | 'startWith'
+ | 'is'
+ | 'isNot'
+ | 'isEmpty'
+ | 'isNotEmpty'
+ | 'isComplete'
+ | 'isIncomplted';
+
+export type TSupportedOperatorsByType = { [keys: number]: TDatabaseOperators[] };
+
+export const SupportedOperatorsByType: TSupportedOperatorsByType = {
+ [FieldType.RichText]: ['contains', 'doesNotContain', 'endsWith', 'startWith', 'is', 'isNot', 'isEmpty', 'isNotEmpty'],
+ [FieldType.SingleSelect]: ['is', 'isNot', 'isEmpty', 'isNotEmpty'],
+ [FieldType.MultiSelect]: ['contains', 'doesNotContain', 'isEmpty', 'isNotEmpty'],
+ [FieldType.Checkbox]: ['is'],
+ [FieldType.Checklist]: ['isComplete', 'isIncomplted'],
+};
+
+export interface IDatabaseFilter {
+ id?: string;
+ fieldId: string;
+ fieldType: FieldType;
+ logicalOperator: 'and' | 'or';
+ operator: TDatabaseOperators;
+ value: string[] | string | boolean;
+}
+
+export interface IDatabaseSort {
+ id?: string;
+ fieldId: string;
+ fieldType: FieldType;
+ order: SortConditionPB;
+}
+
export interface IDatabase {
title: string;
fields: DatabaseFieldMap;
rows: IDatabaseRow[];
columns: IDatabaseColumn[];
+ filters: IDatabaseFilter[];
+ sort: IDatabaseSort[];
}
const initialState: IDatabase = {
@@ -54,6 +95,8 @@ const initialState: IDatabase = {
columns: [],
fields: {},
rows: [],
+ filters: [],
+ sort: [],
};
export const databaseSlice = createSlice({
@@ -89,87 +132,25 @@ export const databaseSlice = createSlice({
state.title = action.payload.title;
},
- /*addField: (state, action: PayloadAction<{ field: IDatabaseField }>) => {
- const { field } = action.payload;
-
- state.fields[field.fieldId] = field;
- state.columns.push({
- fieldId: field.fieldId,
- sort: 'none',
- visible: true,
- });
- state.rows = state.rows.map((r: IDatabaseRow) => {
- const cells = r.cells;
- cells[field.fieldId] = {
- rowId: r.rowId,
- fieldId: field.fieldId,
- data: [''],
- cellId: nanoid(6),
- };
- return {
- rowId: r.rowId,
- cells: cells,
- };
- });
- },*/
-
updateField: (state, action: PayloadAction<{ field: IDatabaseField }>) => {
const { field } = action.payload;
+
state.fields[field.fieldId] = field;
},
- /*addFieldSelectOption: (state, action: PayloadAction<{ fieldId: string; option: ISelectOption }>) => {
- const { fieldId, option } = action.payload;
+ changeWidth: (state, action: PayloadAction<{ fieldId: string; width: number }>) => {
+ const { fieldId, width } = action.payload;
- const field = state.fields[fieldId];
- const selectOptions = field.fieldOptions?.selectOptions;
+ state.fields[fieldId].width = width;
+ },
- if (selectOptions) {
- selectOptions.push(option);
- } else {
- state.fields[field.fieldId].fieldOptions = {
- ...state.fields[field.fieldId].fieldOptions,
- selectOptions: [option],
- };
- }
- },*/
+ updateFilters: (state, action: PayloadAction<{ filters: IDatabaseFilter[] }>) => {
+ state.filters = action.payload.filters;
+ },
- /*updateFieldSelectOption: (state, action: PayloadAction<{ fieldId: string; option: ISelectOption }>) => {
- const { fieldId, option } = action.payload;
-
- const field = state.fields[fieldId];
- const selectOptions = field.fieldOptions?.selectOptions;
- if (selectOptions) {
- selectOptions[selectOptions.findIndex((o) => o.selectOptionId === option.selectOptionId)] = option;
- }
- },*/
-
- /*addRow: (state) => {
- const rowId = nanoid(6);
- const cells: { [keys: string]: ICellData } = {};
- Object.keys(state.fields).forEach((id) => {
- cells[id] = {
- rowId: rowId,
- fieldId: id,
- data: [''],
- cellId: nanoid(6),
- };
- });
- const newRow: IDatabaseRow = {
- rowId: rowId,
- cells: cells,
- };
-
- state.rows.push(newRow);
- },*/
-
- /*updateCellValue: (source, action: PayloadAction<{ cell: ICellData }>) => {
- const { cell } = action.payload;
- const row = source.rows.find((r) => r.rowId === cell.rowId);
- if (row) {
- row.cells[cell.fieldId] = cell;
- }
- },*/
+ updateSorts: (state, action: PayloadAction<{ sorts: IDatabaseSort[] }>) => {
+ state.sort = action.payload.sorts;
+ },
},
});
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/grid/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/grid/slice.ts
deleted file mode 100644
index 2b3e72d768..0000000000
--- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/grid/slice.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-import { createSlice, PayloadAction } from '@reduxjs/toolkit';
-import { nanoid } from 'nanoid';
-import { FieldType } from '@/services/backend/models/flowy-database2/field_entities';
-
-const initialState = {
- title: 'My plans on the week',
- fields: [
- {
- fieldId: '1',
- fieldType: FieldType.RichText,
- fieldOptions: {},
- name: 'Todo',
- },
- {
- fieldId: '2',
- fieldType: FieldType.SingleSelect,
- fieldOptions: [],
- name: 'Status',
- },
- {
- fieldId: '3',
- fieldType: FieldType.Number,
- fieldOptions: [],
- name: 'Progress',
- },
- {
- fieldId: '4',
- fieldType: FieldType.DateTime,
- fieldOptions: [],
- name: 'Due Date',
- },
- ],
- rows: [
- {
- rowId: '1',
- values: [
- {
- fieldId: '1',
- value: 'Name 1',
- cellId: '1',
- },
- {
- fieldId: '2',
- value: 'Status 1',
- cellId: '2',
- },
- {
- fieldId: '3',
- value: 30,
- cellId: '3',
- },
- {
- fieldId: '4',
- value: 'tomorrow',
- cellId: '4',
- },
- ],
- },
- {
- rowId: '2',
- values: [
- {
- fieldId: '1',
- value: 'Name 2',
- cellId: '5',
- },
- {
- fieldId: '2',
- value: 'Status 2',
- cellId: '6',
- },
- {
- fieldId: '3',
- value: 40,
- cellId: '7',
- },
- {
- fieldId: '4',
- value: 'tomorrow',
- cellId: '8',
- },
- ],
- },
- ],
-};
-
-export type field = {
- fieldId: string;
- fieldType: FieldType;
- fieldOptions: any;
- name: string;
-};
-
-export const gridSlice = createSlice({
- name: 'grid',
- initialState: initialState,
- reducers: {
- updateGridTitle: (state, action: PayloadAction<{ title: string }>) => {
- state.title = action.payload.title;
- },
-
- addField: (state, action: PayloadAction<{ field: field }>) => {
- state.fields.push(action.payload.field);
-
- state.rows.map((row) => {
- row.values.push({
- fieldId: action.payload.field.fieldId,
- value: '',
- cellId: nanoid(),
- });
- });
- },
-
- addRow: (state) => {
- const newRow = {
- rowId: nanoid(),
- values: state.fields.map((f) => ({
- fieldId: f.fieldId,
- value: '',
- cellId: nanoid(),
- })),
- };
-
- state.rows.push(newRow);
- },
-
- updateRowValue: (state, action: PayloadAction<{ rowId: string; cellId: string; value: string | number }>) => {
- console.log('updateRowValue', action.payload);
- const row = state.rows.find((r) => r.rowId === action.payload.rowId);
-
- if (row) {
- const cell = row.values.find((c) => c.cellId === action.payload.cellId);
- if (cell) {
- cell.value = action.payload.value;
- }
- }
- },
- },
-});
-
-export const gridActions = gridSlice.actions;
diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts
index 9a25c43099..1c8660c7d6 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts
+++ b/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts
@@ -9,7 +9,6 @@ import {
} from '@reduxjs/toolkit';
import { pagesSlice } from './reducers/pages/slice';
import { currentUserSlice } from './reducers/current-user/slice';
-import { gridSlice } from './reducers/grid/slice';
import { workspaceSlice } from './reducers/workspace/slice';
import { databaseSlice } from './reducers/database/slice';
import { documentReducers } from './reducers/document/slice';
@@ -27,7 +26,6 @@ const store = configureStore({
reducer: {
[pagesSlice.name]: pagesSlice.reducer,
[currentUserSlice.name]: currentUserSlice.reducer,
- [gridSlice.name]: gridSlice.reducer,
[databaseSlice.name]: databaseSlice.reducer,
[boardSlice.name]: boardSlice.reducer,
[workspaceSlice.name]: workspaceSlice.reducer,
diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/GridPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/GridPage.tsx
index b4a0f4157a..9d8d910853 100644
--- a/frontend/appflowy_tauri/src/appflowy_app/views/GridPage.tsx
+++ b/frontend/appflowy_tauri/src/appflowy_app/views/GridPage.tsx
@@ -6,16 +6,15 @@ import { Grid } from '../components/grid/Grid/Grid';
export const GridPage = () => {
const params = useParams();
const [viewId, setViewId] = useState('');
+
useEffect(() => {
if (params?.id?.length) {
setViewId(params.id);
- // setDatabaseId('testDb');
}
}, [params]);
return (
-
Grid: {viewId}
{viewId?.length && }
);
diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json
index 7942ff1726..d718fef6e1 100644
--- a/frontend/resources/translations/en.json
+++ b/frontend/resources/translations/en.json
@@ -403,7 +403,8 @@
"addOption": "Add option",
"editProperty": "Edit property",
"newProperty": "New property",
- "deleteFieldPromptMessage": "Are you sure? This property will be deleted"
+ "deleteFieldPromptMessage": "Are you sure? This property will be deleted",
+ "newColumn": "New Column"
},
"sort": {
"ascending": "Ascending",