mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-24 09:52:49 +08:00
feat: add editor record (#7938)
fix CLOUD-58, CLOUD-61, CLOUD-62, PD-1607, PD-1608
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "snapshot_histories" ADD COLUMN "created_by" VARCHAR;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "snapshots" ADD COLUMN "created_by" VARCHAR,
|
||||||
|
ADD COLUMN "updated_by" VARCHAR;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "updates" ADD COLUMN "created_by" VARCHAR DEFAULT 'system';
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "snapshots" ADD CONSTRAINT "snapshots_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "snapshots" ADD CONSTRAINT "snapshots_updated_by_fkey" FOREIGN KEY ("updated_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "updates" ADD CONSTRAINT "updates_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "snapshot_histories" ADD CONSTRAINT "snapshot_histories_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -33,6 +33,10 @@ model User {
|
|||||||
aiSessions AiSession[]
|
aiSessions AiSession[]
|
||||||
updatedRuntimeConfigs RuntimeConfig[]
|
updatedRuntimeConfigs RuntimeConfig[]
|
||||||
userSnapshots UserSnapshot[]
|
userSnapshots UserSnapshot[]
|
||||||
|
createdSnapshot Snapshot[] @relation("createdSnapshot")
|
||||||
|
updatedSnapshot Snapshot[] @relation("updatedSnapshot")
|
||||||
|
createdUpdate Update[] @relation("createdUpdate")
|
||||||
|
createdHistory SnapshotHistory[] @relation("createdHistory")
|
||||||
|
|
||||||
@@index([email])
|
@@index([email])
|
||||||
@@map("users")
|
@@map("users")
|
||||||
@@ -241,9 +245,16 @@ model Snapshot {
|
|||||||
// the `updated_at` field will not record the time of record changed,
|
// the `updated_at` field will not record the time of record changed,
|
||||||
// but the created time of last seen update that has been merged into snapshot.
|
// but the created time of last seen update that has been merged into snapshot.
|
||||||
updatedAt DateTime @map("updated_at") @db.Timestamptz(3)
|
updatedAt DateTime @map("updated_at") @db.Timestamptz(3)
|
||||||
|
createdBy String? @map("created_by") @db.VarChar
|
||||||
|
updatedBy String? @map("updated_by") @db.VarChar
|
||||||
|
|
||||||
|
// should not delete origin snapshot even if user is deleted
|
||||||
|
// we only delete the snapshot if the workspace is deleted
|
||||||
|
createdByUser User? @relation(name: "createdSnapshot", fields: [createdBy], references: [id], onDelete: SetNull)
|
||||||
|
updatedByUser User? @relation(name: "updatedSnapshot", fields: [updatedBy], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
// @deprecated use updatedAt only
|
// @deprecated use updatedAt only
|
||||||
seq Int? @default(0) @db.Integer
|
seq Int? @default(0) @db.Integer
|
||||||
|
|
||||||
// we need to clear all hanging updates and snapshots before enable the foreign key on workspaceId
|
// we need to clear all hanging updates and snapshots before enable the foreign key on workspaceId
|
||||||
// workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
// workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
@@ -274,9 +285,14 @@ model Update {
|
|||||||
id String @map("guid") @db.VarChar
|
id String @map("guid") @db.VarChar
|
||||||
blob Bytes @db.ByteA
|
blob Bytes @db.ByteA
|
||||||
createdAt DateTime @map("created_at") @db.Timestamptz(3)
|
createdAt DateTime @map("created_at") @db.Timestamptz(3)
|
||||||
|
// TODO(@darkskygit): fullfill old update, remove default value in next release
|
||||||
|
createdBy String? @default("system") @map("created_by") @db.VarChar
|
||||||
|
|
||||||
|
// will delete createor record if createor's account is deleted
|
||||||
|
createdByUser User? @relation(name: "createdUpdate", fields: [createdBy], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
// @deprecated use createdAt only
|
// @deprecated use createdAt only
|
||||||
seq Int? @db.Integer
|
seq Int? @db.Integer
|
||||||
|
|
||||||
@@id([workspaceId, id, createdAt])
|
@@id([workspaceId, id, createdAt])
|
||||||
@@map("updates")
|
@@map("updates")
|
||||||
@@ -289,6 +305,10 @@ model SnapshotHistory {
|
|||||||
blob Bytes @db.ByteA
|
blob Bytes @db.ByteA
|
||||||
state Bytes? @db.ByteA
|
state Bytes? @db.ByteA
|
||||||
expiredAt DateTime @map("expired_at") @db.Timestamptz(3)
|
expiredAt DateTime @map("expired_at") @db.Timestamptz(3)
|
||||||
|
createdBy String? @map("created_by") @db.VarChar
|
||||||
|
|
||||||
|
// will delete createor record if creator's account is deleted
|
||||||
|
createdByUser User? @relation(name: "createdHistory", fields: [createdBy], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
@@id([workspaceId, id, timestamp])
|
@@id([workspaceId, id, timestamp])
|
||||||
@@map("snapshot_histories")
|
@@map("snapshot_histories")
|
||||||
|
|||||||
@@ -45,7 +45,12 @@ export class PgUserspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
return this.getDocSnapshot(spaceId, docId);
|
return this.getDocSnapshot(spaceId, docId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async pushDocUpdates(userId: string, docId: string, updates: Uint8Array[]) {
|
async pushDocUpdates(
|
||||||
|
userId: string,
|
||||||
|
docId: string,
|
||||||
|
updates: Uint8Array[],
|
||||||
|
editorId?: string
|
||||||
|
) {
|
||||||
if (!updates.length) {
|
if (!updates.length) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -67,6 +72,7 @@ export class PgUserspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
docId,
|
docId,
|
||||||
bin,
|
bin,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
editor: editorId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return timestamp;
|
return timestamp;
|
||||||
@@ -135,6 +141,7 @@ export class PgUserspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
docId,
|
docId,
|
||||||
bin: snapshot.blob,
|
bin: snapshot.blob,
|
||||||
timestamp: snapshot.updatedAt.getTime(),
|
timestamp: snapshot.updatedAt.getTime(),
|
||||||
|
editor: snapshot.userId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
async pushDocUpdates(
|
async pushDocUpdates(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
docId: string,
|
docId: string,
|
||||||
updates: Uint8Array[]
|
updates: Uint8Array[],
|
||||||
|
editorId?: string
|
||||||
) {
|
) {
|
||||||
if (!updates.length) {
|
if (!updates.length) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -82,6 +83,7 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
blob: Buffer.from(update),
|
blob: Buffer.from(update),
|
||||||
seq,
|
seq,
|
||||||
createdAt: new Date(createdAt),
|
createdAt: new Date(createdAt),
|
||||||
|
createdBy: editorId || null,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -113,6 +115,7 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
return rows.map(row => ({
|
return rows.map(row => ({
|
||||||
bin: row.blob,
|
bin: row.blob,
|
||||||
timestamp: row.createdAt.getTime(),
|
timestamp: row.createdAt.getTime(),
|
||||||
|
editor: row.createdBy || undefined,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +219,12 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
const histories = await this.db.snapshotHistory.findMany({
|
const histories = await this.db.snapshotHistory.findMany({
|
||||||
select: {
|
select: {
|
||||||
timestamp: true,
|
timestamp: true,
|
||||||
|
createdByUser: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
avatarUrl: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -230,7 +239,10 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
take: query.limit,
|
take: query.limit,
|
||||||
});
|
});
|
||||||
|
|
||||||
return histories.map(h => h.timestamp.getTime());
|
return histories.map(h => ({
|
||||||
|
timestamp: h.timestamp.getTime(),
|
||||||
|
editor: h.createdByUser,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDocHistory(workspaceId: string, docId: string, timestamp: number) {
|
async getDocHistory(workspaceId: string, docId: string, timestamp: number) {
|
||||||
@@ -253,13 +265,15 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
docId,
|
docId,
|
||||||
bin: history.blob,
|
bin: history.blob,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
editor: history.createdBy || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
override async rollbackDoc(
|
override async rollbackDoc(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
docId: string,
|
docId: string,
|
||||||
timestamp: number
|
timestamp: number,
|
||||||
|
editorId?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await using _lock = await this.lockDocForUpdate(spaceId, docId);
|
await using _lock = await this.lockDocForUpdate(spaceId, docId);
|
||||||
const toSnapshot = await this.getDocHistory(spaceId, docId, timestamp);
|
const toSnapshot = await this.getDocHistory(spaceId, docId, timestamp);
|
||||||
@@ -274,7 +288,14 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// force create a new history record after rollback
|
// force create a new history record after rollback
|
||||||
await this.createDocHistory(fromSnapshot, true);
|
await this.createDocHistory(
|
||||||
|
{
|
||||||
|
...fromSnapshot,
|
||||||
|
// override the editor to the one who requested the rollback
|
||||||
|
editor: editorId,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
// WARN:
|
// WARN:
|
||||||
// we should never do the snapshot updating in recovering,
|
// we should never do the snapshot updating in recovering,
|
||||||
// which is not the solution in CRDT.
|
// which is not the solution in CRDT.
|
||||||
@@ -331,6 +352,7 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
id: snapshot.docId,
|
id: snapshot.docId,
|
||||||
timestamp: new Date(snapshot.timestamp),
|
timestamp: new Date(snapshot.timestamp),
|
||||||
blob: Buffer.from(snapshot.bin),
|
blob: Buffer.from(snapshot.bin),
|
||||||
|
createdBy: snapshot.editor,
|
||||||
expiredAt: new Date(
|
expiredAt: new Date(
|
||||||
Date.now() + (await this.options.historyMaxAge(snapshot.spaceId))
|
Date.now() + (await this.options.historyMaxAge(snapshot.spaceId))
|
||||||
),
|
),
|
||||||
@@ -374,6 +396,8 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
docId,
|
docId,
|
||||||
bin: snapshot.blob,
|
bin: snapshot.blob,
|
||||||
timestamp: snapshot.updatedAt.getTime(),
|
timestamp: snapshot.updatedAt.getTime(),
|
||||||
|
// creator and editor may null if their account is deleted
|
||||||
|
editor: snapshot.updatedBy || snapshot.createdBy || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,10 +420,10 @@ export class PgWorkspaceDocStorageAdapter extends DocStorageAdapter {
|
|||||||
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
try {
|
try {
|
||||||
const result: { updatedAt: Date }[] = await this.db.$queryRaw`
|
const result: { updatedAt: Date }[] = await this.db.$queryRaw`
|
||||||
INSERT INTO "snapshots" ("workspace_id", "guid", "blob", "created_at", "updated_at")
|
INSERT INTO "snapshots" ("workspace_id", "guid", "blob", "created_at", "updated_at", "created_by", "updated_by")
|
||||||
VALUES (${spaceId}, ${docId}, ${bin}, DEFAULT, ${updatedAt})
|
VALUES (${spaceId}, ${docId}, ${bin}, DEFAULT, ${updatedAt}, ${snapshot.editor}, ${snapshot.editor})
|
||||||
ON CONFLICT ("workspace_id", "guid")
|
ON CONFLICT ("workspace_id", "guid")
|
||||||
DO UPDATE SET "blob" = ${bin}, "updated_at" = ${updatedAt}
|
DO UPDATE SET "blob" = ${bin}, "updated_at" = ${updatedAt}, "updated_by" = ${snapshot.editor}
|
||||||
WHERE "snapshots"."workspace_id" = ${spaceId} AND "snapshots"."guid" = ${docId} AND "snapshots"."updated_at" <= ${updatedAt}
|
WHERE "snapshots"."workspace_id" = ${spaceId} AND "snapshots"."guid" = ${docId} AND "snapshots"."updated_at" <= ${updatedAt}
|
||||||
RETURNING "snapshots"."workspace_id" as "workspaceId", "snapshots"."guid" as "id", "snapshots"."updated_at" as "updatedAt"
|
RETURNING "snapshots"."workspace_id" as "workspaceId", "snapshots"."guid" as "id", "snapshots"."updated_at" as "updatedAt"
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -22,4 +22,4 @@ import { DocStorageOptions } from './options';
|
|||||||
export class DocStorageModule {}
|
export class DocStorageModule {}
|
||||||
export { PgUserspaceDocStorageAdapter, PgWorkspaceDocStorageAdapter };
|
export { PgUserspaceDocStorageAdapter, PgWorkspaceDocStorageAdapter };
|
||||||
|
|
||||||
export { DocStorageAdapter } from './storage';
|
export { DocStorageAdapter, type Editor } from './storage';
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ export interface DocRecord {
|
|||||||
docId: string;
|
docId: string;
|
||||||
bin: Uint8Array;
|
bin: Uint8Array;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
editor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocUpdate {
|
export interface DocUpdate {
|
||||||
bin: Uint8Array;
|
bin: Uint8Array;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
editor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HistoryFilter {
|
export interface HistoryFilter {
|
||||||
@@ -28,6 +30,11 @@ export interface HistoryFilter {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Editor {
|
||||||
|
name: string;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DocStorageOptions {
|
export interface DocStorageOptions {
|
||||||
mergeUpdates?: (updates: Uint8Array[]) => Promise<Uint8Array> | Uint8Array;
|
mergeUpdates?: (updates: Uint8Array[]) => Promise<Uint8Array> | Uint8Array;
|
||||||
}
|
}
|
||||||
@@ -61,7 +68,7 @@ export abstract class DocStorageAdapter extends Connection {
|
|||||||
const updates = await this.getDocUpdates(spaceId, docId);
|
const updates = await this.getDocUpdates(spaceId, docId);
|
||||||
|
|
||||||
if (updates.length) {
|
if (updates.length) {
|
||||||
const { timestamp, bin } = await this.squash(
|
const { timestamp, bin, editor } = await this.squash(
|
||||||
snapshot ? [snapshot, ...updates] : updates
|
snapshot ? [snapshot, ...updates] : updates
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -70,6 +77,7 @@ export abstract class DocStorageAdapter extends Connection {
|
|||||||
docId,
|
docId,
|
||||||
bin,
|
bin,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
editor,
|
||||||
};
|
};
|
||||||
|
|
||||||
const success = await this.setDocSnapshot(newSnapshot);
|
const success = await this.setDocSnapshot(newSnapshot);
|
||||||
@@ -91,7 +99,8 @@ export abstract class DocStorageAdapter extends Connection {
|
|||||||
abstract pushDocUpdates(
|
abstract pushDocUpdates(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
docId: string,
|
docId: string,
|
||||||
updates: Uint8Array[]
|
updates: Uint8Array[],
|
||||||
|
editorId?: string
|
||||||
): Promise<number>;
|
): Promise<number>;
|
||||||
|
|
||||||
abstract deleteDoc(spaceId: string, docId: string): Promise<void>;
|
abstract deleteDoc(spaceId: string, docId: string): Promise<void>;
|
||||||
@@ -99,7 +108,8 @@ export abstract class DocStorageAdapter extends Connection {
|
|||||||
async rollbackDoc(
|
async rollbackDoc(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
docId: string,
|
docId: string,
|
||||||
timestamp: number
|
timestamp: number,
|
||||||
|
editorId?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await using _lock = await this.lockDocForUpdate(spaceId, docId);
|
await using _lock = await this.lockDocForUpdate(spaceId, docId);
|
||||||
const toSnapshot = await this.getDocHistory(spaceId, docId, timestamp);
|
const toSnapshot = await this.getDocHistory(spaceId, docId, timestamp);
|
||||||
@@ -114,7 +124,7 @@ export abstract class DocStorageAdapter extends Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const change = this.generateChangeUpdate(fromSnapshot.bin, toSnapshot.bin);
|
const change = this.generateChangeUpdate(fromSnapshot.bin, toSnapshot.bin);
|
||||||
await this.pushDocUpdates(spaceId, docId, [change]);
|
await this.pushDocUpdates(spaceId, docId, [change], editorId);
|
||||||
// force create a new history record after rollback
|
// force create a new history record after rollback
|
||||||
await this.createDocHistory(fromSnapshot, true);
|
await this.createDocHistory(fromSnapshot, true);
|
||||||
}
|
}
|
||||||
@@ -127,7 +137,7 @@ export abstract class DocStorageAdapter extends Connection {
|
|||||||
spaceId: string,
|
spaceId: string,
|
||||||
docId: string,
|
docId: string,
|
||||||
query: { skip?: number; limit?: number }
|
query: { skip?: number; limit?: number }
|
||||||
): Promise<number[]>;
|
): Promise<{ timestamp: number; editor: Editor | null }[]>;
|
||||||
abstract getDocHistory(
|
abstract getDocHistory(
|
||||||
spaceId: string,
|
spaceId: string,
|
||||||
docId: string,
|
docId: string,
|
||||||
@@ -173,6 +183,7 @@ export abstract class DocStorageAdapter extends Connection {
|
|||||||
return {
|
return {
|
||||||
bin: finalUpdate,
|
bin: finalUpdate,
|
||||||
timestamp: lastUpdate.timestamp,
|
timestamp: lastUpdate.timestamp,
|
||||||
|
editor: lastUpdate.editor,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,5 +28,6 @@ export {
|
|||||||
DocStorageAdapter,
|
DocStorageAdapter,
|
||||||
type DocStorageOptions,
|
type DocStorageOptions,
|
||||||
type DocUpdate,
|
type DocUpdate,
|
||||||
|
type Editor,
|
||||||
type HistoryFilter,
|
type HistoryFilter,
|
||||||
} from './doc';
|
} from './doc';
|
||||||
|
|||||||
@@ -264,9 +264,11 @@ export class SpaceSyncGateway
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Auth()
|
||||||
@SubscribeMessage('space:push-doc-updates')
|
@SubscribeMessage('space:push-doc-updates')
|
||||||
async onReceiveDocUpdates(
|
async onReceiveDocUpdates(
|
||||||
@ConnectedSocket() client: Socket,
|
@ConnectedSocket() client: Socket,
|
||||||
|
@CurrentUser() user: CurrentUser,
|
||||||
@MessageBody()
|
@MessageBody()
|
||||||
message: PushDocUpdatesMessage
|
message: PushDocUpdatesMessage
|
||||||
): Promise<EventResponse<{ accepted: true; timestamp?: number }>> {
|
): Promise<EventResponse<{ accepted: true; timestamp?: number }>> {
|
||||||
@@ -277,7 +279,8 @@ export class SpaceSyncGateway
|
|||||||
const timestamp = await adapter.push(
|
const timestamp = await adapter.push(
|
||||||
spaceId,
|
spaceId,
|
||||||
docId,
|
docId,
|
||||||
updates.map(update => Buffer.from(update, 'base64'))
|
updates.map(update => Buffer.from(update, 'base64')),
|
||||||
|
user.id
|
||||||
);
|
);
|
||||||
|
|
||||||
// could be put in [adapter.push]
|
// could be put in [adapter.push]
|
||||||
@@ -448,8 +451,10 @@ export class SpaceSyncGateway
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Auth()
|
||||||
@SubscribeMessage('client-update-v2')
|
@SubscribeMessage('client-update-v2')
|
||||||
async handleClientUpdateV2(
|
async handleClientUpdateV2(
|
||||||
|
@CurrentUser() user: CurrentUser,
|
||||||
@MessageBody()
|
@MessageBody()
|
||||||
{
|
{
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -462,7 +467,7 @@ export class SpaceSyncGateway
|
|||||||
},
|
},
|
||||||
@ConnectedSocket() client: Socket
|
@ConnectedSocket() client: Socket
|
||||||
): Promise<EventResponse<{ accepted: true; timestamp?: number }>> {
|
): Promise<EventResponse<{ accepted: true; timestamp?: number }>> {
|
||||||
return this.onReceiveDocUpdates(client, {
|
return this.onReceiveDocUpdates(client, user, {
|
||||||
spaceType: SpaceType.Workspace,
|
spaceType: SpaceType.Workspace,
|
||||||
spaceId: workspaceId,
|
spaceId: workspaceId,
|
||||||
docId: guid,
|
docId: guid,
|
||||||
@@ -596,9 +601,9 @@ abstract class SyncSocketAdapter {
|
|||||||
permission?: Permission
|
permission?: Permission
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
push(spaceId: string, docId: string, updates: Buffer[]) {
|
push(spaceId: string, docId: string, updates: Buffer[], editorId: string) {
|
||||||
this.assertIn(spaceId);
|
this.assertIn(spaceId);
|
||||||
return this.storage.pushDocUpdates(spaceId, docId, updates);
|
return this.storage.pushDocUpdates(spaceId, docId, updates, editorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(spaceId: string, docId: string) {
|
get(spaceId: string, docId: string) {
|
||||||
@@ -621,9 +626,14 @@ class WorkspaceSyncAdapter extends SyncSocketAdapter {
|
|||||||
super(SpaceType.Workspace, client, storage);
|
super(SpaceType.Workspace, client, storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
override push(spaceId: string, docId: string, updates: Buffer[]) {
|
override push(
|
||||||
|
spaceId: string,
|
||||||
|
docId: string,
|
||||||
|
updates: Buffer[],
|
||||||
|
editorId: string
|
||||||
|
) {
|
||||||
const id = new DocID(docId, spaceId);
|
const id = new DocID(docId, spaceId);
|
||||||
return super.push(spaceId, id.guid, updates);
|
return super.push(spaceId, id.guid, updates, editorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
override get(spaceId: string, docId: string) {
|
override get(spaceId: string, docId: string) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { PgWorkspaceDocStorageAdapter } from '../../doc';
|
|||||||
import { Permission, PermissionService } from '../../permission';
|
import { Permission, PermissionService } from '../../permission';
|
||||||
import { DocID } from '../../utils/doc';
|
import { DocID } from '../../utils/doc';
|
||||||
import { WorkspaceType } from '../types';
|
import { WorkspaceType } from '../types';
|
||||||
|
import { EditorType } from './workspace';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
class DocHistoryType implements Partial<SnapshotHistory> {
|
class DocHistoryType implements Partial<SnapshotHistory> {
|
||||||
@@ -27,6 +28,9 @@ class DocHistoryType implements Partial<SnapshotHistory> {
|
|||||||
|
|
||||||
@Field(() => GraphQLISODateTime)
|
@Field(() => GraphQLISODateTime)
|
||||||
timestamp!: Date;
|
timestamp!: Date;
|
||||||
|
|
||||||
|
@Field(() => EditorType, { nullable: true })
|
||||||
|
editor!: EditorType | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Resolver(() => WorkspaceType)
|
@Resolver(() => WorkspaceType)
|
||||||
@@ -47,17 +51,18 @@ export class DocHistoryResolver {
|
|||||||
): Promise<DocHistoryType[]> {
|
): Promise<DocHistoryType[]> {
|
||||||
const docId = new DocID(guid, workspace.id);
|
const docId = new DocID(guid, workspace.id);
|
||||||
|
|
||||||
const timestamps = await this.workspace.listDocHistories(
|
const histories = await this.workspace.listDocHistories(
|
||||||
workspace.id,
|
workspace.id,
|
||||||
docId.guid,
|
docId.guid,
|
||||||
{ before: timestamp.getTime(), limit: take }
|
{ before: timestamp.getTime(), limit: take }
|
||||||
);
|
);
|
||||||
|
|
||||||
return timestamps.map(timestamp => {
|
return histories.map(history => {
|
||||||
return {
|
return {
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
id: docId.guid,
|
id: docId.guid,
|
||||||
timestamp: new Date(timestamp),
|
timestamp: new Date(history.timestamp),
|
||||||
|
editor: history.editor,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -81,7 +86,8 @@ export class DocHistoryResolver {
|
|||||||
await this.workspace.rollbackDoc(
|
await this.workspace.rollbackDoc(
|
||||||
docId.workspace,
|
docId.workspace,
|
||||||
docId.guid,
|
docId.guid,
|
||||||
timestamp.getTime()
|
timestamp.getTime(),
|
||||||
|
user.id
|
||||||
);
|
);
|
||||||
|
|
||||||
return timestamp;
|
return timestamp;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
Args,
|
Args,
|
||||||
|
Field,
|
||||||
Int,
|
Int,
|
||||||
Mutation,
|
Mutation,
|
||||||
|
ObjectType,
|
||||||
Parent,
|
Parent,
|
||||||
Query,
|
Query,
|
||||||
ResolveField,
|
ResolveField,
|
||||||
@@ -16,6 +18,7 @@ import { applyUpdate, Doc } from 'yjs';
|
|||||||
import type { FileUpload } from '../../../fundamentals';
|
import type { FileUpload } from '../../../fundamentals';
|
||||||
import {
|
import {
|
||||||
CantChangeSpaceOwner,
|
CantChangeSpaceOwner,
|
||||||
|
DocNotFound,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
InternalServerError,
|
InternalServerError,
|
||||||
MailService,
|
MailService,
|
||||||
@@ -28,6 +31,7 @@ import {
|
|||||||
UserNotFound,
|
UserNotFound,
|
||||||
} from '../../../fundamentals';
|
} from '../../../fundamentals';
|
||||||
import { CurrentUser, Public } from '../../auth';
|
import { CurrentUser, Public } from '../../auth';
|
||||||
|
import type { Editor } from '../../doc';
|
||||||
import { Permission, PermissionService } from '../../permission';
|
import { Permission, PermissionService } from '../../permission';
|
||||||
import { QuotaManagementService, QuotaQueryType } from '../../quota';
|
import { QuotaManagementService, QuotaQueryType } from '../../quota';
|
||||||
import { WorkspaceBlobStorage } from '../../storage';
|
import { WorkspaceBlobStorage } from '../../storage';
|
||||||
@@ -40,6 +44,30 @@ import {
|
|||||||
} from '../types';
|
} from '../types';
|
||||||
import { defaultWorkspaceAvatar } from '../utils';
|
import { defaultWorkspaceAvatar } from '../utils';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class EditorType implements Partial<Editor> {
|
||||||
|
@Field()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
avatarUrl!: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
class WorkspacePageMeta {
|
||||||
|
@Field(() => Date)
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@Field(() => Date)
|
||||||
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@Field(() => EditorType, { nullable: true })
|
||||||
|
createdBy!: EditorType | null;
|
||||||
|
|
||||||
|
@Field(() => EditorType, { nullable: true })
|
||||||
|
updatedBy!: EditorType | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Workspace resolver
|
* Workspace resolver
|
||||||
* Public apis rate limit: 10 req/m
|
* Public apis rate limit: 10 req/m
|
||||||
@@ -155,6 +183,35 @@ export class WorkspaceResolver {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => WorkspacePageMeta, {
|
||||||
|
description: 'Cloud page metadata of workspace',
|
||||||
|
complexity: 2,
|
||||||
|
})
|
||||||
|
async pageMeta(
|
||||||
|
@Parent() workspace: WorkspaceType,
|
||||||
|
@Args('pageId') pageId: string
|
||||||
|
) {
|
||||||
|
const metadata = await this.prisma.snapshot.findFirst({
|
||||||
|
where: { workspaceId: workspace.id, id: pageId },
|
||||||
|
select: {
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
createdByUser: { select: { name: true, avatarUrl: true } },
|
||||||
|
updatedByUser: { select: { name: true, avatarUrl: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!metadata) {
|
||||||
|
throw new DocNotFound({ spaceId: workspace.id, docId: pageId });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createdAt: metadata.createdAt,
|
||||||
|
updatedAt: metadata.updatedAt,
|
||||||
|
createdBy: metadata.createdByUser || null,
|
||||||
|
updatedBy: metadata.updatedByUser || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@ResolveField(() => QuotaQueryType, {
|
@ResolveField(() => QuotaQueryType, {
|
||||||
name: 'quota',
|
name: 'quota',
|
||||||
description: 'quota of workspace',
|
description: 'quota of workspace',
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ type DocHistoryNotFoundDataType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DocHistoryType {
|
type DocHistoryType {
|
||||||
|
editor: EditorType
|
||||||
id: String!
|
id: String!
|
||||||
timestamp: DateTime!
|
timestamp: DateTime!
|
||||||
workspaceId: String!
|
workspaceId: String!
|
||||||
@@ -199,6 +200,11 @@ type DocNotFoundDataType {
|
|||||||
spaceId: String!
|
spaceId: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EditorType {
|
||||||
|
avatarUrl: String
|
||||||
|
name: String!
|
||||||
|
}
|
||||||
|
|
||||||
union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocAccessDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | InvalidHistoryTimestampDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | VersionRejectedDataType
|
union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocAccessDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | InvalidHistoryTimestampDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | VersionRejectedDataType
|
||||||
|
|
||||||
enum ErrorNames {
|
enum ErrorNames {
|
||||||
@@ -875,6 +881,13 @@ type WorkspacePage {
|
|||||||
workspaceId: String!
|
workspaceId: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WorkspacePageMeta {
|
||||||
|
createdAt: DateTime!
|
||||||
|
createdBy: EditorType
|
||||||
|
updatedAt: DateTime!
|
||||||
|
updatedBy: EditorType
|
||||||
|
}
|
||||||
|
|
||||||
type WorkspaceType {
|
type WorkspaceType {
|
||||||
"""Available features of workspace"""
|
"""Available features of workspace"""
|
||||||
availableFeatures: [FeatureType!]!
|
availableFeatures: [FeatureType!]!
|
||||||
@@ -905,6 +918,9 @@ type WorkspaceType {
|
|||||||
"""Owner of workspace"""
|
"""Owner of workspace"""
|
||||||
owner: UserType!
|
owner: UserType!
|
||||||
|
|
||||||
|
"""Cloud page metadata of workspace"""
|
||||||
|
pageMeta(pageId: String!): WorkspacePageMeta!
|
||||||
|
|
||||||
"""Permission of current signed in user in workspace"""
|
"""Permission of current signed in user in workspace"""
|
||||||
permission: Permission!
|
permission: Permission!
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ const snapshot: Snapshot = {
|
|||||||
seq: 0,
|
seq: 0,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
createdBy: null,
|
||||||
|
updatedBy: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getSnapshot(timestamp: number = Date.now()): DocRecord {
|
function getSnapshot(timestamp: number = Date.now()): DocRecord {
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ test('should be able to merge updates as snapshot', async t => {
|
|||||||
blob: Buffer.from(update),
|
blob: Buffer.from(update),
|
||||||
seq: 1,
|
seq: 1,
|
||||||
createdAt: new Date(Date.now() + 1),
|
createdAt: new Date(Date.now() + 1),
|
||||||
|
createdBy: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -199,6 +200,7 @@ test('should be able to merge updates as snapshot', async t => {
|
|||||||
blob: appendUpdate,
|
blob: appendUpdate,
|
||||||
seq: 2,
|
seq: 2,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
|
createdBy: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export const iconNames = [
|
|||||||
'edgeless',
|
'edgeless',
|
||||||
'journal',
|
'journal',
|
||||||
'payment',
|
'payment',
|
||||||
|
'createdEdited',
|
||||||
] as const satisfies fromLibIconName<LibIconComponentName>[];
|
] as const satisfies fromLibIconName<LibIconComponentName>[];
|
||||||
|
|
||||||
export type PagePropertyIcon = (typeof iconNames)[number];
|
export type PagePropertyIcon = (typeof iconNames)[number];
|
||||||
@@ -109,6 +110,10 @@ export const getDefaultIconName = (
|
|||||||
return 'checkBoxCheckLinear';
|
return 'checkBoxCheckLinear';
|
||||||
case 'number':
|
case 'number':
|
||||||
return 'number';
|
return 'number';
|
||||||
|
case 'createdBy':
|
||||||
|
return 'createdEdited';
|
||||||
|
case 'updatedBy':
|
||||||
|
return 'createdEdited';
|
||||||
default:
|
default:
|
||||||
return 'text';
|
return 'text';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,9 +35,16 @@ export const newPropertyTypes: PagePropertyType[] = [
|
|||||||
PagePropertyType.Number,
|
PagePropertyType.Number,
|
||||||
PagePropertyType.Checkbox,
|
PagePropertyType.Checkbox,
|
||||||
PagePropertyType.Date,
|
PagePropertyType.Date,
|
||||||
|
PagePropertyType.CreatedBy,
|
||||||
|
PagePropertyType.UpdatedBy,
|
||||||
// TODO(@Peng): add more
|
// TODO(@Peng): add more
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const readonlyPropertyTypes: PagePropertyType[] = [
|
||||||
|
PagePropertyType.CreatedBy,
|
||||||
|
PagePropertyType.UpdatedBy,
|
||||||
|
];
|
||||||
|
|
||||||
export class PagePropertiesMetaManager {
|
export class PagePropertiesMetaManager {
|
||||||
constructor(private readonly adapter: WorkspacePropertiesAdapter) {}
|
constructor(private readonly adapter: WorkspacePropertiesAdapter) {}
|
||||||
|
|
||||||
@@ -95,6 +102,7 @@ export class PagePropertiesMetaManager {
|
|||||||
type,
|
type,
|
||||||
order: newOrder,
|
order: newOrder,
|
||||||
icon: icon ?? getDefaultIconName(type),
|
icon: icon ?? getDefaultIconName(type),
|
||||||
|
readonly: readonlyPropertyTypes.includes(type) || undefined,
|
||||||
} as const;
|
} as const;
|
||||||
this.customPropertiesSchema[id] = property;
|
this.customPropertiesSchema[id] = property;
|
||||||
return property;
|
return property;
|
||||||
|
|||||||
@@ -1,14 +1,28 @@
|
|||||||
import { Checkbox, DatePicker, Menu } from '@affine/component';
|
import { Avatar, Checkbox, DatePicker, Menu } from '@affine/component';
|
||||||
|
import { CloudDocMetaService } from '@affine/core/modules/cloud/services/cloud-doc-meta';
|
||||||
import type {
|
import type {
|
||||||
PageInfoCustomProperty,
|
PageInfoCustomProperty,
|
||||||
PageInfoCustomPropertyMeta,
|
PageInfoCustomPropertyMeta,
|
||||||
PagePropertyType,
|
PagePropertyType,
|
||||||
} from '@affine/core/modules/properties/services/schema';
|
} from '@affine/core/modules/properties/services/schema';
|
||||||
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { i18nTime, useI18n } from '@affine/i18n';
|
import { i18nTime, useI18n } from '@affine/i18n';
|
||||||
import { DocService, useService } from '@toeverything/infra';
|
import {
|
||||||
|
DocService,
|
||||||
|
useLiveData,
|
||||||
|
useService,
|
||||||
|
WorkspaceService,
|
||||||
|
} from '@toeverything/infra';
|
||||||
import { noop } from 'lodash-es';
|
import { noop } from 'lodash-es';
|
||||||
import type { ChangeEventHandler } from 'react';
|
import type { ChangeEventHandler } from 'react';
|
||||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
import {
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import { managerContext } from './common';
|
import { managerContext } from './common';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
@@ -190,6 +204,102 @@ export const TagsValue = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CloudUserAvatar = (props: { type: 'CreatedBy' | 'UpdatedBy' }) => {
|
||||||
|
const cloudDocMetaService = useService(CloudDocMetaService);
|
||||||
|
const cloudDocMeta = useLiveData(cloudDocMetaService.cloudDocMeta.meta$);
|
||||||
|
const isRevalidating = useLiveData(
|
||||||
|
cloudDocMetaService.cloudDocMeta.isRevalidating$
|
||||||
|
);
|
||||||
|
const error = useLiveData(cloudDocMetaService.cloudDocMeta.error$);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
cloudDocMetaService.cloudDocMeta.revalidate();
|
||||||
|
}, [cloudDocMetaService]);
|
||||||
|
|
||||||
|
const user = useMemo(() => {
|
||||||
|
if (!cloudDocMeta) return null;
|
||||||
|
if (props.type === 'CreatedBy' && cloudDocMeta.createdBy) {
|
||||||
|
return {
|
||||||
|
name: cloudDocMeta.createdBy.name,
|
||||||
|
avatarUrl: cloudDocMeta.createdBy.avatarUrl,
|
||||||
|
};
|
||||||
|
} else if (props.type === 'UpdatedBy' && cloudDocMeta.updatedBy) {
|
||||||
|
return {
|
||||||
|
name: cloudDocMeta.updatedBy.name,
|
||||||
|
avatarUrl: cloudDocMeta.updatedBy.avatarUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [cloudDocMeta, props.type]);
|
||||||
|
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
if (!cloudDocMeta) {
|
||||||
|
if (isRevalidating) {
|
||||||
|
// TODO: loading ui
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
// error ui
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (user) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Avatar url={user.avatarUrl || ''} name={user.name} size={20} />
|
||||||
|
<span>{user.name}</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Avatar name="?" size={20} />
|
||||||
|
<span>
|
||||||
|
{t['com.affine.page-properties.property-user-avatar-no-record']()}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LocalUserValue = () => {
|
||||||
|
const t = useI18n();
|
||||||
|
return <span>{t['com.affine.page-properties.local-user']()}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreatedUserValue = () => {
|
||||||
|
const workspaceService = useService(WorkspaceService);
|
||||||
|
const isCloud =
|
||||||
|
workspaceService.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||||
|
|
||||||
|
if (!isCloud) {
|
||||||
|
return <LocalUserValue />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.propertyRowValueUserCell}>
|
||||||
|
<CloudUserAvatar type="CreatedBy" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UpdatedUserValue = () => {
|
||||||
|
const workspaceService = useService(WorkspaceService);
|
||||||
|
const isCloud =
|
||||||
|
workspaceService.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||||
|
|
||||||
|
if (!isCloud) {
|
||||||
|
return <LocalUserValue />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.propertyRowValueUserCell}>
|
||||||
|
<CloudUserAvatar type="UpdatedBy" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const propertyValueRenderers: Record<
|
export const propertyValueRenderers: Record<
|
||||||
PagePropertyType,
|
PagePropertyType,
|
||||||
typeof DateValue
|
typeof DateValue
|
||||||
@@ -198,6 +308,8 @@ export const propertyValueRenderers: Record<
|
|||||||
checkbox: CheckboxValue,
|
checkbox: CheckboxValue,
|
||||||
text: TextValue,
|
text: TextValue,
|
||||||
number: NumberValue,
|
number: NumberValue,
|
||||||
|
createdBy: CreatedUserValue,
|
||||||
|
updatedBy: UpdatedUserValue,
|
||||||
// TODO(@Peng): fix following
|
// TODO(@Peng): fix following
|
||||||
tags: TagsValue,
|
tags: TagsValue,
|
||||||
progress: TextValue,
|
progress: TextValue,
|
||||||
|
|||||||
@@ -361,6 +361,16 @@ export const propertyRowValueTextCell = style([
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const propertyRowValueUserCell = style([
|
||||||
|
propertyRowValueCell,
|
||||||
|
{
|
||||||
|
border: 'none',
|
||||||
|
overflow: 'hidden',
|
||||||
|
columnGap: '0.5rem',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
export const propertyRowValueTextarea = style([
|
export const propertyRowValueTextarea = style([
|
||||||
propertyRowValueCell,
|
propertyRowValueCell,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -279,29 +279,51 @@ const CustomPropertyRowsList = ({
|
|||||||
|
|
||||||
return <CustomPropertyRows properties={filtered} statistics={statistics} />;
|
return <CustomPropertyRows properties={filtered} statistics={statistics} />;
|
||||||
} else {
|
} else {
|
||||||
const required = properties.filter(property => property.required);
|
const partition = Object.groupBy(properties, p =>
|
||||||
const optional = properties.filter(property => !property.required);
|
p.required ? 'required' : p.readonly ? 'readonly' : 'optional'
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{required.length > 0 ? (
|
{partition.required && partition.required.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className={styles.subListHeader}>
|
<div className={styles.subListHeader}>
|
||||||
{t[
|
{t[
|
||||||
'com.affine.settings.workspace.properties.required-properties'
|
'com.affine.settings.workspace.properties.required-properties'
|
||||||
]()}
|
]()}
|
||||||
</div>
|
</div>
|
||||||
<CustomPropertyRows properties={required} statistics={statistics} />
|
<CustomPropertyRows
|
||||||
|
properties={partition.required}
|
||||||
|
statistics={statistics}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{optional.length > 0 ? (
|
{partition.optional && partition.optional.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className={styles.subListHeader}>
|
<div className={styles.subListHeader}>
|
||||||
{t[
|
{t[
|
||||||
'com.affine.settings.workspace.properties.general-properties'
|
'com.affine.settings.workspace.properties.general-properties'
|
||||||
]()}
|
]()}
|
||||||
</div>
|
</div>
|
||||||
<CustomPropertyRows properties={optional} statistics={statistics} />
|
<CustomPropertyRows
|
||||||
|
properties={partition.optional}
|
||||||
|
statistics={statistics}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{partition.readonly && partition.readonly.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div className={styles.subListHeader}>
|
||||||
|
{t[
|
||||||
|
'com.affine.settings.workspace.properties.readonly-properties'
|
||||||
|
]()}
|
||||||
|
</div>
|
||||||
|
<CustomPropertyRows
|
||||||
|
properties={partition.readonly}
|
||||||
|
statistics={statistics}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import type { GetWorkspacePageMetaByIdQuery } from '@affine/graphql';
|
||||||
|
import type { DocService, GlobalCache } from '@toeverything/infra';
|
||||||
|
import {
|
||||||
|
backoffRetry,
|
||||||
|
catchErrorInto,
|
||||||
|
effect,
|
||||||
|
Entity,
|
||||||
|
exhaustMapWithTrailing,
|
||||||
|
fromPromise,
|
||||||
|
LiveData,
|
||||||
|
onComplete,
|
||||||
|
onStart,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { EMPTY, mergeMap } from 'rxjs';
|
||||||
|
|
||||||
|
import { isBackendError, isNetworkError } from '../error';
|
||||||
|
import type { CloudDocMetaStore } from '../stores/cloud-doc-meta';
|
||||||
|
|
||||||
|
export type CloudDocMetaType =
|
||||||
|
GetWorkspacePageMetaByIdQuery['workspace']['pageMeta'];
|
||||||
|
|
||||||
|
const CACHE_KEY_PREFIX = 'cloud-doc-meta:';
|
||||||
|
|
||||||
|
export class CloudDocMeta extends Entity {
|
||||||
|
constructor(
|
||||||
|
private readonly store: CloudDocMetaStore,
|
||||||
|
private readonly docService: DocService,
|
||||||
|
private readonly cache: GlobalCache
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly docId = this.docService.doc.id;
|
||||||
|
readonly workspaceId = this.docService.doc.workspace.id;
|
||||||
|
|
||||||
|
readonly cacheKey = `${CACHE_KEY_PREFIX}${this.workspaceId}:${this.docId}`;
|
||||||
|
meta$ = LiveData.from<CloudDocMetaType | undefined>(
|
||||||
|
this.cache.watch<CloudDocMetaType>(this.cacheKey),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
isRevalidating$ = new LiveData(false);
|
||||||
|
error$ = new LiveData<any | null>(null);
|
||||||
|
|
||||||
|
revalidate = effect(
|
||||||
|
exhaustMapWithTrailing(() => {
|
||||||
|
return fromPromise(
|
||||||
|
this.store.fetchCloudDocMeta(this.workspaceId, this.docId)
|
||||||
|
).pipe(
|
||||||
|
backoffRetry({
|
||||||
|
when: isNetworkError,
|
||||||
|
count: Infinity,
|
||||||
|
}),
|
||||||
|
backoffRetry({
|
||||||
|
when: isBackendError,
|
||||||
|
}),
|
||||||
|
mergeMap(meta => {
|
||||||
|
this.cache.set<CloudDocMetaType>(this.cacheKey, meta);
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
catchErrorInto(this.error$),
|
||||||
|
onStart(() => this.isRevalidating$.next(true)),
|
||||||
|
onComplete(() => this.isRevalidating$.next(false))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,11 +16,15 @@ export { UserQuotaService } from './services/user-quota';
|
|||||||
export { WebSocketService } from './services/websocket';
|
export { WebSocketService } from './services/websocket';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
DocScope,
|
||||||
|
DocService,
|
||||||
type Framework,
|
type Framework,
|
||||||
GlobalCacheService,
|
GlobalCache,
|
||||||
GlobalStateService,
|
GlobalState,
|
||||||
|
WorkspaceScope,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { CloudDocMeta } from './entities/cloud-doc-meta';
|
||||||
import { ServerConfig } from './entities/server-config';
|
import { ServerConfig } from './entities/server-config';
|
||||||
import { AuthSession } from './entities/session';
|
import { AuthSession } from './entities/session';
|
||||||
import { Subscription } from './entities/subscription';
|
import { Subscription } from './entities/subscription';
|
||||||
@@ -29,6 +33,7 @@ import { UserCopilotQuota } from './entities/user-copilot-quota';
|
|||||||
import { UserFeature } from './entities/user-feature';
|
import { UserFeature } from './entities/user-feature';
|
||||||
import { UserQuota } from './entities/user-quota';
|
import { UserQuota } from './entities/user-quota';
|
||||||
import { AuthService } from './services/auth';
|
import { AuthService } from './services/auth';
|
||||||
|
import { CloudDocMetaService } from './services/cloud-doc-meta';
|
||||||
import { FetchService } from './services/fetch';
|
import { FetchService } from './services/fetch';
|
||||||
import { GraphQLService } from './services/graphql';
|
import { GraphQLService } from './services/graphql';
|
||||||
import { ServerConfigService } from './services/server-config';
|
import { ServerConfigService } from './services/server-config';
|
||||||
@@ -38,6 +43,7 @@ import { UserFeatureService } from './services/user-feature';
|
|||||||
import { UserQuotaService } from './services/user-quota';
|
import { UserQuotaService } from './services/user-quota';
|
||||||
import { WebSocketService } from './services/websocket';
|
import { WebSocketService } from './services/websocket';
|
||||||
import { AuthStore } from './stores/auth';
|
import { AuthStore } from './stores/auth';
|
||||||
|
import { CloudDocMetaStore } from './stores/cloud-doc-meta';
|
||||||
import { ServerConfigStore } from './stores/server-config';
|
import { ServerConfigStore } from './stores/server-config';
|
||||||
import { SubscriptionStore } from './stores/subscription';
|
import { SubscriptionStore } from './stores/subscription';
|
||||||
import { UserCopilotQuotaStore } from './stores/user-copilot-quota';
|
import { UserCopilotQuotaStore } from './stores/user-copilot-quota';
|
||||||
@@ -53,10 +59,10 @@ export function configureCloudModule(framework: Framework) {
|
|||||||
.entity(ServerConfig, [ServerConfigStore])
|
.entity(ServerConfig, [ServerConfigStore])
|
||||||
.store(ServerConfigStore, [GraphQLService])
|
.store(ServerConfigStore, [GraphQLService])
|
||||||
.service(AuthService, [FetchService, AuthStore])
|
.service(AuthService, [FetchService, AuthStore])
|
||||||
.store(AuthStore, [FetchService, GraphQLService, GlobalStateService])
|
.store(AuthStore, [FetchService, GraphQLService, GlobalState])
|
||||||
.entity(AuthSession, [AuthStore])
|
.entity(AuthSession, [AuthStore])
|
||||||
.service(SubscriptionService, [SubscriptionStore])
|
.service(SubscriptionService, [SubscriptionStore])
|
||||||
.store(SubscriptionStore, [GraphQLService, GlobalCacheService])
|
.store(SubscriptionStore, [GraphQLService, GlobalCache])
|
||||||
.entity(Subscription, [AuthService, ServerConfigService, SubscriptionStore])
|
.entity(Subscription, [AuthService, ServerConfigService, SubscriptionStore])
|
||||||
.entity(SubscriptionPrices, [ServerConfigService, SubscriptionStore])
|
.entity(SubscriptionPrices, [ServerConfigService, SubscriptionStore])
|
||||||
.service(UserQuotaService)
|
.service(UserQuotaService)
|
||||||
@@ -71,5 +77,10 @@ export function configureCloudModule(framework: Framework) {
|
|||||||
])
|
])
|
||||||
.service(UserFeatureService)
|
.service(UserFeatureService)
|
||||||
.entity(UserFeature, [AuthService, UserFeatureStore])
|
.entity(UserFeature, [AuthService, UserFeatureStore])
|
||||||
.store(UserFeatureStore, [GraphQLService]);
|
.store(UserFeatureStore, [GraphQLService])
|
||||||
|
.scope(WorkspaceScope)
|
||||||
|
.scope(DocScope)
|
||||||
|
.service(CloudDocMetaService)
|
||||||
|
.entity(CloudDocMeta, [CloudDocMetaStore, DocService, GlobalCache])
|
||||||
|
.store(CloudDocMetaStore, [GraphQLService]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { CloudDocMeta } from '../entities/cloud-doc-meta';
|
||||||
|
|
||||||
|
export class CloudDocMetaService extends Service {
|
||||||
|
cloudDocMeta = this.framework.createEntity(CloudDocMeta);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
updateUserProfileMutation,
|
updateUserProfileMutation,
|
||||||
uploadAvatarMutation,
|
uploadAvatarMutation,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import type { GlobalStateService } from '@toeverything/infra';
|
import type { GlobalState } from '@toeverything/infra';
|
||||||
import { Store } from '@toeverything/infra';
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
import type { AuthSessionInfo } from '../entities/session';
|
import type { AuthSessionInfo } from '../entities/session';
|
||||||
@@ -24,19 +24,17 @@ export class AuthStore extends Store {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly fetchService: FetchService,
|
private readonly fetchService: FetchService,
|
||||||
private readonly gqlService: GraphQLService,
|
private readonly gqlService: GraphQLService,
|
||||||
private readonly globalStateService: GlobalStateService
|
private readonly globalState: GlobalState
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
watchCachedAuthSession() {
|
watchCachedAuthSession() {
|
||||||
return this.globalStateService.globalState.watch<AuthSessionInfo>(
|
return this.globalState.watch<AuthSessionInfo>('affine-cloud-auth');
|
||||||
'affine-cloud-auth'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setCachedAuthSession(session: AuthSessionInfo | null) {
|
setCachedAuthSession(session: AuthSessionInfo | null) {
|
||||||
this.globalStateService.globalState.set('affine-cloud-auth', session);
|
this.globalState.set('affine-cloud-auth', session);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchSession() {
|
async fetchSession() {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { getWorkspacePageMetaByIdQuery } from '@affine/graphql';
|
||||||
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { type CloudDocMetaType } from '../entities/cloud-doc-meta';
|
||||||
|
import type { GraphQLService } from '../services/graphql';
|
||||||
|
|
||||||
|
export class CloudDocMetaStore extends Store {
|
||||||
|
constructor(private readonly gqlService: GraphQLService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchCloudDocMeta(
|
||||||
|
workspaceId: string,
|
||||||
|
docId: string,
|
||||||
|
abortSignal?: AbortSignal
|
||||||
|
): Promise<CloudDocMetaType> {
|
||||||
|
const serverConfigData = await this.gqlService.gql({
|
||||||
|
query: getWorkspacePageMetaByIdQuery,
|
||||||
|
variables: { id: workspaceId, pageId: docId },
|
||||||
|
context: {
|
||||||
|
signal: abortSignal,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return serverConfigData.workspace.pageMeta;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
subscriptionQuery,
|
subscriptionQuery,
|
||||||
updateSubscriptionMutation,
|
updateSubscriptionMutation,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import type { GlobalCacheService } from '@toeverything/infra';
|
import type { GlobalCache } from '@toeverything/infra';
|
||||||
import { Store } from '@toeverything/infra';
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
import type { SubscriptionType } from '../entities/subscription';
|
import type { SubscriptionType } from '../entities/subscription';
|
||||||
@@ -37,7 +37,7 @@ const getDefaultSubscriptionSuccessCallbackLink = (
|
|||||||
export class SubscriptionStore extends Store {
|
export class SubscriptionStore extends Store {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly gqlService: GraphQLService,
|
private readonly gqlService: GraphQLService,
|
||||||
private readonly globalCacheService: GlobalCacheService
|
private readonly globalCache: GlobalCache
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -97,16 +97,13 @@ export class SubscriptionStore extends Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getCachedSubscriptions(userId: string) {
|
getCachedSubscriptions(userId: string) {
|
||||||
return this.globalCacheService.globalCache.get<SubscriptionType[]>(
|
return this.globalCache.get<SubscriptionType[]>(
|
||||||
SUBSCRIPTION_CACHE_KEY + userId
|
SUBSCRIPTION_CACHE_KEY + userId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
setCachedSubscriptions(userId: string, subscriptions: SubscriptionType[]) {
|
setCachedSubscriptions(userId: string, subscriptions: SubscriptionType[]) {
|
||||||
return this.globalCacheService.globalCache.set(
|
return this.globalCache.set(SUBSCRIPTION_CACHE_KEY + userId, subscriptions);
|
||||||
SUBSCRIPTION_CACHE_KEY + userId,
|
|
||||||
subscriptions
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setSubscriptionRecurring(
|
setSubscriptionRecurring(
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export enum PagePropertyType {
|
|||||||
Progress = 'progress',
|
Progress = 'progress',
|
||||||
Checkbox = 'checkbox',
|
Checkbox = 'checkbox',
|
||||||
Tags = 'tags',
|
Tags = 'tags',
|
||||||
|
CreatedBy = 'createdBy',
|
||||||
|
UpdatedBy = 'updatedBy',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PagePropertyMetaBaseSchema = z.object({
|
export const PagePropertyMetaBaseSchema = z.object({
|
||||||
@@ -30,6 +32,7 @@ export const PagePropertyMetaBaseSchema = z.object({
|
|||||||
type: z.nativeEnum(PagePropertyType),
|
type: z.nativeEnum(PagePropertyType),
|
||||||
icon: z.string(),
|
icon: z.string(),
|
||||||
required: z.boolean().optional(),
|
required: z.boolean().optional(),
|
||||||
|
readonly: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PageSystemPropertyMetaBaseSchema =
|
export const PageSystemPropertyMetaBaseSchema =
|
||||||
|
|||||||
@@ -6,12 +6,13 @@ import {
|
|||||||
catchErrorInto,
|
catchErrorInto,
|
||||||
effect,
|
effect,
|
||||||
Entity,
|
Entity,
|
||||||
|
exhaustMapWithTrailing,
|
||||||
fromPromise,
|
fromPromise,
|
||||||
LiveData,
|
LiveData,
|
||||||
onComplete,
|
onComplete,
|
||||||
onStart,
|
onStart,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { EMPTY, mergeMap, switchMap } from 'rxjs';
|
import { EMPTY, mergeMap } from 'rxjs';
|
||||||
|
|
||||||
import { isBackendError, isNetworkError } from '../../cloud';
|
import { isBackendError, isNetworkError } from '../../cloud';
|
||||||
import type { ShareDocsStore } from '../stores/share-docs';
|
import type { ShareDocsStore } from '../stores/share-docs';
|
||||||
@@ -35,7 +36,7 @@ export class ShareDocsList extends Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
revalidate = effect(
|
revalidate = effect(
|
||||||
switchMap(() =>
|
exhaustMapWithTrailing(() =>
|
||||||
fromPromise(signal => {
|
fromPromise(signal => {
|
||||||
return this.store.getWorkspacesShareDocs(
|
return this.store.getWorkspacesShareDocs(
|
||||||
this.workspaceService.workspace.id,
|
this.workspaceService.workspace.id,
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
query getWorkspacePageMetaById($id: String!, $pageId: String!) {
|
||||||
|
workspace(id: $id) {
|
||||||
|
pageMeta(pageId: $pageId) {
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
createdBy {
|
||||||
|
name
|
||||||
|
avatarUrl
|
||||||
|
}
|
||||||
|
updatedBy {
|
||||||
|
name
|
||||||
|
avatarUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,10 @@ query listHistory(
|
|||||||
histories(guid: $pageDocId, take: $take, before: $before) {
|
histories(guid: $pageDocId, take: $take, before: $before) {
|
||||||
id
|
id
|
||||||
timestamp
|
timestamp
|
||||||
|
editor {
|
||||||
|
name
|
||||||
|
avatarUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -610,6 +610,30 @@ query getWorkspaceFeatures($workspaceId: String!) {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getWorkspacePageMetaByIdQuery = {
|
||||||
|
id: 'getWorkspacePageMetaByIdQuery' as const,
|
||||||
|
operationName: 'getWorkspacePageMetaById',
|
||||||
|
definitionName: 'workspace',
|
||||||
|
containsFile: false,
|
||||||
|
query: `
|
||||||
|
query getWorkspacePageMetaById($id: String!, $pageId: String!) {
|
||||||
|
workspace(id: $id) {
|
||||||
|
pageMeta(pageId: $pageId) {
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
createdBy {
|
||||||
|
name
|
||||||
|
avatarUrl
|
||||||
|
}
|
||||||
|
updatedBy {
|
||||||
|
name
|
||||||
|
avatarUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
export const getWorkspacePublicByIdQuery = {
|
export const getWorkspacePublicByIdQuery = {
|
||||||
id: 'getWorkspacePublicByIdQuery' as const,
|
id: 'getWorkspacePublicByIdQuery' as const,
|
||||||
operationName: 'getWorkspacePublicById',
|
operationName: 'getWorkspacePublicById',
|
||||||
@@ -696,6 +720,10 @@ query listHistory($workspaceId: String!, $pageDocId: String!, $take: Int, $befor
|
|||||||
histories(guid: $pageDocId, take: $take, before: $before) {
|
histories(guid: $pageDocId, take: $take, before: $before) {
|
||||||
id
|
id
|
||||||
timestamp
|
timestamp
|
||||||
|
editor {
|
||||||
|
name
|
||||||
|
avatarUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
|
|||||||
@@ -238,6 +238,7 @@ export interface DocHistoryNotFoundDataType {
|
|||||||
|
|
||||||
export interface DocHistoryType {
|
export interface DocHistoryType {
|
||||||
__typename?: 'DocHistoryType';
|
__typename?: 'DocHistoryType';
|
||||||
|
editor: Maybe<EditorType>;
|
||||||
id: Scalars['String']['output'];
|
id: Scalars['String']['output'];
|
||||||
timestamp: Scalars['DateTime']['output'];
|
timestamp: Scalars['DateTime']['output'];
|
||||||
workspaceId: Scalars['String']['output'];
|
workspaceId: Scalars['String']['output'];
|
||||||
@@ -249,6 +250,12 @@ export interface DocNotFoundDataType {
|
|||||||
spaceId: Scalars['String']['output'];
|
spaceId: Scalars['String']['output'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EditorType {
|
||||||
|
__typename?: 'EditorType';
|
||||||
|
avatarUrl: Maybe<Scalars['String']['output']>;
|
||||||
|
name: Scalars['String']['output'];
|
||||||
|
}
|
||||||
|
|
||||||
export type ErrorDataUnion =
|
export type ErrorDataUnion =
|
||||||
| AlreadyInSpaceDataType
|
| AlreadyInSpaceDataType
|
||||||
| BlobNotFoundDataType
|
| BlobNotFoundDataType
|
||||||
@@ -1189,6 +1196,14 @@ export interface WorkspacePage {
|
|||||||
workspaceId: Scalars['String']['output'];
|
workspaceId: Scalars['String']['output'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorkspacePageMeta {
|
||||||
|
__typename?: 'WorkspacePageMeta';
|
||||||
|
createdAt: Scalars['DateTime']['output'];
|
||||||
|
createdBy: Maybe<EditorType>;
|
||||||
|
updatedAt: Scalars['DateTime']['output'];
|
||||||
|
updatedBy: Maybe<EditorType>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WorkspaceType {
|
export interface WorkspaceType {
|
||||||
__typename?: 'WorkspaceType';
|
__typename?: 'WorkspaceType';
|
||||||
/** Available features of workspace */
|
/** Available features of workspace */
|
||||||
@@ -1211,6 +1226,8 @@ export interface WorkspaceType {
|
|||||||
members: Array<InviteUserType>;
|
members: Array<InviteUserType>;
|
||||||
/** Owner of workspace */
|
/** Owner of workspace */
|
||||||
owner: UserType;
|
owner: UserType;
|
||||||
|
/** Cloud page metadata of workspace */
|
||||||
|
pageMeta: WorkspacePageMeta;
|
||||||
/** Permission of current signed in user in workspace */
|
/** Permission of current signed in user in workspace */
|
||||||
permission: Permission;
|
permission: Permission;
|
||||||
/** is Public workspace */
|
/** is Public workspace */
|
||||||
@@ -1239,6 +1256,10 @@ export interface WorkspaceTypeMembersArgs {
|
|||||||
take: InputMaybe<Scalars['Int']['input']>;
|
take: InputMaybe<Scalars['Int']['input']>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorkspaceTypePageMetaArgs {
|
||||||
|
pageId: Scalars['String']['input'];
|
||||||
|
}
|
||||||
|
|
||||||
export interface WorkspaceTypePublicPageArgs {
|
export interface WorkspaceTypePublicPageArgs {
|
||||||
pageId: Scalars['String']['input'];
|
pageId: Scalars['String']['input'];
|
||||||
}
|
}
|
||||||
@@ -1787,6 +1808,33 @@ export type GetWorkspaceFeaturesQuery = {
|
|||||||
workspace: { __typename?: 'WorkspaceType'; features: Array<FeatureType> };
|
workspace: { __typename?: 'WorkspaceType'; features: Array<FeatureType> };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetWorkspacePageMetaByIdQueryVariables = Exact<{
|
||||||
|
id: Scalars['String']['input'];
|
||||||
|
pageId: Scalars['String']['input'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type GetWorkspacePageMetaByIdQuery = {
|
||||||
|
__typename?: 'Query';
|
||||||
|
workspace: {
|
||||||
|
__typename?: 'WorkspaceType';
|
||||||
|
pageMeta: {
|
||||||
|
__typename?: 'WorkspacePageMeta';
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdBy: {
|
||||||
|
__typename?: 'EditorType';
|
||||||
|
name: string;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
} | null;
|
||||||
|
updatedBy: {
|
||||||
|
__typename?: 'EditorType';
|
||||||
|
name: string;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type GetWorkspacePublicByIdQueryVariables = Exact<{
|
export type GetWorkspacePublicByIdQueryVariables = Exact<{
|
||||||
id: Scalars['String']['input'];
|
id: Scalars['String']['input'];
|
||||||
}>;
|
}>;
|
||||||
@@ -1865,6 +1913,11 @@ export type ListHistoryQuery = {
|
|||||||
__typename?: 'DocHistoryType';
|
__typename?: 'DocHistoryType';
|
||||||
id: string;
|
id: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
|
editor: {
|
||||||
|
__typename?: 'EditorType';
|
||||||
|
name: string;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
} | null;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -2487,6 +2540,11 @@ export type Queries =
|
|||||||
variables: GetWorkspaceFeaturesQueryVariables;
|
variables: GetWorkspaceFeaturesQueryVariables;
|
||||||
response: GetWorkspaceFeaturesQuery;
|
response: GetWorkspaceFeaturesQuery;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'getWorkspacePageMetaByIdQuery';
|
||||||
|
variables: GetWorkspacePageMetaByIdQueryVariables;
|
||||||
|
response: GetWorkspacePageMetaByIdQuery;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'getWorkspacePublicByIdQuery';
|
name: 'getWorkspacePublicByIdQuery';
|
||||||
variables: GetWorkspacePublicByIdQueryVariables;
|
variables: GetWorkspacePublicByIdQueryVariables;
|
||||||
|
|||||||
@@ -858,6 +858,8 @@
|
|||||||
"com.affine.page-properties.page-info": "Info",
|
"com.affine.page-properties.page-info": "Info",
|
||||||
"com.affine.page-properties.page-info.view": "View Info",
|
"com.affine.page-properties.page-info.view": "View Info",
|
||||||
"com.affine.page-properties.property-value-placeholder": "Empty",
|
"com.affine.page-properties.property-value-placeholder": "Empty",
|
||||||
|
"com.affine.page-properties.property-user-avatar-no-record": "No Record",
|
||||||
|
"com.affine.page-properties.property-user-local": "Local User",
|
||||||
"com.affine.page-properties.property.always-hide": "Always hide",
|
"com.affine.page-properties.property.always-hide": "Always hide",
|
||||||
"com.affine.page-properties.property.always-show": "Always show",
|
"com.affine.page-properties.property.always-show": "Always show",
|
||||||
"com.affine.page-properties.property.checkbox": "Checkbox",
|
"com.affine.page-properties.property.checkbox": "Checkbox",
|
||||||
@@ -872,6 +874,9 @@
|
|||||||
"com.affine.page-properties.property.show-in-view": "Show in view",
|
"com.affine.page-properties.property.show-in-view": "Show in view",
|
||||||
"com.affine.page-properties.property.tags": "Tags",
|
"com.affine.page-properties.property.tags": "Tags",
|
||||||
"com.affine.page-properties.property.text": "Text",
|
"com.affine.page-properties.property.text": "Text",
|
||||||
|
"com.affine.page-properties.property.createdBy": "Created by",
|
||||||
|
"com.affine.page-properties.property.updatedBy": "Last edited by",
|
||||||
|
"com.affine.page-properties.local-user": "Local user",
|
||||||
"com.affine.page-properties.settings.title": "customize properties",
|
"com.affine.page-properties.settings.title": "customize properties",
|
||||||
"com.affine.page-properties.tags.open-tags-page": "Open tag page",
|
"com.affine.page-properties.tags.open-tags-page": "Open tag page",
|
||||||
"com.affine.page-properties.tags.selector-header-title": "Select tag or create one",
|
"com.affine.page-properties.tags.selector-header-title": "Select tag or create one",
|
||||||
@@ -1356,6 +1361,7 @@
|
|||||||
"com.affine.settings.workspace.properties.doc_others": "<0>{{count}}</0> docs",
|
"com.affine.settings.workspace.properties.doc_others": "<0>{{count}}</0> docs",
|
||||||
"com.affine.settings.workspace.properties.edit-property": "Edit property",
|
"com.affine.settings.workspace.properties.edit-property": "Edit property",
|
||||||
"com.affine.settings.workspace.properties.general-properties": "General properties",
|
"com.affine.settings.workspace.properties.general-properties": "General properties",
|
||||||
|
"com.affine.settings.workspace.properties.readonly-properties": "Readonly properties",
|
||||||
"com.affine.settings.workspace.properties.header.subtitle": "Manage workspace <1>{{name}}</1> properties",
|
"com.affine.settings.workspace.properties.header.subtitle": "Manage workspace <1>{{name}}</1> properties",
|
||||||
"com.affine.settings.workspace.properties.header.title": "Properties",
|
"com.affine.settings.workspace.properties.header.title": "Properties",
|
||||||
"com.affine.settings.workspace.properties.in-use": "In use",
|
"com.affine.settings.workspace.properties.in-use": "In use",
|
||||||
|
|||||||
@@ -137,4 +137,6 @@ test('add custom property', async ({ page }) => {
|
|||||||
await addCustomProperty(page, 'Number');
|
await addCustomProperty(page, 'Number');
|
||||||
await addCustomProperty(page, 'Date');
|
await addCustomProperty(page, 'Date');
|
||||||
await addCustomProperty(page, 'Checkbox');
|
await addCustomProperty(page, 'Checkbox');
|
||||||
|
await addCustomProperty(page, 'Created by');
|
||||||
|
await addCustomProperty(page, 'Last edited by');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ test('add custom property', async ({ page }) => {
|
|||||||
await addCustomProperty(page, 'Number');
|
await addCustomProperty(page, 'Number');
|
||||||
await addCustomProperty(page, 'Date');
|
await addCustomProperty(page, 'Date');
|
||||||
await addCustomProperty(page, 'Checkbox');
|
await addCustomProperty(page, 'Checkbox');
|
||||||
|
await addCustomProperty(page, 'Created by');
|
||||||
|
await addCustomProperty(page, 'Last edited by');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('add custom property & edit', async ({ page }) => {
|
test('add custom property & edit', async ({ page }) => {
|
||||||
@@ -103,6 +105,8 @@ test('property table reordering', async ({ page }) => {
|
|||||||
await addCustomProperty(page, 'Number');
|
await addCustomProperty(page, 'Number');
|
||||||
await addCustomProperty(page, 'Date');
|
await addCustomProperty(page, 'Date');
|
||||||
await addCustomProperty(page, 'Checkbox');
|
await addCustomProperty(page, 'Checkbox');
|
||||||
|
await addCustomProperty(page, 'Created by');
|
||||||
|
await addCustomProperty(page, 'Last edited by');
|
||||||
|
|
||||||
await dragTo(
|
await dragTo(
|
||||||
page,
|
page,
|
||||||
@@ -119,6 +123,8 @@ test('property table reordering', async ({ page }) => {
|
|||||||
'Date',
|
'Date',
|
||||||
'Checkbox',
|
'Checkbox',
|
||||||
'Text',
|
'Text',
|
||||||
|
'Created by',
|
||||||
|
'Last edited by',
|
||||||
].entries()) {
|
].entries()) {
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
@@ -141,6 +147,8 @@ test('page info show more will show all properties', async ({ page }) => {
|
|||||||
await addCustomProperty(page, 'Number');
|
await addCustomProperty(page, 'Number');
|
||||||
await addCustomProperty(page, 'Date');
|
await addCustomProperty(page, 'Date');
|
||||||
await addCustomProperty(page, 'Checkbox');
|
await addCustomProperty(page, 'Checkbox');
|
||||||
|
await addCustomProperty(page, 'Created by');
|
||||||
|
await addCustomProperty(page, 'Last edited by');
|
||||||
|
|
||||||
await expect(page.getByTestId('page-info-show-more')).toBeVisible();
|
await expect(page.getByTestId('page-info-show-more')).toBeVisible();
|
||||||
await page.click('[data-testid="page-info-show-more"]');
|
await page.click('[data-testid="page-info-show-more"]');
|
||||||
@@ -156,6 +164,8 @@ test('page info show more will show all properties', async ({ page }) => {
|
|||||||
'Number',
|
'Number',
|
||||||
'Date',
|
'Date',
|
||||||
'Checkbox',
|
'Checkbox',
|
||||||
|
'Created by',
|
||||||
|
'Last edited by',
|
||||||
].entries()) {
|
].entries()) {
|
||||||
await expect(
|
await expect(
|
||||||
page
|
page
|
||||||
|
|||||||
Reference in New Issue
Block a user