-
Notifications
You must be signed in to change notification settings - Fork 970
feat(desktop): add task create modal #2656
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
527d59a
264a18f
c82c3f9
cee6834
04c1542
7f2dbe5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # Desktop Task Create: Linear-Style TipTap Plan | ||
|
|
||
| ## Goal | ||
|
|
||
| Add a desktop-only create-task flow in the Tasks view that feels like Linear and uses the same editor surface as task editing. | ||
|
|
||
| ## Scope | ||
|
|
||
| - Desktop only | ||
| - No web work | ||
| - No package extraction unless it becomes necessary later | ||
|
|
||
| ## Key Decision | ||
|
|
||
| Create and edit should share one desktop task composer. Do not build a separate create modal with a second editor implementation. | ||
|
|
||
| ## Existing Anchors | ||
|
|
||
| - Current task editor: `apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/TaskMarkdownRenderer/TaskMarkdownRenderer.tsx` | ||
| - Bubble toolbar: `apps/desktop/src/renderer/components/MarkdownRenderer/components/TipTapMarkdownRenderer/components/BubbleMenuToolbar/BubbleMenuToolbar.tsx` | ||
| - Tasks top bar entry point: `apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/TasksTopBar.tsx` | ||
| - Existing metadata patterns: | ||
| - status: `apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/StatusProperty/StatusProperty.tsx` | ||
| - priority: `apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/shared/PriorityMenuItems.tsx` | ||
| - assignee: `apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/AssigneeFilter/AssigneeFilter.tsx` | ||
|
|
||
| ## Plan | ||
|
|
||
| 1. Extract a shared desktop `TaskComposer` under the desktop tasks feature. | ||
| Start from the current `TaskMarkdownRenderer`, bubble menu, and existing metadata controls. | ||
|
|
||
| 2. Migrate the existing task detail editor to `TaskComposer` in `edit` mode. | ||
| This keeps behavior aligned before adding create. | ||
|
|
||
| 3. Add a `CreateTaskDialog` launched from `TasksTopBar`. | ||
| The dialog should be compact and Linear-style: title first, metadata row, TipTap description, submit via `Cmd/Ctrl+Enter`. | ||
|
|
||
| 4. Add a desktop-facing create path that owns slug/default status resolution. | ||
| Do not change the existing low-level `task.create` contract in place if desktop/mobile sync depends on it. | ||
|
|
||
| 5. On successful create, close the dialog, refresh task data, and navigate to the new task. | ||
|
|
||
| ## Non-Goals for V1 | ||
|
|
||
| - Web create flow | ||
| - Slash commands | ||
| - Image handling | ||
| - Full editor/package sharing outside the desktop task feature | ||
|
|
||
| ## Validation | ||
|
|
||
| ```bash | ||
| bun run typecheck | ||
| bun run lint | ||
| ``` | ||
|
|
||
| Manual checks: | ||
|
|
||
| 1. Create a task from the tasks top bar. | ||
| 2. Edit an existing task with the refactored composer. | ||
| 3. Confirm create and edit feel like the same surface. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||||||||||||||||||
| import "highlight.js/styles/github-dark.css"; | ||||||||||||||||||||||||||||||||||||||
| import "./task-markdown.css"; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import { cn } from "@superset/ui/utils"; | ||||||||||||||||||||||||||||||||||||||
| import { Extension } from "@tiptap/core"; | ||||||||||||||||||||||||||||||||||||||
| import { Blockquote } from "@tiptap/extension-blockquote"; | ||||||||||||||||||||||||||||||||||||||
| import { Bold } from "@tiptap/extension-bold"; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -24,9 +25,15 @@ import TaskItem from "@tiptap/extension-task-item"; | |||||||||||||||||||||||||||||||||||||
| import TaskList from "@tiptap/extension-task-list"; | ||||||||||||||||||||||||||||||||||||||
| import { Text } from "@tiptap/extension-text"; | ||||||||||||||||||||||||||||||||||||||
| import { Underline } from "@tiptap/extension-underline"; | ||||||||||||||||||||||||||||||||||||||
| import { EditorContent, ReactNodeViewRenderer, useEditor } from "@tiptap/react"; | ||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||
| type Editor, | ||||||||||||||||||||||||||||||||||||||
| EditorContent, | ||||||||||||||||||||||||||||||||||||||
| ReactNodeViewRenderer, | ||||||||||||||||||||||||||||||||||||||
| useEditor, | ||||||||||||||||||||||||||||||||||||||
| } from "@tiptap/react"; | ||||||||||||||||||||||||||||||||||||||
| import { BubbleMenu } from "@tiptap/react/menus"; | ||||||||||||||||||||||||||||||||||||||
| import { common, createLowlight } from "lowlight"; | ||||||||||||||||||||||||||||||||||||||
| import { useEffect } from "react"; | ||||||||||||||||||||||||||||||||||||||
| import { BubbleMenuToolbar } from "renderer/components/MarkdownRenderer/components/TipTapMarkdownRenderer/components/BubbleMenuToolbar"; | ||||||||||||||||||||||||||||||||||||||
| import { env } from "renderer/env.renderer"; | ||||||||||||||||||||||||||||||||||||||
| import { Markdown } from "tiptap-markdown"; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -117,14 +124,34 @@ const KeyboardHandler = Extension.create({ | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| interface TaskMarkdownRendererProps { | ||||||||||||||||||||||||||||||||||||||
| content: string; | ||||||||||||||||||||||||||||||||||||||
| onSave: (markdown: string) => void; | ||||||||||||||||||||||||||||||||||||||
| onSave?: (markdown: string) => void; | ||||||||||||||||||||||||||||||||||||||
| onChange?: (markdown: string) => void; | ||||||||||||||||||||||||||||||||||||||
| placeholder?: string; | ||||||||||||||||||||||||||||||||||||||
| autoFocus?: boolean; | ||||||||||||||||||||||||||||||||||||||
| className?: string; | ||||||||||||||||||||||||||||||||||||||
| editorClassName?: string; | ||||||||||||||||||||||||||||||||||||||
| onModEnter?: () => void; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| function getMarkdown(editor: Editor | null): string { | ||||||||||||||||||||||||||||||||||||||
| const storage = editor?.storage as | ||||||||||||||||||||||||||||||||||||||
| | Record<string, { getMarkdown?: () => string }> | ||||||||||||||||||||||||||||||||||||||
| | undefined; | ||||||||||||||||||||||||||||||||||||||
| return storage?.markdown?.getMarkdown?.() ?? ""; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| export function TaskMarkdownRenderer({ | ||||||||||||||||||||||||||||||||||||||
| content, | ||||||||||||||||||||||||||||||||||||||
| onSave, | ||||||||||||||||||||||||||||||||||||||
| onChange, | ||||||||||||||||||||||||||||||||||||||
| placeholder = "Add description...", | ||||||||||||||||||||||||||||||||||||||
| autoFocus = false, | ||||||||||||||||||||||||||||||||||||||
| className, | ||||||||||||||||||||||||||||||||||||||
| editorClassName, | ||||||||||||||||||||||||||||||||||||||
| onModEnter, | ||||||||||||||||||||||||||||||||||||||
| }: TaskMarkdownRendererProps) { | ||||||||||||||||||||||||||||||||||||||
| const editor = useEditor({ | ||||||||||||||||||||||||||||||||||||||
| autofocus: autoFocus ? "end" : false, | ||||||||||||||||||||||||||||||||||||||
| extensions: [ | ||||||||||||||||||||||||||||||||||||||
| Document, | ||||||||||||||||||||||||||||||||||||||
| Text, | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -198,7 +225,7 @@ export function TaskMarkdownRenderer({ | |||||||||||||||||||||||||||||||||||||
| Placeholder.configure({ | ||||||||||||||||||||||||||||||||||||||
| placeholder: ({ node }) => { | ||||||||||||||||||||||||||||||||||||||
| if (node.type.name === "paragraph") { | ||||||||||||||||||||||||||||||||||||||
| return "Add description..."; | ||||||||||||||||||||||||||||||||||||||
| return placeholder; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| return ""; | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -217,21 +244,35 @@ export function TaskMarkdownRenderer({ | |||||||||||||||||||||||||||||||||||||
| content, | ||||||||||||||||||||||||||||||||||||||
| editorProps: { | ||||||||||||||||||||||||||||||||||||||
| attributes: { | ||||||||||||||||||||||||||||||||||||||
| class: "focus:outline-none min-h-[100px]", | ||||||||||||||||||||||||||||||||||||||
| class: cn("focus:outline-none min-h-[100px]", editorClassName), | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| handleKeyDown: (_, event) => { | ||||||||||||||||||||||||||||||||||||||
| if ((event.metaKey || event.ctrlKey) && event.key === "Enter") { | ||||||||||||||||||||||||||||||||||||||
| onModEnter?.(); | ||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+249
to
+255
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's find and examine the TaskMarkdownRenderer component
fd "TaskMarkdownRenderer" --type fRepository: superset-sh/superset Length of output: 193 🏁 Script executed: # Search for the prop interface/type definition and usages
rg -A 15 "interface.*TaskMarkdownRenderer.*Props|type.*TaskMarkdownRenderer.*Props" apps/desktop/srcRepository: superset-sh/superset Length of output: 2605 🏁 Script executed: # Search for usages of TaskMarkdownRenderer to see where onModEnter is/isn't provided
rg "TaskMarkdownRenderer" apps/desktop/src --type tsx --type ts -B 2 -A 5Repository: superset-sh/superset Length of output: 91 🏁 Script executed: # Search for usages of TaskMarkdownRenderer
rg "TaskMarkdownRenderer" apps/desktop/src -A 5 -B 2Repository: superset-sh/superset Length of output: 9181 🏁 Script executed: # Get the full component to understand the current implementation
cat -n apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/TaskMarkdownRenderer/TaskMarkdownRenderer.tsx | head -280 | tail -50Repository: superset-sh/superset Length of output: 218 🏁 Script executed: # Read the TaskMarkdownRenderer component file
cat -n apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/\$taskId/components/TaskMarkdownRenderer/TaskMarkdownRenderer.tsx | sed -n '240,260p'Repository: superset-sh/superset Length of output: 654 🏁 Script executed: # Get more context around the handleKeyDown implementation
cat -n apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/\$taskId/components/TaskMarkdownRenderer/TaskMarkdownRenderer.tsx | sed -n '240,270p'Repository: superset-sh/superset Length of output: 923 Only consume In the task detail view ( Suggested fix handleKeyDown: (_, event) => {
- if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
- onModEnter?.();
+ if (
+ (event.metaKey || event.ctrlKey) &&
+ event.key === "Enter" &&
+ onModEnter
+ ) {
+ onModEnter();
return true;
}
return false;
},📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| onUpdate: ({ editor }) => { | ||||||||||||||||||||||||||||||||||||||
| onChange?.(getMarkdown(editor)); | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| onBlur: ({ editor }) => { | ||||||||||||||||||||||||||||||||||||||
| const storage = editor.storage as unknown as Record< | ||||||||||||||||||||||||||||||||||||||
| string, | ||||||||||||||||||||||||||||||||||||||
| { getMarkdown?: () => string } | ||||||||||||||||||||||||||||||||||||||
| >; | ||||||||||||||||||||||||||||||||||||||
| const markdown = storage.markdown?.getMarkdown?.() ?? ""; | ||||||||||||||||||||||||||||||||||||||
| onSave(markdown); | ||||||||||||||||||||||||||||||||||||||
| onSave?.(getMarkdown(editor)); | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||
| if (!editor || editor.isFocused) return; | ||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: External Prompt for AI agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const currentMarkdown = getMarkdown(editor); | ||||||||||||||||||||||||||||||||||||||
| if (currentMarkdown === content) return; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| editor.commands.setContent(content, { emitUpdate: false }); | ||||||||||||||||||||||||||||||||||||||
| }, [content, editor]); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||
| <div className="w-full"> | ||||||||||||||||||||||||||||||||||||||
| <div className={cn("w-full", className)}> | ||||||||||||||||||||||||||||||||||||||
| {editor && ( | ||||||||||||||||||||||||||||||||||||||
| <BubbleMenu | ||||||||||||||||||||||||||||||||||||||
| editor={editor} | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,10 +3,12 @@ import { ScrollArea } from "@superset/ui/scroll-area"; | |
| import { Separator } from "@superset/ui/separator"; | ||
| import { eq, or } from "@tanstack/db"; | ||
| import { useLiveQuery } from "@tanstack/react-db"; | ||
| import { useQuery } from "@tanstack/react-query"; | ||
| import { createFileRoute, useNavigate } from "@tanstack/react-router"; | ||
| import { useMemo } from "react"; | ||
| import { HiArrowLeft } from "react-icons/hi2"; | ||
| import { LuExternalLink } from "react-icons/lu"; | ||
| import { apiTrpcClient } from "renderer/lib/api-trpc-client"; | ||
| import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; | ||
| import type { TaskWithStatus } from "../components/TasksView/hooks/useTasksTable"; | ||
| import { Route as TasksLayoutRoute } from "../layout"; | ||
|
|
@@ -28,6 +30,10 @@ function TaskDetailPage() { | |
| const { tab, assignee, search } = TasksLayoutRoute.useSearch(); | ||
| const navigate = useNavigate(); | ||
| const collections = useCollections(); | ||
| const isUuidTaskId = | ||
| /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( | ||
| taskId, | ||
| ); | ||
|
|
||
| const backSearch = useMemo(() => { | ||
| const s: Record<string, string> = {}; | ||
|
|
@@ -62,6 +68,17 @@ function TaskDetailPage() { | |
| if (!taskData || taskData.length === 0) return null; | ||
| return taskData[0]; | ||
| }, [taskData]); | ||
| const taskFallbackQuery = useQuery({ | ||
| queryKey: ["task-detail-fallback", taskId, isUuidTaskId ? "id" : "slug"], | ||
| queryFn: () => | ||
| isUuidTaskId | ||
| ? apiTrpcClient.task.byId.query(taskId) | ||
| : apiTrpcClient.task.bySlug.query(taskId), | ||
| enabled: !task, | ||
| retry: false, | ||
| }); | ||
|
Comment on lines
+71
to
+79
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fallback query currently starts before the primary lookup has conclusively missed. On Line 77, 💡 Suggested change- enabled: !task,
+ enabled: taskData !== undefined && taskData.length === 0,🤖 Prompt for AI Agents |
||
| const isTaskSyncing = !task && !!taskFallbackQuery.data; | ||
| const isTaskLoading = !task && taskFallbackQuery.isPending; | ||
|
|
||
| const handleBack = () => { | ||
| navigate({ to: "/tasks", search: backSearch }); | ||
|
|
@@ -82,6 +99,16 @@ function TaskDetailPage() { | |
| }; | ||
|
|
||
| if (!task) { | ||
| if (isTaskLoading || isTaskSyncing) { | ||
| return ( | ||
| <div className="flex-1 flex items-center justify-center"> | ||
| <span className="text-muted-foreground"> | ||
| {isTaskSyncing ? "Syncing task..." : "Loading task..."} | ||
| </span> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="flex-1 flex items-center justify-center"> | ||
| <span className="text-muted-foreground">Task not found</span> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Only consume Cmd/Ctrl+Enter when
onModEnterexists; currently the shortcut is swallowed even when no callback is provided.Prompt for AI agents