fix(server): realtime module not loaded (#14952)
#### PR Dependency Tree * **PR #14952** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Optimized workspace invite link fetching by separating it from general workspace configuration queries for improved performance. * Reorganized transcription-related backend modules to better separate concerns and enable real-time functionality. * **Chores** * Updated generated GraphQL types and iOS query definitions to reflect API changes. [](https://app.coderabbit.ai/change-stack/toeverything/AFFiNE/pull/14952) <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -55,7 +55,7 @@ import { Env } from './env';
|
|||||||
import { ModelsModule } from './models';
|
import { ModelsModule } from './models';
|
||||||
import { CalendarModule } from './plugins/calendar';
|
import { CalendarModule } from './plugins/calendar';
|
||||||
import { CaptchaModule } from './plugins/captcha';
|
import { CaptchaModule } from './plugins/captcha';
|
||||||
import { CopilotModule } from './plugins/copilot';
|
import { CopilotModule, CopilotRealtimeModule } from './plugins/copilot';
|
||||||
import { CustomerIoModule } from './plugins/customerio';
|
import { CustomerIoModule } from './plugins/customerio';
|
||||||
import { GCloudModule } from './plugins/gcloud';
|
import { GCloudModule } from './plugins/gcloud';
|
||||||
import { IndexerModule } from './plugins/indexer';
|
import { IndexerModule } from './plugins/indexer';
|
||||||
@@ -185,7 +185,8 @@ export function buildAppModule(env: Env) {
|
|||||||
.useIf(
|
.useIf(
|
||||||
() => env.flavors.sync || env.flavors.front,
|
() => env.flavors.sync || env.flavors.front,
|
||||||
SyncModule,
|
SyncModule,
|
||||||
TelemetryModule
|
TelemetryModule,
|
||||||
|
CopilotRealtimeModule
|
||||||
)
|
)
|
||||||
// graphql server only
|
// graphql server only
|
||||||
.useIf(
|
.useIf(
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import { getRealtimeInputKey } from '@affine/realtime';
|
|||||||
import test from 'ava';
|
import test from 'ava';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { CopilotTranscriptionReader } from '../../../plugins/copilot/transcript';
|
||||||
|
import { CopilotTranscriptRealtimeProvider } from '../../../plugins/copilot/transcript';
|
||||||
import type { CurrentUser } from '../../auth';
|
import type { CurrentUser } from '../../auth';
|
||||||
|
import type { AccessController } from '../../permission';
|
||||||
import { RealtimeGateway } from '../gateway';
|
import { RealtimeGateway } from '../gateway';
|
||||||
import {
|
import {
|
||||||
realtimeCommentRoom,
|
realtimeCommentRoom,
|
||||||
@@ -191,6 +194,61 @@ test('registerRealtimeLiveQuery registers paired request and topic handlers', as
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('copilot transcript realtime provider registers task live query handlers', async t => {
|
||||||
|
const registry = new RealtimeRegistry();
|
||||||
|
const assertions: unknown[] = [];
|
||||||
|
const ac = {
|
||||||
|
user(userId: string) {
|
||||||
|
return {
|
||||||
|
workspace(workspaceId: string) {
|
||||||
|
return {
|
||||||
|
allowLocal() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
async assert(action: string) {
|
||||||
|
assertions.push({ userId, workspaceId, action });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} as unknown as AccessController;
|
||||||
|
const transcript = {
|
||||||
|
async queryTask(
|
||||||
|
userId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
taskId?: string,
|
||||||
|
blobId?: string
|
||||||
|
) {
|
||||||
|
return { id: taskId ?? blobId, status: 'finished', userId, workspaceId };
|
||||||
|
},
|
||||||
|
} as unknown as CopilotTranscriptionReader;
|
||||||
|
|
||||||
|
new CopilotTranscriptRealtimeProvider(
|
||||||
|
ac,
|
||||||
|
transcript,
|
||||||
|
registry
|
||||||
|
).onModuleInit();
|
||||||
|
|
||||||
|
t.deepEqual(
|
||||||
|
await registry.getRequest('copilot.transcript.task.get').handle(user, {
|
||||||
|
workspaceId: 'space',
|
||||||
|
taskId: 'task',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
task: {
|
||||||
|
id: 'task',
|
||||||
|
status: 'finished',
|
||||||
|
userId: 'u1',
|
||||||
|
workspaceId: 'space',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
t.deepEqual(assertions, [
|
||||||
|
{ userId: 'u1', workspaceId: 'space', action: 'Workspace.Copilot' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test('publisher emits realtime event with shared input key', t => {
|
test('publisher emits realtime event with shared input key', t => {
|
||||||
const registry = new RealtimeRegistry();
|
const registry = new RealtimeRegistry();
|
||||||
registry.registerTopic({
|
registry.registerTopic({
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
COPILOT_API_PROVIDERS,
|
COPILOT_API_PROVIDERS,
|
||||||
COPILOT_FEATURE_PROVIDERS,
|
COPILOT_FEATURE_PROVIDERS,
|
||||||
COPILOT_KERNEL_PROVIDERS,
|
COPILOT_KERNEL_PROVIDERS,
|
||||||
|
COPILOT_TRANSCRIPT_REALTIME_PROVIDERS,
|
||||||
} from './module-providers';
|
} from './module-providers';
|
||||||
|
|
||||||
const COPILOT_SHARED_IMPORTS = [
|
const COPILOT_SHARED_IMPORTS = [
|
||||||
@@ -36,6 +37,12 @@ const COPILOT_SHARED_IMPORTS = [
|
|||||||
})
|
})
|
||||||
export class CopilotKernelModule {}
|
export class CopilotKernelModule {}
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PermissionModule],
|
||||||
|
providers: [...COPILOT_TRANSCRIPT_REALTIME_PROVIDERS],
|
||||||
|
})
|
||||||
|
export class CopilotRealtimeModule {}
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [...COPILOT_SHARED_IMPORTS, CopilotKernelModule],
|
imports: [...COPILOT_SHARED_IMPORTS, CopilotKernelModule],
|
||||||
providers: [...COPILOT_FEATURE_PROVIDERS],
|
providers: [...COPILOT_FEATURE_PROVIDERS],
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import { TurnOrchestrator } from './runtime/turn-orchestrator';
|
|||||||
import { ChatSessionService } from './session';
|
import { ChatSessionService } from './session';
|
||||||
import { CopilotStorage } from './storage';
|
import { CopilotStorage } from './storage';
|
||||||
import {
|
import {
|
||||||
|
CopilotTranscriptionReader,
|
||||||
CopilotTranscriptionResolver,
|
CopilotTranscriptionResolver,
|
||||||
CopilotTranscriptionService,
|
CopilotTranscriptionService,
|
||||||
CopilotTranscriptRealtimeProvider,
|
CopilotTranscriptRealtimeProvider,
|
||||||
@@ -113,10 +114,15 @@ export const COPILOT_CONTEXT_PROVIDERS = [
|
|||||||
CopilotEmbeddingRealtimeProvider,
|
CopilotEmbeddingRealtimeProvider,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const COPILOT_TRANSCRIPT_REALTIME_PROVIDERS = [
|
||||||
|
CopilotTranscriptionReader,
|
||||||
|
CopilotTranscriptRealtimeProvider,
|
||||||
|
];
|
||||||
|
|
||||||
export const COPILOT_TRANSCRIPT_PROVIDERS = [
|
export const COPILOT_TRANSCRIPT_PROVIDERS = [
|
||||||
CopilotTranscriptionService,
|
CopilotTranscriptionService,
|
||||||
CopilotTranscriptionResolver,
|
CopilotTranscriptionResolver,
|
||||||
CopilotTranscriptRealtimeProvider,
|
...COPILOT_TRANSCRIPT_REALTIME_PROVIDERS,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const COPILOT_WORKSPACE_PROVIDERS = [
|
export const COPILOT_WORKSPACE_PROVIDERS = [
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export type { TranscriptionJob } from './job';
|
||||||
|
export { CopilotTranscriptionReader } from './reader';
|
||||||
export { CopilotTranscriptRealtimeProvider } from './realtime';
|
export { CopilotTranscriptRealtimeProvider } from './realtime';
|
||||||
export { CopilotTranscriptionResolver } from './resolver';
|
export { CopilotTranscriptionResolver } from './resolver';
|
||||||
export { CopilotTranscriptionService } from './service';
|
export { CopilotTranscriptionService } from './service';
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { AiJobStatus } from '@prisma/client';
|
||||||
|
|
||||||
|
import { TranscriptPayloadSchema } from './schema';
|
||||||
|
import type { AudioBlobInfos, TranscriptionPayload } from './types';
|
||||||
|
|
||||||
|
export type TranscriptionJob = {
|
||||||
|
id: string;
|
||||||
|
status: AiJobStatus;
|
||||||
|
infos?: AudioBlobInfos;
|
||||||
|
transcription?: TranscriptionPayload;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function taskStatusToPublicStatus(status: string): AiJobStatus {
|
||||||
|
switch (status) {
|
||||||
|
case 'pending':
|
||||||
|
return AiJobStatus.pending;
|
||||||
|
case 'running':
|
||||||
|
return AiJobStatus.running;
|
||||||
|
case 'ready':
|
||||||
|
case 'settled':
|
||||||
|
return AiJobStatus.finished;
|
||||||
|
default:
|
||||||
|
return AiJobStatus.failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function taskToJob(
|
||||||
|
task: {
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
protectedResult: unknown;
|
||||||
|
} | null,
|
||||||
|
mapStatus: (status: string) => AiJobStatus = taskStatusToPublicStatus
|
||||||
|
): TranscriptionJob | null {
|
||||||
|
if (!task) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = mapStatus(task.status);
|
||||||
|
const ret: TranscriptionJob = {
|
||||||
|
id: task.id,
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
if (task.protectedResult) {
|
||||||
|
const parsed = TranscriptPayloadSchema.safeParse(task.protectedResult);
|
||||||
|
ret.infos = parsed.success ? (parsed.data.infos ?? []) : [];
|
||||||
|
if (task.status === 'settled' && parsed.success) {
|
||||||
|
ret.transcription = parsed.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { Models } from '../../../models';
|
||||||
|
import { taskToJob } from './job';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CopilotTranscriptionReader {
|
||||||
|
constructor(private readonly models: Models) {}
|
||||||
|
|
||||||
|
async queryTask(
|
||||||
|
userId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
taskId?: string,
|
||||||
|
blobId?: string
|
||||||
|
) {
|
||||||
|
const task = await this.models.copilotTranscriptTask.getWithUser(
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
taskId,
|
||||||
|
blobId
|
||||||
|
);
|
||||||
|
return taskToJob(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,13 +8,13 @@ import {
|
|||||||
realtimeTranscriptTaskRoom,
|
realtimeTranscriptTaskRoom,
|
||||||
registerRealtimeLiveQuery,
|
registerRealtimeLiveQuery,
|
||||||
} from '../../../core/realtime';
|
} from '../../../core/realtime';
|
||||||
import { CopilotTranscriptionService } from './service';
|
import { CopilotTranscriptionReader } from './reader';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CopilotTranscriptRealtimeProvider implements OnModuleInit {
|
export class CopilotTranscriptRealtimeProvider implements OnModuleInit {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly ac: AccessController,
|
private readonly ac: AccessController,
|
||||||
private readonly transcript: CopilotTranscriptionService,
|
private readonly transcript: CopilotTranscriptionReader,
|
||||||
@Optional() private readonly registry?: RealtimeRegistry
|
@Optional() private readonly registry?: RealtimeRegistry
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ import {
|
|||||||
import { CurrentUser } from '../../../core/auth';
|
import { CurrentUser } from '../../../core/auth';
|
||||||
import { AccessController } from '../../../core/permission';
|
import { AccessController } from '../../../core/permission';
|
||||||
import { CopilotType } from '../resolver';
|
import { CopilotType } from '../resolver';
|
||||||
|
import type { TranscriptionJob } from './job';
|
||||||
import { buildLegacyProjection } from './projection';
|
import { buildLegacyProjection } from './projection';
|
||||||
import { CopilotTranscriptionService, TranscriptionJob } from './service';
|
import { CopilotTranscriptionService } from './service';
|
||||||
import type {
|
import type {
|
||||||
AudioSliceManifestItem,
|
AudioSliceManifestItem,
|
||||||
MeetingActionItem,
|
MeetingActionItem,
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ import { CopilotProviderType } from '../providers/types';
|
|||||||
import { ActionRuntimeBridge } from '../runtime/action-runtime-bridge';
|
import { ActionRuntimeBridge } from '../runtime/action-runtime-bridge';
|
||||||
import { TaskPolicy } from '../runtime/task-policy';
|
import { TaskPolicy } from '../runtime/task-policy';
|
||||||
import { CopilotStorage } from '../storage';
|
import { CopilotStorage } from '../storage';
|
||||||
|
import { taskToJob, type TranscriptionJob } from './job';
|
||||||
import {
|
import {
|
||||||
TranscriptActionResultContract,
|
TranscriptActionResultContract,
|
||||||
TranscriptPayloadSchema,
|
TranscriptPayloadSchema,
|
||||||
} from './schema';
|
} from './schema';
|
||||||
import type {
|
import type {
|
||||||
AudioBlobInfos,
|
AudioBlobInfos,
|
||||||
TranscriptionPayload,
|
|
||||||
TranscriptionPayloadV2,
|
TranscriptionPayloadV2,
|
||||||
TranscriptionSubmitInput,
|
TranscriptionSubmitInput,
|
||||||
} from './types';
|
} from './types';
|
||||||
@@ -36,27 +36,6 @@ const TRANSCRIPT_ACTION_ID = 'transcript.audio.gemini';
|
|||||||
const TRANSCRIPT_ACTION_VERSION = 'v1';
|
const TRANSCRIPT_ACTION_VERSION = 'v1';
|
||||||
const TRANSCRIPT_STRATEGY = 'gemini';
|
const TRANSCRIPT_STRATEGY = 'gemini';
|
||||||
|
|
||||||
export type TranscriptionJob = {
|
|
||||||
id: string;
|
|
||||||
status: AiJobStatus;
|
|
||||||
infos?: AudioBlobInfos;
|
|
||||||
transcription?: TranscriptionPayload;
|
|
||||||
};
|
|
||||||
|
|
||||||
function taskStatusToPublicStatus(status: string): AiJobStatus {
|
|
||||||
switch (status) {
|
|
||||||
case 'pending':
|
|
||||||
return AiJobStatus.pending;
|
|
||||||
case 'running':
|
|
||||||
return AiJobStatus.running;
|
|
||||||
case 'ready':
|
|
||||||
case 'settled':
|
|
||||||
return AiJobStatus.finished;
|
|
||||||
default:
|
|
||||||
return AiJobStatus.failed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CopilotTranscriptionService {
|
export class CopilotTranscriptionService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -85,33 +64,6 @@ export class CopilotTranscriptionService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private taskToJob(
|
|
||||||
task: {
|
|
||||||
id: string;
|
|
||||||
status: string;
|
|
||||||
protectedResult: unknown;
|
|
||||||
} | null,
|
|
||||||
mapStatus: (status: string) => AiJobStatus = taskStatusToPublicStatus
|
|
||||||
): TranscriptionJob | null {
|
|
||||||
if (!task) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = mapStatus(task.status);
|
|
||||||
const ret: TranscriptionJob = {
|
|
||||||
id: task.id,
|
|
||||||
status,
|
|
||||||
};
|
|
||||||
if (task.protectedResult) {
|
|
||||||
const parsed = TranscriptPayloadSchema.safeParse(task.protectedResult);
|
|
||||||
ret.infos = parsed.success ? (parsed.data.infos ?? []) : [];
|
|
||||||
if (task.status === 'settled' && parsed.success) {
|
|
||||||
ret.transcription = parsed.data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async resolveTranscriptStrategy(userId: string, strategy?: string) {
|
private async resolveTranscriptStrategy(userId: string, strategy?: string) {
|
||||||
if (strategy && strategy !== TRANSCRIPT_STRATEGY) {
|
if (strategy && strategy !== TRANSCRIPT_STRATEGY) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
@@ -327,7 +279,7 @@ export class CopilotTranscriptionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (task.status === 'settled') {
|
if (task.status === 'settled') {
|
||||||
return this.taskToJob(task);
|
return taskToJob(task);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.access?.assertQuotaOrByok({
|
await this.access?.assertQuotaOrByok({
|
||||||
@@ -337,7 +289,7 @@ export class CopilotTranscriptionService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const settled = await this.models.copilotTranscriptTask.settle(task.id);
|
const settled = await this.models.copilotTranscriptTask.settle(task.id);
|
||||||
return this.taskToJob(settled);
|
return taskToJob(settled);
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryTask(
|
async queryTask(
|
||||||
@@ -352,10 +304,7 @@ export class CopilotTranscriptionService {
|
|||||||
taskId,
|
taskId,
|
||||||
blobId
|
blobId
|
||||||
);
|
);
|
||||||
if (task) {
|
return taskToJob(task);
|
||||||
return this.taskToJob(task);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob('copilot.transcript.task.submit')
|
@OnJob('copilot.transcript.task.submit')
|
||||||
|
|||||||
@@ -3126,10 +3126,6 @@ export const getWorkspaceConfigQuery = {
|
|||||||
enableSharing
|
enableSharing
|
||||||
enableUrlPreview
|
enableUrlPreview
|
||||||
enableDocEmbedding
|
enableDocEmbedding
|
||||||
inviteLink {
|
|
||||||
link
|
|
||||||
expireTime
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
@@ -3195,6 +3191,19 @@ export const acceptInviteByInviteIdMutation = {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getWorkspaceInviteLinkQuery = {
|
||||||
|
id: 'getWorkspaceInviteLinkQuery' as const,
|
||||||
|
op: 'getWorkspaceInviteLink',
|
||||||
|
query: `query getWorkspaceInviteLink($id: String!) {
|
||||||
|
workspace(id: $id) {
|
||||||
|
inviteLink {
|
||||||
|
link
|
||||||
|
expireTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
export const createInviteLinkMutation = {
|
export const createInviteLinkMutation = {
|
||||||
id: 'createInviteLinkMutation' as const,
|
id: 'createInviteLinkMutation' as const,
|
||||||
op: 'createInviteLink',
|
op: 'createInviteLink',
|
||||||
|
|||||||
@@ -4,9 +4,5 @@ query getWorkspaceConfig($id: String!) {
|
|||||||
enableSharing
|
enableSharing
|
||||||
enableUrlPreview
|
enableUrlPreview
|
||||||
enableDocEmbedding
|
enableDocEmbedding
|
||||||
inviteLink {
|
|
||||||
link
|
|
||||||
expireTime
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
query getWorkspaceInviteLink($id: String!) {
|
||||||
|
workspace(id: $id) {
|
||||||
|
inviteLink {
|
||||||
|
link
|
||||||
|
expireTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7794,11 +7794,6 @@ export type GetWorkspaceConfigQuery = {
|
|||||||
enableSharing: boolean;
|
enableSharing: boolean;
|
||||||
enableUrlPreview: boolean;
|
enableUrlPreview: boolean;
|
||||||
enableDocEmbedding: boolean;
|
enableDocEmbedding: boolean;
|
||||||
inviteLink: {
|
|
||||||
__typename?: 'InviteLink';
|
|
||||||
link: string;
|
|
||||||
expireTime: string;
|
|
||||||
} | null;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -7867,6 +7862,22 @@ export type AcceptInviteByInviteIdMutation = {
|
|||||||
acceptInviteById: boolean;
|
acceptInviteById: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetWorkspaceInviteLinkQueryVariables = Exact<{
|
||||||
|
id: Scalars['String']['input'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type GetWorkspaceInviteLinkQuery = {
|
||||||
|
__typename?: 'Query';
|
||||||
|
workspace: {
|
||||||
|
__typename?: 'WorkspaceType';
|
||||||
|
inviteLink: {
|
||||||
|
__typename?: 'InviteLink';
|
||||||
|
link: string;
|
||||||
|
expireTime: string;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateInviteLinkMutationVariables = Exact<{
|
export type CreateInviteLinkMutationVariables = Exact<{
|
||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
expireTime: WorkspaceInviteLinkExpireTime;
|
expireTime: WorkspaceInviteLinkExpireTime;
|
||||||
@@ -8418,6 +8429,11 @@ export type Queries =
|
|||||||
variables: GetWorkspaceConfigQueryVariables;
|
variables: GetWorkspaceConfigQueryVariables;
|
||||||
response: GetWorkspaceConfigQuery;
|
response: GetWorkspaceConfigQuery;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'getWorkspaceInviteLinkQuery';
|
||||||
|
variables: GetWorkspaceInviteLinkQueryVariables;
|
||||||
|
response: GetWorkspaceInviteLinkQuery;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'workspaceInvoicesQuery';
|
name: 'workspaceInvoicesQuery';
|
||||||
variables: WorkspaceInvoicesQueryVariables;
|
variables: WorkspaceInvoicesQueryVariables;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ public class GetWorkspaceConfigQuery: GraphQLQuery {
|
|||||||
public static let operationName: String = "getWorkspaceConfig"
|
public static let operationName: String = "getWorkspaceConfig"
|
||||||
public static let operationDocument: ApolloAPI.OperationDocument = .init(
|
public static let operationDocument: ApolloAPI.OperationDocument = .init(
|
||||||
definition: .init(
|
definition: .init(
|
||||||
#"query getWorkspaceConfig($id: String!) { workspace(id: $id) { __typename enableAi enableSharing enableUrlPreview enableDocEmbedding inviteLink { __typename link expireTime } } }"#
|
#"query getWorkspaceConfig($id: String!) { workspace(id: $id) { __typename enableAi enableSharing enableUrlPreview enableDocEmbedding } }"#
|
||||||
))
|
))
|
||||||
|
|
||||||
public var id: String
|
public var id: String
|
||||||
@@ -47,7 +47,6 @@ public class GetWorkspaceConfigQuery: GraphQLQuery {
|
|||||||
.field("enableSharing", Bool.self),
|
.field("enableSharing", Bool.self),
|
||||||
.field("enableUrlPreview", Bool.self),
|
.field("enableUrlPreview", Bool.self),
|
||||||
.field("enableDocEmbedding", Bool.self),
|
.field("enableDocEmbedding", Bool.self),
|
||||||
.field("inviteLink", InviteLink?.self),
|
|
||||||
] }
|
] }
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
||||||
GetWorkspaceConfigQuery.Data.Workspace.self
|
GetWorkspaceConfigQuery.Data.Workspace.self
|
||||||
@@ -61,31 +60,6 @@ public class GetWorkspaceConfigQuery: GraphQLQuery {
|
|||||||
public var enableUrlPreview: Bool { __data["enableUrlPreview"] }
|
public var enableUrlPreview: Bool { __data["enableUrlPreview"] }
|
||||||
/// Enable doc embedding
|
/// Enable doc embedding
|
||||||
public var enableDocEmbedding: Bool { __data["enableDocEmbedding"] }
|
public var enableDocEmbedding: Bool { __data["enableDocEmbedding"] }
|
||||||
/// invite link for workspace
|
|
||||||
public var inviteLink: InviteLink? { __data["inviteLink"] }
|
|
||||||
|
|
||||||
/// Workspace.InviteLink
|
|
||||||
///
|
|
||||||
/// Parent Type: `InviteLink`
|
|
||||||
public struct InviteLink: AffineGraphQL.SelectionSet {
|
|
||||||
public let __data: DataDict
|
|
||||||
public init(_dataDict: DataDict) { __data = _dataDict }
|
|
||||||
|
|
||||||
public static var __parentType: any ApolloAPI.ParentType { AffineGraphQL.Objects.InviteLink }
|
|
||||||
public static var __selections: [ApolloAPI.Selection] { [
|
|
||||||
.field("__typename", String.self),
|
|
||||||
.field("link", String.self),
|
|
||||||
.field("expireTime", AffineGraphQL.DateTime.self),
|
|
||||||
] }
|
|
||||||
public static var __fulfilledFragments: [any ApolloAPI.SelectionSet.Type] { [
|
|
||||||
GetWorkspaceConfigQuery.Data.Workspace.InviteLink.self
|
|
||||||
] }
|
|
||||||
|
|
||||||
/// Invite link
|
|
||||||
public var link: String { __data["link"] }
|
|
||||||
/// Invite link expire time
|
|
||||||
public var expireTime: AffineGraphQL.DateTime { __data["expireTime"] }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,12 @@ export const CloudWorkspaceMembersPanel = ({
|
|||||||
membersService.members.revalidate();
|
membersService.members.revalidate();
|
||||||
}, [membersService]);
|
}, [membersService]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOwnerOrAdmin) {
|
||||||
|
workspaceShareSettingService.sharePreview.revalidateInviteLink();
|
||||||
|
}
|
||||||
|
}, [isOwnerOrAdmin, workspaceShareSettingService.sharePreview]);
|
||||||
|
|
||||||
const workspaceQuotaService = useService(WorkspaceQuotaService);
|
const workspaceQuotaService = useService(WorkspaceQuotaService);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
workspaceQuotaService.quota.revalidate();
|
workspaceQuotaService.quota.revalidate();
|
||||||
@@ -178,7 +184,7 @@ export const CloudWorkspaceMembersPanel = ({
|
|||||||
const onGenerateInviteLink = useCallback(
|
const onGenerateInviteLink = useCallback(
|
||||||
async (expireTime: WorkspaceInviteLinkExpireTime) => {
|
async (expireTime: WorkspaceInviteLinkExpireTime) => {
|
||||||
const { link } = await membersService.generateInviteLink(expireTime);
|
const { link } = await membersService.generateInviteLink(expireTime);
|
||||||
workspaceShareSettingService.sharePreview.revalidate();
|
workspaceShareSettingService.sharePreview.revalidateInviteLink();
|
||||||
return link;
|
return link;
|
||||||
},
|
},
|
||||||
[membersService, workspaceShareSettingService.sharePreview]
|
[membersService, workspaceShareSettingService.sharePreview]
|
||||||
@@ -186,7 +192,7 @@ export const CloudWorkspaceMembersPanel = ({
|
|||||||
|
|
||||||
const onRevokeInviteLink = useCallback(async () => {
|
const onRevokeInviteLink = useCallback(async () => {
|
||||||
const success = await membersService.revokeInviteLink();
|
const success = await membersService.revokeInviteLink();
|
||||||
workspaceShareSettingService.sharePreview.revalidate();
|
workspaceShareSettingService.sharePreview.revalidateInviteLink();
|
||||||
return success;
|
return success;
|
||||||
}, [membersService, workspaceShareSettingService.sharePreview]);
|
}, [membersService, workspaceShareSettingService.sharePreview]);
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ export class WorkspaceShareSetting extends Entity {
|
|||||||
this.enableAi$.next(value.enableAi);
|
this.enableAi$.next(value.enableAi);
|
||||||
this.enableSharing$.next(value.enableSharing);
|
this.enableSharing$.next(value.enableSharing);
|
||||||
this.enableUrlPreview$.next(value.enableUrlPreview);
|
this.enableUrlPreview$.next(value.enableUrlPreview);
|
||||||
this.inviteLink$.next(value.inviteLink);
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchErrorInto(this.error$, error => {
|
catchErrorInto(this.error$, error => {
|
||||||
@@ -64,6 +63,22 @@ export class WorkspaceShareSetting extends Entity {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
revalidateInviteLink = effect(
|
||||||
|
exhaustMap(() => {
|
||||||
|
return fromPromise(signal =>
|
||||||
|
this.store.fetchInviteLink(this.workspaceService.workspace.id, signal)
|
||||||
|
).pipe(
|
||||||
|
smartRetry(),
|
||||||
|
tap(value => {
|
||||||
|
this.inviteLink$.next(value);
|
||||||
|
}),
|
||||||
|
catchErrorInto(this.error$, error => {
|
||||||
|
logger.error('Failed to fetch workspace invite link', error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
async waitForRevalidation(signal?: AbortSignal) {
|
async waitForRevalidation(signal?: AbortSignal) {
|
||||||
this.revalidate();
|
this.revalidate();
|
||||||
await this.isLoading$.waitFor(isLoading => !isLoading, signal);
|
await this.isLoading$.waitFor(isLoading => !isLoading, signal);
|
||||||
@@ -95,5 +110,6 @@ export class WorkspaceShareSetting extends Entity {
|
|||||||
|
|
||||||
override dispose(): void {
|
override dispose(): void {
|
||||||
this.revalidate.unsubscribe();
|
this.revalidate.unsubscribe();
|
||||||
|
this.revalidateInviteLink.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { WorkspaceServerService } from '@affine/core/modules/cloud';
|
import type { WorkspaceServerService } from '@affine/core/modules/cloud';
|
||||||
import {
|
import {
|
||||||
getWorkspaceConfigQuery,
|
getWorkspaceConfigQuery,
|
||||||
|
getWorkspaceInviteLinkQuery,
|
||||||
setEnableAiMutation,
|
setEnableAiMutation,
|
||||||
setEnableSharingMutation,
|
setEnableSharingMutation,
|
||||||
setEnableUrlPreviewMutation,
|
setEnableUrlPreviewMutation,
|
||||||
@@ -28,6 +29,22 @@ export class WorkspaceShareSettingStore extends Store {
|
|||||||
return data.workspace;
|
return data.workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchInviteLink(workspaceId: string, signal?: AbortSignal) {
|
||||||
|
if (!this.workspaceServerService.server) {
|
||||||
|
throw new Error('No Server');
|
||||||
|
}
|
||||||
|
const data = await this.workspaceServerService.server.gql({
|
||||||
|
query: getWorkspaceInviteLinkQuery,
|
||||||
|
variables: {
|
||||||
|
id: workspaceId,
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return data.workspace.inviteLink;
|
||||||
|
}
|
||||||
|
|
||||||
async updateWorkspaceEnableAi(
|
async updateWorkspaceEnableAi(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
enableAi: boolean,
|
enableAi: boolean,
|
||||||
|
|||||||
Reference in New Issue
Block a user