mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 19:02:23 +08:00
feat(editor): add date grouping configurations (#12679)
https://github.com/user-attachments/assets/d5578060-2c8c-47a5-ba65-ef2e9430518b This PR adds the ability to group-by date with configuration which an example is shown in the image below:  <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Date-based grouping modes (relative, day, week Sun/Mon, month, year), a date group renderer, and quick lookup for group-by configs by name. * **Improvements** * Enhanced group settings: date sub‑modes, week‑start, per‑group visibility, Hide All/Show All, date sort order, improved drag/drop and reorder. * Consistent popup placement/middleware, nested popup positioning, per‑item close-on-select, and enforced minimum menu heights. * UI: empty groups now display "No <property>"; views defensively handle null/hidden groups. * **Tests** * Added unit tests for date-key sorting and comparison. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Norkz <richardlora557@gmail.com> Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
This commit is contained in:
@@ -23,6 +23,7 @@ export type MenuButtonData = {
|
||||
select: (ele: HTMLElement) => void | false;
|
||||
onHover?: (hover: boolean) => void;
|
||||
testId?: string;
|
||||
closeOnSelect?: boolean;
|
||||
};
|
||||
|
||||
export class MenuButton extends MenuFocusable {
|
||||
@@ -85,7 +86,9 @@ export class MenuButton extends MenuFocusable {
|
||||
onClick() {
|
||||
if (this.data.select(this) !== false) {
|
||||
this.menu.options.onComplete?.();
|
||||
this.menu.close();
|
||||
if (this.data.closeOnSelect !== false) {
|
||||
this.menu.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +153,9 @@ export class MobileMenuButton extends MenuFocusable {
|
||||
onClick() {
|
||||
if (this.data.select(this) !== false) {
|
||||
this.menu.options.onComplete?.();
|
||||
this.menu.close();
|
||||
if (this.data.closeOnSelect !== false) {
|
||||
this.menu.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +205,7 @@ export const menuButtonItems = {
|
||||
select: (ele: HTMLElement) => void | false;
|
||||
onHover?: (hover: boolean) => void;
|
||||
class?: MenuClass;
|
||||
closeOnSelect?: boolean;
|
||||
hide?: () => boolean;
|
||||
testId?: string;
|
||||
}) =>
|
||||
@@ -219,6 +225,7 @@ export const menuButtonItems = {
|
||||
},
|
||||
onHover: config.onHover,
|
||||
select: config.select,
|
||||
closeOnSelect: config.closeOnSelect,
|
||||
class: {
|
||||
'selected-item': config.isSelected ?? false,
|
||||
...config.class,
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
computePosition,
|
||||
type Middleware,
|
||||
offset,
|
||||
type Placement,
|
||||
type ReferenceElement,
|
||||
shift,
|
||||
} from '@floating-ui/dom';
|
||||
@@ -37,7 +38,9 @@ export class MenuComponent
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
min-width: 180px;
|
||||
min-width: 320px;
|
||||
max-width: 320px;
|
||||
max-height: 700px;
|
||||
box-shadow: ${unsafeCSSVar('overlayPanelShadow')};
|
||||
border-radius: 4px;
|
||||
background-color: ${unsafeCSSVarV2('layer/background/overlayPanel')};
|
||||
@@ -439,6 +442,7 @@ export const createPopup = (
|
||||
onClose?: () => void;
|
||||
middleware?: Array<Middleware | null | undefined | false>;
|
||||
container?: HTMLElement;
|
||||
placement?: Placement;
|
||||
}
|
||||
) => {
|
||||
const close = () => {
|
||||
@@ -448,6 +452,7 @@ export const createPopup = (
|
||||
const modal = createModal(target.root);
|
||||
autoUpdate(target.targetRect, content, () => {
|
||||
computePosition(target.targetRect, content, {
|
||||
placement: options?.placement,
|
||||
middleware: options?.middleware ?? [shift({ crossAxis: true })],
|
||||
})
|
||||
.then(({ x, y }) => {
|
||||
@@ -520,6 +525,7 @@ export const popMenu = (
|
||||
options: MenuOptions;
|
||||
middleware?: Array<Middleware | null | undefined | false>;
|
||||
container?: HTMLElement;
|
||||
placement?: Placement;
|
||||
}
|
||||
): MenuHandler => {
|
||||
if (IS_MOBILE) {
|
||||
@@ -551,6 +557,7 @@ export const popMenu = (
|
||||
offset(4),
|
||||
],
|
||||
container: props.container,
|
||||
placement: props.placement,
|
||||
});
|
||||
return {
|
||||
close: closePopup,
|
||||
@@ -563,12 +570,14 @@ export const popMenu = (
|
||||
export const popFilterableSimpleMenu = (
|
||||
target: PopupTarget,
|
||||
options: MenuConfig[],
|
||||
onClose?: () => void
|
||||
onClose?: () => void,
|
||||
placement: Placement = 'bottom-start'
|
||||
) => {
|
||||
popMenu(target, {
|
||||
options: {
|
||||
items: options,
|
||||
onClose,
|
||||
},
|
||||
placement,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -4,12 +4,15 @@ import {
|
||||
autoPlacement,
|
||||
autoUpdate,
|
||||
computePosition,
|
||||
type Middleware,
|
||||
offset,
|
||||
shift,
|
||||
} from '@floating-ui/dom';
|
||||
import { html, nothing, type TemplateResult } from 'lit';
|
||||
import { css, html, nothing, type TemplateResult } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
|
||||
import { MenuButton } from './button.js';
|
||||
import { MenuFocusable } from './focusable.js';
|
||||
import { Menu, type MenuOptions } from './menu.js';
|
||||
import { popMenu, popupTargetFromElement } from './menu-renderer.js';
|
||||
@@ -20,29 +23,55 @@ export type MenuSubMenuData = {
|
||||
options: MenuOptions;
|
||||
select?: () => void;
|
||||
class?: string;
|
||||
openOnHover?: boolean;
|
||||
middleware?: Middleware[];
|
||||
autoHeight?: boolean;
|
||||
closeOnSelect?: boolean;
|
||||
};
|
||||
export const subMenuOffset = offset({
|
||||
mainAxis: 16,
|
||||
crossAxis: -8.5,
|
||||
crossAxis: 0,
|
||||
});
|
||||
export const subMenuPlacements = autoPlacement({
|
||||
allowedPlacements: ['right-start', 'left-start', 'right-end', 'left-end'],
|
||||
allowedPlacements: ['bottom-end'],
|
||||
});
|
||||
export const subMenuMiddleware = [subMenuOffset, subMenuPlacements];
|
||||
|
||||
export const dropdownSubMenuMiddleware = [
|
||||
autoPlacement({ allowedPlacements: ['bottom-end'] }),
|
||||
offset({ mainAxis: 8, crossAxis: 0 }),
|
||||
shift({ crossAxis: true }),
|
||||
];
|
||||
|
||||
export class MenuSubMenu extends MenuFocusable {
|
||||
static override styles = [
|
||||
MenuButton.styles,
|
||||
css`
|
||||
.affine-menu-button svg:last-child {
|
||||
transition: transform 150ms cubic-bezier(0.42, 0, 1, 1);
|
||||
}
|
||||
affine-menu-sub-menu.active .affine-menu-button svg:last-child {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
createTime = 0;
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.createTime = Date.now();
|
||||
this.disposables.addFromEvent(this, 'mouseenter', this.onMouseEnter);
|
||||
if (this.data.openOnHover !== false) {
|
||||
this.disposables.addFromEvent(this, 'mouseenter', this.onMouseEnter);
|
||||
}
|
||||
this.disposables.addFromEvent(this, 'click', e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (this.data.select) {
|
||||
this.data.select();
|
||||
this.menu.close();
|
||||
if (this.data.closeOnSelect !== false) {
|
||||
this.menu.close();
|
||||
}
|
||||
} else {
|
||||
this.openSubMenu();
|
||||
}
|
||||
@@ -60,11 +89,38 @@ export class MenuSubMenu extends MenuFocusable {
|
||||
}
|
||||
|
||||
openSubMenu() {
|
||||
if (this.data.openOnHover === false) {
|
||||
const { menu } = popMenu(popupTargetFromElement(this), {
|
||||
options: {
|
||||
...this.data.options,
|
||||
onComplete: () => {
|
||||
if (this.data.closeOnSelect !== false) {
|
||||
this.menu.close();
|
||||
}
|
||||
},
|
||||
onClose: () => {
|
||||
menu.menuElement.remove();
|
||||
this.data.options.onClose?.();
|
||||
},
|
||||
},
|
||||
middleware: this.data.middleware,
|
||||
});
|
||||
if (this.data.autoHeight) {
|
||||
menu.menuElement.style.minHeight = 'fit-content';
|
||||
menu.menuElement.style.maxHeight = 'fit-content';
|
||||
}
|
||||
menu.menuElement.style.minWidth = '200px';
|
||||
this.menu.openSubMenu(menu);
|
||||
return;
|
||||
}
|
||||
|
||||
const focus = this.menu.currentFocused$.value;
|
||||
const menu = new Menu({
|
||||
...this.data.options,
|
||||
onComplete: () => {
|
||||
this.menu.close();
|
||||
if (this.data.closeOnSelect !== false) {
|
||||
this.menu.close();
|
||||
}
|
||||
},
|
||||
onClose: () => {
|
||||
menu.menuElement.remove();
|
||||
@@ -74,9 +130,14 @@ export class MenuSubMenu extends MenuFocusable {
|
||||
},
|
||||
});
|
||||
this.menu.menuElement.parentElement?.append(menu.menuElement);
|
||||
if (this.data.autoHeight) {
|
||||
menu.menuElement.style.minHeight = 'fit-content';
|
||||
menu.menuElement.style.maxHeight = 'fit-content';
|
||||
}
|
||||
menu.menuElement.style.minWidth = '200px';
|
||||
const unsub = autoUpdate(this, menu.menuElement, () => {
|
||||
computePosition(this, menu.menuElement, {
|
||||
middleware: subMenuMiddleware,
|
||||
middleware: this.data.middleware ?? subMenuMiddleware,
|
||||
})
|
||||
.then(({ x, y }) => {
|
||||
menu.menuElement.style.left = `${x}px`;
|
||||
@@ -125,14 +186,22 @@ export class MobileSubMenu extends MenuFocusable {
|
||||
options: {
|
||||
...this.data.options,
|
||||
onComplete: () => {
|
||||
this.menu.close();
|
||||
if (this.data.closeOnSelect !== false) {
|
||||
this.menu.close();
|
||||
}
|
||||
},
|
||||
onClose: () => {
|
||||
menu.menuElement.remove();
|
||||
this.data.options.onClose?.();
|
||||
},
|
||||
},
|
||||
middleware: this.data.middleware,
|
||||
});
|
||||
if (this.data.autoHeight) {
|
||||
menu.menuElement.style.minHeight = 'fit-content';
|
||||
menu.menuElement.style.maxHeight = 'fit-content';
|
||||
}
|
||||
menu.menuElement.style.minWidth = '200px';
|
||||
this.menu.openSubMenu(menu);
|
||||
}
|
||||
|
||||
@@ -175,6 +244,10 @@ export const subMenuItems = {
|
||||
options: MenuOptions;
|
||||
disableArrow?: boolean;
|
||||
hide?: () => boolean;
|
||||
openOnHover?: boolean;
|
||||
middleware?: Middleware[];
|
||||
autoHeight?: boolean;
|
||||
closeOnSelect?: boolean;
|
||||
}) =>
|
||||
menu => {
|
||||
if (config.hide?.() || !menu.search(config.name)) {
|
||||
@@ -190,6 +263,10 @@ export const subMenuItems = {
|
||||
${config.disableArrow ? nothing : ArrowRightSmallIcon()} `,
|
||||
class: config.class,
|
||||
options: config.options,
|
||||
openOnHover: config.openOnHover,
|
||||
middleware: config.middleware,
|
||||
autoHeight: config.autoHeight,
|
||||
closeOnSelect: config.closeOnSelect,
|
||||
};
|
||||
return renderSubMenu(data, menu);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user