makersclaw 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +180 -0
- package/package.json +3 -2
- package/r/annotate.json +62 -0
- package/r/approval-request.json +52 -0
- package/r/audio-player.json +74 -0
- package/r/auth-form.json +55 -0
- package/r/board.json +10 -6
- package/r/calendar-view.json +105 -0
- package/r/cart.json +62 -0
- package/r/chart.json +83 -0
- package/r/chat.json +95 -0
- package/r/code-editor.json +94 -0
- package/r/comment-box.json +64 -0
- package/r/contact-section.json +53 -0
- package/r/core.json +205 -8
- package/r/csv-import.json +63 -0
- package/r/data-table.json +69 -0
- package/r/diagram.json +74 -0
- package/r/doc-editor.json +82 -0
- package/r/doc-page.json +61 -0
- package/r/email-render.json +73 -0
- package/r/faq.json +50 -0
- package/r/file-upload.json +54 -0
- package/r/flow-canvas.json +93 -0
- package/r/footer.json +52 -0
- package/r/gantt.json +70 -0
- package/r/job-panel.json +12 -7
- package/r/json-editor.json +66 -0
- package/r/map.json +89 -0
- package/r/menubar.json +52 -0
- package/r/mind-map.json +67 -0
- package/r/navigation-menu.json +52 -0
- package/r/order-detail.json +54 -0
- package/r/org-chart.json +66 -0
- package/r/page-builder.json +105 -0
- package/r/payment-methods.json +51 -0
- package/r/pdf-document.json +79 -0
- package/r/pdf-viewer.json +88 -0
- package/r/product-detail.json +76 -0
- package/r/product-grid.json +65 -0
- package/r/record-form.json +67 -0
- package/r/record-notes.json +55 -0
- package/r/registry.json +3411 -29
- package/r/search-dialog.json +51 -0
- package/r/settings-list.json +66 -0
- package/r/setup-checklist.json +51 -0
- package/r/share-dialog.json +52 -0
- package/r/sidebar.json +69 -0
- package/r/signature-pad.json +72 -0
- package/r/sketch-canvas.json +99 -0
- package/r/spreadsheet.json +90 -0
- package/r/stat-cards.json +66 -0
- package/r/task-list.json +63 -0
- package/r/task-model.json +98 -0
- package/r/testimonials.json +52 -0
- package/r/transaction-list.json +53 -0
- package/r/whiteboard.json +66 -0
- package/registry/blocks/annotate/README.md +179 -0
- package/registry/blocks/annotate/annotate-data.js +250 -0
- package/registry/blocks/annotate/annotate.jsx +646 -0
- package/registry/blocks/annotate/block.json +10 -0
- package/registry/blocks/annotate/demo.jsx +135 -0
- package/registry/blocks/annotate/index.js +15 -0
- package/registry/blocks/approval-request/README.md +116 -0
- package/registry/blocks/approval-request/approval-request.jsx +386 -0
- package/registry/blocks/approval-request/block.json +10 -0
- package/registry/blocks/approval-request/demo.jsx +127 -0
- package/registry/blocks/approval-request/index.js +4 -0
- package/registry/blocks/audio-player/README.md +167 -0
- package/registry/blocks/audio-player/audio-player.jsx +623 -0
- package/registry/blocks/audio-player/block.json +14 -0
- package/registry/blocks/audio-player/colors.js +109 -0
- package/registry/blocks/audio-player/demo.jsx +209 -0
- package/registry/blocks/audio-player/index.js +6 -0
- package/registry/blocks/audio-player/time.js +67 -0
- package/registry/blocks/audio-player/wavesurfer.js +9 -0
- package/registry/blocks/auth-form/README.md +126 -0
- package/registry/blocks/auth-form/auth-form.jsx +568 -0
- package/registry/blocks/auth-form/block.json +10 -0
- package/registry/blocks/auth-form/demo.jsx +131 -0
- package/registry/blocks/auth-form/index.js +3 -0
- package/registry/blocks/board/README.md +55 -5
- package/registry/blocks/board/block.json +2 -2
- package/registry/blocks/board/board.jsx +230 -86
- package/registry/blocks/board/demo.jsx +103 -24
- package/registry/blocks/calendar-view/README.md +180 -0
- package/registry/blocks/calendar-view/agenda.jsx +147 -0
- package/registry/blocks/calendar-view/block.json +76 -0
- package/registry/blocks/calendar-view/calendar-model.js +442 -0
- package/registry/blocks/calendar-view/calendar.jsx +627 -0
- package/registry/blocks/calendar-view/demo.jsx +195 -0
- package/registry/blocks/calendar-view/draft.jsx +49 -0
- package/registry/blocks/calendar-view/index.js +39 -0
- package/registry/blocks/calendar-view/month.jsx +346 -0
- package/registry/blocks/calendar-view/paint.js +69 -0
- package/registry/blocks/calendar-view/time-grid.jsx +527 -0
- package/registry/blocks/calendar-view/tray.jsx +87 -0
- package/registry/blocks/cart/README.md +146 -0
- package/registry/blocks/cart/block.json +10 -0
- package/registry/blocks/cart/cart.jsx +597 -0
- package/registry/blocks/cart/demo.jsx +98 -0
- package/registry/blocks/cart/index.js +4 -0
- package/registry/blocks/cart/money.js +28 -0
- package/registry/blocks/chart/README.md +316 -0
- package/registry/blocks/chart/block.json +10 -0
- package/registry/blocks/chart/cartesian.jsx +642 -0
- package/registry/blocks/chart/core.jsx +262 -0
- package/registry/blocks/chart/demo.jsx +357 -0
- package/registry/blocks/chart/heatmap.jsx +336 -0
- package/registry/blocks/chart/index.js +8 -0
- package/registry/blocks/chart/pie.jsx +245 -0
- package/registry/blocks/chart/sparkline.jsx +88 -0
- package/registry/blocks/chat/README.md +273 -0
- package/registry/blocks/chat/attachment.jsx +95 -0
- package/registry/blocks/chat/block.json +10 -0
- package/registry/blocks/chat/chat.jsx +132 -0
- package/registry/blocks/chat/composer.jsx +364 -0
- package/registry/blocks/chat/demo.jsx +578 -0
- package/registry/blocks/chat/header.jsx +34 -0
- package/registry/blocks/chat/index.js +9 -0
- package/registry/blocks/chat/message.jsx +450 -0
- package/registry/blocks/chat/scroller.jsx +247 -0
- package/registry/blocks/chat/text.jsx +104 -0
- package/registry/blocks/code-editor/README.md +210 -0
- package/registry/blocks/code-editor/block.json +22 -0
- package/registry/blocks/code-editor/code-editor.jsx +534 -0
- package/registry/blocks/code-editor/codemirror.js +38 -0
- package/registry/blocks/code-editor/demo.jsx +205 -0
- package/registry/blocks/code-editor/index.js +8 -0
- package/registry/blocks/code-editor/json.js +317 -0
- package/registry/blocks/code-editor/sql.js +136 -0
- package/registry/blocks/code-editor/text.js +101 -0
- package/registry/blocks/code-editor/theme.js +186 -0
- package/registry/blocks/comment-box/README.md +123 -0
- package/registry/blocks/comment-box/block.json +10 -0
- package/registry/blocks/comment-box/comment-body.jsx +217 -0
- package/registry/blocks/comment-box/comment-box.jsx +198 -0
- package/registry/blocks/comment-box/comment-time.jsx +77 -0
- package/registry/blocks/comment-box/demo.jsx +141 -0
- package/registry/blocks/comment-box/index.js +8 -0
- package/registry/blocks/contact-section/README.md +152 -0
- package/registry/blocks/contact-section/block.json +11 -0
- package/registry/blocks/contact-section/contact-section.jsx +509 -0
- package/registry/blocks/contact-section/demo.jsx +93 -0
- package/registry/blocks/contact-section/index.js +4 -0
- package/registry/blocks/csv-import/README.md +133 -0
- package/registry/blocks/csv-import/block.json +13 -0
- package/registry/blocks/csv-import/csv-import.jsx +479 -0
- package/registry/blocks/csv-import/demo.jsx +106 -0
- package/registry/blocks/csv-import/import.js +200 -0
- package/registry/blocks/csv-import/index.js +7 -0
- package/registry/blocks/data-table/README.md +386 -0
- package/registry/blocks/data-table/block.json +10 -0
- package/registry/blocks/data-table/data-table.jsx +1164 -0
- package/registry/blocks/data-table/demo.jsx +588 -0
- package/registry/blocks/data-table/index.js +5 -0
- package/registry/blocks/data-table/model.js +169 -0
- package/registry/blocks/diagram/README.md +108 -0
- package/registry/blocks/diagram/block.json +13 -0
- package/registry/blocks/diagram/demo.jsx +113 -0
- package/registry/blocks/diagram/diagram.jsx +481 -0
- package/registry/blocks/diagram/index.js +7 -0
- package/registry/blocks/diagram/mermaid.js +52 -0
- package/registry/blocks/diagram/palette.js +313 -0
- package/registry/blocks/diagram/source.js +98 -0
- package/registry/blocks/doc-editor/README.md +215 -0
- package/registry/blocks/doc-editor/block.json +15 -0
- package/registry/blocks/doc-editor/demo.jsx +225 -0
- package/registry/blocks/doc-editor/doc-editor.jsx +291 -0
- package/registry/blocks/doc-editor/doc.js +309 -0
- package/registry/blocks/doc-editor/index.js +7 -0
- package/registry/blocks/doc-editor/schema.jsx +56 -0
- package/registry/blocks/doc-editor/shadcn-classes.js +110 -0
- package/registry/blocks/doc-editor/theme.js +82 -0
- package/registry/blocks/doc-page/README.md +119 -0
- package/registry/blocks/doc-page/autosave.js +104 -0
- package/registry/blocks/doc-page/block.json +10 -0
- package/registry/blocks/doc-page/demo.jsx +137 -0
- package/registry/blocks/doc-page/doc-page.jsx +276 -0
- package/registry/blocks/doc-page/index.js +4 -0
- package/registry/blocks/email-render/README.md +151 -0
- package/registry/blocks/email-render/block.json +14 -0
- package/registry/blocks/email-render/demo.jsx +153 -0
- package/registry/blocks/email-render/doc-email.jsx +210 -0
- package/registry/blocks/email-render/email.jsx +221 -0
- package/registry/blocks/email-render/index.js +10 -0
- package/registry/blocks/email-render/render.js +50 -0
- package/registry/blocks/email-render/theme.js +215 -0
- package/registry/blocks/faq/README.md +101 -0
- package/registry/blocks/faq/block.json +11 -0
- package/registry/blocks/faq/demo.jsx +89 -0
- package/registry/blocks/faq/faq.jsx +220 -0
- package/registry/blocks/faq/index.js +4 -0
- package/registry/blocks/file-upload/README.md +138 -0
- package/registry/blocks/file-upload/block.json +10 -0
- package/registry/blocks/file-upload/demo.jsx +204 -0
- package/registry/blocks/file-upload/file-upload.jsx +635 -0
- package/registry/blocks/file-upload/index.js +4 -0
- package/registry/blocks/flow-canvas/README.md +420 -0
- package/registry/blocks/flow-canvas/block.json +13 -0
- package/registry/blocks/flow-canvas/board.js +819 -0
- package/registry/blocks/flow-canvas/chrome.jsx +649 -0
- package/registry/blocks/flow-canvas/context.jsx +35 -0
- package/registry/blocks/flow-canvas/demo.jsx +249 -0
- package/registry/blocks/flow-canvas/edges.jsx +107 -0
- package/registry/blocks/flow-canvas/flow-canvas.jsx +1329 -0
- package/registry/blocks/flow-canvas/index.js +41 -0
- package/registry/blocks/flow-canvas/nodes.jsx +392 -0
- package/registry/blocks/flow-canvas/theme.js +174 -0
- package/registry/blocks/footer/README.md +133 -0
- package/registry/blocks/footer/block.json +11 -0
- package/registry/blocks/footer/demo.jsx +119 -0
- package/registry/blocks/footer/footer.jsx +406 -0
- package/registry/blocks/footer/index.js +4 -0
- package/registry/blocks/gantt/README.md +220 -0
- package/registry/blocks/gantt/block.json +10 -0
- package/registry/blocks/gantt/demo.jsx +148 -0
- package/registry/blocks/gantt/gantt.jsx +923 -0
- package/registry/blocks/gantt/index.js +7 -0
- package/registry/blocks/gantt/rows.jsx +468 -0
- package/registry/blocks/gantt/schedule.js +278 -0
- package/registry/blocks/job-panel/README.md +80 -0
- package/registry/blocks/job-panel/block.json +3 -3
- package/registry/blocks/job-panel/demo.jsx +230 -3
- package/registry/blocks/job-panel/job-panel.jsx +169 -3
- package/registry/blocks/json-editor/README.md +133 -0
- package/registry/blocks/json-editor/block.json +10 -0
- package/registry/blocks/json-editor/demo.jsx +92 -0
- package/registry/blocks/json-editor/index.js +7 -0
- package/registry/blocks/json-editor/json-editor.jsx +305 -0
- package/registry/blocks/json-editor/json-tree.jsx +393 -0
- package/registry/blocks/json-editor/tree.js +225 -0
- package/registry/blocks/map/README.md +221 -0
- package/registry/blocks/map/block.json +14 -0
- package/registry/blocks/map/demo.jsx +201 -0
- package/registry/blocks/map/geo.js +198 -0
- package/registry/blocks/map/index.js +8 -0
- package/registry/blocks/map/list.jsx +128 -0
- package/registry/blocks/map/map-view.jsx +715 -0
- package/registry/blocks/map/maplibre.js +30 -0
- package/registry/blocks/map/marks.jsx +146 -0
- package/registry/blocks/map/theme.js +142 -0
- package/registry/blocks/menubar/README.md +109 -0
- package/registry/blocks/menubar/block.json +10 -0
- package/registry/blocks/menubar/demo.jsx +128 -0
- package/registry/blocks/menubar/index.js +4 -0
- package/registry/blocks/menubar/menubar.jsx +199 -0
- package/registry/blocks/mind-map/README.md +206 -0
- package/registry/blocks/mind-map/block.json +10 -0
- package/registry/blocks/mind-map/demo.jsx +103 -0
- package/registry/blocks/mind-map/index.js +31 -0
- package/registry/blocks/mind-map/mind-map.jsx +1125 -0
- package/registry/blocks/mind-map/topic.jsx +143 -0
- package/registry/blocks/mind-map/tree.js +645 -0
- package/registry/blocks/navigation-menu/README.md +124 -0
- package/registry/blocks/navigation-menu/block.json +10 -0
- package/registry/blocks/navigation-menu/demo.jsx +84 -0
- package/registry/blocks/navigation-menu/index.js +4 -0
- package/registry/blocks/navigation-menu/navigation-menu.jsx +448 -0
- package/registry/blocks/order-detail/README.md +109 -0
- package/registry/blocks/order-detail/block.json +10 -0
- package/registry/blocks/order-detail/demo.jsx +88 -0
- package/registry/blocks/order-detail/index.js +4 -0
- package/registry/blocks/order-detail/order-detail.jsx +354 -0
- package/registry/blocks/org-chart/README.md +145 -0
- package/registry/blocks/org-chart/block.json +10 -0
- package/registry/blocks/org-chart/demo.jsx +94 -0
- package/registry/blocks/org-chart/index.js +7 -0
- package/registry/blocks/org-chart/org-chart.jsx +631 -0
- package/registry/blocks/org-chart/org.js +245 -0
- package/registry/blocks/org-chart/person.jsx +96 -0
- package/registry/blocks/page-builder/README.md +257 -0
- package/registry/blocks/page-builder/block.json +14 -0
- package/registry/blocks/page-builder/demo.jsx +145 -0
- package/registry/blocks/page-builder/grapes.js +18 -0
- package/registry/blocks/page-builder/index.js +11 -0
- package/registry/blocks/page-builder/page-builder.jsx +704 -0
- package/registry/blocks/page-builder/page-view.jsx +33 -0
- package/registry/blocks/page-builder/page.js +118 -0
- package/registry/blocks/page-builder/panels.jsx +588 -0
- package/registry/blocks/page-builder/sections.js +209 -0
- package/registry/blocks/page-builder/theme.js +87 -0
- package/registry/blocks/page-builder/thumbs.jsx +189 -0
- package/registry/blocks/page-builder/tokens.js +329 -0
- package/registry/blocks/payment-methods/README.md +104 -0
- package/registry/blocks/payment-methods/block.json +10 -0
- package/registry/blocks/payment-methods/demo.jsx +71 -0
- package/registry/blocks/payment-methods/index.js +4 -0
- package/registry/blocks/payment-methods/payment-methods.jsx +302 -0
- package/registry/blocks/pdf-document/README.md +213 -0
- package/registry/blocks/pdf-document/block.json +13 -0
- package/registry/blocks/pdf-document/demo.jsx +136 -0
- package/registry/blocks/pdf-document/index.js +33 -0
- package/registry/blocks/pdf-document/money.js +83 -0
- package/registry/blocks/pdf-document/pdf-document.jsx +144 -0
- package/registry/blocks/pdf-document/primitives.jsx +271 -0
- package/registry/blocks/pdf-document/quote.jsx +179 -0
- package/registry/blocks/pdf-document/theme.js +193 -0
- package/registry/blocks/pdf-viewer/README.md +191 -0
- package/registry/blocks/pdf-viewer/block.json +14 -0
- package/registry/blocks/pdf-viewer/demo.jsx +252 -0
- package/registry/blocks/pdf-viewer/helpers.js +270 -0
- package/registry/blocks/pdf-viewer/index.js +8 -0
- package/registry/blocks/pdf-viewer/notes.jsx +195 -0
- package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1368 -0
- package/registry/blocks/pdf-viewer/pdfjs.js +37 -0
- package/registry/blocks/pdf-viewer/ssr-shim-end.js +6 -0
- package/registry/blocks/pdf-viewer/ssr-shim.js +44 -0
- package/registry/blocks/product-detail/README.md +217 -0
- package/registry/blocks/product-detail/block.json +10 -0
- package/registry/blocks/product-detail/demo.jsx +233 -0
- package/registry/blocks/product-detail/index.js +7 -0
- package/registry/blocks/product-detail/price.jsx +115 -0
- package/registry/blocks/product-detail/product-detail.jsx +348 -0
- package/registry/blocks/product-detail/product-media.jsx +102 -0
- package/registry/blocks/product-detail/product-options.jsx +144 -0
- package/registry/blocks/product-grid/README.md +162 -0
- package/registry/blocks/product-grid/block.json +10 -0
- package/registry/blocks/product-grid/demo.jsx +255 -0
- package/registry/blocks/product-grid/index.js +5 -0
- package/registry/blocks/product-grid/product-card.jsx +232 -0
- package/registry/blocks/product-grid/product-grid.jsx +235 -0
- package/registry/blocks/record-form/README.md +196 -0
- package/registry/blocks/record-form/block.json +10 -0
- package/registry/blocks/record-form/demo.jsx +525 -0
- package/registry/blocks/record-form/index.js +4 -0
- package/registry/blocks/record-form/record-fields.jsx +457 -0
- package/registry/blocks/record-form/record-form.jsx +415 -0
- package/registry/blocks/record-form/rules.js +81 -0
- package/registry/blocks/record-notes/README.md +139 -0
- package/registry/blocks/record-notes/block.json +10 -0
- package/registry/blocks/record-notes/demo.jsx +179 -0
- package/registry/blocks/record-notes/index.js +4 -0
- package/registry/blocks/record-notes/record-notes.jsx +266 -0
- package/registry/blocks/search-dialog/README.md +108 -0
- package/registry/blocks/search-dialog/block.json +10 -0
- package/registry/blocks/search-dialog/demo.jsx +116 -0
- package/registry/blocks/search-dialog/index.js +3 -0
- package/registry/blocks/search-dialog/search-dialog.jsx +235 -0
- package/registry/blocks/settings-list/README.md +171 -0
- package/registry/blocks/settings-list/block.json +10 -0
- package/registry/blocks/settings-list/demo.jsx +333 -0
- package/registry/blocks/settings-list/index.js +4 -0
- package/registry/blocks/settings-list/row-state.jsx +93 -0
- package/registry/blocks/settings-list/settings-list.jsx +349 -0
- package/registry/blocks/settings-list/settings-matrix.jsx +118 -0
- package/registry/blocks/setup-checklist/README.md +142 -0
- package/registry/blocks/setup-checklist/block.json +10 -0
- package/registry/blocks/setup-checklist/demo.jsx +159 -0
- package/registry/blocks/setup-checklist/index.js +4 -0
- package/registry/blocks/setup-checklist/setup-checklist.jsx +446 -0
- package/registry/blocks/share-dialog/README.md +113 -0
- package/registry/blocks/share-dialog/block.json +10 -0
- package/registry/blocks/share-dialog/demo.jsx +140 -0
- package/registry/blocks/share-dialog/index.js +3 -0
- package/registry/blocks/share-dialog/share-dialog.jsx +384 -0
- package/registry/blocks/sidebar/README.md +190 -0
- package/registry/blocks/sidebar/block.json +43 -0
- package/registry/blocks/sidebar/demo.jsx +577 -0
- package/registry/blocks/sidebar/index.js +4 -0
- package/registry/blocks/sidebar/sidebar-panel.jsx +149 -0
- package/registry/blocks/sidebar/sidebar.jsx +864 -0
- package/registry/blocks/signature-pad/README.md +137 -0
- package/registry/blocks/signature-pad/block.json +13 -0
- package/registry/blocks/signature-pad/colors.js +115 -0
- package/registry/blocks/signature-pad/demo.jsx +94 -0
- package/registry/blocks/signature-pad/index.js +6 -0
- package/registry/blocks/signature-pad/pad.js +7 -0
- package/registry/blocks/signature-pad/signature-pad.jsx +434 -0
- package/registry/blocks/signature-pad/signature.js +68 -0
- package/registry/blocks/sketch-canvas/README.md +280 -0
- package/registry/blocks/sketch-canvas/block.json +13 -0
- package/registry/blocks/sketch-canvas/demo.jsx +159 -0
- package/registry/blocks/sketch-canvas/excalidraw.js +40 -0
- package/registry/blocks/sketch-canvas/index.js +9 -0
- package/registry/blocks/sketch-canvas/palette.js +226 -0
- package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1188 -0
- package/registry/blocks/sketch-canvas/sketch.js +179 -0
- package/registry/blocks/sketch-canvas/ssr-shim-end.js +6 -0
- package/registry/blocks/sketch-canvas/ssr-shim.js +45 -0
- package/registry/blocks/sketch-canvas/stamps.js +104 -0
- package/registry/blocks/sketch-canvas/theme.js +131 -0
- package/registry/blocks/spreadsheet/README.md +307 -0
- package/registry/blocks/spreadsheet/block.json +13 -0
- package/registry/blocks/spreadsheet/cells.jsx +455 -0
- package/registry/blocks/spreadsheet/chrome.jsx +325 -0
- package/registry/blocks/spreadsheet/demo.jsx +177 -0
- package/registry/blocks/spreadsheet/grid.js +387 -0
- package/registry/blocks/spreadsheet/index.js +34 -0
- package/registry/blocks/spreadsheet/spreadsheet.jsx +1732 -0
- package/registry/blocks/spreadsheet/types.js +473 -0
- package/registry/blocks/stat-cards/README.md +173 -0
- package/registry/blocks/stat-cards/block.json +10 -0
- package/registry/blocks/stat-cards/demo.jsx +257 -0
- package/registry/blocks/stat-cards/index.js +4 -0
- package/registry/blocks/stat-cards/stat-cards.jsx +307 -0
- package/registry/blocks/stat-cards/usage.jsx +243 -0
- package/registry/blocks/task-list/README.md +176 -0
- package/registry/blocks/task-list/block.json +10 -0
- package/registry/blocks/task-list/demo.jsx +134 -0
- package/registry/blocks/task-list/index.js +5 -0
- package/registry/blocks/task-list/task-list.jsx +907 -0
- package/registry/blocks/task-model/README.md +455 -0
- package/registry/blocks/task-model/block.json +167 -0
- package/registry/blocks/task-model/changes.js +89 -0
- package/registry/blocks/task-model/dates.js +371 -0
- package/registry/blocks/task-model/demo.jsx +215 -0
- package/registry/blocks/task-model/dnd.jsx +240 -0
- package/registry/blocks/task-model/index.js +97 -0
- package/registry/blocks/task-model/model.js +919 -0
- package/registry/blocks/task-model/parts.jsx +449 -0
- package/registry/blocks/task-model/toolbar.jsx +271 -0
- package/registry/blocks/testimonials/README.md +139 -0
- package/registry/blocks/testimonials/block.json +11 -0
- package/registry/blocks/testimonials/demo.jsx +137 -0
- package/registry/blocks/testimonials/index.js +4 -0
- package/registry/blocks/testimonials/testimonials.jsx +430 -0
- package/registry/blocks/transaction-list/README.md +126 -0
- package/registry/blocks/transaction-list/block.json +10 -0
- package/registry/blocks/transaction-list/demo.jsx +79 -0
- package/registry/blocks/transaction-list/index.js +4 -0
- package/registry/blocks/transaction-list/transaction-list.jsx +383 -0
- package/registry/blocks/whiteboard/README.md +179 -0
- package/registry/blocks/whiteboard/block.json +10 -0
- package/registry/blocks/whiteboard/demo.jsx +169 -0
- package/registry/blocks/whiteboard/index.js +6 -0
- package/registry/blocks/whiteboard/templates.js +150 -0
- package/registry/blocks/whiteboard/whiteboard.jsx +721 -0
- package/src/install.mjs +18 -0
- package/src/shadcn.mjs +8 -0
- package/templates/next/files/README.md +145 -11
- package/templates/next/files/app/globals.css +55 -4
- package/templates/next/files/check/run.mjs +3 -3
- package/templates/next/files/components/ui/avatar.jsx +141 -0
- package/templates/next/files/components/ui/breadcrumb.jsx +149 -0
- package/templates/next/files/components/ui/button-group.jsx +70 -0
- package/templates/next/files/components/ui/button.jsx +132 -12
- package/templates/next/files/components/ui/calendar.jsx +609 -0
- package/templates/next/files/components/ui/carousel.jsx +281 -0
- package/templates/next/files/components/ui/collapsible.jsx +208 -0
- package/templates/next/files/components/ui/combobox.jsx +608 -0
- package/templates/next/files/components/ui/command.jsx +577 -0
- package/templates/next/files/components/ui/context-menu.jsx +191 -0
- package/templates/next/files/components/ui/date-picker.jsx +360 -0
- package/templates/next/files/components/ui/distribution.jsx +117 -0
- package/templates/next/files/components/ui/feedback.jsx +143 -0
- package/templates/next/files/components/ui/field.jsx +115 -3
- package/templates/next/files/components/ui/file-input.jsx +234 -0
- package/templates/next/files/components/ui/icon.jsx +130 -8
- package/templates/next/files/components/ui/image.jsx +143 -0
- package/templates/next/files/components/ui/index.js +47 -2
- package/templates/next/files/components/ui/input-group.jsx +213 -0
- package/templates/next/files/components/ui/input-otp.jsx +170 -0
- package/templates/next/files/components/ui/item.jsx +206 -0
- package/templates/next/files/components/ui/menu.jsx +454 -82
- package/templates/next/files/components/ui/notice.jsx +126 -14
- package/templates/next/files/components/ui/pagination.jsx +156 -0
- package/templates/next/files/components/ui/popover.jsx +284 -0
- package/templates/next/files/components/ui/radio.jsx +193 -0
- package/templates/next/files/components/ui/rating.jsx +131 -0
- package/templates/next/files/components/ui/resizable.jsx +338 -0
- package/templates/next/files/components/ui/scroll-area.jsx +123 -0
- package/templates/next/files/components/ui/separator.jsx +119 -0
- package/templates/next/files/components/ui/sheet.jsx +96 -11
- package/templates/next/files/components/ui/slider.jsx +174 -0
- package/templates/next/files/components/ui/stat.jsx +574 -0
- package/templates/next/files/components/ui/steps.jsx +338 -0
- package/templates/next/files/components/ui/swatch.jsx +33 -0
- package/templates/next/files/components/ui/table.jsx +130 -0
- package/templates/next/files/components/ui/toast.jsx +440 -0
- package/templates/next/files/components/ui/toggle.jsx +195 -0
- package/templates/next/files/components/ui/tooltip.jsx +270 -0
- package/templates/next/template.json +264 -8
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
// The work-item model every view reads — pure, no React.
|
|
2
|
+
//
|
|
3
|
+
// One shape (a task), one context (its statuses, people, projects, custom
|
|
4
|
+
// fields and today), and the arithmetic every view repeats: search,
|
|
5
|
+
// filter, group by any field, sort, subtask trees, capacity, dependencies,
|
|
6
|
+
// and the Change a view sends out when the founder moves something. List,
|
|
7
|
+
// board, table, calendar, gantt, workload, portfolio and roadmap all run
|
|
8
|
+
// through these, so a task filtered out of one is filtered out of all.
|
|
9
|
+
//
|
|
10
|
+
// README.md beside this file is the contract; this file is the proof.
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
DUE_BUCKETS,
|
|
14
|
+
addDays,
|
|
15
|
+
dayForBucket,
|
|
16
|
+
dayOf,
|
|
17
|
+
daysBetween,
|
|
18
|
+
diffDays,
|
|
19
|
+
dueBucket,
|
|
20
|
+
isWorkingDay,
|
|
21
|
+
minutesOf,
|
|
22
|
+
shiftDates,
|
|
23
|
+
spanOf,
|
|
24
|
+
startOfWeek,
|
|
25
|
+
todayDay,
|
|
26
|
+
withTime,
|
|
27
|
+
} from "./dates";
|
|
28
|
+
|
|
29
|
+
/** The group / filter value of "no value": Unassigned, No date, … */
|
|
30
|
+
export const NONE = "__none";
|
|
31
|
+
|
|
32
|
+
export const PRIORITIES = [
|
|
33
|
+
{ id: "urgent", label: "Urgent", tone: "danger" },
|
|
34
|
+
{ id: "high", label: "High", tone: "warning" },
|
|
35
|
+
{ id: "medium", label: "Medium", tone: "info" },
|
|
36
|
+
{ id: "low", label: "Low", tone: "neutral" },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** A sensible set when the surface has none of its own. */
|
|
40
|
+
export const DEFAULT_STATUSES = [
|
|
41
|
+
{ id: "todo", label: "To do", tone: "neutral" },
|
|
42
|
+
{ id: "doing", label: "In progress", tone: "info" },
|
|
43
|
+
{ id: "review", label: "In review", tone: "warning" },
|
|
44
|
+
{ id: "done", label: "Done", tone: "success", done: true },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** The fields every task has. Custom fields join through `fields`. */
|
|
48
|
+
export const TASK_FIELDS = [
|
|
49
|
+
{ key: "title", label: "Name", kind: "text", groupable: false },
|
|
50
|
+
{ key: "status", label: "Status", kind: "status" },
|
|
51
|
+
{ key: "assignees", label: "Assignee", kind: "people", multi: true },
|
|
52
|
+
{ key: "due", label: "Due date", kind: "date" },
|
|
53
|
+
{ key: "start", label: "Start date", kind: "date" },
|
|
54
|
+
{ key: "priority", label: "Priority", kind: "priority" },
|
|
55
|
+
{ key: "project", label: "Project", kind: "project" },
|
|
56
|
+
{ key: "tags", label: "Tags", kind: "tags", multi: true },
|
|
57
|
+
{ key: "estimate", label: "Estimate", kind: "number", unit: "h", groupable: false },
|
|
58
|
+
{ key: "progress", label: "Progress", kind: "progress", groupable: false },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const EMPTY_WORDS = {
|
|
62
|
+
status: "No status",
|
|
63
|
+
people: "Unassigned",
|
|
64
|
+
priority: "No priority",
|
|
65
|
+
project: "No project",
|
|
66
|
+
tags: "No tags",
|
|
67
|
+
date: "No date",
|
|
68
|
+
select: "None",
|
|
69
|
+
text: "None",
|
|
70
|
+
number: "None",
|
|
71
|
+
checkbox: "No",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const isBlank = (v) => v === null || v === undefined || v === "";
|
|
75
|
+
const asList = (v) => (Array.isArray(v) ? v.filter((x) => !isBlank(x)).map(String) : isBlank(v) ? [] : [String(v)]);
|
|
76
|
+
|
|
77
|
+
// ---- the shape ---------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
/** A task with its lists always lists and its id a string: what every
|
|
80
|
+
* helper assumes. Unknown keys are kept (`…extra`). */
|
|
81
|
+
export function normalizeTask(t) {
|
|
82
|
+
return {
|
|
83
|
+
...t,
|
|
84
|
+
id: String(t?.id ?? ""),
|
|
85
|
+
title: String(t?.title ?? t?.name ?? ""),
|
|
86
|
+
assignees: asList(t?.assignees ?? t?.assignee),
|
|
87
|
+
tags: asList(t?.tags),
|
|
88
|
+
dependsOn: asList(t?.dependsOn),
|
|
89
|
+
parentId: isBlank(t?.parentId) ? null : String(t.parentId),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function normalizeTasks(list) {
|
|
94
|
+
return (list ?? []).map(normalizeTask);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Everything a helper needs beside the tasks, built once per render
|
|
98
|
+
* (useMemo it). `statuses` in order ({ id, label, tone?: neutral · info
|
|
99
|
+
* · warning · success · danger · primary, hue?: 1–5, done? }); `people`
|
|
100
|
+
* ({ id, name, src?, capacity? hours a day }); `projects` ({ id, name,
|
|
101
|
+
* hue? }); `fields` (custom: { key, label, kind: text · number · select
|
|
102
|
+
* · date · checkbox, options? }); `today` ("YYYY-MM-DD", fixed in a
|
|
103
|
+
* demo); `weekStartsOn` (0–6, default Monday). */
|
|
104
|
+
export function createTaskContext({ statuses, people, projects, fields, today, weekStartsOn = 1, weekend, holidays } = {}) {
|
|
105
|
+
const st = (statuses?.length ? statuses : DEFAULT_STATUSES).map((s) => ({ ...s, id: String(s.id), label: s.label ?? String(s.id) }));
|
|
106
|
+
const pp = (people ?? []).map((p) => ({ ...p, id: String(p.id), name: p.name ?? String(p.id) }));
|
|
107
|
+
const pj = (projects ?? []).map((p) => ({ ...p, id: String(p.id), name: p.name ?? String(p.id) }));
|
|
108
|
+
const custom = (fields ?? []).map((f) => ({ ...f, label: f.label ?? f.key, custom: true }));
|
|
109
|
+
const known = new Set(TASK_FIELDS.map((f) => f.key));
|
|
110
|
+
return {
|
|
111
|
+
statuses: st,
|
|
112
|
+
people: pp,
|
|
113
|
+
projects: pj,
|
|
114
|
+
fields: [...TASK_FIELDS, ...custom.filter((f) => !known.has(f.key))],
|
|
115
|
+
statusById: new Map(st.map((s, i) => [s.id, { ...s, index: i }])),
|
|
116
|
+
personById: new Map(pp.map((p) => [p.id, p])),
|
|
117
|
+
projectById: new Map(pj.map((p) => [p.id, p])),
|
|
118
|
+
doneIds: new Set(st.filter((s) => s.done).map((s) => s.id)),
|
|
119
|
+
today: dayOf(today) ?? todayDay(),
|
|
120
|
+
weekStartsOn,
|
|
121
|
+
weekend,
|
|
122
|
+
holidays,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const ctxOf = (ctx) => (ctx && ctx.statusById ? ctx : createTaskContext(ctx ?? {}));
|
|
127
|
+
|
|
128
|
+
export function fieldOf(key, ctx) {
|
|
129
|
+
return ctxOf(ctx).fields.find((f) => f.key === key) ?? { key, label: key, kind: "text", custom: true };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function isDone(task, ctx) {
|
|
133
|
+
const c = ctxOf(ctx);
|
|
134
|
+
return c.doneIds.has(String(task?.status)) || task?.done === true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The first "done" status, and the first that is not. */
|
|
138
|
+
export function doneStatusId(ctx) {
|
|
139
|
+
return ctxOf(ctx).statuses.find((s) => s.done)?.id ?? null;
|
|
140
|
+
}
|
|
141
|
+
export function openStatusId(ctx) {
|
|
142
|
+
return ctxOf(ctx).statuses.find((s) => !s.done)?.id ?? null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The change a completion checkbox saves: to the first done status, or
|
|
146
|
+
* back to the first open one. */
|
|
147
|
+
export function toggleDoneChange(task, ctx) {
|
|
148
|
+
const done = isDone(task, ctx);
|
|
149
|
+
return { type: "update", id: task.id, patch: { status: done ? openStatusId(ctx) : doneStatusId(ctx) } };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let seq = 0;
|
|
153
|
+
/** A client id for a task being created; the page may swap it for the
|
|
154
|
+
* database's (answer `{ ok: true, id }`). */
|
|
155
|
+
export function newTaskId() {
|
|
156
|
+
seq += 1;
|
|
157
|
+
return `new-${Date.now().toString(36)}-${seq}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** A new task with the defaults a quick-add needs: first open status, an
|
|
161
|
+
* `order` after `after` (a list's last task), and the patch of the group
|
|
162
|
+
* it was added in. */
|
|
163
|
+
export function draftTask(title, ctx, patch = {}, { after } = {}) {
|
|
164
|
+
return normalizeTask({
|
|
165
|
+
id: newTaskId(),
|
|
166
|
+
title: String(title ?? "").trim(),
|
|
167
|
+
status: openStatusId(ctx),
|
|
168
|
+
order: rankBetween(orderOf(after), null),
|
|
169
|
+
...patch,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ---- values of a field -------------------------------------------------
|
|
174
|
+
|
|
175
|
+
/** The keys a task is filed under for a field (one, or several for
|
|
176
|
+
* assignees and tags; `NONE` when it has none). Dates are due buckets. */
|
|
177
|
+
export function valuesOf(task, key, ctx) {
|
|
178
|
+
const c = ctxOf(ctx);
|
|
179
|
+
const field = fieldOf(key, c);
|
|
180
|
+
const v = task?.[field.key];
|
|
181
|
+
switch (field.kind) {
|
|
182
|
+
case "people":
|
|
183
|
+
case "tags": {
|
|
184
|
+
const list = asList(v);
|
|
185
|
+
return list.length ? list : [NONE];
|
|
186
|
+
}
|
|
187
|
+
case "date":
|
|
188
|
+
return [dueBucket(v, c.today, { done: isDone(task, c), weekStartsOn: c.weekStartsOn })];
|
|
189
|
+
case "checkbox":
|
|
190
|
+
return [v ? "yes" : "no"];
|
|
191
|
+
default:
|
|
192
|
+
return [isBlank(v) ? NONE : String(v)];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** A field's choices in order, as { id, label, tone?, hue?, person?,
|
|
197
|
+
* project? } — the groups of "group by" and the chips of a filter. The
|
|
198
|
+
* "none" choice comes last. Tags and plain values are read from `tasks`. */
|
|
199
|
+
export function optionsOf(key, ctx, tasks = []) {
|
|
200
|
+
const c = ctxOf(ctx);
|
|
201
|
+
const field = fieldOf(key, c);
|
|
202
|
+
let out;
|
|
203
|
+
switch (field.kind) {
|
|
204
|
+
case "status":
|
|
205
|
+
out = c.statuses.map((s) => ({ id: s.id, label: s.label, tone: s.tone, hue: s.hue, done: Boolean(s.done) }));
|
|
206
|
+
break;
|
|
207
|
+
case "people":
|
|
208
|
+
out = c.people.map((p) => ({ id: p.id, label: p.name, person: p }));
|
|
209
|
+
break;
|
|
210
|
+
case "priority":
|
|
211
|
+
out = PRIORITIES.map((p) => ({ ...p }));
|
|
212
|
+
break;
|
|
213
|
+
case "project":
|
|
214
|
+
out = c.projects.map((p) => ({ id: p.id, label: p.name, hue: p.hue, project: p }));
|
|
215
|
+
break;
|
|
216
|
+
case "date":
|
|
217
|
+
out = DUE_BUCKETS.filter((b) => b.id !== "none").map((b) => ({ ...b }));
|
|
218
|
+
break;
|
|
219
|
+
case "checkbox":
|
|
220
|
+
return [
|
|
221
|
+
{ id: "yes", label: "Yes" },
|
|
222
|
+
{ id: "no", label: "No" },
|
|
223
|
+
];
|
|
224
|
+
case "select":
|
|
225
|
+
out = (field.options ?? []).map((o) => (typeof o === "object" ? { ...o, id: String(o.id ?? o.value), label: o.label ?? String(o.id ?? o.value) } : { id: String(o), label: String(o) }));
|
|
226
|
+
break;
|
|
227
|
+
default: {
|
|
228
|
+
const seen = new Set();
|
|
229
|
+
for (const t of tasks) for (const v of valuesOf(t, key, c)) if (v !== NONE) seen.add(v);
|
|
230
|
+
out = [...seen].sort((a, b) => a.localeCompare(b, undefined, { numeric: true })).map((v) => ({ id: v, label: v }));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return [...out, { id: NONE, label: EMPTY_WORDS[field.kind] ?? "None", none: true }];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The words for one value of a field: labelOf("status", "doing") → "In progress". */
|
|
237
|
+
export function labelOf(key, value, ctx, tasks) {
|
|
238
|
+
const o = optionsOf(key, ctx, tasks).find((x) => x.id === String(value ?? NONE));
|
|
239
|
+
return o?.label ?? String(value ?? "");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---- search, filter, sort ---------------------------------------------
|
|
243
|
+
|
|
244
|
+
/** The words a task can be found by: its title, tags, people, project,
|
|
245
|
+
* status, and any string field. */
|
|
246
|
+
export function searchText(task, ctx) {
|
|
247
|
+
const c = ctxOf(ctx);
|
|
248
|
+
const parts = [task.title, task.description, ...(task.tags ?? [])];
|
|
249
|
+
for (const id of task.assignees ?? []) parts.push(c.personById.get(id)?.name);
|
|
250
|
+
parts.push(c.projectById.get(String(task.project))?.name, c.statusById.get(String(task.status))?.label);
|
|
251
|
+
for (const f of c.fields) if (f.custom && typeof task[f.key] === "string") parts.push(task[f.key]);
|
|
252
|
+
return parts.filter(Boolean).join(" \u0001 ").toLowerCase();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Tasks that hold every word of `query`. */
|
|
256
|
+
export function searchTasks(tasks, query, ctx) {
|
|
257
|
+
const words = String(query ?? "").trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
258
|
+
if (!words.length) return tasks;
|
|
259
|
+
const c = ctxOf(ctx);
|
|
260
|
+
return tasks.filter((t) => {
|
|
261
|
+
const text = searchText(t, c);
|
|
262
|
+
return words.every((w) => text.includes(w));
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Tasks that match `filters` — { status: ["todo", "doing"], assignees:
|
|
267
|
+
* ["maya", NONE], due: ["overdue"] }: any value within a field, every
|
|
268
|
+
* field. An empty list is no filter. */
|
|
269
|
+
export function filterTasks(tasks, filters, ctx) {
|
|
270
|
+
const c = ctxOf(ctx);
|
|
271
|
+
const active = Object.entries(filters ?? {}).filter(([, v]) => Array.isArray(v) && v.length);
|
|
272
|
+
if (!active.length) return tasks;
|
|
273
|
+
return tasks.filter((t) => active.every(([key, want]) => valuesOf(t, key, c).some((v) => want.includes(v))));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** A task's place in a hand-made order (`order`), or null. */
|
|
277
|
+
export function orderOf(task) {
|
|
278
|
+
return typeof task?.order === "number" && Number.isFinite(task.order) ? task.order : null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function sortValue(task, key, c) {
|
|
282
|
+
if (key === "manual") return orderOf(task);
|
|
283
|
+
const field = fieldOf(key, c);
|
|
284
|
+
const v = task[field.key];
|
|
285
|
+
switch (field.kind) {
|
|
286
|
+
case "status":
|
|
287
|
+
return c.statusById.get(String(v))?.index ?? null;
|
|
288
|
+
case "priority": {
|
|
289
|
+
const i = PRIORITIES.findIndex((p) => p.id === v);
|
|
290
|
+
return i < 0 ? null : i;
|
|
291
|
+
}
|
|
292
|
+
case "people":
|
|
293
|
+
return c.personById.get(asList(v)[0])?.name ?? (asList(v)[0] || null);
|
|
294
|
+
case "project":
|
|
295
|
+
return c.projectById.get(String(v))?.name ?? (isBlank(v) ? null : String(v));
|
|
296
|
+
case "tags":
|
|
297
|
+
return asList(v)[0] ?? null;
|
|
298
|
+
case "date": {
|
|
299
|
+
const d = dayOf(v);
|
|
300
|
+
return d ? `${d}T${String(minutesOf(v) ?? 0).padStart(4, "0")}` : null;
|
|
301
|
+
}
|
|
302
|
+
case "select": {
|
|
303
|
+
const i = (field.options ?? []).findIndex((o) => String(typeof o === "object" ? o.id ?? o.value : o) === String(v));
|
|
304
|
+
return i < 0 ? (isBlank(v) ? null : String(v)) : i;
|
|
305
|
+
}
|
|
306
|
+
default:
|
|
307
|
+
return isBlank(v) ? null : v;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function compare(a, b) {
|
|
312
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
313
|
+
if (typeof a === "boolean" && typeof b === "boolean") return Number(a) - Number(b);
|
|
314
|
+
return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: "base" });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Tasks by `sort` ([{ key, dir: "asc" | "desc" }], first wins; key
|
|
318
|
+
* "manual" is the hand-made `order`). Blanks last either way; stable. An
|
|
319
|
+
* empty sort is the manual order. */
|
|
320
|
+
export function sortTasks(tasks, sort, ctx) {
|
|
321
|
+
const c = ctxOf(ctx);
|
|
322
|
+
const keys = sort?.length ? sort : [{ key: "manual", dir: "asc" }];
|
|
323
|
+
return tasks
|
|
324
|
+
.map((task, i) => ({ task, i }))
|
|
325
|
+
.sort((x, y) => {
|
|
326
|
+
for (const { key, dir } of keys) {
|
|
327
|
+
const a = sortValue(x.task, key, c);
|
|
328
|
+
const b = sortValue(y.task, key, c);
|
|
329
|
+
if (a === null || b === null) {
|
|
330
|
+
if (a === null && b === null) continue;
|
|
331
|
+
return a === null ? 1 : -1;
|
|
332
|
+
}
|
|
333
|
+
const r = compare(a, b);
|
|
334
|
+
if (r !== 0) return dir === "desc" ? -r : r;
|
|
335
|
+
}
|
|
336
|
+
return x.i - y.i;
|
|
337
|
+
})
|
|
338
|
+
.map((x) => x.task);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** The sorts a view offers: "manual" and every sortable field. */
|
|
342
|
+
export function sortOptions(ctx) {
|
|
343
|
+
return [{ key: "manual", label: "Manual" }, ...ctxOf(ctx).fields.filter((f) => f.sortable !== false).map((f) => ({ key: f.key, label: f.label }))];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ---- group -------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
/** Tasks filed under a field's values, in the field's order:
|
|
349
|
+
* [{ key, label, tone?, hue?, option, tasks }]. A task with several
|
|
350
|
+
* assignees (or tags) is in each of their groups. `by` null / "none" →
|
|
351
|
+
* one group of everything. Empty groups show for status and priority
|
|
352
|
+
* (somewhere to drop), or always with `showEmpty: true`; the "none" group
|
|
353
|
+
* only when it holds something. */
|
|
354
|
+
export function groupTasks(tasks, by, ctx, { showEmpty } = {}) {
|
|
355
|
+
const c = ctxOf(ctx);
|
|
356
|
+
if (!by || by === "none") return [{ key: "all", label: "All tasks", option: null, tasks: [...tasks] }];
|
|
357
|
+
const field = fieldOf(by, c);
|
|
358
|
+
const keepEmpty = showEmpty ?? (field.kind === "status" || field.kind === "priority");
|
|
359
|
+
const options = optionsOf(by, c, tasks);
|
|
360
|
+
const map = new Map(options.map((o) => [o.id, []]));
|
|
361
|
+
for (const t of tasks) {
|
|
362
|
+
for (const v of valuesOf(t, by, c)) {
|
|
363
|
+
if (!map.has(v)) {
|
|
364
|
+
map.set(v, []);
|
|
365
|
+
options.splice(options.length - 1, 0, { id: v, label: v });
|
|
366
|
+
}
|
|
367
|
+
map.get(v).push(t);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return options
|
|
371
|
+
.map((o) => ({ key: o.id, label: o.label, tone: o.tone, hue: o.hue, option: o, tasks: map.get(o.id) ?? [] }))
|
|
372
|
+
.filter((g) => g.tasks.length > 0 || (keepEmpty && g.key !== NONE && g.key !== "past"));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** The fields a view can group by. */
|
|
376
|
+
export function groupOptions(ctx) {
|
|
377
|
+
return [{ key: "none", label: "None" }, ...ctxOf(ctx).fields.filter((f) => f.groupable !== false).map((f) => ({ key: f.key, label: f.label }))];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** The patch that moves a task from one group to another when grouped by
|
|
381
|
+
* `by`: a new status, an assignee swapped for another (the others stay),
|
|
382
|
+
* a due date for a due bucket (a start moves with it). null when the
|
|
383
|
+
* group is not a place to drop (Overdue). */
|
|
384
|
+
export function movePatch(task, by, fromKey, toKey, ctx) {
|
|
385
|
+
const c = ctxOf(ctx);
|
|
386
|
+
if (!by || by === "none" || fromKey === toKey) return {};
|
|
387
|
+
const field = fieldOf(by, c);
|
|
388
|
+
const to = toKey === NONE ? null : toKey;
|
|
389
|
+
switch (field.kind) {
|
|
390
|
+
case "people":
|
|
391
|
+
case "tags": {
|
|
392
|
+
const list = asList(task[field.key]).filter((v) => v !== fromKey);
|
|
393
|
+
return { [field.key]: to && !list.includes(to) ? [...list, to] : list };
|
|
394
|
+
}
|
|
395
|
+
case "date": {
|
|
396
|
+
const day = dayForBucket(toKey, c.today, { weekStartsOn: c.weekStartsOn });
|
|
397
|
+
if (day === undefined) return null;
|
|
398
|
+
if (day === null) return { [field.key]: null };
|
|
399
|
+
const from = dayOf(task[field.key]);
|
|
400
|
+
if (field.key === "due" && from && dayOf(task.start)) return shiftDates(task, diffDays(from, day));
|
|
401
|
+
return { [field.key]: withTime(day, minutesOf(task[field.key])) };
|
|
402
|
+
}
|
|
403
|
+
case "checkbox":
|
|
404
|
+
return { [field.key]: toKey === "yes" };
|
|
405
|
+
case "number":
|
|
406
|
+
case "progress":
|
|
407
|
+
return { [field.key]: to === null ? null : Number(to) };
|
|
408
|
+
default:
|
|
409
|
+
return { [field.key]: to };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ---- views -------------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
/** What a view remembers: work-views saves one per tab. */
|
|
416
|
+
export const DEFAULT_VIEW = {
|
|
417
|
+
search: "",
|
|
418
|
+
filters: {},
|
|
419
|
+
groupBy: "status",
|
|
420
|
+
sort: [],
|
|
421
|
+
fields: ["assignees", "due", "priority"],
|
|
422
|
+
hideDone: false,
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
/** A view's answer: the tasks it shows (searched, filtered, sorted) and
|
|
426
|
+
* their groups. `hideDone` drops done tasks. */
|
|
427
|
+
export function applyView(tasks, view, ctx) {
|
|
428
|
+
const c = ctxOf(ctx);
|
|
429
|
+
const v = { ...DEFAULT_VIEW, ...view };
|
|
430
|
+
let list = normalizeTasks(tasks);
|
|
431
|
+
if (v.hideDone) list = list.filter((t) => !isDone(t, c));
|
|
432
|
+
list = filterTasks(searchTasks(list, v.search, c), v.filters, c);
|
|
433
|
+
list = sortTasks(list, v.sort, c);
|
|
434
|
+
return { tasks: list, groups: groupTasks(list, v.groupBy, c), total: (tasks ?? []).length };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** How many filters are on (for a "Filter · 2" button). */
|
|
438
|
+
export function activeFilterCount(view) {
|
|
439
|
+
return Object.values(view?.filters ?? {}).filter((v) => Array.isArray(v) && v.length).length + (view?.hideDone ? 1 : 0);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ---- manual order ------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
const GAP = 1024;
|
|
445
|
+
|
|
446
|
+
/** A number between two orders (either may be null: the list's end). */
|
|
447
|
+
export function rankBetween(a, b) {
|
|
448
|
+
if (a == null && b == null) return GAP;
|
|
449
|
+
if (a == null) return b - GAP;
|
|
450
|
+
if (b == null) return a + GAP;
|
|
451
|
+
return (a + b) / 2;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** The change that puts `task` at `index` of `list` (the target group's
|
|
455
|
+
* tasks as shown, without the task itself), with `patch` (a new group)
|
|
456
|
+
* merged in. A midpoint `order` when the neighbours have room; else the
|
|
457
|
+
* list is renumbered in one batch, so the order always holds. */
|
|
458
|
+
export function placeChange(list, task, index, patch = {}) {
|
|
459
|
+
const others = list.filter((t) => t.id !== task.id);
|
|
460
|
+
const i = Math.max(0, Math.min(index, others.length));
|
|
461
|
+
const orders = others.map(orderOf);
|
|
462
|
+
const increasing = orders.every((o, k) => o !== null && (k === 0 || o > orders[k - 1]));
|
|
463
|
+
const prev = i > 0 ? orders[i - 1] : null;
|
|
464
|
+
const next = i < others.length ? orders[i] : null;
|
|
465
|
+
if (increasing && (prev === null || next === null || next - prev > 1e-6)) {
|
|
466
|
+
return { type: "update", id: task.id, patch: { ...patch, order: rankBetween(prev, next) } };
|
|
467
|
+
}
|
|
468
|
+
const seqd = [...others.slice(0, i), task, ...others.slice(i)];
|
|
469
|
+
return {
|
|
470
|
+
type: "batch",
|
|
471
|
+
changes: seqd.map((t, k) =>
|
|
472
|
+
t.id === task.id ? { type: "update", id: t.id, patch: { ...patch, order: (k + 1) * GAP } } : { type: "update", id: t.id, patch: { order: (k + 1) * GAP } },
|
|
473
|
+
),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ---- trees (subtasks) --------------------------------------------------
|
|
478
|
+
|
|
479
|
+
/** parentId → its children, in the order given. */
|
|
480
|
+
export function childrenMap(tasks) {
|
|
481
|
+
const ids = new Set(tasks.map((t) => String(t.id)));
|
|
482
|
+
const map = new Map();
|
|
483
|
+
for (const t of tasks) {
|
|
484
|
+
const p = t.parentId == null ? null : String(t.parentId);
|
|
485
|
+
const key = p && ids.has(p) && p !== String(t.id) ? p : null;
|
|
486
|
+
if (!map.has(key)) map.set(key, []);
|
|
487
|
+
map.get(key).push(t);
|
|
488
|
+
}
|
|
489
|
+
return map;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Tasks as a tree: [{ task, children: [node…], depth }]. A task whose
|
|
493
|
+
* parent is not in `tasks` (filtered out) is a root. Cycles are cut. */
|
|
494
|
+
export function buildTree(tasks) {
|
|
495
|
+
const map = childrenMap(tasks);
|
|
496
|
+
const seen = new Set();
|
|
497
|
+
const walk = (list, depth) =>
|
|
498
|
+
(list ?? [])
|
|
499
|
+
.filter((t) => !seen.has(t.id) && seen.add(t.id))
|
|
500
|
+
.map((task) => ({ task, depth, children: walk(map.get(String(task.id)), depth + 1) }));
|
|
501
|
+
return walk(map.get(null), 0);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** The tree as rows in reading order: [{ task, depth, childCount,
|
|
505
|
+
* hasChildren, collapsed }]; the children of an id in `collapsed` are
|
|
506
|
+
* left out. */
|
|
507
|
+
export function flattenTree(tasks, { collapsed } = {}) {
|
|
508
|
+
const shut = collapsed instanceof Set ? collapsed : new Set(collapsed ?? []);
|
|
509
|
+
const out = [];
|
|
510
|
+
const walk = (nodes) => {
|
|
511
|
+
for (const n of nodes) {
|
|
512
|
+
const closed = shut.has(n.task.id);
|
|
513
|
+
out.push({ task: n.task, depth: n.depth, childCount: n.children.length, hasChildren: n.children.length > 0, collapsed: closed });
|
|
514
|
+
if (!closed) walk(n.children);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
walk(buildTree(tasks));
|
|
518
|
+
return out;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Every id under `id` (children, their children…). */
|
|
522
|
+
export function descendantIds(tasks, id) {
|
|
523
|
+
const map = childrenMap(tasks);
|
|
524
|
+
const out = [];
|
|
525
|
+
const stack = [...(map.get(String(id)) ?? [])];
|
|
526
|
+
const seen = new Set([String(id)]);
|
|
527
|
+
while (stack.length) {
|
|
528
|
+
const t = stack.pop();
|
|
529
|
+
if (seen.has(t.id)) continue;
|
|
530
|
+
seen.add(t.id);
|
|
531
|
+
out.push(t.id);
|
|
532
|
+
stack.push(...(map.get(String(t.id)) ?? []));
|
|
533
|
+
}
|
|
534
|
+
return out;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** The ids above `id`, nearest first. */
|
|
538
|
+
export function ancestorIds(tasks, id) {
|
|
539
|
+
const byId = new Map(tasks.map((t) => [String(t.id), t]));
|
|
540
|
+
const out = [];
|
|
541
|
+
let cur = byId.get(String(id));
|
|
542
|
+
while (cur?.parentId && byId.has(cur.parentId) && !out.includes(cur.parentId) && cur.parentId !== String(id)) {
|
|
543
|
+
out.push(cur.parentId);
|
|
544
|
+
cur = byId.get(cur.parentId);
|
|
545
|
+
}
|
|
546
|
+
return out;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** id → { total, done } of its direct subtasks ("2/5" on a row). */
|
|
550
|
+
export function subtaskCounts(tasks, ctx) {
|
|
551
|
+
const c = ctxOf(ctx);
|
|
552
|
+
const out = new Map();
|
|
553
|
+
for (const [parent, kids] of childrenMap(tasks)) {
|
|
554
|
+
if (parent === null) continue;
|
|
555
|
+
out.set(parent, { total: kids.length, done: kids.filter((k) => isDone(k, c)).length });
|
|
556
|
+
}
|
|
557
|
+
return out;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** A parent's figures from everything under it: count, done, estimate
|
|
561
|
+
* (hours), and progress (0–100: its own, else the share done, weighted
|
|
562
|
+
* by estimate when there are estimates). */
|
|
563
|
+
export function rollup(tasks, id, ctx) {
|
|
564
|
+
const c = ctxOf(ctx);
|
|
565
|
+
const ids = new Set(descendantIds(tasks, id));
|
|
566
|
+
const under = tasks.filter((t) => ids.has(t.id));
|
|
567
|
+
const done = under.filter((t) => isDone(t, c));
|
|
568
|
+
const estimate = under.reduce((s, t) => s + (Number(t.estimate) || 0), 0);
|
|
569
|
+
const doneEstimate = done.reduce((s, t) => s + (Number(t.estimate) || 0), 0);
|
|
570
|
+
const progress = under.length === 0 ? null : estimate > 0 ? Math.round((doneEstimate / estimate) * 100) : Math.round((done.length / under.length) * 100);
|
|
571
|
+
return { count: under.length, done: done.length, estimate, progress };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** A task's progress, 0–100: `progress`, else 100 when done, else from
|
|
575
|
+
* its subtasks, else 0. */
|
|
576
|
+
export function progressOf(task, tasks, ctx) {
|
|
577
|
+
if (typeof task?.progress === "number") return Math.max(0, Math.min(100, task.progress));
|
|
578
|
+
if (isDone(task, ctx)) return 100;
|
|
579
|
+
return rollup(tasks ?? [], task.id, ctx).progress ?? 0;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ---- dependencies ------------------------------------------------------
|
|
583
|
+
|
|
584
|
+
/** [{ from, to }]: `to` depends on `from` (from must finish first). Ids
|
|
585
|
+
* not in `tasks` are skipped. */
|
|
586
|
+
export function dependencyEdges(tasks) {
|
|
587
|
+
const ids = new Set(tasks.map((t) => t.id));
|
|
588
|
+
const out = [];
|
|
589
|
+
for (const t of tasks) for (const d of t.dependsOn ?? []) if (ids.has(d) && d !== t.id) out.push({ from: d, to: t.id });
|
|
590
|
+
return out;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** The tasks `task` waits on that are not done. */
|
|
594
|
+
export function blockersOf(task, tasks, ctx) {
|
|
595
|
+
const want = new Set(task?.dependsOn ?? []);
|
|
596
|
+
return tasks.filter((t) => want.has(t.id) && !isDone(t, ctx));
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Whether "to depends on from" would close a loop. */
|
|
600
|
+
export function wouldCycle(tasks, fromId, toId) {
|
|
601
|
+
if (fromId === toId) return true;
|
|
602
|
+
const deps = new Map(tasks.map((t) => [t.id, t.dependsOn ?? []]));
|
|
603
|
+
const stack = [fromId];
|
|
604
|
+
const seen = new Set();
|
|
605
|
+
while (stack.length) {
|
|
606
|
+
const id = stack.pop();
|
|
607
|
+
if (id === toId) return true;
|
|
608
|
+
if (seen.has(id)) continue;
|
|
609
|
+
seen.add(id);
|
|
610
|
+
stack.push(...(deps.get(id) ?? []));
|
|
611
|
+
}
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** Dependency edges whose dates disagree: `to` starts on or before
|
|
616
|
+
* `from` ends. A gantt draws these arrows in the danger tone. */
|
|
617
|
+
export function dateConflicts(tasks) {
|
|
618
|
+
const byId = new Map(tasks.map((t) => [t.id, t]));
|
|
619
|
+
return dependencyEdges(tasks).filter(({ from, to }) => {
|
|
620
|
+
const a = spanOf(byId.get(from));
|
|
621
|
+
const b = spanOf(byId.get(to));
|
|
622
|
+
return a && b && b.start <= a.end;
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** The ids on the longest chain of dependent, scheduled work (length in
|
|
627
|
+
* days, span to span): the critical path a gantt highlights. */
|
|
628
|
+
export function criticalPath(tasks) {
|
|
629
|
+
const scheduled = tasks.filter((t) => spanOf(t));
|
|
630
|
+
const byId = new Map(scheduled.map((t) => [t.id, t]));
|
|
631
|
+
const memo = new Map();
|
|
632
|
+
const visiting = new Set();
|
|
633
|
+
const best = (id) => {
|
|
634
|
+
if (memo.has(id)) return memo.get(id);
|
|
635
|
+
if (visiting.has(id)) return { len: 0, path: [] };
|
|
636
|
+
visiting.add(id);
|
|
637
|
+
const t = byId.get(id);
|
|
638
|
+
const s = spanOf(t);
|
|
639
|
+
const own = diffDays(s.start, s.end) + 1;
|
|
640
|
+
let top = { len: 0, path: [] };
|
|
641
|
+
for (const d of t.dependsOn ?? []) {
|
|
642
|
+
if (!byId.has(d)) continue;
|
|
643
|
+
const r = best(d);
|
|
644
|
+
if (r.len > top.len) top = r;
|
|
645
|
+
}
|
|
646
|
+
visiting.delete(id);
|
|
647
|
+
const r = { len: top.len + own, path: [...top.path, id] };
|
|
648
|
+
memo.set(id, r);
|
|
649
|
+
return r;
|
|
650
|
+
};
|
|
651
|
+
let winner = { len: 0, path: [] };
|
|
652
|
+
for (const t of scheduled) {
|
|
653
|
+
const r = best(t.id);
|
|
654
|
+
if (r.path.length > 1 && r.len > winner.len) winner = r;
|
|
655
|
+
}
|
|
656
|
+
return new Set(winner.path);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ---- capacity ----------------------------------------------------------
|
|
660
|
+
|
|
661
|
+
/** A task's hours on each working day of its span: estimate ÷ working
|
|
662
|
+
* days, split evenly between its assignees. { day → hours } per person
|
|
663
|
+
* is what workload() adds up. */
|
|
664
|
+
export function hoursPerDay(task, ctx) {
|
|
665
|
+
const c = ctxOf(ctx);
|
|
666
|
+
const s = spanOf(task);
|
|
667
|
+
const est = Number(task?.estimate) || 0;
|
|
668
|
+
if (!s || est <= 0) return 0;
|
|
669
|
+
const opts = { weekend: c.weekend, holidays: c.holidays };
|
|
670
|
+
const days = daysBetween(s.start, s.end).filter((d) => isWorkingDay(d, opts)).length || 1;
|
|
671
|
+
return est / days / Math.max(1, (task.assignees ?? []).length);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/** Load against capacity per person over [from, to], by day or week:
|
|
675
|
+
* { rows: [{ person, total, capacity, buckets: [{ key, start, end, hours,
|
|
676
|
+
* capacity, over, tasks: [{ task, hours }] }] }], unassigned, unscheduled }.
|
|
677
|
+
* `capacity` is the person's hours a day (default `defaultCapacity`, 8)
|
|
678
|
+
* times the bucket's working days. Done tasks are left out unless
|
|
679
|
+
* `includeDone`. */
|
|
680
|
+
export function workload(tasks, ctx, { from, to, unit = "day", defaultCapacity = 8, includeDone = false } = {}) {
|
|
681
|
+
const c = ctxOf(ctx);
|
|
682
|
+
const opts = { weekend: c.weekend, holidays: c.holidays };
|
|
683
|
+
const start = dayOf(from) ?? c.today;
|
|
684
|
+
const end = dayOf(to) ?? addDays(start, 13);
|
|
685
|
+
const days = daysBetween(start, end);
|
|
686
|
+
const bucketOf = (d) => (unit === "week" ? startOfWeek(d, c.weekStartsOn) : d);
|
|
687
|
+
const bucketKeys = [...new Set(days.map(bucketOf))];
|
|
688
|
+
const list = normalizeTasks(tasks).filter((t) => includeDone || !isDone(t, c));
|
|
689
|
+
const rows = c.people.map((person) => {
|
|
690
|
+
const cap = Number(person.capacity ?? defaultCapacity);
|
|
691
|
+
const buckets = bucketKeys.map((key) => {
|
|
692
|
+
const inside = days.filter((d) => bucketOf(d) === key);
|
|
693
|
+
return { key, start: inside[0], end: inside[inside.length - 1], hours: 0, capacity: inside.filter((d) => isWorkingDay(d, opts)).length * cap, tasks: [] };
|
|
694
|
+
});
|
|
695
|
+
const byKey = new Map(buckets.map((b) => [b.key, b]));
|
|
696
|
+
for (const t of list) {
|
|
697
|
+
if (!t.assignees.includes(person.id)) continue;
|
|
698
|
+
const s = spanOf(t);
|
|
699
|
+
const per = hoursPerDay(t, c);
|
|
700
|
+
if (!s || per === 0) continue;
|
|
701
|
+
for (const d of daysBetween(s.start > start ? s.start : start, s.end < end ? s.end : end)) {
|
|
702
|
+
if (!isWorkingDay(d, opts)) continue;
|
|
703
|
+
const b = byKey.get(bucketOf(d));
|
|
704
|
+
if (!b) continue;
|
|
705
|
+
b.hours += per;
|
|
706
|
+
const hit = b.tasks.find((x) => x.task.id === t.id);
|
|
707
|
+
if (hit) hit.hours += per;
|
|
708
|
+
else b.tasks.push({ task: t, hours: per });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
for (const b of buckets) {
|
|
712
|
+
b.hours = Math.round(b.hours * 100) / 100;
|
|
713
|
+
b.over = b.hours > b.capacity + 1e-9;
|
|
714
|
+
}
|
|
715
|
+
const total = buckets.reduce((s, b) => s + b.hours, 0);
|
|
716
|
+
return { person, total: Math.round(total * 100) / 100, capacity: buckets.reduce((s, b) => s + b.capacity, 0), buckets };
|
|
717
|
+
});
|
|
718
|
+
return {
|
|
719
|
+
rows,
|
|
720
|
+
buckets: bucketKeys,
|
|
721
|
+
unassigned: list.filter((t) => t.assignees.length === 0 && spanOf(t)),
|
|
722
|
+
unscheduled: list.filter((t) => !spanOf(t)),
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/** The sum of a number field over tasks (estimate by default). */
|
|
727
|
+
export function sumOf(tasks, key = "estimate") {
|
|
728
|
+
return (tasks ?? []).reduce((s, t) => s + (Number(t?.[key]) || 0), 0);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// ---- changes -----------------------------------------------------------
|
|
732
|
+
//
|
|
733
|
+
// A view never edits tasks. It builds a Change and hands it to
|
|
734
|
+
// `onChange(change)` (async, answering { ok } — see useTaskChanges):
|
|
735
|
+
//
|
|
736
|
+
// { type: "create", task } or { type: "create", tasks: [...] }
|
|
737
|
+
// { type: "update", id, patch } or { type: "update", ids: [...], patch }
|
|
738
|
+
// { type: "delete", id } or { type: "delete", ids: [...] }
|
|
739
|
+
// { type: "batch", changes: [...] } several at once (a renumber)
|
|
740
|
+
|
|
741
|
+
/** The tasks after a change — what the screen shows before the save
|
|
742
|
+
* answers. A create whose id exists is skipped (the page already has it). */
|
|
743
|
+
export function applyChange(tasks, change) {
|
|
744
|
+
if (!change) return tasks;
|
|
745
|
+
switch (change.type) {
|
|
746
|
+
case "create": {
|
|
747
|
+
const add = (change.tasks ?? [change.task]).filter(Boolean).map(normalizeTask);
|
|
748
|
+
const ids = new Set(tasks.map((t) => String(t.id)));
|
|
749
|
+
const fresh = add.filter((t) => !ids.has(t.id));
|
|
750
|
+
return fresh.length ? [...tasks, ...fresh] : tasks;
|
|
751
|
+
}
|
|
752
|
+
case "update": {
|
|
753
|
+
const ids = new Set((change.ids ?? [change.id]).map(String));
|
|
754
|
+
return tasks.map((t) => (ids.has(String(t.id)) ? normalizeTask({ ...t, ...change.patch }) : t));
|
|
755
|
+
}
|
|
756
|
+
case "delete": {
|
|
757
|
+
const ids = new Set((change.ids ?? [change.id]).map(String));
|
|
758
|
+
return tasks.filter((t) => !ids.has(String(t.id)));
|
|
759
|
+
}
|
|
760
|
+
case "batch":
|
|
761
|
+
return (change.changes ?? []).reduce(applyChange, tasks);
|
|
762
|
+
default:
|
|
763
|
+
return tasks;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** The change that undoes `change`, read against the tasks BEFORE it:
|
|
768
|
+
* what an Undo in a toast commits. */
|
|
769
|
+
export function invertChange(tasks, change) {
|
|
770
|
+
if (!change) return null;
|
|
771
|
+
switch (change.type) {
|
|
772
|
+
case "create":
|
|
773
|
+
return { type: "delete", ids: (change.tasks ?? [change.task]).filter(Boolean).map((t) => String(t.id)) };
|
|
774
|
+
case "update": {
|
|
775
|
+
const ids = new Set((change.ids ?? [change.id]).map(String));
|
|
776
|
+
const keys = Object.keys(change.patch ?? {});
|
|
777
|
+
const changes = tasks
|
|
778
|
+
.filter((t) => ids.has(String(t.id)))
|
|
779
|
+
.map((t) => ({ type: "update", id: t.id, patch: Object.fromEntries(keys.map((k) => [k, t[k] ?? null])) }));
|
|
780
|
+
return changes.length === 1 ? changes[0] : { type: "batch", changes };
|
|
781
|
+
}
|
|
782
|
+
case "delete": {
|
|
783
|
+
const ids = new Set((change.ids ?? [change.id]).map(String));
|
|
784
|
+
return { type: "create", tasks: tasks.filter((t) => ids.has(String(t.id))) };
|
|
785
|
+
}
|
|
786
|
+
case "batch": {
|
|
787
|
+
const inverses = [];
|
|
788
|
+
let state = tasks;
|
|
789
|
+
for (const c of change.changes ?? []) {
|
|
790
|
+
inverses.unshift(invertChange(state, c));
|
|
791
|
+
state = applyChange(state, c);
|
|
792
|
+
}
|
|
793
|
+
return { type: "batch", changes: inverses.filter(Boolean) };
|
|
794
|
+
}
|
|
795
|
+
default:
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** A change as plain writes, one per row, in order — the loop a page runs
|
|
801
|
+
* over insertRow / updateRow / deleteRow:
|
|
802
|
+
* [{ op: "insert", values } | { op: "update", id, patch } | { op: "delete", id }].
|
|
803
|
+
* `columns` keeps only the keys your table has (a new task carries
|
|
804
|
+
* empty lists — tags, dependsOn — your table may not); `keepNewIds:
|
|
805
|
+
* false` drops a created task's client id (`new-…`) so the table makes
|
|
806
|
+
* its own. */
|
|
807
|
+
export function changeWrites(change, { columns, keepNewIds = true } = {}) {
|
|
808
|
+
if (!change) return [];
|
|
809
|
+
const pick = (obj) => (columns ? Object.fromEntries(Object.entries(obj ?? {}).filter(([k]) => columns.includes(k))) : obj);
|
|
810
|
+
switch (change.type) {
|
|
811
|
+
case "create":
|
|
812
|
+
return (change.tasks ?? [change.task]).filter(Boolean).map((t) => {
|
|
813
|
+
const { _sample, ...rest } = t;
|
|
814
|
+
const values = pick(rest);
|
|
815
|
+
if (!keepNewIds && isNewId(values.id)) delete values.id;
|
|
816
|
+
return { op: "insert", values };
|
|
817
|
+
});
|
|
818
|
+
case "update":
|
|
819
|
+
return (change.ids ?? [change.id]).map((id) => ({ op: "update", id, patch: pick(change.patch) }));
|
|
820
|
+
case "delete":
|
|
821
|
+
return (change.ids ?? [change.id]).map((id) => ({ op: "delete", id }));
|
|
822
|
+
case "batch":
|
|
823
|
+
return (change.changes ?? []).flatMap((c) => changeWrites(c, { columns, keepNewIds }));
|
|
824
|
+
default:
|
|
825
|
+
return [];
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/** Whether an id is one newTaskId() made (not yet the database's). */
|
|
830
|
+
export function isNewId(id) {
|
|
831
|
+
return typeof id === "string" && id.startsWith("new-");
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/** The ids a change touches. */
|
|
835
|
+
export function changedIds(change) {
|
|
836
|
+
return [...new Set(changeWrites(change).map((w) => String(w.id ?? w.values?.id)))];
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/** A delete that takes the subtasks with it. */
|
|
840
|
+
export function deleteChange(tasks, ids) {
|
|
841
|
+
const all = new Set();
|
|
842
|
+
for (const id of ids) {
|
|
843
|
+
all.add(String(id));
|
|
844
|
+
for (const d of descendantIds(tasks, id)) all.add(d);
|
|
845
|
+
}
|
|
846
|
+
return { type: "delete", ids: [...all] };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// ---- adapters for the existing blocks ----------------------------------
|
|
850
|
+
|
|
851
|
+
/** Board props from tasks: `toBoard(tasks, { groupBy: "status", ctx })` →
|
|
852
|
+
* { rows, groupBy, columns, otherLabel, moveChange(row, to, from) }.
|
|
853
|
+
* One card per task, under its first value (a task with two assignees
|
|
854
|
+
* sits under the first). Spread rows / groupBy / columns / otherLabel
|
|
855
|
+
* onto <Board> and send `moveChange` through your onChange:
|
|
856
|
+
* onMove={(row, to, from) => commit(board.moveChange(row, to, from))}. */
|
|
857
|
+
export function toBoard(tasks, { groupBy = "status", ctx } = {}) {
|
|
858
|
+
const c = ctxOf(ctx);
|
|
859
|
+
const list = normalizeTasks(tasks);
|
|
860
|
+
const options = optionsOf(groupBy, c, list);
|
|
861
|
+
const none = options.find((o) => o.id === NONE);
|
|
862
|
+
const date = fieldOf(groupBy, c).kind === "date";
|
|
863
|
+
const columns = options.filter((o) => o.id !== NONE && !(date && (o.id === "past" || o.id === "overdue"))).map((o) => ({ key: o.id, label: o.label }));
|
|
864
|
+
const rows = list.map((t) => ({ ...t, __group: valuesOf(t, groupBy, c)[0] }));
|
|
865
|
+
return {
|
|
866
|
+
rows,
|
|
867
|
+
groupBy: "__group",
|
|
868
|
+
columns,
|
|
869
|
+
otherLabel: none?.label ?? "None",
|
|
870
|
+
moveChange(row, to, from) {
|
|
871
|
+
const task = list.find((t) => t.id === String(row.id)) ?? row;
|
|
872
|
+
const patch = movePatch(task, groupBy, from ?? NONE, to, c);
|
|
873
|
+
return patch ? { type: "update", id: task.id, patch } : null;
|
|
874
|
+
},
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/** Flat rows for DataTable (or a CSV): the task plus its words —
|
|
879
|
+
* statusLabel, statusIndex, assigneeNames, projectName, priorityLabel,
|
|
880
|
+
* priorityRank, dueDay, dueBucket, parentTitle, subtasks ("2/5"), done.
|
|
881
|
+
* DataTable sorts and searches these without knowing the model. */
|
|
882
|
+
export function toTableRows(tasks, ctx) {
|
|
883
|
+
const c = ctxOf(ctx);
|
|
884
|
+
const list = normalizeTasks(tasks);
|
|
885
|
+
const byId = new Map(list.map((t) => [t.id, t]));
|
|
886
|
+
const counts = subtaskCounts(list, c);
|
|
887
|
+
return list.map((t) => {
|
|
888
|
+
const st = c.statusById.get(String(t.status));
|
|
889
|
+
const pr = PRIORITIES.findIndex((p) => p.id === t.priority);
|
|
890
|
+
const sub = counts.get(t.id);
|
|
891
|
+
return {
|
|
892
|
+
...t,
|
|
893
|
+
statusLabel: st?.label ?? "",
|
|
894
|
+
statusIndex: st?.index ?? null,
|
|
895
|
+
assigneeNames: t.assignees.map((id) => c.personById.get(id)?.name ?? id).join(", "),
|
|
896
|
+
projectName: c.projectById.get(String(t.project))?.name ?? "",
|
|
897
|
+
priorityLabel: pr < 0 ? "" : PRIORITIES[pr].label,
|
|
898
|
+
priorityRank: pr < 0 ? null : pr,
|
|
899
|
+
dueDay: dayOf(t.due),
|
|
900
|
+
dueBucket: valuesOf(t, "due", c)[0],
|
|
901
|
+
parentTitle: t.parentId ? byId.get(t.parentId)?.title ?? "" : "",
|
|
902
|
+
subtasks: sub ? `${sub.done}/${sub.total}` : "",
|
|
903
|
+
done: isDone(t, c),
|
|
904
|
+
};
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** DataTable facets for toTableRows rows: status, assignee (first),
|
|
909
|
+
* priority, project — each with the model's options and words. */
|
|
910
|
+
export function taskFacets(ctx, { keys = ["status", "assignees", "priority", "project"] } = {}) {
|
|
911
|
+
const c = ctxOf(ctx);
|
|
912
|
+
const facet = {
|
|
913
|
+
status: { key: "status", label: "Status", options: c.statuses.map((s) => ({ value: s.id, label: s.label })) },
|
|
914
|
+
assignees: { key: "assignees", label: "Assignee", value: (r) => r.assignees?.[0] ?? "", options: [...c.people.map((p) => ({ value: p.id, label: p.name })), { value: "", label: "Unassigned" }] },
|
|
915
|
+
priority: { key: "priority", label: "Priority", options: [...PRIORITIES.map((p) => ({ value: p.id, label: p.label })), { value: "", label: "No priority" }] },
|
|
916
|
+
project: { key: "project", label: "Project", options: [...c.projects.map((p) => ({ value: p.id, label: p.name })), { value: "", label: "No project" }] },
|
|
917
|
+
};
|
|
918
|
+
return keys.map((k) => facet[k]).filter(Boolean);
|
|
919
|
+
}
|