mulmoclaude 0.1.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/README.md +44 -0
- package/bin/mulmoclaude.js +202 -0
- package/bin/prepare-dist.js +93 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
- package/client/assets/index-D8rhwXLq.js +4906 -0
- package/client/assets/index-KNLBjwuh.css +1 -0
- package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
- package/client/assets/material-icons-Dr0goTwe.woff +0 -0
- package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
- package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
- package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
- package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
- package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
- package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
- package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
- package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
- package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
- package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
- package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
- package/client/index.html +28 -0
- package/package.json +66 -0
- package/server/agent/attachmentConverter.ts +270 -0
- package/server/agent/config.ts +414 -0
- package/server/agent/index.ts +260 -0
- package/server/agent/mcp-server.ts +412 -0
- package/server/agent/mcp-tools/index.ts +63 -0
- package/server/agent/mcp-tools/x.ts +188 -0
- package/server/agent/plugin-names.ts +75 -0
- package/server/agent/prompt.ts +349 -0
- package/server/agent/resumeFailover.ts +129 -0
- package/server/agent/sandboxMounts.ts +329 -0
- package/server/agent/stream.ts +194 -0
- package/server/api/auth/bearerAuth.ts +61 -0
- package/server/api/auth/token.ts +98 -0
- package/server/api/csrfGuard.ts +85 -0
- package/server/api/routes/agent.ts +478 -0
- package/server/api/routes/chart.ts +98 -0
- package/server/api/routes/chat-index.ts +46 -0
- package/server/api/routes/config.ts +258 -0
- package/server/api/routes/dispatchResponse.ts +79 -0
- package/server/api/routes/files.ts +812 -0
- package/server/api/routes/html.ts +101 -0
- package/server/api/routes/image.ts +169 -0
- package/server/api/routes/mulmo-script.ts +712 -0
- package/server/api/routes/mulmoScriptValidate.ts +101 -0
- package/server/api/routes/notifications.ts +69 -0
- package/server/api/routes/pdf.ts +163 -0
- package/server/api/routes/plugins.ts +276 -0
- package/server/api/routes/presentHtml.ts +48 -0
- package/server/api/routes/roles.ts +125 -0
- package/server/api/routes/scheduler.ts +153 -0
- package/server/api/routes/schedulerHandlers.ts +151 -0
- package/server/api/routes/schedulerTasks.ts +163 -0
- package/server/api/routes/sessions.ts +294 -0
- package/server/api/routes/sessionsCursor.ts +59 -0
- package/server/api/routes/skills.ts +195 -0
- package/server/api/routes/sources.ts +540 -0
- package/server/api/routes/todos.ts +263 -0
- package/server/api/routes/todosColumnsHandlers.ts +347 -0
- package/server/api/routes/todosHandlers.ts +274 -0
- package/server/api/routes/todosItemsHandlers.ts +386 -0
- package/server/api/routes/wiki/pageIndex.ts +53 -0
- package/server/api/routes/wiki.ts +363 -0
- package/server/api/sandboxStatus.ts +64 -0
- package/server/events/notifications.ts +160 -0
- package/server/events/pub-sub/index.ts +45 -0
- package/server/events/relay-client.ts +288 -0
- package/server/events/scheduler-adapter.ts +302 -0
- package/server/events/session-store/index.ts +492 -0
- package/server/events/task-manager/index.ts +181 -0
- package/server/index.ts +572 -0
- package/server/system/config.ts +243 -0
- package/server/system/credentials.ts +220 -0
- package/server/system/docker.ts +97 -0
- package/server/system/env.ts +109 -0
- package/server/system/logger/config.ts +112 -0
- package/server/system/logger/formatters.ts +40 -0
- package/server/system/logger/index.ts +53 -0
- package/server/system/logger/rotation.ts +37 -0
- package/server/system/logger/sinks.ts +101 -0
- package/server/system/logger/types.ts +29 -0
- package/server/utils/date.ts +57 -0
- package/server/utils/errors.ts +7 -0
- package/server/utils/fetch.ts +27 -0
- package/server/utils/files/atomic.ts +125 -0
- package/server/utils/files/html-io.ts +20 -0
- package/server/utils/files/image-store.ts +66 -0
- package/server/utils/files/index.ts +45 -0
- package/server/utils/files/journal-io.ts +213 -0
- package/server/utils/files/json.ts +69 -0
- package/server/utils/files/markdown-store.ts +33 -0
- package/server/utils/files/naming.ts +50 -0
- package/server/utils/files/reference-dirs-io.ts +45 -0
- package/server/utils/files/roles-io.ts +45 -0
- package/server/utils/files/safe.ts +106 -0
- package/server/utils/files/scheduler-io.ts +20 -0
- package/server/utils/files/scheduler-overrides-io.ts +64 -0
- package/server/utils/files/session-io.ts +136 -0
- package/server/utils/files/spreadsheet-store.ts +63 -0
- package/server/utils/files/todos-io.ts +29 -0
- package/server/utils/files/user-tasks-io.ts +25 -0
- package/server/utils/files/workspace-io.ts +221 -0
- package/server/utils/gemini.ts +59 -0
- package/server/utils/gitignore.ts +69 -0
- package/server/utils/http.ts +15 -0
- package/server/utils/httpError.ts +61 -0
- package/server/utils/id.ts +16 -0
- package/server/utils/json.ts +83 -0
- package/server/utils/logBackgroundError.ts +22 -0
- package/server/utils/markdown.ts +82 -0
- package/server/utils/request.ts +29 -0
- package/server/utils/slug.ts +50 -0
- package/server/utils/spawn.ts +62 -0
- package/server/utils/time.ts +34 -0
- package/server/utils/types.ts +47 -0
- package/server/workspace/chat-index/index.ts +153 -0
- package/server/workspace/chat-index/indexer.ts +209 -0
- package/server/workspace/chat-index/paths.ts +34 -0
- package/server/workspace/chat-index/summarizer.ts +247 -0
- package/server/workspace/chat-index/types.ts +38 -0
- package/server/workspace/custom-dirs.ts +220 -0
- package/server/workspace/helps/business.md +104 -0
- package/server/workspace/helps/github.md +23 -0
- package/server/workspace/helps/index.md +60 -0
- package/server/workspace/helps/mulmoscript.md +249 -0
- package/server/workspace/helps/sandbox.md +90 -0
- package/server/workspace/helps/spreadsheet.md +43 -0
- package/server/workspace/helps/telegram.md +135 -0
- package/server/workspace/helps/wiki.md +131 -0
- package/server/workspace/journal/archivist.ts +386 -0
- package/server/workspace/journal/dailyPass.ts +743 -0
- package/server/workspace/journal/diff.ts +71 -0
- package/server/workspace/journal/index.ts +185 -0
- package/server/workspace/journal/indexFile.ts +136 -0
- package/server/workspace/journal/linkRewrite.ts +4 -0
- package/server/workspace/journal/memoryExtractor.ts +130 -0
- package/server/workspace/journal/optimizationPass.ts +160 -0
- package/server/workspace/journal/paths.ts +76 -0
- package/server/workspace/journal/state.ts +125 -0
- package/server/workspace/paths.ts +158 -0
- package/server/workspace/reference-dirs.ts +252 -0
- package/server/workspace/roles.ts +37 -0
- package/server/workspace/skills/discovery.ts +125 -0
- package/server/workspace/skills/index.ts +10 -0
- package/server/workspace/skills/parser.ts +144 -0
- package/server/workspace/skills/paths.ts +41 -0
- package/server/workspace/skills/scheduler.ts +149 -0
- package/server/workspace/skills/types.ts +30 -0
- package/server/workspace/skills/user-tasks.ts +257 -0
- package/server/workspace/skills/writer.ts +189 -0
- package/server/workspace/sources/arxivDiscovery.ts +182 -0
- package/server/workspace/sources/classifier.ts +268 -0
- package/server/workspace/sources/fetchers/arxiv.ts +170 -0
- package/server/workspace/sources/fetchers/github.ts +106 -0
- package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
- package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
- package/server/workspace/sources/fetchers/index.ts +71 -0
- package/server/workspace/sources/fetchers/registerAll.ts +15 -0
- package/server/workspace/sources/fetchers/rss.ts +141 -0
- package/server/workspace/sources/fetchers/rssParser.ts +295 -0
- package/server/workspace/sources/httpFetcher.ts +230 -0
- package/server/workspace/sources/interests.ts +120 -0
- package/server/workspace/sources/paths.ts +110 -0
- package/server/workspace/sources/pipeline/dedup.ts +60 -0
- package/server/workspace/sources/pipeline/fetch.ts +136 -0
- package/server/workspace/sources/pipeline/index.ts +249 -0
- package/server/workspace/sources/pipeline/notify.ts +72 -0
- package/server/workspace/sources/pipeline/plan.ts +66 -0
- package/server/workspace/sources/pipeline/summarize.ts +189 -0
- package/server/workspace/sources/pipeline/write.ts +185 -0
- package/server/workspace/sources/rateLimiter.ts +148 -0
- package/server/workspace/sources/registry.ts +326 -0
- package/server/workspace/sources/robots.ts +271 -0
- package/server/workspace/sources/sourceState.ts +135 -0
- package/server/workspace/sources/taxonomy.ts +74 -0
- package/server/workspace/sources/types.ts +144 -0
- package/server/workspace/sources/urls.ts +112 -0
- package/server/workspace/tool-trace/classify.ts +114 -0
- package/server/workspace/tool-trace/index.ts +250 -0
- package/server/workspace/tool-trace/writeSearch.ts +98 -0
- package/server/workspace/wiki-backlinks/index.ts +107 -0
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
- package/server/workspace/workspace.ts +66 -0
- package/src/App.vue +720 -0
- package/src/assets/mulmo_bw.png +0 -0
- package/src/components/CanvasViewToggle.vue +27 -0
- package/src/components/ChatAttachmentPreview.vue +45 -0
- package/src/components/ChatImagePreview.vue +17 -0
- package/src/components/ChatInput.vue +208 -0
- package/src/components/FileContentHeader.vue +49 -0
- package/src/components/FileContentRenderer.vue +162 -0
- package/src/components/FileTree.vue +115 -0
- package/src/components/FileTreePane.vue +85 -0
- package/src/components/FilesView.vue +206 -0
- package/src/components/LockStatusPopup.vue +111 -0
- package/src/components/NotificationBell.vue +131 -0
- package/src/components/NotificationToast.vue +72 -0
- package/src/components/PluginLauncher.vue +138 -0
- package/src/components/RightSidebar.vue +113 -0
- package/src/components/RoleSelector.vue +64 -0
- package/src/components/SessionHistoryPanel.vue +176 -0
- package/src/components/SessionTabBar.vue +81 -0
- package/src/components/SettingsMcpTab.vue +350 -0
- package/src/components/SettingsModal.vue +275 -0
- package/src/components/SettingsReferenceDirsTab.vue +173 -0
- package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
- package/src/components/SidebarHeader.vue +69 -0
- package/src/components/StackView.vue +360 -0
- package/src/components/SuggestionsPanel.vue +65 -0
- package/src/components/TodoExplorer.vue +358 -0
- package/src/components/ToolResultsPanel.vue +77 -0
- package/src/components/todo/TodoAddDialog.vue +131 -0
- package/src/components/todo/TodoEditDialog.vue +47 -0
- package/src/components/todo/TodoEditPanel.vue +113 -0
- package/src/components/todo/TodoKanbanView.vue +249 -0
- package/src/components/todo/TodoListView.vue +79 -0
- package/src/components/todo/TodoTableView.vue +177 -0
- package/src/composables/useActiveSession.ts +40 -0
- package/src/composables/useAppApi.ts +45 -0
- package/src/composables/useCanvasViewMode.ts +121 -0
- package/src/composables/useChatScroll.ts +47 -0
- package/src/composables/useClickOutside.ts +26 -0
- package/src/composables/useClipboardCopy.ts +44 -0
- package/src/composables/useContentDisplay.ts +52 -0
- package/src/composables/useDebugBeat.ts +23 -0
- package/src/composables/useDynamicFavicon.ts +115 -0
- package/src/composables/useEventListeners.ts +42 -0
- package/src/composables/useExpandedDirs.ts +64 -0
- package/src/composables/useFaviconState.ts +30 -0
- package/src/composables/useFileSelection.ts +115 -0
- package/src/composables/useFileSortMode.ts +24 -0
- package/src/composables/useFileTree.ts +85 -0
- package/src/composables/useFreshPluginData.ts +89 -0
- package/src/composables/useHealth.ts +38 -0
- package/src/composables/useImeAwareEnter.ts +57 -0
- package/src/composables/useKeyNavigation.ts +60 -0
- package/src/composables/useMarkdownLinkHandler.ts +46 -0
- package/src/composables/useMarkdownMode.ts +17 -0
- package/src/composables/useMcpTools.ts +71 -0
- package/src/composables/useMergedSessions.ts +27 -0
- package/src/composables/useNotifications.ts +90 -0
- package/src/composables/usePdfDownload.ts +60 -0
- package/src/composables/usePendingCalls.ts +77 -0
- package/src/composables/usePubSub.ts +85 -0
- package/src/composables/useRightSidebar.ts +23 -0
- package/src/composables/useRoles.ts +34 -0
- package/src/composables/useSandboxStatus.ts +67 -0
- package/src/composables/useSelectedResult.ts +49 -0
- package/src/composables/useSessionDerived.ts +51 -0
- package/src/composables/useSessionHistory.ts +81 -0
- package/src/composables/useSessionSync.ts +57 -0
- package/src/composables/useViewLayout.ts +55 -0
- package/src/config/apiRoutes.ts +173 -0
- package/src/config/pubsubChannels.ts +45 -0
- package/src/config/roles.ts +335 -0
- package/src/config/schedulerActions.ts +25 -0
- package/src/config/toolNames.ts +71 -0
- package/src/config/workspacePaths.ts +24 -0
- package/src/index.css +107 -0
- package/src/main.ts +25 -0
- package/src/plugins/canvas/Preview.vue +13 -0
- package/src/plugins/canvas/View.vue +333 -0
- package/src/plugins/canvas/definition.ts +38 -0
- package/src/plugins/canvas/index.ts +36 -0
- package/src/plugins/chart/Preview.vue +49 -0
- package/src/plugins/chart/View.vue +143 -0
- package/src/plugins/chart/definition.ts +58 -0
- package/src/plugins/chart/index.ts +52 -0
- package/src/plugins/editImage/Preview.vue +13 -0
- package/src/plugins/editImage/View.vue +13 -0
- package/src/plugins/editImage/definition.ts +27 -0
- package/src/plugins/editImage/index.ts +36 -0
- package/src/plugins/generateImage/Preview.vue +13 -0
- package/src/plugins/generateImage/View.vue +33 -0
- package/src/plugins/generateImage/definition.ts +32 -0
- package/src/plugins/generateImage/index.ts +56 -0
- package/src/plugins/manageRoles/Preview.vue +49 -0
- package/src/plugins/manageRoles/View.vue +525 -0
- package/src/plugins/manageRoles/definition.ts +43 -0
- package/src/plugins/manageRoles/index.ts +47 -0
- package/src/plugins/manageSkills/Preview.vue +21 -0
- package/src/plugins/manageSkills/View.vue +321 -0
- package/src/plugins/manageSkills/definition.ts +49 -0
- package/src/plugins/manageSkills/index.ts +49 -0
- package/src/plugins/manageSource/Preview.vue +33 -0
- package/src/plugins/manageSource/View.vue +697 -0
- package/src/plugins/manageSource/definition.ts +63 -0
- package/src/plugins/manageSource/index.ts +66 -0
- package/src/plugins/markdown/Preview.vue +77 -0
- package/src/plugins/markdown/View.vue +476 -0
- package/src/plugins/markdown/definition.ts +50 -0
- package/src/plugins/markdown/index.ts +36 -0
- package/src/plugins/presentHtml/Preview.vue +25 -0
- package/src/plugins/presentHtml/View.vue +52 -0
- package/src/plugins/presentHtml/definition.ts +27 -0
- package/src/plugins/presentHtml/helpers.ts +72 -0
- package/src/plugins/presentHtml/index.ts +41 -0
- package/src/plugins/presentMulmoScript/Preview.vue +23 -0
- package/src/plugins/presentMulmoScript/View.vue +1166 -0
- package/src/plugins/presentMulmoScript/definition.ts +95 -0
- package/src/plugins/presentMulmoScript/helpers.ts +162 -0
- package/src/plugins/presentMulmoScript/index.ts +40 -0
- package/src/plugins/scheduler/Preview.vue +67 -0
- package/src/plugins/scheduler/TasksTab.vue +205 -0
- package/src/plugins/scheduler/View.vue +565 -0
- package/src/plugins/scheduler/definition.ts +57 -0
- package/src/plugins/scheduler/index.ts +45 -0
- package/src/plugins/scheduler/viewModes.ts +26 -0
- package/src/plugins/spreadsheet/Preview.vue +29 -0
- package/src/plugins/spreadsheet/View.vue +997 -0
- package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
- package/src/plugins/spreadsheet/definition.ts +121 -0
- package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
- package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
- package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
- package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
- package/src/plugins/spreadsheet/engine/engine.ts +176 -0
- package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
- package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
- package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
- package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
- package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
- package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
- package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
- package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
- package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
- package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
- package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
- package/src/plugins/spreadsheet/engine/index.ts +27 -0
- package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
- package/src/plugins/spreadsheet/engine/parser.ts +143 -0
- package/src/plugins/spreadsheet/engine/registry.ts +150 -0
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
- package/src/plugins/spreadsheet/engine/types.ts +64 -0
- package/src/plugins/spreadsheet/index.ts +36 -0
- package/src/plugins/textResponse/Preview.vue +94 -0
- package/src/plugins/textResponse/View.vue +503 -0
- package/src/plugins/textResponse/definition.ts +34 -0
- package/src/plugins/textResponse/index.ts +27 -0
- package/src/plugins/textResponse/plugin.ts +29 -0
- package/src/plugins/textResponse/samples.ts +97 -0
- package/src/plugins/textResponse/types.ts +11 -0
- package/src/plugins/todo/Preview.vue +63 -0
- package/src/plugins/todo/View.vue +364 -0
- package/src/plugins/todo/composables/useTodos.ts +177 -0
- package/src/plugins/todo/definition.ts +45 -0
- package/src/plugins/todo/index.ts +61 -0
- package/src/plugins/todo/labels.ts +163 -0
- package/src/plugins/todo/priority.ts +98 -0
- package/src/plugins/todo/viewModes.ts +19 -0
- package/src/plugins/ui-image/ImagePreview.vue +23 -0
- package/src/plugins/ui-image/ImageView.vue +34 -0
- package/src/plugins/ui-image/index.ts +3 -0
- package/src/plugins/ui-image/types.ts +4 -0
- package/src/plugins/wiki/Preview.vue +65 -0
- package/src/plugins/wiki/View.vue +342 -0
- package/src/plugins/wiki/definition.ts +25 -0
- package/src/plugins/wiki/helpers.ts +59 -0
- package/src/plugins/wiki/index.ts +52 -0
- package/src/router/guards.ts +61 -0
- package/src/router/index.ts +50 -0
- package/src/tools/index.ts +52 -0
- package/src/tools/types.ts +27 -0
- package/src/types/events.ts +16 -0
- package/src/types/fileTree.ts +13 -0
- package/src/types/notification.ts +67 -0
- package/src/types/session.ts +116 -0
- package/src/types/sse.ts +90 -0
- package/src/types/toolCallHistory.ts +13 -0
- package/src/utils/agent/eventDispatch.ts +74 -0
- package/src/utils/agent/request.ts +55 -0
- package/src/utils/agent/toolCalls.ts +62 -0
- package/src/utils/api.ts +218 -0
- package/src/utils/canvas/viewMode.ts +46 -0
- package/src/utils/dom/authTokenMeta.ts +20 -0
- package/src/utils/dom/clickOutside.ts +11 -0
- package/src/utils/dom/externalLink.ts +57 -0
- package/src/utils/dom/scrollable.ts +24 -0
- package/src/utils/errors.ts +11 -0
- package/src/utils/files/expandedDirs.ts +25 -0
- package/src/utils/files/filename.ts +12 -0
- package/src/utils/files/sortChildren.ts +20 -0
- package/src/utils/filesPreview/schedulerPreview.ts +38 -0
- package/src/utils/filesPreview/todoPreview.ts +40 -0
- package/src/utils/format/date.ts +85 -0
- package/src/utils/format/frontmatter.ts +80 -0
- package/src/utils/format/jsonSyntax.ts +109 -0
- package/src/utils/html/previewCsp.ts +65 -0
- package/src/utils/image/resolve.ts +8 -0
- package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
- package/src/utils/markdown/extractFirstH1.ts +39 -0
- package/src/utils/notification/dispatch.ts +22 -0
- package/src/utils/path/relativeLink.ts +130 -0
- package/src/utils/role/icon.ts +20 -0
- package/src/utils/role/merge.ts +10 -0
- package/src/utils/role/plugins.ts +12 -0
- package/src/utils/session/mergeSessions.ts +103 -0
- package/src/utils/session/seedRoleDefault.ts +35 -0
- package/src/utils/session/sessionEntries.ts +121 -0
- package/src/utils/session/sessionFactory.ts +22 -0
- package/src/utils/session/sessionHelpers.ts +99 -0
- package/src/utils/tools/dedup.ts +17 -0
- package/src/utils/tools/mcp.ts +33 -0
- package/src/utils/tools/pendingCalls.ts +16 -0
- package/src/utils/tools/result.ts +40 -0
- package/src/utils/types.ts +44 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="border-t border-blue-100 bg-blue-50 p-4 space-y-3 rounded-b-lg">
|
|
3
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
4
|
+
<label class="block text-xs text-gray-600 sm:col-span-2">
|
|
5
|
+
Text
|
|
6
|
+
<input
|
|
7
|
+
v-model="text"
|
|
8
|
+
type="text"
|
|
9
|
+
class="mt-1 w-full px-2 py-1.5 text-sm bg-white border border-blue-300 rounded focus:outline-none focus:border-blue-500"
|
|
10
|
+
/>
|
|
11
|
+
</label>
|
|
12
|
+
<label class="block text-xs text-gray-600 sm:col-span-2">
|
|
13
|
+
Note
|
|
14
|
+
<textarea
|
|
15
|
+
v-model="note"
|
|
16
|
+
rows="2"
|
|
17
|
+
class="mt-1 w-full px-2 py-1.5 text-sm bg-white border border-blue-300 rounded resize-y focus:outline-none focus:border-blue-500"
|
|
18
|
+
/>
|
|
19
|
+
</label>
|
|
20
|
+
<label class="block text-xs text-gray-600">
|
|
21
|
+
Status
|
|
22
|
+
<select v-model="status" class="mt-1 w-full px-2 py-1.5 text-sm bg-white border border-blue-300 rounded focus:outline-none focus:border-blue-500">
|
|
23
|
+
<option v-for="col in columns" :key="col.id" :value="col.id">
|
|
24
|
+
{{ col.label }}
|
|
25
|
+
</option>
|
|
26
|
+
</select>
|
|
27
|
+
</label>
|
|
28
|
+
<label class="block text-xs text-gray-600">
|
|
29
|
+
Priority
|
|
30
|
+
<select v-model="priority" class="mt-1 w-full px-2 py-1.5 text-sm bg-white border border-blue-300 rounded focus:outline-none focus:border-blue-500">
|
|
31
|
+
<option value="">— None —</option>
|
|
32
|
+
<option v-for="p in PRIORITIES" :key="p" :value="p">
|
|
33
|
+
{{ PRIORITY_LABELS[p] }}
|
|
34
|
+
</option>
|
|
35
|
+
</select>
|
|
36
|
+
</label>
|
|
37
|
+
<label class="block text-xs text-gray-600">
|
|
38
|
+
Due date
|
|
39
|
+
<input
|
|
40
|
+
v-model="dueDate"
|
|
41
|
+
type="date"
|
|
42
|
+
class="mt-1 w-full px-2 py-1.5 text-sm bg-white border border-blue-300 rounded focus:outline-none focus:border-blue-500"
|
|
43
|
+
/>
|
|
44
|
+
</label>
|
|
45
|
+
<label class="block text-xs text-gray-600">
|
|
46
|
+
Labels (comma-separated)
|
|
47
|
+
<input
|
|
48
|
+
v-model="labelsText"
|
|
49
|
+
type="text"
|
|
50
|
+
placeholder="work, urgent"
|
|
51
|
+
class="mt-1 w-full px-2 py-1.5 text-sm bg-white border border-blue-300 rounded focus:outline-none focus:border-blue-500"
|
|
52
|
+
/>
|
|
53
|
+
</label>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="flex items-center gap-2 pt-1">
|
|
56
|
+
<button class="px-3 py-1.5 text-sm rounded bg-blue-500 text-white hover:bg-blue-600" @click="save">Save</button>
|
|
57
|
+
<button class="px-3 py-1.5 text-sm rounded border border-gray-300 text-gray-600 hover:bg-gray-50" @click="emit('cancel')">Cancel</button>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</template>
|
|
61
|
+
|
|
62
|
+
<script setup lang="ts">
|
|
63
|
+
import { ref } from "vue";
|
|
64
|
+
import type { StatusColumn, TodoItem, TodoPriority } from "../../plugins/todo/index";
|
|
65
|
+
import { PRIORITIES, PRIORITY_LABELS } from "../../plugins/todo/priority";
|
|
66
|
+
import type { PatchItemInput } from "../../plugins/todo/composables/useTodos";
|
|
67
|
+
|
|
68
|
+
const props = defineProps<{
|
|
69
|
+
item: TodoItem;
|
|
70
|
+
columns: StatusColumn[];
|
|
71
|
+
}>();
|
|
72
|
+
|
|
73
|
+
const emit = defineEmits<{
|
|
74
|
+
save: [input: PatchItemInput];
|
|
75
|
+
cancel: [];
|
|
76
|
+
}>();
|
|
77
|
+
|
|
78
|
+
const text = ref(props.item.text);
|
|
79
|
+
const note = ref(props.item.note ?? "");
|
|
80
|
+
const status = ref<string>(props.item.status ?? props.columns[0]?.id ?? "");
|
|
81
|
+
const priority = ref<string>(props.item.priority ?? "");
|
|
82
|
+
const dueDate = ref(props.item.dueDate ?? "");
|
|
83
|
+
const labelsText = ref((props.item.labels ?? []).join(", "));
|
|
84
|
+
|
|
85
|
+
function parseLabels(raw: string): string[] {
|
|
86
|
+
return raw
|
|
87
|
+
.split(",")
|
|
88
|
+
.map((s) => s.trim())
|
|
89
|
+
.filter((s) => s.length > 0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function save(): void {
|
|
93
|
+
const input: PatchItemInput = {
|
|
94
|
+
text: text.value,
|
|
95
|
+
note: note.value === "" ? null : note.value,
|
|
96
|
+
status: status.value,
|
|
97
|
+
labels: parseLabels(labelsText.value),
|
|
98
|
+
};
|
|
99
|
+
// Priority: empty string clears, valid priority sets, anything else
|
|
100
|
+
// is silently ignored (the dropdown ensures we never send garbage).
|
|
101
|
+
if (priority.value === "") {
|
|
102
|
+
input.priority = null;
|
|
103
|
+
} else {
|
|
104
|
+
input.priority = priority.value as TodoPriority;
|
|
105
|
+
}
|
|
106
|
+
if (dueDate.value === "") {
|
|
107
|
+
input.dueDate = null;
|
|
108
|
+
} else {
|
|
109
|
+
input.dueDate = dueDate.value;
|
|
110
|
+
}
|
|
111
|
+
emit("save", input);
|
|
112
|
+
}
|
|
113
|
+
</script>
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="h-full overflow-x-auto overflow-y-hidden">
|
|
3
|
+
<draggable
|
|
4
|
+
:list="columnsLocal"
|
|
5
|
+
item-key="id"
|
|
6
|
+
group="todo-columns"
|
|
7
|
+
handle=".col-handle"
|
|
8
|
+
:animation="150"
|
|
9
|
+
class="flex gap-3 h-full p-3 min-w-max"
|
|
10
|
+
@end="onColumnDragEnd"
|
|
11
|
+
>
|
|
12
|
+
<template #item="{ element: col }: { element: StatusColumn }">
|
|
13
|
+
<div :data-testid="`todo-column-${col.id}`" class="w-72 shrink-0 flex flex-col bg-gray-100 rounded-lg">
|
|
14
|
+
<!-- Column header. The whole header is the drag handle —
|
|
15
|
+
clicking the menu button still works because the menu
|
|
16
|
+
button has its own @click handler that doesn't kick off
|
|
17
|
+
a drag, but pressing-and-holding anywhere on the header
|
|
18
|
+
starts a column drag. -->
|
|
19
|
+
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-200 col-handle cursor-grab active:cursor-grabbing">
|
|
20
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
21
|
+
<span class="w-2 h-2 rounded-full shrink-0" :class="col.isDone ? 'bg-green-500' : 'bg-gray-400'" />
|
|
22
|
+
<span v-if="renamingId !== col.id" class="font-semibold text-sm text-gray-700 truncate" :title="col.label">{{ col.label }}</span>
|
|
23
|
+
<input
|
|
24
|
+
v-else
|
|
25
|
+
ref="renameInput"
|
|
26
|
+
v-model="renameDraft"
|
|
27
|
+
class="px-1 py-0.5 text-sm bg-white border border-blue-400 rounded w-32"
|
|
28
|
+
@keydown.enter="commitRename(col.id)"
|
|
29
|
+
@keydown.escape="renamingId = null"
|
|
30
|
+
@blur="commitRename(col.id)"
|
|
31
|
+
/>
|
|
32
|
+
<span class="text-xs text-gray-500 shrink-0">{{ itemsByColumn(col.id).length }}</span>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="relative">
|
|
35
|
+
<button class="text-gray-400 hover:text-gray-600 px-1" title="Column actions" @click="toggleMenu(col.id)">
|
|
36
|
+
<span class="material-icons text-base">more_horiz</span>
|
|
37
|
+
</button>
|
|
38
|
+
<div
|
|
39
|
+
v-if="menuOpenId === col.id"
|
|
40
|
+
class="absolute right-0 top-6 z-20 bg-white border border-gray-200 rounded shadow-md text-xs w-40 py-1"
|
|
41
|
+
@click.stop
|
|
42
|
+
>
|
|
43
|
+
<button class="w-full text-left px-3 py-1.5 hover:bg-gray-50" @click="startRename(col)">Rename</button>
|
|
44
|
+
<button class="w-full text-left px-3 py-1.5 hover:bg-gray-50" @click="markAsDone(col.id)">
|
|
45
|
+
{{ col.isDone ? "Already done column" : "Mark as done column" }}
|
|
46
|
+
</button>
|
|
47
|
+
<button class="w-full text-left px-3 py-1.5 text-red-600 hover:bg-red-50" @click="deleteColumn(col.id)">Delete column</button>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!-- Cards -->
|
|
53
|
+
<draggable
|
|
54
|
+
:model-value="itemsByColumn(col.id)"
|
|
55
|
+
item-key="id"
|
|
56
|
+
group="todos"
|
|
57
|
+
class="flex-1 overflow-y-auto p-2 space-y-2 min-h-[2rem]"
|
|
58
|
+
:animation="150"
|
|
59
|
+
@change="(e: DragChangeEvent) => onDragChange(col.id, e)"
|
|
60
|
+
>
|
|
61
|
+
<template #item="{ element }: { element: TodoItem }">
|
|
62
|
+
<div
|
|
63
|
+
:data-testid="`todo-card-${element.id}`"
|
|
64
|
+
class="bg-white border border-l-4 border-gray-200 rounded shadow-sm p-2 cursor-grab hover:shadow active:cursor-grabbing"
|
|
65
|
+
:class="element.priority ? PRIORITY_BORDER[element.priority] : 'border-l-gray-200'"
|
|
66
|
+
@click="emit('open', element)"
|
|
67
|
+
>
|
|
68
|
+
<div class="flex items-start gap-2">
|
|
69
|
+
<input
|
|
70
|
+
type="checkbox"
|
|
71
|
+
:checked="element.completed"
|
|
72
|
+
class="mt-0.5 cursor-pointer shrink-0"
|
|
73
|
+
@click.stop
|
|
74
|
+
@change="emit('toggleComplete', element)"
|
|
75
|
+
/>
|
|
76
|
+
<div class="flex-1 min-w-0">
|
|
77
|
+
<div class="text-sm" :class="element.completed ? 'line-through text-gray-400' : 'text-gray-800'">
|
|
78
|
+
{{ element.text }}
|
|
79
|
+
</div>
|
|
80
|
+
<div v-if="element.note" class="text-[11px] text-gray-400 mt-0.5 line-clamp-2">
|
|
81
|
+
{{ element.note }}
|
|
82
|
+
</div>
|
|
83
|
+
<div v-if="(element.labels && element.labels.length > 0) || element.priority || element.dueDate" class="flex flex-wrap gap-1 mt-1.5">
|
|
84
|
+
<span v-if="element.priority" class="px-1.5 py-0.5 rounded-full text-[10px] font-medium" :class="PRIORITY_CLASSES[element.priority]">{{
|
|
85
|
+
PRIORITY_LABELS[element.priority]
|
|
86
|
+
}}</span>
|
|
87
|
+
<span v-if="element.dueDate" class="px-1.5 py-0.5 rounded-full text-[10px] font-medium" :class="dueDateClasses(element.dueDate)">{{
|
|
88
|
+
formatDueLabel(element.dueDate)
|
|
89
|
+
}}</span>
|
|
90
|
+
<span
|
|
91
|
+
v-for="label in element.labels ?? []"
|
|
92
|
+
:key="label"
|
|
93
|
+
class="px-1.5 py-0.5 rounded-full text-[10px] font-medium"
|
|
94
|
+
:class="colorForLabel(label)"
|
|
95
|
+
>{{ label }}</span
|
|
96
|
+
>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</template>
|
|
102
|
+
</draggable>
|
|
103
|
+
|
|
104
|
+
<!-- Add card stub -->
|
|
105
|
+
<button class="m-2 text-xs text-gray-500 hover:text-gray-800 hover:bg-gray-200 rounded py-1.5 transition-colors" @click="emit('quickAdd', col.id)">
|
|
106
|
+
+ Add card
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
</template>
|
|
110
|
+
</draggable>
|
|
111
|
+
</div>
|
|
112
|
+
</template>
|
|
113
|
+
|
|
114
|
+
<script setup lang="ts">
|
|
115
|
+
import { computed, nextTick, ref, watch } from "vue";
|
|
116
|
+
import draggable from "vuedraggable";
|
|
117
|
+
import type { StatusColumn, TodoItem } from "../../plugins/todo/index";
|
|
118
|
+
import { colorForLabel } from "../../plugins/todo/labels";
|
|
119
|
+
import { PRIORITY_BORDER, PRIORITY_CLASSES, PRIORITY_LABELS, dueDateClasses, formatDueLabel } from "../../plugins/todo/priority";
|
|
120
|
+
|
|
121
|
+
// vuedraggable @change event shape. The library emits one of these
|
|
122
|
+
// three keys depending on whether the move was within the same list,
|
|
123
|
+
// added from another list, or removed from the current list. We only
|
|
124
|
+
// react to "added" (the destination column) and "moved" (reorder
|
|
125
|
+
// within a single column) — "removed" is the source side and is
|
|
126
|
+
// always paired with an "added" on the destination, so handling it
|
|
127
|
+
// would double the API calls.
|
|
128
|
+
interface DragChangeEvent {
|
|
129
|
+
added?: { newIndex: number; element: TodoItem };
|
|
130
|
+
moved?: { newIndex: number; oldIndex: number; element: TodoItem };
|
|
131
|
+
removed?: { oldIndex: number; element: TodoItem };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const props = defineProps<{
|
|
135
|
+
filteredItems: TodoItem[];
|
|
136
|
+
columns: StatusColumn[];
|
|
137
|
+
}>();
|
|
138
|
+
|
|
139
|
+
const emit = defineEmits<{
|
|
140
|
+
move: [id: string, statusId: string, position: number];
|
|
141
|
+
open: [item: TodoItem];
|
|
142
|
+
toggleComplete: [item: TodoItem];
|
|
143
|
+
quickAdd: [statusId: string];
|
|
144
|
+
renameColumn: [id: string, label: string];
|
|
145
|
+
deleteColumn: [id: string];
|
|
146
|
+
markDone: [id: string];
|
|
147
|
+
reorderColumns: [ids: string[]];
|
|
148
|
+
}>();
|
|
149
|
+
|
|
150
|
+
// Local mirror of props.columns so vuedraggable can reorder it in
|
|
151
|
+
// place (`:list` mode mutates the bound array). When the parent
|
|
152
|
+
// updates props.columns — either after we successfully persist a
|
|
153
|
+
// reorder, or because some other action changed the column set —
|
|
154
|
+
// we copy the new array in. This also rolls the kanban back if the
|
|
155
|
+
// API call fails: the parent's columns ref stays at the old order,
|
|
156
|
+
// the watch fires, and columnsLocal snaps back.
|
|
157
|
+
const columnsLocal = ref<StatusColumn[]>([...props.columns]);
|
|
158
|
+
watch(
|
|
159
|
+
() => props.columns,
|
|
160
|
+
(next) => {
|
|
161
|
+
columnsLocal.value = [...next];
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
function onColumnDragEnd(): void {
|
|
166
|
+
const before = props.columns.map((column) => column.id);
|
|
167
|
+
const after = columnsLocal.value.map((column) => column.id);
|
|
168
|
+
// No-op drops: avoid an unnecessary network round-trip when the
|
|
169
|
+
// drop position equals the original.
|
|
170
|
+
if (before.length === after.length && before.every((columnId, i) => columnId === after[i])) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
emit("reorderColumns", after);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Group filtered items by status, sorted by `order`. Computed so the
|
|
177
|
+
// kanban re-derives whenever items / filter changes.
|
|
178
|
+
const itemsByStatus = computed(() => {
|
|
179
|
+
const map = new Map<string, TodoItem[]>();
|
|
180
|
+
for (const col of props.columns) map.set(col.id, []);
|
|
181
|
+
for (const item of props.filteredItems) {
|
|
182
|
+
const columnId = item.status ?? props.columns[0]?.id;
|
|
183
|
+
if (!columnId) continue;
|
|
184
|
+
if (!map.has(columnId)) map.set(columnId, []);
|
|
185
|
+
map.get(columnId)!.push(item);
|
|
186
|
+
}
|
|
187
|
+
for (const list of map.values()) {
|
|
188
|
+
list.sort((left, right) => (left.order ?? 0) - (right.order ?? 0));
|
|
189
|
+
}
|
|
190
|
+
return map;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
function itemsByColumn(columnId: string): TodoItem[] {
|
|
194
|
+
return itemsByStatus.value.get(columnId) ?? [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function onDragChange(columnId: string, event: DragChangeEvent): void {
|
|
198
|
+
if (event.added) {
|
|
199
|
+
emit("move", event.added.element.id, columnId, event.added.newIndex);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (event.moved) {
|
|
203
|
+
emit("move", event.moved.element.id, columnId, event.moved.newIndex);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── Column menu / rename ─────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
const menuOpenId = ref<string | null>(null);
|
|
210
|
+
const renamingId = ref<string | null>(null);
|
|
211
|
+
const renameDraft = ref("");
|
|
212
|
+
const renameInput = ref<HTMLInputElement[] | HTMLInputElement | null>(null);
|
|
213
|
+
|
|
214
|
+
function toggleMenu(columnId: string): void {
|
|
215
|
+
menuOpenId.value = menuOpenId.value === columnId ? null : columnId;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function startRename(col: StatusColumn): void {
|
|
219
|
+
menuOpenId.value = null;
|
|
220
|
+
renamingId.value = col.id;
|
|
221
|
+
renameDraft.value = col.label;
|
|
222
|
+
void nextTick(() => {
|
|
223
|
+
const inputRef = renameInput.value;
|
|
224
|
+
const input = Array.isArray(inputRef) ? inputRef[0] : inputRef;
|
|
225
|
+
input?.focus();
|
|
226
|
+
input?.select();
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function commitRename(columnId: string): void {
|
|
231
|
+
if (renamingId.value !== columnId) return;
|
|
232
|
+
const next = renameDraft.value.trim();
|
|
233
|
+
renamingId.value = null;
|
|
234
|
+
if (next.length === 0) return;
|
|
235
|
+
const current = props.columns.find((column) => column.id === columnId);
|
|
236
|
+
if (!current || current.label === next) return;
|
|
237
|
+
emit("renameColumn", columnId, next);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function deleteColumn(columnId: string): void {
|
|
241
|
+
menuOpenId.value = null;
|
|
242
|
+
emit("deleteColumn", columnId);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function markAsDone(columnId: string): void {
|
|
246
|
+
menuOpenId.value = null;
|
|
247
|
+
emit("markDone", columnId);
|
|
248
|
+
}
|
|
249
|
+
</script>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="h-full overflow-y-auto p-4">
|
|
3
|
+
<div v-if="filteredItems.length === 0" class="h-full flex items-center justify-center text-gray-400 text-sm">No items match the current filter</div>
|
|
4
|
+
<ul v-else class="space-y-2 max-w-3xl mx-auto">
|
|
5
|
+
<li v-for="item in filteredItems" :key="item.id" class="rounded-lg border border-gray-200 hover:border-gray-300 transition-colors">
|
|
6
|
+
<div class="flex items-center gap-3 p-3 cursor-pointer group" @click="toggleExpand(item.id)">
|
|
7
|
+
<input type="checkbox" :checked="item.completed" class="cursor-pointer shrink-0" @click.stop @change="toggleComplete(item)" />
|
|
8
|
+
<div class="flex-1 min-w-0">
|
|
9
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
10
|
+
<span class="text-sm" :class="item.completed ? 'line-through text-gray-400' : 'text-gray-800'">{{ item.text }}</span>
|
|
11
|
+
<span v-if="statusLabel(item)" class="px-1.5 py-0.5 rounded-full text-[10px] font-medium bg-gray-100 text-gray-600">{{ statusLabel(item) }}</span>
|
|
12
|
+
<span v-if="item.priority" class="px-1.5 py-0.5 rounded-full text-[10px] font-medium" :class="PRIORITY_CLASSES[item.priority]">{{
|
|
13
|
+
PRIORITY_LABELS[item.priority]
|
|
14
|
+
}}</span>
|
|
15
|
+
<span v-if="item.dueDate" class="px-1.5 py-0.5 rounded-full text-[10px] font-medium" :class="dueDateClasses(item.dueDate)">{{
|
|
16
|
+
formatDueLabel(item.dueDate)
|
|
17
|
+
}}</span>
|
|
18
|
+
<span v-for="label in item.labels ?? []" :key="label" class="px-1.5 py-0.5 rounded-full text-[10px] font-medium" :class="colorForLabel(label)">{{
|
|
19
|
+
label
|
|
20
|
+
}}</span>
|
|
21
|
+
</div>
|
|
22
|
+
<div v-if="item.note" class="text-xs text-gray-400 mt-0.5">
|
|
23
|
+
{{ item.note }}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<button
|
|
27
|
+
class="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 text-xs px-1 shrink-0"
|
|
28
|
+
title="Delete item"
|
|
29
|
+
@click.stop="emit('delete', item.id)"
|
|
30
|
+
>
|
|
31
|
+
✕
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
<TodoEditPanel v-if="expandedId === item.id" :item="item" :columns="columns" @save="(input) => onSave(item.id, input)" @cancel="expandedId = null" />
|
|
35
|
+
</li>
|
|
36
|
+
</ul>
|
|
37
|
+
</div>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<script setup lang="ts">
|
|
41
|
+
import { ref } from "vue";
|
|
42
|
+
import type { StatusColumn, TodoItem } from "../../plugins/todo/index";
|
|
43
|
+
import { colorForLabel } from "../../plugins/todo/labels";
|
|
44
|
+
import { PRIORITY_CLASSES, PRIORITY_LABELS, dueDateClasses, formatDueLabel } from "../../plugins/todo/priority";
|
|
45
|
+
import type { PatchItemInput } from "../../plugins/todo/composables/useTodos";
|
|
46
|
+
import TodoEditPanel from "./TodoEditPanel.vue";
|
|
47
|
+
|
|
48
|
+
const props = defineProps<{
|
|
49
|
+
filteredItems: TodoItem[];
|
|
50
|
+
columns: StatusColumn[];
|
|
51
|
+
}>();
|
|
52
|
+
|
|
53
|
+
const emit = defineEmits<{
|
|
54
|
+
patch: [id: string, input: PatchItemInput];
|
|
55
|
+
delete: [id: string];
|
|
56
|
+
toggleComplete: [item: TodoItem];
|
|
57
|
+
}>();
|
|
58
|
+
|
|
59
|
+
const expandedId = ref<string | null>(null);
|
|
60
|
+
|
|
61
|
+
function toggleExpand(id: string): void {
|
|
62
|
+
expandedId.value = expandedId.value === id ? null : id;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function toggleComplete(item: TodoItem): void {
|
|
66
|
+
emit("toggleComplete", item);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function onSave(id: string, input: PatchItemInput): void {
|
|
70
|
+
emit("patch", id, input);
|
|
71
|
+
expandedId.value = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function statusLabel(item: TodoItem): string {
|
|
75
|
+
if (!item.status) return "";
|
|
76
|
+
const col = props.columns.find((c) => c.id === item.status);
|
|
77
|
+
return col?.label ?? "";
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="h-full overflow-auto">
|
|
3
|
+
<div v-if="filteredItems.length === 0" class="h-full flex items-center justify-center text-gray-400 text-sm">No items match the current filter</div>
|
|
4
|
+
<table v-else class="min-w-full text-sm">
|
|
5
|
+
<thead class="bg-gray-50 sticky top-0 z-10">
|
|
6
|
+
<tr class="text-left text-xs font-medium text-gray-500 uppercase">
|
|
7
|
+
<th v-for="col in COLUMNS" :key="col.key" class="px-3 py-2 cursor-pointer hover:bg-gray-100 select-none" @click="setSort(col.key)">
|
|
8
|
+
{{ col.label }}
|
|
9
|
+
<span v-if="sortKey === col.key" class="material-icons text-xs align-middle">{{ sortDir === "asc" ? "arrow_upward" : "arrow_downward" }}</span>
|
|
10
|
+
</th>
|
|
11
|
+
<th class="px-3 py-2"></th>
|
|
12
|
+
</tr>
|
|
13
|
+
</thead>
|
|
14
|
+
<tbody class="bg-white divide-y divide-gray-100">
|
|
15
|
+
<template v-for="item in sortedItems" :key="item.id">
|
|
16
|
+
<tr class="hover:bg-gray-50">
|
|
17
|
+
<td class="px-3 py-2">
|
|
18
|
+
<input type="checkbox" :checked="item.completed" @change="emit('toggleComplete', item)" />
|
|
19
|
+
</td>
|
|
20
|
+
<td class="px-3 py-2 max-w-md cursor-pointer" @click="toggleExpand(item.id)">
|
|
21
|
+
<div :class="item.completed ? 'line-through text-gray-400' : 'text-gray-800'">
|
|
22
|
+
{{ item.text }}
|
|
23
|
+
</div>
|
|
24
|
+
<div v-if="item.note" class="text-xs text-gray-400 truncate max-w-xs">
|
|
25
|
+
{{ item.note }}
|
|
26
|
+
</div>
|
|
27
|
+
</td>
|
|
28
|
+
<td class="px-3 py-2 text-xs text-gray-600">
|
|
29
|
+
{{ statusLabel(item) }}
|
|
30
|
+
</td>
|
|
31
|
+
<td class="px-3 py-2">
|
|
32
|
+
<span v-if="item.priority" class="px-1.5 py-0.5 rounded-full text-[10px] font-medium" :class="PRIORITY_CLASSES[item.priority]">{{
|
|
33
|
+
PRIORITY_LABELS[item.priority]
|
|
34
|
+
}}</span>
|
|
35
|
+
</td>
|
|
36
|
+
<td class="px-3 py-2">
|
|
37
|
+
<div class="flex flex-wrap gap-1">
|
|
38
|
+
<span
|
|
39
|
+
v-for="label in item.labels ?? []"
|
|
40
|
+
:key="label"
|
|
41
|
+
class="px-1.5 py-0.5 rounded-full text-[10px] font-medium"
|
|
42
|
+
:class="colorForLabel(label)"
|
|
43
|
+
>{{ label }}</span
|
|
44
|
+
>
|
|
45
|
+
</div>
|
|
46
|
+
</td>
|
|
47
|
+
<td class="px-3 py-2 text-xs">
|
|
48
|
+
<span v-if="item.dueDate" class="px-1.5 py-0.5 rounded-full text-[10px] font-medium" :class="dueDateClasses(item.dueDate)">{{
|
|
49
|
+
formatDueLabel(item.dueDate)
|
|
50
|
+
}}</span>
|
|
51
|
+
</td>
|
|
52
|
+
<td class="px-3 py-2 text-xs text-gray-400">
|
|
53
|
+
{{ formatShortDate(item.createdAt) }}
|
|
54
|
+
</td>
|
|
55
|
+
<td class="px-3 py-2 text-right">
|
|
56
|
+
<button class="text-gray-300 hover:text-red-500 text-xs" title="Delete item" @click="emit('delete', item.id)">✕</button>
|
|
57
|
+
</td>
|
|
58
|
+
</tr>
|
|
59
|
+
<tr v-if="expandedId === item.id">
|
|
60
|
+
<td colspan="8" class="bg-blue-50 p-0">
|
|
61
|
+
<TodoEditPanel :item="item" :columns="columns" @save="(input) => onSave(item.id, input)" @cancel="expandedId = null" />
|
|
62
|
+
</td>
|
|
63
|
+
</tr>
|
|
64
|
+
</template>
|
|
65
|
+
</tbody>
|
|
66
|
+
</table>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
69
|
+
|
|
70
|
+
<script setup lang="ts">
|
|
71
|
+
import { computed, ref } from "vue";
|
|
72
|
+
import type { StatusColumn, TodoItem } from "../../plugins/todo/index";
|
|
73
|
+
import { colorForLabel } from "../../plugins/todo/labels";
|
|
74
|
+
import { PRIORITY_CLASSES, PRIORITY_LABELS, PRIORITY_ORDER, dueDateClasses, formatDueLabel } from "../../plugins/todo/priority";
|
|
75
|
+
import type { PatchItemInput } from "../../plugins/todo/composables/useTodos";
|
|
76
|
+
import TodoEditPanel from "./TodoEditPanel.vue";
|
|
77
|
+
import { formatShortDate } from "../../utils/format/date";
|
|
78
|
+
|
|
79
|
+
type SortKey = "completed" | "text" | "status" | "priority" | "labels" | "dueDate" | "createdAt";
|
|
80
|
+
type SortDir = "asc" | "desc";
|
|
81
|
+
|
|
82
|
+
interface ColumnDef {
|
|
83
|
+
key: SortKey;
|
|
84
|
+
label: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const COLUMNS: ColumnDef[] = [
|
|
88
|
+
{ key: "completed", label: "" },
|
|
89
|
+
{ key: "text", label: "Text" },
|
|
90
|
+
{ key: "status", label: "Status" },
|
|
91
|
+
{ key: "priority", label: "Priority" },
|
|
92
|
+
{ key: "labels", label: "Labels" },
|
|
93
|
+
{ key: "dueDate", label: "Due" },
|
|
94
|
+
{ key: "createdAt", label: "Created" },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const props = defineProps<{
|
|
98
|
+
filteredItems: TodoItem[];
|
|
99
|
+
columns: StatusColumn[];
|
|
100
|
+
}>();
|
|
101
|
+
|
|
102
|
+
const emit = defineEmits<{
|
|
103
|
+
patch: [id: string, input: PatchItemInput];
|
|
104
|
+
delete: [id: string];
|
|
105
|
+
toggleComplete: [item: TodoItem];
|
|
106
|
+
}>();
|
|
107
|
+
|
|
108
|
+
const sortKey = ref<SortKey>("createdAt");
|
|
109
|
+
const sortDir = ref<SortDir>("desc");
|
|
110
|
+
const expandedId = ref<string | null>(null);
|
|
111
|
+
|
|
112
|
+
function setSort(key: SortKey): void {
|
|
113
|
+
if (sortKey.value === key) {
|
|
114
|
+
sortDir.value = sortDir.value === "asc" ? "desc" : "asc";
|
|
115
|
+
} else {
|
|
116
|
+
sortKey.value = key;
|
|
117
|
+
sortDir.value = key === "createdAt" ? "desc" : "asc";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function toggleExpand(itemId: string): void {
|
|
122
|
+
expandedId.value = expandedId.value === itemId ? null : itemId;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function onSave(itemId: string, input: PatchItemInput): void {
|
|
126
|
+
emit("patch", itemId, input);
|
|
127
|
+
expandedId.value = null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function statusLabel(item: TodoItem): string {
|
|
131
|
+
if (!item.status) return "";
|
|
132
|
+
return props.columns.find((col) => col.id === item.status)?.label ?? "";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function compareValues(left: unknown, right: unknown): number {
|
|
136
|
+
// Undefined sorts last regardless of direction so empty cells stay
|
|
137
|
+
// at the bottom of the list (ascending) or top (descending — flipped
|
|
138
|
+
// by the caller). This matches GitHub's "issues with no due date"
|
|
139
|
+
// ordering, which I find the least surprising.
|
|
140
|
+
if (left === undefined && right === undefined) return 0;
|
|
141
|
+
if (left === undefined) return 1;
|
|
142
|
+
if (right === undefined) return -1;
|
|
143
|
+
if (typeof left === "number" && typeof right === "number") return left - right;
|
|
144
|
+
if (typeof left === "boolean" && typeof right === "boolean") {
|
|
145
|
+
return left === right ? 0 : left ? 1 : -1;
|
|
146
|
+
}
|
|
147
|
+
return String(left).localeCompare(String(right));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function sortValueOf(item: TodoItem, key: SortKey): unknown {
|
|
151
|
+
switch (key) {
|
|
152
|
+
case "completed":
|
|
153
|
+
return item.completed;
|
|
154
|
+
case "text":
|
|
155
|
+
return item.text.toLowerCase();
|
|
156
|
+
case "status":
|
|
157
|
+
return statusLabel(item).toLowerCase();
|
|
158
|
+
case "priority":
|
|
159
|
+
return item.priority ? PRIORITY_ORDER[item.priority] : undefined;
|
|
160
|
+
case "labels":
|
|
161
|
+
return (item.labels ?? []).join(",").toLowerCase();
|
|
162
|
+
case "dueDate":
|
|
163
|
+
return item.dueDate;
|
|
164
|
+
case "createdAt":
|
|
165
|
+
return item.createdAt;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const sortedItems = computed(() => {
|
|
170
|
+
const list = [...props.filteredItems];
|
|
171
|
+
list.sort((left, right) => {
|
|
172
|
+
const result = compareValues(sortValueOf(left, sortKey.value), sortValueOf(right, sortKey.value));
|
|
173
|
+
return sortDir.value === "asc" ? result : -result;
|
|
174
|
+
});
|
|
175
|
+
return list;
|
|
176
|
+
});
|
|
177
|
+
</script>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Provide/inject contract for the currently-active chat session.
|
|
2
|
+
//
|
|
3
|
+
// Plugin Views (e.g. MulmoScript) need two things from the app-level
|
|
4
|
+
// session state:
|
|
5
|
+
//
|
|
6
|
+
// 1. The `chatSessionId` so they can tag long-running work (image /
|
|
7
|
+
// audio / movie generation) on the right session channel.
|
|
8
|
+
// 2. A reactive view of `pendingGenerations` so they can derive
|
|
9
|
+
// per-beat / per-character "rendering" spinners from the same
|
|
10
|
+
// map the sidebar busy-indicator reads. This lets the spinner
|
|
11
|
+
// survive View unmount/remount across session switches.
|
|
12
|
+
//
|
|
13
|
+
// Rather than thread two new props through every plugin's
|
|
14
|
+
// `<component :is="...">` mount point, we expose the active session
|
|
15
|
+
// via provide/inject — same pattern as useAppApi.
|
|
16
|
+
|
|
17
|
+
import { inject, provide, type Ref } from "vue";
|
|
18
|
+
import type { ActiveSession } from "../types/session";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Ref to the currently-active session. May be `undefined` during the
|
|
22
|
+
* brief window before the first session loads.
|
|
23
|
+
*/
|
|
24
|
+
export type ActiveSessionRef = Ref<ActiveSession | undefined>;
|
|
25
|
+
|
|
26
|
+
const ACTIVE_SESSION_KEY = Symbol("activeSession");
|
|
27
|
+
|
|
28
|
+
/** Called once in App.vue setup to expose the ref to descendants. */
|
|
29
|
+
export function provideActiveSession(ref: ActiveSessionRef): void {
|
|
30
|
+
provide(ACTIVE_SESSION_KEY, ref);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Plugin Views call this to observe the active session. Returns
|
|
35
|
+
* `undefined` when used outside an App.vue subtree (e.g. in a unit
|
|
36
|
+
* test) so plugins can render standalone without the provider.
|
|
37
|
+
*/
|
|
38
|
+
export function useActiveSession(): ActiveSessionRef | undefined {
|
|
39
|
+
return inject<ActiveSessionRef>(ACTIVE_SESSION_KEY);
|
|
40
|
+
}
|