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:
EYHN
2025-05-21 04:49:44 +00:00
parent 41ec438df8
commit 8f352580a7
28 changed files with 778 additions and 154 deletions
@@ -1,6 +1,6 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import clsx from 'clsx';
import React from 'react';
import React, { useCallback, useImperativeHandle, useState } from 'react';
import type { MenuProps } from '../menu.types';
import * as styles from '../styles.css';
@@ -11,18 +11,52 @@ export const DesktopMenu = ({
items,
noPortal,
portalOptions,
rootOptions: { defaultOpen, modal, ...rootOptions } = {},
rootOptions: {
defaultOpen,
modal,
open,
onOpenChange,
onClose,
...rootOptions
} = {},
contentOptions: {
className = '',
style: contentStyle = {},
...otherContentOptions
} = {},
ref,
}: 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;
return (
<DropdownMenu.Root
defaultOpen={defaultOpen}
modal={modal ?? false}
open={finalOpen}
onOpenChange={handleOpenChange}
{...rootOptions}
>
<DropdownMenu.Trigger
@@ -8,15 +8,20 @@ import type {
} from '@radix-ui/react-dropdown-menu';
import type { CSSProperties, ReactNode } from 'react';
export interface MenuRef {
changeOpen: (open: boolean) => void;
}
export interface MenuProps {
children: ReactNode;
items: ReactNode;
title?: string;
portalOptions?: Omit<DropdownMenuPortalProps, 'children'>;
rootOptions?: Omit<DropdownMenuProps, 'children'>;
rootOptions?: Omit<DropdownMenuProps, 'children'> & { onClose?: () => void };
contentOptions?: Omit<DropdownMenuContentProps, 'children'>;
contentWrapperStyle?: CSSProperties;
noPortal?: boolean;
ref?: React.Ref<MenuRef>;
}
export interface MenuItemProps
@@ -2,7 +2,13 @@ import { useI18n } from '@affine/i18n';
import { ArrowLeftSmallIcon } from '@blocksuite/icons/rc';
import { Slot } from '@radix-ui/react-slot';
import clsx from 'clsx';
import { useCallback, useContext, useEffect, useState } from 'react';
import {
useCallback,
useContext,
useEffect,
useImperativeHandle,
useState,
} from 'react';
import { observeResize } from '../../../utils';
import { Button } from '../../button';
@@ -34,6 +40,7 @@ export const MobileMenu = ({
} = {},
contentWrapperStyle,
rootOptions,
ref,
}: MenuProps) => {
const [subMenus, setSubMenus] = useState<SubMenuContent[]>([]);
const [open, setOpen] = useState(false);
@@ -82,10 +89,23 @@ export const MobileMenu = ({
}
setOpen(open);
rootOptions?.onOpenChange?.(open);
if (!open) {
rootOptions?.onClose?.();
}
},
[onInteractOutside, onPointerDownOutside, removeAllSubMenus, rootOptions]
);
useImperativeHandle(
ref,
() => ({
changeOpen: (open: boolean) => {
onOpenChange(open);
},
}),
[onOpenChange]
);
const onItemClick = useCallback(
(e: any) => {
e.preventDefault();