Skip to content

Commit 36b6177

Browse files
committed
feat: local collab
1 parent 85c1f5b commit 36b6177

38 files changed

+458
-276
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ package-lock.json
2424
/blob-report/
2525
/playwright/.cache/
2626
build
27+
.env

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"homepage": "https://nusr.github.io/excel",
3030
"devDependencies": {
3131
"@playwright/test": "1.48.2",
32-
"@rsbuild/core": "1.1.0",
32+
"@rsbuild/core": "1.1.2",
3333
"@rsbuild/plugin-react": "1.0.7",
3434
"@testing-library/jest-dom": "6.6.3",
3535
"@testing-library/react": "16.0.1",
@@ -50,14 +50,15 @@
5050
"typescript": "5.6.3"
5151
},
5252
"dependencies": {
53-
"@sentry/react": "8.37.1",
53+
"@sentry/react": "8.38.0",
54+
"@supabase/supabase-js": "2.46.1",
5455
"chart.js": "4.4.6",
5556
"comlink": "4.4.2",
5657
"jszip": "3.10.1",
5758
"react": "18.3.1",
5859
"react-dom": "18.3.1",
5960
"ssf": "https://cdn.sheetjs.com/ssf-0.11.3/ssf-0.11.3.tgz",
60-
"yjs": "^13.6.20"
61+
"yjs": "13.6.20"
6162
},
6263
"engines": {
6364
"pnpm": "9.12.3"

rsbuild.config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@ export default defineConfig({
1717
},
1818
},
1919
output: {
20-
assetPrefix: '/excel/',
20+
assetPrefix: process.env.CI ? '/excel/' : '',
2121
distPath: {
2222
js: '',
23-
css: '',
2423
},
2524
},
2625
html: {

src/collaboration/index.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { Doc } from 'yjs';
2+
import { initProvider } from './provider';
3+
import * as Y from 'yjs';
4+
5+
export async function initCollaboration(doc: Doc) {
6+
const provider = initProvider(doc);
7+
doc.on('update', (update: Uint8Array) => {
8+
provider.addHistory(doc.guid, update);
9+
});
10+
const result = await provider.retrieveHistory(doc.guid);
11+
if (result.length > 0) {
12+
const newDoc = Y.mergeUpdates(
13+
result.map((v) => new Uint8Array(Buffer.from(v.update))),
14+
);
15+
Y.applyUpdate(doc, newDoc);
16+
}
17+
return provider;
18+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { HistoryItem } from '@/types';
2+
3+
export type DocumentItem = {
4+
id: string;
5+
name: string;
6+
create_time: string;
7+
};
8+
9+
export interface Database {
10+
public: {
11+
Tables: {
12+
document: {
13+
Row: DocumentItem;
14+
Insert: {
15+
// the data to be passed to .insert()
16+
id?: never;
17+
name: string;
18+
create_time?: never;
19+
};
20+
Update: {
21+
// the data to be passed to .update()
22+
id?: never;
23+
name?: string;
24+
create_time?: never;
25+
};
26+
};
27+
history: {
28+
Row: HistoryItem;
29+
Insert: {
30+
// the data to be passed to .insert()
31+
id?: never;
32+
doc_id: string;
33+
update: string;
34+
create_time?: never;
35+
};
36+
};
37+
};
38+
};
39+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { SupabaseClient } from '@supabase/supabase-js';
2+
import type { Database } from './data.type';
3+
import { ServerProvider } from './server';
4+
import { LocalProvider } from './local';
5+
import { CollaborationProvider } from '@/types';
6+
import { type Doc } from 'yjs';
7+
8+
export function initProvider(doc: Doc): CollaborationProvider {
9+
const url = process.env.PUBLIC_SUPABASE_URL;
10+
const key = process.env.PUBLIC_SUPABASE_KEY;
11+
if (url && key) {
12+
const supabase = new SupabaseClient<Database>(url, key);
13+
return new ServerProvider(supabase, doc);
14+
}
15+
return new LocalProvider(doc);
16+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { CollaborationProvider, HistoryItem } from '@/types';
2+
import * as Y from 'yjs';
3+
// import { eventEmitter } from '@/util';
4+
5+
export class LocalProvider implements CollaborationProvider {
6+
private readonly broadcastChannel: BroadcastChannel;
7+
constructor(doc: Y.Doc) {
8+
const docId = doc.guid;
9+
this.broadcastChannel = new BroadcastChannel(docId);
10+
this.broadcastChannel.onmessage = (
11+
event: MessageEvent<{ docId: string; update: Uint8Array }>,
12+
) => {
13+
const { update } = event.data;
14+
console.log(docId, event);
15+
Y.applyUpdate(doc, update);
16+
// eventEmitter.emit('modelChange', { changeSet: new Set(['rangeMap']) });
17+
};
18+
}
19+
addHistory = async (docId: string, update: Uint8Array) => {
20+
this.broadcastChannel.postMessage({ docId, update });
21+
};
22+
retrieveHistory = async (docId: string): Promise<HistoryItem[]> => {
23+
console.log(docId);
24+
return [];
25+
};
26+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { SupabaseClient } from '@supabase/supabase-js';
2+
import type { Database } from './data.type';
3+
import { CollaborationProvider, HistoryItem } from '@/types';
4+
import * as Y from 'yjs';
5+
6+
export class ServerProvider implements CollaborationProvider {
7+
private readonly supabase: SupabaseClient<Database>;
8+
constructor(supabase: SupabaseClient<Database>, doc: Y.Doc) {
9+
this.supabase = supabase;
10+
11+
supabase
12+
.channel('custom-insert-channel')
13+
.on(
14+
'postgres_changes',
15+
{ event: 'INSERT', schema: 'public', table: 'history' },
16+
(payload) => {
17+
// TODO
18+
console.log('Change received!', payload);
19+
},
20+
)
21+
.subscribe();
22+
}
23+
24+
addHistory = async (docId: string, update: Uint8Array) => {
25+
const temp = Buffer.from(update.buffer).toString();
26+
this.supabase.from('history').insert([{ doc_id: docId, update: temp }]);
27+
};
28+
retrieveHistory = async (docId: string): Promise<HistoryItem[]> => {
29+
const result = await this.supabase
30+
.from('history')
31+
.select('*')
32+
.eq('doc_id', docId);
33+
const list = result.data || [];
34+
return list;
35+
};
36+
}

src/containers/Excel/__tests__/importXLSX.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { convertXMLToJSON, importXLSX } from '../importXLSX';
2-
import { WorkBookJSON, EVerticalAlign, EUnderLine } from '@/types';
2+
import { ModelJSON, EVerticalAlign, EUnderLine } from '@/types';
33
import fs from 'fs/promises';
44
import path from 'path';
55
import { initController } from '@/controller';
@@ -29,7 +29,7 @@ describe('importXLSX.test.ts', () => {
2929
const model = await importXLSX(fileData);
3030
const controller = initController();
3131
controller.fromJSON(model);
32-
const result: WorkBookJSON = {
32+
const result: ModelJSON = {
3333
autoFilter: {},
3434
rangeMap: {
3535
'2': { row: 0, col: 2, rowCount: 1, colCount: 1, sheetId: '2' },

src/containers/Excel/importXLSX.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
WorkBookJSON,
2+
ModelJSON,
33
WorksheetType,
44
ModelCellType,
55
StyleType,
@@ -454,7 +454,7 @@ function getCellStyle(
454454
export function convertXMLDataToModel(
455455
xmlData: ObjectItem,
456456
imageSizeMap: Record<string, IWindowSize>,
457-
): WorkBookJSON {
457+
): ModelJSON {
458458
const workbook = xmlData[WORKBOOK_PATH];
459459

460460
const sharedStrings: SharedStringItem[] = getArray(
@@ -468,7 +468,7 @@ export function convertXMLDataToModel(
468468
{},
469469
);
470470

471-
const result: WorkBookJSON = {
471+
const result: ModelJSON = {
472472
workbook: {},
473473
mergeCells: {},
474474
customHeight: {},

0 commit comments

Comments
 (0)