From 957c3782dcbecc3c5322639e83e1d024b8ce5d76 Mon Sep 17 00:00:00 2001 From: David Rousselie Date: Thu, 1 Feb 2024 22:42:12 +0100 Subject: [PATCH] feat: Add "Plan task" action for notifications created from task --- CHANGELOG.md | 12 +- src/action/NotificationActions.tsx | 44 ++++--- src/action/NotificationTaskActions.tsx | 80 +++++++++---- src/action/PlanTask.tsx | 155 +++++++++++++++++++++++++ 4 files changed, 249 insertions(+), 42 deletions(-) create mode 100644 src/action/PlanTask.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a3a620..79268e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,17 @@ ## [Unreleased] -## [0.1.1] - 2024-02-31 +## [0.1.2] - 2024-02-01 + +### Added + +- Add "Plan task" action for notification created from task + +### Fixed + +- Fix "Complete task" action + +## [0.1.1] - 2024-02-01 ### Added diff --git a/src/action/NotificationActions.tsx b/src/action/NotificationActions.tsx index 796b015..71da14f 100644 --- a/src/action/NotificationActions.tsx +++ b/src/action/NotificationActions.tsx @@ -62,28 +62,33 @@ export function NotificationActions({ notification, detailsTarget, mutate }: Not ); } -async function deleteNotification(notification: Notification, mutate: MutatePromise | undefined>) { +export async function deleteNotification( + notification: Notification, + mutate: MutatePromise | undefined>, +) { const preferences = getPreferenceValues(); const toast = await showToast({ style: Toast.Style.Animated, title: "Deleting notification" }); try { if (isNotificationBuiltFromTask(notification) && notification.task) { await mutate( - fetch(`${preferences.universalInboxBaseUrl}/api/tasks/${notification.task.id}`, { - method: "PATCH", - body: JSON.stringify({ status: TaskStatus.Deleted }), - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${preferences.apiKey}`, + handleErrors( + fetch(`${preferences.universalInboxBaseUrl}/api/tasks/${notification.task.id}`, { + method: "PATCH", + body: JSON.stringify({ status: TaskStatus.Deleted }), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${preferences.apiKey}`, + }, + }), + { + optimisticUpdate(page) { + if (page) { + page.content = page.content.filter((n) => n.id !== notification.id); + } + return page; + }, }, - }), - { - optimisticUpdate(page) { - if (page) { - page.content = page.content.filter((n) => n.id !== notification.id); - } - return page; - }, - }, + ), ); } else { await mutate( @@ -118,7 +123,7 @@ async function deleteNotification(notification: Notification, mutate: MutateProm } } -async function unsubscribeFromNotification( +export async function unsubscribeFromNotification( notification: Notification, mutate: MutatePromise | undefined>, ) { @@ -156,7 +161,10 @@ async function unsubscribeFromNotification( } } -async function snoozeNotification(notification: Notification, mutate: MutatePromise | undefined>) { +export async function snoozeNotification( + notification: Notification, + mutate: MutatePromise | undefined>, +) { const preferences = getPreferenceValues(); const toast = await showToast({ style: Toast.Style.Animated, title: "Snoozing notification" }); try { diff --git a/src/action/NotificationTaskActions.tsx b/src/action/NotificationTaskActions.tsx index f36b315..7c31832 100644 --- a/src/action/NotificationTaskActions.tsx +++ b/src/action/NotificationTaskActions.tsx @@ -1,24 +1,13 @@ -import { Notification, getNotificationHtmlUrl } from "../notification"; -import { Action, ActionPanel, Icon } from "@raycast/api"; +import { deleteNotification, snoozeNotification, unsubscribeFromNotification } from "./NotificationActions"; +import { Notification, getNotificationHtmlUrl, isNotificationBuiltFromTask } from "../notification"; +import { Action, ActionPanel, Icon, Toast, getPreferenceValues, showToast } from "@raycast/api"; import { MutatePromise } from "@raycast/utils"; import { useMemo, ReactElement } from "react"; +import { PlanTask } from "./PlanTask"; +import { handleErrors } from "../api"; +import { TaskStatus } from "../task"; import { Page } from "../types"; - -function deleteNotification(notification: Notification) { - console.log(`Deleting notification ${notification.id}`); -} - -function unsubscribeFromNotification(notification: Notification) { - console.log(`Unsubcribing from notification ${notification.id}`); -} - -function snoozeNotification(notification: Notification) { - console.log(`Snoozing notification ${notification.id}`); -} - -function completeTask(notification: Notification) { - console.log(`Completing task ${notification.id}`); -} +import fetch from "node-fetch"; interface NotificationTaskActionsProps { notification: Notification; @@ -26,7 +15,7 @@ interface NotificationTaskActionsProps { mutate: MutatePromise | undefined>; } -export function NotificationTaskActions({ notification, detailsTarget }: NotificationTaskActionsProps) { +export function NotificationTaskActions({ notification, detailsTarget, mutate }: NotificationTaskActionsProps) { const notificationHtmlUrl = useMemo(() => { return getNotificationHtmlUrl(notification); }, [notification]); @@ -39,26 +28,71 @@ export function NotificationTaskActions({ notification, detailsTarget }: Notific title="Delete Notification" icon={Icon.Trash} shortcut={{ modifiers: ["ctrl"], key: "d" }} - onAction={() => deleteNotification(notification)} + onAction={() => deleteNotification(notification, mutate)} /> unsubscribeFromNotification(notification)} + onAction={() => unsubscribeFromNotification(notification, mutate)} /> snoozeNotification(notification)} + onAction={() => snoozeNotification(notification, mutate)} /> completeTask(notification)} + onAction={() => completeTask(notification, mutate)} + /> + } /> ); } + +async function completeTask(notification: Notification, mutate: MutatePromise | undefined>) { + if (!isNotificationBuiltFromTask(notification) || !notification.task) { + return; + } + + const preferences = getPreferenceValues(); + const toast = await showToast({ style: Toast.Style.Animated, title: "Marking task as Done" }); + try { + await mutate( + handleErrors( + fetch(`${preferences.universalInboxBaseUrl}/api/tasks/${notification.task.id}`, { + method: "PATCH", + body: JSON.stringify({ status: TaskStatus.Done }), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${preferences.apiKey}`, + }, + }), + ), + { + optimisticUpdate(page) { + if (page) { + page.content = page.content.filter((n) => n.id !== notification.id); + } + return page; + }, + }, + ); + + toast.style = Toast.Style.Success; + toast.title = "Task successfully marked as Done"; + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Failed to mark task as Done"; + toast.message = (error as Error).message; + throw error; + } +} diff --git a/src/action/PlanTask.tsx b/src/action/PlanTask.tsx new file mode 100644 index 0000000..d57f3f6 --- /dev/null +++ b/src/action/PlanTask.tsx @@ -0,0 +1,155 @@ +import { Action, useNavigation, ActionPanel, Form, Icon, getPreferenceValues, showToast, Toast } from "@raycast/api"; +import { MutatePromise, useForm, FormValidation, useFetch } from "@raycast/utils"; +import { Notification, isNotificationBuiltFromTask } from "../notification"; +import { Page, UniversalInboxPreferences } from "../types"; +import { default as dayjs, extend } from "dayjs"; +import { TaskPriority } from "../task"; +import { handleErrors } from "../api"; +import utc from "dayjs/plugin/utc"; +import { useState } from "react"; +import fetch from "node-fetch"; + +extend(utc); + +interface PlanTaskProps { + notification: Notification; + mutate: MutatePromise | undefined>; +} + +interface TaskPlanningFormValues { + project: string; + dueAt: Date | null; + priority: string; +} + +interface ProjectSummary { + name: string; + source_id: string; +} + +interface TaskPlanning { + project: ProjectSummary; + due_at?: { type: "DateTimeWithTz"; content: string }; + priority: TaskPriority; +} + +export function PlanTask({ notification, mutate }: PlanTaskProps) { + const preferences = getPreferenceValues(); + const { pop } = useNavigation(); + const [searchText, setSearchText] = useState(""); + + const { isLoading, data: projects } = useFetch>( + `${preferences.universalInboxBaseUrl}/api/tasks/projects/search?matches=${searchText}`, + { + keepPreviousData: true, + headers: { + Authorization: `Bearer ${preferences.apiKey}`, + }, + }, + ); + + const { handleSubmit, itemProps } = useForm({ + initialValues: { + dueAt: new Date(), + priority: `${TaskPriority.P4 as number}`, + }, + async onSubmit(values) { + const project = projects?.find((p) => p.source_id === values.project); + if (!project) { + throw new Error("Project not found"); + } + const taskPlanning: TaskPlanning = { + project: project, + due_at: values.dueAt ? { type: "DateTimeWithTz", content: dayjs(values.dueAt).utc().format() } : undefined, + priority: parseInt(values.priority) as TaskPriority, + }; + await planTask(taskPlanning, notification, mutate); + pop(); + }, + validation: { + project: FormValidation.Required, + priority: FormValidation.Required, + }, + }); + + return ( +
+ + + } + > + + + + {projects?.map((project) => { + return ; + })} + + + + + + + + + + ); +} + +async function planTask( + taskPlanning: TaskPlanning, + notification: Notification, + mutate: MutatePromise | undefined>, +) { + if (!isNotificationBuiltFromTask(notification) || !notification.task) { + return; + } + + const preferences = getPreferenceValues(); + const toast = await showToast({ style: Toast.Style.Animated, title: "Planning task" }); + try { + await mutate( + handleErrors( + fetch(`${preferences.universalInboxBaseUrl}/api/tasks/${notification.task.id}`, { + method: "PATCH", + body: JSON.stringify({ + project: taskPlanning.project.name, + due_at: taskPlanning.due_at, + priority: taskPlanning.priority, + }), + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${preferences.apiKey}`, + }, + }), + ), + { + optimisticUpdate(page) { + if (page) { + page.content = page.content.filter((n) => n.id !== notification.id); + } + return page; + }, + }, + ); + + toast.style = Toast.Style.Success; + toast.title = "Task successfully planned"; + } catch (error) { + toast.style = Toast.Style.Failure; + toast.title = "Failed to plan task"; + toast.message = (error as Error).message; + throw error; + } +}