Skip to content

Commit

Permalink
feat: add shared demo (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
huanhuanwa authored Jul 25, 2024
1 parent deafb13 commit 488a8e1
Show file tree
Hide file tree
Showing 15 changed files with 787 additions and 12 deletions.
362 changes: 360 additions & 2 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
"ng-packagr": "^18.0.0",
"cpx": "^1.5.0",
"@worktile/pkg-manager": "^0.1.0",
"chalk": "^2.4.2"
"chalk": "^2.4.2",
"y-websocket": "^2.0.3",
"yjs": "^13.6.16"
}
}
20 changes: 15 additions & 5 deletions packages/grid/src/core/utils/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,23 @@ export const AITableQueries = {
if (!isUndefinedOrNull(fieldIndex) && fieldIndex > -1) {
return [fieldIndex] as AIFieldPath;
}
throw new Error(`Unable to find the path: ${JSON.stringify({ ...(field || {}), ...(record || {}) })}`);
throw new Error(`can not find the path: ${JSON.stringify({ ...(field || {}), ...(record || {}) })}`);
},
getFieldValue(aiTable: AITable, path: [number, number]): any {
if (!aiTable || !aiTable.records() || !aiTable.fields()) {
throw new Error(`Cannot find a descendant at path [${path}]`);
if (!aiTable) {
throw new Error(`aiTable does not exist [${path}]`);
}
const fieldId = aiTable.fields()[path[1]].id;
return aiTable.records()[path[0]].value[fieldId];
if (!aiTable.records()) {
throw new Error(`aiTable has no records [${path}]`);
}
if (!aiTable.fields()) {
throw new Error(`aiTable has no fields [${path}]`);
}

const field = aiTable.fields()[path[1]];
if (!field) {
throw new Error(`can not find field at path [${path}]`);
}
return aiTable.records()[path[0]].value[field.id];
}
};
2 changes: 0 additions & 2 deletions src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,3 @@
[aiFieldConfig]="aiFieldConfig"
(aiTableInitialized)="aiTableInitialized($event)"
></ai-table-grid>


64 changes: 62 additions & 2 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AfterViewInit, Component, OnInit, Signal, signal, WritableSignal } from '@angular/core';
import { AfterViewInit, Component, OnDestroy, OnInit, Signal, signal, WritableSignal } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { RouterOutlet } from '@angular/router';
import {
Expand All @@ -15,6 +15,13 @@ import {
import { ThyIconRegistry } from 'ngx-tethys/icon';
import { ThyPopover, ThyPopoverModule } from 'ngx-tethys/popover';
import { FieldPropertyEditor } from './component/field-property-editor/field-property-editor.component';
import { WebsocketProvider } from 'y-websocket';
import { connectProvider } from './share/provider';
import { SharedType, getSharedType } from './share/shared';
import { YjsAITable } from './share/yjs-table';
import applyActionOps from './share/apply-to-yjs';
import { applyYjsEvents } from './share/apply-to-table';
import { translateSharedTypeToTable } from './share/utils/translate-to-table';

const LOCAL_STORAGE_KEY = 'ai-table-data';

Expand Down Expand Up @@ -121,13 +128,17 @@ const initValue = {
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent implements OnInit, AfterViewInit {
export class AppComponent implements OnInit, AfterViewInit, OnDestroy {
records!: WritableSignal<AITableRecords>;

fields!: WritableSignal<AITableFields>;

aiTable!: AITable;

sharedType!: SharedType | null;

provider!: WebsocketProvider | null;

aiFieldConfig: AIFieldConfig = {
fieldPropertyEditor: FieldPropertyEditor,
fieldMenus: [
Expand Down Expand Up @@ -156,9 +167,39 @@ export class AppComponent implements OnInit, AfterViewInit {
const value = this.getLocalStorage();
this.records = signal(value.records);
this.fields = signal(value.fields);
this.initSharedType();
console.time('render');
}

initSharedType() {
const isInitializeSharedType = localStorage.getItem('ai-table-shared-type');
this.sharedType = getSharedType(
{
records: this.records(),
fields: this.fields()
},
!!isInitializeSharedType
);
let isInitialized = false;
this.provider = connectProvider(this.sharedType.doc!);
this.sharedType.observeDeep((events: any) => {
if (!YjsAITable.isLocal(this.aiTable)) {
if (!isInitialized) {
const data = translateSharedTypeToTable(this.sharedType!);
console.log(123, data);
this.records.set(data.records);
this.fields.set(data.fields);
isInitialized = true;
} else {
applyYjsEvents(this.aiTable, events);
}
}
});
if (!isInitializeSharedType) {
localStorage.setItem('ai-table-shared-type', 'true');
}
}

registryIcon() {
this.iconRegistry.addSvgIconSet(this.sanitizer.bypassSecurityTrustResourceUrl('assets/icons/defs/svg/sprite.defs.svg'));
}
Expand All @@ -175,6 +216,13 @@ export class AppComponent implements OnInit, AfterViewInit {
records: data.records
})
);
if (this.provider) {
if (!YjsAITable.isRemote(this.aiTable) && !YjsAITable.isUndo(this.aiTable)) {
YjsAITable.asLocal(this.aiTable, () => {
applyActionOps(this.sharedType!, data.actions, this.aiTable);
});
}
}
}

aiTableInitialized(aiTable: AITable) {
Expand All @@ -189,4 +237,16 @@ export class AppComponent implements OnInit, AfterViewInit {
const data = localStorage.getItem(`${LOCAL_STORAGE_KEY}`);
return data ? JSON.parse(data) : initValue;
}


disconnect() {
if (this.provider) {
this.provider.disconnect();
this.provider = null;
}
}

ngOnDestroy(): void {
this.disconnect();
}
}
67 changes: 67 additions & 0 deletions src/app/share/apply-to-table/array-event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ActionName, AIFieldValuePath, AITable, AITableAction, AITableField, AITableQueries } from '@ai-table/grid';
import * as Y from 'yjs';
import { toTablePath, translateRecord } from '../utils/translate-to-table';
import { isArray } from 'ngx-tethys/util';

export default function translateArrayEvent(aiTable: AITable, event: Y.YEvent<any>): AITableAction[] {
const actions: AITableAction[] = [];
let offset = 0;
let targetPath = toTablePath(event.path);
const isRecordsTranslate = event.path.includes('records');
const isFieldsTranslate = event.path.includes('fields');

event.changes.delta.forEach((delta) => {
if ('retain' in delta) {
offset += delta.retain ?? 0;
}
if ('insert' in delta) {
if (isArray(delta.insert)) {
if (isRecordsTranslate) {
if (targetPath.length) {
try {
delta.insert?.map((item: any) => {
const path = [targetPath[0], offset] as AIFieldValuePath;
const fieldValue = AITableQueries.getFieldValue(aiTable, path);
// To exclude insert triggered by field inserts.
if (fieldValue !== item) {
actions.push({
type: ActionName.UpdateFieldValue,
path,
fieldValue,
newFieldValue: item
});
}
});
} catch (error) {}
} else {
delta.insert?.map((item: Y.Array<any>, index) => {
const data = item.toJSON();
const [fixedField, customField] = data;
actions.push({
type: ActionName.AddRecord,
path: [offset + index],
record: {
id: fixedField[0],
value: translateRecord(customField, aiTable.fields())
}
});
});
}
}
if (isFieldsTranslate) {
delta.insert?.map((item: Y.Map<any>, index) => {
const data = item.toJSON();
if (event.path.includes('fields')) {
actions.push({
type: ActionName.AddField,
path: [offset + index],
field: data as AITableField
});
}
});
}
}
}
});
return actions;
}
29 changes: 29 additions & 0 deletions src/app/share/apply-to-table/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as Y from 'yjs';
import { AITable, AITableAction } from '@ai-table/grid';
import translateArrayEvent from './array-event';
import { YjsAITable } from '../yjs-table';

export function translateYjsEvent(aiTable: AITable, event: Y.YEvent<any>): AITableAction[] {
if (event instanceof Y.YArrayEvent) {
return translateArrayEvent(aiTable, event);
}
return [];
}

export function applyYjsEvents(aiTable: AITable, events: Y.YEvent<any>[]): void {
if (YjsAITable.isUndo(aiTable)) {
events.forEach((event) =>
translateYjsEvent(aiTable, event).forEach((item) => {
aiTable.apply(item);
})
);
} else {
YjsAITable.asRemote(aiTable, () => {
events.forEach((event) =>
translateYjsEvent(aiTable, event).forEach((item) => {
aiTable.apply(item);
})
);
});
}
}
20 changes: 20 additions & 0 deletions src/app/share/apply-to-yjs/add-field.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { SharedType, SyncArrayElement, toSyncElement } from '../shared';
import { AddFieldAction, getDefaultFieldValue } from '@ai-table/grid';

export default function addField(sharedType: SharedType, action: AddFieldAction): SharedType {
const fields = sharedType.get('fields');
const path = action.path[0];
if (fields) {
fields.insert(path, [toSyncElement(action.field)]);
}
const records = sharedType.get('records') as SyncArrayElement;
if (records) {
for (let value of records) {
const newRecord = getDefaultFieldValue(action.field.type);
const customField = value.get(1);
customField.insert(path, [newRecord]);
}
}

return sharedType;
}
12 changes: 12 additions & 0 deletions src/app/share/apply-to-yjs/add-record.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { SharedType, toRecordSyncElement } from '../shared';
import { AddRecordAction } from '@ai-table/grid';

export default function addRecord(sharedType: SharedType, action: AddRecordAction): SharedType {
const records = sharedType.get('records');
if (records) {
const path = action.path[0];
records.insert(path, [toRecordSyncElement(action.record)]);
}

return sharedType;
}
33 changes: 33 additions & 0 deletions src/app/share/apply-to-yjs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { AITable, AITableAction } from '@ai-table/grid';
import { SharedType } from '../shared';
import updateFieldValue from './update-field-value';
import addRecord from './add-record';
import addField from './add-field';

export type ActionMapper<O extends AITableAction = AITableAction> = {
[K in O['type']]: O extends { type: K } ? ApplyFunc<O> : never;
};

export type ApplyFunc<O extends AITableAction = AITableAction> = (sharedType: SharedType, op: O) => SharedType;

export const actionMappers: Partial<ActionMapper<AITableAction>> = {
update_field_value: updateFieldValue,
add_record: addRecord,
add_field: addField
};

export default function applyActionOps(sharedType: SharedType, actions: AITableAction[], aiTable: AITable): SharedType {
if (actions.length > 0) {
sharedType.doc!.transact(() => {
actions.forEach((action) => {
const apply = actionMappers[action.type] as ApplyFunc<typeof action>;
if (apply) {
return apply(sharedType, action);
}
return null;
});
}, aiTable);
}

return sharedType;
}
15 changes: 15 additions & 0 deletions src/app/share/apply-to-yjs/update-field-value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { SharedType, SyncArrayElement } from '../shared';
import { UpdateFieldValueAction } from '@ai-table/grid';

export default function updateFieldValue(sharedType: SharedType, action: UpdateFieldValueAction): SharedType {
const records = sharedType.get('records');
if (records) {
const record = records?.get(action.path[0]) as SyncArrayElement;
const customField = record.get(1);
const index = action.path[1];
customField.delete(index);
customField.insert(index, [action.newFieldValue]);
}

return sharedType;
}
8 changes: 8 additions & 0 deletions src/app/share/provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { WebsocketProvider } from 'y-websocket';
import * as Y from 'yjs';

export const connectProvider = (doc: Y.Doc) => {
const provider = new WebsocketProvider('wss://demos.yjs.dev/ws', 'ai-table-demo-2024/7/25', doc);
provider.connect();
return provider;
};
Loading

0 comments on commit 488a8e1

Please sign in to comment.