mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat: support for view management (#2892)
This commit is contained in:
@@ -57,7 +57,7 @@
|
||||
"@blocksuite/blocks": "0.0.0-20230629103121-76e6587d-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230629103121-76e6587d-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230629103121-76e6587d-nightly",
|
||||
"@blocksuite/icons": "^2.1.21",
|
||||
"@blocksuite/icons": "^2.1.23",
|
||||
"@blocksuite/lit": "0.0.0-20230629103121-76e6587d-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230629103121-76e6587d-nightly",
|
||||
"@types/react": "^18.2.14",
|
||||
|
||||
@@ -10,6 +10,7 @@ export const root = style({
|
||||
cursor: 'pointer',
|
||||
padding: '0 8px 0 12px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
margin: '2px 0',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
@@ -22,11 +23,12 @@ export const root = style({
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
'&[data-active="true"]:hover': {
|
||||
background:
|
||||
// make this a variable?
|
||||
'linear-gradient(0deg, rgba(0, 0, 0, 0.04), rgba(0, 0, 0, 0.04)), rgba(0, 0, 0, 0.04);',
|
||||
},
|
||||
// this is not visible in dark mode
|
||||
// '&[data-active="true"]:hover': {
|
||||
// background:
|
||||
// // make this a variable?
|
||||
// 'linear-gradient(0deg, rgba(0, 0, 0, 0.04), rgba(0, 0, 0, 0.04)), rgba(0, 0, 0, 0.04)',
|
||||
// },
|
||||
'&[data-collapsible="true"]': {
|
||||
width: 'calc(100% + 8px)',
|
||||
transform: 'translateX(-8px)',
|
||||
@@ -39,6 +41,7 @@ export const content = style({
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
export const icon = style({
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface MenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
disabled?: boolean;
|
||||
collapsed?: boolean; // true, false, undefined. undefined means no collapse
|
||||
onCollapsedChange?: (collapsed: boolean) => void;
|
||||
postfix?: React.ReactElement;
|
||||
}
|
||||
|
||||
export interface MenuLinkItemProps
|
||||
@@ -28,6 +29,7 @@ export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
|
||||
disabled,
|
||||
collapsed,
|
||||
onCollapsedChange,
|
||||
postfix,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -43,7 +45,6 @@ export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={clsx([styles.root, props.className])}
|
||||
onClick={onClick}
|
||||
data-active={active}
|
||||
data-disabled={disabled}
|
||||
data-collapsible={collapsible}
|
||||
@@ -68,11 +69,15 @@ export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
|
||||
)}
|
||||
{React.cloneElement(icon, {
|
||||
className: clsx([styles.icon, icon.props.className]),
|
||||
onClick: onClick,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.content}>{children}</div>
|
||||
<div onClick={onClick} className={styles.content}>
|
||||
{children}
|
||||
</div>
|
||||
{postfix}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { RESET } from 'jotai/utils';
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import { createDefaultFilter, vars } from '../filter/vars';
|
||||
@@ -12,22 +11,22 @@ import { useAllPageSetting } from '../use-all-page-setting';
|
||||
|
||||
test('useAllPageSetting', async () => {
|
||||
const settingHook = renderHook(() => useAllPageSetting());
|
||||
const prevView = settingHook.result.current.currentView;
|
||||
expect(settingHook.result.current.savedViews).toEqual([]);
|
||||
settingHook.result.current.setCurrentView(view => ({
|
||||
...view,
|
||||
const prevCollection = settingHook.result.current.currentCollection;
|
||||
expect(settingHook.result.current.savedCollections).toEqual([]);
|
||||
await settingHook.result.current.updateCollection({
|
||||
...settingHook.result.current.currentCollection,
|
||||
filterList: [createDefaultFilter(vars[0])],
|
||||
}));
|
||||
});
|
||||
settingHook.rerender();
|
||||
const nextView = settingHook.result.current.currentView;
|
||||
expect(nextView).not.toBe(prevView);
|
||||
expect(nextView.filterList).toEqual([createDefaultFilter(vars[0])]);
|
||||
settingHook.result.current.setCurrentView(RESET);
|
||||
await settingHook.result.current.createView({
|
||||
...settingHook.result.current.currentView,
|
||||
const nextCollection = settingHook.result.current.currentCollection;
|
||||
expect(nextCollection).not.toBe(prevCollection);
|
||||
expect(nextCollection.filterList).toEqual([createDefaultFilter(vars[0])]);
|
||||
settingHook.result.current.backToAll();
|
||||
await settingHook.result.current.saveCollection({
|
||||
...settingHook.result.current.currentCollection,
|
||||
id: '1',
|
||||
});
|
||||
settingHook.rerender();
|
||||
expect(settingHook.result.current.savedViews.length).toBe(1);
|
||||
expect(settingHook.result.current.savedViews[0].id).toBe('1');
|
||||
expect(settingHook.result.current.savedCollections.length).toBe(1);
|
||||
expect(settingHook.result.current.savedCollections[0].id).toBe('1');
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { CollectionBar } from '@affine/component/page-list';
|
||||
import { DEFAULT_SORT_KEY } from '@affine/env/constant';
|
||||
import type { GetPageInfoById } from '@affine/env/page-info';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowDownBigIcon, ArrowUpBigIcon } from '@blocksuite/icons';
|
||||
import { useMediaQuery, useTheme } from '@mui/material';
|
||||
@@ -31,12 +33,14 @@ const AllPagesHead = ({
|
||||
createNewPage,
|
||||
createNewEdgeless,
|
||||
importFile,
|
||||
getPageInfo,
|
||||
}: {
|
||||
isPublicWorkspace: boolean;
|
||||
sorter: ReturnType<typeof useSorter<ListData>>;
|
||||
createNewPage: () => void;
|
||||
createNewEdgeless: () => void;
|
||||
importFile: () => void;
|
||||
getPageInfo: GetPageInfoById;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const titleList = [
|
||||
@@ -72,7 +76,6 @@ const AllPagesHead = ({
|
||||
} satisfies CSSProperties,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<TableHead>
|
||||
<TableHeadRow>
|
||||
@@ -107,6 +110,7 @@ const AllPagesHead = ({
|
||||
</TableCell>
|
||||
))}
|
||||
</TableHeadRow>
|
||||
<CollectionBar getPageInfo={getPageInfo} />
|
||||
</TableHead>
|
||||
);
|
||||
};
|
||||
@@ -118,6 +122,7 @@ export const PageList = ({
|
||||
onCreateNewEdgeless,
|
||||
onImportFile,
|
||||
fallback,
|
||||
getPageInfo,
|
||||
}: PageListProps) => {
|
||||
const sorter = useSorter<ListData>({
|
||||
data: list,
|
||||
@@ -160,6 +165,7 @@ export const PageList = ({
|
||||
createNewPage={onCreateNewPage}
|
||||
createNewEdgeless={onCreateNewEdgeless}
|
||||
importFile={onImportFile}
|
||||
getPageInfo={getPageInfo}
|
||||
/>
|
||||
<AllPagesBody
|
||||
isPublicWorkspace={isPublicWorkspace}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Menu } from '../../..';
|
||||
import { Condition } from './condition';
|
||||
import * as styles from './index.css';
|
||||
import { CreateFilterMenu } from './vars';
|
||||
|
||||
export const FilterList = ({
|
||||
value,
|
||||
onChange,
|
||||
@@ -13,7 +14,13 @@ export const FilterList = ({
|
||||
onChange: (value: Filter[]) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
{value.map((filter, i) => {
|
||||
return (
|
||||
<div className={styles.filterItemStyle} key={i}>
|
||||
|
||||
@@ -28,7 +28,6 @@ export const filterItemStyle = style({
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
borderRadius: '8px',
|
||||
background: 'var(--affine-white)',
|
||||
margin: '4px',
|
||||
padding: '4px 8px',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { GetPageInfoById } from '@affine/env/page-info';
|
||||
|
||||
/**
|
||||
* Get the keys of an object type whose values are of a given type
|
||||
*
|
||||
@@ -45,6 +47,7 @@ export type PageListProps = {
|
||||
onCreateNewPage: () => void;
|
||||
onCreateNewEdgeless: () => void;
|
||||
onImportFile: () => void;
|
||||
getPageInfo: GetPageInfoById;
|
||||
};
|
||||
|
||||
export type DraggableTitleCellData = {
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import type { Filter, VariableMap, View } from '@affine/env/filter';
|
||||
import type { Collection, Filter, VariableMap } from '@affine/env/filter';
|
||||
import type { DBSchema } from 'idb';
|
||||
import { openDB } from 'idb';
|
||||
import type { IDBPDatabase } from 'idb/build/entry';
|
||||
import { useAtom } from 'jotai';
|
||||
import { atomWithReset } from 'jotai/utils';
|
||||
import { atomWithReset, RESET } from 'jotai/utils';
|
||||
import { useCallback } from 'react';
|
||||
import useSWRImmutable from 'swr/immutable';
|
||||
import { NIL } from 'uuid';
|
||||
|
||||
import { evalFilterList } from './filter';
|
||||
|
||||
type PersistenceView = View;
|
||||
type PersistenceCollection = Collection;
|
||||
|
||||
export interface PageViewDBV1 extends DBSchema {
|
||||
export interface PageCollectionDBV1 extends DBSchema {
|
||||
view: {
|
||||
key: PersistenceView['id'];
|
||||
value: PersistenceView;
|
||||
key: PersistenceCollection['id'];
|
||||
value: PersistenceCollection;
|
||||
};
|
||||
}
|
||||
|
||||
const pageViewDBPromise: Promise<IDBPDatabase<PageViewDBV1>> =
|
||||
const pageCollectionDBPromise: Promise<IDBPDatabase<PageCollectionDBV1>> =
|
||||
typeof window === 'undefined'
|
||||
? // never resolve in SSR
|
||||
new Promise<any>(() => {})
|
||||
: openDB<PageViewDBV1>('page-view', 1, {
|
||||
: openDB<PageCollectionDBV1>('page-view', 1, {
|
||||
upgrade(database) {
|
||||
database.createObjectStore('view', {
|
||||
keyPath: 'id',
|
||||
@@ -31,18 +31,24 @@ const pageViewDBPromise: Promise<IDBPDatabase<PageViewDBV1>> =
|
||||
},
|
||||
});
|
||||
|
||||
const currentViewAtom = atomWithReset<View>({
|
||||
name: 'default',
|
||||
id: NIL,
|
||||
filterList: [],
|
||||
const collectionAtom = atomWithReset<{
|
||||
currentId: string;
|
||||
defaultCollection: Collection;
|
||||
}>({
|
||||
currentId: NIL,
|
||||
defaultCollection: {
|
||||
id: NIL,
|
||||
name: 'All',
|
||||
filterList: [],
|
||||
},
|
||||
});
|
||||
|
||||
export const useAllPageSetting = () => {
|
||||
const { data: savedViews, mutate } = useSWRImmutable(
|
||||
['affine', 'page-view'],
|
||||
export const useSavedCollections = () => {
|
||||
const { data: savedCollections, mutate } = useSWRImmutable<Collection[]>(
|
||||
['affine', 'page-collection'],
|
||||
{
|
||||
fetcher: async () => {
|
||||
const db = await pageViewDBPromise;
|
||||
const db = await pageCollectionDBPromise;
|
||||
const t = db.transaction('view').objectStore('view');
|
||||
return await t.getAll();
|
||||
},
|
||||
@@ -51,29 +57,98 @@ export const useAllPageSetting = () => {
|
||||
revalidateOnMount: true,
|
||||
}
|
||||
);
|
||||
|
||||
const [currentView, setCurrentView] = useAtom(currentViewAtom);
|
||||
|
||||
const createView = useCallback(
|
||||
async (view: View) => {
|
||||
if (view.id === NIL) {
|
||||
const saveCollection = useCallback(
|
||||
async (collection: Collection) => {
|
||||
if (collection.id === NIL) {
|
||||
return;
|
||||
}
|
||||
const db = await pageViewDBPromise;
|
||||
const db = await pageCollectionDBPromise;
|
||||
const t = db.transaction('view', 'readwrite').objectStore('view');
|
||||
await t.put(view);
|
||||
await t.put(collection);
|
||||
await mutate();
|
||||
},
|
||||
[mutate]
|
||||
);
|
||||
|
||||
const deleteCollection = useCallback(
|
||||
async (id: string) => {
|
||||
if (id === NIL) {
|
||||
return;
|
||||
}
|
||||
const db = await pageCollectionDBPromise;
|
||||
const t = db.transaction('view', 'readwrite').objectStore('view');
|
||||
await t.delete(id);
|
||||
await mutate();
|
||||
},
|
||||
[mutate]
|
||||
);
|
||||
const addPage = useCallback(
|
||||
async (collectionId: string, pageId: string) => {
|
||||
const collection = savedCollections?.find(v => v.id === collectionId);
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
await saveCollection({
|
||||
...collection,
|
||||
allowList: [pageId, ...(collection.allowList ?? [])],
|
||||
});
|
||||
},
|
||||
[saveCollection, savedCollections]
|
||||
);
|
||||
return {
|
||||
currentView,
|
||||
savedViews: savedViews as View[],
|
||||
savedCollections: savedCollections ?? [],
|
||||
saveCollection,
|
||||
deleteCollection,
|
||||
addPage,
|
||||
};
|
||||
};
|
||||
|
||||
export const useAllPageSetting = () => {
|
||||
const { savedCollections, saveCollection, deleteCollection, addPage } =
|
||||
useSavedCollections();
|
||||
const [collectionData, setCollectionData] = useAtom(collectionAtom);
|
||||
|
||||
const updateCollection = useCallback(
|
||||
async (collection: Collection) => {
|
||||
if (collection.id === NIL) {
|
||||
setCollectionData({
|
||||
...collectionData,
|
||||
defaultCollection: collection,
|
||||
});
|
||||
} else {
|
||||
await saveCollection(collection);
|
||||
}
|
||||
},
|
||||
[collectionData, saveCollection, setCollectionData]
|
||||
);
|
||||
const selectCollection = useCallback(
|
||||
(id: string) => {
|
||||
setCollectionData({
|
||||
...collectionData,
|
||||
currentId: id,
|
||||
});
|
||||
},
|
||||
[collectionData, setCollectionData]
|
||||
);
|
||||
const backToAll = useCallback(() => {
|
||||
setCollectionData(RESET);
|
||||
}, [setCollectionData]);
|
||||
const currentCollection =
|
||||
collectionData.currentId === NIL
|
||||
? collectionData.defaultCollection
|
||||
: savedCollections.find(v => v.id === collectionData.currentId) ??
|
||||
collectionData.defaultCollection;
|
||||
return {
|
||||
currentCollection: currentCollection,
|
||||
savedCollections,
|
||||
isDefault: currentCollection.id === NIL,
|
||||
|
||||
// actions
|
||||
createView,
|
||||
setCurrentView,
|
||||
saveCollection,
|
||||
updateCollection,
|
||||
selectCollection,
|
||||
backToAll,
|
||||
deleteCollection,
|
||||
addPage,
|
||||
};
|
||||
};
|
||||
export const filterByFilterList = (filterList: Filter[], varMap: VariableMap) =>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const view = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
height: '100%',
|
||||
paddingLeft: 16,
|
||||
});
|
||||
|
||||
export const option = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 4,
|
||||
cursor: 'pointer',
|
||||
borderRadius: 4,
|
||||
':hover': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
opacity: 0,
|
||||
selectors: {
|
||||
[`${view}:hover &`]: {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const pin = style({
|
||||
opacity: 1,
|
||||
});
|
||||
export const pinedIcon = style({
|
||||
display: 'block',
|
||||
selectors: {
|
||||
[`${option}:hover &`]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const pinIcon = style({
|
||||
display: 'none',
|
||||
selectors: {
|
||||
[`${option}:hover &`]: {
|
||||
display: 'block',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
import { EditCollectionModel } from '@affine/component/page-list';
|
||||
import type { GetPageInfoById } from '@affine/env/page-info';
|
||||
import {
|
||||
DeleteIcon,
|
||||
FilterIcon,
|
||||
PinedIcon,
|
||||
PinIcon,
|
||||
UnpinIcon,
|
||||
ViewLayersIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import clsx from 'clsx';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { Button } from '../../../ui/button/button';
|
||||
import { useAllPageSetting } from '../use-all-page-setting';
|
||||
import * as styles from './collection-bar.css';
|
||||
|
||||
export const CollectionBar = ({
|
||||
getPageInfo,
|
||||
}: {
|
||||
getPageInfo: GetPageInfoById;
|
||||
}) => {
|
||||
const setting = useAllPageSetting();
|
||||
const collection = setting.currentCollection;
|
||||
const [open, setOpen] = useState(false);
|
||||
const actions: {
|
||||
icon: ReactNode;
|
||||
click: () => void;
|
||||
className?: string;
|
||||
name: string;
|
||||
}[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
icon: (
|
||||
<>
|
||||
{collection.pinned ? (
|
||||
<PinedIcon className={styles.pinedIcon}></PinedIcon>
|
||||
) : (
|
||||
<PinIcon className={styles.pinedIcon}></PinIcon>
|
||||
)}
|
||||
{collection.pinned ? (
|
||||
<UnpinIcon className={styles.pinIcon}></UnpinIcon>
|
||||
) : (
|
||||
<PinIcon className={styles.pinIcon}></PinIcon>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
name: 'pin',
|
||||
className: styles.pin,
|
||||
click: () => {
|
||||
return setting.updateCollection({
|
||||
...collection,
|
||||
pinned: !collection.pinned,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <FilterIcon />,
|
||||
name: 'edit',
|
||||
click: () => {
|
||||
setOpen(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <DeleteIcon style={{ color: 'red' }} />,
|
||||
name: 'delete',
|
||||
click: () => {
|
||||
setting.deleteCollection(collection.id).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
[setting, collection]
|
||||
);
|
||||
const onClose = useCallback(() => setOpen(false), []);
|
||||
return !setting.isDefault ? (
|
||||
<tr style={{ userSelect: 'none' }}>
|
||||
<td>
|
||||
<div className={styles.view}>
|
||||
<EditCollectionModel
|
||||
getPageInfo={getPageInfo}
|
||||
init={collection}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onConfirm={setting.updateCollection}
|
||||
></EditCollectionModel>
|
||||
<ViewLayersIcon
|
||||
style={{
|
||||
height: 20,
|
||||
width: 20,
|
||||
}}
|
||||
/>
|
||||
<div style={{ marginRight: 10 }}>
|
||||
{setting.currentCollection.name}
|
||||
</div>
|
||||
{actions.map(action => {
|
||||
return (
|
||||
<div
|
||||
key={action.name}
|
||||
data-testid={`collection-bar-option-${action.name}`}
|
||||
onClick={action.click}
|
||||
className={clsx(styles.option, action.className)}
|
||||
>
|
||||
{action.icon}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'end',
|
||||
}}
|
||||
>
|
||||
<Button style={{ border: 'none' }} onClick={() => setting.backToAll()}>
|
||||
Back to all
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
) : null;
|
||||
};
|
||||
@@ -0,0 +1,207 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const menuTitleStyle = style({
|
||||
marginLeft: '12px',
|
||||
marginTop: '10px',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
});
|
||||
export const menuDividerStyle = style({
|
||||
marginTop: '2px',
|
||||
marginBottom: '2px',
|
||||
marginLeft: '12px',
|
||||
marginRight: '8px',
|
||||
height: '1px',
|
||||
background: 'var(--affine-border-color)',
|
||||
});
|
||||
export const viewButton = style({
|
||||
borderRadius: '8px',
|
||||
height: '100%',
|
||||
padding: '4px 8px',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
background: 'var(--affine-white)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
transition: 'margin-left 0.2s ease-in-out',
|
||||
':hover': {
|
||||
borderColor: 'var(--affine-border-color)',
|
||||
background: 'var(--affine-hover-color)',
|
||||
},
|
||||
marginRight: '20px',
|
||||
});
|
||||
export const viewMenu = style({});
|
||||
export const viewOption = style({
|
||||
borderRadius: 8,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: 6,
|
||||
width: 24,
|
||||
height: 24,
|
||||
opacity: 0,
|
||||
':hover': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
selectors: {
|
||||
[`${viewMenu}:hover &`]: {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const deleteOption = style({
|
||||
':hover': {
|
||||
backgroundColor: '#FFEFE9',
|
||||
},
|
||||
});
|
||||
export const filterButton = style({
|
||||
borderRadius: '8px',
|
||||
height: '100%',
|
||||
padding: '4px 8px',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
background: 'var(--affine-white)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
transition: 'margin-left 0.2s ease-in-out',
|
||||
':hover': {
|
||||
borderColor: 'var(--affine-border-color)',
|
||||
background: 'var(--affine-hover-color)',
|
||||
},
|
||||
});
|
||||
export const filterButtonCollapse = style({
|
||||
marginLeft: '20px',
|
||||
});
|
||||
export const viewDivider = style({
|
||||
'::after': {
|
||||
content: '""',
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
background: 'var(--affine-border-color)',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
margin: '0 1px',
|
||||
},
|
||||
});
|
||||
export const saveButton = style({
|
||||
marginTop: '4px',
|
||||
borderRadius: '8px',
|
||||
padding: '8px 0',
|
||||
':hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
},
|
||||
});
|
||||
export const saveButtonContainer = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '8px',
|
||||
});
|
||||
export const saveIcon = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
marginRight: '8px',
|
||||
});
|
||||
export const saveText = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
});
|
||||
export const cancelButton = style({
|
||||
background: 'var(--affine-hover-color)',
|
||||
borderRadius: '8px',
|
||||
':hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
},
|
||||
});
|
||||
export const saveTitle = style({
|
||||
fontSize: 'var(--affine-font-h-6)',
|
||||
fontWeight: '600',
|
||||
lineHeight: '24px',
|
||||
paddingBottom: 20,
|
||||
});
|
||||
export const allowList = style({});
|
||||
|
||||
export const allowTitle = style({
|
||||
fontSize: 12,
|
||||
margin: '20px 0',
|
||||
});
|
||||
|
||||
export const allowListContent = style({
|
||||
margin: '8px 0',
|
||||
});
|
||||
|
||||
export const excludeList = style({
|
||||
backgroundColor: 'var(--affine-background-warning-color)',
|
||||
padding: 18,
|
||||
borderRadius: 8,
|
||||
});
|
||||
|
||||
export const excludeListContent = style({
|
||||
margin: '8px 0',
|
||||
});
|
||||
|
||||
export const filterTitle = style({
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
marginBottom: 10,
|
||||
});
|
||||
|
||||
export const excludeTitle = style({
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
});
|
||||
|
||||
export const excludeTip = style({
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontSize: 12,
|
||||
});
|
||||
|
||||
export const scrollContainer = style({
|
||||
overflow: 'hidden',
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
export const container = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
export const pageContainer = style({
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
height: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 8,
|
||||
paddingRight: 5,
|
||||
});
|
||||
|
||||
export const pageIcon = style({
|
||||
marginRight: 20,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const pageTitle = style({
|
||||
flex: 1,
|
||||
});
|
||||
export const deleteIcon = style({
|
||||
marginLeft: 20,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
padding: 4,
|
||||
cursor: 'pointer',
|
||||
':hover': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,232 @@
|
||||
import { EditCollectionModel } from '@affine/component/page-list';
|
||||
import type { Collection, Filter } from '@affine/env/filter';
|
||||
import type { GetPageInfoById } from '@affine/env/page-info';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
DeleteIcon,
|
||||
FilteredIcon,
|
||||
FilterIcon,
|
||||
FolderIcon,
|
||||
PinIcon,
|
||||
ViewLayersIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom } from 'jotai';
|
||||
import type { MouseEvent, ReactNode } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { Button, MenuItem } from '../../..';
|
||||
import Menu from '../../../ui/menu/menu';
|
||||
import { appSidebarOpenAtom } from '../../app-sidebar';
|
||||
import { CreateFilterMenu } from '../filter/vars';
|
||||
import type { useAllPageSetting } from '../use-all-page-setting';
|
||||
import * as styles from './collection-list.css';
|
||||
|
||||
const CollectionOption = ({
|
||||
collection,
|
||||
setting,
|
||||
updateCollection,
|
||||
}: {
|
||||
collection: Collection;
|
||||
setting: ReturnType<typeof useAllPageSetting>;
|
||||
updateCollection: (view: Collection) => void;
|
||||
}) => {
|
||||
const actions: {
|
||||
icon: ReactNode;
|
||||
click: () => void;
|
||||
className?: string;
|
||||
name: string;
|
||||
}[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
icon: <PinIcon />,
|
||||
name: 'pin',
|
||||
click: () => {
|
||||
return setting.updateCollection({
|
||||
...collection,
|
||||
pinned: !collection.pinned,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <FilterIcon />,
|
||||
name: 'edit',
|
||||
click: () => {
|
||||
updateCollection(collection);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <DeleteIcon style={{ color: 'red' }} />,
|
||||
name: 'delete',
|
||||
click: () => {
|
||||
setting.deleteCollection(collection.id).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
[setting, updateCollection, collection]
|
||||
);
|
||||
const selectCollection = useCallback(
|
||||
() => setting.selectCollection(collection.id),
|
||||
[setting, collection.id]
|
||||
);
|
||||
return (
|
||||
<MenuItem
|
||||
data-testid="collection-select-option"
|
||||
icon={<ViewLayersIcon></ViewLayersIcon>}
|
||||
onClick={selectCollection}
|
||||
key={collection.id}
|
||||
className={styles.viewMenu}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<div>{collection.name}</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{actions.map((v, i) => {
|
||||
const onClick = (e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
v.click();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
data-testid={`collection-select-option-${v.name}`}
|
||||
key={i}
|
||||
onClick={onClick}
|
||||
style={{ marginLeft: i === 0 ? 28 : undefined }}
|
||||
className={clsx(styles.viewOption, v.className)}
|
||||
>
|
||||
{v.icon}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
export const CollectionList = ({
|
||||
setting,
|
||||
getPageInfo,
|
||||
}: {
|
||||
setting: ReturnType<typeof useAllPageSetting>;
|
||||
getPageInfo: GetPageInfoById;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [open] = useAtom(appSidebarOpenAtom);
|
||||
const [collection, setCollection] = useState<Collection>();
|
||||
const onChange = useCallback(
|
||||
(filterList: Filter[]) => {
|
||||
return setting.updateCollection({
|
||||
...setting.currentCollection,
|
||||
filterList,
|
||||
});
|
||||
},
|
||||
[setting]
|
||||
);
|
||||
const closeUpdateCollectionModal = useCallback(
|
||||
() => setCollection(undefined),
|
||||
[]
|
||||
);
|
||||
const onConfirm = useCallback(
|
||||
(view: Collection) => {
|
||||
return setting.updateCollection(view).then(() => {
|
||||
closeUpdateCollectionModal();
|
||||
});
|
||||
},
|
||||
[closeUpdateCollectionModal, setting]
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={clsx({
|
||||
[styles.filterButtonCollapse]: !open,
|
||||
})}
|
||||
style={{
|
||||
marginLeft: 4,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{setting.savedCollections.length > 0 && (
|
||||
<Menu
|
||||
trigger="click"
|
||||
content={
|
||||
<div style={{ minWidth: 150 }}>
|
||||
<MenuItem
|
||||
icon={<FolderIcon></FolderIcon>}
|
||||
onClick={setting.backToAll}
|
||||
className={styles.viewMenu}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<div>All</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
<div className={styles.menuTitleStyle}>Saved Collection</div>
|
||||
<div className={styles.menuDividerStyle}></div>
|
||||
{setting.savedCollections.map(view => (
|
||||
<CollectionOption
|
||||
key={view.id}
|
||||
collection={view}
|
||||
setting={setting}
|
||||
updateCollection={setCollection}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
className={clsx(styles.viewButton)}
|
||||
hoverColor="var(--affine-icon-color)"
|
||||
data-testid="collection-select"
|
||||
>
|
||||
{setting.currentCollection.name}
|
||||
</Button>
|
||||
</Menu>
|
||||
)}
|
||||
<Menu
|
||||
trigger="click"
|
||||
placement="bottom-start"
|
||||
content={
|
||||
<CreateFilterMenu
|
||||
value={setting.currentCollection.filterList}
|
||||
onChange={onChange}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
icon={<FilteredIcon />}
|
||||
className={clsx(styles.filterButton)}
|
||||
size="small"
|
||||
hoverColor="var(--affine-icon-color)"
|
||||
data-testid="create-first-filter"
|
||||
>
|
||||
{t['com.affine.filter']()}
|
||||
</Button>
|
||||
</Menu>
|
||||
<EditCollectionModel
|
||||
getPageInfo={getPageInfo}
|
||||
init={collection}
|
||||
open={!!collection}
|
||||
onClose={closeUpdateCollectionModal}
|
||||
onConfirm={onConfirm}
|
||||
></EditCollectionModel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,291 @@
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import type { GetPageInfoById } from '@affine/env/page-info';
|
||||
import {
|
||||
EdgelessIcon,
|
||||
PageIcon,
|
||||
RemoveIcon,
|
||||
SaveIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalCloseButton,
|
||||
ModalWrapper,
|
||||
ScrollableContainer,
|
||||
} from '../../..';
|
||||
import { FilterList } from '../filter';
|
||||
import * as styles from './collection-list.css';
|
||||
|
||||
type CreateCollectionProps = {
|
||||
title?: string;
|
||||
init: Collection;
|
||||
onConfirm: (collection: Collection) => void;
|
||||
onConfirmText?: string;
|
||||
getPageInfo: GetPageInfoById;
|
||||
};
|
||||
export const EditCollectionModel = ({
|
||||
init,
|
||||
onConfirm,
|
||||
open,
|
||||
onClose,
|
||||
getPageInfo,
|
||||
}: {
|
||||
init?: Collection;
|
||||
onConfirm: (view: Collection) => void;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
getPageInfo: GetPageInfoById;
|
||||
}) => {
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<ModalWrapper
|
||||
width={600}
|
||||
style={{
|
||||
padding: '40px',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
}}
|
||||
>
|
||||
<ModalCloseButton
|
||||
top={12}
|
||||
right={12}
|
||||
onClick={onClose}
|
||||
hoverColor="var(--affine-icon-color)"
|
||||
/>
|
||||
{init ? (
|
||||
<EditCollection
|
||||
title="Update Collection"
|
||||
onConfirmText="Save"
|
||||
init={init}
|
||||
getPageInfo={getPageInfo}
|
||||
onCancel={onClose}
|
||||
onConfirm={view => {
|
||||
onConfirm(view);
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const Page = ({
|
||||
id,
|
||||
onClick,
|
||||
getPageInfo,
|
||||
}: {
|
||||
id: string;
|
||||
onClick: (id: string) => void;
|
||||
getPageInfo: GetPageInfoById;
|
||||
}) => {
|
||||
const page = getPageInfo(id);
|
||||
if (!page) {
|
||||
return null;
|
||||
}
|
||||
const icon = page.isEdgeless ? (
|
||||
<EdgelessIcon
|
||||
style={{
|
||||
width: 17.5,
|
||||
height: 17.5,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<PageIcon
|
||||
style={{
|
||||
width: 17.5,
|
||||
height: 17.5,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const click = () => {
|
||||
onClick(id);
|
||||
};
|
||||
return (
|
||||
<div className={styles.pageContainer}>
|
||||
<div className={styles.pageIcon}>{icon}</div>
|
||||
<div className={styles.pageTitle}>{page.title}</div>
|
||||
<div onClick={click} className={styles.deleteIcon}>
|
||||
<RemoveIcon />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const EditCollection = ({
|
||||
title,
|
||||
init,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
onConfirmText,
|
||||
getPageInfo,
|
||||
}: CreateCollectionProps & {
|
||||
onCancel: () => void;
|
||||
}) => {
|
||||
const [value, onChange] = useState<Collection>(init);
|
||||
const removeFromExcludeList = useCallback(
|
||||
(id: string) => {
|
||||
onChange({
|
||||
...value,
|
||||
excludeList: value.excludeList?.filter(v => v !== id),
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
const removeFromAllowList = useCallback(
|
||||
(id: string) => {
|
||||
onChange({
|
||||
...value,
|
||||
allowList: value.allowList?.filter(v => v !== id),
|
||||
});
|
||||
},
|
||||
[value]
|
||||
);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
maxHeight: '90vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<div className={styles.saveTitle}>
|
||||
{title ?? 'Save As New Collection'}
|
||||
</div>
|
||||
<ScrollableContainer
|
||||
className={styles.scrollContainer}
|
||||
viewPortClassName={styles.container}
|
||||
>
|
||||
<div className={styles.excludeList}>
|
||||
<div className={styles.excludeTitle}>
|
||||
Exclude from this collection
|
||||
</div>
|
||||
{value.excludeList ? (
|
||||
<div className={styles.excludeListContent}>
|
||||
{value.excludeList.map(id => {
|
||||
return (
|
||||
<Page
|
||||
id={id}
|
||||
getPageInfo={getPageInfo}
|
||||
key={id}
|
||||
onClick={removeFromExcludeList}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
<div className={styles.excludeTip}>
|
||||
These pages will never appear in the current collection
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
borderRadius: 8,
|
||||
padding: 18,
|
||||
marginTop: 20,
|
||||
}}
|
||||
>
|
||||
<div className={styles.filterTitle}>Filters</div>
|
||||
<FilterList
|
||||
value={value.filterList}
|
||||
onChange={list =>
|
||||
onChange({
|
||||
...value,
|
||||
filterList: list,
|
||||
})
|
||||
}
|
||||
></FilterList>
|
||||
{value.allowList ? (
|
||||
<div className={styles.allowList}>
|
||||
<div className={styles.allowTitle}>With follow pages:</div>
|
||||
<div className={styles.allowListContent}>
|
||||
{value.allowList.map(id => {
|
||||
return (
|
||||
<Page
|
||||
key={id}
|
||||
id={id}
|
||||
getPageInfo={getPageInfo}
|
||||
onClick={removeFromAllowList}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Input
|
||||
data-testid="input-collection-title"
|
||||
placeholder="Untitled Collection"
|
||||
value={value.name}
|
||||
onChange={text =>
|
||||
onChange({
|
||||
...value,
|
||||
name: text,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: 40,
|
||||
}}
|
||||
>
|
||||
<Button className={styles.cancelButton} onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style={{
|
||||
marginLeft: 20,
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
data-testid="save-collection"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
if (value.name.trim().length > 0) {
|
||||
onConfirm(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{onConfirmText ?? 'Create'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const SaveCollectionButton = ({
|
||||
init,
|
||||
onConfirm,
|
||||
getPageInfo,
|
||||
}: CreateCollectionProps) => {
|
||||
const [show, changeShow] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className={styles.saveButton}
|
||||
onClick={() => changeShow(true)}
|
||||
size="middle"
|
||||
data-testid="save-as-collection"
|
||||
>
|
||||
<div className={styles.saveButtonContainer}>
|
||||
<div className={styles.saveIcon}>
|
||||
<SaveIcon />
|
||||
</div>
|
||||
<div className={styles.saveText}>Save As Collection</div>
|
||||
</div>
|
||||
</Button>
|
||||
<EditCollectionModel
|
||||
init={init}
|
||||
onConfirm={onConfirm}
|
||||
open={show}
|
||||
getPageInfo={getPageInfo}
|
||||
onClose={() => changeShow(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,112 +0,0 @@
|
||||
import type { Filter, View } from '@affine/env/filter';
|
||||
import { SaveIcon } from '@blocksuite/icons';
|
||||
import { uuidv4 } from '@blocksuite/store';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Button, Input, Modal, ModalCloseButton, ModalWrapper } from '../../..';
|
||||
import { FilterList } from '../filter';
|
||||
import * as styles from './view-list.css';
|
||||
|
||||
type CreateViewProps = {
|
||||
init: Filter[];
|
||||
onConfirm: (view: View) => void;
|
||||
};
|
||||
|
||||
const CreateView = ({
|
||||
init,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: CreateViewProps & { onCancel: () => void }) => {
|
||||
const [value, onChange] = useState<View>({
|
||||
name: '',
|
||||
filterList: init,
|
||||
id: uuidv4(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.saveTitle}>Save As New View</div>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
borderRadius: 8,
|
||||
padding: 20,
|
||||
marginTop: 20,
|
||||
}}
|
||||
>
|
||||
<FilterList
|
||||
value={value.filterList}
|
||||
onChange={list => onChange({ ...value, filterList: list })}
|
||||
></FilterList>
|
||||
</div>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Input
|
||||
placeholder="Untitled View"
|
||||
value={value.name}
|
||||
onChange={text => onChange({ ...value, name: text })}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 40 }}
|
||||
>
|
||||
<Button className={styles.cancelButton} onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style={{ marginLeft: 20, borderRadius: '8px' }}
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
if (value.name.trim().length > 0) {
|
||||
onConfirm(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const SaveViewButton = ({ init, onConfirm }: CreateViewProps) => {
|
||||
const [show, changeShow] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className={styles.saveButton}
|
||||
onClick={() => changeShow(true)}
|
||||
size="middle"
|
||||
>
|
||||
<div className={styles.saveButtonContainer}>
|
||||
<div className={styles.saveIcon}>
|
||||
<SaveIcon />
|
||||
</div>
|
||||
<div className={styles.saveText}>Save View</div>
|
||||
</div>
|
||||
</Button>
|
||||
<Modal open={show} onClose={() => changeShow(false)}>
|
||||
<ModalWrapper
|
||||
width={560}
|
||||
style={{
|
||||
padding: '40px',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
}}
|
||||
>
|
||||
<ModalCloseButton
|
||||
top={12}
|
||||
right={12}
|
||||
onClick={() => changeShow(false)}
|
||||
hoverColor="var(--affine-icon-color)"
|
||||
/>
|
||||
<CreateView
|
||||
init={init}
|
||||
onCancel={() => changeShow(false)}
|
||||
onConfirm={view => {
|
||||
onConfirm(view);
|
||||
changeShow(false);
|
||||
}}
|
||||
/>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './create-view';
|
||||
export * from './view-list';
|
||||
export * from './collection-bar';
|
||||
export * from './collection-list';
|
||||
export * from './create-collection';
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const filterButton = style({
|
||||
borderRadius: '8px',
|
||||
height: '100%',
|
||||
padding: '4px 8px',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
background: 'var(--affine-white)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
transition: 'margin-left 0.2s ease-in-out',
|
||||
':hover': {
|
||||
borderColor: 'var(--affine-border-color)',
|
||||
background: 'var(--affine-hover-color)',
|
||||
},
|
||||
});
|
||||
export const filterButtonCollapse = style({
|
||||
marginLeft: '20px',
|
||||
});
|
||||
export const viewDivider = style({
|
||||
'::after': {
|
||||
content: '""',
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
background: 'var(--affine-border-color)',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
margin: '0 1px',
|
||||
},
|
||||
});
|
||||
export const saveButton = style({
|
||||
marginTop: '4px',
|
||||
borderRadius: '8px',
|
||||
padding: '8px 0',
|
||||
':hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
},
|
||||
});
|
||||
export const saveButtonContainer = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '8px',
|
||||
});
|
||||
export const saveIcon = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
marginRight: '8px',
|
||||
});
|
||||
export const saveText = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
});
|
||||
export const cancelButton = style({
|
||||
background: 'var(--affine-hover-color)',
|
||||
borderRadius: '8px',
|
||||
':hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
},
|
||||
});
|
||||
export const saveTitle = style({
|
||||
fontSize: 'var(--affine-font-h-6)',
|
||||
fontWeight: '600',
|
||||
lineHeight: '24px',
|
||||
});
|
||||
@@ -1,72 +0,0 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { FilteredIcon } from '@blocksuite/icons';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
import { Button, MenuItem } from '../../..';
|
||||
import Menu from '../../../ui/menu/menu';
|
||||
import { appSidebarOpenAtom } from '../../app-sidebar';
|
||||
import { CreateFilterMenu } from '../filter/vars';
|
||||
import type { useAllPageSetting } from '../use-all-page-setting';
|
||||
import * as styles from './view-list.css';
|
||||
export const ViewList = ({
|
||||
setting,
|
||||
}: {
|
||||
setting: ReturnType<typeof useAllPageSetting>;
|
||||
}) => {
|
||||
const [open] = useAtom(appSidebarOpenAtom);
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<div style={{ marginLeft: 4, display: 'flex', alignItems: 'center' }}>
|
||||
{setting.savedViews.length > 0 && (
|
||||
<Menu
|
||||
trigger="click"
|
||||
content={
|
||||
<div>
|
||||
{setting.savedViews.map(view => {
|
||||
return (
|
||||
<MenuItem
|
||||
onClick={() => setting.setCurrentView(view)}
|
||||
key={view.id}
|
||||
>
|
||||
{view.name}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button style={{ marginRight: 12, cursor: 'pointer' }}>
|
||||
{setting.currentView.name}
|
||||
</Button>
|
||||
</Menu>
|
||||
)}
|
||||
<Menu
|
||||
trigger="click"
|
||||
placement="bottom-start"
|
||||
content={
|
||||
<CreateFilterMenu
|
||||
value={setting.currentView.filterList}
|
||||
onChange={filterList => {
|
||||
setting.setCurrentView(view => ({
|
||||
...view,
|
||||
filterList,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
icon={<FilteredIcon />}
|
||||
className={clsx(styles.filterButton, {
|
||||
[styles.filterButtonCollapse]: !open,
|
||||
})}
|
||||
size="small"
|
||||
hoverColor="var(--affine-icon-color)"
|
||||
>
|
||||
{t['com.affine.filter']()}
|
||||
</Button>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -8,22 +8,28 @@ import * as styles from './index.css';
|
||||
export type ScrollableContainerProps = {
|
||||
showScrollTopBorder?: boolean;
|
||||
inTableView?: boolean;
|
||||
className?: string;
|
||||
viewPortClassName?: string;
|
||||
};
|
||||
|
||||
export const ScrollableContainer = ({
|
||||
children,
|
||||
showScrollTopBorder = false,
|
||||
inTableView = false,
|
||||
className,
|
||||
viewPortClassName,
|
||||
}: PropsWithChildren<ScrollableContainerProps>) => {
|
||||
const [hasScrollTop, ref] = useHasScrollTop();
|
||||
return (
|
||||
<ScrollArea.Root className={styles.scrollableContainerRoot}>
|
||||
<ScrollArea.Root
|
||||
className={clsx(styles.scrollableContainerRoot, className)}
|
||||
>
|
||||
<div
|
||||
data-has-scroll-top={hasScrollTop}
|
||||
className={clsx({ [styles.scrollTopBorder]: showScrollTopBorder })}
|
||||
/>
|
||||
<ScrollArea.Viewport
|
||||
className={clsx([styles.scrollableViewport])}
|
||||
className={clsx([styles.scrollableViewport, viewPortClassName])}
|
||||
ref={ref}
|
||||
>
|
||||
<div className={styles.scrollableContainer}>{children}</div>
|
||||
|
||||
5
packages/env/src/filter.ts
vendored
5
packages/env/src/filter.ts
vendored
@@ -25,8 +25,11 @@ export type Filter = {
|
||||
args: Literal[];
|
||||
};
|
||||
|
||||
export type View = {
|
||||
export type Collection = {
|
||||
id: string;
|
||||
name: string;
|
||||
pinned?: boolean;
|
||||
filterList: Filter[];
|
||||
allowList?: string[];
|
||||
excludeList?: string[];
|
||||
};
|
||||
|
||||
7
packages/env/src/page-info.ts
vendored
Normal file
7
packages/env/src/page-info.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export type PageInfo = {
|
||||
isEdgeless: boolean;
|
||||
title: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type GetPageInfoById = (id: string) => PageInfo | undefined;
|
||||
4
packages/env/src/workspace.ts
vendored
4
packages/env/src/workspace.ts
vendored
@@ -7,7 +7,7 @@ import type {
|
||||
} from '@blocksuite/store';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
|
||||
import type { View } from './filter';
|
||||
import type { Collection } from './filter';
|
||||
import type { Workspace as RemoteWorkspace } from './workspace/legacy-cloud';
|
||||
|
||||
export enum WorkspaceVersion {
|
||||
@@ -185,7 +185,7 @@ type PageDetailProps<Flavour extends keyof WorkspaceRegistry> =
|
||||
type PageListProps<_Flavour extends keyof WorkspaceRegistry> = {
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
onOpenPage: (pageId: string, newTab?: boolean) => void;
|
||||
view: View;
|
||||
collection: Collection;
|
||||
};
|
||||
|
||||
export interface WorkspaceUISchema<Flavour extends keyof WorkspaceRegistry> {
|
||||
|
||||
@@ -193,6 +193,7 @@
|
||||
"Check Our Docs": "Check Our Docs",
|
||||
"Get in touch! Join our communities": "Get in touch! Join our communities.",
|
||||
"Favorites": "Favourites",
|
||||
"Collections": "Collections",
|
||||
"Download data": "Download {{CoreOrAll}} data",
|
||||
"Back Home": "Back Home",
|
||||
"Set a Workspace name": "Set a Workspace name",
|
||||
|
||||
Reference in New Issue
Block a user