mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-22 04:21:40 +08:00
Merge branch 'canary' into stable
This commit is contained in:
-69
@@ -1,69 +0,0 @@
|
||||
import { isBrowser } from '@affine/env/constant';
|
||||
import type { UpdateMeta } from '@toeverything/infra/type';
|
||||
import { atomWithObservable, atomWithStorage } from 'jotai/utils';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
// todo: move to utils?
|
||||
function rpcToObservable<
|
||||
T,
|
||||
H extends () => Promise<T>,
|
||||
E extends (callback: (t: T) => void) => () => void,
|
||||
>(
|
||||
initialValue: T | null,
|
||||
{
|
||||
event,
|
||||
handler,
|
||||
onSubscribe,
|
||||
}: {
|
||||
event?: E;
|
||||
handler?: H;
|
||||
onSubscribe?: () => void;
|
||||
}
|
||||
): Observable<T | null> {
|
||||
return new Observable<T | null>(subscriber => {
|
||||
subscriber.next(initialValue);
|
||||
onSubscribe?.();
|
||||
if (!isBrowser || !environment.isDesktop || !event) {
|
||||
subscriber.complete();
|
||||
return;
|
||||
}
|
||||
handler?.()
|
||||
.then(t => {
|
||||
subscriber.next(t);
|
||||
})
|
||||
.catch(err => {
|
||||
subscriber.error(err);
|
||||
});
|
||||
return event(t => {
|
||||
subscriber.next(t);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const updateReadyAtom = atomWithObservable(() => {
|
||||
return rpcToObservable(null as UpdateMeta | null, {
|
||||
event: window.events?.updater.onUpdateReady,
|
||||
});
|
||||
});
|
||||
|
||||
export const updateAvailableAtom = atomWithObservable(() => {
|
||||
return rpcToObservable(null as UpdateMeta | null, {
|
||||
event: window.events?.updater.onUpdateAvailable,
|
||||
onSubscribe: () => {
|
||||
window.apis?.updater.checkForUpdatesAndNotify().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export const downloadProgressAtom = atomWithObservable(() => {
|
||||
return rpcToObservable(null as number | null, {
|
||||
event: window.events?.updater.onDownloadProgress,
|
||||
});
|
||||
});
|
||||
|
||||
export const changelogCheckedAtom = atomWithStorage<Record<string, boolean>>(
|
||||
'affine:client-changelog-checked',
|
||||
{}
|
||||
);
|
||||
+25
-45
@@ -1,18 +1,21 @@
|
||||
import { isBrowser, Unreachable } from '@affine/env/constant';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon, NewIcon, ResetIcon } from '@blocksuite/icons';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import clsx from 'clsx';
|
||||
import { atom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import { startTransition, useCallback, useState } from 'react';
|
||||
|
||||
import * as styles from './index.css';
|
||||
import {
|
||||
changelogCheckedAtom,
|
||||
currentChangelogUnreadAtom,
|
||||
currentVersionAtom,
|
||||
downloadProgressAtom,
|
||||
updateAvailableAtom,
|
||||
updateReadyAtom,
|
||||
} from './index.jotai';
|
||||
useAppUpdater,
|
||||
} from '@toeverything/hooks/use-app-updater';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { startTransition, useCallback } from 'react';
|
||||
|
||||
import * as styles from './index.css';
|
||||
|
||||
export interface AddPageButtonPureProps {
|
||||
onClickUpdate: () => void;
|
||||
@@ -29,26 +32,6 @@ export interface AddPageButtonPureProps {
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const currentVersionAtom = atom(async () => {
|
||||
if (!isBrowser) {
|
||||
return null;
|
||||
}
|
||||
const currentVersion = await window.apis?.updater.currentVersion();
|
||||
return currentVersion;
|
||||
});
|
||||
|
||||
const currentChangelogUnreadAtom = atom(async get => {
|
||||
if (!isBrowser) {
|
||||
return false;
|
||||
}
|
||||
const mapping = get(changelogCheckedAtom);
|
||||
const currentVersion = await get(currentVersionAtom);
|
||||
if (currentVersion) {
|
||||
return !mapping[currentVersion];
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
export function AppUpdaterButtonPure({
|
||||
updateReady,
|
||||
onClickUpdate,
|
||||
@@ -198,12 +181,12 @@ export function AppUpdaterButton({
|
||||
const currentChangelogUnread = useAtomValue(currentChangelogUnreadAtom);
|
||||
const updateReady = useAtomValue(updateReadyAtom);
|
||||
const updateAvailable = useAtomValue(updateAvailableAtom);
|
||||
const currentVersion = useAtomValue(currentVersionAtom);
|
||||
const downloadProgress = useAtomValue(downloadProgressAtom);
|
||||
const currentVersion = useAtomValue(currentVersionAtom);
|
||||
const { quitAndInstall, appQuitting } = useAppUpdater();
|
||||
const setChangelogCheckAtom = useSetAtom(changelogCheckedAtom);
|
||||
const [appQuitting, setAppQuitting] = useState(false);
|
||||
|
||||
const onDismissCurrentChangelog = useCallback(() => {
|
||||
const dismissCurrentChangelog = useCallback(() => {
|
||||
if (!currentVersion) {
|
||||
return;
|
||||
}
|
||||
@@ -216,13 +199,10 @@ export function AppUpdaterButton({
|
||||
})
|
||||
);
|
||||
}, [currentVersion, setChangelogCheckAtom]);
|
||||
const onClickUpdate = useCallback(() => {
|
||||
|
||||
const handleClickUpdate = useCallback(() => {
|
||||
if (updateReady) {
|
||||
setAppQuitting(true);
|
||||
window.apis?.updater.quitAndInstall().catch(err => {
|
||||
// TODO: add error toast here
|
||||
console.error(err);
|
||||
});
|
||||
quitAndInstall();
|
||||
} else if (updateAvailable) {
|
||||
if (updateAvailable.allowAutoUpdate) {
|
||||
// wait for download to finish
|
||||
@@ -234,23 +214,25 @@ export function AppUpdaterButton({
|
||||
}
|
||||
} else if (currentChangelogUnread) {
|
||||
window.open(runtimeConfig.changelogUrl, '_blank');
|
||||
onDismissCurrentChangelog();
|
||||
dismissCurrentChangelog();
|
||||
} else {
|
||||
throw new Unreachable();
|
||||
}
|
||||
}, [
|
||||
currentChangelogUnread,
|
||||
currentVersion,
|
||||
onDismissCurrentChangelog,
|
||||
updateAvailable,
|
||||
updateReady,
|
||||
quitAndInstall,
|
||||
updateAvailable,
|
||||
currentChangelogUnread,
|
||||
dismissCurrentChangelog,
|
||||
currentVersion,
|
||||
]);
|
||||
|
||||
return (
|
||||
<AppUpdaterButtonPure
|
||||
appQuitting={appQuitting}
|
||||
updateReady={!!updateReady}
|
||||
onClickUpdate={onClickUpdate}
|
||||
onDismissCurrentChangelog={onDismissCurrentChangelog}
|
||||
onClickUpdate={handleClickUpdate}
|
||||
onDismissCurrentChangelog={dismissCurrentChangelog}
|
||||
currentChangelogUnread={currentChangelogUnread}
|
||||
updateAvailable={updateAvailable}
|
||||
downloadProgress={downloadProgress}
|
||||
@@ -259,5 +241,3 @@ export function AppUpdaterButton({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export * from './index.jotai';
|
||||
|
||||
@@ -2,8 +2,10 @@ import { atom } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
|
||||
export const APP_SIDEBAR_OPEN = 'app-sidebar-open';
|
||||
export const appSidebarOpenAtom = atomWithStorage(APP_SIDEBAR_OPEN, true);
|
||||
export const appSidebarFloatingAtom = atom(false);
|
||||
export const isMobile = window.innerWidth < 768;
|
||||
|
||||
export const appSidebarOpenAtom = atomWithStorage(APP_SIDEBAR_OPEN, !isMobile);
|
||||
export const appSidebarFloatingAtom = atom(isMobile);
|
||||
|
||||
export const appSidebarResizingAtom = atom(false);
|
||||
export const appSidebarWidthAtom = atomWithStorage(
|
||||
|
||||
@@ -10,9 +10,9 @@ export const RootBlockHub = () => {
|
||||
const div = ref.current;
|
||||
if (blockHub) {
|
||||
if (div.hasChildNodes()) {
|
||||
div.removeChild(div.firstChild as ChildNode);
|
||||
(div.firstChild as ChildNode).remove();
|
||||
}
|
||||
div.appendChild(blockHub);
|
||||
div.append(blockHub);
|
||||
}
|
||||
}
|
||||
}, [blockHub]);
|
||||
|
||||
@@ -192,9 +192,9 @@ const BlockSuiteEditorImpl = ({
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
container.appendChild(editor);
|
||||
container.append(editor);
|
||||
return () => {
|
||||
container.removeChild(editor);
|
||||
editor.remove();
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@ import { typesystem } from './typesystem';
|
||||
type MatcherData<Data, Type extends TType = TType> = { type: Type; data: Data };
|
||||
|
||||
export class Matcher<Data, Type extends TType = TType> {
|
||||
private list: MatcherData<Data, Type>[] = [];
|
||||
private readonly list: MatcherData<Data, Type>[] = [];
|
||||
|
||||
constructor(private _match?: (type: Type, target: TType) => boolean) {}
|
||||
constructor(
|
||||
private readonly _match?: (type: Type, target: TType) => boolean
|
||||
) {}
|
||||
|
||||
register(type: Type, data: Data) {
|
||||
this.list.push({ type, data });
|
||||
|
||||
@@ -92,8 +92,8 @@ export type ValueOfData<T extends DataDefine> = T extends DataDefine<infer R>
|
||||
|
||||
export class DataDefine<Data extends DataTypeShape = Record<string, unknown>> {
|
||||
constructor(
|
||||
private config: DataDefineConfig<Data>,
|
||||
private dataMap: Map<string, DataDefine>
|
||||
private readonly config: DataDefineConfig<Data>,
|
||||
private readonly dataMap: Map<string, DataDefine>
|
||||
) {}
|
||||
|
||||
create(data?: Data): TDataType<Data> {
|
||||
@@ -195,7 +195,7 @@ export class Typesystem {
|
||||
): boolean {
|
||||
if (superType.type === 'typeRef') {
|
||||
// TODO both are ref
|
||||
if (context && sub.type != 'typeRef') {
|
||||
if (context && sub.type !== 'typeRef') {
|
||||
context[superType.name] = sub;
|
||||
}
|
||||
// TODO bound
|
||||
|
||||
@@ -141,7 +141,7 @@ filterMatcher.register(
|
||||
name: 'is',
|
||||
defaultArgs: () => [true],
|
||||
impl: (value, target) => {
|
||||
return value == target;
|
||||
return value === target;
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -209,7 +209,7 @@ filterMatcher.register(
|
||||
defaultArgs: () => [],
|
||||
impl: tags => {
|
||||
const safeTags = safeArray(tags);
|
||||
return safeTags.length == 0;
|
||||
return safeTags.length === 0;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Tag } from '@affine/env/filter';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { EdgelessIcon, PageIcon, ToggleCollapseIcon } from '@blocksuite/icons';
|
||||
@@ -19,93 +18,8 @@ import {
|
||||
useAtom,
|
||||
useAtomValue,
|
||||
} from './scoped-atoms';
|
||||
import type {
|
||||
PageGroupDefinition,
|
||||
PageGroupProps,
|
||||
PageListItemProps,
|
||||
PageListProps,
|
||||
} from './types';
|
||||
import { type DateKey } from './types';
|
||||
import { betweenDaysAgo, shallowEqual, withinDaysAgo } from './utils';
|
||||
|
||||
// todo: optimize date matchers
|
||||
const getDateGroupDefinitions = (key: DateKey): PageGroupDefinition[] => [
|
||||
{
|
||||
id: 'today',
|
||||
label: <Trans i18nKey="com.affine.today" />,
|
||||
match: item => withinDaysAgo(new Date(item[key] ?? item.createDate), 1),
|
||||
},
|
||||
{
|
||||
id: 'yesterday',
|
||||
label: <Trans i18nKey="com.affine.yesterday" />,
|
||||
match: item => betweenDaysAgo(new Date(item[key] ?? item.createDate), 1, 2),
|
||||
},
|
||||
{
|
||||
id: 'last7Days',
|
||||
label: <Trans i18nKey="com.affine.last7Days" />,
|
||||
match: item => betweenDaysAgo(new Date(item[key] ?? item.createDate), 2, 7),
|
||||
},
|
||||
{
|
||||
id: 'last30Days',
|
||||
label: <Trans i18nKey="com.affine.last30Days" />,
|
||||
match: item =>
|
||||
betweenDaysAgo(new Date(item[key] ?? item.createDate), 7, 30),
|
||||
},
|
||||
{
|
||||
id: 'moreThan30Days',
|
||||
label: <Trans i18nKey="com.affine.moreThan30Days" />,
|
||||
match: item => !withinDaysAgo(new Date(item[key] ?? item.createDate), 30),
|
||||
},
|
||||
];
|
||||
|
||||
const pageGroupDefinitions = {
|
||||
createDate: getDateGroupDefinitions('createDate'),
|
||||
updatedDate: getDateGroupDefinitions('updatedDate'),
|
||||
// add more here later
|
||||
// todo: some page group definitions maybe dynamic
|
||||
};
|
||||
|
||||
export function pagesToPageGroups(
|
||||
pages: PageMeta[],
|
||||
key?: DateKey
|
||||
): PageGroupProps[] {
|
||||
if (!key) {
|
||||
return [
|
||||
{
|
||||
id: 'all',
|
||||
items: pages,
|
||||
allItems: pages,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// assume pages are already sorted, we will use the page order to determine the group order
|
||||
const groupDefs = pageGroupDefinitions[key];
|
||||
const groups: PageGroupProps[] = [];
|
||||
|
||||
for (const page of pages) {
|
||||
// for a single page, there could be multiple groups that it belongs to
|
||||
const matchedGroups = groupDefs.filter(def => def.match(page));
|
||||
for (const groupDef of matchedGroups) {
|
||||
const group = groups.find(g => g.id === groupDef.id);
|
||||
if (group) {
|
||||
group.items.push(page);
|
||||
} else {
|
||||
const label =
|
||||
typeof groupDef.label === 'function'
|
||||
? groupDef.label()
|
||||
: groupDef.label;
|
||||
groups.push({
|
||||
id: groupDef.id,
|
||||
label: label,
|
||||
items: [page],
|
||||
allItems: pages,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
import type { PageGroupProps, PageListItemProps, PageListProps } from './types';
|
||||
import { shallowEqual } from './utils';
|
||||
|
||||
export const PageGroupHeader = ({ id, items, label }: PageGroupProps) => {
|
||||
const [collapseState, setCollapseState] = useAtom(pageGroupCollapseStateAtom);
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Trans } from '@affine/i18n';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
|
||||
import type { PageGroupDefinition, PageGroupProps } from './types';
|
||||
import { type DateKey } from './types';
|
||||
import { betweenDaysAgo, withinDaysAgo } from './utils';
|
||||
|
||||
// todo: optimize date matchers
|
||||
const getDateGroupDefinitions = (key: DateKey): PageGroupDefinition[] => [
|
||||
{
|
||||
id: 'today',
|
||||
label: <Trans i18nKey="com.affine.today" />,
|
||||
match: item => withinDaysAgo(new Date(item[key] ?? item.createDate), 1),
|
||||
},
|
||||
{
|
||||
id: 'yesterday',
|
||||
label: <Trans i18nKey="com.affine.yesterday" />,
|
||||
match: item => betweenDaysAgo(new Date(item[key] ?? item.createDate), 1, 2),
|
||||
},
|
||||
{
|
||||
id: 'last7Days',
|
||||
label: <Trans i18nKey="com.affine.last7Days" />,
|
||||
match: item => betweenDaysAgo(new Date(item[key] ?? item.createDate), 2, 7),
|
||||
},
|
||||
{
|
||||
id: 'last30Days',
|
||||
label: <Trans i18nKey="com.affine.last30Days" />,
|
||||
match: item =>
|
||||
betweenDaysAgo(new Date(item[key] ?? item.createDate), 7, 30),
|
||||
},
|
||||
{
|
||||
id: 'moreThan30Days',
|
||||
label: <Trans i18nKey="com.affine.moreThan30Days" />,
|
||||
match: item => !withinDaysAgo(new Date(item[key] ?? item.createDate), 30),
|
||||
},
|
||||
];
|
||||
|
||||
const pageGroupDefinitions = {
|
||||
createDate: getDateGroupDefinitions('createDate'),
|
||||
updatedDate: getDateGroupDefinitions('updatedDate'),
|
||||
// add more here later
|
||||
// todo: some page group definitions maybe dynamic
|
||||
};
|
||||
|
||||
export function pagesToPageGroups(
|
||||
pages: PageMeta[],
|
||||
key?: DateKey
|
||||
): PageGroupProps[] {
|
||||
if (!key) {
|
||||
return [
|
||||
{
|
||||
id: 'all',
|
||||
items: pages,
|
||||
allItems: pages,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// assume pages are already sorted, we will use the page order to determine the group order
|
||||
const groupDefs = pageGroupDefinitions[key];
|
||||
const groups: PageGroupProps[] = [];
|
||||
|
||||
for (const page of pages) {
|
||||
// for a single page, there could be multiple groups that it belongs to
|
||||
const matchedGroups = groupDefs.filter(def => def.match(page));
|
||||
for (const groupDef of matchedGroups) {
|
||||
const group = groups.find(g => g.id === groupDef.id);
|
||||
if (group) {
|
||||
group.items.push(page);
|
||||
} else {
|
||||
const label =
|
||||
typeof groupDef.label === 'function'
|
||||
? groupDef.label()
|
||||
: groupDef.label;
|
||||
groups.push({
|
||||
id: groupDef.id,
|
||||
label: label,
|
||||
items: [page],
|
||||
allItems: pages,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { atom } from 'jotai';
|
||||
import { selectAtom } from 'jotai/utils';
|
||||
import { createIsolation } from 'jotai-scope';
|
||||
|
||||
import { pagesToPageGroups } from './page-group';
|
||||
import { pagesToPageGroups } from './pages-to-page-group';
|
||||
import type {
|
||||
PageListProps,
|
||||
PageMetaRecord,
|
||||
|
||||
@@ -10,9 +10,9 @@ import * as styles from './page-list.css';
|
||||
export function isToday(date: Date): boolean {
|
||||
const today = new Date();
|
||||
return (
|
||||
date.getDate() == today.getDate() &&
|
||||
date.getMonth() == today.getMonth() &&
|
||||
date.getFullYear() == today.getFullYear()
|
||||
date.getDate() === today.getDate() &&
|
||||
date.getMonth() === today.getMonth() &&
|
||||
date.getFullYear() === today.getFullYear()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -162,10 +162,10 @@ export function shallowEqual(objA: any, objB: any) {
|
||||
}
|
||||
|
||||
// Test for A's keys different from B.
|
||||
for (let i = 0; i < keysA.length; i++) {
|
||||
for (const key of keysA) {
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
|
||||
!Object.is(objA[keysA[i]], objB[keysA[i]])
|
||||
!Object.prototype.hasOwnProperty.call(objB, key) ||
|
||||
!Object.is(objA[key], objB[key])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
} from '@affine/env/filter';
|
||||
import type { PropertiesMeta } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { FilteredIcon } from '@blocksuite/icons';
|
||||
import { FilterIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Menu } from '@toeverything/components/menu';
|
||||
import { useCallback, useState } from 'react';
|
||||
@@ -75,7 +75,7 @@ export const CollectionList = ({
|
||||
<Button
|
||||
className={styles.filterMenuTrigger}
|
||||
type="default"
|
||||
icon={<FilteredIcon />}
|
||||
icon={<FilterIcon />}
|
||||
data-testid="create-first-filter"
|
||||
>
|
||||
{t['com.affine.filter']()}
|
||||
@@ -99,7 +99,7 @@ export const CollectionList = ({
|
||||
<Button
|
||||
className={styles.filterMenuTrigger}
|
||||
type="default"
|
||||
icon={<FilteredIcon />}
|
||||
icon={<FilterIcon />}
|
||||
data-testid="create-first-filter"
|
||||
>
|
||||
{t['com.affine.filter']()}
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import {
|
||||
createEmptyCollection,
|
||||
useEditCollectionName,
|
||||
} from '@affine/component/page-list';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { SaveIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Modal } from '@toeverything/components/modal';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import Input from '../../../ui/input';
|
||||
@@ -127,40 +120,3 @@ export const CreateCollection = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface SaveAsCollectionButtonProps {
|
||||
onConfirm: (collection: Collection) => Promise<void>;
|
||||
}
|
||||
|
||||
export const SaveAsCollectionButton = ({
|
||||
onConfirm,
|
||||
}: SaveAsCollectionButtonProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { open, node } = useEditCollectionName({
|
||||
title: t['com.affine.editCollection.saveCollection'](),
|
||||
showTips: true,
|
||||
});
|
||||
const handleClick = useCallback(() => {
|
||||
open('')
|
||||
.then(name => {
|
||||
return onConfirm(createEmptyCollection(nanoid(), { name }));
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [open, onConfirm]);
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
data-testid="save-as-collection"
|
||||
icon={<SaveIcon />}
|
||||
size="large"
|
||||
style={{ padding: '7px 8px' }}
|
||||
>
|
||||
{t['com.affine.editCollection.saveCollection']()}
|
||||
</Button>
|
||||
{node}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { Button } from '@toeverything/components/button';
|
||||
import { Modal } from '@toeverything/components/modal';
|
||||
import { type ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { RadioButton, RadioButtonGroup } from '../../../../index';
|
||||
import { RadioButton, RadioButtonGroup } from '../../../../ui/button';
|
||||
import * as styles from './edit-collection.css';
|
||||
import { PagesMode } from './pages-mode';
|
||||
import { RulesMode } from './rules-mode';
|
||||
|
||||
+2
-51
@@ -1,12 +1,7 @@
|
||||
import {
|
||||
type AllPageListConfig,
|
||||
filterPageByRules,
|
||||
} from '@affine/component/page-list';
|
||||
import type { Filter } from '@affine/env/filter';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import { Modal } from '@toeverything/components/modal';
|
||||
import { type MouseEvent, useCallback, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import type { AllPageListConfig } from './edit-collection';
|
||||
import { SelectPage } from './select-page';
|
||||
export const useSelectPage = ({
|
||||
allPageListConfig,
|
||||
@@ -60,47 +55,3 @@ export const useSelectPage = ({
|
||||
}),
|
||||
};
|
||||
};
|
||||
export const useFilter = (list: PageMeta[]) => {
|
||||
const [filters, changeFilters] = useState<Filter[]>([]);
|
||||
const [showFilter, setShowFilter] = useState(false);
|
||||
const clickFilter = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (showFilter || filters.length !== 0) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowFilter(!showFilter);
|
||||
}
|
||||
},
|
||||
[filters.length, showFilter]
|
||||
);
|
||||
const onCreateFilter = useCallback(
|
||||
(filter: Filter) => {
|
||||
changeFilters([...filters, filter]);
|
||||
setShowFilter(true);
|
||||
},
|
||||
[filters]
|
||||
);
|
||||
return {
|
||||
showFilter,
|
||||
filters,
|
||||
updateFilters: changeFilters,
|
||||
clickFilter,
|
||||
createFilter: onCreateFilter,
|
||||
filteredList: list.filter(v => {
|
||||
if (v.trash) {
|
||||
return false;
|
||||
}
|
||||
return filterPageByRules(filters, [], v);
|
||||
}),
|
||||
};
|
||||
};
|
||||
export const useSearch = (list: PageMeta[]) => {
|
||||
const [value, onChange] = useState('');
|
||||
return {
|
||||
searchText: value,
|
||||
updateSearchText: onChange,
|
||||
searchedList: value
|
||||
? list.filter(v => v.title.toLowerCase().includes(value.toLowerCase()))
|
||||
: list,
|
||||
};
|
||||
};
|
||||
|
||||
+5
-6
@@ -1,8 +1,3 @@
|
||||
import {
|
||||
type AllPageListConfig,
|
||||
FilterList,
|
||||
VirtualizedPageList,
|
||||
} from '@affine/component/page-list';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { FilterIcon } from '@blocksuite/icons';
|
||||
@@ -11,10 +6,14 @@ import { Menu } from '@toeverything/components/menu';
|
||||
import clsx from 'clsx';
|
||||
import { type ReactNode, useCallback } from 'react';
|
||||
|
||||
import { FilterList } from '../../filter/filter-list';
|
||||
import { VariableSelect } from '../../filter/vars';
|
||||
import { VirtualizedPageList } from '../../virtualized-page-list';
|
||||
import type { AllPageListConfig } from './edit-collection';
|
||||
import * as styles from './edit-collection.css';
|
||||
import { useFilter, useSearch } from './hooks';
|
||||
import { EmptyList } from './select-page';
|
||||
import { useFilter } from './use-filter';
|
||||
import { useSearch } from './use-search';
|
||||
|
||||
export const PagesMode = ({
|
||||
switchMode,
|
||||
|
||||
+4
-2
@@ -6,13 +6,15 @@ import { Menu } from '@toeverything/components/menu';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { VirtualizedPageList } from '../..';
|
||||
import { FilterList } from '../../filter';
|
||||
import { VariableSelect } from '../../filter/vars';
|
||||
import { VirtualizedPageList } from '../../virtualized-page-list';
|
||||
import { AffineShapeIcon } from '../affine-shape';
|
||||
import type { AllPageListConfig } from './edit-collection';
|
||||
import * as styles from './edit-collection.css';
|
||||
import { useFilter, useSearch } from './hooks';
|
||||
import { useFilter } from './use-filter';
|
||||
import { useSearch } from './use-search';
|
||||
|
||||
export const SelectPage = ({
|
||||
allPageListConfig,
|
||||
init,
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import type { Filter } from '@affine/env/filter';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import { type MouseEvent, useCallback, useState } from 'react';
|
||||
|
||||
import { filterPageByRules } from '../../use-collection-manager';
|
||||
|
||||
export const useFilter = (list: PageMeta[]) => {
|
||||
const [filters, changeFilters] = useState<Filter[]>([]);
|
||||
const [showFilter, setShowFilter] = useState(false);
|
||||
const clickFilter = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (showFilter || filters.length !== 0) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowFilter(!showFilter);
|
||||
}
|
||||
},
|
||||
[filters.length, showFilter]
|
||||
);
|
||||
const onCreateFilter = useCallback(
|
||||
(filter: Filter) => {
|
||||
changeFilters([...filters, filter]);
|
||||
setShowFilter(true);
|
||||
},
|
||||
[filters]
|
||||
);
|
||||
return {
|
||||
showFilter,
|
||||
filters,
|
||||
updateFilters: changeFilters,
|
||||
clickFilter,
|
||||
createFilter: onCreateFilter,
|
||||
filteredList: list.filter(v => {
|
||||
if (v.trash) {
|
||||
return false;
|
||||
}
|
||||
return filterPageByRules(filters, [], v);
|
||||
}),
|
||||
};
|
||||
};
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const useSearch = (list: PageMeta[]) => {
|
||||
const [value, onChange] = useState('');
|
||||
return {
|
||||
searchText: value,
|
||||
updateSearchText: onChange,
|
||||
searchedList: value
|
||||
? list.filter(v => v.title.toLowerCase().includes(value.toLowerCase()))
|
||||
: list,
|
||||
};
|
||||
};
|
||||
@@ -4,4 +4,5 @@ export * from './collection-list';
|
||||
export * from './collection-operations';
|
||||
export * from './create-collection';
|
||||
export * from './edit-collection/edit-collection';
|
||||
export * from './save-as-collection-button';
|
||||
export * from './use-edit-collection';
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { SaveIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { createEmptyCollection } from '../use-collection-manager';
|
||||
import { useEditCollectionName } from './use-edit-collection';
|
||||
|
||||
interface SaveAsCollectionButtonProps {
|
||||
onConfirm: (collection: Collection) => Promise<void>;
|
||||
}
|
||||
|
||||
export const SaveAsCollectionButton = ({
|
||||
onConfirm,
|
||||
}: SaveAsCollectionButtonProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { open, node } = useEditCollectionName({
|
||||
title: t['com.affine.editCollection.saveCollection'](),
|
||||
showTips: true,
|
||||
});
|
||||
const handleClick = useCallback(() => {
|
||||
open('')
|
||||
.then(name => {
|
||||
return onConfirm(createEmptyCollection(nanoid(), { name }));
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [open, onConfirm]);
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
data-testid="save-as-collection"
|
||||
icon={<SaveIcon />}
|
||||
size="large"
|
||||
style={{ padding: '7px 8px' }}
|
||||
>
|
||||
{t['com.affine.editCollection.saveCollection']()}
|
||||
</Button>
|
||||
{node}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,12 +1,13 @@
|
||||
import {
|
||||
type AllPageListConfig,
|
||||
CreateCollectionModal,
|
||||
EditCollectionModal,
|
||||
type EditCollectionMode,
|
||||
} from '@affine/component/page-list';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { CreateCollectionModal } from './create-collection';
|
||||
import {
|
||||
type AllPageListConfig,
|
||||
EditCollectionModal,
|
||||
type EditCollectionMode,
|
||||
} from './edit-collection/edit-collection';
|
||||
|
||||
export const useEditCollection = (config: AllPageListConfig) => {
|
||||
const [data, setData] = useState<{
|
||||
collection: Collection;
|
||||
|
||||
+13
-11
@@ -8,17 +8,19 @@ export const WorkspaceDetailSkeleton = () => {
|
||||
<>
|
||||
<SettingHeader title={<Skeleton />} subtitle={<Skeleton />} />
|
||||
|
||||
{new Array(3).fill(0).map((_, index) => {
|
||||
return (
|
||||
<SettingWrapper title={<Skeleton />} key={index}>
|
||||
<SettingRow
|
||||
name={<Skeleton />}
|
||||
desc={<Skeleton />}
|
||||
spreadCol={false}
|
||||
></SettingRow>
|
||||
</SettingWrapper>
|
||||
);
|
||||
})}
|
||||
{Array.from({ length: 3 })
|
||||
.fill(0)
|
||||
.map((_, index) => {
|
||||
return (
|
||||
<SettingWrapper title={<Skeleton />} key={index}>
|
||||
<SettingRow
|
||||
name={<Skeleton />}
|
||||
desc={<Skeleton />}
|
||||
spreadCol={false}
|
||||
></SettingRow>
|
||||
</SettingWrapper>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+5
-3
@@ -26,9 +26,11 @@ export const WorkspaceListItemSkeleton = () => {
|
||||
export const WorkspaceListSkeleton = () => {
|
||||
return (
|
||||
<>
|
||||
{new Array(5).fill(0).map((_, index) => {
|
||||
return <WorkspaceListItemSkeleton key={index} />;
|
||||
})}
|
||||
{Array.from({ length: 5 })
|
||||
.fill(0)
|
||||
.map((_, index) => {
|
||||
return <WorkspaceListItemSkeleton key={index} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -61,6 +61,7 @@ export const dropdownIcon = style({
|
||||
|
||||
export const radioButton = style({
|
||||
flexGrow: 1,
|
||||
flex: 1,
|
||||
selectors: {
|
||||
'&:not(:last-of-type)': {
|
||||
marginRight: '4px',
|
||||
@@ -72,7 +73,8 @@ export const radioButtonContent = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '24px',
|
||||
height: '28px',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '8px',
|
||||
filter: 'drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.1))',
|
||||
whiteSpace: 'nowrap',
|
||||
|
||||
@@ -84,7 +84,7 @@ export const playCheckAnimation = async (refElement: Element) => {
|
||||
border-radius: 50%;
|
||||
font-size: inherit;
|
||||
`;
|
||||
refElement.appendChild(sparkingEl);
|
||||
refElement.append(sparkingEl);
|
||||
|
||||
await sparkingEl.animate(
|
||||
[
|
||||
|
||||
@@ -29,7 +29,7 @@ export const Skeleton = ({
|
||||
const style = {
|
||||
width,
|
||||
height,
|
||||
...(_style || {}),
|
||||
..._style,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -46,7 +46,7 @@ const createToastContainer = (portal?: HTMLElement) => {
|
||||
data-testid="affine-toast-container"
|
||||
></div>`;
|
||||
const element = htmlToElement<HTMLDivElement>(template);
|
||||
portal.appendChild(element);
|
||||
portal.append(element);
|
||||
return element;
|
||||
};
|
||||
|
||||
@@ -98,7 +98,7 @@ const createAndShowNewToast = (
|
||||
const toastElement = htmlToElement<HTMLDivElement>(toastTemplate);
|
||||
// message is not trusted
|
||||
toastElement.textContent = message;
|
||||
ToastContainer.appendChild(toastElement);
|
||||
ToastContainer.append(toastElement);
|
||||
logger.debug(`toast with message: "${message}"`);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('affine-toast:emit', { detail: message })
|
||||
|
||||
Reference in New Issue
Block a user