feat(core): add enable url preview to workspace settings (#8089)
This commit is contained in:
@@ -17,6 +17,7 @@ import { ExportPanel } from './export';
|
|||||||
import { LabelsPanel } from './labels';
|
import { LabelsPanel } from './labels';
|
||||||
import { MembersPanel } from './members';
|
import { MembersPanel } from './members';
|
||||||
import { ProfilePanel } from './profile';
|
import { ProfilePanel } from './profile';
|
||||||
|
import { SharingPanel } from './sharing';
|
||||||
import type { WorkspaceSettingDetailProps } from './types';
|
import type { WorkspaceSettingDetailProps } from './types';
|
||||||
|
|
||||||
export const WorkspaceSettingDetail = ({
|
export const WorkspaceSettingDetail = ({
|
||||||
@@ -67,6 +68,7 @@ export const WorkspaceSettingDetail = ({
|
|||||||
<EnableCloudPanel />
|
<EnableCloudPanel />
|
||||||
<MembersPanel />
|
<MembersPanel />
|
||||||
</SettingWrapper>
|
</SettingWrapper>
|
||||||
|
<SharingPanel />
|
||||||
{environment.isElectron && (
|
{environment.isElectron && (
|
||||||
<SettingWrapper title={t['Storage and Export']()}>
|
<SettingWrapper title={t['Storage and Export']()}>
|
||||||
<ExportPanel
|
<ExportPanel
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Switch } from '@affine/component';
|
||||||
|
import {
|
||||||
|
SettingRow,
|
||||||
|
SettingWrapper,
|
||||||
|
} from '@affine/component/setting-components';
|
||||||
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
|
import { WorkspaceShareSettingService } from '@affine/core/modules/share-setting';
|
||||||
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||||
|
|
||||||
|
export const SharingPanel = () => {
|
||||||
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
|
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return <Sharing />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Sharing = () => {
|
||||||
|
const t = useI18n();
|
||||||
|
const shareSetting = useService(WorkspaceShareSettingService).sharePreview;
|
||||||
|
const enableUrlPreview = useLiveData(shareSetting.enableUrlPreview$);
|
||||||
|
const loading = useLiveData(shareSetting.isLoading$);
|
||||||
|
|
||||||
|
const handleCheck = useAsyncCallback(
|
||||||
|
async (checked: boolean) => {
|
||||||
|
await shareSetting.setEnableUrlPreview(checked);
|
||||||
|
},
|
||||||
|
[shareSetting]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingWrapper title={t['com.affine.settings.workspace.sharing.title']()}>
|
||||||
|
<SettingRow
|
||||||
|
name={t['com.affine.settings.workspace.sharing.url-preview.title']()}
|
||||||
|
desc={t[
|
||||||
|
'com.affine.settings.workspace.sharing.url-preview.description'
|
||||||
|
]()}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
checked={enableUrlPreview || false}
|
||||||
|
onChange={handleCheck}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
</SettingWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -19,6 +19,7 @@ import { configurePermissionsModule } from './permissions';
|
|||||||
import { configureWorkspacePropertiesModule } from './properties';
|
import { configureWorkspacePropertiesModule } from './properties';
|
||||||
import { configureQuickSearchModule } from './quicksearch';
|
import { configureQuickSearchModule } from './quicksearch';
|
||||||
import { configureShareDocsModule } from './share-doc';
|
import { configureShareDocsModule } from './share-doc';
|
||||||
|
import { configureShareSettingModule } from './share-setting';
|
||||||
import { configureSystemFontFamilyModule } from './system-font-family';
|
import { configureSystemFontFamilyModule } from './system-font-family';
|
||||||
import { configureTagModule } from './tag';
|
import { configureTagModule } from './tag';
|
||||||
import { configureTelemetryModule } from './telemetry';
|
import { configureTelemetryModule } from './telemetry';
|
||||||
@@ -34,6 +35,7 @@ export function configureCommonModules(framework: Framework) {
|
|||||||
configureQuotaModule(framework);
|
configureQuotaModule(framework);
|
||||||
configurePermissionsModule(framework);
|
configurePermissionsModule(framework);
|
||||||
configureShareDocsModule(framework);
|
configureShareDocsModule(framework);
|
||||||
|
configureShareSettingModule(framework);
|
||||||
configureTelemetryModule(framework);
|
configureTelemetryModule(framework);
|
||||||
configureFindInPageModule(framework);
|
configureFindInPageModule(framework);
|
||||||
configurePeekViewModule(framework);
|
configurePeekViewModule(framework);
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { DebugLogger } from '@affine/debug';
|
||||||
|
import type { GetEnableUrlPreviewQuery } from '@affine/graphql';
|
||||||
|
import type { WorkspaceService } from '@toeverything/infra';
|
||||||
|
import {
|
||||||
|
backoffRetry,
|
||||||
|
catchErrorInto,
|
||||||
|
effect,
|
||||||
|
Entity,
|
||||||
|
fromPromise,
|
||||||
|
LiveData,
|
||||||
|
mapInto,
|
||||||
|
onComplete,
|
||||||
|
onStart,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { exhaustMap } from 'rxjs';
|
||||||
|
|
||||||
|
import { isBackendError, isNetworkError } from '../../cloud';
|
||||||
|
import type { WorkspaceShareSettingStore } from '../stores/share-setting';
|
||||||
|
|
||||||
|
type EnableUrlPreview =
|
||||||
|
GetEnableUrlPreviewQuery['workspace']['enableUrlPreview'];
|
||||||
|
|
||||||
|
const logger = new DebugLogger('affine:workspace-permission');
|
||||||
|
|
||||||
|
export class WorkspaceShareSetting extends Entity {
|
||||||
|
enableUrlPreview$ = new LiveData<EnableUrlPreview | null>(null);
|
||||||
|
isLoading$ = new LiveData(false);
|
||||||
|
error$ = new LiveData<any>(null);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly workspaceService: WorkspaceService,
|
||||||
|
private readonly store: WorkspaceShareSettingStore
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.revalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidate = effect(
|
||||||
|
exhaustMap(() => {
|
||||||
|
return fromPromise(signal =>
|
||||||
|
this.store.fetchWorkspaceEnableUrlPreview(
|
||||||
|
this.workspaceService.workspace.id,
|
||||||
|
signal
|
||||||
|
)
|
||||||
|
).pipe(
|
||||||
|
backoffRetry({
|
||||||
|
when: isNetworkError,
|
||||||
|
count: Infinity,
|
||||||
|
}),
|
||||||
|
backoffRetry({
|
||||||
|
when: isBackendError,
|
||||||
|
count: 3,
|
||||||
|
}),
|
||||||
|
mapInto(this.enableUrlPreview$),
|
||||||
|
catchErrorInto(this.error$, error => {
|
||||||
|
logger.error('Failed to fetch enableUrlPreview', error);
|
||||||
|
}),
|
||||||
|
onStart(() => this.isLoading$.setValue(true)),
|
||||||
|
onComplete(() => this.isLoading$.setValue(false))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
async waitForRevalidation(signal?: AbortSignal) {
|
||||||
|
this.revalidate();
|
||||||
|
await this.isLoading$.waitFor(isLoading => !isLoading, signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setEnableUrlPreview(enableUrlPreview: EnableUrlPreview) {
|
||||||
|
await this.store.updateWorkspaceEnableUrlPreview(
|
||||||
|
this.workspaceService.workspace.id,
|
||||||
|
enableUrlPreview
|
||||||
|
);
|
||||||
|
await this.waitForRevalidation();
|
||||||
|
}
|
||||||
|
|
||||||
|
override dispose(): void {
|
||||||
|
this.revalidate.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
23
packages/frontend/core/src/modules/share-setting/index.ts
Normal file
23
packages/frontend/core/src/modules/share-setting/index.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export { WorkspaceShareSettingService } from './services/share-setting';
|
||||||
|
|
||||||
|
import { GraphQLService } from '@affine/core/modules/cloud';
|
||||||
|
import {
|
||||||
|
type Framework,
|
||||||
|
WorkspaceScope,
|
||||||
|
WorkspaceService,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { WorkspaceShareSetting } from './entities/share-setting';
|
||||||
|
import { WorkspaceShareSettingService } from './services/share-setting';
|
||||||
|
import { WorkspaceShareSettingStore } from './stores/share-setting';
|
||||||
|
|
||||||
|
export function configureShareSettingModule(framework: Framework) {
|
||||||
|
framework
|
||||||
|
.scope(WorkspaceScope)
|
||||||
|
.service(WorkspaceShareSettingService)
|
||||||
|
.store(WorkspaceShareSettingStore, [GraphQLService])
|
||||||
|
.entity(WorkspaceShareSetting, [
|
||||||
|
WorkspaceService,
|
||||||
|
WorkspaceShareSettingStore,
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { WorkspaceShareSetting } from '../entities/share-setting';
|
||||||
|
|
||||||
|
export class WorkspaceShareSettingService extends Service {
|
||||||
|
sharePreview = this.framework.createEntity(WorkspaceShareSetting);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import type { GraphQLService } from '@affine/core/modules/cloud';
|
||||||
|
import {
|
||||||
|
getEnableUrlPreviewQuery,
|
||||||
|
setEnableUrlPreviewMutation,
|
||||||
|
} from '@affine/graphql';
|
||||||
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
|
export class WorkspaceShareSettingStore extends Store {
|
||||||
|
constructor(private readonly graphqlService: GraphQLService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchWorkspaceEnableUrlPreview(
|
||||||
|
workspaceId: string,
|
||||||
|
signal?: AbortSignal
|
||||||
|
) {
|
||||||
|
const data = await this.graphqlService.gql({
|
||||||
|
query: getEnableUrlPreviewQuery,
|
||||||
|
variables: {
|
||||||
|
id: workspaceId,
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return data.workspace.enableUrlPreview;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateWorkspaceEnableUrlPreview(
|
||||||
|
workspaceId: string,
|
||||||
|
enableUrlPreview: boolean,
|
||||||
|
signal?: AbortSignal
|
||||||
|
) {
|
||||||
|
await this.graphqlService.gql({
|
||||||
|
query: setEnableUrlPreviewMutation,
|
||||||
|
variables: {
|
||||||
|
id: workspaceId,
|
||||||
|
enableUrlPreview,
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
signal,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1170,6 +1170,32 @@ mutation verifyEmail($token: String!) {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getEnableUrlPreviewQuery = {
|
||||||
|
id: 'getEnableUrlPreviewQuery' as const,
|
||||||
|
operationName: 'getEnableUrlPreview',
|
||||||
|
definitionName: 'workspace',
|
||||||
|
containsFile: false,
|
||||||
|
query: `
|
||||||
|
query getEnableUrlPreview($id: String!) {
|
||||||
|
workspace(id: $id) {
|
||||||
|
enableUrlPreview
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setEnableUrlPreviewMutation = {
|
||||||
|
id: 'setEnableUrlPreviewMutation' as const,
|
||||||
|
operationName: 'setEnableUrlPreview',
|
||||||
|
definitionName: 'updateWorkspace',
|
||||||
|
containsFile: false,
|
||||||
|
query: `
|
||||||
|
mutation setEnableUrlPreview($id: ID!, $enableUrlPreview: Boolean!) {
|
||||||
|
updateWorkspace(input: {id: $id, enableUrlPreview: $enableUrlPreview}) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
export const enabledFeaturesQuery = {
|
export const enabledFeaturesQuery = {
|
||||||
id: 'enabledFeaturesQuery' as const,
|
id: 'enabledFeaturesQuery' as const,
|
||||||
operationName: 'enabledFeatures',
|
operationName: 'enabledFeatures',
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
query getEnableUrlPreview($id: String!) {
|
||||||
|
workspace(id: $id) {
|
||||||
|
enableUrlPreview
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
mutation setEnableUrlPreview($id: ID!, $enableUrlPreview: Boolean!) {
|
||||||
|
updateWorkspace(input: { id: $id, enableUrlPreview: $enableUrlPreview }) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2325,6 +2325,25 @@ export type VerifyEmailMutation = {
|
|||||||
verifyEmail: boolean;
|
verifyEmail: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetEnableUrlPreviewQueryVariables = Exact<{
|
||||||
|
id: Scalars['String']['input'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type GetEnableUrlPreviewQuery = {
|
||||||
|
__typename?: 'Query';
|
||||||
|
workspace: { __typename?: 'WorkspaceType'; enableUrlPreview: boolean };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SetEnableUrlPreviewMutationVariables = Exact<{
|
||||||
|
id: Scalars['ID']['input'];
|
||||||
|
enableUrlPreview: Scalars['Boolean']['input'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type SetEnableUrlPreviewMutation = {
|
||||||
|
__typename?: 'Mutation';
|
||||||
|
updateWorkspace: { __typename?: 'WorkspaceType'; id: string };
|
||||||
|
};
|
||||||
|
|
||||||
export type EnabledFeaturesQueryVariables = Exact<{
|
export type EnabledFeaturesQueryVariables = Exact<{
|
||||||
id: Scalars['String']['input'];
|
id: Scalars['String']['input'];
|
||||||
}>;
|
}>;
|
||||||
@@ -2619,6 +2638,11 @@ export type Queries =
|
|||||||
variables: SubscriptionQueryVariables;
|
variables: SubscriptionQueryVariables;
|
||||||
response: SubscriptionQuery;
|
response: SubscriptionQuery;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'getEnableUrlPreviewQuery';
|
||||||
|
variables: GetEnableUrlPreviewQueryVariables;
|
||||||
|
response: GetEnableUrlPreviewQuery;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'enabledFeaturesQuery';
|
name: 'enabledFeaturesQuery';
|
||||||
variables: EnabledFeaturesQueryVariables;
|
variables: EnabledFeaturesQueryVariables;
|
||||||
@@ -2831,6 +2855,11 @@ export type Mutations =
|
|||||||
variables: VerifyEmailMutationVariables;
|
variables: VerifyEmailMutationVariables;
|
||||||
response: VerifyEmailMutation;
|
response: VerifyEmailMutation;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'setEnableUrlPreviewMutation';
|
||||||
|
variables: SetEnableUrlPreviewMutationVariables;
|
||||||
|
response: SetEnableUrlPreviewMutation;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'setWorkspaceExperimentalFeatureMutation';
|
name: 'setWorkspaceExperimentalFeatureMutation';
|
||||||
variables: SetWorkspaceExperimentalFeatureMutationVariables;
|
variables: SetWorkspaceExperimentalFeatureMutationVariables;
|
||||||
|
|||||||
@@ -1407,6 +1407,9 @@
|
|||||||
"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.storage.tip": "Click to move storage location.",
|
"com.affine.settings.workspace.storage.tip": "Click to move storage location.",
|
||||||
|
"com.affine.settings.workspace.sharing.title": "Sharing",
|
||||||
|
"com.affine.settings.workspace.sharing.url-preview.title": "Always Enable URL Preview",
|
||||||
|
"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.share-menu.EnableCloudDescription": "Sharing doc requires AFFiNE Cloud.",
|
"com.affine.share-menu.EnableCloudDescription": "Sharing doc requires AFFiNE Cloud.",
|
||||||
"com.affine.share-menu.ShareMode": "Share mode",
|
"com.affine.share-menu.ShareMode": "Share mode",
|
||||||
"com.affine.share-menu.SharePage": "Share doc",
|
"com.affine.share-menu.SharePage": "Share doc",
|
||||||
|
|||||||
Reference in New Issue
Block a user