|
@@ -0,0 +1,314 @@
|
|
|
+import { RowPB, InsertedRowPB, UpdatedRowPB } from '../../../../../services/backend/models/flowy-database/row_entities';
|
|
|
+import { ChangeNotifier } from '../../../../utils/change_notifier';
|
|
|
+import { FieldInfo } from '../field/controller';
|
|
|
+import { CellCache, CellCacheKey } from '../cell/cache';
|
|
|
+import {
|
|
|
+ ViewRowsChangesetPB,
|
|
|
+ ViewRowsVisibilityChangesetPB,
|
|
|
+} from '../../../../../services/backend/models/flowy-database/view_entities';
|
|
|
+import { CellIdentifier } from '../cell/backend_service';
|
|
|
+import { ReorderSingleRowPB } from '../../../../../services/backend/models/flowy-database/sort_entities';
|
|
|
+
|
|
|
+export class RowCache {
|
|
|
+ _rowList: RowList;
|
|
|
+ _cellCache: CellCache;
|
|
|
+ _notifier: RowChangeNotifier;
|
|
|
+
|
|
|
+ constructor(public readonly viewId: string, private readonly getFieldInfos: () => readonly FieldInfo[]) {
|
|
|
+ this._rowList = new RowList();
|
|
|
+ this._cellCache = new CellCache(viewId);
|
|
|
+ this._notifier = new RowChangeNotifier();
|
|
|
+ }
|
|
|
+
|
|
|
+ get rows(): readonly RowInfo[] {
|
|
|
+ return this._rowList.rows;
|
|
|
+ }
|
|
|
+
|
|
|
+ subscribeOnRowsChanged = (callback: (reason: RowChangedReason, cellMap?: Map<string, CellIdentifier>) => void) => {
|
|
|
+ return this._notifier.observer.subscribe((change) => {
|
|
|
+ if (change.rowId !== undefined) {
|
|
|
+ callback(change.reason, this._toCellMap(change.rowId, this.getFieldInfos()));
|
|
|
+ } else {
|
|
|
+ callback(change.reason);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ onFieldUpdated = (fieldInfo: FieldInfo) => {
|
|
|
+ // Remove the cell data if the corresponding field was changed
|
|
|
+ this._cellCache.removeWithFieldId(fieldInfo.field.id);
|
|
|
+ };
|
|
|
+
|
|
|
+ onNumberOfFieldsUpdated = () => {
|
|
|
+ this._notifier.withChange(RowChangedReason.FieldDidChanged);
|
|
|
+ };
|
|
|
+
|
|
|
+ initializeRows = (rows: RowPB[]) => {
|
|
|
+ rows.forEach((rowPB) => {
|
|
|
+ this._rowList.push(this._toRowInfo(rowPB));
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ applyRowsChanged = (changeset: ViewRowsChangesetPB) => {
|
|
|
+ this._deleteRows(changeset.deleted_rows);
|
|
|
+ this._insertRows(changeset.inserted_rows);
|
|
|
+ this._updateRows(changeset.updated_rows);
|
|
|
+ };
|
|
|
+
|
|
|
+ applyRowsVisibility = (changeset: ViewRowsVisibilityChangesetPB) => {
|
|
|
+ this._hideRows(changeset.invisible_rows);
|
|
|
+ this._displayRows(changeset.visible_rows);
|
|
|
+ };
|
|
|
+
|
|
|
+ applyReorderRows = (rowIds: string[]) => {
|
|
|
+ this._rowList.reorderByRowIds(rowIds);
|
|
|
+ this._notifier.withChange(RowChangedReason.ReorderRows);
|
|
|
+ };
|
|
|
+
|
|
|
+ applyReorderSingleRow = (reorderRow: ReorderSingleRowPB) => {
|
|
|
+ const rowInfo = this._rowList.getRow(reorderRow.row_id);
|
|
|
+ if (rowInfo !== undefined) {
|
|
|
+ this._rowList.move({ rowId: reorderRow.row_id, fromIndex: reorderRow.old_index, toIndex: reorderRow.new_index });
|
|
|
+ this._notifier.withChange(RowChangedReason.ReorderSingleRow, reorderRow.row_id);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ _deleteRows = (rowIds: string[]) => {
|
|
|
+ rowIds.forEach((rowId) => {
|
|
|
+ const deletedRow = this._rowList.remove(rowId);
|
|
|
+ if (deletedRow !== undefined) {
|
|
|
+ this._notifier.withChange(RowChangedReason.Delete, deletedRow.rowInfo.row.id);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ _insertRows = (rows: InsertedRowPB[]) => {
|
|
|
+ rows.forEach((insertedRow) => {
|
|
|
+ const rowInfo = this._toRowInfo(insertedRow.row);
|
|
|
+ const insertedIndex = this._rowList.insert(insertedRow.index, rowInfo);
|
|
|
+ if (insertedIndex !== undefined) {
|
|
|
+ this._notifier.withChange(RowChangedReason.Insert, insertedIndex.rowId);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ _updateRows = (updatedRows: UpdatedRowPB[]) => {
|
|
|
+ if (updatedRows.length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const rowInfos: RowInfo[] = [];
|
|
|
+ updatedRows.forEach((updatedRow) => {
|
|
|
+ updatedRow.field_ids.forEach((fieldId) => {
|
|
|
+ const key = new CellCacheKey(fieldId, updatedRow.row.id);
|
|
|
+ this._cellCache.remove(key);
|
|
|
+ });
|
|
|
+
|
|
|
+ rowInfos.push(this._toRowInfo(updatedRow.row));
|
|
|
+ });
|
|
|
+
|
|
|
+ const updatedIndexs = this._rowList.insertRows(rowInfos);
|
|
|
+ updatedIndexs.forEach((row) => {
|
|
|
+ this._notifier.withChange(RowChangedReason.Update, row.rowId);
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ _hideRows = (rowIds: string[]) => {
|
|
|
+ rowIds.forEach((rowId) => {
|
|
|
+ const deletedRow = this._rowList.remove(rowId);
|
|
|
+ if (deletedRow !== undefined) {
|
|
|
+ this._notifier.withChange(RowChangedReason.Delete, deletedRow.rowInfo.row.id);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ _displayRows = (insertedRows: InsertedRowPB[]) => {
|
|
|
+ insertedRows.forEach((insertedRow) => {
|
|
|
+ const insertedIndex = this._rowList.insert(insertedRow.index, this._toRowInfo(insertedRow.row));
|
|
|
+
|
|
|
+ if (insertedIndex !== undefined) {
|
|
|
+ this._notifier.withChange(RowChangedReason.Insert, insertedIndex.rowId);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ dispose = async () => {
|
|
|
+ this._notifier.dispose();
|
|
|
+ };
|
|
|
+
|
|
|
+ _toRowInfo = (rowPB: RowPB) => {
|
|
|
+ return new RowInfo(this.viewId, this.getFieldInfos(), rowPB);
|
|
|
+ };
|
|
|
+
|
|
|
+ _toCellMap = (rowId: string, fieldInfos: readonly FieldInfo[]): Map<string, CellIdentifier> => {
|
|
|
+ const cellIdentifierByFieldId: Map<string, CellIdentifier> = new Map();
|
|
|
+
|
|
|
+ fieldInfos.forEach((fieldInfo) => {
|
|
|
+ const identifier = new CellIdentifier(this.viewId, rowId, fieldInfo.field.id, fieldInfo.field.field_type);
|
|
|
+ cellIdentifierByFieldId.set(fieldInfo.field.id, identifier);
|
|
|
+ });
|
|
|
+
|
|
|
+ return cellIdentifierByFieldId;
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+class RowList {
|
|
|
+ _rowInfos: RowInfo[] = [];
|
|
|
+ _rowInfoByRowId: Map<string, RowInfo> = new Map();
|
|
|
+
|
|
|
+ get rows(): readonly RowInfo[] {
|
|
|
+ return this._rowInfos;
|
|
|
+ }
|
|
|
+
|
|
|
+ getRow = (rowId: string) => {
|
|
|
+ return this._rowInfoByRowId.get(rowId);
|
|
|
+ };
|
|
|
+
|
|
|
+ getRowWithIndex = (rowId: string): { rowInfo: RowInfo; index: number } | undefined => {
|
|
|
+ const rowInfo = this._rowInfoByRowId.get(rowId);
|
|
|
+ if (rowInfo !== undefined) {
|
|
|
+ const index = this._rowInfos.indexOf(rowInfo, 0);
|
|
|
+ return { rowInfo: rowInfo, index: index };
|
|
|
+ }
|
|
|
+ return undefined;
|
|
|
+ };
|
|
|
+
|
|
|
+ indexOfRow = (rowId: string): number => {
|
|
|
+ const rowInfo = this._rowInfoByRowId.get(rowId);
|
|
|
+ if (rowInfo !== undefined) {
|
|
|
+ return this._rowInfos.indexOf(rowInfo, 0);
|
|
|
+ }
|
|
|
+ return -1;
|
|
|
+ };
|
|
|
+
|
|
|
+ push = (rowInfo: RowInfo) => {
|
|
|
+ const index = this.indexOfRow(rowInfo.row.id);
|
|
|
+ if (index !== -1) {
|
|
|
+ this._rowInfos.splice(index, 1, rowInfo);
|
|
|
+ } else {
|
|
|
+ this._rowInfos.push(rowInfo);
|
|
|
+ }
|
|
|
+
|
|
|
+ this._rowInfoByRowId.set(rowInfo.row.id, rowInfo);
|
|
|
+ };
|
|
|
+
|
|
|
+ remove = (rowId: string): DeletedRow | undefined => {
|
|
|
+ const result = this.getRowWithIndex(rowId);
|
|
|
+ if (result !== undefined) {
|
|
|
+ this._rowInfoByRowId.delete(result.rowInfo.row.id);
|
|
|
+ this._rowInfos.splice(result.index, 1);
|
|
|
+ return new DeletedRow(result.index, result.rowInfo);
|
|
|
+ } else {
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ insert = (index: number, newRowInfo: RowInfo): InsertedRow | undefined => {
|
|
|
+ const rowId = newRowInfo.row.id;
|
|
|
+ // Calibrate where to insert
|
|
|
+ let insertedIndex = index;
|
|
|
+ if (this._rowInfos.length <= insertedIndex) {
|
|
|
+ insertedIndex = this._rowInfos.length;
|
|
|
+ }
|
|
|
+ const result = this.getRowWithIndex(rowId);
|
|
|
+
|
|
|
+ if (result !== undefined) {
|
|
|
+ // remove the old row info
|
|
|
+ this._rowInfos.splice(result.index, 1);
|
|
|
+ // insert the new row info to the insertedIndex
|
|
|
+ this._rowInfos.splice(insertedIndex, 0, newRowInfo);
|
|
|
+ this._rowInfoByRowId.set(rowId, newRowInfo);
|
|
|
+ return undefined;
|
|
|
+ } else {
|
|
|
+ this._rowInfos.splice(insertedIndex, 0, newRowInfo);
|
|
|
+ this._rowInfoByRowId.set(rowId, newRowInfo);
|
|
|
+ return new InsertedRow(insertedIndex, rowId);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ insertRows = (rowInfos: RowInfo[]) => {
|
|
|
+ const map = new Map<string, InsertedRow>();
|
|
|
+ rowInfos.forEach((rowInfo) => {
|
|
|
+ const index = this.indexOfRow(rowInfo.row.id);
|
|
|
+ if (index !== -1) {
|
|
|
+ this._rowInfos.splice(index, 1, rowInfo);
|
|
|
+ this._rowInfoByRowId.set(rowInfo.row.id, rowInfo);
|
|
|
+
|
|
|
+ map.set(rowInfo.row.id, new InsertedRow(index, rowInfo.row.id));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return map;
|
|
|
+ };
|
|
|
+
|
|
|
+ move = (params: { rowId: string; fromIndex: number; toIndex: number }) => {
|
|
|
+ const currentIndex = this.indexOfRow(params.rowId);
|
|
|
+ if (currentIndex !== -1 && currentIndex !== params.toIndex) {
|
|
|
+ const rowInfo = this.remove(params.rowId)?.rowInfo;
|
|
|
+ if (rowInfo !== undefined) {
|
|
|
+ this.insert(params.toIndex, rowInfo);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ reorderByRowIds = (rowIds: string[]) => {
|
|
|
+ // remove all the elements
|
|
|
+ this._rowInfos = [];
|
|
|
+ rowIds.forEach((rowId) => {
|
|
|
+ const rowInfo = this._rowInfoByRowId.get(rowId);
|
|
|
+ if (rowInfo !== undefined) {
|
|
|
+ this._rowInfos.push(rowInfo);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ includes = (rowId: string): boolean => {
|
|
|
+ return this._rowInfoByRowId.has(rowId);
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+export class RowInfo {
|
|
|
+ constructor(
|
|
|
+ public readonly databaseId: string,
|
|
|
+ public readonly fieldInfos: readonly FieldInfo[],
|
|
|
+ public readonly row: RowPB
|
|
|
+ ) {}
|
|
|
+}
|
|
|
+
|
|
|
+export class DeletedRow {
|
|
|
+ constructor(public readonly index: number, public readonly rowInfo: RowInfo) {}
|
|
|
+}
|
|
|
+
|
|
|
+export class InsertedRow {
|
|
|
+ constructor(public readonly index: number, public readonly rowId: string) {}
|
|
|
+}
|
|
|
+
|
|
|
+export class RowChanged {
|
|
|
+ constructor(public readonly reason: RowChangedReason, public readonly rowId?: string) {}
|
|
|
+}
|
|
|
+
|
|
|
+// eslint-disable-next-line no-shadow
|
|
|
+export enum RowChangedReason {
|
|
|
+ Insert,
|
|
|
+ Delete,
|
|
|
+ Update,
|
|
|
+ Initial,
|
|
|
+ FieldDidChanged,
|
|
|
+ ReorderRows,
|
|
|
+ ReorderSingleRow,
|
|
|
+}
|
|
|
+
|
|
|
+export class RowChangeNotifier extends ChangeNotifier<RowChanged> {
|
|
|
+ _currentChanged = new RowChanged(RowChangedReason.Initial);
|
|
|
+
|
|
|
+ withChange = (reason: RowChangedReason, rowId?: string) => {
|
|
|
+ const newChange = new RowChanged(reason, rowId);
|
|
|
+ if (this._currentChanged !== newChange) {
|
|
|
+ this._currentChanged = newChange;
|
|
|
+ this.notify(this._currentChanged);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ dispose = () => {
|
|
|
+ this.unsubscribe();
|
|
|
+ };
|
|
|
+}
|