From b258fc3775b5e8f8b256b7cdb4f78944b2bb8211 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Thu, 11 Dec 2025 21:11:35 +0800 Subject: [PATCH 01/11] feat: update dmg compress algorithm --- packages/frontend/apps/electron/forge.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/apps/electron/forge.config.mjs b/packages/frontend/apps/electron/forge.config.mjs index b3daa0f5ecc7f..30c3ef4e885d5 100644 --- a/packages/frontend/apps/electron/forge.config.mjs +++ b/packages/frontend/apps/electron/forge.config.mjs @@ -30,7 +30,7 @@ const makers = [ platform === 'darwin' && { name: '@electron-forge/maker-dmg', config: { - format: 'ULFO', + format: 'ULMO', icon: icnsPath, name: 'AFFiNE', 'icon-size': 128, From f832b28dacb9ab1d0650d2c1e6a526cd468cfc58 Mon Sep 17 00:00:00 2001 From: Richard Lora Date: Thu, 11 Dec 2025 18:32:21 -0400 Subject: [PATCH 02/11] feat(editor): add date grouping configurations (#12679) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: ![image](https://github.com/user-attachments/assets/8762342a-999e-444e-afa2-5cfbf7e24907) ## 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 "; views defensively handle null/hidden groups. * **Tests** * Added unit tests for date-key sorting and comparison. ✏️ Tip: You can customize this high-level summary in your review settings. --------- Co-authored-by: Norkz Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> --- .../blocks/callout/src/callout-block.ts | 2 +- .../components/src/context-menu/button.ts | 11 +- .../src/context-menu/menu-renderer.ts | 13 +- .../components/src/context-menu/sub-menu.ts | 93 ++- .../__tests__/compare-date-keys.unit.spec.ts | 35 ++ .../data-view/src/core/common/properties.ts | 10 +- .../affine/data-view/src/core/common/types.ts | 1 + .../data-view/src/core/filter/add-filter.ts | 3 +- .../src/core/filter/literal/define.ts | 1 + .../src/core/group-by/compare-date-keys.ts | 62 ++ .../data-view/src/core/group-by/default.ts | 1 + .../data-view/src/core/group-by/define.ts | 333 ++++++----- .../data-view/src/core/group-by/matcher.ts | 12 + .../core/group-by/renderer/boolean-group.ts | 8 + .../src/core/group-by/renderer/date-group.ts | 54 ++ .../core/group-by/renderer/number-group.ts | 3 +- .../core/group-by/renderer/select-group.ts | 3 +- .../core/group-by/renderer/string-group.ts | 3 +- .../data-view/src/core/group-by/setting.ts | 532 ++++++++++++++---- .../data-view/src/core/group-by/trait.ts | 395 ++++++++----- .../data-view/src/core/sort/add-sort.ts | 8 +- .../src/core/view-manager/single-view.ts | 1 + .../date/cell-renderer-css.ts | 2 +- .../kanban/kanban-view-manager.ts | 36 +- .../kanban/mobile/kanban-view-ui-logic.ts | 5 +- .../src/view-presets/kanban/mobile/menu.ts | 17 +- .../kanban/pc/kanban-view-ui-logic.ts | 15 +- .../src/view-presets/kanban/pc/menu.ts | 18 +- .../table/mobile/table-view-ui-logic.ts | 5 +- .../pc-virtual/group/bottom/group-footer.ts | 8 +- .../pc-virtual/group/top/group-header.ts | 8 +- .../table/pc-virtual/table-view-ui-logic.ts | 9 +- .../view-presets/table/pc/table-view-style.ts | 11 + .../table/pc/table-view-ui-logic.ts | 26 +- .../view-presets/table/table-view-manager.ts | 33 +- .../filter/condition-view.ts | 25 +- .../filter/root-panel-view.ts | 133 +++-- .../quick-setting-bar/sort/root-panel.ts | 9 +- .../presets/view-options/view-options.ts | 209 +++++-- .../src/plugins/payment/revenuecat/service.ts | 5 +- .../core/src/desktop/dialogs/import/index.tsx | 3 +- .../blocksuite/e2e/database/selection.spec.ts | 18 +- 42 files changed, 1623 insertions(+), 556 deletions(-) create mode 100644 blocksuite/affine/data-view/src/__tests__/compare-date-keys.unit.spec.ts create mode 100644 blocksuite/affine/data-view/src/core/group-by/compare-date-keys.ts create mode 100644 blocksuite/affine/data-view/src/core/group-by/renderer/date-group.ts diff --git a/blocksuite/affine/blocks/callout/src/callout-block.ts b/blocksuite/affine/blocks/callout/src/callout-block.ts index 1b753a31251f4..9252c32dae7c0 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 aab7bb939381b..1ea4bdf69ec26 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 9976f76f30021..54ad6121d2a33 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 b88f1aa0701d9..3ad785f30b7ff 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 0000000000000..876cf2531df5c --- /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 65ce3f17462cb..0fb865e3a8307 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 b798aa684cf77..19c097cc3f23e 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 bf82d91fcd4c9..706c9f4088c19 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 b8b51c53273b6..3a2c1b662e444 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 0000000000000..148e72e86262b --- /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 ae946fa3cd670..b520c31c2d1c9 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 1114d87c9e1a9..26e33d4f183f2 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} -
- `, - label: () => html` -
- Group By + ${renderUniLit(icon, {})} ${gProp.name$.value}
`, - 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 11ed6fc50791a..14b8055bc1e66 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,150 +49,260 @@ 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 { + hideEmpty$ = signal(true); + sortAsc$ = signal(true); + + groupProperties$ = computed(() => { + const data = this.view.data$.value; + return hasGroupProperties(data) ? (data.groupProperties ?? []) : []; + }); + + groupPropertiesMap$ = computed(() => { + const map: Record = {}; + this.groupProperties$.value.forEach(g => { + map[g.key] = g; + }); + return map; + }); + + /** + * Synchronize sortAsc$ with the GroupBy sort descriptor + */ + constructor( + private readonly groupBy$: ReadonlySignal, + public view: SingleView, + private readonly ops: { + groupBySet: (g: GroupBy | undefined) => void; + sortGroup: (keys: string[], asc?: boolean) => string[]; + sortRow: (groupKey: string, rows: Row[]) => Row[]; + changeGroupSort: (keys: string[]) => void; + changeRowSort: ( + groupKeys: string[], + 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; - } + if (!groupBy) return; + const property = this.view.propertyGetOrCreate(groupBy.columnId); - if (!property) { - return; - } + 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, - }; + 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 groupInfo = this.groupInfo$.value; - if (!groupInfo) { - return; - } + const info = this.groupInfo$.value; + if (!info) return; const staticMap = Object.fromEntries( - groupInfo.config - .defaultKeys(groupInfo.tType) - .map(({ key, value }) => [key, new Group(key, value, groupInfo, this)]) + info.config + .defaultKeys(info.tType) + .map(({ key, value }) => [key, new Group(key, value, info, this)]) ); - return { - staticMap, - groupInfo, - }; + return { staticMap, groupInfo: info }; }); groupDataMap$ = computed(() => { - const staticInfo = this.staticInfo$.value; - if (!staticInfo) { - return; - } - const { staticMap, groupInfo } = staticInfo; - const groupMap: Record = {}; + 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]) => { - groupMap[key] = new Group(key, group.value, groupInfo, this); + map[key] = new Group(key, group.value, groupInfo, this); }); + // Assign rows to their respective groups 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); + 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 (!groupMap[key]) { - groupMap[key] = new Group(key, value, groupInfo, this); - } - groupMap[key].rows.push(row); + if (!map[key]) map[key] = new Group(key, value, groupInfo, this); + map[key].rows.push(row); }); }); - return groupMap; + return map; }); groupsDataList$ = computedLock( computed(() => { - const groupMap = this.groupDataMap$.value; - if (!groupMap) { - return; + 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); } - 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); + + 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$ ); - updateData = (data: NonNullable) => { - const property = this.property$.value; - if (!property) { - return; + /** + * 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 }); } - this.view.propertyGetOrCreate(property.id).dataUpdate(() => data); - }; + } - get addGroup() { - return this.property$.value?.meta$.value?.config.addGroup; + isGroupHidden(key: string): boolean { + return this.groupPropertiesMap$.value[key]?.hide ?? false; } - property$ = computed(() => { - const groupInfo = this.groupInfo$.value; - if (!groupInfo) { - return; - } - return groupInfo.property; - }); + setGroupHide(key: string, hide: boolean) { + this.ops.changeGroupHide?.(key, hide); + } - constructor( - private readonly groupBy$: ReadonlySignal, - public view: SingleView, - private readonly ops: { - groupBySet: (groupBy: GroupBy | undefined) => void; - sortGroup: (keys: string[]) => string[]; - sortRow: (groupKey: string, rows: Row[]) => Row[]; - changeGroupSort: (keys: string[]) => void; - changeRowSort: ( - groupKeys: string[], - groupKey: string, - keys: string[] - ) => void; + /** + * 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 cf0d1cd321cc1..40c8c0b217a96 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 8d2e6ce809170..0ecc69720f2d1 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 82573604b2fc6..1a9b47be94a8d 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 afd71de607877..105f8977d1360 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 48aa745f2f361..8df610cc0f74a 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 b4c8c6fac8533..e40e5e69b596b 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 724649636235f..8a14639054f7f 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 b18626bfd9727..6026e3b73fab4 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 29411f9f23820..c74e88a021903 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 dafce47b8d7f7..ff59f3f2cda0e 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 ecf17b9929a35..e05d035f91916 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 3b2d516876721..fe186de2ee971 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 86f1bf18d2bf9..b4413402d5ea2 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 a8ce994814173..1c9a96d15b10a 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 cd5e7c220427b..839cce8c9563d 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: () => { + this.onChange({ + 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); + conditions.splice( + i + 1, + 0, + JSON.parse(JSON.stringify(conditions[i])) + ); this.onChange({ ...this.filterGroup.value, - conditions, + 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 3bd0ce1ccd9ae..58cafd1b351b5 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 5a06688b552c9..d46c96b2f7e4b 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 46410bf9c17d8..4fa201b5688a6 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 30045d7ef1fd1..0e1367fba00d1 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 57d8cae9c2394..8cee95b7a8694 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, }); From 7f96c97b6795c9338a762aca677780cda43500ce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:26:29 +0800 Subject: [PATCH 03/11] chore: bump up rustc version to v1.92.0 (#13624) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Update | Change | |---|---|---| | [rustc](https://redirect.github.com/rust-lang/rust) | minor | `1.91.0` -> `1.92.0` | --- ### Release Notes
rust-lang/rust (rustc) ### [`v1.92.0`](https://redirect.github.com/rust-lang/rust/blob/HEAD/RELEASES.md#Version-1920-2025-12-11) [Compare Source](https://redirect.github.com/rust-lang/rust/compare/1.91.1...1.92.0) \========================== ## Language - [Document `MaybeUninit` representation and validity](https://redirect.github.com/rust-lang/rust/pull/140463) - [Allow `&raw [mut | const]` for union field in safe code](https://redirect.github.com/rust-lang/rust/pull/141469) - [Prefer item bounds of associated types over where-bounds for auto-traits and `Sized`](https://redirect.github.com/rust-lang/rust/pull/144064) - [Do not materialize `X` in `[X; 0]` when `X` is unsizing a const](https://redirect.github.com/rust-lang/rust/pull/145277) - [Support combining `#[track_caller]` and `#[no_mangle]` (requires every declaration specifying `#[track_caller]` as well)](https://redirect.github.com/rust-lang/rust/pull/145724) - [Make never type lints `never_type_fallback_flowing_into_unsafe` and `dependency_on_unit_never_type_fallback` deny-by-default](https://redirect.github.com/rust-lang/rust/pull/146167) - [Allow specifying multiple bounds for same associated item, except in trait objects](https://redirect.github.com/rust-lang/rust/pull/146593) - [Slightly strengthen higher-ranked region handling in coherence](https://redirect.github.com/rust-lang/rust/pull/146725) - [The `unused_must_use` lint no longer warns on `Result<(), Uninhabited>` (for instance, `Result<(), !>`), or `ControlFlow`](https://redirect.github.com/rust-lang/rust/pull/147382). This avoids having to check for an error that can never happen. ## Compiler - [Make `mips64el-unknown-linux-muslabi64` link dynamically](https://redirect.github.com/rust-lang/rust/pull/146858) - [Remove current code for embedding command-line args in PDB](https://redirect.github.com/rust-lang/rust/pull/147022) Command-line information is typically not needed by debugging tools, and the removed code was causing problems for incremental builds even on targets that don't use PDB debuginfo. ## Libraries - [Specialize `Iterator::eq{_by}` for `TrustedLen` iterators](https://redirect.github.com/rust-lang/rust/pull/137122) - [Simplify `Extend` for tuples](https://redirect.github.com/rust-lang/rust/pull/138799) - [Added details to `Debug` for `EncodeWide`](https://redirect.github.com/rust-lang/rust/pull/140153). - [`iter::Repeat::last`](https://redirect.github.com/rust-lang/rust/pull/147258) and [`count`](https://redirect.github.com/rust-lang/rust/pull/146410) will now panic, rather than looping infinitely. ## Stabilized APIs - [`NonZero::div_ceil`](https://doc.rust-lang.org/stable/std/num/struct.NonZero.html#method.div_ceil) - [`Location::file_as_c_str`](https://doc.rust-lang.org/stable/std/panic/struct.Location.html#method.file_as_c_str) - [`RwLockWriteGuard::downgrade`](https://doc.rust-lang.org/stable/std/sync/struct.RwLockWriteGuard.html#method.downgrade) - [`Box::new_zeroed`](https://doc.rust-lang.org/stable/std/boxed/struct.Box.html#method.new_zeroed) - [`Box::new_zeroed_slice`](https://doc.rust-lang.org/stable/std/boxed/struct.Box.html#method.new_zeroed_slice) - [`Rc::new_zeroed`](https://doc.rust-lang.org/stable/std/rc/struct.Rc.html#method.new_zeroed) - [`Rc::new_zeroed_slice`](https://doc.rust-lang.org/stable/std/rc/struct.Rc.html#method.new_zeroed_slice) - [`Arc::new_zeroed`](https://doc.rust-lang.org/stable/std/sync/struct.Arc.html#method.new_zeroed) - [`Arc::new_zeroed_slice`](https://doc.rust-lang.org/stable/std/sync/struct.Arc.html#method.new_zeroed_slice) - [`btree_map::Entry::insert_entry`](https://doc.rust-lang.org/stable/std/collections/btree_map/enum.Entry.html#method.insert_entry) - [`btree_map::VacantEntry::insert_entry`](https://doc.rust-lang.org/stable/std/collections/btree_map/struct.VacantEntry.html#method.insert_entry) - [`impl Extend for proc_macro::TokenStream`](https://doc.rust-lang.org/stable/proc_macro/struct.TokenStream.html#impl-Extend%3CGroup%3E-for-TokenStream) - [`impl Extend for proc_macro::TokenStream`](https://doc.rust-lang.org/stable/proc_macro/struct.TokenStream.html#impl-Extend%3CLiteral%3E-for-TokenStream) - [`impl Extend for proc_macro::TokenStream`](https://doc.rust-lang.org/stable/proc_macro/struct.TokenStream.html#impl-Extend%3CPunct%3E-for-TokenStream) - [`impl Extend for proc_macro::TokenStream`](https://doc.rust-lang.org/stable/proc_macro/struct.TokenStream.html#impl-Extend%3CIdent%3E-for-TokenStream) These previously stable APIs are now stable in const contexts: - [`<[_]>::rotate_left`](https://doc.rust-lang.org/stable/std/primitive.slice.html#method.rotate_left) - [`<[_]>::rotate_right`](https://doc.rust-lang.org/stable/std/primitive.slice.html#method.rotate_right) ## Cargo - [Added a new chapter](https://redirect.github.com/rust-lang/cargo/issues/16119) to the Cargo book, ["Optimizing Build Performance"](https://doc.rust-lang.org/stable/cargo/guide/build-performance.html). ## Rustdoc - [If a trait item appears in rustdoc search, hide the corresponding impl items](https://redirect.github.com/rust-lang/rust/pull/145898). Previously a search for "last" would show both `Iterator::last` as well as impl methods like `std::vec::IntoIter::last`. Now these impl methods will be hidden, freeing up space for inherent methods like `BTreeSet::last`. - [Relax rules for identifiers in search](https://redirect.github.com/rust-lang/rust/pull/147860). Previously you could only search for identifiers that were valid in rust code, now searches only need to be valid as part of an identifier. For example, you can now perform a search that starts with a digit. ## Compatibility Notes - [Fix backtraces with `-C panic=abort` on Linux by generating unwind tables by default](https://redirect.github.com/rust-lang/rust/pull/143613). Build with `-C force-unwind-tables=no` to keep omitting unwind tables. * As part of the larger effort refactoring compiler built-in attributes and their diagnostics, [the future-compatibility lint `invalid_macro_export_arguments` is upgraded to deny-by-default and will be reported in dependencies too.](https://redirect.github.com/rust-lang/rust/pull/143857) * [Update the minimum external LLVM to 20](https://redirect.github.com/rust-lang/rust/pull/145071) * [Prevent downstream `impl DerefMut for Pin`](https://redirect.github.com/rust-lang/rust/pull/145608) * [Don't apply temporary lifetime extension rules to the arguments of non-extended `pin!` and formatting macros](https://redirect.github.com/rust-lang/rust/pull/145838) ### [`v1.91.1`](https://redirect.github.com/rust-lang/rust/blob/HEAD/RELEASES.md#Version-1911-2025-11-10) [Compare Source](https://redirect.github.com/rust-lang/rust/compare/1.91.0...1.91.1) \=========================== - [Enable file locking support in illumos](https://redirect.github.com/rust-lang/rust/pull/148322). This fixes Cargo not locking the build directory on illumos. - [Fix `wasm_import_module` attribute cross-crate](https://redirect.github.com/rust-lang/rust/pull/148363). This fixes linker errors on WASM targets.
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 1612370a6921f..9cf77fc11c622 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.91.0" +channel = "1.92.0" profile = "default" From 246e09e0cd28e51bde7e0294ad054d74d8e7ee04 Mon Sep 17 00:00:00 2001 From: Fangdun Tsai Date: Fri, 12 Dec 2025 10:46:58 +0800 Subject: [PATCH 04/11] fix: roll back electron version to v35 (#14089) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In electron v36, all workers do not work. The webpack configuration is too complicated, so go back first. If start a new project with [forge](https://www.electronforge.io/) and latest electron, the worker works well. ## Summary by CodeRabbit * **Chores** * Downgraded the Electron development/runtime used for building and testing the desktop app from v36 to v35; this is a development-environment change with no functional or API changes affecting end users. ✏️ Tip: You can customize this high-level summary in your review settings. --- package.json | 2 +- packages/frontend/apps/electron/package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 16f703a857ed5..f2b00033fbbce 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@vitest/coverage-istanbul": "^3.2.4", "@vitest/ui": "^3.2.4", "cross-env": "^10.1.0", - "electron": "^36.0.0", + "electron": "^35.0.0", "eslint": "^9.16.0", "eslint-config-prettier": "^10.0.0", "eslint-import-resolver-typescript": "^4.0.0", diff --git a/packages/frontend/apps/electron/package.json b/packages/frontend/apps/electron/package.json index 84a25fb9b329a..1fdb9a62ffde6 100644 --- a/packages/frontend/apps/electron/package.json +++ b/packages/frontend/apps/electron/package.json @@ -59,7 +59,7 @@ "builder-util-runtime": "^9.5.0", "cross-env": "^10.1.0", "debug": "^4.4.0", - "electron": "^36.0.0", + "electron": "^35.0.0", "electron-log": "^5.4.3", "electron-squirrel-startup": "1.0.1", "electron-window-state": "^5.0.3", diff --git a/yarn.lock b/yarn.lock index e5ba5abc220eb..7007750f376ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -583,7 +583,7 @@ __metadata: builder-util-runtime: "npm:^9.5.0" cross-env: "npm:^10.1.0" debug: "npm:^4.4.0" - electron: "npm:^36.0.0" + electron: "npm:^35.0.0" electron-log: "npm:^5.4.3" electron-squirrel-startup: "npm:1.0.1" electron-updater: "npm:^6.6.2" @@ -786,7 +786,7 @@ __metadata: "@vitest/coverage-istanbul": "npm:^3.2.4" "@vitest/ui": "npm:^3.2.4" cross-env: "npm:^10.1.0" - electron: "npm:^36.0.0" + electron: "npm:^35.0.0" eslint: "npm:^9.16.0" eslint-config-prettier: "npm:^10.0.0" eslint-import-resolver-typescript: "npm:^4.0.0" @@ -22380,16 +22380,16 @@ __metadata: languageName: node linkType: hard -"electron@npm:^36.0.0": - version: 36.9.5 - resolution: "electron@npm:36.9.5" +"electron@npm:^35.0.0": + version: 35.7.5 + resolution: "electron@npm:35.7.5" dependencies: "@electron/get": "npm:^2.0.0" "@types/node": "npm:^22.7.7" extract-zip: "npm:^2.0.1" bin: electron: cli.js - checksum: 10/2c78ce37b6cdcf2388dc3f3932f2104f7bd529c51ee8d00e0201ce86089966309c0ba62c2045abc4dad81893ebe7f7070bb047dcb9c05366f31ddd7fb73790ca + checksum: 10/f3757f0b6f21ddd5ebf8b83becdaa3e47dc13473e5375e6e6aee638d7029e73d08d6a4170b37ad8c594057da139e7cdf4c8b82b70c2c1ad0306abdcc274c5fee languageName: node linkType: hard From a0eeed0cdb58b0e1a4baa187c469bdc1ec3feb5f Mon Sep 17 00:00:00 2001 From: Xun Sun Date: Sat, 13 Dec 2025 18:05:25 +0800 Subject: [PATCH 05/11] feat: implement export as PDF (#14057) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I used [pdfmake](https://www.npmjs.com/package/pdfmake) to implement an "export as PDF" feature, and I am happy to share with you! This should fix #13577, fix #8846, and fix #13959. A showcase: [Getting Started.pdf](https://github.com/user-attachments/files/24013057/Getting.Started.pdf) Although it might miss rendering some properties currently, it can evolve in the long run and provide a more native experience for the users. ## Summary by CodeRabbit * **New Features** - Experimental "Export to PDF" option added to the export menu (behind a feature flag) - PDF export supports headings, paragraphs, lists, code blocks, tables, images, callouts, linked documents and embedded content * **Chores** - Added PDF rendering library and consolidated PDF utilities - Feature flag introduced to control rollout * **Tests** - Comprehensive unit tests added for PDF content rendering logic ✏️ Tip: You can customize this high-level summary in your review settings. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: DarkSky --- .../src/__tests__/adapters/pdf.unit.spec.ts | 1622 +++++++++++++++++ .../blocks/list/src/utils/get-list-icon.ts | 3 +- .../affine/blocks/table/src/table-cell.ts | 1 + blocksuite/affine/shared/package.json | 2 + .../affine/shared/src/adapters/index.ts | 1 + .../shared/src/adapters/pdf/css-utils.ts | 25 + .../src/adapters/pdf/delta-converter.ts | 122 ++ .../shared/src/adapters/pdf/image-utils.ts | 114 ++ .../affine/shared/src/adapters/pdf/index.ts | 6 + .../affine/shared/src/adapters/pdf/pdf.ts | 1004 ++++++++++ .../shared/src/adapters/pdf/svg-utils.ts | 42 + .../affine/shared/src/adapters/pdf/utils.ts | 71 + .../src/services/feature-flag-service.ts | 2 + blocksuite/affine/shared/src/utils/index.ts | 1 + .../src/utils/number-prefix.ts} | 11 +- .../linked-doc/src/transformers/index.ts | 1 + .../linked-doc/src/transformers/pdf.ts | 32 + .../components/use-user-management.ts | 1 + .../ai/components/playground/content.ts | 2 + .../frontend/core/src/bootstrap/cleanup.ts | 41 +- .../hooks/affine/use-export-page.ts | 13 +- .../page-list/operation-menu-items/export.tsx | 18 +- .../integration/mcp-server/setting-panel.tsx | 1 + .../core/src/modules/feature-flag/constant.ts | 9 + .../web/components/saved-recording-item.tsx | 1 + tests/kit/src/utils/keyboard.ts | 1 + yarn.lock | 384 ++-- 27 files changed, 3392 insertions(+), 139 deletions(-) create mode 100644 blocksuite/affine/all/src/__tests__/adapters/pdf.unit.spec.ts create mode 100644 blocksuite/affine/shared/src/adapters/pdf/css-utils.ts create mode 100644 blocksuite/affine/shared/src/adapters/pdf/delta-converter.ts create mode 100644 blocksuite/affine/shared/src/adapters/pdf/image-utils.ts create mode 100644 blocksuite/affine/shared/src/adapters/pdf/index.ts create mode 100644 blocksuite/affine/shared/src/adapters/pdf/pdf.ts create mode 100644 blocksuite/affine/shared/src/adapters/pdf/svg-utils.ts create mode 100644 blocksuite/affine/shared/src/adapters/pdf/utils.ts rename blocksuite/affine/{blocks/list/src/utils/get-number-prefix.ts => shared/src/utils/number-prefix.ts} (85%) create mode 100644 blocksuite/affine/widgets/linked-doc/src/transformers/pdf.ts diff --git a/blocksuite/affine/all/src/__tests__/adapters/pdf.unit.spec.ts b/blocksuite/affine/all/src/__tests__/adapters/pdf.unit.spec.ts new file mode 100644 index 0000000000000..185885ba4cf66 --- /dev/null +++ b/blocksuite/affine/all/src/__tests__/adapters/pdf.unit.spec.ts @@ -0,0 +1,1622 @@ +import { DefaultTheme, NoteDisplayMode } from '@blocksuite/affine-model'; +import { PdfAdapter } from '@blocksuite/affine-shared/adapters'; +import type { BlockSnapshot, DocSnapshot } from '@blocksuite/store'; +import { AssetsManager, MemoryBlobCRUD } from '@blocksuite/store'; +import { describe, expect, test } from 'vitest'; + +import { createJob } from '../utils/create-job.js'; +import { getProvider } from '../utils/get-provider.js'; + +const provider = getProvider(); + +// Helper function to create a base snapshot structure +function createBaseSnapshot( + children: BlockSnapshot['children'] +): BlockSnapshot { + return { + type: 'block', + id: 'block:test', + flavour: 'affine:page', + props: { + title: { + '$blocksuite:internal:text$': true, + delta: [], + }, + }, + children: [ + { + type: 'block', + id: 'block:surface', + flavour: 'affine:surface', + props: { + elements: {}, + }, + children: [], + }, + { + type: 'block', + id: 'block:note', + flavour: 'affine:note', + props: { + xywh: '[0,0,800,95]', + background: DefaultTheme.noteBackgrounColor, + index: 'a0', + hidden: false, + displayMode: NoteDisplayMode.DocAndEdgeless, + }, + children, + }, + ], + }; +} + +describe('snapshot to pdf', () => { + test('paragraph', async () => { + const blockSnapshot: BlockSnapshot = { + type: 'block', + id: 'block:test', + flavour: 'affine:page', + props: { + title: { + '$blocksuite:internal:text$': true, + delta: [], + }, + }, + children: [ + { + type: 'block', + id: 'block:surface', + flavour: 'affine:surface', + props: { + elements: {}, + }, + children: [], + }, + { + type: 'block', + id: 'block:note', + flavour: 'affine:note', + props: { + xywh: '[0,0,800,95]', + background: DefaultTheme.noteBackgrounColor, + index: 'a0', + hidden: false, + displayMode: NoteDisplayMode.DocAndEdgeless, + }, + children: [ + { + type: 'block', + id: 'block:paragraph', + flavour: 'affine:paragraph', + props: { + type: 'text', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Hello World', + }, + ], + }, + }, + children: [], + }, + ], + }, + ], + }; + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + expect(definition.content).toBeDefined(); + expect(Array.isArray(definition.content)).toBe(true); + const content = definition.content as any[]; + expect(content.length).toBeGreaterThan(0); + + // Find the paragraph content + const paragraphContent = content.find( + (item: any) => + item.text === 'Hello World' || + (Array.isArray(item.text) && item.text.includes('Hello World')) + ); + expect(paragraphContent).toBeDefined(); + }); + + test('code block', async () => { + const blockSnapshot: BlockSnapshot = { + type: 'block', + id: 'block:test', + flavour: 'affine:page', + props: { + title: { + '$blocksuite:internal:text$': true, + delta: [], + }, + }, + children: [ + { + type: 'block', + id: 'block:surface', + flavour: 'affine:surface', + props: { + elements: {}, + }, + children: [], + }, + { + type: 'block', + id: 'block:note', + flavour: 'affine:note', + props: { + xywh: '[0,0,800,95]', + background: DefaultTheme.noteBackgrounColor, + index: 'a0', + hidden: false, + displayMode: NoteDisplayMode.DocAndEdgeless, + }, + children: [ + { + type: 'block', + id: 'block:code', + flavour: 'affine:code', + props: { + language: 'python', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'print("Hello")', + }, + ], + }, + }, + children: [], + }, + ], + }, + ], + }; + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + expect(definition.content).toBeDefined(); + const content = definition.content as any[]; + + // Find code block table + const codeBlock = content.find( + (item: any) => item.table && item.table.body + ); + expect(codeBlock).toBeDefined(); + expect(codeBlock.table.body).toBeDefined(); + }); + + test('list items', async () => { + const blockSnapshot: BlockSnapshot = { + type: 'block', + id: 'block:test', + flavour: 'affine:page', + props: { + title: { + '$blocksuite:internal:text$': true, + delta: [], + }, + }, + children: [ + { + type: 'block', + id: 'block:surface', + flavour: 'affine:surface', + props: { + elements: {}, + }, + children: [], + }, + { + type: 'block', + id: 'block:note', + flavour: 'affine:note', + props: { + xywh: '[0,0,800,95]', + background: DefaultTheme.noteBackgrounColor, + index: 'a0', + hidden: false, + displayMode: NoteDisplayMode.DocAndEdgeless, + }, + children: [ + { + type: 'block', + id: 'block:list1', + flavour: 'affine:list', + props: { + type: 'bulleted', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Item 1', + }, + ], + }, + }, + children: [], + }, + { + type: 'block', + id: 'block:list2', + flavour: 'affine:list', + props: { + type: 'bulleted', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Item 2', + }, + ], + }, + }, + children: [], + }, + ], + }, + ], + }; + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + expect(definition.content).toBeDefined(); + const content = definition.content as any[]; + + // Find list items (they should be tables with 2 columns) + const listItems = content.filter( + (item: any) => + item.table && item.table.widths && item.table.widths.length === 2 + ); + expect(listItems.length).toBeGreaterThanOrEqual(2); + }); + + test('header', async () => { + const blockSnapshot: BlockSnapshot = { + type: 'block', + id: 'block:test', + flavour: 'affine:page', + props: { + title: { + '$blocksuite:internal:text$': true, + delta: [], + }, + }, + children: [ + { + type: 'block', + id: 'block:surface', + flavour: 'affine:surface', + props: { + elements: {}, + }, + children: [], + }, + { + type: 'block', + id: 'block:note', + flavour: 'affine:note', + props: { + xywh: '[0,0,800,95]', + background: DefaultTheme.noteBackgrounColor, + index: 'a0', + hidden: false, + displayMode: NoteDisplayMode.DocAndEdgeless, + }, + children: [ + { + type: 'block', + id: 'block:header', + flavour: 'affine:paragraph', + props: { + type: 'h1', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Heading 1', + }, + ], + }, + }, + children: [], + }, + ], + }, + ], + }; + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + expect(definition.content).toBeDefined(); + const content = definition.content as any[]; + + // Find header content + const header = content.find( + (item: any) => + item.style === 'header1' || + (item.text && + (item.text === 'Heading 1' || + (Array.isArray(item.text) && item.text.includes('Heading 1')))) + ); + expect(header).toBeDefined(); + if (header.style) { + expect(header.style).toBe('header1'); + } + }); + + test('document with title', async () => { + const docSnapshot: DocSnapshot = { + type: 'page', + meta: { + id: 'testDocument', + title: 'Test Document', + createDate: 1718225423102, + tags: [], + }, + blocks: { + type: 'block', + id: 'block:test', + flavour: 'affine:page', + props: { + title: { + '$blocksuite:internal:text$': true, + delta: [], + }, + }, + children: [ + { + type: 'block', + id: 'block:surface', + flavour: 'affine:surface', + props: { + elements: {}, + }, + children: [], + }, + { + type: 'block', + id: 'block:note', + flavour: 'affine:note', + props: { + xywh: '[0,0,800,95]', + background: DefaultTheme.noteBackgrounColor, + index: 'a0', + hidden: false, + displayMode: NoteDisplayMode.DocAndEdgeless, + }, + children: [], + }, + ], + }, + }; + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [docSnapshot.blocks], + docSnapshot.meta?.title + ); + + expect(definition.content).toBeDefined(); + const content = definition.content as any[]; + + // First item should be the title + expect(content[0].text).toBe('Test Document'); + expect(content[0].style).toBe('title'); + }); + + test('styles definition', async () => { + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition([], undefined); + + expect(definition.styles).toBeDefined(); + expect(definition.styles?.title).toBeDefined(); + expect(definition.styles?.header1).toBeDefined(); + expect(definition.styles?.header2).toBeDefined(); + expect(definition.styles?.header3).toBeDefined(); + expect(definition.styles?.header4).toBeDefined(); + expect(definition.styles?.code).toBeDefined(); + + expect(definition.defaultStyle).toBeDefined(); + expect(definition.defaultStyle?.font).toBe('Roboto'); + }); + + describe('inline text styling', () => { + test('bold text', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:paragraph', + flavour: 'affine:paragraph', + props: { + type: 'text', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Bold text', + attributes: { bold: true }, + }, + ], + }, + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const textContent = content.find((item: any) => item.text); + expect(textContent).toBeDefined(); + + if (Array.isArray(textContent.text)) { + const styledText = textContent.text.find( + (t: any) => typeof t === 'object' && t.bold === true + ); + expect(styledText).toBeDefined(); + expect(styledText.text).toBe('Bold text'); + } + }); + + test('italic text', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:paragraph', + flavour: 'affine:paragraph', + props: { + type: 'text', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Italic text', + attributes: { italic: true }, + }, + ], + }, + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const textContent = content.find((item: any) => item.text); + expect(textContent).toBeDefined(); + + if (Array.isArray(textContent.text)) { + const styledText = textContent.text.find( + (t: any) => typeof t === 'object' && t.italics === true + ); + expect(styledText).toBeDefined(); + expect(styledText.text).toBe('Italic text'); + } + }); + + test('underline text', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:paragraph', + flavour: 'affine:paragraph', + props: { + type: 'text', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Underlined text', + attributes: { underline: true }, + }, + ], + }, + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const textContent = content.find((item: any) => item.text); + expect(textContent).toBeDefined(); + + if (Array.isArray(textContent.text)) { + const styledText = textContent.text.find( + (t: any) => + typeof t === 'object' && + (t.decoration === 'underline' || + (Array.isArray(t.decoration) && + t.decoration.includes('underline'))) + ); + expect(styledText).toBeDefined(); + expect(styledText.text).toBe('Underlined text'); + } + }); + + test('strikethrough text', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:paragraph', + flavour: 'affine:paragraph', + props: { + type: 'text', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Strikethrough text', + attributes: { strike: true }, + }, + ], + }, + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const textContent = content.find((item: any) => item.text); + expect(textContent).toBeDefined(); + + if (Array.isArray(textContent.text)) { + const styledText = textContent.text.find( + (t: any) => + typeof t === 'object' && + (t.decoration === 'lineThrough' || + (Array.isArray(t.decoration) && + t.decoration.includes('lineThrough'))) + ); + expect(styledText).toBeDefined(); + expect(styledText.text).toBe('Strikethrough text'); + } + }); + + test('inline code', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:paragraph', + flavour: 'affine:paragraph', + props: { + type: 'text', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'code', + attributes: { code: true }, + }, + ], + }, + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const textContent = content.find((item: any) => item.text); + expect(textContent).toBeDefined(); + + if (Array.isArray(textContent.text)) { + const codeText = textContent.text.find( + (t: any) => + typeof t === 'object' && + t.font === 'Roboto' && + t.background === '#f5f5f5' + ); + expect(codeText).toBeDefined(); + expect(codeText.text).toContain('code'); + } + }); + + test('combined styles', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:paragraph', + flavour: 'affine:paragraph', + props: { + type: 'text', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Bold and italic', + attributes: { bold: true, italic: true }, + }, + ], + }, + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const textContent = content.find((item: any) => item.text); + expect(textContent).toBeDefined(); + + if (Array.isArray(textContent.text)) { + const styledText = textContent.text.find( + (t: any) => + typeof t === 'object' && t.bold === true && t.italics === true + ); + expect(styledText).toBeDefined(); + } + }); + }); + + describe('links and references', () => { + test('link attribute', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:paragraph', + flavour: 'affine:paragraph', + props: { + type: 'text', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Click here', + attributes: { link: 'https://example.com' }, + }, + ], + }, + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const textContent = content.find((item: any) => item.text); + expect(textContent).toBeDefined(); + + if (Array.isArray(textContent.text)) { + const linkText = textContent.text.find( + (t: any) => typeof t === 'object' && t.link === 'https://example.com' + ); + expect(linkText).toBeDefined(); + expect(linkText.color).toBe('#0066cc'); + } + }); + + test('linked page reference - found', async () => { + const job = createJob(); + const pdfAdapter = new PdfAdapter(job, provider); + pdfAdapter.configs.set('title:page123', 'Referenced Page'); + pdfAdapter.configs.set('docLinkBaseUrl', 'https://example.com/doc'); + + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:paragraph', + flavour: 'affine:paragraph', + props: { + type: 'text', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: ' ', + attributes: { + reference: { + type: 'LinkedPage', + pageId: 'page123', + }, + }, + }, + ], + }, + }, + children: [], + }, + ]); + + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const textContent = content.find((item: any) => item.text); + expect(textContent).toBeDefined(); + + if (Array.isArray(textContent.text)) { + const refText = textContent.text.find( + (t: any) => + typeof t === 'object' && + t.text === 'Referenced Page' && + t.link === 'https://example.com/doc/page123' + ); + expect(refText).toBeDefined(); + expect(refText.color).toBe('#0066cc'); + } + }); + + test('linked page reference - not found', async () => { + const job = createJob(); + const pdfAdapter = new PdfAdapter(job, provider); + pdfAdapter.configs.set('docLinkBaseUrl', 'https://example.com/doc'); + + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:paragraph', + flavour: 'affine:paragraph', + props: { + type: 'text', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: ' ', + attributes: { + reference: { + type: 'LinkedPage', + pageId: 'nonexistent', + }, + }, + }, + ], + }, + }, + children: [], + }, + ]); + + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const textContent = content.find((item: any) => item.text); + expect(textContent).toBeDefined(); + + if (Array.isArray(textContent.text)) { + const refText = textContent.text.find( + (t: any) => + typeof t === 'object' && + t.text === 'Page not found' && + Array.isArray(t.decoration) && + t.decoration.includes('lineThrough') + ); + expect(refText).toBeDefined(); + } + }); + }); + + describe('quote blocks', () => { + test('quote block with text', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:quote', + flavour: 'affine:paragraph', + props: { + type: 'quote', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'This is a quote', + }, + ], + }, + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const quoteBlock = content.find( + (item: any) => + item.table && + item.table.widths && + item.table.widths.length === 2 && + item.table.widths[0] === 2 + ); + expect(quoteBlock).toBeDefined(); + expect(quoteBlock.table.body[0][0].fillColor).toBe('#cccccc'); + }); + + test('quote block with children', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:quote', + flavour: 'affine:paragraph', + props: { + type: 'quote', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Quote text', + }, + ], + }, + }, + children: [ + { + type: 'block', + id: 'block:child', + flavour: 'affine:paragraph', + props: { + type: 'text', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Child paragraph', + }, + ], + }, + }, + children: [], + }, + ], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const quoteBlock = content.find( + (item: any) => + item.table && item.table.widths && item.table.widths.length === 2 + ); + expect(quoteBlock).toBeDefined(); + expect(quoteBlock.table.body[0][1].stack).toBeDefined(); + }); + }); + + describe('callout blocks', () => { + test('callout with default background', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:callout', + flavour: 'affine:callout', + props: { + backgroundColorName: 'grey', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Callout content', + }, + ], + }, + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const calloutBlock = content.find( + (item: any) => + item.table && item.table.widths && item.table.widths.length === 1 + ); + expect(calloutBlock).toBeDefined(); + expect(calloutBlock.table.body[0][0].fillColor).toBeDefined(); + }); + + test('callout with custom background color', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:callout', + flavour: 'affine:callout', + props: { + backgroundColorName: 'blue', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Blue callout', + }, + ], + }, + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const calloutBlock = content.find( + (item: any) => + item.table && item.table.widths && item.table.widths.length === 1 + ); + expect(calloutBlock).toBeDefined(); + expect(calloutBlock.table.body[0][0].fillColor).toBeDefined(); + }); + + test('callout with children', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:callout', + flavour: 'affine:callout', + props: { + backgroundColorName: 'grey', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Callout', + }, + ], + }, + }, + children: [ + { + type: 'block', + id: 'block:child', + flavour: 'affine:paragraph', + props: { + type: 'text', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Child content', + }, + ], + }, + }, + children: [], + }, + ], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const calloutBlock = content.find( + (item: any) => + item.table && item.table.widths && item.table.widths.length === 1 + ); + expect(calloutBlock).toBeDefined(); + expect(calloutBlock.table.body[0][0].stack).toBeDefined(); + }); + }); + + describe('image handling', () => { + test('image without sourceId', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:image', + flavour: 'affine:image', + props: { + sourceId: undefined, + caption: 'Test caption', + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const imageContent = content.find( + (item: any) => + item.text && + (item.text === '[Image: Test caption]' || item.text.includes('Image')) + ); + expect(imageContent).toBeDefined(); + expect(imageContent.italics).toBe(true); + }); + + test('SVG image', async () => { + const blobCRUD = new MemoryBlobCRUD(); + const svgBlob = new Blob( + [ + '', + ], + { + type: 'image/svg+xml', + } + ); + const blobId = await blobCRUD.set(svgBlob); + const assets = new AssetsManager({ blob: blobCRUD }); + await assets.readFromBlob(blobId); + assets.getAssets().set(blobId, svgBlob); + + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:image', + flavour: 'affine:image', + props: { + sourceId: blobId, + caption: 'SVG Image', + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined, + assets + ); + + const content = definition.content as any[]; + const svgContent = content.find((item: any) => item.svg); + expect(svgContent).toBeDefined(); + expect(svgContent.svg).toContain(' { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:image', + flavour: 'affine:image', + props: { + sourceId: 'nonexistent-blob-id', + caption: 'Missing image', + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const fallbackContent = content.find( + (item: any) => + item.text && + (item.text === '[Image: Missing image]' || + item.text.includes('Image')) + ); + expect(fallbackContent).toBeDefined(); + }); + + test('image with width and height', async () => { + const blobCRUD = new MemoryBlobCRUD(); + const svgBlob = new Blob( + [ + '', + ], + { + type: 'image/svg+xml', + } + ); + const blobId = await blobCRUD.set(svgBlob); + const assets = new AssetsManager({ blob: blobCRUD }); + await assets.readFromBlob(blobId); + assets.getAssets().set(blobId, svgBlob); + + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:image', + flavour: 'affine:image', + props: { + sourceId: blobId, + width: 300, + height: 200, + caption: 'Sized image', + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined, + assets + ); + + const content = definition.content as any[]; + const svgContent = content.find((item: any) => item.svg); + expect(svgContent).toBeDefined(); + expect(svgContent.width).toBeDefined(); + expect(svgContent.height).toBeDefined(); + }); + + test('image text alignment', async () => { + const blobCRUD = new MemoryBlobCRUD(); + const svgBlob = new Blob([''], { + type: 'image/svg+xml', + }); + const blobId = await blobCRUD.set(svgBlob); + const assets = new AssetsManager({ blob: blobCRUD }); + await assets.readFromBlob(blobId); + assets.getAssets().set(blobId, svgBlob); + + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:image', + flavour: 'affine:image', + props: { + sourceId: blobId, + textAlign: 'right', + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined, + assets + ); + + const content = definition.content as any[]; + const svgContent = content.find((item: any) => item.svg); + expect(svgContent).toBeDefined(); + expect(svgContent.alignment).toBe('right'); + }); + }); + + describe('table rendering', () => { + test('table with cells', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:table', + flavour: 'affine:table', + props: { + columns: { + col1: { + columnId: 'col1', + order: 'a0', + }, + col2: { + columnId: 'col2', + order: 'a1', + }, + }, + rows: { + row1: { + rowId: 'row1', + order: 'a0', + }, + row2: { + rowId: 'row2', + order: 'a1', + }, + }, + cells: { + 'row1:col1': { + text: { + delta: [{ insert: 'Cell 1-1' }], + }, + }, + 'row1:col2': { + text: { + delta: [{ insert: 'Cell 1-2' }], + }, + }, + 'row2:col1': { + text: { + delta: [{ insert: 'Cell 2-1' }], + }, + }, + 'row2:col2': { + text: { + delta: [{ insert: 'Cell 2-2' }], + }, + }, + }, + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const table = content.find( + (item: any) => + item.table && item.table.body && Array.isArray(item.table.body) + ); + expect(table).toBeDefined(); + expect(table.table.body.length).toBe(2); + expect(table.table.body[0].length).toBe(2); + }); + + test('empty table', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:table', + flavour: 'affine:table', + props: { + columns: {}, + rows: {}, + cells: {}, + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const table = content.find((item: any) => item.table && item.table.body); + expect(table).toBeUndefined(); + }); + }); + + describe('edge cases', () => { + test('empty paragraph', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:paragraph', + flavour: 'affine:paragraph', + props: { + type: 'text', + text: { + '$blocksuite:internal:text$': true, + delta: [], + }, + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + expect(definition.content).toBeDefined(); + const content = definition.content as any[]; + expect(content.length).toBeGreaterThan(0); + }); + + test('paragraph with empty delta', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:paragraph', + flavour: 'affine:paragraph', + props: { + type: 'text', + text: { + '$blocksuite:internal:text$': true, + delta: [{ insert: '' }], + }, + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + expect(definition.content).toBeDefined(); + }); + + test('malformed delta operations', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:paragraph', + flavour: 'affine:paragraph', + props: { + type: 'text', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { insert: 123 as any }, // Invalid type + { insert: 'Valid text' }, + ], + }, + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + expect(definition.content).toBeDefined(); + const content = definition.content as any[]; + const textContent = content.find((item: any) => item.text); + expect(textContent).toBeDefined(); + }); + + test('text alignment', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:paragraph', + flavour: 'affine:paragraph', + props: { + type: 'text', + textAlign: 'center', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Centered text', + }, + ], + }, + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const paragraph = content.find( + (item: any) => + item.text && + (item.text === 'Centered text' || + (Array.isArray(item.text) && + item.text.some((t: any) => + typeof t === 'string' + ? t === 'Centered text' + : t.text === 'Centered text' + ))) + ); + expect(paragraph).toBeDefined(); + expect(paragraph.alignment).toBe('center'); + }); + + test('nested list items', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:list1', + flavour: 'affine:list', + props: { + type: 'bulleted', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Parent item', + }, + ], + }, + }, + children: [ + { + type: 'block', + id: 'block:list2', + flavour: 'affine:list', + props: { + type: 'bulleted', + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Child item', + }, + ], + }, + }, + children: [], + }, + ], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + expect(definition.content).toBeDefined(); + const content = definition.content as any[]; + const listItems = content.filter( + (item: any) => + item.table && item.table.widths && item.table.widths.length === 2 + ); + expect(listItems.length).toBeGreaterThanOrEqual(2); + }); + + test('numbered list with order', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:list1', + flavour: 'affine:list', + props: { + type: 'numbered', + order: 5, + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Item 5', + }, + ], + }, + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const listItem = content.find( + (item: any) => + item.table && + item.table.body && + item.table.body[0] && + item.table.body[0][0] && + item.table.body[0][0].text + ); + expect(listItem).toBeDefined(); + expect(listItem.table.body[0][0].text).toContain('5.'); + }); + + test('todo list checked', async () => { + const blockSnapshot = createBaseSnapshot([ + { + type: 'block', + id: 'block:todo', + flavour: 'affine:list', + props: { + type: 'todo', + checked: true, + text: { + '$blocksuite:internal:text$': true, + delta: [ + { + insert: 'Completed task', + }, + ], + }, + }, + children: [], + }, + ]); + + const pdfAdapter = new PdfAdapter(createJob(), provider); + const definition = await pdfAdapter.getDocDefinition( + [blockSnapshot], + undefined + ); + + const content = definition.content as any[]; + const todoItem = content.find( + (item: any) => + item.table && + item.table.body && + item.table.body[0] && + item.table.body[0][0] && + item.table.body[0][0].svg + ); + expect(todoItem).toBeDefined(); + expect(todoItem.table.body[0][0].svg).toContain('svg'); + }); + }); +}); diff --git a/blocksuite/affine/blocks/list/src/utils/get-list-icon.ts b/blocksuite/affine/blocks/list/src/utils/get-list-icon.ts index 4de1bb39b0e0c..c30f427e176cb 100644 --- a/blocksuite/affine/blocks/list/src/utils/get-list-icon.ts +++ b/blocksuite/affine/blocks/list/src/utils/get-list-icon.ts @@ -1,4 +1,5 @@ import type { ListBlockModel } from '@blocksuite/affine-model'; +import { getNumberPrefix } from '@blocksuite/affine-shared/utils'; import { BulletedList01Icon, BulletedList02Icon, @@ -11,8 +12,6 @@ import { } from '@blocksuite/icons/lit'; import { html } from 'lit'; -import { getNumberPrefix } from './get-number-prefix.js'; - const getListDeep = (model: ListBlockModel): number => { let deep = 0; let parent = model.store.getParent(model); diff --git a/blocksuite/affine/blocks/table/src/table-cell.ts b/blocksuite/affine/blocks/table/src/table-cell.ts index f54319dbb2926..a13a987fab353 100644 --- a/blocksuite/affine/blocks/table/src/table-cell.ts +++ b/blocksuite/affine/blocks/table/src/table-cell.ts @@ -418,6 +418,7 @@ export class TableCell extends SignalWatcher( name: 'Paste', prefix: PasteIcon(), select: () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises navigator.clipboard.readText().then(text => { this.selectionController.doPaste(text, selected); }); diff --git a/blocksuite/affine/shared/package.json b/blocksuite/affine/shared/package.json index 0693687733b18..82a44d4cbc244 100644 --- a/blocksuite/affine/shared/package.json +++ b/blocksuite/affine/shared/package.json @@ -40,6 +40,7 @@ "micromark-extension-gfm-task-list-item": "^2.1.0", "micromark-util-combine-extensions": "^2.0.0", "minimatch": "^10.1.1", + "pdfmake": "^0.2.20", "quick-lru": "^7.3.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", @@ -73,6 +74,7 @@ "!dist/__tests__" ], "devDependencies": { + "@types/pdfmake": "^0.2.12", "vitest": "^3.2.4" }, "version": "0.25.7" diff --git a/blocksuite/affine/shared/src/adapters/index.ts b/blocksuite/affine/shared/src/adapters/index.ts index dab76bd0f3b5a..1c9333a2d5dda 100644 --- a/blocksuite/affine/shared/src/adapters/index.ts +++ b/blocksuite/affine/shared/src/adapters/index.ts @@ -61,6 +61,7 @@ export { NotionHtmlDeltaConverter, } from './notion-html'; export * from './notion-text'; +export { PdfAdapter } from './pdf'; export { BlockPlainTextAdapterExtension, type BlockPlainTextAdapterMatcher, diff --git a/blocksuite/affine/shared/src/adapters/pdf/css-utils.ts b/blocksuite/affine/shared/src/adapters/pdf/css-utils.ts new file mode 100644 index 0000000000000..324073b34a223 --- /dev/null +++ b/blocksuite/affine/shared/src/adapters/pdf/css-utils.ts @@ -0,0 +1,25 @@ +/** + * Resolve CSS variable color (var(--affine-xxx)) using computed styles + */ +export function resolveCssVariable(color: string): string | null { + if (!color || typeof color !== 'string') { + return null; + } + if (!color.startsWith('var(')) { + return color; + } + if (typeof document === 'undefined') { + return null; + } + const rootComputedStyle = getComputedStyle(document.documentElement); + const match = color.match(/var\(([^)]+)\)/); + if (!match || !match[1]) { + return null; + } + const variable = match[1].trim(); + if (!variable.startsWith('--')) { + return null; + } + const value = rootComputedStyle.getPropertyValue(variable).trim(); + return value || null; +} diff --git a/blocksuite/affine/shared/src/adapters/pdf/delta-converter.ts b/blocksuite/affine/shared/src/adapters/pdf/delta-converter.ts new file mode 100644 index 0000000000000..1a993e0baea26 --- /dev/null +++ b/blocksuite/affine/shared/src/adapters/pdf/delta-converter.ts @@ -0,0 +1,122 @@ +/** + * Delta to PDF content converter + */ + +import { resolveCssVariable } from './css-utils.js'; + +/** + * Extract text from delta operations, preserving inline properties + * Returns normalized format: string if simple, array if complex (with inline styles) + */ +export function extractTextWithInline( + props: Record, + configs: Map +): string | Array { + const delta = props?.text?.delta; + if (!Array.isArray(delta)) { + return ' '; + } + + const result: Array = []; + + for (const op of delta) { + if (typeof op.insert !== 'string') { + continue; + } + + const text = op.insert; + const attrs = op.attributes; + + if (!attrs || Object.keys(attrs).length === 0) { + result.push(text); + continue; + } + + const styleObj: { text: string; [key: string]: any } = { text }; + + if (attrs.bold === true) { + styleObj.bold = true; + } + if (attrs.italic === true) { + styleObj.italics = true; + } + const decorations: string[] = []; + if (attrs.strike === true) { + decorations.push('lineThrough'); + } + if (attrs.underline === true) { + decorations.push('underline'); + } + if (decorations.length > 0) { + styleObj.decoration = decorations; + } + if (attrs.code === true) { + styleObj.font = 'Inter'; + styleObj.background = '#f5f5f5'; + styleObj.fontSize = 10; + styleObj.text = ' ' + text + ' '; + } + if (attrs.color && typeof attrs.color === 'string') { + const resolved = resolveCssVariable(attrs.color); + if (resolved) { + styleObj.color = resolved; + } + } + if ( + attrs.background && + typeof attrs.background === 'string' && + !attrs.code + ) { + const resolvedBg = resolveCssVariable(attrs.background); + if (resolvedBg) { + styleObj.background = resolvedBg; + } + } + if (attrs.link) { + styleObj.link = attrs.link; + styleObj.color = '#0066cc'; + } + if (attrs.reference) { + const ref = attrs.reference; + if (ref.type === 'LinkedPage' || ref.type === 'Subpage') { + const docLinkBaseUrl = configs.get('docLinkBaseUrl') || ''; + const linkUrl = docLinkBaseUrl ? `${docLinkBaseUrl}/${ref.pageId}` : ''; + + const pageTitle = configs.get('title:' + ref.pageId); + const isPageFound = pageTitle !== undefined; + const displayTitle = pageTitle || 'Page not found'; + + if (!text || text.trim() === '' || text === ' ') { + styleObj.text = displayTitle; + } + styleObj.color = '#0066cc'; + if (!isPageFound && styleObj.decoration) { + if (!Array.isArray(styleObj.decoration)) { + styleObj.decoration = [styleObj.decoration]; + } + if (!styleObj.decoration.includes('lineThrough')) { + styleObj.decoration.push('lineThrough'); + } + } + if (linkUrl) { + styleObj.link = linkUrl; + } + } + } + if (attrs.latex) { + styleObj.text = attrs.latex; + styleObj.italics = true; + styleObj.color = '#666666'; + } + + result.push(styleObj); + } + + if (result.length === 0) { + return ' '; + } + if (result.length === 1 && typeof result[0] === 'string') { + return result[0] || ' '; + } + return result; +} diff --git a/blocksuite/affine/shared/src/adapters/pdf/image-utils.ts b/blocksuite/affine/shared/src/adapters/pdf/image-utils.ts new file mode 100644 index 0000000000000..6e693a988f124 --- /dev/null +++ b/blocksuite/affine/shared/src/adapters/pdf/image-utils.ts @@ -0,0 +1,114 @@ +/** + * Image dimension utilities + */ + +import { MAX_PAPER_HEIGHT, MAX_PAPER_WIDTH } from './utils.js'; + +/** + * Calculate image dimensions respecting props, original size, and paper constraints + */ +export function calculateImageDimensions( + blockWidth: number | undefined, + blockHeight: number | undefined, + originalWidth: number | undefined, + originalHeight: number | undefined +): { width?: number; height?: number } { + let targetWidth = + blockWidth && blockWidth > 0 + ? blockWidth + : originalWidth && originalWidth > 0 + ? originalWidth + : undefined; + + let targetHeight = + blockHeight && blockHeight > 0 + ? blockHeight + : originalHeight && originalHeight > 0 + ? originalHeight + : undefined; + + if (!targetWidth && !targetHeight) { + return {}; + } + + if (targetWidth && targetWidth > MAX_PAPER_WIDTH) { + const ratio = MAX_PAPER_WIDTH / targetWidth; + targetWidth = MAX_PAPER_WIDTH; + if (targetHeight) { + targetHeight = targetHeight * ratio; + } + } + + if (targetHeight && targetHeight > MAX_PAPER_HEIGHT) { + const ratio = MAX_PAPER_HEIGHT / targetHeight; + targetHeight = MAX_PAPER_HEIGHT; + if (targetWidth) { + targetWidth = targetWidth * ratio; + } + } + + return { + width: targetWidth, + height: targetHeight, + }; +} + +/** + * Extract dimensions from SVG + */ +export function extractSvgDimensions(svgText: string): { + width?: number; + height?: number; +} { + const widthMatch = svgText.match(/width\s*=\s*["']?(\d+(?:\.\d+)?)/i); + const heightMatch = svgText.match(/height\s*=\s*["']?(\d+(?:\.\d+)?)/i); + const viewBoxMatch = svgText.match( + /viewBox\s*=\s*["']?\s*[\d.]+\s+[\d.]+\s+([\d.]+)\s+([\d.]+)/i + ); + + let width: number | undefined; + let height: number | undefined; + + if (widthMatch) { + width = parseFloat(widthMatch[1]); + } + if (heightMatch) { + height = parseFloat(heightMatch[1]); + } + + if ((!width || !height) && viewBoxMatch) { + const viewBoxWidth = parseFloat(viewBoxMatch[1]); + const viewBoxHeight = parseFloat(viewBoxMatch[2]); + if (!width) width = viewBoxWidth; + if (!height) height = viewBoxHeight; + } + + return { width, height }; +} + +/** + * Extract dimensions from JPEG/PNG using Image API + */ +export async function extractImageDimensions( + blob: Blob +): Promise<{ width?: number; height?: number }> { + return new Promise(resolve => { + const img = new Image(); + const url = URL.createObjectURL(blob); + const timeout = setTimeout(() => { + URL.revokeObjectURL(url); + resolve({}); + }, 5000); + img.onload = () => { + clearTimeout(timeout); + URL.revokeObjectURL(url); + resolve({ width: img.width, height: img.height }); + }; + img.onerror = () => { + clearTimeout(timeout); + URL.revokeObjectURL(url); + resolve({}); + }; + img.src = url; + }); +} diff --git a/blocksuite/affine/shared/src/adapters/pdf/index.ts b/blocksuite/affine/shared/src/adapters/pdf/index.ts new file mode 100644 index 0000000000000..eb96f29e1627b --- /dev/null +++ b/blocksuite/affine/shared/src/adapters/pdf/index.ts @@ -0,0 +1,6 @@ +export * from './css-utils.js'; +export * from './delta-converter.js'; +export * from './image-utils.js'; +export * from './pdf.js'; +export * from './svg-utils.js'; +export * from './utils.js'; diff --git a/blocksuite/affine/shared/src/adapters/pdf/pdf.ts b/blocksuite/affine/shared/src/adapters/pdf/pdf.ts new file mode 100644 index 0000000000000..f16f32372b1e9 --- /dev/null +++ b/blocksuite/affine/shared/src/adapters/pdf/pdf.ts @@ -0,0 +1,1004 @@ +import type { + TableCellSerialized, + TableColumn, + TableRow, +} from '@blocksuite/affine-model'; +import type { ServiceProvider } from '@blocksuite/global/di'; +import { + BaseAdapter, + type BlockSnapshot, + type DocSnapshot, + type FromBlockSnapshotPayload, + type FromBlockSnapshotResult, + type FromDocSnapshotPayload, + type FromDocSnapshotResult, + type FromSliceSnapshotPayload, + type FromSliceSnapshotResult, + type SliceSnapshot, + type ToBlockSnapshotPayload, + type ToDocSnapshotPayload, + type ToSliceSnapshotPayload, + type Transformer, +} from '@blocksuite/store'; +import DOMPurify from 'dompurify'; +import pdfMake from 'pdfmake/build/pdfmake'; +import type { + Content, + ContentText, + TDocumentDefinitions, +} from 'pdfmake/interfaces'; + +import { getNumberPrefix } from '../../utils'; +import { resolveCssVariable } from './css-utils.js'; +import { extractTextWithInline } from './delta-converter.js'; +import { + calculateImageDimensions, + extractImageDimensions, + extractSvgDimensions, +} from './image-utils.js'; +import { + getBulletIconSvg, + getCheckboxIconSvg, + getToggleIconSvg, +} from './svg-utils.js'; +import { + BLOCK_CHILDREN_CONTAINER_PADDING_LEFT, + getImagePlaceholder, + hasTextContent, + PDF_COLORS, + TABLE_LAYOUT_NO_BORDERS, + textContentToString, +} from './utils.js'; + +pdfMake.fonts = { + Inter: { + normal: 'https://cdn.affine.pro/fonts/Inter-Regular.woff', + bold: 'https://cdn.affine.pro/fonts/Inter-SemiBold.woff', + italics: 'https://cdn.affine.pro/fonts/Inter-Italic.woff', + bolditalics: 'https://cdn.affine.pro/fonts/Inter-SemiBoldItalic.woff', + }, + SarasaGothicCL: { + normal: 'https://cdn.affine.pro/fonts/SarasaGothicCL-Regular.ttf', + bold: 'https://cdn.affine.pro/fonts/SarasaGothicCL-Regular.ttf', + italics: 'https://cdn.affine.pro/fonts/SarasaGothicCL-Regular.ttf', + bolditalics: 'https://cdn.affine.pro/fonts/SarasaGothicCL-Regular.ttf', + }, +}; + +export type PdfAdapterFile = { + blob: Blob; + fileName: string; +}; + +/** + * PDF export adapter using pdfmake library. + * + * This adapter converts BlockSuite documents to PDF format. It is export-only + * and does not support importing from PDF. + * + * @example + * ```typescript + * const adapter = new PdfAdapter(job, provider); + * const result = await adapter.fromDocSnapshot({ snapshot, assets }); + * download(result.file.blob, result.file.fileName); + * ``` + */ +export class PdfAdapter extends BaseAdapter { + constructor(job: Transformer, provider: ServiceProvider) { + super(job, provider); + } + + async fromBlockSnapshot({ + snapshot, + assets, + }: FromBlockSnapshotPayload): Promise< + FromBlockSnapshotResult + > { + const content = await this._buildContent([snapshot], assets); + const definition = this._createDocDefinition(undefined, content); + const blob = await this._createPdfBlob(definition); + return { + file: { + blob, + fileName: 'block.pdf', + }, + assetsIds: [], + }; + } + + async fromDocSnapshot({ + snapshot, + assets, + }: FromDocSnapshotPayload): Promise> { + const content = await this._buildContent([snapshot.blocks], assets); + const definition = this._createDocDefinition(snapshot.meta?.title, content); + const blob = await this._createPdfBlob(definition); + return { + file: { + blob, + fileName: `${snapshot.meta?.title || 'Untitled'}.pdf`, + }, + assetsIds: [], + }; + } + + async fromSliceSnapshot({ + snapshot, + assets, + }: FromSliceSnapshotPayload): Promise< + FromSliceSnapshotResult + > { + const content = await this._buildContent(snapshot.content, assets); + const definition = this._createDocDefinition(undefined, content); + const blob = await this._createPdfBlob(definition); + return { + file: { + blob, + fileName: 'slice.pdf', + }, + assetsIds: [], + }; + } + + toBlockSnapshot( + _payload: ToBlockSnapshotPayload + ): BlockSnapshot { + throw new Error('PdfAdapter does not support importing blocks from PDF.'); + } + + toDocSnapshot(_payload: ToDocSnapshotPayload): DocSnapshot { + throw new Error('PdfAdapter does not support importing docs from PDF.'); + } + + toSliceSnapshot( + _payload: ToSliceSnapshotPayload + ): SliceSnapshot | null { + throw new Error('PdfAdapter does not support importing slices from PDF.'); + } + + /** + * Get the pdfmake document definition (for testing purposes) + */ + async getDocDefinition( + blocks: BlockSnapshot[], + title?: string, + assets?: FromDocSnapshotPayload['assets'] + ): Promise { + const content = await this._buildContent(blocks, assets); + return this._createDocDefinition(title, content); + } + + private async _buildContent( + blocks: BlockSnapshot[], + assets?: FromDocSnapshotPayload['assets'] + ): Promise { + const content: Content[] = []; + for (const block of blocks) { + const blockContent = await this._blockToContent(block, assets, 0, 0, 0); + content.push(...blockContent); + } + return content; + } + + private async _blockToContent( + block: BlockSnapshot, + assets?: FromDocSnapshotPayload['assets'], + depth: number = 0, + listNestingLevel: number = 0, + parentTextStart: number = 0 + ): Promise { + const content: Content[] = []; + const flavour = block.flavour; + const props = block.props as Record; + const textContent = extractTextWithInline(props, this.configs); + + const baseIndent = + parentTextStart > 0 + ? parentTextStart + : depth * BLOCK_CHILDREN_CONTAINER_PADDING_LEFT; + + if (flavour === 'affine:paragraph') { + content.push( + ...(await this._createParagraphContent( + props, + textContent, + baseIndent, + block, + assets, + depth + )) + ); + } else if (flavour === 'affine:list') { + content.push( + ...(await this._createListContent( + props, + textContent, + baseIndent, + listNestingLevel, + block + )) + ); + } else if (flavour === 'affine:code') { + content.push(...this._createCodeContent(props, textContent, baseIndent)); + } else if (flavour === 'affine:divider') { + content.push({ + canvas: [ + { + type: 'line', + x1: 0, + y1: 0, + x2: 515, + y2: 0, + lineWidth: 1, + lineColor: PDF_COLORS.border, + }, + ], + margin: [0, 10, 0, 10], + }); + } else if (flavour === 'affine:callout') { + const calloutContent = await this._createCalloutContent( + props, + textContent, + baseIndent, + block, + assets, + depth + ); + content.push(...calloutContent); + return content; + } else if (flavour === 'affine:bookmark') { + content.push({ + text: props.title || props.url || '', + link: props.url, + color: PDF_COLORS.link, + margin: [0, 2, 0, 2], + }); + } else if (flavour === 'affine:image') { + const imageContent = await this._createImageContent( + props.sourceId, + props.caption || '', + assets, + props.textAlign || 'center', + props.width, + props.height + ); + content.push(...imageContent); + } else if (flavour === 'affine:latex') { + content.push({ + text: props.latex || '', + margin: [baseIndent, 5, 0, 5], + italics: true, + color: PDF_COLORS.textMuted, + alignment: 'center', + }); + } else if (flavour === 'affine:database') { + content.push(...this._createDatabaseContent(props)); + return content; + } else if (flavour === 'affine:table') { + const tableContent = await this._createTableContent(props); + if (tableContent) { + content.push(tableContent); + } + } else if ( + flavour === 'affine:embed-linked-doc' || + flavour === 'affine:embed-synced-doc' + ) { + content.push(this._createLinkedDocContent(props, baseIndent)); + } else if (hasTextContent(textContent)) { + content.push({ + text: textContent, + margin: [0, 2, 0, 2], + }); + } + + if (block.children && block.children.length) { + const shouldIncrementDepth = + flavour !== 'affine:page' && flavour !== 'affine:note'; + const childDepth = shouldIncrementDepth ? depth + 1 : depth; + + const childListNestingLevel = + flavour === 'affine:list' + ? listNestingLevel + 1 + : parentTextStart > 0 + ? listNestingLevel + : 0; + + const childParentTextStart = + flavour === 'affine:list' + ? baseIndent + BLOCK_CHILDREN_CONTAINER_PADDING_LEFT + : parentTextStart > 0 + ? parentTextStart + : 0; + + for (const child of block.children) { + const childContent = await this._blockToContent( + child, + assets, + childDepth, + childListNestingLevel, + childParentTextStart + ); + content.push(...childContent); + } + } + + return content; + } + + private async _createParagraphContent( + props: Record, + textContent: string | Array, + baseIndent: number, + block: BlockSnapshot, + assets?: FromDocSnapshotPayload['assets'], + depth: number = 0 + ): Promise { + const type = props.type || 'text'; + const textAlign = props.textAlign || 'left'; + const styleMap: Record = { + h1: 'header1', + h2: 'header2', + h3: 'header3', + h4: 'header4', + h5: 'header4', + h6: 'header4', + }; + const style = styleMap[type]; + + if (type === 'quote') { + return this._createQuoteContent( + textContent, + baseIndent, + block, + assets, + depth + ); + } + + const paragraphContent: Content = style + ? { text: textContent, style, margin: [baseIndent, 6, 0, 3] } + : { text: textContent, margin: [baseIndent, 2, 0, 2] }; + + if (textAlign && textAlign !== 'left') { + paragraphContent.alignment = textAlign; + } + + return [paragraphContent]; + } + + private async _createQuoteContent( + textContent: string | Array, + baseIndent: number, + block: BlockSnapshot, + assets?: FromDocSnapshotPayload['assets'], + depth: number = 0 + ): Promise { + const quoteContent: Content[] = []; + + if (hasTextContent(textContent)) { + quoteContent.push({ + text: textContent, + margin: [0, 5, 10, 5], + }); + } + + const childrenContent = await this._processChildrenWithMargins( + block, + assets, + depth, + 0, + 10 + ); + quoteContent.push(...childrenContent); + + return [ + { + table: { + widths: [2, '*'], + body: [ + [ + { text: ' ', fillColor: PDF_COLORS.border }, + { + stack: quoteContent.length > 0 ? quoteContent : [{ text: ' ' }], + margin: [10, 0, 0, 0], + }, + ], + ], + }, + margin: [baseIndent, 5, 0, 5], + layout: TABLE_LAYOUT_NO_BORDERS, + }, + ]; + } + + private async _createListContent( + props: Record, + textContent: string | Array, + baseIndent: number, + listNestingLevel: number, + block: BlockSnapshot + ): Promise { + const type = props.type || 'bulleted'; + const checked = props.checked || false; + const order = props.order; + + let prefixSvg: string | null = null; + let prefixText: string | null = null; + + if (type === 'numbered') { + const number = + order !== null && order !== undefined ? order : listNestingLevel + 1; + prefixText = `${getNumberPrefix(number, listNestingLevel)} `; + } else if (type === 'todo') { + prefixSvg = getCheckboxIconSvg(checked); + } else if (type === 'toggle') { + const hasChildren = block.children && block.children.length > 0; + prefixSvg = getToggleIconSvg(hasChildren); + } else { + prefixSvg = getBulletIconSvg(listNestingLevel); + } + + const listText = Array.isArray(textContent) + ? textContent.length === 0 + ? ' ' + : textContent + : textContent; + + const blueColor = resolveCssVariable('var(--affine-blue-700)') || '#1E96EB'; + + const iconCell: Content = prefixSvg + ? { + svg: prefixSvg, + width: 16, + margin: [0, 0, 4, 0], + } + : prefixText + ? { + text: prefixText, + color: blueColor, + alignment: 'left', + } + : { text: '' }; + + const textCell: Content = + typeof listText === 'string' + ? { text: listText } + : listText.length === 1 + ? typeof listText[0] === 'string' + ? { text: listText[0] } + : listText[0] + : { text: listText }; + + return [ + { + table: { + widths: [BLOCK_CHILDREN_CONTAINER_PADDING_LEFT, '*'], + body: [[iconCell, textCell]], + }, + margin: [baseIndent, 2, 0, 2], + layout: TABLE_LAYOUT_NO_BORDERS, + }, + ]; + } + + private _createCodeContent( + props: Record, + textContent: string | Array, + baseIndent: number + ): Content[] { + const language = props.language || ''; + const lineNumber = props.lineNumber !== false; + const codeText = + typeof textContent === 'string' + ? textContent + : textContentToString(textContent); + const lines = codeText.split('\n'); + + const tableBody: any[][] = []; + if (lineNumber && lines.length > 1) { + const maxLineNumLength = lines.length.toString().length; + for (let i = 0; i < lines.length; i++) { + const lineNum = (i + 1).toString().padStart(maxLineNumLength, ' '); + const isFirstLine = i === 0; + const isLastLine = i === lines.length - 1; + + tableBody.push([ + { + text: lineNum, + style: 'code', + alignment: 'right', + fillColor: PDF_COLORS.codeBackground, + margin: [5, isFirstLine ? 20 : 0, 5, isLastLine ? 20 : 0], + }, + { + text: lines[i], + style: 'code', + fillColor: PDF_COLORS.codeBackground, + margin: [5, isFirstLine ? 20 : 0, 10, isLastLine ? 20 : 0], + }, + ]); + } + } else { + tableBody.push([ + { + text: codeText, + style: 'code', + fillColor: PDF_COLORS.codeBackground, + margin: [10, 5, 10, 5], + colSpan: 2, + }, + '', + ]); + } + + const codeBlockContent: Content[] = [ + { + table: { + widths: ['auto', '*'], + body: tableBody, + }, + margin: [baseIndent, 5, 0, 5], + layout: 'noBorders', + }, + ]; + + if (language) { + codeBlockContent.push({ + text: `Language: ${language}`, + fontSize: 9, + color: PDF_COLORS.textDisabled, + margin: [baseIndent + 10, 0, 0, 5], + italics: true, + }); + } + + return codeBlockContent; + } + + private async _createCalloutContent( + props: Record, + textContent: string | Array, + baseIndent: number, + block: BlockSnapshot, + assets?: FromDocSnapshotPayload['assets'], + depth: number = 0 + ): Promise { + const backgroundColorName = props.backgroundColorName || 'grey'; + const colorVar = + backgroundColorName === 'default' || backgroundColorName === 'grey' + ? 'var(--affine-v2-block-callout-background-grey)' + : `var(--affine-v2-block-callout-background-${backgroundColorName})`; + const backgroundColor = resolveCssVariable(colorVar) || '#f5f5f5'; + + const calloutContent: Content[] = []; + + if (hasTextContent(textContent)) { + calloutContent.push({ + text: textContent, + margin: [10, 5, 10, 0], + }); + } + + const childrenContent = await this._processChildrenWithMargins( + block, + assets, + depth, + 10, + 10 + ); + calloutContent.push(...childrenContent); + + return [ + { + table: { + widths: ['*'], + body: [ + [ + { + stack: + calloutContent.length > 0 ? calloutContent : [{ text: ' ' }], + fillColor: backgroundColor, + margin: [10, 5, 10, 5], + }, + ], + ], + }, + margin: [baseIndent, 5, 0, 5], + layout: 'noBorders', + }, + ]; + } + + private _createDatabaseContent(props: Record): Content[] { + let titleText: + | string + | Array = ''; + + if (props.title) { + if (props.title.delta && Array.isArray(props.title.delta)) { + titleText = extractTextWithInline( + { text: { delta: props.title.delta } }, + this.configs + ); + } else if (props.title.delta) { + titleText = extractTextWithInline({ text: props.title }, this.configs); + } + } + + const content: Content[] = []; + + if (hasTextContent(titleText)) { + content.push({ + text: titleText, + bold: true, + margin: [0, 5, 0, 2], + }); + } + + content.push({ + text: '[Data View - Not exported]', + italics: true, + color: PDF_COLORS.textDisabled, + margin: [0, 2, 0, 5], + }); + + return content; + } + + private _adjustMargins( + content: Content[], + leftAdjustment: number, + rightAdjustment: number + ): Content[] { + return content.map(item => { + if (typeof item === 'object' && 'margin' in item && item.margin) { + const margin = item.margin; + const marginArray = Array.isArray(margin) + ? margin + : [margin, margin, margin, margin]; + const marginTuple: [number, number, number, number] = [ + (marginArray[0] || 0) + leftAdjustment, + marginArray[1] || 0, + (marginArray[2] || 0) + rightAdjustment, + marginArray[3] || 0, + ]; + return { + ...item, + margin: marginTuple, + }; + } + return item; + }); + } + + private _createLinkedDocContent( + props: Record, + baseIndent: number + ): Content { + const pageId = props.pageId || ''; + const titleAlias = props.title; + const configTitle = this.configs.get('title:' + pageId); + const pageTitle = titleAlias || configTitle; + const isPageFound = configTitle !== undefined || titleAlias !== undefined; + const displayTitle = pageTitle || 'Page not found'; + + const docLinkBaseUrl = this.configs.get('docLinkBaseUrl') || ''; + const linkUrl = + docLinkBaseUrl && pageId ? `${docLinkBaseUrl}/${pageId}` : ''; + + const linkedDocContent: Content[] = [ + { + text: displayTitle, + bold: true, + fontSize: 14, + margin: [15, 10, 15, 5], + decoration: isPageFound ? undefined : 'lineThrough', + color: isPageFound ? PDF_COLORS.text : PDF_COLORS.textDisabled, + link: linkUrl || undefined, + }, + ]; + + if (isPageFound) { + linkedDocContent.push({ + text: 'Linked Document', + fontSize: 10, + color: PDF_COLORS.textMuted, + margin: [15, 0, 15, 10], + }); + } + + return { + table: { + widths: ['*'], + body: [ + [{ stack: linkedDocContent, fillColor: PDF_COLORS.cardBackground }], + ], + }, + margin: [baseIndent, 5, 0, 5], + layout: 'noBorders', + }; + } + + private async _createImageContent( + sourceId: string | undefined, + caption: string, + assets?: FromDocSnapshotPayload['assets'], + textAlign: string = 'center', + blockWidth?: number, + blockHeight?: number + ): Promise { + if (!sourceId) { + return [this._getImagePlaceholderContent(caption)]; + } + + try { + const manager = assets ?? this.job.assetsManager; + if (!manager) { + throw new Error('Asset manager not available'); + } + await manager.readFromBlob(sourceId); + const blob = manager.getAssets().get(sourceId); + if (!blob) { + throw new Error('Image asset not found'); + } + + const text = await blob.text(); + const trimmedText = text.trim(); + + if (trimmedText.startsWith('= 2 && data[0] === 0xff && data[1] === 0xd8; + const isPNG = + data.length >= 4 && + data[0] === 0x89 && + data[1] === 0x50 && + data[2] === 0x4e && + data[3] === 0x47; + + if (!isJPEG && !isPNG) { + return [this._getImagePlaceholderContent(caption)]; + } + + const imageDimensions = await extractImageDimensions(blob); + const dimensions = calculateImageDimensions( + blockWidth, + blockHeight, + imageDimensions.width, + imageDimensions.height + ); + + // pdfmake (via pdfkit) accepts ArrayBuffer for images, though the types don't reflect this + const imageBuffer = arrayBuffer as never as string; + const content: Content[] = [ + { + image: imageBuffer, + ...(dimensions.width && { width: dimensions.width }), + ...(dimensions.height && { height: dimensions.height }), + margin: [0, 5, 0, 5], + alignment: textAlign as 'left' | 'center' | 'right', + }, + ]; + + if (caption) { + content.push({ + text: caption, + italics: true, + fontSize: 10, + color: PDF_COLORS.textMuted, + margin: [0, 2, 0, 10], + alignment: textAlign as 'left' | 'center' | 'right', + }); + } + + return content; + } catch { + return [this._getImagePlaceholderContent(caption)]; + } + } + + private async _createTableContent( + props: Record + ): Promise { + const columns: Record = props.columns || {}; + const rows: Record = props.rows || {}; + const cells: Record = props.cells || {}; + + const sortedColumns = Object.values(columns).sort((a, b) => + (a.order || '').localeCompare(b.order || '') + ); + const sortedRows = Object.values(rows).sort((a, b) => + (a.order || '').localeCompare(b.order || '') + ); + + if (sortedRows.length === 0 || sortedColumns.length === 0) { + return null; + } + + const tableBody: any[][] = []; + for (const row of sortedRows) { + const rowData: any[] = []; + for (const col of sortedColumns) { + const cellKey = `${(row as any).rowId}:${(col as any).columnId}`; + const cell = cells[cellKey]; + if (cell?.text?.delta) { + const cellText = extractTextWithInline( + { text: cell.text }, + this.configs + ); + rowData.push(cellText); + } else { + rowData.push(''); + } + } + tableBody.push(rowData); + } + + return { + table: { + headerRows: 0, + widths: Array(sortedColumns.length).fill('*'), + body: tableBody, + }, + margin: [0, 5, 0, 5], + layout: { + hLineWidth: (i: number, node: any) => { + if (i === 0 || i === node.table.body.length) return 1; + return 0.5; + }, + vLineWidth: () => 0.5, + hLineColor: () => PDF_COLORS.border, + vLineColor: () => PDF_COLORS.border, + paddingLeft: () => 5, + paddingRight: () => 5, + paddingTop: () => 5, + paddingBottom: () => 5, + }, + }; + } + + private async _processChildrenWithMargins( + block: BlockSnapshot, + assets: FromDocSnapshotPayload['assets'] | undefined, + depth: number, + leftAdjustment: number, + rightAdjustment: number + ): Promise { + const content: Content[] = []; + if (block.children && block.children.length) { + for (const child of block.children) { + const childContent = await this._blockToContent( + child, + assets, + depth, + 0, + 0 + ); + const adjustedContent = this._adjustMargins( + childContent, + leftAdjustment, + rightAdjustment + ); + content.push(...adjustedContent); + } + } + return content; + } + + private _getImagePlaceholderContent(caption: string): Content { + return { + text: getImagePlaceholder(caption), + italics: true, + color: PDF_COLORS.textMuted, + margin: [0, 5, 0, 5], + }; + } + + private _createDocDefinition( + title: string | undefined, + content: Content[] + ): TDocumentDefinitions { + const docContent = + title === undefined + ? content + : [ + { + text: title || 'Untitled', + style: 'title', + margin: [0, 0, 0, 20], + } as ContentText, + ...content, + ]; + + return { + content: docContent, + styles: { + title: { + fontSize: 24, + bold: true, + alignment: 'left', + }, + header1: { + fontSize: 20, + bold: true, + alignment: 'left', + }, + header2: { + fontSize: 18, + bold: true, + alignment: 'left', + }, + header3: { + fontSize: 16, + bold: true, + alignment: 'left', + }, + header4: { + fontSize: 14, + bold: true, + alignment: 'left', + }, + code: { + fontSize: 10, + font: 'Inter', + color: PDF_COLORS.text, + background: PDF_COLORS.codeBackground, + }, + }, + defaultStyle: { + font: 'SarasaGothicCL', + fontSize: 12, + lineHeight: 1.5, + }, + }; + } + + private async _createPdfBlob( + docDefinition: TDocumentDefinitions + ): Promise { + return new Promise((resolve, reject) => { + try { + const pdfDocGenerator = pdfMake.createPdf(docDefinition); + pdfDocGenerator.getBlob(blob => resolve(blob)); + } catch (error) { + reject(error); + } + }); + } +} diff --git a/blocksuite/affine/shared/src/adapters/pdf/svg-utils.ts b/blocksuite/affine/shared/src/adapters/pdf/svg-utils.ts new file mode 100644 index 0000000000000..7000ac6cb94d9 --- /dev/null +++ b/blocksuite/affine/shared/src/adapters/pdf/svg-utils.ts @@ -0,0 +1,42 @@ +/** + * SVG icon generation utilities + */ + +import { resolveCssVariable } from './css-utils.js'; + +/** + * Get SVG string for bulleted list icon based on depth + */ +export function getBulletIconSvg(depth: number): string { + const bulletIndex = depth % 4; + const blueColor = resolveCssVariable('var(--affine-blue-700)') || '#1E96EB'; + const bulletSvgs = [ + ``, + ``, + ``, + ``, + ]; + return bulletSvgs[bulletIndex]; +} + +/** + * Get SVG string for checkbox icon (checked or unchecked) + */ +export function getCheckboxIconSvg(checked: boolean): string { + if (checked) { + return ''; + } else { + return ''; + } +} + +/** + * Get SVG string for toggle icon (down or right) + */ +export function getToggleIconSvg(expanded: boolean): string { + if (expanded) { + return ''; + } else { + return ''; + } +} diff --git a/blocksuite/affine/shared/src/adapters/pdf/utils.ts b/blocksuite/affine/shared/src/adapters/pdf/utils.ts new file mode 100644 index 0000000000000..22d3f831e037d --- /dev/null +++ b/blocksuite/affine/shared/src/adapters/pdf/utils.ts @@ -0,0 +1,71 @@ +/** + * Pure utility functions for PDF adapter + */ + +// Layout constants +export const BLOCK_CHILDREN_CONTAINER_PADDING_LEFT = 24; +export const MAX_PAPER_WIDTH = 550; +export const MAX_PAPER_HEIGHT = 800; + +// Color constants +export const PDF_COLORS = { + /** Primary link color */ + link: '#0066cc', + /** Primary text color */ + text: '#333333', + /** Secondary/muted text color */ + textMuted: '#666666', + /** Tertiary/disabled text color */ + textDisabled: '#999999', + /** Border/divider color */ + border: '#cccccc', + /** Code block background */ + codeBackground: '#f5f5f5', + /** Card/container background */ + cardBackground: '#f9f9f9', +} as const; + +/** + * Table layout with no borders (for custom styled containers) + */ +export const TABLE_LAYOUT_NO_BORDERS = { + hLineWidth: () => 0, + vLineWidth: () => 0, + paddingLeft: () => 0, + paddingRight: () => 0, + paddingTop: () => 0, + paddingBottom: () => 0, +} as const; + +/** + * Generate placeholder text for images that cannot be rendered + */ +export function getImagePlaceholder(caption?: string): string { + return caption ? `[Image: ${caption}]` : '[Image]'; +} + +/** + * Check if text content has meaningful content + */ +export function hasTextContent( + textContent: string | Array +): boolean { + if (typeof textContent === 'string') { + return textContent.trim() !== ''; + } + return textContent.length > 0; +} + +/** + * Convert text content array to plain string + */ +export function textContentToString( + textContent: string | Array +): string { + if (typeof textContent === 'string') { + return textContent; + } + return textContent + .map(item => (typeof item === 'string' ? item : item.text)) + .join(''); +} diff --git a/blocksuite/affine/shared/src/services/feature-flag-service.ts b/blocksuite/affine/shared/src/services/feature-flag-service.ts index c0ecf548038a6..adacf4f53f729 100644 --- a/blocksuite/affine/shared/src/services/feature-flag-service.ts +++ b/blocksuite/affine/shared/src/services/feature-flag-service.ts @@ -21,6 +21,7 @@ export interface BlockSuiteFlags { enable_table_virtual_scroll: boolean; enable_turbo_renderer: boolean; enable_dom_renderer: boolean; + enable_pdfmake_export: boolean; } export class FeatureFlagService extends StoreExtension { @@ -46,6 +47,7 @@ export class FeatureFlagService extends StoreExtension { enable_table_virtual_scroll: false, enable_turbo_renderer: false, enable_dom_renderer: false, + enable_pdfmake_export: false, }); setFlag(key: keyof BlockSuiteFlags, value: boolean) { diff --git a/blocksuite/affine/shared/src/utils/index.ts b/blocksuite/affine/shared/src/utils/index.ts index 09f88adf2750e..2c1862ccc8636 100644 --- a/blocksuite/affine/shared/src/utils/index.ts +++ b/blocksuite/affine/shared/src/utils/index.ts @@ -15,6 +15,7 @@ export * from './insert'; export * from './is-abort-error'; export * from './math'; export * from './model'; +export * from './number-prefix'; export * from './popper-position'; export * from './print-to-pdf'; export * from './reference'; diff --git a/blocksuite/affine/blocks/list/src/utils/get-number-prefix.ts b/blocksuite/affine/shared/src/utils/number-prefix.ts similarity index 85% rename from blocksuite/affine/blocks/list/src/utils/get-number-prefix.ts rename to blocksuite/affine/shared/src/utils/number-prefix.ts index bb1fa5207b28f..bc716de09179d 100644 --- a/blocksuite/affine/blocks/list/src/utils/get-number-prefix.ts +++ b/blocksuite/affine/shared/src/utils/number-prefix.ts @@ -11,7 +11,7 @@ function number2letter(n: number) { } // Derive from https://gist.github.com/imilu/00f32c61e50b7ca296f91e9d96d8e976 -export function number2roman(num: number) { +function number2roman(num: number) { const lookup: Record = { M: 1000, CM: 900, @@ -28,12 +28,13 @@ export function number2roman(num: number) { I: 1, }; let romanStr = ''; - for (const i in lookup) { - while (num >= lookup[i]) { - romanStr += i; - num -= lookup[i]; + for (const [key, value] of Object.entries(lookup)) { + while (num >= value) { + romanStr += key; + num -= value; } } + return romanStr; } diff --git a/blocksuite/affine/widgets/linked-doc/src/transformers/index.ts b/blocksuite/affine/widgets/linked-doc/src/transformers/index.ts index e0766f80f35c9..28f8f826ec8a7 100644 --- a/blocksuite/affine/widgets/linked-doc/src/transformers/index.ts +++ b/blocksuite/affine/widgets/linked-doc/src/transformers/index.ts @@ -2,5 +2,6 @@ export { DocxTransformer } from './docx.js'; export { HtmlTransformer } from './html.js'; export { MarkdownTransformer } from './markdown.js'; export { NotionHtmlTransformer } from './notion-html.js'; +export { PdfTransformer } from './pdf.js'; export { createAssetsArchive, download } from './utils.js'; export { ZipTransformer } from './zip.js'; diff --git a/blocksuite/affine/widgets/linked-doc/src/transformers/pdf.ts b/blocksuite/affine/widgets/linked-doc/src/transformers/pdf.ts new file mode 100644 index 0000000000000..3ef41402fdac1 --- /dev/null +++ b/blocksuite/affine/widgets/linked-doc/src/transformers/pdf.ts @@ -0,0 +1,32 @@ +import { + docLinkBaseURLMiddleware, + embedSyncedDocMiddleware, + PdfAdapter, + titleMiddleware, +} from '@blocksuite/affine-shared/adapters'; +import type { Store } from '@blocksuite/store'; + +import { download } from './utils.js'; + +async function exportDoc(doc: Store) { + const provider = doc.provider; + const job = doc.getTransformer([ + docLinkBaseURLMiddleware(doc.workspace.id), + titleMiddleware(doc.workspace.meta.docMetas), + embedSyncedDocMiddleware('content'), + ]); + const snapshot = job.docToSnapshot(doc); + if (!snapshot) { + return; + } + const adapter = new PdfAdapter(job, provider); + const { file } = await adapter.fromDocSnapshot({ + snapshot, + assets: job.assetsManager, + }); + download(file.blob, file.fileName); +} + +export const PdfTransformer = { + exportDoc, +}; diff --git a/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts b/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts index 5ea56c6748640..cc2343cadc4f2 100644 --- a/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts +++ b/packages/frontend/admin/src/modules/accounts/components/use-user-management.ts @@ -323,6 +323,7 @@ export const useExportUsers = () => { }); dataToCopy.push(row); }); + // eslint-disable-next-line @typescript-eslint/no-floating-promises navigator.clipboard.writeText(JSON.stringify(dataToCopy, null, 2)); callback?.(); }, diff --git a/packages/frontend/core/src/blocksuite/ai/components/playground/content.ts b/packages/frontend/core/src/blocksuite/ai/components/playground/content.ts index 099751177bff5..ed7b58fee2bd9 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/playground/content.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/playground/content.ts @@ -297,9 +297,11 @@ export class PlaygroundContent extends SignalWatcher( } }; + // eslint-disable-next-line @typescript-eslint/no-misused-promises button.addEventListener('click', handleSendClick); this._disposables.add(() => { + // eslint-disable-next-line @typescript-eslint/no-misused-promises button.removeEventListener('click', handleSendClick); }); } diff --git a/packages/frontend/core/src/bootstrap/cleanup.ts b/packages/frontend/core/src/bootstrap/cleanup.ts index d97ab165f66ec..c3c97a87f61ef 100644 --- a/packages/frontend/core/src/bootstrap/cleanup.ts +++ b/packages/frontend/core/src/bootstrap/cleanup.ts @@ -4,25 +4,30 @@ function cleanupUnusedIndexedDB() { return; } - indexedDB.databases().then(databases => { - databases.forEach(database => { - if (database.name?.endsWith(':server-clock')) { - indexedDB.deleteDatabase(database.name); - } - if (database.name?.endsWith(':sync-metadata')) { - indexedDB.deleteDatabase(database.name); - } - if ( - database.name?.startsWith('idx:') && - (database.name.endsWith(':block') || database.name.endsWith(':doc')) - ) { - indexedDB.deleteDatabase(database.name); - } - if (database.name?.startsWith('jp:')) { - indexedDB.deleteDatabase(database.name); - } + indexedDB + .databases() + .then(databases => { + databases.forEach(database => { + if (database.name?.endsWith(':server-clock')) { + indexedDB.deleteDatabase(database.name); + } + if (database.name?.endsWith(':sync-metadata')) { + indexedDB.deleteDatabase(database.name); + } + if ( + database.name?.startsWith('idx:') && + (database.name.endsWith(':block') || database.name.endsWith(':doc')) + ) { + indexedDB.deleteDatabase(database.name); + } + if (database.name?.startsWith('jp:')) { + indexedDB.deleteDatabase(database.name); + } + }); + }) + .catch(error => { + console.error('Failed to cleanup unused IndexedDB databases:', error); }); - }); } cleanupUnusedIndexedDB(); diff --git a/packages/frontend/core/src/components/hooks/affine/use-export-page.ts b/packages/frontend/core/src/components/hooks/affine/use-export-page.ts index 9ce439f4e497f..4baaf3ad3d9f4 100644 --- a/packages/frontend/core/src/components/hooks/affine/use-export-page.ts +++ b/packages/frontend/core/src/components/hooks/affine/use-export-page.ts @@ -24,6 +24,7 @@ import { download, HtmlTransformer, MarkdownTransformer, + PdfTransformer, ZipTransformer, } from '@blocksuite/affine/widgets/linked-doc'; import { useLiveData, useService } from '@toeverything/infra'; @@ -32,7 +33,13 @@ import { nanoid } from 'nanoid'; import { useAsyncCallback } from '../affine-async-hooks'; -type ExportType = 'pdf' | 'html' | 'png' | 'markdown' | 'snapshot'; +type ExportType = + | 'pdf' + | 'html' + | 'png' + | 'markdown' + | 'snapshot' + | 'pdf-export'; interface ExportHandlerOptions { page: Store; @@ -164,6 +171,10 @@ async function exportHandler({ await editorRoot?.std.get(ExportManager).exportPng(); return; } + case 'pdf-export': { + await PdfTransformer.exportDoc(page); + return; + } } } diff --git a/packages/frontend/core/src/components/page-list/operation-menu-items/export.tsx b/packages/frontend/core/src/components/page-list/operation-menu-items/export.tsx index 279e46b646901..f81f4d79f7eaa 100644 --- a/packages/frontend/core/src/components/page-list/operation-menu-items/export.tsx +++ b/packages/frontend/core/src/components/page-list/operation-menu-items/export.tsx @@ -1,4 +1,5 @@ import { MenuItem, MenuSeparator, MenuSub } from '@affine/component'; +import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { useI18n } from '@affine/i18n'; import { track } from '@affine/track'; import { @@ -9,6 +10,7 @@ import { PageIcon, PrinterIcon, } from '@blocksuite/icons/rc'; +import { useLiveData, useService } from '@toeverything/infra'; import type { ReactNode } from 'react'; import { useCallback } from 'react'; @@ -24,7 +26,7 @@ interface ExportMenuItemProps { interface ExportProps { exportHandler: ( - type: 'pdf' | 'html' | 'png' | 'markdown' | 'snapshot' + type: 'pdf' | 'html' | 'png' | 'markdown' | 'snapshot' | 'pdf-export' ) => void; pageMode?: 'page' | 'edgeless'; className?: string; @@ -72,6 +74,11 @@ export const ExportMenuItems = ({ pageMode = 'page', }: ExportProps) => { const t = useI18n(); + const featureFlags = useService(FeatureFlagService).flags; + const enable_pdfmake_export = useLiveData( + featureFlags.enable_pdfmake_export.$ + ); + return ( <> } label={t['Export to Markdown']()} /> + {pageMode !== 'edgeless' && enable_pdfmake_export && ( + exportHandler('pdf-export')} + className={className} + type="pdf-export" + icon={} + label={t['Export to PDF']()} + /> + )} exportHandler('snapshot')} className={className} diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/mcp-server/setting-panel.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/mcp-server/setting-panel.tsx index a7de24543eafd..17d1e474b6a0d 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/mcp-server/setting-panel.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/integration/mcp-server/setting-panel.tsx @@ -158,6 +158,7 @@ const McpServerSetting = () => { variant="primary" onClick={() => { if (!code) return; + // eslint-disable-next-line @typescript-eslint/no-floating-promises navigator.clipboard.writeText(code); notify.success({ title: t['Copied to clipboard'](), diff --git a/packages/frontend/core/src/modules/feature-flag/constant.ts b/packages/frontend/core/src/modules/feature-flag/constant.ts index c1a212cb67436..172e0ae68e4aa 100644 --- a/packages/frontend/core/src/modules/feature-flag/constant.ts +++ b/packages/frontend/core/src/modules/feature-flag/constant.ts @@ -288,6 +288,15 @@ export const AFFINE_FLAGS = { configurable: isMobile, defaultState: false, }, + enable_pdfmake_export: { + category: 'blocksuite', + bsFlag: 'enable_pdfmake_export', + displayName: 'Enable PDF Export', + description: + 'Experimental export PDFs support, it may contain the wrong style.', + configurable: true, + defaultState: false, + }, } satisfies { [key in string]: FlagInfo }; // oxlint-disable-next-line no-redeclare diff --git a/packages/frontend/media-capture-playground/web/components/saved-recording-item.tsx b/packages/frontend/media-capture-playground/web/components/saved-recording-item.tsx index 493651f1fddb3..988f7308a1aec 100644 --- a/packages/frontend/media-capture-playground/web/components/saved-recording-item.tsx +++ b/packages/frontend/media-capture-playground/web/components/saved-recording-item.tsx @@ -727,6 +727,7 @@ export function SavedRecordingItem({ const handlePlayPause = React.useCallback(() => { if (audioRef.current) { if (audioRef.current.paused) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises void audioRef.current.play(); } else { audioRef.current.pause(); diff --git a/tests/kit/src/utils/keyboard.ts b/tests/kit/src/utils/keyboard.ts index 68025bce68726..7ed9ff71ba26b 100644 --- a/tests/kit/src/utils/keyboard.ts +++ b/tests/kit/src/utils/keyboard.ts @@ -114,6 +114,7 @@ export async function writeTextToClipboard( // paste the url await page.evaluate( async ([text]) => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises navigator.clipboard.writeText(''); const e = new ClipboardEvent('paste', { clipboardData: new DataTransfer(), diff --git a/yarn.lock b/yarn.lock index 7007750f376ed..f7092c1bf3bd9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3835,6 +3835,7 @@ __metadata: "@types/hast": "npm:^3.0.4" "@types/lodash-es": "npm:^4.17.12" "@types/mdast": "npm:^4.0.4" + "@types/pdfmake": "npm:^0.2.12" bytes: "npm:^3.1.2" dompurify: "npm:^3.3.0" fractional-indexing: "npm:^3.2.0" @@ -3852,6 +3853,7 @@ __metadata: micromark-extension-gfm-task-list-item: "npm:^2.1.0" micromark-util-combine-extensions: "npm:^2.0.0" minimatch: "npm:^10.1.1" + pdfmake: "npm:^0.2.20" quick-lru: "npm:^7.3.0" rehype-parse: "npm:^9.0.0" rehype-stringify: "npm:^10.0.0" @@ -5539,7 +5541,7 @@ __metadata: languageName: node linkType: hard -"@emnapi/core@npm:^1.4.3, @emnapi/core@npm:^1.7.1": +"@emnapi/core@npm:^1.4.0, @emnapi/core@npm:^1.4.3, @emnapi/core@npm:^1.7.1": version: 1.7.1 resolution: "@emnapi/core@npm:1.7.1" dependencies: @@ -5549,7 +5551,7 @@ __metadata: languageName: node linkType: hard -"@emnapi/runtime@npm:^1.2.0, @emnapi/runtime@npm:^1.4.3, @emnapi/runtime@npm:^1.5.0, @emnapi/runtime@npm:^1.7.1": +"@emnapi/runtime@npm:^1.2.0, @emnapi/runtime@npm:^1.4.0, @emnapi/runtime@npm:^1.4.3, @emnapi/runtime@npm:^1.5.0, @emnapi/runtime@npm:^1.7.1": version: 1.7.1 resolution: "@emnapi/runtime@npm:1.7.1" dependencies: @@ -6334,6 +6336,52 @@ __metadata: languageName: node linkType: hard +"@foliojs-fork/fontkit@npm:^1.9.2": + version: 1.9.2 + resolution: "@foliojs-fork/fontkit@npm:1.9.2" + dependencies: + "@foliojs-fork/restructure": "npm:^2.0.2" + brotli: "npm:^1.2.0" + clone: "npm:^1.0.4" + deep-equal: "npm:^1.0.0" + dfa: "npm:^1.2.0" + tiny-inflate: "npm:^1.0.2" + unicode-properties: "npm:^1.2.2" + unicode-trie: "npm:^2.0.0" + checksum: 10/143724742532e6fb8288958cff10c99855a44a48ab0fd32708e8c55a84a1ae25118035360fda4ac4e27797332923f2bbe8026b409eacf36dede6deeb79efe4a0 + languageName: node + linkType: hard + +"@foliojs-fork/linebreak@npm:^1.1.1, @foliojs-fork/linebreak@npm:^1.1.2": + version: 1.1.2 + resolution: "@foliojs-fork/linebreak@npm:1.1.2" + dependencies: + base64-js: "npm:1.3.1" + unicode-trie: "npm:^2.0.0" + checksum: 10/5af61cb29a5f6bd055941b6a0251a89f21bb97fb22f56a56b56a4f15dcf59c6088e052bfbce8bdc62d9cd7d6ec14a494292587e60f171f52615bc02bda56d0da + languageName: node + linkType: hard + +"@foliojs-fork/pdfkit@npm:^0.15.3": + version: 0.15.3 + resolution: "@foliojs-fork/pdfkit@npm:0.15.3" + dependencies: + "@foliojs-fork/fontkit": "npm:^1.9.2" + "@foliojs-fork/linebreak": "npm:^1.1.1" + crypto-js: "npm:^4.2.0" + jpeg-exif: "npm:^1.1.4" + png-js: "npm:^1.0.0" + checksum: 10/cefd13f5d2d4b4cb2f7e5f0f0852000c3b868d755dbc0761e59565f42d10e42385a158d8847defef9b255ec5d994a8106b5233c7f7b8aea609597866e3766129 + languageName: node + linkType: hard + +"@foliojs-fork/restructure@npm:^2.0.2": + version: 2.0.2 + resolution: "@foliojs-fork/restructure@npm:2.0.2" + checksum: 10/3b89107426b5887de2844424a4059ab07293715752fb81a1527887d2aafb2ab359b567c8a38ec9dd32a2c1781fc3ac4011a71980ef818cc9323ad80eb19d25ea + languageName: node + linkType: hard + "@gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" @@ -7590,21 +7638,21 @@ __metadata: languageName: node linkType: hard -"@inquirer/checkbox@npm:^4.3.2": - version: 4.3.2 - resolution: "@inquirer/checkbox@npm:4.3.2" +"@inquirer/checkbox@npm:^4.1.6": + version: 4.1.6 + resolution: "@inquirer/checkbox@npm:4.1.6" dependencies: - "@inquirer/ansi": "npm:^1.0.2" - "@inquirer/core": "npm:^10.3.2" - "@inquirer/figures": "npm:^1.0.15" - "@inquirer/type": "npm:^3.0.10" - yoctocolors-cjs: "npm:^2.1.3" + "@inquirer/core": "npm:^10.1.11" + "@inquirer/figures": "npm:^1.0.11" + "@inquirer/type": "npm:^3.0.6" + ansi-escapes: "npm:^4.3.2" + yoctocolors-cjs: "npm:^2.1.2" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/4ac5dd2679981e23f066c51c605cb1c63ccda9ea6e1ad895e675eb26702aaf6cf961bf5ca3acd832efba5edcf9883b6742002c801673d2b35c123a7fa7db7b23 + checksum: 10/28012e16e72393ad6cc5b659620685a75e3e0227c3a2c6d6d1b235742ed7cae0516479e0e1b974c002b8fc7bf49698e9af2900a22cc5b1a83257d9000802401b languageName: node linkType: hard @@ -7635,7 +7683,7 @@ __metadata: languageName: node linkType: hard -"@inquirer/confirm@npm:^5.0.0, @inquirer/confirm@npm:^5.1.21": +"@inquirer/confirm@npm:^5.0.0, @inquirer/confirm@npm:^5.1.10": version: 5.1.21 resolution: "@inquirer/confirm@npm:5.1.21" dependencies: @@ -7737,19 +7785,19 @@ __metadata: languageName: node linkType: hard -"@inquirer/editor@npm:^4.2.23": - version: 4.2.23 - resolution: "@inquirer/editor@npm:4.2.23" +"@inquirer/editor@npm:^4.2.11": + version: 4.2.11 + resolution: "@inquirer/editor@npm:4.2.11" dependencies: - "@inquirer/core": "npm:^10.3.2" - "@inquirer/external-editor": "npm:^1.0.3" - "@inquirer/type": "npm:^3.0.10" + "@inquirer/core": "npm:^10.1.11" + "@inquirer/type": "npm:^3.0.6" + external-editor: "npm:^3.1.0" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/f91b9aadba6ea28a0f4ea5f075af421e076262aebbd737e1b9779f086fa9d559d064e9942a581544645d1dcf56d6b685e8063fe46677880fbca73f6de4e4e7c5 + checksum: 10/dcc65e6dc2cf25fd03939b54ff195521748114d3d2986296d708b4357d48d9ac5843e9774b1d02e0f77b9b0edbf4c8b10a77edd99910e1833864b379e5b66ced languageName: node linkType: hard @@ -7780,19 +7828,19 @@ __metadata: languageName: node linkType: hard -"@inquirer/expand@npm:^4.0.23": - version: 4.0.23 - resolution: "@inquirer/expand@npm:4.0.23" +"@inquirer/expand@npm:^4.0.13": + version: 4.0.13 + resolution: "@inquirer/expand@npm:4.0.13" dependencies: - "@inquirer/core": "npm:^10.3.2" - "@inquirer/type": "npm:^3.0.10" - yoctocolors-cjs: "npm:^2.1.3" + "@inquirer/core": "npm:^10.1.11" + "@inquirer/type": "npm:^3.0.6" + yoctocolors-cjs: "npm:^2.1.2" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/73ad1d6376e5efe2a452c33494d6d16ee2670c638ae470a795fdff4acb59a8e032e38e141f87b603b6e96320977519b375dac6471d86d5e3087a9c1db40e3111 + checksum: 10/25ac3a84dbd0b7763aa85ce75c9f3d2022bcc307973a5a3e0b538e2c1e2a94b5eef0b786536589e5f1554a7654853887d150c80b66e3335cc831aa0a5e7d088a languageName: node linkType: hard @@ -7811,21 +7859,6 @@ __metadata: languageName: node linkType: hard -"@inquirer/external-editor@npm:^1.0.3": - version: 1.0.3 - resolution: "@inquirer/external-editor@npm:1.0.3" - dependencies: - chardet: "npm:^2.1.1" - iconv-lite: "npm:^0.7.0" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10/c95d7237a885b32031715089f92820525731d4d3c2bd7afdb826307dc296cc2b39e7a644b0bb265441963348cca42e7785feb29c3aaf18fd2b63131769bf6587 - languageName: node - linkType: hard - "@inquirer/external-editor@npm:^2.0.2": version: 2.0.2 resolution: "@inquirer/external-editor@npm:2.0.2" @@ -7841,7 +7874,7 @@ __metadata: languageName: node linkType: hard -"@inquirer/figures@npm:^1.0.15, @inquirer/figures@npm:^1.0.6": +"@inquirer/figures@npm:^1.0.11, @inquirer/figures@npm:^1.0.15, @inquirer/figures@npm:^1.0.6": version: 1.0.15 resolution: "@inquirer/figures@npm:1.0.15" checksum: 10/3f858807f361ca29f41ec1076bbece4098cc140d86a06159d42c6e3f6e4d9bec9e10871ccfcbbaa367d6a8462b01dff89f2b1b157d9de6e8726bec85533f525c @@ -7865,18 +7898,18 @@ __metadata: languageName: node linkType: hard -"@inquirer/input@npm:^4.3.1": - version: 4.3.1 - resolution: "@inquirer/input@npm:4.3.1" +"@inquirer/input@npm:^4.1.10": + version: 4.1.10 + resolution: "@inquirer/input@npm:4.1.10" dependencies: - "@inquirer/core": "npm:^10.3.2" - "@inquirer/type": "npm:^3.0.10" + "@inquirer/core": "npm:^10.1.11" + "@inquirer/type": "npm:^3.0.6" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/713aaa4c94263299fbd7adfd65378f788cac1b5047f2b7e1ea349ca669db6c7c91b69ab6e2f6660cdbc28c7f7888c5c77ab4433bd149931597e43976d1ba5f34 + checksum: 10/61ea42f1171fc0113bfde9fd5b5a32a6f436011178fa08613685f337b3f3cb1bc60b1a76b3ab55fc2c895d87196526add2e1b0711249d539eb982428878566f2 languageName: node linkType: hard @@ -7905,18 +7938,18 @@ __metadata: languageName: node linkType: hard -"@inquirer/number@npm:^3.0.23": - version: 3.0.23 - resolution: "@inquirer/number@npm:3.0.23" +"@inquirer/number@npm:^3.0.13": + version: 3.0.13 + resolution: "@inquirer/number@npm:3.0.13" dependencies: - "@inquirer/core": "npm:^10.3.2" - "@inquirer/type": "npm:^3.0.10" + "@inquirer/core": "npm:^10.1.11" + "@inquirer/type": "npm:^3.0.6" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/50694807b71746e15ed69d100aae3c8014d83c90aa660e8a179fe0db1046f26d727947542f64e24cc8b969a61659cb89fe36208cc2b59c1816382b598e686dd2 + checksum: 10/6df930d3ef281dff5b6b59fbdc999bcfeaf49175e2a1739f9db80a4e10b10060045cb265fb2737c8382d8264f457ab2f647c20368e288562068d2bba36fdca54 languageName: node linkType: hard @@ -7946,19 +7979,19 @@ __metadata: languageName: node linkType: hard -"@inquirer/password@npm:^4.0.23": - version: 4.0.23 - resolution: "@inquirer/password@npm:4.0.23" +"@inquirer/password@npm:^4.0.13": + version: 4.0.13 + resolution: "@inquirer/password@npm:4.0.13" dependencies: - "@inquirer/ansi": "npm:^1.0.2" - "@inquirer/core": "npm:^10.3.2" - "@inquirer/type": "npm:^3.0.10" + "@inquirer/core": "npm:^10.1.11" + "@inquirer/type": "npm:^3.0.6" + ansi-escapes: "npm:^4.3.2" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/97364970b01c85946a4a50ad876c53ef0c1857a9144e24fad65e5dfa4b4e5dd42564fbcdfa2b49bb049a25d127efbe0882cb18afcdd47b166ebd01c6c4b5e825 + checksum: 10/f45f51e12326586b205195f5b669ce6f529b7f0bfad9ab667fb90bd4858c86d9bfb372310e0deb7f600fccf2577bd3a992feaf3fcfbc86b86e715878c4ed52e5 languageName: node linkType: hard @@ -7997,25 +8030,25 @@ __metadata: linkType: hard "@inquirer/prompts@npm:^7.4.0, @inquirer/prompts@npm:^7.5.1": - version: 7.10.1 - resolution: "@inquirer/prompts@npm:7.10.1" - dependencies: - "@inquirer/checkbox": "npm:^4.3.2" - "@inquirer/confirm": "npm:^5.1.21" - "@inquirer/editor": "npm:^4.2.23" - "@inquirer/expand": "npm:^4.0.23" - "@inquirer/input": "npm:^4.3.1" - "@inquirer/number": "npm:^3.0.23" - "@inquirer/password": "npm:^4.0.23" - "@inquirer/rawlist": "npm:^4.1.11" - "@inquirer/search": "npm:^3.2.2" - "@inquirer/select": "npm:^4.4.2" + version: 7.5.1 + resolution: "@inquirer/prompts@npm:7.5.1" + dependencies: + "@inquirer/checkbox": "npm:^4.1.6" + "@inquirer/confirm": "npm:^5.1.10" + "@inquirer/editor": "npm:^4.2.11" + "@inquirer/expand": "npm:^4.0.13" + "@inquirer/input": "npm:^4.1.10" + "@inquirer/number": "npm:^3.0.13" + "@inquirer/password": "npm:^4.0.13" + "@inquirer/rawlist": "npm:^4.1.1" + "@inquirer/search": "npm:^3.0.13" + "@inquirer/select": "npm:^4.2.1" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/b3e3386edd255e4e91c7908050674f8a2e69b043883c00feec2f87d697be37bc6e8cd4a360e7e3233a9825ae7ea044a2ac63d5700926d27f9959013d8566f890 + checksum: 10/febb8a1bb6e7ff63b0e6c88ac9af7f7a2daf621f80c0e720cc7a68bd9fa99c7253911271d547ba3b55f18b580298a58440f3f45c974b8e895cfae929fadec868 languageName: node linkType: hard @@ -8053,19 +8086,19 @@ __metadata: languageName: node linkType: hard -"@inquirer/rawlist@npm:^4.1.11": - version: 4.1.11 - resolution: "@inquirer/rawlist@npm:4.1.11" +"@inquirer/rawlist@npm:^4.1.1": + version: 4.1.1 + resolution: "@inquirer/rawlist@npm:4.1.1" dependencies: - "@inquirer/core": "npm:^10.3.2" - "@inquirer/type": "npm:^3.0.10" - yoctocolors-cjs: "npm:^2.1.3" + "@inquirer/core": "npm:^10.1.11" + "@inquirer/type": "npm:^3.0.6" + yoctocolors-cjs: "npm:^2.1.2" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/0d8f6484cfc20749190e95eecfb2d034bafb3644ec4907b84b1673646f5dd71730e38e35565ea98dfd240d8851e3cff653edafcc4e0af617054b127b407e3229 + checksum: 10/e7c272f9f7a1576c9c1212a278c2d4bad7b394ddf512d3bbbf75902baa7a4fe4bde1b707f1d4c0cbe3963d0ba5a92e7fcbc4dffbb817ecec9b4fa70ac97b535d languageName: node linkType: hard @@ -8096,20 +8129,20 @@ __metadata: languageName: node linkType: hard -"@inquirer/search@npm:^3.2.2": - version: 3.2.2 - resolution: "@inquirer/search@npm:3.2.2" +"@inquirer/search@npm:^3.0.13": + version: 3.0.13 + resolution: "@inquirer/search@npm:3.0.13" dependencies: - "@inquirer/core": "npm:^10.3.2" - "@inquirer/figures": "npm:^1.0.15" - "@inquirer/type": "npm:^3.0.10" - yoctocolors-cjs: "npm:^2.1.3" + "@inquirer/core": "npm:^10.1.11" + "@inquirer/figures": "npm:^1.0.11" + "@inquirer/type": "npm:^3.0.6" + yoctocolors-cjs: "npm:^2.1.2" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/abaed2df7763633ff4414b58d1c87233b69ed3cd2ac77629f0d54b72b8b585dc4806c7a2a8261daba58af5b0a2147e586d079fdc82060b6bcf56b75d3d03f3a7 + checksum: 10/2b8a94c5d83e4eced093caa680cb6561037d047702b91f77adc1ab56189d0c78974de0017946004b7acef9f8312772d7369ad227c0fbc59133ad3243981eff3d languageName: node linkType: hard @@ -8142,21 +8175,21 @@ __metadata: languageName: node linkType: hard -"@inquirer/select@npm:^4.4.2": - version: 4.4.2 - resolution: "@inquirer/select@npm:4.4.2" +"@inquirer/select@npm:^4.2.1": + version: 4.2.1 + resolution: "@inquirer/select@npm:4.2.1" dependencies: - "@inquirer/ansi": "npm:^1.0.2" - "@inquirer/core": "npm:^10.3.2" - "@inquirer/figures": "npm:^1.0.15" - "@inquirer/type": "npm:^3.0.10" - yoctocolors-cjs: "npm:^2.1.3" + "@inquirer/core": "npm:^10.1.11" + "@inquirer/figures": "npm:^1.0.11" + "@inquirer/type": "npm:^3.0.6" + ansi-escapes: "npm:^4.3.2" + yoctocolors-cjs: "npm:^2.1.2" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/795ec0ac77d575f20bd6a12fb1c040093e62217ac0c80194829a8d3c3d1e09f70ad738e9a9dd6095cc8358fff4e13882209c09bdf8eb0864a86dcabef5b0a6a6 + checksum: 10/883ff2c359052efe9be021d5cf5133970c49f62ac07ba18fd949d71242a40608708f9a0651a1094c6e1dcbc914c40817646f57ac2b281b485fa331dd49232083 languageName: node linkType: hard @@ -9823,13 +9856,13 @@ __metadata: linkType: hard "@napi-rs/wasm-runtime@npm:^0.2.5, @napi-rs/wasm-runtime@npm:^0.2.7, @napi-rs/wasm-runtime@npm:^0.2.9": - version: 0.2.12 - resolution: "@napi-rs/wasm-runtime@npm:0.2.12" + version: 0.2.9 + resolution: "@napi-rs/wasm-runtime@npm:0.2.9" dependencies: - "@emnapi/core": "npm:^1.4.3" - "@emnapi/runtime": "npm:^1.4.3" - "@tybys/wasm-util": "npm:^0.10.0" - checksum: 10/5fd518182427980c28bc724adf06c5f32f9a8915763ef560b5f7d73607d30cd15ac86d0cbd2eb80d4cfab23fc80d0876d89ca36a9daadcb864bc00917c94187c + "@emnapi/core": "npm:^1.4.0" + "@emnapi/runtime": "npm:^1.4.0" + "@tybys/wasm-util": "npm:^0.9.0" + checksum: 10/8ebc7d85e11e1b8d71908d5615ff24b27ef7af8287d087fb5cff5a3e545915c7545998d976a9cd6a4315dab4ba0f609439fbe6408fec3afebd288efb0dbdc135 languageName: node linkType: hard @@ -15888,7 +15921,7 @@ __metadata: languageName: node linkType: hard -"@tybys/wasm-util@npm:^0.10.0, @tybys/wasm-util@npm:^0.10.1": +"@tybys/wasm-util@npm:^0.10.1": version: 0.10.1 resolution: "@tybys/wasm-util@npm:0.10.1" dependencies: @@ -16941,6 +16974,25 @@ __metadata: languageName: node linkType: hard +"@types/pdfkit@npm:*": + version: 0.17.3 + resolution: "@types/pdfkit@npm:0.17.3" + dependencies: + "@types/node": "npm:*" + checksum: 10/58208a7969be6a1219f211d590b2315f3c50667c0c11c7e153df93fdfc178fd24a34356c6f496d3d9473087d1ac499e3b43df67c86098cc8bc77bb294d426b10 + languageName: node + linkType: hard + +"@types/pdfmake@npm:^0.2.12": + version: 0.2.12 + resolution: "@types/pdfmake@npm:0.2.12" + dependencies: + "@types/node": "npm:*" + "@types/pdfkit": "npm:*" + checksum: 10/a1dff188ec30ac4f3aa995cb74a2d213e7ae229a2aa269390383e91b62e984da1061ce43ced52f358596e51e3b5b7d77a4d3db1234e74e632c6e765a397824d0 + languageName: node + linkType: hard + "@types/pg-pool@npm:2.0.6": version: 2.0.6 resolution: "@types/pg-pool@npm:2.0.6" @@ -18881,7 +18933,14 @@ __metadata: languageName: node linkType: hard -"base64-js@npm:^1.3.0, base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": +"base64-js@npm:1.3.1": + version: 1.3.1 + resolution: "base64-js@npm:1.3.1" + checksum: 10/957b9ced0ea1b39588a117193f801b045a5fb2d6f1b9943dd304bcad46e5681bf837fe092105692b11653658e8443764139d6b11d3c4037093b96e8db4e1dbb2 + languageName: node + linkType: hard + +"base64-js@npm:^1.1.2, base64-js@npm:^1.3.0, base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" checksum: 10/669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 @@ -19133,6 +19192,15 @@ __metadata: languageName: node linkType: hard +"brotli@npm:^1.2.0": + version: 1.3.3 + resolution: "brotli@npm:1.3.3" + dependencies: + base64-js: "npm:^1.1.2" + checksum: 10/78b412f54be3c96b86e2d9805ddc26aa5a52bba45588ff7f8468b80aa84c90052c60eeb2e26ad032c39bab6baa58e0b0625cf4f738279961a31c34cbe4b4b490 + languageName: node + linkType: hard + "browser-fs-access@npm:^0.37.0": version: 0.37.0 resolution: "browser-fs-access@npm:0.37.0" @@ -20166,7 +20234,7 @@ __metadata: languageName: node linkType: hard -"clone@npm:^1.0.2": +"clone@npm:^1.0.2, clone@npm:^1.0.4": version: 1.0.4 resolution: "clone@npm:1.0.4" checksum: 10/d06418b7335897209e77bdd430d04f882189582e67bd1f75a04565f3f07f5b3f119a9d670c943b6697d0afb100f03b866b3b8a1f91d4d02d72c4ecf2bb64b5dd @@ -20953,6 +21021,13 @@ __metadata: languageName: node linkType: hard +"crypto-js@npm:^4.2.0": + version: 4.2.0 + resolution: "crypto-js@npm:4.2.0" + checksum: 10/c7bcc56a6e01c3c397e95aa4a74e4241321f04677f9a618a8f48a63b5781617248afb9adb0629824792e7ec20ca0d4241a49b6b2938ae6f973ec4efc5c53c924 + languageName: node + linkType: hard + "css-declaration-sorter@npm:^7.2.0": version: 7.2.0 resolution: "css-declaration-sorter@npm:7.2.0" @@ -21735,6 +21810,15 @@ __metadata: languageName: node linkType: hard +"deep-equal@npm:@nolyfill/deep-equal@^1": + version: 1.0.44 + resolution: "@nolyfill/deep-equal@npm:1.0.44" + dependencies: + dequal: "npm:2.0.3" + checksum: 10/11f4d3f92b1a98d842a27982dc9d3c2ef7b78917917387acde7f056de55d1afa59b7ea4cc62fc5236246970a4b799d6592670ce1043bb8c97b38da662ea924b6 + languageName: node + linkType: hard + "deep-extend@npm:^0.6.0": version: 0.6.0 resolution: "deep-extend@npm:0.6.0" @@ -21861,7 +21945,7 @@ __metadata: languageName: node linkType: hard -"dequal@npm:^2.0.0, dequal@npm:^2.0.3": +"dequal@npm:2.0.3, dequal@npm:^2.0.0, dequal@npm:^2.0.3": version: 2.0.3 resolution: "dequal@npm:2.0.3" checksum: 10/6ff05a7561f33603df87c45e389c9ac0a95e3c056be3da1a0c4702149e3a7f6fe5ffbb294478687ba51a9e95f3a60e8b6b9005993acd79c292c7d15f71964b6b @@ -21929,6 +22013,13 @@ __metadata: languageName: node linkType: hard +"dfa@npm:^1.2.0": + version: 1.2.0 + resolution: "dfa@npm:1.2.0" + checksum: 10/3b274fe6d2d70f41c1418ac961f7ae6e7f3b7445f20b98395a55943902100dd2491ef91a60c47c14a72645021f02248ccfad79fa65b10d6075bff34237a35bf8 + languageName: node + linkType: hard + "diff@npm:^4.0.1": version: 4.0.2 resolution: "diff@npm:4.0.2" @@ -26583,6 +26674,13 @@ __metadata: languageName: node linkType: hard +"jpeg-exif@npm:^1.1.4": + version: 1.1.4 + resolution: "jpeg-exif@npm:1.1.4" + checksum: 10/75699bd7161de1be99e847166917957bfb405ed736655361c58f0390e7182cc28999b2cbc31e650b71b6d2ad38843789439a121ac64988b2ccaa00f6832bb3d9 + languageName: node + linkType: hard + "js-string-escape@npm:^1.0.1": version: 1.0.1 resolution: "js-string-escape@npm:1.0.1" @@ -30425,6 +30523,13 @@ __metadata: languageName: node linkType: hard +"pako@npm:^0.2.5": + version: 0.2.9 + resolution: "pako@npm:0.2.9" + checksum: 10/627c6842e90af0b3a9ee47345bd66485a589aff9514266f4fa9318557ad819c46fedf97510f2cef9b6224c57913777966a05cb46caf6a9b31177a5401a06fe15 + languageName: node + linkType: hard + "pako@npm:^1.0.10, pako@npm:^1.0.11, pako@npm:^1.0.6, pako@npm:~1.0.2": version: 1.0.11 resolution: "pako@npm:1.0.11" @@ -30776,6 +30881,18 @@ __metadata: languageName: node linkType: hard +"pdfmake@npm:^0.2.20": + version: 0.2.20 + resolution: "pdfmake@npm:0.2.20" + dependencies: + "@foliojs-fork/linebreak": "npm:^1.1.2" + "@foliojs-fork/pdfkit": "npm:^0.15.3" + iconv-lite: "npm:^0.6.3" + xmldoc: "npm:^2.0.1" + checksum: 10/524807370eb9f9c2a55b7aadfb2dfbd183ac81b2d7dbcba8e4f79566f220fcbf8e73d3a28e5988d42b38094615c82b966d7a039a07159db836c800a7395b5d44 + languageName: node + linkType: hard + "pe-library@npm:^0.4.1": version: 0.4.1 resolution: "pe-library@npm:0.4.1" @@ -30995,6 +31112,13 @@ __metadata: languageName: node linkType: hard +"png-js@npm:^1.0.0": + version: 1.0.0 + resolution: "png-js@npm:1.0.0" + checksum: 10/7762c5ec06da1b1a3e99bc78599bb15e7f0b04b49b4507e71f606b280006148122bdf937e2e1cba81d279d1c9966694f5c8c34ceb82fb11f328fac06db5b17cb + languageName: node + linkType: hard + "points-on-curve@npm:0.2.0, points-on-curve@npm:^0.2.0": version: 0.2.0 resolution: "points-on-curve@npm:0.2.0" @@ -34969,6 +35093,13 @@ __metadata: languageName: node linkType: hard +"tiny-inflate@npm:^1.0.0, tiny-inflate@npm:^1.0.2": + version: 1.0.3 + resolution: "tiny-inflate@npm:1.0.3" + checksum: 10/f620114fb51ea4a16ea7b4c62d6dd753f8faf41808a133c53d431ed4bf2ca377b21443653a0096894f2be22ca11bb327f148e7e5431f9246068917724ec01ffc + languageName: node + linkType: hard + "tiny-invariant@npm:^1.3.3": version: 1.3.3 resolution: "tiny-invariant@npm:1.3.3" @@ -35095,9 +35226,9 @@ __metadata: linkType: hard "tmp@npm:^0.2.0": - version: 0.2.5 - resolution: "tmp@npm:0.2.5" - checksum: 10/dd4b78b32385eab4899d3ae296007b34482b035b6d73e1201c4a9aede40860e90997a1452c65a2d21aee73d53e93cd167d741c3db4015d90e63b6d568a93d7ec + version: 0.2.3 + resolution: "tmp@npm:0.2.3" + checksum: 10/7b13696787f159c9754793a83aa79a24f1522d47b87462ddb57c18ee93ff26c74cbb2b8d9138f571d2e0e765c728fb2739863a672b280528512c6d83d511c6fa languageName: node linkType: hard @@ -35624,6 +35755,26 @@ __metadata: languageName: node linkType: hard +"unicode-properties@npm:^1.2.2": + version: 1.4.1 + resolution: "unicode-properties@npm:1.4.1" + dependencies: + base64-js: "npm:^1.3.0" + unicode-trie: "npm:^2.0.0" + checksum: 10/f03d35036291b08aa2572dc51eff712e64fb1d8daaeb65e8add38a24c66c2b8bb3882ee19e6e8de424cfbbc6a4ebe14766816294c7f582b4bb5704402acbd089 + languageName: node + linkType: hard + +"unicode-trie@npm:^2.0.0": + version: 2.0.0 + resolution: "unicode-trie@npm:2.0.0" + dependencies: + pako: "npm:^0.2.5" + tiny-inflate: "npm:^1.0.0" + checksum: 10/60404411dbd363bdcca9e81c9327fa80469f2e685737bac88ec693225ff20b9b545ac37ca2da13ec02f1552167dd010dfefd7c58b72a73d44a89fab1ca9c2479 + languageName: node + linkType: hard + "unicorn-magic@npm:^0.1.0": version: 0.1.0 resolution: "unicorn-magic@npm:0.1.0" @@ -37001,6 +37152,15 @@ __metadata: languageName: node linkType: hard +"xmldoc@npm:^2.0.1": + version: 2.0.2 + resolution: "xmldoc@npm:2.0.2" + dependencies: + sax: "npm:^1.2.4" + checksum: 10/b62fe5b2de3c9f79e5d26d1737583a7988dcc19eb0bb1710480940b0500c50f5d565ebf9682b5be0b43658811ab919e05cab45b8ad5e8fee4bb9300a3e3cd758 + languageName: node + linkType: hard + "xmlhttprequest-ssl@npm:~2.1.1": version: 2.1.2 resolution: "xmlhttprequest-ssl@npm:2.1.2" From 844b9d9592e42e852060a584387fcfa092f9bbe7 Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Sun, 14 Dec 2025 00:28:43 +0800 Subject: [PATCH 06/11] feat(server): impl native reader for server (#14100) --- .devcontainer/build.sh | 1 - .github/actions/server-test-env/action.yml | 5 - .github/workflows/build-images.yml | 2 - .github/workflows/build-test.yml | 5 - .../src/__tests__/adapters/pdf.unit.spec.ts | 10 +- docs/developing-server.md | 6 - packages/backend/native/Cargo.toml | 28 +- packages/backend/native/index.d.ts | 29 + packages/backend/native/src/doc.rs | 93 ++ packages/backend/native/src/lib.rs | 1 + packages/backend/server/package.json | 1 - .../__snapshots__/controller.spec.ts.md | 55 +- .../__snapshots__/controller.spec.ts.snap | Bin 1845 -> 1651 bytes .../reader-from-database.spec.ts.md | 55 +- .../reader-from-database.spec.ts.snap | Bin 1703 -> 1505 bytes .../__snapshots__/reader-from-rpc.spec.ts.md | 55 +- .../reader-from-rpc.spec.ts.snap | Bin 1703 -> 1505 bytes .../backend/server/src/core/doc/reader.ts | 7 +- .../__snapshots__/blocksute.spec.ts.md | 815 ++++++++--------- .../__snapshots__/blocksute.spec.ts.snap | Bin 13070 -> 7330 bytes .../core/utils/__tests__/blocksute.spec.ts | 21 +- .../server/src/core/utils/blocksuite.ts | 91 +- packages/backend/server/src/native.ts | 4 + .../server/src/plugins/indexer/service.ts | 12 +- packages/backend/server/tsconfig.json | 1 - packages/common/native/src/doc_parser.rs | 332 +++++++ tools/utils/src/workspace.gen.ts | 1 - yarn.lock | 820 ++++++++---------- 28 files changed, 1315 insertions(+), 1135 deletions(-) create mode 100644 packages/backend/native/src/doc.rs diff --git a/.devcontainer/build.sh b/.devcontainer/build.sh index 4a19252cbb9de..9642696b66300 100644 --- a/.devcontainer/build.sh +++ b/.devcontainer/build.sh @@ -6,7 +6,6 @@ yarn install # Build Server Dependencies yarn affine @affine/server-native build -yarn affine @affine/reader build # Create database yarn affine @affine/server prisma migrate reset -f diff --git a/.github/actions/server-test-env/action.yml b/.github/actions/server-test-env/action.yml index 789465e65a027..50b3a6bc2b1ff 100644 --- a/.github/actions/server-test-env/action.yml +++ b/.github/actions/server-test-env/action.yml @@ -4,11 +4,6 @@ description: 'Prepare Server Test Environment' runs: using: 'composite' steps: - - name: Bundle @affine/reader - shell: bash - run: | - yarn affine @affine/reader build - - name: Initialize database shell: bash run: | diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml index 1ff7622a92662..1df80bcd67db8 100644 --- a/.github/workflows/build-images.yml +++ b/.github/workflows/build-images.yml @@ -187,8 +187,6 @@ jobs: path: ./packages/backend/native - name: List server-native files run: ls -alh ./packages/backend/native - - name: Build @affine/reader - run: yarn workspace @affine/reader build - name: Build Server run: yarn workspace @affine/server build - name: Upload server dist diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 3ba10309da68b..be7feca525e45 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -152,11 +152,6 @@ jobs: name: server-native.node path: ./packages/backend/native - - name: Bundle @affine/reader - shell: bash - run: | - yarn workspace @affine/reader build - - name: Run Check run: | yarn affine init diff --git a/blocksuite/affine/all/src/__tests__/adapters/pdf.unit.spec.ts b/blocksuite/affine/all/src/__tests__/adapters/pdf.unit.spec.ts index 185885ba4cf66..49f69873a947c 100644 --- a/blocksuite/affine/all/src/__tests__/adapters/pdf.unit.spec.ts +++ b/blocksuite/affine/all/src/__tests__/adapters/pdf.unit.spec.ts @@ -438,7 +438,7 @@ describe('snapshot to pdf', () => { expect(definition.styles?.code).toBeDefined(); expect(definition.defaultStyle).toBeDefined(); - expect(definition.defaultStyle?.font).toBe('Roboto'); + expect(definition.defaultStyle?.font).toBe('SarasaGothicCL'); }); describe('inline text styling', () => { @@ -650,7 +650,7 @@ describe('snapshot to pdf', () => { const codeText = textContent.text.find( (t: any) => typeof t === 'object' && - t.font === 'Roboto' && + t.font === 'Inter' && t.background === '#f5f5f5' ); expect(codeText).toBeDefined(); @@ -837,11 +837,7 @@ describe('snapshot to pdf', () => { if (Array.isArray(textContent.text)) { const refText = textContent.text.find( - (t: any) => - typeof t === 'object' && - t.text === 'Page not found' && - Array.isArray(t.decoration) && - t.decoration.includes('lineThrough') + (t: any) => typeof t === 'object' && t.text === 'Page not found' ); expect(refText).toBeDefined(); } diff --git a/docs/developing-server.md b/docs/developing-server.md index 3aa89e8fa6ef9..e85420a258c39 100644 --- a/docs/developing-server.md +++ b/docs/developing-server.md @@ -35,12 +35,6 @@ Server also requires native packages to be built, you can build them by running yarn affine @affine/server-native build ``` -## Build @affine/reader package - -```sh -yarn affine @affine/reader build -``` - ## Prepare dev environment ```sh diff --git a/packages/backend/native/Cargo.toml b/packages/backend/native/Cargo.toml index 57668a48215ed..4f32224e3df69 100644 --- a/packages/backend/native/Cargo.toml +++ b/packages/backend/native/Cargo.toml @@ -8,18 +8,22 @@ version = "1.0.0" crate-type = ["cdylib"] [dependencies] -affine_common = { workspace = true, features = ["doc-loader", "hashcash"] } -chrono = { workspace = true } -file-format = { workspace = true } -infer = { workspace = true } -mp4parse = { workspace = true } -napi = { workspace = true, features = ["async"] } -napi-derive = { workspace = true } -rand = { workspace = true } -sha3 = { workspace = true } -tiktoken-rs = { workspace = true } -v_htmlescape = { workspace = true } -y-octo = { workspace = true, features = ["large_refs"] } +affine_common = { workspace = true, features = [ + "doc-loader", + "hashcash", + "ydoc-loader", +] } +chrono = { workspace = true } +file-format = { workspace = true } +infer = { workspace = true } +mp4parse = { workspace = true } +napi = { workspace = true, features = ["async"] } +napi-derive = { workspace = true } +rand = { workspace = true } +sha3 = { workspace = true } +tiktoken-rs = { workspace = true } +v_htmlescape = { workspace = true } +y-octo = { workspace = true, features = ["large_refs"] } [target.'cfg(not(target_os = "linux"))'.dependencies] mimalloc = { workspace = true } diff --git a/packages/backend/native/index.d.ts b/packages/backend/native/index.d.ts index 12bbd6e85a198..024db8eaea8e0 100644 --- a/packages/backend/native/index.d.ts +++ b/packages/backend/native/index.d.ts @@ -27,6 +27,29 @@ export declare function mergeUpdatesInApplyWay(updates: Array): Buffer export declare function mintChallengeResponse(resource: string, bits?: number | undefined | null): Promise +export interface NativeBlockInfo { + blockId: string + flavour: string + content?: Array + blob?: Array + refDocId?: Array + refInfo?: Array + parentFlavour?: string + parentBlockId?: string + additional?: string +} + +export interface NativeCrawlResult { + blocks: Array + title: string + summary: string +} + +export interface NativeMarkdownResult { + title: string + markdown: string +} + export interface ParsedDoc { name: string chunks: Array @@ -34,4 +57,10 @@ export interface ParsedDoc { export declare function parseDoc(filePath: string, doc: Buffer): Promise +export declare function parseDocFromBinary(docBin: Buffer, docId: string): NativeCrawlResult + +export declare function parseDocToMarkdown(docBin: Buffer, docId: string, aiEditable?: boolean | undefined | null): NativeMarkdownResult + +export declare function readAllDocIdsFromRootDoc(docBin: Buffer, includeTrash?: boolean | undefined | null): Array + export declare function verifyChallengeResponse(response: string, bits: number, resource: string): Promise diff --git a/packages/backend/native/src/doc.rs b/packages/backend/native/src/doc.rs new file mode 100644 index 0000000000000..c5da3c83b120b --- /dev/null +++ b/packages/backend/native/src/doc.rs @@ -0,0 +1,93 @@ +use affine_common::doc_parser::{self, BlockInfo, CrawlResult, MarkdownResult}; +use napi::bindgen_prelude::*; +use napi_derive::napi; + +#[napi(object)] +pub struct NativeMarkdownResult { + pub title: String, + pub markdown: String, +} + +impl From for NativeMarkdownResult { + fn from(result: MarkdownResult) -> Self { + Self { + title: result.title, + markdown: result.markdown, + } + } +} + +#[napi(object)] +pub struct NativeBlockInfo { + pub block_id: String, + pub flavour: String, + pub content: Option>, + pub blob: Option>, + pub ref_doc_id: Option>, + pub ref_info: Option>, + pub parent_flavour: Option, + pub parent_block_id: Option, + pub additional: Option, +} + +impl From for NativeBlockInfo { + fn from(info: BlockInfo) -> Self { + Self { + block_id: info.block_id, + flavour: info.flavour, + content: info.content, + blob: info.blob, + ref_doc_id: info.ref_doc_id, + ref_info: info.ref_info, + parent_flavour: info.parent_flavour, + parent_block_id: info.parent_block_id, + additional: info.additional, + } + } +} + +#[napi(object)] +pub struct NativeCrawlResult { + pub blocks: Vec, + pub title: String, + pub summary: String, +} + +impl From for NativeCrawlResult { + fn from(result: CrawlResult) -> Self { + Self { + blocks: result.blocks.into_iter().map(Into::into).collect(), + title: result.title, + summary: result.summary, + } + } +} + +#[napi] +pub fn parse_doc_from_binary(doc_bin: Buffer, doc_id: String) -> Result { + let result = doc_parser::parse_doc_from_binary(doc_bin.into(), doc_id) + .map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?; + Ok(result.into()) +} + +#[napi] +pub fn parse_doc_to_markdown( + doc_bin: Buffer, + doc_id: String, + ai_editable: Option, +) -> Result { + let result = + doc_parser::parse_doc_to_markdown(doc_bin.into(), doc_id, ai_editable.unwrap_or(false)) + .map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?; + Ok(result.into()) +} + +#[napi] +pub fn read_all_doc_ids_from_root_doc( + doc_bin: Buffer, + include_trash: Option, +) -> Result> { + let result = doc_parser::get_doc_ids_from_binary(doc_bin.into(), include_trash.unwrap_or(false)) + .map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?; + Ok(result) +} diff --git a/packages/backend/native/src/lib.rs b/packages/backend/native/src/lib.rs index ff2731772335f..c6740387bcb52 100644 --- a/packages/backend/native/src/lib.rs +++ b/packages/backend/native/src/lib.rs @@ -2,6 +2,7 @@ mod utils; +pub mod doc; pub mod doc_loader; pub mod file_type; pub mod hashcash; diff --git a/packages/backend/server/package.json b/packages/backend/server/package.json index 9be2e63ec021c..93c1c076b6224 100644 --- a/packages/backend/server/package.json +++ b/packages/backend/server/package.json @@ -26,7 +26,6 @@ "postinstall": "prisma generate" }, "dependencies": { - "@affine/reader": "workspace:*", "@affine/server-native": "workspace:*", "@ai-sdk/anthropic": "^2.0.54", "@ai-sdk/google": "^2.0.45", diff --git a/packages/backend/server/src/__tests__/e2e/doc-service/__snapshots__/controller.spec.ts.md b/packages/backend/server/src/__tests__/e2e/doc-service/__snapshots__/controller.spec.ts.md index 70274c72c9050..19b5633f8c126 100644 --- a/packages/backend/server/src/__tests__/e2e/doc-service/__snapshots__/controller.spec.ts.md +++ b/packages/backend/server/src/__tests__/e2e/doc-service/__snapshots__/controller.spec.ts.md @@ -9,64 +9,43 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 { - markdown: `AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.␊ - ␊ - ␊ + markdown: `AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro. ␊ ␊ # You own your data, with no compromises␊ - ␊ ## Local-first & Real-time collaborative␊ - ␊ We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊ - ␊ AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊ ␊ - ␊ - ␊ ### Blocks that assemble your next docs, tasks kanban or whiteboard␊ - ␊ - There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊ - ␊ + There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further. ␊ We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊ - ␊ If you want to learn more about the product design of AFFiNE, here goes the concepts:␊ - ␊ To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊ - ␊ ## A true canvas for blocks in any form␊ - ␊ - [Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊ - ␊ - ␊ + Many editor apps claimed to be a canvas for productivity. Since the Mother of All Demos, Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers. ␊ ␊ "We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊ - ␊ - * Quip & Notion with their great concept of "everything is a block"␊ - * Trello with their Kanban␊ - * Airtable & Miro with their no-code programable datasheets␊ - * Miro & Whimiscal with their edgeless visual whiteboard␊ - * Remnote & Capacities with their object-based tag system␊ - For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊ - ␊ + - Quip & Notion with their great concept of "everything is a block"␊ + - Trello with their Kanban␊ + - Airtable & Miro with their no-code programable datasheets␊ + - Miro & Whimiscal with their edgeless visual whiteboard␊ + - Remnote & Capacities with their object-based tag system␊ + For more details, please refer to our RoadMap␊ ## Self Host␊ - ␊ Self host AFFiNE␊ ␊ + ### Learning From␊ ||Title|Tag|␊ |---|---|---|␊ - |Affine Development|Affine Development|AFFiNE|␊ - |For developers or installations guides, please go to AFFiNE Doc|For developers or installations guides, please go to AFFiNE Doc|Developers|␊ - |Quip & Notion with their great concept of "everything is a block"|Quip & Notion with their great concept of "everything is a block"|Reference|␊ - |Trello with their Kanban|Trello with their Kanban|Reference|␊ - |Airtable & Miro with their no-code programable datasheets|Airtable & Miro with their no-code programable datasheets|Reference|␊ - |Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|Reference|␊ + |Affine Development|Affine Development||␊ + |For developers or installations guides, please go to AFFiNE Doc|For developers or installations guides, please go to AFFiNE Doc||␊ + |Quip & Notion with their great concept of "everything is a block"|Quip & Notion with their great concept of "everything is a block"||␊ + |Trello with their Kanban|Trello with their Kanban||␊ + |Airtable & Miro with their no-code programable datasheets|Airtable & Miro with their no-code programable datasheets||␊ + |Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard||␊ |Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊ - ␊ ## Affine Development␊ - ␊ - For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊ - ␊ - ␊ + For developer or installation guides, please go to AFFiNE Development␊ ␊ `, title: 'Write, Draw, Plan all at Once.', diff --git a/packages/backend/server/src/__tests__/e2e/doc-service/__snapshots__/controller.spec.ts.snap b/packages/backend/server/src/__tests__/e2e/doc-service/__snapshots__/controller.spec.ts.snap index e380fc39a00ccfb7c18cfb3e19c50917bc617c1a..97e09a2abc138ffff2e928ec9fa95b7ca46df4a8 100644 GIT binary patch literal 1651 zcmV-(28{VZRzV0 zv{VR)cpr-h00000000B6SIurC#}OXxpY6#SS=%`~1qslJt<(2zMQM0F+9Ts!MM@1w*c&X}-r2uzTR4_tq4>Z|B*; z%Zu5S^t;}-hjiX^h*PcCkgvwID_jdmaTatX)B+ zV!zvIn&-#I$MEZ(dkGpPQ#-fRqvPmp5}k}8qAUa!=;q4lpj${rBU7gOA zRAtSA;P;pCl1oYwE7-+g=3V3t&Ek8S!ZpDPUDVYSOSq-jXkG~e^+e-jr_zcr zc7X&|1I4xOH79NmNF)zc&&DaJ3^e)OQ3ozWhPh^4b*@C!Fxp!O^A=;pp}i-C!$zSe zyeZ6KYgiiT(B3A}7#jA2te`*#(xBh3vx=m$yz1%L4TMn378iq|H5Ki8j5ZuADi-Ic zh`@?~X2d+rzet^IhqEON4pgjz&h=0}aM=}6)op60bm_qwLt<1yI4Pc8K|ado`4z*Z zracD7DK3`i<8)8|3RFz+7+0F5DLb?RC#j2^W0w!wr#i|vBEwwcE5iKdK`#zAGSt`^~K7p%1*7Cmdm#Git6F4;? zChShqWZk!MJSnsp-9i{6N<$PSpY#qVaNU^JNTImjIP1W-B(jzAUb~&Z1+|W&?)efu z7A6{!eP>QzHIq5&@Qt|r^2pCP^b#y7qOmdtZwZBz_ex?;_+K!VuhA=cFUeN$8=~yCKJJ71`Oi4GH*Q{v|@}F_>&iFf0@ibk1wojj42NP)wvw*8H{MMpN zp9L7`S@w_Ye_p1Z9-QfkQfT||!5fZn#+@qJH;44%@wu=U3mJ xoqzpic3|~PBF0YA+CRzVD6W>W`{h_nv#s zsha*~JhEI&(;HT_fginlJ#z8pY*PL zzjrn3UH`cE;c~6TgDd6D-Ti%Y@DPjuI$%!@gxgRMptWEeup^jpn25)M`insUqj43; zTp~3v=8#6mnqbr#Tk5;6Xy7r-xeX8r8BGeF%u-nqH0ZFRhFk=6C6uNz5iOty zMM;kkMie7y{7AuJG*g1eaKO>9OGBQRz{5Px^V{&8TTmd=ii%txW+v9)7z%ED;Km3^ zp5ML=zv2SzU~Ga!*oGrQJBX$sDA-yYaY!rW`7;8`x@1jYN<#2~JqwjEn!{r^fo(XR znOM{cl@1zX!>s^@XbMY!BW|PSqj@PT8>y8f-o!3;VYw}bu-}GQQ)oC)AJ&~ofm5nz z?ZCLfxu!s@F~Wq%%R0QSM?q@iFM|NsLAb2u1*TM*)L=UG6b!k7hIzih6tElcq4(Al z-Q?D@g9k@@C(dc5B!<%xYr^n$||+ErH{b1Ohc^ z9LU@iR4VwZl{zlmz74QBC3#{K#*3O6$C4>|q8Nx|TbHG60$Y__(sm*bK8m9yZ z-nLaLEs4vT3Bgy#@PJE7N-NmKU`B1^AS#8T70u!gG=ygaGjvg(rdYxa#ai=9Am~gq zPF7+a6UH`>z-&Qt?R~|G9|RJ~!?nN)o30kM_U1H34;Sw>_Y5Xl@DCDMO2BK$|+wuwEB=Jl^{;CXIGGq zaxc$M7>+gZJ~&Q+u|yw-8!n(w#RQOXrJ0(tMJuqKtW{uEGZeU)g;{xE%@AnZ3c-|I z$w8$x85q}3wd(70v`GwOo9Mm>L?Yy(L8~BG872?ig&0~=B&XPQZqY=eE;83AO`gAa zqSxvux=na5Uw>N1=f%&WCVpTtVO zE$mUl(tini+*TF^9=eL`2t({^xIo?3q4oN}Rlo+-4whTa+DvN{7^!p-d>f=S9rMtE z8c>6gpfMt9Vwzn=jyfKuy-v|dvpTP)qbB?$?oaFl)I(3#5 z(OBt&w}e8f2&Fft2D~_8ET5q7xGDE`cGQH1I362ELmxQZ?-U$puoUf1U@RN-caj*6 z$&TR>OU(0hS+m52c=P=1>_m@#XD3*l+c&F~ z1GLKdK(dmxoUuAN=mksvg-^Yd%u0>oIO!^6T{d`WNpx z!MApqcbNH}OME*seA{X7xwyA9?6;nG)}2ZI$2#-;oF84Zqs!gs#nO(}alU11DAyx* ze&3p6GLVQN-qBOQg#~9&Ie|Igv8vN{aUkw7q zHaKvt)oznZZ}kjGr$*u>UlV%Y-T0bt<9?Q9ds&vf%ChXQS(g1H%d&rE-}+_Y`h}XV zsjQYa-#Oq2``o(n!l(}#62WRU@T0v&uH#DvUvt=OggiWXxqtZdVDG}H8xk?Ll5VeE j`AhF&kU00NAm2NBczpQu=)uF6t7!iPhnJoSo)G{5A4il+ diff --git a/packages/backend/server/src/core/doc/__tests__/__snapshots__/reader-from-database.spec.ts.md b/packages/backend/server/src/core/doc/__tests__/__snapshots__/reader-from-database.spec.ts.md index 47ef7705d695b..d5e57a9f1436b 100644 --- a/packages/backend/server/src/core/doc/__tests__/__snapshots__/reader-from-database.spec.ts.md +++ b/packages/backend/server/src/core/doc/__tests__/__snapshots__/reader-from-database.spec.ts.md @@ -9,64 +9,43 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 { - markdown: `AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.␊ - ␊ - ␊ + markdown: `AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro. ␊ ␊ # You own your data, with no compromises␊ - ␊ ## Local-first & Real-time collaborative␊ - ␊ We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊ - ␊ AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊ ␊ - ␊ - ␊ ### Blocks that assemble your next docs, tasks kanban or whiteboard␊ - ␊ - There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊ - ␊ + There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further. ␊ We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊ - ␊ If you want to learn more about the product design of AFFiNE, here goes the concepts:␊ - ␊ To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊ - ␊ ## A true canvas for blocks in any form␊ - ␊ - [Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊ - ␊ - ␊ + Many editor apps claimed to be a canvas for productivity. Since the Mother of All Demos, Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers. ␊ ␊ "We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊ - ␊ - * Quip & Notion with their great concept of "everything is a block"␊ - * Trello with their Kanban␊ - * Airtable & Miro with their no-code programable datasheets␊ - * Miro & Whimiscal with their edgeless visual whiteboard␊ - * Remnote & Capacities with their object-based tag system␊ - For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊ - ␊ + - Quip & Notion with their great concept of "everything is a block"␊ + - Trello with their Kanban␊ + - Airtable & Miro with their no-code programable datasheets␊ + - Miro & Whimiscal with their edgeless visual whiteboard␊ + - Remnote & Capacities with their object-based tag system␊ + For more details, please refer to our RoadMap␊ ## Self Host␊ - ␊ Self host AFFiNE␊ ␊ + ### Learning From␊ ||Title|Tag|␊ |---|---|---|␊ - |Affine Development|Affine Development|AFFiNE|␊ - |For developers or installations guides, please go to AFFiNE Doc|For developers or installations guides, please go to AFFiNE Doc|Developers|␊ - |Quip & Notion with their great concept of "everything is a block"|Quip & Notion with their great concept of "everything is a block"|Reference|␊ - |Trello with their Kanban|Trello with their Kanban|Reference|␊ - |Airtable & Miro with their no-code programable datasheets|Airtable & Miro with their no-code programable datasheets|Reference|␊ - |Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|Reference|␊ + |Affine Development|Affine Development||␊ + |For developers or installations guides, please go to AFFiNE Doc|For developers or installations guides, please go to AFFiNE Doc||␊ + |Quip & Notion with their great concept of "everything is a block"|Quip & Notion with their great concept of "everything is a block"||␊ + |Trello with their Kanban|Trello with their Kanban||␊ + |Airtable & Miro with their no-code programable datasheets|Airtable & Miro with their no-code programable datasheets||␊ + |Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard||␊ |Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊ - ␊ ## Affine Development␊ - ␊ - For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊ - ␊ - ␊ + For developer or installation guides, please go to AFFiNE Development␊ ␊ `, title: 'Write, Draw, Plan all at Once.', diff --git a/packages/backend/server/src/core/doc/__tests__/__snapshots__/reader-from-database.spec.ts.snap b/packages/backend/server/src/core/doc/__tests__/__snapshots__/reader-from-database.spec.ts.snap index 3095228f2c6bf40a0a7ad4e6c8028ac77ef330de..e0bf7a84636bf02e62e9ca8e802df245134393b7 100644 GIT binary patch literal 1505 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 Snapshot 1 { - markdown: `AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.␊ - ␊ - ␊ + markdown: `AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro. ␊ ␊ # You own your data, with no compromises␊ - ␊ ## Local-first & Real-time collaborative␊ - ␊ We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊ - ␊ AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊ ␊ - ␊ - ␊ ### Blocks that assemble your next docs, tasks kanban or whiteboard␊ - ␊ - There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊ - ␊ + There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further. ␊ We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊ - ␊ If you want to learn more about the product design of AFFiNE, here goes the concepts:␊ - ␊ To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊ - ␊ ## A true canvas for blocks in any form␊ - ␊ - [Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊ - ␊ - ␊ + Many editor apps claimed to be a canvas for productivity. Since the Mother of All Demos, Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers. ␊ ␊ "We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊ - ␊ - * Quip & Notion with their great concept of "everything is a block"␊ - * Trello with their Kanban␊ - * Airtable & Miro with their no-code programable datasheets␊ - * Miro & Whimiscal with their edgeless visual whiteboard␊ - * Remnote & Capacities with their object-based tag system␊ - For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊ - ␊ + - Quip & Notion with their great concept of "everything is a block"␊ + - Trello with their Kanban␊ + - Airtable & Miro with their no-code programable datasheets␊ + - Miro & Whimiscal with their edgeless visual whiteboard␊ + - Remnote & Capacities with their object-based tag system␊ + For more details, please refer to our RoadMap␊ ## Self Host␊ - ␊ Self host AFFiNE␊ ␊ + ### Learning From␊ ||Title|Tag|␊ |---|---|---|␊ - |Affine Development|Affine Development|AFFiNE|␊ - |For developers or installations guides, please go to AFFiNE Doc|For developers or installations guides, please go to AFFiNE Doc|Developers|␊ - |Quip & Notion with their great concept of "everything is a block"|Quip & Notion with their great concept of "everything is a block"|Reference|␊ - |Trello with their Kanban|Trello with their Kanban|Reference|␊ - |Airtable & Miro with their no-code programable datasheets|Airtable & Miro with their no-code programable datasheets|Reference|␊ - |Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|Reference|␊ + |Affine Development|Affine Development||␊ + |For developers or installations guides, please go to AFFiNE Doc|For developers or installations guides, please go to AFFiNE Doc||␊ + |Quip & Notion with their great concept of "everything is a block"|Quip & Notion with their great concept of "everything is a block"||␊ + |Trello with their Kanban|Trello with their Kanban||␊ + |Airtable & Miro with their no-code programable datasheets|Airtable & Miro with their no-code programable datasheets||␊ + |Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard||␊ |Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊ - ␊ ## Affine Development␊ - ␊ - For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊ - ␊ - ␊ + For developer or installation guides, please go to AFFiNE Development␊ ␊ `, title: 'Write, Draw, Plan all at Once.', diff --git a/packages/backend/server/src/core/doc/__tests__/__snapshots__/reader-from-rpc.spec.ts.snap b/packages/backend/server/src/core/doc/__tests__/__snapshots__/reader-from-rpc.spec.ts.snap index 3095228f2c6bf40a0a7ad4e6c8028ac77ef330de..e0bf7a84636bf02e62e9ca8e802df245134393b7 100644 GIT binary patch literal 1505 zcmV<71s?iARzV=S}@Uw=~` z{H%hcjUS5$00000000B6S50puMH20v5iBarY6rm?g>1>E?mp~k#mzg$0>QvCwy-!x zS4CCjc2-6zBdU8E)d7A82lxm4CGPx(Rb-XhZSM>R50)%-XJvfki}zku|5C3#mmB%% zS5l{*UY|(Iy{{mV_9;NcC3KiJ74HH_UzQ}|Bw&=5Ret)Vs!;L!XMa4M{cUD{^AF}v z|2dn@{xzG;etR~XeKddn!}^N;CVo8f>Q$ls20ojXE2Q{a> zTU&y4?|fy?#&ZMLu;)I35_Yh28@GT>;GL(cA?Pq*LmdSL3>8$aaf%*LgjDhp!kSdc z@++l)>b3;R@HMOBkhi>aiC0h*r|>=ZU^wQ22HXO6PFn~JC3i7#=Y*s^&Y+m8#sfjozt>4oNQB#9rps5 z)Rmq9*W9ahV_g|0M~zh^(dn4G^xRh~xa^a*lsZndfV;8e#JN-Se(A#U-b6rKRM-$j zxl3>LC`s$$n*aa~klu9U2~etBo-VzPlpKYUR>cuZz#oB!F?v^yv3=wKFJ4_-%UJbo zHKdP`ldf!DQ@@ti?{F_5%89D6hTp@kb)KN>y}IZL0#_uEs6!V(_MxQ8us>8<6~*c4 zDg1lnUWUfV)U7>@=mdJLCOTPwqHF{<7}mz=WTlkWjHxQFTS_z?7E?pOTS^|Iu~}Pk z2~fFnC7e8<`-#zYN0jLxWbnueu3OpzCW1iDCVWq3ffKa2B|wP2Z}Pdas=V6}e0K#e zxS}kvl7l9J_s(wb2FL;|~s;x_k& zGdBoCDMCH6aRnv=YyNQ5i3`fGwyx_wRA?GTfA3(?qt=|dx3uukDC~rHgL!BTTPHpG z`$Sqm%W;wwlo&u-jK^~}k<^}dBOUvR5Vo_!&9u?GnhqXghU3M=5(1S9YzSCK%&X#y z>|_U=Jz)x9Vx4quhl+`-zEo4UYl6{b1Zx4AQ5oT^cn&2+l~0RnhAV4(0fBQ~tT5{8 zNdE?uW_Vnv+}2e+dV#a-r~B1tP)E?g>P39N- zO!g@eiBM37-XQR@GVeSGO+6WmTO7wWP0*Qr>}{nhUfL%!61y5=6evBqj;aG|j>0F< z+}7!S1y?SVqb|8N%ldwb6{Oyf$ zXO}GstWBT@u}{*wkzW|VJf=e>=$wf8l=aq7ppk9)Dl-%^d6L+bb8GBn45an#v zH(_m@x_dBg?B(|#e_p{kcvj089S5Q$;|N@k=*<86(P*=PP22kg(5hK|S}ft)-bFaO zy#X_|$6+;w!Y#Ed_X%w&?MENTW|jwaQY_&*k@tKY{AKpRVhQIiX~ym>->k=b0xwH5 zqk9OWqO?RR#c~WdgPYcMPD;c5F4!)7Pa?NcKC;^-yrM3!TFn<2v2^N4j)OVB)hyR& zm#^aOn@4fUX_R0^iq6Xdq9+tG-W!QIeqE*wo zitajZ*xH`tf5GKD#}CEu&XoBJG7QD=HrBoUf#LjEKE6ZbPz;Zw@?*n>VtAwk9vwGi zFaJNr7We4(kZpfXwxcY?gC8F6bKcr zv?xjF#vh9a00000000BMSY2xz$rYWk+0D|h*vsZQTt*;PVtN$v0g2f)DApRAjqOBI zqBtfjce?L%m#4aJZQbe_P0&O3hvZ@NL-J$tGx8K}b2t zW>d?>T)zJYMH4Muj-=++mJlem!9mFdG#KV3FC9o*6eMEAp_iJYeE&mPV#JsC{(X1w zqk;YmKN;Tr*Pu98m2Y1SQ$B!SI=U;;nKnLu}fp8lN0<;#419k*U4s-EX(70a|FdA2Z ztRzwcGY)BWtO=&Av8BH2`UalCirWB@uz{tSn=#BCFD;c7L4yt}YRE-ES3+qj6VU>S zP?YotVM;NQ#*Y*nMzbJ@49__lc4^5o6L^wkS+)l+xdjC>WmM$Gur#p-$53$N12;xU zvTSb;e#-^gd}e|~*oRX>n@7_S6l^U{IiyTkc0piSQ`Q8gBm^JWvrq}s6+Crw*oU*F ziAAkY>7p4n+zMcbrmzG!TF{RR^0n??YV8|6T$+8`$fZc%)y|W#FPm`l?dm#Sk?>$?p9$o9#}I3nzcf(AXjow zY0Y!v#;I3*eT_DWVQdrK`#>Z@E*i87f|W_~&|`?9B}H<9-Q@Zq8g-GCK54S-)ieF9 zj-tne_wtu7>KOflqa&A0d?I`Rg+~*T?F3-X-&sGbf5;* zU?gaah?BopnsF;iFYZzlhsfnUwhuwyKxTsBI zq)_CyoApj$Nn|ba9WOqFQ)(PX&G`sD7A6{!-OZdXKKT^&aE`dXoMewVbm}ZAqOmdt zZwZA|5lU}P19)}HSUyAFaZ?@~9jOUTa5giJCO&ZbepGOvd@b6ez*siu50V(p$j;yi zOU$zLv1WRfMsSLaw=WmkEgZ+^3@)l%Gsys zrbi33EqygQzC5G+mp{LJ{ijj)yjznOY&m>2T44AhUvH0C<=(jY!RTRE<(EgA;^9?x zrI3|fZslDv9EoUc&-1D^C2fJMSW)bK>V%8;-v3U${`7VI>+1RP7ccF*^~wQVMF*pY zC!5N)9ly6={tpq}sp}`z6Zz)FAD%w{^SgCDwXODis$AD=+_vSn>HF<3K5&7zcA59s z`GH%!of+PC+Xrs$mWF-neOKL` Snapshot 1 { - markdown: `AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.␊ - ␊ - ␊ + markdown: `AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro. ␊ ␊ # You own your data, with no compromises␊ - ␊ ## Local-first & Real-time collaborative␊ - ␊ We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊ - ␊ AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊ ␊ - ␊ - ␊ ### Blocks that assemble your next docs, tasks kanban or whiteboard␊ - ␊ - There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊ - ␊ + There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further. ␊ We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊ - ␊ If you want to learn more about the product design of AFFiNE, here goes the concepts:␊ - ␊ To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊ - ␊ ## A true canvas for blocks in any form␊ - ␊ - [Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊ - ␊ - ␊ + Many editor apps claimed to be a canvas for productivity. Since the Mother of All Demos, Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers. ␊ ␊ "We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊ - ␊ - * Quip & Notion with their great concept of "everything is a block"␊ - * Trello with their Kanban␊ - * Airtable & Miro with their no-code programable datasheets␊ - * Miro & Whimiscal with their edgeless visual whiteboard␊ - * Remnote & Capacities with their object-based tag system␊ - For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊ - ␊ + - Quip & Notion with their great concept of "everything is a block"␊ + - Trello with their Kanban␊ + - Airtable & Miro with their no-code programable datasheets␊ + - Miro & Whimiscal with their edgeless visual whiteboard␊ + - Remnote & Capacities with their object-based tag system␊ + For more details, please refer to our RoadMap␊ ## Self Host␊ - ␊ Self host AFFiNE␊ ␊ + ### Learning From␊ ||Title|Tag|␊ |---|---|---|␊ - |Affine Development|Affine Development|AFFiNE|␊ - |For developers or installations guides, please go to AFFiNE Doc|For developers or installations guides, please go to AFFiNE Doc|Developers|␊ - |Quip & Notion with their great concept of "everything is a block"|Quip & Notion with their great concept of "everything is a block"|Reference|␊ - |Trello with their Kanban|Trello with their Kanban|Reference|␊ - |Airtable & Miro with their no-code programable datasheets|Airtable & Miro with their no-code programable datasheets|Reference|␊ - |Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|Reference|␊ + |Affine Development|Affine Development||␊ + |For developers or installations guides, please go to AFFiNE Doc|For developers or installations guides, please go to AFFiNE Doc||␊ + |Quip & Notion with their great concept of "everything is a block"|Quip & Notion with their great concept of "everything is a block"||␊ + |Trello with their Kanban|Trello with their Kanban||␊ + |Airtable & Miro with their no-code programable datasheets|Airtable & Miro with their no-code programable datasheets||␊ + |Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard||␊ |Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊ - ␊ ## Affine Development␊ - ␊ - For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊ - ␊ - ␊ + For developer or installation guides, please go to AFFiNE Development␊ ␊ `, title: 'Write, Draw, Plan all at Once.', @@ -1440,83 +1426,52 @@ Generated by [AVA](https://avajs.dev). > Snapshot 1 { - markdown: `␊ - AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro.␊ - ␊ - ␊ + markdown: `␊ + AFFiNE is an open source all in one workspace, an operating system for all the building blocks of your team wiki, knowledge management and digital assets and a better alternative to Notion and Miro. ␊ ␊ - ␊ - ␊ # You own your data, with no compromises␊ - ␊ - ␊ ## Local-first & Real-time collaborative␊ - ␊ - ␊ We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊ - ␊ - ␊ AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊ ␊ - ␊ - ␊ - ␊ - ␊ + ␊ ### Blocks that assemble your next docs, tasks kanban or whiteboard␊ - ␊ - ␊ - There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊ - ␊ - ␊ + ␊ + There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further. ␊ We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊ - ␊ - ␊ If you want to learn more about the product design of AFFiNE, here goes the concepts:␊ - ␊ - ␊ To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊ - ␊ - ␊ + ␊ ## A true canvas for blocks in any form␊ + Many editor apps claimed to be a canvas for productivity. Since the Mother of All Demos, Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers. ␊ ␊ - ␊ - [Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊ - ␊ - ␊ - ␊ - ␊ - ␊ "We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊ - ␊ - ␊ - * Quip & Notion with their great concept of "everything is a block"␊ - ␊ - * Trello with their Kanban␊ - ␊ - * Airtable & Miro with their no-code programable datasheets␊ - ␊ - * Miro & Whimiscal with their edgeless visual whiteboard␊ - ␊ - * Remnote & Capacities with their object-based tag system␊ - ␊ - For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊ - ␊ - ␊ + - Quip & Notion with their great concept of "everything is a block"␊ + - Trello with their Kanban␊ + - Airtable & Miro with their no-code programable datasheets␊ + - Miro & Whimiscal with their edgeless visual whiteboard␊ + - Remnote & Capacities with their object-based tag system␊ + ␊ + For more details, please refer to our RoadMap␊ ## Self Host␊ - ␊ - ␊ Self host AFFiNE␊ + ␊ ␊ - ␊ - ␊ + ### Learning From␊ + ||Title|Tag|␊ + |---|---|---|␊ + |Affine Development|Affine Development||␊ + |For developers or installations guides, please go to AFFiNE Doc|For developers or installations guides, please go to AFFiNE Doc||␊ + |Quip & Notion with their great concept of "everything is a block"|Quip & Notion with their great concept of "everything is a block"||␊ + |Trello with their Kanban|Trello with their Kanban||␊ + |Airtable & Miro with their no-code programable datasheets|Airtable & Miro with their no-code programable datasheets||␊ + |Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard||␊ + |Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊ + ␊ ## Affine Development␊ + For developer or installation guides, please go to AFFiNE Development␊ ␊ - ␊ - For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊ - ␊ - ␊ - ␊ - ␊ + ␊ `, title: 'Write, Draw, Plan all at Once.', } diff --git a/packages/backend/server/src/core/utils/__tests__/__snapshots__/blocksute.spec.ts.snap b/packages/backend/server/src/core/utils/__tests__/__snapshots__/blocksute.spec.ts.snap index 8670a36042df284dd20372566097284b8081f854..1b99242ee5f7f776d17d8e8c6f8822b79929bbf4 100644 GIT binary patch literal 7330 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 literal 13070 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 OSV745(!buJy!aob%lPg9 diff --git a/packages/backend/server/src/core/utils/__tests__/blocksute.spec.ts b/packages/backend/server/src/core/utils/__tests__/blocksute.spec.ts index e337a07ce58db..f7a126d4cceed 100644 --- a/packages/backend/server/src/core/utils/__tests__/blocksute.spec.ts +++ b/packages/backend/server/src/core/utils/__tests__/blocksute.spec.ts @@ -44,12 +44,7 @@ test('can read all blocks from doc snapshot', async t => { const doc = await models.doc.get(workspace.id, docSnapshot.id); t.truthy(doc); - const result = await readAllBlocksFromDocSnapshot( - workspace.id, - 'doc-0', - docSnapshot.blob, - rootDoc!.blob - ); + const result = await readAllBlocksFromDocSnapshot('doc-0', docSnapshot.blob); t.snapshot({ ...result, @@ -64,11 +59,7 @@ test('can read blob filename from doc snapshot', async t => { snapshotFile: 'test-doc-with-blob.snapshot.bin', }); - const result = await readAllBlocksFromDocSnapshot( - workspace.id, - 'doc-0', - docSnapshot.blob - ); + const result = await readAllBlocksFromDocSnapshot('doc-0', docSnapshot.blob); // NOTE: avoid snapshot result directly, because it will cause hanging t.snapshot(JSON.parse(JSON.stringify(result))); @@ -78,11 +69,7 @@ test('can read all blocks from doc snapshot without workspace snapshot', async t const doc = await models.doc.get(workspace.id, docSnapshot.id); t.truthy(doc); - const result = await readAllBlocksFromDocSnapshot( - workspace.id, - 'doc-0', - docSnapshot.blob - ); + const result = await readAllBlocksFromDocSnapshot('doc-0', docSnapshot.blob); t.snapshot({ ...result, @@ -92,7 +79,6 @@ test('can read all blocks from doc snapshot without workspace snapshot', async t test('can parse doc to markdown from doc snapshot', async t => { const result = parseDocToMarkdownFromDocSnapshot( - workspace.id, docSnapshot.id, docSnapshot.blob ); @@ -102,7 +88,6 @@ test('can parse doc to markdown from doc snapshot', async t => { test('can parse doc to markdown from doc snapshot with ai editable', async t => { const result = parseDocToMarkdownFromDocSnapshot( - workspace.id, docSnapshot.id, docSnapshot.blob, true diff --git a/packages/backend/server/src/core/utils/blocksuite.ts b/packages/backend/server/src/core/utils/blocksuite.ts index d522660dac5b2..fd12fedf336e5 100644 --- a/packages/backend/server/src/core/utils/blocksuite.ts +++ b/packages/backend/server/src/core/utils/blocksuite.ts @@ -1,18 +1,10 @@ -// TODO(@forehalo): -// Because of the `@affine/server` package can't import directly from workspace packages, -// this is a temporary solution to get the block suite data(title, description) from given yjs binary or yjs doc. -// The logic is mainly copied from -// - packages/frontend/core/src/modules/docs-search/worker/in-worker.ts -// - packages/frontend/core/src/components/page-list/use-block-suite-page-preview.ts -// and it's better to be provided by blocksuite - -// eslint-disable-next-line @typescript-eslint/no-restricted-imports -- import from bundle +import { Array as YArray, Doc as YDoc, Map as YMap } from 'yjs'; + import { - parsePageDoc as parseDocToMarkdown, - readAllBlocksFromDoc, + parseYDocFromBinary, + parseYDocToMarkdown, readAllDocIdsFromRootDoc, -} from '@affine/reader/dist'; -import { applyUpdate, Array as YArray, Doc as YDoc, Map as YMap } from 'yjs'; +} from '../../native'; export interface PageDocContent { title: string; @@ -165,64 +157,49 @@ export function parsePageDoc( } export function readAllDocIdsFromWorkspaceSnapshot(snapshot: Uint8Array) { - const rootDoc = new YDoc(); - applyUpdate(rootDoc, snapshot); - return readAllDocIdsFromRootDoc(rootDoc, { - includeTrash: false, - }); + return readAllDocIdsFromRootDoc(Buffer.from(snapshot), false); +} + +function safeParseJson(str: string): T | undefined { + try { + return JSON.parse(str) as T; + } catch { + return undefined; + } } export async function readAllBlocksFromDocSnapshot( - workspaceId: string, docId: string, - docSnapshot: Uint8Array, - workspaceSnapshot?: Uint8Array, - maxSummaryLength?: number + docSnapshot: Uint8Array ) { - let rootYDoc: YDoc | undefined; - if (workspaceSnapshot) { - rootYDoc = new YDoc({ - guid: workspaceId, - }); - applyUpdate(rootYDoc, workspaceSnapshot); - } - const ydoc = new YDoc({ - guid: docId, - }); - applyUpdate(ydoc, docSnapshot); - return await readAllBlocksFromDoc({ - ydoc, - rootYDoc, - spaceId: workspaceId, - maxSummaryLength, - }); + const result = parseYDocFromBinary(Buffer.from(docSnapshot), docId); + + return { + ...result, + blocks: result.blocks.map(block => ({ + ...block, + docId, + ref: block.refInfo, + additional: block.additional + ? safeParseJson(block.additional) + : undefined, + })), + }; } export function parseDocToMarkdownFromDocSnapshot( - workspaceId: string, docId: string, docSnapshot: Uint8Array, aiEditable = false ) { - const ydoc = new YDoc({ - guid: docId, - }); - applyUpdate(ydoc, docSnapshot); - - const parsed = parseDocToMarkdown({ - workspaceId, - doc: ydoc, - buildBlobUrl: (blobId: string) => { - return `/${workspaceId}/blobs/${blobId}`; - }, - buildDocUrl: (docId: string) => { - return `/workspace/${workspaceId}/${docId}`; - }, - aiEditable, - }); + const parsed = parseYDocToMarkdown( + Buffer.from(docSnapshot), + docId, + aiEditable + ); return { title: parsed.title, - markdown: parsed.md, + markdown: parsed.markdown, }; } diff --git a/packages/backend/server/src/native.ts b/packages/backend/server/src/native.ts index 9506cc7e0996a..b4b961e095421 100644 --- a/packages/backend/server/src/native.ts +++ b/packages/backend/server/src/native.ts @@ -40,6 +40,10 @@ export function getTokenEncoder(model?: string | null): Tokenizer | null { export const getMime = serverNativeModule.getMime; export const parseDoc = serverNativeModule.parseDoc; export const htmlSanitize = serverNativeModule.htmlSanitize; +export const parseYDocFromBinary = serverNativeModule.parseDocFromBinary; +export const parseYDocToMarkdown = serverNativeModule.parseDocToMarkdown; +export const readAllDocIdsFromRootDoc = + serverNativeModule.readAllDocIdsFromRootDoc; export const AFFINE_PRO_PUBLIC_KEY = serverNativeModule.AFFINE_PRO_PUBLIC_KEY; export const AFFINE_PRO_LICENSE_AES_KEY = serverNativeModule.AFFINE_PRO_LICENSE_AES_KEY; diff --git a/packages/backend/server/src/plugins/indexer/service.ts b/packages/backend/server/src/plugins/indexer/service.ts index 52a2e0ac8ab99..09974bb2c9d92 100644 --- a/packages/backend/server/src/plugins/indexer/service.ts +++ b/packages/backend/server/src/plugins/indexer/service.ts @@ -227,15 +227,7 @@ export class IndexerService { this.logger.debug(`doc ${workspaceId}/${docId} is empty, skip indexing`); return; } - const MAX_WORKSPACE_SNAPSHOT_SIZE = 1024 * 1024 * 10; // 10MB - const result = await readAllBlocksFromDocSnapshot( - workspaceId, - docId, - docSnapshot.blob, - workspaceSnapshot.blob.length < MAX_WORKSPACE_SNAPSHOT_SIZE - ? workspaceSnapshot.blob - : undefined - ); + const result = await readAllBlocksFromDocSnapshot(docId, docSnapshot.blob); if (!result) { this.logger.warn( `parse doc ${workspaceId}/${docId} failed, workspaceSnapshot size: ${workspaceSnapshot.blob.length}, docSnapshot size: ${docSnapshot.blob.length}` @@ -277,7 +269,7 @@ export class IndexerService { additional: block.additional ? JSON.stringify(block.additional) : undefined, - markdownPreview: block.markdownPreview, + markdownPreview: undefined, createdByUserId: docSnapshot.createdBy ?? '', updatedByUserId: docSnapshot.updatedBy ?? '', createdAt: docSnapshot.createdAt, diff --git a/packages/backend/server/tsconfig.json b/packages/backend/server/tsconfig.json index fec2cae0e58c4..6997a939f77bc 100644 --- a/packages/backend/server/tsconfig.json +++ b/packages/backend/server/tsconfig.json @@ -12,7 +12,6 @@ }, "include": ["./src"], "references": [ - { "path": "../../common/reader" }, { "path": "../native" }, { "path": "../../../tools/cli" }, { "path": "../../../tools/utils" }, diff --git a/packages/common/native/src/doc_parser.rs b/packages/common/native/src/doc_parser.rs index 35ff68a397783..699fa76f982fd 100644 --- a/packages/common/native/src/doc_parser.rs +++ b/packages/common/native/src/doc_parser.rs @@ -79,6 +79,245 @@ impl From for ParseError { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MarkdownResult { + pub title: String, + pub markdown: String, +} + +pub fn parse_doc_to_markdown( + doc_bin: Vec, + doc_id: String, + ai_editable: bool, +) -> Result { + if doc_bin.is_empty() || doc_bin == [0, 0] { + return Err(ParseError::InvalidBinary); + } + + let mut doc = DocOptions::new().with_guid(doc_id.clone()).build(); + doc + .apply_update_from_binary_v1(&doc_bin) + .map_err(|_| ParseError::InvalidBinary)?; + + let blocks_map = doc.get_map("blocks")?; + if blocks_map.is_empty() { + return Ok(MarkdownResult { + title: "".into(), + markdown: "".into(), + }); + } + + let mut block_pool: HashMap = HashMap::new(); + let mut parent_lookup: HashMap = HashMap::new(); + + for (_, value) in blocks_map.iter() { + if let Some(block_map) = value.to_map() { + if let Some(block_id) = get_block_id(&block_map) { + for child_id in collect_child_ids(&block_map) { + parent_lookup.insert(child_id, block_id.clone()); + } + block_pool.insert(block_id, block_map); + } + } + } + + let root_block_id = block_pool + .iter() + .find_map(|(id, block)| { + get_flavour(block) + .filter(|flavour| flavour == PAGE_FLAVOUR) + .map(|_| id.clone()) + }) + .ok_or_else(|| ParseError::ParserError("root block not found".into()))?; + + let mut queue: Vec<(Option, String)> = vec![(None, root_block_id.clone())]; + let mut visited: HashSet = HashSet::from([root_block_id.clone()]); + let mut doc_title = String::from("Untitled"); + let mut markdown = String::new(); + + while let Some((parent_block_id, block_id)) = queue.pop() { + let block = match block_pool.get(&block_id) { + Some(block) => block, + None => continue, + }; + + let flavour = match get_flavour(block) { + Some(flavour) => flavour, + None => continue, + }; + + let parent_id = parent_lookup.get(&block_id); + let parent_flavour = parent_id + .and_then(|id| block_pool.get(id)) + .and_then(get_flavour); + + if parent_flavour.as_deref() == Some("affine:database") { + continue; + } + + // enqueue children first to keep traversal order similar to JS implementation + let mut child_ids = collect_child_ids(block); + for child_id in child_ids.drain(..).rev() { + if visited.insert(child_id.clone()) { + queue.push((Some(block_id.clone()), child_id)); + } + } + + if flavour == PAGE_FLAVOUR { + let title = get_string(block, "prop:title").unwrap_or_default(); + doc_title = title.clone(); + continue; + } + + if flavour == "affine:database" { + let title = get_string(block, "prop:title").unwrap_or_default(); + markdown.push_str(&format!("\n### {}\n", title)); + + let columns_array = block.get("prop:columns").and_then(|v| v.to_array()); + let cells_map = block.get("prop:cells").and_then(|v| v.to_map()); + + if let (Some(columns_array), Some(cells_map)) = (columns_array, cells_map) { + let mut columns = Vec::new(); + for col_val in columns_array.iter() { + if let Some(col_map) = col_val.to_map() { + let id = get_string(&col_map, "id").unwrap_or_default(); + let name = get_string(&col_map, "name").unwrap_or_default(); + let type_ = get_string(&col_map, "type").unwrap_or_default(); + let data = col_map.get("data").and_then(|v| v.to_map()); + columns.push((id, name, type_, data)); + } + } + + let escape_table = |s: &str| s.replace('|', "\\|").replace('\n', "
"); + + markdown.push('|'); + for (_, name, _, _) in &columns { + markdown.push_str(&escape_table(name)); + markdown.push('|'); + } + markdown.push('\n'); + + markdown.push('|'); + for _ in &columns { + markdown.push_str("---|"); + } + markdown.push('\n'); + + let child_ids = collect_child_ids(block); + for child_id in child_ids { + markdown.push('|'); + let row_cells = cells_map.get(&child_id).and_then(|v| v.to_map()); + + for (col_id, _, col_type, col_data) in &columns { + let mut cell_text = String::new(); + if col_type == "title" { + if let Some(child_block) = block_pool.get(&child_id) { + if let Some((text, _)) = text_content(child_block, "prop:text") { + cell_text = text; + } + } + } else if let Some(row_cells) = &row_cells { + if let Some(cell_val) = row_cells.get(col_id).and_then(|v| v.to_map()) { + if let Some(value) = cell_val.get("value").and_then(|v| v.to_any()) { + cell_text = format_cell_value(&value, col_type, col_data.as_ref()); + } + } + } + markdown.push_str(&escape_table(&cell_text)); + markdown.push('|'); + } + markdown.push('\n'); + } + } + continue; + } + + if flavour == "affine:table" { + let contents = gather_table_contents(block); + markdown.push_str(&contents.join("|")); + markdown.push('\n'); + continue; + } + + if ai_editable && parent_block_id.as_ref() == Some(&root_block_id) { + markdown.push_str(&format!( + "\n", + block_id, flavour + )); + } + + if flavour == "affine:paragraph" { + if let Some((text, _)) = text_content(block, "prop:text") { + let type_ = get_string(block, "prop:type").unwrap_or_default(); + let prefix = match type_.as_str() { + "h1" => "# ", + "h2" => "## ", + "h3" => "### ", + "h4" => "#### ", + "h5" => "##### ", + "h6" => "###### ", + "quote" => "> ", + _ => "", + }; + markdown.push_str(prefix); + markdown.push_str(&text); + markdown.push('\n'); + } + continue; + } + + if flavour == "affine:list" { + if let Some((text, _)) = text_content(block, "prop:text") { + let depth = get_list_depth(&block_id, &parent_lookup, &block_pool); + let indent = " ".repeat(depth); + markdown.push_str(&indent); + markdown.push_str("- "); + markdown.push_str(&text); + markdown.push('\n'); + } + continue; + } + + if flavour == "affine:code" { + if let Some((text, _)) = text_content(block, "prop:text") { + let lang = get_string(block, "prop:language").unwrap_or_default(); + markdown.push_str("```"); + markdown.push_str(&lang); + markdown.push('\n'); + markdown.push_str(&text); + markdown.push_str("\n```\n"); + } + continue; + } + } + + Ok(MarkdownResult { + title: doc_title, + markdown, + }) +} + +fn get_list_depth( + block_id: &str, + parent_lookup: &HashMap, + blocks: &HashMap, +) -> usize { + let mut depth = 0; + let mut current_id = block_id.to_string(); + + while let Some(parent_id) = parent_lookup.get(¤t_id) { + if let Some(parent_block) = blocks.get(parent_id) { + if get_flavour(parent_block).as_deref() == Some("affine:list") { + depth += 1; + current_id = parent_id.clone(); + continue; + } + } + break; + } + depth +} + pub fn parse_doc_from_binary(doc_bin: Vec, doc_id: String) -> Result { if doc_bin.is_empty() || doc_bin == [0, 0] { return Err(ParseError::InvalidBinary); @@ -284,6 +523,49 @@ pub fn parse_doc_from_binary(doc_bin: Vec, doc_id: String) -> Result, + include_trash: bool, +) -> Result, ParseError> { + if doc_bin.is_empty() || doc_bin == [0, 0] { + return Err(ParseError::InvalidBinary); + } + + let mut doc = DocOptions::new().build(); + doc + .apply_update_from_binary_v1(&doc_bin) + .map_err(|_| ParseError::InvalidBinary)?; + + let meta = doc.get_map("meta")?; + let pages = match meta.get("pages").and_then(|v| v.to_array()) { + Some(arr) => arr, + None => return Ok(vec![]), + }; + + let mut doc_ids = Vec::new(); + for page_val in pages.iter() { + if let Some(page) = page_val.to_map() { + let id = get_string(&page, "id"); + if let Some(id) = id { + let trash = page + .get("trash") + .and_then(|v| match v.to_any() { + Some(Any::True) => Some(true), + Some(Any::False) => Some(false), + _ => None, + }) + .unwrap_or(false); + + if include_trash || !trash { + doc_ids.push(id); + } + } + } + } + + Ok(doc_ids) +} + fn collect_child_ids(block: &Map) -> Vec { block .get("sys:children") @@ -454,6 +736,56 @@ fn gather_table_contents(block: &Map) -> Vec { contents } +fn format_cell_value(value: &Any, col_type: &str, col_data: Option<&Map>) -> String { + match col_type { + "select" => { + if let Any::String(id) = value { + if let Some(options) = col_data + .and_then(|d| d.get("options")) + .and_then(|v| v.to_array()) + { + for opt in options.iter() { + if let Some(opt_map) = opt.to_map() { + if let Some(opt_id) = get_string(&opt_map, "id") { + if opt_id == *id { + return get_string(&opt_map, "value").unwrap_or_default(); + } + } + } + } + } + } + String::new() + } + "multi-select" => { + if let Any::Array(ids) = value { + let mut selected = Vec::new(); + if let Some(options) = col_data + .and_then(|d| d.get("options")) + .and_then(|v| v.to_array()) + { + for id_val in ids.iter() { + if let Any::String(id) = id_val { + for opt in options.iter() { + if let Some(opt_map) = opt.to_map() { + if let Some(opt_id) = get_string(&opt_map, "id") { + if opt_id == *id { + selected.push(get_string(&opt_map, "value").unwrap_or_default()); + } + } + } + } + } + } + } + return selected.join(", "); + } + String::new() + } + _ => any_to_string(value).unwrap_or_default(), + } +} + fn value_to_string(value: &Value) -> Option { if let Some(text) = value.to_text() { return Some(text.to_string()); diff --git a/tools/utils/src/workspace.gen.ts b/tools/utils/src/workspace.gen.ts index 34ce1e32fbbf8..15c99b4f05bc5 100644 --- a/tools/utils/src/workspace.gen.ts +++ b/tools/utils/src/workspace.gen.ts @@ -1166,7 +1166,6 @@ export const PackageList = [ location: 'packages/backend/server', name: '@affine/server', workspaceDependencies: [ - 'packages/common/reader', 'packages/backend/native', 'tools/cli', 'tools/utils', diff --git a/yarn.lock b/yarn.lock index f7092c1bf3bd9..39b6ef4db165b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -913,7 +913,6 @@ __metadata: "@affine-tools/cli": "workspace:*" "@affine-tools/utils": "workspace:*" "@affine/graphql": "workspace:*" - "@affine/reader": "workspace:*" "@affine/server-native": "workspace:*" "@ai-sdk/anthropic": "npm:^2.0.54" "@ai-sdk/google": "npm:^2.0.45" @@ -1231,6 +1230,24 @@ __metadata: languageName: node linkType: hard +"@apm-js-collab/code-transformer@npm:^0.8.0": + version: 0.8.2 + resolution: "@apm-js-collab/code-transformer@npm:0.8.2" + checksum: 10/ff4b8fea9a27ef4e82a0a022bececfa9966e5e2a4ae1e6ca0b31f4a2bbc6d79f5bd1257f281ed8a90900d693e1a88a79c42982eabe7ca999b267d5d69c57393f + languageName: node + linkType: hard + +"@apm-js-collab/tracing-hooks@npm:^0.3.1": + version: 0.3.1 + resolution: "@apm-js-collab/tracing-hooks@npm:0.3.1" + dependencies: + "@apm-js-collab/code-transformer": "npm:^0.8.0" + debug: "npm:^4.4.1" + module-details-from-path: "npm:^1.0.4" + checksum: 10/0af2220168d4fc2700ec577632eac066f756c3aaa34ef2e833ea4725abb80f7486c1a585a93e8f0d8304c51192f63c53e7ddd70290abac489a734f9693359f7f + languageName: node + linkType: hard + "@apollo/cache-control-types@npm:^1.0.3": version: 1.0.3 resolution: "@apollo/cache-control-types@npm:1.0.3" @@ -10602,65 +10619,65 @@ __metadata: languageName: node linkType: hard -"@next/env@npm:15.5.4": - version: 15.5.4 - resolution: "@next/env@npm:15.5.4" - checksum: 10/0727ef6dbb15c04d85119e3d4b28e000ca0a4dbb8bce46b2657181776545912181c74dd370e75ea8157bffbaf33bc687bd34c49b33e0b3a598c22295892ae1d9 +"@next/env@npm:15.5.9": + version: 15.5.9 + resolution: "@next/env@npm:15.5.9" + checksum: 10/962329701343c2617c85ae99ef35adfd9e8f7b58d9c0316f2f0857f82875a91ef3151ef0743c50964b0066aa3d1214c5193cf229f7757d7c9329f7fb95605a4a languageName: node linkType: hard -"@next/swc-darwin-arm64@npm:15.5.4": - version: 15.5.4 - resolution: "@next/swc-darwin-arm64@npm:15.5.4" +"@next/swc-darwin-arm64@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-darwin-arm64@npm:15.5.7" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@next/swc-darwin-x64@npm:15.5.4": - version: 15.5.4 - resolution: "@next/swc-darwin-x64@npm:15.5.4" +"@next/swc-darwin-x64@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-darwin-x64@npm:15.5.7" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@next/swc-linux-arm64-gnu@npm:15.5.4": - version: 15.5.4 - resolution: "@next/swc-linux-arm64-gnu@npm:15.5.4" +"@next/swc-linux-arm64-gnu@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-linux-arm64-gnu@npm:15.5.7" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-arm64-musl@npm:15.5.4": - version: 15.5.4 - resolution: "@next/swc-linux-arm64-musl@npm:15.5.4" +"@next/swc-linux-arm64-musl@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-linux-arm64-musl@npm:15.5.7" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@next/swc-linux-x64-gnu@npm:15.5.4": - version: 15.5.4 - resolution: "@next/swc-linux-x64-gnu@npm:15.5.4" +"@next/swc-linux-x64-gnu@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-linux-x64-gnu@npm:15.5.7" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-x64-musl@npm:15.5.4": - version: 15.5.4 - resolution: "@next/swc-linux-x64-musl@npm:15.5.4" +"@next/swc-linux-x64-musl@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-linux-x64-musl@npm:15.5.7" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:15.5.4": - version: 15.5.4 - resolution: "@next/swc-win32-arm64-msvc@npm:15.5.4" +"@next/swc-win32-arm64-msvc@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-win32-arm64-msvc@npm:15.5.7" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@next/swc-win32-x64-msvc@npm:15.5.4": - version: 15.5.4 - resolution: "@next/swc-win32-x64-msvc@npm:15.5.4" +"@next/swc-win32-x64-msvc@npm:15.5.7": + version: 15.5.7 + resolution: "@next/swc-win32-x64-msvc@npm:15.5.7" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -11216,15 +11233,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api-logs@npm:0.204.0": - version: 0.204.0 - resolution: "@opentelemetry/api-logs@npm:0.204.0" - dependencies: - "@opentelemetry/api": "npm:^1.3.0" - checksum: 10/698d04b3fc014ec68b5571ef5bebb9c276e290035f060aae6c2554410115d42d41ad41255a809bde4ca26e32bc833dba413c1d90dcda624c6c0307c309ba9d08 - languageName: node - linkType: hard - "@opentelemetry/api-logs@npm:0.208.0": version: 0.208.0 resolution: "@opentelemetry/api-logs@npm:0.208.0" @@ -11234,15 +11242,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api-logs@npm:0.57.2": - version: 0.57.2 - resolution: "@opentelemetry/api-logs@npm:0.57.2" - dependencies: - "@opentelemetry/api": "npm:^1.3.0" - checksum: 10/8e3bac962e8f1fc93bfee6b433121bd2e07e8a8d1b86ef0d9d4a2c54d1759b64c74cf5da400f82f5ab5a4fe0da481726d8635fd1b15d123cf43090fa0adb8ea8 - languageName: node - linkType: hard - "@opentelemetry/api@npm:1.9.0, @opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.9.0": version: 1.9.0 resolution: "@opentelemetry/api@npm:1.9.0" @@ -11250,7 +11249,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/context-async-hooks@npm:2.2.0, @opentelemetry/context-async-hooks@npm:^2.1.0": +"@opentelemetry/context-async-hooks@npm:2.2.0, @opentelemetry/context-async-hooks@npm:^2.2.0": version: 2.2.0 resolution: "@opentelemetry/context-async-hooks@npm:2.2.0" peerDependencies: @@ -11259,18 +11258,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/core@npm:2.1.0": - version: 2.1.0 - resolution: "@opentelemetry/core@npm:2.1.0" - dependencies: - "@opentelemetry/semantic-conventions": "npm:^1.29.0" - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10/735bd1fe8a099c3aa3d7640875a5b30ab304f7e3b13efd622daabe47ff5470e9c453ad9fc119a5a0c0458ec2c7753f9a066afa32a269745b06b429ba7db90516 - languageName: node - linkType: hard - -"@opentelemetry/core@npm:2.2.0, @opentelemetry/core@npm:^2.0.0, @opentelemetry/core@npm:^2.1.0, @opentelemetry/core@npm:^2.2.0": +"@opentelemetry/core@npm:2.2.0, @opentelemetry/core@npm:^2.0.0, @opentelemetry/core@npm:^2.2.0": version: 2.2.0 resolution: "@opentelemetry/core@npm:2.2.0" dependencies: @@ -11463,92 +11451,80 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/instrumentation-amqplib@npm:0.51.0": - version: 0.51.0 - resolution: "@opentelemetry/instrumentation-amqplib@npm:0.51.0" +"@opentelemetry/instrumentation-amqplib@npm:0.55.0": + version: 0.55.0 + resolution: "@opentelemetry/instrumentation-amqplib@npm:0.55.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.204.0" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/be46037ebe797cff15785bf95ce93b8a95984b4cfd2329401d9c5ee0f3a488a9601ee259ca5e104a73a8677639fa8658d9111a356c45ada6b0f35a9271314167 + checksum: 10/03e028c4dfe9a8a9372f6ef6d4c4520343a3ec571e2e04dcd29e684cf25d212f6331be268656006220b476f6b46748aba2d142b7b4c62efd24baebf6a3f1b77b languageName: node linkType: hard -"@opentelemetry/instrumentation-connect@npm:0.48.0": - version: 0.48.0 - resolution: "@opentelemetry/instrumentation-connect@npm:0.48.0" +"@opentelemetry/instrumentation-connect@npm:0.52.0": + version: 0.52.0 + resolution: "@opentelemetry/instrumentation-connect@npm:0.52.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.204.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" "@opentelemetry/semantic-conventions": "npm:^1.27.0" "@types/connect": "npm:3.4.38" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/289edf6d1108f1fb7e9937d0be62fbf44879716c53addd226862c1ae4dedfe030961f8052248485177ae950382046b7c00cc50512f3a195a1c285f4c455dca81 + checksum: 10/c526768a10a4f6c76b791e382edeae2217a7b3c448b1a3458ef34ca46a645b6cf67eeec79bae119e2005f5d766c8eabc03b16058da6e94300110d029157e7e95 languageName: node linkType: hard -"@opentelemetry/instrumentation-dataloader@npm:0.22.0": - version: 0.22.0 - resolution: "@opentelemetry/instrumentation-dataloader@npm:0.22.0" +"@opentelemetry/instrumentation-dataloader@npm:0.26.0": + version: 0.26.0 + resolution: "@opentelemetry/instrumentation-dataloader@npm:0.26.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.204.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/8e15a6c5e5720819fccf406cc847c104d0364ad85a1561f8f96087cd13b5555b33b6c07403a83ef7fd02de29f090b6e7171efded1dd52583b6d6567f047f62ab + checksum: 10/e3efde5515698fc25437d4e347cc829da1f82df95c0523149913030b53e4a359577ee988b66d5b0a3bd8230697cd785e983176c0b4bb14d001c64c8948d46c10 languageName: node linkType: hard -"@opentelemetry/instrumentation-express@npm:0.53.0": - version: 0.53.0 - resolution: "@opentelemetry/instrumentation-express@npm:0.53.0" +"@opentelemetry/instrumentation-express@npm:0.57.0": + version: 0.57.0 + resolution: "@opentelemetry/instrumentation-express@npm:0.57.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.204.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" "@opentelemetry/semantic-conventions": "npm:^1.27.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/30a354dde6c498ac82ee5423965a03e2f31e7ae82687b4135bbb20d967e066eb1accd6ca8a60da1e4cf2be8dfe6d07c413ead7fd89e166c36e7da92645e4493c + checksum: 10/66c10e878433d0e90bbf88dfbf28b23ecdbc3777326f46548c16c5062118b37c623011cef390a5ed3de2855f67f91587703581846f4e1e702ff3a068404f3e89 languageName: node linkType: hard -"@opentelemetry/instrumentation-fs@npm:0.24.0": - version: 0.24.0 - resolution: "@opentelemetry/instrumentation-fs@npm:0.24.0" +"@opentelemetry/instrumentation-fs@npm:0.28.0": + version: 0.28.0 + resolution: "@opentelemetry/instrumentation-fs@npm:0.28.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.204.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10/9ccb9b9bb79bb8a372a42b8544e84f9e18dab5a1b128b891857e3186da67668493666b551c7909d27479003b73979ad85060f1d67cf1e5b1b01dc3810e47ea83 - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-generic-pool@npm:0.48.0": - version: 0.48.0 - resolution: "@opentelemetry/instrumentation-generic-pool@npm:0.48.0" - dependencies: - "@opentelemetry/instrumentation": "npm:^0.204.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/4827243b033e70fd8e62e8453b0932302dc6e2638123a47968661c8cab94a87e6149fe81160f75940bf3ab4e45b400fb3e61babd1e52fea219fa502668da220c + checksum: 10/f62c1256e3c47d958b296b57f8ab1d0b8b8851e9c3d6ad8691af514a33a2341ec1a9345d57a705a420a7a5757a297e03cc04c497a5701ac027a98b63b1a90ab1 languageName: node linkType: hard -"@opentelemetry/instrumentation-graphql@npm:0.52.0": +"@opentelemetry/instrumentation-generic-pool@npm:0.52.0": version: 0.52.0 - resolution: "@opentelemetry/instrumentation-graphql@npm:0.52.0" + resolution: "@opentelemetry/instrumentation-generic-pool@npm:0.52.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.204.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/8f4226237a492928d48750110d548f40c3fa2e2ecb42fcf977bc6cc188afbbb646fad673b3ecb2d21eab28415ee31b1d23afe27e5e2ebdaf17e89b6bf44aff0f + checksum: 10/f94b27d84a4172f95f7a33fa967f2ad562d4dba3182b1e8b6658b4eceb8a26a678b407a232858378a034dd8ffdb4e66bd98b4a8861fe042be59af3e4b400244f languageName: node linkType: hard -"@opentelemetry/instrumentation-graphql@npm:^0.56.0": +"@opentelemetry/instrumentation-graphql@npm:0.56.0, @opentelemetry/instrumentation-graphql@npm:^0.56.0": version: 0.56.0 resolution: "@opentelemetry/instrumentation-graphql@npm:0.56.0" dependencies: @@ -11559,34 +11535,20 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/instrumentation-hapi@npm:0.51.0": - version: 0.51.0 - resolution: "@opentelemetry/instrumentation-hapi@npm:0.51.0" +"@opentelemetry/instrumentation-hapi@npm:0.55.0": + version: 0.55.0 + resolution: "@opentelemetry/instrumentation-hapi@npm:0.55.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.204.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" "@opentelemetry/semantic-conventions": "npm:^1.27.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/e098437a63807ec288c9e139554ac866909aac2c5da483a9d981f6f2bec33b4b28f73a1b04a2a069870998c2c32eaf68572c6bce3992de0f0b6c09a7e6da1b23 - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-http@npm:0.204.0": - version: 0.204.0 - resolution: "@opentelemetry/instrumentation-http@npm:0.204.0" - dependencies: - "@opentelemetry/core": "npm:2.1.0" - "@opentelemetry/instrumentation": "npm:0.204.0" - "@opentelemetry/semantic-conventions": "npm:^1.29.0" - forwarded-parse: "npm:2.1.2" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10/ce62b8a829293b9e584ecee317d5639f621588f7740b7fc21140d7aa831e7ea298d00e6cc87b95c63d80294387a7f236eb9d4e57124f12b28437c8dd4e82e944 + checksum: 10/575c059a2dbcd77256acfca897d2cc08968b4e7e9e41449dfd5de2ca761fa2a544aaa6f2a8213f7374a42026e1f0b7e612ce01337e43108631e20e19fec01c81 languageName: node linkType: hard -"@opentelemetry/instrumentation-http@npm:^0.208.0": +"@opentelemetry/instrumentation-http@npm:0.208.0, @opentelemetry/instrumentation-http@npm:^0.208.0": version: 0.208.0 resolution: "@opentelemetry/instrumentation-http@npm:0.208.0" dependencies: @@ -11600,20 +11562,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/instrumentation-ioredis@npm:0.52.0": - version: 0.52.0 - resolution: "@opentelemetry/instrumentation-ioredis@npm:0.52.0" - dependencies: - "@opentelemetry/instrumentation": "npm:^0.204.0" - "@opentelemetry/redis-common": "npm:^0.38.0" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10/65eb7aa6db4b84b460f264e2ee4c0cdbd3701a818e11c08b2bccf498795bea8e8be4bbf9519c8e7b8043563fd2f3699f57875854bc27be3236f326de3792a292 - languageName: node - linkType: hard - -"@opentelemetry/instrumentation-ioredis@npm:^0.56.0": +"@opentelemetry/instrumentation-ioredis@npm:0.56.0, @opentelemetry/instrumentation-ioredis@npm:^0.56.0": version: 0.56.0 resolution: "@opentelemetry/instrumentation-ioredis@npm:0.56.0" dependencies: @@ -11625,102 +11574,99 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/instrumentation-kafkajs@npm:0.14.0": - version: 0.14.0 - resolution: "@opentelemetry/instrumentation-kafkajs@npm:0.14.0" +"@opentelemetry/instrumentation-kafkajs@npm:0.18.0": + version: 0.18.0 + resolution: "@opentelemetry/instrumentation-kafkajs@npm:0.18.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.204.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" "@opentelemetry/semantic-conventions": "npm:^1.30.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/488b0e7f42392a47dfbce3d1e728b34873793ee239c2646f9dffe1ecc9b1e2ea4337145cb5e4bcaa77a81899062ef586a6b3f391b4b4b49249529f29b871b053 + checksum: 10/e3b998d905dc6c87b542e6b7004e12eeac19903872fe3e7d4c17771f69843b721dd73d0b136b2f21207d4dec7e53fd17d5042cf27a0028ec3c3074c0861654fe languageName: node linkType: hard -"@opentelemetry/instrumentation-knex@npm:0.49.0": - version: 0.49.0 - resolution: "@opentelemetry/instrumentation-knex@npm:0.49.0" +"@opentelemetry/instrumentation-knex@npm:0.53.0": + version: 0.53.0 + resolution: "@opentelemetry/instrumentation-knex@npm:0.53.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.204.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" "@opentelemetry/semantic-conventions": "npm:^1.33.1" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/d4802b024dd2e49e63a65567fe3d6b8a6048c5cd70fb78a78134c56568496c8fc10ef867039f9525cabb5f9c06bb7e8ff07651bb5a15b89bdc0fb70a4cc5c96c + checksum: 10/70506c24957374276ef63a18d103fb74a37888ffeb9c1c6b6419a3abcf970d4c8dfed3129ca840f71f1207e246c7bc337d3bc4ab249b1a1c67dfc854dd7da7fd languageName: node linkType: hard -"@opentelemetry/instrumentation-koa@npm:0.52.0": - version: 0.52.0 - resolution: "@opentelemetry/instrumentation-koa@npm:0.52.0" +"@opentelemetry/instrumentation-koa@npm:0.57.0": + version: 0.57.0 + resolution: "@opentelemetry/instrumentation-koa@npm:0.57.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.204.0" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/semantic-conventions": "npm:^1.36.0" peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10/88a444bd92c3e1906167820e63e40bfe6531f54c28a8aacec51e2d527465818c002a5dfe72fbc1709cac6f6b4f5a0d01e798c4089731b0ec1b28fe94b9a729de + "@opentelemetry/api": ^1.9.0 + checksum: 10/7df972b4a5c6c8cad6e1fe6ab3a6ea391d4db09f0d3e2b0b46f0ffa0289df88fe2446e06c08744d65293aeaff2f28628d51f8dea5d97404907a49f9e567f50f6 languageName: node linkType: hard -"@opentelemetry/instrumentation-lru-memoizer@npm:0.49.0": - version: 0.49.0 - resolution: "@opentelemetry/instrumentation-lru-memoizer@npm:0.49.0" +"@opentelemetry/instrumentation-lru-memoizer@npm:0.53.0": + version: 0.53.0 + resolution: "@opentelemetry/instrumentation-lru-memoizer@npm:0.53.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.204.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/8aaad4a276ab9a754ceca28767261c930be98de728e9fab68ff304b2e6839b6d32c8e48984ff868499bf34765da76ee2e44bb98c54317fc904aed3480fecfd4b + checksum: 10/6f83abdb58e83fe87ce926236c54fbaaf49f64f8149e455b1dc1f33d735faf575e6f6d4df36b8eadf176a74d01cd05c6f3a3babff56a5a7448744e0cba39df5e languageName: node linkType: hard -"@opentelemetry/instrumentation-mongodb@npm:0.57.0": - version: 0.57.0 - resolution: "@opentelemetry/instrumentation-mongodb@npm:0.57.0" +"@opentelemetry/instrumentation-mongodb@npm:0.61.0": + version: 0.61.0 + resolution: "@opentelemetry/instrumentation-mongodb@npm:0.61.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.204.0" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/f6e20ee84c83cb2f9752e198f106c5b7008b4e530c0207863723967008d9741987862cf8cc2a6d8853041c99f029d0d4d80fdf20ed6048eb782ec502bb730888 + checksum: 10/fe5eb79c8924aa268c863af4a5878c47ba5730622cb861899802144a4dfe7431e7af6e9b568dd205fdedfd77fbbfe95539cdcf35e6b326ba695eeaadd61240ed languageName: node linkType: hard -"@opentelemetry/instrumentation-mongoose@npm:0.51.0": - version: 0.51.0 - resolution: "@opentelemetry/instrumentation-mongoose@npm:0.51.0" +"@opentelemetry/instrumentation-mongoose@npm:0.55.0": + version: 0.55.0 + resolution: "@opentelemetry/instrumentation-mongoose@npm:0.55.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.204.0" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/1c6d99a58cbedc8efa921411db62a8f756dd730cd5d0419b4073336c2b2f04d8e230665d5e54be200130caa7f0d55d37ba6514acbfafcc74f95c0035906a4556 + checksum: 10/5ee25946b7a6a50178ff6b50d5a88b8a189f0ba6effc45ee07e6026c353b1cad605f51cb9c61f9200e066833a7f3b588ede02582dc7dbbce4a9d715ec312cea6 languageName: node linkType: hard -"@opentelemetry/instrumentation-mysql2@npm:0.51.0": - version: 0.51.0 - resolution: "@opentelemetry/instrumentation-mysql2@npm:0.51.0" +"@opentelemetry/instrumentation-mysql2@npm:0.55.0": + version: 0.55.0 + resolution: "@opentelemetry/instrumentation-mysql2@npm:0.55.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.204.0" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" - "@opentelemetry/sql-common": "npm:^0.41.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/semantic-conventions": "npm:^1.33.0" + "@opentelemetry/sql-common": "npm:^0.41.2" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/dab850b1a7a6f0a90e319c16a657218f366d9d06d8a78b1678f78b663db121a723276fe649e41cd67b3f36ff516efe5df8b13f684d989f1df94bf7137c05635d + checksum: 10/ca5bb6c99bd8fd3590341955aeece3c69982e07b2df520998ea282ac9135311f8fc37ba55fb66adf07a80adcf98d42892b698422c9817667231064cc251e626d languageName: node linkType: hard -"@opentelemetry/instrumentation-mysql@npm:0.50.0": - version: 0.50.0 - resolution: "@opentelemetry/instrumentation-mysql@npm:0.50.0" +"@opentelemetry/instrumentation-mysql@npm:0.54.0": + version: 0.54.0 + resolution: "@opentelemetry/instrumentation-mysql@npm:0.54.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.204.0" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" "@types/mysql": "npm:2.15.27" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/c615b692e86e51d15f5e0c10669019e0d9cb728a175bda7d2e3ab7df07cb65f872f63f9c01843b3dc5a630379a2ccfaafc3a7c0396f09de7b025a891d5794ce4 + checksum: 10/d4756b4d72c0232f92fc00fd8fb03d02fe27d9108c8c74ae1d8aa50800207f7e6dc10aa8f5a636d9b6c8865b10e10a79741242c08af4d4f0599977182b7508ae languageName: node linkType: hard @@ -11736,32 +11682,32 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/instrumentation-pg@npm:0.57.0": - version: 0.57.0 - resolution: "@opentelemetry/instrumentation-pg@npm:0.57.0" +"@opentelemetry/instrumentation-pg@npm:0.61.0": + version: 0.61.0 + resolution: "@opentelemetry/instrumentation-pg@npm:0.61.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.204.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" "@opentelemetry/semantic-conventions": "npm:^1.34.0" - "@opentelemetry/sql-common": "npm:^0.41.0" - "@types/pg": "npm:8.15.5" + "@opentelemetry/sql-common": "npm:^0.41.2" + "@types/pg": "npm:8.15.6" "@types/pg-pool": "npm:2.0.6" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/7ebaa287c1da8b6c0183e42f7bdc05fda99ad0e046208361fba173da8831ce0645b245c3c4c9ffdc234cc4c96c04622424e91f2cf18ff2d29b7d0f9ce3b7fc6e + checksum: 10/84825695303d79720a87622bfb0bcbc6c46463a595a36d7fb611684bfb23df9645ff17780b60a00cc97a78e70ef109603fc473b2cefd91a7099b14d2d93fdb56 languageName: node linkType: hard -"@opentelemetry/instrumentation-redis@npm:0.53.0": - version: 0.53.0 - resolution: "@opentelemetry/instrumentation-redis@npm:0.53.0" +"@opentelemetry/instrumentation-redis@npm:0.57.0": + version: 0.57.0 + resolution: "@opentelemetry/instrumentation-redis@npm:0.57.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.204.0" - "@opentelemetry/redis-common": "npm:^0.38.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/redis-common": "npm:^0.38.2" "@opentelemetry/semantic-conventions": "npm:^1.27.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/606c8877819c99d62266a106fa15703316fb703a60dd82e4995ebf33d892fecea35e6f4457a56f218b29adac8f573bf5a5437feeb406fb65002e65ed70ee178a + checksum: 10/171f5e57d0141088f8e4cdb8f744226d7999ca045842708e65c8b784f53a41e25a7248f22382bf1b4a80b6fa1cc4c41658502bb15340b3fa2c22f2e9db8b76e0 languageName: node linkType: hard @@ -11776,45 +11722,32 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/instrumentation-tedious@npm:0.23.0": - version: 0.23.0 - resolution: "@opentelemetry/instrumentation-tedious@npm:0.23.0" +"@opentelemetry/instrumentation-tedious@npm:0.27.0": + version: 0.27.0 + resolution: "@opentelemetry/instrumentation-tedious@npm:0.27.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.204.0" - "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" "@types/tedious": "npm:^4.0.14" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10/00d922782c709dbc6e9fdaa77197423f76707904a71a920d60642f669e496f37b64a6376609bfafcda4d0778cdab15b161a56426a20fb1a5c24a131c9d2db8a5 + checksum: 10/453492b72cf16b855da1a4ba38b9619261ef77ed4afe8ae551c0abc529c81c8640d816934aae197fa719e8e79591613d2398a8a7f3b77c82de41bf51db3eebfc languageName: node linkType: hard -"@opentelemetry/instrumentation-undici@npm:0.15.0": - version: 0.15.0 - resolution: "@opentelemetry/instrumentation-undici@npm:0.15.0" +"@opentelemetry/instrumentation-undici@npm:0.19.0": + version: 0.19.0 + resolution: "@opentelemetry/instrumentation-undici@npm:0.19.0" dependencies: "@opentelemetry/core": "npm:^2.0.0" - "@opentelemetry/instrumentation": "npm:^0.204.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/semantic-conventions": "npm:^1.24.0" peerDependencies: "@opentelemetry/api": ^1.7.0 - checksum: 10/5190d2e08785e05c92815e2b1db5d85d7427fc1c7c4529ffb7a076793e8ac71117b8f9007f42ddd22565147dfee5b2d2d3b0e8237d46f2b83d48dd1a353e2c11 + checksum: 10/862ea5e49c2cf38c7a135f5ea6a95f1b8ca009620f9b0e4d6c7ffb69e8e1d4e7d4169e4cf0cd2af4ce574864b37ea3f7c039b0e6600d3e490f3c263a8647325e languageName: node linkType: hard -"@opentelemetry/instrumentation@npm:0.204.0, @opentelemetry/instrumentation@npm:^0.204.0": - version: 0.204.0 - resolution: "@opentelemetry/instrumentation@npm:0.204.0" - dependencies: - "@opentelemetry/api-logs": "npm:0.204.0" - import-in-the-middle: "npm:^1.8.1" - require-in-the-middle: "npm:^7.1.1" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10/a32b93e714e555dc1fca6c212bde565342370f0f62c1421e02cb307193ced1f8167223b3b70adc6e4539bf8508424f56b7249ecb527995406944434b29b73066 - languageName: node - linkType: hard - -"@opentelemetry/instrumentation@npm:0.208.0, @opentelemetry/instrumentation@npm:^0.208.0": +"@opentelemetry/instrumentation@npm:0.208.0, @opentelemetry/instrumentation@npm:>=0.52.0 <1, @opentelemetry/instrumentation@npm:^0.208.0": version: 0.208.0 resolution: "@opentelemetry/instrumentation@npm:0.208.0" dependencies: @@ -11827,22 +11760,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/instrumentation@npm:^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0": - version: 0.57.2 - resolution: "@opentelemetry/instrumentation@npm:0.57.2" - dependencies: - "@opentelemetry/api-logs": "npm:0.57.2" - "@types/shimmer": "npm:^1.2.0" - import-in-the-middle: "npm:^1.8.1" - require-in-the-middle: "npm:^7.1.1" - semver: "npm:^7.5.2" - shimmer: "npm:^1.2.1" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10/b66b840e87976a5edf551a7011a395df8df5985571ac0506412943d07b4309fcc78fe71d3f55217a00f44384fbf61f59f1e54d544ab12f5490f6a7a56b71e02a - languageName: node - linkType: hard - "@opentelemetry/otlp-exporter-base@npm:0.208.0": version: 0.208.0 resolution: "@opentelemetry/otlp-exporter-base@npm:0.208.0" @@ -11908,14 +11825,14 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/redis-common@npm:^0.38.0, @opentelemetry/redis-common@npm:^0.38.2": +"@opentelemetry/redis-common@npm:^0.38.2": version: 0.38.2 resolution: "@opentelemetry/redis-common@npm:0.38.2" checksum: 10/2a4f992572b1990a407ac92c7db941aecb6e8d71f034f4ea0b00b2b1739ad07c22767198a6759ab634cbbe9eebfbea062e2b79a25484289c226c665558041503 languageName: node linkType: hard -"@opentelemetry/resources@npm:2.2.0, @opentelemetry/resources@npm:^2.1.0, @opentelemetry/resources@npm:^2.2.0": +"@opentelemetry/resources@npm:2.2.0, @opentelemetry/resources@npm:^2.2.0": version: 2.2.0 resolution: "@opentelemetry/resources@npm:2.2.0" dependencies: @@ -11984,7 +11901,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-trace-base@npm:2.2.0, @opentelemetry/sdk-trace-base@npm:^2.1.0": +"@opentelemetry/sdk-trace-base@npm:2.2.0, @opentelemetry/sdk-trace-base@npm:^2.2.0": version: 2.2.0 resolution: "@opentelemetry/sdk-trace-base@npm:2.2.0" dependencies: @@ -12010,14 +11927,14 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/semantic-conventions@npm:^1.22.0, @opentelemetry/semantic-conventions@npm:^1.27.0, @opentelemetry/semantic-conventions@npm:^1.29.0, @opentelemetry/semantic-conventions@npm:^1.30.0, @opentelemetry/semantic-conventions@npm:^1.33.1, @opentelemetry/semantic-conventions@npm:^1.34.0, @opentelemetry/semantic-conventions@npm:^1.37.0, @opentelemetry/semantic-conventions@npm:^1.38.0": +"@opentelemetry/semantic-conventions@npm:^1.22.0, @opentelemetry/semantic-conventions@npm:^1.24.0, @opentelemetry/semantic-conventions@npm:^1.27.0, @opentelemetry/semantic-conventions@npm:^1.29.0, @opentelemetry/semantic-conventions@npm:^1.30.0, @opentelemetry/semantic-conventions@npm:^1.33.0, @opentelemetry/semantic-conventions@npm:^1.33.1, @opentelemetry/semantic-conventions@npm:^1.34.0, @opentelemetry/semantic-conventions@npm:^1.36.0, @opentelemetry/semantic-conventions@npm:^1.37.0, @opentelemetry/semantic-conventions@npm:^1.38.0": version: 1.38.0 resolution: "@opentelemetry/semantic-conventions@npm:1.38.0" checksum: 10/9d549f4896e900f644d5e70dd7142505daff88ed83c1cb7bcd976ac55e9496d4ddd686bb2815dd68655c739950514394c3b73ff51e53b2e4ff2d54a7f6d22521 languageName: node linkType: hard -"@opentelemetry/sql-common@npm:^0.41.0": +"@opentelemetry/sql-common@npm:^0.41.2": version: 0.41.2 resolution: "@opentelemetry/sql-common@npm:0.41.2" dependencies: @@ -12304,14 +12221,14 @@ __metadata: languageName: node linkType: hard -"@prisma/instrumentation@npm:6.15.0, @prisma/instrumentation@npm:^6.7.0": - version: 6.15.0 - resolution: "@prisma/instrumentation@npm:6.15.0" +"@prisma/instrumentation@npm:6.19.0, @prisma/instrumentation@npm:^6.7.0": + version: 6.19.0 + resolution: "@prisma/instrumentation@npm:6.19.0" dependencies: - "@opentelemetry/instrumentation": "npm:^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" + "@opentelemetry/instrumentation": "npm:>=0.52.0 <1" peerDependencies: "@opentelemetry/api": ^1.8 - checksum: 10/25f69e3ea581b0cfa058c80acde5e5382d3b5d48e213654c011ab61909dd6311c17235dd26fa36c4e91d620f8ab49f1190732d6a48bbeec545e0664e1e1f86dc + checksum: 10/9d63362f4a4cd52f956d672e32bc8ad54619675e14cd33271d225c7848a685f137316dc98bab40dc5c5032b86efeb21fe60b6e6bb840608696d61f251d6f0e75 languageName: node linkType: hard @@ -14054,12 +13971,12 @@ __metadata: languageName: node linkType: hard -"@sentry-internal/browser-utils@npm:10.17.0": - version: 10.17.0 - resolution: "@sentry-internal/browser-utils@npm:10.17.0" +"@sentry-internal/browser-utils@npm:10.29.0": + version: 10.29.0 + resolution: "@sentry-internal/browser-utils@npm:10.29.0" dependencies: - "@sentry/core": "npm:10.17.0" - checksum: 10/328851bbbbd563837105e1b6205130aa1eccbd679b84d54c4a657af47339e63ac868415d37e80b493005e9d7543dc61a5e47e52a17ebc5af1790f4c7cf09467e + "@sentry/core": "npm:10.29.0" + checksum: 10/eacb0b4d08a732370b9d70a53f1348fe80463abb39d70aa2935cc1bd611f034d0cd6c1e7b783b654856a24e7a525524f9d305ec9133d0574abb09e1412ea8237 languageName: node linkType: hard @@ -14072,12 +13989,12 @@ __metadata: languageName: node linkType: hard -"@sentry-internal/feedback@npm:10.17.0": - version: 10.17.0 - resolution: "@sentry-internal/feedback@npm:10.17.0" +"@sentry-internal/feedback@npm:10.29.0": + version: 10.29.0 + resolution: "@sentry-internal/feedback@npm:10.29.0" dependencies: - "@sentry/core": "npm:10.17.0" - checksum: 10/f6753f11b06809109a5d5189720370a78a66859738a1e8c9285db4fd7871030927458e92909e9729a6cd94c216de3d578b3bb836f39cbd697da972922960b57a + "@sentry/core": "npm:10.29.0" + checksum: 10/3b19a1f8d0ecce20b67bee4aa866d10338bad968461b772944ced96413c650c655f5345f9cab193716f381e354ae08c4e055d356d108953e6411c5be51c85550 languageName: node linkType: hard @@ -14090,13 +14007,13 @@ __metadata: languageName: node linkType: hard -"@sentry-internal/replay-canvas@npm:10.17.0": - version: 10.17.0 - resolution: "@sentry-internal/replay-canvas@npm:10.17.0" +"@sentry-internal/replay-canvas@npm:10.29.0": + version: 10.29.0 + resolution: "@sentry-internal/replay-canvas@npm:10.29.0" dependencies: - "@sentry-internal/replay": "npm:10.17.0" - "@sentry/core": "npm:10.17.0" - checksum: 10/9dbfb29385fc82d0985dda5f5d981635778cb0c141faf863c6dc6d17ab22b7ad1666a824b0f3000d0165ddc3814b8c9b35393959b18a0f148d2c12adc37ebadc + "@sentry-internal/replay": "npm:10.29.0" + "@sentry/core": "npm:10.29.0" + checksum: 10/af5cea43b144eefd565c2af48515bf9f8ed120ed6671a86a671e04c1930e5d0532f64e3b618c7d72b0881e0caa812692c02d6af7207388a07170835521164e38 languageName: node linkType: hard @@ -14110,13 +14027,13 @@ __metadata: languageName: node linkType: hard -"@sentry-internal/replay@npm:10.17.0": - version: 10.17.0 - resolution: "@sentry-internal/replay@npm:10.17.0" +"@sentry-internal/replay@npm:10.29.0": + version: 10.29.0 + resolution: "@sentry-internal/replay@npm:10.29.0" dependencies: - "@sentry-internal/browser-utils": "npm:10.17.0" - "@sentry/core": "npm:10.17.0" - checksum: 10/b9cce490dd0840f6284f9fecd1ec00f77c7e7a39daeffc8da25622ae4006ceaa20bd1dbb94756db20438dd2e60f341dbb3f5348a4839fcde59f508d13e5c097a + "@sentry-internal/browser-utils": "npm:10.29.0" + "@sentry/core": "npm:10.29.0" + checksum: 10/2314d9d6da1e57a830732351a7a57bca44e2f75a79b52e9998da20666fe3a15b6e64844758256d1281f8c9f9f07e71d84d05e5648792268cfe61762965484e7e languageName: node linkType: hard @@ -14130,23 +14047,23 @@ __metadata: languageName: node linkType: hard -"@sentry/babel-plugin-component-annotate@npm:3.5.0": - version: 3.5.0 - resolution: "@sentry/babel-plugin-component-annotate@npm:3.5.0" - checksum: 10/da8b356a00445b6dd711b59fbfa5b9e2f808dcf1e364d6a37b1d4931ce53e87734b1178cee14af59f9e36ffa1e9a6eb8081caa455c4e803800f10aad80b5bb58 +"@sentry/babel-plugin-component-annotate@npm:3.6.1": + version: 3.6.1 + resolution: "@sentry/babel-plugin-component-annotate@npm:3.6.1" + checksum: 10/b65732b1be4123a0da37875b608b66fa3bef50698df90dc4d3236cd182f21708aeb50a94f9108d2cf6d199d76a55c698c52d9cb4bbdc2f859108b4d46321a15a languageName: node linkType: hard -"@sentry/browser@npm:10.17.0": - version: 10.17.0 - resolution: "@sentry/browser@npm:10.17.0" +"@sentry/browser@npm:10.29.0": + version: 10.29.0 + resolution: "@sentry/browser@npm:10.29.0" dependencies: - "@sentry-internal/browser-utils": "npm:10.17.0" - "@sentry-internal/feedback": "npm:10.17.0" - "@sentry-internal/replay": "npm:10.17.0" - "@sentry-internal/replay-canvas": "npm:10.17.0" - "@sentry/core": "npm:10.17.0" - checksum: 10/6bfc8ae92d988b79a4f2ced0be438f389fbf8d7d0ee82cbce1cbac02693b02a9a76df8375582bfca7578ec856c025d074f49de2c3e1f75a2185f9d52d2a61f65 + "@sentry-internal/browser-utils": "npm:10.29.0" + "@sentry-internal/feedback": "npm:10.29.0" + "@sentry-internal/replay": "npm:10.29.0" + "@sentry-internal/replay-canvas": "npm:10.29.0" + "@sentry/core": "npm:10.29.0" + checksum: 10/b1ab4f7a9a0304174134be4e432b6cf03d7cf91402107549648996d541336ff5846004d5c5bea34769bdd2c46d62a5e00e3f0ba317f25fb8ec4c5d6321d74ce8 languageName: node linkType: hard @@ -14163,82 +14080,90 @@ __metadata: languageName: node linkType: hard -"@sentry/bundler-plugin-core@npm:3.5.0": - version: 3.5.0 - resolution: "@sentry/bundler-plugin-core@npm:3.5.0" +"@sentry/bundler-plugin-core@npm:3.6.1": + version: 3.6.1 + resolution: "@sentry/bundler-plugin-core@npm:3.6.1" dependencies: "@babel/core": "npm:^7.18.5" - "@sentry/babel-plugin-component-annotate": "npm:3.5.0" - "@sentry/cli": "npm:2.42.2" + "@sentry/babel-plugin-component-annotate": "npm:3.6.1" + "@sentry/cli": "npm:^2.49.0" dotenv: "npm:^16.3.1" find-up: "npm:^5.0.0" glob: "npm:^9.3.2" magic-string: "npm:0.30.8" unplugin: "npm:1.0.1" - checksum: 10/335715545a311d84d5c7adfa12d7204b7472d42d80af7e7f1fab6e3ca33bcf9169433cae3ee7bb2e6e55e0222c71d77393c30cd05b9265bb5135518bd870607e + checksum: 10/6445bda6a963acc8fedd857c3783a21aa2ca147f8c5dced5b064d8d5756e87903346ce113a1f792f47f908184332917f8e413f52441dd33d0fb0eb197d638dc1 languageName: node linkType: hard -"@sentry/cli-darwin@npm:2.42.2": - version: 2.42.2 - resolution: "@sentry/cli-darwin@npm:2.42.2" +"@sentry/cli-darwin@npm:2.58.4": + version: 2.58.4 + resolution: "@sentry/cli-darwin@npm:2.58.4" conditions: os=darwin languageName: node linkType: hard -"@sentry/cli-linux-arm64@npm:2.42.2": - version: 2.42.2 - resolution: "@sentry/cli-linux-arm64@npm:2.42.2" - conditions: (os=linux | os=freebsd) & cpu=arm64 +"@sentry/cli-linux-arm64@npm:2.58.4": + version: 2.58.4 + resolution: "@sentry/cli-linux-arm64@npm:2.58.4" + conditions: (os=linux | os=freebsd | os=android) & cpu=arm64 languageName: node linkType: hard -"@sentry/cli-linux-arm@npm:2.42.2": - version: 2.42.2 - resolution: "@sentry/cli-linux-arm@npm:2.42.2" - conditions: (os=linux | os=freebsd) & cpu=arm +"@sentry/cli-linux-arm@npm:2.58.4": + version: 2.58.4 + resolution: "@sentry/cli-linux-arm@npm:2.58.4" + conditions: (os=linux | os=freebsd | os=android) & cpu=arm languageName: node linkType: hard -"@sentry/cli-linux-i686@npm:2.42.2": - version: 2.42.2 - resolution: "@sentry/cli-linux-i686@npm:2.42.2" - conditions: (os=linux | os=freebsd) & (cpu=x86 | cpu=ia32) +"@sentry/cli-linux-i686@npm:2.58.4": + version: 2.58.4 + resolution: "@sentry/cli-linux-i686@npm:2.58.4" + conditions: (os=linux | os=freebsd | os=android) & (cpu=x86 | cpu=ia32) languageName: node linkType: hard -"@sentry/cli-linux-x64@npm:2.42.2": - version: 2.42.2 - resolution: "@sentry/cli-linux-x64@npm:2.42.2" - conditions: (os=linux | os=freebsd) & cpu=x64 +"@sentry/cli-linux-x64@npm:2.58.4": + version: 2.58.4 + resolution: "@sentry/cli-linux-x64@npm:2.58.4" + conditions: (os=linux | os=freebsd | os=android) & cpu=x64 languageName: node linkType: hard -"@sentry/cli-win32-i686@npm:2.42.2": - version: 2.42.2 - resolution: "@sentry/cli-win32-i686@npm:2.42.2" +"@sentry/cli-win32-arm64@npm:2.58.4": + version: 2.58.4 + resolution: "@sentry/cli-win32-arm64@npm:2.58.4" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@sentry/cli-win32-i686@npm:2.58.4": + version: 2.58.4 + resolution: "@sentry/cli-win32-i686@npm:2.58.4" conditions: os=win32 & (cpu=x86 | cpu=ia32) languageName: node linkType: hard -"@sentry/cli-win32-x64@npm:2.42.2": - version: 2.42.2 - resolution: "@sentry/cli-win32-x64@npm:2.42.2" +"@sentry/cli-win32-x64@npm:2.58.4": + version: 2.58.4 + resolution: "@sentry/cli-win32-x64@npm:2.58.4" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@sentry/cli@npm:2.42.2": - version: 2.42.2 - resolution: "@sentry/cli@npm:2.42.2" +"@sentry/cli@npm:^2.49.0": + version: 2.58.4 + resolution: "@sentry/cli@npm:2.58.4" dependencies: - "@sentry/cli-darwin": "npm:2.42.2" - "@sentry/cli-linux-arm": "npm:2.42.2" - "@sentry/cli-linux-arm64": "npm:2.42.2" - "@sentry/cli-linux-i686": "npm:2.42.2" - "@sentry/cli-linux-x64": "npm:2.42.2" - "@sentry/cli-win32-i686": "npm:2.42.2" - "@sentry/cli-win32-x64": "npm:2.42.2" + "@sentry/cli-darwin": "npm:2.58.4" + "@sentry/cli-linux-arm": "npm:2.58.4" + "@sentry/cli-linux-arm64": "npm:2.58.4" + "@sentry/cli-linux-i686": "npm:2.58.4" + "@sentry/cli-linux-x64": "npm:2.58.4" + "@sentry/cli-win32-arm64": "npm:2.58.4" + "@sentry/cli-win32-i686": "npm:2.58.4" + "@sentry/cli-win32-x64": "npm:2.58.4" https-proxy-agent: "npm:^5.0.0" node-fetch: "npm:^2.6.7" progress: "npm:^2.0.3" @@ -14255,20 +14180,22 @@ __metadata: optional: true "@sentry/cli-linux-x64": optional: true + "@sentry/cli-win32-arm64": + optional: true "@sentry/cli-win32-i686": optional: true "@sentry/cli-win32-x64": optional: true bin: sentry-cli: bin/sentry-cli - checksum: 10/f92800b54a88c2f9f1d7a2604bb6b4a17c429a836871df2b966595af7d382bdac490586b19e5ab81d278d38d498207fcdbd94ceba0a50acec2ab77f1a6a4f3af + checksum: 10/cf5df020ed96c05e74f742c80b4c5f16750ee82a6b6f3619c0edb964c5b02b4dfca5688eb8a5278274fce38f9cd2b770e20d9365253bb86e9f24ceb309f9f6eb languageName: node linkType: hard -"@sentry/core@npm:10.17.0": - version: 10.17.0 - resolution: "@sentry/core@npm:10.17.0" - checksum: 10/556eaedf23402fe9f9b8a8cd577d61e71b42b9ef3e72bc22ee70705fc0050d1bf8a08dff44434f8a2ad1415e177b2e09d12f4d7ef51e2553c067ff805b3030e2 +"@sentry/core@npm:10.29.0": + version: 10.29.0 + resolution: "@sentry/core@npm:10.29.0" + checksum: 10/aa7a233a8c6327c5f60321cd65e2704e46945f5140da55e6016d949ad97529e362b3e2885c16e5defc53e5de0f870074aa94defd8376e50931bdf677c73c66fa languageName: node linkType: hard @@ -14280,106 +14207,107 @@ __metadata: linkType: hard "@sentry/electron@npm:^7.0.0": - version: 7.2.0 - resolution: "@sentry/electron@npm:7.2.0" + version: 7.5.0 + resolution: "@sentry/electron@npm:7.5.0" dependencies: - "@sentry/browser": "npm:10.17.0" - "@sentry/core": "npm:10.17.0" - "@sentry/node": "npm:10.17.0" + "@sentry/browser": "npm:10.29.0" + "@sentry/core": "npm:10.29.0" + "@sentry/node": "npm:10.29.0" peerDependencies: - "@sentry/node-native": 10.17.0 + "@sentry/node-native": 10.29.0 peerDependenciesMeta: "@sentry/node-native": optional: true - checksum: 10/0d95dff9cf2d3f86bc1244720a00cc06ae05e63fd82b310964333fe2cb0c1686a23207f11fb7f27df336b630495fc50ef276d09116c5f00a319c241d8110d2bd + checksum: 10/b5d5a512fa6dd748a393b8574b6d975a6b0e213c0081aa2e7c1305e9fb55221b1af956c88617cb2fc7b097c560ccbedf2903c8d7cd40bafc49294939a70ec85a languageName: node linkType: hard "@sentry/esbuild-plugin@npm:^3.0.0": - version: 3.5.0 - resolution: "@sentry/esbuild-plugin@npm:3.5.0" + version: 3.6.1 + resolution: "@sentry/esbuild-plugin@npm:3.6.1" dependencies: - "@sentry/bundler-plugin-core": "npm:3.5.0" + "@sentry/bundler-plugin-core": "npm:3.6.1" unplugin: "npm:1.0.1" uuid: "npm:^9.0.0" - checksum: 10/66a1f1a88d7c8a4ff79d7207d4a42998f8db7edc60138be2966bd54113eea6dc2979e7969c3262fea9c13597e40c988615c42c4149f3a6911b168d02a090d952 + checksum: 10/b2b64e404d13589d98ff5ebc57a7e0a7cb9c3bada8aab20f9eb1beefb375deabf61658c96659f3a4f2a5dbd69e44ddf69fd1b5a17f31830a97ddbee8f589473a languageName: node linkType: hard -"@sentry/node-core@npm:10.17.0": - version: 10.17.0 - resolution: "@sentry/node-core@npm:10.17.0" +"@sentry/node-core@npm:10.29.0": + version: 10.29.0 + resolution: "@sentry/node-core@npm:10.29.0" dependencies: - "@sentry/core": "npm:10.17.0" - "@sentry/opentelemetry": "npm:10.17.0" - import-in-the-middle: "npm:^1.14.2" + "@apm-js-collab/tracing-hooks": "npm:^0.3.1" + "@sentry/core": "npm:10.29.0" + "@sentry/opentelemetry": "npm:10.29.0" + import-in-the-middle: "npm:^2" peerDependencies: "@opentelemetry/api": ^1.9.0 - "@opentelemetry/context-async-hooks": ^1.30.1 || ^2.1.0 - "@opentelemetry/core": ^1.30.1 || ^2.1.0 + "@opentelemetry/context-async-hooks": ^1.30.1 || ^2.1.0 || ^2.2.0 + "@opentelemetry/core": ^1.30.1 || ^2.1.0 || ^2.2.0 "@opentelemetry/instrumentation": ">=0.57.1 <1" - "@opentelemetry/resources": ^1.30.1 || ^2.1.0 - "@opentelemetry/sdk-trace-base": ^1.30.1 || ^2.1.0 + "@opentelemetry/resources": ^1.30.1 || ^2.1.0 || ^2.2.0 + "@opentelemetry/sdk-trace-base": ^1.30.1 || ^2.1.0 || ^2.2.0 "@opentelemetry/semantic-conventions": ^1.37.0 - checksum: 10/9881e8803ce74c4759692b89b058049b8c024b9486fb989fc90da5d0c8a4e6e49924681b80f013c7d4dc3e49c30158f965c5d4092c228b91a8bc316192e6f278 + checksum: 10/5a5c307b5719f50fdbbc7e0f9c38c37814de1ec8552786cdf8d9ae302c8b3fa2f7521550958d49514035ee0ab293b4dd729c3eb371e730fde5e93aea379ce10f languageName: node linkType: hard -"@sentry/node@npm:10.17.0": - version: 10.17.0 - resolution: "@sentry/node@npm:10.17.0" +"@sentry/node@npm:10.29.0": + version: 10.29.0 + resolution: "@sentry/node@npm:10.29.0" dependencies: "@opentelemetry/api": "npm:^1.9.0" - "@opentelemetry/context-async-hooks": "npm:^2.1.0" - "@opentelemetry/core": "npm:^2.1.0" - "@opentelemetry/instrumentation": "npm:^0.204.0" - "@opentelemetry/instrumentation-amqplib": "npm:0.51.0" - "@opentelemetry/instrumentation-connect": "npm:0.48.0" - "@opentelemetry/instrumentation-dataloader": "npm:0.22.0" - "@opentelemetry/instrumentation-express": "npm:0.53.0" - "@opentelemetry/instrumentation-fs": "npm:0.24.0" - "@opentelemetry/instrumentation-generic-pool": "npm:0.48.0" - "@opentelemetry/instrumentation-graphql": "npm:0.52.0" - "@opentelemetry/instrumentation-hapi": "npm:0.51.0" - "@opentelemetry/instrumentation-http": "npm:0.204.0" - "@opentelemetry/instrumentation-ioredis": "npm:0.52.0" - "@opentelemetry/instrumentation-kafkajs": "npm:0.14.0" - "@opentelemetry/instrumentation-knex": "npm:0.49.0" - "@opentelemetry/instrumentation-koa": "npm:0.52.0" - "@opentelemetry/instrumentation-lru-memoizer": "npm:0.49.0" - "@opentelemetry/instrumentation-mongodb": "npm:0.57.0" - "@opentelemetry/instrumentation-mongoose": "npm:0.51.0" - "@opentelemetry/instrumentation-mysql": "npm:0.50.0" - "@opentelemetry/instrumentation-mysql2": "npm:0.51.0" - "@opentelemetry/instrumentation-pg": "npm:0.57.0" - "@opentelemetry/instrumentation-redis": "npm:0.53.0" - "@opentelemetry/instrumentation-tedious": "npm:0.23.0" - "@opentelemetry/instrumentation-undici": "npm:0.15.0" - "@opentelemetry/resources": "npm:^2.1.0" - "@opentelemetry/sdk-trace-base": "npm:^2.1.0" + "@opentelemetry/context-async-hooks": "npm:^2.2.0" + "@opentelemetry/core": "npm:^2.2.0" + "@opentelemetry/instrumentation": "npm:^0.208.0" + "@opentelemetry/instrumentation-amqplib": "npm:0.55.0" + "@opentelemetry/instrumentation-connect": "npm:0.52.0" + "@opentelemetry/instrumentation-dataloader": "npm:0.26.0" + "@opentelemetry/instrumentation-express": "npm:0.57.0" + "@opentelemetry/instrumentation-fs": "npm:0.28.0" + "@opentelemetry/instrumentation-generic-pool": "npm:0.52.0" + "@opentelemetry/instrumentation-graphql": "npm:0.56.0" + "@opentelemetry/instrumentation-hapi": "npm:0.55.0" + "@opentelemetry/instrumentation-http": "npm:0.208.0" + "@opentelemetry/instrumentation-ioredis": "npm:0.56.0" + "@opentelemetry/instrumentation-kafkajs": "npm:0.18.0" + "@opentelemetry/instrumentation-knex": "npm:0.53.0" + "@opentelemetry/instrumentation-koa": "npm:0.57.0" + "@opentelemetry/instrumentation-lru-memoizer": "npm:0.53.0" + "@opentelemetry/instrumentation-mongodb": "npm:0.61.0" + "@opentelemetry/instrumentation-mongoose": "npm:0.55.0" + "@opentelemetry/instrumentation-mysql": "npm:0.54.0" + "@opentelemetry/instrumentation-mysql2": "npm:0.55.0" + "@opentelemetry/instrumentation-pg": "npm:0.61.0" + "@opentelemetry/instrumentation-redis": "npm:0.57.0" + "@opentelemetry/instrumentation-tedious": "npm:0.27.0" + "@opentelemetry/instrumentation-undici": "npm:0.19.0" + "@opentelemetry/resources": "npm:^2.2.0" + "@opentelemetry/sdk-trace-base": "npm:^2.2.0" "@opentelemetry/semantic-conventions": "npm:^1.37.0" - "@prisma/instrumentation": "npm:6.15.0" - "@sentry/core": "npm:10.17.0" - "@sentry/node-core": "npm:10.17.0" - "@sentry/opentelemetry": "npm:10.17.0" - import-in-the-middle: "npm:^1.14.2" + "@prisma/instrumentation": "npm:6.19.0" + "@sentry/core": "npm:10.29.0" + "@sentry/node-core": "npm:10.29.0" + "@sentry/opentelemetry": "npm:10.29.0" + import-in-the-middle: "npm:^2" minimatch: "npm:^9.0.0" - checksum: 10/085078c391f2c3bbedfb63f85c70ac46e155d3ff21c74b6a58f1c82418655369d6970782d924c2e914fed80c3cd587f2e88545f99b23ecf15498c6256b5b4f0d + checksum: 10/76fb7e83a16db29b37b7520800053ffe3418993fc58ff43e178477eae1c247a8a3968151534289bb764a81fa64b3d6d2c1f1cb10b59d240691e04316bbc95556 languageName: node linkType: hard -"@sentry/opentelemetry@npm:10.17.0": - version: 10.17.0 - resolution: "@sentry/opentelemetry@npm:10.17.0" +"@sentry/opentelemetry@npm:10.29.0": + version: 10.29.0 + resolution: "@sentry/opentelemetry@npm:10.29.0" dependencies: - "@sentry/core": "npm:10.17.0" + "@sentry/core": "npm:10.29.0" peerDependencies: "@opentelemetry/api": ^1.9.0 - "@opentelemetry/context-async-hooks": ^1.30.1 || ^2.1.0 - "@opentelemetry/core": ^1.30.1 || ^2.1.0 - "@opentelemetry/sdk-trace-base": ^1.30.1 || ^2.1.0 + "@opentelemetry/context-async-hooks": ^1.30.1 || ^2.1.0 || ^2.2.0 + "@opentelemetry/core": ^1.30.1 || ^2.1.0 || ^2.2.0 + "@opentelemetry/sdk-trace-base": ^1.30.1 || ^2.1.0 || ^2.2.0 "@opentelemetry/semantic-conventions": ^1.37.0 - checksum: 10/8a5b575612cbe342f4267beca0b7793e0008373ee61bcd89b1723029f612a6936f3d248199868af002167d6f7d3cc35c0b263609273a6103721f3f5602be0286 + checksum: 10/5937847cbca24980b2f14557bc21bae9f69561b4668897843456026564047d9f07b49f20efcea0b229355a4ffc5f0c1eaa379cfd188b627020a22d121b8e4c65 languageName: node linkType: hard @@ -14397,15 +14325,15 @@ __metadata: linkType: hard "@sentry/webpack-plugin@npm:^3.0.0": - version: 3.5.0 - resolution: "@sentry/webpack-plugin@npm:3.5.0" + version: 3.6.1 + resolution: "@sentry/webpack-plugin@npm:3.6.1" dependencies: - "@sentry/bundler-plugin-core": "npm:3.5.0" + "@sentry/bundler-plugin-core": "npm:3.6.1" unplugin: "npm:1.0.1" uuid: "npm:^9.0.0" peerDependencies: webpack: ">=4.40.0" - checksum: 10/adde407b5ba5215e33fcf58bcf62b3df79f1e55345579ed0b846b4a3a24134d347a35b099d1f1a8b2f6e1ad1af3cfdb674c3a132ee345aa95b5c9ff26fa56826 + checksum: 10/2d3fbb2688398f58e859d36058a88b3668583741fc3be036ba5f09dc6ad9703ddcad486ba4f76e5fa257a8473dc66d7f0e47407e60d6b4943f8952615752d3fd languageName: node linkType: hard @@ -17002,14 +16930,14 @@ __metadata: languageName: node linkType: hard -"@types/pg@npm:*, @types/pg@npm:8.15.5": - version: 8.15.5 - resolution: "@types/pg@npm:8.15.5" +"@types/pg@npm:*, @types/pg@npm:8.15.6": + version: 8.15.6 + resolution: "@types/pg@npm:8.15.6" dependencies: "@types/node": "npm:*" pg-protocol: "npm:*" pg-types: "npm:^2.2.0" - checksum: 10/31bae283d044cd87a62fcbf1dc4d5a431783f4b19cad769bf5ad23794ccff969ae6fe7915c352a7b78a7159bc8b945560a5f76e74091e283c089618d1aacfff5 + checksum: 10/4bc1bb274e0fc105be93e3a9cc8c9aa57fc50b78ed78a56348468157332daaecd71fcab762ee620c766510ffbc7018b56ca394787d6d41ff1726b152770aa532 languageName: node linkType: hard @@ -17138,13 +17066,6 @@ __metadata: languageName: node linkType: hard -"@types/shimmer@npm:^1.2.0": - version: 1.2.0 - resolution: "@types/shimmer@npm:1.2.0" - checksum: 10/f081a31d826ce7bfe8cc7ba8129d2b1dffae44fd580eba4fcf741237646c4c2494ae6de2cada4b7713d138f35f4bc512dbf01311d813dee82020f97d7d8c491c - languageName: node - linkType: hard - "@types/sinon@npm:^17.0.3": version: 17.0.4 resolution: "@types/sinon@npm:17.0.4" @@ -25745,19 +25666,7 @@ __metadata: languageName: node linkType: hard -"import-in-the-middle@npm:^1.14.2, import-in-the-middle@npm:^1.8.1": - version: 1.15.0 - resolution: "import-in-the-middle@npm:1.15.0" - dependencies: - acorn: "npm:^8.14.0" - acorn-import-attributes: "npm:^1.9.5" - cjs-module-lexer: "npm:^1.2.2" - module-details-from-path: "npm:^1.0.3" - checksum: 10/a1ff65ea557ffe67e63dd67b411255fedd8622b324ff22ba99f4436b8fcf74da0333e62b4e8142f447e5db64a42ec9e65f926d50fa55e89c4e4d64626d8cf5f8 - languageName: node - linkType: hard - -"import-in-the-middle@npm:^2.0.0": +"import-in-the-middle@npm:^2, import-in-the-middle@npm:^2.0.0": version: 2.0.0 resolution: "import-in-the-middle@npm:2.0.0" dependencies: @@ -26702,7 +26611,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:4.1.0, js-yaml@npm:^4.0.0, js-yaml@npm:^4.1.0": +"js-yaml@npm:4.1.0": version: 4.1.0 resolution: "js-yaml@npm:4.1.0" dependencies: @@ -26714,14 +26623,25 @@ __metadata: linkType: hard "js-yaml@npm:^3.13.1, js-yaml@npm:^3.14.1": - version: 3.14.1 - resolution: "js-yaml@npm:3.14.1" + version: 3.14.2 + resolution: "js-yaml@npm:3.14.2" dependencies: argparse: "npm:^1.0.7" esprima: "npm:^4.0.0" bin: js-yaml: bin/js-yaml.js - checksum: 10/9e22d80b4d0105b9899135365f746d47466ed53ef4223c529b3c0f7a39907743fdbd3c4379f94f1106f02755b5e90b2faaf84801a891135544e1ea475d1a1379 + checksum: 10/172e0b6007b0bf0fc8d2469c94424f7dd765c64a047d2b790831fecef2204a4054eabf4d911eb73ab8c9a3256ab8ba1ee8d655b789bf24bf059c772acc2075a1 + languageName: node + linkType: hard + +"js-yaml@npm:^4.0.0, js-yaml@npm:^4.1.0": + version: 4.1.1 + resolution: "js-yaml@npm:4.1.1" + dependencies: + argparse: "npm:^2.0.1" + bin: + js-yaml: bin/js-yaml.js + checksum: 10/a52d0519f0f4ef5b4adc1cde466cb54c50d56e2b4a983b9d5c9c0f2f99462047007a6274d7e95617a21d3c91fde3ee6115536ed70991cd645ba8521058b78f77 languageName: node linkType: hard @@ -29244,7 +29164,7 @@ __metadata: languageName: node linkType: hard -"module-details-from-path@npm:^1.0.3": +"module-details-from-path@npm:^1.0.3, module-details-from-path@npm:^1.0.4": version: 1.0.4 resolution: "module-details-from-path@npm:1.0.4" checksum: 10/2ebfada5358492f6ab496b70f70a1042f2ee7a4c79d29467f59ed6704f741fb4461d7cecb5082144ed39a05fec4d19e9ff38b731c76228151be97227240a05b2 @@ -29599,18 +29519,18 @@ __metadata: linkType: hard "next@npm:^15.3.1": - version: 15.5.4 - resolution: "next@npm:15.5.4" - dependencies: - "@next/env": "npm:15.5.4" - "@next/swc-darwin-arm64": "npm:15.5.4" - "@next/swc-darwin-x64": "npm:15.5.4" - "@next/swc-linux-arm64-gnu": "npm:15.5.4" - "@next/swc-linux-arm64-musl": "npm:15.5.4" - "@next/swc-linux-x64-gnu": "npm:15.5.4" - "@next/swc-linux-x64-musl": "npm:15.5.4" - "@next/swc-win32-arm64-msvc": "npm:15.5.4" - "@next/swc-win32-x64-msvc": "npm:15.5.4" + version: 15.5.9 + resolution: "next@npm:15.5.9" + dependencies: + "@next/env": "npm:15.5.9" + "@next/swc-darwin-arm64": "npm:15.5.7" + "@next/swc-darwin-x64": "npm:15.5.7" + "@next/swc-linux-arm64-gnu": "npm:15.5.7" + "@next/swc-linux-arm64-musl": "npm:15.5.7" + "@next/swc-linux-x64-gnu": "npm:15.5.7" + "@next/swc-linux-x64-musl": "npm:15.5.7" + "@next/swc-win32-arm64-msvc": "npm:15.5.7" + "@next/swc-win32-x64-msvc": "npm:15.5.7" "@swc/helpers": "npm:0.5.15" caniuse-lite: "npm:^1.0.30001579" postcss: "npm:8.4.31" @@ -29653,7 +29573,7 @@ __metadata: optional: true bin: next: dist/bin/next - checksum: 10/6cc933dd0e829e122e0232776b6fda5d9ff1a3c9c5f423163bb5962c524c479e0ad21cd8a6556ac028a17758b56e3988af4e11c5792ad986c973a370e42d1689 + checksum: 10/ac27b82de08c9720e8e99cd64102af5e30306a8c861630d50da347a02c288cc441273ada64875c47e7a4d5991ff69cc92b23e906ce3271f9835a7911a0f060cc languageName: node linkType: hard @@ -32673,17 +32593,6 @@ __metadata: languageName: node linkType: hard -"require-in-the-middle@npm:^7.1.1": - version: 7.5.2 - resolution: "require-in-the-middle@npm:7.5.2" - dependencies: - debug: "npm:^4.3.5" - module-details-from-path: "npm:^1.0.3" - resolve: "npm:^1.22.8" - checksum: 10/d8f137d72eec1c53987647d19cd3bd2c64d5417bcd06b9ac8f7a14e83924c1e7636e327df7d96066a2b446b41f50d0bc1856a521388d5e90ba5c3b18dd5ab4e8 - languageName: node - linkType: hard - "require-in-the-middle@npm:^8.0.0": version: 8.0.1 resolution: "require-in-the-middle@npm:8.0.1" @@ -33291,7 +33200,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.3, semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.2, semver@npm:^7.7.3": +"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.3, semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.2, semver@npm:^7.7.3": version: 7.7.3 resolution: "semver@npm:7.7.3" bin: @@ -33734,13 +33643,6 @@ __metadata: languageName: node linkType: hard -"shimmer@npm:^1.2.1": - version: 1.2.1 - resolution: "shimmer@npm:1.2.1" - checksum: 10/aa0d6252ad1c682a4fdfda69e541be987f7a265ac7b00b1208e5e48cc68dc55f293955346ea4c71a169b7324b82c70f8400b3d3d2d60b2a7519f0a3522423250 - languageName: node - linkType: hard - "side-channel@npm:@nolyfill/side-channel@^1": version: 1.0.44 resolution: "@nolyfill/side-channel@npm:1.0.44" From 4717886c9e0c46bb750b98d144cf8aa092cdaa81 Mon Sep 17 00:00:00 2001 From: DarkSky <25152247+darkskygit@users.noreply.github.com> Date: Sun, 14 Dec 2025 01:00:01 +0800 Subject: [PATCH 07/11] fix: title icon display (#14101) fix #14073 --- .../src/ui/icon-name-editor/icon-name-editor.css.ts | 5 +++++ .../component/src/ui/icon-name-editor/icon-name-editor.tsx | 1 + .../src/ui/icon-picker/picker/emoji/emoji-button.tsx | 1 + 3 files changed, 7 insertions(+) diff --git a/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.css.ts b/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.css.ts index fdc18aa977291..8bcbb0f1505c9 100644 --- a/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.css.ts +++ b/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.css.ts @@ -18,6 +18,11 @@ export const iconPicker = style({ lineHeight: 1, color: cssVarV2.icon.primary, }); + +export const iconContent = style({ + display: 'contents', +}); + globalStyle(`${iconPicker} span:has(svg)`, { lineHeight: 0, }); diff --git a/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.tsx b/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.tsx index 6f0a9d31fb491..91f7ca230f29a 100644 --- a/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.tsx +++ b/packages/frontend/component/src/ui/icon-name-editor/icon-name-editor.tsx @@ -85,6 +85,7 @@ export const IconEditor = ({ data-icon-type={icon?.type} aria-label={icon ? 'Change Icon' : 'Select Icon'} title={icon ? 'Change Icon' : 'Select Icon'} + contentClassName={styles.iconContent} > diff --git a/packages/frontend/component/src/ui/icon-picker/picker/emoji/emoji-button.tsx b/packages/frontend/component/src/ui/icon-picker/picker/emoji/emoji-button.tsx index 6a93b897225c5..0080cd22cda30 100644 --- a/packages/frontend/component/src/ui/icon-picker/picker/emoji/emoji-button.tsx +++ b/packages/frontend/component/src/ui/icon-picker/picker/emoji/emoji-button.tsx @@ -20,6 +20,7 @@ export const EmojiButton = memo(function EmojiButton({ size={24} style={{ padding: 4 }} icon={{emoji}} + iconStyle={{ justifyContent: 'center' }} onClick={handleClick} /> ); From f5076a37aed404142d1eb7dfe1eff20fc35cb1e5 Mon Sep 17 00:00:00 2001 From: Daniel Dybing Date: Mon, 15 Dec 2025 08:32:00 +0100 Subject: [PATCH 08/11] feat(core): find todo list based on 'checkbox' search query (#13982) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This feature enhances the /slash command by allowing users to search for 'checkbox' and have the to-do list item show up as a result. Users come from different systems and environments, and some may use the name 'checkbox' but be confused as they cannot find it in the search menu. This is achieved by adding a `searchAlias` property on the to-do list item block that contains the string `checkbox`. ## Summary by CodeRabbit * **New Features** * Added search-alias support for slash menu items so entries can be found by alternative terms. * To-do List entry now includes "checkbox" as an additional searchable alias to improve discoverability. * Slash menu search results updated to reflect alias-driven matches (additional item appears when searching). ✏️ Tip: You can customize this high-level summary in your review settings. --------- Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com> --- blocksuite/affine/blocks/note/src/configs/slash-menu.ts | 3 ++- blocksuite/affine/rich-text/src/conversion.ts | 2 ++ tests/blocksuite/e2e/slash-menu.spec.ts | 6 +++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/blocksuite/affine/blocks/note/src/configs/slash-menu.ts b/blocksuite/affine/blocks/note/src/configs/slash-menu.ts index 3c41811cbe942..8efc0b2998cd0 100644 --- a/blocksuite/affine/blocks/note/src/configs/slash-menu.ts +++ b/blocksuite/affine/blocks/note/src/configs/slash-menu.ts @@ -82,12 +82,13 @@ function createConversionItem( config: TextConversionConfig, group?: SlashMenuItem['group'] ): SlashMenuActionItem { - const { name, description, icon, flavour, type } = config; + const { name, description, icon, flavour, type, searchAlias = [] } = config; return { name, group, description, icon, + searchAlias, tooltip: tooltips[name], when: ({ model }) => model.store.schema.flavourSchemaMap.has(flavour), action: ({ std }) => { diff --git a/blocksuite/affine/rich-text/src/conversion.ts b/blocksuite/affine/rich-text/src/conversion.ts index 61612c8b5ed8a..a48250bea8104 100644 --- a/blocksuite/affine/rich-text/src/conversion.ts +++ b/blocksuite/affine/rich-text/src/conversion.ts @@ -26,6 +26,7 @@ export interface TextConversionConfig { description?: string; hotkey: string[] | null; icon: TemplateResult<1>; + searchAlias?: string[]; } export const textConversionConfigs: TextConversionConfig[] = [ @@ -106,6 +107,7 @@ export const textConversionConfigs: TextConversionConfig[] = [ type: 'todo', name: 'To-do List', description: 'Add tasks to a to-do list.', + searchAlias: ['checkbox'], hotkey: null, icon: CheckBoxIcon, }, diff --git a/tests/blocksuite/e2e/slash-menu.spec.ts b/tests/blocksuite/e2e/slash-menu.spec.ts index e7c38748af6bf..b23f7f1322773 100644 --- a/tests/blocksuite/e2e/slash-menu.spec.ts +++ b/tests/blocksuite/e2e/slash-menu.spec.ts @@ -577,7 +577,7 @@ test.describe('slash search', () => { // search should active the first item await type(page, 'co'); - await expect(slashItems).toHaveCount(4); + await expect(slashItems).toHaveCount(5); await expect(slashItems.nth(0).locator('.text')).toHaveText(['Copy']); await expect(slashItems.nth(1).locator('.text')).toHaveText(['Code Block']); await expect(slashItems.nth(2).locator('.text')).toHaveText(['Callout']); @@ -589,7 +589,7 @@ test.describe('slash search', () => { // assert backspace works await pressBackspace(page); - await expect(slashItems).toHaveCount(4); + await expect(slashItems).toHaveCount(5); await expect(slashItems.nth(0).locator('.text')).toHaveText(['Copy']); await expect(slashItems.nth(1).locator('.text')).toHaveText(['Code Block']); await expect(slashItems.nth(2).locator('.text')).toHaveText(['Callout']); @@ -608,7 +608,7 @@ test.describe('slash search', () => { await expect(slashMenu).toBeVisible(); await type(page, 'c'); - await expect(slashItems).toHaveCount(10); + await expect(slashItems).toHaveCount(11); await expect(slashItems.nth(0).locator('.text')).toHaveText(['Copy']); await expect(slashItems.nth(1).locator('.text')).toHaveText(['Italic']); await expect(slashItems.nth(2).locator('.text')).toHaveText(['Callout']); From 66407f2b2fe08f7b1da761f8cba716fdce3fe582 Mon Sep 17 00:00:00 2001 From: Daniel Dybing Date: Tue, 16 Dec 2025 03:55:34 +0100 Subject: [PATCH 09/11] feat(core): adapt date fields in database for notion import (#14111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is related to issue/feature request https://github.com/toeverything/AFFiNE/issues/13962. This PR extends the Notion import functionality to properly handle date fields from databases. Previously, these were imported as text (see photo below), which served little purpose. These Notion date fields are now parsed as actual dates, and imported to AFFiNE as epoch time (which is what the date field in AFFiNe expects). Because of this, even date fields with time (e.g. 09:00 AM) are also handled correctly - although they are only shown as dates, since AFFiNE's `Date` field does not support time. Tested with several Notion imports both with and without time, and they all seem to work correctly. Affected files: - blocksuite/affine/blocks/database/src/adapters/notion-html.ts Old: image New: image ## Summary by CodeRabbit * **New Features** * Enhanced Notion imports with automatic date column detection. When importing Notion databases, date fields are now automatically recognized, properly configured as date columns, and formatted correctly. This improvement ensures accurate data preservation, eliminates manual type corrections, and provides a streamlined import experience for all users working with date-rich Notion databases. ✏️ Tip: You can customize this high-level summary in your review settings. --- .../database/src/adapters/notion-html.ts | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/blocksuite/affine/blocks/database/src/adapters/notion-html.ts b/blocksuite/affine/blocks/database/src/adapters/notion-html.ts index a8b7fcb41ae9e..12a39f1393514 100644 --- a/blocksuite/affine/blocks/database/src/adapters/notion-html.ts +++ b/blocksuite/affine/blocks/database/src/adapters/notion-html.ts @@ -15,6 +15,7 @@ const ColumnClassMap: Record = { typesCheckbox: 'checkbox', typesText: 'rich-text', typesTitle: 'title', + typesDate: 'date', }; const NotionDatabaseToken = '.collection-content'; @@ -165,7 +166,36 @@ export const databaseBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatche if (!column) { return; } - if (HastUtils.querySelector(child, '.selected-value')) { + + // Check for