refactor(core): move infra modules to core (#9207)

This commit is contained in:
EYHN
2024-12-23 04:53:59 +00:00
parent 2ea79d25ad
commit 129f94ee78
337 changed files with 908 additions and 1367 deletions
@@ -1,9 +1,6 @@
import {
type GlobalState,
type Memento,
wrapMemento,
} from '@toeverything/infra';
import { type Memento, wrapMemento } from '@toeverything/infra';
import type { GlobalState } from '../../storage';
import type { AppSidebarState } from '../providers/storage';
export class AppSidebarStateImpl implements AppSidebarState {
@@ -1,5 +1,6 @@
import { type Framework, GlobalState } from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { GlobalState } from '../storage';
import { AppSidebar } from './entities/app-sidebar';
import { AppSidebarStateImpl } from './impls/storage';
import { AppSidebarState } from './providers/storage';
@@ -1,5 +1,6 @@
import { GlobalState, Service } from '@toeverything/infra';
import { Service } from '@toeverything/infra';
import { GlobalState } from '../../storage';
import { AppSidebar } from '../entities/app-sidebar';
export class AppSidebarService extends Service {
@@ -1,10 +1,11 @@
import { IconButton } from '@affine/component';
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { isNewTabTrigger } from '@affine/core/utils';
import { useI18n } from '@affine/i18n';
import track from '@affine/track';
import { PlusIcon } from '@blocksuite/icons/rc';
import { useService, WorkspaceService } from '@toeverything/infra';
import { useService } from '@toeverything/infra';
import clsx from 'clsx';
import type React from 'react';
import { type MouseEvent, useCallback } from 'react';
@@ -8,13 +8,13 @@ import {
useLiveData,
useService,
useServiceOptional,
WorkspaceService,
} from '@toeverything/infra';
import clsx from 'clsx';
import { debounce } from 'lodash-es';
import type { PropsWithChildren, ReactElement } from 'react';
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { WorkspaceService } from '../../workspace';
import { AppSidebarService } from '../services/app-sidebar';
import * as styles from './fallback.css';
import {
@@ -285,7 +285,7 @@ const RandomBars = ({ count, header }: { count: number; header?: boolean }) => {
/>
) : null}
{Array.from({ length: count }).map((_, index) => (
// eslint-disable-next-line react/no-array-index-key
// oxlint-disable-next-line eslint-plugin-react(no-array-index-key)
<RandomBar key={index} />
))}
</div>
@@ -1,16 +1,13 @@
import {
DocsService,
type Framework,
WorkspaceScope,
WorkspaceService,
} from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { WorkspaceDialogService } from '../dialogs';
import { DocsService } from '../doc';
import { DocDisplayMetaService } from '../doc-display-meta';
import { DocsSearchService } from '../docs-search';
import { EditorSettingService } from '../editor-setting';
import { JournalService } from '../journal';
import { RecentDocsService } from '../quicksearch';
import { WorkspaceScope, WorkspaceService } from '../workspace';
import { AtMenuConfigService } from './services';
export function configAtMenuConfigModule(framework: Framework) {
@@ -18,18 +18,19 @@ import {
} from '@blocksuite/icons/lit';
import type { DocMeta } from '@blocksuite/store';
import { signal } from '@preact/signals-core';
import type { DocsService, WorkspaceService } from '@toeverything/infra';
import { Service } from '@toeverything/infra';
import { cssVarV2 } from '@toeverything/theme/v2';
import { html } from 'lit';
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
import type { WorkspaceDialogService } from '../../dialogs';
import type { DocsService } from '../../doc';
import type { DocDisplayMetaService } from '../../doc-display-meta';
import type { DocsSearchService } from '../../docs-search';
import type { EditorSettingService } from '../../editor-setting';
import { type JournalService, suggestJournalDate } from '../../journal';
import type { RecentDocsService } from '../../quicksearch';
import type { WorkspaceService } from '../../workspace';
const MAX_DOCS = 3;
const LOAD_CHUNK = 100;
@@ -1,5 +1,4 @@
import type { GetWorkspacePageMetaByIdQuery } from '@affine/graphql';
import type { DocService, GlobalCache } from '@toeverything/infra';
import {
backoffRetry,
catchErrorInto,
@@ -13,6 +12,8 @@ import {
} from '@toeverything/infra';
import { EMPTY, mergeMap } from 'rxjs';
import type { DocService } from '../../doc';
import type { GlobalCache } from '../../storage';
import { isBackendError, isNetworkError } from '../error';
import type { CloudDocMetaStore } from '../stores/cloud-doc-meta';
@@ -1,5 +1,4 @@
import type { InvoicesQuery } from '@affine/graphql';
import type { WorkspaceService } from '@toeverything/infra';
import {
backoffRetry,
catchErrorInto,
@@ -13,6 +12,7 @@ import {
} from '@toeverything/infra';
import { EMPTY, map, mergeMap } from 'rxjs';
import type { WorkspaceService } from '../../workspace';
import { isBackendError, isNetworkError } from '../error';
import type { WorkspaceServerService } from '../services/workspace-server';
import { InvoicesStore } from '../stores/invoices';
@@ -1,6 +1,5 @@
import type { SubscriptionQuery, SubscriptionRecurring } from '@affine/graphql';
import { SubscriptionPlan } from '@affine/graphql';
import type { WorkspaceService } from '@toeverything/infra';
import {
backoffRetry,
catchErrorInto,
@@ -14,10 +13,10 @@ import {
} from '@toeverything/infra';
import { EMPTY, mergeMap } from 'rxjs';
import type { WorkspaceService } from '../../workspace';
import { isBackendError, isNetworkError } from '../error';
import type { WorkspaceServerService } from '../services/workspace-server';
import { SubscriptionStore } from '../stores/subscription';
export type SubscriptionType = NonNullable<
SubscriptionQuery['currentUser']
>['subscriptions'][number];
@@ -33,18 +33,12 @@ export { WorkspaceServerService } from './services/workspace-server';
export { WorkspaceSubscriptionService } from './services/workspace-subscription';
export type { ServerConfig } from './types';
import {
DocScope,
DocService,
type Framework,
GlobalCache,
GlobalState,
GlobalStateService,
WorkspaceScope,
WorkspaceService,
} from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { DocScope, DocService } from '../doc';
import { GlobalCache, GlobalState, GlobalStateService } from '../storage';
import { UrlService } from '../url';
import { WorkspaceScope, WorkspaceService } from '../workspace';
import { CloudDocMeta } from './entities/cloud-doc-meta';
import { Invoices } from './entities/invoices';
import { Server } from './entities/server';
@@ -1,9 +1,10 @@
import { AIProvider } from '@affine/core/blocksuite/presets/ai';
import type { OAuthProviderType } from '@affine/graphql';
import { track } from '@affine/track';
import { ApplicationFocused, OnEvent, Service } from '@toeverything/infra';
import { OnEvent, Service } from '@toeverything/infra';
import { distinctUntilChanged, map, skip } from 'rxjs';
import { ApplicationFocused } from '../../lifecycle';
import type { UrlService } from '../../url';
import { type AuthAccountInfo, AuthSession } from '../entities/session';
import { BackendError } from '../error';
@@ -1,6 +1,7 @@
import { ApplicationStarted, OnEvent, Service } from '@toeverything/infra';
import { OnEvent, Service } from '@toeverything/infra';
import { Manager } from 'socket.io-client';
import { ApplicationStarted } from '../../lifecycle';
import { AccountChanged } from '../events/account-changed';
import type { WebSocketAuthProvider } from '../provider/websocket-auth';
import type { AuthService } from './auth';
@@ -3,9 +3,9 @@ import {
updateUserProfileMutation,
uploadAvatarMutation,
} from '@affine/graphql';
import type { GlobalState } from '@toeverything/infra';
import { Store } from '@toeverything/infra';
import type { GlobalState } from '../../storage';
import type { AuthSessionInfo } from '../entities/session';
import type { FetchService } from '../services/fetch';
import type { GraphQLService } from '../services/graphql';
@@ -1,7 +1,7 @@
import type { GlobalStateService } from '@toeverything/infra';
import { Store } from '@toeverything/infra';
import { map } from 'rxjs';
import type { GlobalStateService } from '../../storage';
import { BUILD_IN_SERVERS } from '../constant';
import type { ServerConfig, ServerMetadata } from '../types';
@@ -12,14 +12,13 @@ import {
subscriptionQuery,
updateSubscriptionMutation,
} from '@affine/graphql';
import type { GlobalCache } from '@toeverything/infra';
import { Store } from '@toeverything/infra';
import type { GlobalCache } from '../../storage';
import type { UrlService } from '../../url';
import type { SubscriptionType } from '../entities/subscription';
import type { GraphQLService } from '../services/graphql';
import type { ServerService } from '../services/server';
const SUBSCRIPTION_CACHE_KEY = 'subscription:';
const getDefaultSubscriptionSuccessCallbackLink = (
@@ -1,11 +1,8 @@
export { CollectionService } from './services/collection';
import {
type Framework,
WorkspaceScope,
WorkspaceService,
} from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { WorkspaceScope, WorkspaceService } from '../workspace';
import { CollectionService } from './services/collection';
export function configureCollectionModule(framework: Framework) {
@@ -3,11 +3,12 @@ import type {
DeleteCollectionInfo,
DeletedCollection,
} from '@affine/env/filter';
import type { WorkspaceService } from '@toeverything/infra';
import { LiveData, Service } from '@toeverything/infra';
import { Observable } from 'rxjs';
import { Array as YArray } from 'yjs';
import type { WorkspaceService } from '../../workspace';
const SETTING_KEY = 'setting';
const COLLECTIONS_KEY = 'collections';
@@ -0,0 +1,33 @@
import {
type DBSchemaBuilder,
Entity,
type TableMap,
} from '@toeverything/infra';
import { WorkspaceDBTable } from './table';
export class WorkspaceDB<Schema extends DBSchemaBuilder> extends Entity<{
db: TableMap<Schema>;
schema: Schema;
storageDocId: (tableName: string) => string;
}> {
readonly db = this.props.db;
constructor() {
super();
Object.entries(this.props.schema).forEach(([tableName]) => {
const table = this.framework.createEntity(WorkspaceDBTable, {
table: this.db[tableName],
storageDocId: this.props.storageDocId(tableName),
});
Object.defineProperty(this, tableName, {
get: () => table,
});
});
}
}
export type WorkspaceDBWithTables<Schema extends DBSchemaBuilder> =
WorkspaceDB<Schema> & {
[K in keyof Schema]: WorkspaceDBTable<Schema[K]>;
};
@@ -0,0 +1,39 @@
import type {
Table as OrmTable,
TableSchemaBuilder,
} from '@toeverything/infra';
import { Entity } from '@toeverything/infra';
import type { WorkspaceService } from '../../workspace';
export class WorkspaceDBTable<
Schema extends TableSchemaBuilder,
> extends Entity<{
table: OrmTable<Schema>;
storageDocId: string;
}> {
readonly table = this.props.table;
constructor(private readonly workspaceService: WorkspaceService) {
super();
}
isSyncing$ = this.workspaceService.workspace.engine.doc
.docState$(this.props.storageDocId)
.map(docState => docState.syncing);
isLoading$ = this.workspaceService.workspace.engine.doc
.docState$(this.props.storageDocId)
.map(docState => docState.loading);
create = this.table.create.bind(this.table) as typeof this.table.create;
update = this.table.update.bind(this.table) as typeof this.table.update;
get = this.table.get.bind(this.table) as typeof this.table.get;
// eslint-disable-next-line rxjs/finnish
get$ = this.table.get$.bind(this.table) as typeof this.table.get$;
find = this.table.find.bind(this.table) as typeof this.table.find;
// eslint-disable-next-line rxjs/finnish
find$ = this.table.find$.bind(this.table) as typeof this.table.find$;
keys = this.table.keys.bind(this.table) as typeof this.table.keys;
delete = this.table.delete.bind(this.table) as typeof this.table.delete;
}
@@ -0,0 +1,18 @@
import type { Framework } from '@toeverything/infra';
import { WorkspaceScope, WorkspaceService } from '../workspace';
import { WorkspaceDB } from './entities/db';
import { WorkspaceDBTable } from './entities/table';
import { WorkspaceDBService } from './services/db';
export type { DocCustomPropertyInfo, DocProperties } from './schema';
export { WorkspaceDBService } from './services/db';
export { transformWorkspaceDBLocalToCloud } from './services/db';
export function configureWorkspaceDBModule(framework: Framework) {
framework
.scope(WorkspaceScope)
.service(WorkspaceDBService, [WorkspaceService])
.entity(WorkspaceDB)
.entity(WorkspaceDBTable, [WorkspaceService]);
}
@@ -0,0 +1,7 @@
export type { DocCustomPropertyInfo, DocProperties } from './schema';
export {
AFFiNE_WORKSPACE_DB_SCHEMA,
AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA,
type AFFiNEWorkspaceDbSchema,
type AFFiNEWorkspaceUserdataDbSchema,
} from './schema';
@@ -0,0 +1,52 @@
import {
type DBSchemaBuilder,
f,
type ORMEntity,
t,
} from '@toeverything/infra';
import { nanoid } from 'nanoid';
export const AFFiNE_WORKSPACE_DB_SCHEMA = {
folders: {
id: f.string().primaryKey().optional().default(nanoid),
parentId: f.string().optional(),
data: f.string(),
type: f.string(),
index: f.string(),
},
docProperties: t.document({
// { [`custom:{customPropertyId}`]: any }
id: f.string().primaryKey(),
primaryMode: f.string().optional(),
edgelessColorTheme: f.string().optional(),
journal: f.string().optional(),
pageWidth: f.string().optional(),
}),
docCustomPropertyInfo: {
id: f.string().primaryKey().optional().default(nanoid),
name: f.string().optional(),
type: f.string(),
show: f.string().optional(),
index: f.string().optional(),
icon: f.string().optional(),
additionalData: f.json().optional(),
isDeleted: f.boolean().optional(),
// we will keep deleted properties in the database, for override legacy data
},
} as const satisfies DBSchemaBuilder;
export type AFFiNEWorkspaceDbSchema = typeof AFFiNE_WORKSPACE_DB_SCHEMA;
export type DocProperties = ORMEntity<AFFiNEWorkspaceDbSchema['docProperties']>;
export type DocCustomPropertyInfo = ORMEntity<
AFFiNEWorkspaceDbSchema['docCustomPropertyInfo']
>;
export const AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA = {
favorite: {
key: f.string().primaryKey(),
index: f.string(),
},
} as const satisfies DBSchemaBuilder;
export type AFFiNEWorkspaceUserdataDbSchema =
typeof AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA;
@@ -0,0 +1,128 @@
import {
createORMClient,
type DocStorage,
ObjectPool,
Service,
YjsDBAdapter,
} from '@toeverything/infra';
import { Doc as YDoc } from 'yjs';
import type { WorkspaceService } from '../../workspace';
import { WorkspaceDB, type WorkspaceDBWithTables } from '../entities/db';
import {
AFFiNE_WORKSPACE_DB_SCHEMA,
AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA,
type AFFiNEWorkspaceDbSchema,
type AFFiNEWorkspaceUserdataDbSchema,
} from '../schema';
const WorkspaceDBClient = createORMClient(AFFiNE_WORKSPACE_DB_SCHEMA);
const WorkspaceUserdataDBClient = createORMClient(
AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA
);
export class WorkspaceDBService extends Service {
db: WorkspaceDBWithTables<AFFiNEWorkspaceDbSchema>;
userdataDBPool = new ObjectPool<
string,
WorkspaceDB<AFFiNEWorkspaceUserdataDbSchema>
>({
onDangling() {
return false; // never release
},
});
constructor(private readonly workspaceService: WorkspaceService) {
super();
this.db = this.framework.createEntity(
WorkspaceDB<AFFiNEWorkspaceDbSchema>,
{
db: new WorkspaceDBClient(
new YjsDBAdapter(AFFiNE_WORKSPACE_DB_SCHEMA, {
getDoc: guid => {
const ydoc = new YDoc({
// guid format: db${workspaceId}${guid}
guid: `db$${this.workspaceService.workspace.id}$${guid}`,
});
this.workspaceService.workspace.engine.doc.addDoc(ydoc, false);
this.workspaceService.workspace.engine.doc.setPriority(
ydoc.guid,
50
);
return ydoc;
},
})
),
schema: AFFiNE_WORKSPACE_DB_SCHEMA,
storageDocId: tableName =>
`db$${this.workspaceService.workspace.id}$${tableName}`,
}
) as WorkspaceDBWithTables<AFFiNEWorkspaceDbSchema>;
}
userdataDB(userId: (string & {}) | '__local__') {
// __local__ for local workspace
const userdataDb = this.userdataDBPool.get(userId);
if (userdataDb) {
return userdataDb.obj as WorkspaceDBWithTables<AFFiNEWorkspaceUserdataDbSchema>;
}
const newDB = this.framework.createEntity(
WorkspaceDB<AFFiNEWorkspaceUserdataDbSchema>,
{
db: new WorkspaceUserdataDBClient(
new YjsDBAdapter(AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA, {
getDoc: guid => {
const ydoc = new YDoc({
// guid format: userdata${userId}${workspaceId}${guid}
guid: `userdata$${userId}$${this.workspaceService.workspace.id}$${guid}`,
});
this.workspaceService.workspace.engine.doc.addDoc(ydoc, false);
this.workspaceService.workspace.engine.doc.setPriority(
ydoc.guid,
50
);
return ydoc;
},
})
),
schema: AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA,
storageDocId: tableName =>
`userdata$${userId}$${this.workspaceService.workspace.id}$${tableName}`,
}
);
this.userdataDBPool.put(userId, newDB);
return newDB as WorkspaceDBWithTables<AFFiNEWorkspaceUserdataDbSchema>;
}
static isDBDocId(docId: string) {
return docId.startsWith('db$') || docId.startsWith('userdata$');
}
}
export async function transformWorkspaceDBLocalToCloud(
localWorkspaceId: string,
cloudWorkspaceId: string,
localDocStorage: DocStorage,
cloudDocStorage: DocStorage,
accountId: string
) {
for (const tableName of Object.keys(AFFiNE_WORKSPACE_DB_SCHEMA)) {
const localDocName = `db$${localWorkspaceId}$${tableName}`;
const localDoc = await localDocStorage.doc.get(localDocName);
if (localDoc) {
const cloudDocName = `db$${cloudWorkspaceId}$${tableName}`;
await cloudDocStorage.doc.set(cloudDocName, localDoc);
}
}
for (const tableName of Object.keys(AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA)) {
const localDocName = `userdata$__local__$${localWorkspaceId}$${tableName}`;
const localDoc = await localDocStorage.doc.get(localDocName);
if (localDoc) {
const cloudDocName = `userdata$${accountId}$${cloudWorkspaceId}$${tableName}`;
await cloudDocStorage.doc.set(cloudDocName, localDoc);
}
}
}
@@ -5,7 +5,7 @@ import {
reactRouterV6BrowserTracingIntegration,
setTags,
} from '@sentry/react';
import { ApplicationStarted, OnEvent, Service } from '@toeverything/infra';
import { OnEvent, Service } from '@toeverything/infra';
import { debounce } from 'lodash-es';
import { useEffect } from 'react';
import {
@@ -16,6 +16,7 @@ import {
} from 'react-router-dom';
import { AuthService, DefaultServerService, ServersService } from '../../cloud';
import { ApplicationStarted } from '../../lifecycle';
import type { DesktopApi } from '../entities/electron-api';
@OnEvent(ApplicationStarted, e => e.setupStartListener)
@@ -1,5 +1,6 @@
import type { DocMode } from '@blocksuite/affine/blocks';
import type { WorkspaceMetadata } from '@toeverything/infra';
import type { WorkspaceMetadata } from '../workspace';
export type SettingTab =
| 'shortcuts'
@@ -1,6 +1,6 @@
import type { Framework } from '@toeverything/infra';
import { WorkspaceScope } from '@toeverything/infra';
import { WorkspaceScope } from '../workspace';
import { GlobalDialogService } from './services/dialog';
import { WorkspaceDialogService } from './services/workspace-dialog';
@@ -1,10 +1,7 @@
import {
DocsService,
type Framework,
WorkspaceScope,
WorkspaceService,
} from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { DocsService } from '../doc';
import { WorkspaceScope, WorkspaceService } from '../workspace';
import { DndService } from './services';
export function configureDndModule(framework: Framework) {
@@ -8,10 +8,11 @@ import type { AffineDNDData } from '@affine/core/types/dnd';
import { BlockStdScope } from '@blocksuite/affine/block-std';
import { DndApiExtensionIdentifier } from '@blocksuite/affine/blocks';
import { type SliceSnapshot } from '@blocksuite/affine/store';
import type { DocsService, WorkspaceService } from '@toeverything/infra';
import { Service } from '@toeverything/infra';
import type { DocsService } from '../../doc';
import { resolveLinkToDoc } from '../../navigation';
import type { WorkspaceService } from '../../workspace';
type Entity = AffineDNDData['draggable']['entity'];
type EntityResolver = (data: string) => Entity | null;
@@ -1,12 +1,10 @@
import {
DocsService,
FeatureFlagService,
type Framework,
WorkspaceScope,
} from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { DocsService } from '../doc';
import { FeatureFlagService } from '../feature-flag';
import { I18nService } from '../i18n';
import { JournalService } from '../journal';
import { WorkspaceScope } from '../workspace';
import { DocDisplayMetaService } from './services/doc-display-meta';
export { DocDisplayMetaService };
@@ -22,15 +22,12 @@ import {
TomorrowIcon,
YesterdayIcon,
} from '@blocksuite/icons/rc';
import type {
DocRecord,
DocsService,
FeatureFlagService,
} from '@toeverything/infra';
import { LiveData, Service } from '@toeverything/infra';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import type { DocRecord, DocsService } from '../../doc';
import type { FeatureFlagService } from '../../feature-flag';
import type { I18nService } from '../../i18n';
import type { JournalService } from '../../journal';
@@ -1,10 +1,8 @@
import {
DocsService,
type Framework,
WorkspaceScope,
} from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { DocsService } from '../doc/services/docs';
import { DocsSearchService } from '../docs-search';
import { WorkspaceScope } from '../workspace';
import { DocDatabaseBacklinksService } from './services/doc-database-backlinks';
export { DocDatabaseBacklinkInfo } from './views/database-properties/doc-database-backlink-info';
@@ -2,11 +2,11 @@ import {
DatabaseBlockDataSource,
type DatabaseBlockModel,
} from '@blocksuite/affine/blocks';
import type { DocsService } from '@toeverything/infra';
import { Service } from '@toeverything/infra';
import { isEqual } from 'lodash-es';
import { combineLatest, distinctUntilChanged, map, Observable } from 'rxjs';
import type { DocsService } from '../../doc';
import type { DocsSearchService } from '../../docs-search';
import type { DatabaseRow, DatabaseValueCell } from '../types';
import { signalToLiveData, signalToObservable } from '../utils';
@@ -1,5 +1,7 @@
import type { DatabaseBlockDataSource } from '@blocksuite/affine/blocks';
import type { Doc, LiveData } from '@toeverything/infra';
import type { LiveData } from '@toeverything/infra';
import type { Doc } from '../doc';
// make database property type to be compatible with DocCustomPropertyInfo
export type DatabaseProperty<Data = Record<string, unknown>> = {
@@ -5,15 +5,11 @@ import {
PropertyName,
} from '@affine/component';
import { AffinePageReference } from '@affine/core/components/affine/reference-link';
import { DocService } from '@affine/core/modules/doc';
import { useI18n } from '@affine/i18n';
import type { DatabaseBlockDataSource } from '@blocksuite/affine/blocks';
import { DatabaseTableViewIcon, PageIcon } from '@blocksuite/icons/rc';
import {
DocService,
LiveData,
useLiveData,
useService,
} from '@toeverything/infra';
import { LiveData, useLiveData, useService } from '@toeverything/infra';
import { Fragment, useMemo } from 'react';
import type { Observable } from 'rxjs';
@@ -1,6 +1,6 @@
import type { DocService } from '@toeverything/infra';
import { Entity, LiveData } from '@toeverything/infra';
import type { DocService } from '../../doc';
import type { DocsSearchService } from '../../docs-search';
export interface Backlink {
@@ -1,6 +1,6 @@
import type { DocService } from '@toeverything/infra';
import { Entity, LiveData } from '@toeverything/infra';
import type { DocService } from '../../doc';
import type { DocsSearchService } from '../../docs-search';
export interface Link {
@@ -1,11 +1,9 @@
import {
DocScope,
DocService,
type Framework,
WorkspaceScope,
} from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { DocScope } from '../doc/scopes/doc';
import { DocService } from '../doc/services/doc';
import { DocsSearchService } from '../docs-search';
import { WorkspaceScope } from '../workspace';
import { DocBacklinks } from './entities/doc-backlinks';
import { DocLinks } from './entities/doc-links';
import { DocLinksService } from './services/doc-links';
@@ -0,0 +1,54 @@
import type { DocCustomPropertyInfo } from '../db';
/**
* default built-in custom property, user can update and delete them
*
* 'id' and 'type' is request, 'index' is a manually maintained incremental key.
*/
export const BUILT_IN_CUSTOM_PROPERTY_TYPE = [
{
id: 'tags',
type: 'tags',
index: 'a0000001',
},
{
id: 'docPrimaryMode',
type: 'docPrimaryMode',
show: 'always-hide',
index: 'a0000002',
},
{
id: 'journal',
type: 'journal',
show: 'always-hide',
index: 'a0000003',
},
{
id: 'createdAt',
type: 'createdAt',
index: 'a0000004',
},
{
id: 'updatedAt',
type: 'updatedAt',
index: 'a0000005',
},
{
id: 'createdBy',
type: 'createdBy',
show: 'always-hide',
index: 'a0000006',
},
{
id: 'edgelessTheme',
type: 'edgelessTheme',
show: 'always-hide',
index: 'a0000007',
},
{
id: 'pageWidth',
type: 'pageWidth',
show: 'always-hide',
index: 'a0000008',
},
] as DocCustomPropertyInfo[];
@@ -0,0 +1,86 @@
import type { DocMode, RootBlockModel } from '@blocksuite/affine/blocks';
import { Entity } from '@toeverything/infra';
import type { WorkspaceService } from '../../workspace';
import type { DocScope } from '../scopes/doc';
import type { DocsStore } from '../stores/docs';
export class Doc extends Entity {
constructor(
public readonly scope: DocScope,
private readonly store: DocsStore,
private readonly workspaceService: WorkspaceService
) {
super();
}
/**
* for convenience
*/
get workspace() {
return this.workspaceService.workspace;
}
get id() {
return this.scope.props.docId;
}
public readonly blockSuiteDoc = this.scope.props.blockSuiteDoc;
public readonly record = this.scope.props.record;
readonly meta$ = this.record.meta$;
readonly properties$ = this.record.properties$;
readonly primaryMode$ = this.record.primaryMode$;
readonly title$ = this.record.title$;
readonly trash$ = this.record.trash$;
customProperty$(propertyId: string) {
return this.record.customProperty$(propertyId);
}
setCustomProperty(propertyId: string, value: string) {
return this.record.setCustomProperty(propertyId, value);
}
setPrimaryMode(mode: DocMode) {
return this.record.setPrimaryMode(mode);
}
getPrimaryMode() {
return this.record.getPrimaryMode();
}
togglePrimaryMode() {
this.setPrimaryMode(
(this.getPrimaryMode() === 'edgeless' ? 'page' : 'edgeless') as DocMode
);
}
moveToTrash() {
return this.record.moveToTrash();
}
restoreFromTrash() {
return this.record.restoreFromTrash();
}
waitForSyncReady() {
return this.store.waitForDocLoadReady(this.id);
}
setPriorityLoad(priority: number) {
return this.store.setPriorityLoad(this.id, priority);
}
changeDocTitle(newTitle: string) {
const pageBlock = this.blockSuiteDoc.getBlocksByFlavour('affine:page').at(0)
?.model as RootBlockModel | undefined;
if (pageBlock) {
this.blockSuiteDoc.transact(() => {
pageBlock.title.delete(0, pageBlock.title.length);
pageBlock.title.insert(newTitle, 0);
});
this.record.setMeta({ title: newTitle });
}
}
}
@@ -0,0 +1,77 @@
import {
Entity,
generateFractionalIndexingKeyBetween,
LiveData,
} from '@toeverything/infra';
import type { DocCustomPropertyInfo } from '../../db/schema/schema';
import type { DocPropertiesStore } from '../stores/doc-properties';
export class DocPropertyList extends Entity {
constructor(private readonly docPropertiesStore: DocPropertiesStore) {
super();
}
properties$ = LiveData.from(
this.docPropertiesStore.watchDocPropertyInfoList(),
[]
);
sortedProperties$ = this.properties$.map(list =>
// default index key is '', so always before any others
list.toSorted((a, b) => ((a.index ?? '') > (b.index ?? '') ? 1 : -1))
);
propertyInfo$(id: string) {
return this.properties$.map(list => list.find(info => info.id === id));
}
updatePropertyInfo(id: string, properties: Partial<DocCustomPropertyInfo>) {
this.docPropertiesStore.updateDocPropertyInfo(id, properties);
}
createProperty(
properties: Omit<DocCustomPropertyInfo, 'id'> & { id?: string }
) {
return this.docPropertiesStore.createDocPropertyInfo(properties);
}
removeProperty(id: string) {
this.docPropertiesStore.removeDocPropertyInfo(id);
}
indexAt(at: 'before' | 'after', targetId?: string) {
const sortedChildren = this.sortedProperties$.value.filter(
node => node.index
) as (DocCustomPropertyInfo & { index: string })[];
const targetIndex = targetId
? sortedChildren.findIndex(node => node.id === targetId)
: -1;
if (targetIndex === -1) {
if (at === 'before') {
const first = sortedChildren.at(0);
return generateFractionalIndexingKeyBetween(null, first?.index ?? null);
} else {
const last = sortedChildren.at(-1);
return generateFractionalIndexingKeyBetween(last?.index ?? null, null);
}
} else {
const target = sortedChildren[targetIndex];
const before: DocCustomPropertyInfo | null =
sortedChildren[targetIndex - 1] || null;
const after: DocCustomPropertyInfo | null =
sortedChildren[targetIndex + 1] || null;
if (at === 'before') {
return generateFractionalIndexingKeyBetween(
before?.index ?? null,
target.index
);
} else {
return generateFractionalIndexingKeyBetween(
target.index,
after?.index ?? null
);
}
}
}
}
@@ -0,0 +1,85 @@
import type { DocMode } from '@blocksuite/affine/blocks';
import { Entity, LiveData } from '@toeverything/infra';
import { map } from 'rxjs';
import type { DocsStore } from '../stores/docs';
import { DocRecord } from './record';
export class DocRecordList extends Entity {
constructor(private readonly store: DocsStore) {
super();
}
private readonly pool = new Map<string, DocRecord>();
public readonly docsMap$ = LiveData.from<Map<string, DocRecord>>(
this.store.watchDocIds().pipe(
map(
ids =>
new Map(
ids.map(id => {
const exists = this.pool.get(id);
if (exists) {
return [id, exists];
}
const record = this.framework.createEntity(DocRecord, { id });
this.pool.set(id, record);
return [id, record];
})
)
)
),
new Map()
);
public readonly docs$ = this.docsMap$.selector(d => Array.from(d.values()));
public readonly trashDocs$ = LiveData.from<DocRecord[]>(
this.store.watchTrashDocIds().pipe(
map(ids =>
ids.map(id => {
const exists = this.pool.get(id);
if (exists) {
return exists;
}
const record = this.framework.createEntity(DocRecord, { id });
this.pool.set(id, record);
return record;
})
)
),
[]
);
public readonly isReady$ = LiveData.from(
this.store.watchDocListReady(),
false
);
public doc$(id: string) {
return this.docsMap$.selector(map => map.get(id));
}
public setPrimaryMode(id: string, mode: DocMode) {
return this.store.setDocPrimaryModeSetting(id, mode);
}
public getPrimaryMode(id: string) {
return this.store.getDocPrimaryModeSetting(id);
}
public togglePrimaryMode(id: string) {
const mode = (
this.getPrimaryMode(id) === 'edgeless' ? 'page' : 'edgeless'
) as DocMode;
this.setPrimaryMode(id, mode);
return this.getPrimaryMode(id);
}
public primaryMode$(id: string) {
return LiveData.from(
this.store.watchDocPrimaryModeSetting(id),
this.getPrimaryMode(id)
);
}
}
@@ -0,0 +1,79 @@
import type { DocMode } from '@blocksuite/affine/blocks';
import type { DocMeta } from '@blocksuite/affine/store';
import { Entity, LiveData } from '@toeverything/infra';
import type { DocProperties } from '../../db';
import type { DocPropertiesStore } from '../stores/doc-properties';
import type { DocsStore } from '../stores/docs';
/**
* # DocRecord
*
* Some data you can use without open a doc.
*/
export class DocRecord extends Entity<{ id: string }> {
id: string = this.props.id;
constructor(
private readonly docsStore: DocsStore,
private readonly docPropertiesStore: DocPropertiesStore
) {
super();
}
meta$ = LiveData.from<Partial<DocMeta>>(
this.docsStore.watchDocMeta(this.id),
{}
);
properties$ = LiveData.from<DocProperties>(
this.docPropertiesStore.watchDocProperties(this.id),
{ id: this.id }
);
customProperty$(propertyId: string) {
return this.properties$.selector(
p => p['custom:' + propertyId]
) as LiveData<string | undefined | null>;
}
setCustomProperty(propertyId: string, value: string) {
this.docPropertiesStore.updateDocProperties(this.id, {
['custom:' + propertyId]: value,
});
}
setProperty(propertyId: string, value: string) {
this.docPropertiesStore.updateDocProperties(this.id, {
[propertyId]: value,
});
}
setMeta(meta: Partial<DocMeta>): void {
this.docsStore.setDocMeta(this.id, meta);
}
primaryMode$: LiveData<DocMode> = LiveData.from(
this.docsStore.watchDocPrimaryModeSetting(this.id),
'page' as DocMode
).map(mode => (mode === 'edgeless' ? 'edgeless' : 'page') as DocMode);
setPrimaryMode(mode: DocMode) {
return this.docsStore.setDocPrimaryModeSetting(this.id, mode);
}
getPrimaryMode() {
return this.docsStore.getDocPrimaryModeSetting(this.id);
}
moveToTrash() {
return this.setMeta({ trash: true, trashDate: Date.now() });
}
restoreFromTrash() {
return this.setMeta({ trash: false, trashDate: undefined });
}
title$ = this.meta$.map(meta => meta.title ?? '');
trash$ = this.meta$.map(meta => meta.trash ?? false);
}
@@ -0,0 +1,5 @@
import { createEvent } from '@toeverything/infra';
import type { DocRecord } from '../entities/record';
export const DocCreated = createEvent<DocRecord>('DocCreated');
@@ -0,0 +1,35 @@
export { Doc } from './entities/doc';
export { DocRecord } from './entities/record';
export { DocRecordList } from './entities/record-list';
export { DocCreated } from './events';
export { DocScope } from './scopes/doc';
export { DocService } from './services/doc';
export { DocsService } from './services/docs';
import type { Framework } from '@toeverything/infra';
import { WorkspaceDBService } from '../db';
import { WorkspaceScope, WorkspaceService } from '../workspace';
import { Doc } from './entities/doc';
import { DocPropertyList } from './entities/property-list';
import { DocRecord } from './entities/record';
import { DocRecordList } from './entities/record-list';
import { DocScope } from './scopes/doc';
import { DocService } from './services/doc';
import { DocsService } from './services/docs';
import { DocPropertiesStore } from './stores/doc-properties';
import { DocsStore } from './stores/docs';
export function configureDocModule(framework: Framework) {
framework
.scope(WorkspaceScope)
.service(DocsService, [DocsStore])
.store(DocPropertiesStore, [WorkspaceService, WorkspaceDBService])
.store(DocsStore, [WorkspaceService, DocPropertiesStore])
.entity(DocRecord, [DocsStore, DocPropertiesStore])
.entity(DocRecordList, [DocsStore])
.entity(DocPropertyList, [DocPropertiesStore])
.scope(DocScope)
.entity(Doc, [DocScope, DocsStore, WorkspaceService])
.service(DocService);
}
@@ -0,0 +1,10 @@
import type { Doc as BlockSuiteDoc } from '@blocksuite/affine/store';
import { Scope } from '@toeverything/infra';
import type { DocRecord } from '../entities/record';
export class DocScope extends Scope<{
docId: string;
record: DocRecord;
blockSuiteDoc: BlockSuiteDoc;
}> {}
@@ -0,0 +1,7 @@
import { Service } from '@toeverything/infra';
import { Doc } from '../entities/doc';
export class DocService extends Service {
public readonly doc = this.framework.createEntity(Doc);
}
@@ -0,0 +1,127 @@
import { DebugLogger } from '@affine/debug';
import { Unreachable } from '@affine/env/constant';
import type { DocMode } from '@blocksuite/affine/blocks';
import type { DeltaInsert } from '@blocksuite/affine/inline';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import {
type DocProps,
initDocFromProps,
ObjectPool,
Service,
} from '@toeverything/infra';
import type { Doc } from '../entities/doc';
import { DocPropertyList } from '../entities/property-list';
import { DocRecordList } from '../entities/record-list';
import { DocCreated } from '../events';
import { DocScope } from '../scopes/doc';
import type { DocsStore } from '../stores/docs';
import { DocService } from './doc';
const logger = new DebugLogger('DocsService');
export class DocsService extends Service {
list = this.framework.createEntity(DocRecordList);
pool = new ObjectPool<string, Doc>({
onDelete(obj) {
obj.scope.dispose();
},
});
propertyList = this.framework.createEntity(DocPropertyList);
constructor(private readonly store: DocsStore) {
super();
}
open(docId: string) {
const docRecord = this.list.doc$(docId).value;
if (!docRecord) {
throw new Error('Doc record not found');
}
const blockSuiteDoc = this.store.getBlockSuiteDoc(docId);
if (!blockSuiteDoc) {
throw new Error('Doc not found');
}
const exists = this.pool.get(docId);
if (exists) {
return { doc: exists.obj, release: exists.release };
}
const docScope = this.framework.createScope(DocScope, {
docId,
blockSuiteDoc,
record: docRecord,
});
try {
blockSuiteDoc.load();
} catch (e) {
logger.error('Failed to load doc', {
docId,
error: e,
});
}
const doc = docScope.get(DocService).doc;
const { obj, release } = this.pool.put(docId, doc);
return { doc: obj, release };
}
createDoc(
options: {
primaryMode?: DocMode;
docProps?: DocProps;
} = {}
) {
const doc = this.store.createBlockSuiteDoc();
initDocFromProps(doc, options.docProps);
this.store.markDocSyncStateAsReady(doc.id);
const docRecord = this.list.doc$(doc.id).value;
if (!docRecord) {
throw new Unreachable();
}
if (options.primaryMode) {
docRecord.setPrimaryMode(options.primaryMode);
}
this.eventBus.emit(DocCreated, docRecord);
return docRecord;
}
async addLinkedDoc(targetDocId: string, linkedDocId: string) {
const { doc, release } = this.open(targetDocId);
doc.setPriorityLoad(10);
await doc.waitForSyncReady();
const text = new doc.blockSuiteDoc.Text([
{
insert: ' ',
attributes: {
reference: {
type: 'LinkedPage',
pageId: linkedDocId,
},
},
},
] as DeltaInsert<AffineTextAttributes>[]);
const [frame] = doc.blockSuiteDoc.getBlocksByFlavour('affine:note');
frame &&
doc.blockSuiteDoc.addBlock(
'affine:paragraph' as never, // TODO(eyhn): fix type
{ text },
frame.id
);
release();
}
async changeDocTitle(docId: string, newTitle: string) {
const { doc, release } = this.open(docId);
doc.setPriorityLoad(10);
await doc.waitForSyncReady();
doc.changeDocTitle(newTitle);
release();
}
}
@@ -0,0 +1,258 @@
import { Store, yjsObserveByPath, yjsObserveDeep } from '@toeverything/infra';
import { differenceBy, isNil, omitBy } from 'lodash-es';
import { combineLatest, map, switchMap } from 'rxjs';
import { AbstractType as YAbstractType } from 'yjs';
import type { WorkspaceDBService } from '../../db';
import type {
DocCustomPropertyInfo,
DocProperties,
} from '../../db/schema/schema';
import type { WorkspaceService } from '../../workspace';
import { BUILT_IN_CUSTOM_PROPERTY_TYPE } from '../constants';
interface LegacyDocProperties {
custom?: Record<string, { value: unknown } | undefined>;
system?: Record<string, { value: unknown } | undefined>;
}
type LegacyDocPropertyInfo = {
id?: string;
name?: string;
type?: string;
icon?: string;
};
type LegacyDocPropertyInfoList = Record<
string,
LegacyDocPropertyInfo | undefined
>;
export class DocPropertiesStore extends Store {
constructor(
private readonly workspaceService: WorkspaceService,
private readonly dbService: WorkspaceDBService
) {
super();
}
updateDocProperties(id: string, config: Partial<DocProperties>) {
return this.dbService.db.docProperties.create({
id,
...config,
});
}
getDocPropertyInfoList() {
const db = this.dbService.db.docCustomPropertyInfo.find();
const legacy = this.upgradeLegacyDocPropertyInfoList(
this.getLegacyDocPropertyInfoList()
);
const builtIn = BUILT_IN_CUSTOM_PROPERTY_TYPE;
const withLegacy = [...db, ...differenceBy(legacy, db, i => i.id)];
const all = [
...withLegacy,
...differenceBy(builtIn, withLegacy, i => i.id),
];
return all.filter(i => !i.isDeleted);
}
createDocPropertyInfo(
config: Omit<DocCustomPropertyInfo, 'id'> & { id?: string }
) {
return this.dbService.db.docCustomPropertyInfo.create(config);
}
removeDocPropertyInfo(id: string) {
this.updateDocPropertyInfo(id, {
additionalData: {}, // also remove additional data to reduce size
isDeleted: true,
});
}
updateDocPropertyInfo(id: string, config: Partial<DocCustomPropertyInfo>) {
const needMigration = !this.dbService.db.docCustomPropertyInfo.get(id);
const isBuiltIn =
needMigration && BUILT_IN_CUSTOM_PROPERTY_TYPE.some(i => i.id === id);
if (isBuiltIn) {
this.createPropertyFromBuiltIn(id, config);
} else if (needMigration) {
// if this property is not in db, we need to migration it from legacy to db, only type and name is needed
this.migrateLegacyDocPropertyInfo(id, config);
} else {
this.dbService.db.docCustomPropertyInfo.update(id, config);
}
}
migrateLegacyDocPropertyInfo(
id: string,
override: Partial<DocCustomPropertyInfo>
) {
const legacy = this.getLegacyDocPropertyInfo(id);
this.dbService.db.docCustomPropertyInfo.create({
id,
type:
legacy?.type ??
'unknown' /* should never reach here, just for safety, we need handle unknown property type */,
name: legacy?.name,
...override,
});
}
createPropertyFromBuiltIn(
id: string,
override: Partial<DocCustomPropertyInfo>
) {
const builtIn = BUILT_IN_CUSTOM_PROPERTY_TYPE.find(i => i.id === id);
if (!builtIn) {
return;
}
this.createDocPropertyInfo({ ...builtIn, ...override });
}
watchDocPropertyInfoList() {
return combineLatest([
this.watchLegacyDocPropertyInfoList().pipe(
map(this.upgradeLegacyDocPropertyInfoList)
),
this.dbService.db.docCustomPropertyInfo.find$(),
]).pipe(
map(([legacy, db]) => {
const builtIn = BUILT_IN_CUSTOM_PROPERTY_TYPE;
const withLegacy = [...db, ...differenceBy(legacy, db, i => i.id)];
const all = [
...withLegacy,
...differenceBy(builtIn, withLegacy, i => i.id),
];
return all.filter(i => !i.isDeleted);
})
);
}
getDocProperties(id: string) {
return {
...this.upgradeLegacyDocProperties(this.getLegacyDocProperties(id)),
...omitBy(this.dbService.db.docProperties.get(id), isNil),
// db always override legacy, but nil value should not override
};
}
watchDocProperties(id: string) {
return combineLatest([
this.watchLegacyDocProperties(id).pipe(
map(this.upgradeLegacyDocProperties)
),
this.dbService.db.docProperties.get$(id),
]).pipe(
map(
([legacy, db]) =>
({
...legacy,
...omitBy(db, isNil), // db always override legacy, but nil value should not override
}) as DocProperties
)
);
}
private upgradeLegacyDocProperties(properties?: LegacyDocProperties) {
if (!properties) {
return {};
}
const newProperties: Record<string, string> = {};
for (const [key, info] of Object.entries(properties.system ?? {})) {
if (info?.value !== undefined && info.value !== null) {
newProperties[key] = info.value.toString();
}
}
for (const [key, info] of Object.entries(properties.custom ?? {})) {
if (info?.value !== undefined && info.value !== null) {
newProperties['custom:' + key] = info.value.toString();
}
}
return newProperties;
}
private upgradeLegacyDocPropertyInfoList(
infoList?: LegacyDocPropertyInfoList
) {
if (!infoList) {
return [];
}
const newInfoList: DocCustomPropertyInfo[] = [];
for (const [id, info] of Object.entries(infoList ?? {})) {
if (info?.type) {
newInfoList.push({
id,
name: info.name,
type: info.type,
icon: info.icon,
});
}
}
return newInfoList;
}
private getLegacyDocProperties(id: string) {
return this.workspaceService.workspace.rootYDoc
.getMap<any>('affine:workspace-properties')
.get('pageProperties')
?.get(id)
?.toJSON() as LegacyDocProperties | undefined;
}
private watchLegacyDocProperties(id: string) {
return yjsObserveByPath(
this.workspaceService.workspace.rootYDoc.getMap<any>(
'affine:workspace-properties'
),
`pageProperties.${id}`
).pipe(
switchMap(yjsObserveDeep),
map(
p =>
(p instanceof YAbstractType ? p.toJSON() : p) as
| LegacyDocProperties
| undefined
)
);
}
private getLegacyDocPropertyInfoList() {
return this.workspaceService.workspace.rootYDoc
.getMap<any>('affine:workspace-properties')
.get('schema')
?.get('pageProperties')
?.get('custom')
?.toJSON() as LegacyDocPropertyInfoList | undefined;
}
private watchLegacyDocPropertyInfoList() {
return yjsObserveByPath(
this.workspaceService.workspace.rootYDoc.getMap<any>(
'affine:workspace-properties'
),
'schema.pageProperties.custom'
).pipe(
switchMap(yjsObserveDeep),
map(
p =>
(p instanceof YAbstractType ? p.toJSON() : p) as
| LegacyDocPropertyInfoList
| undefined
)
);
}
private getLegacyDocPropertyInfo(id: string) {
return this.workspaceService.workspace.rootYDoc
.getMap<any>('affine:workspace-properties')
.get('schema')
?.get('pageProperties')
?.get('custom')
?.get(id)
?.toJSON() as LegacyDocPropertyInfo | undefined;
}
}
@@ -0,0 +1,144 @@
import type { DocMode } from '@blocksuite/affine/blocks';
import type { DocMeta } from '@blocksuite/affine/store';
import {
Store,
yjsObserve,
yjsObserveByPath,
yjsObserveDeep,
} from '@toeverything/infra';
import { distinctUntilChanged, map, switchMap } from 'rxjs';
import { Array as YArray, Map as YMap } from 'yjs';
import type { WorkspaceService } from '../../workspace';
import type { DocPropertiesStore } from './doc-properties';
export class DocsStore extends Store {
constructor(
private readonly workspaceService: WorkspaceService,
private readonly docPropertiesStore: DocPropertiesStore
) {
super();
}
getBlockSuiteDoc(id: string) {
return this.workspaceService.workspace.docCollection.getDoc(id);
}
createBlockSuiteDoc() {
return this.workspaceService.workspace.docCollection.createDoc();
}
watchDocIds() {
return yjsObserveByPath(
this.workspaceService.workspace.rootYDoc.getMap('meta'),
'pages'
).pipe(
switchMap(yjsObserve),
map(meta => {
if (meta instanceof YArray) {
return meta.map(v => v.get('id') as string);
} else {
return [];
}
})
);
}
watchTrashDocIds() {
return yjsObserveByPath(
this.workspaceService.workspace.rootYDoc.getMap('meta'),
'pages'
).pipe(
switchMap(yjsObserveDeep),
map(meta => {
if (meta instanceof YArray) {
return meta
.map(v => (v.get('trash') ? v.get('id') : null))
.filter(Boolean) as string[];
} else {
return [];
}
})
);
}
watchDocMeta(id: string) {
let docMetaIndexCache = -1;
return yjsObserveByPath(
this.workspaceService.workspace.rootYDoc.getMap('meta'),
'pages'
).pipe(
switchMap(yjsObserve),
map(meta => {
if (meta instanceof YArray) {
if (docMetaIndexCache >= 0) {
const doc = meta.get(docMetaIndexCache);
if (doc && doc.get('id') === id) {
return doc as YMap<any>;
}
}
// meta is YArray, `for-of` is faster then `for`
let i = 0;
for (const doc of meta) {
if (doc && doc.get('id') === id) {
docMetaIndexCache = i;
return doc as YMap<any>;
}
i++;
}
return null;
} else {
return null;
}
}),
switchMap(yjsObserveDeep),
map(meta => {
if (meta instanceof YMap) {
return meta.toJSON() as Partial<DocMeta>;
} else {
return {};
}
})
);
}
watchDocListReady() {
return this.workspaceService.workspace.engine.rootDocState$
.map(state => !state.syncing)
.asObservable();
}
setDocMeta(id: string, meta: Partial<DocMeta>) {
this.workspaceService.workspace.docCollection.setDocMeta(id, meta);
}
setDocPrimaryModeSetting(id: string, mode: DocMode) {
return this.docPropertiesStore.updateDocProperties(id, {
primaryMode: mode,
});
}
getDocPrimaryModeSetting(id: string) {
return this.docPropertiesStore.getDocProperties(id)?.primaryMode;
}
watchDocPrimaryModeSetting(id: string) {
return this.docPropertiesStore.watchDocProperties(id).pipe(
map(config => config?.primaryMode),
distinctUntilChanged((p, c) => p === c)
);
}
waitForDocLoadReady(id: string) {
return this.workspaceService.workspace.engine.doc.waitForReady(id);
}
setPriorityLoad(id: string, priority: number) {
return this.workspaceService.workspace.engine.doc.setPriority(id, priority);
}
markDocSyncStateAsReady(id: string) {
this.workspaceService.workspace.engine.doc.markAsReady(id);
}
}
@@ -1,20 +1,16 @@
import { DebugLogger } from '@affine/debug';
import type {
Job,
JobQueue,
WorkspaceLocalState,
WorkspaceService,
} from '@toeverything/infra';
import type { Job, JobQueue } from '@toeverything/infra';
import {
Entity,
IndexedDBIndexStorage,
IndexedDBJobQueue,
JobRunner,
LiveData,
WorkspaceDBService,
} from '@toeverything/infra';
import { map } from 'rxjs';
import { WorkspaceDBService } from '../../db';
import type { WorkspaceLocalState, WorkspaceService } from '../../workspace';
import { blockIndexSchema, docIndexSchema } from '../schema';
import { createWorker, type IndexerWorker } from '../worker/out-worker';
@@ -1,12 +1,12 @@
export { DocsSearchService } from './services/docs-search';
import { type Framework } from '@toeverything/infra';
import {
type Framework,
WorkspaceLocalState,
WorkspaceScope,
WorkspaceService,
} from '@toeverything/infra';
} from '../workspace';
import { DocsIndexer } from './entities/docs-indexer';
import { DocsSearchService } from './services/docs-search';
@@ -1,16 +1,12 @@
import { toURLSearchParams } from '@affine/core/modules/navigation';
import type { ReferenceParams } from '@blocksuite/blocks';
import type { WorkspaceService } from '@toeverything/infra';
import {
fromPromise,
OnEvent,
Service,
WorkspaceEngineBeforeStart,
} from '@toeverything/infra';
import { fromPromise, OnEvent, Service } from '@toeverything/infra';
import { isEmpty, omit } from 'lodash-es';
import { map, type Observable, switchMap } from 'rxjs';
import { z } from 'zod';
import type { WorkspaceService } from '../../workspace';
import { WorkspaceEngineBeforeStart } from '../../workspace';
import { DocsIndexer } from '../entities/docs-indexer';
@OnEvent(WorkspaceEngineBeforeStart, s => s.handleWorkspaceEngineBeforeStart)
@@ -15,7 +15,7 @@ import {
} from '@blocksuite/affine/store';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import type { DeltaInsert } from '@blocksuite/inline';
import { Document, getAFFiNEWorkspaceSchema } from '@toeverything/infra';
import { Document } from '@toeverything/infra';
import { toHexString } from 'lib0/buffer.js';
import { digest as lib0Digest } from 'lib0/hash/sha256';
import { difference, uniq } from 'lodash-es';
@@ -27,6 +27,7 @@ import {
Text as YText,
} from 'yjs';
import { getAFFiNEWorkspaceSchema } from '../../workspace/global-schema';
import type { BlockIndexSchema, DocIndexSchema } from '../schema';
import type {
WorkerIngoingMessage,
@@ -1,51 +0,0 @@
import { Framework, GlobalState, MemoryMemento } from '@toeverything/infra';
import { expect, test } from 'vitest';
import { EditorSetting } from '../entities/editor-setting';
import { GlobalStateEditorSettingProvider } from '../impls/global-state';
import { EditorSettingProvider } from '../provider/editor-setting-provider';
import { EditorSettingService } from '../services/editor-setting';
test('editor setting service', () => {
const framework = new Framework();
framework
.service(EditorSettingService)
.entity(EditorSetting, [EditorSettingProvider])
.impl(EditorSettingProvider, GlobalStateEditorSettingProvider, [
GlobalState,
])
.impl(GlobalState, MemoryMemento);
const provider = framework.provider();
const editorSettingService = provider.get(EditorSettingService);
// default value
expect(editorSettingService.editorSetting.get('fontFamily')).toBe('Sans');
// set plain object
editorSettingService.editorSetting.set('fontFamily', 'Serif');
expect(editorSettingService.editorSetting.get('fontFamily')).toBe('Serif');
// set nested object
editorSettingService.editorSetting.set('connector', {
stroke: {
dark: '#000000',
light: '#ffffff',
},
});
expect(editorSettingService.editorSetting.get('connector').stroke).toEqual({
dark: '#000000',
light: '#ffffff',
});
// invalid font family
editorSettingService.editorSetting.provider.set(
'fontFamily',
JSON.stringify('abc')
);
// fallback to default value
expect(editorSettingService.editorSetting.get('fontFamily')).toBe('Sans');
});
@@ -1,7 +1,7 @@
import type { GlobalState } from '@toeverything/infra';
import { Service } from '@toeverything/infra';
import { map, type Observable } from 'rxjs';
import type { GlobalState } from '../../storage';
import type { EditorSettingProvider } from '../provider/editor-setting-provider';
const storageKey = 'editor-setting';
@@ -1,8 +1,8 @@
import type { GlobalState } from '@toeverything/infra';
import { Service } from '@toeverything/infra';
import { map, type Observable, switchMap } from 'rxjs';
import type { ServersService } from '../../cloud';
import type { GlobalState } from '../../storage';
import { UserDBService } from '../../userspace';
import type { EditorSettingProvider } from '../provider/editor-setting-provider';
@@ -1,12 +1,9 @@
import {
type Framework,
GlobalState,
GlobalStateService,
} from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { ServersService } from '../cloud';
import { DesktopApiService } from '../desktop-api';
import { I18n } from '../i18n';
import { GlobalState, GlobalStateService } from '../storage';
import { EditorSetting } from './entities/editor-setting';
import { CurrentUserDBEditorSettingProvider } from './impls/user-db';
import { EditorSettingProvider } from './provider/editor-setting-provider';
@@ -1,11 +1,8 @@
import type { Workspace } from '@toeverything/infra';
import {
DocsService,
OnEvent,
Service,
WorkspaceInitialized,
} from '@toeverything/infra';
import { OnEvent, Service } from '@toeverything/infra';
import { DocsService } from '../../doc';
import type { Workspace } from '../../workspace';
import { WorkspaceInitialized } from '../../workspace';
import {
EditorSetting,
type EditorSettingExt,
@@ -3,11 +3,11 @@ import type {
SpellCheckStateSchema,
} from '@affine/electron/main/shared-state-schema';
import type { Language } from '@affine/i18n';
import type { GlobalStateService } from '@toeverything/infra';
import { LiveData, Service } from '@toeverything/infra';
import type { DesktopApiService } from '../../desktop-api';
import type { I18n } from '../../i18n';
import type { GlobalStateService } from '../../storage';
const SPELL_CHECK_SETTING_KEY: typeof SpellCheckStateKey = 'spellCheckState';
@@ -10,13 +10,14 @@ import type {
} from '@blocksuite/affine/presets';
import type { InlineEditor } from '@blocksuite/inline';
import { effect } from '@preact/signals-core';
import type { DocService, WorkspaceService } from '@toeverything/infra';
import { Entity, LiveData } from '@toeverything/infra';
import { defaults, isEqual, omit } from 'lodash-es';
import { skip } from 'rxjs';
import type { DocService } from '../../doc';
import { paramsParseOptions, preprocessParams } from '../../navigation/utils';
import type { WorkbenchView } from '../../workbench';
import type { WorkspaceService } from '../../workspace';
import { EditorScope } from '../scopes/editor';
import type { EditorSelector } from '../types';
@@ -1,11 +1,7 @@
import {
DocScope,
DocService,
type Framework,
WorkspaceScope,
WorkspaceService,
} from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { DocScope, DocService } from '../doc';
import { WorkspaceScope, WorkspaceService } from '../workspace';
import { Editor } from './entities/editor';
import { EditorScope } from './scopes/editor';
import { EditorService } from './services/editor';
@@ -1,7 +1,7 @@
import type { GlobalCache } from '@toeverything/infra';
import { Entity, LiveData } from '@toeverything/infra';
import { map } from 'rxjs';
import type { GlobalCache } from '../../storage';
import type { CollapsibleSectionName } from '../types';
const DEFAULT_COLLAPSABLE_STATE: Record<CollapsibleSectionName, boolean> = {
@@ -1,9 +1,7 @@
import {
type Framework,
GlobalCache,
WorkspaceScope,
} from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { GlobalCache } from '../storage';
import { WorkspaceScope } from '../workspace';
import { ExplorerSection } from './entities/explore-section';
import { ExplorerService } from './services/explorer';
export { ExplorerService } from './services/explorer';
@@ -8,7 +8,9 @@ import {
import { filterPage } from '@affine/core/components/page-list';
import { CollectionService } from '@affine/core/modules/collection';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { DocsService } from '@affine/core/modules/doc';
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
import { GlobalContextService } from '@affine/core/modules/global-context';
import { ShareDocsListService } from '@affine/core/modules/share-doc';
import type { AffineDNDData } from '@affine/core/types/dnd';
import type { Collection } from '@affine/env/filter';
@@ -18,8 +20,6 @@ import { track } from '@affine/track';
import type { DocMeta } from '@blocksuite/affine/store';
import { FilterMinusIcon } from '@blocksuite/icons/rc';
import {
DocsService,
GlobalContextService,
LiveData,
useLiveData,
useService,
@@ -9,7 +9,9 @@ import { useDeleteCollectionInfo } from '@affine/core/components/hooks/affine/us
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
import { CollectionService } from '@affine/core/modules/collection';
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
import {
@@ -19,12 +21,7 @@ import {
PlusIcon,
SplitViewIcon,
} from '@blocksuite/icons/rc';
import {
FeatureFlagService,
useLiveData,
useServices,
WorkspaceService,
} from '@toeverything/infra';
import { useLiveData, useServices } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
import type { NodeOperation } from '../../tree/types';
@@ -7,15 +7,15 @@ import {
} from '@affine/component';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { DocsService } from '@affine/core/modules/doc';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import { DocsSearchService } from '@affine/core/modules/docs-search';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { GlobalContextService } from '@affine/core/modules/global-context';
import type { AffineDNDData } from '@affine/core/types/dnd';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
import {
DocsService,
FeatureFlagService,
GlobalContextService,
LiveData,
useLiveData,
useService,
@@ -9,8 +9,11 @@ import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-pa
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
import { DocsService } from '@affine/core/modules/doc';
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
import {
@@ -22,13 +25,7 @@ import {
PlusIcon,
SplitViewIcon,
} from '@blocksuite/icons/rc';
import {
DocsService,
FeatureFlagService,
useLiveData,
useServices,
WorkspaceService,
} from '@toeverything/infra';
import { useLiveData, useServices } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
import type { NodeOperation } from '../../tree/types';
@@ -12,10 +12,12 @@ import {
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import {
type FolderNode,
OrganizeService,
} from '@affine/core/modules/organize';
import { WorkspaceService } from '@affine/core/modules/workspace';
import type { AffineDNDData } from '@affine/core/types/dnd';
import { Unreachable } from '@affine/env/constant';
import { useI18n } from '@affine/i18n';
@@ -29,12 +31,7 @@ import {
RemoveFolderIcon,
TagsIcon,
} from '@blocksuite/icons/rc';
import {
FeatureFlagService,
useLiveData,
useServices,
WorkspaceService,
} from '@toeverything/infra';
import { useLiveData, useServices } from '@toeverything/infra';
import { difference } from 'lodash-es';
import { useCallback, useMemo, useState } from 'react';
@@ -3,16 +3,13 @@ import {
type DropTargetOptions,
toast,
} from '@affine/component';
import { GlobalContextService } from '@affine/core/modules/global-context';
import type { Tag } from '@affine/core/modules/tag';
import { TagService } from '@affine/core/modules/tag';
import type { AffineDNDData } from '@affine/core/types/dnd';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
import {
GlobalContextService,
useLiveData,
useServices,
} from '@toeverything/infra';
import { useLiveData, useServices } from '@toeverything/infra';
import clsx from 'clsx';
import { useCallback, useMemo, useState } from 'react';
@@ -1,9 +1,12 @@
import { IconButton, MenuItem, MenuSeparator, toast } from '@affine/component';
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
import { DocsService } from '@affine/core/modules/doc';
import { FavoriteService } from '@affine/core/modules/favorite';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { TagService } from '@affine/core/modules/tag';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
import {
@@ -12,13 +15,7 @@ import {
PlusIcon,
SplitViewIcon,
} from '@blocksuite/icons/rc';
import {
DocsService,
FeatureFlagService,
useLiveData,
useServices,
WorkspaceService,
} from '@toeverything/infra';
import { useLiveData, useServices } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
import type { NodeOperation } from '../../tree/types';
@@ -13,16 +13,13 @@ import {
FavoriteService,
isFavoriteSupportType,
} from '@affine/core/modules/favorite';
import { WorkspaceService } from '@affine/core/modules/workspace';
import type { AffineDNDData } from '@affine/core/types/dnd';
import { isNewTabTrigger } from '@affine/core/utils';
import { useI18n } from '@affine/i18n';
import { track } from '@affine/track';
import { PlusIcon } from '@blocksuite/icons/rc';
import {
useLiveData,
useServices,
WorkspaceService,
} from '@toeverything/infra';
import { useLiveData, useServices } from '@toeverything/infra';
import { type MouseEventHandler, useCallback } from 'react';
import { ExplorerService } from '../../../services/explorer';
@@ -1,10 +1,11 @@
import { IconButton, useConfirmModal } from '@affine/component';
import { DocsService } from '@affine/core/modules/doc';
import { ExplorerTreeRoot } from '@affine/core/modules/explorer/views/tree';
import { MigrationFavoriteItemsAdapter } from '@affine/core/modules/favorite';
import { Trans, useI18n } from '@affine/i18n';
import { track } from '@affine/track';
import { BroomIcon, HelpIcon } from '@blocksuite/icons/rc';
import { DocsService, useLiveData, useServices } from '@toeverything/infra';
import { useLiveData, useServices } from '@toeverything/infra';
import { useCallback } from 'react';
import { CollapsibleSection } from '../../layouts/collapsible-section';
@@ -1,11 +1,8 @@
import {
type Framework,
WorkspaceDBService,
WorkspaceScope,
WorkspaceService,
} from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { WorkspaceServerService } from '../cloud';
import { WorkspaceDBService } from '../db';
import { WorkspaceScope, WorkspaceService } from '../workspace';
import { FavoriteList } from './entities/favorite-list';
import { FavoriteService } from './services/favorite';
import {
@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
// the adapter is to bridge the workspace rootdoc & native js bindings
import type { WorkspaceService } from '@affine/core/modules/workspace';
import { createYProxy, type Y } from '@blocksuite/affine/store';
import type { WorkspaceService } from '@toeverything/infra';
import { LiveData, Service } from '@toeverything/infra';
import { defaultsDeep } from 'lodash-es';
import { Observable } from 'rxjs';
@@ -155,6 +154,7 @@ class WorkspacePropertiesAdapter {
setJournalPageDateString(id: string, date: string) {
this.ensurePageProperties(id);
const pageProperties = this.pageProperties?.[id];
// oxlint-disable-next-line no-non-null-assertion
pageProperties!.system[PageSystemPropertyId.Journal].value = date;
}
@@ -1,8 +1,9 @@
import type { WorkspaceDBService, WorkspaceService } from '@toeverything/infra';
import { LiveData, Store } from '@toeverything/infra';
import { map } from 'rxjs';
import { AuthService, type WorkspaceServerService } from '../../cloud';
import type { WorkspaceDBService } from '../../db';
import type { WorkspaceService } from '../../workspace';
import type { FavoriteSupportTypeUnion } from '../constant';
import { isFavoriteSupportType } from '../constant';
@@ -0,0 +1,240 @@
import type { FlagInfo } from './types';
const isNotStableBuild = BUILD_CONFIG.appBuildType !== 'stable';
const isDesktopEnvironment = BUILD_CONFIG.isElectron;
const isCanaryBuild = BUILD_CONFIG.appBuildType === 'canary';
const isMobile = BUILD_CONFIG.isMobileEdition;
export const AFFINE_FLAGS = {
enable_ai: {
category: 'affine',
displayName:
'com.affine.settings.workspace.experimental-features.enable-ai.name',
description:
'com.affine.settings.workspace.experimental-features.enable-ai.description',
hide: true,
configurable: true,
defaultState: true,
},
enable_new_dnd: {
category: 'blocksuite',
bsFlag: 'enable_new_dnd',
displayName:
'com.affine.settings.workspace.experimental-features.enable-new-dnd.name',
description:
'com.affine.settings.workspace.experimental-features.enable-new-dnd.description',
configurable: false,
defaultState: true,
},
enable_database_full_width: {
category: 'blocksuite',
bsFlag: 'enable_database_full_width',
displayName:
'com.affine.settings.workspace.experimental-features.enable-database-full-width.name',
description:
'com.affine.settings.workspace.experimental-features.enable-database-full-width.description',
configurable: isCanaryBuild,
},
enable_database_attachment_note: {
category: 'blocksuite',
bsFlag: 'enable_database_attachment_note',
displayName:
'com.affine.settings.workspace.experimental-features.enable-database-attachment-note.name',
description:
'com.affine.settings.workspace.experimental-features.enable-database-attachment-note.description',
configurable: isNotStableBuild,
},
enable_block_query: {
category: 'blocksuite',
bsFlag: 'enable_block_query',
displayName:
'com.affine.settings.workspace.experimental-features.enable-block-query.name',
description:
'com.affine.settings.workspace.experimental-features.enable-block-query.description',
configurable: isCanaryBuild,
},
enable_synced_doc_block: {
category: 'blocksuite',
bsFlag: 'enable_synced_doc_block',
displayName:
'com.affine.settings.workspace.experimental-features.enable-synced-doc-block.name',
description:
'com.affine.settings.workspace.experimental-features.enable-synced-doc-block.description',
configurable: false,
defaultState: true,
},
enable_edgeless_text: {
category: 'blocksuite',
bsFlag: 'enable_edgeless_text',
displayName:
'com.affine.settings.workspace.experimental-features.enable-edgeless-text.name',
description:
'com.affine.settings.workspace.experimental-features.enable-edgeless-text.description',
configurable: false,
defaultState: true,
},
enable_color_picker: {
category: 'blocksuite',
bsFlag: 'enable_color_picker',
displayName:
'com.affine.settings.workspace.experimental-features.enable-color-picker.name',
description:
'com.affine.settings.workspace.experimental-features.enable-color-picker.description',
configurable: false,
defaultState: true,
},
enable_ai_chat_block: {
category: 'blocksuite',
bsFlag: 'enable_ai_chat_block',
displayName:
'com.affine.settings.workspace.experimental-features.enable-ai-chat-block.name',
description:
'com.affine.settings.workspace.experimental-features.enable-ai-chat-block.description',
configurable: false,
defaultState: true,
},
enable_ai_onboarding: {
category: 'blocksuite',
bsFlag: 'enable_ai_onboarding',
displayName:
'com.affine.settings.workspace.experimental-features.enable-ai-onboarding.name',
description:
'com.affine.settings.workspace.experimental-features.enable-ai-onboarding.description',
configurable: false,
defaultState: true,
},
enable_mind_map_import: {
category: 'blocksuite',
bsFlag: 'enable_mind_map_import',
displayName:
'com.affine.settings.workspace.experimental-features.enable-mind-map-import.name',
description:
'com.affine.settings.workspace.experimental-features.enable-mind-map-import.description',
configurable: false,
defaultState: true,
},
enable_multi_view: {
category: 'affine',
displayName:
'com.affine.settings.workspace.experimental-features.enable-multi-view.name',
description:
'com.affine.settings.workspace.experimental-features.enable-multi-view.description',
feedbackType: 'discord',
feedbackLink:
'https://discord.com/channels/959027316334407691/1280009690004324405',
configurable: isDesktopEnvironment,
defaultState: isCanaryBuild,
},
enable_emoji_folder_icon: {
category: 'affine',
displayName:
'com.affine.settings.workspace.experimental-features.enable-emoji-folder-icon.name',
description:
'com.affine.settings.workspace.experimental-features.enable-emoji-folder-icon.description',
feedbackType: 'discord',
feedbackLink:
'https://discord.com/channels/959027316334407691/1280014319865696351/1280014319865696351',
configurable: true,
defaultState: true,
},
enable_emoji_doc_icon: {
category: 'affine',
displayName:
'com.affine.settings.workspace.experimental-features.enable-emoji-doc-icon.name',
description:
'com.affine.settings.workspace.experimental-features.enable-emoji-doc-icon.description',
feedbackType: 'discord',
feedbackLink:
'https://discord.com/channels/959027316334407691/1280014319865696351',
configurable: true,
defaultState: true,
},
enable_editor_settings: {
category: 'affine',
displayName:
'com.affine.settings.workspace.experimental-features.enable-editor-settings.name',
description:
'com.affine.settings.workspace.experimental-features.enable-editor-settings.description',
configurable: false,
defaultState: true,
},
enable_offline_mode: {
category: 'affine',
displayName:
'com.affine.settings.workspace.experimental-features.enable-offline-mode.name',
description:
'com.affine.settings.workspace.experimental-features.enable-offline-mode.description',
configurable: isDesktopEnvironment,
defaultState: false,
},
enable_theme_editor: {
category: 'affine',
displayName:
'com.affine.settings.workspace.experimental-features.enable-theme-editor.name',
description:
'com.affine.settings.workspace.experimental-features.enable-theme-editor.description',
configurable: isCanaryBuild && !isMobile,
defaultState: isCanaryBuild,
},
enable_local_workspace: {
category: 'affine',
displayName:
'com.affine.settings.workspace.experimental-features.enable-local-workspace.name',
description:
'com.affine.settings.workspace.experimental-features.enable-local-workspace.description',
configurable: isCanaryBuild,
defaultState: isDesktopEnvironment || isCanaryBuild,
},
enable_advanced_block_visibility: {
category: 'blocksuite',
bsFlag: 'enable_advanced_block_visibility',
displayName:
'com.affine.settings.workspace.experimental-features.enable-advanced-block-visibility.name',
description:
'com.affine.settings.workspace.experimental-features.enable-advanced-block-visibility.description',
configurable: true,
defaultState: false,
},
enable_mobile_keyboard_toolbar: {
category: 'blocksuite',
bsFlag: 'enable_mobile_keyboard_toolbar',
displayName:
'com.affine.settings.workspace.experimental-features.enable-mobile-keyboard-toolbar.name',
description:
'com.affine.settings.workspace.experimental-features.enable-mobile-keyboard-toolbar.description',
configurable: false,
defaultState: isMobile,
},
enable_mobile_linked_doc_menu: {
category: 'blocksuite',
bsFlag: 'enable_mobile_linked_doc_menu',
displayName:
'com.affine.settings.workspace.experimental-features.enable-mobile-linked-doc-menu.name',
description:
'com.affine.settings.workspace.experimental-features.enable-mobile-linked-doc-menu.description',
configurable: false,
defaultState: isMobile,
},
enable_multiple_cloud_servers: {
category: 'affine',
displayName:
'com.affine.settings.workspace.experimental-features.enable-multiple-cloud-servers.name',
description:
'com.affine.settings.workspace.experimental-features.enable-multiple-cloud-servers.description',
configurable: isDesktopEnvironment,
defaultState: false,
},
enable_mobile_edgeless_editing: {
category: 'affine',
displayName:
'com.affine.settings.workspace.experimental-features.enable-mobile-edgeless-editing.name',
description:
'com.affine.settings.workspace.experimental-features.enable-mobile-edgeless-editing.description',
configurable: isMobile,
defaultState: false,
},
} satisfies { [key in string]: FlagInfo };
// eslint-disable-next-line no-redeclare
export type AFFINE_FLAGS = typeof AFFINE_FLAGS;
@@ -0,0 +1,66 @@
import { Entity, LiveData } from '@toeverything/infra';
import { NEVER } from 'rxjs';
import type { GlobalStateService } from '../../storage';
import { AFFINE_FLAGS } from '../constant';
import type { FlagInfo } from '../types';
const FLAG_PREFIX = 'affine-flag:';
export type Flag<F extends FlagInfo = FlagInfo> = {
readonly value: F['defaultState'] extends boolean
? boolean
: boolean | undefined;
set: (value: boolean) => void;
// eslint-disable-next-line rxjs/finnish
$: F['defaultState'] extends boolean
? LiveData<boolean>
: LiveData<boolean> | LiveData<boolean | undefined>;
} & F;
export class Flags extends Entity {
private readonly globalState = this.globalStateService.globalState;
constructor(private readonly globalStateService: GlobalStateService) {
super();
Object.entries(AFFINE_FLAGS).forEach(([flagKey, flag]) => {
const configurable = flag.configurable ?? true;
const defaultState =
'defaultState' in flag ? flag.defaultState : undefined;
const getValue = () => {
return configurable
? (this.globalState.get<boolean>(FLAG_PREFIX + flagKey) ??
defaultState)
: defaultState;
};
const item = {
...flag,
get value() {
return getValue();
},
set: (value: boolean) => {
if (!configurable) {
return;
}
this.globalState.set(FLAG_PREFIX + flagKey, value);
},
$: configurable
? LiveData.from<boolean | undefined>(
this.globalState.watch<boolean>(FLAG_PREFIX + flagKey),
undefined
).map(value => value ?? defaultState)
: LiveData.from(NEVER, defaultState),
} as Flag<typeof flag>;
Object.defineProperty(this, flagKey, {
get: () => {
return item;
},
});
});
}
}
export type FlagsExt = Flags & {
[K in keyof AFFINE_FLAGS]: Flag<AFFINE_FLAGS[K]>;
};
@@ -0,0 +1,14 @@
import type { Framework } from '@toeverything/infra';
import { GlobalStateService } from '../storage';
import { Flags } from './entities/flags';
import { FeatureFlagService } from './services/feature-flag';
export { AFFINE_FLAGS } from './constant';
export type { Flag } from './entities/flags';
export { FeatureFlagService } from './services/feature-flag';
export type { FlagInfo } from './types';
export function configureFeatureFlagModule(framework: Framework) {
framework.service(FeatureFlagService).entity(Flags, [GlobalStateService]);
}
@@ -0,0 +1,34 @@
import { OnEvent, Service } from '@toeverything/infra';
import { distinctUntilChanged, skip } from 'rxjs';
import { ApplicationStarted } from '../../lifecycle';
import type { Workspace } from '../../workspace';
import { WorkspaceInitialized } from '../../workspace/events';
import { AFFINE_FLAGS } from '../constant';
import { Flags, type FlagsExt } from '../entities/flags';
@OnEvent(WorkspaceInitialized, e => e.setupBlocksuiteEditorFlags)
@OnEvent(ApplicationStarted, e => e.setupRestartListener)
export class FeatureFlagService extends Service {
flags = this.framework.createEntity(Flags) as FlagsExt;
setupBlocksuiteEditorFlags(workspace: Workspace) {
for (const [key, flag] of Object.entries(AFFINE_FLAGS)) {
if (flag.category === 'blocksuite') {
const value = this.flags[key as keyof AFFINE_FLAGS].value;
if (value !== undefined) {
workspace.docCollection.awarenessStore.setFlag(flag.bsFlag, value);
}
}
}
}
setupRestartListener() {
this.flags.enable_ai.$.pipe(distinctUntilChanged(), skip(1)).subscribe(
() => {
// when enable_ai flag changes, reload the page.
window.location.reload();
}
);
}
}
@@ -0,0 +1,24 @@
import type { BlockSuiteFlags } from '@blocksuite/affine/global/types';
type FeedbackType = 'discord' | 'email' | 'github';
export type FlagInfo = {
displayName: string;
description?: string;
configurable?: boolean;
defaultState?: boolean; // default to open and not controlled by user
/**
* hide in the feature flag settings, but still can be controlled by the code
*/
hide?: boolean;
feedbackType?: FeedbackType;
feedbackLink?: string;
} & (
| {
category: 'affine';
}
| {
category: 'blocksuite';
bsFlag: keyof BlockSuiteFlags;
}
);
@@ -0,0 +1,51 @@
import type { DocMode } from '@blocksuite/affine/blocks';
import { Entity, LiveData, MemoryMemento } from '@toeverything/infra';
export class GlobalContext extends Entity {
memento = new MemoryMemento();
workspaceId = this.define<string>('workspaceId');
workspaceFlavour = this.define<string>('workspaceFlavour');
serverId = this.define<string>('serverId');
/**
* is in doc page
*/
isDoc = this.define<boolean>('isDoc');
isTrashDoc = this.define<boolean>('isTrashDoc');
docId = this.define<string>('docId');
docMode = this.define<DocMode>('docMode');
/**
* is in collection page
*/
isCollection = this.define<boolean>('isCollection');
collectionId = this.define<string>('collectionId');
/**
* is in trash page
*/
isTrash = this.define<boolean>('isTrash');
/**
* is in tag page
*/
isTag = this.define<boolean>('isTag');
tagId = this.define<string>('tagId');
/**
* is in all docs page
*/
isAllDocs = this.define<boolean>('isAllDocs');
define<T>(key: string) {
this.memento.set(key, null);
const livedata$ = LiveData.from(this.memento.watch<T>(key), null);
return {
get: () => this.memento.get(key) as T | null,
set: (value: T | null) => this.memento.set(key, value),
$: livedata$,
};
}
}
@@ -0,0 +1,10 @@
export { GlobalContextService } from './services/global-context';
import type { Framework } from '@toeverything/infra';
import { GlobalContext } from './entities/global-context';
import { GlobalContextService } from './services/global-context';
export function configureGlobalContextModule(framework: Framework) {
framework.service(GlobalContextService).entity(GlobalContext);
}
@@ -0,0 +1,7 @@
import { Service } from '@toeverything/infra';
import { GlobalContext } from '../entities/global-context';
export class GlobalContextService extends Service {
globalContext = this.framework.createEntity(GlobalContext);
}
@@ -6,10 +6,11 @@ import {
type Language,
SUPPORTED_LANGUAGES,
} from '@affine/i18n';
import type { GlobalCache } from '@toeverything/infra';
import { effect, Entity, fromPromise, LiveData } from '@toeverything/infra';
import { catchError, EMPTY, exhaustMap, mergeMap } from 'rxjs';
import type { GlobalCache } from '../../storage';
export type LanguageInfo = {
key: Language;
name: string;
@@ -1,5 +1,6 @@
import { type Framework, GlobalCache } from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { GlobalCache } from '../storage';
import { I18nProvider } from './context';
import { I18n, type LanguageInfo } from './entities/i18n';
import { I18nService } from './services/i18n';
@@ -1,6 +1,7 @@
import { type Framework, WorkspacesService } from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { FetchService } from '../cloud';
import { WorkspacesService } from '../workspace';
import { ImportTemplateDialog } from './entities/dialog';
import { TemplateDownloader } from './entities/downloader';
import { TemplateDownloaderService } from './services/downloader';
@@ -1,6 +1,8 @@
import { type DocMode, ZipTransformer } from '@blocksuite/affine/blocks';
import type { WorkspaceMetadata, WorkspacesService } from '@toeverything/infra';
import { DocsService, Service } from '@toeverything/infra';
import { Service } from '@toeverything/infra';
import { DocsService } from '../../doc';
import type { WorkspaceMetadata, WorkspacesService } from '../../workspace';
export class ImportTemplateService extends Service {
constructor(private readonly workspacesService: WorkspacesService) {
+18 -3
View File
@@ -1,12 +1,14 @@
import { configureQuotaModule } from '@affine/core/modules/quota';
import { configureInfraModules, type Framework } from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { configureAppSidebarModule } from './app-sidebar';
import { configAtMenuConfigModule } from './at-menu-config';
import { configureCloudModule } from './cloud';
import { configureCollectionModule } from './collection';
import { configureWorkspaceDBModule } from './db';
import { configureDialogModule } from './dialogs';
import { configureDndModule } from './dnd';
import { configureDocModule } from './doc';
import { configureDocDisplayMetaModule } from './doc-display-meta';
import { configureDocInfoModule } from './doc-info';
import { configureDocLinksModule } from './doc-link';
@@ -15,9 +17,12 @@ import { configureEditorModule } from './editor';
import { configureEditorSettingModule } from './editor-setting';
import { configureExplorerModule } from './explorer';
import { configureFavoriteModule } from './favorite';
import { configureFeatureFlagModule } from './feature-flag';
import { configureGlobalContextModule } from './global-context';
import { configureI18nModule } from './i18n';
import { configureImportTemplateModule } from './import-template';
import { configureJournalModule } from './journal';
import { configureLifecycleModule } from './lifecycle';
import { configureNavigationModule } from './navigation';
import { configureOpenInApp } from './open-in-app';
import { configureOrganizeModule } from './organize';
@@ -27,7 +32,10 @@ import { configurePermissionsModule } from './permissions';
import { configureQuickSearchModule } from './quicksearch';
import { configureShareDocsModule } from './share-doc';
import { configureShareSettingModule } from './share-setting';
import { configureCommonGlobalStorageImpls } from './storage';
import {
configureCommonGlobalStorageImpls,
configureGlobalStorageModule,
} from './storage';
import { configureSystemFontFamilyModule } from './system-font-family';
import { configureTagModule } from './tag';
import { configureTelemetryModule } from './telemetry';
@@ -35,10 +43,17 @@ import { configureAppThemeModule } from './theme';
import { configureThemeEditorModule } from './theme-editor';
import { configureUrlModule } from './url';
import { configureUserspaceModule } from './userspace';
import { configureWorkspaceModule } from './workspace';
export function configureCommonModules(framework: Framework) {
configureI18nModule(framework);
configureInfraModules(framework);
configureWorkspaceModule(framework);
configureDocModule(framework);
configureWorkspaceDBModule(framework);
configureGlobalStorageModule(framework);
configureGlobalContextModule(framework);
configureLifecycleModule(framework);
configureFeatureFlagModule(framework);
configureCollectionModule(framework);
configureNavigationModule(framework);
configureTagModule(framework);
@@ -1,12 +1,8 @@
import {
DocScope,
DocService,
DocsService,
type Framework,
WorkspaceScope,
} from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { DocScope, DocService, DocsService } from '../doc';
import { EditorSettingService } from '../editor-setting';
import { WorkspaceScope } from '../workspace';
import { JournalService } from './services/journal';
import { JournalDocService } from './services/journal-doc';
import { JournalStore } from './store/journal';
@@ -1,5 +1,6 @@
import { type DocService, Service } from '@toeverything/infra';
import { Service } from '@toeverything/infra';
import type { DocService } from '../../doc';
import type { JournalService } from './journal';
export class JournalDocService extends Service {
@@ -1,8 +1,9 @@
import { Text } from '@blocksuite/affine/store';
import type { DocProps, DocsService } from '@toeverything/infra';
import type { DocProps } from '@toeverything/infra';
import { initDocFromProps, LiveData, Service } from '@toeverything/infra';
import dayjs from 'dayjs';
import type { DocsService } from '../../doc';
import type { EditorSettingService } from '../../editor-setting';
import type { JournalStore } from '../store/journal';
@@ -1,7 +1,8 @@
import type { DocsService } from '@toeverything/infra';
import { LiveData, Store } from '@toeverything/infra';
import type { Observable } from 'rxjs';
import type { DocsService } from '../../doc';
function isJournalString(j?: string | false) {
return j ? !!j?.match(/^\d{4}-\d{2}-\d{2}$/) : false;
}
@@ -0,0 +1,13 @@
import type { Framework } from '@toeverything/infra';
import { LifecycleService } from './service/lifecycle';
export {
ApplicationFocused,
ApplicationStarted,
LifecycleService,
} from './service/lifecycle';
export function configureLifecycleModule(framework: Framework) {
framework.service(LifecycleService);
}
@@ -0,0 +1,26 @@
import { createEvent, Service } from '@toeverything/infra';
/**
* Event that is emitted when application is started.
*/
export const ApplicationStarted = createEvent<boolean>('ApplicationStartup');
/**
* Event that is emitted when browser tab or windows is focused again, after being blurred.
* Can be used to actively refresh some data.
*/
export const ApplicationFocused = createEvent<boolean>('ApplicationFocused');
export class LifecycleService extends Service {
constructor() {
super();
}
applicationStart() {
this.eventBus.emit(ApplicationStarted, true);
}
applicationFocus() {
this.eventBus.emit(ApplicationFocused, true);
}
}
@@ -6,9 +6,10 @@ export {
} from './utils';
export { NavigationButtons } from './view/navigation-buttons';
import { type Framework, WorkspaceScope } from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { WorkbenchService } from '../workbench/services/workbench';
import { WorkspaceScope } from '../workspace';
import { Navigator } from './entities/navigator';
import { NavigatorService } from './services/navigator';
@@ -1,9 +1,7 @@
import {
type Framework,
GlobalState,
WorkspacesService,
} from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { GlobalState } from '../storage';
import { WorkspacesService } from '../workspace';
import { OpenInAppService } from './services';
export { OpenInAppService, OpenLinkMode } from './services';
@@ -1,8 +1,9 @@
import type { GlobalState, WorkspacesService } from '@toeverything/infra';
import { LiveData, OnEvent, Service } from '@toeverything/infra';
import { resolveLinkToDoc } from '../../navigation';
import type { GlobalState } from '../../storage';
import { WorkbenchLocationChanged } from '../../workbench/services/workbench';
import type { WorkspacesService } from '../../workspace';
import { getLocalWorkspaceIds } from '../../workspace-engine/impls/local';
const storageKey = 'open-link-mode';
@@ -1,9 +1,7 @@
import {
type Framework,
WorkspaceDBService,
WorkspaceScope,
} from '@toeverything/infra';
import { type Framework } from '@toeverything/infra';
import { WorkspaceDBService } from '../db';
import { WorkspaceScope } from '../workspace';
import { FolderNode } from './entities/folder-node';
import { FolderTree } from './entities/folder-tree';
import { OrganizeService } from './services/organize';
@@ -1,6 +1,7 @@
import type { WorkspaceDBService } from '@toeverything/infra';
import { Store } from '@toeverything/infra';
import type { WorkspaceDBService } from '../../db';
export class FolderStore extends Store {
constructor(private readonly dbService: WorkspaceDBService) {
super();

Some files were not shown because too many files have changed in this diff Show More