Skip to content

Commit d896859

Browse files
committed
plugin createTaskModal: add split button for add task actions
1 parent 05fa7bf commit d896859

File tree

4 files changed

+122
-18
lines changed

4 files changed

+122
-18
lines changed

plugin/src/i18n/langs/en.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ export const en: Translations = {
123123
"A link to this page will be appended to the task description",
124124
cancelButtonLabel: "Cancel",
125125
addTaskButtonLabel: "Add task",
126+
addTaskAndCopyAppLabel: "Add task and copy link (app)",
127+
addTaskAndCopyWebLabel: "Add task and copy link (web)",
128+
actionMenuLabel: "Add task action menu",
129+
linkCopiedNotice: "Task created and link copied to clipboard",
130+
linkCopyFailedNotice: "Task created, but failed to copy link to clipboard",
126131
failedToFindInboxNotice: "Error: could not find inbox project",
127132
defaultProjectDeletedNotice: (projectName: string) =>
128133
`Default project "${projectName}" no longer exists. Using Inbox instead.`,

plugin/src/i18n/translation.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ export type Translations = {
118118
appendedLinkToDescriptionMessage: string;
119119
cancelButtonLabel: string;
120120
addTaskButtonLabel: string;
121+
addTaskAndCopyAppLabel: string;
122+
addTaskAndCopyWebLabel: string;
123+
actionMenuLabel: string;
124+
linkCopiedNotice: string;
125+
linkCopyFailedNotice: string;
121126
failedToFindInboxNotice: string;
122127
defaultProjectDeletedNotice: (projectName: string) => string;
123128
defaultLabelsDeletedNotice: (labelNames: string) => string;

plugin/src/ui/createTaskModal/index.tsx

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { toCalendarDateTime, toZoned } from "@internationalized/date";
22
import { Notice, type TFile } from "obsidian";
33
import type React from "react";
44
import { useEffect, useState } from "react";
5-
import { Button } from "react-aria-components";
5+
import { Button, Label, Menu, MenuItem, MenuTrigger } from "react-aria-components";
66

77
import { t } from "@/i18n";
88
import { timezone, today } from "@/infra/time";
99
import {
10+
type AddTaskAction,
1011
type DueDateDefaultSetting,
1112
type LabelsDefaultSetting,
1213
type ProjectDefaultSetting,
@@ -15,11 +16,13 @@ import {
1516
import { ModalContext, PluginContext } from "@/ui/context";
1617

1718
import type TodoistPlugin from "../..";
18-
import type { Label } from "../../api/domain/label";
19+
import type { Label as TodoistLabel } from "../../api/domain/label";
1920
import type { CreateTaskParams, Priority } from "../../api/domain/task";
21+
import { ObsidianIcon } from "../components/obsidian-icon";
2022
import { type Deadline, DeadlineSelector } from "./DeadlineSelector";
2123
import { type DueDate, DueDateSelector } from "./DueDateSelector";
2224
import { LabelSelector } from "./LabelSelector";
25+
import { Popover } from "./Popover";
2326
import { PrioritySelector } from "./PrioritySelector";
2427
import { type ProjectIdentifier, ProjectSelector } from "./ProjectSelector";
2528
import { TaskContentInput } from "./TaskContentInput";
@@ -94,13 +97,13 @@ const calculateDefaultProject = (
9497
const calculateDefaultLabels = (
9598
plugin: TodoistPlugin,
9699
labelsSetting: LabelsDefaultSetting,
97-
): Label[] => {
100+
): TodoistLabel[] => {
98101
if (labelsSetting.length === 0) {
99102
return [];
100103
}
101104

102105
const allLabels = Array.from(plugin.services.todoist.data().labels.iter());
103-
const validLabels: Label[] = [];
106+
const validLabels: TodoistLabel[] = [];
104107
const deletedLabelNames: string[] = [];
105108

106109
for (const defaultLabel of labelsSetting) {
@@ -163,7 +166,7 @@ const CreateTaskModalContent: React.FC<CreateTaskProps> = ({
163166
calculateDefaultDueDate(settings.taskCreationDefaultDueDate),
164167
);
165168
const [priority, setPriority] = useState<Priority>(1);
166-
const [labels, setLabels] = useState<Label[]>(() =>
169+
const [labels, setLabels] = useState<TodoistLabel[]>(() =>
167170
calculateDefaultLabels(plugin, settings.taskCreationDefaultLabels),
168171
);
169172
const [deadline, setDeadline] = useState<Deadline | undefined>();
@@ -172,6 +175,7 @@ const CreateTaskModalContent: React.FC<CreateTaskProps> = ({
172175
);
173176

174177
const [options, setOptions] = useState<TaskCreationOptions>(initialOptions);
178+
const [currentAction, setCurrentAction] = useState<AddTaskAction>(settings.defaultAddTaskAction);
175179

176180
const isPremium = plugin.services.todoist.isPremium();
177181
const isSubmitButtonDisabled = content === "" && options.appendLinkTo !== "content";
@@ -194,7 +198,7 @@ const CreateTaskModalContent: React.FC<CreateTaskProps> = ({
194198
return builder.join("");
195199
};
196200

197-
const createTask = async () => {
201+
const createTask = async (action: AddTaskAction) => {
198202
if (isSubmitButtonDisabled) {
199203
return;
200204
}
@@ -225,17 +229,50 @@ const CreateTaskModalContent: React.FC<CreateTaskProps> = ({
225229
}
226230

227231
try {
228-
await plugin.services.todoist.actions.createTask(
232+
const task = await plugin.services.todoist.actions.createTask(
229233
buildWithLink(content, options.appendLinkTo === "content"),
230234
params,
231235
);
232-
new Notice(i18n.successNotice);
236+
237+
let url: string | undefined;
238+
switch (action) {
239+
case "add-copy-app":
240+
url = `todoist://task?id=${task.id}`;
241+
break;
242+
case "add-copy-web":
243+
url = `https://todoist.com/app/project/${task.projectId}/task/${task.id}`;
244+
break;
245+
}
246+
247+
if (url !== undefined) {
248+
const markdownLink = `[${task.content}](${url})`;
249+
try {
250+
await navigator.clipboard.writeText(markdownLink);
251+
new Notice(i18n.linkCopiedNotice);
252+
} catch (clipboardErr) {
253+
new Notice(i18n.linkCopyFailedNotice);
254+
console.error("Failed to copy to clipboard", clipboardErr);
255+
}
256+
} else {
257+
new Notice(i18n.successNotice);
258+
}
233259
} catch (err) {
234260
new Notice(i18n.errorNotice);
235261
console.error("Failed to create task", err);
236262
}
237263
};
238264

265+
const getActionLabel = (action: AddTaskAction): string => {
266+
switch (action) {
267+
case "add":
268+
return i18n.addTaskButtonLabel;
269+
case "add-copy-app":
270+
return i18n.addTaskAndCopyAppLabel;
271+
case "add-copy-web":
272+
return i18n.addTaskAndCopyWebLabel;
273+
}
274+
};
275+
239276
const linkDestinationMessage = getLinkDestinationMessage(options.appendLinkTo, i18n);
240277

241278
return (
@@ -246,7 +283,7 @@ const CreateTaskModalContent: React.FC<CreateTaskProps> = ({
246283
content={content}
247284
onChange={setContent}
248285
autofocus={true}
249-
onEnterKey={createTask}
286+
onEnterKey={() => createTask(currentAction)}
250287
/>
251288
<TaskContentInput
252289
className="task-description"
@@ -277,14 +314,42 @@ const CreateTaskModalContent: React.FC<CreateTaskProps> = ({
277314
<Button onPress={() => modal.close()} aria-label={i18n.cancelButtonLabel}>
278315
{i18n.cancelButtonLabel}
279316
</Button>
280-
<Button
281-
className="mod-cta"
282-
isDisabled={isSubmitButtonDisabled}
283-
onPress={createTask}
284-
aria-label={i18n.addTaskButtonLabel}
285-
>
286-
{i18n.addTaskButtonLabel}
287-
</Button>
317+
<div className="add-task-button-group">
318+
<Button
319+
className="mod-cta add-task-primary"
320+
isDisabled={isSubmitButtonDisabled}
321+
onPress={() => createTask(currentAction)}
322+
aria-label={getActionLabel(currentAction)}
323+
>
324+
{getActionLabel(currentAction)}
325+
</Button>
326+
<MenuTrigger>
327+
<Button
328+
className="mod-cta add-task-dropdown"
329+
isDisabled={isSubmitButtonDisabled}
330+
aria-label={i18n.actionMenuLabel}
331+
>
332+
<ObsidianIcon id="chevron-down" size="s" />
333+
</Button>
334+
<Popover>
335+
<Menu
336+
className="add-task-action-menu task-option-dialog"
337+
aria-label={i18n.actionMenuLabel}
338+
onAction={(key) => setCurrentAction(key as AddTaskAction)}
339+
>
340+
<MenuItem key="add" id="add">
341+
<Label>{i18n.addTaskButtonLabel}</Label>
342+
</MenuItem>
343+
<MenuItem key="add-copy-app" id="add-copy-app">
344+
<Label>{i18n.addTaskAndCopyAppLabel}</Label>
345+
</MenuItem>
346+
<MenuItem key="add-copy-web" id="add-copy-web">
347+
<Label>{i18n.addTaskAndCopyWebLabel}</Label>
348+
</MenuItem>
349+
</Menu>
350+
</Popover>
351+
</MenuTrigger>
352+
</div>
288353
</div>
289354
</div>
290355
</div>

plugin/src/ui/createTaskModal/styles.scss

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,42 @@
9696
}
9797
}
9898

99+
.add-task-button-group {
100+
display: flex;
101+
gap: 0;
102+
103+
.add-task-primary {
104+
border-top-right-radius: 0;
105+
border-bottom-right-radius: 0;
106+
flex: 1;
107+
}
108+
109+
.add-task-dropdown {
110+
border-top-left-radius: 0;
111+
border-bottom-left-radius: 0;
112+
border-left: 1px solid var(--background-modifier-border);
113+
padding: 0 8px;
114+
min-width: auto;
115+
}
116+
}
117+
99118
hr {
100119
margin: 1em 0;
101120
border-color: var(--color-base-25);
102121
}
103122
}
104123

124+
.add-task-action-menu {
125+
.react-aria-MenuItem {
126+
padding: 8px 1em;
127+
font-size: var(--font-smaller);
128+
129+
&:hover {
130+
background-color: var(--background-modifier-cover);
131+
}
132+
}
133+
}
134+
105135
.task-option-dialog {
106136
background-color: var(--modal-background);
107137
border: var(--modal-border-width) solid var(--modal-border-color);
@@ -281,7 +311,6 @@
281311
}
282312
}
283313

284-
285314
.task-options-menu {
286315
.task-option-dialog-item {
287316
padding: 8px 1em;

0 commit comments

Comments
 (0)