mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-22 19:53:48 +08:00
+2
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "workspaces" ADD COLUMN "enable_sharing" BOOLEAN NOT NULL DEFAULT true;
|
||||||
@@ -117,6 +117,7 @@ model Workspace {
|
|||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||||
// workspace level feature flags
|
// workspace level feature flags
|
||||||
enableAi Boolean @default(true) @map("enable_ai")
|
enableAi Boolean @default(true) @map("enable_ai")
|
||||||
|
enableSharing Boolean @default(true) @map("enable_sharing")
|
||||||
enableUrlPreview Boolean @default(false) @map("enable_url_preview")
|
enableUrlPreview Boolean @default(false) @map("enable_url_preview")
|
||||||
enableDocEmbedding Boolean @default(true) @map("enable_doc_embedding")
|
enableDocEmbedding Boolean @default(true) @map("enable_doc_embedding")
|
||||||
name String? @db.VarChar
|
name String? @db.VarChar
|
||||||
@@ -147,17 +148,17 @@ model Workspace {
|
|||||||
// Only the ones that have ever changed will have records here,
|
// Only the ones that have ever changed will have records here,
|
||||||
// and for others we will make sure it's has a default value return in our business logic.
|
// and for others we will make sure it's has a default value return in our business logic.
|
||||||
model WorkspaceDoc {
|
model WorkspaceDoc {
|
||||||
workspaceId String @map("workspace_id") @db.VarChar
|
workspaceId String @map("workspace_id") @db.VarChar
|
||||||
docId String @map("page_id") @db.VarChar
|
docId String @map("page_id") @db.VarChar
|
||||||
public Boolean @default(false)
|
public Boolean @default(false)
|
||||||
// Workspace user's default role in this page, default is `Manager`
|
// Workspace user's default role in this page, default is `Manager`
|
||||||
defaultRole Int @default(30) @db.SmallInt
|
defaultRole Int @default(30) @db.SmallInt
|
||||||
// Page/Edgeless
|
// Page/Edgeless
|
||||||
mode Int @default(0) @db.SmallInt
|
mode Int @default(0) @db.SmallInt
|
||||||
// Whether the doc is blocked
|
// Whether the doc is blocked
|
||||||
blocked Boolean @default(false)
|
blocked Boolean @default(false)
|
||||||
title String? @db.VarChar
|
title String? @db.VarChar
|
||||||
summary String? @db.VarChar
|
summary String? @db.VarChar
|
||||||
publishedAt DateTime? @map("published_at") @db.Timestamptz(3)
|
publishedAt DateTime? @map("published_at") @db.Timestamptz(3)
|
||||||
|
|
||||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ test('should update workspace', async t => {
|
|||||||
const data = {
|
const data = {
|
||||||
public: true,
|
public: true,
|
||||||
enableAi: true,
|
enableAi: true,
|
||||||
|
enableSharing: false,
|
||||||
enableUrlPreview: true,
|
enableUrlPreview: true,
|
||||||
enableDocEmbedding: false,
|
enableDocEmbedding: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -85,6 +85,26 @@ export async function updateWorkspace(
|
|||||||
return res.updateWorkspace.public;
|
return res.updateWorkspace.public;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function setWorkspaceSharing(
|
||||||
|
app: TestingApp,
|
||||||
|
workspaceId: string,
|
||||||
|
enableSharing: boolean
|
||||||
|
) {
|
||||||
|
const res = await app.gql(
|
||||||
|
`
|
||||||
|
mutation {
|
||||||
|
updateWorkspace(
|
||||||
|
input: { id: "${workspaceId}", enableSharing: ${enableSharing} }
|
||||||
|
) {
|
||||||
|
enableSharing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.updateWorkspace.enableSharing as boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteWorkspace(
|
export async function deleteWorkspace(
|
||||||
app: TestingApp,
|
app: TestingApp,
|
||||||
workspaceId: string
|
workspaceId: string
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
inviteUser,
|
inviteUser,
|
||||||
publishDoc,
|
publishDoc,
|
||||||
revokePublicDoc,
|
revokePublicDoc,
|
||||||
|
setWorkspaceSharing,
|
||||||
TestingApp,
|
TestingApp,
|
||||||
updateWorkspace,
|
updateWorkspace,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
@@ -180,4 +181,17 @@ test('should be able to get public workspace doc', async t => {
|
|||||||
.type('application/octet-stream');
|
.type('application/octet-stream');
|
||||||
|
|
||||||
t.deepEqual(res.body, Buffer.from([0, 0]), 'failed to get public doc');
|
t.deepEqual(res.body, Buffer.from([0, 0]), 'failed to get public doc');
|
||||||
|
|
||||||
|
const disabled = await setWorkspaceSharing(app, workspace.id, false);
|
||||||
|
t.false(disabled, 'failed to disable workspace sharing');
|
||||||
|
|
||||||
|
// owner should still be able to access
|
||||||
|
await app
|
||||||
|
.GET(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
await app.logout();
|
||||||
|
await app
|
||||||
|
.GET(`/api/workspaces/${workspace.id}/docs/${workspace.id}`)
|
||||||
|
.expect(403);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -100,6 +100,11 @@ export class DocRendererController {
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
docId: string
|
docId: string
|
||||||
): Promise<RenderOptions | null> {
|
): Promise<RenderOptions | null> {
|
||||||
|
const allowSharing = await this.models.workspace.allowSharing(workspaceId);
|
||||||
|
if (!allowSharing) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
let allowUrlPreview = await this.models.doc.isPublic(workspaceId, docId);
|
let allowUrlPreview = await this.models.doc.isPublic(workspaceId, docId);
|
||||||
|
|
||||||
if (!allowUrlPreview) {
|
if (!allowUrlPreview) {
|
||||||
@@ -118,6 +123,11 @@ export class DocRendererController {
|
|||||||
private async getWorkspaceContent(
|
private async getWorkspaceContent(
|
||||||
workspaceId: string
|
workspaceId: string
|
||||||
): Promise<RenderOptions | null> {
|
): Promise<RenderOptions | null> {
|
||||||
|
const allowSharing = await this.models.workspace.allowSharing(workspaceId);
|
||||||
|
if (!allowSharing) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const allowUrlPreview =
|
const allowUrlPreview =
|
||||||
await this.models.workspace.allowUrlPreview(workspaceId);
|
await this.models.workspace.allowUrlPreview(workspaceId);
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ let ws: Workspace;
|
|||||||
test.before(async () => {
|
test.before(async () => {
|
||||||
module = await createTestingModule({ imports: [PermissionModule] });
|
module = await createTestingModule({ imports: [PermissionModule] });
|
||||||
models = module.get<Models>(Models);
|
models = module.get<Models>(Models);
|
||||||
ac = new DocAccessController();
|
ac = module.get(DocAccessController);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.beforeEach(async () => {
|
test.beforeEach(async () => {
|
||||||
|
|||||||
@@ -80,6 +80,20 @@ test('should fallback to [External] if workspace is public', async t => {
|
|||||||
t.is(role, WorkspaceRole.External);
|
t.is(role, WorkspaceRole.External);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should return null if workspace is public but sharing disabled', async t => {
|
||||||
|
await models.workspace.update(ws.id, {
|
||||||
|
public: true,
|
||||||
|
enableSharing: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const role = await ac.getRole({
|
||||||
|
workspaceId: ws.id,
|
||||||
|
userId: 'random-user-id',
|
||||||
|
});
|
||||||
|
|
||||||
|
t.is(role, null);
|
||||||
|
});
|
||||||
|
|
||||||
test('should return null even workspace has public doc', async t => {
|
test('should return null even workspace has public doc', async t => {
|
||||||
await models.doc.publish(ws.id, 'doc1');
|
await models.doc.publish(ws.id, 'doc1');
|
||||||
|
|
||||||
@@ -91,6 +105,18 @@ test('should return null even workspace has public doc', async t => {
|
|||||||
t.is(role, null);
|
t.is(role, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should return null even workspace has public doc when sharing disabled', async t => {
|
||||||
|
await models.doc.publish(ws.id, 'doc1');
|
||||||
|
await models.workspace.update(ws.id, { enableSharing: false });
|
||||||
|
|
||||||
|
const role = await ac.getRole({
|
||||||
|
workspaceId: ws.id,
|
||||||
|
userId: 'random-user-id',
|
||||||
|
});
|
||||||
|
|
||||||
|
t.is(role, null);
|
||||||
|
});
|
||||||
|
|
||||||
test('should return mapped external permission for workspace has public docs', async t => {
|
test('should return mapped external permission for workspace has public docs', async t => {
|
||||||
await models.doc.publish(ws.id, 'doc1');
|
await models.doc.publish(ws.id, 'doc1');
|
||||||
|
|
||||||
@@ -105,6 +131,24 @@ test('should return mapped external permission for workspace has public docs', a
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should reject external doc roles when sharing disabled', async t => {
|
||||||
|
await models.workspace.update(ws.id, {
|
||||||
|
public: true,
|
||||||
|
enableSharing: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [docRole] = await ac.docRoles(
|
||||||
|
{
|
||||||
|
workspaceId: ws.id,
|
||||||
|
userId: 'random-user-id',
|
||||||
|
},
|
||||||
|
['doc1']
|
||||||
|
);
|
||||||
|
|
||||||
|
t.is(docRole.role, null);
|
||||||
|
t.false(docRole.permissions['Doc.Read']);
|
||||||
|
});
|
||||||
|
|
||||||
test('should return mapped permissions', async t => {
|
test('should return mapped permissions', async t => {
|
||||||
const { permissions } = await ac.role({
|
const { permissions } = await ac.role({
|
||||||
workspaceId: ws.id,
|
workspaceId: ws.id,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { DocActionDenied } from '../../base';
|
import { DocActionDenied } from '../../base';
|
||||||
|
import { Models } from '../../models';
|
||||||
import { AccessController, getAccessController } from './controller';
|
import { AccessController, getAccessController } from './controller';
|
||||||
import type { Resource } from './resource';
|
import type { Resource } from './resource';
|
||||||
import {
|
import {
|
||||||
@@ -14,14 +15,21 @@ import { WorkspaceAccessController } from './workspace';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class DocAccessController extends AccessController<'doc'> {
|
export class DocAccessController extends AccessController<'doc'> {
|
||||||
protected readonly type = 'doc';
|
protected readonly type = 'doc';
|
||||||
|
constructor(private readonly models: Models) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
async role(resource: Resource<'doc'>) {
|
async role(resource: Resource<'doc'>) {
|
||||||
const role = await this.getRole(resource);
|
const role = await this.getRole(resource);
|
||||||
|
const permissions = mapDocRoleToPermissions(role);
|
||||||
|
const sharingAllowed = await this.models.workspace.allowSharing(
|
||||||
|
resource.workspaceId
|
||||||
|
);
|
||||||
|
if (!sharingAllowed) {
|
||||||
|
permissions['Doc.Publish'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return { role, permissions };
|
||||||
role,
|
|
||||||
permissions: mapDocRoleToPermissions(role),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async can(resource: Resource<'doc'>, action: DocAction) {
|
async can(resource: Resource<'doc'>, action: DocAction) {
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ export class WorkspaceAccessController extends AccessController<'ws'> {
|
|||||||
// NOTE(@forehalo): special case for public page
|
// NOTE(@forehalo): special case for public page
|
||||||
// Currently, we can not only load binary of a public Doc to render in a shared page,
|
// Currently, we can not only load binary of a public Doc to render in a shared page,
|
||||||
// so we need to ensure anyone has basic 'read' permission to a workspace that has public pages.
|
// so we need to ensure anyone has basic 'read' permission to a workspace that has public pages.
|
||||||
if (!role && (await this.models.doc.hasPublic(resource.workspaceId))) {
|
if (
|
||||||
|
!role &&
|
||||||
|
(await this.models.workspace.allowSharing(resource.workspaceId)) &&
|
||||||
|
(await this.models.doc.hasPublic(resource.workspaceId))
|
||||||
|
) {
|
||||||
role = WorkspaceRole.External;
|
role = WorkspaceRole.External;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +96,15 @@ export class WorkspaceAccessController extends AccessController<'ws'> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const workspaceRole = await this.getRole(payload);
|
const workspaceRole = await this.getRole(payload);
|
||||||
|
const sharingAllowed = await this.models.workspace.allowSharing(
|
||||||
|
payload.workspaceId
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!sharingAllowed &&
|
||||||
|
(workspaceRole === null || workspaceRole === WorkspaceRole.External)
|
||||||
|
) {
|
||||||
|
return docIds.map(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
const userRoles = await this.models.docUser.findMany(
|
const userRoles = await this.models.docUser.findMany(
|
||||||
payload.workspaceId,
|
payload.workspaceId,
|
||||||
@@ -190,7 +203,8 @@ export class WorkspaceAccessController extends AccessController<'ws'> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ws.public) {
|
if (ws.public) {
|
||||||
return WorkspaceRole.External;
|
const sharingAllowed = await this.models.workspace.allowSharing(ws.id);
|
||||||
|
return sharingAllowed ? WorkspaceRole.External : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -56,6 +56,21 @@ class ListWorkspaceInput {
|
|||||||
|
|
||||||
@Field(() => AdminWorkspaceSort, { nullable: true })
|
@Field(() => AdminWorkspaceSort, { nullable: true })
|
||||||
orderBy?: AdminWorkspaceSort;
|
orderBy?: AdminWorkspaceSort;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
public?: boolean;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
enableAi?: boolean;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
enableSharing?: boolean;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
enableUrlPreview?: boolean;
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
enableDocEmbedding?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
@@ -111,6 +126,9 @@ export class AdminWorkspace {
|
|||||||
@Field()
|
@Field()
|
||||||
enableAi!: boolean;
|
enableAi!: boolean;
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
enableSharing!: boolean;
|
||||||
|
|
||||||
@Field()
|
@Field()
|
||||||
enableUrlPreview!: boolean;
|
enableUrlPreview!: boolean;
|
||||||
|
|
||||||
@@ -150,6 +168,7 @@ class AdminUpdateWorkspaceInput extends PartialType(
|
|||||||
PickType(AdminWorkspace, [
|
PickType(AdminWorkspace, [
|
||||||
'public',
|
'public',
|
||||||
'enableAi',
|
'enableAi',
|
||||||
|
'enableSharing',
|
||||||
'enableUrlPreview',
|
'enableUrlPreview',
|
||||||
'enableDocEmbedding',
|
'enableDocEmbedding',
|
||||||
'name',
|
'name',
|
||||||
@@ -183,6 +202,13 @@ export class AdminWorkspaceResolver {
|
|||||||
keyword: filter.keyword,
|
keyword: filter.keyword,
|
||||||
features: filter.features,
|
features: filter.features,
|
||||||
order: this.mapSort(filter.orderBy),
|
order: this.mapSort(filter.orderBy),
|
||||||
|
flags: {
|
||||||
|
public: filter.public ?? undefined,
|
||||||
|
enableAi: filter.enableAi ?? undefined,
|
||||||
|
enableSharing: filter.enableSharing ?? undefined,
|
||||||
|
enableUrlPreview: filter.enableUrlPreview ?? undefined,
|
||||||
|
enableDocEmbedding: filter.enableDocEmbedding ?? undefined,
|
||||||
|
},
|
||||||
includeTotal: false,
|
includeTotal: false,
|
||||||
});
|
});
|
||||||
return rows;
|
return rows;
|
||||||
@@ -196,6 +222,13 @@ export class AdminWorkspaceResolver {
|
|||||||
const total = await this.models.workspace.adminCountWorkspaces({
|
const total = await this.models.workspace.adminCountWorkspaces({
|
||||||
keyword: filter.keyword,
|
keyword: filter.keyword,
|
||||||
features: filter.features,
|
features: filter.features,
|
||||||
|
flags: {
|
||||||
|
public: filter.public ?? undefined,
|
||||||
|
enableAi: filter.enableAi ?? undefined,
|
||||||
|
enableSharing: filter.enableSharing ?? undefined,
|
||||||
|
enableUrlPreview: filter.enableUrlPreview ?? undefined,
|
||||||
|
enableDocEmbedding: filter.enableDocEmbedding ?? undefined,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ export class WorkspaceType extends WorkspaceFeatureType {
|
|||||||
@Field({ description: 'Enable AI' })
|
@Field({ description: 'Enable AI' })
|
||||||
enableAi!: boolean;
|
enableAi!: boolean;
|
||||||
|
|
||||||
|
@Field({ description: 'Enable workspace sharing' })
|
||||||
|
enableSharing!: boolean;
|
||||||
|
|
||||||
@Field({ description: 'Enable url previous when sharing' })
|
@Field({ description: 'Enable url previous when sharing' })
|
||||||
enableUrlPreview!: boolean;
|
enableUrlPreview!: boolean;
|
||||||
|
|
||||||
@@ -130,7 +133,13 @@ export class InvitationType {
|
|||||||
@InputType()
|
@InputType()
|
||||||
export class UpdateWorkspaceInput extends PickType(
|
export class UpdateWorkspaceInput extends PickType(
|
||||||
PartialType(WorkspaceType),
|
PartialType(WorkspaceType),
|
||||||
['public', 'enableAi', 'enableUrlPreview', 'enableDocEmbedding'],
|
[
|
||||||
|
'public',
|
||||||
|
'enableAi',
|
||||||
|
'enableSharing',
|
||||||
|
'enableUrlPreview',
|
||||||
|
'enableDocEmbedding',
|
||||||
|
],
|
||||||
InputType
|
InputType
|
||||||
) {
|
) {
|
||||||
@Field(() => ID)
|
@Field(() => ID)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type RawWorkspaceSummary = {
|
|||||||
name: string | null;
|
name: string | null;
|
||||||
avatarKey: string | null;
|
avatarKey: string | null;
|
||||||
enableAi: boolean;
|
enableAi: boolean;
|
||||||
|
enableSharing: boolean;
|
||||||
enableUrlPreview: boolean;
|
enableUrlPreview: boolean;
|
||||||
enableDocEmbedding: boolean;
|
enableDocEmbedding: boolean;
|
||||||
memberCount: bigint | number | null;
|
memberCount: bigint | number | null;
|
||||||
@@ -36,6 +37,7 @@ export type AdminWorkspaceSummary = {
|
|||||||
name: string | null;
|
name: string | null;
|
||||||
avatarKey: string | null;
|
avatarKey: string | null;
|
||||||
enableAi: boolean;
|
enableAi: boolean;
|
||||||
|
enableSharing: boolean;
|
||||||
enableUrlPreview: boolean;
|
enableUrlPreview: boolean;
|
||||||
enableDocEmbedding: boolean;
|
enableDocEmbedding: boolean;
|
||||||
memberCount: number;
|
memberCount: number;
|
||||||
@@ -67,6 +69,7 @@ export type UpdateWorkspaceInput = Pick<
|
|||||||
Partial<Workspace>,
|
Partial<Workspace>,
|
||||||
| 'public'
|
| 'public'
|
||||||
| 'enableAi'
|
| 'enableAi'
|
||||||
|
| 'enableSharing'
|
||||||
| 'enableUrlPreview'
|
| 'enableUrlPreview'
|
||||||
| 'enableDocEmbedding'
|
| 'enableDocEmbedding'
|
||||||
| 'name'
|
| 'name'
|
||||||
@@ -169,6 +172,11 @@ export class WorkspaceModel extends BaseModel {
|
|||||||
return workspace?.enableUrlPreview ?? false;
|
return workspace?.enableUrlPreview ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async allowSharing(workspaceId: string) {
|
||||||
|
const workspace = await this.get(workspaceId);
|
||||||
|
return workspace?.enableSharing ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
async allowEmbedding(workspaceId: string) {
|
async allowEmbedding(workspaceId: string) {
|
||||||
const workspace = await this.get(workspaceId);
|
const workspace = await this.get(workspaceId);
|
||||||
return workspace?.enableDocEmbedding ?? false;
|
return workspace?.enableDocEmbedding ?? false;
|
||||||
@@ -185,6 +193,13 @@ export class WorkspaceModel extends BaseModel {
|
|||||||
first: number;
|
first: number;
|
||||||
keyword?: string | null;
|
keyword?: string | null;
|
||||||
features?: WorkspaceFeatureName[] | null;
|
features?: WorkspaceFeatureName[] | null;
|
||||||
|
flags?: {
|
||||||
|
public?: boolean;
|
||||||
|
enableAi?: boolean;
|
||||||
|
enableSharing?: boolean;
|
||||||
|
enableUrlPreview?: boolean;
|
||||||
|
enableDocEmbedding?: boolean;
|
||||||
|
};
|
||||||
order?:
|
order?:
|
||||||
| 'createdAt'
|
| 'createdAt'
|
||||||
| 'snapshotSize'
|
| 'snapshotSize'
|
||||||
@@ -197,9 +212,10 @@ export class WorkspaceModel extends BaseModel {
|
|||||||
}): Promise<{ rows: AdminWorkspaceSummary[]; total: number }> {
|
}): Promise<{ rows: AdminWorkspaceSummary[]; total: number }> {
|
||||||
const keyword = options.keyword?.trim();
|
const keyword = options.keyword?.trim();
|
||||||
const features = options.features ?? [];
|
const features = options.features ?? [];
|
||||||
|
const flags = options.flags ?? {};
|
||||||
const includeTotal = options.includeTotal ?? true;
|
const includeTotal = options.includeTotal ?? true;
|
||||||
const total = includeTotal
|
const total = includeTotal
|
||||||
? await this.adminCountWorkspaces({ keyword, features })
|
? await this.adminCountWorkspaces({ keyword, features, flags })
|
||||||
: 0;
|
: 0;
|
||||||
if (includeTotal && total === 0) {
|
if (includeTotal && total === 0) {
|
||||||
return { rows: [], total: 0 };
|
return { rows: [], total: 0 };
|
||||||
@@ -251,6 +267,7 @@ export class WorkspaceModel extends BaseModel {
|
|||||||
w.name,
|
w.name,
|
||||||
w.avatar_key AS "avatarKey",
|
w.avatar_key AS "avatarKey",
|
||||||
w.enable_ai AS "enableAi",
|
w.enable_ai AS "enableAi",
|
||||||
|
w.enable_sharing AS "enableSharing",
|
||||||
w.enable_url_preview AS "enableUrlPreview",
|
w.enable_url_preview AS "enableUrlPreview",
|
||||||
w.enable_doc_embedding AS "enableDocEmbedding",
|
w.enable_doc_embedding AS "enableDocEmbedding",
|
||||||
o.owner_id AS "ownerId",
|
o.owner_id AS "ownerId",
|
||||||
@@ -283,6 +300,14 @@ export class WorkspaceModel extends BaseModel {
|
|||||||
`
|
`
|
||||||
: Prisma.sql`TRUE`
|
: Prisma.sql`TRUE`
|
||||||
}
|
}
|
||||||
|
${
|
||||||
|
this.buildAdminFlagWhere(flags).length
|
||||||
|
? Prisma.sql`AND ${Prisma.join(
|
||||||
|
this.buildAdminFlagWhere(flags),
|
||||||
|
' AND '
|
||||||
|
)}`
|
||||||
|
: Prisma.empty
|
||||||
|
}
|
||||||
${groupAndHaving}
|
${groupAndHaving}
|
||||||
)
|
)
|
||||||
SELECT f.*,
|
SELECT f.*,
|
||||||
@@ -307,6 +332,7 @@ export class WorkspaceModel extends BaseModel {
|
|||||||
name: row.name,
|
name: row.name,
|
||||||
avatarKey: row.avatarKey,
|
avatarKey: row.avatarKey,
|
||||||
enableAi: row.enableAi,
|
enableAi: row.enableAi,
|
||||||
|
enableSharing: row.enableSharing,
|
||||||
enableUrlPreview: row.enableUrlPreview,
|
enableUrlPreview: row.enableUrlPreview,
|
||||||
enableDocEmbedding: row.enableDocEmbedding,
|
enableDocEmbedding: row.enableDocEmbedding,
|
||||||
memberCount: Number(row.memberCount ?? 0),
|
memberCount: Number(row.memberCount ?? 0),
|
||||||
@@ -332,9 +358,17 @@ export class WorkspaceModel extends BaseModel {
|
|||||||
async adminCountWorkspaces(options: {
|
async adminCountWorkspaces(options: {
|
||||||
keyword?: string | null;
|
keyword?: string | null;
|
||||||
features?: WorkspaceFeatureName[] | null;
|
features?: WorkspaceFeatureName[] | null;
|
||||||
|
flags?: {
|
||||||
|
public?: boolean;
|
||||||
|
enableAi?: boolean;
|
||||||
|
enableSharing?: boolean;
|
||||||
|
enableUrlPreview?: boolean;
|
||||||
|
enableDocEmbedding?: boolean;
|
||||||
|
};
|
||||||
}) {
|
}) {
|
||||||
const keyword = options.keyword?.trim();
|
const keyword = options.keyword?.trim();
|
||||||
const features = options.features ?? [];
|
const features = options.features ?? [];
|
||||||
|
const flags = options.flags ?? {};
|
||||||
|
|
||||||
const featuresHaving =
|
const featuresHaving =
|
||||||
features.length > 0
|
features.length > 0
|
||||||
@@ -393,6 +427,14 @@ export class WorkspaceModel extends BaseModel {
|
|||||||
`
|
`
|
||||||
: Prisma.sql`TRUE`
|
: Prisma.sql`TRUE`
|
||||||
}
|
}
|
||||||
|
${
|
||||||
|
this.buildAdminFlagWhere(flags).length
|
||||||
|
? Prisma.sql`AND ${Prisma.join(
|
||||||
|
this.buildAdminFlagWhere(flags),
|
||||||
|
' AND '
|
||||||
|
)}`
|
||||||
|
: Prisma.empty
|
||||||
|
}
|
||||||
${groupAndHaving}
|
${groupAndHaving}
|
||||||
)
|
)
|
||||||
SELECT COUNT(*) AS total FROM filtered
|
SELECT COUNT(*) AS total FROM filtered
|
||||||
@@ -401,6 +443,36 @@ export class WorkspaceModel extends BaseModel {
|
|||||||
return row?.total ? Number(row.total) : 0;
|
return row?.total ? Number(row.total) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildAdminFlagWhere(flags: {
|
||||||
|
public?: boolean;
|
||||||
|
enableAi?: boolean;
|
||||||
|
enableSharing?: boolean;
|
||||||
|
enableUrlPreview?: boolean;
|
||||||
|
enableDocEmbedding?: boolean;
|
||||||
|
}) {
|
||||||
|
const conditions: Prisma.Sql[] = [];
|
||||||
|
if (flags.public !== undefined) {
|
||||||
|
conditions.push(Prisma.sql`w.public = ${flags.public}`);
|
||||||
|
}
|
||||||
|
if (flags.enableAi !== undefined) {
|
||||||
|
conditions.push(Prisma.sql`w.enable_ai = ${flags.enableAi}`);
|
||||||
|
}
|
||||||
|
if (flags.enableSharing !== undefined) {
|
||||||
|
conditions.push(Prisma.sql`w.enable_sharing = ${flags.enableSharing}`);
|
||||||
|
}
|
||||||
|
if (flags.enableUrlPreview !== undefined) {
|
||||||
|
conditions.push(
|
||||||
|
Prisma.sql`w.enable_url_preview = ${flags.enableUrlPreview}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (flags.enableDocEmbedding !== undefined) {
|
||||||
|
conditions.push(
|
||||||
|
Prisma.sql`w.enable_doc_embedding = ${flags.enableDocEmbedding}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return conditions;
|
||||||
|
}
|
||||||
|
|
||||||
private buildAdminOrder(
|
private buildAdminOrder(
|
||||||
order?:
|
order?:
|
||||||
| 'createdAt'
|
| 'createdAt'
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ input AdminUpdateWorkspaceInput {
|
|||||||
avatarKey: String
|
avatarKey: String
|
||||||
enableAi: Boolean
|
enableAi: Boolean
|
||||||
enableDocEmbedding: Boolean
|
enableDocEmbedding: Boolean
|
||||||
|
enableSharing: Boolean
|
||||||
enableUrlPreview: Boolean
|
enableUrlPreview: Boolean
|
||||||
features: [FeatureType!]
|
features: [FeatureType!]
|
||||||
id: String!
|
id: String!
|
||||||
@@ -49,6 +50,7 @@ type AdminWorkspace {
|
|||||||
createdAt: DateTime!
|
createdAt: DateTime!
|
||||||
enableAi: Boolean!
|
enableAi: Boolean!
|
||||||
enableDocEmbedding: Boolean!
|
enableDocEmbedding: Boolean!
|
||||||
|
enableSharing: Boolean!
|
||||||
enableUrlPreview: Boolean!
|
enableUrlPreview: Boolean!
|
||||||
features: [FeatureType!]!
|
features: [FeatureType!]!
|
||||||
id: String!
|
id: String!
|
||||||
@@ -1226,10 +1228,15 @@ input ListUserInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input ListWorkspaceInput {
|
input ListWorkspaceInput {
|
||||||
|
enableAi: Boolean
|
||||||
|
enableDocEmbedding: Boolean
|
||||||
|
enableSharing: Boolean
|
||||||
|
enableUrlPreview: Boolean
|
||||||
features: [FeatureType!]
|
features: [FeatureType!]
|
||||||
first: Int! = 20
|
first: Int! = 20
|
||||||
keyword: String
|
keyword: String
|
||||||
orderBy: AdminWorkspaceSort
|
orderBy: AdminWorkspaceSort
|
||||||
|
public: Boolean
|
||||||
skip: Int! = 0
|
skip: Int! = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2207,6 +2214,9 @@ input UpdateWorkspaceInput {
|
|||||||
"""Enable doc embedding"""
|
"""Enable doc embedding"""
|
||||||
enableDocEmbedding: Boolean
|
enableDocEmbedding: Boolean
|
||||||
|
|
||||||
|
"""Enable workspace sharing"""
|
||||||
|
enableSharing: Boolean
|
||||||
|
|
||||||
"""Enable url previous when sharing"""
|
"""Enable url previous when sharing"""
|
||||||
enableUrlPreview: Boolean
|
enableUrlPreview: Boolean
|
||||||
id: ID!
|
id: ID!
|
||||||
@@ -2432,6 +2442,9 @@ type WorkspaceType {
|
|||||||
"""Enable doc embedding"""
|
"""Enable doc embedding"""
|
||||||
enableDocEmbedding: Boolean!
|
enableDocEmbedding: Boolean!
|
||||||
|
|
||||||
|
"""Enable workspace sharing"""
|
||||||
|
enableSharing: Boolean!
|
||||||
|
|
||||||
"""Enable url previous when sharing"""
|
"""Enable url previous when sharing"""
|
||||||
enableUrlPreview: Boolean!
|
enableUrlPreview: Boolean!
|
||||||
histories(before: DateTime, guid: String!, take: Int): [DocHistoryType!]!
|
histories(before: DateTime, guid: String!, take: Int): [DocHistoryType!]!
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ mutation adminUpdateWorkspace($input: AdminUpdateWorkspaceInput!) {
|
|||||||
name
|
name
|
||||||
avatarKey
|
avatarKey
|
||||||
enableAi
|
enableAi
|
||||||
|
enableSharing
|
||||||
enableUrlPreview
|
enableUrlPreview
|
||||||
enableDocEmbedding
|
enableDocEmbedding
|
||||||
features
|
features
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ query adminWorkspace(
|
|||||||
name
|
name
|
||||||
avatarKey
|
avatarKey
|
||||||
enableAi
|
enableAi
|
||||||
|
enableSharing
|
||||||
enableUrlPreview
|
enableUrlPreview
|
||||||
enableDocEmbedding
|
enableDocEmbedding
|
||||||
features
|
features
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ query adminWorkspaces($filter: ListWorkspaceInput!) {
|
|||||||
name
|
name
|
||||||
avatarKey
|
avatarKey
|
||||||
enableAi
|
enableAi
|
||||||
|
enableSharing
|
||||||
enableUrlPreview
|
enableUrlPreview
|
||||||
enableDocEmbedding
|
enableDocEmbedding
|
||||||
features
|
features
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ export const adminUpdateWorkspaceMutation = {
|
|||||||
name
|
name
|
||||||
avatarKey
|
avatarKey
|
||||||
enableAi
|
enableAi
|
||||||
|
enableSharing
|
||||||
enableUrlPreview
|
enableUrlPreview
|
||||||
enableDocEmbedding
|
enableDocEmbedding
|
||||||
features
|
features
|
||||||
@@ -175,6 +176,7 @@ export const adminWorkspaceQuery = {
|
|||||||
name
|
name
|
||||||
avatarKey
|
avatarKey
|
||||||
enableAi
|
enableAi
|
||||||
|
enableSharing
|
||||||
enableUrlPreview
|
enableUrlPreview
|
||||||
enableDocEmbedding
|
enableDocEmbedding
|
||||||
features
|
features
|
||||||
@@ -218,6 +220,7 @@ export const adminWorkspacesQuery = {
|
|||||||
name
|
name
|
||||||
avatarKey
|
avatarKey
|
||||||
enableAi
|
enableAi
|
||||||
|
enableSharing
|
||||||
enableUrlPreview
|
enableUrlPreview
|
||||||
enableDocEmbedding
|
enableDocEmbedding
|
||||||
features
|
features
|
||||||
@@ -2545,6 +2548,7 @@ export const getWorkspaceConfigQuery = {
|
|||||||
query: `query getWorkspaceConfig($id: String!) {
|
query: `query getWorkspaceConfig($id: String!) {
|
||||||
workspace(id: $id) {
|
workspace(id: $id) {
|
||||||
enableAi
|
enableAi
|
||||||
|
enableSharing
|
||||||
enableUrlPreview
|
enableUrlPreview
|
||||||
enableDocEmbedding
|
enableDocEmbedding
|
||||||
inviteLink {
|
inviteLink {
|
||||||
@@ -2575,6 +2579,16 @@ export const setEnableDocEmbeddingMutation = {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setEnableSharingMutation = {
|
||||||
|
id: 'setEnableSharingMutation' as const,
|
||||||
|
op: 'setEnableSharing',
|
||||||
|
query: `mutation setEnableSharing($id: ID!, $enableSharing: Boolean!) {
|
||||||
|
updateWorkspace(input: {id: $id, enableSharing: $enableSharing}) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
export const setEnableUrlPreviewMutation = {
|
export const setEnableUrlPreviewMutation = {
|
||||||
id: 'setEnableUrlPreviewMutation' as const,
|
id: 'setEnableUrlPreviewMutation' as const,
|
||||||
op: 'setEnableUrlPreview',
|
op: 'setEnableUrlPreview',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
query getWorkspaceConfig($id: String!) {
|
query getWorkspaceConfig($id: String!) {
|
||||||
workspace(id: $id) {
|
workspace(id: $id) {
|
||||||
enableAi
|
enableAi
|
||||||
|
enableSharing
|
||||||
enableUrlPreview
|
enableUrlPreview
|
||||||
enableDocEmbedding
|
enableDocEmbedding
|
||||||
inviteLink {
|
inviteLink {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
mutation setEnableSharing($id: ID!, $enableSharing: Boolean!) {
|
||||||
|
updateWorkspace(input: { id: $id, enableSharing: $enableSharing }) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,6 +71,7 @@ export interface AdminUpdateWorkspaceInput {
|
|||||||
avatarKey?: InputMaybe<Scalars['String']['input']>;
|
avatarKey?: InputMaybe<Scalars['String']['input']>;
|
||||||
enableAi?: InputMaybe<Scalars['Boolean']['input']>;
|
enableAi?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
enableDocEmbedding?: InputMaybe<Scalars['Boolean']['input']>;
|
enableDocEmbedding?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
|
enableSharing?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
enableUrlPreview?: InputMaybe<Scalars['Boolean']['input']>;
|
enableUrlPreview?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
features?: InputMaybe<Array<FeatureType>>;
|
features?: InputMaybe<Array<FeatureType>>;
|
||||||
id: Scalars['String']['input'];
|
id: Scalars['String']['input'];
|
||||||
@@ -86,6 +87,7 @@ export interface AdminWorkspace {
|
|||||||
createdAt: Scalars['DateTime']['output'];
|
createdAt: Scalars['DateTime']['output'];
|
||||||
enableAi: Scalars['Boolean']['output'];
|
enableAi: Scalars['Boolean']['output'];
|
||||||
enableDocEmbedding: Scalars['Boolean']['output'];
|
enableDocEmbedding: Scalars['Boolean']['output'];
|
||||||
|
enableSharing: Scalars['Boolean']['output'];
|
||||||
enableUrlPreview: Scalars['Boolean']['output'];
|
enableUrlPreview: Scalars['Boolean']['output'];
|
||||||
features: Array<FeatureType>;
|
features: Array<FeatureType>;
|
||||||
id: Scalars['String']['output'];
|
id: Scalars['String']['output'];
|
||||||
@@ -1411,10 +1413,15 @@ export interface ListUserInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ListWorkspaceInput {
|
export interface ListWorkspaceInput {
|
||||||
|
enableAi?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
|
enableDocEmbedding?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
|
enableSharing?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
|
enableUrlPreview?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
features?: InputMaybe<Array<FeatureType>>;
|
features?: InputMaybe<Array<FeatureType>>;
|
||||||
first?: Scalars['Int']['input'];
|
first?: Scalars['Int']['input'];
|
||||||
keyword?: InputMaybe<Scalars['String']['input']>;
|
keyword?: InputMaybe<Scalars['String']['input']>;
|
||||||
orderBy?: InputMaybe<AdminWorkspaceSort>;
|
orderBy?: InputMaybe<AdminWorkspaceSort>;
|
||||||
|
public?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
skip?: Scalars['Int']['input'];
|
skip?: Scalars['Int']['input'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2881,6 +2888,8 @@ export interface UpdateWorkspaceInput {
|
|||||||
enableAi?: InputMaybe<Scalars['Boolean']['input']>;
|
enableAi?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
/** Enable doc embedding */
|
/** Enable doc embedding */
|
||||||
enableDocEmbedding?: InputMaybe<Scalars['Boolean']['input']>;
|
enableDocEmbedding?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
|
/** Enable workspace sharing */
|
||||||
|
enableSharing?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
/** Enable url previous when sharing */
|
/** Enable url previous when sharing */
|
||||||
enableUrlPreview?: InputMaybe<Scalars['Boolean']['input']>;
|
enableUrlPreview?: InputMaybe<Scalars['Boolean']['input']>;
|
||||||
id: Scalars['ID']['input'];
|
id: Scalars['ID']['input'];
|
||||||
@@ -3115,6 +3124,8 @@ export interface WorkspaceType {
|
|||||||
enableAi: Scalars['Boolean']['output'];
|
enableAi: Scalars['Boolean']['output'];
|
||||||
/** Enable doc embedding */
|
/** Enable doc embedding */
|
||||||
enableDocEmbedding: Scalars['Boolean']['output'];
|
enableDocEmbedding: Scalars['Boolean']['output'];
|
||||||
|
/** Enable workspace sharing */
|
||||||
|
enableSharing: Scalars['Boolean']['output'];
|
||||||
/** Enable url previous when sharing */
|
/** Enable url previous when sharing */
|
||||||
enableUrlPreview: Scalars['Boolean']['output'];
|
enableUrlPreview: Scalars['Boolean']['output'];
|
||||||
histories: Array<DocHistoryType>;
|
histories: Array<DocHistoryType>;
|
||||||
@@ -3332,6 +3343,7 @@ export type AdminUpdateWorkspaceMutation = {
|
|||||||
name: string | null;
|
name: string | null;
|
||||||
avatarKey: string | null;
|
avatarKey: string | null;
|
||||||
enableAi: boolean;
|
enableAi: boolean;
|
||||||
|
enableSharing: boolean;
|
||||||
enableUrlPreview: boolean;
|
enableUrlPreview: boolean;
|
||||||
enableDocEmbedding: boolean;
|
enableDocEmbedding: boolean;
|
||||||
features: Array<FeatureType>;
|
features: Array<FeatureType>;
|
||||||
@@ -3368,6 +3380,7 @@ export type AdminWorkspaceQuery = {
|
|||||||
name: string | null;
|
name: string | null;
|
||||||
avatarKey: string | null;
|
avatarKey: string | null;
|
||||||
enableAi: boolean;
|
enableAi: boolean;
|
||||||
|
enableSharing: boolean;
|
||||||
enableUrlPreview: boolean;
|
enableUrlPreview: boolean;
|
||||||
enableDocEmbedding: boolean;
|
enableDocEmbedding: boolean;
|
||||||
features: Array<FeatureType>;
|
features: Array<FeatureType>;
|
||||||
@@ -3416,6 +3429,7 @@ export type AdminWorkspacesQuery = {
|
|||||||
name: string | null;
|
name: string | null;
|
||||||
avatarKey: string | null;
|
avatarKey: string | null;
|
||||||
enableAi: boolean;
|
enableAi: boolean;
|
||||||
|
enableSharing: boolean;
|
||||||
enableUrlPreview: boolean;
|
enableUrlPreview: boolean;
|
||||||
enableDocEmbedding: boolean;
|
enableDocEmbedding: boolean;
|
||||||
features: Array<FeatureType>;
|
features: Array<FeatureType>;
|
||||||
@@ -6534,6 +6548,7 @@ export type GetWorkspaceConfigQuery = {
|
|||||||
workspace: {
|
workspace: {
|
||||||
__typename?: 'WorkspaceType';
|
__typename?: 'WorkspaceType';
|
||||||
enableAi: boolean;
|
enableAi: boolean;
|
||||||
|
enableSharing: boolean;
|
||||||
enableUrlPreview: boolean;
|
enableUrlPreview: boolean;
|
||||||
enableDocEmbedding: boolean;
|
enableDocEmbedding: boolean;
|
||||||
inviteLink: {
|
inviteLink: {
|
||||||
@@ -6564,6 +6579,16 @@ export type SetEnableDocEmbeddingMutation = {
|
|||||||
updateWorkspace: { __typename?: 'WorkspaceType'; id: string };
|
updateWorkspace: { __typename?: 'WorkspaceType'; id: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SetEnableSharingMutationVariables = Exact<{
|
||||||
|
id: Scalars['ID']['input'];
|
||||||
|
enableSharing: Scalars['Boolean']['input'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type SetEnableSharingMutation = {
|
||||||
|
__typename?: 'Mutation';
|
||||||
|
updateWorkspace: { __typename?: 'WorkspaceType'; id: string };
|
||||||
|
};
|
||||||
|
|
||||||
export type SetEnableUrlPreviewMutationVariables = Exact<{
|
export type SetEnableUrlPreviewMutationVariables = Exact<{
|
||||||
id: Scalars['ID']['input'];
|
id: Scalars['ID']['input'];
|
||||||
enableUrlPreview: Scalars['Boolean']['input'];
|
enableUrlPreview: Scalars['Boolean']['input'];
|
||||||
@@ -7587,6 +7612,11 @@ export type Mutations =
|
|||||||
variables: SetEnableDocEmbeddingMutationVariables;
|
variables: SetEnableDocEmbeddingMutationVariables;
|
||||||
response: SetEnableDocEmbeddingMutation;
|
response: SetEnableDocEmbeddingMutation;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'setEnableSharingMutation';
|
||||||
|
variables: SetEnableSharingMutationVariables;
|
||||||
|
response: SetEnableSharingMutation;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'setEnableUrlPreviewMutation';
|
name: 'setEnableUrlPreviewMutation';
|
||||||
variables: SetEnableUrlPreviewMutationVariables;
|
variables: SetEnableUrlPreviewMutationVariables;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from '../../../components/ui/popover';
|
} from '../../../components/ui/popover';
|
||||||
import { useDebouncedValue } from '../../../hooks/use-debounced-value';
|
import { useDebouncedValue } from '../../../hooks/use-debounced-value';
|
||||||
import { useServerConfig } from '../../common';
|
import { useServerConfig } from '../../common';
|
||||||
|
import type { WorkspaceFlagFilter } from '../schema';
|
||||||
|
|
||||||
interface DataTableToolbarProps<TData> {
|
interface DataTableToolbarProps<TData> {
|
||||||
table?: Table<TData>;
|
table?: Table<TData>;
|
||||||
@@ -25,19 +26,21 @@ interface DataTableToolbarProps<TData> {
|
|||||||
onKeywordChange: (keyword: string) => void;
|
onKeywordChange: (keyword: string) => void;
|
||||||
selectedFeatures: FeatureType[];
|
selectedFeatures: FeatureType[];
|
||||||
onFeaturesChange: (features: FeatureType[]) => void;
|
onFeaturesChange: (features: FeatureType[]) => void;
|
||||||
|
flags: WorkspaceFlagFilter;
|
||||||
|
onFlagsChange: (flags: WorkspaceFlagFilter) => void;
|
||||||
sort: AdminWorkspaceSort | undefined;
|
sort: AdminWorkspaceSort | undefined;
|
||||||
onSortChange: (sort: AdminWorkspaceSort | undefined) => void;
|
onSortChange: (sort: AdminWorkspaceSort | undefined) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortOptions: { value: AdminWorkspaceSort; label: string }[] = [
|
const sortOptions: { value: AdminWorkspaceSort; label: string }[] = [
|
||||||
{ value: AdminWorkspaceSort.SnapshotSize, label: 'Snapshot size' },
|
{ value: AdminWorkspaceSort.CreatedAt, label: 'Created time' },
|
||||||
{ value: AdminWorkspaceSort.BlobCount, label: 'Blob count' },
|
{ value: AdminWorkspaceSort.BlobCount, label: 'Blob count' },
|
||||||
{ value: AdminWorkspaceSort.BlobSize, label: 'Blob size' },
|
{ value: AdminWorkspaceSort.BlobSize, label: 'Blob size' },
|
||||||
{ value: AdminWorkspaceSort.SnapshotCount, label: 'Snapshot count' },
|
{ value: AdminWorkspaceSort.SnapshotCount, label: 'Snapshot count' },
|
||||||
|
{ value: AdminWorkspaceSort.SnapshotSize, label: 'Snapshot size' },
|
||||||
{ value: AdminWorkspaceSort.MemberCount, label: 'Member count' },
|
{ value: AdminWorkspaceSort.MemberCount, label: 'Member count' },
|
||||||
{ value: AdminWorkspaceSort.PublicPageCount, label: 'Public pages' },
|
{ value: AdminWorkspaceSort.PublicPageCount, label: 'Public pages' },
|
||||||
{ value: AdminWorkspaceSort.CreatedAt, label: 'Created time' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export function DataTableToolbar<TData>({
|
export function DataTableToolbar<TData>({
|
||||||
@@ -45,6 +48,8 @@ export function DataTableToolbar<TData>({
|
|||||||
onKeywordChange,
|
onKeywordChange,
|
||||||
selectedFeatures,
|
selectedFeatures,
|
||||||
onFeaturesChange,
|
onFeaturesChange,
|
||||||
|
flags,
|
||||||
|
onFlagsChange,
|
||||||
sort,
|
sort,
|
||||||
onSortChange,
|
onSortChange,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@@ -80,6 +85,35 @@ export function DataTableToolbar<TData>({
|
|||||||
[sort]
|
[sort]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const flagOptions: { key: keyof WorkspaceFlagFilter; label: string }[] = [
|
||||||
|
{ key: 'public', label: 'Public' },
|
||||||
|
{ key: 'enableSharing', label: 'Enable sharing' },
|
||||||
|
{ key: 'enableAi', label: 'Enable AI' },
|
||||||
|
{ key: 'enableUrlPreview', label: 'Enable URL preview' },
|
||||||
|
{ key: 'enableDocEmbedding', label: 'Enable doc embedding' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const flagLabel = (value: boolean | undefined) => {
|
||||||
|
if (value === true) return 'On';
|
||||||
|
if (value === false) return 'Off';
|
||||||
|
return 'Any';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFlagToggle = useCallback(
|
||||||
|
(key: keyof WorkspaceFlagFilter) => {
|
||||||
|
const current = flags[key];
|
||||||
|
const next =
|
||||||
|
current === undefined ? true : current === true ? false : undefined;
|
||||||
|
onFlagsChange({ ...flags, [key]: next });
|
||||||
|
},
|
||||||
|
[flags, onFlagsChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasFlagFilter = useMemo(
|
||||||
|
() => Object.values(flags).some(v => v !== undefined),
|
||||||
|
[flags]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-y-2 gap-x-4 flex-wrap">
|
<div className="flex items-center justify-between gap-y-2 gap-x-4 flex-wrap">
|
||||||
<FeatureFilterPopover
|
<FeatureFilterPopover
|
||||||
@@ -119,6 +153,37 @@ export function DataTableToolbar<TData>({
|
|||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
<Popover open={disabled ? false : undefined}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant={hasFlagFilter ? 'secondary' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
className="h-8 px-2 lg:px-3"
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
Flags
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[260px] p-2">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{flagOptions.map(option => (
|
||||||
|
<Button
|
||||||
|
key={option.key}
|
||||||
|
variant="ghost"
|
||||||
|
className="justify-between"
|
||||||
|
size="sm"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => handleFlagToggle(option.key)}
|
||||||
|
>
|
||||||
|
<span>{option.label}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{flagLabel(flags[option.key])}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Search Workspace / Owner"
|
placeholder="Search Workspace / Owner"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { ColumnDef, PaginationState } from '@tanstack/react-table';
|
|||||||
import type { Dispatch, SetStateAction } from 'react';
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
|
|
||||||
import { SharedDataTable } from '../../../components/shared/data-table';
|
import { SharedDataTable } from '../../../components/shared/data-table';
|
||||||
|
import type { WorkspaceFlagFilter } from '../schema';
|
||||||
import { DataTableToolbar } from './data-table-toolbar';
|
import { DataTableToolbar } from './data-table-toolbar';
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
@@ -14,6 +15,8 @@ interface DataTableProps<TData, TValue> {
|
|||||||
onKeywordChange: (value: string) => void;
|
onKeywordChange: (value: string) => void;
|
||||||
selectedFeatures: FeatureType[];
|
selectedFeatures: FeatureType[];
|
||||||
onFeaturesChange: (features: FeatureType[]) => void;
|
onFeaturesChange: (features: FeatureType[]) => void;
|
||||||
|
flags: WorkspaceFlagFilter;
|
||||||
|
onFlagsChange: Dispatch<SetStateAction<WorkspaceFlagFilter>>;
|
||||||
sort: AdminWorkspaceSort | undefined;
|
sort: AdminWorkspaceSort | undefined;
|
||||||
onSortChange: (sort: AdminWorkspaceSort | undefined) => void;
|
onSortChange: (sort: AdminWorkspaceSort | undefined) => void;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
@@ -34,6 +37,8 @@ export function DataTable<TData extends { id: string }, TValue>({
|
|||||||
onKeywordChange,
|
onKeywordChange,
|
||||||
selectedFeatures,
|
selectedFeatures,
|
||||||
onFeaturesChange,
|
onFeaturesChange,
|
||||||
|
flags,
|
||||||
|
onFlagsChange,
|
||||||
sort,
|
sort,
|
||||||
onSortChange,
|
onSortChange,
|
||||||
onPaginationChange,
|
onPaginationChange,
|
||||||
@@ -46,7 +51,7 @@ export function DataTable<TData extends { id: string }, TValue>({
|
|||||||
totalCount={workspacesCount}
|
totalCount={workspacesCount}
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
onPaginationChange={onPaginationChange}
|
onPaginationChange={onPaginationChange}
|
||||||
resetFiltersDeps={[keyword, selectedFeatures, sort]}
|
resetFiltersDeps={[keyword, selectedFeatures, sort, flags]}
|
||||||
renderToolbar={table => (
|
renderToolbar={table => (
|
||||||
<DataTableToolbar
|
<DataTableToolbar
|
||||||
table={table}
|
table={table}
|
||||||
@@ -54,6 +59,8 @@ export function DataTable<TData extends { id: string }, TValue>({
|
|||||||
onKeywordChange={onKeywordChange}
|
onKeywordChange={onKeywordChange}
|
||||||
selectedFeatures={selectedFeatures}
|
selectedFeatures={selectedFeatures}
|
||||||
onFeaturesChange={onFeaturesChange}
|
onFeaturesChange={onFeaturesChange}
|
||||||
|
flags={flags}
|
||||||
|
onFlagsChange={onFlagsChange}
|
||||||
sort={sort}
|
sort={sort}
|
||||||
onSortChange={onSortChange}
|
onSortChange={onSortChange}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ function WorkspacePanelContent({
|
|||||||
flags: {
|
flags: {
|
||||||
public: workspace.public,
|
public: workspace.public,
|
||||||
enableAi: workspace.enableAi,
|
enableAi: workspace.enableAi,
|
||||||
|
enableSharing: workspace.enableSharing,
|
||||||
enableUrlPreview: workspace.enableUrlPreview,
|
enableUrlPreview: workspace.enableUrlPreview,
|
||||||
enableDocEmbedding: workspace.enableDocEmbedding,
|
enableDocEmbedding: workspace.enableDocEmbedding,
|
||||||
name: workspace.name ?? '',
|
name: workspace.name ?? '',
|
||||||
@@ -110,6 +111,7 @@ function WorkspacePanelContent({
|
|||||||
return (
|
return (
|
||||||
flags.public !== baseline.flags.public ||
|
flags.public !== baseline.flags.public ||
|
||||||
flags.enableAi !== baseline.flags.enableAi ||
|
flags.enableAi !== baseline.flags.enableAi ||
|
||||||
|
flags.enableSharing !== baseline.flags.enableSharing ||
|
||||||
flags.enableUrlPreview !== baseline.flags.enableUrlPreview ||
|
flags.enableUrlPreview !== baseline.flags.enableUrlPreview ||
|
||||||
flags.enableDocEmbedding !== baseline.flags.enableDocEmbedding ||
|
flags.enableDocEmbedding !== baseline.flags.enableDocEmbedding ||
|
||||||
flags.name !== baseline.flags.name ||
|
flags.name !== baseline.flags.name ||
|
||||||
@@ -134,6 +136,7 @@ function WorkspacePanelContent({
|
|||||||
id: workspace.id,
|
id: workspace.id,
|
||||||
public: flags.public,
|
public: flags.public,
|
||||||
enableAi: flags.enableAi,
|
enableAi: flags.enableAi,
|
||||||
|
enableSharing: flags.enableSharing,
|
||||||
enableUrlPreview: flags.enableUrlPreview,
|
enableUrlPreview: flags.enableUrlPreview,
|
||||||
enableDocEmbedding: flags.enableDocEmbedding,
|
enableDocEmbedding: flags.enableDocEmbedding,
|
||||||
name: flags.name || null,
|
name: flags.name || null,
|
||||||
@@ -231,6 +234,15 @@ function WorkspacePanelContent({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
<FlagItem
|
||||||
|
label="Allow Workspace Sharing"
|
||||||
|
description="Allow pages in this workspace to be shared publicly"
|
||||||
|
checked={flags.enableSharing}
|
||||||
|
onCheckedChange={value =>
|
||||||
|
setFlags(prev => ({ ...prev, enableSharing: value }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Separator />
|
||||||
<FlagItem
|
<FlagItem
|
||||||
label="Enable Doc Embedding"
|
label="Enable Doc Embedding"
|
||||||
description="Allow document embedding for search"
|
description="Allow document embedding for search"
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import { useState } from 'react';
|
|||||||
import { Header } from '../header';
|
import { Header } from '../header';
|
||||||
import { useColumns } from './components/columns';
|
import { useColumns } from './components/columns';
|
||||||
import { DataTable } from './components/data-table';
|
import { DataTable } from './components/data-table';
|
||||||
|
import type { WorkspaceFlagFilter } from './schema';
|
||||||
import { useWorkspaceList } from './use-workspace-list';
|
import { useWorkspaceList } from './use-workspace-list';
|
||||||
|
|
||||||
export function WorkspacePage() {
|
export function WorkspacePage() {
|
||||||
const [keyword, setKeyword] = useState('');
|
const [keyword, setKeyword] = useState('');
|
||||||
const [featureFilters, setFeatureFilters] = useState<FeatureType[]>([]);
|
const [featureFilters, setFeatureFilters] = useState<FeatureType[]>([]);
|
||||||
|
const [flagFilters, setFlagFilters] = useState<WorkspaceFlagFilter>({});
|
||||||
const [sort, setSort] = useState<AdminWorkspaceSort | undefined>(
|
const [sort, setSort] = useState<AdminWorkspaceSort | undefined>(
|
||||||
AdminWorkspaceSort.CreatedAt
|
AdminWorkspaceSort.CreatedAt
|
||||||
);
|
);
|
||||||
@@ -18,6 +20,7 @@ export function WorkspacePage() {
|
|||||||
keyword,
|
keyword,
|
||||||
features: featureFilters,
|
features: featureFilters,
|
||||||
orderBy: sort,
|
orderBy: sort,
|
||||||
|
flags: flagFilters,
|
||||||
});
|
});
|
||||||
|
|
||||||
const columns = useColumns();
|
const columns = useColumns();
|
||||||
@@ -36,6 +39,8 @@ export function WorkspacePage() {
|
|||||||
onKeywordChange={setKeyword}
|
onKeywordChange={setKeyword}
|
||||||
selectedFeatures={featureFilters}
|
selectedFeatures={featureFilters}
|
||||||
onFeaturesChange={setFeatureFilters}
|
onFeaturesChange={setFeatureFilters}
|
||||||
|
flags={flagFilters}
|
||||||
|
onFlagsChange={setFlagFilters}
|
||||||
sort={sort}
|
sort={sort}
|
||||||
onSortChange={setSort}
|
onSortChange={setSort}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
|||||||
@@ -16,3 +16,11 @@ export type WorkspaceUpdateInput =
|
|||||||
AdminUpdateWorkspaceMutation['adminUpdateWorkspace'];
|
AdminUpdateWorkspaceMutation['adminUpdateWorkspace'];
|
||||||
|
|
||||||
export type WorkspaceFeatureFilter = FeatureType[];
|
export type WorkspaceFeatureFilter = FeatureType[];
|
||||||
|
|
||||||
|
export type WorkspaceFlagFilter = {
|
||||||
|
public?: boolean;
|
||||||
|
enableAi?: boolean;
|
||||||
|
enableSharing?: boolean;
|
||||||
|
enableUrlPreview?: boolean;
|
||||||
|
enableDocEmbedding?: boolean;
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ import {
|
|||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import type { WorkspaceFlagFilter } from './schema';
|
||||||
|
|
||||||
export const useWorkspaceList = (filter?: {
|
export const useWorkspaceList = (filter?: {
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
features?: FeatureType[];
|
features?: FeatureType[];
|
||||||
orderBy?: AdminWorkspaceSort;
|
orderBy?: AdminWorkspaceSort;
|
||||||
|
flags?: WorkspaceFlagFilter;
|
||||||
}) => {
|
}) => {
|
||||||
const [pagination, setPagination] = useState({
|
const [pagination, setPagination] = useState({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
@@ -21,8 +24,10 @@ export const useWorkspaceList = (filter?: {
|
|||||||
() =>
|
() =>
|
||||||
`${filter?.keyword ?? ''}-${[...(filter?.features ?? [])]
|
`${filter?.keyword ?? ''}-${[...(filter?.features ?? [])]
|
||||||
.sort()
|
.sort()
|
||||||
.join(',')}-${filter?.orderBy ?? ''}`,
|
.join(',')}-${filter?.orderBy ?? ''}-${JSON.stringify(
|
||||||
[filter?.features, filter?.keyword, filter?.orderBy]
|
filter?.flags ?? {}
|
||||||
|
)}`,
|
||||||
|
[filter?.features, filter?.flags, filter?.keyword, filter?.orderBy]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -40,10 +45,20 @@ export const useWorkspaceList = (filter?: {
|
|||||||
? filter.features
|
? filter.features
|
||||||
: undefined,
|
: undefined,
|
||||||
orderBy: filter?.orderBy,
|
orderBy: filter?.orderBy,
|
||||||
|
public: filter?.flags?.public,
|
||||||
|
enableAi: filter?.flags?.enableAi,
|
||||||
|
enableSharing: filter?.flags?.enableSharing,
|
||||||
|
enableUrlPreview: filter?.flags?.enableUrlPreview,
|
||||||
|
enableDocEmbedding: filter?.flags?.enableDocEmbedding,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
filter?.features,
|
filter?.features,
|
||||||
|
filter?.flags?.enableAi,
|
||||||
|
filter?.flags?.enableDocEmbedding,
|
||||||
|
filter?.flags?.enableSharing,
|
||||||
|
filter?.flags?.enableUrlPreview,
|
||||||
|
filter?.flags?.public,
|
||||||
filter?.keyword,
|
filter?.keyword,
|
||||||
filter?.orderBy,
|
filter?.orderBy,
|
||||||
pagination.pageIndex,
|
pagination.pageIndex,
|
||||||
|
|||||||
+22
@@ -21,11 +21,19 @@ export const SharingPanel = () => {
|
|||||||
export const Sharing = () => {
|
export const Sharing = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const shareSetting = useService(WorkspaceShareSettingService).sharePreview;
|
const shareSetting = useService(WorkspaceShareSettingService).sharePreview;
|
||||||
|
const enableSharing = useLiveData(shareSetting.enableSharing$);
|
||||||
const enableUrlPreview = useLiveData(shareSetting.enableUrlPreview$);
|
const enableUrlPreview = useLiveData(shareSetting.enableUrlPreview$);
|
||||||
const loading = useLiveData(shareSetting.isLoading$);
|
const loading = useLiveData(shareSetting.isLoading$);
|
||||||
const permissionService = useService(WorkspacePermissionService);
|
const permissionService = useService(WorkspacePermissionService);
|
||||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||||
|
|
||||||
|
const handleToggleSharing = useAsyncCallback(
|
||||||
|
async (checked: boolean) => {
|
||||||
|
await shareSetting.setEnableSharing(checked);
|
||||||
|
},
|
||||||
|
[shareSetting]
|
||||||
|
);
|
||||||
|
|
||||||
const handleCheck = useAsyncCallback(
|
const handleCheck = useAsyncCallback(
|
||||||
async (checked: boolean) => {
|
async (checked: boolean) => {
|
||||||
await shareSetting.setEnableUrlPreview(checked);
|
await shareSetting.setEnableUrlPreview(checked);
|
||||||
@@ -51,6 +59,20 @@ export const Sharing = () => {
|
|||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
|
<SettingRow
|
||||||
|
name={t[
|
||||||
|
'com.affine.settings.workspace.sharing.workspace-sharing.title'
|
||||||
|
]()}
|
||||||
|
desc={t[
|
||||||
|
'com.affine.settings.workspace.sharing.workspace-sharing.description'
|
||||||
|
]()}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
checked={enableSharing ?? true}
|
||||||
|
onChange={handleToggleSharing}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
</SettingWrapper>
|
</SettingWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||||
|
import { WorkspaceShareSettingService } from '@affine/core/modules/share-setting';
|
||||||
import type { Workspace } from '@affine/core/modules/workspace';
|
import type { Workspace } from '@affine/core/modules/workspace';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import type { Store } from '@blocksuite/affine/store';
|
import type { Store } from '@blocksuite/affine/store';
|
||||||
import { useCallback } from 'react';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import { ShareMenu } from './share-menu';
|
import { ShareMenu } from './share-menu';
|
||||||
export { CloudSvg } from './cloud-svg';
|
export { CloudSvg } from './cloud-svg';
|
||||||
@@ -14,6 +17,10 @@ type SharePageModalProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SharePageButton = ({ workspace, page }: SharePageModalProps) => {
|
export const SharePageButton = ({ workspace, page }: SharePageModalProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const shareSetting = useService(WorkspaceShareSettingService).sharePreview;
|
||||||
|
const enableSharing = useLiveData(shareSetting.enableSharing$);
|
||||||
|
|
||||||
const confirmEnableCloud = useEnableCloud();
|
const confirmEnableCloud = useEnableCloud();
|
||||||
const handleOpenShareModal = useCallback((open: boolean) => {
|
const handleOpenShareModal = useCallback((open: boolean) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -21,6 +28,18 @@ export const SharePageButton = ({ workspace, page }: SharePageModalProps) => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (workspace.meta.flavour === 'local') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
shareSetting.revalidate();
|
||||||
|
}, [shareSetting, workspace.meta.flavour]);
|
||||||
|
|
||||||
|
const sharingDisabled = enableSharing === false;
|
||||||
|
const disabledReason = sharingDisabled
|
||||||
|
? t['com.affine.share-menu.workspace-sharing.disabled.tooltip']()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShareMenu
|
<ShareMenu
|
||||||
workspaceMetadata={workspace.meta}
|
workspaceMetadata={workspace.meta}
|
||||||
@@ -31,6 +50,8 @@ export const SharePageButton = ({ workspace, page }: SharePageModalProps) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
onOpenShareModal={handleOpenShareModal}
|
onOpenShareModal={handleOpenShareModal}
|
||||||
|
disabled={sharingDisabled}
|
||||||
|
disabledReason={disabledReason}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export interface ShareMenuProps extends PropsWithChildren {
|
|||||||
onOpenShareModal?: (open: boolean) => void;
|
onOpenShareModal?: (open: boolean) => void;
|
||||||
openPaywallModal?: () => void;
|
openPaywallModal?: () => void;
|
||||||
hittingPaywall?: boolean;
|
hittingPaywall?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
disabledReason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ShareMenuTab {
|
export enum ShareMenuTab {
|
||||||
@@ -203,7 +205,7 @@ export const ShareMenuContent = (props: ShareMenuProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const DefaultShareButton = forwardRef(function DefaultShareButton(
|
const DefaultShareButton = forwardRef(function DefaultShareButton(
|
||||||
_,
|
props: { disabled?: boolean; tooltip?: string },
|
||||||
ref: Ref<HTMLButtonElement>
|
ref: Ref<HTMLButtonElement>
|
||||||
) {
|
) {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
@@ -211,18 +213,26 @@ const DefaultShareButton = forwardRef(function DefaultShareButton(
|
|||||||
const shared = useLiveData(shareInfoService.shareInfo.isShared$);
|
const shared = useLiveData(shareInfoService.shareInfo.isShared$);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (props.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
shareInfoService.shareInfo.revalidate();
|
shareInfoService.shareInfo.revalidate();
|
||||||
}, [shareInfoService]);
|
}, [props.disabled, shareInfoService]);
|
||||||
|
|
||||||
|
const tooltip =
|
||||||
|
props.tooltip ??
|
||||||
|
(shared
|
||||||
|
? t['com.affine.share-menu.option.link.readonly.description']()
|
||||||
|
: t['com.affine.share-menu.option.link.no-access.description']());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip content={tooltip}>
|
||||||
content={
|
<Button
|
||||||
shared
|
ref={ref}
|
||||||
? t['com.affine.share-menu.option.link.readonly.description']()
|
className={styles.button}
|
||||||
: t['com.affine.share-menu.option.link.no-access.description']()
|
variant="primary"
|
||||||
}
|
disabled={props.disabled}
|
||||||
>
|
>
|
||||||
<Button ref={ref} className={styles.button} variant="primary">
|
|
||||||
<div className={styles.buttonContainer}>
|
<div className={styles.buttonContainer}>
|
||||||
{shared ? <PublishIcon fontSize={16} /> : <LockIcon fontSize={16} />}
|
{shared ? <PublishIcon fontSize={16} /> : <LockIcon fontSize={16} />}
|
||||||
{t['com.affine.share-menu.shareButton']()}
|
{t['com.affine.share-menu.shareButton']()}
|
||||||
@@ -233,6 +243,13 @@ const DefaultShareButton = forwardRef(function DefaultShareButton(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const LocalShareMenu = (props: ShareMenuProps) => {
|
const LocalShareMenu = (props: ShareMenuProps) => {
|
||||||
|
if (props.disabled) {
|
||||||
|
return (
|
||||||
|
<div data-testid="local-share-menu-button">
|
||||||
|
<DefaultShareButton disabled tooltip={props.disabledReason} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
items={<ShareMenuContent {...props} />}
|
items={<ShareMenuContent {...props} />}
|
||||||
@@ -254,6 +271,13 @@ const LocalShareMenu = (props: ShareMenuProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const CloudShareMenu = (props: ShareMenuProps) => {
|
const CloudShareMenu = (props: ShareMenuProps) => {
|
||||||
|
if (props.disabled) {
|
||||||
|
return (
|
||||||
|
<div data-testid="cloud-share-menu-button">
|
||||||
|
<DefaultShareButton disabled tooltip={props.disabledReason} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
items={<ShareMenuContent {...props} />}
|
items={<ShareMenuContent {...props} />}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type { WorkspaceService } from '../../workspace';
|
|||||||
import type { WorkspaceShareSettingStore } from '../stores/share-setting';
|
import type { WorkspaceShareSettingStore } from '../stores/share-setting';
|
||||||
|
|
||||||
type EnableAi = GetWorkspaceConfigQuery['workspace']['enableAi'];
|
type EnableAi = GetWorkspaceConfigQuery['workspace']['enableAi'];
|
||||||
|
type EnableSharing = GetWorkspaceConfigQuery['workspace']['enableSharing'];
|
||||||
type EnableUrlPreview =
|
type EnableUrlPreview =
|
||||||
GetWorkspaceConfigQuery['workspace']['enableUrlPreview'];
|
GetWorkspaceConfigQuery['workspace']['enableUrlPreview'];
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ const logger = new DebugLogger('affine:workspace-permission');
|
|||||||
|
|
||||||
export class WorkspaceShareSetting extends Entity {
|
export class WorkspaceShareSetting extends Entity {
|
||||||
enableAi$ = new LiveData<EnableAi | null>(null);
|
enableAi$ = new LiveData<EnableAi | null>(null);
|
||||||
|
enableSharing$ = new LiveData<EnableSharing | null>(null);
|
||||||
enableUrlPreview$ = new LiveData<EnableUrlPreview | null>(null);
|
enableUrlPreview$ = new LiveData<EnableUrlPreview | null>(null);
|
||||||
inviteLink$ = new LiveData<InviteLink | null>(null);
|
inviteLink$ = new LiveData<InviteLink | null>(null);
|
||||||
isLoading$ = new LiveData(false);
|
isLoading$ = new LiveData(false);
|
||||||
@@ -48,12 +50,13 @@ export class WorkspaceShareSetting extends Entity {
|
|||||||
tap(value => {
|
tap(value => {
|
||||||
if (value) {
|
if (value) {
|
||||||
this.enableAi$.next(value.enableAi);
|
this.enableAi$.next(value.enableAi);
|
||||||
|
this.enableSharing$.next(value.enableSharing);
|
||||||
this.enableUrlPreview$.next(value.enableUrlPreview);
|
this.enableUrlPreview$.next(value.enableUrlPreview);
|
||||||
this.inviteLink$.next(value.inviteLink);
|
this.inviteLink$.next(value.inviteLink);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
catchErrorInto(this.error$, error => {
|
catchErrorInto(this.error$, error => {
|
||||||
logger.error('Failed to fetch enableUrlPreview', error);
|
logger.error('Failed to fetch workspace share settings', error);
|
||||||
}),
|
}),
|
||||||
onStart(() => this.isLoading$.setValue(true)),
|
onStart(() => this.isLoading$.setValue(true)),
|
||||||
onComplete(() => this.isLoading$.setValue(false))
|
onComplete(() => this.isLoading$.setValue(false))
|
||||||
@@ -74,6 +77,14 @@ export class WorkspaceShareSetting extends Entity {
|
|||||||
await this.waitForRevalidation();
|
await this.waitForRevalidation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setEnableSharing(enableSharing: EnableSharing) {
|
||||||
|
await this.store.updateWorkspaceEnableSharing(
|
||||||
|
this.workspaceService.workspace.id,
|
||||||
|
enableSharing
|
||||||
|
);
|
||||||
|
await this.waitForRevalidation();
|
||||||
|
}
|
||||||
|
|
||||||
async setEnableAi(enableAi: EnableAi) {
|
async setEnableAi(enableAi: EnableAi) {
|
||||||
await this.store.updateWorkspaceEnableAi(
|
await this.store.updateWorkspaceEnableAi(
|
||||||
this.workspaceService.workspace.id,
|
this.workspaceService.workspace.id,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { WorkspaceServerService } from '@affine/core/modules/cloud';
|
|||||||
import {
|
import {
|
||||||
getWorkspaceConfigQuery,
|
getWorkspaceConfigQuery,
|
||||||
setEnableAiMutation,
|
setEnableAiMutation,
|
||||||
|
setEnableSharingMutation,
|
||||||
setEnableUrlPreviewMutation,
|
setEnableUrlPreviewMutation,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
import { Store } from '@toeverything/infra';
|
import { Store } from '@toeverything/infra';
|
||||||
@@ -47,6 +48,26 @@ export class WorkspaceShareSettingStore extends Store {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateWorkspaceEnableSharing(
|
||||||
|
workspaceId: string,
|
||||||
|
enableSharing: boolean,
|
||||||
|
signal?: AbortSignal
|
||||||
|
) {
|
||||||
|
if (!this.workspaceServerService.server) {
|
||||||
|
throw new Error('No Server');
|
||||||
|
}
|
||||||
|
await this.workspaceServerService.server.gql({
|
||||||
|
query: setEnableSharingMutation,
|
||||||
|
variables: {
|
||||||
|
id: workspaceId,
|
||||||
|
enableSharing,
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async updateWorkspaceEnableUrlPreview(
|
async updateWorkspaceEnableUrlPreview(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
enableUrlPreview: boolean,
|
enableUrlPreview: boolean,
|
||||||
|
|||||||
@@ -6346,6 +6346,14 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Always enable url preview`
|
* `Always enable url preview`
|
||||||
*/
|
*/
|
||||||
["com.affine.settings.workspace.sharing.url-preview.title"](): string;
|
["com.affine.settings.workspace.sharing.url-preview.title"](): string;
|
||||||
|
/**
|
||||||
|
* `Control whether pages in this workspace can be shared publicly. Turn off to block new shares and external access for existing shares.`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.sharing.workspace-sharing.description"](): string;
|
||||||
|
/**
|
||||||
|
* `Allow workspace page sharing`
|
||||||
|
*/
|
||||||
|
["com.affine.settings.workspace.sharing.workspace-sharing.title"](): string;
|
||||||
/**
|
/**
|
||||||
* `AFFiNE AI`
|
* `AFFiNE AI`
|
||||||
*/
|
*/
|
||||||
@@ -6605,6 +6613,10 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Anyone can access this link`
|
* `Anyone can access this link`
|
||||||
*/
|
*/
|
||||||
["com.affine.share-menu.option.link.readonly.description"](): string;
|
["com.affine.share-menu.option.link.readonly.description"](): string;
|
||||||
|
/**
|
||||||
|
* `Sharing for this workspace is turned off. Please contact an admin to enable it.`
|
||||||
|
*/
|
||||||
|
["com.affine.share-menu.workspace-sharing.disabled.tooltip"](): string;
|
||||||
/**
|
/**
|
||||||
* `Can manage`
|
* `Can manage`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1591,6 +1591,8 @@
|
|||||||
"com.affine.settings.workspace.sharing.title": "Sharing",
|
"com.affine.settings.workspace.sharing.title": "Sharing",
|
||||||
"com.affine.settings.workspace.sharing.url-preview.description": "Allow URL unfurling by Slack & other social apps, even if a doc is only accessible by workspace members.",
|
"com.affine.settings.workspace.sharing.url-preview.description": "Allow URL unfurling by Slack & other social apps, even if a doc is only accessible by workspace members.",
|
||||||
"com.affine.settings.workspace.sharing.url-preview.title": "Always enable url preview",
|
"com.affine.settings.workspace.sharing.url-preview.title": "Always enable url preview",
|
||||||
|
"com.affine.settings.workspace.sharing.workspace-sharing.description": "Control whether pages in this workspace can be shared publicly. Turn off to block new shares and external access for existing shares.",
|
||||||
|
"com.affine.settings.workspace.sharing.workspace-sharing.title": "Allow workspace page sharing",
|
||||||
"com.affine.settings.workspace.affine-ai.title": "AFFiNE AI",
|
"com.affine.settings.workspace.affine-ai.title": "AFFiNE AI",
|
||||||
"com.affine.settings.workspace.affine-ai.label": "Allow AFFiNE AI Assistant",
|
"com.affine.settings.workspace.affine-ai.label": "Allow AFFiNE AI Assistant",
|
||||||
"com.affine.settings.workspace.affine-ai.description": "Allow workspace members to use AFFiNE AI features. This setting doesn't affect billing. Workspace members use AFFiNE AI through their personal accounts.",
|
"com.affine.settings.workspace.affine-ai.description": "Allow workspace members to use AFFiNE AI features. This setting doesn't affect billing. Workspace members use AFFiNE AI through their personal accounts.",
|
||||||
@@ -1655,6 +1657,7 @@
|
|||||||
"com.affine.share-menu.option.link.no-access.description": "Only workspace members can access this link",
|
"com.affine.share-menu.option.link.no-access.description": "Only workspace members can access this link",
|
||||||
"com.affine.share-menu.option.link.readonly": "Read only",
|
"com.affine.share-menu.option.link.readonly": "Read only",
|
||||||
"com.affine.share-menu.option.link.readonly.description": "Anyone can access this link",
|
"com.affine.share-menu.option.link.readonly.description": "Anyone can access this link",
|
||||||
|
"com.affine.share-menu.workspace-sharing.disabled.tooltip": "Sharing for this workspace is turned off. Please contact an admin to enable it.",
|
||||||
"com.affine.share-menu.option.permission.can-manage": "Can manage",
|
"com.affine.share-menu.option.permission.can-manage": "Can manage",
|
||||||
"com.affine.share-menu.option.permission.can-edit": "Can edit",
|
"com.affine.share-menu.option.permission.can-edit": "Can edit",
|
||||||
"com.affine.share-menu.option.permission.can-read": "Can read",
|
"com.affine.share-menu.option.permission.can-read": "Can read",
|
||||||
|
|||||||
@@ -1589,6 +1589,8 @@
|
|||||||
"com.affine.settings.workspace.sharing.title": "分享",
|
"com.affine.settings.workspace.sharing.title": "分享",
|
||||||
"com.affine.settings.workspace.sharing.url-preview.description": "允许 Slack 和其他社交应用程序展开 URL,即使文档仅由工作区成员访问。",
|
"com.affine.settings.workspace.sharing.url-preview.description": "允许 Slack 和其他社交应用程序展开 URL,即使文档仅由工作区成员访问。",
|
||||||
"com.affine.settings.workspace.sharing.url-preview.title": "始终启用 URL 预览",
|
"com.affine.settings.workspace.sharing.url-preview.title": "始终启用 URL 预览",
|
||||||
|
"com.affine.settings.workspace.sharing.workspace-sharing.description": "控制此工作区的页面是否允许公开分享。关闭后,禁止新的分享且现有分享外部无法访问。",
|
||||||
|
"com.affine.settings.workspace.sharing.workspace-sharing.title": "允许工作区页面分享",
|
||||||
"com.affine.settings.workspace.affine-ai.title": "AFFiNE AI",
|
"com.affine.settings.workspace.affine-ai.title": "AFFiNE AI",
|
||||||
"com.affine.settings.workspace.affine-ai.label": "启用 AFFiNE AI 助手",
|
"com.affine.settings.workspace.affine-ai.label": "启用 AFFiNE AI 助手",
|
||||||
"com.affine.settings.workspace.affine-ai.description": "允许工作区成员使用 AFFiNE AI 功能。此设置不会影响计费。工作区成员通过个人帐户使用 AFFiNE AI。",
|
"com.affine.settings.workspace.affine-ai.description": "允许工作区成员使用 AFFiNE AI 功能。此设置不会影响计费。工作区成员通过个人帐户使用 AFFiNE AI。",
|
||||||
@@ -1653,6 +1655,7 @@
|
|||||||
"com.affine.share-menu.option.link.no-access.description": "只有此工作区的成员可以打开此链接。",
|
"com.affine.share-menu.option.link.no-access.description": "只有此工作区的成员可以打开此链接。",
|
||||||
"com.affine.share-menu.option.link.readonly": "只读",
|
"com.affine.share-menu.option.link.readonly": "只读",
|
||||||
"com.affine.share-menu.option.link.readonly.description": "任何人可以访问该链接",
|
"com.affine.share-menu.option.link.readonly.description": "任何人可以访问该链接",
|
||||||
|
"com.affine.share-menu.workspace-sharing.disabled.tooltip": "该工作区已禁用分享,请联系管理员开启。",
|
||||||
"com.affine.share-menu.option.permission.can-manage": "可管理",
|
"com.affine.share-menu.option.permission.can-manage": "可管理",
|
||||||
"com.affine.share-menu.option.permission.can-edit": "可编辑",
|
"com.affine.share-menu.option.permission.can-edit": "可编辑",
|
||||||
"com.affine.share-menu.option.permission.can-read": "可阅读",
|
"com.affine.share-menu.option.permission.can-read": "可阅读",
|
||||||
|
|||||||
@@ -1566,6 +1566,8 @@
|
|||||||
"com.affine.settings.workspace.sharing.title": "分享",
|
"com.affine.settings.workspace.sharing.title": "分享",
|
||||||
"com.affine.settings.workspace.sharing.url-preview.description": "允許 Slack 和其他社交應用程序展開 URL,即使文件僅由工作區成員訪問。",
|
"com.affine.settings.workspace.sharing.url-preview.description": "允許 Slack 和其他社交應用程序展開 URL,即使文件僅由工作區成員訪問。",
|
||||||
"com.affine.settings.workspace.sharing.url-preview.title": "始終啟用 URL 預覽",
|
"com.affine.settings.workspace.sharing.url-preview.title": "始終啟用 URL 預覽",
|
||||||
|
"com.affine.settings.workspace.sharing.workspace-sharing.description": "控制此工作區的頁面是否允許公開分享。關閉後,禁止新的分享且现有分享外部無法訪問。",
|
||||||
|
"com.affine.settings.workspace.sharing.workspace-sharing.title": "允許工作區頁面分享",
|
||||||
"com.affine.settings.workspace.affine-ai.title": "AFFiNE AI",
|
"com.affine.settings.workspace.affine-ai.title": "AFFiNE AI",
|
||||||
"com.affine.settings.workspace.affine-ai.label": "啟用 AFFiNE AI 助理",
|
"com.affine.settings.workspace.affine-ai.label": "啟用 AFFiNE AI 助理",
|
||||||
"com.affine.settings.workspace.affine-ai.description": "允許工作區成員使用 AFFiNE AI 功能。此設置不影響計費。工作區成員透過他們的個人帳號使用 AFFiNE AI。",
|
"com.affine.settings.workspace.affine-ai.description": "允許工作區成員使用 AFFiNE AI 功能。此設置不影響計費。工作區成員透過他們的個人帳號使用 AFFiNE AI。",
|
||||||
@@ -1630,6 +1632,7 @@
|
|||||||
"com.affine.share-menu.option.link.no-access.description": "只有此工作區的成員可以打開此連結。",
|
"com.affine.share-menu.option.link.no-access.description": "只有此工作區的成員可以打開此連結。",
|
||||||
"com.affine.share-menu.option.link.readonly": "只讀",
|
"com.affine.share-menu.option.link.readonly": "只讀",
|
||||||
"com.affine.share-menu.option.link.readonly.description": "任何人可以訪問該連結",
|
"com.affine.share-menu.option.link.readonly.description": "任何人可以訪問該連結",
|
||||||
|
"com.affine.share-menu.workspace-sharing.disabled.tooltip": "此工作區已停用分享,請聯絡管理員開啟。",
|
||||||
"com.affine.share-menu.option.permission.can-manage": "可管理",
|
"com.affine.share-menu.option.permission.can-manage": "可管理",
|
||||||
"com.affine.share-menu.option.permission.can-edit": "可編輯",
|
"com.affine.share-menu.option.permission.can-edit": "可編輯",
|
||||||
"com.affine.share-menu.option.permission.can-read": "可閱讀",
|
"com.affine.share-menu.option.permission.can-read": "可閱讀",
|
||||||
|
|||||||
Reference in New Issue
Block a user