mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-02 18:09:58 +08:00
feat(core): page info adapter for journal (#5561)
Page info adapter + schema. Adapted for journal features. 
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { usePageMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
import { usePageMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||||
import { useBlockSuiteWorkspacePage } from '@affine/core/hooks/use-block-suite-workspace-page';
|
import { useBlockSuiteWorkspacePage } from '@affine/core/hooks/use-block-suite-workspace-page';
|
||||||
|
import { timestampToLocalDate } from '@affine/core/utils';
|
||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
import {
|
import {
|
||||||
fetchWithTraceReport,
|
fetchWithTraceReport,
|
||||||
@@ -176,11 +177,7 @@ export const useSnapshotPage = (
|
|||||||
export const historyListGroupByDay = (histories: DocHistory[]) => {
|
export const historyListGroupByDay = (histories: DocHistory[]) => {
|
||||||
const map = new Map<string, DocHistory[]>();
|
const map = new Map<string, DocHistory[]>();
|
||||||
for (const history of histories) {
|
for (const history of histories) {
|
||||||
const day = new Date(history.timestamp).toLocaleDateString(undefined, {
|
const day = timestampToLocalDate(history.timestamp);
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
});
|
|
||||||
const list = map.get(day) ?? [];
|
const list = map.get(day) ?? [];
|
||||||
list.push(history);
|
list.push(history);
|
||||||
map.set(day, list);
|
map.set(day, list);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useIsWorkspaceOwner } from '@affine/core/hooks/affine/use-is-workspace-
|
|||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import { useUserSubscription } from '@affine/core/hooks/use-subscription';
|
import { useUserSubscription } from '@affine/core/hooks/use-subscription';
|
||||||
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
|
import { waitForCurrentWorkspaceAtom } from '@affine/core/modules/workspace';
|
||||||
|
import { timestampToLocalTime } from '@affine/core/utils';
|
||||||
import { SubscriptionPlan } from '@affine/graphql';
|
import { SubscriptionPlan } from '@affine/graphql';
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
@@ -88,14 +89,6 @@ const ModalContainer = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const localTimeFormatter = new Intl.DateTimeFormat('en', {
|
|
||||||
timeStyle: 'short',
|
|
||||||
});
|
|
||||||
|
|
||||||
const timestampToLocalTime = (ts: string) => {
|
|
||||||
return localTimeFormatter.format(new Date(ts));
|
|
||||||
};
|
|
||||||
|
|
||||||
interface HistoryEditorPreviewProps {
|
interface HistoryEditorPreviewProps {
|
||||||
ts?: string;
|
ts?: string;
|
||||||
snapshotPage?: Page;
|
snapshotPage?: Page;
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ export const JournalWeekDatePicker = ({
|
|||||||
page,
|
page,
|
||||||
}: JournalWeekDatePickerProps) => {
|
}: JournalWeekDatePickerProps) => {
|
||||||
const handleRef = useRef<WeekDatePickerHandle>(null);
|
const handleRef = useRef<WeekDatePickerHandle>(null);
|
||||||
const { journalDate } = useJournalInfoHelper(page.meta);
|
const { journalDate } = useJournalInfoHelper(workspace, page.id);
|
||||||
const { openJournal } = useJournalHelper(workspace);
|
const { openJournal } = useJournalHelper(workspace);
|
||||||
const [date, setDate] = useState(
|
const [date, setDate] = useState(
|
||||||
(journalDate ?? dayjs()).format('YYYY-MM-DD')
|
(journalDate ?? dayjs()).format('YYYY-MM-DD')
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ export const AppSidebarJournalButton = ({
|
|||||||
}: AppSidebarJournalButtonProps) => {
|
}: AppSidebarJournalButtonProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||||
const currentPage = currentPageId ? workspace.getPage(currentPageId) : null;
|
|
||||||
const { openToday } = useJournalHelper(workspace);
|
const { openToday } = useJournalHelper(workspace);
|
||||||
const { journalDate, isJournal } = useJournalInfoHelper(currentPage?.meta);
|
const { journalDate, isJournal } = useJournalInfoHelper(
|
||||||
|
workspace,
|
||||||
|
currentPageId
|
||||||
|
);
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const isJournalActive = isJournal && !!params.pageId;
|
const isJournalActive = isJournal && !!params.pageId;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import type { Workspace } from '@blocksuite/store';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { useEffect, useMemo, useReducer } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
currentWorkspacePropertiesAdapterAtom,
|
||||||
|
WorkspacePropertiesAdapter,
|
||||||
|
} from '../modules/workspace';
|
||||||
|
|
||||||
|
const useReactiveAdapter = (adapter: WorkspacePropertiesAdapter) => {
|
||||||
|
const [, forceUpdate] = useReducer(c => c + 1, 0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// todo: track which properties are used and then filter by property path change
|
||||||
|
// using Y.YEvent.path
|
||||||
|
function observe() {
|
||||||
|
forceUpdate();
|
||||||
|
}
|
||||||
|
adapter.properties.observeDeep(observe);
|
||||||
|
return () => {
|
||||||
|
adapter.properties.unobserveDeep(observe);
|
||||||
|
};
|
||||||
|
}, [adapter]);
|
||||||
|
|
||||||
|
return adapter;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useCurrentWorkspacePropertiesAdapter() {
|
||||||
|
const adapter = useAtomValue(currentWorkspacePropertiesAdapterAtom);
|
||||||
|
return useReactiveAdapter(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWorkspacePropertiesAdapter(workspace: Workspace) {
|
||||||
|
const adapter = useMemo(
|
||||||
|
() => new WorkspacePropertiesAdapter(workspace),
|
||||||
|
[workspace]
|
||||||
|
);
|
||||||
|
return useReactiveAdapter(adapter);
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { useBlockSuitePageMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
|
||||||
import type { GetPageInfoById } from '@affine/env/page-info';
|
|
||||||
import type { Workspace } from '@blocksuite/store';
|
|
||||||
import { useAtomValue } from 'jotai';
|
|
||||||
import { useCallback, useMemo } from 'react';
|
|
||||||
|
|
||||||
import { pageSettingsAtom } from '../atoms';
|
|
||||||
|
|
||||||
export const useGetPageInfoById = (workspace: Workspace): GetPageInfoById => {
|
|
||||||
const pageMetas = useBlockSuitePageMeta(workspace);
|
|
||||||
const pageMap = useMemo(
|
|
||||||
() => Object.fromEntries(pageMetas.map(page => [page.id, page])),
|
|
||||||
[pageMetas]
|
|
||||||
);
|
|
||||||
const pageSettings = useAtomValue(pageSettingsAtom);
|
|
||||||
return useCallback(
|
|
||||||
(id: string) => {
|
|
||||||
const page = pageMap[id];
|
|
||||||
if (!page) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...page,
|
|
||||||
isEdgeless: pageSettings[id]?.mode === 'edgeless',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[pageMap, pageSettings]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,31 +1,34 @@
|
|||||||
import type { PageMeta } from '@blocksuite/store';
|
|
||||||
import { initEmptyPage } from '@toeverything/infra/blocksuite';
|
import { initEmptyPage } from '@toeverything/infra/blocksuite';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import type { BlockSuiteWorkspace } from '../shared';
|
import type { BlockSuiteWorkspace } from '../shared';
|
||||||
|
import { timestampToLocalDate } from '../utils';
|
||||||
|
import { useWorkspacePropertiesAdapter } from './use-affine-adapter';
|
||||||
import { useBlockSuiteWorkspaceHelper } from './use-block-suite-workspace-helper';
|
import { useBlockSuiteWorkspaceHelper } from './use-block-suite-workspace-helper';
|
||||||
import { useNavigateHelper } from './use-navigate-helper';
|
import { useNavigateHelper } from './use-navigate-helper';
|
||||||
|
|
||||||
type MaybeDate = Date | string | number;
|
type MaybeDate = Date | string | number;
|
||||||
export const JOURNAL_DATE_FORMAT = 'YYYY-MM-DD';
|
export const JOURNAL_DATE_FORMAT = 'YYYY-MM-DD';
|
||||||
|
|
||||||
function isPageJournal(pageMeta?: PageMeta) {
|
function isJournalString(j?: string | false) {
|
||||||
if (!runtimeConfig.enableJournal) return false;
|
if (!runtimeConfig.enableJournal) return false;
|
||||||
return !!(pageMeta && pageMeta.title.match(/^\d{4}-\d{2}-\d{2}$/));
|
return j ? !!j?.match(/^\d{4}-\d{2}-\d{2}$/) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getJournalDate(pageMeta?: PageMeta) {
|
function toDayjs(j?: string | false) {
|
||||||
if (!isPageJournal(pageMeta)) return null;
|
if (!j || !isJournalString(j)) return null;
|
||||||
if (!pageMeta?.title) return null;
|
const day = dayjs(j);
|
||||||
if (!dayjs(pageMeta.title).isValid()) return null;
|
if (!day.isValid()) return null;
|
||||||
return dayjs(pageMeta.title);
|
return day;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useJournalHelper = (workspace: BlockSuiteWorkspace) => {
|
export const useJournalHelper = (workspace: BlockSuiteWorkspace) => {
|
||||||
const bsWorkspaceHelper = useBlockSuiteWorkspaceHelper(workspace);
|
const bsWorkspaceHelper = useBlockSuiteWorkspaceHelper(workspace);
|
||||||
const navigateHelper = useNavigateHelper();
|
const navigateHelper = useNavigateHelper();
|
||||||
|
|
||||||
|
const adapter = useWorkspacePropertiesAdapter(workspace);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@@ -36,9 +39,17 @@ export const useJournalHelper = (workspace: BlockSuiteWorkspace) => {
|
|||||||
initEmptyPage(page, title).catch(err =>
|
initEmptyPage(page, title).catch(err =>
|
||||||
console.error('Failed to load journal page', err)
|
console.error('Failed to load journal page', err)
|
||||||
);
|
);
|
||||||
|
adapter.setJournalPageDateString(page.id, title);
|
||||||
return page;
|
return page;
|
||||||
},
|
},
|
||||||
[bsWorkspaceHelper]
|
[adapter, bsWorkspaceHelper]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isPageJournal = useCallback(
|
||||||
|
(pageId: string) => {
|
||||||
|
return !!adapter.getJournalPageDateString(pageId);
|
||||||
|
},
|
||||||
|
[adapter]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,14 +59,15 @@ export const useJournalHelper = (workspace: BlockSuiteWorkspace) => {
|
|||||||
(maybeDate: MaybeDate) => {
|
(maybeDate: MaybeDate) => {
|
||||||
const day = dayjs(maybeDate);
|
const day = dayjs(maybeDate);
|
||||||
return Array.from(workspace.pages.values()).filter(page => {
|
return Array.from(workspace.pages.values()).filter(page => {
|
||||||
if (!isPageJournal(page.meta)) return false;
|
const pageId = page.id;
|
||||||
|
if (!isPageJournal(pageId)) return false;
|
||||||
if (page.meta.trash) return false;
|
if (page.meta.trash) return false;
|
||||||
const journalDate = getJournalDate(page.meta);
|
const journalDate = adapter.getJournalPageDateString(page.id);
|
||||||
if (!journalDate) return false;
|
if (!journalDate) return false;
|
||||||
return day.isSame(journalDate, 'day');
|
return day.isSame(journalDate, 'day');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[workspace.pages]
|
[adapter, isPageJournal, workspace.pages]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,26 +101,59 @@ export const useJournalHelper = (workspace: BlockSuiteWorkspace) => {
|
|||||||
openJournal(date);
|
openJournal(date);
|
||||||
}, [openJournal]);
|
}, [openJournal]);
|
||||||
|
|
||||||
|
const getJournalDateString = useCallback(
|
||||||
|
(pageId: string) => {
|
||||||
|
return adapter.getJournalPageDateString(pageId);
|
||||||
|
},
|
||||||
|
[adapter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getLocalizedJournalDateString = useCallback(
|
||||||
|
(pageId: string) => {
|
||||||
|
const journalDateString = getJournalDateString(pageId);
|
||||||
|
if (!journalDateString) return null;
|
||||||
|
return timestampToLocalDate(journalDateString);
|
||||||
|
},
|
||||||
|
[getJournalDateString]
|
||||||
|
);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
getJournalsByDate,
|
getJournalsByDate,
|
||||||
getJournalByDate,
|
getJournalByDate,
|
||||||
|
getJournalDateString,
|
||||||
|
getLocalizedJournalDateString,
|
||||||
openJournal,
|
openJournal,
|
||||||
openToday,
|
openToday,
|
||||||
|
isPageJournal,
|
||||||
}),
|
}),
|
||||||
[getJournalByDate, getJournalsByDate, openJournal, openToday]
|
[
|
||||||
|
getJournalByDate,
|
||||||
|
getJournalDateString,
|
||||||
|
getJournalsByDate,
|
||||||
|
getLocalizedJournalDateString,
|
||||||
|
isPageJournal,
|
||||||
|
openJournal,
|
||||||
|
openToday,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useJournalInfoHelper = (pageMeta?: PageMeta) => {
|
export const useJournalInfoHelper = (
|
||||||
const isJournal = isPageJournal(pageMeta);
|
workspace: BlockSuiteWorkspace,
|
||||||
const journalDate = useMemo(() => getJournalDate(pageMeta), [pageMeta]);
|
pageId?: string | null
|
||||||
|
) => {
|
||||||
|
const { isPageJournal, getJournalDateString } = useJournalHelper(workspace);
|
||||||
|
const journalDate = useMemo(
|
||||||
|
() => (pageId ? toDayjs(getJournalDateString(pageId)) : null),
|
||||||
|
[getJournalDateString, pageId]
|
||||||
|
);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
isJournal,
|
isJournal: pageId ? isPageJournal(pageId) : false,
|
||||||
journalDate,
|
journalDate,
|
||||||
}),
|
}),
|
||||||
[isJournal, journalDate]
|
[isPageJournal, journalDate, pageId]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from './atoms';
|
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
|
||||||
|
>;
|
||||||
@@ -178,7 +178,7 @@ export function NormalPageHeader({
|
|||||||
|
|
||||||
export function DetailPageHeader(props: PageHeaderProps) {
|
export function DetailPageHeader(props: PageHeaderProps) {
|
||||||
const { page } = props;
|
const { page } = props;
|
||||||
const { isJournal } = useJournalInfoHelper(page.meta);
|
const { isJournal } = useJournalInfoHelper(page.workspace, page.id);
|
||||||
const isInTrash = page.meta.trash;
|
const isInTrash = page.meta.trash;
|
||||||
|
|
||||||
return isJournal && !isInTrash ? (
|
return isJournal && !isInTrash ? (
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ export const ExtensionTabs = ({ page }: ExtensionTabsProps) => {
|
|||||||
FeatureType.Copilot
|
FeatureType.Copilot
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isJournal } = useJournalInfoHelper(page.meta);
|
const { isJournal } = useJournalInfoHelper(page.workspace, page.id);
|
||||||
|
|
||||||
const exts = useAtomValue(editorExtensionsAtom).filter(ext => {
|
const exts = useAtomValue(editorExtensionsAtom).filter(ext => {
|
||||||
if (ext.name === 'copilot' && !copilotEnabled) return false;
|
if (ext.name === 'copilot' && !copilotEnabled) return false;
|
||||||
|
|||||||
+5
-2
@@ -43,7 +43,7 @@ interface PageItemProps extends HTMLAttributes<HTMLDivElement> {
|
|||||||
right?: ReactNode;
|
right?: ReactNode;
|
||||||
}
|
}
|
||||||
const PageItem = ({ page, right, className, ...attrs }: PageItemProps) => {
|
const PageItem = ({ page, right, className, ...attrs }: PageItemProps) => {
|
||||||
const { isJournal } = useJournalInfoHelper(page.meta);
|
const { isJournal } = useJournalInfoHelper(page.workspace, page.id);
|
||||||
|
|
||||||
const Icon = isJournal
|
const Icon = isJournal
|
||||||
? TodayIcon
|
? TodayIcon
|
||||||
@@ -77,7 +77,10 @@ interface JournalBlockProps extends EditorExtensionProps {
|
|||||||
|
|
||||||
const EditorJournalPanel = (props: EditorExtensionProps) => {
|
const EditorJournalPanel = (props: EditorExtensionProps) => {
|
||||||
const { workspace, page } = props;
|
const { workspace, page } = props;
|
||||||
const { journalDate, isJournal } = useJournalInfoHelper(page?.meta);
|
const { journalDate, isJournal } = useJournalInfoHelper(
|
||||||
|
page.workspace,
|
||||||
|
page.id
|
||||||
|
);
|
||||||
const { openJournal } = useJournalHelper(workspace);
|
const { openJournal } = useJournalHelper(workspace);
|
||||||
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
|
const [date, setDate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const Component = (): ReactElement => {
|
|||||||
setCurrentWorkspace(null);
|
setCurrentWorkspace(null);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
setCurrentWorkspace(workspace ?? null);
|
setCurrentWorkspace(workspace);
|
||||||
|
|
||||||
// for debug purpose
|
// for debug purpose
|
||||||
window.currentWorkspace = workspace;
|
window.currentWorkspace = workspace;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './create-emotion-cache';
|
export * from './create-emotion-cache';
|
||||||
|
export * from './intl-formatter';
|
||||||
export * from './string2color';
|
export * from './string2color';
|
||||||
export * from './toast';
|
export * from './toast';
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
const timeFormatter = new Intl.DateTimeFormat(undefined, {
|
||||||
|
timeStyle: 'short',
|
||||||
|
});
|
||||||
|
|
||||||
|
const dateFormatter = new Intl.DateTimeFormat(undefined, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const timestampToLocalTime = (ts: string) => {
|
||||||
|
return timeFormatter.format(new Date(ts));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const timestampToLocalDate = (ts: string) => {
|
||||||
|
return dateFormatter.format(new Date(ts));
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user