diff --git a/.vscode/launch.template.json b/.vscode/launch.template.json index ec18a9d4cc18..39508d69b862 100644 --- a/.vscode/launch.template.json +++ b/.vscode/launch.template.json @@ -19,7 +19,7 @@ "request": "launch", "name": "Debug Playground", "skipFiles": ["/**", "**/node_modules/**"], - "url": "http://localhost:5173/starter/?init" + "url": "http://localhost:8001/starter/?init" } ] } diff --git a/packages/affine/block-paragraph/src/paragraph-block.ts b/packages/affine/block-paragraph/src/paragraph-block.ts index ca4178f30a2c..3eabf389f16e 100644 --- a/packages/affine/block-paragraph/src/paragraph-block.ts +++ b/packages/affine/block-paragraph/src/paragraph-block.ts @@ -53,6 +53,17 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent< return false; }; + private _isInMicrosheet = () => { + let parent = this.parentElement; + while (parent && parent !== document.body) { + if (parent.tagName.toLowerCase() === 'affine-microsheet') { + return true; + } + parent = parent.parentElement; + } + return false; + }; + get attributeRenderer() { return this.inlineManager.getRenderer(); } @@ -132,7 +143,8 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent< .then(() => { if ( (this.inlineEditor?.yTextLength ?? 0) > 0 || - this._isInDatabase() + this._isInDatabase() || + this._isInMicrosheet() ) { this._displayPlaceholder.value = false; return; diff --git a/packages/affine/microsheet-data-view/CHANGELOG.md b/packages/affine/microsheet-data-view/CHANGELOG.md new file mode 100644 index 000000000000..f0254fcccfdc --- /dev/null +++ b/packages/affine/microsheet-data-view/CHANGELOG.md @@ -0,0 +1,417 @@ +# @blocksuite/data-view + +## 0.17.19 + +### Patch Changes + +- b69b00e: --- + + '@blocksuite/affine-block-list': patch + '@blocksuite/affine-block-paragraph': patch + '@blocksuite/affine-block-surface': patch + '@blocksuite/affine-components': patch + '@blocksuite/data-view': patch + '@blocksuite/affine-model': patch + '@blocksuite/affine-shared': patch + '@blocksuite/blocks': patch + '@blocksuite/docs': patch + '@blocksuite/block-std': patch + '@blocksuite/global': patch + '@blocksuite/inline': patch + '@blocksuite/store': patch + '@blocksuite/sync': patch + '@blocksuite/presets': patch + + *** + + [feat: markdown adapter with latex](https://github.com/toeverything/blocksuite/pull/8503) + + [feat: support notion block equation html import](https://github.com/toeverything/blocksuite/pull/8504) + + [feat: support edgeless tidy up](https://github.com/toeverything/blocksuite/pull/8516) + + [feat: support notion callout block to blocksuite quote block](https://github.com/toeverything/blocksuite/pull/8523) + + [feat(playground): add import notion zip entry](https://github.com/toeverything/blocksuite/pull/8527) + + [fix(blocks): auto focus latex block](https://github.com/toeverything/blocksuite/pull/8505) + + [fix: enhance button layout with icon alignment](https://github.com/toeverything/blocksuite/pull/8508) + + [fix(edgeless): ime will crash edgeless text width](https://github.com/toeverything/blocksuite/pull/8506) + + [fix(edgeless): edgeless text is deleted when first block is empty](https://github.com/toeverything/blocksuite/pull/8512) + + [fix: notion html quote block import](https://github.com/toeverything/blocksuite/pull/8515) + + [fix: yjs warning](https://github.com/toeverything/blocksuite/pull/8519) + + [fix(blocks): real nested list on html export](https://github.com/toeverything/blocksuite/pull/8511) + + [fix(edgeless): cmd a will select element inner frame](https://github.com/toeverything/blocksuite/pull/8517) + + [fix(edgeless): disable contenteditable when edgeless text not in editing state](https://github.com/toeverything/blocksuite/pull/8525) + + [fix: import notion toggle list as toggle bulleted list](https://github.com/toeverything/blocksuite/pull/8528) + + [refactor(microsheet): signals version datasource api](https://github.com/toeverything/blocksuite/pull/8513) + + [refactor(edgeless): element tree manager](https://github.com/toeverything/blocksuite/pull/8239) + + [refactor(blocks): simplify frame manager implementation](https://github.com/toeverything/blocksuite/pull/8507) + + [refactor: update group test utils using container interface](https://github.com/toeverything/blocksuite/pull/8518) + + [refactor: update frame test with container test uitls](https://github.com/toeverything/blocksuite/pull/8520) + + [refactor(microsheet): context-menu ui and ux](https://github.com/toeverything/blocksuite/pull/8467) + + [refactor: move chat block to affine](https://github.com/toeverything/blocksuite/pull/8420) + + [perf: optimize snapshot job handling](https://github.com/toeverything/blocksuite/pull/8428) + + [perf(edgeless): disable shape shadow blur](https://github.com/toeverything/blocksuite/pull/8532) + + [chore: bump up all non-major dependencies](https://github.com/toeverything/blocksuite/pull/8514) + + [chore: Lock file maintenance](https://github.com/toeverything/blocksuite/pull/8510) + + [docs: fix table structure warning](https://github.com/toeverything/blocksuite/pull/8509) + + [docs: edgeless data structure desc](https://github.com/toeverything/blocksuite/pull/8531) + + [docs: update link](https://github.com/toeverything/blocksuite/pull/8533) + +- Updated dependencies [b69b00e] + - @blocksuite/affine-components@0.17.19 + - @blocksuite/affine-shared@0.17.19 + - @blocksuite/block-std@0.17.19 + - @blocksuite/global@0.17.19 + - @blocksuite/store@0.17.19 + +## 0.17.18 + +### Patch Changes + +- 9f70715: Bug Fixes: + + - fix(blocks): can not search in at menu with IME. [#8481](https://github.com/toeverything/blocksuite/pull/8481) + - fix(std): dispatcher pointerUp calls twice. [#8485](https://github.com/toeverything/blocksuite/pull/8485) + - fix(blocks): pasting elements with css inline style. [#8491](https://github.com/toeverything/blocksuite/pull/8491) + - fix(blocks): hide outline panel toggle button when callback is null. [#8493](https://github.com/toeverything/blocksuite/pull/8493) + - fix(blocks): pasting twice when span inside h tag. [#8496](https://github.com/toeverything/blocksuite/pull/8496) + - fix(blocks): image should be displayed when in vertical mode. [#8497](https://github.com/toeverything/blocksuite/pull/8497) + - fix: press backspace at the start of first line when edgeless text exist. [#8498](https://github.com/toeverything/blocksuite/pull/8498) + +- Updated dependencies [9f70715] + - @blocksuite/affine-components@0.17.18 + - @blocksuite/affine-shared@0.17.18 + - @blocksuite/block-std@0.17.18 + - @blocksuite/global@0.17.18 + - @blocksuite/store@0.17.18 + +## 0.17.17 + +### Patch Changes + +- a89c9c1: ## Features + + - feat: selection extension [#8464](https://github.com/toeverything/blocksuite/pull/8464) + + ## Bug Fixes + + - perf(edgeless): reduce refresh of frame overlay [#8476](https://github.com/toeverything/blocksuite/pull/8476) + - fix(blocks): improve edgeless text block resizing behavior [#8473](https://github.com/toeverything/blocksuite/pull/8473) + - fix: turn off smooth scaling and cache bounds [#8472](https://github.com/toeverything/blocksuite/pull/8472) + - fix: add strategy option for portal [#8470](https://github.com/toeverything/blocksuite/pull/8470) + - fix(blocks): fix slash menu is triggered in ignored blocks [#8469](https://github.com/toeverything/blocksuite/pull/8469) + - fix(blocks): incorrect width of embed-linked-doc-block in edgeless [#8463](https://github.com/toeverything/blocksuite/pull/8463) + - fix: improve open link on link popup [#8462](https://github.com/toeverything/blocksuite/pull/8462) + - fix: do not enable shift-click center peek in edgeless [#8460](https://github.com/toeverything/blocksuite/pull/8460) + - fix(microsheet): disable microsheet block full-width in edgeless mode [#8461](https://github.com/toeverything/blocksuite/pull/8461) + - fix: check editable element active more accurately [#8457](https://github.com/toeverything/blocksuite/pull/8457) + - fix: edgeless image block rotate [#8458](https://github.com/toeverything/blocksuite/pull/8458) + - fix: outline popup ref area [#8456](https://github.com/toeverything/blocksuite/pull/8456) + +- Updated dependencies [a89c9c1] + - @blocksuite/affine-components@0.17.17 + - @blocksuite/affine-shared@0.17.17 + - @blocksuite/block-std@0.17.17 + - @blocksuite/global@0.17.17 + - @blocksuite/store@0.17.17 + +## 0.17.16 + +### Patch Changes + +- ce9a242: Fix bugs and improve experience: + + - fix slash menu and @ menu issues with IME [#8444](https://github.com/toeverything/blocksuite/pull/8444) + - improve trigger way of latex editor [#8445](https://github.com/toeverything/blocksuite/pull/8445) + - support in-app link jump [#8499](https://github.com/toeverything/blocksuite/pull/8449) + - some ui improvements [#8446](https://github.com/toeverything/blocksuite/pull/8446), [#8450](https://github.com/toeverything/blocksuite/pull/8450) + +- Updated dependencies [ce9a242] + - @blocksuite/affine-components@0.17.16 + - @blocksuite/affine-shared@0.17.16 + - @blocksuite/block-std@0.17.16 + - @blocksuite/global@0.17.16 + - @blocksuite/store@0.17.16 + +## 0.17.15 + +### Patch Changes + +- 931315f: - Fix: Improved scroll behavior to target elements + - Fix: Enhanced bookmark and synced document block styles + - Fix: Resolved issues with PDF printing completion + - Fix: Prevented LaTeX editor from triggering at the start of a line + - Fix: Adjusted portal position in blocks + - Fix: Improved mindmap layout for existing models + - Feature: Added file type detection for exports + - Feature: Enhanced block visibility UI in Edgeless mode + - Refactor: Improved data source API for microsheet + - Refactor: Ensured new block elements are always on top in Edgeless mode + - Chore: Upgraded non-major dependencies + - Chore: Improved ThemeObserver and added tests +- Updated dependencies [931315f] + - @blocksuite/affine-components@0.17.15 + - @blocksuite/affine-shared@0.17.15 + - @blocksuite/block-std@0.17.15 + - @blocksuite/global@0.17.15 + - @blocksuite/store@0.17.15 + +## 0.17.14 + +### Patch Changes + +- 163cb11: - Provide an all-in-one package for Affine. + - Fix duplication occurs when card view is switched to embed view. + - Improve linked block status detection. + - Separate user extensions and internal extensions in std. + - Fix add note feature in microsheet. + - Fix pasting multiple times when span nested in p. + - Refactor range sync. +- Updated dependencies [163cb11] + - @blocksuite/affine-components@0.17.14 + - @blocksuite/affine-shared@0.17.14 + - @blocksuite/block-std@0.17.14 + - @blocksuite/global@0.17.14 + - @blocksuite/store@0.17.14 + +## 0.17.13 + +### Patch Changes + +- 9de68e3: Update mindmap uitls export +- Updated dependencies [9de68e3] + - @blocksuite/affine-components@0.17.13 + - @blocksuite/affine-shared@0.17.13 + - @blocksuite/block-std@0.17.13 + - @blocksuite/global@0.17.13 + - @blocksuite/store@0.17.13 + +## 0.17.12 + +### Patch Changes + +- c334c91: - fix(microsheet): remove image column + - fix: frame preview should update correctly after mode switched + - refactor: move with-disposable and signal-watcher to global package + - fix(edgeless): failed to alt clone move frame when it contains container element + - fix: wrong size limit config +- Updated dependencies [c334c91] + - @blocksuite/affine-components@0.17.12 + - @blocksuite/affine-shared@0.17.12 + - @blocksuite/block-std@0.17.12 + - @blocksuite/global@0.17.12 + - @blocksuite/store@0.17.12 + +## 0.17.11 + +### Patch Changes + +- 1052ebd: - Refactor drag handle widget + - Split embed blocks to `@blocksuite/affine-block-embed` + - Fix latex selected state in edgeless mode + - Fix unclear naming + - Fix prototype pollution + - Fix portal interaction in affine modal + - Fix paste linked block on edgeless + - Add scroll anchoring widget + - Add highlight selection +- Updated dependencies [1052ebd] + - @blocksuite/affine-components@0.17.11 + - @blocksuite/affine-shared@0.17.11 + - @blocksuite/block-std@0.17.11 + - @blocksuite/global@0.17.11 + - @blocksuite/store@0.17.11 + +## 0.17.10 + +### Patch Changes + +- e0d0016: - Fix microsheet performance issue + - Fix frame panel display issue + - Fix editor settings for color with transparency + - Fix portal in modals + - Fix group selection rendering delay + - Remove unused and duplicated code + - Improve frame model + - Improve ParseDocUrl service + - Support custom max zoom +- Updated dependencies [e0d0016] + - @blocksuite/affine-components@0.17.10 + - @blocksuite/affine-shared@0.17.10 + - @blocksuite/block-std@0.17.10 + - @blocksuite/global@0.17.10 + - @blocksuite/store@0.17.10 + +## 0.17.9 + +### Patch Changes + +- 5f29800: - Fix latex issues + - Fix inline embed gap + - Fix edgeless text color + - Fix outline panel note status + - Improve mindmap + - Add sideEffects: false to all packages + - Add parse url service + - Add ref node slots extension +- Updated dependencies [5f29800] + - @blocksuite/affine-components@0.17.9 + - @blocksuite/affine-shared@0.17.9 + - @blocksuite/block-std@0.17.9 + - @blocksuite/global@0.17.9 + - @blocksuite/store@0.17.9 + +## 0.17.8 + +### Patch Changes + +- 2f7dbe9: - feat(microsheet): easy access to property visibility + - fix: mind map issues + - feat(microsheet): supports switching view types + - fix(blocks): should use cardStyle for rendering + - test: add mini-mindmap test + - feat(microsheet): full width POC +- Updated dependencies [2f7dbe9] + - @blocksuite/affine-components@0.17.8 + - @blocksuite/affine-shared@0.17.8 + - @blocksuite/block-std@0.17.8 + - @blocksuite/global@0.17.8 + - @blocksuite/store@0.17.8 + +## 0.17.7 + +### Patch Changes + +- 5ab06c3: - Peek view as extension + - Editor settings as extension + - Edit props store as extension + - Notifications as extension + - Fix mini mindmap get service error + - Fix generating placeholder style + - Fix brush menu settings + - Fix brush element line width + - Fix edgeless preview pointer events + - Fix latex editor focus shake +- Updated dependencies [5ab06c3] + - @blocksuite/affine-components@0.17.7 + - @blocksuite/affine-shared@0.17.7 + - @blocksuite/block-std@0.17.7 + - @blocksuite/global@0.17.7 + - @blocksuite/store@0.17.7 + +## 0.17.6 + +### Patch Changes + +- d8d5656: - Fix latex block export + - Fix rich text reference config export + - Fix mindmap export dependency error + - Fix toast position + - Fix frame remember settings + - Microsheet statistic improvements + - Add keymap extension +- Updated dependencies [d8d5656] + - @blocksuite/affine-components@0.17.6 + - @blocksuite/affine-shared@0.17.6 + - @blocksuite/block-std@0.17.6 + - @blocksuite/global@0.17.6 + - @blocksuite/store@0.17.6 + +## 0.17.5 + +### Patch Changes + +- debf65c: - Fix latex export + - Fix add group in microsheet kanban view + - Fix presentation mode `Esc` key + - Fix url parse and paste for block reference + - Frame improvement + - Microsheet checkbox statistics improvement + - Inline extensions + - Mindmap remember last settings +- Updated dependencies [debf65c] + - @blocksuite/affine-components@0.17.5 + - @blocksuite/affine-shared@0.17.5 + - @blocksuite/block-std@0.17.5 + - @blocksuite/global@0.17.5 + - @blocksuite/store@0.17.5 + +## 0.17.4 + +### Patch Changes + +- 9978a71: Create git tag +- Updated dependencies [9978a71] + - @blocksuite/affine-components@0.17.4 + - @blocksuite/affine-shared@0.17.4 + - @blocksuite/block-std@0.17.4 + - @blocksuite/global@0.17.4 + - @blocksuite/store@0.17.4 + +## 0.17.3 + +### Patch Changes + +- be60caf: Generate git tag +- Updated dependencies [be60caf] + - @blocksuite/affine-components@0.17.3 + - @blocksuite/affine-shared@0.17.3 + - @blocksuite/block-std@0.17.3 + - @blocksuite/global@0.17.3 + - @blocksuite/store@0.17.3 + +## 0.17.2 + +### Patch Changes + +- 5543e32: Fix missing export in dataview +- Updated dependencies [5543e32] + - @blocksuite/affine-components@0.17.2 + - @blocksuite/affine-shared@0.17.2 + - @blocksuite/block-std@0.17.2 + - @blocksuite/global@0.17.2 + - @blocksuite/store@0.17.2 + +## 0.17.1 + +### Patch Changes + +- 21b5d47: BlockSuite 0.17.1 + + Add @blocksuite/data-view package. + Make font loader an extension. + Frame improvement. + Fix missing xywh when copy/paste mind map. + Fix connector label text. + +- Updated dependencies [21b5d47] + - @blocksuite/affine-components@0.17.1 + - @blocksuite/affine-shared@0.17.1 + - @blocksuite/block-std@0.17.1 + - @blocksuite/global@0.17.1 + - @blocksuite/store@0.17.1 diff --git a/packages/affine/microsheet-data-view/package.json b/packages/affine/microsheet-data-view/package.json new file mode 100644 index 000000000000..8eb9b074f4f7 --- /dev/null +++ b/packages/affine/microsheet-data-view/package.json @@ -0,0 +1,85 @@ +{ + "name": "@blocksuite/microsheet-data-view", + "version": "0.17.19", + "description": "Views of microsheet in affine", + "type": "module", + "repository": { + "type": "git", + "url": "https://github.com/toeverything/blocksuite.git", + "directory": "packages/affine/microsheet-data-view" + }, + "scripts": { + "build": "tsc", + "test:unit": "nx vite:test --run --passWithNoTests", + "test:unit:coverage": "nx vite:test --run --coverage", + "test:e2e": "playwright test" + }, + "sideEffects": false, + "keywords": [], + "author": "toeverything", + "license": "MPL-2.0", + "dependencies": { + "@blocksuite/affine-components": "workspace:*", + "@blocksuite/affine-shared": "workspace:*", + "@blocksuite/block-std": "workspace:*", + "@blocksuite/global": "workspace:*", + "@blocksuite/icons": "^2.1.68", + "@blocksuite/store": "workspace:*", + "@floating-ui/dom": "^1.6.10", + "@lit/context": "^1.1.2", + "@preact/signals-core": "^1.8.0", + "@toeverything/theme": "^1.0.8", + "date-fns": "^4.0.0", + "lit": "^3.2.0", + "sortablejs": "^1.15.2", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/sortablejs": "^1.15.8" + }, + "exports": { + ".": "./src/index.ts", + "./property-presets": "./src/property-presets/index.ts", + "./property-pure-presets": "./src/property-presets/pure-index.ts", + "./view-presets": "./src/view-presets/index.ts", + "./widget-presets": "./src/widget-presets/index.ts", + "./effects": "./src/effects.ts" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./property-presets": { + "import": "./dist/property-presets/index.js", + "types": "./dist/property-presets/index.d.ts" + }, + "./property-pure-presets": { + "import": "./dist/property-presets/pure-index.js", + "types": "./dist/property-presets/pure-index.d.ts" + }, + "./view-presets": { + "import": "./dist/view-presets/index.js", + "types": "./dist/view-presets/index.d.ts" + }, + "./widget-presets": { + "import": "./dist/widget-presets/index.js", + "types": "./dist/widget-presets/index.d.ts" + }, + "./effects": { + "import": "./dist/effects.js", + "types": "./dist/effects.d.ts" + } + } + }, + "files": [ + "src", + "dist", + "!src/__tests__", + "!dist/__tests__" + ] +} diff --git a/packages/affine/microsheet-data-view/src/core/common/ast.ts b/packages/affine/microsheet-data-view/src/core/common/ast.ts new file mode 100644 index 000000000000..710ae963516d --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/ast.ts @@ -0,0 +1,33 @@ +import type { TType } from '../logical/typesystem.js'; +import type { UniComponent } from '../utils/uni-component/uni-component.js'; + +export type Variable = { + name: string; + type: TType; + id: string; + icon?: UniComponent; +}; +export type VariableRef = { + type: 'ref'; + name: string; +}; + +export type Property = { + type: 'property'; + ref: VariableRef; + propertyFuncName: string; +}; + +export type VariableOrProperty = VariableRef | Property; + +export type Literal = { + type: 'literal'; + value: unknown; +}; +export type Value = /*VariableRef*/ Literal; +export type GroupExp = { + left: VariableOrProperty; + type: 'asc' | 'desc'; +}; + +export type GroupList = GroupExp[]; diff --git a/packages/affine/microsheet-data-view/src/core/common/component/overflow/overflow.ts b/packages/affine/microsheet-data-view/src/core/common/component/overflow/overflow.ts new file mode 100644 index 000000000000..8423ddee4f8e --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/component/overflow/overflow.ts @@ -0,0 +1,107 @@ +import { ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { css, html, type PropertyValues, type TemplateResult } from 'lit'; +import { property, query, queryAll, state } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { repeat } from 'lit/directives/repeat.js'; + +export class Overflow extends SignalWatcher(WithDisposable(ShadowlessElement)) { + static override styles = css` + microsheet-component-overflow { + display: flex; + flex-wrap: wrap; + width: 100%; + position: relative; + } + + .microsheet-component-overflow-item { + } + .microsheet-component-overflow-item.hidden { + opacity: 0; + pointer-events: none; + position: absolute; + } + `; + + protected frameId: number | undefined = undefined; + + protected widthList: number[] = []; + + adjustStyle() { + if (this.frameId) { + cancelAnimationFrame(this.frameId); + } + + this.frameId = requestAnimationFrame(() => { + this.doAdjustStyle(); + }); + } + + override connectedCallback() { + super.connectedCallback(); + const resize = new ResizeObserver(() => { + this.adjustStyle(); + }); + resize.observe(this); + this.disposables.add(() => { + resize.unobserve(this); + }); + } + + protected doAdjustStyle() { + const moreWidth = this.more.getBoundingClientRect().width; + this.widthList[this.renderCount] = moreWidth; + + const containerWidth = this.getBoundingClientRect().width; + + let width = 0; + for (let i = 0; i < this.items.length; i++) { + const itemWidth = this.items[i].getBoundingClientRect().width; + // Try to calculate the width occupied by rendering n+1 items; + // if it exceeds the limit, render n items(in i++ round). + const totalWidth = + width + itemWidth + (this.widthList[i + 1] ?? moreWidth); + if (totalWidth > containerWidth) { + this.renderCount = i; + return; + } + width += itemWidth; + } + this.renderCount = this.items.length; + } + + override render() { + return html` + ${repeat(this.renderItem, (render, index) => { + const className = classMap({ + 'microsheet-component-overflow-item': true, + hidden: index >= this.renderCount, + }); + return html`
${render()}
`; + })} +
+ ${this.renderMore(this.renderCount)} +
+ `; + } + + protected override updated(_changedProperties: PropertyValues) { + super.updated(_changedProperties); + this.adjustStyle(); + } + + @queryAll(':scope > .microsheet-component-overflow-item') + accessor items!: HTMLDivElement[] & NodeList; + + @query(':scope > .microsheet-component-overflow-more') + accessor more!: HTMLDivElement; + + @state() + accessor renderCount = 0; + + @property({ attribute: false }) + accessor renderItem!: Array<() => TemplateResult>; + + @property({ attribute: false }) + accessor renderMore!: (count: number) => TemplateResult; +} diff --git a/packages/affine/microsheet-data-view/src/core/common/css-variable.ts b/packages/affine/microsheet-data-view/src/core/common/css-variable.ts new file mode 100644 index 000000000000..c4b928d0f0ad --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/css-variable.ts @@ -0,0 +1,60 @@ +export const dataViewCssVariable = () => { + return ` + --data-view-cell-text-size:14px; + --data-view-cell-text-line-height:22px; +`; +}; +export const dataViewCommonStyle = (selector: string) => ` + ${selector}{ + ${dataViewCssVariable()} + } + .with-data-view-css-variable{ + ${dataViewCssVariable()} + font-family: var(--affine-font-family) + } + .dv-pd-2{ + padding:2px; + } + .dv-pd-4{ + padding:4px; + } + .dv-pd-8{ + padding:8px; + } + .dv-hover:hover{ + background-color: var(--affine-hover-color); + cursor: pointer; + } + .dv-icon-16 svg{ + width: 16px; + height: 16px; + color: var(--affine-icon-color); + fill: var(--affine-icon-color); + } + .dv-icon-20 svg{ + width: 20px; + height: 20px; + color: var(--affine-icon-color); + fill: var(--affine-icon-color); + } + .dv-border{ + border: 1px solid var(--affine-border-color); + } + .dv-round-4{ + border-radius: 4px; + } + .dv-round-8{ + border-radius: 8px; + } + .dv-color-2{ + color: var(--affine-text-secondary-color); + } + .dv-shadow-2{ + box-shadow: var(--affine-shadow-2) + } + .dv-divider-h{ + height: 1px; + background-color: var(--affine-divider-color); + margin: 8px 0; + } +`; diff --git a/packages/affine/microsheet-data-view/src/core/common/data-source/base.ts b/packages/affine/microsheet-data-view/src/core/common/data-source/base.ts new file mode 100644 index 000000000000..e46e450f5ced --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/data-source/base.ts @@ -0,0 +1,215 @@ +import type { InsertToPosition } from '@blocksuite/affine-shared/utils'; + +import { computed, type ReadonlySignal } from '@preact/signals-core'; + +import type { TType } from '../../logical/index.js'; +import type { PropertyMetaConfig } from '../../property/property-config.js'; +import type { DataViewDataType, ViewMeta } from '../../view/data-view.js'; +import type { ViewManager } from '../../view-manager/view-manager.js'; +import type { DataViewContextKey } from './context.js'; + +export interface DataSource { + readonly$: ReadonlySignal; + properties$: ReadonlySignal; + + cellValueGet(rowId: string, propertyId: string): unknown; + cellRefGet(rowId: string, propertyId: string): unknown; + cellValueGet$( + rowId: string, + propertyId: string + ): ReadonlySignal; + cellValueChange(rowId: string, propertyId: string, value: unknown): void; + + rows$: ReadonlySignal; + rowAdd(InsertToPosition: InsertToPosition | number): string; + rowDelete(ids: string[]): void; + rowMove(rowId: string, position: InsertToPosition): void; + + propertyMetas: PropertyMetaConfig[]; + + propertyNameGet$(propertyId: string): ReadonlySignal; + propertyNameGet(propertyId: string): string; + propertyNameSet(propertyId: string, name: string): void; + + propertyTypeGet(propertyId: string): string | undefined; + propertyTypeGet$(propertyId: string): ReadonlySignal; + + propertyDataGet(propertyId: string): Record; + propertyDataGet$( + propertyId: string + ): ReadonlySignal | undefined>; + propertyDataSet(propertyId: string, data: Record): void; + + propertyDataTypeGet(propertyId: string): TType | undefined; + propertyDataTypeGet$(propertyId: string): ReadonlySignal; + + propertyReadonlyGet(propertyId: string): boolean; + propertyReadonlyGet$(propertyId: string): ReadonlySignal; + + propertyMetaGet(type: string): PropertyMetaConfig; + propertyAdd(insertToPosition: InsertToPosition, type?: string): string; + propertyDuplicate(propertyId: string): string; + propertyDelete(id: string): void; + + contextGet(key: DataViewContextKey): T; + + viewManager: ViewManager; + viewMetas: ViewMeta[]; + viewDataList$: ReadonlySignal; + + viewDataGet(viewId: string): DataViewDataType | undefined; + viewDataGet$(viewId: string): ReadonlySignal; + + viewDataAdd(viewData: DataViewDataType): string; + viewDataDuplicate(id: string): string; + viewDataDelete(viewId: string): void; + viewDataMoveTo(id: string, position: InsertToPosition): void; + viewDataUpdate( + id: string, + updater: (data: ViewData) => Partial + ): void; + + viewMetaGet(type: string): ViewMeta; + viewMetaGet$(type: string): ReadonlySignal; + + viewMetaGetById(viewId: string): ViewMeta; + viewMetaGetById$(viewId: string): ReadonlySignal; +} + +export abstract class DataSourceBase implements DataSource { + context = new Map(); + + abstract properties$: ReadonlySignal; + + abstract propertyMetas: PropertyMetaConfig[]; + + abstract readonly$: ReadonlySignal; + + abstract rows$: ReadonlySignal; + + abstract viewDataList$: ReadonlySignal; + + abstract viewManager: ViewManager; + + abstract viewMetas: ViewMeta[]; + + abstract cellRefGet(rowId: string, propertyId: string): unknown; + + abstract cellValueChange( + rowId: string, + propertyId: string, + value: unknown + ): void; + + abstract cellValueChange( + rowId: string, + propertyId: string, + value: unknown + ): void; + + abstract cellValueGet(rowId: string, propertyId: string): unknown; + + cellValueGet$( + rowId: string, + propertyId: string + ): ReadonlySignal { + return computed(() => this.cellValueGet(rowId, propertyId)); + } + + contextGet(key: DataViewContextKey): T { + return (this.context.get(key.key) as T) ?? key.defaultValue; + } + + contextSet(key: DataViewContextKey, value: T): void { + this.context.set(key.key, value); + } + + abstract propertyAdd( + insertToPosition: InsertToPosition, + type?: string + ): string; + + abstract propertyDataGet(propertyId: string): Record; + + propertyDataGet$( + propertyId: string + ): ReadonlySignal | undefined> { + return computed(() => this.propertyDataGet(propertyId)); + } + + abstract propertyDataSet( + propertyId: string, + data: Record + ): void; + + abstract propertyDataTypeGet(propertyId: string): TType | undefined; + + propertyDataTypeGet$(propertyId: string): ReadonlySignal { + return computed(() => this.propertyDataTypeGet(propertyId)); + } + + abstract propertyDelete(id: string): void; + + abstract propertyDuplicate(propertyId: string): string; + + abstract propertyMetaGet(type: string): PropertyMetaConfig; + + abstract propertyNameGet(propertyId: string): string; + + propertyNameGet$(propertyId: string): ReadonlySignal { + return computed(() => this.propertyNameGet(propertyId)); + } + + abstract propertyNameSet(propertyId: string, name: string): void; + + propertyReadonlyGet(_propertyId: string): boolean { + return false; + } + + propertyReadonlyGet$(propertyId: string): ReadonlySignal { + return computed(() => this.propertyReadonlyGet(propertyId)); + } + + abstract propertyTypeGet(propertyId: string): string; + + propertyTypeGet$(propertyId: string): ReadonlySignal { + return computed(() => this.propertyTypeGet(propertyId)); + } + + abstract rowAdd(InsertToPosition: InsertToPosition | number): string; + + abstract rowDelete(ids: string[]): void; + + abstract rowMove(rowId: string, position: InsertToPosition): void; + + abstract viewDataAdd(viewData: DataViewDataType): string; + + abstract viewDataDelete(viewId: string): void; + + abstract viewDataDuplicate(id: string): string; + + abstract viewDataGet(viewId: string): DataViewDataType; + + viewDataGet$(viewId: string): ReadonlySignal { + return computed(() => this.viewDataGet(viewId)); + } + + abstract viewDataMoveTo(id: string, position: InsertToPosition): void; + + abstract viewDataUpdate( + id: string, + updater: (data: ViewData) => Partial + ): void; + + abstract viewMetaGet(type: string): ViewMeta; + + viewMetaGet$(type: string): ReadonlySignal { + return computed(() => this.viewMetaGet(type)); + } + + abstract viewMetaGetById(viewId: string): ViewMeta; + + viewMetaGetById$(viewId: string): ReadonlySignal { + return computed(() => this.viewMetaGetById(viewId)); + } +} diff --git a/packages/affine/microsheet-data-view/src/core/common/data-source/context.ts b/packages/affine/microsheet-data-view/src/core/common/data-source/context.ts new file mode 100644 index 000000000000..f81b56973c5f --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/data-source/context.ts @@ -0,0 +1,12 @@ +export interface DataViewContextKey { + key: symbol; + defaultValue: T; +} + +export const createContextKey = ( + name: string, + defaultValue: T +): DataViewContextKey => ({ + key: Symbol(name), + defaultValue, +}); diff --git a/packages/affine/microsheet-data-view/src/core/common/data-source/index.ts b/packages/affine/microsheet-data-view/src/core/common/data-source/index.ts new file mode 100644 index 000000000000..ffe27a828483 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/data-source/index.ts @@ -0,0 +1,2 @@ +export * from './base.js'; +export * from './context.js'; diff --git a/packages/affine/microsheet-data-view/src/core/common/detail/detail.ts b/packages/affine/microsheet-data-view/src/core/common/detail/detail.ts new file mode 100644 index 000000000000..29f313e1733e --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/detail/detail.ts @@ -0,0 +1,282 @@ +import { + menu, + popFilterableSimpleMenu, + popupTargetFromElement, +} from '@blocksuite/affine-components/context-menu'; +import { ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { + ArrowDownBigIcon, + ArrowUpBigIcon, + PlusIcon, +} from '@blocksuite/icons/lit'; +import { computed } from '@preact/signals-core'; +import { css, nothing, unsafeCSS } from 'lit'; +import { property, query } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { keyed } from 'lit/directives/keyed.js'; +import { repeat } from 'lit/directives/repeat.js'; +import { html } from 'lit/static-html.js'; + +import type { SingleView } from '../../view-manager/single-view.js'; + +import { + renderUniLit, + type UniComponent, +} from '../../utils/uni-component/uni-component.js'; +import { dataViewCommonStyle } from '../css-variable.js'; +import { DetailSelection } from './selection.js'; + +export type DetailSlotProps = { + view: SingleView; + rowId: string; +}; + +export interface DetailSlots { + header?: UniComponent; + note?: UniComponent; +} + +const styles = css` + ${unsafeCSS(dataViewCommonStyle('affine-microsheet-data-view-record-detail'))} + affine-microsheet-data-view-record-detail { + position: relative; + display: flex; + flex: 1; + flex-direction: column; + padding: 20px; + gap: 12px; + background-color: var(--affine-background-primary-color); + border-radius: 8px; + height: 100%; + width: 100%; + } + + .add-property { + display: flex; + align-items: center; + gap: 4px; + font-size: var(--data-view-cell-text-size); + font-style: normal; + font-weight: 400; + line-height: var(--data-view-cell-text-line-height); + color: var(--affine-text-disable-color); + border-radius: 4px; + padding: 6px 8px 6px 4px; + cursor: pointer; + margin-top: 8px; + width: max-content; + } + + .add-property:hover { + background-color: var(--affine-hover-color); + } + + .add-property .icon { + display: flex; + align-items: center; + } + + .add-property .icon svg { + fill: var(--affine-icon-color); + width: 20px; + height: 20px; + } + + .switch-row { + display: flex; + align-items: center; + justify-content: center; + padding: 2px; + border-radius: 4px; + cursor: pointer; + font-size: 22px; + color: var(--affine-icon-color); + } + + .switch-row:hover { + background-color: var(--affine-hover-color); + } + + .switch-row.disable { + cursor: default; + background: none; + opacity: 0.5; + } +`; + +export class RecordDetail extends SignalWatcher( + WithDisposable(ShadowlessElement) +) { + static override styles = styles; + + _clickAddProperty = () => { + popFilterableSimpleMenu( + popupTargetFromElement(this.addPropertyButton), + this.view.propertyMetas.map(meta => { + return menu.action({ + name: meta.config.name, + prefix: renderUniLit(this.view.IconGet(meta.type)), + select: () => { + this.view.propertyAdd('end', meta.type); + }, + }); + }) + ); + }; + + @property({ attribute: false }) + accessor view!: SingleView; + + properties$ = computed(() => { + return this.view.detailProperties$.value.map(id => + this.view.propertyGet(id) + ); + }); + + selection = new DetailSelection(this); + + private get readonly() { + return this.view.readonly$.value; + } + + private renderHeader() { + const header = this.detailSlots?.header; + if (header) { + const props: DetailSlotProps = { + view: this.view, + rowId: this.rowId, + }; + return renderUniLit(header, props); + } + return undefined; + } + + private renderNote() { + const note = this.detailSlots?.note; + if (note) { + const props: DetailSlotProps = { + view: this.view, + rowId: this.rowId, + }; + return renderUniLit(note, props); + } + return undefined; + } + + override connectedCallback() { + super.connectedCallback(); + + this.disposables.addFromEvent(this, 'click', e => { + e.stopPropagation(); + this.selection.selection = undefined; + }); + //FIXME: simulate as a widget + this.dataset.widgetId = 'affine-detail-widget'; + } + + hasNext() { + return this.view.rowNextGet(this.rowId) != null; + } + + hasPrev() { + return this.view.rowPrevGet(this.rowId) != null; + } + + nextRow() { + const rowId = this.view.rowNextGet(this.rowId); + if (rowId == null) { + return; + } + this.rowId = rowId; + this.requestUpdate(); + } + + prevRow() { + const rowId = this.view.rowPrevGet(this.rowId); + if (rowId == null) { + return; + } + this.rowId = rowId; + this.requestUpdate(); + } + + override render() { + const properties = this.properties$.value; + const upClass = classMap({ + 'switch-row': true, + disable: !this.hasPrev(), + }); + const downClass = classMap({ + 'switch-row': true, + disable: !this.hasNext(), + }); + return html` +
+
+ ${ArrowUpBigIcon()} +
+
+ ${ArrowDownBigIcon()} +
+
+
+ ${keyed(this.rowId, this.renderHeader())} + ${repeat( + properties, + v => v.id, + property => { + return keyed( + this.rowId, + html` ` + ); + } + )} + ${!this.readonly + ? html`
+
${PlusIcon()}
+ Add Property +
` + : nothing} +
+
+ ${keyed(this.rowId, this.renderNote())} + `; + } + + @query('.add-property') + accessor addPropertyButton!: HTMLElement; + + @property({ attribute: false }) + accessor detailSlots: DetailSlots | undefined; + + @property({ attribute: false }) + accessor rowId!: string; +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-microsheet-data-view-record-detail': RecordDetail; + } +} +export const createRecordDetail = (ops: { + view: SingleView; + rowId: string; + detail: DetailSlots; +}) => { + return html` `; +}; diff --git a/packages/affine/microsheet-data-view/src/core/common/detail/field.ts b/packages/affine/microsheet-data-view/src/core/common/detail/field.ts new file mode 100644 index 000000000000..973534a80bb5 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/detail/field.ts @@ -0,0 +1,290 @@ +import { + menu, + popMenu, + popupTargetFromElement, +} from '@blocksuite/affine-components/context-menu'; +import { ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { + DeleteIcon, + DuplicateIcon, + MoveLeftIcon, + MoveRightIcon, +} from '@blocksuite/icons/lit'; +import { computed } from '@preact/signals-core'; +import { css } from 'lit'; +import { property, state } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { createRef } from 'lit/directives/ref.js'; +import { html } from 'lit/static-html.js'; + +import type { + CellRenderProps, + DataViewCellLifeCycle, +} from '../../property/index.js'; +import type { Property } from '../../view-manager/property.js'; +import type { SingleView } from '../../view-manager/single-view.js'; + +import { renderUniLit } from '../../utils/uni-component/uni-component.js'; +import { inputConfig, typeConfig } from '../property-menu.js'; + +export class RecordField extends SignalWatcher( + WithDisposable(ShadowlessElement) +) { + static override styles = css` + affine-microsheet-data-view-record-field { + display: flex; + gap: 12px; + } + + .field-left { + padding: 6px; + display: flex; + height: max-content; + align-items: center; + gap: 6px; + font-size: var(--data-view-cell-text-size); + line-height: var(--data-view-cell-text-line-height); + color: var(--affine-text-secondary-color); + width: 160px; + border-radius: 4px; + cursor: pointer; + user-select: none; + } + + .field-left:hover { + background-color: var(--affine-hover-color); + } + + affine-microsheet-data-view-record-field .icon { + display: flex; + align-items: center; + width: 16px; + height: 16px; + } + + affine-microsheet-data-view-record-field .icon svg { + width: 16px; + height: 16px; + fill: var(--affine-icon-color); + } + + .filed-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .field-content { + padding: 6px 8px; + border-radius: 4px; + flex: 1; + cursor: pointer; + display: flex; + align-items: center; + border: 1px solid transparent; + } + + .field-content .affine-microsheet-number { + text-align: left; + justify-content: start; + } + + .field-content:hover { + background-color: var(--affine-hover-color); + } + + .field-content.is-editing { + box-shadow: 0px 0px 0px 2px rgba(30, 150, 235, 0.3); + } + + .field-content.is-focus { + border: 1px solid var(--affine-primary-color); + } + + .field-content.empty::before { + content: 'Empty'; + color: var(--affine-text-disable-color); + font-size: 14px; + line-height: 22px; + } + `; + + private _cell = createRef(); + + _click = (e: MouseEvent) => { + e.stopPropagation(); + if (this.readonly) return; + + this.changeEditing(true); + }; + + _clickLeft = (e: MouseEvent) => { + if (this.readonly) return; + const ele = e.currentTarget as HTMLElement; + const properties = this.view.detailProperties$.value; + popMenu(popupTargetFromElement(ele), { + options: { + items: [ + menu.group({ + name: 'Column Prop Group ', + items: [inputConfig(this.column), typeConfig(this.column)], + }), + menu.group({ + items: [ + menu.action({ + name: 'Move Up', + prefix: html`
+ ${MoveLeftIcon()} +
`, + hide: () => + properties.findIndex(v => v === this.column.id) === 0, + select: () => { + const index = properties.findIndex(v => v === this.column.id); + const targetId = properties[index - 1]; + if (!targetId) { + return; + } + this.view.propertyMove(this.column.id, { + id: targetId, + before: true, + }); + }, + }), + menu.action({ + name: 'Move Down', + prefix: html`
+ ${MoveRightIcon()} +
`, + hide: () => + properties.findIndex(v => v === this.column.id) === + properties.length - 1, + select: () => { + const index = properties.findIndex(v => v === this.column.id); + const targetId = properties[index + 1]; + if (!targetId) { + return; + } + this.view.propertyMove(this.column.id, { + id: targetId, + before: false, + }); + }, + }), + ], + }), + menu.group({ + name: 'operation', + items: [ + menu.action({ + name: 'Duplicate', + prefix: DuplicateIcon(), + hide: () => + !this.column.duplicate || this.column.type$.value === 'title', + select: () => { + this.column.duplicate?.(); + }, + }), + menu.action({ + name: 'Delete', + prefix: DeleteIcon(), + hide: () => + !this.column.delete || this.column.type$.value === 'title', + select: () => { + this.column.delete?.(); + }, + class: { 'delete-item': true }, + }), + ], + }), + ], + }, + }); + }; + + @property({ attribute: false }) + accessor column!: Property; + + @property({ attribute: false }) + accessor rowId!: string; + + cell$ = computed(() => { + return this.column.cellGet(this.rowId); + }); + + changeEditing = (editing: boolean) => { + const selection = this.closest( + 'affine-microsheet-data-view-record-detail' + )?.selection; + if (selection) { + selection.selection = { + propertyId: this.column.id, + isEditing: editing, + }; + } + }; + + get cell(): DataViewCellLifeCycle | undefined { + return this._cell.value; + } + + private get readonly() { + return this.view.readonly$.value; + } + + override render() { + const column = this.column; + + const props: CellRenderProps = { + cell: this.cell$.value, + isEditing: this.editing, + selectCurrentCell: this.changeEditing, + }; + const renderer = this.column.renderer$.value; + if (!renderer) { + return; + } + const { view, edit } = renderer; + const contentClass = classMap({ + 'field-content': true, + empty: !this.editing && this.cell$.value.isEmpty$.value, + 'is-editing': this.editing, + 'is-focus': this.isFocus, + }); + return html` +
+
+
+ +
+
${column.name$.value}
+
+
+
+ ${renderUniLit(this.editing && edit ? edit : view, props, { + ref: this._cell, + class: 'kanban-cell', + })} +
+ `; + } + + @state() + accessor editing = false; + + @state() + accessor isFocus = false; + + @property({ attribute: false }) + accessor view!: SingleView; +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-microsheet-data-view-record-field': RecordField; + } +} diff --git a/packages/affine/microsheet-data-view/src/core/common/detail/selection.ts b/packages/affine/microsheet-data-view/src/core/common/detail/selection.ts new file mode 100644 index 000000000000..8f117165f7ea --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/detail/selection.ts @@ -0,0 +1,136 @@ +import type { RecordDetail } from './detail.js'; + +import { RecordField } from './field.js'; + +type DetailViewSelection = { + propertyId: string; + isEditing: boolean; +}; + +export class DetailSelection { + _selection?: DetailViewSelection; + + onSelect = (selection?: DetailViewSelection) => { + const old = this._selection; + if (old) { + this.blur(old); + } + this._selection = selection; + if (selection) { + this.focus(selection); + } + }; + + get selection(): DetailViewSelection | undefined { + return this._selection; + } + + set selection(selection: DetailViewSelection | undefined) { + if (!selection) { + this.onSelect(); + return; + } + if (selection.isEditing) { + const container = this.getFocusCellContainer(selection); + const cell = container?.cell; + const isEditing = cell + ? cell.beforeEnterEditMode() + ? selection.isEditing + : false + : false; + this.onSelect({ + propertyId: selection.propertyId, + isEditing, + }); + } else { + this.onSelect(selection); + } + } + + constructor(private viewEle: RecordDetail) {} + + blur(selection: DetailViewSelection) { + const container = this.getFocusCellContainer(selection); + if (!container) { + return; + } + + container.isFocus = false; + const cell = container.cell; + + if (selection.isEditing) { + requestAnimationFrame(() => { + cell?.onExitEditMode(); + }); + if (cell?.blurCell()) { + container.blur(); + } + container.editing = false; + } else { + container.blur(); + } + } + + deleteProperty() { + // + } + + focus(selection: DetailViewSelection) { + const container = this.getFocusCellContainer(selection); + if (!container) { + return; + } + container.isFocus = true; + const cell = container.cell; + if (selection.isEditing) { + cell?.onEnterEditMode(); + if (cell?.focusCell()) { + container.focus(); + } + container.editing = true; + } else { + container.focus(); + } + } + + focusDown() { + const selection = this.selection; + if (!selection || selection?.isEditing) { + return; + } + this.getFocusCellContainer(selection)?.nextElementSibling; + } + + focusFirstCell() { + const firstId = this.viewEle.querySelector( + 'affine-microsheet-data-view-record-field' + )?.column.id; + if (firstId) { + this.selection = { + propertyId: firstId, + isEditing: true, + }; + } + } + + focusUp() { + const selection = this.selection; + if (!selection || selection?.isEditing) { + return; + } + const preContainer = + this.getFocusCellContainer(selection)?.previousElementSibling; + if (preContainer instanceof RecordField) { + this.selection = { + propertyId: preContainer.column.id, + isEditing: false, + }; + } + } + + getFocusCellContainer(selection: DetailViewSelection) { + return this.viewEle.querySelector( + `affine-microsheet-data-view-record-field[data-column-id="${selection.propertyId}"]` + ) as RecordField | undefined; + } +} diff --git a/packages/affine/microsheet-data-view/src/core/common/group-by.ts b/packages/affine/microsheet-data-view/src/core/common/group-by.ts new file mode 100644 index 000000000000..e2186fb4df76 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/group-by.ts @@ -0,0 +1,19 @@ +import type { PropertyMetaConfig } from '../property/property-config.js'; +import type { GroupBy } from './types.js'; + +import { groupByMatcher } from './group-by/matcher.js'; + +export const defaultGroupBy = ( + propertyMeta: PropertyMetaConfig, + propertyId: string, + data: NonNullable +): GroupBy | undefined => { + const name = groupByMatcher.match(propertyMeta.config.type(data))?.name; + return name != null + ? { + type: 'groupBy', + columnId: propertyId, + name: name, + } + : undefined; +}; diff --git a/packages/affine/microsheet-data-view/src/core/common/group-by/define.ts b/packages/affine/microsheet-data-view/src/core/common/group-by/define.ts new file mode 100644 index 000000000000..d9889bf47b4b --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/group-by/define.ts @@ -0,0 +1,35 @@ +import type { GroupByConfig } from './types.js'; + +import { tString } from '../../logical/data-type.js'; +import { MatcherCreator } from '../../logical/matcher.js'; +import { createUniComponentFromWebComponent } from '../../utils/uni-component/uni-component.js'; +import { StringGroupView } from './renderer/string-group.js'; + +const groupByMatcherCreator = new MatcherCreator(); +const ungroups = { + key: 'Ungroups', + value: null, +}; +export const groupByMatchers = [ + groupByMatcherCreator.createMatcher(tString.create(), { + name: 'text', + groupName: (_type, value) => { + return `${value ?? ''}`; + }, + defaultKeys: _type => { + return [ungroups]; + }, + valuesGroup: (value, _type) => { + if (!value) { + return [ungroups]; + } + return [ + { + key: `g:${value}`, + value, + }, + ]; + }, + view: createUniComponentFromWebComponent(StringGroupView), + }), +]; diff --git a/packages/affine/microsheet-data-view/src/core/common/group-by/group-title.ts b/packages/affine/microsheet-data-view/src/core/common/group-by/group-title.ts new file mode 100644 index 000000000000..859666dab7f8 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/group-by/group-title.ts @@ -0,0 +1,120 @@ +import { MoreHorizontalIcon, PlusIcon } from '@blocksuite/icons/lit'; +import { nothing } from 'lit'; +import { html } from 'lit/static-html.js'; + +import type { GroupData } from './helper.js'; +import type { GroupRenderProps } from './types.js'; + +import { renderUniLit } from '../../utils/uni-component/uni-component.js'; + +function GroupHeaderCount(group: GroupData) { + const cards = group.rows; + if (!cards.length) { + return; + } + return html`
${cards.length}
`; +} + +export function GroupTitle( + groupData: GroupData, + ops: { + readonly: boolean; + clickAdd: (evt: MouseEvent) => void; + clickOps: (evt: MouseEvent) => void; + } +) { + const data = groupData.manager.config$.value; + if (!data) return nothing; + + const icon = + groupData.value == null + ? '' + : html` `; + const props: GroupRenderProps = { + value: groupData.value, + data: groupData.property.data$.value, + updateData: groupData.manager.updateData, + updateValue: value => groupData.manager.updateValue(groupData.rows, value), + readonly: ops.readonly, + }; + + return html` + +
+ ${icon} ${renderUniLit(data.view, props)} ${GroupHeaderCount(groupData)} +
+ ${ops.readonly + ? nothing + : html`
+
+ ${PlusIcon()} +
+
+ ${MoreHorizontalIcon()} +
+
`} + `; +} diff --git a/packages/affine/microsheet-data-view/src/core/common/group-by/helper.ts b/packages/affine/microsheet-data-view/src/core/common/group-by/helper.ts new file mode 100644 index 000000000000..c2291b15c923 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/group-by/helper.ts @@ -0,0 +1,280 @@ +import { + insertPositionToIndex, + type InsertToPosition, +} from '@blocksuite/affine-shared/utils'; +import { computed, type ReadonlySignal } from '@preact/signals-core'; + +import type { TType } from '../../logical/typesystem.js'; +import type { Property } from '../../view-manager/property.js'; +import type { SingleView } from '../../view-manager/single-view.js'; +import type { GroupBy, GroupProperty } from '../types.js'; + +import { groupByMatcher } from './matcher.js'; + +export type GroupData = { + manager: GroupManager; + property: Property; + key: string; + name: string; + type: TType; + value: unknown; + rows: string[]; +}; + +export class GroupManager { + config$ = computed(() => { + const groupBy = this.groupBy$.value; + if (!groupBy) { + return; + } + const result = groupByMatcher.find(v => v.data.name === groupBy.name); + if (!result) { + return; + } + return result.data; + }); + + property$ = computed(() => { + const groupBy = this.groupBy$.value; + if (!groupBy) { + return; + } + return this.viewManager.propertyGet(groupBy.columnId); + }); + + staticGroupDataMap$ = computed< + Record> | undefined + >(() => { + const config = this.config$.value; + const property = this.property$.value; + const tType = property?.dataType$.value; + if (!config || !tType || !property) { + return; + } + return Object.fromEntries( + config.defaultKeys(tType).map(({ key, value }) => [ + key, + { + key, + property, + name: config.groupName(tType, value), + manager: this, + type: tType, + value, + }, + ]) + ); + }); + + groupDataMap$ = computed | undefined>(() => { + const staticGroupMap = this.staticGroupDataMap$.value; + const config = this.config$.value; + const groupBy = this.groupBy$.value; + const property = this.property$.value; + const tType = property?.dataType$.value; + if (!staticGroupMap || !config || !groupBy || !tType || !property) { + return; + } + const groupMap: Record = Object.fromEntries( + Object.entries(staticGroupMap).map(([k, v]) => [k, { ...v, rows: [] }]) + ); + this.viewManager.rows$.value.forEach(id => { + const value = this.viewManager.cellJsonValueGet(id, groupBy.columnId); + const keys = config.valuesGroup(value, tType); + keys.forEach(({ key, value }) => { + if (!groupMap[key]) { + groupMap[key] = { + key, + property: property, + name: config.groupName(tType, value), + manager: this, + value, + rows: [], + type: tType, + }; + } + groupMap[key].rows.push(id); + }); + }); + return groupMap; + }); + + groupsDataList$ = computed(() => { + const groupMap = this.groupDataMap$.value; + if (!groupMap) { + return; + } + const sortedGroup = this.ops.sortGroup(Object.keys(groupMap)); + sortedGroup.forEach(key => { + groupMap[key].rows = this.ops.sortRow(key, groupMap[key].rows); + }); + return sortedGroup.map(key => groupMap[key]); + }); + + updateData = (data: NonNullable) => { + const propertyId = this.propertyId; + if (!propertyId) { + return; + } + this.viewManager.propertyDataSet(propertyId, data); + }; + + get addGroup() { + const type = this.property$.value?.type$.value; + if (!type) { + return; + } + return this.viewManager.propertyMetaGet(type)?.config.addGroup; + } + + get propertyId() { + return this.groupBy$.value?.columnId; + } + + constructor( + private groupBy$: ReadonlySignal, + private viewManager: SingleView, + private ops: { + sortGroup: (keys: string[]) => string[]; + sortRow: (groupKey: string, rowIds: string[]) => string[]; + changeGroupSort: (keys: string[]) => void; + changeRowSort: ( + groupKeys: string[], + groupKey: string, + keys: string[] + ) => void; + } + ) {} + + addToGroup(rowId: string, key: string) { + const groupMap = this.groupDataMap$.value; + const propertyId = this.propertyId; + if (!groupMap || !propertyId) { + return; + } + const addTo = this.config$.value?.addToGroup ?? (value => value); + const newValue = addTo( + groupMap[key].value, + this.viewManager.cellJsonValueGet(rowId, propertyId) + ); + this.viewManager.cellValueSet(rowId, propertyId, newValue); + } + + changeCardSort(groupKey: string, cardIds: string[]) { + const groups = this.groupsDataList$.value; + if (!groups) { + return; + } + this.ops.changeRowSort( + groups.map(v => v.key), + groupKey, + cardIds + ); + } + + changeGroupSort(keys: string[]) { + this.ops.changeGroupSort(keys); + } + + defaultGroupProperty(key: string): GroupProperty { + return { + key, + hide: false, + manuallyCardSort: [], + }; + } + + moveCardTo( + rowId: string, + fromGroupKey: string | undefined, + toGroupKey: string, + position: InsertToPosition + ) { + const groupMap = this.groupDataMap$.value; + if (!groupMap) { + return; + } + if (fromGroupKey !== toGroupKey) { + const propertyId = this.propertyId; + if (!propertyId) { + return; + } + const remove = this.config$.value?.removeFromGroup ?? (() => undefined); + const group = fromGroupKey != null ? groupMap[fromGroupKey] : undefined; + let newValue: unknown = undefined; + if (group) { + newValue = remove( + group.value, + this.viewManager.cellJsonValueGet(rowId, propertyId) + ); + } + const addTo = this.config$.value?.addToGroup ?? (value => value); + newValue = addTo(groupMap[toGroupKey].value, newValue); + this.viewManager.cellValueSet(rowId, propertyId, newValue); + } + const rows = groupMap[toGroupKey].rows.filter(id => id !== rowId); + const index = insertPositionToIndex(position, rows, id => id); + rows.splice(index, 0, rowId); + this.changeCardSort(toGroupKey, rows); + } + + moveGroupTo(groupKey: string, position: InsertToPosition) { + const groups = this.groupsDataList$.value; + if (!groups) { + return; + } + const keys = groups.map(v => v.key); + keys.splice( + keys.findIndex(key => key === groupKey), + 1 + ); + const index = insertPositionToIndex(position, keys, key => key); + keys.splice(index, 0, groupKey); + this.changeGroupSort(keys); + } + + removeFromGroup(rowId: string, key: string) { + const groupMap = this.groupDataMap$.value; + if (!groupMap) { + return; + } + const propertyId = this.propertyId; + if (!propertyId) { + return; + } + const remove = this.config$.value?.removeFromGroup ?? (() => undefined); + const newValue = remove( + groupMap[key].value, + this.viewManager.cellJsonValueGet(rowId, propertyId) + ); + this.viewManager.cellValueSet(rowId, propertyId, newValue); + } + + updateValue(rows: string[], value: unknown) { + const propertyId = this.propertyId; + if (!propertyId) { + return; + } + rows.forEach(id => { + this.viewManager.cellValueSet(id, propertyId, value); + }); + } +} + +export const sortByManually = ( + arr: T[], + getId: (v: T) => string, + ids: string[] +) => { + const map = new Map(arr.map(v => [getId(v), v])); + const result: T[] = []; + for (const id of ids) { + const value = map.get(id); + if (value) { + map.delete(id); + result.push(value); + } + } + result.push(...map.values()); + return result; +}; diff --git a/packages/affine/microsheet-data-view/src/core/common/group-by/matcher.ts b/packages/affine/microsheet-data-view/src/core/common/group-by/matcher.ts new file mode 100644 index 000000000000..53789fd141f3 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/group-by/matcher.ts @@ -0,0 +1,6 @@ +import type { GroupByConfig } from './types.js'; + +import { Matcher } from '../../logical/matcher.js'; +import { groupByMatchers } from './define.js'; + +export const groupByMatcher = new Matcher(groupByMatchers); diff --git a/packages/affine/microsheet-data-view/src/core/common/group-by/renderer/base.ts b/packages/affine/microsheet-data-view/src/core/common/group-by/renderer/base.ts new file mode 100644 index 000000000000..608762b8ec3f --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/group-by/renderer/base.ts @@ -0,0 +1,25 @@ +import { ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { property } from 'lit/decorators.js'; + +import type { GroupRenderProps } from '../types.js'; + +export class BaseGroup, Value> + extends SignalWatcher(WithDisposable(ShadowlessElement)) + implements GroupRenderProps +{ + @property({ attribute: false }) + accessor data!: Data; + + @property({ attribute: false }) + accessor readonly!: boolean; + + @property({ attribute: false }) + accessor updateData: ((data: Data) => void) | undefined = undefined; + + @property({ attribute: false }) + accessor updateValue: ((value: Value) => void) | undefined = undefined; + + @property({ attribute: false }) + accessor value!: Value; +} diff --git a/packages/affine/microsheet-data-view/src/core/common/group-by/renderer/boolean-group.ts b/packages/affine/microsheet-data-view/src/core/common/group-by/renderer/boolean-group.ts new file mode 100644 index 000000000000..04655ebfb9d2 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/group-by/renderer/boolean-group.ts @@ -0,0 +1,25 @@ +import { CheckBoxCkeckSolidIcon, CheckBoxUnIcon } from '@blocksuite/icons/lit'; +import { css, html } from 'lit'; + +import { BaseGroup } from './base.js'; + +export class BooleanGroupView extends BaseGroup, boolean> { + static override styles = css` + .data-view-group-title-boolean-view { + display: flex; + align-items: center; + } + .data-view-group-title-boolean-view svg { + width: 20px; + height: 20px; + } + `; + + protected override render(): unknown { + return html`
+ ${this.value + ? CheckBoxCkeckSolidIcon({ style: `color:#1E96EB` }) + : CheckBoxUnIcon()} +
`; + } +} diff --git a/packages/affine/microsheet-data-view/src/core/common/group-by/renderer/number-group.ts b/packages/affine/microsheet-data-view/src/core/common/group-by/renderer/number-group.ts new file mode 100644 index 000000000000..04a986825629 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/group-by/renderer/number-group.ts @@ -0,0 +1,65 @@ +import { + menu, + popMenu, + popupTargetFromElement, +} from '@blocksuite/affine-components/context-menu'; +import { css, html } from 'lit'; + +import { BaseGroup } from './base.js'; + +export class NumberGroupView extends BaseGroup, number> { + static override styles = css` + .data-view-group-title-number-view { + border-radius: 8px; + padding: 4px 8px; + width: max-content; + cursor: pointer; + } + + .data-view-group-title-number-view:hover { + background-color: var(--affine-hover-color); + } + `; + + private _click = () => { + if (this.readonly) { + return; + } + popMenu(popupTargetFromElement(this), { + options: { + items: [ + menu.input({ + initialValue: this.value ? `${this.value * 10}` : '', + onComplete: text => { + const num = Number.parseFloat(text); + if (Number.isNaN(num)) { + return; + } + this.updateValue?.(num); + }, + }), + ], + }, + }); + }; + + protected override render(): unknown { + if (this.value == null) { + return html`
Ungroups
`; + } + if (this.value >= 10) { + return html`
+ >= 100 +
`; + } + return html`
+ ${this.value * 10} - ${this.value * 10 + 9} +
`; + } +} diff --git a/packages/affine/microsheet-data-view/src/core/common/group-by/renderer/string-group.ts b/packages/affine/microsheet-data-view/src/core/common/group-by/renderer/string-group.ts new file mode 100644 index 000000000000..8d1f6df0c68b --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/group-by/renderer/string-group.ts @@ -0,0 +1,53 @@ +import { + menu, + popMenu, + popupTargetFromElement, +} from '@blocksuite/affine-components/context-menu'; +import { css, html } from 'lit'; + +import { BaseGroup } from './base.js'; + +export class StringGroupView extends BaseGroup, string> { + static override styles = css` + .data-view-group-title-string-view { + border-radius: 8px; + padding: 4px 8px; + width: max-content; + cursor: pointer; + } + + .data-view-group-title-string-view:hover { + background-color: var(--affine-hover-color); + } + `; + + private _click = () => { + if (this.readonly) { + return; + } + popMenu(popupTargetFromElement(this), { + options: { + items: [ + menu.input({ + initialValue: this.value ?? '', + onComplete: text => { + this.updateValue?.(text); + }, + }), + ], + }, + }); + }; + + protected override render(): unknown { + if (!this.value) { + return html`
Ungroups
`; + } + return html`
+ ${this.value} +
`; + } +} diff --git a/packages/affine/microsheet-data-view/src/core/common/group-by/setting.ts b/packages/affine/microsheet-data-view/src/core/common/group-by/setting.ts new file mode 100644 index 000000000000..4bddfc97b103 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/group-by/setting.ts @@ -0,0 +1,297 @@ +import type { PropertyValues } from 'lit'; + +import { + menu, + type MenuConfig, + type MenuOptions, + popMenu, + type PopupTarget, +} from '@blocksuite/affine-components/context-menu'; +import { ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { DeleteIcon } from '@blocksuite/icons/lit'; +import { css, html, unsafeCSS } from 'lit'; +import { property, query } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; +import Sortable from 'sortablejs'; + +import type { TableViewData } from '../../../view-presets/index.js'; +import type { SingleView } from '../../view-manager/single-view.js'; +import type { GroupRenderProps } from './types.js'; + +import { TableSingleView } from '../../../view-presets/table/table-view-manager.js'; +import { renderUniLit } from '../../utils/uni-component/uni-component.js'; +import { dataViewCssVariable } from '../css-variable.js'; +import { groupByMatcher } from './matcher.js'; + +export class GroupSetting extends SignalWatcher( + WithDisposable(ShadowlessElement) +) { + static override styles = css` + data-view-group-setting { + display: flex; + flex-direction: column; + gap: 4px; + ${unsafeCSS(dataViewCssVariable())}; + } + + .group-item { + display: flex; + padding: 4px 12px; + position: relative; + cursor: grab; + } + + .group-item-drag-bar { + width: 4px; + height: 12px; + border-radius: 1px; + background-color: #efeff0; + position: absolute; + left: 4px; + top: 0; + bottom: 0; + margin: auto; + } + + .group-item:hover .group-item-drag-bar { + background-color: #c0bfc1; + } + `; + + override connectedCallback() { + super.connectedCallback(); + this._disposables.addFromEvent(this, 'pointerdown', e => { + e.stopPropagation(); + }); + } + + protected override firstUpdated(_changedProperties: PropertyValues) { + super.firstUpdated(_changedProperties); + const sortable = new Sortable(this.groupContainer, { + animation: 150, + group: `group-sort-${this.view.id}`, + onEnd: evt => { + const groupManager = this.view.groupManager; + const oldGroups = groupManager.groupsDataList$.value; + if (!oldGroups) { + return; + } + const groups = [...oldGroups]; + const index = evt.oldIndex ?? -1; + const from = groups[index]; + groups.splice(index, 1); + const to = groups[evt.newIndex ?? -1]; + groupManager.moveGroupTo( + from.key, + to + ? { + before: true, + id: to.key, + } + : 'end' + ); + }, + }); + this._disposables.add({ + dispose: () => sortable.destroy(), + }); + } + + protected override render(): unknown { + const groups = this.view.groupManager.groupsDataList$.value; + if (!groups) { + return; + } + return html` +
+
+ Groups +
+
+
+
+ ${repeat( + groups, + group => group.key, + group => { + const props: GroupRenderProps = { + value: group.value, + data: group.property.data$.value, + readonly: true, + }; + const config = group.manager.config$.value; + return html`
+
+
+ ${renderUniLit(config?.view, props)} +
+
+
`; + } + )} +
+ `; + } + + @query('.group-sort-setting') + accessor groupContainer!: HTMLElement; + + @property({ attribute: false }) + accessor view!: TableSingleView; +} + +export const selectGroupByProperty = ( + view: SingleView, + ops?: { + onSelect?: (id?: string) => void; + onClose?: () => void; + onBack?: () => void; + } +): MenuOptions => { + return { + onClose: ops?.onClose, + title: { + text: 'Group by', + onBack: ops?.onBack, + }, + items: [ + ...view.propertiesWithoutFilter$.value + .filter(id => { + if (view.propertyGet(id).type$.value === 'title') { + return false; + } + return !!groupByMatcher.match(view.propertyGet(id).dataType$.value); + }) + .map(id => { + const property = view.propertyGet(id); + return menu.action({ + name: property.name$.value, + isSelected: view.data$.value?.groupBy?.columnId === id, + prefix: html` `, + select: () => { + if (view instanceof TableSingleView) { + view.changeGroup(id); + ops?.onSelect?.(id); + } + }, + }); + }), + menu.group({ + items: [ + menu.action({ + prefix: DeleteIcon(), + hide: () => view.data$.value?.groupBy == null, + class: { 'delete-item': true }, + name: 'Remove Grouping', + select: () => { + if (view instanceof TableSingleView) { + view.changeGroup(undefined); + ops?.onSelect?.(); + } + }, + }), + ], + }), + ], + }; +}; +export const popSelectGroupByProperty = ( + target: PopupTarget, + view: SingleView, + ops?: { + onSelect?: () => void; + onClose?: () => void; + onBack?: () => void; + } +) => { + popMenu(target, { + options: selectGroupByProperty(view, ops), + }); +}; +export const popGroupSetting = ( + target: PopupTarget, + view: SingleView, + onBack: () => void +) => { + const groupBy = view.data$.value?.groupBy; + if (groupBy == null) { + return; + } + const type = view.propertyTypeGet(groupBy.columnId); + if (!type) { + return; + } + const icon = view.IconGet(type); + const menuHandler = popMenu(target, { + options: { + title: { + text: 'Group', + onBack: onBack, + }, + items: [ + menu.group({ + items: [ + menu.subMenu({ + name: 'Group By', + postfix: html` +
+ ${renderUniLit(icon, {})} + ${view.propertyNameGet(groupBy.columnId)} +
+ `, + label: () => html` +
+ Group By +
+ `, + options: selectGroupByProperty(view, { + onSelect: () => { + menuHandler.close(); + popGroupSetting(target, view, onBack); + }, + }), + }), + ], + }), + menu.group({ + items: [ + menu => + html` `, + ], + }), + menu.group({ + items: [ + menu.action({ + name: 'Remove grouping', + prefix: DeleteIcon(), + class: { 'delete-item': true }, + hide: () => !(view instanceof TableSingleView), + select: () => { + if (view instanceof TableSingleView) { + view.changeGroup(undefined); + } + }, + }), + ], + }), + ], + }, + }); +}; diff --git a/packages/affine/microsheet-data-view/src/core/common/group-by/types.ts b/packages/affine/microsheet-data-view/src/core/common/group-by/types.ts new file mode 100644 index 000000000000..ac014e4cc139 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/group-by/types.ts @@ -0,0 +1,32 @@ +import type { TType } from '../../logical/index.js'; +import type { UniComponent } from '../../utils/index.js'; + +export interface GroupRenderProps< + Data extends NonNullable = NonNullable, + Value = unknown, +> { + data: Data; + updateData?: (data: Data) => void; + value: Value; + updateValue?: (value: Value) => void; + readonly: boolean; +} + +export type GroupByConfig = { + name: string; + groupName: (type: TType, value: unknown) => string; + defaultKeys: (type: TType) => { + key: string; + value: unknown; + }[]; + valuesGroup: ( + value: unknown, + type: TType + ) => { + key: string; + value: unknown; + }[]; + addToGroup?: (value: unknown, oldValue: unknown) => unknown; + removeFromGroup?: (value: unknown, oldValue: unknown) => unknown; + view: UniComponent; +}; diff --git a/packages/affine/microsheet-data-view/src/core/common/index.ts b/packages/affine/microsheet-data-view/src/core/common/index.ts new file mode 100644 index 000000000000..4d2b453875ed --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/index.ts @@ -0,0 +1,10 @@ +export * from './ast.js'; +export * from './css-variable.js'; +export * from './data-source/index.js'; +export * from './detail/detail.js'; +export * from './group-by.js'; +export * from './group-by/matcher.js'; +export type { GroupByConfig } from './group-by/types.js'; +export type { GroupRenderProps } from './group-by/types.js'; +export * from './selection.js'; +export * from './types.js'; diff --git a/packages/affine/microsheet-data-view/src/core/common/literal/define.ts b/packages/affine/microsheet-data-view/src/core/common/literal/define.ts new file mode 100644 index 000000000000..906b6f92124a --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/literal/define.ts @@ -0,0 +1,29 @@ +import { menu, popMenu } from '@blocksuite/affine-components/context-menu'; + +import type { LiteralData } from './types.js'; + +import { tString } from '../../logical/data-type.js'; +import { MatcherCreator } from '../../logical/matcher.js'; +import { createUniComponentFromWebComponent } from '../../utils/uni-component/uni-component.js'; +import { StringLiteral } from './renderer/literal-element.js'; + +const literalMatcherCreator = new MatcherCreator(); +export const literalMatchers = [ + literalMatcherCreator.createMatcher(tString.create(), { + view: createUniComponentFromWebComponent(StringLiteral), + popEdit: (position, { value$, onChange }) => { + popMenu(position, { + options: { + items: [ + menu.input({ + initialValue: value$.value?.toString() ?? '', + onComplete: text => { + onChange(text || undefined); + }, + }), + ], + }, + }); + }, + }), +]; diff --git a/packages/affine/microsheet-data-view/src/core/common/literal/matcher.ts b/packages/affine/microsheet-data-view/src/core/common/literal/matcher.ts new file mode 100644 index 000000000000..817b68f3ca69 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/literal/matcher.ts @@ -0,0 +1,36 @@ +import type { PopupTarget } from '@blocksuite/affine-components/context-menu'; +import type { ReadonlySignal } from '@preact/signals-core'; + +import type { TType } from '../../logical/typesystem.js'; +import type { LiteralData } from './types.js'; + +import { Matcher } from '../../logical/matcher.js'; +import { renderUniLit } from '../../utils/uni-component/uni-component.js'; +import { literalMatchers } from './define.js'; + +export const renderLiteral = ( + type: TType, + value: ReadonlySignal, + onChange: (value: unknown) => void +) => { + const data = literalMatcher.match(type); + if (!data) { + return; + } + return renderUniLit(data.view, { value$: value, onChange, type }); +}; + +export const popLiteralEdit = ( + target: PopupTarget, + type: TType, + value: ReadonlySignal, + onChange: (value: unknown) => void +) => { + const data = literalMatcher.match(type); + if (!data) { + return; + } + data.popEdit(target, { value$: value, onChange, type: type }); +}; + +export const literalMatcher = new Matcher(literalMatchers); diff --git a/packages/affine/microsheet-data-view/src/core/common/literal/renderer/literal-element.ts b/packages/affine/microsheet-data-view/src/core/common/literal/renderer/literal-element.ts new file mode 100644 index 000000000000..7e09ba823581 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/literal/renderer/literal-element.ts @@ -0,0 +1,67 @@ +import type { ReadonlySignal } from '@preact/signals-core'; + +import { ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { css, html } from 'lit'; +import { property } from 'lit/decorators.js'; + +import type { TType } from '../../../logical/typesystem.js'; +import type { LiteralViewProps } from '../types.js'; + +export abstract class LiteralElement + extends SignalWatcher(WithDisposable(ShadowlessElement)) + implements LiteralViewProps +{ + @property({ attribute: false }) + accessor onChange!: (value?: T) => void; + + @property({ attribute: false }) + accessor type!: Type; + + @property({ attribute: false }) + accessor value$!: ReadonlySignal; +} + +export class BooleanLiteral extends LiteralElement { + override render() { + return this.value$.value ? 'True' : 'False'; + } +} + +export class NumberLiteral extends LiteralElement { + static override styles = css` + data-view-literal-number-view { + display: block; + max-width: 100px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + `; + + override render() { + return ( + this.value$.value?.toString() ?? + html`Value` + ); + } +} + +export class StringLiteral extends LiteralElement { + static override styles = css` + data-view-literal-string-view { + display: block; + max-width: 100px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + `; + + override render() { + return ( + this.value$.value?.toString() ?? + html`Value` + ); + } +} diff --git a/packages/affine/microsheet-data-view/src/core/common/literal/types.ts b/packages/affine/microsheet-data-view/src/core/common/literal/types.ts new file mode 100644 index 000000000000..70f0b5a4c9ab --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/literal/types.ts @@ -0,0 +1,15 @@ +import type { PopupTarget } from '@blocksuite/affine-components/context-menu'; +import type { ReadonlySignal } from '@preact/signals-core'; + +import type { TType } from '../../logical/index.js'; +import type { UniComponent } from '../../utils/index.js'; + +export type LiteralViewProps = { + type: Type; + value$: ReadonlySignal; + onChange: (value?: Value) => void; +}; +export type LiteralData = { + view: UniComponent>; + popEdit: (position: PopupTarget, props: LiteralViewProps) => void; +}; diff --git a/packages/affine/microsheet-data-view/src/core/common/popup.ts b/packages/affine/microsheet-data-view/src/core/common/popup.ts new file mode 100644 index 000000000000..9eb489eda324 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/popup.ts @@ -0,0 +1,31 @@ +import { autoPlacement, computePosition } from '@floating-ui/dom'; + +import { onClickOutside } from '../utils/utils.js'; + +export const createMicrosheetPopup = ( + target: HTMLElement, + content: HTMLElement, + options?: { + onClose?: () => void; + } +) => { + target.parentElement?.append(content); + computePosition(target, content, { + middleware: [autoPlacement()], + }) + .then(({ x, y }) => { + Object.assign(content.style, { + left: `${x}px`, + top: `${y}px`, + }); + }) + .catch(console.error); + onClickOutside( + content, + () => { + content.remove(); + options?.onClose?.(); + }, + 'mousedown' + ); +}; diff --git a/packages/affine/microsheet-data-view/src/core/common/properties.ts b/packages/affine/microsheet-data-view/src/core/common/properties.ts new file mode 100644 index 000000000000..c5812a885614 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/properties.ts @@ -0,0 +1,267 @@ +import { + menu, + popMenu, + type PopupTarget, +} from '@blocksuite/affine-components/context-menu'; +import { ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { InvisibleIcon, ViewIcon } from '@blocksuite/icons/lit'; +import { css, html } from 'lit'; +import { property, query } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { repeat } from 'lit/directives/repeat.js'; +import Sortable from 'sortablejs'; + +import type { Property } from '../view-manager/property.js'; +import type { SingleView } from '../view-manager/single-view.js'; + +export class DataViewPropertiesSettingView extends SignalWatcher( + WithDisposable(ShadowlessElement) +) { + static override styles = css` + .properties-group-header { + user-select: none; + padding: 4px 12px 12px 12px; + margin-bottom: 12px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--affine-divider-color); + } + + .properties-group-title { + font-size: 12px; + line-height: 20px; + color: var(--affine-text-secondary-color); + display: flex; + align-items: center; + gap: 8px; + } + + .properties-group-op { + padding: 4px 8px; + font-size: 12px; + line-height: 20px; + font-weight: 500; + border-radius: 4px; + cursor: pointer; + } + + .properties-group-op:hover { + background-color: var(--affine-hover-color); + } + + .properties-group { + min-height: 40px; + } + + .property-item { + padding: 4px; + display: flex; + align-items: center; + gap: 8px; + user-select: none; + cursor: pointer; + border-radius: 4px; + } + + .property-item:hover { + background-color: var(--affine-hover-color); + } + + .property-item-drag-bar { + width: 4px; + height: 12px; + border-radius: 1px; + background-color: #efeff0; + } + + .property-item:hover .property-item-drag-bar { + background-color: #c0bfc1; + } + + .property-item-icon { + display: flex; + align-items: center; + } + + .property-item-icon svg { + color: var(--affine-icon-color); + fill: var(--affine-icon-color); + width: 20px; + height: 20px; + } + + .property-item-op-icon { + display: flex; + align-items: center; + border-radius: 4px; + } + + .property-item-op-icon:hover { + background-color: var(--affine-hover-color); + } + .property-item-op-icon.disabled:hover { + background-color: transparent; + } + + .property-item-op-icon svg { + fill: var(--affine-icon-color); + color: var(--affine-icon-color); + width: 20px; + height: 20px; + } + + .property-item-op-icon.disabled svg { + fill: var(--affine-text-disable-color); + color: var(--affine-text-disable-color); + } + + .property-item-name { + font-size: 14px; + line-height: 22px; + flex: 1; + } + `; + + renderProperty = (property: Property) => { + const isTitle = property.type$.value === 'title'; + const icon = property.hide$.value ? InvisibleIcon() : ViewIcon(); + const changeVisible = () => { + if (property.type$.value !== 'title') { + property.hideSet(!property.hide$.value); + } + }; + const classList = classMap({ + 'property-item-op-icon': true, + disabled: isTitle, + }); + return html`
+
+ +
${property.name$.value}
+
${icon}
+
`; + }; + + private itemsGroup() { + return this.view.propertiesWithoutFilter$.value.map(id => + this.view.propertyGet(id) + ); + } + + override connectedCallback() { + super.connectedCallback(); + this._disposables.addFromEvent(this, 'pointerdown', e => { + e.stopPropagation(); + }); + } + + override firstUpdated() { + const sortable = new Sortable(this.groupContainer, { + animation: 150, + group: `properties-sort-${this.view.id}`, + onEnd: evt => { + const properties = [...this.view.propertiesWithoutFilter$.value]; + const index = evt.oldIndex ?? -1; + const from = properties[index]; + properties.splice(index, 1); + const to = properties[evt.newIndex ?? -1]; + this.view.propertyMove( + from, + to + ? { + before: true, + id: to, + } + : 'end' + ); + }, + }); + this._disposables.add({ + dispose: () => sortable.destroy(), + }); + } + + override render() { + const items = this.itemsGroup(); + return html` +
+ ${repeat(items, v => v.id, this.renderProperty)} +
+ `; + } + + @query('.properties-group') + accessor groupContainer!: HTMLElement; + + @property({ attribute: false }) + accessor onBack: (() => void) | undefined = undefined; + + @property({ attribute: false }) + accessor view!: SingleView; +} + +declare global { + interface HTMLElementTagNameMap { + 'microsheet-data-view-properties-setting': DataViewPropertiesSettingView; + } +} + +export const popPropertiesSetting = ( + target: PopupTarget, + props: { + view: SingleView; + onClose?: () => void; + onBack?: () => void; + } +) => { + popMenu(target, { + options: { + title: { + text: 'Properties', + onBack: props.onBack, + postfix: () => { + const items = props.view.propertiesWithoutFilter$.value.map(id => + props.view.propertyGet(id) + ); + const isAllShowed = items.every(v => !v.hide$.value); + const clickChangeAll = () => { + props.view.propertiesWithoutFilter$.value.forEach(id => { + if (props.view.propertyTypeGet(id) !== 'title') { + props.view.propertyHideSet(id, isAllShowed); + } + }); + }; + return html`
+ ${isAllShowed ? 'Hide All' : 'Show All'} +
`; + }, + }, + items: [ + menu.group({ + items: [ + () => + html``, + ], + }), + ], + }, + }); + + // const view = new DataViewPropertiesSettingView(); + // view.view = props.view; + // view.onBack = () => { + // close(); + // props.onBack?.(); + // }; + // const close = createPopup(target, view, { onClose: props.onClose }); +}; diff --git a/packages/affine/microsheet-data-view/src/core/common/property-menu.ts b/packages/affine/microsheet-data-view/src/core/common/property-menu.ts new file mode 100644 index 000000000000..9f28317934f5 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/property-menu.ts @@ -0,0 +1,49 @@ +import { menu } from '@blocksuite/affine-components/context-menu'; +import { html } from 'lit/static-html.js'; + +import type { Property } from '../view-manager/property.js'; + +import { renderUniLit } from '../utils/uni-component/index.js'; + +export const inputConfig = (property: Property) => { + return menu.input({ + prefix: html` +
+ ${renderUniLit(property.icon)} +
+ `, + initialValue: property.name$.value, + onComplete: text => { + property.nameSet(text); + }, + }); +}; +export const typeConfig = (property: Property) => { + return menu.subMenu({ + name: 'Type', + hide: () => !property.typeSet || property.type$.value === 'title', + postfix: html`
+ ${renderUniLit(property.icon)} + ${property.view.propertyMetas.find(v => v.type === property.type$.value) + ?.config.name} +
`, + options: { + items: property.view.propertyMetas.map(config => { + return menu.action({ + isSelected: config.type === property.type$.value, + name: config.config.name, + prefix: renderUniLit(property.view.IconGet(config.type)), + select: () => { + if (property.type$.value === config.type) { + return; + } + property.typeSet?.(config.type); + }, + }); + }), + }, + }); +}; diff --git a/packages/affine/microsheet-data-view/src/core/common/ref/ref.ts b/packages/affine/microsheet-data-view/src/core/common/ref/ref.ts new file mode 100644 index 000000000000..31e2c0b809e8 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/ref/ref.ts @@ -0,0 +1,108 @@ +import { + menu, + popFilterableSimpleMenu, + popupTargetFromElement, +} from '@blocksuite/affine-components/context-menu'; +import { ShadowlessElement } from '@blocksuite/block-std'; +import { WithDisposable } from '@blocksuite/global/utils'; +import { css, html } from 'lit'; +import { property } from 'lit/decorators.js'; + +import type { Variable, VariableOrProperty } from '../ast.js'; + +import { renderUniLit } from '../../utils/uni-component/uni-component.js'; + +export class VariableRefView extends WithDisposable(ShadowlessElement) { + static override styles = css` + microsheet-variable-ref-view { + font-size: 12px; + line-height: 20px; + display: flex; + align-items: center; + gap: 6px; + padding: 0 4px; + border-radius: 4px; + cursor: pointer; + } + + microsheet-variable-ref-view:hover { + background-color: var(--affine-hover-color); + } + + microsheet-variable-ref-view svg { + width: 16px; + height: 16px; + fill: var(--affine-icon-color); + color: var(--affine-icon-color); + } + `; + + get field() { + if (!this.data) { + return; + } + if (this.data.type === 'ref') { + return this.data.name; + } + return this.data.ref.name; + } + + get fieldData() { + const id = this.field; + if (!id) { + return; + } + return this.vars.find(v => v.id === id); + } + + get property() { + if (!this.data) { + return; + } + if (this.data.type === 'ref') { + return; + } + return this.data.propertyFuncName; + } + + override connectedCallback() { + super.connectedCallback(); + this.disposables.addFromEvent(this, 'click', e => { + popFilterableSimpleMenu( + popupTargetFromElement(e.target as HTMLElement), + this.vars.map(v => + menu.action({ + name: v.name, + prefix: renderUniLit(v.icon, {}), + select: () => { + this.setData({ + type: 'ref', + name: v.id, + }); + }, + }) + ) + ); + }); + } + + override render() { + const data = this.fieldData; + return html` ${renderUniLit(data?.icon, {})} ${data?.name} `; + } + + @property({ attribute: false }) + accessor data: VariableOrProperty | undefined = undefined; + + @property({ attribute: false }) + accessor setData!: (filter: VariableOrProperty) => void; + + @property({ attribute: false }) + accessor vars!: Variable[]; +} + +declare global { + interface HTMLElementTagNameMap { + 'microsheet-variable-ref-view': VariableRefView; + } +} diff --git a/packages/affine/microsheet-data-view/src/core/common/selection.ts b/packages/affine/microsheet-data-view/src/core/common/selection.ts new file mode 100644 index 000000000000..ce96a49e8d3e --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/selection.ts @@ -0,0 +1,109 @@ +import { BaseSelection, SelectionExtension } from '@blocksuite/block-std'; +import { z } from 'zod'; + +import type { + GetMicrosheetDataViewSelection, + MicrosheetDataViewSelection, +} from '../types.js'; + +const TableViewSelectionSchema = z.union([ + z.object({ + viewId: z.string(), + type: z.literal('table'), + selectionType: z.literal('area'), + rowsSelection: z.object({ + start: z.number(), + end: z.number(), + }), + columnsSelection: z.object({ + start: z.number(), + end: z.number(), + }), + focus: z.object({ + rowIndex: z.number(), + columnIndex: z.number(), + }), + isEditing: z.boolean(), + }), + z.object({ + viewId: z.string(), + type: z.literal('table'), + selectionType: z.literal('row'), + rows: z.array( + z.object({ id: z.string(), groupKey: z.string().optional() }) + ), + }), +]); + +const MicrosheetSelectionSchema = z.object({ + blockId: z.string(), + viewSelection: TableViewSelectionSchema, +}); + +export class MicrosheetSelection extends BaseSelection { + static override group = 'note'; + + static override type = 'microsheet'; + + readonly viewSelection: MicrosheetDataViewSelection; + + get viewId() { + return this.viewSelection.viewId; + } + + constructor({ + blockId, + viewSelection, + }: { + blockId: string; + viewSelection: MicrosheetDataViewSelection; + }) { + super({ + blockId, + }); + + this.viewSelection = viewSelection; + } + + static override fromJSON(json: Record): MicrosheetSelection { + MicrosheetSelectionSchema.parse(json); + return new MicrosheetSelection({ + blockId: json.blockId as string, + viewSelection: json.viewSelection as MicrosheetDataViewSelection, + }); + } + + override equals(other: BaseSelection): boolean { + if (!(other instanceof MicrosheetSelection)) { + return false; + } + return this.blockId === other.blockId; + } + + getSelection( + type: T + ): GetMicrosheetDataViewSelection | undefined { + return this.viewSelection.type === type + ? (this.viewSelection as GetMicrosheetDataViewSelection) + : undefined; + } + + override toJSON(): Record { + return { + type: 'microsheet', + blockId: this.blockId, + viewSelection: this.viewSelection, + }; + } +} + +declare global { + namespace BlockSuite { + interface Selection { + microsheet: typeof MicrosheetSelection; + } + } +} + +export const MicrosheetSelectionExtension = + SelectionExtension(MicrosheetSelection); diff --git a/packages/affine/microsheet-data-view/src/core/common/types.ts b/packages/affine/microsheet-data-view/src/core/common/types.ts new file mode 100644 index 000000000000..46369e4fa89f --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/common/types.ts @@ -0,0 +1,23 @@ +import type { VariableOrProperty } from './ast.js'; + +export type GroupBy = { + type: 'groupBy'; + columnId: string; + name: string; + sort?: { + desc: boolean; + }; +}; +export type GroupProperty = { + key: string; + hide?: boolean; + manuallyCardSort: string[]; +}; +export type SortBy = { + ref: VariableOrProperty; + desc: boolean; +}; +export type Sort = { + sortBy: SortBy[]; + manuallySort: string[]; +}; diff --git a/packages/affine/microsheet-data-view/src/core/data-view.ts b/packages/affine/microsheet-data-view/src/core/data-view.ts new file mode 100644 index 000000000000..959230e16945 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/data-view.ts @@ -0,0 +1,193 @@ +import type { BlockStdScope } from '@blocksuite/block-std'; + +import { ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { computed, type ReadonlySignal } from '@preact/signals-core'; +import { css, unsafeCSS } from 'lit'; +import { property, state } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { keyed } from 'lit/directives/keyed.js'; +import { createRef, ref } from 'lit/directives/ref.js'; +import { html } from 'lit/static-html.js'; + +import type { DataSource } from './common/data-source/base.js'; +import type { + MicrosheetDataViewSelection, + MicrosheetDataViewSelectionState, +} from './types.js'; +import type { DataViewExpose, DataViewProps } from './view/types.js'; +import type { SingleView } from './view-manager/single-view.js'; + +import { dataViewCommonStyle } from './common/css-variable.js'; +import { renderUniLit } from './utils/uni-component/index.js'; + +type ViewProps = { + view: SingleView; + selection$: ReadonlySignal; + setSelection: (selection?: MicrosheetDataViewSelectionState) => void; + bindHotkey: DataViewProps['bindHotkey']; + handleEvent: DataViewProps['handleEvent']; +}; + +export type DataViewRendererConfig = { + bindHotkey: DataViewProps['bindHotkey']; + handleEvent: DataViewProps['handleEvent']; + virtualPadding$: DataViewProps['virtualPadding$']; + selection$: ReadonlySignal; + setSelection: (selection: MicrosheetDataViewSelection | undefined) => void; + dataSource: DataSource; + headerWidget: DataViewProps['headerWidget']; + onDrag?: DataViewProps['onDrag']; + std: BlockStdScope; +}; + +export class DataViewRenderer extends SignalWatcher( + WithDisposable(ShadowlessElement) +) { + static override styles = css` + ${unsafeCSS(dataViewCommonStyle('affine-microsheet-data-view-renderer'))} + affine-microsheet-data-view-renderer { + background-color: var(--affine-background-primary-color); + display: contents; + } + `; + + private _view = createRef<{ expose: DataViewExpose }>(); + + @property({ attribute: false }) + accessor config!: DataViewRendererConfig; + + private currentViewId$ = computed(() => { + return this.config.dataSource.viewManager.currentViewId$.value; + }); + + viewMap$ = computed(() => { + const manager = this.config.dataSource.viewManager; + return Object.fromEntries( + manager.views$.value.map(view => [view, manager.viewGet(view)]) + ); + }); + + currentViewConfig$ = computed(() => { + const currentViewId = this.currentViewId$.value; + if (!currentViewId) { + return; + } + const view = this.viewMap$.value[currentViewId]; + return { + view: view, + selection$: computed(() => { + const selection$ = this.config.selection$; + if (selection$.value?.viewId === currentViewId) { + return selection$.value; + } + return; + }), + setSelection: selection => { + this.config.setSelection(selection); + }, + handleEvent: (name, handler) => + this.config.handleEvent(name, context => { + return handler(context); + }), + bindHotkey: hotkeys => + this.config.bindHotkey( + Object.fromEntries( + Object.entries(hotkeys).map(([key, fn]) => [ + key, + ctx => { + return fn(ctx); + }, + ]) + ) + ), + }; + }); + + focusFirstCell = () => { + this.view?.expose.focusFirstCell(); + }; + + get view() { + return this._view.value; + } + + private renderView(viewData?: ViewProps) { + if (!viewData) { + return; + } + const props: DataViewProps = { + dataViewEle: this, + headerWidget: this.config.headerWidget, + view: viewData.view, + selection$: viewData.selection$, + setSelection: viewData.setSelection, + bindHotkey: viewData.bindHotkey, + handleEvent: viewData.handleEvent, + onDrag: this.config.onDrag, + std: this.config.std, + dataSource: this.config.dataSource, + virtualPadding$: this.config.virtualPadding$, + }; + return keyed( + viewData.view.id, + renderUniLit( + viewData.view.meta.renderer.view, + { props }, + { + ref: this._view, + } + ) + ); + } + + override connectedCallback() { + super.connectedCallback(); + let preId: string | undefined = undefined; + this.disposables.add( + this.currentViewId$.subscribe(current => { + if (current !== preId) { + this.config.setSelection(undefined); + } + preId = current; + }) + ); + } + + override render() { + const containerClass = classMap({ + 'toolbar-hover-container': true, + 'data-view-root': true, + 'prevent-reference-popup': true, + }); + return html` +
+ ${this.renderView(this.currentViewConfig$.value)} +
+ `; + } + + @state() + accessor currentView: string | undefined = undefined; +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-microsheet-data-view-renderer': DataViewRenderer; + } +} + +export class DataView { + private _ref = createRef(); + + get expose() { + return this._ref.value?.view?.expose; + } + + render(props: DataViewRendererConfig) { + return html``; + } +} diff --git a/packages/affine/microsheet-data-view/src/core/index.ts b/packages/affine/microsheet-data-view/src/core/index.ts new file mode 100644 index 000000000000..4fc71153d8bb --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/index.ts @@ -0,0 +1,11 @@ +export { DataSourceBase } from './common/data-source/base.js'; +export * from './common/index.js'; +export { DataView } from './data-view.js'; +export * from './logical/index.js'; +export * from './property/index.js'; +export type { MicrosheetDataViewSelection } from './types.js'; +export * from './types.js'; +export * from './utils/index.js'; +export * from './view/index.js'; +export * from './view-manager/index.js'; +export * from './widget/index.js'; diff --git a/packages/affine/microsheet-data-view/src/core/logical/data-type.ts b/packages/affine/microsheet-data-view/src/core/logical/data-type.ts new file mode 100644 index 000000000000..eedb423a72ff --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/logical/data-type.ts @@ -0,0 +1,10 @@ +import { typesystem } from './typesystem.js'; + +export const tString = typesystem.defineData<{ value: string }>({ + name: 'String', + supers: [], +}); +export const tRichText = typesystem.defineData<{ value: string }>({ + name: 'RichText', + supers: [tString], +}); diff --git a/packages/affine/microsheet-data-view/src/core/logical/index.ts b/packages/affine/microsheet-data-view/src/core/logical/index.ts new file mode 100644 index 000000000000..02659e1e1bc2 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/logical/index.ts @@ -0,0 +1,2 @@ +export * from './data-type.js'; +export * from './typesystem.js'; diff --git a/packages/affine/microsheet-data-view/src/core/logical/matcher.ts b/packages/affine/microsheet-data-view/src/core/logical/matcher.ts new file mode 100644 index 000000000000..72feed1c3041 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/logical/matcher.ts @@ -0,0 +1,71 @@ +import type { TType } from './typesystem.js'; + +import { typesystem } from './typesystem.js'; + +type MatcherData = { + type: Type; + data: Data; +}; + +export class MatcherCreator { + createMatcher(type: Type, data: Data) { + return { type, data }; + } +} + +export class Matcher { + constructor( + private list: MatcherData[], + private _match: ( + type: Type, + target: TType + ) => boolean = typesystem.isSubtype.bind(typesystem) + ) {} + + all(): MatcherData[] { + return this.list; + } + + allMatched(type: TType): MatcherData[] { + const result: MatcherData[] = []; + for (const t of this.list) { + if (this._match(t.type, type)) { + result.push(t); + } + } + return result; + } + + allMatchedData(type: TType): Data[] { + const result: Data[] = []; + for (const t of this.list) { + if (this._match(t.type, type)) { + result.push(t.data); + } + } + return result; + } + + find( + f: (data: MatcherData) => boolean + ): MatcherData | undefined { + return this.list.find(f); + } + + findData(f: (data: Data) => boolean): Data | undefined { + return this.list.find(data => f(data.data))?.data; + } + + isMatched(type: Type, target: TType) { + return this._match(type, target); + } + + match(type: TType) { + for (const t of this.list) { + if (this._match(t.type, type)) { + return t.data; + } + } + return; + } +} diff --git a/packages/affine/microsheet-data-view/src/core/logical/typesystem.ts b/packages/affine/microsheet-data-view/src/core/logical/typesystem.ts new file mode 100644 index 000000000000..e67498d2e1ef --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/logical/typesystem.ts @@ -0,0 +1,292 @@ +import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; + +export interface TUnion { + type: 'union'; + title: 'union'; + list: TType[]; +} + +export const tUnion = (list: TType[]): TUnion => ({ + type: 'union', + title: 'union', + list, +}); + +// TODO treat as data type +export interface TArray<_Ele extends TType = TType> { + type: 'array'; + ele: TType; + title: 'array'; +} + +export const tArray = (ele: T): TArray => { + return { + type: 'array', + title: 'array', + ele, + }; +}; +export const isTArray = (type: TType): type is TArray => { + return type.type === 'array'; +}; +export type TTypeVar = { + type: 'typeVar'; + title: 'typeVar'; + name: string; + bound: TType; +}; +export const tTypeVar = (name: string, bound: TType): TTypeVar => { + return { + type: 'typeVar', + title: 'typeVar', + name, + bound, + }; +}; +export type TTypeRef = { + type: 'typeRef'; + title: 'typeRef'; + name: string; +}; +export const tTypeRef = (name: string): TTypeRef => { + return { + type: 'typeRef', + title: 'typeRef', + name, + }; +}; + +export type TFunction = { + type: 'function'; + title: 'function'; + typeVars: TTypeVar[]; + args: TType[]; + rt: TType; +}; + +export const tFunction = (fn: { + typeVars?: TTypeVar[]; + args: TType[]; + rt: TType; +}): TFunction => { + return { + type: 'function', + title: 'function', + typeVars: fn.typeVars ?? [], + args: fn.args, + rt: fn.rt, + }; +}; + +export type TType = TDataType | TArray | TUnion | TTypeRef | TFunction; + +export type DataTypeShape = Record; +export type TDataType> = { + type: 'data'; + name: string; + data?: Data; +}; +export type ValueOfData = + T extends DataDefine ? R : never; +export type TypeOfData = + T extends DataDefine ? TDataType : never; + +export class DataDefine> { + constructor( + private config: DataDefineConfig, + private dataMap: Map + ) {} + + private isByName(name: string): boolean { + return name === this.config.name; + } + + private isSubOfByName(superType: string): boolean { + if (this.isByName(superType)) { + return true; + } + return this.config.supers.some(sup => sup.isSubOfByName(superType)); + } + + create(data?: Data): TDataType { + return { + type: 'data', + name: this.config.name, + data, + }; + } + + is(data: TType): data is TDataType { + if (data.type !== 'data') { + return false; + } + return data.name === this.config.name; + } + + isSubOf(superType: TDataType): boolean { + if (this.is(superType)) { + return true; + } + return this.config.supers.some(sup => sup.isSubOf(superType)); + } + + isSuperOf(subType: TDataType): boolean { + const dataDefine = this.dataMap.get(subType.name); + if (!dataDefine) { + throw new BlockSuiteError( + ErrorCode.MicrosheetBlockError, + 'data config not found' + ); + } + return dataDefine.isSubOfByName(this.config.name); + } +} + +// type DataTypeVar = {}; + +// TODO support generic data type +interface DataDefineConfig<_T extends DataTypeShape> { + name: string; + supers: DataDefine[]; +} + +interface DataHelper { + create>(name: string): DataDefineConfig; + + extends( + dataDefine: DataDefine + ): DataHelper; +} + +const createDataHelper = >( + ...supers: DataDefine[] +): DataHelper => { + return { + create(name: string) { + return { + name, + supers, + }; + }, + extends(dataDefine) { + return createDataHelper(...supers, dataDefine); + }, + }; +}; +const DataHelper = createDataHelper(); + +export class Typesystem { + dataMap = new Map(); + + defineData( + config: DataDefineConfig + ): DataDefine { + const result = new DataDefine(config, this.dataMap); + this.dataMap.set(config.name, result); + return result; + } + + instance( + context: Record, + realArgs: TType[], + realRt: TType, + template: TFunction + ): TFunction { + const ctx = { ...context }; + template.args.forEach((arg, i) => { + const realArg = realArgs[i]; + if (realArg) { + this.isSubtype(arg, realArg, ctx); + } + }); + this.isSubtype(realRt, template.rt); + return this.subst(ctx, template); + } + + isDataType(t: TType): t is TDataType { + return t.type === 'data'; + } + + isSubtype( + superType: TType, + sub: TType, + context?: Record + ): boolean { + if (superType.type === 'typeRef') { + // TODO both are ref + if (context && sub.type != 'typeRef') { + context[superType.name] = sub; + } + // TODO bound + return true; + } + if (sub.type === 'typeRef') { + // TODO both are ref + if (context) { + context[sub.name] = superType; + } + return true; + } + if (tUnknown.is(superType)) { + return true; + } + if (superType.type === 'union') { + return superType.list.some(type => this.isSubtype(type, sub, context)); + } + if (sub.type === 'union') { + return sub.list.every(type => this.isSubtype(superType, type, context)); + } + + if (this.isDataType(sub)) { + const dataDefine = this.dataMap.get(sub.name); + if (!dataDefine) { + throw new BlockSuiteError( + ErrorCode.MicrosheetBlockError, + 'data config not found' + ); + } + if (!this.isDataType(superType)) { + return false; + } + return dataDefine.isSubOf(superType); + } + + if (superType.type === 'array' || sub.type === 'array') { + if (superType.type !== 'array' || sub.type !== 'array') { + return false; + } + return this.isSubtype(superType.ele, sub.ele, context); + } + return false; + } + + subst(context: Record, template: TFunction): TFunction { + const subst = (type: TType): TType => { + if (this.isDataType(type)) { + return type; + } + switch (type.type) { + case 'typeRef': + return { ...context[type.name] }; + case 'union': + return tUnion(type.list.map(type => subst(type))); + case 'array': + return tArray(subst(type.ele)); + case 'function': + throw new BlockSuiteError( + ErrorCode.MicrosheetBlockError, + 'not implement yet' + ); + } + }; + const result = tFunction({ + args: template.args.map(type => subst(type)), + rt: subst(template.rt), + }); + return result; + } +} + +export const typesystem = new Typesystem(); + +export const tUnknown = typesystem.defineData(DataHelper.create('Unknown')); diff --git a/packages/affine/microsheet-data-view/src/core/property/base-cell.ts b/packages/affine/microsheet-data-view/src/core/property/base-cell.ts new file mode 100644 index 000000000000..054d8bbcebd3 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/property/base-cell.ts @@ -0,0 +1,114 @@ +import { ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { computed } from '@preact/signals-core'; +import { property } from 'lit/decorators.js'; + +import type { Cell } from '../view-manager/cell.js'; +import type { CellRenderProps, DataViewCellLifeCycle } from './manager.js'; + +export abstract class BaseCellRenderer< + Value, + Data extends Record = Record, + > + extends SignalWatcher(WithDisposable(ShadowlessElement)) + implements DataViewCellLifeCycle, CellRenderProps +{ + @property({ attribute: false }) + accessor cell!: Cell; + + readonly$ = computed(() => { + return this.cell.property.readonly$.value; + }); + + value$ = computed(() => { + return this.cell.value$.value; + }); + + get property() { + return this.cell.property; + } + + get readonly() { + return this.readonly$.value; + } + + get row() { + return this.cell.row; + } + + get value() { + return this.value$.value; + } + + get view() { + return this.cell.view; + } + + beforeEnterEditMode(): boolean { + return true; + } + + blurCell() { + return true; + } + + override connectedCallback() { + super.connectedCallback(); + this.style.width = '100%'; + this._disposables.addFromEvent(this, 'click', e => { + if (this.isEditing) { + e.stopPropagation(); + } + }); + + this._disposables.addFromEvent(this, 'copy', e => { + if (!this.isEditing) return; + e.stopPropagation(); + this.onCopy(e); + }); + + this._disposables.addFromEvent(this, 'cut', e => { + if (!this.isEditing) return; + e.stopPropagation(); + this.onCut(e); + }); + + this._disposables.addFromEvent(this, 'paste', e => { + if (!this.isEditing) return; + e.stopPropagation(); + this.onPaste(e); + }); + } + + focusCell() { + return true; + } + + forceUpdate(): void { + this.requestUpdate(); + } + + onChange(value: Value | undefined): void { + this.cell.valueSet(value); + } + + onCopy(_e: ClipboardEvent) {} + + onCut(_e: ClipboardEvent) {} + + onEnterEditMode(): void { + // do nothing + } + + onExitEditMode() { + // do nothing + } + + onPaste(_e: ClipboardEvent) {} + + @property({ attribute: false }) + accessor isEditing!: boolean; + + @property({ attribute: false }) + accessor selectCurrentCell!: (editing: boolean) => void; +} diff --git a/packages/affine/microsheet-data-view/src/core/property/convert.ts b/packages/affine/microsheet-data-view/src/core/property/convert.ts new file mode 100644 index 000000000000..d5f6a5607588 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/property/convert.ts @@ -0,0 +1,32 @@ +import type { PropertyModel } from './property-config.js'; +import type { + GetCellDataFromConfig, + GetPropertyDataFromConfig, +} from './types.js'; + +export type ConvertFunction< + From extends PropertyModel = PropertyModel, + To extends PropertyModel = PropertyModel, +> = ( + property: GetPropertyDataFromConfig, + cells: (GetCellDataFromConfig | undefined)[] +) => { + property: GetPropertyDataFromConfig; + cells: (GetCellDataFromConfig | undefined)[]; +}; +export const createPropertyConvert = < + // eslint-disable-next-line @typescript-eslint/no-explicit-any + From extends PropertyModel, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + To extends PropertyModel, +>( + from: From, + to: To, + convert: ConvertFunction +) => { + return { + from: from.type, + to: to.type, + convert, + }; +}; diff --git a/packages/affine/microsheet-data-view/src/core/property/index.ts b/packages/affine/microsheet-data-view/src/core/property/index.ts new file mode 100644 index 000000000000..5ca2e9268347 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/property/index.ts @@ -0,0 +1,6 @@ +export * from './base-cell.js'; +export * from './convert.js'; +export * from './manager.js'; +export * from './property-config.js'; +export * from './renderer.js'; +export * from './types.js'; diff --git a/packages/affine/microsheet-data-view/src/core/property/manager.ts b/packages/affine/microsheet-data-view/src/core/property/manager.ts new file mode 100644 index 000000000000..1e27a5818f1d --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/property/manager.ts @@ -0,0 +1,38 @@ +import type { UniComponent } from '../utils/uni-component/index.js'; +import type { Cell } from '../view-manager/cell.js'; + +export interface CellRenderProps< + Data extends NonNullable = NonNullable, + Value = unknown, +> { + cell: Cell; + isEditing: boolean; + selectCurrentCell: (editing: boolean) => void; +} + +export interface DataViewCellLifeCycle { + beforeEnterEditMode(): boolean; + + onEnterEditMode(): void; + + onExitEditMode(): void; + + focusCell(): boolean; + + blurCell(): boolean; + + forceUpdate(): void; +} + +export type DataViewCellComponent< + Data extends NonNullable = NonNullable, + Value = unknown, +> = UniComponent, DataViewCellLifeCycle>; + +export type CellRenderer< + Data extends NonNullable = NonNullable, + Value = unknown, +> = { + view: DataViewCellComponent; + edit?: DataViewCellComponent; +}; diff --git a/packages/affine/microsheet-data-view/src/core/property/property-config.ts b/packages/affine/microsheet-data-view/src/core/property/property-config.ts new file mode 100644 index 000000000000..540cfaa7ff2f --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/property/property-config.ts @@ -0,0 +1,72 @@ +import type { Renderer } from './renderer.js'; +import type { PropertyConfig } from './types.js'; + +export type PropertyMetaConfig< + Type extends string = string, + PropertyData extends NonNullable = NonNullable, + CellData = unknown, +> = { + type: Type; + config: PropertyConfig; + create: Create; + renderer: Renderer; +}; +type CreatePropertyMeta< + Type extends string = string, + PropertyData extends Record = Record, + CellData = unknown, +> = ( + renderer: Omit, 'type'> +) => PropertyMetaConfig; +type Create< + PropertyData extends Record = Record, +> = ( + name: string, + data?: PropertyData +) => { + type: string; + name: string; + statCalcOp?: string; + data: PropertyData; +}; +export type PropertyModel< + Type extends string = string, + PropertyData extends Record = Record, + CellData = unknown, +> = { + type: Type; + config: PropertyConfig; + create: Create; + createPropertyMeta: CreatePropertyMeta; +}; +export const propertyType = (type: Type) => ({ + type: type, + modelConfig: < + CellData, + PropertyData extends Record = Record, + >( + ops: PropertyConfig + ): PropertyModel => { + const create: Create = (name, data) => { + return { + type, + name, + data: data ?? ops.defaultData(), + }; + }; + return { + type, + config: ops, + create, + createPropertyMeta: renderer => ({ + type, + config: ops, + create, + renderer: { + type, + ...renderer, + }, + }), + }; + }, +}); diff --git a/packages/affine/microsheet-data-view/src/core/property/renderer.ts b/packages/affine/microsheet-data-view/src/core/property/renderer.ts new file mode 100644 index 000000000000..2670219cc158 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/property/renderer.ts @@ -0,0 +1,25 @@ +import type { BaseCellRenderer } from './base-cell.js'; +import type { CellRenderer, DataViewCellComponent } from './manager.js'; + +import { + createUniComponentFromWebComponent, + type UniComponent, +} from '../utils/uni-component/index.js'; + +export interface Renderer< + Data extends NonNullable = NonNullable, + Value = unknown, +> { + type: string; + icon?: UniComponent; + cellRenderer: CellRenderer; +} + +export const createFromBaseCellRenderer = < + Value, + Data extends Record = Record, +>( + renderer: new () => BaseCellRenderer +): DataViewCellComponent => { + return createUniComponentFromWebComponent(renderer as never) as never; +}; diff --git a/packages/affine/microsheet-data-view/src/core/property/types.ts b/packages/affine/microsheet-data-view/src/core/property/types.ts new file mode 100644 index 000000000000..376a4633e8a8 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/property/types.ts @@ -0,0 +1,43 @@ +import type { Disposable } from '@blocksuite/global/utils'; + +import type { TType } from '../logical/index.js'; + +export type GetPropertyDataFromConfig = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends PropertyConfig ? R : never; +export type GetCellDataFromConfig = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + T extends PropertyConfig ? R : never; +export type PropertyConfig< + Data extends NonNullable = NonNullable, + Value = unknown, +> = { + name: string; + defaultData: () => Data; + type: (data: Data) => TType; + formatValue?: (value: unknown, colData: Data) => Value; + isEmpty: (value?: Value) => boolean; + values?: (value?: Value) => unknown[]; + cellToString: (data: Value, colData: Data) => string; + cellFromString: ( + data: string, + colData: Data + ) => { + value: unknown; + data?: Record; + }; + cellToJson: (data: Value, colData: Data) => DVJSON; + addGroup?: (text: string, oldData: Data) => Data; + onUpdate?: (value: Value, Data: Data, callback: () => void) => Disposable; + valueUpdate?: (value: Value, Data: Data, newValue: Value) => Value; +}; + +export type DVJSON = + | null + | number + | string + | boolean + | DVJSON[] + | { + [k: string]: DVJSON; + }; diff --git a/packages/affine/microsheet-data-view/src/core/types.ts b/packages/affine/microsheet-data-view/src/core/types.ts new file mode 100644 index 000000000000..fa804cc79280 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/types.ts @@ -0,0 +1,25 @@ +import type { TableViewSelectionWithType } from '../view-presets/table/types.js'; + +export type MicrosheetDataViewSelection = TableViewSelectionWithType; +export type GetMicrosheetDataViewSelection< + K extends MicrosheetDataViewSelection['type'], + T = MicrosheetDataViewSelection, +> = T extends { + type: K; +} + ? T + : never; +export type MicrosheetDataViewSelectionState = + | MicrosheetDataViewSelection + | undefined; +export type PropertyDataUpdater< + Data extends Record = Record, +> = (data: Data) => Partial; + +export interface MicrosheetFlags { + enable_number_formatting: boolean; +} + +export const defaultMicrosheetFlags: Readonly = { + enable_number_formatting: false, +}; diff --git a/packages/affine/microsheet-data-view/src/core/utils/drag.ts b/packages/affine/microsheet-data-view/src/core/utils/drag.ts new file mode 100644 index 000000000000..e670893c98cd --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/utils/drag.ts @@ -0,0 +1,54 @@ +export const startDrag = < + T extends Record | void, + P = { + x: number; + }, +>( + evt: MouseEvent, + ops: { + transform?: (evt: MouseEvent) => P; + onDrag: (p: P) => T; + onMove: (p: P) => T; + onDrop: (result: T) => void; + onClear: () => void; + } +) => { + const transform = ops?.transform ?? (e => e as P); + const param = transform(evt); + const result = { + data: ops.onDrag(param), + last: param, + move: (p: P) => { + result.data = ops.onMove(p); + }, + }; + const clear = () => { + window.removeEventListener('pointermove', move); + window.removeEventListener('pointerup', up); + window.removeEventListener('keydown', keydown); + ops.onClear(); + }; + const keydown = (evt: KeyboardEvent) => { + if (evt.key === 'Escape') { + clear(); + } + }; + const move = (evt: PointerEvent) => { + evt.preventDefault(); + const p = transform(evt); + result.last = p; + result.data = ops.onMove(p); + }; + const up = () => { + try { + ops.onDrop(result.data); + } finally { + clear(); + } + }; + window.addEventListener('pointermove', move); + window.addEventListener('pointerup', up); + window.addEventListener('keydown', keydown); + + return result; +}; diff --git a/packages/affine/microsheet-data-view/src/core/utils/event.ts b/packages/affine/microsheet-data-view/src/core/utils/event.ts new file mode 100644 index 000000000000..ab7001f33733 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/utils/event.ts @@ -0,0 +1,3 @@ +export function stopPropagation(event: Event) { + event.stopPropagation(); +} diff --git a/packages/affine/microsheet-data-view/src/core/utils/frame-loop.ts b/packages/affine/microsheet-data-view/src/core/utils/frame-loop.ts new file mode 100644 index 000000000000..7b55d959503f --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/utils/frame-loop.ts @@ -0,0 +1,93 @@ +export const startFrameLoop = (fn: (delta: number) => void) => { + let handle = 0; + let preTime = 0; + const run = () => { + handle = requestAnimationFrame(time => { + try { + fn(time - preTime); + } finally { + preTime = time; + run(); + } + }); + }; + run(); + return () => { + cancelAnimationFrame(handle); + }; +}; +const timeWeight = 1 / 16; +const distanceWeight = 1 / 8; +export const autoScrollOnBoundary = ( + container: HTMLElement, + ops?: { + vertical?: boolean; + horizontal?: boolean; + boundary?: + | number + | { + left?: number; + right?: number; + top?: number; + bottom?: number; + }; + onScroll?: () => void; + } +) => { + const { vertical = false, horizontal = true, boundary } = ops ?? {}; + const defaultBoundary = 20; + const { + left = defaultBoundary, + right = defaultBoundary, + top = defaultBoundary, + bottom = defaultBoundary, + } = typeof boundary === 'number' + ? { + left: boundary, + right: boundary, + top: boundary, + bottom: boundary, + } + : (boundary ?? { + left: defaultBoundary, + right: defaultBoundary, + top: defaultBoundary, + bottom: defaultBoundary, + }); + const mousePosition = { x: 0, y: 0 }; + const mouseMove = (e: MouseEvent) => { + mousePosition.x = e.clientX; + mousePosition.y = e.clientY; + }; + document.addEventListener('mousemove', mouseMove); + const scroll = (delta: number) => { + const rect = container.getBoundingClientRect(); + const { x, y } = mousePosition; + const getResult = (diff: number) => + (diff * distanceWeight + 1) * delta * timeWeight; + if (horizontal) { + const leftBound = rect.left + left; + const rightBound = rect.right - right; + if (x < leftBound) { + container.scrollLeft -= getResult(leftBound - x); + } else if (x > rightBound) { + container.scrollLeft += getResult(x - rightBound); + } + } + if (vertical) { + const topBound = rect.top + top; + const bottomBound = rect.bottom - bottom; + if (y < topBound) { + container.scrollTop -= getResult(topBound - x); + } else if (y > bottomBound) { + container.scrollTop += getResult(x - bottomBound); + } + } + ops?.onScroll?.(); + }; + const cancel = startFrameLoop(scroll); + return () => { + cancel(); + document.removeEventListener('mousemove', mouseMove); + }; +}; diff --git a/packages/affine/microsheet-data-view/src/core/utils/index.ts b/packages/affine/microsheet-data-view/src/core/utils/index.ts new file mode 100644 index 000000000000..09090f1e8c10 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/utils/index.ts @@ -0,0 +1,2 @@ +export * from './uni-component/index.js'; +export * from './uni-icon.js'; diff --git a/packages/affine/microsheet-data-view/src/core/utils/menu-title.ts b/packages/affine/microsheet-data-view/src/core/utils/menu-title.ts new file mode 100644 index 000000000000..10c118723721 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/utils/menu-title.ts @@ -0,0 +1,23 @@ +import { ArrowLeftBigIcon } from '@blocksuite/icons/lit'; +import { html } from 'lit'; + +export const menuTitle = (name: string, onBack: () => void) => { + return html` +
+
+ ${ArrowLeftBigIcon()} +
+
+ ${name} +
+
+ `; +}; diff --git a/packages/affine/microsheet-data-view/src/core/utils/uni-component/index.ts b/packages/affine/microsheet-data-view/src/core/utils/uni-component/index.ts new file mode 100644 index 000000000000..f035644582cf --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/utils/uni-component/index.ts @@ -0,0 +1,2 @@ +export * from './operation.js'; +export * from './uni-component.js'; diff --git a/packages/affine/microsheet-data-view/src/core/utils/uni-component/operation.ts b/packages/affine/microsheet-data-view/src/core/utils/uni-component/operation.ts new file mode 100644 index 000000000000..559ba1fbdb7b --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/utils/uni-component/operation.ts @@ -0,0 +1,17 @@ +import type { UniComponent } from './uni-component.js'; + +export const uniMap = >( + component: UniComponent, + map: (r: R) => T +): UniComponent => { + return (ele, props) => { + const result = component(ele, map(props)); + return { + unmount: result.unmount, + update: props => { + result.update(map(props)); + }, + expose: result.expose, + }; + }; +}; diff --git a/packages/affine/microsheet-data-view/src/core/utils/uni-component/render-template.ts b/packages/affine/microsheet-data-view/src/core/utils/uni-component/render-template.ts new file mode 100644 index 000000000000..f57d227506d0 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/utils/uni-component/render-template.ts @@ -0,0 +1,25 @@ +import type { TemplateResult } from 'lit'; + +import { ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher } from '@blocksuite/global/utils'; +import { property } from 'lit/decorators.js'; + +export class AnyRender extends SignalWatcher(ShadowlessElement) { + override render() { + return this.renderTemplate(this.props); + } + + @property({ attribute: false }) + accessor props!: T; + + @property({ attribute: false }) + accessor renderTemplate!: (props: T) => TemplateResult | symbol; +} + +export const renderTemplate = ( + renderTemplate: (props: T) => TemplateResult | symbol +) => { + const ins = new AnyRender(); + ins.renderTemplate = renderTemplate; + return ins; +}; diff --git a/packages/affine/microsheet-data-view/src/core/utils/uni-component/uni-component.ts b/packages/affine/microsheet-data-view/src/core/utils/uni-component/uni-component.ts new file mode 100644 index 000000000000..c50eac5a723a --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/utils/uni-component/uni-component.ts @@ -0,0 +1,161 @@ +import type { LitElement, PropertyValues, TemplateResult } from 'lit'; +import type { Ref } from 'lit/directives/ref.js'; + +import { ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher } from '@blocksuite/global/utils'; +import { css, html } from 'lit'; +import { property } from 'lit/decorators.js'; +import { type StyleInfo, styleMap } from 'lit/directives/style-map.js'; + +export type UniComponentReturn< + Props = NonNullable, + Expose extends NonNullable = NonNullable, +> = { + update: (props: Props) => void; + unmount: () => void; + expose: Expose; +}; +export type UniComponent< + Props = NonNullable, + Expose extends NonNullable = NonNullable, +> = (ele: HTMLElement, props: Props) => UniComponentReturn; +export const renderUniLit = >( + uni: UniComponent | undefined, + props?: Props, + options?: { + ref?: Ref; + style?: Readonly; + class?: string; + } +): TemplateResult => { + return html` `; +}; + +export class UniLit< + Props, + Expose extends NonNullable = NonNullable, +> extends ShadowlessElement { + static override styles = css` + microsheet-uni-lit { + display: contents; + } + `; + + uniReturn?: UniComponentReturn; + + get expose(): Expose | undefined { + return this.uniReturn?.expose; + } + + private mount() { + this.uniReturn = this.uni?.(this, this.props); + if (this.ref) { + // @ts-expect-error + this.ref.value = this.uniReturn?.expose; + } + } + + private unmount() { + this.uniReturn?.unmount(); + } + + override connectedCallback() { + super.connectedCallback(); + this.mount(); + } + + override disconnectedCallback() { + super.disconnectedCallback(); + this.unmount(); + } + + protected override render(): unknown { + return html``; + } + + protected override updated(_changedProperties: PropertyValues) { + super.updated(_changedProperties); + if (_changedProperties.has('uni')) { + this.unmount(); + this.mount(); + } else if (_changedProperties.has('props')) { + this.uniReturn?.update(this.props); + } + } + + @property({ attribute: false }) + accessor props!: Props; + + @property({ attribute: false }) + accessor ref: Ref | undefined = undefined; + + @property({ attribute: false }) + accessor uni: UniComponent | undefined = undefined; +} + +export const createUniComponentFromWebComponent = < + T, + Expose extends NonNullable = NonNullable, +>( + component: typeof LitElement +): UniComponent => { + return (ele, props) => { + const ins = new component(); + Object.assign(ins, props); + ele.append(ins); + return { + update: props => { + Object.assign(ins, props); + ins.requestUpdate(); + }, + unmount: () => { + ins.remove(); + }, + expose: ins as never as Expose, + }; + }; +}; + +export class UniAnyRender< + T, + Expose extends NonNullable, +> extends SignalWatcher(ShadowlessElement) { + override render() { + return this.renderTemplate(this.props, this.expose); + } + + @property({ attribute: false }) + accessor expose!: Expose; + + @property({ attribute: false }) + accessor props!: T; + + @property({ attribute: false }) + accessor renderTemplate!: (props: T, expose: Expose) => TemplateResult; +} +export const defineUniComponent = >( + renderTemplate: (props: T, expose: Expose) => TemplateResult +): UniComponent => { + return (ele, props) => { + const ins = new UniAnyRender(); + ins.props = props; + ins.expose = {} as Expose; + ins.renderTemplate = renderTemplate; + ele.append(ins); + return { + update: props => { + ins.props = props; + ins.requestUpdate(); + }, + unmount: () => { + ins.remove(); + }, + expose: ins.expose, + }; + }; +}; diff --git a/packages/affine/microsheet-data-view/src/core/utils/uni-icon.ts b/packages/affine/microsheet-data-view/src/core/utils/uni-icon.ts new file mode 100644 index 000000000000..c70fef824b41 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/utils/uni-icon.ts @@ -0,0 +1,36 @@ +import { ShadowlessElement } from '@blocksuite/block-std'; +import * as icons from '@blocksuite/icons/lit'; +import { css, html, type TemplateResult } from 'lit'; +import { property } from 'lit/decorators.js'; + +import { uniMap } from './uni-component/operation.js'; +import { createUniComponentFromWebComponent } from './uni-component/uni-component.js'; + +export class AffineLitIcon extends ShadowlessElement { + static override styles = css` + affine-microsheet-lit-icon { + display: flex; + align-items: center; + justify-content: center; + } + + affine-microsheet-lit-icon svg { + fill: var(--affine-icon-color); + } + `; + + protected override render(): unknown { + const createIcon = icons[this.name] as () => TemplateResult; + return html`${createIcon?.()}`; + } + + @property({ attribute: false }) + accessor name!: keyof typeof icons; +} + +const litIcon = createUniComponentFromWebComponent<{ name: string }>( + AffineLitIcon +); +export const createIcon = (name: keyof typeof icons) => { + return uniMap(litIcon, () => ({ name })); +}; diff --git a/packages/affine/microsheet-data-view/src/core/utils/utils.ts b/packages/affine/microsheet-data-view/src/core/utils/utils.ts new file mode 100644 index 000000000000..010d5447f2ed --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/utils/utils.ts @@ -0,0 +1,39 @@ +// source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js +function isVisible(elem: HTMLElement) { + return ( + !!elem && + !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length) + ); +} + +export function onClickOutside( + element: HTMLElement, + callback: (element: HTMLElement, target: HTMLElement) => void, + event: 'click' | 'mousedown' = 'click', + reusable = false +): () => void { + const outsideClickListener = (event: Event) => { + // support shadow dom + const path = event.composedPath && event.composedPath(); + const isOutside = path + ? path.indexOf(element) < 0 + : !element.contains(event.target as Node) && isVisible(element); + + if (!isOutside) return; + + callback(element, event.target as HTMLElement); + // if reuseable, need to manually remove the listener + if (!reusable) removeClickListener(); + }; + + document.addEventListener(event, outsideClickListener); + const removeClickListener = () => { + document.removeEventListener(event, outsideClickListener); + }; + + return removeClickListener; +} + +export const getResultInRange = (value: number, min: number, max: number) => { + return Math.max(min, Math.min(max, value)); +}; diff --git a/packages/affine/microsheet-data-view/src/core/view-manager/cell.ts b/packages/affine/microsheet-data-view/src/core/view-manager/cell.ts new file mode 100644 index 000000000000..94cd39595ade --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/view-manager/cell.ts @@ -0,0 +1,79 @@ +import { computed, type ReadonlySignal } from '@preact/signals-core'; + +import type { Property } from './property.js'; +import type { Row } from './row.js'; +import type { SingleView } from './single-view.js'; + +export interface Cell< + Value = unknown, + Data extends Record = Record, +> { + readonly rowId: string; + readonly view: SingleView; + readonly row: Row; + readonly propertyId: string; + readonly property: Property; + readonly isEmpty$: ReadonlySignal; + readonly stringValue$: ReadonlySignal; + readonly jsonValue$: ReadonlySignal; + + readonly value$: ReadonlySignal; + valueSet(value: Value | undefined): void; +} + +export class CellBase< + Value = unknown, + Data extends Record = Record, +> implements Cell +{ + meta$ = computed(() => { + return this.view.manager.dataSource.propertyMetaGet( + this.property.type$.value + ); + }); + + value$ = computed(() => { + return this.view.manager.dataSource.cellValueGet( + this.rowId, + this.propertyId + ) as Value; + }); + + isEmpty$: ReadonlySignal = computed(() => { + return this.meta$.value.config.isEmpty(this.value$.value); + }); + + jsonValue$: ReadonlySignal = computed(() => { + return this.view.cellJsonValueGet(this.rowId, this.propertyId); + }); + + property$ = computed(() => { + return this.view.propertyGet(this.propertyId) as Property; + }); + + stringValue$: ReadonlySignal = computed(() => { + return this.view.cellStringValueGet(this.rowId, this.propertyId)!; + }); + + get property(): Property { + return this.property$.value; + } + + get row(): Row { + return this.view.rowGet(this.rowId); + } + + constructor( + public view: SingleView, + public propertyId: string, + public rowId: string + ) {} + + valueSet(value: unknown | undefined): void { + this.view.manager.dataSource.cellValueChange( + this.rowId, + this.propertyId, + value + ); + } +} diff --git a/packages/affine/microsheet-data-view/src/core/view-manager/index.ts b/packages/affine/microsheet-data-view/src/core/view-manager/index.ts new file mode 100644 index 000000000000..98a19b6eb9b3 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/view-manager/index.ts @@ -0,0 +1,2 @@ +export * from './single-view.js'; +export * from './view-manager.js'; diff --git a/packages/affine/microsheet-data-view/src/core/view-manager/property.ts b/packages/affine/microsheet-data-view/src/core/view-manager/property.ts new file mode 100644 index 000000000000..7919f0fa38d6 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/view-manager/property.ts @@ -0,0 +1,165 @@ +import { computed, type ReadonlySignal } from '@preact/signals-core'; + +import type { TType } from '../logical/typesystem.js'; +import type { CellRenderer } from '../property/index.js'; +import type { PropertyDataUpdater } from '../types.js'; +import type { UniComponent } from '../utils/uni-component/index.js'; +import type { Cell } from './cell.js'; +import type { SingleView } from './single-view.js'; + +export interface Property< + Value = unknown, + Data extends Record = Record, +> { + readonly id: string; + readonly index: number; + readonly view: SingleView; + readonly isFirst: boolean; + readonly isLast: boolean; + readonly readonly$: ReadonlySignal; + readonly renderer$: ReadonlySignal; + readonly cells$: ReadonlySignal; + readonly dataType$: ReadonlySignal; + readonly icon?: UniComponent; + + readonly delete?: () => void; + readonly duplicate?: () => void; + + cellGet(rowId: string): Cell; + + readonly data$: ReadonlySignal; + dataUpdate(updater: PropertyDataUpdater): void; + + readonly type$: ReadonlySignal; + readonly typeSet?: (type: string) => void; + + readonly name$: ReadonlySignal; + nameSet(name: string): void; + + readonly hide$: ReadonlySignal; + hideSet(hide: boolean): void; + + valueGet(rowId: string): Value | undefined; + valueSet(rowId: string, value: Value | undefined): void; + + stringValueGet(rowId: string): string; + valueSetFromString(rowId: string, value: string): void; +} + +export abstract class PropertyBase< + Value = unknown, + Data extends Record = Record, +> implements Property +{ + cells$ = computed(() => { + return this.view.rows$.value.map(id => this.cellGet(id)); + }); + + data$ = computed(() => { + return this.view.propertyDataGet(this.id) as Data; + }); + + dataType$ = computed(() => { + return this.view.propertyDataTypeGet(this.id)!; + }); + + hide$ = computed(() => { + return this.view.propertyHideGet(this.id); + }); + + name$ = computed(() => { + return this.view.propertyNameGet(this.id); + }); + + readonly$ = computed(() => { + return this.view.readonly$.value || this.view.propertyReadonlyGet(this.id); + }); + + type$ = computed(() => { + return this.view.propertyTypeGet(this.id)!; + }); + + renderer$ = computed(() => { + return this.view.propertyMetaGet(this.type$.value)?.renderer.cellRenderer; + }); + + get delete(): (() => void) | undefined { + return () => this.view.propertyDelete(this.id); + } + + get duplicate(): (() => void) | undefined { + return () => this.view.propertyDuplicate(this.id); + } + + get icon(): UniComponent | undefined { + if (!this.type$.value) return undefined; + return this.view.IconGet(this.type$.value); + } + + get id(): string { + return this.propertyId; + } + + get index(): number { + return this.view.propertyIndexGet(this.id); + } + + get isFirst(): boolean { + return this.view.propertyIndexGet(this.id) === 0; + } + + get isLast(): boolean { + return ( + this.view.propertyIndexGet(this.id) === + this.view.properties$.value.length - 1 + ); + } + + constructor( + public view: SingleView, + public propertyId: string + ) {} + + cellGet(rowId: string): Cell { + return this.view.cellGet(rowId, this.id) as Cell; + } + + dataUpdate(updater: PropertyDataUpdater): void { + const data = this.data$.value; + this.view.propertyDataSet(this.id, { + ...data, + ...updater(data), + }); + } + + hideSet(hide: boolean): void { + this.view.propertyHideSet(this.id, hide); + } + + nameSet(name: string): void { + this.view.propertyNameSet(this.id, name); + } + + stringValueGet(rowId: string): string { + return this.cellGet(rowId).stringValue$.value; + } + + valueGet(rowId: string): Value | undefined { + return this.cellGet(rowId).value$.value; + } + + valueSet(rowId: string, value: Value | undefined): void { + return this.cellGet(rowId).valueSet(value); + } + + valueSetFromString(rowId: string, value: string): void { + const data = this.view.propertyParseValueFromString(this.id, value); + if (!data) { + return; + } + if (data.data) { + this.dataUpdate(() => data.data as Data); + } + this.valueSet(rowId, data.value as Value); + } +} diff --git a/packages/affine/microsheet-data-view/src/core/view-manager/row.ts b/packages/affine/microsheet-data-view/src/core/view-manager/row.ts new file mode 100644 index 000000000000..99057bc7492a --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/view-manager/row.ts @@ -0,0 +1,23 @@ +import { computed, type ReadonlySignal } from '@preact/signals-core'; + +import type { SingleView } from './single-view.js'; + +import { type Cell, CellBase } from './cell.js'; + +export interface Row { + readonly cells$: ReadonlySignal; + readonly rowId: string; +} + +export class RowBase implements Row { + cells$ = computed(() => { + return this.singleView.propertyIds$.value.map(propertyId => { + return new CellBase(this.singleView, propertyId, this.rowId); + }); + }); + + constructor( + readonly singleView: SingleView, + readonly rowId: string + ) {} +} diff --git a/packages/affine/microsheet-data-view/src/core/view-manager/single-view.ts b/packages/affine/microsheet-data-view/src/core/view-manager/single-view.ts new file mode 100644 index 000000000000..ec5bbfc1a9ab --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/view-manager/single-view.ts @@ -0,0 +1,384 @@ +import type { InsertToPosition } from '@blocksuite/affine-shared/utils'; + +import { computed, type ReadonlySignal, signal } from '@preact/signals-core'; + +import type { Variable } from '../common/ast.js'; +import type { DataViewContextKey } from '../common/data-source/context.js'; +import type { TType } from '../logical/typesystem.js'; +import type { PropertyMetaConfig } from '../property/property-config.js'; +import type { UniComponent } from '../utils/uni-component/index.js'; +import type { DataViewDataType, ViewMeta } from '../view/data-view.js'; +import type { Property } from './property.js'; +import type { ViewManager } from './view-manager.js'; + +import { type Cell, CellBase } from './cell.js'; +import { type Row, RowBase } from './row.js'; + +export type MainProperties = { + titleColumn?: string; + iconColumn?: string; + imageColumn?: string; +}; + +export interface SingleView< + ViewData extends DataViewDataType = DataViewDataType, +> { + readonly id: string; + readonly type: string; + readonly manager: ViewManager; + readonly meta: ViewMeta; + readonly readonly$: ReadonlySignal; + delete(): void; + duplicate(): void; + + data$: ReadonlySignal; + dataUpdate(updater: (viewData: ViewData) => Partial): void; + + readonly name$: ReadonlySignal; + nameSet(name: string): void; + + readonly propertyIds$: ReadonlySignal; + readonly propertiesWithoutFilter$: ReadonlySignal; + readonly properties$: ReadonlySignal; + readonly detailProperties$: ReadonlySignal; + readonly rows$: ReadonlySignal; + + readonly vars$: ReadonlySignal; + + cellValueGet(rowId: string, propertyId: string): unknown; + cellValueSet(rowId: string, propertyId: string, value: unknown): void; + + cellRefGet(rowId: string, propertyId: string): unknown; + + cellJsonValueGet(rowId: string, propertyId: string): unknown; + cellStringValueGet(rowId: string, propertyId: string): string | undefined; + cellRenderValueSet(rowId: string, propertyId: string, value: unknown): void; + cellGet(rowId: string, propertyId: string): Cell; + + propertyParseValueFromString( + propertyId: string, + value: string + ): + | { + value: unknown; + data?: Record; + } + | undefined; + + rowAdd(insertPosition: InsertToPosition): string; + rowDelete(ids: string[]): void; + rowMove(rowId: string, position: InsertToPosition): void; + rowGet(rowId: string): Row; + + rowPrevGet(rowId: string): string; + rowNextGet(rowId: string): string; + + readonly propertyMetas: PropertyMetaConfig[]; + propertyAdd(toAfterOfProperty: InsertToPosition, type?: string): string; + propertyDelete(propertyId: string): void; + propertyDuplicate(propertyId: string): void; + propertyGet(propertyId: string): Property; + propertyMetaGet(type: string): PropertyMetaConfig | undefined; + + propertyPreGet(propertyId: string): Property | undefined; + propertyNextGet(propertyId: string): Property | undefined; + + propertyNameGet(propertyId: string): string; + propertyNameSet(propertyId: string, name: string): void; + + propertyTypeGet(propertyId: string): string | undefined; + + propertyHideGet(propertyId: string): boolean; + propertyHideSet(propertyId: string, hide: boolean): void; + + propertyDataGet(propertyId: string): Record; + propertyDataSet(propertyId: string, data: Record): void; + + propertyDataTypeGet(propertyId: string): TType | undefined; + propertyIndexGet(propertyId: string): number; + propertyIdGetByIndex(index: number): string; + propertyReadonlyGet(propertyId: string): boolean; + propertyMove(propertyId: string, position: InsertToPosition): void; + + IconGet(type: string): UniComponent | undefined; + + contextGet(key: DataViewContextKey): T; + + mainProperties$: ReadonlySignal; +} + +export abstract class SingleViewBase< + ViewData extends DataViewDataType = DataViewDataType, +> implements SingleView +{ + private searchString = signal(''); + + data$ = computed(() => { + return this.dataSource.viewDataGet(this.id) as ViewData | undefined; + }); + + abstract detailProperties$: ReadonlySignal; + + abstract mainProperties$: ReadonlySignal; + + name$: ReadonlySignal = computed(() => { + return this.data$.value?.name ?? ''; + }); + + abstract propertyIds$: ReadonlySignal; + + properties$ = computed(() => { + return this.propertyIds$.value.map( + id => this.propertyGet(id) as ReturnType + ); + }); + + abstract propertiesWithoutFilter$: ReadonlySignal; + + abstract readonly$: ReadonlySignal; + + rows$ = computed(() => { + return this.dataSource.rows$.value; + }); + + vars$ = computed(() => { + return this.propertiesWithoutFilter$.value.map(id => { + const v = this.propertyGet(id); + const propertyMeta = this.dataSource.propertyMetaGet(v.type$.value); + return { + id: v.id, + name: v.name$.value, + type: propertyMeta.config.type(v.data$.value), + icon: v.icon, + }; + }); + }); + + protected get dataSource() { + return this.manager.dataSource; + } + + get meta() { + return this.dataSource.viewMetaGet(this.type); + } + + get propertyMetas(): PropertyMetaConfig[] { + return this.dataSource.propertyMetas; + } + + abstract get type(): string; + + constructor( + public manager: ViewManager, + public id: string + ) {} + + cellGet(rowId: string, propertyId: string): Cell { + return new CellBase(this, propertyId, rowId); + } + + cellJsonValueGet(rowId: string, propertyId: string): unknown { + const type = this.propertyTypeGet(propertyId); + if (!type) { + return; + } + return this.dataSource + .propertyMetaGet(type) + .config.cellToJson( + this.dataSource.cellValueGet(rowId, propertyId), + this.propertyDataGet(propertyId) + ); + } + + cellRefGet(rowId: string, propertyId: string): unknown { + const cellRef = this.dataSource.cellRefGet(rowId, propertyId); + return cellRef; + } + + cellRenderValueSet(rowId: string, propertyId: string, value: unknown): void { + this.dataSource.cellValueChange(rowId, propertyId, value); + } + + cellStringValueGet(rowId: string, propertyId: string): string | undefined { + const type = this.propertyTypeGet(propertyId); + if (!type) { + return; + } + return ( + this.dataSource + .propertyMetaGet(type) + .config.cellToString( + this.dataSource.cellValueGet(rowId, propertyId), + this.propertyDataGet(propertyId) + ) ?? '' + ); + } + + cellValueGet(rowId: string, propertyId: string): unknown { + const type = this.propertyTypeGet(propertyId); + if (!type) { + return; + } + const cellValue = this.dataSource.cellValueGet(rowId, propertyId); + return ( + this.dataSource + .propertyMetaGet(type) + .config.formatValue?.(cellValue, this.propertyDataGet(propertyId)) ?? + cellValue + ); + } + + cellValueSet(rowId: string, propertyId: string, value: unknown): void { + this.dataSource.cellValueChange(rowId, propertyId, value); + } + + contextGet(key: DataViewContextKey): T { + return this.dataSource.contextGet(key); + } + + dataUpdate(updater: (viewData: ViewData) => Partial): void { + this.dataSource.viewDataUpdate(this.id, updater); + } + + delete(): void { + this.manager.viewDelete(this.id); + } + + duplicate(): void { + this.manager.viewDuplicate(this.id); + } + + IconGet(type: string): UniComponent | undefined { + return this.dataSource.propertyMetaGet(type).renderer.icon; + } + + abstract isShow(rowId: string): boolean; + + nameSet(name: string): void { + this.dataUpdate(() => { + return { + name, + } as ViewData; + }); + } + + propertyAdd(position: InsertToPosition, type?: string): string { + const id = this.dataSource.propertyAdd(position, type); + this.propertyMove(id, position); + return id; + } + + propertyDataGet(propertyId: string): Record { + return this.dataSource.propertyDataGet(propertyId); + } + + propertyDataSet(propertyId: string, data: Record): void { + this.dataSource.propertyDataSet(propertyId, data); + } + + propertyDataTypeGet(propertyId: string): TType | undefined { + const type = this.propertyTypeGet(propertyId); + if (!type) { + return; + } + return this.dataSource + .propertyMetaGet(type) + .config.type(this.propertyDataGet(propertyId)); + } + + propertyDelete(propertyId: string): void { + this.dataSource.propertyDelete(propertyId); + } + + propertyDuplicate(propertyId: string): void { + const id = this.dataSource.propertyDuplicate(propertyId); + this.propertyMove(id, { + before: false, + id: propertyId, + }); + } + + abstract propertyGet(propertyId: string): Property; + + abstract propertyHideGet(propertyId: string): boolean; + + abstract propertyHideSet(propertyId: string, hide: boolean): void; + + propertyIdGetByIndex(index: number): string { + return this.propertyIds$.value[index]; + } + + propertyIndexGet(propertyId: string): number { + return this.propertyIds$.value.indexOf(propertyId); + } + + propertyMetaGet(type: string): PropertyMetaConfig { + return this.dataSource.propertyMetaGet(type); + } + + abstract propertyMove(propertyId: string, position: InsertToPosition): void; + + propertyNameGet(propertyId: string): string { + return this.dataSource.propertyNameGet(propertyId); + } + + propertyNameSet(propertyId: string, name: string): void { + this.dataSource.propertyNameSet(propertyId, name); + } + + propertyNextGet(propertyId: string): Property | undefined { + return this.propertyGet( + this.propertyIdGetByIndex(this.propertyIndexGet(propertyId) + 1) + ); + } + + propertyParseValueFromString(propertyId: string, cellData: string) { + const type = this.propertyTypeGet(propertyId); + if (!type) { + return; + } + return ( + this.dataSource + .propertyMetaGet(type) + .config.cellFromString(cellData, this.propertyDataGet(propertyId)) ?? '' + ); + } + + propertyPreGet(propertyId: string): Property | undefined { + return this.propertyGet( + this.propertyIdGetByIndex(this.propertyIndexGet(propertyId) - 1) + ); + } + + propertyReadonlyGet(propertyId: string): boolean { + return this.dataSource.propertyReadonlyGet(propertyId); + } + + propertyTypeGet(propertyId: string): string | undefined { + return this.dataSource.propertyTypeGet(propertyId); + } + + rowAdd(insertPosition: InsertToPosition | number): string { + return this.dataSource.rowAdd(insertPosition); + } + + rowDelete(ids: string[]): void { + this.dataSource.rowDelete(ids); + } + + rowGet(rowId: string): Row { + return new RowBase(this, rowId); + } + + rowMove(rowId: string, position: InsertToPosition): void { + this.dataSource.rowMove(rowId, position); + } + + abstract rowNextGet(rowId: string): string; + + abstract rowPrevGet(rowId: string): string; + + setSearch(str: string): void { + this.searchString.value = str; + } +} diff --git a/packages/affine/microsheet-data-view/src/core/view-manager/view-manager.ts b/packages/affine/microsheet-data-view/src/core/view-manager/view-manager.ts new file mode 100644 index 000000000000..236e61e0eeed --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/view-manager/view-manager.ts @@ -0,0 +1,103 @@ +import type { InsertToPosition } from '@blocksuite/affine-shared/utils'; + +import { nanoid } from '@blocksuite/store'; +import { computed, type ReadonlySignal, signal } from '@preact/signals-core'; + +import type { DataSource } from '../common/data-source/base.js'; +import type { + DataViewDataType, + DataViewMode, + ViewMeta, +} from '../view/data-view.js'; +import type { SingleView } from './single-view.js'; + +export interface ViewManager { + viewMetas: ViewMeta[]; + dataSource: DataSource; + readonly$: ReadonlySignal; + + currentViewId$: ReadonlySignal; + currentView$: ReadonlySignal; + + setCurrentView(id: string): void; + + views$: ReadonlySignal; + + viewGet(id: string): SingleView; + + viewAdd(type: DataViewMode): string; + + viewDelete(id: string): void; + + viewDuplicate(id: string): void; + + viewDataGet(id: string): DataViewDataType | undefined; + + moveTo(id: string, position: InsertToPosition): void; +} + +export class ViewManagerBase implements ViewManager { + _currentViewId$ = signal(undefined); + + views$ = computed(() => { + return this.dataSource.viewDataList$.value.map(data => data.id); + }); + + currentViewId$ = computed(() => { + return this._currentViewId$.value ?? this.views$.value[0]; + }); + + currentView$ = computed(() => { + return this.viewGet(this.currentViewId$.value); + }); + + readonly$ = computed(() => { + return this.dataSource.readonly$.value; + }); + + get viewMetas() { + return this.dataSource.viewMetas; + } + + constructor(public dataSource: DataSource) {} + + moveTo(id: string, position: InsertToPosition): void { + this.dataSource.viewDataMoveTo(id, position); + } + + setCurrentView(id: string): void { + this._currentViewId$.value = id; + } + + viewAdd(type: DataViewMode): string { + const meta = this.dataSource.viewMetaGet(type); + const data = meta.model.defaultData(this); + const id = this.dataSource.viewDataAdd({ + ...data, + id: nanoid(), + name: meta.model.defaultName, + mode: type, + }); + this.setCurrentView(id); + return id; + } + + viewDataGet(id: string): DataViewDataType | undefined { + return this.dataSource.viewDataGet(id); + } + + viewDelete(id: string): void { + this.dataSource.viewDataDelete(id); + this.setCurrentView(this.views$.value[0]); + } + + viewDuplicate(id: string): void { + const newId = this.dataSource.viewDataDuplicate(id); + this.setCurrentView(newId); + } + + viewGet(id: string): SingleView { + const meta = this.dataSource.viewMetaGetById(id); + return new meta.model.dataViewManager(this, id); + } +} diff --git a/packages/affine/microsheet-data-view/src/core/view/convert.ts b/packages/affine/microsheet-data-view/src/core/view/convert.ts new file mode 100644 index 000000000000..9ad392c23af9 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/view/convert.ts @@ -0,0 +1,27 @@ +import type { DataViewModel, GetDataFromDataViewModel } from './data-view.js'; + +export type ViewConvertFunction< + From extends DataViewModel = DataViewModel, + To extends DataViewModel = DataViewModel, +> = ( + data: GetDataFromDataViewModel +) => Partial>; +export type ViewConvertConfig = { + from: string; + to: string; + convert: ViewConvertFunction; +}; +export const createViewConvert = < + From extends DataViewModel, + To extends DataViewModel, +>( + from: From, + to: To, + convert: ViewConvertFunction +): ViewConvertConfig => { + return { + from: from.type, + to: to.type, + convert, + }; +}; diff --git a/packages/affine/microsheet-data-view/src/core/view/data-view-base.ts b/packages/affine/microsheet-data-view/src/core/view/data-view-base.ts new file mode 100644 index 000000000000..244a57e490a6 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/view/data-view-base.ts @@ -0,0 +1,17 @@ +import { ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { property } from 'lit/decorators.js'; + +import type { MicrosheetDataViewSelection } from '../types.js'; +import type { SingleView } from '../view-manager/single-view.js'; +import type { DataViewExpose, DataViewProps } from './types.js'; + +export abstract class DataViewBase< + T extends SingleView = SingleView, + Selection extends MicrosheetDataViewSelection = MicrosheetDataViewSelection, +> extends SignalWatcher(WithDisposable(ShadowlessElement)) { + abstract expose: DataViewExpose; + + @property({ attribute: false }) + accessor props!: DataViewProps; +} diff --git a/packages/affine/microsheet-data-view/src/core/view/data-view.ts b/packages/affine/microsheet-data-view/src/core/view/data-view.ts new file mode 100644 index 000000000000..f538fc0b1ba8 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/view/data-view.ts @@ -0,0 +1,77 @@ +import type { UniComponent } from '../utils/uni-component/index.js'; +import type { SingleView } from '../view-manager/single-view.js'; +import type { ViewManager } from '../view-manager/view-manager.js'; +import type { DataViewExpose, DataViewProps } from './types.js'; + +export type BasicViewDataType< + Type extends string = string, + T = NonNullable, +> = { + id: string; + name: string; + mode: Type; +} & T; + +export type DefaultViewDataType = BasicViewDataType & { + mode: string; +}; + +export type DataViewDataType = DefaultViewDataType; + +export type DataViewMode = string; + +export interface DataViewModelConfig< + Data extends DataViewDataType = DataViewDataType, +> { + defaultName: string; + dataViewManager: new ( + viewManager: ViewManager, + viewId: string + ) => SingleView; + defaultData: (viewManager: ViewManager) => Omit; +} + +export type DataViewModel< + Type extends string = DataViewMode, + Data extends DataViewDataType = DataViewDataType, +> = { + type: Type; + model: DataViewModelConfig; +}; + +export type GetDataFromDataViewModel = + Model extends DataViewModel ? R : never; + +export interface DataViewRendererConfig { + view: UniComponent< + { + props: DataViewProps; + }, + { expose: DataViewExpose } + >; + icon: UniComponent; +} + +export type ViewMeta< + Type extends string = DataViewMode, + Data extends DataViewDataType = DataViewDataType, +> = DataViewModel & { + renderer: DataViewRendererConfig; +}; + +export const viewType = (type: Type) => ({ + type, + createModel: ( + model: DataViewModelConfig + ): DataViewModel & { + createMeta: (renderer: DataViewRendererConfig) => ViewMeta; + } => ({ + type, + model, + createMeta: renderer => ({ + type, + model, + renderer, + }), + }), +}); diff --git a/packages/affine/microsheet-data-view/src/core/view/index.ts b/packages/affine/microsheet-data-view/src/core/view/index.ts new file mode 100644 index 000000000000..3b38c96b44c8 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/view/index.ts @@ -0,0 +1,3 @@ +export * from './convert.js'; +export * from './data-view.js'; +export * from './types.js'; diff --git a/packages/affine/microsheet-data-view/src/core/view/types.ts b/packages/affine/microsheet-data-view/src/core/view/types.ts new file mode 100644 index 000000000000..b8429cf7f342 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/view/types.ts @@ -0,0 +1,54 @@ +import type { InsertToPosition } from '@blocksuite/affine-shared/utils'; +import type { + BlockStdScope, + EventName, + UIEventHandler, +} from '@blocksuite/block-std'; +import type { Disposable } from '@blocksuite/global/utils'; +import type { ReadonlySignal } from '@preact/signals-core'; + +import type { DataSource } from '../common/index.js'; +import type { DataViewRenderer } from '../data-view.js'; +import type { MicrosheetDataViewSelection } from '../types.js'; +import type { SingleView } from '../view-manager/index.js'; +import type { MicrosheetDataViewWidget } from '../widget/index.js'; + +export interface DataViewProps< + T extends SingleView = SingleView, + Selection extends MicrosheetDataViewSelection = MicrosheetDataViewSelection, +> { + dataViewEle: DataViewRenderer; + + headerWidget?: MicrosheetDataViewWidget; + + view: T; + dataSource: DataSource; + + bindHotkey: (hotkeys: Record) => Disposable; + + handleEvent: (name: EventName, handler: UIEventHandler) => Disposable; + + setSelection: (selection?: Selection) => void; + + selection$: ReadonlySignal; + + virtualPadding$: ReadonlySignal; + + onDrag?: (evt: MouseEvent, id: string) => () => void; + + std: BlockStdScope; +} + +export interface DataViewExpose { + addRow?(position: InsertToPosition | number): void; + + getSelection?(): MicrosheetDataViewSelection | undefined; + + focusFirstCell(): void; + + showIndicator?(evt: MouseEvent): boolean; + + hideIndicator?(): void; + + moveTo?(id: string, evt: MouseEvent): void; +} diff --git a/packages/affine/microsheet-data-view/src/core/widget/index.ts b/packages/affine/microsheet-data-view/src/core/widget/index.ts new file mode 100644 index 000000000000..d4702960d547 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/widget/index.ts @@ -0,0 +1 @@ +export * from './types.js'; diff --git a/packages/affine/microsheet-data-view/src/core/widget/types.ts b/packages/affine/microsheet-data-view/src/core/widget/types.ts new file mode 100644 index 000000000000..dfadc3ca6d0a --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/widget/types.ts @@ -0,0 +1,10 @@ +import type { UniComponent } from '../utils/uni-component/index.js'; +import type { DataViewExpose } from '../view/types.js'; +import type { SingleView } from '../view-manager/single-view.js'; + +export type MicrosheetDataViewWidgetProps = { + view: SingleView; + viewMethods: DataViewExpose; +}; +export type MicrosheetDataViewWidget = + UniComponent; diff --git a/packages/affine/microsheet-data-view/src/core/widget/widget-base.ts b/packages/affine/microsheet-data-view/src/core/widget/widget-base.ts new file mode 100644 index 000000000000..6eb1925a9024 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/core/widget/widget-base.ts @@ -0,0 +1,26 @@ +import { ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { property } from 'lit/decorators.js'; + +import type { DataViewExpose } from '../view/types.js'; +import type { SingleView } from '../view-manager/single-view.js'; +import type { MicrosheetDataViewWidgetProps } from './types.js'; + +export class WidgetBase + extends SignalWatcher(WithDisposable(ShadowlessElement)) + implements MicrosheetDataViewWidgetProps +{ + get dataSource() { + return this.view.manager.dataSource; + } + + get viewManager() { + return this.view.manager; + } + + @property({ attribute: false }) + accessor view!: SingleView; + + @property({ attribute: false }) + accessor viewMethods!: DataViewExpose; +} diff --git a/packages/affine/microsheet-data-view/src/effects.ts b/packages/affine/microsheet-data-view/src/effects.ts new file mode 100644 index 000000000000..9be84ff18b4d --- /dev/null +++ b/packages/affine/microsheet-data-view/src/effects.ts @@ -0,0 +1,96 @@ +import { Overflow } from './core/common/component/overflow/overflow.js'; +import { RecordDetail } from './core/common/detail/detail.js'; +import { RecordField } from './core/common/detail/field.js'; +import { + NumberLiteral, + StringLiteral, +} from './core/common/literal/renderer/literal-element.js'; +import { DataViewPropertiesSettingView } from './core/common/properties.js'; +import { VariableRefView } from './core/common/ref/ref.js'; +import { DataViewRenderer } from './core/data-view.js'; +import { AffineLitIcon, UniAnyRender, UniLit } from './core/index.js'; +import { AnyRender } from './core/utils/uni-component/render-template.js'; +import { + TextCell, + TextCellEditing, +} from './property-presets/text/cell-renderer.js'; +import { DataViewTable } from './view-presets/index.js'; +import { MicrosheetCellContainer } from './view-presets/table/cell.js'; +import { DragToFillElement } from './view-presets/table/controller/drag-to-fill.js'; +import { SelectionElement } from './view-presets/table/controller/selection.js'; +import { TableGroup } from './view-presets/table/group.js'; +import { MicrosheetColumnHeader } from './view-presets/table/header/column-header.js'; +import { DataViewColumnPreview } from './view-presets/table/header/column-renderer.js'; +import { MicrosheetHeaderColumn } from './view-presets/table/header/microsheet-header-column.js'; +import { TableVerticalIndicator } from './view-presets/table/header/vertical-indicator.js'; +import { TableRow } from './view-presets/table/row/row.js'; +import { RowSelectCheckbox } from './view-presets/table/row/row-select-checkbox.js'; +import { DataViewHeaderTools } from './widget-presets/tools/tools-renderer.js'; + +export function effects() { + customElements.define( + 'microsheet-data-view-header-tools', + DataViewHeaderTools + ); + customElements.define( + 'affine-microsheet-cell-container', + MicrosheetCellContainer + ); + customElements.define( + 'affine-microsheet-data-view-renderer', + DataViewRenderer + ); + customElements.define('microsheet-any-render', AnyRender); + customElements.define( + 'microsheet-data-view-properties-setting', + DataViewPropertiesSettingView + ); + customElements.define('affine-microsheet-text-cell', TextCell); + customElements.define('affine-microsheet-text-cell-editing', TextCellEditing); + customElements.define( + 'affine-microsheet-data-view-record-field', + RecordField + ); + customElements.define('microsheet-data-view-drag-to-fill', DragToFillElement); + customElements.define('affine-microsheet-data-view-table-group', TableGroup); + customElements.define( + 'affine-microsheet-data-view-column-preview', + DataViewColumnPreview + ); + customElements.define('microsheet-component-overflow', Overflow); + customElements.define('affine-microsheet-lit-icon', AffineLitIcon); + customElements.define( + 'microsheet-data-view-literal-number-view', + NumberLiteral + ); + customElements.define( + 'microsheet-data-view-literal-string-view', + StringLiteral + ); + customElements.define('affine-microsheet-table', DataViewTable); + customElements.define('microsheet-uni-lit', UniLit); + customElements.define('microsheet-uni-any-render', UniAnyRender); + customElements.define( + 'microsheet-data-view-table-selection', + SelectionElement + ); + customElements.define('microsheet-variable-ref-view', VariableRefView); + customElements.define( + 'affine-microsheet-data-view-record-detail', + RecordDetail + ); + customElements.define( + 'affine-microsheet-column-header', + MicrosheetColumnHeader + ); + customElements.define( + 'affine-microsheet-header-column', + MicrosheetHeaderColumn + ); + customElements.define('microsheet-row-select-checkbox', RowSelectCheckbox); + customElements.define( + 'microsheet-data-view-table-vertical-indicator', + TableVerticalIndicator + ); + customElements.define('microsheet-data-view-table-row', TableRow); +} diff --git a/packages/affine/microsheet-data-view/src/index.ts b/packages/affine/microsheet-data-view/src/index.ts new file mode 100644 index 000000000000..17f45946d4e5 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/index.ts @@ -0,0 +1 @@ +export * from './core/index.js'; diff --git a/packages/affine/microsheet-data-view/src/property-presets/index.ts b/packages/affine/microsheet-data-view/src/property-presets/index.ts new file mode 100644 index 000000000000..f109b1d7207c --- /dev/null +++ b/packages/affine/microsheet-data-view/src/property-presets/index.ts @@ -0,0 +1,5 @@ +import { textPropertyConfig } from './text/cell-renderer.js'; + +export const propertyPresets = { + textPropertyConfig, +}; diff --git a/packages/affine/microsheet-data-view/src/property-presets/pure-index.ts b/packages/affine/microsheet-data-view/src/property-presets/pure-index.ts new file mode 100644 index 000000000000..069db2290174 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/property-presets/pure-index.ts @@ -0,0 +1,5 @@ +import { textPropertyModelConfig } from './text/define.js'; + +export const propertyModelPresets = { + textPropertyModelConfig, +}; diff --git a/packages/affine/microsheet-data-view/src/property-presets/text/cell-renderer.ts b/packages/affine/microsheet-data-view/src/property-presets/text/cell-renderer.ts new file mode 100644 index 000000000000..99223a2c3b65 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/property-presets/text/cell-renderer.ts @@ -0,0 +1,120 @@ +import { baseTheme } from '@toeverything/theme'; +import { css, html, unsafeCSS } from 'lit'; +import { query } from 'lit/decorators.js'; + +import { BaseCellRenderer } from '../../core/property/index.js'; +import { createFromBaseCellRenderer } from '../../core/property/renderer.js'; +import { createIcon } from '../../core/utils/uni-icon.js'; +import { textPropertyModelConfig } from './define.js'; + +export class TextCell extends BaseCellRenderer { + static override styles = css` + affine-microsheet-text-cell { + display: block; + width: 100%; + height: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .affine-microsheet-text { + display: flex; + align-items: center; + height: 100%; + width: 100%; + padding: 0; + border: none; + font-family: ${unsafeCSS(baseTheme.fontSansFamily)}; + font-size: var(--affine-font-base); + line-height: var(--affine-line-height); + color: var(--affine-text-primary-color); + font-weight: 400; + background-color: transparent; + } + `; + + override render() { + return html`
${this.value ?? ''}
`; + } +} +export class TextCellEditing extends BaseCellRenderer { + static override styles = css` + affine-microsheet-text-cell-editing { + display: block; + width: 100%; + height: 100%; + cursor: text; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .affine-microsheet-text { + display: flex; + align-items: center; + height: 100%; + width: 100%; + padding: 0; + border: none; + font-family: ${unsafeCSS(baseTheme.fontSansFamily)}; + font-size: var(--affine-font-base); + line-height: var(--affine-line-height); + color: var(--affine-text-primary-color); + font-weight: 400; + background-color: transparent; + } + + .affine-microsheet-text:focus { + outline: none; + } + `; + + private _keydown = (e: KeyboardEvent) => { + if (e.key === 'Enter' && !e.isComposing) { + this._setValue(); + setTimeout(() => { + this.selectCurrentCell(false); + }); + } + }; + + private _setValue = (str: string = this._inputEle.value) => { + this._inputEle.value = `${this.value ?? ''}`; + this.onChange(str); + }; + + focusEnd = () => { + const end = this._inputEle.value.length; + this._inputEle.focus(); + this._inputEle.setSelectionRange(end, end); + }; + + override firstUpdated() { + this.focusEnd(); + } + + override onExitEditMode() { + this._setValue(); + } + + override render() { + return html``; + } + + @query('input') + private accessor _inputEle!: HTMLInputElement; +} + +export const textPropertyConfig = textPropertyModelConfig.createPropertyMeta({ + icon: createIcon('TextIcon'), + + cellRenderer: { + view: createFromBaseCellRenderer(TextCell), + edit: createFromBaseCellRenderer(TextCellEditing), + }, +}); diff --git a/packages/affine/microsheet-data-view/src/property-presets/text/define.ts b/packages/affine/microsheet-data-view/src/property-presets/text/define.ts new file mode 100644 index 000000000000..258390ac4323 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/property-presets/text/define.ts @@ -0,0 +1,18 @@ +import { tString } from '../../core/logical/data-type.js'; +import { propertyType } from '../../core/property/property-config.js'; + +export const textPropertyType = propertyType('text'); + +export const textPropertyModelConfig = textPropertyType.modelConfig({ + name: 'Plain-Text', + type: () => tString.create(), + defaultData: () => ({}), + cellToString: data => data ?? '', + cellFromString: data => { + return { + value: data, + }; + }, + cellToJson: data => data ?? null, + isEmpty: data => data == null || data.length === 0, +}); diff --git a/packages/affine/microsheet-data-view/src/view-presets/index.ts b/packages/affine/microsheet-data-view/src/view-presets/index.ts new file mode 100644 index 000000000000..211281f14539 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/index.ts @@ -0,0 +1,7 @@ +import { tableViewMeta } from './table/index.js'; + +export * from './table/index.js'; + +export const viewPresets = { + tableViewMeta: tableViewMeta, +}; diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/cell.ts b/packages/affine/microsheet-data-view/src/view-presets/table/cell.ts new file mode 100644 index 000000000000..d4c3c1c46e32 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/cell.ts @@ -0,0 +1,191 @@ +import type { RichText } from '@blocksuite/affine-components/rich-text'; + +import { type BlockStdScope, ShadowlessElement } from '@blocksuite/block-std'; +import { + assertExists, + noop, + SignalWatcher, + WithDisposable, +} from '@blocksuite/global/utils'; +import { computed } from '@preact/signals-core'; +import { css, html, nothing } from 'lit'; +import { property, state } from 'lit/decorators.js'; +import { createRef } from 'lit/directives/ref.js'; + +import type { DataViewCellLifeCycle } from '../../core/property/index.js'; +import type { SingleView } from '../../core/view-manager/single-view.js'; +import type { TableGroup } from './group.js'; +import type { TableColumn } from './table-view-manager.js'; +import type { TableViewSelectionWithType } from './types.js'; + +export class MicrosheetCellContainer extends SignalWatcher( + WithDisposable(ShadowlessElement) +) { + static override styles = css` + affine-microsheet-cell-container { + display: flex; + align-items: start; + width: 100%; + height: 100%; + border: none; + outline: none; + padding: 2px 8px; + } + + affine-microsheet-cell-container * { + box-sizing: border-box; + } + + affine-microsheet-cell-container microsheet-uni-lit > *:first-child { + padding: 8px; + } + `; + + private _cell = createRef(); + + @property({ attribute: false }) + accessor column!: TableColumn; + + @property({ attribute: false }) + accessor rowId!: string; + + cell$ = computed(() => { + return this.column.cellGet(this.rowId); + }); + + selectCurrentCell = (focusTo?: 'start' | 'end') => { + if (this.view.readonly$.value) { + return; + } + const selectionView = this.selectionView; + if (selectionView) { + if (selectionView) { + this.selectionView.focus = { + rowIndex: this.rowIndex, + columnIndex: this.columnIndex, + }; + } + + assertExists(this.refModel); + + const focus = () => { + if (focusTo && this.std) { + const richTexts = this.querySelectorAll('rich-text'); + + if (richTexts.length) { + if (focusTo === 'start') { + (richTexts[0] as RichText).inlineEditor?.focusStart(); + } else { + richTexts[richTexts.length - 1].inlineEditor?.focusEnd(); + } + } + } + }; + + if (this.refModel.children.length === 0) { + this.std.doc.addBlock( + 'affine:paragraph', + { + text: new this.std.doc.Text(), + }, + this.refModel + ); + void this.updateComplete + .then(() => { + focus(); + }) + .catch(noop); + } else { + focus(); + } + } + }; + + get cell(): DataViewCellLifeCycle | undefined { + return this._cell.value; + } + + private get groupKey() { + return ( + this.closest('affine-microsheet-data-view-table-group') as TableGroup + )?.group?.key; + } + + get refModel() { + const refId = this.view.cellRefGet(this.rowId, this.column.id); + if (!refId) return; + return this.std.doc.getBlockById(refId as string); + } + + private get selectionView() { + return this.closest('affine-microsheet-table')?.selectionController; + } + + get table() { + const table = this.closest('affine-microsheet-table'); + assertExists(table); + return table; + } + + override connectedCallback() { + super.connectedCallback(); + this._disposables.addFromEvent(this, 'click', (e: UIEvent) => { + if (!this.isEditing) { + if ( + e.target && + e.target instanceof HTMLElement && + e.target.tagName === 'AFFINE-MICROSHEET-CELL-CONTAINER' + ) { + this.selectCurrentCell('end'); + } else { + this.selectCurrentCell(); + } + // this.selectCurrentCell(); + } + }); + } + + isSelected(selection: TableViewSelectionWithType) { + if (selection.selectionType !== 'area') { + return false; + } + if (selection.groupKey !== this.groupKey) { + return; + } + if (selection.focus.columnIndex !== this.columnIndex) { + return; + } + return selection.focus.rowIndex === this.rowIndex; + } + + override render() { + if (!this.std) return nothing; + + assertExists(this.refModel); + return html``; + } + + @property({ attribute: false }) + accessor columnId!: string; + + @property({ attribute: false }) + accessor columnIndex!: number; + + @state() + accessor isEditing = false; + + @property({ attribute: false }) + accessor rowIndex!: number; + + @property({ attribute: false }) + accessor std!: BlockStdScope; + + @property({ attribute: false }) + accessor view!: SingleView; +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-microsheet-cell-container': MicrosheetCellContainer; + } +} diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/consts.ts b/packages/affine/microsheet-data-view/src/view-presets/table/consts.ts new file mode 100644 index 000000000000..f42e53e0a303 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/consts.ts @@ -0,0 +1,21 @@ +/** column default width */ +export const DEFAULT_COLUMN_WIDTH = 180; +/** column min width */ +export const DEFAULT_COLUMN_MIN_WIDTH = 100; +/** column title height */ +export const DEFAULT_COLUMN_TITLE_HEIGHT = 10; +/** column title height */ +export const DEFAULT_ADD_BUTTON_WIDTH = 40; +export const LEFT_TOOL_BAR_WIDTH = 24; +export const STATS_BAR_HEIGHT = 34; + +/** microseet column default width */ +export const DEFAULT_MICROSHEET_COLUMN_WIDTH = 100; +/** microseet column min width */ +export const DEFAULT_MICROSHEET_COLUMN_MIN_WIDTH = 100; +/** microseet column title height */ +export const DEFAULT_MICROSHEET_COLUMN_TITLE_HEIGHT = 10; +/** microseet column title height */ +export const DEFAULT_MICROSHEET_ADD_BUTTON_WIDTH = 40; +export const MICROSHEET_LEFT_TOOL_BAR_WIDTH = 24; +export const MICROSHEET_STATS_BAR_HEIGHT = 34; diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/controller/clipboard.ts b/packages/affine/microsheet-data-view/src/view-presets/table/controller/clipboard.ts new file mode 100644 index 000000000000..291cd233285c --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/controller/clipboard.ts @@ -0,0 +1,352 @@ +import type { UIEventStateContext } from '@blocksuite/block-std'; +import type { ReactiveController } from 'lit'; + +import { toast } from '@blocksuite/affine-components/toast'; +import { Slice } from '@blocksuite/store'; + +import type { Cell } from '../../../core/view-manager/cell.js'; +import type { Row } from '../../../core/view-manager/row.js'; +import type { MicrosheetCellContainer } from '../cell.js'; +import type { DataViewTable } from '../table-view.js'; + +import { + TableAreaSelection, + TableRowSelection, + type TableViewSelection, + type TableViewSelectionWithType, +} from '../types.js'; + +const BLOCKSUITE_MICROSHEET_TABLE = 'blocksuite/microsheet'; +const TEXT = 'text/plain'; + +export class TableClipboardController implements ReactiveController { + private _onCopy = async ( + tableSelection: TableViewSelectionWithType, + isCut = false + ) => { + const table = this.host; + + const area = getSelectedAreaValues(tableSelection, table); + if (!area) { + return; + } + + const promiseArr: Promise[] = []; + area.forEach(row => { + row.forEach(cell => { + promiseArr.push( + (async () => { + const cellContainerModel = this.std.doc.getBlockById( + cell.ref as string + ); + if (cellContainerModel) { + const slice = Slice.fromModels( + this.std.doc, + cellContainerModel.children + ); + // @ts-expect-error + const item = await this.std.clipboard._getClipboardItem( + slice, + 'BLOCKSUITE/SNAPSHOT' + ); + cell['cellContainerSlice'] = item as string; + if (isCut) { + const children = cellContainerModel.children; + children.forEach(b => this.std.doc.deleteBlock(b)); + this.std.doc.addBlock( + 'affine:paragraph', + {}, + cellContainerModel.id + ); + } + } + })() + ); + }); + }); + await Promise.all(promiseArr); + this.std.clipboard + .writeToClipboard(items => { + return { + ...items, + [TEXT]: 'microsheet-copy-block', + [BLOCKSUITE_MICROSHEET_TABLE]: JSON.stringify(area), + }; + }) + .then(() => { + const count = area.flatMap(row => row).length; + toast( + this.std.host, + `${count} cell${count > 1 ? 's' : ''} copied to clipboard` + ); + }) + .catch(console.error); + + return true; + }; + + private _onCut = (tableSelection: TableViewSelectionWithType) => { + this._onCopy(tableSelection, true) + .then() + .catch(() => {}); + }; + + private _onPaste = async (_context: UIEventStateContext) => { + const event = _context.get('clipboardState').raw; + event.stopPropagation(); + const view = this.host; + + const clipboardData = event.clipboardData; + if (!clipboardData) return; + + const tableSelection = this.host.selectionController.selection; + if (TableRowSelection.is(tableSelection)) { + return; + } + if (tableSelection) { + const json = await this.std.clipboard.readFromClipboard(clipboardData); + const copiedValues = json[BLOCKSUITE_MICROSHEET_TABLE]; + if (!copiedValues) return; + const jsonAreaData = JSON.parse(copiedValues) as CopyedSelectionData; + this.pasteToCells(view, jsonAreaData, tableSelection); + } else if (this.host.selectionController.focus) { + const json = await this.std.clipboard.readFromClipboard(clipboardData); + const copiedValues = json[BLOCKSUITE_MICROSHEET_TABLE]; + if (!copiedValues) return; + const copyedSelectionData = JSON.parse( + copiedValues + ) as CopyedSelectionData; + const rowStartIndex = this.host.selectionController.focus.rowIndex; + const columnStartIndex = this.host.selectionController.focus.columnIndex; + const rowLength = copyedSelectionData.length; + const columnLength = copyedSelectionData[0].length; + const tableAreaSelection = TableAreaSelection.create({ + focus: { + rowIndex: rowStartIndex, + columnIndex: columnStartIndex, + }, + rowsSelection: { + start: rowStartIndex, + end: rowStartIndex + rowLength - 1, + }, + columnsSelection: { + start: columnStartIndex, + end: columnStartIndex + columnLength - 1, + }, + isEditing: false, + }); + this.pasteToCells(view, copyedSelectionData, tableAreaSelection); + } + + return true; + }; + + get props() { + return this.host.props; + } + + private get readonly() { + return this.props.view.readonly$.value; + } + + private get std() { + return this.props.std; + } + + constructor(public host: DataViewTable) { + host.addController(this); + } + + private pasteToCells( + table: DataViewTable, + copied: CopyedSelectionData, + tableAreaSelection: TableAreaSelection + ) { + const { view } = table.props; + for ( + let i = 0; + i <= + tableAreaSelection.rowsSelection.end - + tableAreaSelection.rowsSelection.start; + i++ + ) { + for ( + let j = 0; + j <= + tableAreaSelection.columnsSelection.end - + tableAreaSelection.columnsSelection.start; + j++ + ) { + const copyCell = copied?.[i]?.[j]; + if (!copyCell) continue; + const targetContainer = table.selectionController.getCellContainer( + tableAreaSelection.groupKey, + i + tableAreaSelection.rowsSelection.start, + j + tableAreaSelection.columnsSelection.start + ); + const rowId = targetContainer?.dataset.rowId; + const columnId = targetContainer?.dataset.columnId; + if (rowId && columnId) { + const { cellContainerSlice } = copyCell; + const targetCellContainerId = view.cellRefGet( + rowId, + columnId + ) as string; + if (targetCellContainerId) { + const cellContainerBlock = this.std.doc.getBlockById( + targetCellContainerId + ); + if (cellContainerBlock) { + const children = cellContainerBlock.children; + children.forEach(b => { + this.std.doc.deleteBlock(b); + }); + } + (async () => { + await this.std.clipboard.pasteCellSliceSnapshot( + JSON.parse(cellContainerSlice as string)?.snapshot, + this.std.doc, + targetCellContainerId + ); + })() + .then() + .catch(() => {}); + } + } + } + } + } + + copy() { + const tableSelection = this.host.selectionController.selection; + if (!tableSelection) { + return; + } + this._onCopy(tableSelection).catch(console.error); + } + + cut() { + const tableSelection = this.host.selectionController.selection; + if (!tableSelection) { + return; + } + this._onCopy(tableSelection, true).catch(err => console.log(err)); + } + + hostConnected() { + this.host.disposables.add( + this.props.handleEvent('copy', _ctx => { + const tableSelection = this.host.selectionController.selection; + if (!tableSelection) return false; + + this._onCopy(tableSelection).catch(console.error); + return true; + }) + ); + + this.host.disposables.add( + this.props.handleEvent('cut', _ctx => { + const tableSelection = this.host.selectionController.selection; + if (!tableSelection) return false; + + this._onCut(tableSelection); + return true; + }) + ); + + this.host.disposables.add( + this.props.handleEvent('paste', ctx => { + if (this.readonly) return false; + + this._onPaste(ctx).catch(console.error); + return true; + }) + ); + } +} + +function getSelectedAreaValues( + selection: TableViewSelection, + table: DataViewTable +): { ref: string; cellContainerSlice?: string }[][] { + const view = table.props.view; + const rsl: { ref: string; cellContainerSlice?: string }[][] = []; + const values = getSelectedArea(selection, table); + values?.forEach((row, index) => { + const cells = row.cells; + if (!rsl[index]) { + rsl[index] = []; + } + cells.forEach(cell => { + rsl[index].push({ + ref: view.cellRefGet(cell.rowId, cell.propertyId) as string, + }); + }); + }); + return rsl; +} +function getSelectedArea( + selection: TableViewSelection, + table: DataViewTable +): SelectedArea | undefined { + const view = table.props.view; + if (TableRowSelection.is(selection)) { + const rows = TableRowSelection.rows(selection) + .map(row => { + const y = + table.selectionController + .getRow(row.groupKey, row.id) + ?.getBoundingClientRect().y ?? 0; + return { + y, + row, + }; + }) + .sort((a, b) => a.y - b.y) + .map(v => v.row); + return rows.map(r => { + const row = view.rowGet(r.id); + return { + row, + cells: row.cells$.value, + }; + }); + } + const { rowsSelection, columnsSelection, groupKey } = selection; + const data: SelectedArea = []; + const rows = groupKey + ? view.groupManager.groupDataMap$.value?.[groupKey].rows + : view.rows$.value; + const columns = view.propertyIds$.value; + if (!rows) { + return; + } + for (let i = rowsSelection.start; i <= rowsSelection.end; i++) { + const row: SelectedArea[number] = { + cells: [], + }; + const rowId = rows[i]; + for (let j = columnsSelection.start; j <= columnsSelection.end; j++) { + const columnId = columns[j]; + const cell = view.cellGet(rowId, columnId); + row.cells.push(cell); + } + data.push(row); + } + + return data; +} + +type SelectedArea = { + row?: Row; + cells: Cell[]; +}[]; + +type CopyedColumn = { + type: string; + value: string; + ref: unknown; + cellContainerSlice?: unknown; + container?: MicrosheetCellContainer; +}; +type CopyedSelectionData = CopyedColumn[][]; diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/controller/drag-to-fill.ts b/packages/affine/microsheet-data-view/src/view-presets/table/controller/drag-to-fill.ts new file mode 100644 index 000000000000..f2a1c00ffceb --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/controller/drag-to-fill.ts @@ -0,0 +1,111 @@ +import { ShadowlessElement } from '@blocksuite/block-std'; +import { assertEquals } from '@blocksuite/global/utils'; +import { DocCollection, type Text } from '@blocksuite/store'; +import { css, html } from 'lit'; +import { state } from 'lit/decorators.js'; +import { createRef, ref } from 'lit/directives/ref.js'; + +import type { DataViewTable } from '../table-view.js'; +import type { TableAreaSelection } from '../types.js'; + +import { tRichText } from '../../../core/logical/data-type.js'; + +export class DragToFillElement extends ShadowlessElement { + static override styles = css` + .drag-to-fill { + border-radius: 50%; + box-sizing: border-box; + background-color: var(--affine-background-primary-color); + border: 2px solid var(--affine-primary-color); + display: none; + position: absolute; + cursor: ns-resize; + width: 10px; + height: 10px; + transform: translate(-50%, -50%); + pointer-events: auto; + user-select: none; + transition: scale 0.2s ease; + z-index: 2; + } + .drag-to-fill.dragging { + scale: 1.1; + } + `; + + dragToFillRef = createRef(); + + override render() { + // TODO add tooltip + return html`
`; + } + + @state() + accessor dragging = false; +} + +export function fillSelectionWithFocusCellData( + host: DataViewTable, + selection: TableAreaSelection +) { + const { groupKey, rowsSelection, columnsSelection, focus } = selection; + + const focusCell = host.selectionController.getCellContainer( + groupKey, + focus.rowIndex, + focus.columnIndex + ); + + if (!focusCell) return; + + if (rowsSelection && columnsSelection) { + assertEquals( + columnsSelection.start, + columnsSelection.end, + 'expected selections on a single column' + ); + + const curCol = focusCell.column; // we are sure that we are always in the same column while iterating through rows + const cell = focusCell.cell$.value; + const focusData = cell.value$.value; + + const draggingColIdx = columnsSelection.start; + const { start, end } = rowsSelection; + + for (let i = start; i <= end; i++) { + if (i === focus.rowIndex) continue; + + const cellContainer = host.selectionController.getCellContainer( + groupKey, + i, + draggingColIdx + ); + + if (!cellContainer) continue; + + const curCell = cellContainer.cell$.value; + + if (tRichText.is(curCol.dataType$.value)) { + const focusCellText = focusData as Text | undefined; + + const delta = focusCellText?.toDelta() ?? [{ insert: '' }]; + const curCellText = curCell.value$.value as Text | undefined; + + if (curCellText) { + curCellText.clear(); + curCellText.applyDelta(delta); + } else { + const newText = new DocCollection.Y.Text(); + newText.applyDelta(delta); + curCell.valueSet(newText); + } + } else { + curCell.valueSet(focusData); + } + } + } +} diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/controller/drag.ts b/packages/affine/microsheet-data-view/src/view-presets/table/controller/drag.ts new file mode 100644 index 000000000000..28f1dd12c818 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/controller/drag.ts @@ -0,0 +1,206 @@ +// related component + +import type { InsertToPosition } from '@blocksuite/affine-shared/utils'; +import type { ReactiveController } from 'lit'; + +import type { TableRow } from '../row/row.js'; +import type { DataViewTable } from '../table-view.js'; + +import { startDrag } from '../../../core/utils/drag.js'; + +export class TableDragController implements ReactiveController { + dragStart = (row: TableRow, evt: PointerEvent) => { + const eleRect = row.getBoundingClientRect(); + const offsetLeft = evt.x - eleRect.left; + const offsetTop = evt.y - eleRect.top; + const preview = createDragPreview( + row, + evt.x - offsetLeft, + evt.y - offsetTop + ); + const fromGroup = row.groupKey; + + // debugger + startDrag< + | undefined + | { + type: 'self'; + groupKey?: string; + position: InsertToPosition; + } + | { type: 'out'; callback: () => void }, + PointerEvent + >(evt, { + onDrag: () => undefined, + onMove: evt => { + preview.display(evt.x - offsetLeft, evt.y - offsetTop); + if (!this.host.contains(evt.target as Node)) { + const callback = this.host.props.onDrag; + if (callback) { + this.dropPreview.remove(); + return { + type: 'out', + callback: callback(evt, row.rowId), + }; + } + return; + } + const result = this.showIndicator(evt); + if (result) { + return { + type: 'self', + groupKey: result.groupKey, + position: result.position, + }; + } + return; + }, + onClear: () => { + preview.remove(); + this.dropPreview.remove(); + }, + onDrop: result => { + if (!result) { + return; + } + if (result.type === 'out') { + result.callback(); + return; + } + if (result.type === 'self') { + this.host.props.view.rowMove( + row.rowId, + result.position, + fromGroup, + result.groupKey + ); + } + }, + }); + }; + + dropPreview = createDropPreview(); + + getInsertPosition = ( + evt: MouseEvent + ): + | { + groupKey: string | undefined; + position: InsertToPosition; + y: number; + width: number; + x: number; + } + | undefined => { + const y = evt.y; + const tableRect = this.host.getBoundingClientRect(); + const rows = this.host.querySelectorAll('microsheet-data-view-table-row'); + if (!rows || !tableRect || y < tableRect.top) { + return; + } + for (let i = 0; i < rows.length; i++) { + const row = rows.item(i); + const rect = row.getBoundingClientRect(); + const mid = (rect.top + rect.bottom) / 2; + if (y < rect.bottom) { + return { + groupKey: (row as TableRow).groupKey, + position: { + id: (row as TableRow).dataset.rowId as string, + before: y < mid, + }, + y: y < mid ? rect.top : rect.bottom, + width: tableRect.width, + x: tableRect.left, + }; + } + } + return; + }; + + showIndicator = (evt: MouseEvent) => { + const position = this.getInsertPosition(evt); + if (position) { + this.dropPreview.display(position.x, position.y, position.width); + } else { + this.dropPreview.remove(); + } + return position; + }; + + constructor(private host: DataViewTable) { + this.host.addController(this); + } + + hostConnected() { + if (this.host.props.view.readonly$.value) { + return; + } + this.host.disposables.add( + this.host.props.handleEvent('dragStart', context => { + const event = context.get('pointerState').raw; + const target = event.target; + if ( + target instanceof Element && + this.host.contains(target) && + target.closest('.microsheet-data-view-table-view-drag-handler') + ) { + event.preventDefault(); + const row = target.closest('microsheet-data-view-table-row'); + if (row) { + getSelection()?.removeAllRanges(); + this.dragStart(row as TableRow, event); + } + return true; + } + return false; + }) + ); + } +} + +const createDragPreview = (row: TableRow, x: number, y: number) => { + const div = document.createElement('div'); + div.append(row.cloneNode(true)); + div.className = 'with-data-view-css-variable'; + div.style.opacity = '0.8'; + div.style.position = 'fixed'; + div.style.pointerEvents = 'none'; + div.style.backgroundColor = 'var(--affine-background-primary-color)'; + div.style.boxShadow = 'var(--affine-shadow-2)'; + div.style.left = `${x}px`; + div.style.top = `${y}px`; + div.style.zIndex = '9999'; + document.body.append(div); + return { + display(x: number, y: number) { + div.style.left = `${Math.round(x)}px`; + div.style.top = `${Math.round(y)}px`; + }, + remove() { + div.remove(); + }, + }; +}; +const createDropPreview = () => { + const div = document.createElement('div'); + div.dataset.isDropPreview = 'true'; + div.style.pointerEvents = 'none'; + div.style.position = 'fixed'; + div.style.zIndex = '9999'; + div.style.height = '2px'; + div.style.borderRadius = '1px'; + div.style.backgroundColor = 'var(--affine-primary-color)'; + div.style.boxShadow = '0px 0px 8px 0px rgba(30, 150, 235, 0.35)'; + return { + display(x: number, y: number, width: number) { + document.body.append(div); + div.style.left = `${x}px`; + div.style.top = `${y - 2}px`; + div.style.width = `${width}px`; + }, + remove() { + div.remove(); + }, + }; +}; diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/controller/hotkeys.ts b/packages/affine/microsheet-data-view/src/view-presets/table/controller/hotkeys.ts new file mode 100644 index 000000000000..1b9158a005f2 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/controller/hotkeys.ts @@ -0,0 +1,60 @@ +import type { ReactiveController } from 'lit'; + +import type { DataViewTable } from '../table-view.js'; + +export class TableHotkeysController implements ReactiveController { + get selectionController() { + return this.host.selectionController; + } + + constructor(private host: DataViewTable) { + this.host.addController(this); + } + + hostConnected() { + this.host.disposables.add( + this.host.props.bindHotkey({ + 'Mod-a': () => { + return; + // const selection = this.selectionController.selection; + // if (TableRowSelection.is(selection)) { + // return false; + // } + // if (!selection) { + // const microsheet = this.host.closest('affine-microsheet'); + // assertExists(microsheet); + // if (!(microsheet instanceof CaptionedBlockComponent)) { + // return false; + // } + // const stdSelection = this.host.std.selection; + + // stdSelection.set([ + // stdSelection.create('block', { + // blockId: microsheet.blockId, + // }), + // ]); + // return true; + // } + // if (selection?.isEditing) { + // return true; + // } + // if (selection) { + // context.get('keyboardState').raw.preventDefault(); + // this.selectionController.selection = TableRowSelection.create({ + // rows: + // this.host.props.view.groupManager.groupsDataList$.value?.flatMap( + // group => group.rows.map(id => ({ groupKey: group.key, id })) + // ) ?? + // this.host.props.view.rows$.value.map(id => ({ + // groupKey: undefined, + // id, + // })), + // }); + // return true; + // } + // return; + }, + }) + ); + } +} diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/controller/selection.ts b/packages/affine/microsheet-data-view/src/view-presets/table/controller/selection.ts new file mode 100644 index 000000000000..176ef115320a --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/controller/selection.ts @@ -0,0 +1,1153 @@ +import type { ReactiveController } from 'lit'; +import type { Ref } from 'lit/directives/ref.js'; + +import { ShadowlessElement } from '@blocksuite/block-std'; +import { WithDisposable } from '@blocksuite/global/utils'; +import { effect } from '@preact/signals-core'; +import { css, html } from 'lit'; +import { property } from 'lit/decorators.js'; +import { createRef, ref } from 'lit/directives/ref.js'; + +import type { MicrosheetCellContainer } from '../cell.js'; +import type { TableGroup } from '../group.js'; +import type { DataViewTable } from '../table-view.js'; + +import { startDrag } from '../../../core/utils/drag.js'; +import { autoScrollOnBoundary } from '../../../core/utils/frame-loop.js'; +import { TableRow } from '../row/row.js'; +import { + type CellFocus, + type MultiSelection, + type RowWithGroup, + TableAreaSelection, + TableRowSelection, + type TableViewSelection, + type TableViewSelectionWithType, +} from '../types.js'; +import { + DragToFillElement, + fillSelectionWithFocusCellData, +} from './drag-to-fill.js'; + +export class TableSelectionController implements ReactiveController { + private _tableViewSelection?: TableViewSelectionWithType; + + private getFocusCellContainer = () => { + if ( + !this._tableViewSelection || + this._tableViewSelection.selectionType !== 'area' + ) + return null; + const { groupKey, focus } = this._tableViewSelection; + + const dragStartCell = this.getCellContainer( + groupKey, + focus.rowIndex, + focus.columnIndex + ); + return dragStartCell ?? null; + }; + + __dragToFillElement = new DragToFillElement(); + + __selectionElement; + + // custom + focus: null | CellFocus = null; + + selectionStyleUpdateTask = 0; + + private get areaSelectionElement() { + return this.__selectionElement.selectionRef.value; + } + + get dragToFillDraggable() { + return this.__dragToFillElement.dragToFillRef.value; + } + + private get focusSelectionElement() { + return this.__selectionElement.focusRef.value; + } + + get selection(): TableViewSelectionWithType | undefined { + return this._tableViewSelection; + } + + set selection(data: TableViewSelection | undefined) { + if (!data) { + this.clearSelection(); + return; + } + + const selection: TableViewSelectionWithType = { + ...data, + viewId: this.view.id, + type: 'table', + }; + if (selection.selectionType === 'area' && selection.isEditing) { + const focus = selection.focus; + const container = this.getCellContainer( + selection.groupKey, + focus.rowIndex, + focus.columnIndex + ); + const cell = container?.cell; + const isEditing = cell ? cell.beforeEnterEditMode() : true; + this.host.props.setSelection({ + ...selection, + isEditing, + }); + } else { + this.host.props.setSelection(selection); + } + } + + get tableContainer() { + return this.host.querySelector('.affine-microsheet-table-container'); + } + + get view() { + return this.host.props.view; + } + + get viewData() { + return this.view; + } + + constructor(public host: DataViewTable) { + host.addController(this); + this.__selectionElement = new SelectionElement(); + this.__selectionElement.controller = this; + } + + private clearSelection() { + this.host.props.setSelection(); + } + + private handleDragEvent() { + this.host.disposables.add( + this.host.props.handleEvent('dragStart', context => { + if (this.host.props.view.readonly$.value) { + return; + } + const event = context.get('pointerState').raw; + const target = event.target; + if (target instanceof HTMLElement) { + const [cell, fillValues] = this.resolveDragStartTarget(target); + + if (cell) { + const selection = this.selection; + if ( + selection && + selection.selectionType === 'area' && + selection.isEditing && + selection.focus.rowIndex === cell.rowIndex && + selection.focus.columnIndex === cell.columnIndex + ) { + return false; + } + this.startDrag(event, cell, fillValues); + event.preventDefault(); + // return true; + } + return false; + } + return false; + }) + ); + } + + private handleSelectionChange() { + this.host.disposables.add( + this.host.props.selection$.subscribe(tableSelection => { + if (!this.isValidSelection(tableSelection)) { + this.selection = undefined; + return; + } + const old = + this._tableViewSelection?.selectionType === 'area' + ? this._tableViewSelection + : undefined; + const newSelection = + tableSelection?.selectionType === 'area' ? tableSelection : undefined; + if ( + old?.focus.rowIndex !== newSelection?.focus.rowIndex || + old?.focus.columnIndex !== newSelection?.focus.columnIndex + ) { + requestAnimationFrame(() => { + this.scrollToFocus(); + }); + } + + if ( + this.isRowSelection() && + (old?.rowsSelection?.start !== newSelection?.rowsSelection?.start || + old?.rowsSelection?.end !== newSelection?.rowsSelection?.end) + ) { + requestAnimationFrame(() => { + this.scrollToAreaSelection(); + }); + } + + if (old) { + const container = this.getCellContainer( + old.groupKey, + old.focus.rowIndex, + old.focus.columnIndex + ); + if (container) { + const cell = container.cell; + if (old.isEditing) { + requestAnimationFrame(() => { + cell?.onExitEditMode(); + }); + cell?.blurCell(); + container.isEditing = false; + } + } + } + this._tableViewSelection = tableSelection; + + if (newSelection) { + const container = this.getCellContainer( + newSelection.groupKey, + newSelection.focus.rowIndex, + newSelection.focus.columnIndex + ); + if (container) { + const cell = container.cell; + if (newSelection.isEditing) { + cell?.onEnterEditMode(); + container.isEditing = true; + cell?.focusCell(); + } + } + } + }) + ); + } + + private insertTo( + groupKey: string | undefined, + rowId: string, + before: boolean + ) { + const id = this.view.rowAdd({ before, id: rowId }); + if (groupKey != null) { + this.view.groupManager.moveCardTo(id, undefined, groupKey, { + before, + id: rowId, + }); + } + const rows = + groupKey != null + ? this.view.groupManager.groupDataMap$.value?.[groupKey].rows + : this.view.rows$.value; + requestAnimationFrame(() => { + const index = this.host.props.view.properties$.value.findIndex( + v => v.type$.value === 'title' + ); + this.selection = TableAreaSelection.create({ + groupKey: groupKey, + focus: { + rowIndex: rows?.findIndex(v => v === id) ?? 0, + columnIndex: index, + }, + isEditing: true, + }); + }); + } + + private resolveDragStartTarget( + target: HTMLElement + ): [cell: MicrosheetCellContainer | null, fillValues: boolean] { + let cell: MicrosheetCellContainer | null; + const fillValues = !!target.dataset.dragToFill; + if (fillValues) { + const focusCellContainer = this.getFocusCellContainer(); + cell = focusCellContainer ?? null; + } else { + cell = target.closest('affine-microsheet-cell-container'); + } + return [cell, fillValues]; + } + + private scrollToAreaSelection() { + this.areaSelectionElement?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + }); + } + + private scrollToFocus() { + this.focusSelectionElement?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + }); + } + + areaToRows(selection: TableAreaSelection) { + const rows = this.rows(selection.groupKey) ?? []; + const ids = Array.from({ + length: selection.rowsSelection.end - selection.rowsSelection.start + 1, + }) + .map((_, index) => index + selection.rowsSelection.start) + .map(row => (rows[row] as TableRow)?.rowId); + return ids.map(id => ({ id, groupKey: selection.groupKey })); + } + + cellPosition(groupKey: string | undefined) { + const rows = this.rows(groupKey); + const cells = rows + ?.item(0) + .querySelectorAll('affine-microsheet-cell-container'); + + return (x1: number, x2: number, y1: number, y2: number) => { + const rowOffsets: number[] = Array.from(rows ?? []).map( + v => v.getBoundingClientRect().top + ); + const columnOffsets: number[] = Array.from(cells ?? []).map( + v => v.getBoundingClientRect().left + ); + const [startX, endX] = x1 < x2 ? [x1, x2] : [x2, x1]; + const [startY, endY] = y1 < y2 ? [y1, y2] : [y2, y1]; + const row: MultiSelection = { + start: 0, + end: 0, + }; + const column: MultiSelection = { + start: 0, + end: 0, + }; + for (let i = 0; i < rowOffsets.length; i++) { + const offset = rowOffsets[i]; + if (offset < startY) { + row.start = i; + } + if (offset < endY) { + row.end = i; + } + } + for (let i = 0; i < columnOffsets.length; i++) { + const offset = columnOffsets[i]; + if (offset < startX) { + column.start = i; + } + if (offset < endX) { + column.end = i; + } + } + return { + row, + column, + }; + }; + } + + deleteRow(rowId: string) { + this.view.rowDelete([rowId]); + // this.focusToCell('up'); + this.clearSelection(); + } + + focusFirstCell() { + this.selection = TableAreaSelection.create({ + focus: { + rowIndex: 0, + columnIndex: 0, + }, + isEditing: false, + }); + } + + focusToArea(selection: TableAreaSelection) { + return { + ...selection, + rowsSelection: selection.rowsSelection ?? { + start: selection.focus.rowIndex, + end: selection.focus.rowIndex, + }, + columnsSelection: selection.columnsSelection ?? { + start: selection.focus.columnIndex, + end: selection.focus.columnIndex, + }, + isEditing: false, + } satisfies TableAreaSelection; + } + + focusToCell( + position: 'left' | 'right' | 'up' | 'down', + focusTo?: 'start' | 'end' + ) { + // if (!this.selection || this.selection.selectionType !== 'area') { + // return; + // } + if (!this.focus) { + return; + } + const cell = this.getCellContainer( + undefined, + this.focus.rowIndex, + this.focus.columnIndex + ); + if (!cell) { + return; + } + const row = cell.closest('microsheet-data-view-table-row'); + const rows = Array.from( + row + ?.closest('.affine-microsheet-table-container') + ?.querySelectorAll('microsheet-data-view-table-row') ?? [] + ); + const cells = Array.from( + row?.querySelectorAll('affine-microsheet-cell-container') ?? [] + ); + if (!row || !rows || !cells) { + return; + } + let rowIndex = rows.indexOf(row); + let columnIndex = cells.indexOf(cell); + if (position === 'left') { + if (columnIndex === 0) { + columnIndex = cells.length - 1; + rowIndex--; + } else { + columnIndex--; + } + } + if (position === 'right') { + if (columnIndex === cells.length - 1) { + columnIndex = 0; + rowIndex++; + } else { + columnIndex++; + } + } + if (position === 'up') { + if (rowIndex === 0) { + // + return false; + } else { + rowIndex--; + } + } + if (position === 'down') { + if (rowIndex === rows.length - 1) { + // + return false; + } else { + rowIndex++; + } + } + + rows[rowIndex] + ?.querySelectorAll('affine-microsheet-cell-container') + ?.item(columnIndex) + ?.selectCurrentCell( + focusTo + ? focusTo + : position === 'up' || position === 'left' + ? 'end' + : 'start' + ); + return true; + } + + getCellContainer( + groupKey: string | undefined, + rowIndex: number, + columnIndex: number + ): MicrosheetCellContainer | undefined { + const row = this.rows(groupKey)?.item(rowIndex); + return row + ?.querySelectorAll('affine-microsheet-cell-container') + .item(columnIndex); + } + + getGroup(groupKey: string | undefined) { + const container = + groupKey != null + ? this.tableContainer?.querySelector( + `affine-microsheet-data-view-table-group[data-group-key="${groupKey}"]` + ) + : this.tableContainer; + return container ?? null; + } + + getRect( + groupKey: string | undefined, + top: number, + bottom: number, + left: number, + right: number + ): + | undefined + | { + top: number; + left: number; + width: number; + height: number; + scale: number; + } { + const rows = this.rows(groupKey); + const topRow = rows?.item(top); + const bottomRow = rows?.item(bottom); + if (!topRow || !bottomRow) { + return; + } + const topCells = topRow.querySelectorAll( + 'affine-microsheet-cell-container' + ); + const leftCell = topCells.item(left); + const rightCell = topCells.item(right); + if (!leftCell || !rightCell) { + return; + } + const leftRect = leftCell.getBoundingClientRect(); + const scale = leftRect.width / leftCell.column.width$.value; + return { + top: leftRect.top / scale, + left: leftRect.left / scale, + width: (rightCell.getBoundingClientRect().right - leftRect.left) / scale, + height: (bottomRow.getBoundingClientRect().bottom - leftRect.top) / scale, + scale, + }; + } + + getRow(groupKey: string | undefined, rowId: string) { + return this.getGroup(groupKey)?.querySelector( + `microsheet-data-view-table-row[data-row-id='${rowId}']` + ); + } + + getSelectionAreaBorder(position: 'left' | 'right' | 'top' | 'bottom') { + return this.__selectionElement.selectionRef.value?.querySelector( + `.area-border.area-${position}` + ); + } + + hostConnected() { + requestAnimationFrame(() => { + this.tableContainer?.append(this.__selectionElement); + this.tableContainer?.append(this.__dragToFillElement); + }); + this.handleDragEvent(); + this.handleSelectionChange(); + } + + insertRowAfter(groupKey: string | undefined, rowId: string) { + this.insertTo(groupKey, rowId, false); + } + + insertRowBefore(groupKey: string | undefined, rowId: string) { + this.insertTo(groupKey, rowId, true); + } + + isRowSelection() { + return this.selection?.selectionType === 'row'; + } + + isValidSelection(selection?: TableViewSelectionWithType): boolean { + if (!selection || selection.selectionType === 'row') { + return true; + } + if (selection.focus.rowIndex > this.view.rows$.value.length - 1) { + this.selection = undefined; + return false; + } + if (selection.focus.columnIndex > this.view.propertyIds$.value.length - 1) { + this.selection = undefined; + return false; + } + return true; + } + + rows(groupKey: string | undefined) { + const container = + groupKey != null + ? this.tableContainer?.querySelector( + `affine-microsheet-data-view-table-group[data-group-key="${groupKey}"]` + ) + : this.tableContainer; + return container?.querySelectorAll('microsheet-data-view-table-row'); + } + + rowSelectionChange({ + add, + remove, + }: { + add: RowWithGroup[]; + remove: RowWithGroup[]; + }) { + const key = (r: RowWithGroup) => `${r.id}.${r.groupKey ? r.groupKey : ''}`; + const rows = new Set( + TableRowSelection.rows(this.selection).map(r => key(r)) + ); + remove.forEach(row => rows.delete(key(row))); + add.forEach(row => rows.add(key(row))); + const result = [...rows] + .map(r => r.split('.')) + .map(([id, groupKey]) => ({ + id, + groupKey: groupKey ? groupKey : undefined, + })); + this.selection = TableRowSelection.create({ + rows: result, + }); + } + + rowsToArea( + rows: string[] + ): { start: number; end: number; groupKey?: string } | undefined { + let groupKey: string | undefined = undefined; + let minIndex: number | undefined = undefined; + let maxIndex: number | undefined = undefined; + const set = new Set(rows); + if (!this.tableContainer) return; + for (const row of this.tableContainer + ?.querySelectorAll('microsheet-data-view-table-row') + .values() ?? []) { + if (!(row instanceof TableRow)) { + continue; + } + if (!set.has(row.rowId)) { + continue; + } + minIndex = + minIndex != null ? Math.min(minIndex, row.rowIndex) : row.rowIndex; + maxIndex = + maxIndex != null ? Math.max(maxIndex, row.rowIndex) : row.rowIndex; + if (groupKey == null) { + groupKey = row.groupKey; + } else if (groupKey !== row.groupKey) { + return; + } + } + if (minIndex == null || maxIndex == null) { + return; + } + return { + groupKey, + start: minIndex, + end: maxIndex, + }; + } + + selectionAreaDown() { + const selection = this.selection; + if (!selection || selection.selectionType !== 'area') { + return; + } + const newSelection = this.focusToArea(selection); + if (newSelection.rowsSelection.start === newSelection.focus.rowIndex) { + newSelection.rowsSelection.end = Math.min( + (this.rows(newSelection.groupKey)?.length ?? 0) - 1, + newSelection.rowsSelection.end + 1 + ); + requestAnimationFrame(() => { + this.getSelectionAreaBorder('bottom')?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }); + }); + } else { + newSelection.rowsSelection.start += 1; + requestAnimationFrame(() => { + this.getSelectionAreaBorder('top')?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }); + }); + } + this.selection = newSelection; + } + + selectionAreaLeft() { + const selection = this.selection; + if (!selection || selection.selectionType !== 'area') { + return; + } + const newSelection = this.focusToArea(selection); + if (newSelection.columnsSelection.end === newSelection.focus.columnIndex) { + newSelection.columnsSelection.start = Math.max( + 0, + newSelection.columnsSelection.start - 1 + ); + requestAnimationFrame(() => { + this.getSelectionAreaBorder('left')?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }); + }); + } else { + newSelection.columnsSelection.end -= 1; + requestAnimationFrame(() => { + this.getSelectionAreaBorder('right')?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }); + }); + } + this.selection = newSelection; + } + + selectionAreaRight() { + const selection = this.selection; + if (!selection || selection.selectionType !== 'area') { + return; + } + const newSelection = this.focusToArea(selection); + if ( + newSelection.columnsSelection.start === newSelection.focus.columnIndex + ) { + const max = + (this.rows(newSelection.groupKey) + ?.item(0) + .querySelectorAll('affine-microsheet-cell-container').length ?? 0) - + 1; + newSelection.columnsSelection.end = Math.min( + max, + newSelection.columnsSelection.end + 1 + ); + requestAnimationFrame(() => { + this.getSelectionAreaBorder('right')?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }); + }); + } else { + newSelection.columnsSelection.start += 1; + requestAnimationFrame(() => { + this.getSelectionAreaBorder('left')?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }); + }); + } + this.selection = newSelection; + } + + selectionAreaUp() { + const selection = this.selection; + if (!selection || selection.selectionType !== 'area') { + return; + } + const newSelection = this.focusToArea(selection); + if (newSelection.rowsSelection.end === newSelection.focus.rowIndex) { + newSelection.rowsSelection.start = Math.max( + 0, + newSelection.rowsSelection.start - 1 + ); + requestAnimationFrame(() => { + this.getSelectionAreaBorder('top')?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }); + }); + } else { + newSelection.rowsSelection.end -= 1; + requestAnimationFrame(() => { + this.getSelectionAreaBorder('bottom')?.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }); + }); + } + this.selection = newSelection; + } + + startDrag( + evt: PointerEvent, + cell: MicrosheetCellContainer, + fillValues?: boolean + ) { + const groupKey = ( + cell.closest('affine-microsheet-data-view-table-group') as TableGroup + )?.group?.key; + const table = this.tableContainer; + const scrollContainer = table?.parentElement; + if (!table || !scrollContainer) { + return; + } + const tableRect = table.getBoundingClientRect(); + const startOffsetX = evt.x - tableRect.left; + const startOffsetY = evt.y - tableRect.top; + const offsetToSelection = this.cellPosition(groupKey); + const isInSingleCell = ( + selection: ReturnType + ) => { + const { row, column } = selection; + return row.end - row.start === 0 && column.end - column.start === 0; + }; + const select = (selection: { + row: MultiSelection; + column: MultiSelection; + }) => { + this.selection = TableAreaSelection.create({ + groupKey: groupKey, + rowsSelection: selection.row, + columnsSelection: selection.column, + focus: { + rowIndex: cell.rowIndex, + columnIndex: cell.columnIndex, + }, + isEditing: false, + }); + }; + const cancelScroll = autoScrollOnBoundary(scrollContainer, { + onScroll() { + drag.move({ x: drag.last.x, y: drag.last.y }); + }, + }); + const drag = startDrag< + | { + row: MultiSelection; + column: MultiSelection; + } + | undefined, + { + x: number; + y: number; + } + >(evt, { + transform: evt => ({ + x: evt.x, + y: evt.y, + }), + onDrag: () => { + if (fillValues) this.__dragToFillElement.dragging = true; + return undefined; + }, + onMove: ({ x, y }) => { + if (!table) return; + const tableRect = table.getBoundingClientRect(); + const startX = tableRect.left + startOffsetX; + const startY = tableRect.top + startOffsetY; + const selection = offsetToSelection(startX, x, startY, y); + + if (fillValues) + selection.column = { + start: cell.columnIndex, + end: cell.columnIndex, + }; + if (isInSingleCell(selection)) return selection; + select(selection); + return selection; + }, + // @ts-expect-error + onDrop: selection => { + if (!selection) { + return; + } + if (isInSingleCell(selection)) return selection; + select(selection); + if (fillValues && this.selection) { + this.__dragToFillElement.dragging = false; + fillSelectionWithFocusCellData( + this.host, + TableAreaSelection.create({ + groupKey: groupKey, + rowsSelection: selection.row, + columnsSelection: selection.column, + focus: { + rowIndex: cell.rowIndex, + columnIndex: cell.columnIndex, + }, + isEditing: false, + }) + ); + } + }, + onClear: () => { + cancelScroll(); + }, + }); + } + + toggleRow(rowId: string, groupKey?: string) { + const row = { + id: rowId, + groupKey, + }; + const isSelected = TableRowSelection.includes(this.selection, row); + if (isSelected) { + this.selection = TableRowSelection.create({ + rows: [], + }); + } else { + this.selection = TableRowSelection.create({ + rows: [row], + }); + } + } +} + +export class SelectionElement extends WithDisposable(ShadowlessElement) { + static override styles = css` + .microsheet-selection { + position: absolute; + z-index: 2; + box-sizing: border-box; + background: var(--affine-primary-color-04); + pointer-events: none; + display: none; + } + + .microsheet-focus { + position: absolute; + width: 100%; + z-index: 2; + box-sizing: border-box; + border: 1px solid var(--affine-primary-color); + border-radius: 2px; + pointer-events: none; + display: none; + outline: none; + } + + .area-border { + position: absolute; + pointer-events: none; + } + .area-left { + left: 0; + height: 100%; + width: 1px; + } + .area-right { + right: 0; + height: 100%; + width: 1px; + } + .area-top { + top: 0; + width: 100%; + height: 1px; + } + .area-bottom { + bottom: 0; + width: 100%; + height: 1px; + } + @media print { + data-view-table-selection { + display: none; + } + } + `; + + focusRef: Ref = createRef(); + + preTask = 0; + + selectionRef: Ref = createRef(); + + get selection$() { + return this.controller.host.props.selection$; + } + + clearAreaStyle() { + const div = this.selectionRef.value; + if (!div) return; + div.style.display = 'none'; + } + + clearFocusStyle() { + const div = this.focusRef.value; + const dragToFill = this.controller.dragToFillDraggable; + if (!div || !dragToFill) return; + div.style.display = 'none'; + dragToFill.style.display = 'none'; + } + + override connectedCallback() { + super.connectedCallback(); + this.disposables.add( + effect(() => { + this.startUpdate(this.selection$.value); + }) + ); + } + + override render() { + return html` +
+
+
+
+
+
+ `; + } + + startUpdate(selection?: TableViewSelection) { + if (this.preTask) { + cancelAnimationFrame(this.preTask); + this.preTask = 0; + } + if ( + selection?.selectionType === 'area' && + !this.controller.host.props.view.readonly$.value + ) { + this.updateAreaSelectionStyle( + selection.groupKey, + selection.rowsSelection, + selection.columnsSelection + ); + + const columnSelection = selection.columnsSelection; + const rowSelection = selection.rowsSelection; + + const isSingleRowSelection = rowSelection.end - rowSelection.start === 0; + const isSingleColumnSelection = + columnSelection.end - columnSelection.start === 0; + + const isDragElemDragging = this.controller.__dragToFillElement.dragging; + const isEditing = selection.isEditing; + + const showDragToFillHandle = + !isEditing && + (isDragElemDragging || isSingleRowSelection) && + isSingleColumnSelection; + + this.updateFocusSelectionStyle( + selection.groupKey, + selection.focus, + isEditing, + showDragToFillHandle + ); + this.preTask = requestAnimationFrame(() => + this.startUpdate(this.selection$.value) + ); + } else if (selection?.selectionType === 'row') { + this.updateRowSelectionStyle(selection.rows[0]); + } else { + this.clearFocusStyle(); + this.clearAreaStyle(); + } + } + + updateAreaSelectionStyle( + groupKey: string | undefined, + rowSelection: MultiSelection, + columnSelection: MultiSelection + ) { + const div = this.selectionRef.value; + if (!div) return; + const tableContainer = this.controller.tableContainer; + if (!tableContainer) return; + const tableRect = tableContainer.getBoundingClientRect(); + const rect = this.controller.getRect( + groupKey, + rowSelection?.start ?? 0, + rowSelection?.end ?? this.controller.view.rows$.value.length - 1, + columnSelection?.start ?? 0, + columnSelection?.end ?? this.controller.view.properties$.value.length - 1 + ); + if (!rect) { + this.clearAreaStyle(); + return; + } + const { left, top, width, height, scale } = rect; + div.style.left = `${left - tableRect.left / scale}px`; + div.style.top = `${top - tableRect.top / scale}px`; + div.style.width = `${width}px`; + div.style.height = `${height}px`; + div.style.display = 'block'; + } + + updateFocusSelectionStyle( + groupKey: string | undefined, + focus: CellFocus, + isEditing: boolean, + showDragToFillHandle = false + ) { + const div = this.focusRef.value; + const dragToFill = this.controller.dragToFillDraggable; + if (!div || !dragToFill) return; + // Check if row is removed. + const rows = this.controller.rows(groupKey) ?? []; + if (rows.length <= focus.rowIndex) return; + + const rect = this.controller.getRect( + groupKey, + focus.rowIndex, + focus.rowIndex, + focus.columnIndex, + focus.columnIndex + ); + if (!rect) { + this.clearFocusStyle(); + return; + } + const { left, top, width, height, scale } = rect; + const tableContainer = this.controller.tableContainer; + if (!tableContainer) return; + const tableRect = tableContainer?.getBoundingClientRect(); + if (!tableRect) { + this.clearFocusStyle(); + return; + } + + const x = left - tableRect.left / scale; + const y = top - 1 - tableRect.top / scale; + const w = width + 1; + const h = height + 1; + div.style.left = `${x}px`; + div.style.top = `${y}px`; + div.style.width = `${w}px`; + div.style.height = `${h}px`; + div.style.borderColor = 'var(--affine-primary-color)'; + div.style.borderStyle = this.controller.__dragToFillElement.dragging + ? 'dashed' + : 'solid'; + div.style.boxShadow = isEditing + ? '0px 0px 0px 2px rgba(30, 150, 235, 0.30)' + : 'unset'; + div.style.display = 'block'; + + dragToFill.style.left = `${x + w}px`; + dragToFill.style.top = `${y + h}px`; + dragToFill.style.display = showDragToFillHandle ? 'block' : 'none'; + } + + updateRowSelectionStyle(row: RowWithGroup) { + const div = this.selectionRef.value; + if (!div) return; + const tableContainer = this.controller.tableContainer; + if (!tableContainer) return; + const tableRect = tableContainer.getBoundingClientRect(); + const rowIndex = this.controller.view.rows$.value?.findIndex( + r => r === row.id + ); + if (rowIndex === -1) return; + const rect = this.controller.getRect( + undefined, + rowIndex, + rowIndex, + 0, + this.controller.view.properties$.value.length - 1 + ); + if (!rect) { + this.clearAreaStyle(); + return; + } + const { left, top, width, height, scale } = rect; + div.style.left = `${left - tableRect.left / scale}px`; + div.style.top = `${top - tableRect.top / scale}px`; + div.style.width = `${width}px`; + div.style.height = `${height}px`; + div.style.display = 'block'; + } + + @property({ attribute: false }) + accessor controller!: TableSelectionController; +} diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/define.ts b/packages/affine/microsheet-data-view/src/view-presets/table/define.ts new file mode 100644 index 000000000000..6a3572aa2da6 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/define.ts @@ -0,0 +1,49 @@ +import type { GroupBy, GroupProperty, Sort } from '../../core/common/types.js'; + +import { type BasicViewDataType, viewType } from '../../core/view/data-view.js'; +import { TableSingleView } from './table-view-manager.js'; + +export const tableViewType = viewType('table'); + +export type TableViewColumn = { + id: string; + width: number; + statCalcType?: string; + hide?: boolean; +}; +type DataType = { + columns: TableViewColumn[]; + groupBy?: GroupBy; + groupProperties?: GroupProperty[]; + sort?: Sort; + header?: { + titleColumn?: string; + iconColumn?: string; + imageColumn?: string; + }; +}; +export type TableViewData = BasicViewDataType< + typeof tableViewType.type, + DataType +>; +export const tableViewModel = tableViewType.createModel({ + defaultName: 'Table View', + dataViewManager: TableSingleView, + defaultData: viewManager => { + return { + mode: 'table', + columns: [], + filter: { + type: 'group', + op: 'and', + conditions: [], + }, + header: { + titleColumn: viewManager.dataSource.properties$.value.find( + id => viewManager.dataSource.propertyTypeGet(id) === 'title' + ), + iconColumn: 'type', + }, + }; + }, +}); diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/group.ts b/packages/affine/microsheet-data-view/src/view-presets/table/group.ts new file mode 100644 index 000000000000..1e878c6b90ee --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/group.ts @@ -0,0 +1,180 @@ +import { + menu, + popFilterableSimpleMenu, + popupTargetFromElement, +} from '@blocksuite/affine-components/context-menu'; +import { BlockComponent, ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { css, html, type PropertyValues } from 'lit'; +import { property } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; + +import type { GroupData } from '../../core/common/group-by/helper.js'; +import type { DataViewRenderer } from '../../core/data-view.js'; +import type { DataViewTable } from './table-view.js'; +import type { TableSingleView } from './table-view-manager.js'; + +import { GroupTitle } from '../../core/common/group-by/group-title.js'; +import { LEFT_TOOL_BAR_WIDTH } from './consts.js'; +import { TableAreaSelection } from './types.js'; + +const styles = css` + affine-microsheet-data-view-table-group:hover .group-header-op { + visibility: visible; + opacity: 1; + } + .microsheet-data-view-table-group-add-row { + display: flex; + width: 100%; + height: 28px; + position: relative; + z-index: 0; + cursor: pointer; + transition: opacity 0.2s ease-in-out; + padding: 4px 8px; + border-bottom: 1px solid var(--affine-border-color); + } + + @media print { + .microsheet-data-view-table-group-add-row { + display: none; + } + } + + .microsheet-data-view-table-group-add-row-button { + position: sticky; + left: ${8 + LEFT_TOOL_BAR_WIDTH}px; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + user-select: none; + font-size: 12px; + line-height: 20px; + color: var(--affine-text-secondary-color); + } +`; + +export class TableGroup extends SignalWatcher( + WithDisposable(ShadowlessElement) +) { + static override styles = styles; + + private clickAddRowInStart = () => { + this.view.rowAdd('start', this.group?.key); + requestAnimationFrame(() => { + const selectionController = this.viewEle.selectionController; + const index = this.view.properties$.value.findIndex( + v => v.type$.value === 'title' + ); + selectionController.selection = TableAreaSelection.create({ + groupKey: this.group?.key, + focus: { + rowIndex: 0, + columnIndex: index, + }, + isEditing: true, + }); + }); + }; + + private clickGroupOptions = (e: MouseEvent) => { + const group = this.group; + if (!group) { + return; + } + const ele = e.currentTarget as HTMLElement; + popFilterableSimpleMenu(popupTargetFromElement(ele), [ + menu.action({ + name: 'Ungroup', + hide: () => group.value == null, + select: () => { + group.rows.forEach(id => { + group.manager.removeFromGroup(id, group.key); + }); + }, + }), + menu.action({ + name: 'Delete Cards', + select: () => { + this.view.rowDelete(group.rows); + }, + }), + ]); + }; + + private renderGroupHeader = () => { + if (!this.group) { + return null; + } + return html` +
+ ${GroupTitle(this.group, { + readonly: this.view.readonly$.value, + clickAdd: this.clickAddRowInStart, + clickOps: this.clickGroupOptions, + })} +
+ `; + }; + + get rows() { + return this.group?.rows ?? this.view.rows$.value; + } + + private renderRows(ids: string[]) { + return html` + +
+ ${repeat( + ids, + id => id, + (id, idx) => { + return html``; + } + )} +
+ `; + } + + override render() { + return this.renderRows(this.rows); + } + + protected override updated(_changedProperties: PropertyValues) { + super.updated(_changedProperties); + this.querySelectorAll('microsheet-data-view-table-row').forEach(ele => { + if (ele instanceof BlockComponent) { + ele.requestUpdate(); + } + }); + } + + @property({ attribute: false }) + accessor dataViewEle!: DataViewRenderer; + + @property({ attribute: false }) + accessor group: GroupData | undefined = undefined; + + @property({ attribute: false }) + accessor view!: TableSingleView; + + @property({ attribute: false }) + accessor viewEle!: DataViewTable; +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-microsheet-data-view-table-group': TableGroup; + } +} diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/header/column-header.ts b/packages/affine/microsheet-data-view/src/view-presets/table/header/column-header.ts new file mode 100644 index 000000000000..8ff2eefdd9e7 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/header/column-header.ts @@ -0,0 +1,114 @@ +import { getScrollContainer } from '@blocksuite/affine-shared/utils'; +import { ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { autoUpdate } from '@floating-ui/dom'; +import { nothing } from 'lit'; +import { property, query } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; +import { styleMap } from 'lit/directives/style-map.js'; +import { html } from 'lit/static-html.js'; + +import type { TableGroup } from '../group.js'; +import type { TableSingleView } from '../table-view-manager.js'; + +import { styles } from './styles.js'; + +export class MicrosheetColumnHeader extends SignalWatcher( + WithDisposable(ShadowlessElement) +) { + static override styles = styles; + + editLastColumnTitle = () => { + const columns = this.querySelectorAll('affine-microsheet-header-column'); + const column = columns.item(columns.length - 1); + column.scrollIntoView({ block: 'nearest', inline: 'nearest' }); + column.editTitle(); + }; + + preMove = 0; + + private get readonly() { + return this.tableViewManager.readonly$.value; + } + + private autoSetHeaderPosition( + group: TableGroup, + scrollContainer: HTMLElement + ) { + const referenceRect = group.getBoundingClientRect(); + const floatingRect = this.getBoundingClientRect(); + const rootRect = scrollContainer.getBoundingClientRect(); + let moveX = 0; + if (rootRect.top > referenceRect.top) { + moveX = + Math.min(referenceRect.bottom - floatingRect.height, rootRect.top) - + referenceRect.top; + } + if (moveX === 0 && this.preMove === 0) { + return; + } + this.preMove = moveX; + this.style.transform = `translate3d(0,${moveX / this.getScale()}px,0)`; + } + + override connectedCallback() { + super.connectedCallback(); + const scrollContainer = getScrollContainer( + this.closest('affine-microsheet-data-view-renderer')! + ); + const group = this.closest('affine-microsheet-data-view-table-group'); + if (group) { + const cancel = autoUpdate(group, this, () => { + if (!scrollContainer) { + return; + } + this.autoSetHeaderPosition(group, scrollContainer); + }); + this.disposables.add(cancel); + } + } + + getScale() { + return this.scaleDiv?.getBoundingClientRect().width ?? 1; + } + + override render() { + return html` +
+ ${this.readonly + ? nothing + : html`
`} + ${repeat( + this.tableViewManager.properties$.value, + column => column.id, + (column, index) => { + const style = styleMap({ + width: `${column.width$.value}px`, + border: index === 0 ? 'none' : undefined, + }); + return html` `; + } + )} +
+ `; + } + + @query('.scale-div') + accessor scaleDiv!: HTMLDivElement; + + @property({ attribute: false }) + accessor tableViewManager!: TableSingleView; +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-microsheet-column-header': MicrosheetColumnHeader; + } +} diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/header/column-renderer.ts b/packages/affine/microsheet-data-view/src/view-presets/table/header/column-renderer.ts new file mode 100644 index 000000000000..0c666cac8157 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/header/column-renderer.ts @@ -0,0 +1,87 @@ +import { ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { css } from 'lit'; +import { property } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; +import { styleMap } from 'lit/directives/style-map.js'; +import { html } from 'lit/static-html.js'; + +import type { Property } from '../../../core/view-manager/property.js'; +import type { TableSingleView } from '../table-view-manager.js'; + +export class DataViewColumnPreview extends SignalWatcher( + WithDisposable(ShadowlessElement) +) { + static override styles = css` + affine-microsheet-data-view-column-preview { + pointer-events: none; + display: block; + } + `; + + private renderGroup(rows: string[]) { + const columnIndex = this.tableViewManager.propertyIndexGet(this.column.id); + return html` +
+ + ${repeat(rows, (id, index) => { + const height = this.table.querySelector( + `affine-microsheet-cell-container[data-row-id="${id}"]` + )?.clientHeight; + const style = styleMap({ + height: height + 'px', + }); + return html`
+
+ +
+
`; + })} +
+
+ `; + } + + override render() { + const groups = this.tableViewManager.groupManager.groupsDataList$.value; + if (!groups) { + const rows = this.tableViewManager.rows$.value; + return this.renderGroup(rows); + } + return groups.map(group => { + return html` +
+ ${this.renderGroup(group.rows)} + `; + }); + } + + @property({ attribute: false }) + accessor column!: Property; + + @property({ attribute: false }) + accessor table!: HTMLElement; + + @property({ attribute: false }) + accessor tableViewManager!: TableSingleView; +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-microsheet-data-view-column-preview': DataViewColumnPreview; + } +} diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/header/microsheet-header-column.ts b/packages/affine/microsheet-data-view/src/view-presets/table/header/microsheet-header-column.ts new file mode 100644 index 000000000000..5a836eb52236 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/header/microsheet-header-column.ts @@ -0,0 +1,451 @@ +import { + menu, + popMenu, + popupTargetFromElement, +} from '@blocksuite/affine-components/context-menu'; +import { + insertPositionToIndex, + type InsertToPosition, +} from '@blocksuite/affine-shared/utils'; +import { ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { AddCursorIcon, DeleteIcon } from '@blocksuite/icons/lit'; +import { css } from 'lit'; +import { property } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { createRef, ref } from 'lit/directives/ref.js'; +import { styleMap } from 'lit/directives/style-map.js'; +import { html } from 'lit/static-html.js'; + +import type { TableColumn, TableSingleView } from '../table-view-manager.js'; + +import { startDrag } from '../../../core/utils/drag.js'; +import { autoScrollOnBoundary } from '../../../core/utils/frame-loop.js'; +import { getResultInRange } from '../../../core/utils/utils.js'; +import { DEFAULT_COLUMN_TITLE_HEIGHT } from '../consts.js'; +import { getTableContainer } from '../types.js'; +import { DataViewColumnPreview } from './column-renderer.js'; +import { + getTableGroupRects, + getVerticalIndicator, + startDragWidthAdjustmentBar, +} from './vertical-indicator.js'; + +export class MicrosheetHeaderColumn extends SignalWatcher( + WithDisposable(ShadowlessElement) +) { + static override styles = css` + affine-microsheet-header-column { + display: flex; + } + + .affine-microsheet-header-column-grabbing * { + cursor: grabbing; + } + `; + + private _clickColumn = () => { + if (this.tableViewManager.readonly$.value) { + return; + } + this.popMenu(); + }; + + private _columnsOffset = (header: Element, _scale: number) => { + const columns = header.querySelectorAll('affine-microsheet-header-column'); + const left: ColumnOffset[] = []; + const right: ColumnOffset[] = []; + let curr = left; + const offsetArr: number[] = []; + const columnsArr = Array.from(columns); + for (let i = 0; i < columnsArr.length; i++) { + const v = columnsArr[i]; + if (v === this) { + curr = right; + offsetArr.push(-1); + continue; + } + curr.push({ + x: v.offsetLeft + v.offsetWidth / 2, + ele: v, + }); + offsetArr.push( + v.getBoundingClientRect().left - header.getBoundingClientRect().left + ); + if (i === columnsArr.length - 1) { + offsetArr.push( + v.getBoundingClientRect().right - header.getBoundingClientRect().left + ); + } + } + left.reverse(); + const getInsertPosition = (offset: number, width: number) => { + let result: InsertToPosition | undefined = undefined; + for (let i = 0; i < left.length; i++) { + const { x, ele } = left[i]; + if (x < offset) { + if (result) { + return result; + } + break; + } else { + result = { + before: true, + id: ele.column.id, + }; + } + } + const offsetRight = offset + width; + for (const { x, ele } of right) { + if (x > offsetRight) { + if (result) { + return result; + } + break; + } else { + result = { + before: false, + id: ele.column.id, + }; + } + } + return result; + }; + const fixedColumns = columnsArr.map(v => ({ id: v.column.id })); + const getInsertOffset = (insertPosition: InsertToPosition) => { + return offsetArr[insertPositionToIndex(insertPosition, fixedColumns)]; + }; + return { + computeInsertInfo: (offset: number, width: number) => { + const insertPosition = getInsertPosition(offset, width); + return { + insertPosition: insertPosition, + insertOffset: insertPosition + ? getInsertOffset(insertPosition) + : undefined, + }; + }, + }; + }; + + private _contextMenu = (e: MouseEvent) => { + if (this.tableViewManager.readonly$.value) { + return; + } + e.preventDefault(); + this.popMenu(e.currentTarget as HTMLElement); + }; + + private _enterWidthDragBar = () => { + if (this.tableViewManager.readonly$.value) { + return; + } + if (this.drawWidthDragBarTask) { + cancelAnimationFrame(this.drawWidthDragBarTask); + this.drawWidthDragBarTask = 0; + } + this.drawWidthDragBar(); + }; + + private _leaveWidthDragBar = () => { + cancelAnimationFrame(this.drawWidthDragBarTask); + this.drawWidthDragBarTask = 0; + getVerticalIndicator().remove(); + }; + + private drawWidthDragBar = () => { + const tableContainer = getTableContainer(this); + const tableRect = tableContainer.getBoundingClientRect(); + const rectList = getTableGroupRects(tableContainer); + getVerticalIndicator().display( + 0, + tableRect.top, + rectList, + this.getBoundingClientRect().right + ); + this.drawWidthDragBarTask = requestAnimationFrame(this.drawWidthDragBar); + }; + + private drawWidthDragBarTask = 0; + + private moveColumn = (evt: PointerEvent) => { + const tableContainer = getTableContainer(this); + const headerContainer = this.closest('affine-microsheet-column-header'); + const scrollContainer = tableContainer?.parentElement; + + if (!tableContainer || !headerContainer || !scrollContainer) return; + + const columnHeaderRect = this.getBoundingClientRect(); + const scale = columnHeaderRect.width / this.column.width$.value; + const headerContainerRect = tableContainer.getBoundingClientRect(); + + const rectOffsetLeft = evt.x - columnHeaderRect.left; + const offsetRight = columnHeaderRect.right - evt.x; + + const startOffset = + (columnHeaderRect.left - headerContainerRect.left) / scale; + const max = (headerContainerRect.width - columnHeaderRect.width) / scale; + + const { computeInsertInfo } = this._columnsOffset(headerContainer, scale); + const column = new DataViewColumnPreview(); + column.tableViewManager = this.tableViewManager; + column.column = this.column; + column.table = tableContainer; + + const dragPreview = createDragPreview( + tableContainer, + columnHeaderRect.width / scale, + headerContainerRect.height / scale, + startOffset, + this.column.id + ); + const rectList = getTableGroupRects(tableContainer); + const dropPreview = getVerticalIndicator(); + const cancelScroll = autoScrollOnBoundary(scrollContainer, { + boundary: { + left: rectOffsetLeft, + right: offsetRight, + }, + onScroll: () => { + drag.move({ x: drag.last.x }); + }, + }); + const html = document.querySelector('html'); + html?.classList.toggle('affine-microsheet-header-column-grabbing', true); + const drag = startDrag<{ + insertPosition?: InsertToPosition; + }>(evt, { + onDrag: () => { + this.grabStatus = 'grabbing'; + return {}; + }, + onMove: ({ x }: { x: number }) => { + this.grabStatus = 'grabbing'; + const currentOffset = getResultInRange( + (x - tableContainer.getBoundingClientRect().left - rectOffsetLeft) / + scale, + 0, + max + ); + const insertInfo = computeInsertInfo( + currentOffset, + columnHeaderRect.width / scale + ); + if (insertInfo.insertOffset != null) { + dropPreview.display( + 0, + headerContainerRect.top, + rectList, + tableContainer.getBoundingClientRect().left + + insertInfo.insertOffset, + true + ); + } else { + dropPreview.remove(); + } + dragPreview.display(currentOffset); + return { + insertPosition: insertInfo.insertPosition, + }; + }, + onDrop: ({ insertPosition }) => { + this.grabStatus = 'grabEnd'; + if (insertPosition) { + this.tableViewManager.propertyMove(this.column.id, insertPosition); + } + }, + onClear: () => { + cancelScroll(); + html?.classList.toggle( + 'affine-microsheet-header-column-grabbing', + false + ); + dropPreview.remove(); + dragPreview.remove(); + }, + }); + }; + + private widthDragBar = createRef(); + + editTitle = () => { + this._clickColumn(); + }; + + private get readonly() { + return this.tableViewManager.readonly$.value; + } + + private popMenu(ele?: HTMLElement) { + popMenu(popupTargetFromElement(ele ?? this), { + options: { + items: [ + menu.group({ + items: [ + menu.action({ + name: 'Delete', + prefix: DeleteIcon(), + hide: () => + !this.column.delete || this.column.type$.value === 'title', + select: () => { + this.column.delete?.(); + }, + class: { 'delete-item': true }, + }), + ], + }), + ], + }, + }); + } + + private widthDragStart(event: PointerEvent) { + startDragWidthAdjustmentBar( + event, + getTableContainer(this), + this.getBoundingClientRect().width, + this.column + ); + } + + override connectedCallback() { + super.connectedCallback(); + const table = this.closest('affine-microsheet-table'); + if (table) { + this.disposables.add( + table.props.handleEvent('dragStart', context => { + if (this.tableViewManager.readonly$.value) { + return; + } + const event = context.get('pointerState').raw; + const target = event.target; + if (target instanceof Element) { + if (this.widthDragBar.value?.contains(target)) { + event.preventDefault(); + this.widthDragStart(event); + return true; + } + if (this.contains(target)) { + event.preventDefault(); + this.moveColumn(event); + return true; + } + } + return false; + }) + ); + } + } + + override render() { + const style = styleMap({ + height: DEFAULT_COLUMN_TITLE_HEIGHT + 'px', + }); + const classes = classMap({ + 'affine-microsheet-column-move': true, + [this.grabStatus]: true, + }); + return html` +
+ ${this.readonly + ? null + : html` `} +
+
+
+
{ + this.tableViewManager.propertyAdd({ + id: this.column.id, + before: true, + }); + }} + > +
+ ${AddCursorIcon()} +
+
{ + this.tableViewManager.propertyAdd({ + id: this.column.id, + before: false, + }); + }} + > +
+ ${AddCursorIcon()} +
+
+ `; + } + + @property({ attribute: false }) + accessor column!: TableColumn; + + @property({ attribute: false }) + accessor grabStatus: 'grabStart' | 'grabEnd' | 'grabbing' = 'grabEnd'; + + @property({ attribute: false }) + accessor tableViewManager!: TableSingleView; +} + +type ColumnOffset = { + x: number; + ele: MicrosheetHeaderColumn; +}; + +const createDragPreview = ( + container: Element, + width: number, + height: number, + startLeft: number, + id: string +) => { + const div = document.createElement('div'); + const cells = container.querySelectorAll( + `affine-microsheet-cell-container[data-column-id="${id}"]` + ); + cells.forEach(cell => { + div.append(cell.cloneNode(true)); + }); + div.style.pointerEvents = 'none'; + div.style.opacity = '0.8'; + div.style.position = 'absolute'; + div.style.width = `${width}px`; + div.style.height = `${height}px`; + div.style.left = `${startLeft}px`; + div.style.opacity = '0.8'; + div.style.top = `0px`; + div.style.zIndex = '9'; + div.style.backgroundColor = 'var(--affine-background-primary-color)'; + container.append(div); + return { + display(offset: number) { + div.style.left = `${Math.round(offset)}px`; + }, + remove() { + div.remove(); + }, + }; +}; + +declare global { + interface HTMLElementTagNameMap { + 'affine-microsheet-header-column': MicrosheetHeaderColumn; + } +} diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/header/styles.ts b/packages/affine/microsheet-data-view/src/view-presets/table/header/styles.ts new file mode 100644 index 000000000000..0da5142282ed --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/header/styles.ts @@ -0,0 +1,418 @@ +import { baseTheme } from '@toeverything/theme'; +import { css, unsafeCSS } from 'lit'; + +import { + DEFAULT_ADD_BUTTON_WIDTH, + DEFAULT_COLUMN_MIN_WIDTH, + DEFAULT_COLUMN_TITLE_HEIGHT, +} from '../consts.js'; + +export const styles = css` + affine-microsheet-column-header { + display: block; + background-color: var(--affine-background-primary-color); + position: relative; + z-index: 2; + } + + .affine-microsheet-column-header { + position: relative; + display: flex; + flex-direction: row; + box-sizing: border-box; + user-select: none; + background-color: var(--affine-background-primary-color); + visibility: hidden; + } + + .affine-microsheet-column { + cursor: pointer; + } + + .microsheet-cell { + min-width: ${DEFAULT_COLUMN_MIN_WIDTH}px; + user-select: none; + } + + .microsheet-cell.add-column-button { + flex: 1; + min-width: ${DEFAULT_ADD_BUTTON_WIDTH}px; + min-height: 100%; + display: flex; + align-items: center; + } + + .affine-microsheet-column-content { + display: flex; + align-items: center; + gap: 6px; + width: 100%; + height: 100%; + padding: 8px; + box-sizing: border-box; + position: relative; + padding:0; + background-color: #eee; + } + + .affine-microsheet-column-move:hover { + background-color: blue; + } + + /* .affine-microsheet-column-content:hover, + .affine-microsheet-column-content.edit { + background-color: blue + } */ + + .affine-microsheet-column-content.edit .affine-microsheet-column-text-icon { + opacity: 1; + } + + .affine-microsheet-column-text { + flex: 1; + display: flex; + align-items: center; + gap: 6px; + /* https://stackoverflow.com/a/36247448/15443637 */ + overflow: hidden; + color: var(--affine-text-secondary-color); + font-size: 14px; + position: relative; + } + + .affine-microsheet-column-type-icon { + display: flex; + align-items: center; + border-radius: 4px; + padding: 2px; + } + + .affine-microsheet-column-type-icon svg { + width: 16px; + height: 16px; + fill: var(--affine-icon-color); + } + + .affine-microsheet-column-text-content { + flex: 1; + display: flex; + align-items: center; + overflow: hidden; + } + + .affine-microsheet-column-content:hover .affine-microsheet-column-text-icon { + opacity: 1; + } + + .affine-microsheet-column-text-input { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .affine-microsheet-column-text-icon { + display: flex; + align-items: center; + width: 16px; + height: 16px; + background: var(--affine-white); + border: 1px solid var(--affine-border-color); + border-radius: 4px; + opacity: 0; + } + + .affine-microsheet-column-text-save-icon { + display: flex; + align-items: center; + width: 16px; + height: 16px; + border: 1px solid transparent; + border-radius: 4px; + fill: var(--affine-icon-color); + } + + .affine-microsheet-column-text-save-icon:hover { + background: var(--affine-white); + border-color: var(--affine-border-color); + } + + .affine-microsheet-column-text-icon svg { + fill: var(--affine-icon-color); + } + + .affine-microsheet-column-input { + width: 100%; + height: 24px; + padding: 0; + border: none; + color: inherit; + font-weight: 600; + font-size: 14px; + font-family: ${unsafeCSS(baseTheme.fontSansFamily)}; + background: transparent; + } + + .affine-microsheet-column-input:focus { + outline: none; + } + + .affine-microsheet-column-move { + display: flex; + align-items: center; + padding: 0; + } + + .affine-microsheet-column-move svg { + width: 10px; + height: 14px; + color: var(--affine-black-10); + cursor: grab; + opacity: 0; + } + + .affine-microsheet-column-content:hover svg { + opacity: 1; + } + + .affine-microsheet-add-column-button { + position: sticky; + right: 0; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 38px; + cursor: pointer; + } + + .header-add-column-button { + height: ${DEFAULT_COLUMN_TITLE_HEIGHT}px; + background-color: var(--affine-background-primary-color); + display: flex; + align-items: center; + justify-content: center; + width: 40px; + cursor: pointer; + } + + @media print { + .header-add-column-button { + display: none; + } + } + + .header-add-column-button svg { + color: var(--affine-icon-color); + } + + .affine-microsheet-column-type-menu-icon { + border: 1px solid var(--affine-border-color); + border-radius: 4px; + padding: 5px; + background-color: var(--affine-background-secondary-color); + } + + .affine-microsheet-column-type-menu-icon svg { + color: var(--affine-text-secondary-color); + width: 20px; + height: 20px; + + } + + .affine-microsheet-column-move-preview { + position: fixed; + z-index: 100; + width: 100px; + height: 100px; + background: var(--affine-text-emphasis-color); + } + + .affine-microsheet-column-move { + --color: var(--affine-placeholder-color); + --active: var(--affine-black-10); + --bw: 1px; + --bw2: -1px; + cursor: grab; + background: none; + border: none; + border-radius: 0; + position: absolute; + inset: 0; + } + + .affine-microsheet-column-move .control-l::before, + .affine-microsheet-column-move .control-h::before, + .affine-microsheet-column-move .control-l::after, + .affine-microsheet-column-move .control-h::after, + .affine-microsheet-column-move .control-r, + .affine-microsheet-column-move .hover-trigger { + --delay: 0s; + --delay-opacity: 0s; + content: ''; + position: absolute; + transition: all 0.2s ease var(--delay), + opacity 0.2s ease var(--delay-opacity); + } + + .affine-microsheet-column-move .control-r { + --delay: 0s; + --delay-opacity: 0.6s; + width: 4px; + border-radius: 1px; + height: 32%; + background: var(--color); + right: 6px; + top: 50%; + transform: translateY(-50%); + opacity: 0; + pointer-events: none; + } + + .affine-microsheet-column-move .hover-trigger { + width: 12px; + height: 80%; + right: 3px; + top: 10%; + background: transparent + z-index: 1; + opacity: 1; + } + + .affine-microsheet-column-move:hover .control-r { + opacity: 1; + } + + .affine-microsheet-column-move .control-h::before, + .affine-microsheet-column-move .control-h::after { + --delay: 0.2s; + width: calc(100% - var(--bw2) * 2); + opacity: 0; + height: var(--bw); + right: var(--bw2); + background: var(--active); + } + + .affine-microsheet-column-move .control-h::before { + top: var(--bw2); + } + + .affine-microsheet-column-move .control-h::after { + bottom: var(--bw2); + } + + .affine-microsheet-column-move .control-l::before { + --delay: 0s; + width: var(--bw); + height: 100%; + opacity: 0; + background: var(--active); + left: var(--bw2); + } + + .affine-microsheet-column-move .control-l::before { + top: 0; + } + + .affine-microsheet-column-move .control-l::after { + bottom: 0; + } + + /* handle--active style */ + + .affine-microsheet-column-move:hover .control-r { + --delay-opacity: 0s; + opacity: 1; + } + + .affine-microsheet-column-move:active .control-r, + .hover-trigger:hover ~ .control-r, + .grabbing.affine-microsheet-column-move .control-r { + opacity: 1; + --delay: 0s; + --delay-opacity: 0s; + right: var(--bw2); + width: var(--bw); + height: 100%; + background: var(--active); + } + + .affine-microsheet-column-move:active .control-h::before, + .affine-microsheet-column-move:active .control-h::after, + .hover-trigger:hover ~ .control-h::before, + .hover-trigger:hover ~ .control-h::after, + .grabbing.affine-microsheet-column-move .control-h::before, + .grabbing.affine-microsheet-column-move .control-h::after { + --delay: 0.2s; + width: calc(100% - var(--bw2) * 2); + opacity: 1; + } + + .affine-microsheet-column-move:active .control-l::before, + .affine-microsheet-column-move:active .control-l::after, + .hover-trigger:hover ~ .control-l::before, + .hover-trigger:hover ~ .control-l::after, + .grabbing.affine-microsheet-column-move .control-l::before, + .grabbing.affine-microsheet-column-move .control-l::after { + --delay: 0.4s; + opacity: 1; + } + + + .affine-microsheet-column-add-icon { + position: absolute; + // right: -12px; + left: -10px; + top: -16px; + z-index: 9; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + } + + .affine-microsheet-column-add-icon svg { + width: 20px; + height: 20px; + border-radius: 100px; + background: #4949fe; + color: white; + display: none; + } + + .affine-microsheet-column-right-add-icon { + left: unset; + right: -10px; + } + + .affine-microsheet-column-add-icon:hover svg { + display: block; + } + + .affine-microsheet-column-add-icon:hover + .affine-microsheet-column-add-not-active-icon { + display: none; + } + + .affine-microsheet-column-add-not-active-icon { + margin-top: -4px; + width: 4px; + height: 4px; + border-radius: 4px; + background: #ddd; + } + + .data-view-table-left-bar{ + padding-left: 16px; + display: flex; + align-items: center; + position: sticky; + left: 0; + width: 24px; + flex-shrink: 0; + visibility: hidden; + background-color: var(--affine-background-primary-color); + z-index: 9; + } +`; diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/header/vertical-indicator.ts b/packages/affine/microsheet-data-view/src/view-presets/table/header/vertical-indicator.ts new file mode 100644 index 000000000000..f23976faf647 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/header/vertical-indicator.ts @@ -0,0 +1,191 @@ +import { ShadowlessElement } from '@blocksuite/block-std'; +import { WithDisposable } from '@blocksuite/global/utils'; +import { css, html } from 'lit'; +import { property } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { repeat } from 'lit/directives/repeat.js'; +import { styleMap } from 'lit/directives/style-map.js'; + +import type { TableColumn } from '../table-view-manager.js'; + +import { startDrag } from '../../../core/utils/drag.js'; +import { getResultInRange } from '../../../core/utils/utils.js'; +import { DEFAULT_COLUMN_MIN_WIDTH } from '../consts.js'; + +type GroupRectList = { + top: number; + bottom: number; +}[]; + +export class TableVerticalIndicator extends WithDisposable(ShadowlessElement) { + static override styles = css` + data-view-table-vertical-indicator { + position: fixed; + left: 0; + top: 0; + z-index: 1; + pointer-events: none; + } + + .vertical-indicator-container { + position: absolute; + pointer-events: none; + } + + .vertical-indicator-group { + position: absolute; + z-index: 1; + width: 100%; + background-color: var(--affine-hover-color); + pointer-events: none; + } + .vertical-indicator-group::after { + position: absolute; + z-index: 1; + width: 2px; + height: 100%; + content: ''; + right: 0; + background-color: var(--affine-primary-color); + border-radius: 1px; + } + .with-shadow.vertical-indicator-group::after { + box-shadow: 0px 0px 8px 0px rgba(30, 150, 235, 0.35); + } + `; + + protected override render(): unknown { + const containerStyle = styleMap({ + top: `${this.top}px`, + left: `${this.left}px`, + width: `${Math.max(this.width, 1)}px`, + }); + return html` +
+ ${repeat(this.lines, ({ top, bottom }) => { + const groupStyle = styleMap({ + top: `${top}px`, + height: `${bottom - top}px`, + }); + const groupClass = classMap({ + 'with-shadow': this.shadow, + 'vertical-indicator-group': true, + }); + return html`
`; + })} +
+ `; + } + + @property({ attribute: false }) + accessor left!: number; + + @property({ attribute: false }) + accessor lines!: GroupRectList; + + @property({ attribute: false }) + accessor shadow = false; + + @property({ attribute: false }) + accessor top!: number; + + @property({ attribute: false }) + accessor width!: number; +} + +export const getTableGroupRects = (tableContainer: HTMLElement) => { + const tableRect = tableContainer.getBoundingClientRect(); + const groups = tableContainer.querySelectorAll( + 'affine-microsheet-data-view-table-group' + ); + return Array.from(groups).map(group => { + const groupRect = group.getBoundingClientRect(); + const top = + group + .querySelector('.affine-microsheet-column-header') + ?.getBoundingClientRect().top ?? groupRect.top; + const bottom = + group + .querySelector('.affine-microsheet-block-rows') + ?.getBoundingClientRect().bottom ?? groupRect.bottom; + return { + top: top - tableRect.top, + bottom: bottom - tableRect.top, + }; + }); +}; +export const startDragWidthAdjustmentBar = ( + evt: PointerEvent, + tableContainer: HTMLElement, + width: number, + column: TableColumn +) => { + const scale = width / column.width$.value; + const tableRect = tableContainer.getBoundingClientRect(); + const left = + tableContainer + .querySelector( + `affine-microsheet-header-column[data-column-id='${column.id}']` + ) + ?.getBoundingClientRect().left ?? 0; + const rectList = getTableGroupRects(tableContainer); + const preview = getVerticalIndicator(); + preview.display(column.width$.value * scale, tableRect.top, rectList, left); + tableContainer.style.pointerEvents = 'none'; + startDrag<{ width: number }>(evt, { + onDrag: () => ({ width: column.width$.value }), + onMove: ({ x }) => { + const width = Math.round( + getResultInRange((x - left) / scale, DEFAULT_COLUMN_MIN_WIDTH, Infinity) + ); + preview.display(width * scale, tableRect.top, rectList, left); + return { + width, + }; + }, + onDrop: ({ width }) => { + column.updateWidth(width); + }, + onClear: () => { + tableContainer.style.pointerEvents = 'auto'; + preview.remove(); + }, + }); +}; +let preview: VerticalIndicator | null = null; +type VerticalIndicator = { + display: ( + width: number, + top: number, + lines: GroupRectList, + left: number, + shadow?: boolean + ) => void; + remove: () => void; +}; +export const getVerticalIndicator = (): VerticalIndicator => { + if (!preview) { + const dragBar = new TableVerticalIndicator(); + preview = { + display( + width: number, + top: number, + lines: GroupRectList, + left: number, + shadow = false + ) { + document.body.append(dragBar); + dragBar.left = left; + dragBar.lines = lines; + dragBar.top = top; + dragBar.width = width; + dragBar.shadow = shadow; + }, + remove() { + dragBar.remove(); + }, + }; + } + + return preview; +}; diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/index.ts b/packages/affine/microsheet-data-view/src/view-presets/table/index.ts new file mode 100644 index 000000000000..2ef547ad3b2d --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/index.ts @@ -0,0 +1,4 @@ +export * from './define.js'; +export * from './renderer.js'; +export * from './table-view.js'; +export * from './table-view-manager.js'; diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/renderer.ts b/packages/affine/microsheet-data-view/src/view-presets/table/renderer.ts new file mode 100644 index 000000000000..142c080c652f --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/renderer.ts @@ -0,0 +1,9 @@ +import { createUniComponentFromWebComponent } from '../../core/utils/uni-component/uni-component.js'; +import { createIcon } from '../../core/utils/uni-icon.js'; +import { tableViewModel } from './define.js'; +import { DataViewTable } from './table-view.js'; + +export const tableViewMeta = tableViewModel.createMeta({ + view: createUniComponentFromWebComponent(DataViewTable), + icon: createIcon('DatabaseTableViewIcon'), +}); diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/row/row-select-checkbox.ts b/packages/affine/microsheet-data-view/src/view-presets/table/row/row-select-checkbox.ts new file mode 100644 index 000000000000..35305a5d3acf --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/row/row-select-checkbox.ts @@ -0,0 +1,82 @@ +import { ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { CheckBoxCkeckSolidIcon, CheckBoxUnIcon } from '@blocksuite/icons/lit'; +import { computed, type ReadonlySignal } from '@preact/signals-core'; +import { css, html } from 'lit'; +import { property } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; + +import { + TableRowSelection, + type TableViewSelectionWithType, +} from '../types.js'; + +export class RowSelectCheckbox extends SignalWatcher( + WithDisposable(ShadowlessElement) +) { + static override styles = css` + microsheet-row-select-checkbox { + display: contents; + } + .microsheet-row-select-checkbox { + display: flex; + align-items: center; + background-color: var(--affine-background-primary-color); + opacity: 0; + cursor: pointer; + font-size: 20px; + color: var(--affine-icon-color); + } + .microsheet-row-select-checkbox:hover { + opacity: 1; + } + .microsheet-row-select-checkbox.selected { + opacity: 1; + } + `; + + @property({ attribute: false }) + accessor groupKey: string | undefined; + + @property({ attribute: false }) + accessor rowId!: string; + + @property({ attribute: false }) + accessor selection!: ReadonlySignal; + + isSelected$ = computed(() => { + const selection = this.selection.value; + if (!selection || selection.selectionType !== 'row') { + return false; + } + return TableRowSelection.includes(selection, { + id: this.rowId, + groupKey: this.groupKey, + }); + }); + + override connectedCallback() { + super.connectedCallback(); + this.disposables.addFromEvent(this, 'click', () => { + this.closest('affine-microsheet-table')?.selectionController.toggleRow( + this.rowId, + this.groupKey + ); + }); + } + + override render() { + const classString = classMap({ + 'row-selected-bg': true, + 'microsheet-row-select-checkbox': true, + selected: this.isSelected$.value, + }); + return html` +
+ ${this.isSelected$.value + ? CheckBoxCkeckSolidIcon({ style: `color:#1E96EB` }) + : CheckBoxUnIcon()} +
+ `; + } +} diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/row/row.ts b/packages/affine/microsheet-data-view/src/view-presets/table/row/row.ts new file mode 100644 index 000000000000..941c98fe0156 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/row/row.ts @@ -0,0 +1,372 @@ +import { + menu, + popMenu, + popupTargetFromElement, +} from '@blocksuite/affine-components/context-menu'; +import { type BlockStdScope, ShadowlessElement } from '@blocksuite/block-std'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils'; +import { + AddCursorIcon, + CenterPeekIcon, + DeleteIcon, + MoreHorizontalIcon, +} from '@blocksuite/icons/lit'; +import { css, nothing } from 'lit'; +import { property } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; +import { styleMap } from 'lit/directives/style-map.js'; +import { html } from 'lit/static-html.js'; + +import type { DataViewRenderer } from '../../../core/data-view.js'; +import type { TableSingleView } from '../table-view-manager.js'; + +import { DEFAULT_COLUMN_MIN_WIDTH } from '../consts.js'; +import { TableRowSelection, type TableViewSelection } from '../types.js'; + +export class TableRow extends SignalWatcher(WithDisposable(ShadowlessElement)) { + static override styles = css` + .affine-microsheet-block-row:has(.microsheet-row-select-checkbox.selected) { + background: var(--affine-primary-color-04); + } + .affine-microsheet-block-row:has(.microsheet-row-select-checkbox.selected) + .row-selected-bg { + position: relative; + } + .affine-microsheet-block-row:has(.microsheet-row-select-checkbox.selected) + .row-selected-bg:before { + content: ''; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + background: var(--affine-primary-color-04); + } + .affine-microsheet-block-row { + width: 100%; + display: flex; + flex-direction: row; + /* border-bottom: 1px solid var(--affine-border-color); */ + position: relative; + } + + .affine-microsheet-block-row.selected > .microsheet-cell { + background: transparent; + } + + .microsheet-cell { + min-width: ${DEFAULT_COLUMN_MIN_WIDTH}px; + } + + .row-ops { + position: relative; + width: 0; + margin-top: 8px; + height: max-content; + visibility: hidden; + display: flex; + gap: 4px; + cursor: pointer; + justify-content: end; + } + + .row-op:last-child { + margin-right: 8px; + } + + .affine-microsheet-block-row:hover .show-on-hover-row { + visibility: visible; + opacity: 1; + } + + .row-op { + display: flex; + padding: 4px; + border-radius: 4px; + box-shadow: 0px 0px 4px 0px rgba(66, 65, 73, 0.14); + background-color: var(--affine-background-primary-color); + position: relative; + } + + .row-op:hover:before { + content: ''; + border-radius: 4px; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + background-color: var(--affine-hover-color); + } + + .row-op svg { + fill: var(--affine-icon-color); + color: var(--affine-icon-color); + width: 16px; + height: 16px; + } + .data-view-table-view-drag-handler { + width: 4px; + height: 38px; + display: flex; + align-items: center; + justify-content: center; + cursor: grab; + background-color: var(--affine-background-primary-color); + } + .microsheet-data-view-table-left-bar { + padding-left: 16px; + display: flex; + align-items: center; + position: sticky; + left: 0; + width: 24px; + flex-shrink: 0; + visibility: hidden; + z-index: 9; + background-color: var(--affine-background-primary-color); + } + .microsheet-data-view-table-view-drag-handler { + width: 8px; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + cursor: grab; + background-color: #eee; + } + .microsheet-data-view-table-view-drag-handler:hover { + background-color: blue; + } + .microsheet-data-view-table-view-add-icon { + position: absolute; + left: 0px; + top: -10px; + z-index: 9; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + } + .microsheet-data-view-table-view-add-not-active-icon { + margin-left: -2px; + width: 4px; + height: 4px; + border-radius: 4px; + background: #ddd; + } + .microsheet-data-view-table-view-bottom-add-icon { + top: unset; + bottom: -10px; + } + .microsheet-data-view-table-view-add-icon svg { + width: 20px; + height: 20px; + border-radius: 100px; + background: #4949fe; + color: white; + display: none; + } + .microsheet-data-view-table-view-add-icon:hover svg { + display: block; + } + `; + + private _clickDragHandler = (e: MouseEvent) => { + if (this.view.readonly$.value) { + return; + } + this.selectionController?.toggleRow(this.rowId, this.groupKey); + this.popMenu(e.currentTarget as HTMLElement); + }; + + private rowAdd = (before: boolean) => { + this.view.rowAdd({ + id: this.rowId, + before: before, + }); + }; + + contextMenu = (e: MouseEvent) => { + if (this.view.readonly$.value) { + return; + } + const selection = this.selectionController; + if (!selection) { + return; + } + e.preventDefault(); + const row = { id: this.rowId, groupKey: this.groupKey }; + if (!TableRowSelection.includes(selection.selection, row)) { + selection.selection = TableRowSelection.create({ + rows: [row], + }); + } + }; + + setSelection = (selection?: TableViewSelection) => { + if (this.selectionController) { + this.selectionController.selection = selection; + } + }; + + get groupKey() { + return this.closest('affine-microsheet-data-view-table-group')?.group?.key; + } + + get selectionController() { + return this.closest('affine-microsheet-table')?.selectionController; + } + + private popMenu(ele?: HTMLElement) { + popMenu(popupTargetFromElement(ele ?? this), { + options: { + items: [ + menu.group({ + items: [ + menu.action({ + name: 'Delete', + prefix: DeleteIcon(), + select: () => { + const selection = this.selectionController; + if (selection) { + selection.deleteRow(this.rowId); + } + }, + class: { 'delete-item': true }, + }), + ], + }), + ], + }, + }); + } + + override connectedCallback() { + super.connectedCallback(); + this.disposables.addFromEvent(this, 'contextmenu', this.contextMenu); + + this.classList.add('affine-microsheet-block-row', 'microsheet-row'); + } + + protected override render(): unknown { + const view = this.view; + return html` + ${view.readonly$.value + ? nothing + : html`
+
+
+
+ ${AddCursorIcon()} +
+
+
+ ${AddCursorIcon()} +
+
`} + ${repeat( + view.properties$.value, + v => v.id, + (column, i) => { + const clickDetail = () => { + if (!this.selectionController) { + return; + } + this.setSelection( + TableRowSelection.create({ + rows: [{ id: this.rowId, groupKey: this.groupKey }], + }) + ); + }; + const openMenu = () => { + if (!this.selectionController) { + return; + } + const row = { id: this.rowId, groupKey: this.groupKey }; + this.setSelection( + TableRowSelection.create({ + rows: [row], + }) + ); + }; + return html` +
+ + +
+ ${!column.readonly$.value && + column.view.mainProperties$.value.titleColumn === column.id + ? html`
+
+ ${CenterPeekIcon()} +
+ ${!view.readonly$.value + ? html`
+ ${MoreHorizontalIcon()} +
` + : nothing} +
` + : nothing} + `; + } + )} + + `; + } + + @property({ attribute: false }) + accessor dataViewEle!: DataViewRenderer; + + @property({ attribute: false }) + accessor rowId!: string; + + @property({ attribute: false }) + accessor rowIndex!: number; + + @property({ attribute: false }) + accessor std!: BlockStdScope; + + @property({ attribute: false }) + accessor view!: TableSingleView; +} + +declare global { + interface HTMLElementTagNameMap { + 'microsheet-data-view-table-row': TableRow; + } +} diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/table-view-manager.ts b/packages/affine/microsheet-data-view/src/view-presets/table/table-view-manager.ts new file mode 100644 index 000000000000..fab201f0d58b --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/table-view-manager.ts @@ -0,0 +1,328 @@ +import { + insertPositionToIndex, + type InsertToPosition, +} from '@blocksuite/affine-shared/utils'; +import { computed, type ReadonlySignal } from '@preact/signals-core'; + +import type { ViewManager } from '../../core/view-manager/view-manager.js'; +import type { TableViewData } from './define.js'; +import type { StatCalcOpType } from './types.js'; + +import { defaultGroupBy } from '../../core/common/group-by.js'; +import { + GroupManager, + sortByManually, +} from '../../core/common/group-by/helper.js'; +import { PropertyBase } from '../../core/view-manager/property.js'; +import { + type SingleView, + SingleViewBase, +} from '../../core/view-manager/single-view.js'; +import { DEFAULT_COLUMN_WIDTH } from './consts.js'; + +export class TableSingleView extends SingleViewBase { + propertiesWithoutFilter$ = computed(() => { + const needShow = new Set(this.dataSource.properties$.value); + const result: string[] = []; + this.data$.value?.columns.forEach(v => { + if (needShow.has(v.id)) { + result.push(v.id); + needShow.delete(v.id); + } + }); + result.push(...needShow); + return result; + }); + + private computedColumns$ = computed(() => { + return this.propertiesWithoutFilter$.value.map(id => { + const column = this.propertyGet(id); + return { + id: column.id, + hide: column.hide$.value, + width: column.width$.value, + statCalcType: column.statCalcOp$.value, + }; + }); + }); + + detailProperties$ = computed(() => { + return this.propertiesWithoutFilter$.value.filter(id => { + return this.propertyTypeGet(id) !== 'title'; + }); + }); + + groupBy$ = computed(() => { + return this.data$.value?.groupBy; + }); + + groupManager = new GroupManager(this.groupBy$, this, { + sortGroup: ids => + sortByManually( + ids, + v => v, + this.groupProperties.map(v => v.key) + ), + sortRow: (key, ids) => { + const property = this.groupProperties.find(v => v.key === key); + return sortByManually(ids, v => v, property?.manuallyCardSort ?? []); + }, + changeGroupSort: keys => { + const map = new Map(this.groupProperties.map(v => [v.key, v])); + this.dataUpdate(() => { + return { + groupProperties: keys.map(key => { + const property = map.get(key); + if (property) { + return property; + } + return { + key, + hide: false, + manuallyCardSort: [], + }; + }), + }; + }); + }, + changeRowSort: (groupKeys, groupKey, keys) => { + const map = new Map(this.groupProperties.map(v => [v.key, v])); + this.dataUpdate(() => { + return { + groupProperties: groupKeys.map(key => { + if (key === groupKey) { + const group = map.get(key); + return group + ? { + ...group, + manuallyCardSort: keys, + } + : { + key, + hide: false, + manuallyCardSort: keys, + }; + } else { + return ( + map.get(key) ?? { + key, + hide: false, + manuallyCardSort: [], + } + ); + } + }), + }; + }); + }, + }); + + mainProperties$ = computed(() => { + return ( + this.data$.value?.header ?? { + titleColumn: this.propertiesWithoutFilter$.value.find( + id => this.propertyTypeGet(id) === 'title' + ), + iconColumn: 'type', + } + ); + }); + + propertyIds$ = computed(() => { + return this.detailProperties$.value.filter(id => !this.propertyHideGet(id)); + }); + + readonly$ = computed(() => { + return this.manager.readonly$.value; + }); + + get groupProperties() { + return this.data$.value?.groupProperties ?? []; + } + + get name(): string { + return this.data$.value?.name ?? ''; + } + + override get type(): string { + return this.data$.value?.mode ?? 'table'; + } + + constructor(viewManager: ViewManager, viewId: string) { + super(viewManager, viewId); + } + + changeGroup(columnId: string | undefined) { + if (columnId == null) { + this.dataUpdate(() => { + return { + groupBy: undefined, + }; + }); + return; + } + const column = this.propertyGet(columnId); + this.dataUpdate(_view => { + return { + groupBy: defaultGroupBy( + this.propertyMetaGet(column.type$.value), + column.id, + column.data$.value + ), + }; + }); + } + + columnGetStatCalcOp(columnId: string): StatCalcOpType { + return this.data$.value?.columns.find(v => v.id === columnId)?.statCalcType; + } + + columnGetWidth(columnId: string): number { + const column = this.data$.value?.columns.find(v => v.id === columnId); + if (column?.width != null) { + return column.width; + } + const type = this.propertyTypeGet(columnId); + if (type === 'title') { + return 260; + } + return DEFAULT_COLUMN_WIDTH; + } + + columnUpdateStatCalcOp(columnId: string, op?: string): void { + this.dataUpdate(() => { + return { + columns: this.computedColumns$.value.map(v => + v.id === columnId + ? { + ...v, + statCalcType: op, + } + : v + ), + }; + }); + } + + columnUpdateWidth(columnId: string, width: number): void { + this.dataUpdate(() => { + return { + columns: this.computedColumns$.value.map(v => + v.id === columnId + ? { + ...v, + width: width, + } + : v + ), + }; + }); + } + + override isShow(): boolean { + return true; + } + + propertyGet(columnId: string): TableColumn { + return new TableColumn(this, columnId); + } + + propertyHideGet(columnId: string): boolean { + return ( + this.data$.value?.columns.find(v => v.id === columnId)?.hide ?? false + ); + } + + propertyHideSet(columnId: string, hide: boolean): void { + this.dataUpdate(() => { + return { + columns: this.computedColumns$.value.map(v => + v.id === columnId + ? { + ...v, + hide, + } + : v + ), + }; + }); + } + + propertyMove(columnId: string, toAfterOfColumn: InsertToPosition): void { + this.dataUpdate(() => { + const columnIndex = this.computedColumns$.value.findIndex( + v => v.id === columnId + ); + if (columnIndex < 0) { + return {}; + } + const columns = [...this.computedColumns$.value]; + const [column] = columns.splice(columnIndex, 1); + const index = insertPositionToIndex(toAfterOfColumn, columns); + columns.splice(index, 0, column); + return { + columns, + }; + }); + } + + override rowAdd( + insertPosition: InsertToPosition | number, + groupKey?: string + ): string { + const id = super.rowAdd(insertPosition); + if (!groupKey) { + return id; + } + this.groupManager.addToGroup(id, groupKey); + return id; + } + + override rowMove( + rowId: string, + position: InsertToPosition, + fromGroup?: string, + toGroup?: string + ) { + if (toGroup == null) { + super.rowMove(rowId, position); + return; + } + this.groupManager.moveCardTo(rowId, fromGroup, toGroup, position); + } + + override rowNextGet(rowId: string): string { + const index = this.rows$.value.indexOf(rowId); + return this.rows$.value[index + 1]; + } + + override rowPrevGet(rowId: string): string { + const index = this.rows$.value.indexOf(rowId); + return this.rows$.value[index - 1]; + } +} + +export class TableColumn extends PropertyBase { + statCalcOp$ = computed(() => { + return this.tableView.columnGetStatCalcOp(this.id); + }); + + width$: ReadonlySignal = computed(() => { + return this.tableView.columnGetWidth(this.id); + }); + + constructor( + private tableView: TableSingleView, + columnId: string + ) { + super(tableView as SingleView, columnId); + } + + updateStatCalcOp(type?: string): void { + return this.tableView.columnUpdateStatCalcOp(this.id, type); + } + + updateWidth(width: number): void { + this.tableView.columnUpdateWidth(this.id, width); + } +} diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/table-view.ts b/packages/affine/microsheet-data-view/src/view-presets/table/table-view.ts new file mode 100644 index 000000000000..d170de84b449 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/table-view.ts @@ -0,0 +1,307 @@ +import { + menu, + popMenu, + popupTargetFromElement, +} from '@blocksuite/affine-components/context-menu'; +import { + insertPositionToIndex, + type InsertToPosition, +} from '@blocksuite/affine-shared/utils'; +import { AddCursorIcon } from '@blocksuite/icons/lit'; +import { css } from 'lit'; +import { styleMap } from 'lit/directives/style-map.js'; +import { html } from 'lit/static-html.js'; + +import type { GroupManager } from '../../core/common/group-by/helper.js'; +import type { DataViewExpose } from '../../core/index.js'; +import type { TableSingleView } from './table-view-manager.js'; + +import { DataViewBase } from '../../core/view/data-view-base.js'; +import { LEFT_TOOL_BAR_WIDTH } from './consts.js'; +import { TableClipboardController } from './controller/clipboard.js'; +import { TableDragController } from './controller/drag.js'; +import { TableHotkeysController } from './controller/hotkeys.js'; +import { TableSelectionController } from './controller/selection.js'; +import { + TableAreaSelection, + type TableViewSelectionWithType, +} from './types.js'; + +const styles = css` + affine-microsheet-table { + position: relative; + display: flex; + flex-direction: column; + margin-left: -16px; + overflow: hidden; + } + + affine-microsheet-table * { + box-sizing: border-box; + } + + .affine-microsheet-table { + overflow-y: auto; + padding-top: 16px; + padding-bottom: 10px; + } + .affine-microsheet-table::-webkit-scrollbar { + display: none; + } + .affine-microsheet-block-title-container { + display: flex; + align-items: center; + justify-content: space-between; + height: 44px; + margin: 2px 0 2px; + } + + .affine-microsheet-block-table { + position: relative; + width: 100%; + z-index: 1; + /* overflow-x: scroll; + overflow-y: hidden; */ + } + + .affine-microsheet-block-table:hover { + padding-bottom: 0px; + } + + .affine-microsheet-block-table::-webkit-scrollbar { + -webkit-appearance: none; + display: block; + } + + .affine-microsheet-block-table::-webkit-scrollbar:horizontal { + height: 4px; + } + + .affine-microsheet-block-table::-webkit-scrollbar-thumb { + border-radius: 2px; + background-color: transparent; + } + + .affine-microsheet-block-table:hover::-webkit-scrollbar:horizontal { + height: 8px; + } + + .affine-microsheet-block-table:hover::-webkit-scrollbar-thumb { + border-radius: 16px; + background-color: var(--affine-black-30); + } + + .affine-microsheet-block-table:hover::-webkit-scrollbar-track { + background-color: var(--affine-hover-color); + } + + .affine-microsheet-table-container { + position: relative; + width: fit-content; + min-width: 100%; + } + + .affine-microsheet-block-tag-circle { + width: 12px; + height: 12px; + border-radius: 50%; + display: inline-block; + } + + .affine-microsheet-block-tag { + display: inline-flex; + border-radius: 11px; + align-items: center; + padding: 0 8px; + cursor: pointer; + } + + .microsheet-cell { + border-left: 1px solid var(--affine-border-color); + border-top: 1px solid var(--affine-border-color); + } + + .data-view-table-left-bar { + display: flex; + align-items: center; + position: sticky; + z-index: 1; + left: 0; + width: ${LEFT_TOOL_BAR_WIDTH}px; + flex-shrink: 0; + } + + .affine-microsheet-block-rows { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + } + + .affine-microsheet-block-rows + > affine-row:last-child + microsheet-data-view-table-row + affine-microsheet-cell-container { + border-bottom: 1px solid var(--affine-border-color); + } +`; + +export class DataViewTable extends DataViewBase< + TableSingleView, + TableViewSelectionWithType +> { + static override styles = styles; + + private _addRow = ( + tableViewManager: TableSingleView, + position: InsertToPosition | number + ) => { + if (this.readonly) return; + + const index = + typeof position === 'number' + ? position + : insertPositionToIndex( + position, + this.props.view.rows$.value.map(id => ({ id })) + ); + tableViewManager.rowAdd(position); + requestAnimationFrame(() => { + this.selectionController.selection = TableAreaSelection.create({ + focus: { + rowIndex: index, + columnIndex: 0, + }, + isEditing: true, + }); + }); + }; + + clipboardController = new TableClipboardController(this); + + dragController = new TableDragController(this); + + selectionController = new TableSelectionController(this); + + expose: DataViewExpose = { + addRow: position => { + this._addRow(this.props.view, position); + }, + focusFirstCell: () => { + this.selectionController.focusFirstCell(); + }, + showIndicator: evt => { + return this.dragController.showIndicator(evt) != null; + }, + hideIndicator: () => { + this.dragController.dropPreview.remove(); + }, + moveTo: (id, evt) => { + const result = this.dragController.getInsertPosition(evt); + if (result) { + this.props.view.rowMove( + id, + result.position, + undefined, + result.groupKey + ); + } + }, + getSelection: () => { + return this.selectionController.selection; + }, + }; + + hotkeysController = new TableHotkeysController(this); + + onWheel = (event: WheelEvent) => { + if (event.metaKey || event.ctrlKey) { + return; + } + const ele = event.currentTarget; + if (ele instanceof HTMLElement) { + if (ele.scrollWidth === ele.clientWidth) { + return; + } + event.stopPropagation(); + } + }; + + renderAddGroup = (groupHelper: GroupManager) => { + const addGroup = groupHelper.addGroup; + if (!addGroup) { + return; + } + const add = (e: MouseEvent) => { + const ele = e.currentTarget as HTMLElement; + popMenu(popupTargetFromElement(ele), { + options: { + items: [ + menu.input({ + onComplete: text => { + const column = groupHelper.property$.value; + if (column) { + column.dataUpdate( + () => addGroup(text, column.data$.value) as never + ); + } + }, + }), + ], + }, + }); + }; + return html`
+
+
${AddCursorIcon()}
+
New Group
+
+
`; + }; + + private get readonly() { + return this.props.view.readonly$.value; + } + + private renderTable() { + return html` `; + } + + override render() { + const vPadding = this.props.virtualPadding$.value; + const wrapperStyle = styleMap({ + marginLeft: `-${vPadding}px`, + marginRight: `-${vPadding}px`, + }); + const containerStyle = styleMap({ + paddingLeft: `${vPadding}px`, + paddingRight: `${vPadding}px`, + }); + return html` +
+
+
+ ${this.renderTable()} +
+
+
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-microsheet-table': DataViewTable; + } +} diff --git a/packages/affine/microsheet-data-view/src/view-presets/table/types.ts b/packages/affine/microsheet-data-view/src/view-presets/table/types.ts new file mode 100644 index 000000000000..588560b14ce4 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/view-presets/table/types.ts @@ -0,0 +1,126 @@ +import { assertExists } from '@blocksuite/global/utils'; + +export type ColumnType = string; + +export interface Column< + Data extends Record = Record, +> { + id: string; + type: ColumnType; + name: string; + data: Data; +} + +export type StatCalcOpType = string | undefined; + +export const getTableContainer = (ele: HTMLElement) => { + const element = ele.closest( + '.affine-microsheet-table-container' + ) as HTMLElement; + assertExists(element); + return element; +}; +type WithTableViewType = T extends unknown + ? { + viewId: string; + type: 'table'; + } & T + : never; +export type RowWithGroup = { + id: string; + groupKey?: string; +}; +export const RowWithGroup = { + equal(a?: RowWithGroup, b?: RowWithGroup) { + if (a == null || b == null) { + return false; + } + return a.id === b.id && a.groupKey === b.groupKey; + }, +}; +export type TableRowSelection = { + selectionType: 'row'; + rows: RowWithGroup[]; +}; +export const TableRowSelection = { + rows: (selection?: TableViewSelection): RowWithGroup[] => { + if (selection?.selectionType === 'row') { + return selection.rows; + } + return []; + }, + rowsIds: (selection?: TableViewSelection): string[] => { + return TableRowSelection.rows(selection).map(v => v.id); + }, + includes( + selection: TableViewSelection | undefined, + row: RowWithGroup + ): boolean { + if (!selection) { + return false; + } + return TableRowSelection.rows(selection).some(v => + RowWithGroup.equal(v, row) + ); + }, + create(options: { rows: RowWithGroup[] }): TableRowSelection { + return { + selectionType: 'row', + rows: options.rows, + }; + }, + is(selection?: TableViewSelection): selection is TableRowSelection { + return selection?.selectionType === 'row'; + }, +}; +export type TableAreaSelection = { + selectionType: 'area'; + groupKey?: string; + rowsSelection: MultiSelection; + columnsSelection: MultiSelection; + focus: CellFocus; + isEditing: boolean; +}; +export const TableAreaSelection = { + create: (options: { + groupKey?: string; + focus: CellFocus; + rowsSelection?: MultiSelection; + columnsSelection?: MultiSelection; + isEditing: boolean; + }): TableAreaSelection => { + return { + ...options, + selectionType: 'area', + rowsSelection: options.rowsSelection ?? { + start: options.focus.rowIndex, + end: options.focus.rowIndex, + }, + columnsSelection: options.columnsSelection ?? { + start: options.focus.columnIndex, + end: options.focus.columnIndex, + }, + }; + }, + isFocus(selection: TableAreaSelection) { + return ( + selection.focus.rowIndex === selection.rowsSelection.start && + selection.focus.rowIndex === selection.rowsSelection.end && + selection.focus.columnIndex === selection.columnsSelection.start && + selection.focus.columnIndex === selection.columnsSelection.end + ); + }, +}; + +export type CellFocus = { + rowIndex: number; + columnIndex: number; +}; +export type MultiSelection = { + start: number; + end: number; +}; +export type TableViewSelection = TableAreaSelection | TableRowSelection; +export type TableViewSelectionWithType = WithTableViewType< + TableAreaSelection | TableRowSelection +>; diff --git a/packages/affine/microsheet-data-view/src/widget-presets/index.ts b/packages/affine/microsheet-data-view/src/widget-presets/index.ts new file mode 100644 index 000000000000..ca54b0d46056 --- /dev/null +++ b/packages/affine/microsheet-data-view/src/widget-presets/index.ts @@ -0,0 +1,7 @@ +import { createWidgetTools, toolsWidgetPresets } from './tools/index.js'; + +export const widgetPresets = { + viewBar: null, + createTools: createWidgetTools, + tools: toolsWidgetPresets, +}; diff --git a/packages/affine/microsheet-data-view/src/widget-presets/tools/index.ts b/packages/affine/microsheet-data-view/src/widget-presets/tools/index.ts new file mode 100644 index 000000000000..5cbdde7d788f --- /dev/null +++ b/packages/affine/microsheet-data-view/src/widget-presets/tools/index.ts @@ -0,0 +1,21 @@ +import type { + MicrosheetDataViewWidget, + MicrosheetDataViewWidgetProps, +} from '../../core/widget/types.js'; + +import { createUniComponentFromWebComponent } from '../../core/index.js'; +import { uniMap } from '../../core/utils/uni-component/operation.js'; +import { DataViewHeaderTools } from './tools-renderer.js'; + +export const toolsWidgetPresets = {}; +export const createWidgetTools = ( + toolsMap: Record +) => { + return uniMap( + createUniComponentFromWebComponent(DataViewHeaderTools), + (props: MicrosheetDataViewWidgetProps) => ({ + ...props, + toolsMap, + }) + ); +}; diff --git a/packages/affine/microsheet-data-view/src/widget-presets/tools/tools-renderer.ts b/packages/affine/microsheet-data-view/src/widget-presets/tools/tools-renderer.ts new file mode 100644 index 000000000000..0cba0ea6cb4b --- /dev/null +++ b/packages/affine/microsheet-data-view/src/widget-presets/tools/tools-renderer.ts @@ -0,0 +1,85 @@ +import { css, html } from 'lit'; +import { property, state } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { repeat } from 'lit/directives/repeat.js'; + +import type { SingleView } from '../../core/view-manager/single-view.js'; +import type { ViewManager } from '../../core/view-manager/view-manager.js'; +import type { + MicrosheetDataViewWidget, + MicrosheetDataViewWidgetProps, +} from '../../core/widget/types.js'; + +import { type DataViewExpose, renderUniLit } from '../../core/index.js'; +import { WidgetBase } from '../../core/widget/widget-base.js'; + +const styles = css` + .affine-microsheet-toolbar { + display: flex; + align-items: center; + gap: 6px; + visibility: hidden; + opacity: 0; + transition: opacity 150ms cubic-bezier(0.42, 0, 1, 1); + } + + .toolbar-hover-container:hover .affine-microsheet-toolbar { + visibility: visible; + opacity: 1; + } + + .show-toolbar { + visibility: visible; + opacity: 1; + } + + @media print { + .affine-microsheet-toolbar { + display: none; + } + } +`; + +export class DataViewHeaderTools extends WidgetBase { + static override styles = styles; + + override render() { + const classList = classMap({ + 'show-toolbar': this.showToolBar, + 'affine-microsheet-toolbar': true, + }); + const tools = this.toolsMap[this.view.type]; + return html`
+ ${repeat(tools ?? [], uni => { + const props: MicrosheetDataViewWidgetProps = { + view: this.view, + viewMethods: this.viewMethods, + }; + return renderUniLit(uni, props); + })} +
`; + } + + @state() + accessor showToolBar = false; + + @property({ attribute: false }) + accessor toolsMap!: Record; +} + +declare global { + interface HTMLElementTagNameMap { + 'microsheet-data-view-header-tools': DataViewHeaderTools; + } +} +export const renderTools = ( + view: SingleView, + viewMethods: DataViewExpose, + viewSource: ViewManager +) => { + return html``; +}; diff --git a/packages/affine/microsheet-data-view/tsconfig.json b/packages/affine/microsheet-data-view/tsconfig.json new file mode 100644 index 000000000000..36f0b6ac4da7 --- /dev/null +++ b/packages/affine/microsheet-data-view/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src/", + "outDir": "./dist/", + "noEmit": false + }, + "include": ["./src"], + "references": [ + { + "path": "../components" + }, + { + "path": "../shared" + }, + { + "path": "../../framework/block-std" + }, + { + "path": "../../framework/global" + }, + { + "path": "../../framework/store" + } + ] +} diff --git a/packages/affine/microsheet-data-view/typedoc.json b/packages/affine/microsheet-data-view/typedoc.json new file mode 100644 index 000000000000..101e923dbadb --- /dev/null +++ b/packages/affine/microsheet-data-view/typedoc.json @@ -0,0 +1,4 @@ +{ + "extends": ["../../../typedoc.base.json"], + "entryPoints": ["src/index.ts"] +} diff --git a/packages/affine/microsheet-data-view/vitest.config.ts b/packages/affine/microsheet-data-view/vitest.config.ts new file mode 100644 index 000000000000..1e76565bf5f7 --- /dev/null +++ b/packages/affine/microsheet-data-view/vitest.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + esbuild: { + target: 'es2018', + }, + test: { + globalSetup: '../../scripts/vitest-global.ts', + include: ['src/__tests__/**/*.unit.spec.ts'], + testTimeout: 1000, + coverage: { + provider: 'istanbul', // or 'c8' + reporter: ['lcov'], + reportsDirectory: '../../.coverage/blocks', + }, + /** + * Custom handler for console.log in tests. + * + * Return `false` to ignore the log. + */ + onConsoleLog(log, type) { + if (log.includes('https://lit.dev/msg/dev-mode')) { + return false; + } + console.warn(`Unexpected ${type} log`, log); + throw new Error(log); + }, + environment: 'happy-dom', + }, +}); diff --git a/packages/affine/model/src/blocks/attachment/attachment-model.ts b/packages/affine/model/src/blocks/attachment/attachment-model.ts index 585e93d4b2cd..e283caedece1 100644 --- a/packages/affine/model/src/blocks/attachment/attachment-model.ts +++ b/packages/affine/model/src/blocks/attachment/attachment-model.ts @@ -81,6 +81,7 @@ export const AttachmentBlockSchema = defineBlockSchema({ 'affine:edgeless-text', 'affine:paragraph', 'affine:list', + 'affine:cell', ], }, transformer: () => new AttachmentBlockTransformer(), diff --git a/packages/affine/model/src/blocks/bookmark/bookmark-model.ts b/packages/affine/model/src/blocks/bookmark/bookmark-model.ts index 79da4d91e418..d36588c540bb 100644 --- a/packages/affine/model/src/blocks/bookmark/bookmark-model.ts +++ b/packages/affine/model/src/blocks/bookmark/bookmark-model.ts @@ -50,6 +50,7 @@ export const BookmarkBlockSchema = defineBlockSchema({ 'affine:edgeless-text', 'affine:paragraph', 'affine:list', + 'affine:cell', ], }, toModel: () => new BookmarkBlockModel(), diff --git a/packages/affine/model/src/blocks/cell/cell-model.ts b/packages/affine/model/src/blocks/cell/cell-model.ts new file mode 100644 index 000000000000..3714eedb2ee0 --- /dev/null +++ b/packages/affine/model/src/blocks/cell/cell-model.ts @@ -0,0 +1,31 @@ +import { defineBlockSchema, type SchemaToModel } from '@blocksuite/store'; + +export const CellBlockSchema = defineBlockSchema({ + flavour: 'affine:cell', + metadata: { + version: 1, + role: 'hub', + parent: ['affine:row'], + children: [ + 'affine:paragraph', + 'affine:list', + 'affine:code', + 'affine:divider', + 'affine:image', + 'affine:bookmark', + 'affine:attachment', + 'affine:surface-ref', + 'affine:embed-*', + ], + }, +}); + +export type CellBlockModel = SchemaToModel; + +declare global { + namespace BlockSuite { + interface BlockModels { + 'affine:cell': CellBlockModel; + } + } +} diff --git a/packages/affine/model/src/blocks/cell/index.ts b/packages/affine/model/src/blocks/cell/index.ts new file mode 100644 index 000000000000..6f2a1ac6e550 --- /dev/null +++ b/packages/affine/model/src/blocks/cell/index.ts @@ -0,0 +1 @@ +export * from './cell-model.js'; diff --git a/packages/affine/model/src/blocks/code/code-model.ts b/packages/affine/model/src/blocks/code/code-model.ts index 74fbb8e33234..997ac5a21c1a 100644 --- a/packages/affine/model/src/blocks/code/code-model.ts +++ b/packages/affine/model/src/blocks/code/code-model.ts @@ -28,6 +28,7 @@ export const CodeBlockSchema = defineBlockSchema({ 'affine:paragraph', 'affine:list', 'affine:edgeless-text', + 'affine:cell', ], children: [], }, diff --git a/packages/affine/model/src/blocks/index.ts b/packages/affine/model/src/blocks/index.ts index 67d6f1e4bbd2..7b8e0c107d00 100644 --- a/packages/affine/model/src/blocks/index.ts +++ b/packages/affine/model/src/blocks/index.ts @@ -1,5 +1,6 @@ export * from './attachment/index.js'; export * from './bookmark/index.js'; +export * from './cell/index.js'; export * from './code/index.js'; export * from './database/index.js'; export * from './divider/index.js'; @@ -9,7 +10,9 @@ export * from './frame/index.js'; export * from './image/index.js'; export * from './latex/index.js'; export * from './list/index.js'; +export * from './microsheet/index.js'; export * from './note/index.js'; export * from './paragraph/index.js'; export * from './root/index.js'; +export * from './row/index.js'; export * from './surface-ref/index.js'; diff --git a/packages/affine/model/src/blocks/latex/latex-model.ts b/packages/affine/model/src/blocks/latex/latex-model.ts index 31f1a6b23dc4..746d04c16436 100644 --- a/packages/affine/model/src/blocks/latex/latex-model.ts +++ b/packages/affine/model/src/blocks/latex/latex-model.ts @@ -27,6 +27,7 @@ export const LatexBlockSchema = defineBlockSchema({ 'affine:edgeless-text', 'affine:paragraph', 'affine:list', + 'affine:cell', ], }, toModel: () => { diff --git a/packages/affine/model/src/blocks/list/list-model.ts b/packages/affine/model/src/blocks/list/list-model.ts index a56d34f3db06..3d8337a1d2d9 100644 --- a/packages/affine/model/src/blocks/list/list-model.ts +++ b/packages/affine/model/src/blocks/list/list-model.ts @@ -34,6 +34,7 @@ export const ListBlockSchema = defineBlockSchema({ 'affine:list', 'affine:paragraph', 'affine:edgeless-text', + 'affine:cell', ], }, }); diff --git a/packages/affine/model/src/blocks/microsheet/index.ts b/packages/affine/model/src/blocks/microsheet/index.ts new file mode 100644 index 000000000000..d650b1350482 --- /dev/null +++ b/packages/affine/model/src/blocks/microsheet/index.ts @@ -0,0 +1,2 @@ +export * from './microsheet-model.js'; +export * from './types.js'; diff --git a/packages/affine/model/src/blocks/microsheet/microsheet-model.ts b/packages/affine/model/src/blocks/microsheet/microsheet-model.ts new file mode 100644 index 000000000000..4b52f4d0479e --- /dev/null +++ b/packages/affine/model/src/blocks/microsheet/microsheet-model.ts @@ -0,0 +1,37 @@ +import type { Text } from '@blocksuite/store'; + +import { BlockModel, defineBlockSchema } from '@blocksuite/store'; + +import type { + MicrosheetColumn as Column, + MicrosheetSerializedCells as SerializedCells, + MicrosheetViewBasicDataType as ViewBasicDataType, +} from './types.js'; + +export type MicrosheetBlockProps = { + views: ViewBasicDataType[]; + title: Text; + cells: SerializedCells; + columns: Array; + // rowId -> pageId + notes?: Record; +}; + +export class MicrosheetBlockModel extends BlockModel {} + +export const MicrosheetBlockSchema = defineBlockSchema({ + flavour: 'affine:microsheet', + props: (internal): MicrosheetBlockProps => ({ + views: [], + title: internal.Text(), + cells: Object.create(null), + columns: [], + }), + metadata: { + role: 'hub', + version: 3, + parent: ['affine:note'], + children: ['affine:row'], + }, + toModel: () => new MicrosheetBlockModel(), +}); diff --git a/packages/affine/model/src/blocks/microsheet/types.ts b/packages/affine/model/src/blocks/microsheet/types.ts new file mode 100644 index 000000000000..52c22638166d --- /dev/null +++ b/packages/affine/model/src/blocks/microsheet/types.ts @@ -0,0 +1,27 @@ +export interface MicrosheetColumn< + Data extends Record = Record, +> { + id: string; + type: string; + name: string; + data: Data; +} + +export type MicrosheetColumnUpdater< + T extends MicrosheetColumn = MicrosheetColumn, +> = (data: T) => Partial; +export type MicrosheetCell = { + columnId: MicrosheetColumn['id']; + value: ValueType; + ref: string; +}; + +export type MicrosheetSerializedCells = Record< + string, + Record +>; +export type MicrosheetViewBasicDataType = { + id: string; + name: string; + mode: string; +}; diff --git a/packages/affine/model/src/blocks/note/note-model.ts b/packages/affine/model/src/blocks/note/note-model.ts index 4a5767150dfb..4b2032cfa395 100644 --- a/packages/affine/model/src/blocks/note/note-model.ts +++ b/packages/affine/model/src/blocks/note/note-model.ts @@ -48,6 +48,8 @@ export const NoteBlockSchema = defineBlockSchema({ 'affine:code', 'affine:divider', 'affine:database', + 'affine:microsheet', + 'affine:microsheet-data-view', 'affine:data-view', 'affine:image', 'affine:bookmark', diff --git a/packages/affine/model/src/blocks/paragraph/paragraph-model.ts b/packages/affine/model/src/blocks/paragraph/paragraph-model.ts index f8d37b6caaa1..418eb24f4e72 100644 --- a/packages/affine/model/src/blocks/paragraph/paragraph-model.ts +++ b/packages/affine/model/src/blocks/paragraph/paragraph-model.ts @@ -32,6 +32,7 @@ export const ParagraphBlockSchema = defineBlockSchema({ 'affine:paragraph', 'affine:list', 'affine:edgeless-text', + 'affine:cell', ], }, toModel: () => new ParagraphBlockModel(), diff --git a/packages/affine/model/src/blocks/row/index.ts b/packages/affine/model/src/blocks/row/index.ts new file mode 100644 index 000000000000..8f67cb41153b --- /dev/null +++ b/packages/affine/model/src/blocks/row/index.ts @@ -0,0 +1 @@ +export * from './row-model.js'; diff --git a/packages/affine/model/src/blocks/row/row-model.ts b/packages/affine/model/src/blocks/row/row-model.ts new file mode 100644 index 000000000000..d32db7770324 --- /dev/null +++ b/packages/affine/model/src/blocks/row/row-model.ts @@ -0,0 +1,21 @@ +import { defineBlockSchema, type SchemaToModel } from '@blocksuite/store'; + +export const RowBlockSchema = defineBlockSchema({ + flavour: 'affine:row', + metadata: { + version: 1, + role: 'hub', + parent: ['affine:microsheet'], + children: ['affine:cell'], + }, +}); + +export type RowBlockModel = SchemaToModel; + +declare global { + namespace BlockSuite { + interface BlockModels { + 'affine:row': RowBlockModel; + } + } +} diff --git a/packages/affine/model/src/blocks/surface-ref/surface-ref-model.ts b/packages/affine/model/src/blocks/surface-ref/surface-ref-model.ts index 636b161b8e33..422cd52ecfbe 100644 --- a/packages/affine/model/src/blocks/surface-ref/surface-ref-model.ts +++ b/packages/affine/model/src/blocks/surface-ref/surface-ref-model.ts @@ -16,7 +16,7 @@ export const SurfaceRefBlockSchema = defineBlockSchema({ metadata: { version: 1, role: 'content', - parent: ['affine:note', 'affine:paragraph', 'affine:list'], + parent: ['affine:note', 'affine:paragraph', 'affine:list', 'affine:cell'], }, }); diff --git a/packages/affine/shared/src/types/index.ts b/packages/affine/shared/src/types/index.ts index d36c71ed7a03..44ace55dbb2e 100644 --- a/packages/affine/shared/src/types/index.ts +++ b/packages/affine/shared/src/types/index.ts @@ -20,6 +20,8 @@ export type NoteChildrenFlavour = | 'affine:divider' | 'affine:database' | 'affine:data-view' + | 'affine:microsheet' + | 'affine:microsheet-data-view' | 'affine:image' | 'affine:bookmark' | 'affine:attachment' diff --git a/packages/blocks/package.json b/packages/blocks/package.json index d2ea76b142b8..32dc051c65ae 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -33,6 +33,7 @@ "@blocksuite/global": "workspace:*", "@blocksuite/icons": "^2.1.75", "@blocksuite/inline": "workspace:*", + "@blocksuite/microsheet-data-view": "workspace:*", "@blocksuite/store": "workspace:*", "@floating-ui/dom": "^1.6.10", "@lit/context": "^1.1.2", diff --git a/packages/blocks/src/_common/transformers/middlewares.ts b/packages/blocks/src/_common/transformers/middlewares.ts index 369bdc3783d8..d32d3056921a 100644 --- a/packages/blocks/src/_common/transformers/middlewares.ts +++ b/packages/blocks/src/_common/transformers/middlewares.ts @@ -125,6 +125,12 @@ export const replaceIdMiddleware: JobMiddleware = ({ slots, collection }) => { if (payload.type === 'block') { const { snapshot } = payload; + if ( + snapshot.flavour === 'affine:cell' || + snapshot.flavour === 'affine:row' + ) { + return; + } if (snapshot.flavour === 'affine:page') { const index = snapshot.children.findIndex( c => c.flavour === 'affine:surface' diff --git a/packages/blocks/src/_specs/common.ts b/packages/blocks/src/_specs/common.ts index ce4f106377d0..c235edcd94e0 100644 --- a/packages/blocks/src/_specs/common.ts +++ b/packages/blocks/src/_specs/common.ts @@ -12,15 +12,19 @@ import { } from '../_common/adapters/extension.js'; import { AttachmentBlockSpec } from '../attachment-block/attachment-spec.js'; import { BookmarkBlockSpec } from '../bookmark-block/bookmark-spec.js'; +import { CellBlockSpec } from '../cell-block/cell-spec.js'; import { CodeBlockSpec } from '../code-block/code-block-spec.js'; import { DataViewBlockSpec } from '../data-view-block/data-view-spec.js'; import { DatabaseBlockSpec } from '../database-block/database-spec.js'; import { DividerBlockSpec } from '../divider-block/divider-spec.js'; import { ImageBlockSpec } from '../image-block/image-spec.js'; +import { MicrosheetBlockSpec } from '../microsheet-block/microsheet-spec.js'; +import { MicrosheetDataViewBlockSpec } from '../microsheet-data-view-block/data-view-spec.js'; import { EdgelessNoteBlockSpec, NoteBlockSpec, } from '../note-block/note-spec.js'; +import { RowBlockSpec } from '../row-block/row-spec.js'; export const CommonFirstPartyBlockSpecs: ExtensionType[] = [ RichTextExtensions, @@ -28,7 +32,11 @@ export const CommonFirstPartyBlockSpecs: ExtensionType[] = [ ListBlockSpec, NoteBlockSpec, DatabaseBlockSpec, + MicrosheetBlockSpec, + RowBlockSpec, + CellBlockSpec, DataViewBlockSpec, + MicrosheetDataViewBlockSpec, DividerBlockSpec, CodeBlockSpec, ImageBlockSpec, @@ -46,7 +54,11 @@ export const EdgelessFirstPartyBlockSpecs: ExtensionType[] = [ ListBlockSpec, EdgelessNoteBlockSpec, DatabaseBlockSpec, + MicrosheetBlockSpec, + RowBlockSpec, + CellBlockSpec, DataViewBlockSpec, + MicrosheetDataViewBlockSpec, DividerBlockSpec, CodeBlockSpec, ImageBlockSpec, diff --git a/packages/blocks/src/_specs/group/common.ts b/packages/blocks/src/_specs/group/common.ts index 10df7ac8ba7f..5e37a4197772 100644 --- a/packages/blocks/src/_specs/group/common.ts +++ b/packages/blocks/src/_specs/group/common.ts @@ -17,6 +17,8 @@ import { DataViewBlockSpec } from '../../data-view-block/data-view-spec.js'; import { DatabaseBlockSpec } from '../../database-block/database-spec.js'; import { DividerBlockSpec } from '../../divider-block/divider-spec.js'; import { ImageBlockSpec } from '../../image-block/image-spec.js'; +import { MicrosheetBlockSpec } from '../../microsheet-block/microsheet-spec.js'; +import { MicrosheetDataViewBlockSpec } from '../../microsheet-data-view-block/data-view-spec.js'; import { EdgelessNoteBlockSpec, NoteBlockSpec, @@ -39,6 +41,8 @@ export { EmbedYoutubeBlockSpec, ImageBlockSpec, ListBlockSpec, + MicrosheetBlockSpec, + MicrosheetDataViewBlockSpec, NoteBlockSpec, ParagraphBlockSpec, }; diff --git a/packages/blocks/src/cell-block/cell-block.ts b/packages/blocks/src/cell-block/cell-block.ts new file mode 100644 index 000000000000..c5c488d7fabf --- /dev/null +++ b/packages/blocks/src/cell-block/cell-block.ts @@ -0,0 +1,39 @@ +/// + +import type { CellBlockModel } from '@blocksuite/affine-model'; + +import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption'; +import { html } from 'lit'; +import { property } from 'lit/decorators.js'; + +import type { CellBlockService } from './cell-service.js'; + +import { KeymapController } from './keymap-controller.js'; +import { cellBlockStyles } from './styles.js'; + +export class CellBlockComponent extends CaptionedBlockComponent< + CellBlockModel, + CellBlockService +> { + static override styles = cellBlockStyles; + + keymapController = new KeymapController(this); + + override connectedCallback() { + super.connectedCallback(); + this.keymapController.bind(); + } + + override renderBlock() { + return html`${this.renderChildren(this.model)}`; + } + + @property({ attribute: false }) + override accessor widgets = {}; +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-cell': CellBlockComponent; + } +} diff --git a/packages/blocks/src/cell-block/cell-service.ts b/packages/blocks/src/cell-block/cell-service.ts new file mode 100644 index 000000000000..784516768f39 --- /dev/null +++ b/packages/blocks/src/cell-block/cell-service.ts @@ -0,0 +1,10 @@ +import { CellBlockSchema } from '@blocksuite/affine-model'; +import { BlockService } from '@blocksuite/block-std'; + +export class CellBlockService extends BlockService { + static override readonly flavour = CellBlockSchema.model.flavour; + + override mounted(): void { + super.mounted(); + } +} diff --git a/packages/blocks/src/cell-block/cell-spec.ts b/packages/blocks/src/cell-block/cell-spec.ts new file mode 100644 index 000000000000..ee725f704623 --- /dev/null +++ b/packages/blocks/src/cell-block/cell-spec.ts @@ -0,0 +1,14 @@ +import { + BlockViewExtension, + type ExtensionType, + FlavourExtension, +} from '@blocksuite/block-std'; +import { literal } from 'lit/static-html.js'; + +import { CellBlockService } from './cell-service.js'; + +export const CellBlockSpec: ExtensionType[] = [ + FlavourExtension('affine:cell'), + CellBlockService, + BlockViewExtension('affine:cell', literal`affine-cell`), +]; diff --git a/packages/blocks/src/cell-block/index.ts b/packages/blocks/src/cell-block/index.ts new file mode 100644 index 000000000000..70919afb5da5 --- /dev/null +++ b/packages/blocks/src/cell-block/index.ts @@ -0,0 +1,16 @@ +import type { CellBlockModel } from '@blocksuite/affine-model'; + +import type { CellBlockService } from './cell-service.js'; + +export * from './cell-block.js'; +export * from './cell-service.js'; +declare global { + namespace BlockSuite { + interface BlockServices { + 'affine:cell': CellBlockService; + } + interface BlockModels { + 'affine:cell': CellBlockModel; + } + } +} diff --git a/packages/blocks/src/cell-block/keymap-controller.ts b/packages/blocks/src/cell-block/keymap-controller.ts new file mode 100644 index 000000000000..519bd48404df --- /dev/null +++ b/packages/blocks/src/cell-block/keymap-controller.ts @@ -0,0 +1,127 @@ +/* eslint-disable */ +import type { + BaseSelection, + BlockComponent, + UIEventHandler, + UIEventStateContext, +} from '@blocksuite/block-std'; +import type { ReactiveController } from 'lit'; + +import type { ReactiveControllerHost } from 'lit'; +import type { BlockModel } from '@blocksuite/store'; + +export const ensureBlockInContainer = ( + blockElement: BlockComponent, + containerElement: BlockComponent +) => + containerElement.contains(blockElement) && blockElement !== containerElement; + +export class KeymapController implements ReactiveController { + private _onEnter = () => { + const [result] = this._std.command + // @ts-expect-error + .pipe() + .getBlockSelections() + // @ts-expect-error + .inline((ctx, next) => { + const blockSelection = ctx.currentBlockSelections?.at(-1); + if (!blockSelection) { + return; + } + + const { view, page, selection } = ctx.std; + + const element = view.viewFromPath('block', blockSelection.path); + if (!element) { + return; + } + + const { model } = element; + const parent = page.getParent(model); + console.log(model, parent); + if (!parent) { + return; + } + + const index = parent.children.indexOf(model) ?? undefined; + + const blockId = page.addBlock( + 'affine:paragraph', + {}, + parent, + index + 1 + ); + + const sel = selection.create('text', { + from: { + path: element.parentPath.concat(blockId), + index: 0, + length: 0, + }, + to: null, + }); + + selection.setGroup('cell', [sel]); + + return next(); + }) + .run(); + + return result; + }; + + private _onSelectAll: UIEventHandler = () => { + const childrenModels = this.host.model.children; + if ( + this._std.selection.filter('block').length === childrenModels.length && + this._std.selection + .filter('block') + .every((block: BaseSelection) => + childrenModels.some((model: BlockModel) => model.id === block.blockId) + ) + ) { + return; + } + const childrenBlocksSelection = this.host.model.children.map( + (model: BlockModel) => + this._std.selection.create('block', { blockId: model.id }) + ); + this._std.selection.setGroup('note', childrenBlocksSelection); + return true; + }; + + private _reset = () => {}; + + bind = () => { + this.host.handleEvent('keyDown', (ctx: UIEventStateContext) => { + const state = ctx.get('keyboardState'); + if (state.raw.key === 'Shift') { + return; + } + this._reset(); + }); + + this.host.bindHotKey({ + Enter: this._onEnter, + 'Mod-a': this._onSelectAll, + }); + }; + + host: ReactiveControllerHost & BlockComponent; + + private get _std() { + return this.host.std; + } + + constructor(host: ReactiveControllerHost & BlockComponent) { + (this.host = host).addController(this); + } + + hostConnected() { + this._reset(); + } + + hostDisconnected() { + this._reset(); + } +} diff --git a/packages/blocks/src/cell-block/styles.ts b/packages/blocks/src/cell-block/styles.ts new file mode 100644 index 000000000000..797e0d2f2019 --- /dev/null +++ b/packages/blocks/src/cell-block/styles.ts @@ -0,0 +1,13 @@ +import { css } from 'lit'; + +export const cellBlockStyles = css` + affine-cell { + width: 100%; + } + .affine-cell-block-container { + display: flow-root; + } + .affine-cell-block-container.selected { + background-color: var(--affine-hover-color); + } +`; diff --git a/packages/blocks/src/effects.ts b/packages/blocks/src/effects.ts index 0e2a35e94292..39812845b36b 100644 --- a/packages/blocks/src/effects.ts +++ b/packages/blocks/src/effects.ts @@ -17,6 +17,7 @@ import { effects as widgetScrollAnchoringEffects } from '@blocksuite/affine-widg import { effects as stdEffects } from '@blocksuite/block-std/effects'; import { effects as dataViewEffects } from '@blocksuite/data-view/effects'; import { effects as inlineEffects } from '@blocksuite/inline/effects'; +import { effects as microsheetDataViewEffects } from '@blocksuite/microsheet-data-view/effects'; import type { insertBookmarkCommand } from './bookmark-block/commands/insert-bookmark.js'; import type { insertEdgelessTextCommand } from './edgeless-text-block/commands/insert-edgeless-text.js'; @@ -62,6 +63,10 @@ import { BookmarkBlockComponent, type BookmarkBlockService, } from './bookmark-block/index.js'; +import { + CellBlockComponent, + type CellBlockService, +} from './cell-block/index.js'; import { AffineCodeUnit } from './code-block/highlight/affine-code-unit.js'; import { CodeBlockComponent, @@ -104,6 +109,11 @@ import { } from './image-block/index.js'; import { effects as blockLatexEffects } from './latex-block/effects.js'; import { LatexBlockComponent } from './latex-block/index.js'; +import { + MicrosheetBlockComponent, + type MicrosheetBlockService, +} from './microsheet-block/index.js'; +import { MicrosheetDataViewBlockComponent } from './microsheet-data-view-block/data-view-block.js'; import { EdgelessNoteBlockComponent, EdgelessNoteMask, @@ -271,6 +281,7 @@ import { AFFINE_VIEWPORT_OVERLAY_WIDGET, AffineViewportOverlayWidget, } from './root-block/widgets/viewport-overlay/viewport-overlay.js'; +import { RowBlockComponent, type RowBlockService } from './row-block/index.js'; import { MindmapRootBlock, MindmapSurfaceBlock, @@ -300,6 +311,7 @@ export function effects() { blockDatabaseEffects(); blockSurfaceRefEffects(); blockLatexEffects(); + microsheetDataViewEffects(); componentCaptionEffects(); componentContextMenuEffects(); @@ -360,6 +372,10 @@ export function effects() { customElements.define('affine-frame', FrameBlockComponent); customElements.define('mini-mindmap-surface-block', MindmapSurfaceBlock); customElements.define('affine-data-view', DataViewBlockComponent); + customElements.define( + 'affine-microsheet-data-view', + MicrosheetDataViewBlockComponent + ); customElements.define('affine-edgeless-root', EdgelessRootBlockComponent); customElements.define('affine-divider', DividerBlockComponent); customElements.define('edgeless-copilot-panel', EdgelessCopilotPanel); @@ -383,6 +399,9 @@ export function effects() { ); customElements.define('affine-custom-modal', AffineCustomModal); customElements.define('affine-database', DatabaseBlockComponent); + customElements.define('affine-microsheet', MicrosheetBlockComponent); + customElements.define('affine-row', RowBlockComponent); + customElements.define('affine-cell', CellBlockComponent); customElements.define('affine-surface-ref', SurfaceRefBlockComponent); customElements.define('pie-node-child', PieNodeChild); customElements.define('pie-node-content', PieNodeContent); @@ -624,6 +643,9 @@ declare global { 'affine:attachment': AttachmentBlockService; 'affine:bookmark': BookmarkBlockService; 'affine:database': DatabaseBlockService; + 'affine:microsheet': MicrosheetBlockService; + 'affine:row': RowBlockService; + 'affine:cell': CellBlockService; 'affine:image': ImageBlockService; 'affine:surface-ref': SurfaceRefBlockService; } diff --git a/packages/blocks/src/index.ts b/packages/blocks/src/index.ts index 17f0f2ff4e32..7631b47d75d2 100644 --- a/packages/blocks/src/index.ts +++ b/packages/blocks/src/index.ts @@ -21,6 +21,7 @@ export { type AbstractEditor } from './_common/types.js'; export * from './_specs/index.js'; export * from './attachment-block/index.js'; export * from './bookmark-block/index.js'; +export * from './cell-block/index.js'; export * from './code-block/index.js'; export * from './data-view-block/index.js'; export * from './database-block/index.js'; @@ -29,6 +30,8 @@ export * from './edgeless-text-block/index.js'; export * from './frame-block/index.js'; export * from './image-block/index.js'; export * from './latex-block/index.js'; +export * from './microsheet-block/index.js'; +export * from './microsheet-data-view-block/index.js'; export * from './note-block/index.js'; export { EdgelessTemplatePanel } from './root-block/edgeless/components/toolbar/template/template-panel.js'; export type { @@ -47,6 +50,7 @@ export { EditPropsMiddlewareBuilder } from './root-block/edgeless/middlewares/ba export * from './root-block/edgeless/utils/common.js'; export { EdgelessSnapManager } from './root-block/edgeless/utils/snap-manager.js'; export * from './root-block/index.js'; +export * from './row-block/index.js'; export * from './schemas.js'; export { markdownToMindmap, diff --git a/packages/blocks/src/microsheet-block/block-icons.ts b/packages/blocks/src/microsheet-block/block-icons.ts new file mode 100644 index 000000000000..8a6868003c53 --- /dev/null +++ b/packages/blocks/src/microsheet-block/block-icons.ts @@ -0,0 +1,47 @@ +import type { ParagraphType } from '@blocksuite/affine-model'; +import type { BlockModel } from '@blocksuite/store'; +import type { TemplateResult } from 'lit'; + +import { + BulletedListIcon, + CheckBoxCheckLinearIcon, + Heading1Icon, + Heading2Icon, + Heading3Icon, + Heading4Icon, + Heading5Icon, + Heading6Icon, + NumberedListIcon, + QuoteIcon, + TextIcon, +} from '@blocksuite/icons/lit'; + +export const getIcon = ( + model: BlockModel & { type?: string } +): TemplateResult => { + if (model.flavour === 'affine:paragraph') { + const type = model.type as ParagraphType; + return ( + { + text: TextIcon(), + quote: QuoteIcon(), + h1: Heading1Icon(), + h2: Heading2Icon(), + h3: Heading3Icon(), + h4: Heading4Icon(), + h5: Heading5Icon(), + h6: Heading6Icon(), + } as Record + )[type]; + } + if (model.flavour === 'affine:list') { + return ( + { + bulleted: BulletedListIcon(), + numbered: NumberedListIcon(), + todo: CheckBoxCheckLinearIcon(), + }[model.type ?? 'bulleted'] ?? BulletedListIcon() + ); + } + return TextIcon(); +}; diff --git a/packages/blocks/src/microsheet-block/components/layout.ts b/packages/blocks/src/microsheet-block/components/layout.ts new file mode 100644 index 000000000000..8d9846ecf43f --- /dev/null +++ b/packages/blocks/src/microsheet-block/components/layout.ts @@ -0,0 +1,69 @@ +import { createModal } from '@blocksuite/affine-components/context-menu'; +import { ShadowlessElement } from '@blocksuite/block-std'; +import { CloseIcon } from '@blocksuite/icons/lit'; +import { css, html, type TemplateResult } from 'lit'; +import { property } from 'lit/decorators.js'; + +export class CenterPeek extends ShadowlessElement { + static override styles = css` + center-peek { + flex-direction: column; + position: absolute; + top: 5%; + left: 5%; + width: 90%; + height: 90%; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.05); + border-radius: 12px; + } + + .side-modal-content { + flex: 1; + overflow-y: auto; + } + + .close-modal:hover { + background-color: var(--affine-hover-color); + } + .close-modal { + position: absolute; + right: -32px; + top: 0; + width: 24px; + height: 24px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + } + `; + + override render() { + return html` +
${CloseIcon()}
+ ${this.content} + `; + } + + @property({ attribute: false }) + accessor close: (() => void) | undefined = undefined; + + @property({ attribute: false }) + accessor content: TemplateResult | undefined = undefined; +} + +export const popSideDetail = (template: TemplateResult) => { + return new Promise(res => { + const modal = createModal(document.body); + const close = () => { + modal.remove(); + res(); + }; + const sideContainer = new CenterPeek(); + sideContainer.content = template; + sideContainer.close = close; + modal.onclick = e => e.target === modal && close(); + modal.append(sideContainer); + }); +}; diff --git a/packages/blocks/src/microsheet-block/components/title/index.ts b/packages/blocks/src/microsheet-block/components/title/index.ts new file mode 100644 index 000000000000..b0ed66efc9c5 --- /dev/null +++ b/packages/blocks/src/microsheet-block/components/title/index.ts @@ -0,0 +1,176 @@ +import type { RichText } from '@blocksuite/affine-components/rich-text'; +import type { InlineRange } from '@blocksuite/inline'; +import type { Text } from '@blocksuite/store'; + +import { getViewportElement } from '@blocksuite/affine-shared/utils'; +import { ShadowlessElement } from '@blocksuite/block-std'; +import { assertExists, WithDisposable } from '@blocksuite/global/utils'; +import { effect } from '@preact/signals-core'; +import { css, html } from 'lit'; +import { property, query, state } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; + +import type { MicrosheetBlockComponent } from '../../microsheet-block.js'; + +export class MicrosheetTitle extends WithDisposable(ShadowlessElement) { + static override styles = css` + .affine-microsheet-title { + position: relative; + flex: 1; + } + + .microsheet-title { + font-size: 20px; + font-weight: 600; + line-height: 28px; + color: var(--affine-text-primary-color); + font-family: inherit; + /* overflow-x: scroll; */ + overflow: hidden; + cursor: text; + } + + .microsheet-title [data-v-text='true'] { + display: block; + word-break: break-all !important; + } + + .microsheet-title.ellipsis [data-v-text='true'] { + white-space: nowrap !important; + text-overflow: ellipsis; + overflow: hidden; + } + + .affine-microsheet-title [data-title-empty='true']::before { + content: 'Untitled'; + position: absolute; + pointer-events: none; + color: var(--affine-text-primary-color); + } + + .affine-microsheet-title [data-title-focus='true']::before { + color: var(--affine-placeholder-color); + } + `; + + private _onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Enter' && !event.isComposing) { + // prevent insert v-line + event.preventDefault(); + // insert new row + this.onPressEnterKey?.(); + return; + } + }; + + get inlineEditor() { + assertExists(this.richText.inlineEditor); + return this.richText.inlineEditor; + } + + get microsheet() { + return this.closest('affine-microsheet'); + } + + get topContenteditableElement() { + return this.microsheet?.topContenteditableElement; + } + + override firstUpdated() { + // for title placeholder + this.titleText.yText.observe(() => { + this.requestUpdate(); + }); + + this.updateComplete + .then(() => { + this.disposables.add( + this.inlineEditor.slots.keydown.on(this._onKeyDown) + ); + + this.disposables.add( + this.inlineEditor.slots.inputting.on(() => { + this.isComposing = this.inlineEditor.isComposing; + }) + ); + + let beforeInlineRange: InlineRange | null = null; + this.disposables.add( + effect(() => { + const inlineRange = this.inlineEditor.inlineRange$.value; + if (inlineRange) { + if (!beforeInlineRange) { + this.isActive = true; + } + } else { + if (beforeInlineRange) { + this.isActive = false; + } + } + beforeInlineRange = inlineRange; + }) + ); + }) + .catch(console.error); + } + + override async getUpdateComplete(): Promise { + const result = await super.getUpdateComplete(); + await this.richText?.updateComplete; + return result; + } + + override render() { + const isEmpty = + (!this.titleText || !this.titleText.length) && !this.isComposing; + + const classList = classMap({ + 'microsheet-title': true, + ellipsis: !this.isActive, + }); + + return html`
+ + this.topContenteditableElement?.host + ? getViewportElement(this.topContenteditableElement.host) + : null} + class="${classList}" + data-title-empty="${isEmpty}" + data-title-focus="${this.isActive}" + data-block-is-microsheet-title="true" + title="${this.titleText.toString()}" + > +
Untitled
+
`; + } + + @state() + private accessor isActive = false; + + @state() + accessor isComposing = false; + + @property({ attribute: false }) + accessor onPressEnterKey: (() => void) | undefined = undefined; + + @property({ attribute: false }) + accessor readonly!: boolean; + + @query('rich-text') + private accessor richText!: RichText; + + @property({ attribute: false }) + accessor titleText!: Text; +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-microsheet-title': MicrosheetTitle; + } +} diff --git a/packages/blocks/src/microsheet-block/config.ts b/packages/blocks/src/microsheet-block/config.ts new file mode 100644 index 000000000000..aff1e033c1ba --- /dev/null +++ b/packages/blocks/src/microsheet-block/config.ts @@ -0,0 +1,61 @@ +import type { MenuOptions } from '@blocksuite/affine-components/context-menu'; + +import { + type MicrosheetBlockModel, + MicrosheetBlockSchema, +} from '@blocksuite/affine-model'; +import { DragHandleConfigExtension } from '@blocksuite/affine-shared/services'; +import { captureEventTarget } from '@blocksuite/affine-shared/utils'; + +export interface MicrosheetOptionsConfig { + configure: (model: MicrosheetBlockModel, options: MenuOptions) => MenuOptions; +} + +let canDrop = false; +export const MicrosheetDragHandleOption = DragHandleConfigExtension({ + flavour: MicrosheetBlockSchema.model.flavour, + onDragMove: ({ state }) => { + const target = captureEventTarget(state.raw.target); + const microsheet = target?.closest('affine-microsheet'); + if (!microsheet) return false; + const view = microsheet.view; + if (view && target instanceof HTMLElement && microsheet.contains(target)) { + canDrop = view.showIndicator?.(state.raw) ?? false; + return false; + } + if (canDrop) { + view?.hideIndicator?.(); + canDrop = false; + } + return false; + }, + onDragEnd: ({ state, draggingElements, editorHost }) => { + const target = state.raw.target; + const targetEl = captureEventTarget(state.raw.target); + const microsheet = targetEl?.closest('affine-microsheet'); + if (!microsheet) { + return false; + } + const view = microsheet.view; + if ( + canDrop && + view && + view.moveTo && + target instanceof HTMLElement && + microsheet.parentElement?.contains(target) + ) { + const blocks = draggingElements.map(v => v.model); + editorHost.doc.moveBlocks(blocks, microsheet.model); + blocks.forEach(model => { + view.moveTo?.(model.id, state.raw); + }); + view.hideIndicator?.(); + return false; + } + if (canDrop) { + view?.hideIndicator?.(); + canDrop = false; + } + return false; + }, +}); diff --git a/packages/blocks/src/microsheet-block/context/host-context.ts b/packages/blocks/src/microsheet-block/context/host-context.ts new file mode 100644 index 000000000000..5b4a62dd7587 --- /dev/null +++ b/packages/blocks/src/microsheet-block/context/host-context.ts @@ -0,0 +1,8 @@ +import type { EditorHost } from '@blocksuite/block-std'; + +import { createContextKey } from '@blocksuite/microsheet-data-view'; + +export const HostContextKey = createContextKey( + 'editor-host', + undefined +); diff --git a/packages/blocks/src/microsheet-block/data-source.ts b/packages/blocks/src/microsheet-block/data-source.ts new file mode 100644 index 000000000000..7951236626a8 --- /dev/null +++ b/packages/blocks/src/microsheet-block/data-source.ts @@ -0,0 +1,477 @@ +import type { MicrosheetBlockModel } from '@blocksuite/affine-model'; +import type { EditorHost } from '@blocksuite/block-std'; + +import { + insertPositionToIndex, + type InsertToPosition, +} from '@blocksuite/affine-shared/utils'; +import { assertExists } from '@blocksuite/global/utils'; +import { + DataSourceBase, + type DataViewDataType, + type PropertyMetaConfig, + type TType, + type ViewManager, + ViewManagerBase, + type ViewMeta, +} from '@blocksuite/microsheet-data-view'; +import { propertyPresets } from '@blocksuite/microsheet-data-view/property-presets'; +import { type BlockModel, Text } from '@blocksuite/store'; +import { computed, type ReadonlySignal } from '@preact/signals-core'; + +import { getIcon } from './block-icons.js'; +import { + microsheetBlockAllPropertyMap, + microsheetBlockPropertyList, +} from './properties/index.js'; +import { titlePurePropertyConfig } from './properties/title/define.js'; +import { + addProperty, + applyCellsUpdate, + applyPropertyUpdate, + copyCellsByProperty, + deleteRows, + deleteView, + duplicateView, + findPropertyIndex, + getCell, + getProperty, + moveViewTo, + updateCell, + updateProperty, + updateView, +} from './utils.js'; +import { microsheetBlockViewMap, microsheetBlockViews } from './views/index.js'; + +export class MicrosheetBlockDataSource extends DataSourceBase { + private _batch = 0; + + private readonly _model: MicrosheetBlockModel; + + properties$: ReadonlySignal = computed(() => { + return this._model.columns$.value.map(column => column.id); + }); + + readonly$: ReadonlySignal = computed(() => { + return this._model.doc.awarenessStore.isReadonly( + this._model.doc.blockCollection + ); + }); + + rows$: ReadonlySignal = computed(() => { + return this._model.children.map(v => v.id); + }); + + viewDataList$: ReadonlySignal = computed(() => { + return this._model.views$.value as DataViewDataType[]; + }); + + override viewManager: ViewManager = new ViewManagerBase(this); + + viewMetas = microsheetBlockViews; + + get doc() { + return this._model.doc; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + get propertyMetas(): PropertyMetaConfig[] { + return microsheetBlockPropertyList; + } + + constructor(model: MicrosheetBlockModel) { + super(); + this._model = model; + } + + private _runCapture() { + if (this._batch) { + return; + } + + this._batch = requestAnimationFrame(() => { + this.doc.captureSync(); + this._batch = 0; + }); + } + + private getModelById(rowId: string): BlockModel | undefined { + return this._model.children[this._model.childMap.value.get(rowId) ?? -1]; + } + + private newPropertyName() { + let i = 1; + while ( + this._model.columns$.value.some(column => column.name === `Column ${i}`) + ) { + i++; + } + return `Column ${i}`; + } + + cellRefGet(rowId: string, propertyId: string): unknown { + return getCell(this._model, rowId, propertyId)?.ref; + } + + cellValueChange(rowId: string, propertyId: string, value: unknown): void { + this._runCapture(); + + const type = this.propertyTypeGet(propertyId); + const update = this.propertyMetaGet(type).config.valueUpdate; + let newValue = value; + if (update) { + const old = this.cellValueGet(rowId, propertyId); + newValue = update(old, this.propertyDataGet(propertyId), value); + } + if (type === 'title' && newValue instanceof Text) { + this._model.doc.transact(() => { + this._model.text?.clear(); + this._model.text?.join(newValue); + }); + return; + } + if (this._model.columns$.value.some(v => v.id === propertyId)) { + updateCell(this._model, rowId, { + columnId: propertyId, + value: newValue, + ref: '', + }); + applyCellsUpdate(this._model); + } + } + + cellValueGet(rowId: string, propertyId: string): unknown { + if (propertyId === 'type') { + const model = this.getModelById(rowId); + if (!model) { + return; + } + return getIcon(model); + } + const type = this.propertyTypeGet(propertyId); + if (type === 'title') { + const model = this.getModelById(rowId); + return model?.text; + } + return getCell(this._model, rowId, propertyId)?.value; + } + + propertyAdd(insertToPosition: InsertToPosition, type?: string): string { + this.doc.captureSync(); + const result = addProperty( + this._model, + insertToPosition, + microsheetBlockAllPropertyMap[ + type ?? propertyPresets.textPropertyConfig.type + ].create(this.newPropertyName()) + ); + applyPropertyUpdate(this._model); + return result; + } + + propertyDataGet(propertyId: string): Record { + return ( + this._model.columns$.value.find(v => v.id === propertyId)?.data ?? {} + ); + } + + propertyDataSet(propertyId: string, data: Record): void { + this._runCapture(); + + updateProperty(this._model, propertyId, () => ({ data })); + applyPropertyUpdate(this._model); + } + + propertyDataTypeGet(propertyId: string): TType | undefined { + const data = this._model.columns$.value.find(v => v.id === propertyId); + if (!data) { + return; + } + const meta = this.propertyMetaGet(data.type); + return meta.config.type(data); + } + + propertyDelete(id: string): void { + this.doc.captureSync(); + const index = findPropertyIndex(this._model, id); + if (index < 0) return; + + this.rows$.value.forEach(rowId => { + const cell = this._model.cells[rowId][id]; + this.doc.getBlock(cell.ref)?.model && + this.doc.deleteBlock(this.doc.getBlock(cell.ref)!.model); + }); + + this.doc.transact(() => { + this._model.columns = this._model.columns.filter((_, i) => i !== index); + }); + } + + propertyDuplicate(propertyId: string): string { + this.doc.captureSync(); + const currentSchema = getProperty(this._model, propertyId); + assertExists(currentSchema); + const { id: copyId, ...nonIdProps } = currentSchema; + const names = new Set(this._model.columns$.value.map(v => v.name)); + let index = 1; + while (names.has(`${nonIdProps.name}(${index})`)) { + index++; + } + const schema = { ...nonIdProps, name: `${nonIdProps.name}(${index})` }; + const id = addProperty( + this._model, + { + before: false, + id: propertyId, + }, + schema + ); + copyCellsByProperty(this._model, copyId, id); + applyPropertyUpdate(this._model); + return id; + } + + propertyMetaGet(type: string): PropertyMetaConfig { + return microsheetBlockAllPropertyMap[type]; + } + + propertyNameGet(propertyId: string): string { + if (propertyId === 'type') { + return 'Block Type'; + } + return ( + this._model.columns$.value.find(v => v.id === propertyId)?.name ?? '' + ); + } + + propertyNameSet(propertyId: string, name: string): void { + this.doc.captureSync(); + updateProperty(this._model, propertyId, () => ({ name })); + applyPropertyUpdate(this._model); + } + + override propertyReadonlyGet(propertyId: string): boolean { + if (propertyId === 'type') return true; + return false; + } + + propertyTypeGet(propertyId: string): string { + if (propertyId === 'type') { + return 'image'; + } + return ( + this._model.columns$.value.find(v => v.id === propertyId)?.type ?? '' + ); + } + + refContentDelete(rowId: string, columnId: string): void { + const cellId = this.cellRefGet(rowId, columnId); + const doc = this.doc; + if (typeof cellId === 'string') { + const cellBlock = doc.getBlock(cellId); + if (cellBlock) { + const children = cellBlock.model.children; + children.forEach(b => doc.deleteBlock(b)); + doc.addBlock('affine:paragraph', {}, cellId); + } + } + } + + rowAdd(insertPosition: InsertToPosition | number): string { + this.doc.captureSync(); + const index = + typeof insertPosition === 'number' + ? insertPosition + : insertPositionToIndex(insertPosition, this._model.children); + const rowId = this.doc.addBlock('affine:row', {}, this._model.id, index); + const columnIds = this._model.columns.map(column => column.id); + columnIds.forEach((id: string, index: number) => { + if (!index) return; + // 调用cellContainer的add + const cellContainerId = this.doc.addBlock('affine:cell', {}, rowId); + this.doc.addBlock( + 'affine:paragraph', + { + text: new this.doc.Text(``), + }, + cellContainerId + ); + + updateCell(this._model, rowId, { + columnId: id, + value: '', + ref: cellContainerId, + }); + }); + return rowId; + } + + rowDelete(ids: string[]): void { + this.doc.captureSync(); + for (const id of ids) { + const block = this.doc.getBlock(id); + if (block) { + this.doc.deleteBlock(block.model); + const cell = this._model.cells[id]; + Object.values(cell).forEach(v => { + this.doc.getBlock(v.ref)?.model && + this.doc.deleteBlock(this.doc.getBlock(v.ref)!.model); + }); + } + } + deleteRows(this._model, ids); + } + + rowMove(rowId: string, position: InsertToPosition): void { + const model = this.doc.getBlockById(rowId); + if (model) { + const index = insertPositionToIndex(position, this._model.children); + const target = this._model.children[index]; + if (target?.id === rowId) { + return; + } + this.doc.moveBlocks([model], this._model, target); + } + } + + viewDataAdd(viewData: DataViewDataType): string { + this._model.doc.captureSync(); + this._model.doc.transact(() => { + this._model.views = [...this._model.views, viewData]; + }); + return viewData.id; + } + + viewDataDelete(viewId: string): void { + this._model.doc.captureSync(); + deleteView(this._model, viewId); + } + + viewDataDuplicate(id: string): string { + return duplicateView(this._model, id); + } + + viewDataGet(viewId: string): DataViewDataType { + return this.viewDataList$.value.find(data => data.id === viewId)!; + } + + viewDataMoveTo(id: string, position: InsertToPosition): void { + moveViewTo(this._model, id, position); + } + + viewDataUpdate( + id: string, + updater: (data: ViewData) => Partial + ): void { + updateView(this._model, id, updater); + } + + viewMetaGet(type: string): ViewMeta { + return microsheetBlockViewMap[type]; + } + + viewMetaGetById(viewId: string): ViewMeta { + const view = this.viewDataGet(viewId); + return this.viewMetaGet(view.mode); + } +} + +export const microsheetViewAddView = ( + model: MicrosheetBlockModel, + viewType: string +) => { + const dataSource = new MicrosheetBlockDataSource(model); + dataSource.viewManager.viewAdd(viewType); +}; +export const microsheetViewInitEmpty = ( + model: MicrosheetBlockModel, + viewType: string +) => { + addProperty( + model, + 'start', + titlePurePropertyConfig.create(titlePurePropertyConfig.config.name) + ); + microsheetViewAddView(model, viewType); +}; +export const microsheetViewInitConvert = ( + model: MicrosheetBlockModel, + viewType: string +) => { + addProperty( + model, + 'end', + propertyPresets.textPropertyConfig.create('Tag', {}) + ); + microsheetViewInitEmpty(model, viewType); +}; +export const microsheetViewInitTemplate = ( + model: MicrosheetBlockModel, + viewType: string +) => { + const columnIds = []; + for (let u = 0; u < 3; u++) { + columnIds.push( + addProperty( + model, + 'end', + propertyPresets.textPropertyConfig.create('', {}) + ) + ); + } + for (let i = 0; i < 2; i++) { + const rowId = model.doc.addBlock('affine:row', {}, model.id); + for (let u = 0; u < 3; u++) { + const cellId = model.doc.addBlock('affine:cell', {}, rowId); + model.doc.addBlock( + 'affine:paragraph', + { + text: new model.doc.Text(`Cell...`), + }, + cellId + ); + updateCell(model, rowId, { + columnId: columnIds[u], + value: '', + ref: cellId, + }); + } + } + microsheetViewInitEmpty(model, viewType); +}; +export const convertToMicrosheet = (host: EditorHost, viewType: string) => { + const [_, ctx] = host.std.command + .chain() + .getSelectedModels({ + types: ['block', 'text'], + }) + .run(); + const { selectedModels } = ctx; + if (!selectedModels || selectedModels.length === 0) return; + + host.doc.captureSync(); + + const parentModel = host.doc.getParent(selectedModels[0]); + if (!parentModel) { + return; + } + + const id = host.doc.addBlock( + 'affine:microsheet', + {}, + parentModel, + parentModel.children.indexOf(selectedModels[0]) + ); + const microsheetModel = host.doc.getBlock(id)?.model as + | MicrosheetBlockModel + | undefined; + if (!microsheetModel) { + return; + } + microsheetViewInitConvert(microsheetModel, viewType); + applyPropertyUpdate(microsheetModel); + host.doc.moveBlocks(selectedModels, microsheetModel); + + const selectionManager = host.selection; + selectionManager.clear(); +}; diff --git a/packages/blocks/src/microsheet-block/index.ts b/packages/blocks/src/microsheet-block/index.ts new file mode 100644 index 000000000000..0954b9608651 --- /dev/null +++ b/packages/blocks/src/microsheet-block/index.ts @@ -0,0 +1,15 @@ +import type { MicrosheetBlockModel } from '@blocksuite/affine-model'; + +export type { MicrosheetOptionsConfig } from './config.js'; + +export * from './data-source.js'; +export * from './microsheet-block.js'; +export * from './microsheet-service.js'; +export { microsheetBlockColumns } from './properties/index.js'; +declare global { + namespace BlockSuite { + interface BlockModels { + 'affine:microsheet': MicrosheetBlockModel; + } + } +} diff --git a/packages/blocks/src/microsheet-block/microsheet-block.ts b/packages/blocks/src/microsheet-block/microsheet-block.ts new file mode 100644 index 000000000000..0c9aae60c070 --- /dev/null +++ b/packages/blocks/src/microsheet-block/microsheet-block.ts @@ -0,0 +1,509 @@ +import type { MicrosheetBlockModel } from '@blocksuite/affine-model'; +import type { DataViewTable } from '@blocksuite/microsheet-data-view/view-presets'; + +import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption'; +import { + menu, + popMenu, + popupTargetFromElement, +} from '@blocksuite/affine-components/context-menu'; +import { DragIndicator } from '@blocksuite/affine-components/drag-indicator'; +import { toast } from '@blocksuite/affine-components/toast'; +import { NOTE_SELECTOR } from '@blocksuite/affine-shared/consts'; +import { Rect, Slot } from '@blocksuite/global/utils'; +import { + CopyIcon, + DeleteIcon, + MoreHorizontalIcon, +} from '@blocksuite/icons/lit'; +import { + DataView, + dataViewCommonStyle, + type DataViewExpose, + type DataViewProps, + defineUniComponent, + type MicrosheetDataViewSelection, + type MicrosheetDataViewWidget, + type MicrosheetDataViewWidgetProps, + MicrosheetSelection, + renderUniLit, +} from '@blocksuite/microsheet-data-view'; +import { widgetPresets } from '@blocksuite/microsheet-data-view/widget-presets'; +import { Slice } from '@blocksuite/store'; +import { computed, type ReadonlySignal, signal } from '@preact/signals-core'; +import { css, html, nothing, unsafeCSS } from 'lit'; +import { query } from 'lit/decorators.js'; + +import type { NoteBlockComponent } from '../note-block/index.js'; +import type { MicrosheetOptionsConfig } from './config.js'; +import type { MicrosheetBlockService } from './microsheet-service.js'; + +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { TableRowSelection } from '../../../affine/data-view/src/view-presets/table/types.js'; +import { + EdgelessRootBlockComponent, + type RootService, +} from '../root-block/index.js'; +import { getDropResult } from '../root-block/widgets/drag-handle/utils.js'; +import { HostContextKey } from './context/host-context.js'; +import { MicrosheetBlockDataSource } from './data-source.js'; +import { calculateLineNum, isInCellEnd, isInCellStart } from './utils.js'; + +export class MicrosheetBlockComponent extends CaptionedBlockComponent< + MicrosheetBlockModel, + MicrosheetBlockService +> { + static override styles = css` + ${unsafeCSS(dataViewCommonStyle('affine-microsheet'))} + affine-microsheet { + display: block; + background-color: var(--affine-background-primary-color); + } + + affine-microsheet:hover .affine-microsheet-column-header { + visibility: visible; + } + + affine-microsheet:hover .microsheet-data-view-table-left-bar { + visibility: visible; + } + affine-microsheet:hover .data-view-table-left-bar { + visibility: visible; + } + + .microsheet-block-selected { + background-color: var(--affine-hover-color); + border-radius: 4px; + } + + .microsheet-ops { + margin-top: 4px; + padding: 2px; + border-radius: 4px; + display: flex; + cursor: pointer; + } + + .microsheet-ops svg { + width: 16px; + height: 16px; + color: var(--affine-icon-color); + } + + .microsheet-ops:hover { + background-color: var(--affine-hover-color); + } + + @media print { + .microsheet-ops { + display: none; + } + + .microsheet-header-bar { + display: none !important; + } + } + `; + + private _clickMicrosheetOps = (e: MouseEvent) => { + const options = this.optionsConfig.configure(this.model, { + items: [ + menu.input({ + initialValue: this.model.title.toString(), + placeholder: 'Untitled', + onComplete: text => { + this.model.title.replace(0, this.model.title.length, text); + }, + }), + menu.action({ + prefix: CopyIcon(), + name: 'Copy', + select: () => { + const slice = Slice.fromModels(this.doc, [this.model]); + this.std.clipboard + .copySlice(slice) + .then(() => { + toast(this.host, 'Copied to clipboard'); + }) + .catch(console.error); + }, + }), + menu.group({ + items: [ + menu.action({ + prefix: DeleteIcon(), + class: { 'delete-item': true }, + name: 'Delete Microsheet', + select: () => { + this.model.children.slice().forEach(block => { + this.doc.deleteBlock(block); + }); + this.doc.deleteBlock(this.model); + }, + }), + ], + }), + ], + }); + + popMenu(popupTargetFromElement(e.currentTarget as HTMLElement), { + options, + }); + }; + + private _dataSource?: MicrosheetBlockDataSource; + + private dataView = new DataView(); + + private renderTitle = (dataViewMethod: DataViewExpose) => { + const addRow = () => dataViewMethod.addRow?.('start'); + return html` `; + }; + + _bindHotkey: DataViewProps['bindHotkey'] = hotkeys => { + return { + dispose: this.host.event.bindHotkey(hotkeys, { + blockId: this.topContenteditableElement?.blockId ?? this.blockId, + }), + }; + }; + + _handleEvent: DataViewProps['handleEvent'] = (name, handler) => { + return { + dispose: this.host.event.add(name, handler, { + blockId: this.blockId, + }), + }; + }; + + getRootService = () => { + return this.std.getService('affine:page'); + }; + + headerWidget: MicrosheetDataViewWidget = defineUniComponent( + (props: MicrosheetDataViewWidgetProps) => { + return html` +
+
+ ${this.renderTitle(props.viewMethods)} ${this.renderMicrosheetOps()} +
+
+ ${renderUniLit(this.toolsWidget, props)} +
+
+ `; + } + ); + + indicator = new DragIndicator(); + + onDrag = (evt: MouseEvent, id: string): (() => void) => { + const result = getDropResult(evt); + if (result && result.rect) { + document.body.append(this.indicator); + this.indicator.rect = Rect.fromLWTH( + result.rect.left, + result.rect.width, + result.rect.top, + result.rect.height + ); + return () => { + this.indicator.remove(); + const model = this.doc.getBlock(id)?.model; + const target = this.doc.getBlock(result.dropBlockId)?.model ?? null; + let parent = this.doc.getParent(result.dropBlockId); + const shouldInsertIn = result.dropType === 'in'; + if (shouldInsertIn) { + parent = target; + } + if (model && target && parent) { + if (shouldInsertIn) { + this.doc.moveBlocks([model], parent); + } else { + this.doc.moveBlocks( + [model], + parent, + target, + result.dropType === 'before' + ); + } + } + }; + } + this.indicator.remove(); + return () => {}; + }; + + selectionUpdated: Slot = new Slot< + MicrosheetDataViewSelection | undefined + >(); + + setSelection = (selection: MicrosheetDataViewSelection | undefined) => { + this.selection.setGroup( + 'note', + selection + ? [ + new MicrosheetSelection({ + blockId: this.blockId, + viewSelection: selection, + }), + ] + : [] + ); + }; + + toolsWidget: MicrosheetDataViewWidget = widgetPresets.createTools({ + table: [], + }); + + viewSelection$: ReadonlySignal = + computed(() => { + const microsheetSelection = this.selection.value.find( + (selection): selection is MicrosheetSelection => { + if (selection.blockId !== this.blockId) { + return false; + } + return selection instanceof MicrosheetSelection; + } + ); + return microsheetSelection?.viewSelection; + }); + + virtualPadding$ = signal(0); + + get dataSource(): MicrosheetBlockDataSource { + if (!this._dataSource) { + this._dataSource = new MicrosheetBlockDataSource(this.model); + this._dataSource.contextSet(HostContextKey, this.host); + } + return this._dataSource; + } + + get dataViewTableElement() { + return this._DataViewTableElement; + } + + get optionsConfig(): MicrosheetOptionsConfig { + return { + configure: (_model, options) => options, + // @ts-expect-error + ...this.std.getConfig('affine:page')?.microsheetOptions, + }; + } + + override get topContenteditableElement() { + if (this.rootComponent instanceof EdgelessRootBlockComponent) { + const note = this.closest(NOTE_SELECTOR); + return note; + } + return this.rootComponent; + } + + get view() { + return this.dataView.expose; + } + + private renderMicrosheetOps() { + if (this.doc.readonly) { + return nothing; + } + return html`
+ ${MoreHorizontalIcon()} +
`; + } + + override connectedCallback() { + super.connectedCallback(); + this._disposables.add( + this.bindHotKey({ + Backspace: () => { + const selectionController = + this.dataViewTableElement?.selectionController; + const selection = selectionController?.selection; + if (!selectionController || !selection) return; + const data = this.dataSource; + if (TableRowSelection.is(selection)) { + const rows = TableRowSelection.rowsIds(selection); + selectionController.selection = undefined; + rows.forEach(rowId => { + this.model.columns.forEach(column => { + if (rowId && column.id) { + data.refContentDelete(rowId, column.id); + } + }); + }); + return; + } + const { + focus, + rowsSelection, + columnsSelection, + isEditing, + groupKey, + } = selection; + if (focus && !isEditing) { + if (rowsSelection && columnsSelection) { + // multi cell + for (let i = rowsSelection.start; i <= rowsSelection.end; i++) { + const { start, end } = columnsSelection; + for (let j = start; j <= end; j++) { + const container = selectionController.getCellContainer( + groupKey, + i, + j + ); + const rowId = container?.dataset.rowId; + const columnId = container?.dataset.columnId; + if (rowId && columnId) { + data.refContentDelete(rowId, columnId); + } + } + } + } else { + // single cell + const container = selectionController.getCellContainer( + groupKey, + focus.rowIndex, + focus.columnIndex + ); + const rowId = container?.dataset.rowId; + const columnId = container?.dataset.columnId; + if (rowId && columnId) { + data.refContentDelete(rowId, columnId); + } + } + } + }, + Tab: context => { + const selectionController = + this.dataViewTableElement?.selectionController; + if (!selectionController || !selectionController.focus) return; + context.get('keyboardState').raw.preventDefault(); + selectionController.focusToCell('right', 'end'); + return true; + }, + 'Shift-Tab': context => { + const selectionController = + this.dataViewTableElement?.selectionController; + if (!selectionController) return; + context.get('keyboardState').raw.preventDefault(); + selectionController.focusToCell('left', 'end'); + return true; + }, + ArrowLeft: context => { + const selectionController = + this.dataViewTableElement?.selectionController; + if (!selectionController) return; + if (isInCellStart(this.host.std, true)) { + const stop = selectionController.focusToCell('left'); + if (stop) { + context.get('keyboardState').raw.preventDefault(); + return true; + } + } + return; + }, + ArrowRight: context => { + const selectionController = + this.dataViewTableElement?.selectionController; + if (!selectionController || !selectionController.focus) return; + if (isInCellEnd(this.host.std, true)) { + const stop = selectionController.focusToCell('right'); + if (stop) { + context.get('keyboardState').raw.preventDefault(); + return true; + } + } + return; + }, + ArrowUp: context => { + const selectionController = + this.dataViewTableElement?.selectionController; + if (!selectionController || !selectionController.focus) return; + if (isInCellStart(this.host.std)) { + const { isFirst } = calculateLineNum(this.host.std); + if (!isFirst) return false; + + const stop = selectionController.focusToCell('up'); + if (stop) { + context.get('keyboardState').raw.preventDefault(); + return true; + } + } + return; + }, + ArrowDown: context => { + const selectionController = + this.dataViewTableElement?.selectionController; + if (!selectionController || !selectionController.focus) return; + if (isInCellEnd(this.host.std)) { + const { isLast } = calculateLineNum(this.host.std); + if (!isLast) return; + + const stop = selectionController.focusToCell('down'); + if (stop) { + context.get('keyboardState').raw.preventDefault(); + return true; + } + } + return; + }, + 'Mod-a': () => { + if ( + this.std.selection.filter('block').length === 1 && + this.std.selection.filter('block')[0].blockId === this.blockId + ) { + return; + } + this.std.selection.setGroup('note', [ + this.std.selection.create('block', { blockId: this.blockId }), + ]); + return true; + }, + }) + ); + } + + override renderBlock() { + return html` +
+ ${this.dataView.render({ + virtualPadding$: this.virtualPadding$, + bindHotkey: this._bindHotkey, + handleEvent: this._handleEvent, + selection$: this.viewSelection$, + setSelection: this.setSelection, + dataSource: this.dataSource, + headerWidget: this.headerWidget, + onDrag: this.onDrag, + std: this.std, + })} +
+ `; + } + + @query('affine-microsheet-table') + private accessor _DataViewTableElement: DataViewTable | null = null; + + override accessor useZeroWidth = true; +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-microsheet': MicrosheetBlockComponent; + } +} diff --git a/packages/blocks/src/microsheet-block/microsheet-service.ts b/packages/blocks/src/microsheet-block/microsheet-service.ts new file mode 100644 index 000000000000..5fe29910af58 --- /dev/null +++ b/packages/blocks/src/microsheet-block/microsheet-service.ts @@ -0,0 +1,49 @@ +import type { Doc } from '@blocksuite/store'; + +import { + type MicrosheetBlockModel, + MicrosheetBlockSchema, +} from '@blocksuite/affine-model'; +import { BlockService } from '@blocksuite/block-std'; +import { viewPresets } from '@blocksuite/microsheet-data-view/view-presets'; + +import { + microsheetViewAddView, + microsheetViewInitEmpty, + microsheetViewInitTemplate, +} from './data-source.js'; +import { + addProperty, + applyPropertyUpdate, + updateCell, + updateView, +} from './utils.js'; + +export class MicrosheetBlockService extends BlockService { + static override readonly flavour = MicrosheetBlockSchema.model.flavour; + + addColumn = addProperty; + + applyColumnUpdate = applyPropertyUpdate; + + microsheetViewAddView = microsheetViewAddView; + + microsheetViewInitEmpty = microsheetViewInitEmpty; + + updateCell = updateCell; + + updateView = updateView; + + viewPresets = viewPresets; + + initMicrosheetBlock(doc: Doc, microsheetId: string, viewType: string) { + const blockModel = doc.getBlock(microsheetId)?.model as + | MicrosheetBlockModel + | undefined; + if (!blockModel) { + return; + } + microsheetViewInitTemplate(blockModel, viewType); + applyPropertyUpdate(blockModel); + } +} diff --git a/packages/blocks/src/microsheet-block/microsheet-spec.ts b/packages/blocks/src/microsheet-block/microsheet-spec.ts new file mode 100644 index 000000000000..cd1c83cdb58c --- /dev/null +++ b/packages/blocks/src/microsheet-block/microsheet-spec.ts @@ -0,0 +1,18 @@ +import { + BlockViewExtension, + type ExtensionType, + FlavourExtension, +} from '@blocksuite/block-std'; +import { MicrosheetSelectionExtension } from '@blocksuite/microsheet-data-view'; +import { literal } from 'lit/static-html.js'; + +import { MicrosheetDragHandleOption } from './config.js'; +import { MicrosheetBlockService } from './microsheet-service.js'; + +export const MicrosheetBlockSpec: ExtensionType[] = [ + FlavourExtension('affine:microsheet'), + MicrosheetBlockService, + BlockViewExtension('affine:microsheet', literal`affine-microsheet`), + MicrosheetDragHandleOption, + MicrosheetSelectionExtension, +]; diff --git a/packages/blocks/src/microsheet-block/properties/index.ts b/packages/blocks/src/microsheet-block/properties/index.ts new file mode 100644 index 000000000000..e67b4cad66b9 --- /dev/null +++ b/packages/blocks/src/microsheet-block/properties/index.ts @@ -0,0 +1,19 @@ +import type { PropertyMetaConfig } from '@blocksuite/microsheet-data-view'; + +import { richTextColumnConfig } from './rich-text/cell-renderer.js'; +import { titleColumnConfig } from './title/cell-renderer.js'; + +export const microsheetBlockColumns = { + richTextColumnConfig, +}; +export const microsheetBlockPropertyList = Object.values( + microsheetBlockColumns +); +export const microsheetBlockHiddenColumns = [titleColumnConfig]; +const microsheetBlockAllColumns = [ + ...microsheetBlockPropertyList, + ...microsheetBlockHiddenColumns, +]; +export const microsheetBlockAllPropertyMap = Object.fromEntries( + microsheetBlockAllColumns.map(v => [v.type, v as PropertyMetaConfig]) +); diff --git a/packages/blocks/src/microsheet-block/properties/link/cell-renderer.ts b/packages/blocks/src/microsheet-block/properties/link/cell-renderer.ts new file mode 100644 index 000000000000..69b209cc9f19 --- /dev/null +++ b/packages/blocks/src/microsheet-block/properties/link/cell-renderer.ts @@ -0,0 +1,254 @@ +import { RefNodeSlotsProvider } from '@blocksuite/affine-components/rich-text'; +import { ParseDocUrlProvider } from '@blocksuite/affine-shared/services'; +import { + isValidUrl, + normalizeUrl, + stopPropagation, +} from '@blocksuite/affine-shared/utils'; +import { PenIcon } from '@blocksuite/icons/lit'; +import { + BaseCellRenderer, + createFromBaseCellRenderer, + createIcon, +} from '@blocksuite/microsheet-data-view'; +import { baseTheme } from '@toeverything/theme'; +import { css, unsafeCSS } from 'lit'; +import { query, state } from 'lit/decorators.js'; +import { html } from 'lit/static-html.js'; + +import { HostContextKey } from '../../context/host-context.js'; +import { linkColumnModelConfig } from './define.js'; + +export class LinkCell extends BaseCellRenderer { + static override styles = css` + affine-microsheet-link-cell { + width: 100%; + user-select: none; + } + + affine-microsheet-link-cell:hover .affine-microsheet-link-icon { + visibility: visible; + } + + .affine-microsheet-link { + display: flex; + position: relative; + align-items: center; + width: 100%; + height: 100%; + outline: none; + overflow: hidden; + font-size: var(--microsheet-data-view-cell-text-size); + line-height: var(--microsheet-data-view-cell-text-line-height); + word-break: break-all; + } + + affine-microsheet-link-node { + flex: 1; + word-break: break-all; + } + + .affine-microsheet-link-icon { + position: absolute; + right: 0; + display: flex; + align-items: center; + visibility: hidden; + cursor: pointer; + background: var(--affine-background-primary-color); + border-radius: 4px; + } + .affine-microsheet-link-icon:hover { + background: var(--affine-hover-color); + } + + .affine-microsheet-link-icon svg { + width: 16px; + height: 16px; + fill: var(--affine-icon-color); + } + .microsheet-data-view-link-column-linked-doc { + text-decoration: underline; + text-decoration-color: var(--affine-divider-color); + transition: text-decoration-color 0.2s ease-out; + cursor: pointer; + } + .microsheet-data-view-link-column-linked-doc:hover { + text-decoration-color: var(--affine-icon-color); + } + `; + + private _onClick = (event: Event) => { + event.stopPropagation(); + const value = this.value ?? ''; + + if (!value || !isValidUrl(value)) { + this.selectCurrentCell(true); + return; + } + + if (isValidUrl(value)) { + const target = event.target as HTMLElement; + const link = target.querySelector('.link-node'); + if (link) { + event.preventDefault(); + link.click(); + } + return; + } + }; + + private _onEdit = (e: Event) => { + e.stopPropagation(); + this.selectCurrentCell(true); + }; + + private preValue?: string; + + openDoc = (e: MouseEvent) => { + e.stopPropagation(); + if (!this.docId) { + return; + } + const std = this.std; + if (!std) { + return; + } + + std + .getOptional(RefNodeSlotsProvider) + ?.docLinkClicked.emit({ pageId: this.docId }); + }; + + get std() { + const host = this.view.contextGet(HostContextKey); + return host?.std; + } + + override render() { + const linkText = this.value ?? ''; + const docName = + this.docId && this.std?.collection.getDoc(this.docId)?.meta?.title; + return html` + + `; + } + + override updated() { + if (this.value !== this.preValue) { + const std = this.std; + this.preValue = this.value; + if (!this.value || !isValidUrl(this.value)) { + this.docId = undefined; + return; + } + const result = std + ?.getOptional(ParseDocUrlProvider) + ?.parseDocUrl(this.value); + if (result) { + this.docId = result.docId; + } else { + this.docId = undefined; + } + } + } + + @state() + accessor docId: string | undefined = undefined; +} + +export class LinkCellEditing extends BaseCellRenderer { + static override styles = css` + affine-microsheet-link-cell-editing { + width: 100%; + cursor: text; + } + + .affine-microsheet-link-editing { + display: flex; + align-items: center; + width: 100%; + padding: 0; + border: none; + font-family: ${unsafeCSS(baseTheme.fontSansFamily)}; + color: var(--affine-text-primary-color); + font-weight: 400; + background-color: transparent; + font-size: var(--microsheet-data-view-cell-text-size); + line-height: var(--microsheet-data-view-cell-text-line-height); + word-break: break-all; + } + + .affine-microsheet-link-editing:focus { + outline: none; + } + `; + + private _focusEnd = () => { + const end = this._container.value.length; + this._container.focus(); + this._container.setSelectionRange(end, end); + }; + + private _onKeydown = (e: KeyboardEvent) => { + if (e.key === 'Enter' && !e.isComposing) { + this._setValue(); + setTimeout(() => { + this.selectCurrentCell(false); + }); + } + }; + + private _setValue = (value: string = this._container.value) => { + let url = value; + if (isValidUrl(value)) { + url = normalizeUrl(value); + } + + this.onChange(url); + this._container.value = url; + }; + + override firstUpdated() { + this._focusEnd(); + } + + override onExitEditMode() { + this._setValue(); + } + + override render() { + const linkText = this.value ?? ''; + + return html``; + } + + @query('.affine-microsheet-link-editing') + private accessor _container!: HTMLInputElement; +} + +export const linkColumnConfig = linkColumnModelConfig.createPropertyMeta({ + icon: createIcon('LinkIcon'), + cellRenderer: { + view: createFromBaseCellRenderer(LinkCell), + edit: createFromBaseCellRenderer(LinkCellEditing), + }, +}); diff --git a/packages/blocks/src/microsheet-block/properties/link/components/link-node.ts b/packages/blocks/src/microsheet-block/properties/link/components/link-node.ts new file mode 100644 index 000000000000..9566173585ee --- /dev/null +++ b/packages/blocks/src/microsheet-block/properties/link/components/link-node.ts @@ -0,0 +1,41 @@ +import { isValidUrl } from '@blocksuite/affine-shared/utils'; +import { ShadowlessElement } from '@blocksuite/block-std'; +import { css, html } from 'lit'; +import { property } from 'lit/decorators.js'; + +export class LinkNode extends ShadowlessElement { + static override styles = css` + .link-node { + word-break: break-all; + color: var(--affine-link-color); + fill: var(--affine-link-color); + cursor: pointer; + font-weight: normal; + font-style: normal; + text-decoration: none; + } + `; + + protected override render() { + if (!isValidUrl(this.link)) { + return html`${this.link}`; + } + + return html`${this.link}`; + } + + @property({ attribute: false }) + accessor link!: string; +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-microsheet-link-node': LinkNode; + } +} diff --git a/packages/blocks/src/microsheet-block/properties/link/define.ts b/packages/blocks/src/microsheet-block/properties/link/define.ts new file mode 100644 index 000000000000..48b77703214e --- /dev/null +++ b/packages/blocks/src/microsheet-block/properties/link/define.ts @@ -0,0 +1,16 @@ +import { propertyType, tString } from '@blocksuite/microsheet-data-view'; + +export const linkColumnType = propertyType('link'); +export const linkColumnModelConfig = linkColumnType.modelConfig({ + name: 'Link', + type: () => tString.create(), + defaultData: () => ({}), + cellToString: data => data?.toString() ?? '', + cellFromString: data => { + return { + value: data, + }; + }, + cellToJson: data => data ?? null, + isEmpty: data => data == null || data.length == 0, +}); diff --git a/packages/blocks/src/microsheet-block/properties/rich-text/cell-renderer.ts b/packages/blocks/src/microsheet-block/properties/rich-text/cell-renderer.ts new file mode 100644 index 000000000000..39de84571239 --- /dev/null +++ b/packages/blocks/src/microsheet-block/properties/rich-text/cell-renderer.ts @@ -0,0 +1,396 @@ +import type { AffineTextAttributes } from '@blocksuite/affine-shared/types'; + +import { + type AffineInlineEditor, + DefaultInlineManagerExtension, + type RichText, +} from '@blocksuite/affine-components/rich-text'; +import { getViewportElement } from '@blocksuite/affine-shared/utils'; +import { IS_MAC } from '@blocksuite/global/env'; +import { assertExists } from '@blocksuite/global/utils'; +import { + BaseCellRenderer, + createFromBaseCellRenderer, + createIcon, +} from '@blocksuite/microsheet-data-view'; +import { Text } from '@blocksuite/store'; +import { css, nothing, type PropertyValues } from 'lit'; +import { query } from 'lit/decorators.js'; +import { keyed } from 'lit/directives/keyed.js'; +import { html } from 'lit/static-html.js'; + +import type { MicrosheetBlockComponent } from '../../microsheet-block.js'; + +import { HostContextKey } from '../../context/host-context.js'; +import { richTextColumnModelConfig } from './define.js'; + +function toggleStyle( + inlineEditor: AffineInlineEditor, + attrs: AffineTextAttributes +): void { + const inlineRange = inlineEditor.getInlineRange(); + if (!inlineRange) return; + + const root = inlineEditor.rootElement; + if (!root) { + return; + } + + const deltas = inlineEditor.getDeltasByInlineRange(inlineRange); + let oldAttributes: AffineTextAttributes = {}; + + for (const [delta] of deltas) { + const attributes = delta.attributes; + + if (!attributes) { + continue; + } + + oldAttributes = { ...attributes }; + } + + const newAttributes = Object.fromEntries( + Object.entries(attrs).map(([k, v]) => { + if ( + typeof v === 'boolean' && + v === (oldAttributes as Record)[k] + ) { + return [k, !v]; + } else { + return [k, v]; + } + }) + ); + + inlineEditor.formatText(inlineRange, newAttributes, { + mode: 'merge', + }); + root.blur(); + + inlineEditor.syncInlineRange(); +} + +export class RichTextCell extends BaseCellRenderer { + static override styles = css` + affine-microsheet-rich-text-cell { + display: flex; + align-items: center; + width: 100%; + user-select: none; + } + + .affine-microsheet-rich-text { + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + height: 100%; + outline: none; + font-size: var(--microsheet-data-view-cell-text-size); + line-height: var(--microsheet-data-view-cell-text-line-height); + word-break: break-all; + } + + .affine-microsheet-rich-text v-line { + display: flex !important; + align-items: center; + height: 100%; + width: 100%; + } + + .affine-microsheet-rich-text v-line > div { + flex-grow: 1; + } + `; + + get attributeRenderer() { + return this.inlineManager?.getRenderer(); + } + + get attributesSchema() { + return this.inlineManager?.getSchema(); + } + + get inlineEditor() { + assertExists(this._richTextElement); + const inlineEditor = this._richTextElement.inlineEditor; + assertExists(inlineEditor); + return inlineEditor; + } + + get inlineManager() { + return this.view + .contextGet(HostContextKey) + ?.std.get(DefaultInlineManagerExtension.identifier); + } + + get service() { + return this.view + .contextGet(HostContextKey) + ?.std.getService('affine:microsheet'); + } + + get topContenteditableElement() { + const microsheetBlock = + this.closest('affine-microsheet'); + return microsheetBlock?.topContenteditableElement; + } + + private changeUserSelectAccordToReadOnly() { + if (this && this instanceof HTMLElement) { + this.style.userSelect = this.readonly ? 'text' : 'none'; + } + } + + override connectedCallback() { + super.connectedCallback(); + this.changeUserSelectAccordToReadOnly(); + } + + override render() { + if (!this.service) return nothing; + if (!this.value || !(this.value instanceof Text)) { + return html`
`; + } + return keyed( + this.value, + html`` + ); + } + + override updated(changedProperties: PropertyValues) { + if (changedProperties.has('readonly')) { + this.changeUserSelectAccordToReadOnly(); + } + } + + @query('rich-text') + private accessor _richTextElement: RichText | null = null; +} + +export class RichTextCellEditing extends BaseCellRenderer { + static override styles = css` + affine-microsheet-rich-text-cell-editing { + display: flex; + align-items: center; + width: 100%; + min-width: 1px; + cursor: text; + } + + .affine-microsheet-rich-text { + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + height: 100%; + outline: none; + } + + .affine-microsheet-rich-text v-line { + display: flex !important; + align-items: center; + height: 100%; + width: 100%; + } + + .affine-microsheet-rich-text v-line > div { + flex-grow: 1; + } + `; + + private _handleKeyDown = (event: KeyboardEvent) => { + if (event.key !== 'Escape') { + if (event.key === 'Tab') { + event.preventDefault(); + return; + } + event.stopPropagation(); + } + + if (event.key === 'Enter' && !event.isComposing) { + if (event.shiftKey) { + // soft enter + this._onSoftEnter(); + } else { + // exit editing + this.selectCurrentCell(false); + } + event.preventDefault(); + return; + } + + const inlineEditor = this.inlineEditor; + + switch (event.key) { + // bold ctrl+b + case 'B': + case 'b': + if (event.metaKey || event.ctrlKey) { + event.preventDefault(); + toggleStyle(this.inlineEditor, { bold: true }); + } + break; + // italic ctrl+i + case 'I': + case 'i': + if (event.metaKey || event.ctrlKey) { + event.preventDefault(); + toggleStyle(this.inlineEditor, { italic: true }); + } + break; + // underline ctrl+u + case 'U': + case 'u': + if (event.metaKey || event.ctrlKey) { + event.preventDefault(); + toggleStyle(this.inlineEditor, { underline: true }); + } + break; + // strikethrough ctrl+shift+s + case 'S': + case 's': + if ((event.metaKey || event.ctrlKey) && event.shiftKey) { + event.preventDefault(); + toggleStyle(inlineEditor, { strike: true }); + } + break; + // inline code ctrl+shift+e + case 'E': + case 'e': + if ((event.metaKey || event.ctrlKey) && event.shiftKey) { + event.preventDefault(); + toggleStyle(inlineEditor, { code: true }); + } + break; + default: + break; + } + }; + + private _initYText = (text?: string) => { + const yText = new Text(text); + this.onChange(yText); + }; + + private _onSoftEnter = () => { + if (this.value && this.inlineEditor) { + const inlineRange = this.inlineEditor.getInlineRange(); + assertExists(inlineRange); + + const text = new Text(this.inlineEditor.yText); + text.replace(inlineRange.index, inlineRange.length, '\n'); + this.inlineEditor.setInlineRange({ + index: inlineRange.index + 1, + length: 0, + }); + } + }; + + get attributeRenderer() { + return this.inlineManager?.getRenderer(); + } + + get attributesSchema() { + return this.inlineManager?.getSchema(); + } + + get inlineEditor() { + assertExists(this._richTextElement); + const inlineEditor = this._richTextElement.inlineEditor; + assertExists(inlineEditor); + return inlineEditor; + } + + get inlineManager() { + return this.view + .contextGet(HostContextKey) + ?.std.get(DefaultInlineManagerExtension.identifier); + } + + get service() { + return this.view + .contextGet(HostContextKey) + ?.std.getService('affine:microsheet'); + } + + get topContenteditableElement() { + const microsheetBlock = + this.closest('affine-microsheet'); + return microsheetBlock?.topContenteditableElement; + } + + override connectedCallback() { + super.connectedCallback(); + + if (!this.value || typeof this.value === 'string') { + this._initYText(this.value); + } + + const selectAll = (e: KeyboardEvent) => { + if (e.key === 'a' && (IS_MAC ? e.metaKey : e.ctrlKey)) { + e.stopPropagation(); + e.preventDefault(); + this.inlineEditor.selectAll(); + } + }; + this.addEventListener('keydown', selectAll); + this.disposables.addFromEvent(this, 'keydown', selectAll); + } + + override firstUpdated() { + this._richTextElement?.updateComplete + .then(() => { + this.disposables.add( + this.inlineEditor.slots.keydown.on(this._handleKeyDown) + ); + + this.inlineEditor.focusEnd(); + }) + .catch(console.error); + } + + override render() { + if (!this.service) return nothing; + return html` + this.topContenteditableElement?.host + ? getViewportElement(this.topContenteditableElement.host) + : null} + class="affine-microsheet-rich-text inline-editor" + >`; + } + + @query('rich-text') + private accessor _richTextElement: RichText | null = null; +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-microsheet-rich-text-cell-editing': RichTextCellEditing; + } +} + +export const richTextColumnConfig = + richTextColumnModelConfig.createPropertyMeta({ + icon: createIcon('TextIcon'), + + cellRenderer: { + view: createFromBaseCellRenderer(RichTextCell), + edit: createFromBaseCellRenderer(RichTextCellEditing), + }, + }); diff --git a/packages/blocks/src/microsheet-block/properties/rich-text/define.ts b/packages/blocks/src/microsheet-block/properties/rich-text/define.ts new file mode 100644 index 000000000000..49d89aebb66b --- /dev/null +++ b/packages/blocks/src/microsheet-block/properties/rich-text/define.ts @@ -0,0 +1,32 @@ +import { propertyType, tRichText } from '@blocksuite/microsheet-data-view'; +import { Text } from '@blocksuite/store'; + +import { type RichTextCellType, toYText } from '../utils.js'; + +export const richTextColumnType = propertyType('rich-text'); + +export const richTextColumnModelConfig = + richTextColumnType.modelConfig({ + name: 'Text', + type: () => tRichText.create(), + defaultData: () => ({}), + cellToString: data => data?.toString() ?? '', + cellFromString: data => { + return { + value: new Text(data), + }; + }, + cellToJson: data => data?.toString() ?? null, + onUpdate: (value, _data, callback) => { + const yText = toYText(value); + yText.observe(callback); + callback(); + return { + dispose: () => { + yText.unobserve(callback); + }, + }; + }, + isEmpty: data => data == null || data.length === 0, + values: data => (data?.toString() ? [data.toString()] : []), + }); diff --git a/packages/blocks/src/microsheet-block/properties/title/cell-renderer.ts b/packages/blocks/src/microsheet-block/properties/title/cell-renderer.ts new file mode 100644 index 000000000000..6da9faa091f4 --- /dev/null +++ b/packages/blocks/src/microsheet-block/properties/title/cell-renderer.ts @@ -0,0 +1,30 @@ +import { + type CellRenderProps, + createFromBaseCellRenderer, + createIcon, + uniMap, +} from '@blocksuite/microsheet-data-view'; +import { TableSingleView } from '@blocksuite/microsheet-data-view/view-presets'; + +import { titlePurePropertyConfig } from './define.js'; +import { HeaderAreaTextCell, HeaderAreaTextCellEditing } from './text.js'; + +export const titleColumnConfig = titlePurePropertyConfig.createPropertyMeta({ + icon: createIcon('TitleIcon'), + cellRenderer: { + view: uniMap( + createFromBaseCellRenderer(HeaderAreaTextCell), + (props: CellRenderProps) => ({ + ...props, + showIcon: props.cell.view instanceof TableSingleView, + }) + ), + edit: uniMap( + createFromBaseCellRenderer(HeaderAreaTextCellEditing), + (props: CellRenderProps) => ({ + ...props, + showIcon: props.cell.view instanceof TableSingleView, + }) + ), + }, +}); diff --git a/packages/blocks/src/microsheet-block/properties/title/define.ts b/packages/blocks/src/microsheet-block/properties/title/define.ts new file mode 100644 index 000000000000..69b9f06470e9 --- /dev/null +++ b/packages/blocks/src/microsheet-block/properties/title/define.ts @@ -0,0 +1,41 @@ +import type { Text } from '@blocksuite/store'; + +import { propertyType, tRichText } from '@blocksuite/microsheet-data-view'; + +export const titleColumnType = propertyType('title'); + +export const titlePurePropertyConfig = titleColumnType.modelConfig({ + name: 'Title', + type: () => tRichText.create(), + defaultData: () => ({}), + cellToString: data => data?.toString() ?? '', + cellFromString: data => { + return { + value: data, + }; + }, + cellToJson: data => data?.toString() ?? null, + onUpdate: (value, _data, callback) => { + value.yText.observe(callback); + callback(); + return { + dispose: () => { + value.yText.unobserve(callback); + }, + }; + }, + valueUpdate: (value, _data, newValue) => { + const v = newValue as unknown; + if (typeof v === 'string') { + value.replace(0, value.length, v); + return value; + } + if (v == null) { + value.replace(0, value.length, ''); + return value; + } + return newValue; + }, + isEmpty: data => data == null || data.length === 0, + values: data => (data?.toString() ? [data.toString()] : []), +}); diff --git a/packages/blocks/src/microsheet-block/properties/title/icon.ts b/packages/blocks/src/microsheet-block/properties/title/icon.ts new file mode 100644 index 000000000000..3484d32ed7ed --- /dev/null +++ b/packages/blocks/src/microsheet-block/properties/title/icon.ts @@ -0,0 +1,21 @@ +import { BaseCellRenderer } from '@blocksuite/microsheet-data-view'; +import { css, html } from 'lit'; + +export class IconCell extends BaseCellRenderer { + static override styles = css` + affine-microsheet-image-cell { + width: 100%; + height: 100%; + display: flex; + align-items: center; + } + affine-microsheet-image-cell img { + width: 20px; + height: 20px; + } + `; + + override render() { + return html``; + } +} diff --git a/packages/blocks/src/microsheet-block/properties/title/text.ts b/packages/blocks/src/microsheet-block/properties/title/text.ts new file mode 100644 index 000000000000..725d7d89194f --- /dev/null +++ b/packages/blocks/src/microsheet-block/properties/title/text.ts @@ -0,0 +1,321 @@ +import type { Text } from '@blocksuite/store'; + +import { + DefaultInlineManagerExtension, + type RichText, +} from '@blocksuite/affine-components/rich-text'; +import { ParseDocUrlProvider } from '@blocksuite/affine-shared/services'; +import { + getViewportElement, + isValidUrl, +} from '@blocksuite/affine-shared/utils'; +import { IS_MAC } from '@blocksuite/global/env'; +import { assertExists } from '@blocksuite/global/utils'; +import { BaseCellRenderer } from '@blocksuite/microsheet-data-view'; +import { effect } from '@preact/signals-core'; +import { css } from 'lit'; +import { property, query } from 'lit/decorators.js'; +import { html } from 'lit/static-html.js'; + +import type { MicrosheetBlockComponent } from '../../microsheet-block.js'; + +import { HostContextKey } from '../../context/host-context.js'; + +const styles = css` + microsheet-data-view-header-area-text { + width: 100%; + display: flex; + } + + microsheet-data-view-header-area-text rich-text { + pointer-events: none; + user-select: none; + } + + microsheet-data-view-header-area-text-editing { + width: 100%; + display: flex; + cursor: text; + } + + .microsheet-data-view-header-area-rich-text { + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + height: 100%; + outline: none; + word-break: break-all; + font-size: var(--microsheet-data-view-cell-text-size); + line-height: var(--microsheet-data-view-cell-text-line-height); + } + + .microsheet-data-view-header-area-rich-text v-line { + display: flex !important; + align-items: center; + height: 100%; + width: 100%; + } + + .microsheet-data-view-header-area-rich-text v-line > div { + flex-grow: 1; + } + + .microsheet-data-view-header-area-icon { + height: max-content; + display: flex; + align-items: center; + margin-right: 8px; + padding: 2px; + border-radius: 4px; + margin-top: 2px; + background-color: var(--affine-background-secondary-color); + } + + .microsheet-data-view-header-area-icon svg { + width: 14px; + height: 14px; + fill: var(--affine-icon-color); + color: var(--affine-icon-color); + } +`; + +abstract class BaseTextCell extends BaseCellRenderer { + static override styles = styles; + + get attributeRenderer() { + return this.inlineManager?.getRenderer(); + } + + get attributesSchema() { + return this.inlineManager?.getSchema(); + } + + get inlineEditor() { + assertExists(this.richText); + const inlineEditor = this.richText.inlineEditor; + assertExists(inlineEditor); + return inlineEditor; + } + + get inlineManager() { + return this.view + .contextGet(HostContextKey) + ?.std.get(DefaultInlineManagerExtension.identifier); + } + + get service() { + return this.view + .contextGet(HostContextKey) + ?.std.getService('affine:microsheet'); + } + + get topContenteditableElement() { + const microsheetBlock = + this.closest('affine-microsheet'); + return microsheetBlock?.topContenteditableElement; + } + + renderIcon() { + if (!this.showIcon) { + return; + } + const iconColumn = this.view.mainProperties$.value.iconColumn; + if (!iconColumn) return; + + const icon = this.view.cellValueGet(this.cell.rowId, iconColumn) as string; + if (!icon) return; + + return html`
+ ${icon} +
`; + } + + @query('rich-text') + accessor richText!: RichText; + + @property({ attribute: false }) + accessor showIcon = false; +} + +export class HeaderAreaTextCell extends BaseTextCell { + override render() { + return html`${this.renderIcon()} + `; + } +} + +export class HeaderAreaTextCellEditing extends BaseTextCell { + private _onCopy = (e: ClipboardEvent) => { + const inlineEditor = this.inlineEditor; + assertExists(inlineEditor); + + const inlineRange = inlineEditor.getInlineRange(); + if (!inlineRange) return; + + const text = inlineEditor.yTextString.slice( + inlineRange.index, + inlineRange.index + inlineRange.length + ); + + e.clipboardData?.setData('text/plain', text); + e.preventDefault(); + e.stopPropagation(); + }; + + private _onCut = (e: ClipboardEvent) => { + const inlineEditor = this.inlineEditor; + assertExists(inlineEditor); + + const inlineRange = inlineEditor.getInlineRange(); + if (!inlineRange) return; + + const text = inlineEditor.yTextString.slice( + inlineRange.index, + inlineRange.index + inlineRange.length + ); + inlineEditor.deleteText(inlineRange); + inlineEditor.setInlineRange({ + index: inlineRange.index, + length: 0, + }); + + e.clipboardData?.setData('text/plain', text); + e.preventDefault(); + e.stopPropagation(); + }; + + private _onPaste = (e: ClipboardEvent) => { + const inlineEditor = this.inlineEditor; + assertExists(inlineEditor); + + const inlineRange = inlineEditor.getInlineRange(); + if (!inlineRange) return; + + const text = e.clipboardData + ?.getData('text/plain') + ?.replace(/\r?\n|\r/g, '\n'); + if (!text) return; + e.preventDefault(); + e.stopPropagation(); + if (isValidUrl(text)) { + const std = this.std; + const result = std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(text); + if (result) { + const text = ' '; + inlineEditor.insertText(inlineRange, text, { + reference: { + type: 'LinkedPage', + pageId: result.docId, + params: { + blockIds: result.blockIds, + elementIds: result.elementIds, + mode: result.mode, + }, + }, + }); + inlineEditor.setInlineRange({ + index: inlineRange.index + text.length, + length: 0, + }); + } else { + inlineEditor.insertText(inlineRange, text, { + link: text, + }); + inlineEditor.setInlineRange({ + index: inlineRange.index + text.length, + length: 0, + }); + } + } else { + inlineEditor.insertText(inlineRange, text); + inlineEditor.setInlineRange({ + index: inlineRange.index + text.length, + length: 0, + }); + } + }; + + private get std() { + const host = this.view.contextGet(HostContextKey); + return host?.std; + } + + override connectedCallback() { + super.connectedCallback(); + const selectAll = (e: KeyboardEvent) => { + if (e.key === 'a' && (IS_MAC ? e.metaKey : e.ctrlKey)) { + e.stopPropagation(); + e.preventDefault(); + this.inlineEditor.selectAll(); + } + }; + this.addEventListener('keydown', selectAll); + this.disposables.add(() => { + this.removeEventListener('keydown', selectAll); + }); + } + + override firstUpdated(props: Map) { + super.firstUpdated(props); + this.disposables.addFromEvent(this.richText, 'copy', this._onCopy); + this.disposables.addFromEvent(this.richText, 'cut', this._onCut); + this.disposables.addFromEvent(this.richText, 'paste', e => { + this._onPaste(e); + }); + this.richText.updateComplete + .then(() => { + this.inlineEditor.focusEnd(); + + this.disposables.add( + effect(() => { + const inlineRange = this.inlineEditor.inlineRange$.value; + if (inlineRange) { + if (!this.isEditing) { + this.selectCurrentCell(true); + } + } else { + if (this.isEditing) { + this.selectCurrentCell(false); + } + } + }) + ); + }) + .catch(console.error); + } + + override render() { + return html`${this.renderIcon()} + + this.topContenteditableElement?.host + ? getViewportElement(this.topContenteditableElement.host) + : null} + class="microsheet-data-view-header-area-rich-text can-link-doc" + >`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'microsheet-data-view-header-area-text': HeaderAreaTextCell; + 'microsheet-data-view-header-area-text-editing': HeaderAreaTextCellEditing; + } +} diff --git a/packages/blocks/src/microsheet-block/properties/utils.ts b/packages/blocks/src/microsheet-block/properties/utils.ts new file mode 100644 index 000000000000..851c5eb61aec --- /dev/null +++ b/packages/blocks/src/microsheet-block/properties/utils.ts @@ -0,0 +1,9 @@ +import { Text } from '@blocksuite/store'; + +export type RichTextCellType = Text | Text['yText']; +export const toYText = (text: RichTextCellType): Text['yText'] => { + if (text instanceof Text) { + return text.yText; + } + return text; +}; diff --git a/packages/blocks/src/microsheet-block/utils.ts b/packages/blocks/src/microsheet-block/utils.ts new file mode 100644 index 000000000000..ea0ed571b97a --- /dev/null +++ b/packages/blocks/src/microsheet-block/utils.ts @@ -0,0 +1,357 @@ +import type { + MicrosheetCell as Cell, + MicrosheetColumn as Column, + MicrosheetColumnUpdater as ColumnUpdater, + MicrosheetBlockModel, + MicrosheetViewBasicDataType as ViewBasicDataType, +} from '@blocksuite/affine-model'; +import type { BlockStdScope, TextSelection } from '@blocksuite/block-std'; +import type { BlockModel } from '@blocksuite/store'; + +import { + arrayMove, + insertPositionToIndex, + type InsertToPosition, +} from '@blocksuite/affine-shared/utils'; +import { assertExists } from '@blocksuite/global/utils'; + +export function addProperty( + model: MicrosheetBlockModel, + position: InsertToPosition, + column: Omit & { + id?: string; + } +): string { + const id = column.id ?? model.doc.generateBlockId(); + if (model.columns.some(v => v.id === id)) { + return id; + } + model.doc.transact(() => { + const col: Column = { + ...column, + id, + }; + model.columns.splice( + insertPositionToIndex(position, model.columns), + 0, + col + ); + }); + model.children.forEach(item => { + const cellContainerId = model.doc.addBlock('affine:cell', {}, item.id); + model.doc.addBlock( + 'affine:paragraph', + { + text: new model.doc.Text(``), + }, + cellContainerId + ); + updateCell(model, item.id, { + columnId: id, + value: '', + ref: cellContainerId, + }); + }); + return id; +} + +export function applyCellsUpdate(model: MicrosheetBlockModel) { + model.doc.updateBlock(model, { + cells: model.cells, + }); +} + +export function applyPropertyUpdate(model: MicrosheetBlockModel) { + model.doc.updateBlock(model, { + columns: model.columns, + }); +} + +export function applyViewsUpdate(model: MicrosheetBlockModel) { + model.doc.updateBlock(model, { + views: model.views, + }); +} + +export function copyCellsByProperty( + model: MicrosheetBlockModel, + fromId: Column['id'], + toId: Column['id'] +) { + model.doc.transact(() => { + Object.keys(model.cells).forEach(rowId => { + const cell = model.cells[rowId][fromId]; + if (cell) { + model.cells[rowId][toId] = { + ...cell, + columnId: toId, + }; + } + }); + }); +} + +export function deleteColumn( + model: MicrosheetBlockModel, + columnId: Column['id'] +) { + const index = findPropertyIndex(model, columnId); + if (index < 0) return; + + model.doc.transact(() => { + model.columns.splice(index, 1); + }); +} + +export function deleteRows(model: MicrosheetBlockModel, rowIds: string[]) { + model.doc.transact(() => { + for (const rowId of rowIds) { + delete model.cells[rowId]; + } + }); +} + +export function deleteView(model: MicrosheetBlockModel, id: string) { + model.doc.captureSync(); + model.doc.transact(() => { + model.views = model.views.filter(v => v.id !== id); + }); +} + +export function duplicateView(model: MicrosheetBlockModel, id: string): string { + const newId = model.doc.generateBlockId(); + model.doc.transact(() => { + const index = model.views.findIndex(v => v.id === id); + const view = model.views[index]; + if (view) { + model.views.splice( + index + 1, + 0, + JSON.parse(JSON.stringify({ ...view, id: newId })) + ); + } + }); + return newId; +} + +export function findPropertyIndex( + model: MicrosheetBlockModel, + id: Column['id'] +) { + return model.columns.findIndex(v => v.id === id); +} + +export function getCell( + model: MicrosheetBlockModel, + rowId: BlockModel['id'], + columnId: Column['id'] +): Cell | null { + if (columnId === 'title') { + return { + columnId: 'title', + value: rowId, + ref: '', + }; + } + const yRow = model.cells$.value[rowId]; + const yCell = yRow?.[columnId] ?? null; + if (!yCell) return null; + + return { + ...yCell, + }; +} + +export function getProperty( + model: MicrosheetBlockModel, + id: Column['id'] +): Column | undefined { + return model.columns.find(v => v.id === id); +} + +export function moveViewTo( + model: MicrosheetBlockModel, + id: string, + position: InsertToPosition +) { + model.doc.transact(() => { + model.views = arrayMove( + model.views, + v => v.id === id, + arr => insertPositionToIndex(position, arr) + ); + }); + applyViewsUpdate(model); +} + +export function updateCell( + model: MicrosheetBlockModel, + rowId: string, + cell: Cell +) { + const hasRow = rowId in model.cells; + if (!hasRow) { + model.cells[rowId] = Object.create(null); + } + model.doc.transact(() => { + model.cells[rowId][cell.columnId] = { + ...cell, + // columnId: cell.columnId, + // value: cell.value, + }; + }); +} + +export function updateCells( + model: MicrosheetBlockModel, + columnId: string, + cells: Record +) { + model.doc.transact(() => { + // @ts-expect-error + Object.entries(cells).forEach(([rowId, value, ref]) => { + if (!model.cells[rowId]) { + model.cells[rowId] = Object.create(null); + } + model.cells[rowId][columnId] = { + columnId, + value, + // @ts-expect-error + ref, + }; + }); + }); +} + +export function updateProperty( + model: MicrosheetBlockModel, + id: string, + updater: ColumnUpdater +) { + const index = model.columns.findIndex(v => v.id === id); + if (index == null) { + return; + } + model.doc.transact(() => { + const column = model.columns[index]; + const result = updater(column); + model.columns[index] = { ...column, ...result }; + }); + return id; +} + +export const updateView = ( + model: MicrosheetBlockModel, + id: string, + update: (data: ViewData) => Partial +) => { + model.doc.transact(() => { + model.views = model.views.map(v => { + if (v.id !== id) { + return v; + } + return { ...v, ...update(v as ViewData) }; + }); + }); + applyViewsUpdate(model); +}; +export const MICROSHEET_CONVERT_WHITE_LIST = [ + 'affine:list', + 'affine:paragraph', +]; + +const checkTypes = ['affine:paragraph', 'affine:list']; + +export function getTheOnlyTextSelection( + std: BlockStdScope +): TextSelection | null { + const value = std.selection.value; + if (value.length === 1 && value[0].type === 'text') { + return value[0] as TextSelection; + } + + return null; +} + +export function isInCellStart(std: BlockStdScope, atTextStart = false) { + const value = getTheOnlyTextSelection(std); + const doc = std.doc; + + if (value) { + const currentModel = doc.getBlockById(value.blockId); + if (currentModel && checkTypes.includes(currentModel.flavour)) { + const parentModel = doc.getParent(currentModel); + if ( + parentModel?.flavour === 'affine:cell' && + parentModel.firstChild() === currentModel + ) { + if (!atTextStart) return true; + return value.start.index === 0; + } + } + } + + return false; +} + +export function isInCellEnd(std: BlockStdScope, atTextEnd = false) { + const value = getTheOnlyTextSelection(std); + const doc = std.doc; + + if (value) { + const currentModel = doc.getBlockById(value.blockId); + if (currentModel && checkTypes.includes(currentModel.flavour)) { + const parentModel = doc.getParent(currentModel); + if ( + parentModel?.flavour === 'affine:cell' && + parentModel.lastChild() === currentModel + ) { + if (!atTextEnd) return true; + const textLength = currentModel.text?.length; + return textLength ? value.end.index === textLength : true; + } + } + } + + return false; +} + +export function calculateLineNum(std: BlockStdScope) { + const value = getTheOnlyTextSelection(std); + const doc = std.doc; + assertExists(value); + + const currentModel = doc.getBlockById(value.blockId); + const element = std.host.querySelector( + `[data-block-id="${value.blockId}"] .inline-editor` + ); + assertExists(element); + + const text = currentModel?.text?.toString().slice(0, value.start.index + 1); + assertExists(text); + + const temp = document.createElement('div'); + temp.style.margin = '0'; + temp.style.padding = '0'; + // @ts-ignore + temp.style.fontFamily = element.style.fontFamily; + // @ts-ignore + temp.style.fontSize = element.style.fontSize; + temp.style.width = element.getBoundingClientRect().width + 'px'; + + element.parentElement?.append(temp); + temp.innerHTML = 'A'; + const lineHeight = temp.clientHeight; + + temp.innerHTML = text || 'A'; + const currentHeight = temp.clientHeight; + temp?.remove(); + + const lines = Math.floor(element.getBoundingClientRect().height / lineHeight); + const line = Math.floor(currentHeight / lineHeight); + + return { + isFirst: line <= 1, + isLast: line >= lines, + }; +} diff --git a/packages/blocks/src/microsheet-block/views/index.ts b/packages/blocks/src/microsheet-block/views/index.ts new file mode 100644 index 000000000000..4e0f06b78153 --- /dev/null +++ b/packages/blocks/src/microsheet-block/views/index.ts @@ -0,0 +1,9 @@ +import type { ViewMeta } from '@blocksuite/microsheet-data-view'; + +import { viewPresets } from '@blocksuite/microsheet-data-view/view-presets'; + +export const microsheetBlockViews: ViewMeta[] = [viewPresets.tableViewMeta]; + +export const microsheetBlockViewMap = Object.fromEntries( + microsheetBlockViews.map(view => [view.type, view]) +); diff --git a/packages/blocks/src/microsheet-block/widgets/index.ts b/packages/blocks/src/microsheet-block/widgets/index.ts new file mode 100644 index 000000000000..75e9c52b6c7b --- /dev/null +++ b/packages/blocks/src/microsheet-block/widgets/index.ts @@ -0,0 +1 @@ +export const commonTools = []; diff --git a/packages/blocks/src/microsheet-data-view-block/block-meta/base.ts b/packages/blocks/src/microsheet-data-view-block/block-meta/base.ts new file mode 100644 index 000000000000..162403f518ed --- /dev/null +++ b/packages/blocks/src/microsheet-data-view-block/block-meta/base.ts @@ -0,0 +1,36 @@ +import type { Disposable } from '@blocksuite/global/utils'; +import type { PropertyMetaConfig } from '@blocksuite/microsheet-data-view'; +import type { Block, BlockModel } from '@blocksuite/store'; + +type PropertyMeta< + T extends BlockModel = BlockModel, + Value = unknown, + ColumnData extends NonNullable = NonNullable, +> = { + name: string; + key: string; + metaConfig: PropertyMetaConfig; + getColumnData?: (block: T) => ColumnData; + setColumnData?: (block: T, data: ColumnData) => void; + get: (block: T) => Value; + set?: (block: T, value: Value) => void; + updated: (block: T, callback: () => void) => Disposable; +}; +export type BlockMeta = { + selector: (block: Block) => boolean; + properties: PropertyMeta[]; +}; +export const createBlockMeta = ( + options: Omit, 'properties'> +) => { + const meta: BlockMeta = { + ...options, + properties: [], + }; + return { + ...meta, + addProperty: (property: PropertyMeta) => { + meta.properties.push(property as PropertyMeta); + }, + }; +}; diff --git a/packages/blocks/src/microsheet-data-view-block/block-meta/index.ts b/packages/blocks/src/microsheet-data-view-block/block-meta/index.ts new file mode 100644 index 000000000000..583105b7cf10 --- /dev/null +++ b/packages/blocks/src/microsheet-data-view-block/block-meta/index.ts @@ -0,0 +1,3 @@ +import type { BlockMeta } from './base.js'; + +export const blockMetaMap = {} satisfies Record; diff --git a/packages/blocks/src/microsheet-data-view-block/columns/index.ts b/packages/blocks/src/microsheet-data-view-block/columns/index.ts new file mode 100644 index 000000000000..54765a9c0efb --- /dev/null +++ b/packages/blocks/src/microsheet-data-view-block/columns/index.ts @@ -0,0 +1,12 @@ +import type { PropertyMetaConfig } from '@blocksuite/microsheet-data-view'; + +import { propertyPresets } from '@blocksuite/microsheet-data-view/property-presets'; + +import { richTextColumnConfig } from '../../database-block/properties/rich-text/cell-renderer.js'; + +export const queryBlockColumns = [propertyPresets.textPropertyConfig]; +export const queryBlockHiddenColumns = [richTextColumnConfig]; +const queryBlockAllColumns = [...queryBlockColumns, ...queryBlockHiddenColumns]; +export const queryBlockAllColumnMap = Object.fromEntries( + queryBlockAllColumns.map(v => [v.type, v as PropertyMetaConfig]) +); diff --git a/packages/blocks/src/microsheet-data-view-block/data-source.ts b/packages/blocks/src/microsheet-data-view-block/data-source.ts new file mode 100644 index 000000000000..ab7ea8d3e32a --- /dev/null +++ b/packages/blocks/src/microsheet-data-view-block/data-source.ts @@ -0,0 +1,320 @@ +import type { Column } from '@blocksuite/affine-model'; +import type { EditorHost } from '@blocksuite/block-std'; +import type { Block, Doc } from '@blocksuite/store'; + +import { + insertPositionToIndex, + type InsertToPosition, +} from '@blocksuite/affine-shared/utils'; +import { assertExists, Slot } from '@blocksuite/global/utils'; +import { + DataSourceBase, + type PropertyMetaConfig, +} from '@blocksuite/microsheet-data-view'; +import { propertyPresets } from '@blocksuite/microsheet-data-view/property-presets'; + +import type { BlockMeta } from './block-meta/base.js'; +import type { MicrosheetDataViewBlockModel } from './data-view-model.js'; + +import { + databaseBlockAllPropertyMap, + databasePropertyConverts, +} from '../database-block/properties/index.js'; +import { blockMetaMap } from './block-meta/index.js'; +import { queryBlockAllColumnMap, queryBlockColumns } from './columns/index.js'; + +export type BlockQueryDataSourceConfig = { + type: keyof typeof blockMetaMap; +}; + +// @ts-ignore +export class BlockQueryDataSource extends DataSourceBase { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private columnMetaMap = new Map>(); + + private meta: BlockMeta; + + blockMap = new Map(); + + docDisposeMap = new Map void>(); + + slots = { + update: new Slot(), + }; + + private get blocks() { + return [...this.blockMap.values()]; + } + + get properties(): string[] { + return [ + ...this.meta.properties.map(v => v.key), + ...this.block.columns.map(v => v.id), + ]; + } + + get propertyMetas(): PropertyMetaConfig[] { + return queryBlockColumns as PropertyMetaConfig[]; + } + + get rows(): string[] { + return this.blocks.map(v => v.id); + } + + get workspace() { + return this.host.doc.collection; + } + + constructor( + private host: EditorHost, + private block: MicrosheetDataViewBlockModel, + config: BlockQueryDataSourceConfig + ) { + super(); + this.meta = blockMetaMap[config.type]; + for (const property of this.meta.properties) { + this.columnMetaMap.set(property.metaConfig.type, property.metaConfig); + } + for (const collection of this.workspace.docs.values()) { + for (const block of Object.values(collection.getDoc().blocks.peek())) { + if (this.meta.selector(block)) { + this.blockMap.set(block.id, block); + } + } + } + this.workspace.docs.forEach(doc => { + this.listenToDoc(doc.getDoc()); + }); + this.workspace.slots.docAdded.on(id => { + const doc = this.workspace.getDoc(id); + if (doc) { + this.listenToDoc(doc); + } + }); + this.workspace.slots.docRemoved.on(id => { + this.docDisposeMap.get(id)?.(); + }); + } + + private getProperty(propertyId: string) { + const property = this.meta.properties.find(v => v.key === propertyId); + assertExists(property, `property ${propertyId} not found`); + return property; + } + + private newColumnName() { + let i = 1; + while (this.block.columns.some(column => column.name === `Column ${i}`)) { + i++; + } + return `Column ${i}`; + } + + cellRefGet(rowId: string, propertyId: string): string { + const block = this.blockMap.get(rowId); + if (block) { + // @ts-expect-error + return this.getProperty(propertyId)?.get(block.model)?.ref ?? ''; + } + return ''; + } + + cellValueChange(rowId: string, propertyId: string, value: unknown): void { + const viewColumn = this.getViewColumn(propertyId); + if (viewColumn) { + this.block.cells[rowId] = { + ...this.block.cells[rowId], + [propertyId]: value, + }; + return; + } + const block = this.blockMap.get(rowId); + if (block) { + this.meta.properties + .find(v => v.key === propertyId) + ?.set?.(block.model, value); + } + } + + cellValueGet(rowId: string, propertyId: string): unknown { + const viewColumn = this.getViewColumn(propertyId); + if (viewColumn) { + return this.block.cells[rowId]?.[propertyId]; + } + const block = this.blockMap.get(rowId); + if (block) { + return this.getProperty(propertyId)?.get(block.model); + } + return; + } + + getViewColumn(id: string) { + return this.block.columns.find(v => v.id === id); + } + + listenToDoc(doc: Doc) { + this.docDisposeMap.set( + doc.id, + doc.slots.blockUpdated.on(v => { + if (v.type === 'add') { + const blockById = doc.getBlock(v.id); + if (blockById && this.meta.selector(blockById)) { + this.blockMap.set(v.id, blockById); + } + } else if (v.type === 'delete') { + this.blockMap.delete(v.id); + } + this.slots.update.emit(); + }).dispose + ); + } + + propertyAdd( + insertToPosition: InsertToPosition, + type: string | undefined + ): string { + const doc = this.block.doc; + doc.captureSync(); + const column = databaseBlockAllPropertyMap[ + type ?? propertyPresets.textPropertyConfig.type + ].create(this.newColumnName()); + + const id = doc.generateBlockId(); + if (this.block.columns.some(v => v.id === id)) { + return id; + } + doc.transact(() => { + const col: Column = { + ...column, + id, + }; + this.block.columns.splice( + insertPositionToIndex(insertToPosition, this.block.columns), + 0, + col + ); + }); + return id; + } + + propertyDataGet(propertyId: string): Record { + const viewColumn = this.getViewColumn(propertyId); + if (viewColumn) { + return viewColumn.data; + } + const property = this.getProperty(propertyId); + return ( + property.getColumnData?.(this.blocks[0].model) ?? + property.metaConfig.config.defaultData() + ); + } + + propertyDataSet(propertyId: string, data: Record): void { + const viewColumn = this.getViewColumn(propertyId); + if (viewColumn) { + viewColumn.data = data; + } + } + + propertyDelete(_id: string): void { + const index = this.block.columns.findIndex(v => v.id === _id); + if (index >= 0) { + this.block.columns.splice(index, 1); + } + } + + propertyDuplicate(_columnId: string): string { + throw new Error('Method not implemented.'); + } + + propertyMetaGet(type: string): PropertyMetaConfig { + const meta = this.columnMetaMap.get(type); + if (meta) { + return meta; + } + return queryBlockAllColumnMap[type]; + } + + propertyNameGet(propertyId: string): string { + const viewColumn = this.getViewColumn(propertyId); + if (viewColumn) { + return viewColumn.name; + } + if (propertyId === 'type') { + return 'Block Type'; + } + return this.getProperty(propertyId)?.name ?? ''; + } + + propertyNameSet(propertyId: string, name: string): void { + const viewColumn = this.getViewColumn(propertyId); + if (viewColumn) { + viewColumn.name = name; + } + } + + override propertyReadonlyGet(propertyId: string): boolean { + const viewColumn = this.getViewColumn(propertyId); + if (viewColumn) { + return false; + } + if (propertyId === 'type') return true; + return this.getProperty(propertyId)?.set == null; + } + + propertyTypeGet(propertyId: string): string { + const viewColumn = this.getViewColumn(propertyId); + if (viewColumn) { + return viewColumn.type; + } + if (propertyId === 'type') { + return 'image'; + } + return this.getProperty(propertyId).metaConfig.type; + } + + propertyTypeSet(propertyId: string, toType: string): void { + const viewColumn = this.getViewColumn(propertyId); + if (viewColumn) { + const currentType = viewColumn.type; + const currentData = viewColumn.data; + const rows = this.rows$.value; + const currentCells = rows.map(rowId => + this.cellValueGet(rowId, propertyId) + ); + const convertFunction = databasePropertyConverts.find( + v => v.from === currentType && v.to === toType + )?.convert; + const result = convertFunction?.( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + currentData as any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + currentCells as any + ) ?? { + property: databaseBlockAllPropertyMap[toType].config.defaultData(), + cells: currentCells.map(() => undefined), + }; + this.block.doc.captureSync(); + viewColumn.type = toType; + viewColumn.data = result.property; + currentCells.forEach((value, i) => { + if (value != null || result.cells[i] != null) { + this.block.cells[rows[i]] = { + ...this.block.cells[rows[i]], + [propertyId]: result.cells[i], + }; + } + }); + } + } + + rowAdd(_insertPosition: InsertToPosition | number): string { + throw new Error('Method not implemented.'); + } + + rowDelete(_ids: string[]): void { + throw new Error('Method not implemented.'); + } + + rowMove(_rowId: string, _position: InsertToPosition): void {} +} diff --git a/packages/blocks/src/microsheet-data-view-block/data-view-block.ts b/packages/blocks/src/microsheet-data-view-block/data-view-block.ts new file mode 100644 index 000000000000..0464efd916ed --- /dev/null +++ b/packages/blocks/src/microsheet-data-view-block/data-view-block.ts @@ -0,0 +1,250 @@ +import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption'; +import { + menu, + popMenu, + popupTargetFromElement, +} from '@blocksuite/affine-components/context-menu'; +import { + CopyIcon, + DeleteIcon, + MoreHorizontalIcon, +} from '@blocksuite/affine-components/icons'; +import { RANGE_SYNC_EXCLUDE_ATTR } from '@blocksuite/block-std'; +import { + type DataSource, + DataView, + dataViewCommonStyle, + type DataViewProps, + defineUniComponent, + type MicrosheetDataViewSelection, + type MicrosheetDataViewWidget, + type MicrosheetDataViewWidgetProps, + MicrosheetSelection, + renderUniLit, +} from '@blocksuite/microsheet-data-view'; +import { widgetPresets } from '@blocksuite/microsheet-data-view/widget-presets'; +import { Slice } from '@blocksuite/store'; +import { computed, type ReadonlySignal, signal } from '@preact/signals-core'; +import { css, nothing, unsafeCSS } from 'lit'; +import { html } from 'lit/static-html.js'; + +import type { NoteBlockComponent } from '../note-block/index.js'; +import type { MicrosheetDataViewBlockModel } from './data-view-model.js'; + +import { + EdgelessRootBlockComponent, + type RootService, +} from '../root-block/index.js'; + +export class MicrosheetDataViewBlockComponent extends CaptionedBlockComponent { + static override styles = css` + ${unsafeCSS(dataViewCommonStyle('affine-database'))} + affine-database { + display: block; + border-radius: 8px; + background-color: var(--affine-background-primary-color); + padding: 8px; + margin: 8px -8px -8px; + } + + .database-block-selected { + background-color: var(--affine-hover-color); + border-radius: 4px; + } + + .database-ops { + margin-top: 4px; + padding: 2px; + border-radius: 4px; + display: flex; + cursor: pointer; + } + + .database-ops svg { + width: 16px; + height: 16px; + color: var(--affine-icon-color); + } + + .database-ops:hover { + background-color: var(--affine-hover-color); + } + + @media print { + .database-ops { + display: none; + } + + .database-header-bar { + display: none !important; + } + } + `; + + private _clickDatabaseOps = (e: MouseEvent) => { + popMenu(popupTargetFromElement(e.currentTarget as HTMLElement), { + options: { + items: [ + menu.input({ + initialValue: this.model.title, + placeholder: 'Untitled', + onComplete: text => { + this.model.title = text; + }, + }), + menu.action({ + prefix: CopyIcon, + name: 'Copy', + select: () => { + const slice = Slice.fromModels(this.doc, [this.model]); + this.std.clipboard.copySlice(slice).catch(console.error); + }, + }), + menu.group({ + name: '', + items: [ + menu.action({ + prefix: DeleteIcon, + class: { 'delete-item': true }, + name: 'Delete Database', + select: () => { + this.model.children.slice().forEach(block => { + this.doc.deleteBlock(block); + }); + this.doc.deleteBlock(this.model); + }, + }), + ], + }), + ], + }, + }); + }; + + private _dataSource?: DataSource; + + private dataView = new DataView(); + + _bindHotkey: DataViewProps['bindHotkey'] = hotkeys => { + return { + dispose: this.host.event.bindHotkey(hotkeys, { + blockId: this.topContenteditableElement?.blockId ?? this.blockId, + }), + }; + }; + + _handleEvent: DataViewProps['handleEvent'] = (name, handler) => { + return { + dispose: this.host.event.add(name, handler, { + blockId: this.blockId, + }), + }; + }; + + getRootService = () => { + return this.std.getService('affine:page'); + }; + + headerWidget: MicrosheetDataViewWidget = defineUniComponent( + (props: MicrosheetDataViewWidgetProps) => { + return html` +
+
+
${this.model.title}
+ ${this.renderDatabaseOps()} +
+
+ ${renderUniLit(this.toolsWidget, props)} +
+
+ `; + } + ); + + selection$: ReadonlySignal = computed(() => { + const microsheetSelection = this.selection.value.find( + (selection): selection is MicrosheetSelection => { + if (selection.blockId !== this.blockId) { + return false; + } + return selection instanceof MicrosheetSelection; + } + ); + return microsheetSelection?.viewSelection as MicrosheetDataViewSelection; + }); + + setSelection = (selection: MicrosheetDataViewSelection | undefined) => { + this.selection.setGroup( + 'note', + selection + ? [ + new MicrosheetSelection({ + blockId: this.blockId, + viewSelection: selection, + }), + ] + : [] + ); + }; + + toolsWidget = widgetPresets.createTools({ + table: [], + }); + + get dataSource(): DataSource { + return this._dataSource as DataSource; + } + + override get topContenteditableElement() { + if (this.rootComponent instanceof EdgelessRootBlockComponent) { + const note = this.closest('affine-note'); + return note; + } + return this.rootComponent; + } + + get view() { + return this.dataView.expose; + } + + private renderDatabaseOps() { + if (this.doc.readonly) { + return nothing; + } + return html`
+ ${MoreHorizontalIcon} +
`; + } + + override connectedCallback() { + super.connectedCallback(); + + this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true'); + } + + override renderBlock() { + return html` +
+ ${this.dataView.render({ + virtualPadding$: signal(0), + bindHotkey: this._bindHotkey, + handleEvent: this._handleEvent, + selection$: this.selection$, + setSelection: this.setSelection, + dataSource: this.dataSource, + headerWidget: this.headerWidget, + std: this.std, + })} +
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-microsheet-data-view': MicrosheetDataViewBlockComponent; + } +} diff --git a/packages/blocks/src/microsheet-data-view-block/data-view-model.ts b/packages/blocks/src/microsheet-data-view-block/data-view-model.ts new file mode 100644 index 000000000000..3fb48f6b1558 --- /dev/null +++ b/packages/blocks/src/microsheet-data-view-block/data-view-model.ts @@ -0,0 +1,96 @@ +import type { Column } from '@blocksuite/affine-model'; +import type { DataViewDataType } from '@blocksuite/microsheet-data-view'; + +import { + arrayMove, + insertPositionToIndex, + type InsertToPosition, +} from '@blocksuite/affine-shared/utils'; +import { BlockModel, defineBlockSchema } from '@blocksuite/store'; + +type Props = { + title: string; + views: DataViewDataType[]; + columns: Column[]; + cells: Record>; +}; + +export class MicrosheetDataViewBlockModel extends BlockModel { + constructor() { + super(); + } + + applyViewsUpdate() { + this.doc.updateBlock(this, { + views: this.views, + }); + } + + deleteView(id: string) { + this.doc.captureSync(); + this.doc.transact(() => { + this.views = this.views.filter(v => v.id !== id); + }); + } + + duplicateView(id: string): string { + const newId = this.doc.generateBlockId(); + this.doc.transact(() => { + const index = this.views.findIndex(v => v.id === id); + const view = this.views[index]; + if (view) { + this.views.splice( + index + 1, + 0, + JSON.parse(JSON.stringify({ ...view, id: newId })) + ); + } + }); + return newId; + } + + moveViewTo(id: string, position: InsertToPosition) { + this.doc.transact(() => { + this.views = arrayMove( + this.views, + v => v.id === id, + arr => insertPositionToIndex(position, arr) + ); + }); + this.applyViewsUpdate(); + } + + updateView( + id: string, + update: (data: DataViewDataType) => Partial + ) { + this.doc.transact(() => { + this.views = this.views.map(v => { + if (v.id !== id) { + return v; + } + return { ...v, ...(update(v) as DataViewDataType) }; + }); + }); + this.applyViewsUpdate(); + } +} + +export const MicrosheetDataViewBlockSchema = defineBlockSchema({ + flavour: 'affine:microsheet-data-view', + props: (): Props => ({ + views: [], + title: '', + columns: [], + cells: {}, + }), + metadata: { + role: 'hub', + version: 1, + parent: ['affine:note'], + children: ['affine:paragraph', 'affine:list'], + }, + toModel: () => { + return new MicrosheetDataViewBlockModel(); + }, +}); diff --git a/packages/blocks/src/microsheet-data-view-block/data-view-spec.ts b/packages/blocks/src/microsheet-data-view-block/data-view-spec.ts new file mode 100644 index 000000000000..9c8c516a4634 --- /dev/null +++ b/packages/blocks/src/microsheet-data-view-block/data-view-spec.ts @@ -0,0 +1,17 @@ +import { + BlockViewExtension, + type ExtensionType, + FlavourExtension, +} from '@blocksuite/block-std'; +import { literal } from 'lit/static-html.js'; + +import { MicrosheetDataViewBlockService } from './microsheet-service.js'; + +export const MicrosheetDataViewBlockSpec: ExtensionType[] = [ + FlavourExtension('affine:microsheet-data-view'), + MicrosheetDataViewBlockService, + BlockViewExtension( + 'affine:microsheet-data-view', + literal`affine-microsheet-data-view` + ), +]; diff --git a/packages/blocks/src/microsheet-data-view-block/index.ts b/packages/blocks/src/microsheet-data-view-block/index.ts new file mode 100644 index 000000000000..e0a82084d385 --- /dev/null +++ b/packages/blocks/src/microsheet-data-view-block/index.ts @@ -0,0 +1,12 @@ +import type { MicrosheetDataViewBlockModel } from './data-view-model.js'; + +export * from './data-view-block.js'; +export * from './data-view-model.js'; + +declare global { + namespace BlockSuite { + interface BlockModels { + 'affine:microsheet-data-view': MicrosheetDataViewBlockModel; + } + } +} diff --git a/packages/blocks/src/microsheet-data-view-block/microsheet-service.ts b/packages/blocks/src/microsheet-data-view-block/microsheet-service.ts new file mode 100644 index 000000000000..a99a6cfbafca --- /dev/null +++ b/packages/blocks/src/microsheet-data-view-block/microsheet-service.ts @@ -0,0 +1,14 @@ +import { BlockService } from '@blocksuite/block-std'; +import { MicrosheetSelection } from '@blocksuite/microsheet-data-view'; + +import { MicrosheetDataViewBlockSchema } from './data-view-model.js'; + +export class MicrosheetDataViewBlockService extends BlockService { + static override readonly flavour = + MicrosheetDataViewBlockSchema.model.flavour; + + override mounted(): void { + super.mounted(); + this.selectionManager.register(MicrosheetSelection); + } +} diff --git a/packages/blocks/src/microsheet-data-view-block/utils.ts b/packages/blocks/src/microsheet-data-view-block/utils.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/blocks/src/microsheet-data-view-block/views/index.ts b/packages/blocks/src/microsheet-data-view-block/views/index.ts new file mode 100644 index 000000000000..59ac89911948 --- /dev/null +++ b/packages/blocks/src/microsheet-data-view-block/views/index.ts @@ -0,0 +1,9 @@ +import type { ViewMeta } from '@blocksuite/microsheet-data-view'; + +import { viewPresets } from '@blocksuite/microsheet-data-view/view-presets'; + +export const blockQueryViews: ViewMeta[] = [viewPresets.tableViewMeta]; + +export const blockQueryViewMap = Object.fromEntries( + blockQueryViews.map(view => [view.type, view]) +); diff --git a/packages/blocks/src/root-block/clipboard/adapter.ts b/packages/blocks/src/root-block/clipboard/adapter.ts index 342282a17fab..7401934cbafa 100644 --- a/packages/blocks/src/root-block/clipboard/adapter.ts +++ b/packages/blocks/src/root-block/clipboard/adapter.ts @@ -1,3 +1,4 @@ +import type { CellBlockModel } from '@blocksuite/affine-model'; import type { BlockSnapshot, DocSnapshot, @@ -16,6 +17,7 @@ import type { import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import { assertExists } from '@blocksuite/global/utils'; import { BaseAdapter } from '@blocksuite/store'; +import { nanoid } from 'nanoid'; import { decodeClipboardBlobs, encodeClipboardBlobs } from './utils.js'; @@ -91,3 +93,261 @@ export class ClipboardAdapter extends BaseAdapter { return Promise.resolve(snapshot); } } + +export class MicrosheetAdapter extends BaseAdapter { + static MIME = 'blocksuite/microsheet'; + + override fromBlockSnapshot(): + | Promise> + | FromBlockSnapshotResult { + throw new Error('Method not implemented.'); + } + + override fromDocSnapshot(): + | Promise> + | FromDocSnapshotResult { + throw new Error('Method not implemented.'); + } + + override fromSliceSnapshot( + payload: FromSliceSnapshotPayload + ): + | Promise> + | FromSliceSnapshotResult { + // @ts-expect-error + return payload; + } + + override toBlockSnapshot(): Promise | BlockSnapshot { + throw new Error('Method not implemented.'); + } + + override toDocSnapshot(): Promise | DocSnapshot { + throw new Error('Method not implemented.'); + } + + override toSliceSnapshot( + payload: ToSliceSnapshotPayload + ): Promise | SliceSnapshot | null { + let copiedCells = []; + try { + copiedCells = JSON.parse(payload.file); + } catch (err) { + console.error(err); + } + if (copiedCells.length === 0) return null; + const microsheetSnapshotContent = new MicrosheetSnapshotContent( + copiedCells + ); + const snapshot: SliceSnapshot = { + type: 'slice', + // @ts-expect-error + pageVersion: payload.pageVersion, + // @ts-expect-error + workspaceVersion: payload.workspaceVersion, + // @ts-expect-error + workspaceId: payload.workspaceId, + // @ts-expect-error + pageId: payload.pageId, + content: [microsheetSnapshotContent.toSnapshotContent()], + }; + + return snapshot; + } +} + +interface CopiedCellItem { + cellContainerSlice: string; +} + +interface PropCellItem { + columnId: string; + value: string; + ref: string; +} + +type PropCells = Record>; + +interface PropColumnItem { + type: 'title' | 'rich-text'; + name: 'Title' | 'content'; + data: {}; + id: string; +} + +class MicrosheetSnapshotContent { + cells: PropCells = {}; + + colCount: number; + + columns: PropColumnItem[] = []; + + copiedCells: CopiedCellItem[][]; + + rowCount: number; + + rows: RowSnapshot[] = []; + + titleColumnsId: string; + + constructor(copiedCells: CopiedCellItem[][]) { + this.copiedCells = copiedCells; + this.titleColumnsId = nanoid(); + this.rowCount = copiedCells.length; + this.colCount = copiedCells[0]?.length || 0; + + this.init(); + } + + private addColumn(props: Partial = {}) { + const newColumn: PropColumnItem = { + type: 'rich-text', + name: 'Title', + data: {}, + id: nanoid(), + ...props, + }; + this.columns.push(newColumn); + return newColumn.id; + } + + private addRow() { + const row = new RowSnapshot(this); + this.rows.push(row); + return row; + } + + private getCellContent(i: number, j: number) { + const item = this.copiedCells[i][j]; + try { + const snapshot = JSON.parse(item.cellContainerSlice) + ?.snapshot as SliceSnapshot; + return snapshot.content; + } catch (err) { + console.error(err); + return []; + } + } + + private getProps() { + return { + views: [ + { + id: nanoid(), + name: 'Table View', + mode: 'table', + columns: [], + filter: { + type: 'group', + op: 'and', + conditions: [], + }, + header: { + titleColumn: this.titleColumnsId, + iconColumn: 'type', + }, + }, + ], + title: { + '$blocksuite:internal:text$': true, + delta: [], + }, + cells: this.cells, + columns: this.columns, + }; + } + + private init() { + this.addColumn({ id: this.titleColumnsId, type: 'title' }); + + const contentColumnIds = []; + for (let i = 0; i < this.colCount; i++) { + contentColumnIds.push(this.addColumn()); + } + + for (let i = 0; i < this.rowCount; i++) { + const row = this.addRow(); + for (let j = 0; j < this.colCount; j++) { + const cell = row.addCell(contentColumnIds[j]); + // @ts-expect-error + cell.addChildren(this.getCellContent(i, j)); + } + } + } + + toSnapshotContent() { + // @ts-expect-error + return { + type: 'block', + id: nanoid(), + flavour: 'affine:microsheet', + version: 1, + props: this.getProps(), + children: this.rows.map(row => row.toSnapshotContent()), + } as SliceSnapshot['content'][number]; + } +} + +class RowSnapshot { + cells: CellSnapshot[] = []; + + ctx: MicrosheetSnapshotContent; + + id: string; + + constructor(ctx: MicrosheetSnapshotContent) { + this.ctx = ctx; + this.id = nanoid(); + } + + addCell(columnId: string) { + const cell = new CellSnapshot(); + this.cells.push(cell); + + if (!this.ctx.cells[this.id]) { + this.ctx.cells[this.id] = {}; + } + this.ctx.cells[this.id][columnId] = { + columnId, + value: '', + ref: cell.id, + }; + return cell; + } + + toSnapshotContent() { + return { + type: 'block', + id: this.id, + flavour: 'affine:row', + version: 1, + props: {}, + children: this.cells.map(cell => cell.toSnapshotContent()), + }; + } +} + +class CellSnapshot { + children: CellBlockModel[] = []; + + id: string; + + constructor() { + this.id = nanoid(); + } + + addChildren(items: CellBlockModel[]) { + this.children.push(...items); + } + + toSnapshotContent() { + return { + type: 'block', + id: this.id, + flavour: 'affine:cell', + version: 1, + props: {}, + children: this.children, + }; + } +} diff --git a/packages/blocks/src/root-block/clipboard/index.ts b/packages/blocks/src/root-block/clipboard/index.ts index f72ccc6c6a96..319aed4d5a0e 100644 --- a/packages/blocks/src/root-block/clipboard/index.ts +++ b/packages/blocks/src/root-block/clipboard/index.ts @@ -15,7 +15,7 @@ import { replaceIdMiddleware, titleMiddleware, } from '../../_common/transformers/middlewares.js'; -import { ClipboardAdapter } from './adapter.js'; +import { ClipboardAdapter, MicrosheetAdapter } from './adapter.js'; import { copyMiddleware, pasteMiddleware } from './middlewares/index.js'; export class PageClipboard { @@ -36,6 +36,11 @@ export class PageClipboard { ClipboardAdapter, 100 ); + this._std.clipboard.registerAdapter( + MicrosheetAdapter.MIME, + MicrosheetAdapter, + 98 + ); this._std.clipboard.registerAdapter( 'text/_notion-text-production', NotionTextAdapter, diff --git a/packages/blocks/src/root-block/widgets/slash-menu/config.ts b/packages/blocks/src/root-block/widgets/slash-menu/config.ts index 61763b8147ef..05698a29bed5 100644 --- a/packages/blocks/src/root-block/widgets/slash-menu/config.ts +++ b/packages/blocks/src/root-block/widgets/slash-menu/config.ts @@ -59,6 +59,7 @@ import { formatDate, formatTime } from '../../utils/misc.js'; import { type SlashMenuTooltip, slashMenuToolTips } from './tooltips/index.js'; import { createConversionItem, + createMicrosheetBlockInNextLine, createTextFormatItem, insideEdgelessText, tryRemoveEmptyLine, @@ -537,6 +538,7 @@ export const defaultSlashMenuConfig: SlashMenuConfig = { // --------------------------------------------------------- { groupName: 'Database' }, + { name: 'Table View', description: 'Display items in a table format.', @@ -610,6 +612,30 @@ export const defaultSlashMenuConfig: SlashMenuConfig = { .run(); }, }, + { + name: 'Normal Table', + description: 'Display items in a table format.', + alias: ['database'], + icon: DatabaseTableViewIcon20, + tooltip: slashMenuToolTips['Table'], + showWhen: ({ model }) => + model.doc.schema.flavourSchemaMap.has('affine:microsheet') && + !insideEdgelessText(model), + action: ({ rootComponent, model }) => { + const id = createMicrosheetBlockInNextLine(model); + if (!id) { + return; + } + const service = rootComponent.std.getService('affine:microsheet'); + if (!service) return; + service.initMicrosheetBlock( + rootComponent.doc, + id, + viewPresets.tableViewMeta.type + ); + tryRemoveEmptyLine(model); + }, + }, // --------------------------------------------------------- { groupName: 'Actions' }, diff --git a/packages/blocks/src/root-block/widgets/slash-menu/utils.ts b/packages/blocks/src/root-block/widgets/slash-menu/utils.ts index 27c8446fe3bc..61f933e9db99 100644 --- a/packages/blocks/src/root-block/widgets/slash-menu/utils.ts +++ b/packages/blocks/src/root-block/widgets/slash-menu/utils.ts @@ -80,10 +80,46 @@ export function getFirstNotDividerItem( return firstItem ?? null; } +export function insideDatabase(model: BlockModel) { + return isInsideBlockByFlavour(model.doc, model, 'affine:database'); +} + +export function insideMicrosheet(model: BlockModel) { + return isInsideBlockByFlavour(model.doc, model, 'affine:microsheet'); +} + export function insideEdgelessText(model: BlockModel) { return isInsideBlockByFlavour(model.doc, model, 'affine:edgeless-text'); } +export function createDatabaseBlockInNextLine(model: BlockModel) { + let parent = model.doc.getParent(model); + while (parent && parent.flavour !== 'affine:note') { + model = parent; + parent = model.doc.getParent(parent); + } + if (!parent) { + return; + } + const index = parent.children.indexOf(model); + + return model.doc.addBlock('affine:database', {}, parent, index + 1); +} + +export function createMicrosheetBlockInNextLine(model: BlockModel) { + let parent = model.doc.getParent(model); + while (parent && parent.flavour !== 'affine:note') { + model = parent; + parent = model.doc.getParent(parent); + } + if (!parent) { + return; + } + const index = parent.children.indexOf(model); + + return model.doc.addBlock('affine:microsheet', {}, parent, index + 1); +} + export function tryRemoveEmptyLine(model: BlockModel) { if (model.text?.length === 0) { model.doc.deleteBlock(model); diff --git a/packages/blocks/src/row-block/index.ts b/packages/blocks/src/row-block/index.ts new file mode 100644 index 000000000000..253dfa8e49bc --- /dev/null +++ b/packages/blocks/src/row-block/index.ts @@ -0,0 +1,16 @@ +import type { RowBlockModel } from '@blocksuite/affine-model'; + +import type { RowBlockService } from './row-service.js'; + +export * from './row-block.js'; +export * from './row-service.js'; +declare global { + namespace BlockSuite { + interface BlockServices { + 'affine:row': RowBlockService; + } + interface BlockModels { + 'affine:row': RowBlockModel; + } + } +} diff --git a/packages/blocks/src/row-block/row-block.ts b/packages/blocks/src/row-block/row-block.ts new file mode 100644 index 000000000000..a238e2df391c --- /dev/null +++ b/packages/blocks/src/row-block/row-block.ts @@ -0,0 +1,62 @@ +/// + +import type { RowBlockModel } from '@blocksuite/affine-model'; +import type { TableSingleView } from '@blocksuite/microsheet-data-view/view-presets'; + +import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption'; +import { css, html } from 'lit'; +import { property } from 'lit/decorators.js'; + +import type { RowBlockService } from './row-service.js'; + +export class RowBlockComponent extends CaptionedBlockComponent< + RowBlockModel, + RowBlockService +> { + static override styles = css` + affine-row > .database-cell { + padding: 10px; + } + affine-row { + border-right: 1px solid var(--affine-border-color); + // border-left: 1px solid var(--affine-border-color); + // border-top: 1px solid var(--affine-border-color); + } + .affine-row-block-container { + display: flow-root; + } + .affine-row-block-container.selected { + background-color: var(--affine-hover-color); + } + `; + + override renderBlock() { + const { view, rowId, rowIndex, std } = this; + return html``; + } + + @property({ attribute: false }) + accessor rowId!: string; + + @property({ attribute: false }) + accessor rowIndex!: number; + + @property({ attribute: false }) + accessor view!: TableSingleView; + + @property({ attribute: false }) + override accessor widgets = {}; +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-row': RowBlockComponent; + } +} diff --git a/packages/blocks/src/row-block/row-service.ts b/packages/blocks/src/row-block/row-service.ts new file mode 100644 index 000000000000..7e8c7024a510 --- /dev/null +++ b/packages/blocks/src/row-block/row-service.ts @@ -0,0 +1,10 @@ +import { RowBlockSchema } from '@blocksuite/affine-model'; +import { BlockService } from '@blocksuite/block-std'; + +export class RowBlockService extends BlockService { + static override readonly flavour = RowBlockSchema.model.flavour; + + override mounted() { + super.mounted(); + } +} diff --git a/packages/blocks/src/row-block/row-spec.ts b/packages/blocks/src/row-block/row-spec.ts new file mode 100644 index 000000000000..29ea9433e085 --- /dev/null +++ b/packages/blocks/src/row-block/row-spec.ts @@ -0,0 +1,14 @@ +import { + BlockViewExtension, + type ExtensionType, + FlavourExtension, +} from '@blocksuite/block-std'; +import { literal } from 'lit/static-html.js'; + +import { RowBlockService } from './row-service.js'; + +export const RowBlockSpec: ExtensionType[] = [ + FlavourExtension('affine:row'), + RowBlockService, + BlockViewExtension('affine:row', literal`affine-row`), +]; diff --git a/packages/blocks/src/row-block/styles.ts b/packages/blocks/src/row-block/styles.ts new file mode 100644 index 000000000000..9eb1df55a2d7 --- /dev/null +++ b/packages/blocks/src/row-block/styles.ts @@ -0,0 +1,3 @@ +import { css } from 'lit'; + +export const rowBlockStyles = css``; diff --git a/packages/blocks/src/schemas.ts b/packages/blocks/src/schemas.ts index 0fb0cf9a702e..349982d5d04a 100644 --- a/packages/blocks/src/schemas.ts +++ b/packages/blocks/src/schemas.ts @@ -6,6 +6,7 @@ import { SurfaceBlockSchema } from '@blocksuite/affine-block-surface'; import { AttachmentBlockSchema, BookmarkBlockSchema, + CellBlockSchema, CodeBlockSchema, DatabaseBlockSchema, DividerBlockSchema, @@ -21,13 +22,16 @@ import { ImageBlockSchema, LatexBlockSchema, ListBlockSchema, + MicrosheetBlockSchema, NoteBlockSchema, ParagraphBlockSchema, RootBlockSchema, + RowBlockSchema, SurfaceRefBlockSchema, } from '@blocksuite/affine-model'; import { DataViewBlockSchema } from './data-view-block/data-view-model.js'; +import { MicrosheetDataViewBlockSchema } from './microsheet-data-view-block/data-view-model.js'; /** Built-in first party block models built for affine */ export const AffineSchemas: z.infer[] = [ @@ -42,8 +46,12 @@ export const AffineSchemas: z.infer[] = [ BookmarkBlockSchema, FrameBlockSchema, DatabaseBlockSchema, + MicrosheetBlockSchema, + RowBlockSchema, + CellBlockSchema, SurfaceRefBlockSchema, DataViewBlockSchema, + MicrosheetDataViewBlockSchema, AttachmentBlockSchema, EmbedYoutubeBlockSchema, EmbedFigmaBlockSchema, diff --git a/packages/framework/block-std/src/clipboard/index.ts b/packages/framework/block-std/src/clipboard/index.ts index 8196fe34f1dc..a39877cfab34 100644 --- a/packages/framework/block-std/src/clipboard/index.ts +++ b/packages/framework/block-std/src/clipboard/index.ts @@ -4,6 +4,7 @@ import type { Doc, JobMiddleware, Slice, + SliceSnapshot, } from '@blocksuite/store'; import type { RootContentMap } from 'hast'; @@ -243,6 +244,16 @@ export class Clipboard extends LifeCycleWatcher { return this._getJob().snapshotToBlock(snapshot, doc, parent, index); }; + pasteCellSliceSnapshot = async ( + snapshot: SliceSnapshot, + doc: Doc, + parent?: string, + index?: number + ) => { + const job = this._getJob(); + return job.snapshotToCellSlice(snapshot, doc, parent, index); + }; + registerAdapter = ( mimeType: string, adapter: AdapterConstructor, diff --git a/packages/framework/global/src/exceptions/code.ts b/packages/framework/global/src/exceptions/code.ts index bcfa95276e44..170a10c6d0a2 100644 --- a/packages/framework/global/src/exceptions/code.ts +++ b/packages/framework/global/src/exceptions/code.ts @@ -18,6 +18,7 @@ export enum ErrorCode { GfxBlockElementError, MissingViewModelError, DatabaseBlockError, + MicrosheetBlockError, ParsingError, UserAbortError, ExecutionError, diff --git a/packages/framework/inline/src/__tests__/utils.ts b/packages/framework/inline/src/__tests__/utils.ts index b6b8255d7091..df2715c78262 100644 --- a/packages/framework/inline/src/__tests__/utils.ts +++ b/packages/framework/inline/src/__tests__/utils.ts @@ -3,7 +3,7 @@ import { expect, type Page } from '@playwright/test'; import type { DeltaInsert, InlineEditor, InlineRange } from '../index.js'; const defaultPlaygroundURL = new URL( - `http://localhost:${process.env.CI ? 4173 : 5173}/` + `http://localhost:${process.env.CI ? 4173 : 8001}/` ); export async function type(page: Page, content: string) { diff --git a/packages/framework/store/src/transformer/job.ts b/packages/framework/store/src/transformer/job.ts index b771071a6911..1ab425c02d25 100644 --- a/packages/framework/store/src/transformer/job.ts +++ b/packages/framework/store/src/transformer/job.ts @@ -192,6 +192,68 @@ export class Job { } }; + snapshotToCellSlice = async ( + snapshot: SliceSnapshot, + doc: Doc, + parent?: string, + index?: number + ): Promise => { + SliceSnapshotSchema.parse(snapshot); + try { + const { + content, + // pageVersion, workspaceVersion, + workspaceId, + pageId, + } = snapshot; + + // Create a temporary root snapshot to encompass all content blocks + const tmpRootSnapshot: BlockSnapshot = { + id: 'temporary-root', + flavour: 'affine:cell', + props: {}, + type: 'block', + children: content, + }; + + for (const block of content) { + this._triggerBeforeImportEvent(block, parent, index); + } + const flatSnapshots: FlatSnapshot[] = []; + this._flattenSnapshot(tmpRootSnapshot, flatSnapshots, parent, index); + + const blockTree = await this._convertFlatSnapshots(flatSnapshots); + + await this._insertBlockTree(blockTree.children, doc, parent, index); + + const contentBlocks = blockTree.children + .map(tree => { + return doc.getBlockById(tree.draft.id); + }) + .filter(Boolean) as DraftModel[]; + + const slice = new Slice({ + content: contentBlocks, + // pageVersion, + // workspaceVersion, + workspaceId, + pageId, + }); + + this._slots.afterImport.emit({ + type: 'slice', + snapshot, + slice, + }); + + return slice; + } catch (error) { + console.error(`Error when transforming snapshot to slice:`); + console.error(error); + return; + } + }; + snapshotToDoc = async (snapshot: DocSnapshot): Promise => { try { this._slots.beforeImport.emit({ @@ -271,13 +333,14 @@ export class Job { } const flatSnapshots: FlatSnapshot[] = []; this._flattenSnapshot(tmpRootSnapshot, flatSnapshots, parent, index); - const blockTree = await this._convertFlatSnapshots(flatSnapshots); await this._insertBlockTree(blockTree.children, doc, parent, index); const contentBlocks = blockTree.children - .map(tree => doc.getBlockById(tree.draft.id)) + .map(tree => { + return doc.getBlockById(tree.draft.id); + }) .filter(Boolean) as DraftModel[]; const slice = new Slice({ @@ -425,6 +488,7 @@ export class Job { return { id: flat.snapshot.id, flavour: flat.snapshot.flavour, + // children: flat.snapshot.children, children: [], ...props, } as DraftModel; diff --git a/packages/playground/package.json b/packages/playground/package.json index 96acc00b6514..c50be36ac757 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -18,6 +18,7 @@ "@blocksuite/data-view": "workspace:*", "@blocksuite/global": "workspace:*", "@blocksuite/inline": "workspace:*", + "@blocksuite/microsheet-data-view": "workspace:*", "@blocksuite/presets": "workspace:*", "@blocksuite/store": "workspace:*", "@blocksuite/sync": "workspace:*", diff --git a/packages/playground/vite.config.ts b/packages/playground/vite.config.ts index 1b8e054fdc45..f24744d594c0 100644 --- a/packages/playground/vite.config.ts +++ b/packages/playground/vite.config.ts @@ -102,6 +102,7 @@ const chunkGroups = { require.resolve('@blocksuite/affine-block-paragraph'), require.resolve('@blocksuite/affine-block-surface'), require.resolve('@blocksuite/data-view'), + require.resolve('@blocksuite/microsheet-data-view'), ], datefns: [path.dirname(require.resolve('date-fns'))], dompurify: [path.dirname(require.resolve('dompurify'))], @@ -257,5 +258,8 @@ export default ({ mode }) => { }, }, }, + server: { + port: 8001, + }, }); }; diff --git a/tests/playwright.config.ts b/tests/playwright.config.ts index 728b25ad5bba..a28f1a34ce77 100644 --- a/tests/playwright.config.ts +++ b/tests/playwright.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ snapshotPathTemplate: 'snapshots/{testFilePath}/{arg}{ext}', webServer: { command: process.env.CI ? 'yarn run -T preview' : 'yarn run -T dev', - port: process.env.CI ? 4173 : 5173, + port: process.env.CI ? 4173 : 8001, reuseExistingServer: !process.env.CI, env: { COVERAGE: process.env.COVERAGE ?? '', diff --git a/tests/selection/native.spec.ts b/tests/selection/native.spec.ts index bd2a8163fa49..dbcb3e02de7e 100644 --- a/tests/selection/native.spec.ts +++ b/tests/selection/native.spec.ts @@ -289,6 +289,7 @@ test('cursor move to up and down with children block', async ({ page }) => { await page.keyboard.press('ArrowLeft'); } await page.keyboard.press('ArrowUp'); + await page.waitForTimeout(0); const indexTwo = await getInlineSelectionIndex(page); const textTwo = await getInlineSelectionText(page); expect(textTwo).toBe('arrow down test 1'); diff --git a/tests/slash-menu.spec.ts b/tests/slash-menu.spec.ts index 076c65cc21bf..36f96dc9686a 100644 --- a/tests/slash-menu.spec.ts +++ b/tests/slash-menu.spec.ts @@ -613,9 +613,12 @@ test.describe('slash search', () => { const slashItems = slashMenu.locator('icon-button'); await type(page, 'database'); - await expect(slashItems).toHaveCount(2); + await expect(slashItems).toHaveCount(3); await expect(slashItems.nth(0).locator('.text')).toHaveText(['Table View']); await expect(slashItems.nth(1).locator('.text')).toHaveText([ + 'Normal Table', + ]); + await expect(slashItems.nth(2).locator('.text')).toHaveText([ 'Kanban View', ]); await type(page, 'v'); diff --git a/tests/utils/actions/misc.ts b/tests/utils/actions/misc.ts index 208147c95a53..627cb309a550 100644 --- a/tests/utils/actions/misc.ts +++ b/tests/utils/actions/misc.ts @@ -36,7 +36,7 @@ declare global { } export const defaultPlaygroundURL = new URL( - `http://localhost:${process.env.CI ? 4173 : 5173}/starter/` + `http://localhost:${process.env.CI ? 4173 : 8001}/starter/` ); const NEXT_FRAME_TIMEOUT = 50; diff --git a/yarn.lock b/yarn.lock index 8dc13ec51be8..72af3595ba81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -48,145 +48,145 @@ __metadata: languageName: node linkType: hard -"@algolia/client-abtesting@npm:5.17.1": - version: 5.17.1 - resolution: "@algolia/client-abtesting@npm:5.17.1" +"@algolia/client-abtesting@npm:5.15.0": + version: 5.15.0 + resolution: "@algolia/client-abtesting@npm:5.15.0" dependencies: - "@algolia/client-common": "npm:5.17.1" - "@algolia/requester-browser-xhr": "npm:5.17.1" - "@algolia/requester-fetch": "npm:5.17.1" - "@algolia/requester-node-http": "npm:5.17.1" - checksum: 10/5860a040b0052a82d435a944d9615f5a2a377fb04a0ea20769db76f301629e68b921dba512d4b5b9b02da336454131f61c7fe70d5442077690834af2585d8890 + "@algolia/client-common": "npm:5.15.0" + "@algolia/requester-browser-xhr": "npm:5.15.0" + "@algolia/requester-fetch": "npm:5.15.0" + "@algolia/requester-node-http": "npm:5.15.0" + checksum: 10/07a2193043038b9ab2f61f23041de5f8bb72dcd10bd9d7b6e7dededba3911075fa2a8f7b2377c0d1d2c9b2494ae7bd0e2b39a70fc6499768e2be28323e8ee2ff languageName: node linkType: hard -"@algolia/client-analytics@npm:5.17.1": - version: 5.17.1 - resolution: "@algolia/client-analytics@npm:5.17.1" +"@algolia/client-analytics@npm:5.15.0": + version: 5.15.0 + resolution: "@algolia/client-analytics@npm:5.15.0" dependencies: - "@algolia/client-common": "npm:5.17.1" - "@algolia/requester-browser-xhr": "npm:5.17.1" - "@algolia/requester-fetch": "npm:5.17.1" - "@algolia/requester-node-http": "npm:5.17.1" - checksum: 10/012fbcdff361a04fe649fa21bd16b114a7bbb7ec677c190c75468e68d4ae15b5e5a4c6457a0e0882002c129b8e20dd8c824b771776266449f0f58cc6de12949f + "@algolia/client-common": "npm:5.15.0" + "@algolia/requester-browser-xhr": "npm:5.15.0" + "@algolia/requester-fetch": "npm:5.15.0" + "@algolia/requester-node-http": "npm:5.15.0" + checksum: 10/7284b412ab90e556480ea74adfa1347dfc895bc8c2e0fc913c5b726f27a3ae0c661dc0a856abb4b8bc5e25210981e63896ffabb16390703ad1facb71fe42f5f3 languageName: node linkType: hard -"@algolia/client-common@npm:5.17.1": - version: 5.17.1 - resolution: "@algolia/client-common@npm:5.17.1" - checksum: 10/ad7d4c97accb9a37853921c92191af5471a5f95caffa627ee66703a548eb4e8dad7bf3b390808cfd7b7e3178817662cfab99dce5df4e588e0302166b82be8bea +"@algolia/client-common@npm:5.15.0": + version: 5.15.0 + resolution: "@algolia/client-common@npm:5.15.0" + checksum: 10/75df9575861df14278beaf480edacfa87ce36e0fb16ecdded5f11f79799ac163435225b4632ddb09bdd0c81594692ebb1fe7fcdd731aef6702c6f1a4ba602dce languageName: node linkType: hard -"@algolia/client-insights@npm:5.17.1": - version: 5.17.1 - resolution: "@algolia/client-insights@npm:5.17.1" +"@algolia/client-insights@npm:5.15.0": + version: 5.15.0 + resolution: "@algolia/client-insights@npm:5.15.0" dependencies: - "@algolia/client-common": "npm:5.17.1" - "@algolia/requester-browser-xhr": "npm:5.17.1" - "@algolia/requester-fetch": "npm:5.17.1" - "@algolia/requester-node-http": "npm:5.17.1" - checksum: 10/f93983b1537ca824efe0ed11ad73f3773e5432b1f3a33ef9a78e382cba4f28f03c7001e345cbdd01a444f75d2f448a0077ab54b7885136cde09abf88acce1df4 + "@algolia/client-common": "npm:5.15.0" + "@algolia/requester-browser-xhr": "npm:5.15.0" + "@algolia/requester-fetch": "npm:5.15.0" + "@algolia/requester-node-http": "npm:5.15.0" + checksum: 10/be6c0f8b6297c0a58d35f66df4111546fed4c41c2d9182e2cdfb6552f1d5fe74ec2806d01ef14194072e427f90f1c22a5ae47ca662522723f860f42bf94f4720 languageName: node linkType: hard -"@algolia/client-personalization@npm:5.17.1": - version: 5.17.1 - resolution: "@algolia/client-personalization@npm:5.17.1" +"@algolia/client-personalization@npm:5.15.0": + version: 5.15.0 + resolution: "@algolia/client-personalization@npm:5.15.0" dependencies: - "@algolia/client-common": "npm:5.17.1" - "@algolia/requester-browser-xhr": "npm:5.17.1" - "@algolia/requester-fetch": "npm:5.17.1" - "@algolia/requester-node-http": "npm:5.17.1" - checksum: 10/9ddffd7e6369a84e7f2cef3da804b05cbb4cbe639b66033db9a29a3676b0fb502d32c0e5d78414ee31597cfa2d5a8bf65068d6362341f83df9bb29b7a6913fd2 + "@algolia/client-common": "npm:5.15.0" + "@algolia/requester-browser-xhr": "npm:5.15.0" + "@algolia/requester-fetch": "npm:5.15.0" + "@algolia/requester-node-http": "npm:5.15.0" + checksum: 10/b9ec040d9ae74a60ed6ac5b12a3ea0222c9459258c1ee58d81af2d41f9b4892120c855742b78e139fcc9fe96dec77bdbc0046948198ab042e29d23071342de96 languageName: node linkType: hard -"@algolia/client-query-suggestions@npm:5.17.1": - version: 5.17.1 - resolution: "@algolia/client-query-suggestions@npm:5.17.1" +"@algolia/client-query-suggestions@npm:5.15.0": + version: 5.15.0 + resolution: "@algolia/client-query-suggestions@npm:5.15.0" dependencies: - "@algolia/client-common": "npm:5.17.1" - "@algolia/requester-browser-xhr": "npm:5.17.1" - "@algolia/requester-fetch": "npm:5.17.1" - "@algolia/requester-node-http": "npm:5.17.1" - checksum: 10/40ae07f264c05498180a7d8f00cb3445b47f17cbf1a2111cb4302686ddc00c4781a91720f387a925c22e32addb6bdec0638e33bcc7f7194a2e709dc600cc1e68 + "@algolia/client-common": "npm:5.15.0" + "@algolia/requester-browser-xhr": "npm:5.15.0" + "@algolia/requester-fetch": "npm:5.15.0" + "@algolia/requester-node-http": "npm:5.15.0" + checksum: 10/15744211062cb52d412997370ae73e426df46eb5f9c632ab8b85ccbde4a89991a78a545367bd75579a7e0c0d157c239dcaa9a659693f66e85e428d972df5b236 languageName: node linkType: hard -"@algolia/client-search@npm:5.17.1": - version: 5.17.1 - resolution: "@algolia/client-search@npm:5.17.1" +"@algolia/client-search@npm:5.15.0": + version: 5.15.0 + resolution: "@algolia/client-search@npm:5.15.0" dependencies: - "@algolia/client-common": "npm:5.17.1" - "@algolia/requester-browser-xhr": "npm:5.17.1" - "@algolia/requester-fetch": "npm:5.17.1" - "@algolia/requester-node-http": "npm:5.17.1" - checksum: 10/a8f48988a4781cc3b0bb14f6a77b769e110a2b5884e0b1a7343aa65debf4a80ebe04bc037262e3a1502113f5653946686dee816d9150024e156968922018e10b + "@algolia/client-common": "npm:5.15.0" + "@algolia/requester-browser-xhr": "npm:5.15.0" + "@algolia/requester-fetch": "npm:5.15.0" + "@algolia/requester-node-http": "npm:5.15.0" + checksum: 10/d75305d35c1faa5497ad0dc9035cf2aa5025c04864f3073ccde3dc96dc1e53ece063e9668d4f1323637100393da521ccd97ab615938184d1d7c847d748a5db3d languageName: node linkType: hard -"@algolia/ingestion@npm:1.17.1": - version: 1.17.1 - resolution: "@algolia/ingestion@npm:1.17.1" +"@algolia/ingestion@npm:1.15.0": + version: 1.15.0 + resolution: "@algolia/ingestion@npm:1.15.0" dependencies: - "@algolia/client-common": "npm:5.17.1" - "@algolia/requester-browser-xhr": "npm:5.17.1" - "@algolia/requester-fetch": "npm:5.17.1" - "@algolia/requester-node-http": "npm:5.17.1" - checksum: 10/5ecae692566fbd5a9ec441bd5bdd53c7f6fdebc5e5712baf08b4cee68ff616d8f333533d8b0b8e874fe2eaeb7dbf7df8521afa1bef28b99a23fa3b303f15b105 + "@algolia/client-common": "npm:5.15.0" + "@algolia/requester-browser-xhr": "npm:5.15.0" + "@algolia/requester-fetch": "npm:5.15.0" + "@algolia/requester-node-http": "npm:5.15.0" + checksum: 10/ebea119d511ca0c47345f8da85840e7d222d1d5f531e64a60feeec35a296caff56c2a1cfc8f34bfd08d8495316c94894bfc041afe49bc61346fe1dc0072f0972 languageName: node linkType: hard -"@algolia/monitoring@npm:1.17.1": - version: 1.17.1 - resolution: "@algolia/monitoring@npm:1.17.1" +"@algolia/monitoring@npm:1.15.0": + version: 1.15.0 + resolution: "@algolia/monitoring@npm:1.15.0" dependencies: - "@algolia/client-common": "npm:5.17.1" - "@algolia/requester-browser-xhr": "npm:5.17.1" - "@algolia/requester-fetch": "npm:5.17.1" - "@algolia/requester-node-http": "npm:5.17.1" - checksum: 10/6766cf7de0e3d5cc5fbe534dc73b1494bf3e815ede8987fceb44d7360911d318a101c60740329786d039d10d6b016a6ce2ade50d3225f4f6b1e828a556b0127e + "@algolia/client-common": "npm:5.15.0" + "@algolia/requester-browser-xhr": "npm:5.15.0" + "@algolia/requester-fetch": "npm:5.15.0" + "@algolia/requester-node-http": "npm:5.15.0" + checksum: 10/3fba40fd78d49db7fc26d5dc4a6c9c6ac5992f0cd2ccca7d728e77e0878ffd8fe7182bdf90007bbf6cf15bed4442066513115ee259b68c66d3704c6e0ab9d92c languageName: node linkType: hard -"@algolia/recommend@npm:5.17.1": - version: 5.17.1 - resolution: "@algolia/recommend@npm:5.17.1" +"@algolia/recommend@npm:5.15.0": + version: 5.15.0 + resolution: "@algolia/recommend@npm:5.15.0" dependencies: - "@algolia/client-common": "npm:5.17.1" - "@algolia/requester-browser-xhr": "npm:5.17.1" - "@algolia/requester-fetch": "npm:5.17.1" - "@algolia/requester-node-http": "npm:5.17.1" - checksum: 10/e3ffbfe5cca01aafdf32e6dc44c28e43991c5d2fda2c07ea3821be1eaee4a9f9d8a01c73415794321655caaed73b2cc2e329d2a8668ffb183c052996575f93d7 + "@algolia/client-common": "npm:5.15.0" + "@algolia/requester-browser-xhr": "npm:5.15.0" + "@algolia/requester-fetch": "npm:5.15.0" + "@algolia/requester-node-http": "npm:5.15.0" + checksum: 10/f021544b7962d908264eb7acd90c4da93be424e2bdb83a7ddab2f7f651a1efcee146cefe43de462dfdf80b847aa9d587f412c8a32df2b3b3ed3f19115f652378 languageName: node linkType: hard -"@algolia/requester-browser-xhr@npm:5.17.1": - version: 5.17.1 - resolution: "@algolia/requester-browser-xhr@npm:5.17.1" +"@algolia/requester-browser-xhr@npm:5.15.0": + version: 5.15.0 + resolution: "@algolia/requester-browser-xhr@npm:5.15.0" dependencies: - "@algolia/client-common": "npm:5.17.1" - checksum: 10/b8bf3701e6a45b982e5e4792f7e1a071696d790cad689ebc1bdd83768fabc1f65617408659df2029ab4bc5a6014f91304a2f2571b8c15dcf8e6cb1c3d15af3e0 + "@algolia/client-common": "npm:5.15.0" + checksum: 10/7834318a601342eb09eb671ba69ad57c0deee4f336b544249aa29b0e04a766aa20af7b558578c34fa1b2ca3124e68c41132a90516322d2aafd379bdd73857462 languageName: node linkType: hard -"@algolia/requester-fetch@npm:5.17.1": - version: 5.17.1 - resolution: "@algolia/requester-fetch@npm:5.17.1" +"@algolia/requester-fetch@npm:5.15.0": + version: 5.15.0 + resolution: "@algolia/requester-fetch@npm:5.15.0" dependencies: - "@algolia/client-common": "npm:5.17.1" - checksum: 10/b7d51142ca5b92b16aaa8a96bd5280511301f05fc7ea719c15da61e7129391004b3bd1135c9f6bd19fdbb4845f66286449b483c1685a2077d5b16f7306540b92 + "@algolia/client-common": "npm:5.15.0" + checksum: 10/a86a4a614966c1b87f3010534adaf2e364c5be2fcd1aad66977fd11ca19b3d1912e7d4d7bef0b7afd3d15a127f1923d512321f49a4bf02a1a9284c874b65c1a9 languageName: node linkType: hard -"@algolia/requester-node-http@npm:5.17.1": - version: 5.17.1 - resolution: "@algolia/requester-node-http@npm:5.17.1" +"@algolia/requester-node-http@npm:5.15.0": + version: 5.15.0 + resolution: "@algolia/requester-node-http@npm:5.15.0" dependencies: - "@algolia/client-common": "npm:5.17.1" - checksum: 10/14c35e1f590a00250c262cef1177ff773ad912bdcf69d52a88e8de725dabcc68aa2eb15972137821ac27549475e641947000e3643caa659e94ff81280e727ec3 + "@algolia/client-common": "npm:5.15.0" + checksum: 10/62f477f781bbd1d63c14290363ad9ca0551b7fd5b52487a3a11b8308cdbd3e992a8b100ea82cec3ab6f4f2f79e7120760179c88a9171ceafab1e19903fe38d7d languageName: node linkType: hard @@ -200,7 +200,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.0, @babel/code-frame@npm:^7.26.2": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.0": version: 7.26.2 resolution: "@babel/code-frame@npm:7.26.2" dependencies: @@ -212,9 +212,9 @@ __metadata: linkType: hard "@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.25.9, @babel/compat-data@npm:^7.26.0": - version: 7.26.3 - resolution: "@babel/compat-data@npm:7.26.3" - checksum: 10/0bf4e491680722aa0eac26f770f2fae059f92e2ac083900b241c90a2c10f0fc80e448b1feccc2b332687fab4c3e33e9f83dee9ef56badca1fb9f3f71266d9ebf + version: 7.26.2 + resolution: "@babel/compat-data@npm:7.26.2" + checksum: 10/ed9eed6b62ce803ef4a320b1dac76b0302abbb29c49dddf96f3e3207d9717eb34e299a8651bb1582e9c3346ead74b6d595ffced5b3dae718afa08b18741f8402 languageName: node linkType: hard @@ -241,7 +241,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.26.0, @babel/generator@npm:^7.26.3": +"@babel/generator@npm:^7.25.9, @babel/generator@npm:^7.26.0": version: 7.26.3 resolution: "@babel/generator@npm:7.26.3" dependencies: @@ -263,6 +263,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-builder-binary-assignment-operator-visitor@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-builder-binary-assignment-operator-visitor@npm:7.25.9" + dependencies: + "@babel/traverse": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + checksum: 10/e1bb465b3b0155702d82cfef09e3813e87a6d777cdd2c513796861eac14953340491eafea1d4109278bf4ceb48b54074c45758f042c0544d00c498090bee5a6f + languageName: node + linkType: hard + "@babel/helper-compilation-targets@npm:^7.22.6, @babel/helper-compilation-targets@npm:^7.25.9": version: 7.25.9 resolution: "@babel/helper-compilation-targets@npm:7.25.9" @@ -294,15 +304,15 @@ __metadata: linkType: hard "@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.25.9": - version: 7.26.3 - resolution: "@babel/helper-create-regexp-features-plugin@npm:7.26.3" + version: 7.25.9 + resolution: "@babel/helper-create-regexp-features-plugin@npm:7.25.9" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.25.9" - regexpu-core: "npm:^6.2.0" + regexpu-core: "npm:^6.1.1" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/4c44122ea11c4253ee78a9c083b7fbce96c725e2cb43cc864f0e8ea2749f7b6658617239c6278df9f132d09a7545c8fe0336ed2895ad7c80c71507828a7bc8ba + checksum: 10/bc2b6a365ddf490c416661833dbf4430ae0c66132acccb5ce257e82026dd9db54da788bfbdcb7e0032aa0cba965cb1be169b1e1fb2c8c029b81625da4963f6b9 languageName: node linkType: hard @@ -396,6 +406,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-simple-access@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-simple-access@npm:7.25.9" + dependencies: + "@babel/traverse": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + checksum: 10/a16a6cfa5e8ac7144e856bcdaaf0022cf5de028fc0c56ce21dd664a6e900999a4285c587a209f2acf9de438c0d60bfb497f5f34aa34cbaf29da3e2f8d8d7feb7 + languageName: node + linkType: hard + "@babel/helper-skip-transparent-expression-wrappers@npm:^7.25.9": version: 7.25.9 resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.25.9" @@ -809,13 +829,14 @@ __metadata: linkType: hard "@babel/plugin-transform-exponentiation-operator@npm:^7.25.9": - version: 7.26.3 - resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.26.3" + version: 7.25.9 + resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.25.9" dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor": "npm:^7.25.9" "@babel/helper-plugin-utils": "npm:^7.25.9" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/0d8da2e552a50a775fe8e6e3c32621d20d3c5d1af7ab40ca2f5c7603de057b57b1b5850f74040e4ecbe36c09ac86d92173ad1e223a2a3b3df3cc359ca4349738 + checksum: 10/57e1bb4135dd16782fe84b49dd360cce8f9bf5f62eb10424dcdaf221e54a8bacdf50f2541c5ac01dea9f833a6c628613d71be915290938a93454389cba4de06b languageName: node linkType: hard @@ -924,14 +945,15 @@ __metadata: linkType: hard "@babel/plugin-transform-modules-commonjs@npm:^7.24.7, @babel/plugin-transform-modules-commonjs@npm:^7.25.9": - version: 7.26.3 - resolution: "@babel/plugin-transform-modules-commonjs@npm:7.26.3" + version: 7.25.9 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.25.9" dependencies: - "@babel/helper-module-transforms": "npm:^7.26.0" + "@babel/helper-module-transforms": "npm:^7.25.9" "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-simple-access": "npm:^7.25.9" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/f817f02fa04d13f1578f3026239b57f1003bebcf9f9b8d854714bed76a0e4986c79bd6d2e0ac14282c5d309454a8dab683c179709ca753b0152a69c69f3a78e3 + checksum: 10/a7390ca999373ccdef91075f274d1ace3a5cb79f9b9118ed6f76e94867ed454cf798a6f312ce2c4cdc1e035a25d810d754e4cb2e4d866acb4219490f3585de60 languageName: node linkType: hard @@ -1209,8 +1231,8 @@ __metadata: linkType: hard "@babel/plugin-transform-typescript@npm:^7.25.9": - version: 7.26.3 - resolution: "@babel/plugin-transform-typescript@npm:7.26.3" + version: 7.25.9 + resolution: "@babel/plugin-transform-typescript@npm:7.25.9" dependencies: "@babel/helper-annotate-as-pure": "npm:^7.25.9" "@babel/helper-create-class-features-plugin": "npm:^7.25.9" @@ -1219,7 +1241,7 @@ __metadata: "@babel/plugin-syntax-typescript": "npm:^7.25.9" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/71e82045fc931112ca6cba1826a7d521a30514ea5e8370c3c083f6ee1ed624d62d91e1415fbc41ce9033c4e78ba638a904c43b2d7e023873f36675844b8a4963 + checksum: 10/91e2ec805f89a813e0bf9cf42dffb767f798429e983af3e2f919885a2826b10f29223dd8b40ccc569eb61858d3273620e82e14431603a893e4a7f9b4c1a3a3cf languageName: node linkType: hard @@ -1426,17 +1448,17 @@ __metadata: linkType: hard "@babel/traverse@npm:^7.16.0, @babel/traverse@npm:^7.25.9": - version: 7.26.4 - resolution: "@babel/traverse@npm:7.26.4" + version: 7.25.9 + resolution: "@babel/traverse@npm:7.25.9" dependencies: - "@babel/code-frame": "npm:^7.26.2" - "@babel/generator": "npm:^7.26.3" - "@babel/parser": "npm:^7.26.3" + "@babel/code-frame": "npm:^7.25.9" + "@babel/generator": "npm:^7.25.9" + "@babel/parser": "npm:^7.25.9" "@babel/template": "npm:^7.25.9" - "@babel/types": "npm:^7.26.3" + "@babel/types": "npm:^7.25.9" debug: "npm:^4.3.1" globals: "npm:^11.1.0" - checksum: 10/30c81a80d66fc39842814bc2e847f4705d30f3859156f130d90a0334fe1d53aa81eed877320141a528ecbc36448acc0f14f544a7d410fa319d1c3ab63b50b58f + checksum: 10/7431614d76d4a053e429208db82f2846a415833f3d9eb2e11ef72eeb3c64dfd71f4a4d983de1a4a047b36165a1f5a64de8ca2a417534cc472005c740ffcb9c6a languageName: node linkType: hard @@ -1673,6 +1695,7 @@ __metadata: "@blocksuite/global": "workspace:*" "@blocksuite/icons": "npm:^2.1.75" "@blocksuite/inline": "workspace:*" + "@blocksuite/microsheet-data-view": "workspace:*" "@blocksuite/store": "workspace:*" "@floating-ui/dom": "npm:^1.6.10" "@lit/context": "npm:^1.1.2" @@ -1777,7 +1800,7 @@ __metadata: languageName: unknown linkType: soft -"@blocksuite/icons@npm:^2.1.75": +"@blocksuite/icons@npm:^2.1.68, @blocksuite/icons@npm:^2.1.75": version: 2.1.75 resolution: "@blocksuite/icons@npm:2.1.75" peerDependencies: @@ -1808,6 +1831,28 @@ __metadata: languageName: unknown linkType: soft +"@blocksuite/microsheet-data-view@workspace:*, @blocksuite/microsheet-data-view@workspace:packages/affine/microsheet-data-view": + version: 0.0.0-use.local + resolution: "@blocksuite/microsheet-data-view@workspace:packages/affine/microsheet-data-view" + dependencies: + "@blocksuite/affine-components": "workspace:*" + "@blocksuite/affine-shared": "workspace:*" + "@blocksuite/block-std": "workspace:*" + "@blocksuite/global": "workspace:*" + "@blocksuite/icons": "npm:^2.1.68" + "@blocksuite/store": "workspace:*" + "@floating-ui/dom": "npm:^1.6.10" + "@lit/context": "npm:^1.1.2" + "@preact/signals-core": "npm:^1.8.0" + "@toeverything/theme": "npm:^1.0.8" + "@types/sortablejs": "npm:^1.15.8" + date-fns: "npm:^4.0.0" + lit: "npm:^3.2.0" + sortablejs: "npm:^1.15.2" + zod: "npm:^3.23.8" + languageName: unknown + linkType: soft + "@blocksuite/playground@workspace:packages/playground": version: 0.0.0-use.local resolution: "@blocksuite/playground@workspace:packages/playground" @@ -1819,6 +1864,7 @@ __metadata: "@blocksuite/data-view": "workspace:*" "@blocksuite/global": "workspace:*" "@blocksuite/inline": "workspace:*" + "@blocksuite/microsheet-data-view": "workspace:*" "@blocksuite/presets": "workspace:*" "@blocksuite/store": "workspace:*" "@blocksuite/sync": "workspace:*" @@ -2244,8 +2290,8 @@ __metadata: linkType: hard "@codemirror/language@npm:^6.0.0, @codemirror/language@npm:^6.10.3, @codemirror/language@npm:^6.4.0, @codemirror/language@npm:^6.6.0": - version: 6.10.6 - resolution: "@codemirror/language@npm:6.10.6" + version: 6.10.4 + resolution: "@codemirror/language@npm:6.10.4" dependencies: "@codemirror/state": "npm:^6.0.0" "@codemirror/view": "npm:^6.23.0" @@ -2253,22 +2299,22 @@ __metadata: "@lezer/highlight": "npm:^1.0.0" "@lezer/lr": "npm:^1.0.0" style-mod: "npm:^4.0.0" - checksum: 10/7fc7019ae1ed956b40dd4c6bacd96558d4f5477a072d4ae73eace5faddf0c898cfa3bb75c8881a28f6e683179086a576a25e6d6c619c67b1de2d1ceb88287212 + checksum: 10/73b7404adc54ae7881b082ef10a678442b2f9d5a6d7077039ae86897beee7ca3dd748614e0360e5de4795a71d7c20b03ac967a20878c3b3266dc3ade560b4856 languageName: node linkType: hard "@codemirror/lint@npm:^6.0.0": - version: 6.8.4 - resolution: "@codemirror/lint@npm:6.8.4" + version: 6.8.3 + resolution: "@codemirror/lint@npm:6.8.3" dependencies: "@codemirror/state": "npm:^6.0.0" "@codemirror/view": "npm:^6.35.0" crelt: "npm:^1.0.5" - checksum: 10/401ead0591d88d31d1bf6527d4caba26e0deb7b49382dfbb8c712037d858047b0699fa2c15831a07db928194549eea9b942004fee42f334b34ff5973c7dbec58 + checksum: 10/9ec684f79d3b2d04ca1b9664dde9c6a72b8bba0a71c2fb7801c6e8c7f81d765980ebc97c6b86cff888d73c82a8bf58f2ae5ec9e25b454d86504db0af3c0b5f8b languageName: node linkType: hard -"@codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.4.0, @codemirror/state@npm:^6.4.1, @codemirror/state@npm:^6.5.0": +"@codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.4.0, @codemirror/state@npm:^6.4.1": version: 6.5.0 resolution: "@codemirror/state@npm:6.5.0" dependencies: @@ -2278,13 +2324,13 @@ __metadata: linkType: hard "@codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.34.1, @codemirror/view@npm:^6.35.0": - version: 6.35.3 - resolution: "@codemirror/view@npm:6.35.3" + version: 6.35.0 + resolution: "@codemirror/view@npm:6.35.0" dependencies: - "@codemirror/state": "npm:^6.5.0" + "@codemirror/state": "npm:^6.4.0" style-mod: "npm:^4.1.0" w3c-keyname: "npm:^2.2.4" - checksum: 10/d971df96cd8fa5336378b4780f986fcd73c0bf5c76bf8cc5f0212666ed582a1b55469808a455fac76afea339dfbae246acec96bed559527212ce4d1196cce578 + checksum: 10/edf9cb81cf2c5d80cc852f924d2d86299bf30aa009cf36afb370a8e2b4b0d03ceeda2de9a809c06aa8c06fcf1c8ba0d857c3d5292813808742446435d2ed533c languageName: node linkType: hard @@ -2320,19 +2366,19 @@ __metadata: linkType: hard "@commitlint/cli@npm:^19.4.0": - version: 19.6.1 - resolution: "@commitlint/cli@npm:19.6.1" + version: 19.6.0 + resolution: "@commitlint/cli@npm:19.6.0" dependencies: "@commitlint/format": "npm:^19.5.0" "@commitlint/lint": "npm:^19.6.0" - "@commitlint/load": "npm:^19.6.1" + "@commitlint/load": "npm:^19.5.0" "@commitlint/read": "npm:^19.5.0" "@commitlint/types": "npm:^19.5.0" tinyexec: "npm:^0.3.0" yargs: "npm:^17.0.0" bin: commitlint: cli.js - checksum: 10/b63b8a716d943419a0bb0542e752d380d62f2715266ae0bb53911f3fe749ae9c3bf0fd591abd87be6b863e522ca183c7bac6a82ef73255a5c5bbeceb620ecbb1 + checksum: 10/12ed3ad48de1b838c6603f39970b6fde6fd0594f86402e40cccb72940b4c9b1b1321e11606caffdf46b54ce8730c38970bc08e56b8878b9709c44ceb2a520bfa languageName: node linkType: hard @@ -2409,9 +2455,9 @@ __metadata: languageName: node linkType: hard -"@commitlint/load@npm:^19.6.1": - version: 19.6.1 - resolution: "@commitlint/load@npm:19.6.1" +"@commitlint/load@npm:^19.5.0": + version: 19.5.0 + resolution: "@commitlint/load@npm:19.5.0" dependencies: "@commitlint/config-validator": "npm:^19.5.0" "@commitlint/execute-rule": "npm:^19.5.0" @@ -2419,11 +2465,11 @@ __metadata: "@commitlint/types": "npm:^19.5.0" chalk: "npm:^5.3.0" cosmiconfig: "npm:^9.0.0" - cosmiconfig-typescript-loader: "npm:^6.1.0" + cosmiconfig-typescript-loader: "npm:^5.0.0" lodash.isplainobject: "npm:^4.0.6" lodash.merge: "npm:^4.6.2" lodash.uniq: "npm:^4.5.0" - checksum: 10/f340060751016de8e06f67137373f9ec51aff85ceb7ac8d5eec1bfb3693df38f7ad2c231fd1dd61acf58affb2f514761c12281328aacceaacef455d25d58c2ce + checksum: 10/87a9450c768632c09e9d98993752a5622aee698642eee5a9b31c3c48625455e043406b7ea6e02a8f41d86c524c9ecbdb9b823caf67da3048f0d96531177fda28 languageName: node linkType: hard @@ -3130,22 +3176,20 @@ __metadata: linkType: hard "@eslint/config-array@npm:^0.19.0": - version: 0.19.1 - resolution: "@eslint/config-array@npm:0.19.1" + version: 0.19.0 + resolution: "@eslint/config-array@npm:0.19.0" dependencies: - "@eslint/object-schema": "npm:^2.1.5" + "@eslint/object-schema": "npm:^2.1.4" debug: "npm:^4.3.1" minimatch: "npm:^3.1.2" - checksum: 10/1243b01f463de85c970c18f0994f9d1850dafe8cc8c910edb64105d845edd3cacaa0bbf028bf35a6daaf5a179021140b6a8b1dc7a2f915b42c2d35f022a9c201 + checksum: 10/16e4ec468ebcb10255ab8c61234c1b3e7ac5506016e432fb489a1c5528cace7a60ddb07515516e7fc166b1dbe6c407d8a6bfbaa2e7531d445d8feb845c989913 languageName: node linkType: hard "@eslint/core@npm:^0.9.0": - version: 0.9.1 - resolution: "@eslint/core@npm:0.9.1" - dependencies: - "@types/json-schema": "npm:^7.0.15" - checksum: 10/f2263f8f94fdf84fc34573e027de98f1fce6287120513ae672ddf0652c75b9fa77c314d565628fc58e0a6f959766acc34c8191f9b94f1757b910408ffa04adde + version: 0.9.0 + resolution: "@eslint/core@npm:0.9.0" + checksum: 10/2d11e9c6fac14cfa817c7a9939fd6b79f2120928e4933952d061651db93797e0fcd67c858a14980ac26e90f6e0e49051436aefa4a4b06a26f24e3028366f73d9 languageName: node linkType: hard @@ -3166,26 +3210,33 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:9.17.0, @eslint/js@npm:^9.9.1": +"@eslint/js@npm:9.15.0": + version: 9.15.0 + resolution: "@eslint/js@npm:9.15.0" + checksum: 10/cdea71574a8be164147f426ffa5eca05a9c7fbfbae98387ed0cf772292fc9fb5ded69ce96eac110aaa633f6b7504ec551e1d33f2d6690ae95b11ec395553bae1 + languageName: node + linkType: hard + +"@eslint/js@npm:^9.9.1": version: 9.17.0 resolution: "@eslint/js@npm:9.17.0" checksum: 10/1a89e62f5c50e75d44565b7f3b91701455a999132c991e10bac59c118fbb54bdd54be22b9bda1ac730f78a2e64604403d65ce5dd7726d80b2632982cfc3d84ac languageName: node linkType: hard -"@eslint/object-schema@npm:^2.1.5": - version: 2.1.5 - resolution: "@eslint/object-schema@npm:2.1.5" - checksum: 10/bb07ec53357047f20de923bcd61f0306d9eee83ef41daa32e633e154a44796b5bd94670169eccb8fd8cb4ff42228a43b86953a6321f789f98194baba8207b640 +"@eslint/object-schema@npm:^2.1.4": + version: 2.1.4 + resolution: "@eslint/object-schema@npm:2.1.4" + checksum: 10/221e8d9f281c605948cd6e030874aacce83fe097f8f9c1964787037bccf08e82b7aa9eff1850a30fffac43f1d76555727ec22a2af479d91e268e89d1e035131e languageName: node linkType: hard "@eslint/plugin-kit@npm:^0.2.3": - version: 0.2.4 - resolution: "@eslint/plugin-kit@npm:0.2.4" + version: 0.2.3 + resolution: "@eslint/plugin-kit@npm:0.2.3" dependencies: levn: "npm:^0.4.1" - checksum: 10/e34d02ea1dccd716e51369620263a4b2167aff3c0510ed776e21336cc3ad7158087449a76931baf07cdc33810cb6919db375f2e9f409435d2c6e0dd5f4786b25 + checksum: 10/0d0653ef840823fd5c0354ef8f1937e7763dbe830173eb6d2d55a19374bf04a06dff0e5214330c10a9425cf38655f632bb0d7d0666249b366e506ae291d82f7e languageName: node linkType: hard @@ -3281,20 +3332,20 @@ __metadata: linkType: hard "@inquirer/confirm@npm:^5.0.0": - version: 5.1.0 - resolution: "@inquirer/confirm@npm:5.1.0" + version: 5.0.2 + resolution: "@inquirer/confirm@npm:5.0.2" dependencies: - "@inquirer/core": "npm:^10.1.1" + "@inquirer/core": "npm:^10.1.0" "@inquirer/type": "npm:^3.0.1" peerDependencies: "@types/node": ">=18" - checksum: 10/b38187a61c4dd8f1784c6807dbef1022fb476fe36a7fa843b53abfac8919da7d63a1946ae0797b8471318c57aab8549ac3d54f1db1db559b79e4a0a3470f0931 + checksum: 10/4e775b80b689adeb0b2852ed79b368ef23a82fe3d5f580a562f4af7cdf002a19e0ec1b3b95acc6d49427a72c0fcb5b6548e0cdcafe2f0d3f3d6a923e04aabd0c languageName: node linkType: hard -"@inquirer/core@npm:^10.1.1": - version: 10.1.1 - resolution: "@inquirer/core@npm:10.1.1" +"@inquirer/core@npm:^10.1.0": + version: 10.1.0 + resolution: "@inquirer/core@npm:10.1.0" dependencies: "@inquirer/figures": "npm:^1.0.8" "@inquirer/type": "npm:^3.0.1" @@ -3305,7 +3356,7 @@ __metadata: strip-ansi: "npm:^6.0.1" wrap-ansi: "npm:^6.2.0" yoctocolors-cjs: "npm:^2.1.2" - checksum: 10/4dd9536967391b7bff0135fa81b350ed0c71cb3f9151aea1ea6107512136aa0153efbd4df253ec51656227963421f09d42c8c59773bd13f1ed92f8fabaf14ca3 + checksum: 10/5d097d0484c1b758f788b792d29395199bdc84af3e8cd4d9273e31de2c5202839b6edf299056956044ba7fb097c4cee7b5c0288e094a380c045082b044f9946e languageName: node linkType: hard @@ -3339,15 +3390,6 @@ __metadata: languageName: node linkType: hard -"@isaacs/fs-minipass@npm:^4.0.0": - version: 4.0.1 - resolution: "@isaacs/fs-minipass@npm:4.0.1" - dependencies: - minipass: "npm:^7.0.4" - checksum: 10/4412e9e6713c89c1e66d80bb0bb5a2a93192f10477623a27d08f228ba0316bb880affabc5bfe7f838f58a34d26c2c190da726e576cdfc18c49a72e89adabdcf5 - languageName: node - linkType: hard - "@istanbuljs/load-nyc-config@npm:^1.0.0, @istanbuljs/load-nyc-config@npm:^1.1.0": version: 1.1.0 resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" @@ -3378,13 +3420,13 @@ __metadata: linkType: hard "@jridgewell/gen-mapping@npm:^0.3.5": - version: 0.3.8 - resolution: "@jridgewell/gen-mapping@npm:0.3.8" + version: 0.3.5 + resolution: "@jridgewell/gen-mapping@npm:0.3.5" dependencies: "@jridgewell/set-array": "npm:^1.2.1" "@jridgewell/sourcemap-codec": "npm:^1.4.10" "@jridgewell/trace-mapping": "npm:^0.3.24" - checksum: 10/9d3a56ab3612ab9b85d38b2a93b87f3324f11c5130859957f6500e4ac8ce35f299d5ccc3ecd1ae87597601ecf83cee29e9afd04c18777c24011073992ff946df + checksum: 10/81587b3c4dd8e6c60252122937cea0c637486311f4ed208b52b62aae2e7a87598f63ec330e6cd0984af494bfb16d3f0d60d3b21d7e5b4aedd2602ff3fe9d32e2 languageName: node linkType: hard @@ -3494,13 +3536,13 @@ __metadata: linkType: hard "@lezer/javascript@npm:^1.0.0": - version: 1.4.21 - resolution: "@lezer/javascript@npm:1.4.21" + version: 1.4.19 + resolution: "@lezer/javascript@npm:1.4.19" dependencies: "@lezer/common": "npm:^1.2.0" "@lezer/highlight": "npm:^1.1.3" "@lezer/lr": "npm:^1.3.0" - checksum: 10/7f8b1f469103e74dc2c39e7e75e6cc670e4cf6f48b5317e2a0e267521c9924641e8de41c6e740af8cc919f5c7e03c0a97fc2f261486c96f1625c3e3bbb23b80a + checksum: 10/4f9811df15fd20797b4eaaac829016ba8d33ac327f21ef70e9e9a3fc35527bc3d5c2ded2dc3fb3405dc61145760fa6069d15cb73d572a2fe6b1984d8309baa18 languageName: node linkType: hard @@ -3598,8 +3640,8 @@ __metadata: linkType: hard "@mswjs/interceptors@npm:^0.37.0": - version: 0.37.3 - resolution: "@mswjs/interceptors@npm:0.37.3" + version: 0.37.1 + resolution: "@mswjs/interceptors@npm:0.37.1" dependencies: "@open-draft/deferred-promise": "npm:^2.2.0" "@open-draft/logger": "npm:^0.3.0" @@ -3607,7 +3649,7 @@ __metadata: is-node-process: "npm:^1.2.0" outvariant: "npm:^1.4.3" strict-event-emitter: "npm:^0.5.1" - checksum: 10/3d3e2e073feead8702c18dc97e5201785865292b32bd882c4d80461adc3380483b33517c55d7c6c1e53723f5e2ecf50cca0412e6ecd2eb771f4eaabfa2138932 + checksum: 10/332d8aa50beb4834ccbda6a800ca00b1204adc0eba23e1c1f7bb9f4e564a92707e563f7a2424d4a8607404ec91424e5d8c34a87c250b191ca7b24dff12eba2c5 languageName: node linkType: hard @@ -3667,25 +3709,25 @@ __metadata: languageName: node linkType: hard -"@npmcli/agent@npm:^3.0.0": - version: 3.0.0 - resolution: "@npmcli/agent@npm:3.0.0" +"@npmcli/agent@npm:^2.0.0": + version: 2.2.2 + resolution: "@npmcli/agent@npm:2.2.2" dependencies: agent-base: "npm:^7.1.0" http-proxy-agent: "npm:^7.0.0" https-proxy-agent: "npm:^7.0.1" lru-cache: "npm:^10.0.1" socks-proxy-agent: "npm:^8.0.3" - checksum: 10/775c9a7eb1f88c195dfb3bce70c31d0fe2a12b28b754e25c08a3edb4bc4816bfedb7ac64ef1e730579d078ca19dacf11630e99f8f3c3e0fd7b23caa5fd6d30a6 + checksum: 10/96fc0036b101bae5032dc2a4cd832efb815ce9b33f9ee2f29909ee49d96a0026b3565f73c507a69eb8603f5cb32e0ae45a70cab1e2655990a4e06ae99f7f572a languageName: node linkType: hard -"@npmcli/fs@npm:^4.0.0": - version: 4.0.0 - resolution: "@npmcli/fs@npm:4.0.0" +"@npmcli/fs@npm:^3.1.0": + version: 3.1.1 + resolution: "@npmcli/fs@npm:3.1.1" dependencies: semver: "npm:^7.3.5" - checksum: 10/405c4490e1ff11cf299775449a3c254a366a4b1ffc79d87159b0ee7d5558ac9f6a2f8c0735fd6ff3873cef014cb1a44a5f9127cb6a1b2dbc408718cca9365b5a + checksum: 10/1e0e04087049b24b38bc0b30d87a9388ee3ca1d3fdfc347c2f77d84fcfe6a51f250bc57ba2c1f614d7e4285c6c62bf8c769bc19aa0949ea39e5b043ee023b0bd languageName: node linkType: hard @@ -4031,135 +4073,128 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.28.1" +"@rollup/rollup-android-arm-eabi@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.27.4" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-android-arm64@npm:4.28.1" +"@rollup/rollup-android-arm64@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-android-arm64@npm:4.27.4" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-darwin-arm64@npm:4.28.1" +"@rollup/rollup-darwin-arm64@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-darwin-arm64@npm:4.27.4" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-darwin-x64@npm:4.28.1" +"@rollup/rollup-darwin-x64@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-darwin-x64@npm:4.27.4" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.28.1" +"@rollup/rollup-freebsd-arm64@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.27.4" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-freebsd-x64@npm:4.28.1" +"@rollup/rollup-freebsd-x64@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-freebsd-x64@npm:4.27.4" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.28.1" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.27.4" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.28.1" +"@rollup/rollup-linux-arm-musleabihf@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.27.4" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.28.1" +"@rollup/rollup-linux-arm64-gnu@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.27.4" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.28.1" +"@rollup/rollup-linux-arm64-musl@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.27.4" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loongarch64-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.28.1" - conditions: os=linux & cpu=loong64 & libc=glibc - languageName: node - linkType: hard - -"@rollup/rollup-linux-powerpc64le-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.28.1" +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.27.4" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.28.1" +"@rollup/rollup-linux-riscv64-gnu@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.27.4" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.28.1" +"@rollup/rollup-linux-s390x-gnu@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.27.4" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.28.1" +"@rollup/rollup-linux-x64-gnu@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.27.4" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.28.1" +"@rollup/rollup-linux-x64-musl@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.27.4" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.28.1" +"@rollup/rollup-win32-arm64-msvc@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.27.4" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.28.1" +"@rollup/rollup-win32-ia32-msvc@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.27.4" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.28.1": - version: 4.28.1 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.28.1" +"@rollup/rollup-win32-x64-msvc@npm:4.27.4": + version: 4.27.4 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.27.4" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -4323,15 +4358,15 @@ __metadata: linkType: hard "@stylistic/eslint-plugin-ts@npm:^2.4.0": - version: 2.12.1 - resolution: "@stylistic/eslint-plugin-ts@npm:2.12.1" + version: 2.11.0 + resolution: "@stylistic/eslint-plugin-ts@npm:2.11.0" dependencies: "@typescript-eslint/utils": "npm:^8.13.0" eslint-visitor-keys: "npm:^4.2.0" espree: "npm:^10.3.0" peerDependencies: eslint: ">=8.40.0" - checksum: 10/8633eed9ec775b11ffeaf3066fb01890374bd70bef1ea5838663f2304f5399e20bc609a95606cb4ef21d97f406c5bff32955e2281ded6178bee393a5cb7e24c5 + checksum: 10/2ddebe404a12607544e8b3d9d5b74efc5afbb4a46009eceebb121a2e508f1b14c17332c6970ae188ad300bd7ebbf5144851a4374d3acc5cbc8a440d8f5015896 languageName: node linkType: hard @@ -4373,90 +4408,90 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.10.1": - version: 1.10.1 - resolution: "@swc/core-darwin-arm64@npm:1.10.1" +"@swc/core-darwin-arm64@npm:1.9.3": + version: 1.9.3 + resolution: "@swc/core-darwin-arm64@npm:1.9.3" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.10.1": - version: 1.10.1 - resolution: "@swc/core-darwin-x64@npm:1.10.1" +"@swc/core-darwin-x64@npm:1.9.3": + version: 1.9.3 + resolution: "@swc/core-darwin-x64@npm:1.9.3" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.10.1": - version: 1.10.1 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.10.1" +"@swc/core-linux-arm-gnueabihf@npm:1.9.3": + version: 1.9.3 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.9.3" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.10.1": - version: 1.10.1 - resolution: "@swc/core-linux-arm64-gnu@npm:1.10.1" +"@swc/core-linux-arm64-gnu@npm:1.9.3": + version: 1.9.3 + resolution: "@swc/core-linux-arm64-gnu@npm:1.9.3" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.10.1": - version: 1.10.1 - resolution: "@swc/core-linux-arm64-musl@npm:1.10.1" +"@swc/core-linux-arm64-musl@npm:1.9.3": + version: 1.9.3 + resolution: "@swc/core-linux-arm64-musl@npm:1.9.3" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.10.1": - version: 1.10.1 - resolution: "@swc/core-linux-x64-gnu@npm:1.10.1" +"@swc/core-linux-x64-gnu@npm:1.9.3": + version: 1.9.3 + resolution: "@swc/core-linux-x64-gnu@npm:1.9.3" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.10.1": - version: 1.10.1 - resolution: "@swc/core-linux-x64-musl@npm:1.10.1" +"@swc/core-linux-x64-musl@npm:1.9.3": + version: 1.9.3 + resolution: "@swc/core-linux-x64-musl@npm:1.9.3" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.10.1": - version: 1.10.1 - resolution: "@swc/core-win32-arm64-msvc@npm:1.10.1" +"@swc/core-win32-arm64-msvc@npm:1.9.3": + version: 1.9.3 + resolution: "@swc/core-win32-arm64-msvc@npm:1.9.3" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.10.1": - version: 1.10.1 - resolution: "@swc/core-win32-ia32-msvc@npm:1.10.1" +"@swc/core-win32-ia32-msvc@npm:1.9.3": + version: 1.9.3 + resolution: "@swc/core-win32-ia32-msvc@npm:1.9.3" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.10.1": - version: 1.10.1 - resolution: "@swc/core-win32-x64-msvc@npm:1.10.1" +"@swc/core-win32-x64-msvc@npm:1.9.3": + version: 1.9.3 + resolution: "@swc/core-win32-x64-msvc@npm:1.9.3" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@swc/core@npm:^1.7.14": - version: 1.10.1 - resolution: "@swc/core@npm:1.10.1" - dependencies: - "@swc/core-darwin-arm64": "npm:1.10.1" - "@swc/core-darwin-x64": "npm:1.10.1" - "@swc/core-linux-arm-gnueabihf": "npm:1.10.1" - "@swc/core-linux-arm64-gnu": "npm:1.10.1" - "@swc/core-linux-arm64-musl": "npm:1.10.1" - "@swc/core-linux-x64-gnu": "npm:1.10.1" - "@swc/core-linux-x64-musl": "npm:1.10.1" - "@swc/core-win32-arm64-msvc": "npm:1.10.1" - "@swc/core-win32-ia32-msvc": "npm:1.10.1" - "@swc/core-win32-x64-msvc": "npm:1.10.1" + version: 1.9.3 + resolution: "@swc/core@npm:1.9.3" + dependencies: + "@swc/core-darwin-arm64": "npm:1.9.3" + "@swc/core-darwin-x64": "npm:1.9.3" + "@swc/core-linux-arm-gnueabihf": "npm:1.9.3" + "@swc/core-linux-arm64-gnu": "npm:1.9.3" + "@swc/core-linux-arm64-musl": "npm:1.9.3" + "@swc/core-linux-x64-gnu": "npm:1.9.3" + "@swc/core-linux-x64-musl": "npm:1.9.3" + "@swc/core-win32-arm64-msvc": "npm:1.9.3" + "@swc/core-win32-ia32-msvc": "npm:1.9.3" + "@swc/core-win32-x64-msvc": "npm:1.9.3" "@swc/counter": "npm:^0.1.3" "@swc/types": "npm:^0.1.17" peerDependencies: @@ -4485,7 +4520,7 @@ __metadata: peerDependenciesMeta: "@swc/helpers": optional: true - checksum: 10/e9c8a42488db77b3c199d1696d45bb9c7e917267e810c091e5161a7096f3db9c01813ade1e942290285210b4590ec7a00399b2ba3edc4d8cae9b2e824fac7d45 + checksum: 10/0a95ce8a2d21370c82e2b0e744c30eacdbd709a7b470950786f3c25a6272c0aa079206a3543aaccc022ca98af87a2a5536387a0259b5377e94d34fac28143cd0 languageName: node linkType: hard @@ -4563,7 +4598,7 @@ __metadata: languageName: node linkType: hard -"@toeverything/theme@npm:^1.1.1": +"@toeverything/theme@npm:^1.0.8, @toeverything/theme@npm:^1.1.1": version: 1.1.1 resolution: "@toeverything/theme@npm:1.1.1" checksum: 10/a4df493ec8c43312d2b0caa5ec0fa296732042e169ffaef57e115c44f82ca13cb75679c95cf3ba12fc25c73a9dda8a6331196d145acb27445e08e0eafd7fc78a @@ -4590,38 +4625,38 @@ __metadata: languageName: node linkType: hard -"@ts-graphviz/adapter@npm:^2.0.6": - version: 2.0.6 - resolution: "@ts-graphviz/adapter@npm:2.0.6" +"@ts-graphviz/adapter@npm:^2.0.5": + version: 2.0.5 + resolution: "@ts-graphviz/adapter@npm:2.0.5" dependencies: - "@ts-graphviz/common": "npm:^2.1.5" - checksum: 10/6f15c14a166126e29dddb13f2ad012d8ac4d883e6fa789263b05ee50e0b47f2c09cf4c7f09b549e711563c30d6a6c2e45b52df0fd2701db2953dff112c0e9ea6 + "@ts-graphviz/common": "npm:^2.1.4" + checksum: 10/1af46f5908ab1bc8f13f5e478722e3f18718f060ab122c81e6f9544184b584f9e2b1324a2a4766ebff90dab272b85a621b3cccdb8c88c480f264deda3eff6560 languageName: node linkType: hard -"@ts-graphviz/ast@npm:^2.0.6": - version: 2.0.6 - resolution: "@ts-graphviz/ast@npm:2.0.6" +"@ts-graphviz/ast@npm:^2.0.5": + version: 2.0.5 + resolution: "@ts-graphviz/ast@npm:2.0.5" dependencies: - "@ts-graphviz/common": "npm:^2.1.5" - checksum: 10/b1d6d57e286aad2a6f711895e9b6c6a90ce7b4e019d307883a82a541bd06dd8e72b62cc76577ac016f614feb65bdcd417350e53170f77dc7f5f233b0b61734a0 + "@ts-graphviz/common": "npm:^2.1.4" + checksum: 10/57fac48dbb18d0ba23652547aa03dc0ce79d8834ea371f0fb0bb6c2557bb4efe1143ce1486f76b41cc932c9a42086abb49132a0f224ecf9116f1a814b5b4e69e languageName: node linkType: hard -"@ts-graphviz/common@npm:^2.1.5": - version: 2.1.5 - resolution: "@ts-graphviz/common@npm:2.1.5" - checksum: 10/421ea1a0d32917d6f26bceaf7b8e99db9f405afd3bbbb83bb9d2c759ee4bdcf15d5e611b3590becf55615f0ff35a9766fb19268c8c01c3dee0b02a74d0ef1a27 +"@ts-graphviz/common@npm:^2.1.4": + version: 2.1.4 + resolution: "@ts-graphviz/common@npm:2.1.4" + checksum: 10/246f92db0da3070f365c757a71edc6f5a6405065835afb4a4bc0813d8408440b368418a52f3280f5f1b176d0ca0609e0428bf0397136312736aeb8a6ae409f2b languageName: node linkType: hard -"@ts-graphviz/core@npm:^2.0.6": - version: 2.0.6 - resolution: "@ts-graphviz/core@npm:2.0.6" +"@ts-graphviz/core@npm:^2.0.5": + version: 2.0.5 + resolution: "@ts-graphviz/core@npm:2.0.5" dependencies: - "@ts-graphviz/ast": "npm:^2.0.6" - "@ts-graphviz/common": "npm:^2.1.5" - checksum: 10/1545d709db656972e568672519b147b35441f259564c8042d3d0297ca2c6d36e7d343cfc4dceaa02dc69f178fdd5f8eabbb2c25a87c7e9198ea37864837c2101 + "@ts-graphviz/ast": "npm:^2.0.5" + "@ts-graphviz/common": "npm:^2.1.4" + checksum: 10/d526c2e370200e46b37cb9ded5a971fc72632c7d1070158b2c596b5b4a8b1bc08038a6f09425f45742758d68cdff801c182d33ce445b7ab95f9dcaf6f1284de7 languageName: node linkType: hard @@ -4990,11 +5025,11 @@ __metadata: linkType: hard "@types/node@npm:*, @types/node@npm:^22.5.0": - version: 22.10.2 - resolution: "@types/node@npm:22.10.2" + version: 22.10.0 + resolution: "@types/node@npm:22.10.0" dependencies: undici-types: "npm:~6.20.0" - checksum: 10/451adfefed4add58b069407173e616220fd4aaa3307cdde1bb701aa053b65b54ced8483db2f870dcedec7a58cb3b06101fbc19d85852716672ec1fd3660947fa + checksum: 10/e2561e15eaac6261cf828cd7dcd4882a5ce2e10104db4123566b00418acf30e18518e39c7a906682f44118b420c555cc774ac3e0e6f80e37f55884cfe6cf1f78 languageName: node linkType: hard @@ -5061,6 +5096,13 @@ __metadata: languageName: node linkType: hard +"@types/sortablejs@npm:^1.15.8": + version: 1.15.8 + resolution: "@types/sortablejs@npm:1.15.8" + checksum: 10/aea58b08cf45f5e9633707a8df0df1212595c731bbdfd29805487138fdd0d8c51fa5c741999738a645c1e801d43a92ba0d3fb5b45625b52e247c56588aef6c55 + languageName: node + linkType: hard + "@types/statuses@npm:^2.0.4": version: 2.0.5 resolution: "@types/statuses@npm:2.0.5" @@ -5106,14 +5148,14 @@ __metadata: linkType: hard "@typescript-eslint/eslint-plugin@npm:^8.0.0": - version: 8.18.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.18.0" + version: 8.16.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.16.0" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:8.18.0" - "@typescript-eslint/type-utils": "npm:8.18.0" - "@typescript-eslint/utils": "npm:8.18.0" - "@typescript-eslint/visitor-keys": "npm:8.18.0" + "@typescript-eslint/scope-manager": "npm:8.16.0" + "@typescript-eslint/type-utils": "npm:8.16.0" + "@typescript-eslint/utils": "npm:8.16.0" + "@typescript-eslint/visitor-keys": "npm:8.16.0" graphemer: "npm:^1.4.0" ignore: "npm:^5.3.1" natural-compare: "npm:^1.4.0" @@ -5121,28 +5163,42 @@ __metadata: peerDependencies: "@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.8.0" - checksum: 10/fc163212ab626b8880bcc6c166da6e1c907c1e9eac720a217e58bec64af3866dc18e990a15a7dcd9593643f390d921625a89fb235a7e126fbb0a2f52e4abf0f5 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/aa3d551d4f09940eee0c08328cb0db3a2391a8bba6d044f6bb38c51ac864896519c647d4b8fd99f7c094cc677bcf22454b27322014a08b2f2fb25695a43820db languageName: node linkType: hard "@typescript-eslint/parser@npm:^8.0.0": - version: 8.18.0 - resolution: "@typescript-eslint/parser@npm:8.18.0" + version: 8.16.0 + resolution: "@typescript-eslint/parser@npm:8.16.0" dependencies: - "@typescript-eslint/scope-manager": "npm:8.18.0" - "@typescript-eslint/types": "npm:8.18.0" - "@typescript-eslint/typescript-estree": "npm:8.18.0" - "@typescript-eslint/visitor-keys": "npm:8.18.0" + "@typescript-eslint/scope-manager": "npm:8.16.0" + "@typescript-eslint/types": "npm:8.16.0" + "@typescript-eslint/typescript-estree": "npm:8.16.0" + "@typescript-eslint/visitor-keys": "npm:8.16.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.8.0" - checksum: 10/5f4a1c431868ee677a6a1f55197c26c5c6e528a07fd8d8dee3648697c3617343693709c9f77cba86f8bdc1738c5727f5badfd3a9745f0e0719edb77fd0c01ba3 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/ac1e2bfdbfe212da470bb17915b5228f7a6b027332b05eb8bcbbad440a81b2476c649e54e232084838e1edc005e6d7dc7a44899587d73672dd3d5484d9dbf9f8 + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/scope-manager@npm:8.16.0" + dependencies: + "@typescript-eslint/types": "npm:8.16.0" + "@typescript-eslint/visitor-keys": "npm:8.16.0" + checksum: 10/e0aea61f248b39049d4ce21c19f9c8af1a8024f4f92abc8c1d5b79ea65b013c6c4ff41efb92995050036aa95b6a705601917b56809d9ec1fbbab387054aeb269 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.18.0, @typescript-eslint/scope-manager@npm:^8.1.0": +"@typescript-eslint/scope-manager@npm:8.18.0": version: 8.18.0 resolution: "@typescript-eslint/scope-manager@npm:8.18.0" dependencies: @@ -5152,18 +5208,20 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.18.0": - version: 8.18.0 - resolution: "@typescript-eslint/type-utils@npm:8.18.0" +"@typescript-eslint/type-utils@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/type-utils@npm:8.16.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:8.18.0" - "@typescript-eslint/utils": "npm:8.18.0" + "@typescript-eslint/typescript-estree": "npm:8.16.0" + "@typescript-eslint/utils": "npm:8.16.0" debug: "npm:^4.3.4" ts-api-utils: "npm:^1.3.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.8.0" - checksum: 10/d857a0b6a52aad10dfd51465b8fc667f579c4a590e7fedd372f834abd2fb438186e2ebc25b61f8a5e4a90d40ebdf614367088d73ec7fe5ac0e8c9dc47ae02258 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/b91f6cef6af7e4f82a1dba9622d5ec9f46d1983eecfb88a1adbd310c7f980fedf5c8a198bfe968aae59fc386e4c437f55a7533988252eb9cbb0bdac8321e3dba languageName: node linkType: hard @@ -5174,6 +5232,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/types@npm:8.16.0" + checksum: 10/b37b26cd0e45b0cd6f7d492a07af583e4877d798495ab5fc1cfacb3c561b6d7981e3166f0475bb997e6c6d56ef903e160895174c7e63c08322dbb42d026cf7dc + languageName: node + linkType: hard + "@typescript-eslint/types@npm:8.18.0, @typescript-eslint/types@npm:^8.9.0": version: 8.18.0 resolution: "@typescript-eslint/types@npm:8.18.0" @@ -5181,6 +5246,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.16.0" + dependencies: + "@typescript-eslint/types": "npm:8.16.0" + "@typescript-eslint/visitor-keys": "npm:8.16.0" + debug: "npm:^4.3.4" + fast-glob: "npm:^3.3.2" + is-glob: "npm:^4.0.3" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^1.3.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/823cf55d331cf7283547a2860a5d7bfd7dbd497be6e87b226dd7456b36db214de1504855afbbaef8d89932c11a1e589d4cb2a4093b6f1c542a4ce8319d988006 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:8.18.0": version: 8.18.0 resolution: "@typescript-eslint/typescript-estree@npm:8.18.0" @@ -5218,7 +5302,24 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.18.0, @typescript-eslint/utils@npm:^8.1.0, @typescript-eslint/utils@npm:^8.13.0, @typescript-eslint/utils@npm:^8.9.0": +"@typescript-eslint/utils@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/utils@npm:8.16.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:8.16.0" + "@typescript-eslint/types": "npm:8.16.0" + "@typescript-eslint/typescript-estree": "npm:8.16.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/80ba35b97a8e80ac2b54a56ac041b4f4583328d764e1693e7d3750de383cbcefcb7e838b75e550e8aa4df446f4b41460da6dc83543517280a4e3a61546c1a8dc + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:^8.1.0, @typescript-eslint/utils@npm:^8.13.0, @typescript-eslint/utils@npm:^8.9.0": version: 8.18.0 resolution: "@typescript-eslint/utils@npm:8.18.0" dependencies: @@ -5243,6 +5344,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.16.0": + version: 8.16.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.16.0" + dependencies: + "@typescript-eslint/types": "npm:8.16.0" + eslint-visitor-keys: "npm:^4.2.0" + checksum: 10/e3f231a3e8ca2f7a3dc0e9ebdc3ea1f51a377b1285727413b4c89c44dbfaf342f2574b1b4e7f478f295963045a6058e27b4827816fe2a5a2d09f565eb68522c7 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:8.18.0": version: 8.18.0 resolution: "@typescript-eslint/visitor-keys@npm:8.18.0" @@ -5254,19 +5365,19 @@ __metadata: linkType: hard "@ungap/structured-clone@npm:^1.0.0": - version: 1.2.1 - resolution: "@ungap/structured-clone@npm:1.2.1" - checksum: 10/6770f71e8183311b2871601ddb02d62a26373be7cf2950cb546a345a2305c75b502e36ce80166120aa2f5f1ea1562141684651ebbfcc711c58acd32035d3e545 + version: 1.2.0 + resolution: "@ungap/structured-clone@npm:1.2.0" + checksum: 10/c6fe89a505e513a7592e1438280db1c075764793a2397877ff1351721fe8792a966a5359769e30242b3cd023f2efb9e63ca2ca88019d73b564488cc20e3eab12 languageName: node linkType: hard "@vitejs/plugin-vue@npm:^5.1.4": - version: 5.2.1 - resolution: "@vitejs/plugin-vue@npm:5.2.1" + version: 5.2.0 + resolution: "@vitejs/plugin-vue@npm:5.2.0" peerDependencies: - vite: ^5.0.0 || ^6.0.0 + vite: ^5.0.0 vue: ^3.2.25 - checksum: 10/60edb926bf919aebe5ef527402bbb84902a23bcba57ea718285e4d700abf9718bc9411806440dc7e8ea0cef06d93c0078e2d456137b4eb3fd70d17288e9db081 + checksum: 10/536ba28cca29bdecdc0032a08401699030fa9fa541035a8f409a6f658aa440167928ff9eb99abc2db54e4f449d120d86c0761c1f75aa11df659dc78e03ad9dc4 languageName: node linkType: hard @@ -5417,9 +5528,9 @@ __metadata: linkType: hard "@vscode/web-custom-data@npm:^0.4.2": - version: 0.4.13 - resolution: "@vscode/web-custom-data@npm:0.4.13" - checksum: 10/b2443a7745004bf2722bcf99e0fe423b1f8c529fa2a71b0784671ddd1a23462b9aa45d9af02132ca63691c915f72567419a25ef3085e33745eba18514d7839c1 + version: 0.4.12 + resolution: "@vscode/web-custom-data@npm:0.4.12" + checksum: 10/b002f1266f2aabec36446a1cc14dc3881c0a28685ee6ec9b023888858638b53c9a6c95c6dadf9cbab0c1c7d7b2a1ec055850106dfb506a43a2e3f89059b93b37 languageName: node linkType: hard @@ -5769,7 +5880,7 @@ __metadata: languageName: node linkType: hard -"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": version: 7.1.3 resolution: "agent-base@npm:7.1.3" checksum: 10/3db6d8d4651f2aa1a9e4af35b96ab11a7607af57a24f3bc721a387eaa3b5f674e901f0a648b0caefd48f3fd117c7761b79a3b55854e2aebaa96c3f32cf76af84 @@ -5811,23 +5922,23 @@ __metadata: linkType: hard "algoliasearch@npm:^5.12.0": - version: 5.17.1 - resolution: "algoliasearch@npm:5.17.1" - dependencies: - "@algolia/client-abtesting": "npm:5.17.1" - "@algolia/client-analytics": "npm:5.17.1" - "@algolia/client-common": "npm:5.17.1" - "@algolia/client-insights": "npm:5.17.1" - "@algolia/client-personalization": "npm:5.17.1" - "@algolia/client-query-suggestions": "npm:5.17.1" - "@algolia/client-search": "npm:5.17.1" - "@algolia/ingestion": "npm:1.17.1" - "@algolia/monitoring": "npm:1.17.1" - "@algolia/recommend": "npm:5.17.1" - "@algolia/requester-browser-xhr": "npm:5.17.1" - "@algolia/requester-fetch": "npm:5.17.1" - "@algolia/requester-node-http": "npm:5.17.1" - checksum: 10/d4914b469ef2b5c6aa5a7be565fc29f136c61166879d5beba234109a58224f11428a5a6509e939c2a31acc91f3deda1b90d2289f6d8b7b72124ee6cb46a22f43 + version: 5.15.0 + resolution: "algoliasearch@npm:5.15.0" + dependencies: + "@algolia/client-abtesting": "npm:5.15.0" + "@algolia/client-analytics": "npm:5.15.0" + "@algolia/client-common": "npm:5.15.0" + "@algolia/client-insights": "npm:5.15.0" + "@algolia/client-personalization": "npm:5.15.0" + "@algolia/client-query-suggestions": "npm:5.15.0" + "@algolia/client-search": "npm:5.15.0" + "@algolia/ingestion": "npm:1.15.0" + "@algolia/monitoring": "npm:1.15.0" + "@algolia/recommend": "npm:5.15.0" + "@algolia/requester-browser-xhr": "npm:5.15.0" + "@algolia/requester-fetch": "npm:5.15.0" + "@algolia/requester-node-http": "npm:5.15.0" + checksum: 10/41a6d3d9f5676fe814e4040b989eed906a7e333381a260ee1f77c4618b0402129762ad38c59e73b9492e803114fa0ae650769a9084d0fd29bf0a582486c9f0d6 languageName: node linkType: hard @@ -6057,13 +6168,13 @@ __metadata: linkType: hard "axios@npm:^1.7.4": - version: 1.7.9 - resolution: "axios@npm:1.7.9" + version: 1.7.8 + resolution: "axios@npm:1.7.8" dependencies: follow-redirects: "npm:^1.15.6" form-data: "npm:^4.0.0" proxy-from-env: "npm:^1.1.0" - checksum: 10/b7a5f660ea53ba9c2a745bf5ad77ad8bf4f1338e13ccc3f9f09f810267d6c638c03dac88b55dae8dc98b79c57d2d6835be651d58d2af97c174f43d289a9fd007 + checksum: 10/7ddcde188041ac55090186254b4025eb2af842be3cf615ce45393fd7f543c1eab0ad2fdd2017a5f6190695e3ecea73ee5e9c37f204854aec2698f9579046efdf languageName: node linkType: hard @@ -6298,16 +6409,16 @@ __metadata: linkType: hard "browserslist@npm:^4.24.0, browserslist@npm:^4.24.2": - version: 4.24.3 - resolution: "browserslist@npm:4.24.3" + version: 4.24.2 + resolution: "browserslist@npm:4.24.2" dependencies: - caniuse-lite: "npm:^1.0.30001688" - electron-to-chromium: "npm:^1.5.73" - node-releases: "npm:^2.0.19" + caniuse-lite: "npm:^1.0.30001669" + electron-to-chromium: "npm:^1.5.41" + node-releases: "npm:^2.0.18" update-browserslist-db: "npm:^1.1.1" bin: browserslist: cli.js - checksum: 10/f5b22757302a4c04036c4ed82ef82d8005c15b809fa006132765f306e8d8a5c02703479f6738db6640f27c0935ebecde4fa5ae3457fc7ad4805156430dba6bc7 + checksum: 10/f8a9d78bbabe466c57ffd5c50a9e5582a5df9aa68f43078ca62a9f6d0d6c70ba72eca72d0a574dbf177cf55cdca85a46f7eb474917a47ae5398c66f8b76f7d1c languageName: node linkType: hard @@ -6368,11 +6479,11 @@ __metadata: languageName: node linkType: hard -"cacache@npm:^19.0.1": - version: 19.0.1 - resolution: "cacache@npm:19.0.1" +"cacache@npm:^18.0.0": + version: 18.0.4 + resolution: "cacache@npm:18.0.4" dependencies: - "@npmcli/fs": "npm:^4.0.0" + "@npmcli/fs": "npm:^3.1.0" fs-minipass: "npm:^3.0.0" glob: "npm:^10.2.2" lru-cache: "npm:^10.0.1" @@ -6380,11 +6491,11 @@ __metadata: minipass-collect: "npm:^2.0.1" minipass-flush: "npm:^1.0.5" minipass-pipeline: "npm:^1.2.4" - p-map: "npm:^7.0.2" - ssri: "npm:^12.0.0" - tar: "npm:^7.4.3" - unique-filename: "npm:^4.0.0" - checksum: 10/ea026b27b13656330c2bbaa462a88181dcaa0435c1c2e705db89b31d9bdf7126049d6d0445ba746dca21454a0cfdf1d6f47fd39d34c8c8435296b30bc5738a13 + p-map: "npm:^4.0.0" + ssri: "npm:^10.0.0" + tar: "npm:^6.1.11" + unique-filename: "npm:^3.0.0" + checksum: 10/ca2f7b2d3003f84d362da9580b5561058ccaecd46cba661cbcff0375c90734b610520d46b472a339fd032d91597ad6ed12dde8af81571197f3c9772b5d35b104 languageName: node linkType: hard @@ -6424,10 +6535,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.30001688": - version: 1.0.30001688 - resolution: "caniuse-lite@npm:1.0.30001688" - checksum: 10/2125e900af866ee211c66beca01220c98e72c8a91d25c87b8ab456d3916f56fb1be5feef72556bca746da7aa852fc0118a04669f5ec2e6511eb77c960479e1c0 +"caniuse-lite@npm:^1.0.30001669": + version: 1.0.30001684 + resolution: "caniuse-lite@npm:1.0.30001684" + checksum: 10/35dd0941dd32319c87409441e8400faea32114c4a74938c29262a613160d2890a4f57902e24c770f076dbd0b85c4442aa135f9f641d4a74a9246fe624e6f780a languageName: node linkType: hard @@ -6542,10 +6653,10 @@ __metadata: languageName: node linkType: hard -"chownr@npm:^3.0.0": - version: 3.0.0 - resolution: "chownr@npm:3.0.0" - checksum: 10/b63cb1f73d171d140a2ed8154ee6566c8ab775d3196b0e03a2a94b5f6a0ce7777ee5685ca56849403c8d17bd457a6540672f9a60696a6137c7a409097495b82c +"chownr@npm:^2.0.0": + version: 2.0.0 + resolution: "chownr@npm:2.0.0" + checksum: 10/c57cf9dd0791e2f18a5ee9c1a299ae6e801ff58fee96dc8bfd0dcb4738a6ce58dd252a3605b1c93c6418fe4f9d5093b28ffbf4d66648cb2a9c67eaef9679be2f languageName: node linkType: hard @@ -6926,16 +7037,16 @@ __metadata: languageName: node linkType: hard -"cosmiconfig-typescript-loader@npm:^6.1.0": - version: 6.1.0 - resolution: "cosmiconfig-typescript-loader@npm:6.1.0" +"cosmiconfig-typescript-loader@npm:^5.0.0": + version: 5.1.0 + resolution: "cosmiconfig-typescript-loader@npm:5.1.0" dependencies: - jiti: "npm:^2.4.1" + jiti: "npm:^1.21.6" peerDependencies: "@types/node": "*" - cosmiconfig: ">=9" - typescript: ">=5" - checksum: 10/e8b28b08759753c46a991e3d4db675480ea0081da9c098e426a89f4a12395e448c3090536d1ec1cb7adb5d7beb0ea266b7717053e3adbc283806a3b62339b68d + cosmiconfig: ">=8.2" + typescript: ">=4" + checksum: 10/a3ea9de9633899867ddc8368735d085f9e050dce2c319ca13dbe51187476edd7818586618eabe38bc81e5e981863903415e90da17e346f839bb442eeb29aa38a languageName: node linkType: hard @@ -6983,7 +7094,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.5, cross-spawn@npm:^7.0.6": +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.5": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" dependencies: @@ -7024,7 +7135,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.7, debug@npm:~4.4.0": +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.7": version: 4.4.0 resolution: "debug@npm:4.4.0" dependencies: @@ -7045,6 +7156,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:~4.3.6": + version: 4.3.7 + resolution: "debug@npm:4.3.7" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10/71168908b9a78227ab29d5d25fe03c5867750e31ce24bf2c44a86efc5af041758bb56569b0a3d48a9b5344c00a24a777e6f4100ed6dfd9534a42c1dde285125a + languageName: node + linkType: hard + "decamelize@npm:^1.2.0": version: 1.2.0 resolution: "decamelize@npm:1.2.0" @@ -7378,14 +7501,14 @@ __metadata: linkType: hard "dompurify@npm:^3.1.6": - version: 3.2.3 - resolution: "dompurify@npm:3.2.3" + version: 3.2.1 + resolution: "dompurify@npm:3.2.1" dependencies: "@types/trusted-types": "npm:^2.0.7" dependenciesMeta: "@types/trusted-types": optional: true - checksum: 10/aad472bcdff40afdbb307fd02abbca86acefee9c39cb35e9634ebbc5e047750a7eeb021b02cd66894d60cf75ad021f69394de2e9e8786b0dd91c5832f497a9af + checksum: 10/f5f906fadc5e92305ba4272ff60df77a803c0832f3f2ce941099f3dfe015cfb15db44e8623bd4ec1f2297a17bcdac90066c5cb0c5cf1059a09364d060c304ddb languageName: node linkType: hard @@ -7408,9 +7531,9 @@ __metadata: linkType: hard "dotenv@npm:^16.0.3, dotenv@npm:^16.4.5, dotenv@npm:~16.4.5": - version: 16.4.7 - resolution: "dotenv@npm:16.4.7" - checksum: 10/f13bfe97db88f0df4ec505eeffb8925ec51f2d56a3d0b6d916964d8b4af494e6fb1633ba5d09089b552e77ab2a25de58d70259b2c5ed45ec148221835fc99a0c + version: 16.4.5 + resolution: "dotenv@npm:16.4.5" + checksum: 10/55a3134601115194ae0f924e54473459ed0d9fc340ae610b676e248cca45aa7c680d86365318ea964e6da4e2ea80c4514c1adab5adb43d6867fb57ff068f95c8 languageName: node linkType: hard @@ -7439,10 +7562,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.5.73": - version: 1.5.73 - resolution: "electron-to-chromium@npm:1.5.73" - checksum: 10/07d0885656c49ea4deb98340db23c68b43615c0325566e0ae6df751328460d4cdfb86ca0fcaaa745cf7b44c6141440fe2eb2f5d9518d1c110ba3872c73ed817d +"electron-to-chromium@npm:^1.5.41": + version: 1.5.65 + resolution: "electron-to-chromium@npm:1.5.65" + checksum: 10/9d4e5609de75fc92aeff10976fd8fa2b9b6294edb3eb7c205e51e3e5f42c3d5f6d1e0323bb52ed4831121ee617962c9a8eafff5aee5651d33bee98677940b4be languageName: node linkType: hard @@ -7921,17 +8044,17 @@ __metadata: linkType: hard "eslint-import-resolver-typescript@npm:^3.6.3": - version: 3.7.0 - resolution: "eslint-import-resolver-typescript@npm:3.7.0" + version: 3.6.3 + resolution: "eslint-import-resolver-typescript@npm:3.6.3" dependencies: "@nolyfill/is-core-module": "npm:1.0.39" - debug: "npm:^4.3.7" + debug: "npm:^4.3.5" enhanced-resolve: "npm:^5.15.0" + eslint-module-utils: "npm:^2.8.1" fast-glob: "npm:^3.3.2" get-tsconfig: "npm:^4.7.5" is-bun-module: "npm:^1.0.2" is-glob: "npm:^4.0.3" - stable-hash: "npm:^0.0.4" peerDependencies: eslint: "*" eslint-plugin-import: "*" @@ -7941,15 +8064,26 @@ __metadata: optional: true eslint-plugin-import-x: optional: true - checksum: 10/8158730c11e562c56ed9bf7236dc75bce35b6992dc32c39ac2f4177ab77fca97b95999850204a6458054243607b54aee88c028a61fed4184f24f425fa1afff01 + checksum: 10/5f9956dbbd0becc3d6c6cb945dad0e5e6f529cfd0f488d5688f3c59840cd7f4a44ab6aee0f54b5c4188134dab9a01cb63c1201767bde7fc330b7c1a14747f8ac + languageName: node + linkType: hard + +"eslint-module-utils@npm:^2.8.1": + version: 2.12.0 + resolution: "eslint-module-utils@npm:2.12.0" + dependencies: + debug: "npm:^3.2.7" + peerDependenciesMeta: + eslint: + optional: true + checksum: 10/dd27791147eca17366afcb83f47d6825b6ce164abb256681e5de4ec1d7e87d8605641eb869298a0dbc70665e2446dbcc2f40d3e1631a9475dd64dd23d4ca5dee languageName: node linkType: hard "eslint-plugin-import-x@npm:^4.2.1": - version: 4.5.0 - resolution: "eslint-plugin-import-x@npm:4.5.0" + version: 4.4.3 + resolution: "eslint-plugin-import-x@npm:4.4.3" dependencies: - "@typescript-eslint/scope-manager": "npm:^8.1.0" "@typescript-eslint/utils": "npm:^8.1.0" debug: "npm:^4.3.4" doctrine: "npm:^3.0.0" @@ -7962,7 +8096,7 @@ __metadata: tslib: "npm:^2.6.3" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - checksum: 10/c894408247344655f1f5db7c0d808ae5f7de331572968fa12074b54d490e6967cc5921f2ae616b161cafa171ff751c16bfd0201137fb47a1617105aabf5197d8 + checksum: 10/e0c459b3bab2af70a52caac5291e5f177e7a5445d29b0a062e47be767eeaef9c1e7901e4ae5f5381b3d7d17389db0e31184f868ec90f15c0d277d7f768b0a252 languageName: node linkType: hard @@ -8082,15 +8216,15 @@ __metadata: linkType: hard "eslint@npm:^9.0.0": - version: 9.17.0 - resolution: "eslint@npm:9.17.0" + version: 9.15.0 + resolution: "eslint@npm:9.15.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.2.0" "@eslint-community/regexpp": "npm:^4.12.1" "@eslint/config-array": "npm:^0.19.0" "@eslint/core": "npm:^0.9.0" "@eslint/eslintrc": "npm:^3.2.0" - "@eslint/js": "npm:9.17.0" + "@eslint/js": "npm:9.15.0" "@eslint/plugin-kit": "npm:^0.2.3" "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" @@ -8099,7 +8233,7 @@ __metadata: "@types/json-schema": "npm:^7.0.15" ajv: "npm:^6.12.4" chalk: "npm:^4.0.0" - cross-spawn: "npm:^7.0.6" + cross-spawn: "npm:^7.0.5" debug: "npm:^4.3.2" escape-string-regexp: "npm:^4.0.0" eslint-scope: "npm:^8.2.0" @@ -8126,7 +8260,7 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10/a48ee67dd4e737974bbb49ca5d12d0ce35bcd874507807599e3655bb398320ab27c9deed1aad508a963967815e626c21208f52158c2fc0796d0cc8186528efeb + checksum: 10/7ac1a2e6070bae64b2b0588fabad528cd3e478a6ba5e9f8185d8d9f2dce17a36630bd019b5d32d1052ea177444ab9c83f3c08baa76121c13e1ed0584ef158956 languageName: node linkType: hard @@ -8516,9 +8650,9 @@ __metadata: linkType: hard "flow-parser@npm:0.*": - version: 0.256.0 - resolution: "flow-parser@npm:0.256.0" - checksum: 10/e3c680f84683310475642ebcb85e8c26f854b36e872c3d797093fd9494164adf1b106fd6f563259f44d0a1f1c3453703994482b31e7fa1db516130ee2f8458ef + version: 0.255.0 + resolution: "flow-parser@npm:0.255.0" + checksum: 10/7570e9a2256086d3d011707822315695e1107665b81e30919347cea7db1d95ad0dd8bc33e0fcce114b35dcadbb614e8f2604647f8f856afdb4eefc3f6755e666 languageName: node linkType: hard @@ -8631,6 +8765,15 @@ __metadata: languageName: node linkType: hard +"fs-minipass@npm:^2.0.0": + version: 2.1.0 + resolution: "fs-minipass@npm:2.1.0" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10/03191781e94bc9a54bd376d3146f90fe8e082627c502185dbf7b9b3032f66b0b142c1115f3b2cc5936575fc1b44845ce903dd4c21bec2a8d69f3bd56f9cee9ec + languageName: node + linkType: hard + "fs-minipass@npm:^3.0.0": version: 3.0.3 resolution: "fs-minipass@npm:3.0.3" @@ -8794,7 +8937,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7, glob@npm:^10.4.1": +"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.4.1": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -8848,9 +8991,9 @@ __metadata: linkType: hard "globals@npm:^15.9.0": - version: 15.13.0 - resolution: "globals@npm:15.13.0" - checksum: 10/ba84d0612d516bcc1dabdd9ce66667956e1a87401fb53be6c379f8f6a04f8e6ce415b584801ae2689a90e788e89bb38adfafc854a8a50ae8e322bb4dd35a2105 + version: 15.12.0 + resolution: "globals@npm:15.12.0" + checksum: 10/07cac4ee7cc9befa7894be9b4d1a57f46eeedf9065939f39ffb875009394908eb7bac84147712cfd4bbabab5abc7ab98fc3a6d0fd881f9548fffa10ba2e4bf67 languageName: node linkType: hard @@ -8894,20 +9037,20 @@ __metadata: linkType: hard "graphql@npm:^16.8.1, graphql@npm:^16.9.0": - version: 16.10.0 - resolution: "graphql@npm:16.10.0" - checksum: 10/d42cf81ddcf3a61dfb213217576bf33c326f15b02c4cee369b373dc74100cbdcdc4479b3b797e79b654dabd8fddf50ef65ff75420e9ce5596c02e21f24c9126a + version: 16.9.0 + resolution: "graphql@npm:16.9.0" + checksum: 10/5833f82bb6c31bec120bbf9cd400eda873e1bb7ef5c17974fa262cd82dc68728fda5d4cb859dc8aaa4c4fe4f6fe1103a9c47efc01a12c02ae5cb581d8e4029e2 languageName: node linkType: hard "happy-dom@npm:^15.0.0": - version: 15.11.7 - resolution: "happy-dom@npm:15.11.7" + version: 15.11.6 + resolution: "happy-dom@npm:15.11.6" dependencies: entities: "npm:^4.5.0" webidl-conversions: "npm:^7.0.0" whatwg-mimetype: "npm:^3.0.0" - checksum: 10/82fb8505a940ebc2b732d1c70ae4ba08128cc82f2d469702f73e541d3bf10fc69b726386f8aaf579ccd2697f85e86dee87aa9d4f229b781fb05628d733fc93d7 + checksum: 10/895a40bda00753f833ea563f64aef80558b6f701a3238e9fe7143265a1246bde57bc5a7a58ed89e77e2bbe309a119dcf956a70c48033fa84893c41206a6b4128 languageName: node linkType: hard @@ -8975,8 +9118,8 @@ __metadata: linkType: hard "hast-util-to-html@npm:^9.0.0, hast-util-to-html@npm:^9.0.3": - version: 9.0.4 - resolution: "hast-util-to-html@npm:9.0.4" + version: 9.0.3 + resolution: "hast-util-to-html@npm:9.0.3" dependencies: "@types/hast": "npm:^3.0.0" "@types/unist": "npm:^3.0.0" @@ -8989,7 +9132,7 @@ __metadata: space-separated-tokens: "npm:^2.0.0" stringify-entities: "npm:^4.0.0" zwitch: "npm:^2.0.4" - checksum: 10/a0b4ed9058e57fa2ca010d10c077fda78d2ab2af99f5bd09fe4b9948970025ac4a2a1a03ec7e2e0f3b0444066b1b35d602fa3e9fbd9b7fc9cdd35d0cafa909ca + checksum: 10/cdf860be567137d045490b0f27590bcafc7032f0725a84667e8950d7bf2ce175d0dfc635b7ce05f3a8d1963ac4c74cae4d93513047429aad909222decdb2f7d1 languageName: node linkType: hard @@ -9122,12 +9265,12 @@ __metadata: linkType: hard "https-proxy-agent@npm:^7.0.1": - version: 7.0.6 - resolution: "https-proxy-agent@npm:7.0.6" + version: 7.0.5 + resolution: "https-proxy-agent@npm:7.0.5" dependencies: - agent-base: "npm:^7.1.2" + agent-base: "npm:^7.0.2" debug: "npm:4" - checksum: 10/784b628cbd55b25542a9d85033bdfd03d4eda630fb8b3c9477959367f3be95dc476ed2ecbb9836c359c7c698027fc7b45723a302324433590f45d6c1706e8c13 + checksum: 10/6679d46159ab3f9a5509ee80c3a3fc83fba3a920a5e18d32176c3327852c3c00ad640c0c4210a8fd70ea3c4a6d3a1b375bf01942516e7df80e2646bdc77658ab languageName: node linkType: hard @@ -9319,11 +9462,11 @@ __metadata: linkType: hard "is-bun-module@npm:^1.0.2": - version: 1.3.0 - resolution: "is-bun-module@npm:1.3.0" + version: 1.2.1 + resolution: "is-bun-module@npm:1.2.1" dependencies: semver: "npm:^7.6.3" - checksum: 10/b23d9ec7b4d4bfd89e4e72b5cd52e1bc153facad59fdd7394c656f8859a78740ef35996a2066240a32f39cc9a9da4b4eb69e68df3c71755a61ebbaf56d3daef0 + checksum: 10/1c2cbcf1a76991add1b640d2d7fe09848e8697a76f96e1289dff44133a48c97f5dc601d4a66d3f3a86217a77178d72d33d10d0c9e14194e58e70ec8df3eae41a languageName: node linkType: hard @@ -9409,6 +9552,13 @@ __metadata: languageName: node linkType: hard +"is-lambda@npm:^1.0.1": + version: 1.0.1 + resolution: "is-lambda@npm:1.0.1" + checksum: 10/93a32f01940220532e5948538699ad610d5924ac86093fcee83022252b363eb0cc99ba53ab084a04e4fb62bf7b5731f55496257a4c38adf87af9c4d352c71c35 + languageName: node + linkType: hard + "is-node-process@npm:^1.2.0": version: 1.2.0 resolution: "is-node-process@npm:1.2.0" @@ -9749,7 +9899,16 @@ __metadata: languageName: node linkType: hard -"jiti@npm:^2.0.0, jiti@npm:^2.4.1": +"jiti@npm:^1.21.6": + version: 1.21.6 + resolution: "jiti@npm:1.21.6" + bin: + jiti: bin/jiti.js + checksum: 10/289b124cea411c130a14ffe88e3d38376ab44b6695616dfa0a1f32176a8f20ec90cdd6d2b9d81450fc6467cfa4d865f04f49b98452bff0f812bc400fd0ae78d6 + languageName: node + linkType: hard + +"jiti@npm:^2.0.0": version: 2.4.1 resolution: "jiti@npm:2.4.1" bin: @@ -9835,12 +9994,12 @@ __metadata: languageName: node linkType: hard -"jsesc@npm:^3.0.2": - version: 3.1.0 - resolution: "jsesc@npm:3.1.0" +"jsesc@npm:^3.0.2, jsesc@npm:~3.0.2": + version: 3.0.2 + resolution: "jsesc@npm:3.0.2" bin: jsesc: bin/jsesc - checksum: 10/20bd37a142eca5d1794f354db8f1c9aeb54d85e1f5c247b371de05d23a9751ecd7bd3a9c4fc5298ea6fa09a100dafb4190fa5c98c6610b75952c3487f3ce7967 + checksum: 10/8e5a7de6b70a8bd71f9cb0b5a7ade6a73ae6ab55e697c74cc997cede97417a3a65ed86c36f7dd6125fe49766e8386c845023d9e213916ca92c9dfdd56e2babf3 languageName: node linkType: hard @@ -9853,15 +10012,6 @@ __metadata: languageName: node linkType: hard -"jsesc@npm:~3.0.2": - version: 3.0.2 - resolution: "jsesc@npm:3.0.2" - bin: - jsesc: bin/jsesc - checksum: 10/8e5a7de6b70a8bd71f9cb0b5a7ade6a73ae6ab55e697c74cc997cede97417a3a65ed86c36f7dd6125fe49766e8386c845023d9e213916ca92c9dfdd56e2babf3 - languageName: node - linkType: hard - "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -9945,13 +10095,13 @@ __metadata: linkType: hard "katex@npm:^0.16.0, katex@npm:^0.16.11": - version: 0.16.15 - resolution: "katex@npm:0.16.15" + version: 0.16.11 + resolution: "katex@npm:0.16.11" dependencies: commander: "npm:^8.3.0" bin: katex: cli.js - checksum: 10/4c1076a066d600809c790def05d88c2a4caba1f695a3a61b44a5085e4fb0f513f3c2acc42720e62df7b08561647d3a96361e775d219d5637cfcc15aa90493747 + checksum: 10/adfb95a70168f732c26f44a443d27df393ca641a3533aa9321f37b1b69134cf4b15142d533c187ec9a0b02c0bbfebab5ab26f15bd0cc08a57114e1f767f0d7ae languageName: node linkType: hard @@ -10076,15 +10226,15 @@ __metadata: linkType: hard "lib0@npm:^0.2.74, lib0@npm:^0.2.85, lib0@npm:^0.2.97, lib0@npm:^0.2.98": - version: 0.2.99 - resolution: "lib0@npm:0.2.99" + version: 0.2.98 + resolution: "lib0@npm:0.2.98" dependencies: isomorphic.js: "npm:^0.2.4" bin: 0ecdsa-generate-keypair: bin/0ecdsa-generate-keypair.js 0gentesthtml: bin/gentesthtml.js 0serve: bin/0serve.js - checksum: 10/825f248d31fc85241d322365d838aa4902ee5d97e12b8a14ac1234d0ec1c3263fa217057a2d167ec734f3037b89fd6d514a29b4a768cdf677e57d2a2692a5827 + checksum: 10/8ec7fb66e211b5041d988e93d768b47946f820dfbf65b9aeb5350041b09ec6fa86d528dc1605fc79eb4d66db6b5391a0e6bf73392c3d53f5a4601430664712c9 languageName: node linkType: hard @@ -10097,7 +10247,7 @@ __metadata: languageName: node linkType: hard -"lilconfig@npm:^3.1.2, lilconfig@npm:~3.1.3": +"lilconfig@npm:^3.1.2, lilconfig@npm:~3.1.2": version: 3.1.3 resolution: "lilconfig@npm:3.1.3" checksum: 10/b932ce1af94985f0efbe8896e57b1f814a48c8dbd7fc0ef8469785c6303ed29d0090af3ccad7e36b626bfca3a4dc56cc262697e9a8dd867623cf09a39d54e4c3 @@ -10128,26 +10278,26 @@ __metadata: linkType: hard "lint-staged@npm:^15.2.9": - version: 15.2.11 - resolution: "lint-staged@npm:15.2.11" + version: 15.2.10 + resolution: "lint-staged@npm:15.2.10" dependencies: chalk: "npm:~5.3.0" commander: "npm:~12.1.0" - debug: "npm:~4.4.0" + debug: "npm:~4.3.6" execa: "npm:~8.0.1" - lilconfig: "npm:~3.1.3" - listr2: "npm:~8.2.5" + lilconfig: "npm:~3.1.2" + listr2: "npm:~8.2.4" micromatch: "npm:~4.0.8" pidtree: "npm:~0.6.0" string-argv: "npm:~0.3.2" - yaml: "npm:~2.6.1" + yaml: "npm:~2.5.0" bin: lint-staged: bin/lint-staged.js - checksum: 10/b3fa66401fee667c39b28ed8d0dc28213e6c23d5cb88ca599e030e1eb7fd4ed199f2e3c4b02ae65ded9f5a30e0168a4c3cf3a3cf12959f195864962dba37e917 + checksum: 10/ab6930cd633dbb5b6ec7c81fc06c65df41e9f80d93dd22e0d79c6e272cdfd8110a0fbdec60303d46a06b30bcd92261153630e2c937531b77ec5ae41e7e9d90d3 languageName: node linkType: hard -"listr2@npm:~8.2.5": +"listr2@npm:~8.2.4": version: 8.2.5 resolution: "listr2@npm:8.2.5" dependencies: @@ -10511,22 +10661,23 @@ __metadata: languageName: node linkType: hard -"make-fetch-happen@npm:^14.0.3": - version: 14.0.3 - resolution: "make-fetch-happen@npm:14.0.3" +"make-fetch-happen@npm:^13.0.0": + version: 13.0.1 + resolution: "make-fetch-happen@npm:13.0.1" dependencies: - "@npmcli/agent": "npm:^3.0.0" - cacache: "npm:^19.0.1" + "@npmcli/agent": "npm:^2.0.0" + cacache: "npm:^18.0.0" http-cache-semantics: "npm:^4.1.1" + is-lambda: "npm:^1.0.1" minipass: "npm:^7.0.2" - minipass-fetch: "npm:^4.0.0" + minipass-fetch: "npm:^3.0.0" minipass-flush: "npm:^1.0.5" minipass-pipeline: "npm:^1.2.4" - negotiator: "npm:^1.0.0" - proc-log: "npm:^5.0.0" + negotiator: "npm:^0.6.3" + proc-log: "npm:^4.2.0" promise-retry: "npm:^2.0.1" - ssri: "npm:^12.0.0" - checksum: 10/fce0385840b6d86b735053dfe941edc2dd6468fda80fe74da1eeff10cbd82a75760f406194f2bc2fa85b99545b2bc1f84c08ddf994b21830775ba2d1a87e8bdf + ssri: "npm:^10.0.0" + checksum: 10/11bae5ad6ac59b654dbd854f30782f9de052186c429dfce308eda42374528185a100ee40ac9ffdc36a2b6c821ecaba43913e4730a12f06f15e895ea9cb23fa59 languageName: node linkType: hard @@ -11183,18 +11334,18 @@ __metadata: languageName: node linkType: hard -"minipass-fetch@npm:^4.0.0": - version: 4.0.0 - resolution: "minipass-fetch@npm:4.0.0" +"minipass-fetch@npm:^3.0.0": + version: 3.0.5 + resolution: "minipass-fetch@npm:3.0.5" dependencies: encoding: "npm:^0.1.13" minipass: "npm:^7.0.3" minipass-sized: "npm:^1.0.3" - minizlib: "npm:^3.0.1" + minizlib: "npm:^2.1.2" dependenciesMeta: encoding: optional: true - checksum: 10/4b0772dbee77727b469dc5bfc371541d9aba1e243fbb46ddc1b9ff7efa4de4a4cf5ff3a359d6a3b3a460ca26df9ae67a9c93be26ab6417c225e49d63b52b2801 + checksum: 10/c669948bec1373313aaa8f104b962a3ced9f45c49b26366a4b0ae27ccdfa9c5740d72c8a84d3f8623d7a61c5fc7afdfda44789008c078f61a62441142efc4a97 languageName: node linkType: hard @@ -11234,7 +11385,14 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2": +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 10/61682162d29f45d3152b78b08bab7fb32ca10899bc5991ffe98afc18c9e9543bd1e3be94f8b8373ba6262497db63607079dc242ea62e43e7b2270837b7347c93 + languageName: node + linkType: hard + +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.1.2": version: 7.1.2 resolution: "minipass@npm:7.1.2" checksum: 10/c25f0ee8196d8e6036661104bacd743785b2599a21de5c516b32b3fa2b83113ac89a2358465bc04956baab37ffb956ae43be679b2262bf7be15fce467ccd7950 @@ -11248,13 +11406,13 @@ __metadata: languageName: node linkType: hard -"minizlib@npm:^3.0.1": - version: 3.0.1 - resolution: "minizlib@npm:3.0.1" +"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": + version: 2.1.2 + resolution: "minizlib@npm:2.1.2" dependencies: - minipass: "npm:^7.0.4" - rimraf: "npm:^5.0.5" - checksum: 10/622cb85f51e5c206a080a62d20db0d7b4066f308cb6ce82a9644da112367c3416ae7062017e631eb7ac8588191cfa4a9a279b8651c399265202b298e98c4acef + minipass: "npm:^3.0.0" + yallist: "npm:^4.0.0" + checksum: 10/ae0f45436fb51344dcb87938446a32fbebb540d0e191d63b35e1c773d47512e17307bf54aa88326cc6d176594d00e4423563a091f7266c2f9a6872cdc1e234d1 languageName: node linkType: hard @@ -11265,12 +11423,12 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^3.0.1": - version: 3.0.1 - resolution: "mkdirp@npm:3.0.1" +"mkdirp@npm:^1.0.3": + version: 1.0.4 + resolution: "mkdirp@npm:1.0.4" bin: - mkdirp: dist/cjs/src/bin.js - checksum: 10/16fd79c28645759505914561e249b9a1f5fe3362279ad95487a4501e4467abeb714fd35b95307326b8fd03f3c7719065ef11a6f97b7285d7888306d1bd2232ba + mkdirp: bin/cmd.js + checksum: 10/d71b8dcd4b5af2fe13ecf3bd24070263489404fe216488c5ba7e38ece1f54daf219e72a833a3a2dc404331e870e9f44963a33399589490956bff003a3404d3b2 languageName: node linkType: hard @@ -11362,29 +11520,29 @@ __metadata: linkType: hard "nanoid@npm:^3.3.7": - version: 3.3.8 - resolution: "nanoid@npm:3.3.8" + version: 3.3.7 + resolution: "nanoid@npm:3.3.7" bin: nanoid: bin/nanoid.cjs - checksum: 10/2d1766606cf0d6f47b6f0fdab91761bb81609b2e3d367027aff45e6ee7006f660fb7e7781f4a34799fe6734f1268eeed2e37a5fdee809ade0c2d4eb11b0f9c40 + checksum: 10/ac1eb60f615b272bccb0e2b9cd933720dad30bf9708424f691b8113826bb91aca7e9d14ef5d9415a6ba15c266b37817256f58d8ce980c82b0ba3185352565679 languageName: node linkType: hard "nanoid@npm:^5.0.1, nanoid@npm:^5.0.7": - version: 5.0.9 - resolution: "nanoid@npm:5.0.9" + version: 5.0.8 + resolution: "nanoid@npm:5.0.8" bin: nanoid: bin/nanoid.js - checksum: 10/8a3f9104f81095e3e4785f58caae47a05755599824b8611b9730cbf73db706b664f100e6189f8303f08764f144d499613d8e4a39e83125c53f4b4986d6576621 + checksum: 10/df131a515465053ff25c8cf0450ef191e1db83b45fe125af43f50d39feddf1f161d3b2abb34cb993df35a76b427f8d6d982e16e47d67b2fbe843664af025b5e2 languageName: node linkType: hard "nanospinner@npm:^1.1.0": - version: 1.2.2 - resolution: "nanospinner@npm:1.2.2" + version: 1.2.0 + resolution: "nanospinner@npm:1.2.0" dependencies: picocolors: "npm:^1.1.1" - checksum: 10/40ed63364c95b58806a9989b16af8728fdb57d19a8bf05e643542a0c64b184df29435308a07a618af48817f22850dc1ef5827724c226047cab7e70bd22705a0e + checksum: 10/083db6e33e6b33646af11debb53b8bb8970ade6a83899725f9aa42610cdadd7c1cd759f0efb047ec8c1d81ef2001365af294864a87c97d17a050e823ed4208bb languageName: node linkType: hard @@ -11409,10 +11567,10 @@ __metadata: languageName: node linkType: hard -"negotiator@npm:^1.0.0": - version: 1.0.0 - resolution: "negotiator@npm:1.0.0" - checksum: 10/b5734e87295324fabf868e36fb97c84b7d7f3156ec5f4ee5bf6e488079c11054f818290fc33804cef7b1ee21f55eeb14caea83e7dafae6492a409b3e573153e5 +"negotiator@npm:^0.6.3": + version: 0.6.4 + resolution: "negotiator@npm:0.6.4" + checksum: 10/d98c04a136583afd055746168f1067d58ce4bfe6e4c73ca1d339567f81ea1f7e665b5bd1e81f4771c67b6c2ea89b21cb2adaea2b16058c7dc31317778f931dab languageName: node linkType: hard @@ -11424,22 +11582,22 @@ __metadata: linkType: hard "node-gyp@npm:latest": - version: 11.0.0 - resolution: "node-gyp@npm:11.0.0" + version: 10.2.0 + resolution: "node-gyp@npm:10.2.0" dependencies: env-paths: "npm:^2.2.0" exponential-backoff: "npm:^3.1.1" glob: "npm:^10.3.10" graceful-fs: "npm:^4.2.6" - make-fetch-happen: "npm:^14.0.3" - nopt: "npm:^8.0.0" - proc-log: "npm:^5.0.0" + make-fetch-happen: "npm:^13.0.0" + nopt: "npm:^7.0.0" + proc-log: "npm:^4.1.0" semver: "npm:^7.3.5" - tar: "npm:^7.4.3" - which: "npm:^5.0.0" + tar: "npm:^6.2.1" + which: "npm:^4.0.0" bin: node-gyp: bin/node-gyp.js - checksum: 10/5d07430e887a906f85c7c6ed87e8facb7ecd4ce42d948a2438c471df2e24ae6af70f4def114ec1a03127988d164648dda8d75fe666f3c4b431e53856379fdf13 + checksum: 10/41773093b1275751dec942b985982fd4e7a69b88cae719b868babcef3880ee6168aaec8dcaa8cd0b9fa7c84873e36cc549c6cac6a124ee65ba4ce1f1cc108cfe languageName: node linkType: hard @@ -11459,10 +11617,10 @@ __metadata: languageName: node linkType: hard -"node-releases@npm:^2.0.19": - version: 2.0.19 - resolution: "node-releases@npm:2.0.19" - checksum: 10/c2b33b4f0c40445aee56141f13ca692fa6805db88510e5bbb3baadb2da13e1293b738e638e15e4a8eb668bb9e97debb08e7a35409b477b5cc18f171d35a83045 +"node-releases@npm:^2.0.18": + version: 2.0.18 + resolution: "node-releases@npm:2.0.18" + checksum: 10/241e5fa9556f1c12bafb83c6c3e94f8cf3d8f2f8f904906ecef6e10bcaa1d59aa61212d4651bec70052015fc54bd3fdcdbe7fc0f638a17e6685aa586c076ec4e languageName: node linkType: hard @@ -11475,14 +11633,14 @@ __metadata: languageName: node linkType: hard -"nopt@npm:^8.0.0": - version: 8.0.0 - resolution: "nopt@npm:8.0.0" +"nopt@npm:^7.0.0": + version: 7.2.1 + resolution: "nopt@npm:7.2.1" dependencies: abbrev: "npm:^2.0.0" bin: nopt: bin/nopt.js - checksum: 10/2d137f64b6f9331ec97047dd1cbbe4dcd9a61ceef4fd0f2252c0bbac1d69ba15671e6fd83a441328824b3ca78afe6ebe1694f12ebcd162b73a221582a06179ff + checksum: 10/95a1f6dec8a81cd18cdc2fed93e6f0b4e02cf6bdb4501c848752c6e34f9883d9942f036a5e3b21a699047d8a448562d891e67492df68ec9c373e6198133337ae languageName: node linkType: hard @@ -11946,10 +12104,12 @@ __metadata: languageName: node linkType: hard -"p-map@npm:^7.0.2": - version: 7.0.3 - resolution: "p-map@npm:7.0.3" - checksum: 10/2ef48ccfc6dd387253d71bf502604f7893ed62090b2c9d73387f10006c342606b05233da0e4f29388227b61eb5aeface6197e166520c465c234552eeab2fe633 +"p-map@npm:^4.0.0": + version: 4.0.0 + resolution: "p-map@npm:4.0.0" + dependencies: + aggregate-error: "npm:^3.0.0" + checksum: 10/7ba4a2b1e24c05e1fc14bbaea0fc6d85cf005ae7e9c9425d4575550f37e2e584b1af97bcde78eacd7559208f20995988d52881334db16cf77bc1bcf68e48ed7c languageName: node linkType: hard @@ -11980,9 +12140,9 @@ __metadata: linkType: hard "package-manager-detector@npm:^0.2.0": - version: 0.2.7 - resolution: "package-manager-detector@npm:0.2.7" - checksum: 10/c7ebe2482f013891dd34365033a1c8a7d053049f566006cea2fe8704c47d7425a14ad9544dbe9c3353d83f98fe33b3f9adda3ce4eb855498796f3849c6b6fcd9 + version: 0.2.5 + resolution: "package-manager-detector@npm:0.2.5" + checksum: 10/97127289fffc238f6af5af3339c309a3b25f5b360b8d503f8d27cc3afa259732a08e7df9331f703b1db0c585504f70ade9b068ddf40949f6dd8a5ebbb9ab8d67 languageName: node linkType: hard @@ -12291,9 +12451,9 @@ __metadata: linkType: hard "preact@npm:^10.0.0": - version: 10.25.2 - resolution: "preact@npm:10.25.2" - checksum: 10/4910dbb6ba7112a418b75ebe87d72b5f6a201c275d6618b8d749bd8b273df6bee1811bc6d26047039d7c59c5a905f5ec6083549301d51dea1a6fcabb5caf9916 + version: 10.25.0 + resolution: "preact@npm:10.25.0" + checksum: 10/576700f2781e33577804ba7f1fc5f7ff858106fbb598475ecd12d27ac1123327b6337cdda9de20081c67735750fbb1cd8cbfd17451b98635e740c3f52912aab2 languageName: node linkType: hard @@ -12339,11 +12499,11 @@ __metadata: linkType: hard "prettier@npm:^3.3.3": - version: 3.4.2 - resolution: "prettier@npm:3.4.2" + version: 3.4.0 + resolution: "prettier@npm:3.4.0" bin: prettier: bin/prettier.cjs - checksum: 10/a3e806fb0b635818964d472d35d27e21a4e17150c679047f5501e1f23bd4aa806adf660f0c0d35214a210d5d440da6896c2e86156da55f221a57938278dc326e + checksum: 10/9116f4dfd1b064025b776bc5b8ca1fe68555d606fb38771f8d73cfdafd9bf60ca9eea27a56f5c526abdf0f4a711122a9c683fbcd029d74415a2163dbb1eb416b languageName: node linkType: hard @@ -12392,10 +12552,10 @@ __metadata: languageName: node linkType: hard -"proc-log@npm:^5.0.0": - version: 5.0.0 - resolution: "proc-log@npm:5.0.0" - checksum: 10/35610bdb0177d3ab5d35f8827a429fb1dc2518d9e639f2151ac9007f01a061c30e0c635a970c9b00c39102216160f6ec54b62377c92fac3b7bfc2ad4b98d195c +"proc-log@npm:^4.1.0, proc-log@npm:^4.2.0": + version: 4.2.0 + resolution: "proc-log@npm:4.2.0" + checksum: 10/4e1394491b717f6c1ade15c570ecd4c2b681698474d3ae2d303c1e4b6ab9455bd5a81566211e82890d5a5ae9859718cc6954d5150bb18b09b72ecb297beae90a languageName: node linkType: hard @@ -12440,11 +12600,11 @@ __metadata: linkType: hard "psl@npm:^1.1.33": - version: 1.15.0 - resolution: "psl@npm:1.15.0" + version: 1.13.0 + resolution: "psl@npm:1.13.0" dependencies: punycode: "npm:^2.3.1" - checksum: 10/5e7467eb5196eb7900d156783d12907d445c0122f76c73203ce96b148a6ccf8c5450cc805887ffada38ff92d634afcf33720c24053cb01d5b6598d1c913c5caf + checksum: 10/75e4f441f42b2a33ae5100b6e37241dec2eb6c548d7d319ca86ffed8ebd772da7a32ce52f7537a51505bca78362311166063ccf0f636c2f70a51da548e992a4a languageName: node linkType: hard @@ -12686,7 +12846,7 @@ __metadata: languageName: node linkType: hard -"regexpu-core@npm:^6.2.0": +"regexpu-core@npm:^6.1.1": version: 6.2.0 resolution: "regexpu-core@npm:6.2.0" dependencies: @@ -12889,28 +13049,28 @@ __metadata: linkType: hard "resolve@npm:^1.10.0, resolve@npm:^1.12.0, resolve@npm:^1.14.2, resolve@npm:^1.22.4, resolve@npm:^1.22.8": - version: 1.22.9 - resolution: "resolve@npm:1.22.9" + version: 1.22.8 + resolution: "resolve@npm:1.22.8" dependencies: - is-core-module: "npm:^2.16.0" + is-core-module: "npm:^2.13.0" path-parse: "npm:^1.0.7" supports-preserve-symlinks-flag: "npm:^1.0.0" bin: resolve: bin/resolve - checksum: 10/787b122cffd34944e8e899dc6f934278142df9f47c1228672cf80d21791364f0a9ff17b766374e9e83b78dee1cf4ded28d8387d264343861db77dc1141c5ec78 + checksum: 10/c473506ee01eb45cbcfefb68652ae5759e092e6b0fb64547feadf9736a6394f258fbc6f88e00c5ca36d5477fbb65388b272432a3600fa223062e54333c156753 languageName: node linkType: hard "resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.12.0#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin, resolve@patch:resolve@npm%3A^1.22.8#optional!builtin": - version: 1.22.9 - resolution: "resolve@patch:resolve@npm%3A1.22.9#optional!builtin::version=1.22.9&hash=c3c19d" + version: 1.22.8 + resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" dependencies: - is-core-module: "npm:^2.16.0" + is-core-module: "npm:^2.13.0" path-parse: "npm:^1.0.7" supports-preserve-symlinks-flag: "npm:^1.0.0" bin: resolve: bin/resolve - checksum: 10/423e54ddf58784c85ba2382f1e982f57e55dc19967f348214e1e6bc80d2fdbdaef35453d1a6a3c31810ac5e4e87e05ad9f5b3a3b1f117d3e673de313690eb54a + checksum: 10/f345cd37f56a2c0275e3fe062517c650bb673815d885e7507566df589375d165bbbf4bdb6aa95600a9bc55f4744b81f452b5a63f95b9f10a72787dba3c90890a languageName: node linkType: hard @@ -12966,40 +13126,28 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:^5.0.5": - version: 5.0.10 - resolution: "rimraf@npm:5.0.10" - dependencies: - glob: "npm:^10.3.7" - bin: - rimraf: dist/esm/bin.mjs - checksum: 10/f3b8ce81eecbde4628b07bdf9e2fa8b684e0caea4999acb1e3b0402c695cd41f28cd075609a808e61ce2672f528ca079f675ab1d8e8d5f86d56643a03e0b8d2e - languageName: node - linkType: hard - "rollup@npm:^4.20.0, rollup@npm:^4.21.0": - version: 4.28.1 - resolution: "rollup@npm:4.28.1" - dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.28.1" - "@rollup/rollup-android-arm64": "npm:4.28.1" - "@rollup/rollup-darwin-arm64": "npm:4.28.1" - "@rollup/rollup-darwin-x64": "npm:4.28.1" - "@rollup/rollup-freebsd-arm64": "npm:4.28.1" - "@rollup/rollup-freebsd-x64": "npm:4.28.1" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.28.1" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.28.1" - "@rollup/rollup-linux-arm64-gnu": "npm:4.28.1" - "@rollup/rollup-linux-arm64-musl": "npm:4.28.1" - "@rollup/rollup-linux-loongarch64-gnu": "npm:4.28.1" - "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.28.1" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.28.1" - "@rollup/rollup-linux-s390x-gnu": "npm:4.28.1" - "@rollup/rollup-linux-x64-gnu": "npm:4.28.1" - "@rollup/rollup-linux-x64-musl": "npm:4.28.1" - "@rollup/rollup-win32-arm64-msvc": "npm:4.28.1" - "@rollup/rollup-win32-ia32-msvc": "npm:4.28.1" - "@rollup/rollup-win32-x64-msvc": "npm:4.28.1" + version: 4.27.4 + resolution: "rollup@npm:4.27.4" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.27.4" + "@rollup/rollup-android-arm64": "npm:4.27.4" + "@rollup/rollup-darwin-arm64": "npm:4.27.4" + "@rollup/rollup-darwin-x64": "npm:4.27.4" + "@rollup/rollup-freebsd-arm64": "npm:4.27.4" + "@rollup/rollup-freebsd-x64": "npm:4.27.4" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.27.4" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.27.4" + "@rollup/rollup-linux-arm64-gnu": "npm:4.27.4" + "@rollup/rollup-linux-arm64-musl": "npm:4.27.4" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.27.4" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.27.4" + "@rollup/rollup-linux-s390x-gnu": "npm:4.27.4" + "@rollup/rollup-linux-x64-gnu": "npm:4.27.4" + "@rollup/rollup-linux-x64-musl": "npm:4.27.4" + "@rollup/rollup-win32-arm64-msvc": "npm:4.27.4" + "@rollup/rollup-win32-ia32-msvc": "npm:4.27.4" + "@rollup/rollup-win32-x64-msvc": "npm:4.27.4" "@types/estree": "npm:1.0.6" fsevents: "npm:~2.3.2" dependenciesMeta: @@ -13023,8 +13171,6 @@ __metadata: optional: true "@rollup/rollup-linux-arm64-musl": optional: true - "@rollup/rollup-linux-loongarch64-gnu": - optional: true "@rollup/rollup-linux-powerpc64le-gnu": optional: true "@rollup/rollup-linux-riscv64-gnu": @@ -13045,7 +13191,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10/4337898d07e646835b52494b43b4ccd6929da87af2b0febc05ab217fd2425cfda05af5efaea6037c1641c90d803eb5b3e491eefdd47b28fda85af4f46a0dad34 + checksum: 10/ff7dcb877fcb6240b5135292dcb5c6aa66d06071e6570bb8aa2ce0863ae1f879e5dd04aff7ed3a77d29633da40393361553549bf819c02cc199d7be94801da66 languageName: node linkType: hard @@ -13321,13 +13467,13 @@ __metadata: linkType: hard "socks-proxy-agent@npm:^8.0.3": - version: 8.0.5 - resolution: "socks-proxy-agent@npm:8.0.5" + version: 8.0.4 + resolution: "socks-proxy-agent@npm:8.0.4" dependencies: - agent-base: "npm:^7.1.2" + agent-base: "npm:^7.1.1" debug: "npm:^4.3.4" socks: "npm:^2.8.3" - checksum: 10/ee99e1dacab0985b52cbe5a75640be6e604135e9489ebdc3048635d186012fbaecc20fbbe04b177dee434c319ba20f09b3e7dfefb7d932466c0d707744eac05c + checksum: 10/c8e7c2b398338b49a0a0f4d2bae5c0602aeeca6b478b99415927b6c5db349ca258448f2c87c6958ebf83eea17d42cbc5d1af0bfecb276cac10b9658b0f07f7d7 languageName: node linkType: hard @@ -13341,6 +13487,13 @@ __metadata: languageName: node linkType: hard +"sortablejs@npm:^1.15.2": + version: 1.15.3 + resolution: "sortablejs@npm:1.15.3" + checksum: 10/85d39a172ef47adedf273afa65daa8aefcbaafd43a5b5c480d8637add93033f5784da697d0d3545d9bb6e11fd71f1847f307ee26be452942f3785a683fd44bb5 + languageName: node + linkType: hard + "source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" @@ -13475,12 +13628,12 @@ __metadata: languageName: node linkType: hard -"ssri@npm:^12.0.0": - version: 12.0.0 - resolution: "ssri@npm:12.0.0" +"ssri@npm:^10.0.0": + version: 10.0.6 + resolution: "ssri@npm:10.0.6" dependencies: minipass: "npm:^7.0.3" - checksum: 10/7024c1a6e39b3f18aa8f1c8290e884fe91b0f9ca5a6c6d410544daad54de0ba664db879afe16412e187c6c292fd60b937f047ee44292e5c2af2dcc6d8e1a9b48 + checksum: 10/f92c1b3cc9bfd0a925417412d07d999935917bc87049f43ebec41074661d64cf720315661844106a77da9f8204b6d55ae29f9514e673083cae39464343af2a8b languageName: node linkType: hard @@ -13696,12 +13849,12 @@ __metadata: linkType: hard "strtok3@npm:^9.0.1": - version: 9.1.1 - resolution: "strtok3@npm:9.1.1" + version: 9.0.1 + resolution: "strtok3@npm:9.0.1" dependencies: "@tokenizer/token": "npm:^0.3.0" peek-readable: "npm:^5.3.1" - checksum: 10/1800693f749911bc6b4162d935690488609803825a5dcd8ef8133988b1568ae4ddf09b70b69b51bb2bf328f5b491189970287d06a482155b13fdbbef53c5756a + checksum: 10/b06488e8f3c24a70a2113eabb5b1a804b73e80a4759a827230a481abbf18dea3f994e9d79883d148053d3468170b46da7b10c04baeff6e6c2ea86b22336789bd languageName: node linkType: hard @@ -13724,11 +13877,11 @@ __metadata: linkType: hard "superjson@npm:^2.2.1": - version: 2.2.2 - resolution: "superjson@npm:2.2.2" + version: 2.2.1 + resolution: "superjson@npm:2.2.1" dependencies: copy-anything: "npm:^3.0.2" - checksum: 10/6fdc709db4f69d586a18379948e0ade8268c851c791701fea960e29cea12672d7561b4ca89c4049c2e787eb1cec08a51df51d357aa6852078bc0d71d7e17b401 + checksum: 10/bb8743a87c97f7845e0c27af1af0731d3185b32099ebce2aee0e67ac9a6ae9a7c4b9edfca7e1fe48693a78b56d5922d1cd13ef80c2fa12b788d3fc0ca25afe47 languageName: node linkType: hard @@ -13784,17 +13937,17 @@ __metadata: languageName: node linkType: hard -"tar@npm:^7.4.3": - version: 7.4.3 - resolution: "tar@npm:7.4.3" +"tar@npm:^6.1.11, tar@npm:^6.2.1": + version: 6.2.1 + resolution: "tar@npm:6.2.1" dependencies: - "@isaacs/fs-minipass": "npm:^4.0.0" - chownr: "npm:^3.0.0" - minipass: "npm:^7.1.2" - minizlib: "npm:^3.0.1" - mkdirp: "npm:^3.0.1" - yallist: "npm:^5.0.0" - checksum: 10/12a2a4fc6dee23e07cc47f1aeb3a14a1afd3f16397e1350036a8f4cdfee8dcac7ef5978337a4e7b2ac2c27a9a6d46388fc2088ea7c80cb6878c814b1425f8ecf + chownr: "npm:^2.0.0" + fs-minipass: "npm:^2.0.0" + minipass: "npm:^5.0.0" + minizlib: "npm:^2.1.1" + mkdirp: "npm:^1.0.3" + yallist: "npm:^4.0.0" + checksum: 10/bfbfbb2861888077fc1130b84029cdc2721efb93d1d1fb80f22a7ac3a98ec6f8972f29e564103bbebf5e97be67ebc356d37fa48dbc4960600a1eb7230fbd1ea0 languageName: node linkType: hard @@ -13806,8 +13959,8 @@ __metadata: linkType: hard "terser@npm:^5.31.6": - version: 5.37.0 - resolution: "terser@npm:5.37.0" + version: 5.36.0 + resolution: "terser@npm:5.36.0" dependencies: "@jridgewell/source-map": "npm:^0.3.3" acorn: "npm:^8.8.2" @@ -13815,7 +13968,7 @@ __metadata: source-map-support: "npm:~0.5.20" bin: terser: bin/terser - checksum: 10/3afacf7c38c47a5a25dbe1ba2e7aafd61166474d4377ec0af490bd41ab3686ab12679818d5fe4a3e7f76efee26f639c92ac334940c378bbc31176520a38379c3 + checksum: 10/52e641419f79d7ccdecd136b9a8e0b03f93cfe3b53cce556253aaabc347d3f2af1745419b9e622abc95d592084dc76e57774b8f9e68d29d543f4dd11c044daf4 languageName: node linkType: hard @@ -13992,23 +14145,23 @@ __metadata: linkType: hard "ts-api-utils@npm:^1.3.0": - version: 1.4.3 - resolution: "ts-api-utils@npm:1.4.3" + version: 1.4.2 + resolution: "ts-api-utils@npm:1.4.2" peerDependencies: typescript: ">=4.2.0" - checksum: 10/713c51e7392323305bd4867422ba130fbf70873ef6edbf80ea6d7e9c8f41eeeb13e40e8e7fe7cd321d74e4864777329797077268c9f570464303a1723f1eed39 + checksum: 10/9c92217d4eb9ee656f19181c412d01e7b4370faa21de1d5cc0edf972d416eeabf4422343b40c574462a27329afa8dcf9795223b253a6abc8695a079700893b64 languageName: node linkType: hard "ts-graphviz@npm:^2.1.2": - version: 2.1.5 - resolution: "ts-graphviz@npm:2.1.5" + version: 2.1.4 + resolution: "ts-graphviz@npm:2.1.4" dependencies: - "@ts-graphviz/adapter": "npm:^2.0.6" - "@ts-graphviz/ast": "npm:^2.0.6" - "@ts-graphviz/common": "npm:^2.1.5" - "@ts-graphviz/core": "npm:^2.0.6" - checksum: 10/cdc49ed88a0b2f6ea83ee616bf9daf7abbfbd585b4af619ffae1a5bb68439753690781b894cebec85f1c06808d4dc43e064639490da782e19c9c28af190bb7be + "@ts-graphviz/adapter": "npm:^2.0.5" + "@ts-graphviz/ast": "npm:^2.0.5" + "@ts-graphviz/common": "npm:^2.1.4" + "@ts-graphviz/core": "npm:^2.0.5" + checksum: 10/e912e15f0d91d822e874864728a92e846427652f9386011640afcc417000ab19b3f5c92820779c9ef395c3bd7c4543255dbf839aa58c7dc77fe9e218032332df languageName: node linkType: hard @@ -14153,9 +14306,9 @@ __metadata: linkType: hard "type-fest@npm:^4.26.1": - version: 4.30.1 - resolution: "type-fest@npm:4.30.1" - checksum: 10/0a2af4d096053b89f6c4323108feeee1865a77f7440b16e2cafef8429afa28539ff1cb99fb8703f07c3893530b1f27e9b51f344dc1ff627d7d2fe2b7c07cc6f6 + version: 4.28.1 + resolution: "type-fest@npm:4.28.1" + checksum: 10/52dcde6009dcda553a202fd4433be004c5ee6fd7dde0ef72373616f1b3bda437d45b490fbf07af8ca83b014918e3f9ce251cde546f66e70fc29974f3f917eff2 languageName: node linkType: hard @@ -14318,21 +14471,21 @@ __metadata: languageName: node linkType: hard -"unique-filename@npm:^4.0.0": - version: 4.0.0 - resolution: "unique-filename@npm:4.0.0" +"unique-filename@npm:^3.0.0": + version: 3.0.0 + resolution: "unique-filename@npm:3.0.0" dependencies: - unique-slug: "npm:^5.0.0" - checksum: 10/6a62094fcac286b9ec39edbd1f8f64ff92383baa430af303dfed1ffda5e47a08a6b316408554abfddd9730c78b6106bef4ca4d02c1231a735ddd56ced77573df + unique-slug: "npm:^4.0.0" + checksum: 10/8e2f59b356cb2e54aab14ff98a51ac6c45781d15ceaab6d4f1c2228b780193dc70fae4463ce9e1df4479cb9d3304d7c2043a3fb905bdeca71cc7e8ce27e063df languageName: node linkType: hard -"unique-slug@npm:^5.0.0": - version: 5.0.0 - resolution: "unique-slug@npm:5.0.0" +"unique-slug@npm:^4.0.0": + version: 4.0.0 + resolution: "unique-slug@npm:4.0.0" dependencies: imurmurhash: "npm:^0.1.4" - checksum: 10/beafdf3d6f44990e0a5ce560f8f881b4ee811be70b6ba0db25298c31c8cf525ed963572b48cd03be1c1349084f9e339be4241666d7cf1ebdad20598d3c652b27 + checksum: 10/40912a8963fc02fb8b600cf50197df4a275c602c60de4cac4f75879d3c48558cfac48de08a25cc10df8112161f7180b3bbb4d662aadb711568602f9eddee54f0 languageName: node linkType: hard @@ -14896,14 +15049,14 @@ __metadata: languageName: node linkType: hard -"which@npm:^5.0.0": - version: 5.0.0 - resolution: "which@npm:5.0.0" +"which@npm:^4.0.0": + version: 4.0.0 + resolution: "which@npm:4.0.0" dependencies: isexe: "npm:^3.1.1" bin: node-which: bin/which.js - checksum: 10/6ec99e89ba32c7e748b8a3144e64bfc74aa63e2b2eacbb61a0060ad0b961eb1a632b08fb1de067ed59b002cec3e21de18299216ebf2325ef0f78e0f121e14e90 + checksum: 10/f17e84c042592c21e23c8195108cff18c64050b9efb8459589116999ea9da6dd1509e6a1bac3aeebefd137be00fabbb61b5c2bc0aa0f8526f32b58ee2f545651 languageName: node linkType: hard @@ -15088,13 +15241,6 @@ __metadata: languageName: node linkType: hard -"yallist@npm:^5.0.0": - version: 5.0.0 - resolution: "yallist@npm:5.0.0" - checksum: 10/1884d272d485845ad04759a255c71775db0fac56308764b4c77ea56a20d56679fad340213054c8c9c9c26fcfd4c4b2a90df993b7e0aaf3cdb73c618d1d1a802a - languageName: node - linkType: hard - "yaml@npm:^1.7.2": version: 1.10.2 resolution: "yaml@npm:1.10.2" @@ -15102,7 +15248,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.6.0, yaml@npm:^2.6.1, yaml@npm:~2.6.1": +"yaml@npm:^2.6.0, yaml@npm:^2.6.1": version: 2.6.1 resolution: "yaml@npm:2.6.1" bin: @@ -15111,6 +15257,15 @@ __metadata: languageName: node linkType: hard +"yaml@npm:~2.5.0": + version: 2.5.1 + resolution: "yaml@npm:2.5.1" + bin: + yaml: bin.mjs + checksum: 10/0eecb679db75ea6a989ad97715a9fa5d946972945aa6aa7d2175bca66c213b5564502ccb1cdd04b1bf816ee38b5c43e4e2fda3ff6f5e09da24dabb51ae92c57d + languageName: node + linkType: hard + "yargs-parser@npm:21.1.1, yargs-parser@npm:^21.1.1": version: 21.1.1 resolution: "yargs-parser@npm:21.1.1" @@ -15207,9 +15362,9 @@ __metadata: linkType: hard "zod@npm:^3.23.8": - version: 3.24.1 - resolution: "zod@npm:3.24.1" - checksum: 10/54e25956495dec22acb9399c168c6ba657ff279801a7fcd0530c414d867f1dcca279335e160af9b138dd70c332e17d548be4bc4d2f7eaf627dead50d914fec27 + version: 3.23.8 + resolution: "zod@npm:3.23.8" + checksum: 10/846fd73e1af0def79c19d510ea9e4a795544a67d5b34b7e1c4d0425bf6bfd1c719446d94cdfa1721c1987d891321d61f779e8236fde517dc0e524aa851a6eff1 languageName: node linkType: hard