feat(core): support draft filter (#12400)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added support for draft mode and completion callbacks across filter components, enabling stepwise filter creation and editing. - Enhanced filter menus and editors with external control via refs and new callback props for open/close state management. - Introduced new filter option group component for multi-step filter interactions. - Expanded tag filter methods for more granular filtering options. - Enabled controlled open state and close event handling for desktop and mobile menus. - Added programmatic control and completion callbacks to member selector and tags inline editors. - **Improvements** - Updated filter and tag editors with improved UI layouts and added "Done" buttons for easier completion. - Improved menu and editor accessibility by allowing programmatic open/close and completion event handling. - Refactored date filter components for modularity and consistent draft handling. - Separated draft filter state management in filter UI for clearer user interactions. - **Bug Fixes** - Refined date filter logic for more accurate "after" and "before" comparisons. - **Style** - Adjusted styles for draft filters and editor layouts to enhance visual clarity and user experience. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React, { useCallback, useImperativeHandle, useState } from 'react';
|
||||||
|
|
||||||
import type { MenuProps } from '../menu.types';
|
import type { MenuProps } from '../menu.types';
|
||||||
import * as styles from '../styles.css';
|
import * as styles from '../styles.css';
|
||||||
@@ -11,18 +11,52 @@ export const DesktopMenu = ({
|
|||||||
items,
|
items,
|
||||||
noPortal,
|
noPortal,
|
||||||
portalOptions,
|
portalOptions,
|
||||||
rootOptions: { defaultOpen, modal, ...rootOptions } = {},
|
rootOptions: {
|
||||||
|
defaultOpen,
|
||||||
|
modal,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onClose,
|
||||||
|
...rootOptions
|
||||||
|
} = {},
|
||||||
contentOptions: {
|
contentOptions: {
|
||||||
className = '',
|
className = '',
|
||||||
style: contentStyle = {},
|
style: contentStyle = {},
|
||||||
...otherContentOptions
|
...otherContentOptions
|
||||||
} = {},
|
} = {},
|
||||||
|
ref,
|
||||||
}: MenuProps) => {
|
}: MenuProps) => {
|
||||||
|
const [innerOpen, setInnerOpen] = useState(defaultOpen);
|
||||||
|
const finalOpen = open ?? innerOpen;
|
||||||
|
|
||||||
|
const handleOpenChange = useCallback(
|
||||||
|
(open: boolean) => {
|
||||||
|
setInnerOpen(open);
|
||||||
|
onOpenChange?.(open);
|
||||||
|
if (!open) {
|
||||||
|
onClose?.();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onOpenChange, onClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
changeOpen: (open: boolean) => {
|
||||||
|
setInnerOpen(open);
|
||||||
|
onOpenChange?.(open);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[onOpenChange]
|
||||||
|
);
|
||||||
|
|
||||||
const ContentWrapper = noPortal ? React.Fragment : DropdownMenu.Portal;
|
const ContentWrapper = noPortal ? React.Fragment : DropdownMenu.Portal;
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Root
|
<DropdownMenu.Root
|
||||||
defaultOpen={defaultOpen}
|
|
||||||
modal={modal ?? false}
|
modal={modal ?? false}
|
||||||
|
open={finalOpen}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
{...rootOptions}
|
{...rootOptions}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
|
|||||||
@@ -8,15 +8,20 @@ import type {
|
|||||||
} from '@radix-ui/react-dropdown-menu';
|
} from '@radix-ui/react-dropdown-menu';
|
||||||
import type { CSSProperties, ReactNode } from 'react';
|
import type { CSSProperties, ReactNode } from 'react';
|
||||||
|
|
||||||
|
export interface MenuRef {
|
||||||
|
changeOpen: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MenuProps {
|
export interface MenuProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
items: ReactNode;
|
items: ReactNode;
|
||||||
title?: string;
|
title?: string;
|
||||||
portalOptions?: Omit<DropdownMenuPortalProps, 'children'>;
|
portalOptions?: Omit<DropdownMenuPortalProps, 'children'>;
|
||||||
rootOptions?: Omit<DropdownMenuProps, 'children'>;
|
rootOptions?: Omit<DropdownMenuProps, 'children'> & { onClose?: () => void };
|
||||||
contentOptions?: Omit<DropdownMenuContentProps, 'children'>;
|
contentOptions?: Omit<DropdownMenuContentProps, 'children'>;
|
||||||
contentWrapperStyle?: CSSProperties;
|
contentWrapperStyle?: CSSProperties;
|
||||||
noPortal?: boolean;
|
noPortal?: boolean;
|
||||||
|
ref?: React.Ref<MenuRef>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuItemProps
|
export interface MenuItemProps
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import { useI18n } from '@affine/i18n';
|
|||||||
import { ArrowLeftSmallIcon } from '@blocksuite/icons/rc';
|
import { ArrowLeftSmallIcon } from '@blocksuite/icons/rc';
|
||||||
import { Slot } from '@radix-ui/react-slot';
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
import {
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import { observeResize } from '../../../utils';
|
import { observeResize } from '../../../utils';
|
||||||
import { Button } from '../../button';
|
import { Button } from '../../button';
|
||||||
@@ -34,6 +40,7 @@ export const MobileMenu = ({
|
|||||||
} = {},
|
} = {},
|
||||||
contentWrapperStyle,
|
contentWrapperStyle,
|
||||||
rootOptions,
|
rootOptions,
|
||||||
|
ref,
|
||||||
}: MenuProps) => {
|
}: MenuProps) => {
|
||||||
const [subMenus, setSubMenus] = useState<SubMenuContent[]>([]);
|
const [subMenus, setSubMenus] = useState<SubMenuContent[]>([]);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -82,10 +89,23 @@ export const MobileMenu = ({
|
|||||||
}
|
}
|
||||||
setOpen(open);
|
setOpen(open);
|
||||||
rootOptions?.onOpenChange?.(open);
|
rootOptions?.onOpenChange?.(open);
|
||||||
|
if (!open) {
|
||||||
|
rootOptions?.onClose?.();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[onInteractOutside, onPointerDownOutside, removeAllSubMenus, rootOptions]
|
[onInteractOutside, onPointerDownOutside, removeAllSubMenus, rootOptions]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
changeOpen: (open: boolean) => {
|
||||||
|
onOpenChange(open);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[onOpenChange]
|
||||||
|
);
|
||||||
|
|
||||||
const onItemClick = useCallback(
|
const onItemClick = useCallback(
|
||||||
(e: any) => {
|
(e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -3,22 +3,32 @@ import type { FilterParams } from '@affine/core/modules/collection-rules';
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
|
||||||
|
import { FilterOptionsGroup } from '../options';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
export const Condition = ({
|
export const Condition = ({
|
||||||
filter,
|
filter,
|
||||||
|
isDraft,
|
||||||
|
onDraftCompleted,
|
||||||
icon,
|
icon,
|
||||||
name,
|
name,
|
||||||
methods,
|
methods,
|
||||||
onChange,
|
onChange,
|
||||||
value,
|
value: Value,
|
||||||
}: {
|
}: {
|
||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
name: React.ReactNode;
|
name: React.ReactNode;
|
||||||
methods?: [string, React.ReactNode][];
|
methods?: [string, React.ReactNode][];
|
||||||
onChange?: (filter: FilterParams) => void;
|
onChange?: (filter: FilterParams) => void;
|
||||||
value?: React.ReactNode;
|
value?: React.ElementType<{
|
||||||
|
filter: FilterParams;
|
||||||
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
|
onChange?: (filter: FilterParams) => void;
|
||||||
|
}>;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -26,40 +36,68 @@ export const Condition = ({
|
|||||||
{icon && <div className={styles.filterTypeIconStyle}>{icon}</div>}
|
{icon && <div className={styles.filterTypeIconStyle}>{icon}</div>}
|
||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
{methods && (
|
<FilterOptionsGroup
|
||||||
<Menu
|
isDraft={isDraft}
|
||||||
items={methods.map(([method, name]) => (
|
onDraftCompleted={onDraftCompleted}
|
||||||
<MenuItem
|
items={[
|
||||||
onClick={() => {
|
methods &&
|
||||||
onChange?.({
|
(({ onDraftCompleted, menuRef }) => {
|
||||||
...filter,
|
return (
|
||||||
method,
|
<Menu
|
||||||
});
|
key={'method'}
|
||||||
}}
|
ref={menuRef}
|
||||||
selected={filter.method === method}
|
rootOptions={{
|
||||||
key={method}
|
onClose: onDraftCompleted,
|
||||||
>
|
}}
|
||||||
{name}
|
items={methods.map(([method, name]) => (
|
||||||
</MenuItem>
|
<MenuItem
|
||||||
))}
|
onClick={() => {
|
||||||
>
|
onChange?.({
|
||||||
<div
|
...filter,
|
||||||
className={clsx(styles.switchStyle, styles.ellipsisTextStyle)}
|
method,
|
||||||
data-testid="filter-method"
|
});
|
||||||
>
|
}}
|
||||||
{methods.find(([method]) => method === filter.method)?.[1] ??
|
selected={filter.method === method}
|
||||||
'unknown'}
|
key={method}
|
||||||
</div>
|
>
|
||||||
</Menu>
|
{name}
|
||||||
)}
|
</MenuItem>
|
||||||
{value && (
|
))}
|
||||||
<div
|
>
|
||||||
className={clsx(styles.filterValueStyle, styles.ellipsisTextStyle)}
|
<div
|
||||||
data-testid="filter-method"
|
className={clsx(
|
||||||
>
|
styles.switchStyle,
|
||||||
{value}
|
styles.ellipsisTextStyle
|
||||||
</div>
|
)}
|
||||||
)}
|
data-testid="filter-method"
|
||||||
|
>
|
||||||
|
{methods.find(
|
||||||
|
([method]) => method === filter.method
|
||||||
|
)?.[1] ?? 'unknown'}
|
||||||
|
</div>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
Value &&
|
||||||
|
(({ isDraft, onDraftCompleted }) => (
|
||||||
|
<div
|
||||||
|
key={'value'}
|
||||||
|
className={clsx(
|
||||||
|
styles.filterValueStyle,
|
||||||
|
styles.ellipsisTextStyle
|
||||||
|
)}
|
||||||
|
data-testid="filter-method"
|
||||||
|
>
|
||||||
|
<Value
|
||||||
|
filter={filter}
|
||||||
|
isDraft={isDraft}
|
||||||
|
onDraftCompleted={onDraftCompleted}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,9 +13,13 @@ import { UnknownFilterCondition } from './unknown';
|
|||||||
|
|
||||||
export const PropertyFilterCondition = ({
|
export const PropertyFilterCondition = ({
|
||||||
filter,
|
filter,
|
||||||
|
isDraft,
|
||||||
|
onDraftCompleted,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
onChange: (filter: FilterParams) => void;
|
onChange: (filter: FilterParams) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
@@ -34,19 +38,27 @@ export const PropertyFilterCondition = ({
|
|||||||
const Value = type?.filterValue;
|
const Value = type?.filterValue;
|
||||||
|
|
||||||
if (!propertyInfo || !type || !methods) {
|
if (!propertyInfo || !type || !methods) {
|
||||||
return <UnknownFilterCondition filter={filter} />;
|
return (
|
||||||
|
<UnknownFilterCondition
|
||||||
|
isDraft={isDraft}
|
||||||
|
onDraftCompleted={onDraftCompleted}
|
||||||
|
filter={filter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Condition
|
<Condition
|
||||||
filter={filter}
|
filter={filter}
|
||||||
|
isDraft={isDraft}
|
||||||
|
onDraftCompleted={onDraftCompleted}
|
||||||
icon={<WorkspacePropertyIcon propertyInfo={propertyInfo} />}
|
icon={<WorkspacePropertyIcon propertyInfo={propertyInfo} />}
|
||||||
name={<WorkspacePropertyName propertyInfo={propertyInfo} />}
|
name={<WorkspacePropertyName propertyInfo={propertyInfo} />}
|
||||||
methods={Object.entries(methods).map(([key, i18nKey]) => [
|
methods={Object.entries(methods).map(([key, i18nKey]) => [
|
||||||
key,
|
key,
|
||||||
t.t(i18nKey as string),
|
t.t(i18nKey as string),
|
||||||
])}
|
])}
|
||||||
value={Value && <Value filter={filter} onChange={onChange} />}
|
value={Value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ import { UnknownFilterCondition } from './unknown';
|
|||||||
|
|
||||||
export const SystemFilterCondition = ({
|
export const SystemFilterCondition = ({
|
||||||
filter,
|
filter,
|
||||||
|
isDraft,
|
||||||
|
onDraftCompleted,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
onChange: (filter: FilterParams) => void;
|
onChange: (filter: FilterParams) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
@@ -21,7 +25,13 @@ export const SystemFilterCondition = ({
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (!type) {
|
if (!type) {
|
||||||
return <UnknownFilterCondition filter={filter} />;
|
return (
|
||||||
|
<UnknownFilterCondition
|
||||||
|
isDraft={isDraft}
|
||||||
|
onDraftCompleted={onDraftCompleted}
|
||||||
|
filter={filter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const methods = type.filterMethod;
|
const methods = type.filterMethod;
|
||||||
@@ -31,12 +41,14 @@ export const SystemFilterCondition = ({
|
|||||||
<Condition
|
<Condition
|
||||||
filter={filter}
|
filter={filter}
|
||||||
icon={<type.icon />}
|
icon={<type.icon />}
|
||||||
|
isDraft={isDraft}
|
||||||
|
onDraftCompleted={onDraftCompleted}
|
||||||
name={t.t(type.name)}
|
name={t.t(type.name)}
|
||||||
methods={Object.entries(methods).map(([key, i18nKey]) => [
|
methods={Object.entries(methods).map(([key, i18nKey]) => [
|
||||||
key,
|
key,
|
||||||
t.t(i18nKey as string),
|
t.t(i18nKey as string),
|
||||||
])}
|
])}
|
||||||
value={Value && <Value filter={filter} onChange={onChange} />}
|
value={Value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
import { WarningIcon } from '@blocksuite/icons/rc';
|
import { WarningIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { Condition } from './condition';
|
import { Condition } from './condition';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
export const UnknownFilterCondition = ({
|
export const UnknownFilterCondition = ({
|
||||||
filter,
|
filter,
|
||||||
|
isDraft,
|
||||||
|
onDraftCompleted,
|
||||||
}: {
|
}: {
|
||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDraft) {
|
||||||
|
// should not reach here
|
||||||
|
onDraftCompleted?.();
|
||||||
|
}
|
||||||
|
}, [isDraft, onDraftCompleted]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Condition
|
<Condition
|
||||||
filter={filter}
|
filter={filter}
|
||||||
|
|||||||
@@ -7,20 +7,39 @@ import * as styles from './styles.css';
|
|||||||
|
|
||||||
export const Filter = ({
|
export const Filter = ({
|
||||||
filter,
|
filter,
|
||||||
|
isDraft,
|
||||||
|
onDraftCompleted,
|
||||||
onDelete,
|
onDelete,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onChange: (filter: FilterParams) => void;
|
onChange: (filter: FilterParams) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const type = filter.type;
|
const type = filter.type;
|
||||||
|
|
||||||
|
const Condition =
|
||||||
|
type === 'property'
|
||||||
|
? PropertyFilterCondition
|
||||||
|
: type === 'system'
|
||||||
|
? SystemFilterCondition
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.filterItemStyle}>
|
<div
|
||||||
{type === 'property' ? (
|
className={styles.filterItemStyle}
|
||||||
<PropertyFilterCondition filter={filter} onChange={onChange} />
|
data-draft={isDraft}
|
||||||
) : type === 'system' ? (
|
data-type={type}
|
||||||
<SystemFilterCondition filter={filter} onChange={onChange} />
|
>
|
||||||
|
{Condition ? (
|
||||||
|
<Condition
|
||||||
|
isDraft={isDraft}
|
||||||
|
filter={filter}
|
||||||
|
onChange={onChange}
|
||||||
|
onDraftCompleted={onDraftCompleted}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<div className={styles.filterItemCloseStyle} onClick={onDelete}>
|
<div className={styles.filterItemCloseStyle} onClick={onDelete}>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { AddFilter } from './add-filter';
|
import { AddFilter } from './add-filter';
|
||||||
import { Filter } from './filter';
|
import { Filter } from './filter';
|
||||||
@@ -9,11 +10,25 @@ export const Filters = ({
|
|||||||
filters,
|
filters,
|
||||||
className,
|
className,
|
||||||
onChange,
|
onChange,
|
||||||
|
defaultDraftFilter,
|
||||||
}: {
|
}: {
|
||||||
filters: FilterParams[];
|
filters: FilterParams[];
|
||||||
className?: string;
|
className?: string;
|
||||||
onChange?: (filters: FilterParams[]) => void;
|
onChange?: (filters: FilterParams[]) => void;
|
||||||
|
defaultDraftFilter?: FilterParams | null;
|
||||||
}) => {
|
}) => {
|
||||||
|
const [draftFilter, setDraftFilter] = useState<FilterParams | null>(
|
||||||
|
defaultDraftFilter ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
// When draftChange and draftCompleted are triggered consecutively,
|
||||||
|
// we might save an outdated draft filter value.
|
||||||
|
// Using a ref helps us avoid this issue by always accessing the latest value.
|
||||||
|
const draftFilterRef = useRef<FilterParams | null>(draftFilter);
|
||||||
|
useEffect(() => {
|
||||||
|
draftFilterRef.current = draftFilter;
|
||||||
|
}, [draftFilter]);
|
||||||
|
|
||||||
const handleDelete = (index: number) => {
|
const handleDelete = (index: number) => {
|
||||||
onChange?.(filters.filter((_, i) => i !== index));
|
onChange?.(filters.filter((_, i) => i !== index));
|
||||||
};
|
};
|
||||||
@@ -22,6 +37,20 @@ export const Filters = ({
|
|||||||
onChange?.(filters.map((f, i) => (i === index ? filter : f)));
|
onChange?.(filters.map((f, i) => (i === index ? filter : f)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDraftCompleted = useCallback(() => {
|
||||||
|
if (draftFilterRef.current) {
|
||||||
|
onChange?.(filters.concat(draftFilterRef.current));
|
||||||
|
setDraftFilter(null);
|
||||||
|
}
|
||||||
|
}, [onChange, filters]);
|
||||||
|
|
||||||
|
const handleAdd = useCallback((filter: FilterParams) => {
|
||||||
|
// Add a small delay to ensure the previous menu is closed before opening the next one
|
||||||
|
setTimeout(() => {
|
||||||
|
setDraftFilter(filter);
|
||||||
|
}, 50);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx(styles.container, className)}>
|
<div className={clsx(styles.container, className)}>
|
||||||
{filters.map((filter, index) => {
|
{filters.map((filter, index) => {
|
||||||
@@ -39,11 +68,21 @@ export const Filters = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<AddFilter
|
{draftFilter && (
|
||||||
onAdd={filter => {
|
<Filter
|
||||||
onChange?.(filters.concat(filter));
|
filter={draftFilter}
|
||||||
}}
|
isDraft
|
||||||
/>
|
onDelete={() => {
|
||||||
|
setDraftFilter(null);
|
||||||
|
}}
|
||||||
|
onChange={filter => {
|
||||||
|
setDraftFilter(filter);
|
||||||
|
}}
|
||||||
|
onDraftCompleted={handleDraftCompleted}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AddFilter onAdd={handleAdd} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
75
packages/frontend/core/src/components/filter/options.tsx
Normal file
75
packages/frontend/core/src/components/filter/options.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { type MenuRef } from '@affine/component';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
type FilterOptionsGroupChildren =
|
||||||
|
| React.ReactNode
|
||||||
|
| ((args: {
|
||||||
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
|
menuRef: React.Ref<MenuRef>;
|
||||||
|
}) => React.ReactNode);
|
||||||
|
|
||||||
|
export const FilterOptionsGroup = ({
|
||||||
|
isDraft,
|
||||||
|
onDraftCompleted,
|
||||||
|
items,
|
||||||
|
}: {
|
||||||
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
|
items?: FilterOptionsGroupChildren[];
|
||||||
|
}) => {
|
||||||
|
const stepCount =
|
||||||
|
items?.filter(v => {
|
||||||
|
if (typeof v === 'function') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}).length ?? 0;
|
||||||
|
|
||||||
|
const childRefs = useRef<(MenuRef | null)[]>([]);
|
||||||
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
|
|
||||||
|
const handleNextStep = useCallback(() => {
|
||||||
|
// Add a small delay between steps to prevent the next menu from automatically closing due to the previous menu's close event
|
||||||
|
setTimeout(() => {
|
||||||
|
if (currentStep < stepCount - 1) {
|
||||||
|
setCurrentStep(currentStep + 1);
|
||||||
|
} else {
|
||||||
|
onDraftCompleted?.();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}, [currentStep, stepCount, onDraftCompleted]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDraft) {
|
||||||
|
childRefs.current[currentStep]?.changeOpen(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}, [isDraft, currentStep]);
|
||||||
|
|
||||||
|
let renderStep = 0;
|
||||||
|
return items?.map(child => {
|
||||||
|
if (typeof child === 'function') {
|
||||||
|
const currentRenderStep = renderStep;
|
||||||
|
renderStep++;
|
||||||
|
const childIsDraft = isDraft
|
||||||
|
? currentRenderStep === currentStep
|
||||||
|
: undefined;
|
||||||
|
return child({
|
||||||
|
isDraft: childIsDraft,
|
||||||
|
onDraftCompleted: () => {
|
||||||
|
if (childIsDraft) {
|
||||||
|
handleNextStep();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
menuRef: (ref: MenuRef) => {
|
||||||
|
childRefs.current[currentRenderStep] = ref;
|
||||||
|
return () => {
|
||||||
|
childRefs.current[currentRenderStep] = null;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -20,6 +20,11 @@ export const filterItemStyle = style({
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
selectors: {
|
||||||
|
'&[data-draft="true"]': {
|
||||||
|
borderStyle: 'dashed',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const filterItemCloseStyle = style({
|
export const filterItemCloseStyle = style({
|
||||||
|
|||||||
@@ -1,13 +1,29 @@
|
|||||||
import { Avatar, Divider, Menu, RowInput, Scrollable } from '@affine/component';
|
import {
|
||||||
|
Avatar,
|
||||||
|
Divider,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
type MenuRef,
|
||||||
|
RowInput,
|
||||||
|
Scrollable,
|
||||||
|
} from '@affine/component';
|
||||||
import {
|
import {
|
||||||
type Member,
|
type Member,
|
||||||
MemberSearchService,
|
MemberSearchService,
|
||||||
} from '@affine/core/modules/permissions';
|
} from '@affine/core/modules/permissions';
|
||||||
|
import { DoneIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { clamp, debounce } from 'lodash-es';
|
import { clamp, debounce } from 'lodash-es';
|
||||||
import type { KeyboardEvent, ReactNode } from 'react';
|
import type { KeyboardEvent, ReactNode } from 'react';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import { ConfigModal } from '../mobile';
|
import { ConfigModal } from '../mobile';
|
||||||
import { InlineMemberList } from './inline-member-list';
|
import { InlineMemberList } from './inline-member-list';
|
||||||
@@ -26,6 +42,8 @@ export interface MemberSelectorInlineProps extends MemberSelectorProps {
|
|||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
title?: ReactNode; // only used for mobile
|
title?: ReactNode; // only used for mobile
|
||||||
placeholder?: ReactNode;
|
placeholder?: ReactNode;
|
||||||
|
ref?: React.Ref<MenuRef>;
|
||||||
|
onEditorClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MemberSelectItemProps {
|
interface MemberSelectItemProps {
|
||||||
@@ -219,9 +237,15 @@ export const MemberSelector = ({
|
|||||||
/>
|
/>
|
||||||
</InlineMemberList>
|
</InlineMemberList>
|
||||||
{BUILD_CONFIG.isMobileEdition ? null : (
|
{BUILD_CONFIG.isMobileEdition ? null : (
|
||||||
<Divider size="thinner" className={styles.memberDivider} />
|
<MenuItem
|
||||||
|
className={styles.memberSelectorDoneButton}
|
||||||
|
prefixIcon={<DoneIcon />}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{BUILD_CONFIG.isMobileEdition ? null : (
|
||||||
|
<Divider size="thinner" className={styles.memberDivider} />
|
||||||
|
)}
|
||||||
<div className={styles.memberSelectorBody}>
|
<div className={styles.memberSelectorBody}>
|
||||||
<Scrollable.Root>
|
<Scrollable.Root>
|
||||||
<Scrollable.Viewport
|
<Scrollable.Viewport
|
||||||
@@ -267,10 +291,25 @@ const MobileMemberSelectorInline = ({
|
|||||||
className,
|
className,
|
||||||
title,
|
title,
|
||||||
style,
|
style,
|
||||||
|
onEditorClose,
|
||||||
|
ref,
|
||||||
...props
|
...props
|
||||||
}: MemberSelectorInlineProps) => {
|
}: MemberSelectorInlineProps) => {
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
changeOpen: (open: boolean) => {
|
||||||
|
setEditing(open);
|
||||||
|
if (!open) {
|
||||||
|
onEditorClose?.();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[onEditorClose]
|
||||||
|
);
|
||||||
|
|
||||||
const empty = !props.selected || props.selected.length === 0;
|
const empty = !props.selected || props.selected.length === 0;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -278,7 +317,10 @@ const MobileMemberSelectorInline = ({
|
|||||||
title={title}
|
title={title}
|
||||||
open={editing}
|
open={editing}
|
||||||
onOpenChange={setEditing}
|
onOpenChange={setEditing}
|
||||||
onBack={() => setEditing(false)}
|
onBack={() => {
|
||||||
|
setEditing(false);
|
||||||
|
onEditorClose?.();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<MemberSelector {...props} />
|
<MemberSelector {...props} />
|
||||||
</ConfigModal>
|
</ConfigModal>
|
||||||
@@ -303,11 +345,14 @@ const DesktopMemberSelectorInline = ({
|
|||||||
menuClassName,
|
menuClassName,
|
||||||
style,
|
style,
|
||||||
selected,
|
selected,
|
||||||
|
ref,
|
||||||
|
onEditorClose,
|
||||||
...props
|
...props
|
||||||
}: MemberSelectorInlineProps) => {
|
}: MemberSelectorInlineProps) => {
|
||||||
const empty = !selected || selected.length === 0;
|
const empty = !selected || selected.length === 0;
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
|
ref={ref}
|
||||||
contentOptions={{
|
contentOptions={{
|
||||||
side: 'bottom',
|
side: 'bottom',
|
||||||
align: 'start',
|
align: 'start',
|
||||||
@@ -321,6 +366,7 @@ const DesktopMemberSelectorInline = ({
|
|||||||
rootOptions={{
|
rootOptions={{
|
||||||
open: readonly ? false : undefined,
|
open: readonly ? false : undefined,
|
||||||
modal: modalMenu,
|
modal: modalMenu,
|
||||||
|
onClose: onEditorClose,
|
||||||
}}
|
}}
|
||||||
items={<MemberSelector selected={selected} {...props} />}
|
items={<MemberSelector selected={selected} {...props} />}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -37,9 +37,15 @@ export const memberSelectorMenu = style({
|
|||||||
minWidth: 400,
|
minWidth: 400,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const memberSelectorDoneButton = style({
|
||||||
|
height: '32px',
|
||||||
|
width: '28px',
|
||||||
|
});
|
||||||
|
|
||||||
export const memberSelectorSelectedTags = style({
|
export const memberSelectorSelectedTags = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexWrap: 'wrap',
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'nowrap',
|
||||||
padding: '10px 12px 0px',
|
padding: '10px 12px 0px',
|
||||||
minHeight: 42,
|
minHeight: 42,
|
||||||
selectors: {
|
selectors: {
|
||||||
|
|||||||
@@ -1,20 +1,37 @@
|
|||||||
import { Menu, MenuItem } from '@affine/component';
|
import { Menu, MenuItem, type MenuRef } from '@affine/component';
|
||||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
export const FavoriteFilterValue = ({
|
export const FavoriteFilterValue = ({
|
||||||
filter,
|
filter,
|
||||||
|
isDraft,
|
||||||
|
onDraftCompleted,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
onChange: (filter: FilterParams) => void;
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
|
onChange?: (filter: FilterParams) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const menuRef = useRef<MenuRef>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDraft) {
|
||||||
|
menuRef.current?.changeOpen(true);
|
||||||
|
}
|
||||||
|
}, [isDraft]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
|
ref={menuRef}
|
||||||
|
rootOptions={{
|
||||||
|
onClose: onDraftCompleted,
|
||||||
|
}}
|
||||||
items={
|
items={
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange({
|
onChange?.({
|
||||||
...filter,
|
...filter,
|
||||||
value: 'true',
|
value: 'true',
|
||||||
});
|
});
|
||||||
@@ -25,7 +42,7 @@ export const FavoriteFilterValue = ({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange({
|
onChange?.({
|
||||||
...filter,
|
...filter,
|
||||||
value: 'false',
|
value: 'false',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ export const SystemPropertyTypes = {
|
|||||||
icon: TagIcon,
|
icon: TagIcon,
|
||||||
name: 'Tags',
|
name: 'Tags',
|
||||||
filterMethod: {
|
filterMethod: {
|
||||||
include: 'com.affine.filter.contains all',
|
'include-all': 'com.affine.filter.contains all',
|
||||||
|
'include-any-of': 'com.affine.filter.contains one of',
|
||||||
|
'not-include-all': 'com.affine.filter.does not contains all',
|
||||||
|
'not-include-any-of': 'com.affine.filter.does not contains one of',
|
||||||
'is-not-empty': 'com.affine.filter.is not empty',
|
'is-not-empty': 'com.affine.filter.is not empty',
|
||||||
'is-empty': 'com.affine.filter.is empty',
|
'is-empty': 'com.affine.filter.is empty',
|
||||||
},
|
},
|
||||||
@@ -129,7 +132,9 @@ export const SystemPropertyTypes = {
|
|||||||
filterMethod: { [key: string]: I18nString };
|
filterMethod: { [key: string]: I18nString };
|
||||||
filterValue: React.FC<{
|
filterValue: React.FC<{
|
||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
onChange: (filter: FilterParams) => void;
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
|
onChange?: (filter: FilterParams) => void;
|
||||||
}>;
|
}>;
|
||||||
defaultFilter?: Omit<FilterParams, 'type' | 'key'>;
|
defaultFilter?: Omit<FilterParams, 'type' | 'key'>;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,20 +1,37 @@
|
|||||||
import { Menu, MenuItem } from '@affine/component';
|
import { Menu, MenuItem, type MenuRef } from '@affine/component';
|
||||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
export const SharedFilterValue = ({
|
export const SharedFilterValue = ({
|
||||||
filter,
|
filter,
|
||||||
|
isDraft,
|
||||||
|
onDraftCompleted,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
onChange: (filter: FilterParams) => void;
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
|
onChange?: (filter: FilterParams) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const menuRef = useRef<MenuRef>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDraft) {
|
||||||
|
menuRef.current?.changeOpen(true);
|
||||||
|
}
|
||||||
|
}, [isDraft]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
|
ref={menuRef}
|
||||||
|
rootOptions={{
|
||||||
|
onClose: onDraftCompleted,
|
||||||
|
}}
|
||||||
items={
|
items={
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange({
|
onChange?.({
|
||||||
...filter,
|
...filter,
|
||||||
value: 'true',
|
value: 'true',
|
||||||
});
|
});
|
||||||
@@ -25,7 +42,7 @@ export const SharedFilterValue = ({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange({
|
onChange?.({
|
||||||
...filter,
|
...filter,
|
||||||
value: 'false',
|
value: 'false',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ export const tagsMenu = style({
|
|||||||
|
|
||||||
export const tagsEditorSelectedTags = style({
|
export const tagsEditorSelectedTags = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexWrap: 'wrap',
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'nowrap',
|
||||||
padding: '10px 12px 0px',
|
padding: '10px 12px 0px',
|
||||||
minHeight: 42,
|
minHeight: 42,
|
||||||
selectors: {
|
selectors: {
|
||||||
@@ -51,6 +52,11 @@ export const tagsEditorSelectedTags = style({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const tagsEditorDoneButton = style({
|
||||||
|
height: '32px',
|
||||||
|
width: '28px',
|
||||||
|
});
|
||||||
|
|
||||||
export const tagDivider = style({
|
export const tagDivider = style({
|
||||||
borderBottomColor: cssVarV2('tab/divider/divider'),
|
borderBottomColor: cssVarV2('tab/divider/divider'),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,17 +2,26 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
IconButton,
|
IconButton,
|
||||||
Menu,
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
type MenuRef,
|
||||||
RowInput,
|
RowInput,
|
||||||
Scrollable,
|
Scrollable,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { TagService, useDeleteTagConfirmModal } from '@affine/core/modules/tag';
|
import { TagService, useDeleteTagConfirmModal } from '@affine/core/modules/tag';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
import { DoneIcon, MoreHorizontalIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
import type { KeyboardEvent, ReactNode } from 'react';
|
import type { KeyboardEvent, ReactNode } from 'react';
|
||||||
import { useCallback, useMemo, useReducer, useRef, useState } from 'react';
|
import {
|
||||||
|
useCallback,
|
||||||
|
useImperativeHandle,
|
||||||
|
useMemo,
|
||||||
|
useReducer,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import { useAsyncCallback } from '../hooks/affine-async-hooks';
|
import { useAsyncCallback } from '../hooks/affine-async-hooks';
|
||||||
import { ConfigModal } from '../mobile';
|
import { ConfigModal } from '../mobile';
|
||||||
@@ -44,6 +53,8 @@ export interface TagsInlineEditorProps extends TagsEditorProps {
|
|||||||
modalMenu?: boolean;
|
modalMenu?: boolean;
|
||||||
menuClassName?: string;
|
menuClassName?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
ref?: React.Ref<MenuRef>;
|
||||||
|
onEditorClose?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TagOption = TagLike | { readonly create: true; readonly value: string };
|
type TagOption = TagLike | { readonly create: true; readonly value: string };
|
||||||
@@ -269,10 +280,17 @@ export const TagsEditor = ({
|
|||||||
placeholder="Type here ..."
|
placeholder="Type here ..."
|
||||||
/>
|
/>
|
||||||
</InlineTagList>
|
</InlineTagList>
|
||||||
|
|
||||||
{BUILD_CONFIG.isMobileEdition ? null : (
|
{BUILD_CONFIG.isMobileEdition ? null : (
|
||||||
<Divider size="thinner" className={styles.tagDivider} />
|
<MenuItem
|
||||||
|
className={styles.tagsEditorDoneButton}
|
||||||
|
prefixIcon={<DoneIcon />}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{BUILD_CONFIG.isMobileEdition ? null : (
|
||||||
|
<Divider size="thinner" className={styles.tagDivider} />
|
||||||
|
)}
|
||||||
<div className={styles.tagsEditorTagsSelector}>
|
<div className={styles.tagsEditorTagsSelector}>
|
||||||
<div className={styles.tagsEditorTagsSelectorHeader}>
|
<div className={styles.tagsEditorTagsSelectorHeader}>
|
||||||
{t['com.affine.page-properties.tags.selector-header-title']()}
|
{t['com.affine.page-properties.tags.selector-header-title']()}
|
||||||
@@ -350,10 +368,25 @@ const MobileInlineEditor = ({
|
|||||||
className,
|
className,
|
||||||
title,
|
title,
|
||||||
style,
|
style,
|
||||||
|
onEditorClose,
|
||||||
|
ref,
|
||||||
...props
|
...props
|
||||||
}: TagsInlineEditorProps) => {
|
}: TagsInlineEditorProps) => {
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
changeOpen: (open: boolean) => {
|
||||||
|
setEditing(open);
|
||||||
|
if (!open) {
|
||||||
|
onEditorClose?.();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[onEditorClose]
|
||||||
|
);
|
||||||
|
|
||||||
const empty = !props.selectedTags || props.selectedTags.length === 0;
|
const empty = !props.selectedTags || props.selectedTags.length === 0;
|
||||||
const selectedTags = useMemo(() => {
|
const selectedTags = useMemo(() => {
|
||||||
return props.selectedTags
|
return props.selectedTags
|
||||||
@@ -366,7 +399,10 @@ const MobileInlineEditor = ({
|
|||||||
title={title}
|
title={title}
|
||||||
open={editing}
|
open={editing}
|
||||||
onOpenChange={setEditing}
|
onOpenChange={setEditing}
|
||||||
onBack={() => setEditing(false)}
|
onBack={() => {
|
||||||
|
setEditing(false);
|
||||||
|
onEditorClose?.();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<TagsEditor {...props} />
|
<TagsEditor {...props} />
|
||||||
</ConfigModal>
|
</ConfigModal>
|
||||||
@@ -394,6 +430,8 @@ const DesktopTagsInlineEditor = ({
|
|||||||
modalMenu,
|
modalMenu,
|
||||||
menuClassName,
|
menuClassName,
|
||||||
style,
|
style,
|
||||||
|
ref,
|
||||||
|
onEditorClose,
|
||||||
...props
|
...props
|
||||||
}: TagsInlineEditorProps) => {
|
}: TagsInlineEditorProps) => {
|
||||||
const empty = !props.selectedTags || props.selectedTags.length === 0;
|
const empty = !props.selectedTags || props.selectedTags.length === 0;
|
||||||
@@ -404,6 +442,7 @@ const DesktopTagsInlineEditor = ({
|
|||||||
}, [props.selectedTags, props.tags]);
|
}, [props.selectedTags, props.tags]);
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
|
ref={ref}
|
||||||
contentOptions={{
|
contentOptions={{
|
||||||
side: 'bottom',
|
side: 'bottom',
|
||||||
align: 'start',
|
align: 'start',
|
||||||
@@ -416,6 +455,7 @@ const DesktopTagsInlineEditor = ({
|
|||||||
}}
|
}}
|
||||||
rootOptions={{
|
rootOptions={{
|
||||||
modal: modalMenu,
|
modal: modalMenu,
|
||||||
|
onClose: onEditorClose,
|
||||||
}}
|
}}
|
||||||
items={<TagsEditor {...props} />}
|
items={<TagsEditor {...props} />}
|
||||||
>
|
>
|
||||||
@@ -447,6 +487,8 @@ export const TagsInlineEditor = BUILD_CONFIG.isMobileEdition
|
|||||||
export const WorkspaceTagsInlineEditor = ({
|
export const WorkspaceTagsInlineEditor = ({
|
||||||
selectedTags,
|
selectedTags,
|
||||||
onDeselectTag,
|
onDeselectTag,
|
||||||
|
ref,
|
||||||
|
onEditorClose,
|
||||||
...otherProps
|
...otherProps
|
||||||
}: Omit<
|
}: Omit<
|
||||||
TagsInlineEditorProps,
|
TagsInlineEditorProps,
|
||||||
@@ -505,6 +547,8 @@ export const WorkspaceTagsInlineEditor = ({
|
|||||||
onCreateTag={onCreateTag}
|
onCreateTag={onCreateTag}
|
||||||
onDeleteTag={onDeleteTag}
|
onDeleteTag={onDeleteTag}
|
||||||
onTagChange={onTagChange}
|
onTagChange={onTagChange}
|
||||||
|
ref={ref}
|
||||||
|
onEditorClose={onEditorClose}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { Checkbox, Menu, MenuItem, PropertyValue } from '@affine/component';
|
import {
|
||||||
|
Checkbox,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
type MenuRef,
|
||||||
|
PropertyValue,
|
||||||
|
} from '@affine/component';
|
||||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { CheckBoxCheckLinearIcon } from '@blocksuite/icons/rc';
|
import { CheckBoxCheckLinearIcon } from '@blocksuite/icons/rc';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
||||||
import { StackProperty } from '../explorer/docs-view/stack-property';
|
import { StackProperty } from '../explorer/docs-view/stack-property';
|
||||||
@@ -40,18 +46,34 @@ export const CheckboxValue = ({
|
|||||||
|
|
||||||
export const CheckboxFilterValue = ({
|
export const CheckboxFilterValue = ({
|
||||||
filter,
|
filter,
|
||||||
|
isDraft,
|
||||||
|
onDraftCompleted,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
onChange: (filter: FilterParams) => void;
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
|
onChange?: (filter: FilterParams) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const menuRef = useRef<MenuRef>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDraft) {
|
||||||
|
menuRef.current?.changeOpen(true);
|
||||||
|
}
|
||||||
|
}, [isDraft]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
|
ref={menuRef}
|
||||||
|
rootOptions={{
|
||||||
|
onClose: onDraftCompleted,
|
||||||
|
}}
|
||||||
items={
|
items={
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange({
|
onChange?.({
|
||||||
...filter,
|
...filter,
|
||||||
value: 'true',
|
value: 'true',
|
||||||
});
|
});
|
||||||
@@ -62,7 +84,7 @@ export const CheckboxFilterValue = ({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange({
|
onChange?.({
|
||||||
...filter,
|
...filter,
|
||||||
value: 'false',
|
value: 'false',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PropertyValue } from '@affine/component';
|
import { type MenuRef, PropertyValue } from '@affine/component';
|
||||||
import { PublicUserLabel } from '@affine/core/modules/cloud/views/public-user';
|
import { PublicUserLabel } from '@affine/core/modules/cloud/views/public-user';
|
||||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
import { type DocRecord, DocService } from '@affine/core/modules/doc';
|
import { type DocRecord, DocService } from '@affine/core/modules/doc';
|
||||||
@@ -6,7 +6,7 @@ import { WorkspaceService } from '@affine/core/modules/workspace';
|
|||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { type ReactNode, useCallback, useMemo } from 'react';
|
import { type ReactNode, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
||||||
import type { GroupHeaderProps } from '../explorer/types';
|
import type { GroupHeaderProps } from '../explorer/types';
|
||||||
@@ -100,12 +100,23 @@ export const UpdatedByValue = () => {
|
|||||||
|
|
||||||
export const CreatedByUpdatedByFilterValue = ({
|
export const CreatedByUpdatedByFilterValue = ({
|
||||||
filter,
|
filter,
|
||||||
|
isDraft,
|
||||||
|
onDraftCompleted,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
onChange: (filter: FilterParams) => void;
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
|
onChange?: (filter: FilterParams) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
const menuRef = useRef<MenuRef>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDraft) {
|
||||||
|
menuRef.current?.changeOpen(true);
|
||||||
|
}
|
||||||
|
}, [isDraft]);
|
||||||
|
|
||||||
const selected = useMemo(
|
const selected = useMemo(
|
||||||
() => filter.value?.split(',').filter(Boolean) ?? [],
|
() => filter.value?.split(',').filter(Boolean) ?? [],
|
||||||
@@ -114,7 +125,7 @@ export const CreatedByUpdatedByFilterValue = ({
|
|||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(selected: string[]) => {
|
(selected: string[]) => {
|
||||||
onChange({
|
onChange?.({
|
||||||
...filter,
|
...filter,
|
||||||
value: selected.join(','),
|
value: selected.join(','),
|
||||||
});
|
});
|
||||||
@@ -131,6 +142,8 @@ export const CreatedByUpdatedByFilterValue = ({
|
|||||||
}
|
}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
ref={menuRef}
|
||||||
|
onEditorClose={onDraftCompleted}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,25 @@
|
|||||||
import { DatePicker, Menu, PropertyValue } from '@affine/component';
|
import {
|
||||||
|
DatePicker,
|
||||||
|
Menu,
|
||||||
|
type MenuRef,
|
||||||
|
PropertyValue,
|
||||||
|
} from '@affine/component';
|
||||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
import { i18nTime, useI18n } from '@affine/i18n';
|
import { i18nTime, useI18n } from '@affine/i18n';
|
||||||
import { DateTimeIcon } from '@blocksuite/icons/rc';
|
import { DateTimeIcon } from '@blocksuite/icons/rc';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { useCallback } from 'react';
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
||||||
import { StackProperty } from '../explorer/docs-view/stack-property';
|
import { StackProperty } from '../explorer/docs-view/stack-property';
|
||||||
import type { DocListPropertyProps, GroupHeaderProps } from '../explorer/types';
|
import type { DocListPropertyProps, GroupHeaderProps } from '../explorer/types';
|
||||||
|
import { FilterOptionsGroup } from '../filter/options';
|
||||||
import type { PropertyValueProps } from '../properties/types';
|
import type { PropertyValueProps } from '../properties/types';
|
||||||
import * as styles from './date.css';
|
import * as styles from './date.css';
|
||||||
|
|
||||||
@@ -64,22 +76,89 @@ export const DateValue = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DateFilterValue = ({
|
const DateSelectorMenu = ({
|
||||||
|
ref,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
ref?: React.Ref<MenuRef>;
|
||||||
|
value?: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
onClose?: () => void;
|
||||||
|
}) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
changeOpen: (open: boolean) => {
|
||||||
|
setOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
onClose?.();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[onClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOpenChange = useCallback(
|
||||||
|
(open: boolean) => {
|
||||||
|
setOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
onClose?.();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
onChange(value);
|
||||||
|
setOpen(false);
|
||||||
|
onClose?.();
|
||||||
|
},
|
||||||
|
[onChange, onClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
rootOptions={{
|
||||||
|
open,
|
||||||
|
onOpenChange: handleOpenChange,
|
||||||
|
}}
|
||||||
|
items={<DatePicker value={value || undefined} onChange={handleChange} />}
|
||||||
|
>
|
||||||
|
{value ? (
|
||||||
|
<span>{value}</span>
|
||||||
|
) : (
|
||||||
|
<span style={{ color: cssVarV2('text/placeholder') }}>
|
||||||
|
{t['com.affine.filter.empty']()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DateFilterValueAfterBefore = ({
|
||||||
filter,
|
filter,
|
||||||
|
isDraft,
|
||||||
|
onDraftCompleted,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
onChange: (filter: FilterParams) => void;
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
|
onChange?: (filter: FilterParams) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const menuRef = useRef<MenuRef>(null);
|
||||||
const value = filter.value;
|
const value = filter.value;
|
||||||
const values = value?.split(',') ?? [];
|
const values = value?.split(',') ?? [];
|
||||||
const displayDates =
|
|
||||||
values.map(t => i18nTime(t, { absolute: { accuracy: 'day' } })) ?? [];
|
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(date: string) => {
|
(date: string) => {
|
||||||
onChange({
|
onChange?.({
|
||||||
...filter,
|
...filter,
|
||||||
value: date,
|
value: date,
|
||||||
});
|
});
|
||||||
@@ -87,56 +166,90 @@ export const DateFilterValue = ({
|
|||||||
[onChange, filter]
|
[onChange, filter]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDraft) {
|
||||||
|
menuRef.current?.changeOpen(true);
|
||||||
|
}
|
||||||
|
}, [isDraft]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DateSelectorMenu
|
||||||
|
ref={menuRef}
|
||||||
|
value={values[0]}
|
||||||
|
onChange={handleChange}
|
||||||
|
onClose={onDraftCompleted}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DateFilterValue = ({
|
||||||
|
filter,
|
||||||
|
isDraft,
|
||||||
|
onDraftCompleted,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
filter: FilterParams;
|
||||||
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
|
onChange?: (filter: FilterParams) => void;
|
||||||
|
}) => {
|
||||||
|
const value = filter.value;
|
||||||
|
const values = value?.split(',') ?? [];
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(date: string) => {
|
||||||
|
onChange?.({
|
||||||
|
...filter,
|
||||||
|
value: date,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[onChange, filter]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
isDraft &&
|
||||||
|
filter.method !== 'after' &&
|
||||||
|
filter.method !== 'before' &&
|
||||||
|
filter.method !== 'between'
|
||||||
|
) {
|
||||||
|
onDraftCompleted?.();
|
||||||
|
}
|
||||||
|
}, [isDraft, filter.method, onDraftCompleted]);
|
||||||
|
|
||||||
return filter.method === 'after' || filter.method === 'before' ? (
|
return filter.method === 'after' || filter.method === 'before' ? (
|
||||||
<Menu
|
<DateFilterValueAfterBefore
|
||||||
items={
|
filter={filter}
|
||||||
<DatePicker value={values[0] || undefined} onChange={handleChange} />
|
isDraft={isDraft}
|
||||||
}
|
onDraftCompleted={onDraftCompleted}
|
||||||
>
|
onChange={onChange}
|
||||||
{displayDates[0] ? (
|
/>
|
||||||
<span>{displayDates[0]}</span>
|
|
||||||
) : (
|
|
||||||
<span style={{ color: cssVarV2('text/placeholder') }}>
|
|
||||||
{t['com.affine.filter.empty']()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
) : filter.method === 'between' ? (
|
) : filter.method === 'between' ? (
|
||||||
<>
|
<FilterOptionsGroup
|
||||||
<Menu
|
isDraft={isDraft}
|
||||||
items={
|
onDraftCompleted={onDraftCompleted}
|
||||||
<DatePicker
|
items={[
|
||||||
value={values[0] || undefined}
|
({ onDraftCompleted, menuRef }) => (
|
||||||
|
<DateSelectorMenu
|
||||||
|
ref={menuRef}
|
||||||
|
value={values[0]}
|
||||||
onChange={value => handleChange(`${value},${values[1] || ''}`)}
|
onChange={value => handleChange(`${value},${values[1] || ''}`)}
|
||||||
|
onClose={onDraftCompleted}
|
||||||
/>
|
/>
|
||||||
}
|
),
|
||||||
>
|
<span key="between" style={{ color: cssVarV2('text/placeholder') }}>
|
||||||
{displayDates[0] ? (
|
-
|
||||||
<span>{displayDates[0]}</span>
|
</span>,
|
||||||
) : (
|
({ onDraftCompleted, menuRef }) => (
|
||||||
<span style={{ color: cssVarV2('text/placeholder') }}>
|
<DateSelectorMenu
|
||||||
{t['com.affine.filter.empty']()}
|
ref={menuRef}
|
||||||
</span>
|
value={values[1]}
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
<span style={{ color: cssVarV2('text/placeholder') }}> - </span>
|
|
||||||
<Menu
|
|
||||||
items={
|
|
||||||
<DatePicker
|
|
||||||
value={values[1] || undefined}
|
|
||||||
onChange={value => handleChange(`${values[0] || ''},${value}`)}
|
onChange={value => handleChange(`${values[0] || ''},${value}`)}
|
||||||
|
onClose={onDraftCompleted}
|
||||||
/>
|
/>
|
||||||
}
|
),
|
||||||
>
|
]}
|
||||||
{displayDates[1] ? (
|
></FilterOptionsGroup>
|
||||||
<span>{displayDates[1]}</span>
|
|
||||||
) : (
|
|
||||||
<span style={{ color: cssVarV2('text/placeholder') }}>
|
|
||||||
{t['com.affine.filter.empty']()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
</>
|
|
||||||
) : undefined;
|
) : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
type MenuRef,
|
||||||
notify,
|
notify,
|
||||||
PropertyValue,
|
PropertyValue,
|
||||||
type RadioItem,
|
type RadioItem,
|
||||||
@@ -11,7 +12,7 @@ import { useI18n } from '@affine/i18n';
|
|||||||
import type { DocMode } from '@blocksuite/affine/model';
|
import type { DocMode } from '@blocksuite/affine/model';
|
||||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons/rc';
|
import { EdgelessIcon, PageIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
||||||
import { StackProperty } from '../explorer/docs-view/stack-property';
|
import { StackProperty } from '../explorer/docs-view/stack-property';
|
||||||
@@ -78,20 +79,35 @@ export const DocPrimaryModeValue = ({
|
|||||||
|
|
||||||
export const DocPrimaryModeFilterValue = ({
|
export const DocPrimaryModeFilterValue = ({
|
||||||
filter,
|
filter,
|
||||||
|
isDraft,
|
||||||
|
onDraftCompleted,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
onChange: (filter: FilterParams) => void;
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
|
onChange?: (filter: FilterParams) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
const menuRef = useRef<MenuRef>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDraft) {
|
||||||
|
menuRef.current?.changeOpen(true);
|
||||||
|
}
|
||||||
|
}, [isDraft]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
|
ref={menuRef}
|
||||||
|
rootOptions={{
|
||||||
|
onClose: onDraftCompleted,
|
||||||
|
}}
|
||||||
items={
|
items={
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange({
|
onChange?.({
|
||||||
...filter,
|
...filter,
|
||||||
value: 'page',
|
value: 'page',
|
||||||
});
|
});
|
||||||
@@ -102,7 +118,7 @@ export const DocPrimaryModeFilterValue = ({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange({
|
onChange?.({
|
||||||
...filter,
|
...filter,
|
||||||
value: 'edgeless',
|
value: 'edgeless',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -324,7 +324,8 @@ export const WorkspacePropertyTypes = {
|
|||||||
filterMethod?: { [key in WorkspacePropertyFilter<type>]: I18nString };
|
filterMethod?: { [key in WorkspacePropertyFilter<type>]: I18nString };
|
||||||
filterValue?: React.FC<{
|
filterValue?: React.FC<{
|
||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
onChange: (filter: FilterParams) => void;
|
isDraft?: boolean;
|
||||||
|
onChange?: (filter: FilterParams) => void;
|
||||||
}>;
|
}>;
|
||||||
defaultFilter?: Omit<FilterParams, 'type' | 'key'>;
|
defaultFilter?: Omit<FilterParams, 'type' | 'key'>;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
DatePicker,
|
DatePicker,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
|
type MenuRef,
|
||||||
PropertyValue,
|
PropertyValue,
|
||||||
} from '@affine/component';
|
} from '@affine/component';
|
||||||
import { MobileJournalConflictList } from '@affine/core/mobile/pages/workspace/detail/menu/journal-conflicts';
|
import { MobileJournalConflictList } from '@affine/core/mobile/pages/workspace/detail/menu/journal-conflicts';
|
||||||
@@ -182,18 +183,34 @@ export const JournalValue = ({ readonly }: PropertyValueProps) => {
|
|||||||
|
|
||||||
export const JournalFilterValue = ({
|
export const JournalFilterValue = ({
|
||||||
filter,
|
filter,
|
||||||
|
isDraft,
|
||||||
|
onDraftCompleted,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
onChange: (filter: FilterParams) => void;
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
|
onChange?: (filter: FilterParams) => void;
|
||||||
}) => {
|
}) => {
|
||||||
|
const menuRef = useRef<MenuRef>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDraft) {
|
||||||
|
menuRef.current?.changeOpen(true);
|
||||||
|
}
|
||||||
|
}, [isDraft]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
|
ref={menuRef}
|
||||||
|
rootOptions={{
|
||||||
|
onClose: onDraftCompleted,
|
||||||
|
}}
|
||||||
items={
|
items={
|
||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange({
|
onChange?.({
|
||||||
...filter,
|
...filter,
|
||||||
value: 'true',
|
value: 'true',
|
||||||
});
|
});
|
||||||
@@ -204,7 +221,7 @@ export const JournalFilterValue = ({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChange({
|
onChange?.({
|
||||||
...filter,
|
...filter,
|
||||||
value: 'false',
|
value: 'false',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PropertyValue } from '@affine/component';
|
import { type MenuRef, PropertyValue } from '@affine/component';
|
||||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
import { type DocRecord, DocService } from '@affine/core/modules/doc';
|
import { type DocRecord, DocService } from '@affine/core/modules/doc';
|
||||||
import { type Tag, TagService } from '@affine/core/modules/tag';
|
import { type Tag, TagService } from '@affine/core/modules/tag';
|
||||||
@@ -7,7 +7,7 @@ import { useI18n } from '@affine/i18n';
|
|||||||
import { TagsIcon } from '@blocksuite/icons/rc';
|
import { TagsIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
||||||
import { StackProperty } from '../explorer/docs-view/stack-property';
|
import { StackProperty } from '../explorer/docs-view/stack-property';
|
||||||
@@ -51,14 +51,25 @@ export const TagsValue = ({ readonly }: PropertyValueProps) => {
|
|||||||
|
|
||||||
export const TagsFilterValue = ({
|
export const TagsFilterValue = ({
|
||||||
filter,
|
filter,
|
||||||
|
isDraft,
|
||||||
|
onDraftCompleted,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
onChange: (filter: FilterParams) => void;
|
onChange: (filter: FilterParams) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const tagService = useService(TagService);
|
const tagService = useService(TagService);
|
||||||
const allTagMetas = useLiveData(tagService.tagList.tagMetas$);
|
const allTagMetas = useLiveData(tagService.tagList.tagMetas$);
|
||||||
|
const menuRef = useRef<MenuRef>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDraft) {
|
||||||
|
menuRef.current?.changeOpen(true);
|
||||||
|
}
|
||||||
|
}, [isDraft]);
|
||||||
|
|
||||||
const selectedTags = useMemo(
|
const selectedTags = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -98,6 +109,8 @@ export const TagsFilterValue = ({
|
|||||||
onSelectTag={handleSelectTag}
|
onSelectTag={handleSelectTag}
|
||||||
onDeselectTag={handleDeselectTag}
|
onDeselectTag={handleDeselectTag}
|
||||||
tagMode="inline-tag"
|
tagMode="inline-tag"
|
||||||
|
ref={menuRef}
|
||||||
|
onEditorClose={onDraftCompleted}
|
||||||
/>
|
/>
|
||||||
) : undefined;
|
) : undefined;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Input, Menu, PropertyValue } from '@affine/component';
|
import { Input, Menu, type MenuRef, PropertyValue } from '@affine/component';
|
||||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { TextIcon, TextTypeIcon } from '@blocksuite/icons/rc';
|
import { TextIcon, TextTypeIcon } from '@blocksuite/icons/rc';
|
||||||
@@ -177,15 +177,26 @@ export const TextValue = BUILD_CONFIG.isMobileWeb
|
|||||||
|
|
||||||
export const TextFilterValue = ({
|
export const TextFilterValue = ({
|
||||||
filter,
|
filter,
|
||||||
|
isDraft,
|
||||||
|
onDraftCompleted,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
onChange: (filter: FilterParams) => void;
|
isDraft?: boolean;
|
||||||
|
onDraftCompleted?: () => void;
|
||||||
|
onChange?: (filter: FilterParams) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const [tempValue, setTempValue] = useState(filter.value || '');
|
const [tempValue, setTempValue] = useState(filter.value || '');
|
||||||
const [valueMenuOpen, setValueMenuOpen] = useState(false);
|
const [valueMenuOpen, setValueMenuOpen] = useState(false);
|
||||||
|
const menuRef = useRef<MenuRef>(null);
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDraft) {
|
||||||
|
menuRef.current?.changeOpen(true);
|
||||||
|
}
|
||||||
|
}, [isDraft]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// update temp value with new filter value
|
// update temp value with new filter value
|
||||||
setTempValue(filter.value || '');
|
setTempValue(filter.value || '');
|
||||||
@@ -193,7 +204,7 @@ export const TextFilterValue = ({
|
|||||||
|
|
||||||
const submitTempValue = useCallback(() => {
|
const submitTempValue = useCallback(() => {
|
||||||
if (tempValue !== (filter.value || '')) {
|
if (tempValue !== (filter.value || '')) {
|
||||||
onChange({
|
onChange?.({
|
||||||
...filter,
|
...filter,
|
||||||
value: tempValue,
|
value: tempValue,
|
||||||
});
|
});
|
||||||
@@ -216,9 +227,11 @@ export const TextFilterValue = ({
|
|||||||
|
|
||||||
return filter.method !== 'is-not-empty' && filter.method !== 'is-empty' ? (
|
return filter.method !== 'is-not-empty' && filter.method !== 'is-empty' ? (
|
||||||
<Menu
|
<Menu
|
||||||
|
ref={menuRef}
|
||||||
rootOptions={{
|
rootOptions={{
|
||||||
open: valueMenuOpen,
|
open: valueMenuOpen,
|
||||||
onOpenChange: setValueMenuOpen,
|
onOpenChange: setValueMenuOpen,
|
||||||
|
onClose: onDraftCompleted,
|
||||||
}}
|
}}
|
||||||
contentOptions={{
|
contentOptions={{
|
||||||
onPointerDownOutside: submitTempValue,
|
onPointerDownOutside: submitTempValue,
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ export const AllPage = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [tempFilters, setTempFilters] = useState<FilterParams[] | null>(null);
|
const [tempFilters, setTempFilters] = useState<FilterParams[] | null>(null);
|
||||||
|
const [tempFiltersInitial, setTempFiltersInitial] =
|
||||||
|
useState<FilterParams | null>(null);
|
||||||
|
|
||||||
const [explorerContextValue] = useState(() =>
|
const [explorerContextValue] = useState(() =>
|
||||||
createDocExplorerContext(initialState)
|
createDocExplorerContext(initialState)
|
||||||
@@ -281,7 +283,8 @@ export const AllPage = () => {
|
|||||||
|
|
||||||
const handleNewTempFilter = useCallback((params: FilterParams) => {
|
const handleNewTempFilter = useCallback((params: FilterParams) => {
|
||||||
setSelectedCollectionId(null);
|
setSelectedCollectionId(null);
|
||||||
setTempFilters([params]);
|
setTempFilters([]);
|
||||||
|
setTempFiltersInitial(params);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDisplayPreferenceChange = useCallback(
|
const handleDisplayPreferenceChange = useCallback(
|
||||||
@@ -320,6 +323,7 @@ export const AllPage = () => {
|
|||||||
className={styles.filters}
|
className={styles.filters}
|
||||||
filters={tempFilters}
|
filters={tempFilters}
|
||||||
onChange={handleFilterChange}
|
onChange={handleFilterChange}
|
||||||
|
defaultDraftFilter={tempFiltersInitial}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="plain"
|
variant="plain"
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ function isAfter(
|
|||||||
: referenceDate;
|
: referenceDate;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
targetYear >= refYear ||
|
targetYear > refYear ||
|
||||||
(targetYear === refYear && targetMonth >= refMonth) ||
|
(targetYear === refYear && targetMonth > refMonth) ||
|
||||||
(targetYear === refYear && targetMonth === refMonth && targetDay >= refDay)
|
(targetYear === refYear && targetMonth === refMonth && targetDay >= refDay)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -153,8 +153,8 @@ function isBefore(
|
|||||||
const [refYear, refMonth, refDay] = referenceDate;
|
const [refYear, refMonth, refDay] = referenceDate;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
targetYear <= refYear ||
|
targetYear < refYear ||
|
||||||
(targetYear === refYear && targetMonth <= refMonth) ||
|
(targetYear === refYear && targetMonth < refMonth) ||
|
||||||
(targetYear === refYear && targetMonth === refMonth && targetDay <= refDay)
|
(targetYear === refYear && targetMonth === refMonth && targetDay <= refDay)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user