Repository navigation
feat(database): a column of the linked rows that pass a condition - #15633
ThanhDangNgo wants to merge 4 commits into
Conversation
A database row is a page, and every page in AFFiNE has an icon — except in the table, where the title cell always draws the block's own glyph. This adds an `icon` property so a row can carry its own, and renders it where a page icon belongs: inside the title cell, not in a column of its own. `spacialProperties.type` is what the title cell draws before the text, so that is where the override goes. It returns the row's icon when one is set and falls back to the block glyph otherwise, which means a table nobody has decorated looks exactly as it did before, and every view reading `iconColumn` — table, kanban, the detail panel — picks the emoji up with no further work. Clicking it opens the editor's own icon picker. The picker element is borrowed rather than copied: the callout block already owns the `icon-picker-wrapper` tag, and two classes cannot claim one tag name. `utils/database-lookup.ts` is new here because the title cell needs to find the database a row belongs to. It holds only store-level lookups — `getDatabases`, `getDatabase`, `findDatabaseContainingRow`, `getColumn` — next to the existing `block-utils.ts`, which works inside a single database.
|
Understand this PR’s impact Explore downstream dependencies and potential security impact with Blast Radius. 📝 WalkthroughWalkthroughChangesDatabase property extensions
Priority: ➖ Normal Estimated code review effort: 4 (Complex) | ~60 minutes Change: Feature Sequence Diagram(s)sequenceDiagram
participant User
participant RelationCell
participant resolveLinkedRows
participant DatabaseBlockModel
participant TargetDatabase
User->>RelationCell: configure relation or select rows
RelationCell->>resolveLinkedRows: resolve relation property
resolveLinkedRows->>DatabaseBlockModel: read relation metadata and row IDs
resolveLinkedRows->>TargetDatabase: resolve live linked rows
TargetDatabase-->>RelationCell: return linked row models
RelationCell-->>User: render relation chips
Suggested reviewers: Merge Risk: 🟡 Moderate · up to Icon editing can lose forward-compatible data and exclude keyboard-only users, while relation and rollup edge cases can show stale links or degrade database responsiveness. Resolve these issues before merging unless the risks are explicitly accepted. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Advanced
Run ID: 3b083a6c-d87b-43f1-bfd8-f72c299ed157
📒 Files selected for processing (25)
blocksuite/affine/blocks/database/src/data-source.tsblocksuite/affine/blocks/database/src/effects.tsblocksuite/affine/blocks/database/src/properties/icon/cell-renderer-css.tsblocksuite/affine/blocks/database/src/properties/icon/cell-renderer.tsblocksuite/affine/blocks/database/src/properties/icon/define.tsblocksuite/affine/blocks/database/src/properties/icon/open-picker.tsblocksuite/affine/blocks/database/src/properties/icon/read.tsblocksuite/affine/blocks/database/src/properties/icon/render.tsblocksuite/affine/blocks/database/src/properties/index.tsblocksuite/affine/blocks/database/src/properties/relation-filter/cell-renderer.tsblocksuite/affine/blocks/database/src/properties/relation-filter/compute.tsblocksuite/affine/blocks/database/src/properties/relation-filter/define.tsblocksuite/affine/blocks/database/src/properties/relation/cell-renderer-css.tsblocksuite/affine/blocks/database/src/properties/relation/cell-renderer.tsblocksuite/affine/blocks/database/src/properties/relation/chips.tsblocksuite/affine/blocks/database/src/properties/relation/define.tsblocksuite/affine/blocks/database/src/properties/relation/resolve.tsblocksuite/affine/blocks/database/src/properties/relation/reverse-define.tsblocksuite/affine/blocks/database/src/properties/rollup/cell-renderer-css.tsblocksuite/affine/blocks/database/src/properties/rollup/cell-renderer.tsblocksuite/affine/blocks/database/src/properties/rollup/compute.tsblocksuite/affine/blocks/database/src/properties/rollup/define.tsblocksuite/affine/blocks/database/src/properties/title/cell-renderer-css.tsblocksuite/affine/blocks/database/src/properties/title/text.tsblocksuite/affine/blocks/database/src/utils/database-lookup.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review.
| if (this.closePicker) { | ||
| this.closePicker(); | ||
| this.closePicker = undefined; | ||
| return; | ||
| } | ||
| this.closePicker = openIconPicker( | ||
| this.std, | ||
| (event.currentTarget as HTMLElement) ?? this, | ||
| icon => { | ||
| this.valueSetImmediate(icon ? (icon as unknown as IconValue) : null); | ||
| this.selectCurrentCell(false); | ||
| } | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Clear closePicker when the picker closes.
After selection or an outside click, openIconPicker removes the wrapper but closePicker remains assigned. The next click calls the stale close function and returns without opening the picker.
Add a close notification to openIconPicker, or clear this state from every close path.
| toString: ({ value }) => value?.unicode ?? '', | ||
| fromString: ({ value }) => ({ | ||
| value: value ? { type: 'emoji', unicode: value } : null, | ||
| }), | ||
| toJson: ({ value }) => value?.unicode ?? '', | ||
| fromJson: ({ value }) => (value ? { type: 'emoji', unicode: value } : null), |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Preserve AFFiNE icons during JSON serialization.
The picker can store { type: 'affine-icon', name, color }, but toJson writes only unicode. It therefore serializes an AFFiNE icon as an empty string. fromJson also reconstructs every non-empty value as an emoji.
Use a JSON schema and conversion that preserve the complete IconValue, or restrict the stored value to emoji data.
| private readonly setTargetDatabase = (e: Event) => { | ||
| const targetDatabaseId = (e.target as HTMLSelectElement).value; | ||
| this.property.dataUpdate(() => ({ targetDatabaseId })); | ||
| // Rows of the old target mean nothing under the new one. | ||
| this.valueSetImmediate([]); | ||
|
|
||
| // Give the other database its half straight away. A relation people have | ||
| // to wire twice is one they will wire inconsistently. | ||
| const store = this.std?.store; | ||
| const own = this.ownDatabase$.value; | ||
| if (!store || !own) return; | ||
| const target = getDatabase(store, targetDatabaseId); | ||
| if (!target) return; | ||
| ensureReverseProperty( | ||
| own, | ||
| this.cell.propertyId, | ||
| target, | ||
| (db, name, data) => { | ||
| const dataSource = this.view.manager.dataSource; | ||
| const meta = dataSource.propertyMetaGet(REVERSE_RELATION_TYPE); | ||
| if (!meta) return; | ||
| addProperty(db, 'end', { ...meta.create(name), data }); | ||
| } | ||
| ); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '30,125p' blocksuite/affine/blocks/database/src/properties/relation/resolve.ts
sed -n '105,155p' blocksuite/affine/blocks/database/src/properties/relation/cell-renderer.ts
rg -n "relation-reverse|ensureReverseProperty|propertyDelete|deleteProperty|removeProperty" blocksuite/affine/blocks/database/srcRepository: toeverything/AFFiNE
Length of output: 6165
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- property deletion implementation ---'
sed -n '410,480p' blocksuite/affine/blocks/database/src/data-source.ts
printf '%s\n' '--- relation files and tests ---'
fd -i 'relation|database.*test|property.*test' blocksuite/affine/blocks/database --type f | sort | head -200
printf '%s\n' '--- reverse/relation lifecycle references ---'
rg -n -C 4 "REVERSE_RELATION_TYPE|relation-reverse|ensureReverseProperty|propertyDelete\\(|addProperty\\(" blocksuite/affine/blocks/database --glob '!**/node_modules/**' --glob '!**/dist/**' | head -400Repository: toeverything/AFFiNE
Length of output: 24744
🏁 Script executed:
#!/bin/bash
set -e
sed -n '410,480p' blocksuite/affine/blocks/database/src/data-source.ts
fd -i 'relation|database.*test|property.*test' blocksuite/affine/blocks/database --type f | sort | head -200
rg -n -C 4 "REVERSE_RELATION_TYPE|relation-reverse|ensureReverseProperty|propertyDelete\\(|addProperty\\(" blocksuite/affine/blocks/database --glob '!**/node_modules/**' --glob '!**/dist/**' | head -400Repository: toeverything/AFFiNE
Length of output: 24624
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- relation renderer value update context ---'
sed -n '1,190p' blocksuite/affine/blocks/database/src/properties/relation/cell-renderer.ts
printf '%s\n' '--- valueSetImmediate definitions and uses ---'
rg -n -C 5 "valueSetImmediate|dataUpdate\\(" blocksuite/affine/blocks/database/src blocksuite/affine | head -300Repository: toeverything/AFFiNE
Length of output: 35836
Remove the old reverse property when the target changes.
When targetDatabaseId changes or is cleared, this handler does not remove the reverse property from the previous target. The previous database keeps a visible relation-reverse column that still scans the source relation. Because valueSetImmediate([]) clears only the current cell, that column can expose stale links from other source rows.
Capture the previous target before dataUpdate. During the target change, remove the reverse property matching both the source database and this.cell.propertyId through propertyDelete. Do not remove it when the target remains unchanged.
| const values = rowIds.map(id => | ||
| readValue(store, target, id, targetPropertyId, depth + 1, nextSeen) | ||
| ); |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift
🔎 Supported by static analysis
🏁 Script executed:
sed -n '43,125p' blocksuite/affine/blocks/database/src/properties/rollup/compute.ts
sed -n '105,145p' blocksuite/affine/blocks/database/src/data-source.ts
rg -n "computeRollup|MAX_RELATION_DEPTH" blocksuite/affine/blocks/database/srcRepository: toeverything/AFFiNE
Length of output: 5594
🏁 Script executed:
set -e
printf '%s\n' '--- relation resolver ---'
cat -n blocksuite/affine/blocks/database/src/properties/relation/resolve.ts
printf '%s\n' '--- rollup implementation ---'
cat -n blocksuite/affine/blocks/database/src/properties/rollup/compute.ts
printf '%s\n' '--- relation-filter implementation ---'
cat -n blocksuite/affine/blocks/database/src/properties/relation-filter/compute.ts
printf '%s\n' '--- related definitions and call sites ---'
rg -n -C 3 "resolveLinkedRows|type Linked|interface Linked|rowIds|relationPropertyId|targetPropertyId|valueGet\\(" blocksuite/affine/blocks/database/src/properties blocksuite/affine/blocks/database/src | head -240
printf '%s\n' '--- cache and tests ---'
rg -n -i "memo|cache|computeRollup|rollup" blocksuite/affine/blocks/database/src blocksuite/affine/blocks/database/test blocksuite/affine/blocks/database/tests 2>/dev/null | head -260Repository: toeverything/AFFiNE
Length of output: 44508
🏁 Script executed:
set -e
cat -n blocksuite/affine/blocks/database/src/properties/relation/resolve.ts
cat -n blocksuite/affine/blocks/database/src/properties/relation-filter/compute.ts
rg -n -C 3 "resolveLinkedRows|computeRollup|rowIds|relationPropertyId|targetPropertyId" blocksuite/affine/blocks/database/src/properties blocksuite/affine/blocks/database/src | head -260
rg -n -i "memo|cache|computeRollup|rollup" blocksuite/affine/blocks/database/src blocksuite/affine/blocks/database/test blocksuite/affine/blocks/database/tests 2>/dev/null | head -260Repository: toeverything/AFFiNE
Length of output: 40621
Memoize repeated non-count rollup nodes within one traversal.
When linked rows overlap across a rollup-of-rollup chain, seen prevents cycles but does not reuse completed values. The same { database, row, property } node can therefore be evaluated repeatedly. With fan-out b, this can produce O(b⁸) recursive calls before MAX_RELATION_DEPTH stops traversal and can make derived editor reads expensive.
Use a traversal-local memo, but do not key it only by the node key. The result can also depend on depth and the path-local seen set when a descendant reaches an ancestor. Cache only results that are independent of that path context, or include the relevant context in the memo key while retaining seen for cycle detection.
AFFiNE's database block has no way to point one table at another, so a structure whose levels depend on each other cannot be expressed in it. Issue toeverything#14068 asks for exactly this and is still open. This adds two property types. `relation` is stored: an array of row ids pointing into another database on the same page, rendered as chips that carry each target row's icon and title. `relation-reverse` is its other half and stores nothing — it scans the owning side for rows that point back. Creating a relation creates the reverse property on the target automatically, so linking two databases gives both directions at once instead of leaving the second to be wired by hand. The reverse side is derived rather than stored on purpose. Two stored arrays can drift apart; one array with a computed mirror cannot. It is registered in `spacialProperties`, the mechanism upstream already uses for `created-time` and `title`, which routes it through `cellValueGet` — so filters, sorts and the column footer see the same value the cell shows, and the signals read while resolving make it re-evaluate on its own. Storing it would have meant inventing an invalidation scheme and getting it wrong. Deleted rows are dropped when resolving rather than left to each caller, and `findOrphanIds` reports ids that no longer resolve so the editor can show them.
With relations in place, a column can now aggregate a property of the rows it reaches: count, countChecked, percentChecked, sum, avg, min, max. It renders as a number, or as a filled bar when the result is a percentage. A rollup may target another rollup, which is what lets a figure climb several levels — Tasks to Goals to Yearly Goals to Pillars, four hops, each recomputed the moment a checkbox changes. Following a chain means a cycle is possible (A rolls up B rolls up A), so the walk carries a visited set and stops at `MAX_RELATION_DEPTH` rather than hanging the editor. Like `relation-reverse`, this is derived and registered in `spacialProperties`, so filters, sorts and the column footer see the same value the cell shows and the signals read while computing make it re-evaluate on its own. `ROLLUP_TYPE` sits in `compute.ts` rather than `define.ts` so that code which only needs to recognise a rollup while walking a chain does not pull in the renderer and its lit dependencies.
A rollup answers "how many of these are done". This answers "which ones", rendering the linked rows that satisfy a condition as chips instead of reducing them to a number — a "Done Tasks" column lists the finished tasks rather than counting them. It walks the same chain a rollup does and understands rollups along the way, so a filter can sit above one. Derived and registered in `spacialProperties` for the same reasons as the others.
b71f243 to
96bd732
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Advanced
Run ID: a6a78459-b180-49a3-a587-a390134db027
📒 Files selected for processing (3)
blocksuite/affine/blocks/database/src/properties/icon/cell-renderer.tsblocksuite/affine/blocks/database/src/properties/icon/define.tsblocksuite/affine/blocks/database/src/properties/icon/open-picker.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 2 remain after this review.
| return html`<span | ||
| class="${glyph ? iconGlyphStyle : iconEmptyStyle}" | ||
| @click="${this.openPicker}" | ||
| >${glyph ?? '+'}</span |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '1,110p' blocksuite/affine/blocks/database/src/properties/icon/cell-renderer.ts
rg -n "class BaseCellRenderer|keydown|Enter|Space|selectCurrentCell|beforeEnterEditingMode" blocksuite/affine/data-view/src blocksuite/affine/blocks/database/src | head -240Repository: toeverything/AFFiNE
Length of output: 16789
🏁 Script executed:
sed -n '1,180p' blocksuite/affine/data-view/src/core/property/base-cell.ts
printf '\n--- table pc cell ---\n'
sed -n '1,180p' blocksuite/affine/data-view/src/view-presets/table/pc/cell.ts
printf '\n--- table virtual cell ---\n'
sed -n '1,210p' blocksuite/affine/data-view/src/view-presets/table/pc-virtual/row/cell.ts
printf '\n--- kanban pc cell ---\n'
sed -n '1,155p' blocksuite/affine/data-view/src/view-presets/kanban/pc/cell.ts
printf '\n--- table hotkeys ---\n'
sed -n '1,190p' blocksuite/affine/data-view/src/view-presets/table/pc/controller/hotkeys.ts
printf '\n--- virtual hotkeys ---\n'
sed -n '1,190p' blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/hotkeys.tsRepository: toeverything/AFFiNE
Length of output: 30608
🏁 Script executed:
sed -n '1,180p' blocksuite/affine/data-view/src/core/property/base-cell.ts
sed -n '1,180p' blocksuite/affine/data-view/src/view-presets/table/pc/cell.ts
sed -n '1,210p' blocksuite/affine/data-view/src/view-presets/table/pc-virtual/row/cell.ts
sed -n '1,155p' blocksuite/affine/data-view/src/view-presets/kanban/pc/cell.ts
sed -n '1,190p' blocksuite/affine/data-view/src/view-presets/table/pc/controller/hotkeys.ts
sed -n '1,190p' blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/hotkeys.tsRepository: toeverything/AFFiNE
Length of output: 30485
Make the icon picker trigger keyboard operable.
Keyboard interaction can select the containing cell and enter editing mode, but it does not invoke IconCell.openPicker. The span itself has only a click handler and is not focusable. Use a labeled button, or add button semantics with tabindex="0" and Enter/Space handling.
| export const IconValueSchema = zod | ||
| .object({ | ||
| type: zod.string(), | ||
| unicode: zod.string().optional(), | ||
| name: zod.string().optional(), | ||
| color: zod.string().optional(), | ||
| }) | ||
| .nullable(); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
rg -n "IconValueSchema|encodeIconValue|decodeIconValue|IconData|affine-icon|iconPickerComponent" blocksuite/affine packages 2>/dev/null | head -200
sed -n '1,100p' blocksuite/affine/blocks/database/src/properties/icon/define.tsRepository: toeverything/AFFiNE
Length of output: 23438
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- picker type and producers ---'
cat -n packages/frontend/component/src/ui/icon-picker/type.ts
sed -n '1,240p' packages/frontend/component/src/ui/icon-picker/picker/affine-icon/affine-icon-picker.tsx
sed -n '1,180p' packages/frontend/component/src/ui/icon-picker/picker/emoji/emoji-picker.tsx 2>/dev/null || true
printf '%s\n' '--- shared service type ---'
cat -n blocksuite/affine/shared/src/services/icon-picker-service/index.ts
printf '%s\n' '--- database icon paths ---'
cat -n blocksuite/affine/blocks/database/src/properties/icon/define.ts
cat -n blocksuite/affine/blocks/database/src/properties/icon/render.ts
printf '%s\n' '--- consumers and tests ---'
rg -n -C 5 "fromJson|toJson|decodeIconValue|IconValue|rawValue|IconData" blocksuite/affine/blocks/database/src/properties/icon blocksuite/affine/data-view packages/frontend/core/src/modules/db/schema packages/frontend/component/src/ui/icon-picker -g '*.{ts,tsx}' | head -260Repository: toeverything/AFFiNE
Length of output: 39024
🏁 Script executed:
set -o pipefail
printf '%s\n' '--- picker type and producers ---'
cat -n packages/frontend/component/src/ui/icon-picker/type.ts
sed -n '1,240p' packages/frontend/component/src/ui/icon-picker/picker/affine-icon/affine-icon-picker.tsx
printf '%s\n' '--- shared service type ---'
cat -n blocksuite/affine/shared/src/services/icon-picker-service/index.ts
printf '%s\n' '--- database icon paths ---'
cat -n blocksuite/affine/blocks/database/src/properties/icon/define.ts
cat -n blocksuite/affine/blocks/database/src/properties/icon/render.ts
printf '%s\n' '--- consumers and tests ---'
rg -n -C 5 "fromJson|toJson|decodeIconValue|IconValue|rawValue|IconData" blocksuite/affine/blocks/database/src/properties/icon blocksuite/affine/data-view packages/frontend/core/src/modules/db/schema packages/frontend/component/src/ui/icon-picker -g '*.{ts,tsx}' | head -260Repository: toeverything/AFFiNE
Length of output: 36192
🏁 Script executed:
printf '%s\n' '--- icon picker output binding ---'
cat -n packages/frontend/component/src/ui/icon-picker/icon-picker.tsx
printf '%s\n' '--- blob icon references ---'
rg -n -C 3 "IconType\.Blob|type:\s*['\"]blob['\"]|blob:\s*" packages/frontend blocksuite/affine --glob '*.{ts,tsx}' | head -180Repository: toeverything/AFFiNE
Length of output: 16980
Preserve unknown icon fields in IconValueSchema.
fromJson calls decodeIconValue(), which parses JSON with IconValueSchema.safeParse(). The default zod.object(...) behavior strips keys that the schema does not declare. A newer picker value can therefore lose fields during deserialization.
Use .passthrough() before .nullable().
Proposed fix
export const IconValueSchema = zod
.object({
type: zod.string(),
unicode: zod.string().optional(),
name: zod.string().optional(),
color: zod.string().optional(),
})
+ .passthrough()
.nullable();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const IconValueSchema = zod | |
| .object({ | |
| type: zod.string(), | |
| unicode: zod.string().optional(), | |
| name: zod.string().optional(), | |
| color: zod.string().optional(), | |
| }) | |
| .nullable(); | |
| export const IconValueSchema = zod | |
| .object({ | |
| type: zod.string(), | |
| unicode: zod.string().optional(), | |
| name: zod.string().optional(), | |
| color: zod.string().optional(), | |
| }) | |
| .passthrough() | |
| .nullable(); |
| borderRadius: '8px', | ||
| }); | ||
| document.body.append(wrapper); | ||
| setTimeout(() => document.addEventListener('click', onOutside, true), 0); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Do not register the outside-click listener after closure.
If the caller invokes close() before the timer runs, close() removes a listener that does not exist yet. The queued callback then installs the listener. Later calls to close() return because closed is true, so the document listener remains registered.
Guard the deferred registration with closed.
Proposed fix
- setTimeout(() => document.addEventListener('click', onOutside, true), 0);
+ setTimeout(() => {
+ if (!closed) {
+ document.addEventListener('click', onOutside, true);
+ }
+ }, 0);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| setTimeout(() => document.addEventListener('click', onOutside, true), 0); | |
| setTimeout(() => { | |
| if (!closed) { | |
| document.addEventListener('click', onOutside, true); | |
| } | |
| }, 0); |
|
well thank you for relations and rollup job. Hope they will add it ASAP, as it's a long time awaited feature. I did it with claude code on my _ynh package. Don't have the knowledge to publish anything. Just if someone wanna check a different path. |
A rollup answers how many of these are done. This answers which ones, rendering the linked rows that satisfy a condition as chips instead of reducing them to a number — a "Finished" column lists the completed tasks rather than counting them.
It walks the same chain a rollup does and understands rollups along the way, so a filter can sit above one. Derived and registered in
spacialPropertiesfor the same reasons as the others.Try it in about thirty seconds
Open any doc, add one database (slash menu → Table View), then paste this into the browser console:
Demo script — a Finished column listing the done tasks as chips
Tick or untick a task and the chip appears or disappears immediately, with nothing stored on the parent. The condition can be switched to
unchecked, or to one of the numeric ones with a threshold.Notes
yarn typecheckgives the same error count ascanarydoes on this machine (a clean, cache-cleared run on both).Summary by CodeRabbit