@agent-native/core 0.101.14 → 0.102.1
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +12 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/templates-meta.ts +13 -0
- package/corpus/core/src/client/agent-page/AgentTabsPage.tsx +1 -1
- package/corpus/core/src/client/org/OrgSwitcher.tsx +2 -0
- package/corpus/templates/analytics/changelog/2026-07-15-fixed-the-agent-discarding-a-successful-dashboard-edit-and-r.md +6 -0
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +30 -1
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +15 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +59 -4
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +9 -0
- package/corpus/templates/content/actions/preview-builder-source-review.ts +6 -4
- package/corpus/templates/content/app/components/editor/DescriptionField.tsx +10 -2
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +4 -0
- package/corpus/templates/content/changelog/2026-07-15-page-and-database-descriptions-now-use-a-clean-single-line-f.md +6 -0
- package/corpus/templates/content/changelog/2026-07-15-selected-builder-reviews-now-load-only-the-chosen-rows-avoid.md +6 -0
- package/corpus/templates/content/shared/api.ts +1 -0
- package/corpus/templates/tasks/.agents/commands/visual-plan.md +9 -0
- package/corpus/templates/tasks/.agents/commands/visual-recap.md +8 -0
- package/corpus/templates/tasks/.agents/skills/actions/SKILL.md +492 -0
- package/corpus/templates/tasks/.agents/skills/adding-a-feature/SKILL.md +189 -0
- package/corpus/templates/tasks/.agents/skills/agent-native-docs/SKILL.md +76 -0
- package/corpus/templates/tasks/.agents/skills/agent-watchdog/README.md +43 -0
- package/corpus/templates/tasks/.agents/skills/agent-watchdog/SKILL.md +110 -0
- package/corpus/templates/tasks/.agents/skills/agent-watchdog/agents/openai.yaml +4 -0
- package/corpus/templates/tasks/.agents/skills/capture-learnings/SKILL.md +89 -0
- package/corpus/templates/tasks/.agents/skills/change-summary/SKILL.md +83 -0
- package/corpus/templates/tasks/.agents/skills/create-skill/SKILL.md +221 -0
- package/corpus/templates/tasks/.agents/skills/delegate-to-agent/SKILL.md +263 -0
- package/corpus/templates/tasks/.agents/skills/efficient-fable/README.md +75 -0
- package/corpus/templates/tasks/.agents/skills/efficient-fable/SKILL.md +95 -0
- package/corpus/templates/tasks/.agents/skills/efficient-frontier/README.md +53 -0
- package/corpus/templates/tasks/.agents/skills/efficient-frontier/SKILL.md +73 -0
- package/corpus/templates/tasks/.agents/skills/frontend-design/SKILL.md +112 -0
- package/corpus/templates/tasks/.agents/skills/plan-arbiter/README.md +41 -0
- package/corpus/templates/tasks/.agents/skills/plan-arbiter/SKILL.md +114 -0
- package/corpus/templates/tasks/.agents/skills/plan-arbiter/agents/openai.yaml +4 -0
- package/corpus/templates/tasks/.agents/skills/plow-ahead/README.md +42 -0
- package/corpus/templates/tasks/.agents/skills/plow-ahead/SKILL.md +104 -0
- package/corpus/templates/tasks/.agents/skills/plow-ahead/agents/openai.yaml +4 -0
- package/corpus/templates/tasks/.agents/skills/quick-recap/README.md +55 -0
- package/corpus/templates/tasks/.agents/skills/quick-recap/SKILL.md +54 -0
- package/corpus/templates/tasks/.agents/skills/read-the-damn-docs/README.md +54 -0
- package/corpus/templates/tasks/.agents/skills/read-the-damn-docs/SKILL.md +126 -0
- package/corpus/templates/tasks/.agents/skills/read-the-damn-docs/agents/openai.yaml +4 -0
- package/corpus/templates/tasks/.agents/skills/real-time-sync/SKILL.md +232 -0
- package/corpus/templates/tasks/.agents/skills/security/SKILL.md +280 -0
- package/corpus/templates/tasks/.agents/skills/self-modifying-code/SKILL.md +106 -0
- package/corpus/templates/tasks/.agents/skills/shadcn-ui/SKILL.md +95 -0
- package/corpus/templates/tasks/.agents/skills/stay-within-limits/README.md +61 -0
- package/corpus/templates/tasks/.agents/skills/stay-within-limits/SKILL.md +83 -0
- package/corpus/templates/tasks/.agents/skills/upgrade-agent-native/SKILL.md +87 -0
- package/corpus/templates/tasks/.agents/skills/visual-plan/SKILL.md +547 -0
- package/corpus/templates/tasks/.agents/skills/visual-plan/agent-native-skill.json +12 -0
- package/corpus/templates/tasks/.agents/skills/visual-plan/references/canvas.md +121 -0
- package/corpus/templates/tasks/.agents/skills/visual-plan/references/document-quality.md +178 -0
- package/corpus/templates/tasks/.agents/skills/visual-plan/references/exemplar.md +62 -0
- package/corpus/templates/tasks/.agents/skills/visual-plan/references/wireframe.md +264 -0
- package/corpus/templates/tasks/.agents/skills/visual-recap/SKILL.md +634 -0
- package/corpus/templates/tasks/.agents/skills/visual-recap/agent-native-skill.json +12 -0
- package/corpus/templates/tasks/.agents/skills/visual-recap/references/wireframe.md +264 -0
- package/corpus/templates/tasks/.claude/commands/visual-plan.md +9 -0
- package/corpus/templates/tasks/.claude/commands/visual-recap.md +8 -0
- package/corpus/templates/tasks/.env.example +5 -0
- package/corpus/templates/tasks/.ignore +0 -0
- package/corpus/templates/tasks/.prettierrc +5 -0
- package/corpus/templates/tasks/AGENTS.md +158 -0
- package/corpus/templates/tasks/DEVELOPING.md +185 -0
- package/corpus/templates/tasks/README.md +16 -0
- package/corpus/templates/tasks/actions/bulk-delete-inbox-items.ts +24 -0
- package/corpus/templates/tasks/actions/bulk-delete-tasks.ts +21 -0
- package/corpus/templates/tasks/actions/bulk-mark-inbox-items-ready.ts +27 -0
- package/corpus/templates/tasks/actions/bulk-update-tasks.ts +35 -0
- package/corpus/templates/tasks/actions/create-custom-field.ts +22 -0
- package/corpus/templates/tasks/actions/create-inbox-item.ts +30 -0
- package/corpus/templates/tasks/actions/create-task.ts +33 -0
- package/corpus/templates/tasks/actions/delete-custom-field.ts +19 -0
- package/corpus/templates/tasks/actions/delete-inbox-item.ts +17 -0
- package/corpus/templates/tasks/actions/delete-task.ts +17 -0
- package/corpus/templates/tasks/actions/lib/boolean-query-param.ts +30 -0
- package/corpus/templates/tasks/actions/list-custom-fields.ts +18 -0
- package/corpus/templates/tasks/actions/list-inbox-items.ts +16 -0
- package/corpus/templates/tasks/actions/list-tasks.ts +52 -0
- package/corpus/templates/tasks/actions/list-visible-task-fields.ts +18 -0
- package/corpus/templates/tasks/actions/mark-inbox-item-ready.ts +31 -0
- package/corpus/templates/tasks/actions/navigate.ts +51 -0
- package/corpus/templates/tasks/actions/reorder-custom-fields.ts +27 -0
- package/corpus/templates/tasks/actions/reorder-inbox-items.ts +24 -0
- package/corpus/templates/tasks/actions/reorder-tasks.ts +29 -0
- package/corpus/templates/tasks/actions/run.ts +2 -0
- package/corpus/templates/tasks/actions/update-custom-field.ts +33 -0
- package/corpus/templates/tasks/actions/update-inbox-item.ts +24 -0
- package/corpus/templates/tasks/actions/update-task.ts +68 -0
- package/corpus/templates/tasks/actions/update-visible-task-fields.ts +27 -0
- package/corpus/templates/tasks/actions/view-screen-helpers.ts +68 -0
- package/corpus/templates/tasks/actions/view-screen.ts +207 -0
- package/corpus/templates/tasks/app/components/custom-fields/FieldCreateBar.tsx +121 -0
- package/corpus/templates/tasks/app/components/custom-fields/FieldListRow.tsx +116 -0
- package/corpus/templates/tasks/app/components/custom-fields/FieldsList.tsx +225 -0
- package/corpus/templates/tasks/app/components/custom-fields/FieldsListSkeleton.tsx +18 -0
- package/corpus/templates/tasks/app/components/custom-fields/FieldsPage.tsx +42 -0
- package/corpus/templates/tasks/app/components/custom-fields/editor/FieldEditorSidebar.tsx +69 -0
- package/corpus/templates/tasks/app/components/custom-fields/editor/FieldTitleSection.tsx +32 -0
- package/corpus/templates/tasks/app/components/custom-fields/editor/config/CurrencyConfigControl.tsx +49 -0
- package/corpus/templates/tasks/app/components/custom-fields/editor/config/FieldConfigControl.tsx +56 -0
- package/corpus/templates/tasks/app/components/custom-fields/editor/config/NumberConfigControl.tsx +52 -0
- package/corpus/templates/tasks/app/components/custom-fields/editor/config/PercentConfigControl.tsx +30 -0
- package/corpus/templates/tasks/app/components/custom-fields/editor/config/SelectConfigControl.tsx +135 -0
- package/corpus/templates/tasks/app/components/custom-fields/editor/config/select-colors.ts +23 -0
- package/corpus/templates/tasks/app/components/custom-fields/editor/config/utils.ts +52 -0
- package/corpus/templates/tasks/app/components/custom-fields/editor/types.ts +7 -0
- package/corpus/templates/tasks/app/components/dnd/SortableItem.tsx +72 -0
- package/corpus/templates/tasks/app/components/dnd/SortableList.tsx +136 -0
- package/corpus/templates/tasks/app/components/dnd/reorder-moving-items.ts +160 -0
- package/corpus/templates/tasks/app/components/dnd/sortable-drag-props.ts +60 -0
- package/corpus/templates/tasks/app/components/inbox/InboxList.tsx +222 -0
- package/corpus/templates/tasks/app/components/inbox/InboxListPage.tsx +39 -0
- package/corpus/templates/tasks/app/components/inbox/InboxListRow.tsx +122 -0
- package/corpus/templates/tasks/app/components/layout/Header.tsx +55 -0
- package/corpus/templates/tasks/app/components/layout/HeaderActions.tsx +84 -0
- package/corpus/templates/tasks/app/components/layout/Layout.tsx +134 -0
- package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +211 -0
- package/corpus/templates/tasks/app/components/shared/AddListItemInput.tsx +57 -0
- package/corpus/templates/tasks/app/components/shared/AlertDialog.tsx +53 -0
- package/corpus/templates/tasks/app/components/shared/BulkDeleteDialog.tsx +76 -0
- package/corpus/templates/tasks/app/components/shared/ChipSelect.tsx +240 -0
- package/corpus/templates/tasks/app/components/shared/DeleteItemDialog.tsx +40 -0
- package/corpus/templates/tasks/app/components/shared/InlineEditable.tsx +166 -0
- package/corpus/templates/tasks/app/components/shared/ListEmptyState.tsx +13 -0
- package/corpus/templates/tasks/app/components/shared/ListErrorMessage.tsx +15 -0
- package/corpus/templates/tasks/app/components/shared/ListViewHeader.tsx +75 -0
- package/corpus/templates/tasks/app/components/shared/PageHeader.tsx +25 -0
- package/corpus/templates/tasks/app/components/shared/RowActionsMenu.tsx +42 -0
- package/corpus/templates/tasks/app/components/shared/SidePanel.tsx +99 -0
- package/corpus/templates/tasks/app/components/shared/dnd/ListRowPreview.tsx +37 -0
- package/corpus/templates/tasks/app/components/shared/list/List.tsx +63 -0
- package/corpus/templates/tasks/app/components/shared/list/ListRow.tsx +159 -0
- package/corpus/templates/tasks/app/components/shared/list/ListRowDragHandle.tsx +52 -0
- package/corpus/templates/tasks/app/components/shared/list/ListSkeletonRows.tsx +11 -0
- package/corpus/templates/tasks/app/components/shared/list/SortableListItemShell.tsx +32 -0
- package/corpus/templates/tasks/app/components/shared/list/types.ts +1 -0
- package/corpus/templates/tasks/app/components/shared/selection/ListSelectionBar.tsx +158 -0
- package/corpus/templates/tasks/app/components/shared/selection/ListSelectionHeaderToggle.tsx +28 -0
- package/corpus/templates/tasks/app/components/shared/selection/ListSelectionToolbar.tsx +81 -0
- package/corpus/templates/tasks/app/components/shared/selection/get-list-row-selection-ui.ts +41 -0
- package/corpus/templates/tasks/app/components/shared/selection/use-list-selection.ts +196 -0
- package/corpus/templates/tasks/app/components/tasks/TaskList.tsx +333 -0
- package/corpus/templates/tasks/app/components/tasks/TaskListHeaderRow.tsx +46 -0
- package/corpus/templates/tasks/app/components/tasks/TaskListPage.tsx +115 -0
- package/corpus/templates/tasks/app/components/tasks/TaskListRow.css +49 -0
- package/corpus/templates/tasks/app/components/tasks/TaskListRow.tsx +307 -0
- package/corpus/templates/tasks/app/components/tasks/fields/TaskFieldsSidebar.tsx +102 -0
- package/corpus/templates/tasks/app/components/tasks/fields/TaskTitleSection.tsx +27 -0
- package/corpus/templates/tasks/app/components/tasks/fields/controls/DateValueControl.tsx +21 -0
- package/corpus/templates/tasks/app/components/tasks/fields/controls/FieldValueControl.tsx +76 -0
- package/corpus/templates/tasks/app/components/tasks/fields/controls/MultiSelectValueControl.tsx +52 -0
- package/corpus/templates/tasks/app/components/tasks/fields/controls/NumberValueControl.tsx +56 -0
- package/corpus/templates/tasks/app/components/tasks/fields/controls/RichTextValueControl.tsx +115 -0
- package/corpus/templates/tasks/app/components/tasks/fields/controls/SingleSelectValueControl.tsx +61 -0
- package/corpus/templates/tasks/app/components/tasks/fields/controls/TextValueControl.tsx +20 -0
- package/corpus/templates/tasks/app/components/tasks/use-task-row-completion-animation.ts +181 -0
- package/corpus/templates/tasks/app/components/ui/accordion.tsx +56 -0
- package/corpus/templates/tasks/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/alert.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/aspect-ratio.tsx +5 -0
- package/corpus/templates/tasks/app/components/ui/avatar.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/badge.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/breadcrumb.tsx +115 -0
- package/corpus/templates/tasks/app/components/ui/button.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/calendar.tsx +70 -0
- package/corpus/templates/tasks/app/components/ui/card.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/carousel.tsx +260 -0
- package/corpus/templates/tasks/app/components/ui/chart.tsx +379 -0
- package/corpus/templates/tasks/app/components/ui/checkbox.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/collapsible.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/command.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/context-menu.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/dialog.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/drawer.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/form.tsx +177 -0
- package/corpus/templates/tasks/app/components/ui/hover-card.tsx +27 -0
- package/corpus/templates/tasks/app/components/ui/input-otp.tsx +69 -0
- package/corpus/templates/tasks/app/components/ui/input.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/label.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/menubar.tsx +254 -0
- package/corpus/templates/tasks/app/components/ui/navigation-menu.tsx +128 -0
- package/corpus/templates/tasks/app/components/ui/pagination.tsx +121 -0
- package/corpus/templates/tasks/app/components/ui/popover.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/progress.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/radio-group.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/resizable.tsx +43 -0
- package/corpus/templates/tasks/app/components/ui/scroll-area.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/select.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/separator.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/sheet.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/sidebar.tsx +782 -0
- package/corpus/templates/tasks/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/slider.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/sonner.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/spinner.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/table.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/tabs.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/textarea.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/toast.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/toaster.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/toggle-group.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/toggle.tsx +43 -0
- package/corpus/templates/tasks/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/tasks/app/components/ui/use-toast.ts +3 -0
- package/corpus/templates/tasks/app/entry.client.tsx +19 -0
- package/corpus/templates/tasks/app/entry.server.tsx +10 -0
- package/corpus/templates/tasks/app/global.css +93 -0
- package/corpus/templates/tasks/app/hooks/cache.ts +31 -0
- package/corpus/templates/tasks/app/hooks/use-custom-fields.ts +93 -0
- package/corpus/templates/tasks/app/hooks/use-inbox-items.ts +196 -0
- package/corpus/templates/tasks/app/hooks/use-mobile.tsx +21 -0
- package/corpus/templates/tasks/app/hooks/use-navigation-state.ts +40 -0
- package/corpus/templates/tasks/app/hooks/use-tasks.ts +276 -0
- package/corpus/templates/tasks/app/hooks/use-toast.ts +188 -0
- package/corpus/templates/tasks/app/hooks/use-visible-task-fields.ts +140 -0
- package/corpus/templates/tasks/app/lib/app-config.ts +11 -0
- package/corpus/templates/tasks/app/lib/tab-id.ts +1 -0
- package/corpus/templates/tasks/app/lib/utils.ts +1 -0
- package/corpus/templates/tasks/app/root.tsx +130 -0
- package/corpus/templates/tasks/app/routes/_index.tsx +22 -0
- package/corpus/templates/tasks/app/routes/extensions.$id.$slug.tsx +2 -0
- package/corpus/templates/tasks/app/routes/extensions.$id.tsx +11 -0
- package/corpus/templates/tasks/app/routes/extensions._index.tsx +11 -0
- package/corpus/templates/tasks/app/routes/extensions.tsx +5 -0
- package/corpus/templates/tasks/app/routes/fields.tsx +16 -0
- package/corpus/templates/tasks/app/routes/inbox.tsx +16 -0
- package/corpus/templates/tasks/app/routes/tasks.tsx +17 -0
- package/corpus/templates/tasks/app/routes/team.tsx +17 -0
- package/corpus/templates/tasks/app/routes.ts +4 -0
- package/corpus/templates/tasks/app/vite-env.d.ts +6 -0
- package/corpus/templates/tasks/components.json +20 -0
- package/corpus/templates/tasks/data/sync-config.json +1 -0
- package/corpus/templates/tasks/docs/features/README.md +81 -0
- package/corpus/templates/tasks/docs/features/f1-tasks.mdx +67 -0
- package/corpus/templates/tasks/docs/features/f2-custom-fields.mdx +68 -0
- package/corpus/templates/tasks/docs/features/f3-projects.mdx +33 -0
- package/corpus/templates/tasks/docs/features/f4-inbox.mdx +55 -0
- package/corpus/templates/tasks/docs/features/f5-views.mdx +65 -0
- package/corpus/templates/tasks/docs/features/f6-search.mdx +43 -0
- package/corpus/templates/tasks/docs/features/f7-first-run.mdx +66 -0
- package/corpus/templates/tasks/docs/features/f8-chat.mdx +120 -0
- package/corpus/templates/tasks/e2e/global-setup.ts +42 -0
- package/corpus/templates/tasks/e2e/helpers/inbox.ts +71 -0
- package/corpus/templates/tasks/e2e/helpers/tasks.ts +99 -0
- package/corpus/templates/tasks/learnings.defaults.md +5 -0
- package/corpus/templates/tasks/netlify.toml +10 -0
- package/corpus/templates/tasks/package.json +104 -0
- package/corpus/templates/tasks/playwright.config.ts +34 -0
- package/corpus/templates/tasks/public/agent-native-icon-dark.svg +10 -0
- package/corpus/templates/tasks/public/agent-native-icon-light.svg +10 -0
- package/corpus/templates/tasks/public/agent-native-logo-dark.svg +21 -0
- package/corpus/templates/tasks/public/agent-native-logo-light.svg +21 -0
- package/corpus/templates/tasks/public/favicon.svg +1 -0
- package/corpus/templates/tasks/public/icon-180.svg +1 -0
- package/corpus/templates/tasks/public/icon-192.svg +1 -0
- package/corpus/templates/tasks/public/icon-512.svg +1 -0
- package/corpus/templates/tasks/public/manifest.json +21 -0
- package/corpus/templates/tasks/react-router.config.ts +7 -0
- package/corpus/templates/tasks/server/custom-fields/normalize.ts +163 -0
- package/corpus/templates/tasks/server/custom-fields/parse.ts +146 -0
- package/corpus/templates/tasks/server/custom-fields/schema.ts +147 -0
- package/corpus/templates/tasks/server/custom-fields/store.ts +425 -0
- package/corpus/templates/tasks/server/custom-fields/task-fields.ts +104 -0
- package/corpus/templates/tasks/server/custom-fields/types.ts +98 -0
- package/corpus/templates/tasks/server/custom-fields/validate.ts +210 -0
- package/corpus/templates/tasks/server/custom-fields/values/store.ts +386 -0
- package/corpus/templates/tasks/server/db/bulk-write.ts +30 -0
- package/corpus/templates/tasks/server/db/index.ts +5 -0
- package/corpus/templates/tasks/server/db/record-utils.ts +7 -0
- package/corpus/templates/tasks/server/db/schema.ts +67 -0
- package/corpus/templates/tasks/server/db/test-tasks-table.ts +76 -0
- package/corpus/templates/tasks/server/db/transaction.ts +6 -0
- package/corpus/templates/tasks/server/errors.ts +31 -0
- package/corpus/templates/tasks/server/inbox/store.ts +216 -0
- package/corpus/templates/tasks/server/middleware/auth.ts +15 -0
- package/corpus/templates/tasks/server/plugins/agent-chat.ts +46 -0
- package/corpus/templates/tasks/server/plugins/auth.ts +15 -0
- package/corpus/templates/tasks/server/plugins/db.ts +90 -0
- package/corpus/templates/tasks/server/routes/[...page].get.ts +5 -0
- package/corpus/templates/tasks/server/stored-items/store.ts +548 -0
- package/corpus/templates/tasks/server/tasks/store.ts +306 -0
- package/corpus/templates/tasks/server/user-config/store.ts +138 -0
- package/corpus/templates/tasks/shared/app-state-tabs.ts +15 -0
- package/corpus/templates/tasks/shared/boolean-param.ts +22 -0
- package/corpus/templates/tasks/shared/bulk-limits.ts +7 -0
- package/corpus/templates/tasks/shared/list-screen-snapshot.ts +90 -0
- package/corpus/templates/tasks/shared/navigation.ts +104 -0
- package/corpus/templates/tasks/shared/visible-task-fields.ts +2 -0
- package/corpus/templates/tasks/ssr-entry.ts +15 -0
- package/corpus/templates/tasks/tsconfig.json +24 -0
- package/corpus/templates/tasks/vite.config.ts +19 -0
- package/corpus/templates/tasks/vitest.config.ts +8 -0
- package/dist/cli/templates-meta.d.ts.map +1 -1
- package/dist/cli/templates-meta.js +13 -0
- package/dist/cli/templates-meta.js.map +1 -1
- package/dist/client/agent-page/AgentTabsPage.js +1 -1
- package/dist/client/agent-page/AgentTabsPage.js.map +1 -1
- package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
- package/dist/client/org/OrgSwitcher.js +2 -1
- package/dist/client/org/OrgSwitcher.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { UserInputError } from "../errors.js";
|
|
2
|
+
import {
|
|
3
|
+
SELECT_COLOR_TOKENS,
|
|
4
|
+
type FieldConfigInput,
|
|
5
|
+
type FieldDefinition,
|
|
6
|
+
type FieldType,
|
|
7
|
+
type FieldValue,
|
|
8
|
+
type FieldValueInput,
|
|
9
|
+
type SelectConfigInput,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
|
|
12
|
+
export function validateFieldTitle(title: string): void {
|
|
13
|
+
if (!title.trim()) throw new UserInputError("Field title is required.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function validatePrecision(precision: number | undefined): void {
|
|
17
|
+
if (precision === undefined) return;
|
|
18
|
+
if (!Number.isInteger(precision) || precision < 0 || precision > 6) {
|
|
19
|
+
throw new UserInputError("precision must be an integer between 0 and 6.");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validateSelectColor(color: string | undefined): void {
|
|
24
|
+
if (color === undefined) return;
|
|
25
|
+
if (!(SELECT_COLOR_TOKENS as readonly string[]).includes(color)) {
|
|
26
|
+
throw new UserInputError("Select option color is invalid.");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validateSelectConfig(config: SelectConfigInput): void {
|
|
31
|
+
for (const [index, option] of (config.options ?? []).entries()) {
|
|
32
|
+
if (!option.name.trim()) {
|
|
33
|
+
throw new UserInputError(`Select option #${index + 1} name is required.`);
|
|
34
|
+
}
|
|
35
|
+
if (option.id?.trim() === "") {
|
|
36
|
+
throw new UserInputError(
|
|
37
|
+
`Select option #${index + 1} id cannot be empty.`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const sortOrder = option.sortOrder;
|
|
41
|
+
if (
|
|
42
|
+
sortOrder !== undefined &&
|
|
43
|
+
(!Number.isInteger(sortOrder) || sortOrder < 0)
|
|
44
|
+
) {
|
|
45
|
+
throw new UserInputError(
|
|
46
|
+
`Select option #${index + 1} sortOrder must be a non-negative integer.`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
validateSelectColor(option.color);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function validateFieldConfig<T extends FieldType>(
|
|
54
|
+
type: T,
|
|
55
|
+
config?: FieldConfigInput<T>,
|
|
56
|
+
): void {
|
|
57
|
+
if (type === "text" || type === "rich_text" || type === "date") {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const input = (config ?? {}) as FieldConfigInput<T>;
|
|
62
|
+
|
|
63
|
+
if (type === "number") {
|
|
64
|
+
const numberConfig = input as FieldConfigInput<"number">;
|
|
65
|
+
validatePrecision(numberConfig.precision);
|
|
66
|
+
if (
|
|
67
|
+
numberConfig.positiveOnly !== undefined &&
|
|
68
|
+
typeof numberConfig.positiveOnly !== "boolean"
|
|
69
|
+
) {
|
|
70
|
+
throw new UserInputError("positiveOnly must be a boolean.");
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (type === "percent") {
|
|
76
|
+
validatePrecision((input as FieldConfigInput<"percent">).precision);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (type === "currency") {
|
|
81
|
+
const currencyConfig = input as FieldConfigInput<"currency">;
|
|
82
|
+
if (currencyConfig.symbol !== undefined) {
|
|
83
|
+
const symbol = currencyConfig.symbol.trim();
|
|
84
|
+
if (!symbol) throw new UserInputError("Currency symbol is required.");
|
|
85
|
+
if (symbol.length > 8) {
|
|
86
|
+
throw new UserInputError(
|
|
87
|
+
"Currency symbol must be at most 8 characters.",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
validatePrecision(currencyConfig.precision);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
validateSelectConfig(input as SelectConfigInput);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function assertPrecision(value: number, precision: number): void {
|
|
99
|
+
const factor = 10 ** precision;
|
|
100
|
+
const scaled = value * factor;
|
|
101
|
+
const tolerance = Number.EPSILON * Math.max(1, Math.abs(scaled)) * 100;
|
|
102
|
+
if (Math.abs(scaled - Math.round(scaled)) > tolerance) {
|
|
103
|
+
throw new UserInputError(
|
|
104
|
+
precision === 0
|
|
105
|
+
? "Value must be a whole number."
|
|
106
|
+
: `Value must have at most ${precision} decimal places.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function validateNumericValue(
|
|
112
|
+
value: number,
|
|
113
|
+
precision: number,
|
|
114
|
+
options?: { positiveOnly?: boolean },
|
|
115
|
+
): void {
|
|
116
|
+
if (!Number.isFinite(value)) {
|
|
117
|
+
throw new UserInputError("Invalid number value.");
|
|
118
|
+
}
|
|
119
|
+
assertPrecision(value, precision);
|
|
120
|
+
if (options?.positiveOnly && value < 0) {
|
|
121
|
+
throw new UserInputError("Value must be zero or positive.");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function validateIsoDate(value: string): void {
|
|
126
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
127
|
+
throw new UserInputError("Invalid date value.");
|
|
128
|
+
}
|
|
129
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
130
|
+
const parsed = new Date(Date.UTC(year, month - 1, day));
|
|
131
|
+
if (
|
|
132
|
+
parsed.getUTCFullYear() !== year ||
|
|
133
|
+
parsed.getUTCMonth() !== month - 1 ||
|
|
134
|
+
parsed.getUTCDate() !== day
|
|
135
|
+
) {
|
|
136
|
+
throw new UserInputError("Invalid date value.");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function validateSelectValue(
|
|
141
|
+
field: Extract<FieldDefinition, { type: "single_select" | "multi_select" }>,
|
|
142
|
+
value: FieldValue,
|
|
143
|
+
): void {
|
|
144
|
+
const allowed = new Set(field.config.options.map((option) => option.id));
|
|
145
|
+
|
|
146
|
+
if (field.type === "single_select") {
|
|
147
|
+
if (!allowed.has(value as string)) {
|
|
148
|
+
throw new UserInputError("Select value is not a valid option.");
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const unique = [...new Set(value as string[])];
|
|
154
|
+
for (const optionId of unique) {
|
|
155
|
+
if (!allowed.has(optionId)) {
|
|
156
|
+
throw new UserInputError("Select value is not a valid option.");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function validateFieldValue(
|
|
162
|
+
field: FieldDefinition,
|
|
163
|
+
value: FieldValueInput,
|
|
164
|
+
): void {
|
|
165
|
+
switch (field.type) {
|
|
166
|
+
case "text":
|
|
167
|
+
case "rich_text":
|
|
168
|
+
if (typeof value !== "string") {
|
|
169
|
+
throw new UserInputError("Invalid text value.");
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
case "number":
|
|
173
|
+
if (typeof value !== "number") {
|
|
174
|
+
throw new UserInputError("Invalid number value.");
|
|
175
|
+
}
|
|
176
|
+
validateNumericValue(value, field.config.precision ?? 0, {
|
|
177
|
+
positiveOnly: field.config.positiveOnly,
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
case "percent":
|
|
181
|
+
if (typeof value !== "number") {
|
|
182
|
+
throw new UserInputError("Invalid number value.");
|
|
183
|
+
}
|
|
184
|
+
validateNumericValue(value, field.config.precision ?? 0);
|
|
185
|
+
return;
|
|
186
|
+
case "currency":
|
|
187
|
+
if (typeof value !== "number") {
|
|
188
|
+
throw new UserInputError("Invalid number value.");
|
|
189
|
+
}
|
|
190
|
+
validateNumericValue(value, field.config.precision ?? 2);
|
|
191
|
+
return;
|
|
192
|
+
case "date":
|
|
193
|
+
if (typeof value !== "string") {
|
|
194
|
+
throw new UserInputError("Invalid date value.");
|
|
195
|
+
}
|
|
196
|
+
validateIsoDate(value);
|
|
197
|
+
return;
|
|
198
|
+
case "single_select":
|
|
199
|
+
if (typeof value !== "string") {
|
|
200
|
+
throw new UserInputError("Invalid select value.");
|
|
201
|
+
}
|
|
202
|
+
validateSelectValue(field, value);
|
|
203
|
+
return;
|
|
204
|
+
case "multi_select":
|
|
205
|
+
if (!Array.isArray(value)) {
|
|
206
|
+
throw new UserInputError("Invalid select value.");
|
|
207
|
+
}
|
|
208
|
+
validateSelectValue(field, value);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { and, eq, inArray, sql, type SQL } from "drizzle-orm";
|
|
2
|
+
|
|
3
|
+
import { caseById, chunk } from "../../db/bulk-write.js";
|
|
4
|
+
import { getDb } from "../../db/index.js";
|
|
5
|
+
import { createRecordId, timestamp } from "../../db/record-utils.js";
|
|
6
|
+
import {
|
|
7
|
+
customFields,
|
|
8
|
+
customFieldValues,
|
|
9
|
+
type StoredCustomFieldValue,
|
|
10
|
+
} from "../../db/schema.js";
|
|
11
|
+
import type { DbHandle } from "../../db/transaction.js";
|
|
12
|
+
import { NotFoundError, UserInputError } from "../../errors.js";
|
|
13
|
+
import { getStoredItem } from "../../stored-items/store.js";
|
|
14
|
+
import { isEmptyFieldValue, normalizeFieldValue } from "../normalize.js";
|
|
15
|
+
import {
|
|
16
|
+
parseField,
|
|
17
|
+
parseStoredValue,
|
|
18
|
+
parseFieldValueShape,
|
|
19
|
+
} from "../parse.js";
|
|
20
|
+
import type { FieldDefinition, FieldValue, FieldValueInput } from "../types.js";
|
|
21
|
+
import { validateFieldValue } from "../validate.js";
|
|
22
|
+
|
|
23
|
+
export type { FieldValue, FieldValueInput } from "../types.js";
|
|
24
|
+
|
|
25
|
+
export type PreparedFieldValuePatch = {
|
|
26
|
+
field: FieldDefinition;
|
|
27
|
+
value: FieldValue | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export async function prepareCustomFieldValuePatches(
|
|
31
|
+
input: {
|
|
32
|
+
ownerEmail: string;
|
|
33
|
+
taskId: string;
|
|
34
|
+
values: Array<{ fieldId: string; value: FieldValueInput }>;
|
|
35
|
+
},
|
|
36
|
+
db: DbHandle = getDb(),
|
|
37
|
+
): Promise<Map<string, PreparedFieldValuePatch>> {
|
|
38
|
+
if (input.values.length === 0) {
|
|
39
|
+
return new Map();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const task = await getStoredItem(
|
|
43
|
+
{
|
|
44
|
+
ownerEmail: input.ownerEmail,
|
|
45
|
+
id: input.taskId,
|
|
46
|
+
promotedToTask: true,
|
|
47
|
+
},
|
|
48
|
+
db,
|
|
49
|
+
);
|
|
50
|
+
if (!task) throw new NotFoundError("Task not found.");
|
|
51
|
+
|
|
52
|
+
const fields = await db
|
|
53
|
+
.select()
|
|
54
|
+
.from(customFields)
|
|
55
|
+
.where(
|
|
56
|
+
and(
|
|
57
|
+
eq(customFields.ownerEmail, input.ownerEmail),
|
|
58
|
+
inArray(
|
|
59
|
+
customFields.id,
|
|
60
|
+
input.values.map((value) => value.fieldId),
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const fieldsById = new Map(
|
|
66
|
+
fields.map((field) => [field.id, parseField(field)]),
|
|
67
|
+
);
|
|
68
|
+
const normalizedValues = new Map<string, PreparedFieldValuePatch>();
|
|
69
|
+
for (const value of input.values) {
|
|
70
|
+
const field = fieldsById.get(value.fieldId);
|
|
71
|
+
if (!field) throw new NotFoundError("Custom field not found.");
|
|
72
|
+
let normalizedValue: FieldValue | null;
|
|
73
|
+
const shaped = parseFieldValueShape(value.value);
|
|
74
|
+
if (isEmptyFieldValue(shaped)) {
|
|
75
|
+
normalizedValue = null;
|
|
76
|
+
} else {
|
|
77
|
+
validateFieldValue(field, shaped);
|
|
78
|
+
normalizedValue = normalizeFieldValue(field, shaped);
|
|
79
|
+
}
|
|
80
|
+
normalizedValues.set(value.fieldId, {
|
|
81
|
+
field,
|
|
82
|
+
value: normalizedValue,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return normalizedValues;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function applyCustomFieldValuePatches(
|
|
90
|
+
input: {
|
|
91
|
+
ownerEmail: string;
|
|
92
|
+
taskId: string;
|
|
93
|
+
patches: Map<string, PreparedFieldValuePatch>;
|
|
94
|
+
updatedAt: string;
|
|
95
|
+
},
|
|
96
|
+
db: DbHandle = getDb(),
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
const patches = [...input.patches.values()];
|
|
99
|
+
const clearedFieldIds = patches
|
|
100
|
+
.filter((patch) => patch.value === null)
|
|
101
|
+
.map((patch) => patch.field.id);
|
|
102
|
+
const rows = patches
|
|
103
|
+
.filter((patch) => patch.value !== null)
|
|
104
|
+
.map((patch) => ({
|
|
105
|
+
id: createRecordId("cfv"),
|
|
106
|
+
fieldId: patch.field.id,
|
|
107
|
+
taskId: input.taskId,
|
|
108
|
+
valueJson: JSON.stringify(patch.value),
|
|
109
|
+
ownerEmail: input.ownerEmail,
|
|
110
|
+
createdAt: input.updatedAt,
|
|
111
|
+
updatedAt: input.updatedAt,
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
if (clearedFieldIds.length > 0) {
|
|
115
|
+
await db
|
|
116
|
+
.delete(customFieldValues)
|
|
117
|
+
.where(
|
|
118
|
+
and(
|
|
119
|
+
eq(customFieldValues.ownerEmail, input.ownerEmail),
|
|
120
|
+
eq(customFieldValues.taskId, input.taskId),
|
|
121
|
+
inArray(customFieldValues.fieldId, clearedFieldIds),
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const group of chunk(rows)) {
|
|
127
|
+
await db
|
|
128
|
+
.insert(customFieldValues)
|
|
129
|
+
.values(group)
|
|
130
|
+
.onConflictDoUpdate({
|
|
131
|
+
target: [
|
|
132
|
+
customFieldValues.ownerEmail,
|
|
133
|
+
customFieldValues.taskId,
|
|
134
|
+
customFieldValues.fieldId,
|
|
135
|
+
],
|
|
136
|
+
set: {
|
|
137
|
+
valueJson: sql`excluded.value_json`,
|
|
138
|
+
updatedAt: input.updatedAt,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function getCustomFieldValue(
|
|
145
|
+
input: {
|
|
146
|
+
ownerEmail: string;
|
|
147
|
+
taskId: string;
|
|
148
|
+
fieldId: string;
|
|
149
|
+
},
|
|
150
|
+
db: DbHandle = getDb(),
|
|
151
|
+
): Promise<FieldValue | null> {
|
|
152
|
+
const [row] = await db
|
|
153
|
+
.select()
|
|
154
|
+
.from(customFieldValues)
|
|
155
|
+
.where(
|
|
156
|
+
and(
|
|
157
|
+
eq(customFieldValues.ownerEmail, input.ownerEmail),
|
|
158
|
+
eq(customFieldValues.taskId, input.taskId),
|
|
159
|
+
eq(customFieldValues.fieldId, input.fieldId),
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
.limit(1);
|
|
163
|
+
if (!row) return null;
|
|
164
|
+
|
|
165
|
+
const [fieldRow] = await db
|
|
166
|
+
.select()
|
|
167
|
+
.from(customFields)
|
|
168
|
+
.where(
|
|
169
|
+
and(
|
|
170
|
+
eq(customFields.ownerEmail, input.ownerEmail),
|
|
171
|
+
eq(customFields.id, input.fieldId),
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
.limit(1);
|
|
175
|
+
if (!fieldRow) throw new NotFoundError("Custom field not found.");
|
|
176
|
+
|
|
177
|
+
return parseStoredValue(parseField(fieldRow), row);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function listCustomFieldValues(
|
|
181
|
+
input: {
|
|
182
|
+
ownerEmail: string;
|
|
183
|
+
ids?: string[];
|
|
184
|
+
taskIds?: string[];
|
|
185
|
+
fieldIds?: string[];
|
|
186
|
+
},
|
|
187
|
+
db: DbHandle = getDb(),
|
|
188
|
+
): Promise<StoredCustomFieldValue[]> {
|
|
189
|
+
const selector = buildValueSelector(input);
|
|
190
|
+
if (!selector) {
|
|
191
|
+
throw new UserInputError(
|
|
192
|
+
"Provide ids, taskIds, or fieldIds to list custom field values.",
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (selector.selectsNothing) return [];
|
|
196
|
+
|
|
197
|
+
return db
|
|
198
|
+
.select()
|
|
199
|
+
.from(customFieldValues)
|
|
200
|
+
.where(and(...selector.conditions));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function updateCustomFieldValues(
|
|
204
|
+
input: {
|
|
205
|
+
ownerEmail: string;
|
|
206
|
+
entries: Array<{ id: string; value: FieldValue }>;
|
|
207
|
+
now?: string;
|
|
208
|
+
},
|
|
209
|
+
db: DbHandle = getDb(),
|
|
210
|
+
): Promise<void> {
|
|
211
|
+
if (input.entries.length === 0) return;
|
|
212
|
+
|
|
213
|
+
const updatedAt = timestamp(input.now);
|
|
214
|
+
const entries = input.entries.map((entry) => ({
|
|
215
|
+
id: entry.id,
|
|
216
|
+
value: JSON.stringify(entry.value),
|
|
217
|
+
}));
|
|
218
|
+
|
|
219
|
+
for (const group of chunk(entries)) {
|
|
220
|
+
await db
|
|
221
|
+
.update(customFieldValues)
|
|
222
|
+
.set({ valueJson: caseById(customFieldValues.id, group), updatedAt })
|
|
223
|
+
.where(
|
|
224
|
+
and(
|
|
225
|
+
eq(customFieldValues.ownerEmail, input.ownerEmail),
|
|
226
|
+
inArray(
|
|
227
|
+
customFieldValues.id,
|
|
228
|
+
group.map((entry) => entry.id),
|
|
229
|
+
),
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function updateCustomFieldValue(
|
|
236
|
+
input: {
|
|
237
|
+
ownerEmail: string;
|
|
238
|
+
id: string;
|
|
239
|
+
value: FieldValue;
|
|
240
|
+
now?: string;
|
|
241
|
+
},
|
|
242
|
+
db: DbHandle = getDb(),
|
|
243
|
+
): Promise<void> {
|
|
244
|
+
await updateCustomFieldValues(
|
|
245
|
+
{
|
|
246
|
+
ownerEmail: input.ownerEmail,
|
|
247
|
+
entries: [{ id: input.id, value: input.value }],
|
|
248
|
+
now: input.now,
|
|
249
|
+
},
|
|
250
|
+
db,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Upsert a task's values by field id; an empty value clears the stored row. */
|
|
255
|
+
export async function updateCustomFieldValuesByTaskId(
|
|
256
|
+
input: {
|
|
257
|
+
ownerEmail: string;
|
|
258
|
+
taskId: string;
|
|
259
|
+
values: Array<{ fieldId: string; value: FieldValueInput }>;
|
|
260
|
+
now?: string;
|
|
261
|
+
},
|
|
262
|
+
db: DbHandle = getDb(),
|
|
263
|
+
): Promise<void> {
|
|
264
|
+
const patches = await prepareCustomFieldValuePatches(input, db);
|
|
265
|
+
if (patches.size === 0) return;
|
|
266
|
+
|
|
267
|
+
const updatedAt = timestamp(input.now);
|
|
268
|
+
await db.transaction(async (tx) => {
|
|
269
|
+
await applyCustomFieldValuePatches(
|
|
270
|
+
{
|
|
271
|
+
ownerEmail: input.ownerEmail,
|
|
272
|
+
taskId: input.taskId,
|
|
273
|
+
patches,
|
|
274
|
+
updatedAt,
|
|
275
|
+
},
|
|
276
|
+
tx,
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function deleteCustomFieldValues(
|
|
282
|
+
input: { ownerEmail: string; ids: string[] },
|
|
283
|
+
db: DbHandle = getDb(),
|
|
284
|
+
): Promise<{ deletedValues: number }> {
|
|
285
|
+
const ids = [...new Set(input.ids)];
|
|
286
|
+
if (ids.length === 0) return { deletedValues: 0 };
|
|
287
|
+
|
|
288
|
+
let deletedValues = 0;
|
|
289
|
+
for (const group of chunk(ids)) {
|
|
290
|
+
const result = await deleteValuesWhere(
|
|
291
|
+
input.ownerEmail,
|
|
292
|
+
[inArray(customFieldValues.id, group)],
|
|
293
|
+
db,
|
|
294
|
+
);
|
|
295
|
+
deletedValues += result.deletedValues;
|
|
296
|
+
}
|
|
297
|
+
return { deletedValues };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function deleteCustomFieldValue(
|
|
301
|
+
input: { ownerEmail: string; id: string },
|
|
302
|
+
db: DbHandle = getDb(),
|
|
303
|
+
): Promise<{ deletedValues: number }> {
|
|
304
|
+
return deleteCustomFieldValues(
|
|
305
|
+
{ ownerEmail: input.ownerEmail, ids: [input.id] },
|
|
306
|
+
db,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export async function deleteCustomFieldValuesByTaskIds(
|
|
311
|
+
input: { ownerEmail: string; taskIds: string[] },
|
|
312
|
+
db: DbHandle = getDb(),
|
|
313
|
+
): Promise<{ deletedValues: number }> {
|
|
314
|
+
const taskIds = [...new Set(input.taskIds)];
|
|
315
|
+
if (taskIds.length === 0) return { deletedValues: 0 };
|
|
316
|
+
|
|
317
|
+
return deleteValuesWhere(
|
|
318
|
+
input.ownerEmail,
|
|
319
|
+
[inArray(customFieldValues.taskId, taskIds)],
|
|
320
|
+
db,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export async function deleteCustomFieldValuesByFieldIds(
|
|
325
|
+
input: { ownerEmail: string; fieldIds: string[] },
|
|
326
|
+
db: DbHandle = getDb(),
|
|
327
|
+
): Promise<{ deletedValues: number }> {
|
|
328
|
+
const fieldIds = [...new Set(input.fieldIds)];
|
|
329
|
+
if (fieldIds.length === 0) return { deletedValues: 0 };
|
|
330
|
+
|
|
331
|
+
return deleteValuesWhere(
|
|
332
|
+
input.ownerEmail,
|
|
333
|
+
[inArray(customFieldValues.fieldId, fieldIds)],
|
|
334
|
+
db,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function buildValueSelector(input: {
|
|
339
|
+
ownerEmail: string;
|
|
340
|
+
ids?: string[];
|
|
341
|
+
taskIds?: string[];
|
|
342
|
+
fieldIds?: string[];
|
|
343
|
+
}): { conditions: SQL[]; selectsNothing: boolean } | null {
|
|
344
|
+
const ids = input.ids ? [...new Set(input.ids)] : undefined;
|
|
345
|
+
const taskIds = input.taskIds ? [...new Set(input.taskIds)] : undefined;
|
|
346
|
+
const fieldIds = input.fieldIds ? [...new Set(input.fieldIds)] : undefined;
|
|
347
|
+
if (!ids && !taskIds && !fieldIds) return null;
|
|
348
|
+
|
|
349
|
+
// An explicit empty id list selects nothing; without this it would fall
|
|
350
|
+
// through to matching every value the owner has.
|
|
351
|
+
const selectsNothing =
|
|
352
|
+
ids?.length === 0 || taskIds?.length === 0 || fieldIds?.length === 0;
|
|
353
|
+
|
|
354
|
+
const conditions: SQL[] = [
|
|
355
|
+
eq(customFieldValues.ownerEmail, input.ownerEmail),
|
|
356
|
+
];
|
|
357
|
+
if (ids?.length) conditions.push(inArray(customFieldValues.id, ids));
|
|
358
|
+
if (taskIds?.length)
|
|
359
|
+
conditions.push(inArray(customFieldValues.taskId, taskIds));
|
|
360
|
+
if (fieldIds?.length)
|
|
361
|
+
conditions.push(inArray(customFieldValues.fieldId, fieldIds));
|
|
362
|
+
|
|
363
|
+
return { conditions, selectsNothing };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function deleteValuesWhere(
|
|
367
|
+
ownerEmail: string,
|
|
368
|
+
selectors: SQL[],
|
|
369
|
+
db: DbHandle,
|
|
370
|
+
): Promise<{ deletedValues: number }> {
|
|
371
|
+
const conditions = [
|
|
372
|
+
eq(customFieldValues.ownerEmail, ownerEmail),
|
|
373
|
+
...selectors,
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
const values = await db
|
|
377
|
+
.select({ id: customFieldValues.id })
|
|
378
|
+
.from(customFieldValues)
|
|
379
|
+
.where(and(...conditions));
|
|
380
|
+
|
|
381
|
+
if (values.length > 0) {
|
|
382
|
+
await db.delete(customFieldValues).where(and(...conditions));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { deletedValues: values.length };
|
|
386
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { sql, type Column, type SQL } from "drizzle-orm";
|
|
2
|
+
|
|
3
|
+
export type CaseEntry<T> = { id: string; value: T };
|
|
4
|
+
|
|
5
|
+
/** `CASE <id> WHEN 'a' THEN … END` — writes a different value per row in one statement. */
|
|
6
|
+
export function caseById<T>(idColumn: Column, entries: CaseEntry<T>[]): SQL<T> {
|
|
7
|
+
const whens = entries.map(
|
|
8
|
+
(entry) => sql`when ${entry.id} then ${entry.value}`,
|
|
9
|
+
);
|
|
10
|
+
return sql`case ${idColumn} ${sql.join(whens, sql` `)} end`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* An entry costs three bind parameters (id and value in the CASE, id again in
|
|
15
|
+
* the IN list). Reorder and field cleanup rewrite every row an owner has, which
|
|
16
|
+
* `BULK_ID_LIMIT` does not cap, so the work is chunked to stay under SQLite's
|
|
17
|
+
* 999-parameter default.
|
|
18
|
+
*/
|
|
19
|
+
export const BULK_WRITE_CHUNK_SIZE = 200;
|
|
20
|
+
|
|
21
|
+
export function chunk<T>(
|
|
22
|
+
items: T[],
|
|
23
|
+
size: number = BULK_WRITE_CHUNK_SIZE,
|
|
24
|
+
): T[][] {
|
|
25
|
+
const chunks: T[][] = [];
|
|
26
|
+
for (let index = 0; index < items.length; index += size) {
|
|
27
|
+
chunks.push(items.slice(index, index + size));
|
|
28
|
+
}
|
|
29
|
+
return chunks;
|
|
30
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
integer,
|
|
3
|
+
now,
|
|
4
|
+
table,
|
|
5
|
+
text,
|
|
6
|
+
uniqueIndex,
|
|
7
|
+
} from "@agent-native/core/db/schema";
|
|
8
|
+
|
|
9
|
+
export const tasks = table("tasks", {
|
|
10
|
+
id: text("id").primaryKey(),
|
|
11
|
+
title: text("title").notNull(),
|
|
12
|
+
done: integer("done", { mode: "boolean" }).notNull().default(false),
|
|
13
|
+
promotedToTask: integer("promoted_to_task", { mode: "boolean" })
|
|
14
|
+
.notNull()
|
|
15
|
+
.default(true),
|
|
16
|
+
sortOrder: integer("sort_order").notNull().default(0),
|
|
17
|
+
ownerEmail: text("owner_email").notNull(),
|
|
18
|
+
createdAt: text("created_at").notNull().default(now()),
|
|
19
|
+
updatedAt: text("updated_at").notNull().default(now()),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export type StoredItem = typeof tasks.$inferSelect;
|
|
23
|
+
export type NewStoredItem = typeof tasks.$inferInsert;
|
|
24
|
+
|
|
25
|
+
export const customFields = table("custom_fields", {
|
|
26
|
+
id: text("id").primaryKey(),
|
|
27
|
+
title: text("title").notNull(),
|
|
28
|
+
type: text("type").notNull(),
|
|
29
|
+
configJson: text("config_json").notNull(),
|
|
30
|
+
sortOrder: integer("sort_order").notNull().default(0),
|
|
31
|
+
ownerEmail: text("owner_email").notNull(),
|
|
32
|
+
createdAt: text("created_at").notNull().default(now()),
|
|
33
|
+
updatedAt: text("updated_at").notNull().default(now()),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const customFieldValues = table(
|
|
37
|
+
"custom_field_values",
|
|
38
|
+
{
|
|
39
|
+
id: text("id").primaryKey(),
|
|
40
|
+
fieldId: text("field_id").notNull(),
|
|
41
|
+
taskId: text("task_id").notNull(),
|
|
42
|
+
valueJson: text("value_json").notNull(),
|
|
43
|
+
ownerEmail: text("owner_email").notNull(),
|
|
44
|
+
createdAt: text("created_at").notNull().default(now()),
|
|
45
|
+
updatedAt: text("updated_at").notNull().default(now()),
|
|
46
|
+
},
|
|
47
|
+
(values) => ({
|
|
48
|
+
uniqueTaskField: uniqueIndex(
|
|
49
|
+
"idx_custom_field_values_unique_task_field",
|
|
50
|
+
).on(values.ownerEmail, values.taskId, values.fieldId),
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export const userConfig = table("user_config", {
|
|
55
|
+
ownerEmail: text("owner_email").primaryKey(),
|
|
56
|
+
taskCardFieldIdsJson: text("task_card_field_ids_json")
|
|
57
|
+
.notNull()
|
|
58
|
+
.default("[]"),
|
|
59
|
+
updatedAt: text("updated_at").notNull().default(now()),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export type StoredCustomField = typeof customFields.$inferSelect;
|
|
63
|
+
export type NewCustomField = typeof customFields.$inferInsert;
|
|
64
|
+
export type StoredCustomFieldValue = typeof customFieldValues.$inferSelect;
|
|
65
|
+
export type NewCustomFieldValue = typeof customFieldValues.$inferInsert;
|
|
66
|
+
export type StoredUserConfig = typeof userConfig.$inferSelect;
|
|
67
|
+
export type NewUserConfig = typeof userConfig.$inferInsert;
|