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:
42 files changed
+1642
-575
No files matched your search
@@ -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