diff --git a/blocksuite/affine/blocks/callout/src/callout-block.ts b/blocksuite/affine/blocks/callout/src/callout-block.ts index 1b753a3125..9252c32dae 100644 --- a/blocksuite/affine/blocks/callout/src/callout-block.ts +++ b/blocksuite/affine/blocks/callout/src/callout-block.ts @@ -6,7 +6,7 @@ import { import { DefaultInlineManagerExtension } from '@blocksuite/affine-inline-preset'; import { type CalloutBlockModel, - ParagraphBlockModel, + type ParagraphBlockModel, } from '@blocksuite/affine-model'; import { focusTextModel } from '@blocksuite/affine-rich-text'; import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '@blocksuite/affine-shared/consts'; diff --git a/blocksuite/affine/components/src/context-menu/button.ts b/blocksuite/affine/components/src/context-menu/button.ts index aab7bb9393..1ea4bdf69e 100644 --- a/blocksuite/affine/components/src/context-menu/button.ts +++ b/blocksuite/affine/components/src/context-menu/button.ts @@ -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, diff --git a/blocksuite/affine/components/src/context-menu/menu-renderer.ts b/blocksuite/affine/components/src/context-menu/menu-renderer.ts index 9976f76f30..54ad6121d2 100644 --- a/blocksuite/affine/components/src/context-menu/menu-renderer.ts +++ b/blocksuite/affine/components/src/context-menu/menu-renderer.ts @@ -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; 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; 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, }); }; diff --git a/blocksuite/affine/components/src/context-menu/sub-menu.ts b/blocksuite/affine/components/src/context-menu/sub-menu.ts index b88f1aa070..3ad785f30b 100644 --- a/blocksuite/affine/components/src/context-menu/sub-menu.ts +++ b/blocksuite/affine/components/src/context-menu/sub-menu.ts @@ -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); }, diff --git a/blocksuite/affine/data-view/src/__tests__/compare-date-keys.unit.spec.ts b/blocksuite/affine/data-view/src/__tests__/compare-date-keys.unit.spec.ts new file mode 100644 index 0000000000..876cf2531d --- /dev/null +++ b/blocksuite/affine/data-view/src/__tests__/compare-date-keys.unit.spec.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest'; + +import { compareDateKeys } from '../core/group-by/compare-date-keys.js'; + +describe('compareDateKeys', () => { + it('sorts relative keys ascending', () => { + const cmp = compareDateKeys('date-relative', true); + const keys = ['today', 'last7', 'yesterday', 'last30']; + const sorted = [...keys].sort(cmp); + expect(sorted).toEqual(['last30', 'last7', 'yesterday', 'today']); + }); + + it('sorts relative keys descending', () => { + const cmp = compareDateKeys('date-relative', false); + const keys = ['today', 'last7', 'yesterday', 'last30']; + const sorted = [...keys].sort(cmp); + expect(sorted).toEqual(['today', 'yesterday', 'last7', 'last30']); + }); + + it('sorts numeric keys correctly', () => { + const asc = compareDateKeys('date-day', true); + const desc = compareDateKeys('date-day', false); + const keys = ['3', '1', '2']; + expect([...keys].sort(asc)).toEqual(['1', '2', '3']); + expect([...keys].sort(desc)).toEqual(['3', '2', '1']); + }); + + it('handles mixed relative and numeric keys', () => { + const cmp = compareDateKeys('date-relative', true); + const keys = ['today', '1', 'yesterday', '2']; + const sorted = [...keys].sort(cmp); + expect(sorted[0]).toBe('1'); + expect(sorted[sorted.length - 1]).toBe('today'); + }); +}); diff --git a/blocksuite/affine/data-view/src/core/common/properties.ts b/blocksuite/affine/data-view/src/core/common/properties.ts index 65ce3f1746..0fb865e3a8 100644 --- a/blocksuite/affine/data-view/src/core/common/properties.ts +++ b/blocksuite/affine/data-view/src/core/common/properties.ts @@ -6,6 +6,7 @@ import { import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { InvisibleIcon, ViewIcon } from '@blocksuite/icons/lit'; import { ShadowlessElement } from '@blocksuite/std'; +import type { Middleware } from '@floating-ui/dom'; import { computed } from '@preact/signals-core'; import { cssVarV2 } from '@toeverything/theme/v2'; import { css, html, unsafeCSS } from 'lit'; @@ -235,13 +236,16 @@ export const popPropertiesSetting = ( view: SingleView; onClose?: () => void; onBack?: () => void; - } + }, + middleware?: Array ) => { - popMenu(target, { + const handler = popMenu(target, { + middleware, options: { title: { text: 'Properties', onBack: props.onBack, + onClose: props.onClose, postfix: () => { const items = props.view.propertiesRaw$.value; const isAllShowed = items.every(property => !property.hide$.value); @@ -270,8 +274,10 @@ export const popPropertiesSetting = ( ], }), ], + onClose: props.onClose, }, }); + handler.menu.menuElement.style.minHeight = '550px'; // const view = new DataViewPropertiesSettingView(); // view.view = props.view; diff --git a/blocksuite/affine/data-view/src/core/common/types.ts b/blocksuite/affine/data-view/src/core/common/types.ts index b798aa684c..19c097cc3f 100644 --- a/blocksuite/affine/data-view/src/core/common/types.ts +++ b/blocksuite/affine/data-view/src/core/common/types.ts @@ -2,6 +2,7 @@ export type GroupBy = { type: 'groupBy'; columnId: string; name: string; + hideEmpty?: boolean; sort?: { desc: boolean; }; diff --git a/blocksuite/affine/data-view/src/core/filter/add-filter.ts b/blocksuite/affine/data-view/src/core/filter/add-filter.ts index bf82d91fcd..706c9f4088 100644 --- a/blocksuite/affine/data-view/src/core/filter/add-filter.ts +++ b/blocksuite/affine/data-view/src/core/filter/add-filter.ts @@ -24,7 +24,7 @@ export const popCreateFilter = ( middleware?: Middleware[]; } ) => { - popMenu(target, { + const subHandler = popMenu(target, { middleware: ops?.middleware, options: { onClose: props.onClose, @@ -64,4 +64,5 @@ export const popCreateFilter = ( ], }, }); + subHandler.menu.menuElement.style.minHeight = '550px'; }; diff --git a/blocksuite/affine/data-view/src/core/filter/literal/define.ts b/blocksuite/affine/data-view/src/core/filter/literal/define.ts index b8b51c5327..3a2c1b662e 100644 --- a/blocksuite/affine/data-view/src/core/filter/literal/define.ts +++ b/blocksuite/affine/data-view/src/core/filter/literal/define.ts @@ -15,6 +15,7 @@ export const allLiteralConfig: LiteralItemsConfig[] = [ () => { return html` + ${displayName} + `; + } + return html`
${this.value ? CheckBoxCheckSolidIcon({ style: `color:#1E96EB` }) diff --git a/blocksuite/affine/data-view/src/core/group-by/renderer/date-group.ts b/blocksuite/affine/data-view/src/core/group-by/renderer/date-group.ts new file mode 100644 index 0000000000..148e72e862 --- /dev/null +++ b/blocksuite/affine/data-view/src/core/group-by/renderer/date-group.ts @@ -0,0 +1,54 @@ +import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; +import { ShadowlessElement } from '@blocksuite/std'; +import { css, html } from 'lit'; +import { property } from 'lit/decorators.js'; + +import type { Group } from '../trait.js'; + +export class DateGroupView extends SignalWatcher( + WithDisposable(ShadowlessElement) +) { + static override styles = css` + .dv-date-group { + border-radius: 8px; + padding: 4px 8px; + width: max-content; + cursor: default; + display: flex; + align-items: center; + gap: 6px; + } + .dv-date-group:hover { + background-color: var(--affine-hover-color); + } + .counter { + flex-shrink: 0; + min-width: 22px; + height: 22px; + border-radius: 4px; + background: var(--affine-background-secondary-color); + color: var(--affine-text-secondary-color); + font-size: var(--data-view-cell-text-size); + display: flex; + align-items: center; + justify-content: center; + } + `; + + @property({ attribute: false }) + accessor group!: Group; + + protected override render() { + const name = this.group.name$.value; + // Use contextual name based on the property when value is null + const displayName = + name || + (this.group.value === null + ? `No ${this.group.property.name$.value}` + : 'Ungroups'); + return html`
+ ${displayName} +
`; + } +} +customElements.define('data-view-date-group-view', DateGroupView); diff --git a/blocksuite/affine/data-view/src/core/group-by/renderer/number-group.ts b/blocksuite/affine/data-view/src/core/group-by/renderer/number-group.ts index ae946fa3cd..b520c31c2d 100644 --- a/blocksuite/affine/data-view/src/core/group-by/renderer/number-group.ts +++ b/blocksuite/affine/data-view/src/core/group-by/renderer/number-group.ts @@ -45,7 +45,8 @@ export class NumberGroupView extends BaseGroup> { protected override render(): unknown { if (this.value == null) { - return html`
Ungroups
`; + const displayName = `No ${this.group.property.name$.value}`; + return html`
${displayName}
`; } if (this.value >= 10) { return html`
- Ungroups + ${displayName}
`; } const style = styleMap({ diff --git a/blocksuite/affine/data-view/src/core/group-by/renderer/string-group.ts b/blocksuite/affine/data-view/src/core/group-by/renderer/string-group.ts index 1114d87c9e..26e33d4f18 100644 --- a/blocksuite/affine/data-view/src/core/group-by/renderer/string-group.ts +++ b/blocksuite/affine/data-view/src/core/group-by/renderer/string-group.ts @@ -41,7 +41,8 @@ export class StringGroupView extends BaseGroup> { protected override render(): unknown { if (!this.value) { - return html`
Ungroups
`; + const displayName = `No ${this.group.property.name$.value}`; + return html`
${displayName}
`; } return html`
{ + switch (key) { + case 'date-relative': + return 'Relative'; + case 'date-day': + return 'Day'; + case 'date-week-mon': + case 'date-week-sun': + return 'Week'; + case 'date-month': + return 'Month'; + case 'date-year': + return 'Year'; + default: + return ''; + } +}; + export class GroupSetting extends SignalWatcher( WithDisposable(ShadowlessElement) ) { @@ -39,13 +61,44 @@ export class GroupSetting extends SignalWatcher( ${unsafeCSS(dataViewCssVariable())}; } + .group-sort-setting { + display: flex; + flex-direction: column; + gap: 4px; + z-index: 1; + max-height: 200px; + overflow: hidden auto; + margin-right: 0; + margin-bottom: 0; + } + + /* WebKit-based browser scrollbar styling */ + .group-sort-setting::-webkit-scrollbar { + width: 8px; + } + + .group-sort-setting::-webkit-scrollbar-thumb { + background-color: #b0b0b0; /* Grey slider */ + border-radius: 4px; + } + + .group-sort-setting::-webkit-scrollbar-track { + background: transparent; + } + + .group-sort-setting { + scrollbar-width: thin; + scrollbar-color: #b0b0b0 transparent; + } + .group-hidden { + opacity: 0.5; + } .group-item { display: flex; padding: 4px 12px; position: relative; cursor: grab; } - .group-item-drag-bar { width: 4px; height: 12px; @@ -57,18 +110,49 @@ export class GroupSetting extends SignalWatcher( bottom: 0; margin: auto; } - .group-item:hover .group-item-drag-bar { background-color: #c0bfc1; } + .group-item-op-icon { + display: flex; + align-items: center; + border-radius: 4px; + } + .group-item-op-icon:hover { + background-color: var(--affine-hover-color); + } + .group-item-op-icon svg { + fill: var(--affine-icon-color); + color: var(--affine-icon-color); + width: 20px; + height: 20px; + } + + .group-item-name { + font-size: 14px; + line-height: 22px; + flex: 1; + } + + .properties-group-op { + padding: 4px 8px; + font-size: 12px; + line-height: 20px; + font-weight: 500; + border-radius: 4px; + cursor: pointer; + color: ${unsafeCSS(cssVarV2.button.primary)}; + } + + .properties-group-op:hover { + background-color: var(--affine-hover-color); + } `; @property({ attribute: false }) accessor groupTrait!: GroupTrait; - groups$ = computed(() => { - return this.groupTrait.groupsDataList$.value; - }); + groups$ = computed(() => this.groupTrait.groupsDataListAll$.value); sortContext = createSortContext({ activators: defaultActivators, @@ -78,99 +162,101 @@ export class GroupSetting extends SignalWatcher( const activeId = evt.active.id; const groups = this.groups$.value; if (over && over.id !== activeId && groups) { - const activeIndex = groups.findIndex(data => data?.key === activeId); - const overIndex = groups.findIndex(data => data?.key === over.id); - + const aIndex = groups.findIndex(g => g?.key === activeId); + const oIndex = groups.findIndex(g => g?.key === over.id); this.groupTrait.moveGroupTo( activeId, - activeIndex > overIndex - ? { - before: true, - id: over.id, - } - : { - before: false, - id: over.id, - } + aIndex > oIndex + ? { before: true, id: over.id } + : { before: false, id: over.id } ); } }, - modifiers: [ - ({ transform }) => { - return { - ...transform, - x: 0, - }; - }, - ], - items: computed(() => { - return ( - this.groupTrait.groupsDataList$.value?.map( - v => v?.key ?? 'default key' - ) ?? [] - ); - }), + modifiers: [({ transform }) => ({ ...transform, x: 0 })], + items: computed( + () => + this.groupTrait.groupsDataListAll$.value?.map(v => v?.key ?? '') ?? [] + ), strategy: verticalListSortingStrategy, }); override connectedCallback() { super.connectedCallback(); - this._disposables.addFromEvent(this, 'pointerdown', e => { - e.stopPropagation(); - }); + this._disposables.addFromEvent(this, 'pointerdown', e => + e.stopPropagation() + ); } - protected override render(): unknown { - const groups = this.groupTrait.groupsDataList$.value; - if (!groups) { - return; - } + protected override render() { + const groups = this.groupTrait.groupsDataListAll$.value; + if (!groups) return; + const map = this.groupTrait.groupDataMap$.value; + const isAllShowed = map + ? Object.keys(map).every(k => !this.groupTrait.isGroupHidden(k)) + : true; + const clickChangeAll = () => { + if (!map) return; + Object.keys(map).forEach(key => { + this.groupTrait.setGroupHide(key, isAllShowed); + }); + }; return html` -
+
Groups
-
+
+ ${isAllShowed ? 'Hide All' : 'Show All'} +
-
+ +
${repeat( groups, - group => group?.key ?? 'default key', - group => { - const type = group.property.dataType$.value; + g => g?.key ?? 'k', + g => { + if (!g) return; + const type = g.property.dataType$.value; if (!type) return; - const props: GroupRenderProps = { - group, - readonly: true, - }; - return html`
-
+ const props: GroupRenderProps = { group: g, readonly: true }; + const icon = g.hide$.value ? InvisibleIcon() : ViewIcon(); + return html`
- ${renderUniLit(group.view, props)} +
+ class="group-item-name" + style="padding:0 4px;position:relative;pointer-events:none;max-width:330px;" + > + ${renderUniLit(g.view, props)} +
+
+
+ ${icon} +
-
`; + `; } )}
`; } - @query('.group-sort-setting') - accessor groupContainer!: HTMLElement; + @query('.group-sort-setting') accessor groupContainer!: HTMLElement; } export const selectGroupByProperty = ( @@ -184,10 +270,7 @@ export const selectGroupByProperty = ( const view = group.view; return { onClose: ops?.onClose, - title: { - text: 'Group by', - onBack: ops?.onBack, - }, + title: { text: 'Group by', onBack: ops?.onBack, onClose: ops?.onClose }, items: [ menu.group({ items: view.propertiesRaw$.value @@ -219,7 +302,7 @@ export const selectGroupByProperty = ( menu.action({ prefix: DeleteIcon(), hide: () => - view instanceof KanbanSingleView || group.property$.value == null, + view instanceof KanbanSingleView || !group.property$.value, class: { 'delete-item': true }, name: 'Remove Grouping', select: () => { @@ -232,77 +315,305 @@ export const selectGroupByProperty = ( ], }; }; + export const popSelectGroupByProperty = ( target: PopupTarget, group: GroupTrait, - ops?: { - onSelect?: () => void; - onClose?: () => void; - onBack?: () => void; - } + ops?: { onSelect?: () => void; onClose?: () => void; onBack?: () => void }, + middleware?: Array ) => { - popMenu(target, { + const handler = popMenu(target, { options: selectGroupByProperty(group, ops), + middleware, }); + handler.menu.menuElement.style.minHeight = '550px'; }; + export const popGroupSetting = ( target: PopupTarget, group: GroupTrait, - onBack: () => void + onBack: () => void, + onClose?: () => void, + middleware?: Array ) => { const view = group.view; - const groupProperty = group.property$.value; - if (groupProperty == null) { - return; - } - const type = groupProperty.type$.value; - if (!type) { - return; - } - const icon = groupProperty.icon; + const gProp = group.property$.value; + if (!gProp) return; + const type = gProp.type$.value; + if (!type) return; + + const icon = gProp.icon; const menuHandler = popMenu(target, { options: { title: { text: 'Group', - onBack: onBack, + onBack, + onClose, }, items: [ menu.group({ items: [ - menu.subMenu({ + menu.action({ name: 'Group By', postfix: html`
- ${renderUniLit(icon, {})} ${groupProperty.name$.value} + ${renderUniLit(icon, {})} ${gProp.name$.value}
`, - label: () => html` -
- Group By -
- `, - options: selectGroupByProperty(group, { - onSelect: () => { - menuHandler.close(); - popGroupSetting(target, group, onBack); + select: () => { + const subHandler = popMenu(target, { + options: selectGroupByProperty(group, { + onSelect: () => { + menuHandler.close(); + popGroupSetting( + target, + group, + onBack, + onClose, + middleware + ); + }, + onBack: () => { + menuHandler.close(); + popGroupSetting( + target, + group, + onBack, + onClose, + middleware + ); + }, + onClose, + }), + middleware: [ + autoPlacement({ + allowedPlacements: ['bottom-start', 'top-start'], + }), + offset({ mainAxis: 15, crossAxis: -162 }), + shift({ crossAxis: true }), + ], + }); + subHandler.menu.menuElement.style.minHeight = '550px'; + }, + }), + ], + }), + + ...(type === 'date' + ? [ + menu.group({ + items: [ + menu.dynamic(() => [ + menu.subMenu({ + name: 'Date by', + openOnHover: false, + middleware: dropdownSubMenuMiddleware, + autoHeight: true, + postfix: html` +
+ ${dateModeLabel(group.groupInfo$.value?.config.name)} +
+ `, + options: { + items: [ + menu.dynamic(() => + ( + [ + ['Relative', 'date-relative'], + ['Day', 'date-day'], + [ + 'Week', + group.groupInfo$.value?.config.name === + 'date-week-mon' + ? 'date-week-mon' + : 'date-week-sun', + ], + ['Month', 'date-month'], + ['Year', 'date-year'], + ] as [string, string][] + ).map( + ([label, key]): MenuConfig => + menu.action({ + name: label, + label: () => { + const isSelected = + group.groupInfo$.value?.config.name === + key; + return html`${label}`; + }, + isSelected: + group.groupInfo$.value?.config.name === key, + select: () => { + group.changeGroupMode(key); + return false; + }, + }) + ) + ), + ], + }, + }), + ]), + ], + }), + + ...(group.groupInfo$.value?.config.name?.startsWith('date-week') + ? [ + menu.group({ + items: [ + menu.dynamic(() => [ + menu.subMenu({ + name: 'Start week on', + postfix: html` +
+ ${group.groupInfo$.value?.config.name === + 'date-week-mon' + ? 'Monday' + : 'Sunday'} +
+ `, + options: { + items: [ + menu.dynamic(() => + ( + [ + ['Monday', 'date-week-mon'], + ['Sunday', 'date-week-sun'], + ] as [string, string][] + ).map(([label, key]) => + menu.action({ + name: label, + label: () => { + const isSelected = + group.groupInfo$.value?.config + .name === key; + return html`${label}`; + }, + isSelected: + group.groupInfo$.value?.config.name === + key, + select: () => { + group.changeGroupMode(key); + return false; + }, + }) + ) + ), + ], + }, + }), + ]), + ], + }), + ] + : []), + menu.group({ + items: [ + menu.dynamic(() => [ + menu.subMenu({ + name: 'Sort', + openOnHover: false, + middleware: dropdownSubMenuMiddleware, + autoHeight: true, + postfix: html` +
+ ${group.sortAsc$.value + ? 'Oldest first' + : 'Newest first'} +
+ `, + options: { + items: [ + menu.dynamic(() => [ + menu.action({ + name: 'Oldest first', + label: () => { + const isSelected = group.sortAsc$.value; + return html`Oldest first`; + }, + isSelected: group.sortAsc$.value, + select: () => { + group.setDateSortOrder(true); + return false; + }, + }), + menu.action({ + name: 'Newest first', + label: () => { + const isSelected = !group.sortAsc$.value; + return html`Newest first`; + }, + isSelected: !group.sortAsc$.value, + select: () => { + group.setDateSortOrder(false); + return false; + }, + }), + ]), + ], + }, + }), + ]), + ], + }), + ] + : []), + + menu.group({ + items: [ + menu.dynamic(() => [ + menu.action({ + name: 'Hide empty groups', + isSelected: group.hideEmpty$.value, + select: () => { + group.setHideEmpty(!group.hideEmpty$.value); + return false; }, }), - }), + ]), ], }), menu.group({ items: [ - menu => - html` `, + menu => html` + menu.closeSubMenu()} + .groupTrait=${group} + .columnId=${gProp.id} + > + `, ], }), + menu.group({ items: [ menu.action({ @@ -312,11 +623,14 @@ export const popGroupSetting = ( hide: () => !(view instanceof TableSingleView), select: () => { group.changeGroup(undefined); + return false; }, }), ], }), ], }, + middleware, }); + menuHandler.menu.menuElement.style.minHeight = '550px'; }; diff --git a/blocksuite/affine/data-view/src/core/group-by/trait.ts b/blocksuite/affine/data-view/src/core/group-by/trait.ts index 11ed6fc507..14b8055bc1 100644 --- a/blocksuite/affine/data-view/src/core/group-by/trait.ts +++ b/blocksuite/affine/data-view/src/core/group-by/trait.ts @@ -2,7 +2,12 @@ import { insertPositionToIndex, type InsertToPosition, } from '@blocksuite/affine-shared/utils'; -import { computed, type ReadonlySignal } from '@preact/signals-core'; +import { + computed, + effect, + type ReadonlySignal, + signal, +} from '@preact/signals-core'; import type { GroupBy, GroupProperty } from '../common/types.js'; import type { TypeInstance } from '../logical/type.js'; @@ -11,8 +16,10 @@ import { computedLock } from '../utils/lock.js'; import type { Property } from '../view-manager/property.js'; import type { Row } from '../view-manager/row.js'; import type { SingleView } from '../view-manager/single-view.js'; +import { compareDateKeys } from './compare-date-keys.js'; import { defaultGroupBy } from './default.js'; -import { getGroupByService } from './matcher.js'; +import { findGroupByConfigByName, getGroupByService } from './matcher.js'; +// Test import type { GroupByConfig } from './types.js'; export type GroupInfo< @@ -42,138 +49,71 @@ export class Group< get property() { return this.groupInfo.property; } - name$ = computed(() => { const type = this.property.dataType$.value; - if (!type) { - return ''; - } - return this.groupInfo.config.groupName(type, this.value); + return type ? this.groupInfo.config.groupName(type, this.value) : ''; }); - private get config() { return this.groupInfo.config; } - get tType() { return this.groupInfo.tType; } - get view() { return this.config.view; } + + hide$ = computed(() => { + const groupHide = + this.manager.groupPropertiesMap$.value[this.key]?.hide ?? false; + const emptyHidden = this.manager.hideEmpty$.value && this.rows.length === 0; + return groupHide || emptyHidden; + }); + + hideSet(hide: boolean) { + this.manager.setGroupHide(this.key, hide); + } +} + +function hasGroupProperties( + data: unknown +): data is { groupProperties?: GroupProperty[] } { + if (typeof data !== 'object' || data === null) { + return false; + } + if (!('groupProperties' in data)) { + return false; + } + const value = (data as { groupProperties?: unknown }).groupProperties; + return value === undefined || Array.isArray(value); } export class GroupTrait { - groupInfo$ = computed(() => { - const groupBy = this.groupBy$.value; - if (!groupBy) { - return; - } - const property = this.view.propertyGetOrCreate(groupBy.columnId); - if (!property) { - return; - } - const tType = property.dataType$.value; - if (!tType) { - return; - } - const groupByService = getGroupByService(this.view.manager.dataSource); - const result = groupByService?.matcher.match(tType); - if (!result) { - return; - } - return { - config: result, - property, - tType: tType, - }; + hideEmpty$ = signal(true); + sortAsc$ = signal(true); + + groupProperties$ = computed(() => { + const data = this.view.data$.value; + return hasGroupProperties(data) ? (data.groupProperties ?? []) : []; }); - staticInfo$ = computed(() => { - const groupInfo = this.groupInfo$.value; - if (!groupInfo) { - return; - } - const staticMap = Object.fromEntries( - groupInfo.config - .defaultKeys(groupInfo.tType) - .map(({ key, value }) => [key, new Group(key, value, groupInfo, this)]) - ); - return { - staticMap, - groupInfo, - }; - }); - - groupDataMap$ = computed(() => { - const staticInfo = this.staticInfo$.value; - if (!staticInfo) { - return; - } - const { staticMap, groupInfo } = staticInfo; - const groupMap: Record = {}; - Object.entries(staticMap).forEach(([key, group]) => { - groupMap[key] = new Group(key, group.value, groupInfo, this); + groupPropertiesMap$ = computed(() => { + const map: Record = {}; + this.groupProperties$.value.forEach(g => { + map[g.key] = g; }); - this.view.rows$.value.forEach(row => { - const value = this.view.cellGetOrCreate(row.rowId, groupInfo.property.id) - .jsonValue$.value; - const keys = groupInfo.config.valuesGroup(value, groupInfo.tType); - keys.forEach(({ key, value }) => { - if (!groupMap[key]) { - groupMap[key] = new Group(key, value, groupInfo, this); - } - groupMap[key].rows.push(row); - }); - }); - return groupMap; - }); - - groupsDataList$ = computedLock( - computed(() => { - const groupMap = this.groupDataMap$.value; - if (!groupMap) { - return; - } - const sortedGroup = this.ops.sortGroup(Object.keys(groupMap)); - sortedGroup.forEach(key => { - if (!groupMap[key]) return; - groupMap[key].rows = this.ops.sortRow(key, groupMap[key].rows); - }); - return sortedGroup - .map(key => groupMap[key]) - .filter((v): v is Group => v != null); - }), - this.view.isLocked$ - ); - - updateData = (data: NonNullable) => { - const property = this.property$.value; - if (!property) { - return; - } - this.view.propertyGetOrCreate(property.id).dataUpdate(() => data); - }; - - get addGroup() { - return this.property$.value?.meta$.value?.config.addGroup; - } - - property$ = computed(() => { - const groupInfo = this.groupInfo$.value; - if (!groupInfo) { - return; - } - return groupInfo.property; + return map; }); + /** + * Synchronize sortAsc$ with the GroupBy sort descriptor + */ constructor( private readonly groupBy$: ReadonlySignal, public view: SingleView, private readonly ops: { - groupBySet: (groupBy: GroupBy | undefined) => void; - sortGroup: (keys: string[]) => string[]; + groupBySet: (g: GroupBy | undefined) => void; + sortGroup: (keys: string[], asc?: boolean) => string[]; sortRow: (groupKey: string, rows: Row[]) => Row[]; changeGroupSort: (keys: string[]) => void; changeRowSort: ( @@ -181,11 +121,188 @@ export class GroupTrait { groupKey: string, keys: string[] ) => void; + changeGroupHide?: (key: string, hide: boolean) => void; } - ) {} + ) { + effect(() => { + const desc = this.groupBy$.value?.sort?.desc; + if (desc != null && this.sortAsc$.value === desc) { + this.sortAsc$.value = !desc; + } + }); + + // Sync hideEmpty state with GroupBy data + effect(() => { + const hide = this.groupBy$.value?.hideEmpty; + if (hide != null && this.hideEmpty$.value !== hide) { + this.hideEmpty$.value = hide; + } + }); + } + + groupInfo$ = computed(() => { + const groupBy = this.groupBy$.value; + if (!groupBy) return; + + const property = this.view.propertyGetOrCreate(groupBy.columnId); + if (!property) return; + + const tType = property.dataType$.value; + if (!tType) return; + + const svc = getGroupByService(this.view.manager.dataSource); + const res = + groupBy.name != null + ? (findGroupByConfigByName( + this.view.manager.dataSource, + groupBy.name + ) ?? svc?.matcher.match(tType)) + : svc?.matcher.match(tType); + + if (!res) return; + return { config: res, property, tType }; + }); + + staticInfo$ = computed(() => { + const info = this.groupInfo$.value; + if (!info) return; + const staticMap = Object.fromEntries( + info.config + .defaultKeys(info.tType) + .map(({ key, value }) => [key, new Group(key, value, info, this)]) + ); + return { staticMap, groupInfo: info }; + }); + + groupDataMap$ = computed(() => { + const si = this.staticInfo$.value; + if (!si) return; + const { staticMap, groupInfo } = si; + // Create fresh Group instances with empty rows arrays + const map: Record = {}; + Object.entries(staticMap).forEach(([key, group]) => { + map[key] = new Group(key, group.value, groupInfo, this); + }); + // Assign rows to their respective groups + this.view.rows$.value.forEach(row => { + const cell = this.view.cellGetOrCreate(row.rowId, groupInfo.property.id); + const jv = cell.jsonValue$.value; + const keys = groupInfo.config.valuesGroup(jv, groupInfo.tType); + keys.forEach(({ key, value }) => { + if (!map[key]) map[key] = new Group(key, value, groupInfo, this); + map[key].rows.push(row); + }); + }); + return map; + }); + + groupsDataList$ = computedLock( + computed(() => { + const map = this.groupDataMap$.value; + if (!map) return; + + const gi = this.groupInfo$.value; + let ordered: string[]; + + if (gi?.config.matchType.name === 'Date') { + ordered = Object.keys(map).sort( + compareDateKeys(gi.config.name, this.sortAsc$.value) + ); + } else { + ordered = this.ops.sortGroup(Object.keys(map), this.sortAsc$.value); + } + + return ordered + .map(k => map[k]) + .filter( + (g): g is Group => + !!g && + !this.isGroupHidden(g.key) && + (!this.hideEmpty$.value || g.rows.length > 0) + ); + }), + this.view.isLocked$ + ); + + /** + * Computed list of groups including hidden ones, used by settings UI. + */ + groupsDataListAll$ = computedLock( + computed(() => { + const map = this.groupDataMap$.value; + const info = this.groupInfo$.value; + if (!map || !info) return; + + let orderedKeys: string[]; + if (info.config.matchType.name === 'Date') { + orderedKeys = Object.keys(map).sort( + compareDateKeys(info.config.name, this.sortAsc$.value) + ); + } else { + orderedKeys = this.ops.sortGroup(Object.keys(map), this.sortAsc$.value); + } + + const visible: Group[] = []; + const hidden: Group[] = []; + orderedKeys + .map(key => map[key]) + .filter((g): g is Group => g != null) + .forEach(g => { + if (g.hide$.value) { + hidden.push(g); + } else { + visible.push(g); + } + }); + return [...visible, ...hidden]; + }), + this.view.isLocked$ + ); + + /** Whether all groups are currently hidden */ + allHidden$ = computed(() => { + const map = this.groupDataMap$.value; + if (!map) return false; + return Object.keys(map).every(key => this.isGroupHidden(key)); + }); + + /** + * Toggle hiding of empty groups. + */ + + setHideEmpty(value: boolean) { + this.hideEmpty$.value = value; + const gb = this.groupBy$.value; + if (gb) { + this.ops.groupBySet({ ...gb, hideEmpty: value }); + } + } + + isGroupHidden(key: string): boolean { + return this.groupPropertiesMap$.value[key]?.hide ?? false; + } + + setGroupHide(key: string, hide: boolean) { + this.ops.changeGroupHide?.(key, hide); + } + + /** + * Set sort order for date groupings and update GroupBy sort descriptor. + */ + setDateSortOrder(asc: boolean) { + this.sortAsc$.value = asc; + + const gb = this.groupBy$.value; + if (gb) { + this.ops.groupBySet({ + ...gb, + sort: { desc: !asc }, + hideEmpty: gb.hideEmpty, + }); + } + } addToGroup(rowId: string, key: string) { - this.view.lockRows(false); const groupMap = this.groupDataMap$.value; const groupInfo = this.groupInfo$.value; if (!groupMap || !groupInfo) { @@ -205,18 +322,34 @@ export class GroupTrait { .cellGetOrCreate(rowId, groupInfo.property.id) .valueSet(newValue); } - } + const map = this.groupDataMap$.value; + const info = this.groupInfo$.value; + if (!map || !info) return; - changeCardSort(groupKey: string, cardIds: string[]) { - const groups = this.groupsDataList$.value; - if (!groups) { - return; - } - this.ops.changeRowSort( - groups.map(v => v.key), - groupKey, - cardIds + const addFn = info.config.addToGroup; + if (addFn === false) return; + + const group = map[key]; + if (!group) return; + + const current = group.value; + // Handle both null and non-null values to ensure proper group assignment + const newVal = addFn( + current, + this.view.cellGetOrCreate(rowId, info.property.id).jsonValue$.value ); + this.view.cellGetOrCreate(rowId, info.property.id).valueSet(newVal); + } + changeGroupMode(modeName: string) { + const propId = this.property$.value?.id; + if (!propId) return; + this.ops.groupBySet({ + type: 'groupBy', + columnId: propId, + name: modeName, + sort: { desc: !this.sortAsc$.value }, + hideEmpty: this.hideEmpty$.value, + }); } changeGroup(columnId: string | undefined) { @@ -225,31 +358,38 @@ export class GroupTrait { return; } const column = this.view.propertyGetOrCreate(columnId); - const propertyMeta = this.view.manager.dataSource.propertyMetaGet( + const meta = this.view.manager.dataSource.propertyMetaGet( column.type$.value ); - if (propertyMeta) { - this.ops.groupBySet( - defaultGroupBy( - this.view.manager.dataSource, - propertyMeta, - column.id, - column.data$.value - ) + if (meta) { + const gb = defaultGroupBy( + this.view.manager.dataSource, + meta, + column.id, + column.data$.value ); + if (gb) { + gb.sort = { desc: !this.sortAsc$.value }; + gb.hideEmpty = this.hideEmpty$.value; + } + this.ops.groupBySet(gb); } } - changeGroupSort(keys: string[]) { - this.ops.changeGroupSort(keys); + property$ = computed(() => this.groupInfo$.value?.property); + + get addGroup() { + return this.property$.value?.meta$.value?.config.addGroup; } - defaultGroupProperty(key: string): GroupProperty { - return { - key, - hide: false, - manuallyCardSort: [], - }; + updateData = (data: NonNullable) => { + const prop = this.property$.value; + if (!prop) return; + this.view.propertyGetOrCreate(prop.id).dataUpdate(() => data); + }; + + changeGroupSort(keys: string[]) { + this.ops.changeGroupSort(keys); } moveCardTo( @@ -258,7 +398,6 @@ export class GroupTrait { toGroupKey: string, position: InsertToPosition ) { - this.view.lockRows(false); const groupMap = this.groupDataMap$.value; if (!groupMap) { return; @@ -291,16 +430,16 @@ export class GroupTrait { .map(row => row.rowId) ?? []; const index = insertPositionToIndex(position, rows, row => row); rows.splice(index, 0, rowId); - this.changeCardSort(toGroupKey, rows); + const groupKeys = Object.keys(groupMap); + this.ops.changeRowSort(groupKeys, toGroupKey, rows); } moveGroupTo(groupKey: string, position: InsertToPosition) { - this.view.lockRows(false); const groups = this.groupsDataList$.value; if (!groups) { return; } - const keys = groups.map(v => v.key); + const keys = groups.map(v => v!.key); keys.splice( keys.findIndex(key => key === groupKey), 1 @@ -311,7 +450,6 @@ export class GroupTrait { } removeFromGroup(rowId: string, key: string) { - this.view.lockRows(false); const groupMap = this.groupDataMap$.value; if (!groupMap) { return; @@ -330,7 +468,6 @@ export class GroupTrait { } updateValue(rows: string[], value: unknown) { - this.view.lockRows(false); const propertyId = this.property$.value?.id; if (!propertyId) { return; diff --git a/blocksuite/affine/data-view/src/core/sort/add-sort.ts b/blocksuite/affine/data-view/src/core/sort/add-sort.ts index cf0d1cd321..40c8c0b217 100644 --- a/blocksuite/affine/data-view/src/core/sort/add-sort.ts +++ b/blocksuite/affine/data-view/src/core/sort/add-sort.ts @@ -3,6 +3,7 @@ import { popMenu, type PopupTarget, } from '@blocksuite/affine-components/context-menu'; +import type { Middleware } from '@floating-ui/dom'; import { renderUniLit } from '../utils/index.js'; import type { SortUtils } from './utils.js'; @@ -13,9 +14,13 @@ export const popCreateSort = ( sortUtils: SortUtils; onClose?: () => void; onBack?: () => void; + }, + ops?: { + middleware?: Middleware[]; } ) => { - popMenu(target, { + const subHandler = popMenu(target, { + middleware: ops?.middleware, options: { onClose: props.onClose, title: { @@ -50,4 +55,5 @@ export const popCreateSort = ( ], }, }); + subHandler.menu.menuElement.style.minHeight = '550px'; }; diff --git a/blocksuite/affine/data-view/src/core/view-manager/single-view.ts b/blocksuite/affine/data-view/src/core/view-manager/single-view.ts index 8d2e6ce809..0ecc69720f 100644 --- a/blocksuite/affine/data-view/src/core/view-manager/single-view.ts +++ b/blocksuite/affine/data-view/src/core/view-manager/single-view.ts @@ -20,6 +20,7 @@ export type MainProperties = { }; export interface SingleView { + data$: any; readonly id: string; readonly type: string; readonly manager: ViewManager; diff --git a/blocksuite/affine/data-view/src/property-presets/date/cell-renderer-css.ts b/blocksuite/affine/data-view/src/property-presets/date/cell-renderer-css.ts index 82573604b2..1a9b47be94 100644 --- a/blocksuite/affine/data-view/src/property-presets/date/cell-renderer-css.ts +++ b/blocksuite/affine/data-view/src/property-presets/date/cell-renderer-css.ts @@ -23,7 +23,7 @@ export const dateValueContainerStyle = css({ color: 'var(--text-secondary)', fontSize: '17px', lineHeight: '22px', - height: '46px', + height: '30px', }); export const datePickerContainerStyle = css({ diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/kanban-view-manager.ts b/blocksuite/affine/data-view/src/view-presets/kanban/kanban-view-manager.ts index afd71de607..105f8977d1 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/kanban-view-manager.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/kanban-view-manager.ts @@ -74,12 +74,15 @@ export class KanbanSingleView extends SingleViewBase { }; }); }, - sortGroup: ids => - sortByManually( + sortGroup: (ids, asc) => { + const sorted = sortByManually( ids, v => v, this.view?.groupProperties.map(v => v.key) ?? [] - ), + ); + // If descending order is requested, reverse the sorted array + return asc === false ? sorted.reverse() : sorted; + }, sortRow: (key, rows) => { const property = this.view?.groupProperties.find(v => v.key === key); return sortByManually( @@ -136,6 +139,33 @@ export class KanbanSingleView extends SingleViewBase { }; }); }, + changeGroupHide: (key, hide) => { + this.dataUpdate(() => { + const list = [...(this.view?.groupProperties ?? [])]; + const idx = list.findIndex(g => g.key === key); + if (idx >= 0) { + const target = list[idx]; + if (!target) { + return { groupProperties: list }; + } + list[idx] = { ...target, hide }; + } else { + // maintain existing order when inserting a new entry + const order = (this.groupTrait.groupsDataListAll$.value ?? []) + .map(g => g?.key) + .filter((k): k is string => typeof k === 'string'); + let insertPos = 0; + for (const k of order) { + if (k === key) break; + if (list.findIndex(g => g.key === k) !== -1) { + insertPos++; + } + } + list.splice(insertPos, 0, { key, hide, manuallyCardSort: [] }); + } + return { groupProperties: list }; + }); + }, }) ); diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/mobile/kanban-view-ui-logic.ts b/blocksuite/affine/data-view/src/view-presets/kanban/mobile/kanban-view-ui-logic.ts index 48aa745f2f..8df610cc0f 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/mobile/kanban-view-ui-logic.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/mobile/kanban-view-ui-logic.ts @@ -136,6 +136,9 @@ export class MobileKanbanViewUI extends DataViewUIBase if (!groups) { return html``; } + const groupEntries = groups.filter( + (group): group is NonNullable<(typeof groups)[number]> => group != null + ); const vPadding = this.logic.root.config.virtualPadding$.value; const wrapperStyle = styleMap({ marginLeft: `-${vPadding}px`, @@ -149,7 +152,7 @@ export class MobileKanbanViewUI extends DataViewUIBase })}
${repeat( - groups, + groupEntries, group => group.key, group => { return html` => v != null + ); popFilterableSimpleMenu(ele, [ menu.group({ items: [ @@ -47,12 +50,10 @@ export const popCardMenu = ( prefix: ArrowRightBigIcon(), options: { items: - groupTrait.groupsDataList$.value - ?.filter(v => { - return v.key !== groupKey; - }) - .map(group => { - return menu.action({ + groups + .filter(v => v.key !== groupKey) + .map(group => + menu.action({ name: group.value != null ? group.name$.value : 'Ungroup', select: () => { groupTrait.moveCardTo( @@ -62,8 +63,8 @@ export const popCardMenu = ( 'start' ); }, - }); - }) ?? [], + }) + ) ?? [], }, }), ], diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/pc/kanban-view-ui-logic.ts b/blocksuite/affine/data-view/src/view-presets/kanban/pc/kanban-view-ui-logic.ts index b4c8c6fac8..e40e5e69b5 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/pc/kanban-view-ui-logic.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/pc/kanban-view-ui-logic.ts @@ -202,7 +202,11 @@ export class KanbanViewUI extends DataViewUIBase { return html``; } - return html`${groups.map(group => { + const safeGroups = groups.filter( + (group): group is NonNullable<(typeof groups)[number]> => group != null + ); + + return html`${safeGroups.map(group => { return html` { } override render(): TemplateResult { - const groups = this.logic.groups$.value; - if (!groups) { + const groups = this.logic.groups$.value?.filter( + ( + group + ): group is NonNullable<(typeof this.logic.groups$.value)[number]> => + group != null + ); + if (!groups || groups.length === 0) { return html``; } diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/pc/menu.ts b/blocksuite/affine/data-view/src/view-presets/kanban/pc/menu.ts index 7246496362..8a14639054 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/pc/menu.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/pc/menu.ts @@ -37,6 +37,9 @@ export const popCardMenu = ( rowId: string, selection: KanbanSelectionController ) => { + const groups = (selection.view.groupTrait.groupsDataList$.value ?? []).filter( + (v): v is NonNullable => v != null + ); popFilterableSimpleMenu(ele, [ menu.action({ name: 'Expand Card', @@ -50,22 +53,23 @@ export const popCardMenu = ( prefix: ArrowRightBigIcon(), options: { items: - selection.view.groupTrait.groupsDataList$.value - ?.filter(v => { + groups + .filter(v => { const cardSelection = selection.selection; if (cardSelection?.selectionType === 'card') { - return v.key !== cardSelection?.cards[0].groupKey; + const currentGroup = cardSelection.cards[0]?.groupKey; + return currentGroup ? v.key !== currentGroup : true; } return false; }) - .map(group => { - return menu.action({ + .map(group => + menu.action({ name: group.value != null ? group.name$.value : 'Ungroup', select: () => { selection.moveCard(rowId, group.key); }, - }); - }) ?? [], + }) + ) ?? [], }, }), menu.group({ diff --git a/blocksuite/affine/data-view/src/view-presets/table/mobile/table-view-ui-logic.ts b/blocksuite/affine/data-view/src/view-presets/table/mobile/table-view-ui-logic.ts index b18626bfd9..6026e3b73f 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/mobile/table-view-ui-logic.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/mobile/table-view-ui-logic.ts @@ -108,10 +108,13 @@ export class MobileTableViewUI extends DataViewUIBase { private renderTable() { const groups = this.logic.view.groupTrait.groupsDataList$.value; if (groups) { + const groupEntries = groups.filter( + (group): group is NonNullable<(typeof groups)[number]> => group != null + ); return html`
${repeat( - groups, + groupEntries, v => v.key, group => { return html` { - return this.tableViewLogic.groupTrait$.value?.groupsDataList$.value?.find( - g => g.key === this.gridGroup.groupId - ); + const groups = + this.tableViewLogic.groupTrait$.value?.groupsDataList$.value ?? []; + return groups + .filter((group): group is NonNullable => group != null) + .find(g => g.key === this.gridGroup.groupId); }); get selectionController() { diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/group-header.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/group-header.ts index 29411f9f23..c74e88a021 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/group-header.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/group-header.ts @@ -35,9 +35,11 @@ export class TableGroupHeader extends SignalWatcher( } group$ = computed(() => { - return this.tableViewLogic.groupTrait$.value?.groupsDataList$.value?.find( - g => g.key === this.gridGroup.groupId - ); + const groups = + this.tableViewLogic.groupTrait$.value?.groupsDataList$.value ?? []; + return groups + .filter((group): group is NonNullable => group != null) + .find(g => g.key === this.gridGroup.groupId); }); groupKey$ = computed(() => { diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/table-view-ui-logic.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/table-view-ui-logic.ts index dafce47b8d..ff59f3f2cd 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/table-view-ui-logic.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/table-view-ui-logic.ts @@ -95,7 +95,14 @@ export class VirtualTableViewUILogic extends DataViewUILogicBase< }, ]; } - return groupTrait.groupsDataList$.value.map(group => ({ + const groups = groupTrait.groupsDataList$.value.filter( + ( + group + ): group is NonNullable< + (typeof groupTrait.groupsDataList$.value)[number] + > => group != null + ); + return groups.map(group => ({ id: group.key, rows: group.rows.map(v => v.rowId), })); diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc/table-view-style.ts b/blocksuite/affine/data-view/src/view-presets/table/pc/table-view-style.ts index ecf17b9929..e05d035f91 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc/table-view-style.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc/table-view-style.ts @@ -92,6 +92,17 @@ export const addGroupIconStyle = css({ fill: 'var(--affine-icon-color)', }, }); +export const groupsHiddenMessageStyle = css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + height: '80px', + zIndex: 0, + color: 'var(--affine-text-secondary-color)', + fontSize: '14px', + textAlign: 'center', +}); const cellDividerStyle = css({ width: '1px', height: '100%', diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc/table-view-ui-logic.ts b/blocksuite/affine/data-view/src/view-presets/table/pc/table-view-ui-logic.ts index 3b2d516876..fe186de2ee 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc/table-view-ui-logic.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc/table-view-ui-logic.ts @@ -12,7 +12,7 @@ import { repeat } from 'lit/directives/repeat.js'; import { styleMap } from 'lit/directives/style-map.js'; import { html } from 'lit/static-html.js'; -import type { GroupTrait } from '../../../core/group-by/trait.js'; +import type { Group, GroupTrait } from '../../../core/group-by/trait.js'; import { createUniComponentFromWebComponent, renderUniLit, @@ -30,6 +30,7 @@ import { TableSelectionController } from './controller/selection.js'; import { addGroupIconStyle, addGroupStyle, + groupsHiddenMessageStyle, tableGroupsContainerStyle, tableScrollContainerStyle, tableViewStyle, @@ -154,26 +155,27 @@ export class TableViewUI extends DataViewUIBase { } private renderTable() { - const groups = this.logic.view.groupTrait.groupsDataList$.value; - if (groups) { + const groups = this.logic.view.groupTrait.groupsDataList$.value?.filter( + (g): g is Group => g !== undefined + ); + if (groups && groups.length) { return html`
${repeat( groups, - v => v.key, - group => { - return html` group.key, + group => + html``; - } + >` )} ${this.logic.renderAddGroup(this.logic.view.groupTrait)}
`; } - return html` `; } @@ -205,7 +207,11 @@ export class TableViewUI extends DataViewUIBase { class="affine-database-table-container" style="${containerStyle}" > - ${this.renderTable()} + ${this.logic.view.groupTrait.allHidden$.value + ? html`
+ All groups are hidden +
` + : this.renderTable()}
diff --git a/blocksuite/affine/data-view/src/view-presets/table/table-view-manager.ts b/blocksuite/affine/data-view/src/view-presets/table/table-view-manager.ts index 86f1bf18d2..b4413402d5 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/table-view-manager.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/table-view-manager.ts @@ -101,12 +101,15 @@ export class TableSingleView extends SingleViewBase { }; }); }, - sortGroup: ids => - sortByManually( + sortGroup: (ids, asc) => { + const sorted = sortByManually( ids, v => v, this.groupProperties.map(v => v.key) - ), + ); + // If descending order is requested, reverse the sorted array + return asc === false ? sorted.reverse() : sorted; + }, sortRow: (key, rows) => { const property = this.groupProperties.find(v => v.key === key); return sortByManually( @@ -163,6 +166,30 @@ export class TableSingleView extends SingleViewBase { }; }); }, + changeGroupHide: (key, hide) => { + this.dataUpdate(() => { + const list = [...this.groupProperties]; + const idx = list.findIndex(g => g.key === key); + if (idx >= 0) { + const target = list[idx]; + if (!target) { + return { groupProperties: list }; + } + list[idx] = { ...target, hide }; + } else { + const order = (this.groupTrait.groupsDataListAll$.value ?? []) + .map(g => g?.key) + .filter((k): k is string => !!k); + let insertPos = 0; + for (const k of order) { + if (k === key) break; + if (list.some(g => g.key === k)) insertPos++; + } + list.splice(insertPos, 0, { key, hide, manuallyCardSort: [] }); + } + return { groupProperties: list }; + }); + }, }) ); diff --git a/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/filter/condition-view.ts b/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/filter/condition-view.ts index a8ce994814..1c9a96d15b 100644 --- a/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/filter/condition-view.ts +++ b/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/filter/condition-view.ts @@ -4,7 +4,6 @@ import { popMenu, type PopupTarget, popupTargetFromElement, - subMenuMiddleware, } from '@blocksuite/affine-components/context-menu'; import { SignalWatcher } from '@blocksuite/global/lit'; import { @@ -13,6 +12,7 @@ import { DeleteIcon, } from '@blocksuite/icons/lit'; import { ShadowlessElement } from '@blocksuite/std'; +import { autoPlacement, offset, shift } from '@floating-ui/dom'; import { computed, type ReadonlySignal } from '@preact/signals-core'; import { css, html } from 'lit'; import { property } from 'lit/decorators.js'; @@ -99,6 +99,11 @@ export class FilterConditionView extends SignalWatcher(ShadowlessElement) { return; } const handler = popMenu(target, { + middleware: [ + autoPlacement({ allowedPlacements: ['bottom-start'] }), + offset({ mainAxis: 4, crossAxis: 0 }), + shift({ crossAxis: true }), + ], options: { items: [ menu.group({ @@ -107,7 +112,7 @@ export class FilterConditionView extends SignalWatcher(ShadowlessElement) { name: fn.label, postfix: ArrowRightSmallIcon(), select: ele => { - popMenu(popupTargetFromElement(ele), { + const subHandler = popMenu(popupTargetFromElement(ele), { options: { items: [ menu.group({ @@ -117,8 +122,18 @@ export class FilterConditionView extends SignalWatcher(ShadowlessElement) { }), ], }, - middleware: subMenuMiddleware, + middleware: [ + autoPlacement({ + allowedPlacements: ['bottom-start'], + }), + offset({ mainAxis: 4, crossAxis: 0 }), + shift({ crossAxis: true }), + ], }); + // allow submenu height and width to adjust to content + subHandler.menu.menuElement.style.minHeight = 'fit-content'; + subHandler.menu.menuElement.style.maxHeight = 'fit-content'; + subHandler.menu.menuElement.style.minWidth = '200px'; return false; }, }), @@ -142,6 +157,10 @@ export class FilterConditionView extends SignalWatcher(ShadowlessElement) { ], }, }); + // allow main menu height and width to adjust to calendar size + handler.menu.menuElement.style.minHeight = 'fit-content'; + handler.menu.menuElement.style.maxHeight = 'fit-content'; + handler.menu.menuElement.style.minWidth = '200px'; }; @property({ attribute: false }) diff --git a/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/filter/root-panel-view.ts b/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/filter/root-panel-view.ts index cd5e7c2204..839cce8c95 100644 --- a/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/filter/root-panel-view.ts +++ b/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/filter/root-panel-view.ts @@ -1,10 +1,8 @@ import { menu, - popFilterableSimpleMenu, popMenu, type PopupTarget, popupTargetFromElement, - subMenuMiddleware, } from '@blocksuite/affine-components/context-menu'; import { SignalWatcher } from '@blocksuite/global/lit'; import { @@ -17,6 +15,7 @@ import { PlusIcon, } from '@blocksuite/icons/lit'; import { ShadowlessElement } from '@blocksuite/std'; +import { type Middleware, offset } from '@floating-ui/dom'; import { computed, type ReadonlySignal } from '@preact/signals-core'; import { css, html } from 'lit'; import { property, state } from 'lit/decorators.js'; @@ -208,66 +207,64 @@ export class FilterRootView extends SignalWatcher(ShadowlessElement) { if (!filter) { return; } - popFilterableSimpleMenu(popupTargetFromElement(target), [ - menu.action({ - name: filter.type === 'filter' ? 'Turn into group' : 'Wrap in group', - prefix: ConvertIcon(), - onHover: hover => { - this.containerClass = hover - ? { index: i, class: 'hover-style' } - : undefined; - }, - hide: () => getDepth(filter) > 3, - select: () => { - this.onChange({ - type: 'group', - op: 'and', - conditions: [this.filterGroup.value], - }); - }, - }), - menu.action({ - name: 'Duplicate', - prefix: DuplicateIcon(), - onHover: hover => { - this.containerClass = hover - ? { index: i, class: 'hover-style' } - : undefined; - }, - select: () => { - const conditions = [...this.filterGroup.value.conditions]; - conditions.splice( - i + 1, - 0, - JSON.parse(JSON.stringify(conditions[i])) - ); - this.onChange({ ...this.filterGroup.value, conditions: conditions }); - }, - }), - menu.group({ - name: '', + const handler = popMenu(popupTargetFromElement(target), { + placement: 'bottom-end', + middleware: [offset({ mainAxis: 12, crossAxis: 0 })], + options: { items: [ menu.action({ - name: 'Delete', - prefix: DeleteIcon(), - class: { 'delete-item': true }, - onHover: hover => { - this.containerClass = hover - ? { index: i, class: 'delete-style' } - : undefined; - }, + name: + filter.type === 'filter' ? 'Turn into group' : 'Wrap in group', + prefix: ConvertIcon(), + hide: () => getDepth(filter) > 3, select: () => { - const conditions = [...this.filterGroup.value.conditions]; - conditions.splice(i, 1); this.onChange({ - ...this.filterGroup.value, - conditions, + type: 'group', + op: 'and', + conditions: [this.filterGroup.value], }); }, }), + menu.action({ + name: 'Duplicate', + prefix: DuplicateIcon(), + select: () => { + const conditions = [...this.filterGroup.value.conditions]; + conditions.splice( + i + 1, + 0, + JSON.parse(JSON.stringify(conditions[i])) + ); + this.onChange({ + ...this.filterGroup.value, + conditions: conditions, + }); + }, + }), + menu.group({ + name: '', + items: [ + menu.action({ + name: 'Delete', + prefix: DeleteIcon(), + class: { 'delete-item': true }, + select: () => { + const conditions = [...this.filterGroup.value.conditions]; + conditions.splice(i, 1); + this.onChange({ + ...this.filterGroup.value, + conditions, + }); + }, + }), + ], + }), ], - }), - ]); + }, + }); + handler.menu.menuElement.style.minWidth = '200px'; + handler.menu.menuElement.style.maxWidth = 'fit-content'; + handler.menu.menuElement.style.minHeight = 'fit-content'; } private deleteFilter(i: number) { @@ -378,16 +375,20 @@ export const popFilterRoot = ( props: { filterTrait: FilterTrait; onBack: () => void; + onClose?: () => void; dataViewLogic: DataViewUILogicBase; - } + }, + middleware?: Array ) => { const filterTrait = props.filterTrait; const view = filterTrait.view; - popMenu(target, { + const handler = popMenu(target, { + middleware, options: { title: { text: 'Filters', onBack: props.onBack, + onClose: props.onClose, }, items: [ menu.group({ @@ -409,23 +410,16 @@ export const popFilterRoot = ( prefix: PlusIcon(), select: ele => { const value = filterTrait.filter$.value; - popCreateFilter( - popupTargetFromElement(ele), - { - vars: view.vars$, - onSelect: filter => { - filterTrait.filterSet({ - ...value, - conditions: [...value.conditions, filter], - }); - props.dataViewLogic.eventTrace( - 'CreateDatabaseFilter', - {} - ); - }, + popCreateFilter(popupTargetFromElement(ele), { + vars: view.vars$, + onSelect: filter => { + filterTrait.filterSet({ + ...value, + conditions: [...value.conditions, filter], + }); + props.dataViewLogic.eventTrace('CreateDatabaseFilter', {}); }, - { middleware: subMenuMiddleware } - ); + }); return false; }, }), @@ -434,4 +428,5 @@ export const popFilterRoot = ( ], }, }); + handler.menu.menuElement.style.minHeight = '550px'; }; diff --git a/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/sort/root-panel.ts b/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/sort/root-panel.ts index 3bd0ce1ccd..58cafd1b35 100644 --- a/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/sort/root-panel.ts +++ b/blocksuite/affine/data-view/src/widget-presets/quick-setting-bar/sort/root-panel.ts @@ -13,6 +13,7 @@ import { PlusIcon, } from '@blocksuite/icons/lit'; import { ShadowlessElement } from '@blocksuite/std'; +import type { Middleware } from '@floating-ui/dom'; import { computed } from '@preact/signals-core'; import { css, html } from 'lit'; import { property } from 'lit/decorators.js'; @@ -203,11 +204,14 @@ export const popSortRoot = ( title?: { text: string; onBack?: () => void; + onClose?: () => void; }; - } + }, + middleware?: Array ) => { const sortUtils = props.sortUtils; - popMenu(target, { + const handler = popMenu(target, { + middleware, options: { title: props.title, items: [ @@ -237,4 +241,5 @@ export const popSortRoot = ( ], }, }); + handler.menu.menuElement.style.minHeight = '550px'; }; diff --git a/blocksuite/affine/data-view/src/widget-presets/tools/presets/view-options/view-options.ts b/blocksuite/affine/data-view/src/widget-presets/tools/presets/view-options/view-options.ts index 5a06688b55..d46c96b2f7 100644 --- a/blocksuite/affine/data-view/src/widget-presets/tools/presets/view-options/view-options.ts +++ b/blocksuite/affine/data-view/src/widget-presets/tools/presets/view-options/view-options.ts @@ -18,6 +18,7 @@ import { MoreHorizontalIcon, SortIcon, } from '@blocksuite/icons/lit'; +import { autoPlacement, offset, shift } from '@floating-ui/dom'; import { css, html } from 'lit'; import { styleMap } from 'lit/directives/style-map.js'; @@ -97,7 +98,8 @@ declare global { const createSettingMenus = ( target: PopupTarget, dataViewLogic: DataViewUILogicBase, - reopen: () => void + reopen: () => void, + closeMenu: () => void ) => { const view = dataViewLogic.view; const settingItems: MenuConfig[] = []; @@ -105,15 +107,25 @@ const createSettingMenus = ( menu.action({ name: 'Properties', prefix: InfoIcon(), + closeOnSelect: false, postfix: html`
${view.properties$.value.length} shown
${ArrowRightSmallIcon()}`, select: () => { - popPropertiesSetting(target, { - view: view, - onBack: reopen, - }); + popPropertiesSetting( + target, + { + view: view, + onBack: reopen, + onClose: closeMenu, + }, + [ + autoPlacement({ allowedPlacements: ['bottom-start', 'top-start'] }), + offset({ mainAxis: 15, crossAxis: -162 }), + shift({ crossAxis: true }), + ] + ); }, }) ); @@ -124,6 +136,7 @@ const createSettingMenus = ( menu.action({ name: 'Filter', prefix: FilterIcon(), + closeOnSelect: false, postfix: html`
${filterCount === 0 ? '' @@ -134,28 +147,66 @@ const createSettingMenus = ( ${ArrowRightSmallIcon()}`, select: () => { if (!filterTrait.filter$.value.conditions.length) { - popCreateFilter(target, { - vars: view.vars$, - onBack: reopen, - onSelect: filter => { - filterTrait.filterSet({ - ...(filterTrait.filter$.value ?? emptyFilterGroup), - conditions: [...filterTrait.filter$.value.conditions, filter], - }); - popFilterRoot(target, { - filterTrait: filterTrait, - onBack: reopen, - dataViewLogic: dataViewLogic, - }); - dataViewLogic.eventTrace('CreateDatabaseFilter', {}); + popCreateFilter( + target, + { + vars: view.vars$, + onBack: reopen, + onClose: closeMenu, + onSelect: filter => { + filterTrait.filterSet({ + ...(filterTrait.filter$.value ?? emptyFilterGroup), + conditions: [ + ...filterTrait.filter$.value.conditions, + filter, + ], + }); + popFilterRoot( + target, + { + filterTrait: filterTrait, + onBack: reopen, + onClose: closeMenu, + dataViewLogic: dataViewLogic, + }, + [ + autoPlacement({ + allowedPlacements: ['bottom-start', 'top-start'], + }), + offset({ mainAxis: 15, crossAxis: -162 }), + shift({ crossAxis: true }), + ] + ); + dataViewLogic.eventTrace('CreateDatabaseFilter', {}); + }, }, - }); + { + middleware: [ + autoPlacement({ + allowedPlacements: ['bottom-start', 'top-start'], + }), + offset({ mainAxis: 15, crossAxis: -162 }), + shift({ crossAxis: true }), + ], + } + ); } else { - popFilterRoot(target, { - filterTrait: filterTrait, - onBack: reopen, - dataViewLogic: dataViewLogic, - }); + popFilterRoot( + target, + { + filterTrait: filterTrait, + onBack: reopen, + onClose: closeMenu, + dataViewLogic: dataViewLogic, + }, + [ + autoPlacement({ + allowedPlacements: ['bottom-start', 'top-start'], + }), + offset({ mainAxis: 15, crossAxis: -162 }), + shift({ crossAxis: true }), + ] + ); } }, }) @@ -168,6 +219,7 @@ const createSettingMenus = ( menu.action({ name: 'Sort', prefix: SortIcon(), + closeOnSelect: false, postfix: html`
${sortCount === 0 ? '' @@ -183,18 +235,42 @@ const createSettingMenus = ( dataViewLogic.eventTrace ); if (!sortList.length) { - popCreateSort(target, { - sortUtils: sortUtils, - onBack: reopen, - }); - } else { - popSortRoot(target, { - sortUtils: sortUtils, - title: { - text: 'Sort', + popCreateSort( + target, + { + sortUtils: sortUtils, onBack: reopen, + onClose: closeMenu, }, - }); + { + middleware: [ + autoPlacement({ + allowedPlacements: ['bottom-start', 'top-start'], + }), + offset({ mainAxis: 15, crossAxis: -162 }), + shift({ crossAxis: true }), + ], + } + ); + } else { + popSortRoot( + target, + { + sortUtils: sortUtils, + title: { + text: 'Sort', + onBack: reopen, + onClose: closeMenu, + }, + }, + [ + autoPlacement({ + allowedPlacements: ['bottom-start', 'top-start'], + }), + offset({ mainAxis: 15, crossAxis: -162 }), + shift({ crossAxis: true }), + ] + ); } }, }) @@ -206,6 +282,7 @@ const createSettingMenus = ( menu.action({ name: 'Group', prefix: GroupingIcon(), + closeOnSelect: false, postfix: html`
${groupTrait.property$.value?.name$.value ?? ''}
@@ -213,12 +290,37 @@ const createSettingMenus = ( select: () => { const groupBy = groupTrait.property$.value; if (!groupBy) { - popSelectGroupByProperty(target, groupTrait, { - onSelect: () => popGroupSetting(target, groupTrait, reopen), - onBack: reopen, - }); + popSelectGroupByProperty( + target, + groupTrait, + { + onSelect: () => + popGroupSetting(target, groupTrait, reopen, closeMenu, [ + autoPlacement({ + allowedPlacements: ['bottom-start', 'top-start'], + }), + offset({ mainAxis: 15, crossAxis: -162 }), + shift({ crossAxis: true }), + ]), + onBack: reopen, + onClose: closeMenu, + }, + [ + autoPlacement({ + allowedPlacements: ['bottom-start', 'top-start'], + }), + offset({ mainAxis: 15, crossAxis: -162 }), + shift({ crossAxis: true }), + ] + ); } else { - popGroupSetting(target, groupTrait, reopen); + popGroupSetting(target, groupTrait, reopen, closeMenu, [ + autoPlacement({ + allowedPlacements: ['bottom-start', 'top-start'], + }), + offset({ mainAxis: 15, crossAxis: -162 }), + shift({ crossAxis: true }), + ]); } }, }) @@ -308,7 +410,7 @@ export const popViewOptions = ( >`; }; }); - popMenu(target, { + const subHandler = popMenu(target, { options: { title: { onBack: reopen, @@ -338,7 +440,15 @@ export const popViewOptions = ( // }), ], }, + middleware: [ + autoPlacement({ + allowedPlacements: ['bottom-start', 'top-start'], + }), + offset({ mainAxis: 15, crossAxis: -162 }), + shift({ crossAxis: true }), + ], }); + subHandler.menu.menuElement.style.minHeight = '550px'; }, prefix: LayoutIcon(), }), @@ -348,7 +458,9 @@ export const popViewOptions = ( items.push( menu.group({ - items: createSettingMenus(target, dataViewLogic, reopen), + items: createSettingMenus(target, dataViewLogic, reopen, () => + handler.close() + ), }) ); items.push( @@ -357,6 +469,7 @@ export const popViewOptions = ( menu.action({ name: 'Duplicate', prefix: DuplicateIcon(), + closeOnSelect: false, select: () => { view.duplicate(); }, @@ -364,6 +477,7 @@ export const popViewOptions = ( menu.action({ name: 'Delete', prefix: DeleteIcon(), + closeOnSelect: false, select: () => { view.delete(); }, @@ -372,13 +486,22 @@ export const popViewOptions = ( ], }) ); - popMenu(target, { + let handler: ReturnType; + handler = popMenu(target, { options: { title: { text: 'View settings', + onClose: () => handler.close(), }, items, onClose: onClose, }, + middleware: [ + autoPlacement({ allowedPlacements: ['bottom-start'] }), + offset({ mainAxis: 15, crossAxis: -162 }), + shift({ crossAxis: true }), + ], }); + handler.menu.menuElement.style.minHeight = '550px'; + return handler; }; diff --git a/packages/backend/server/src/plugins/payment/revenuecat/service.ts b/packages/backend/server/src/plugins/payment/revenuecat/service.ts index 46410bf9c1..4fa201b568 100644 --- a/packages/backend/server/src/plugins/payment/revenuecat/service.ts +++ b/packages/backend/server/src/plugins/payment/revenuecat/service.ts @@ -183,8 +183,9 @@ export class RevenueCatService { return ent.products.items; } const entId = ent.id; - if (this.productsCache.has(entId)) { - return this.productsCache.get(entId)!; + const cachedProduct = this.productsCache.get(entId); + if (cachedProduct) { + return cachedProduct; } const res = await fetch( diff --git a/packages/frontend/core/src/desktop/dialogs/import/index.tsx b/packages/frontend/core/src/desktop/dialogs/import/index.tsx index 30045d7ef1..0e1367fba0 100644 --- a/packages/frontend/core/src/desktop/dialogs/import/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/import/index.tsx @@ -1,5 +1,4 @@ -import { Button, IconButton, Modal } from '@affine/component'; -import { IconType } from '@affine/component'; +import { Button, IconButton, IconType, Modal } from '@affine/component'; import { getStoreManager } from '@affine/core/blocksuite/manager/store'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper'; diff --git a/tests/blocksuite/e2e/database/selection.spec.ts b/tests/blocksuite/e2e/database/selection.spec.ts index 57d8cae9c2..8cee95b7a8 100644 --- a/tests/blocksuite/e2e/database/selection.spec.ts +++ b/tests/blocksuite/e2e/database/selection.spec.ts @@ -350,22 +350,23 @@ test.describe('kanban view selection', () => { await focusKanbanCardHeader(page); await assertKanbanCellSelected(page, { - // group by `number` column, the first(groupIndex: 0) group is `Ungroups` - groupIndex: 1, + // group by `number` column, `Ungroups` is hidden because it's empty (hideEmpty: true by default) + // so the first visible group is the one with value "1" at groupIndex: 0 + groupIndex: 0, cardIndex: 0, cellIndex: 0, }); await pressArrowDown(page, 3); await assertKanbanCellSelected(page, { - groupIndex: 1, + groupIndex: 0, cardIndex: 0, cellIndex: 0, }); await pressArrowUp(page); await assertKanbanCellSelected(page, { - groupIndex: 1, + groupIndex: 0, cardIndex: 0, cellIndex: 2, }); @@ -380,7 +381,8 @@ test.describe('kanban view selection', () => { columns: [ { type: 'number', - value: [1, 2], + // Both rows have value 1 to put them in the same group + value: [1, 1], }, { type: 'rich-text', @@ -392,14 +394,16 @@ test.describe('kanban view selection', () => { await focusKanbanCardHeader(page); await pressArrowUp(page); await assertKanbanCellSelected(page, { - groupIndex: 1, + // `Ungroups` is hidden because it's empty (hideEmpty: true by default) + // so the first visible group is "1" at groupIndex: 0 + groupIndex: 0, cardIndex: 1, cellIndex: 2, }); await pressArrowDown(page); await assertKanbanCellSelected(page, { - groupIndex: 1, + groupIndex: 0, cardIndex: 0, cellIndex: 0, });