mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 08:38:34 +00:00
feat(core): cloud doc meta service
This commit is contained in:
@@ -1,38 +1,6 @@
|
||||
import { useQuery } from '@affine/core/hooks/use-query';
|
||||
import type { PagePropertyType } from '@affine/core/modules/properties/services/schema';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { getWorkspacePageMetaByIdQuery } from '@affine/graphql';
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { Middleware } from 'swr';
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { PagePropertiesManager } from './page-properties-manager';
|
||||
|
||||
// @ts-expect-error this should always be set
|
||||
export const managerContext = createContext<PagePropertiesManager>();
|
||||
|
||||
export const useCloudPageMeta = (type: PagePropertyType) => {
|
||||
const manager = useContext(managerContext);
|
||||
const isCloud = manager.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
const { data, error, isLoading } = useQuery({
|
||||
query: getWorkspacePageMetaByIdQuery,
|
||||
variables: {
|
||||
id: manager.workspace.id,
|
||||
pageId: manager.pageId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!!error || !data?.workspace?.pageMeta) return { isCloud, isLoading };
|
||||
const pageMeta = data.workspace.pageMeta;
|
||||
return {
|
||||
isCloud,
|
||||
isLoading,
|
||||
metadata: pageMeta[type as keyof typeof pageMeta],
|
||||
};
|
||||
};
|
||||
|
||||
export const SWRCustomHandler: Middleware =
|
||||
next => (key, fetcher: any, config: any) => {
|
||||
const wrappedFetcher = (...args: any[]) =>
|
||||
fetcher?.(...args).catch(() => null);
|
||||
return next(key, wrappedFetcher, config);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { Avatar, Checkbox, DatePicker, Menu } from '@affine/component';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { CloudDocMetaService } from '@affine/core/modules/cloud/services/cloud-doc-meta';
|
||||
import type {
|
||||
PageInfoCustomProperty,
|
||||
PageInfoCustomPropertyMeta,
|
||||
PagePropertyType,
|
||||
} from '@affine/core/modules/properties/services/schema';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { i18nTime, useI18n } from '@affine/i18n';
|
||||
import { DocService, useLiveData, useService } from '@toeverything/infra';
|
||||
import {
|
||||
DocService,
|
||||
useLiveData,
|
||||
useService,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { noop } from 'lodash-es';
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
import {
|
||||
@@ -17,9 +23,8 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
import { managerContext, SWRCustomHandler, useCloudPageMeta } from './common';
|
||||
import { managerContext } from './common';
|
||||
import * as styles from './styles.css';
|
||||
import { TagsInlineEditor } from './tags-inline-editor';
|
||||
|
||||
@@ -199,66 +204,98 @@ export const TagsValue = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const CloudUserAvatar = (props: { type: PagePropertyType }) => {
|
||||
const session = useService(AuthService).session;
|
||||
const account = useLiveData(session.account$);
|
||||
const { isCloud, isLoading, metadata } = useCloudPageMeta(props.type);
|
||||
const CloudUserAvatar = (props: { type: 'CreatedBy' | 'UpdatedBy' }) => {
|
||||
const cloudDocMetaService = useService(CloudDocMetaService);
|
||||
const cloudDocMeta = useLiveData(cloudDocMetaService.cloudDocMeta.meta$);
|
||||
const isRevalidating = useLiveData(
|
||||
cloudDocMetaService.cloudDocMeta.isRevalidating$
|
||||
);
|
||||
const error = useLiveData(cloudDocMetaService.cloudDocMeta.error$);
|
||||
|
||||
useEffect(() => {
|
||||
cloudDocMetaService.cloudDocMeta.revalidate();
|
||||
}, [cloudDocMetaService]);
|
||||
|
||||
const user = useMemo(() => {
|
||||
if (isCloud) {
|
||||
if (!metadata || typeof metadata !== 'object') return null;
|
||||
return metadata;
|
||||
} else {
|
||||
if (!account) return null;
|
||||
return { name: account.label, avatarUrl: account.avatar };
|
||||
if (!cloudDocMeta) return null;
|
||||
if (props.type === 'CreatedBy' && cloudDocMeta.createdBy) {
|
||||
return {
|
||||
name: cloudDocMeta.createdBy.name,
|
||||
avatarUrl: cloudDocMeta.createdBy.avatarUrl,
|
||||
};
|
||||
} else if (props.type === 'UpdatedBy' && cloudDocMeta.updatedBy) {
|
||||
return {
|
||||
name: cloudDocMeta.updatedBy.name,
|
||||
avatarUrl: cloudDocMeta.updatedBy.avatarUrl,
|
||||
};
|
||||
}
|
||||
}, [account, isCloud, metadata]);
|
||||
return null;
|
||||
}, [cloudDocMeta, props.type]);
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
if (isLoading) return null;
|
||||
if (isCloud) {
|
||||
if (user) {
|
||||
return (
|
||||
<>
|
||||
<Avatar url={user.avatarUrl || ''} name={user.name} size={20} />
|
||||
<span>{user.name}</span>
|
||||
</>
|
||||
);
|
||||
if (!cloudDocMeta) {
|
||||
if (isRevalidating) {
|
||||
// TODO: loading ui
|
||||
return null;
|
||||
}
|
||||
if (error) {
|
||||
// error ui
|
||||
return;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (user) {
|
||||
return (
|
||||
<>
|
||||
<Avatar name="?" size={20} />
|
||||
<span>
|
||||
{t['com.affine.page-properties.property-user-avatar-no-record']()}
|
||||
</span>
|
||||
<Avatar url={user.avatarUrl || ''} name={user.name} size={20} />
|
||||
<span>{user.name}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (account) {
|
||||
return (
|
||||
<>
|
||||
<Avatar url={account.avatar || ''} name={account.label} size={20} />
|
||||
<span>{account.label}</span>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Avatar name="?" size={20} />
|
||||
<span>{t['com.affine.page-properties.property-user-local']()}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
return (
|
||||
<>
|
||||
<Avatar name="?" size={20} />
|
||||
<span>
|
||||
{t['com.affine.page-properties.property-user-avatar-no-record']()}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserValue = ({ meta }: PropertyRowValueProps) => {
|
||||
export const LocalUserValue = () => {
|
||||
const t = useI18n();
|
||||
return <span>{t['com.affine.page-properties.local-user']()}</span>;
|
||||
};
|
||||
|
||||
export const CreatedUserValue = () => {
|
||||
const workspaceService = useService(WorkspaceService);
|
||||
const isCloud =
|
||||
workspaceService.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
|
||||
if (!isCloud) {
|
||||
return <LocalUserValue />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.propertyRowValueUserCell}>
|
||||
<SWRConfig value={parent => ({ ...parent, use: [SWRCustomHandler] })}>
|
||||
<CloudUserAvatar type={meta.type} />
|
||||
</SWRConfig>
|
||||
<CloudUserAvatar type="CreatedBy" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const UpdatedUserValue = () => {
|
||||
const workspaceService = useService(WorkspaceService);
|
||||
const isCloud =
|
||||
workspaceService.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
|
||||
if (!isCloud) {
|
||||
return <LocalUserValue />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.propertyRowValueUserCell}>
|
||||
<CloudUserAvatar type="UpdatedBy" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -271,8 +308,8 @@ export const propertyValueRenderers: Record<
|
||||
checkbox: CheckboxValue,
|
||||
text: TextValue,
|
||||
number: NumberValue,
|
||||
createdBy: UserValue,
|
||||
updatedBy: UserValue,
|
||||
createdBy: CreatedUserValue,
|
||||
updatedBy: UpdatedUserValue,
|
||||
// TODO(@Peng): fix following
|
||||
tags: TagsValue,
|
||||
progress: TextValue,
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { GetWorkspacePageMetaByIdQuery } from '@affine/graphql';
|
||||
import type { DocService, GlobalCache } from '@toeverything/infra';
|
||||
import {
|
||||
backoffRetry,
|
||||
catchErrorInto,
|
||||
effect,
|
||||
Entity,
|
||||
exhaustMapWithTrailing,
|
||||
fromPromise,
|
||||
LiveData,
|
||||
onComplete,
|
||||
onStart,
|
||||
} from '@toeverything/infra';
|
||||
import { EMPTY, mergeMap } from 'rxjs';
|
||||
|
||||
import { isBackendError, isNetworkError } from '../error';
|
||||
import type { CloudDocMetaStore } from '../stores/cloud-doc-meta';
|
||||
|
||||
export type CloudDocMetaType =
|
||||
GetWorkspacePageMetaByIdQuery['workspace']['pageMeta'];
|
||||
|
||||
const CACHE_KEY_PREFIX = 'cloud-doc-meta:';
|
||||
|
||||
export class CloudDocMeta extends Entity {
|
||||
constructor(
|
||||
private readonly store: CloudDocMetaStore,
|
||||
private readonly docService: DocService,
|
||||
private readonly cache: GlobalCache
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
readonly docId = this.docService.doc.id;
|
||||
readonly workspaceId = this.docService.doc.workspace.id;
|
||||
|
||||
readonly cacheKey = `${CACHE_KEY_PREFIX}${this.workspaceId}:${this.docId}`;
|
||||
meta$ = LiveData.from<CloudDocMetaType | undefined>(
|
||||
this.cache.watch<CloudDocMetaType>(this.cacheKey),
|
||||
undefined
|
||||
);
|
||||
isRevalidating$ = new LiveData(false);
|
||||
error$ = new LiveData<any | null>(null);
|
||||
|
||||
revalidate = effect(
|
||||
exhaustMapWithTrailing(() => {
|
||||
return fromPromise(
|
||||
this.store.fetchCloudDocMeta(this.workspaceId, this.docId)
|
||||
).pipe(
|
||||
backoffRetry({
|
||||
when: isNetworkError,
|
||||
count: Infinity,
|
||||
}),
|
||||
backoffRetry({
|
||||
when: isBackendError,
|
||||
}),
|
||||
mergeMap(meta => {
|
||||
this.cache.set<CloudDocMetaType>(this.cacheKey, meta);
|
||||
return EMPTY;
|
||||
}),
|
||||
catchErrorInto(this.error$),
|
||||
onStart(() => this.isRevalidating$.next(true)),
|
||||
onComplete(() => this.isRevalidating$.next(false))
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -16,11 +16,15 @@ export { UserQuotaService } from './services/user-quota';
|
||||
export { WebSocketService } from './services/websocket';
|
||||
|
||||
import {
|
||||
DocScope,
|
||||
DocService,
|
||||
type Framework,
|
||||
GlobalCacheService,
|
||||
GlobalStateService,
|
||||
GlobalCache,
|
||||
GlobalState,
|
||||
WorkspaceScope,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
import { CloudDocMeta } from './entities/cloud-doc-meta';
|
||||
import { ServerConfig } from './entities/server-config';
|
||||
import { AuthSession } from './entities/session';
|
||||
import { Subscription } from './entities/subscription';
|
||||
@@ -29,6 +33,7 @@ import { UserCopilotQuota } from './entities/user-copilot-quota';
|
||||
import { UserFeature } from './entities/user-feature';
|
||||
import { UserQuota } from './entities/user-quota';
|
||||
import { AuthService } from './services/auth';
|
||||
import { CloudDocMetaService } from './services/cloud-doc-meta';
|
||||
import { FetchService } from './services/fetch';
|
||||
import { GraphQLService } from './services/graphql';
|
||||
import { ServerConfigService } from './services/server-config';
|
||||
@@ -38,6 +43,7 @@ import { UserFeatureService } from './services/user-feature';
|
||||
import { UserQuotaService } from './services/user-quota';
|
||||
import { WebSocketService } from './services/websocket';
|
||||
import { AuthStore } from './stores/auth';
|
||||
import { CloudDocMetaStore } from './stores/cloud-doc-meta';
|
||||
import { ServerConfigStore } from './stores/server-config';
|
||||
import { SubscriptionStore } from './stores/subscription';
|
||||
import { UserCopilotQuotaStore } from './stores/user-copilot-quota';
|
||||
@@ -53,10 +59,10 @@ export function configureCloudModule(framework: Framework) {
|
||||
.entity(ServerConfig, [ServerConfigStore])
|
||||
.store(ServerConfigStore, [GraphQLService])
|
||||
.service(AuthService, [FetchService, AuthStore])
|
||||
.store(AuthStore, [FetchService, GraphQLService, GlobalStateService])
|
||||
.store(AuthStore, [FetchService, GraphQLService, GlobalState])
|
||||
.entity(AuthSession, [AuthStore])
|
||||
.service(SubscriptionService, [SubscriptionStore])
|
||||
.store(SubscriptionStore, [GraphQLService, GlobalCacheService])
|
||||
.store(SubscriptionStore, [GraphQLService, GlobalCache])
|
||||
.entity(Subscription, [AuthService, ServerConfigService, SubscriptionStore])
|
||||
.entity(SubscriptionPrices, [ServerConfigService, SubscriptionStore])
|
||||
.service(UserQuotaService)
|
||||
@@ -71,5 +77,10 @@ export function configureCloudModule(framework: Framework) {
|
||||
])
|
||||
.service(UserFeatureService)
|
||||
.entity(UserFeature, [AuthService, UserFeatureStore])
|
||||
.store(UserFeatureStore, [GraphQLService]);
|
||||
.store(UserFeatureStore, [GraphQLService])
|
||||
.scope(WorkspaceScope)
|
||||
.scope(DocScope)
|
||||
.service(CloudDocMetaService)
|
||||
.entity(CloudDocMeta, [CloudDocMetaStore, DocService, GlobalCache])
|
||||
.store(CloudDocMetaStore, [GraphQLService]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Service } from '@toeverything/infra';
|
||||
|
||||
import { CloudDocMeta } from '../entities/cloud-doc-meta';
|
||||
|
||||
export class CloudDocMetaService extends Service {
|
||||
cloudDocMeta = this.framework.createEntity(CloudDocMeta);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
updateUserProfileMutation,
|
||||
uploadAvatarMutation,
|
||||
} from '@affine/graphql';
|
||||
import type { GlobalStateService } from '@toeverything/infra';
|
||||
import type { GlobalState } from '@toeverything/infra';
|
||||
import { Store } from '@toeverything/infra';
|
||||
|
||||
import type { AuthSessionInfo } from '../entities/session';
|
||||
@@ -24,19 +24,17 @@ export class AuthStore extends Store {
|
||||
constructor(
|
||||
private readonly fetchService: FetchService,
|
||||
private readonly gqlService: GraphQLService,
|
||||
private readonly globalStateService: GlobalStateService
|
||||
private readonly globalState: GlobalState
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
watchCachedAuthSession() {
|
||||
return this.globalStateService.globalState.watch<AuthSessionInfo>(
|
||||
'affine-cloud-auth'
|
||||
);
|
||||
return this.globalState.watch<AuthSessionInfo>('affine-cloud-auth');
|
||||
}
|
||||
|
||||
setCachedAuthSession(session: AuthSessionInfo | null) {
|
||||
this.globalStateService.globalState.set('affine-cloud-auth', session);
|
||||
this.globalState.set('affine-cloud-auth', session);
|
||||
}
|
||||
|
||||
async fetchSession() {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { getWorkspacePageMetaByIdQuery } from '@affine/graphql';
|
||||
import { Store } from '@toeverything/infra';
|
||||
|
||||
import { type CloudDocMetaType } from '../entities/cloud-doc-meta';
|
||||
import type { GraphQLService } from '../services/graphql';
|
||||
|
||||
export class CloudDocMetaStore extends Store {
|
||||
constructor(private readonly gqlService: GraphQLService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async fetchCloudDocMeta(
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
abortSignal?: AbortSignal
|
||||
): Promise<CloudDocMetaType> {
|
||||
const serverConfigData = await this.gqlService.gql({
|
||||
query: getWorkspacePageMetaByIdQuery,
|
||||
variables: { id: workspaceId, pageId: docId },
|
||||
context: {
|
||||
signal: abortSignal,
|
||||
},
|
||||
});
|
||||
return serverConfigData.workspace.pageMeta;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
subscriptionQuery,
|
||||
updateSubscriptionMutation,
|
||||
} from '@affine/graphql';
|
||||
import type { GlobalCacheService } from '@toeverything/infra';
|
||||
import type { GlobalCache } from '@toeverything/infra';
|
||||
import { Store } from '@toeverything/infra';
|
||||
|
||||
import type { SubscriptionType } from '../entities/subscription';
|
||||
@@ -37,7 +37,7 @@ const getDefaultSubscriptionSuccessCallbackLink = (
|
||||
export class SubscriptionStore extends Store {
|
||||
constructor(
|
||||
private readonly gqlService: GraphQLService,
|
||||
private readonly globalCacheService: GlobalCacheService
|
||||
private readonly globalCache: GlobalCache
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -97,16 +97,13 @@ export class SubscriptionStore extends Store {
|
||||
}
|
||||
|
||||
getCachedSubscriptions(userId: string) {
|
||||
return this.globalCacheService.globalCache.get<SubscriptionType[]>(
|
||||
return this.globalCache.get<SubscriptionType[]>(
|
||||
SUBSCRIPTION_CACHE_KEY + userId
|
||||
);
|
||||
}
|
||||
|
||||
setCachedSubscriptions(userId: string, subscriptions: SubscriptionType[]) {
|
||||
return this.globalCacheService.globalCache.set(
|
||||
SUBSCRIPTION_CACHE_KEY + userId,
|
||||
subscriptions
|
||||
);
|
||||
return this.globalCache.set(SUBSCRIPTION_CACHE_KEY + userId, subscriptions);
|
||||
}
|
||||
|
||||
setSubscriptionRecurring(
|
||||
|
||||
@@ -6,12 +6,13 @@ import {
|
||||
catchErrorInto,
|
||||
effect,
|
||||
Entity,
|
||||
exhaustMapWithTrailing,
|
||||
fromPromise,
|
||||
LiveData,
|
||||
onComplete,
|
||||
onStart,
|
||||
} from '@toeverything/infra';
|
||||
import { EMPTY, mergeMap, switchMap } from 'rxjs';
|
||||
import { EMPTY, mergeMap } from 'rxjs';
|
||||
|
||||
import { isBackendError, isNetworkError } from '../../cloud';
|
||||
import type { ShareDocsStore } from '../stores/share-docs';
|
||||
@@ -35,7 +36,7 @@ export class ShareDocsList extends Entity {
|
||||
}
|
||||
|
||||
revalidate = effect(
|
||||
switchMap(() =>
|
||||
exhaustMapWithTrailing(() =>
|
||||
fromPromise(signal => {
|
||||
return this.store.getWorkspacesShareDocs(
|
||||
this.workspaceService.workspace.id,
|
||||
|
||||
@@ -874,6 +874,7 @@
|
||||
"com.affine.page-properties.property.text": "Text",
|
||||
"com.affine.page-properties.property.createdBy": "Created by",
|
||||
"com.affine.page-properties.property.updatedBy": "Last edited by",
|
||||
"com.affine.page-properties.local-user": "Local user",
|
||||
"com.affine.page-properties.settings.title": "customize properties",
|
||||
"com.affine.page-properties.tags.open-tags-page": "Open tag page",
|
||||
"com.affine.page-properties.tags.selector-header-title": "Select tag or create one",
|
||||
|
||||
Reference in New Issue
Block a user