mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 04:48:53 +00:00
feat(core): page info adapter for journal (#5561)
Page info adapter + schema. Adapted for journal features. 
This commit is contained in:
@@ -1 +1,2 @@
|
||||
export * from './atoms';
|
||||
export * from './properties';
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
// the adapter is to bridge the workspace rootdoc & native js bindings
|
||||
|
||||
import { createYProxy, type Workspace, type Y } from '@blocksuite/store';
|
||||
import { defaultsDeep } from 'lodash-es';
|
||||
|
||||
import {
|
||||
PagePropertyType,
|
||||
PageSystemPropertyId,
|
||||
type TagOption,
|
||||
type WorkspaceAffineProperties,
|
||||
type WorkspaceFavoriteItem,
|
||||
} from './schema';
|
||||
|
||||
const AFFINE_PROPERTIES_ID = 'affine:workspace-properties';
|
||||
|
||||
/**
|
||||
* WorkspacePropertiesAdapter is a wrapper for workspace properties.
|
||||
* Users should not directly access the workspace properties via yjs, but use this adapter instead.
|
||||
*
|
||||
* Question for enhancement in the future:
|
||||
* May abstract the adapter for each property type, e.g. PagePropertiesAdapter, SchemaAdapter, etc.
|
||||
* So that the adapter could be more focused and easier to maintain (like assigning default values)
|
||||
* However the properties for an abstraction may not be limited to a single yjs map.
|
||||
*/
|
||||
export class WorkspacePropertiesAdapter {
|
||||
// provides a easy-to-use interface for workspace properties
|
||||
private readonly proxy: WorkspaceAffineProperties;
|
||||
public readonly properties: Y.Map<any>;
|
||||
|
||||
constructor(private readonly workspace: Workspace) {
|
||||
// check if properties exists, if not, create one
|
||||
const rootDoc = workspace.doc;
|
||||
this.properties = rootDoc.getMap(AFFINE_PROPERTIES_ID);
|
||||
this.proxy = createYProxy(this.properties);
|
||||
|
||||
// fixme: deal with migration issue?
|
||||
this.ensureRootProperties();
|
||||
}
|
||||
|
||||
private ensureRootProperties() {
|
||||
// todo: deal with schema change issue
|
||||
// fixme: may not to be called every time
|
||||
defaultsDeep(this.proxy, {
|
||||
schema: {
|
||||
pageProperties: {
|
||||
custom: {},
|
||||
system: {
|
||||
journal: {
|
||||
id: PageSystemPropertyId.Journal,
|
||||
name: 'Journal',
|
||||
source: 'system',
|
||||
type: PagePropertyType.Date,
|
||||
},
|
||||
tags: {
|
||||
id: PageSystemPropertyId.Tags,
|
||||
name: 'Tags',
|
||||
source: 'system',
|
||||
type: PagePropertyType.Tags,
|
||||
options: this.workspace.meta.properties.tags?.options ?? [], // better use a one time migration
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
favorites: {},
|
||||
pageProperties: {},
|
||||
});
|
||||
}
|
||||
|
||||
private ensurePageProperties(pageId: string) {
|
||||
// fixme: may not to be called every time
|
||||
defaultsDeep(this.proxy.pageProperties, {
|
||||
[pageId]: {
|
||||
custom: {},
|
||||
system: {
|
||||
[PageSystemPropertyId.Journal]: {
|
||||
id: PageSystemPropertyId.Journal,
|
||||
value: false,
|
||||
},
|
||||
[PageSystemPropertyId.Tags]: {
|
||||
id: PageSystemPropertyId.Tags,
|
||||
value: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
get schema() {
|
||||
return this.proxy.schema;
|
||||
}
|
||||
|
||||
get favorites() {
|
||||
return this.proxy.favorites;
|
||||
}
|
||||
|
||||
get pageProperties() {
|
||||
return this.proxy.pageProperties;
|
||||
}
|
||||
|
||||
// ====== utilities ======
|
||||
|
||||
getPageProperties(pageId: string) {
|
||||
return this.pageProperties[pageId];
|
||||
}
|
||||
|
||||
isFavorite(id: string, type: WorkspaceFavoriteItem['type']) {
|
||||
return this.favorites[id]?.type === type;
|
||||
}
|
||||
|
||||
getJournalPageDateString(id: string) {
|
||||
this.ensurePageProperties(id);
|
||||
return this.pageProperties[id].system[PageSystemPropertyId.Journal].value;
|
||||
}
|
||||
|
||||
setJournalPageDateString(id: string, date: string) {
|
||||
this.ensurePageProperties(id);
|
||||
const pageProperties = this.pageProperties[id];
|
||||
pageProperties.system[PageSystemPropertyId.Journal].value = date;
|
||||
}
|
||||
|
||||
get tagOptions() {
|
||||
return this.schema.pageProperties.system[PageSystemPropertyId.Tags].options;
|
||||
}
|
||||
|
||||
// page tags could be reactive
|
||||
getPageTags(pageId: string) {
|
||||
this.ensurePageProperties(pageId);
|
||||
const tags =
|
||||
this.pageProperties[pageId].system[PageSystemPropertyId.Tags].value;
|
||||
const optionsMap = Object.fromEntries(this.tagOptions.map(o => [o.id, o]));
|
||||
return tags.map(tag => optionsMap[tag]).filter((t): t is TagOption => !!t);
|
||||
}
|
||||
|
||||
addPageTag(pageId: string, tag: TagOption | string) {
|
||||
this.ensurePageProperties(pageId);
|
||||
const tags = this.getPageTags(pageId);
|
||||
const tagId = typeof tag === 'string' ? tag : tag.id;
|
||||
if (tags.some(t => t.id === tagId)) {
|
||||
return;
|
||||
}
|
||||
const pageProperties = this.pageProperties[pageId];
|
||||
pageProperties.system[PageSystemPropertyId.Tags].value.push(tagId);
|
||||
}
|
||||
|
||||
removePageTag(pageId: string, tag: TagOption | string) {
|
||||
this.ensurePageProperties(pageId);
|
||||
const tags = this.getPageTags(pageId);
|
||||
const tagId = typeof tag === 'string' ? tag : tag.id;
|
||||
const index = tags.findIndex(t => t.id === tagId);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
const pageProperties = this.pageProperties[pageId];
|
||||
pageProperties.system[PageSystemPropertyId.Tags].value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Workspace } from '@affine/workspace/workspace';
|
||||
import { atomWithObservable } from 'jotai/utils';
|
||||
import { filter, map, of } from 'rxjs';
|
||||
|
||||
import { currentWorkspaceAtom } from '../atoms';
|
||||
import { WorkspacePropertiesAdapter } from './adapter';
|
||||
|
||||
export const currentWorkspacePropertiesAdapterAtom =
|
||||
atomWithObservable<WorkspacePropertiesAdapter>(get => {
|
||||
return of(get(currentWorkspaceAtom)).pipe(
|
||||
filter((workspace): workspace is Workspace => !!workspace),
|
||||
map(workspace => {
|
||||
return new WorkspacePropertiesAdapter(workspace.blockSuiteWorkspace);
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './adapter';
|
||||
export * from './atom';
|
||||
@@ -0,0 +1,101 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// ===== workspace-wide page property schema =====
|
||||
export const TagOptionSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
color: z.string(),
|
||||
});
|
||||
|
||||
export type TagOption = z.infer<typeof TagOptionSchema>;
|
||||
|
||||
export enum PageSystemPropertyId {
|
||||
Tags = 'tags',
|
||||
Journal = 'journal',
|
||||
}
|
||||
|
||||
export enum PagePropertyType {
|
||||
String = 'string',
|
||||
Number = 'number',
|
||||
Boolean = 'boolean',
|
||||
Date = 'date',
|
||||
Tags = 'tags',
|
||||
}
|
||||
|
||||
export const PagePropertyMetaBaseSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
source: z.string(),
|
||||
type: z.string(),
|
||||
});
|
||||
|
||||
export const PageSystemPropertyMetaBaseSchema =
|
||||
PagePropertyMetaBaseSchema.extend({
|
||||
source: z.literal('system'),
|
||||
});
|
||||
|
||||
export const PageCustomPropertyMetaSchema = PagePropertyMetaBaseSchema.extend({
|
||||
source: z.literal('custom'),
|
||||
type: z.nativeEnum(PagePropertyType),
|
||||
});
|
||||
|
||||
// ====== page info schema ======
|
||||
export const PageInfoItemSchema = z.object({
|
||||
id: z.string(), // property id. Maps to PagePropertyMetaSchema.id
|
||||
hidden: z.boolean().optional(),
|
||||
value: z.any(), // corresponds to PagePropertyMetaSchema.type
|
||||
});
|
||||
|
||||
export const PageInfoJournalItemSchema = PageInfoItemSchema.extend({
|
||||
id: z.literal(PageSystemPropertyId.Journal),
|
||||
value: z.union([z.string(), z.literal(false)]),
|
||||
});
|
||||
|
||||
export const PageInfoTagsItemSchema = PageInfoItemSchema.extend({
|
||||
id: z.literal(PageSystemPropertyId.Tags),
|
||||
value: z.array(z.string()),
|
||||
});
|
||||
|
||||
// ====== workspace properties schema ======
|
||||
export const WorkspaceFavoriteItemSchema = z.object({
|
||||
id: z.string(),
|
||||
order: z.number(),
|
||||
type: z.enum(['page', 'collection']),
|
||||
});
|
||||
|
||||
export type WorkspaceFavoriteItem = z.infer<typeof WorkspaceFavoriteItemSchema>;
|
||||
|
||||
const WorkspaceAffinePropertiesSchemaSchema = z.object({
|
||||
pageProperties: z.object({
|
||||
custom: z.record(PageCustomPropertyMetaSchema),
|
||||
system: z.object({
|
||||
[PageSystemPropertyId.Journal]: PageSystemPropertyMetaBaseSchema.extend({
|
||||
id: z.literal(PageSystemPropertyId.Journal),
|
||||
type: z.literal(PagePropertyType.Date),
|
||||
}),
|
||||
[PageSystemPropertyId.Tags]: PagePropertyMetaBaseSchema.extend({
|
||||
id: z.literal(PageSystemPropertyId.Tags),
|
||||
type: z.literal(PagePropertyType.Tags),
|
||||
options: z.array(TagOptionSchema),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
const WorkspacePagePropertiesSchema = z.object({
|
||||
custom: z.record(PageInfoItemSchema.extend({ order: z.number() })),
|
||||
system: z.object({
|
||||
[PageSystemPropertyId.Journal]: PageInfoJournalItemSchema,
|
||||
[PageSystemPropertyId.Tags]: PageInfoTagsItemSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
export const WorkspaceAffinePropertiesSchema = z.object({
|
||||
schema: WorkspaceAffinePropertiesSchemaSchema,
|
||||
favorites: z.record(WorkspaceFavoriteItemSchema),
|
||||
pageProperties: z.record(WorkspacePagePropertiesSchema),
|
||||
});
|
||||
|
||||
export type WorkspaceAffineProperties = z.infer<
|
||||
typeof WorkspaceAffinePropertiesSchema
|
||||
>;
|
||||
Reference in New Issue
Block a user