feat: Add notification actions
This commit is contained in:
152
src/action/CreateTaskFromNotification.tsx
Normal file
152
src/action/CreateTaskFromNotification.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { Action, useNavigation, ActionPanel, Form, Icon, getPreferenceValues, showToast, Toast } from "@raycast/api";
|
||||
import { MutatePromise, useForm, FormValidation, useFetch } from "@raycast/utils";
|
||||
import { Page, UniversalInboxPreferences } from "../types";
|
||||
import { Notification } from "../notification";
|
||||
import { TaskPriority } from "../task";
|
||||
import { handleErrors } from "../api";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { useState } from "react";
|
||||
import fetch from "node-fetch";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
interface CreateTaskFromNotificationProps {
|
||||
notification: Notification;
|
||||
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||
}
|
||||
|
||||
interface TaskCreationFormValues {
|
||||
title: string;
|
||||
project: string;
|
||||
dueAt: Date | null;
|
||||
priority: string;
|
||||
}
|
||||
|
||||
interface ProjectSummary {
|
||||
name: string;
|
||||
source_id: string;
|
||||
}
|
||||
|
||||
interface TaskCreation {
|
||||
title: string;
|
||||
project: ProjectSummary;
|
||||
due_at?: { type: "DateTimeWithTz"; content: string };
|
||||
priority: TaskPriority;
|
||||
}
|
||||
|
||||
export function CreateTaskFromNotification({ notification, mutate }: CreateTaskFromNotificationProps) {
|
||||
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||
const { pop } = useNavigation();
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const { isLoading, data: projects } = useFetch<Array<ProjectSummary>>(
|
||||
`${preferences.universalInboxBaseUrl}/api/tasks/projects/search?matches=${searchText}`,
|
||||
{
|
||||
keepPreviousData: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${preferences.apiKey}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { handleSubmit, itemProps } = useForm<TaskCreationFormValues>({
|
||||
initialValues: {
|
||||
title: notification.title,
|
||||
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 taskCreation: TaskCreation = {
|
||||
title: values.title,
|
||||
project: project,
|
||||
due_at: values.dueAt ? { type: "DateTimeWithTz", content: dayjs(values.dueAt).utc().format() } : undefined,
|
||||
priority: parseInt(values.priority) as TaskPriority,
|
||||
};
|
||||
await createTaskFromNotification(taskCreation, notification, mutate);
|
||||
pop();
|
||||
},
|
||||
validation: {
|
||||
title: FormValidation.Required,
|
||||
project: FormValidation.Required,
|
||||
priority: FormValidation.Required,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Form
|
||||
navigationTitle="Create task from notification"
|
||||
actions={
|
||||
<ActionPanel>
|
||||
<Action.SubmitForm title="Create Task" icon={Icon.Calendar} onSubmit={handleSubmit} />
|
||||
</ActionPanel>
|
||||
}
|
||||
>
|
||||
<Form.TextField title="Task title" placeholder="Enter task title" {...itemProps.title} />
|
||||
<Form.Dropdown
|
||||
title="Project"
|
||||
placeholder="Search project..."
|
||||
filtering={true}
|
||||
throttle={true}
|
||||
isLoading={isLoading}
|
||||
onSearchTextChange={setSearchText}
|
||||
{...itemProps.project}
|
||||
>
|
||||
<Form.Dropdown.Item value="" title="" key={0} />
|
||||
{projects?.map((project) => {
|
||||
return <Form.Dropdown.Item title={project.name} value={project.source_id} key={project.source_id} />;
|
||||
})}
|
||||
</Form.Dropdown>
|
||||
<Form.DatePicker title="Due at" min={new Date()} type={Form.DatePicker.Type.Date} {...itemProps.dueAt} />
|
||||
<Form.Dropdown title="Priority" {...itemProps.priority}>
|
||||
<Form.Dropdown.Item title="Priority 1" value={`${TaskPriority.P1 as number}`} key={TaskPriority.P1} />
|
||||
<Form.Dropdown.Item title="Priority 2" value={`${TaskPriority.P2 as number}`} key={TaskPriority.P2} />
|
||||
<Form.Dropdown.Item title="Priority 3" value={`${TaskPriority.P3 as number}`} key={TaskPriority.P3} />
|
||||
<Form.Dropdown.Item title="Priority 4" value={`${TaskPriority.P4 as number}`} key={TaskPriority.P4} />
|
||||
</Form.Dropdown>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
async function createTaskFromNotification(
|
||||
taskCreation: TaskCreation,
|
||||
notification: Notification,
|
||||
mutate: MutatePromise<Page<Notification> | undefined>,
|
||||
) {
|
||||
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||
const toast = await showToast({ style: Toast.Style.Animated, title: "Creating task from notification" });
|
||||
try {
|
||||
await mutate(
|
||||
handleErrors(
|
||||
fetch(`${preferences.universalInboxBaseUrl}/api/notifications/${notification.id}/task`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(taskCreation),
|
||||
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 created";
|
||||
} catch (error) {
|
||||
toast.style = Toast.Style.Failure;
|
||||
toast.title = "Failed to create task from notification";
|
||||
toast.message = (error as Error).message;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
109
src/action/LinkNotificationToTask.tsx
Normal file
109
src/action/LinkNotificationToTask.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Action, ActionPanel, useNavigation, Form, Icon, getPreferenceValues, showToast, Toast } from "@raycast/api";
|
||||
import { MutatePromise, useForm, FormValidation, useFetch } from "@raycast/utils";
|
||||
import { Page, UniversalInboxPreferences } from "../types";
|
||||
import { Notification } from "../notification";
|
||||
import { Task, TaskStatus } from "../task";
|
||||
import { handleErrors } from "../api";
|
||||
import { useState } from "react";
|
||||
import fetch from "node-fetch";
|
||||
|
||||
interface LinkNotificationToTaskProps {
|
||||
notification: Notification;
|
||||
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||
}
|
||||
|
||||
interface TaskLinkFormValues {
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
export function LinkNotificationToTask({ notification, mutate }: LinkNotificationToTaskProps) {
|
||||
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||
const { pop } = useNavigation();
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const { isLoading, data: tasks } = useFetch<Array<Task>>(
|
||||
`${preferences.universalInboxBaseUrl}/api/tasks/search?matches=${searchText}`,
|
||||
{
|
||||
keepPreviousData: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${preferences.apiKey}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { handleSubmit, itemProps } = useForm<TaskLinkFormValues>({
|
||||
async onSubmit(values) {
|
||||
await linkNotificationToTask(notification, values.taskId, mutate);
|
||||
pop();
|
||||
},
|
||||
validation: {
|
||||
taskId: FormValidation.Required,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Form
|
||||
navigationTitle="Link notification with task"
|
||||
actions={
|
||||
<ActionPanel>
|
||||
<Action.SubmitForm title="Link to Task" icon={Icon.Link} onSubmit={handleSubmit} />
|
||||
</ActionPanel>
|
||||
}
|
||||
>
|
||||
<Form.Dropdown
|
||||
title="Task"
|
||||
placeholder="Search task..."
|
||||
filtering={true}
|
||||
throttle={true}
|
||||
isLoading={isLoading}
|
||||
onSearchTextChange={setSearchText}
|
||||
{...itemProps.taskId}
|
||||
>
|
||||
<Form.Dropdown.Item value="" title="" key={0} />
|
||||
{tasks?.map((task) => {
|
||||
return <Form.Dropdown.Item title={task.title} value={task.id} key={task.id} />;
|
||||
})}
|
||||
</Form.Dropdown>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
async function linkNotificationToTask(
|
||||
notification: Notification,
|
||||
taskId: string,
|
||||
mutate: MutatePromise<Page<Notification> | undefined>,
|
||||
) {
|
||||
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||
const toast = await showToast({ style: Toast.Style.Animated, title: "Linking notification to task" });
|
||||
try {
|
||||
await mutate(
|
||||
handleErrors(
|
||||
fetch(`${preferences.universalInboxBaseUrl}/api/notifications/${notification.id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status: TaskStatus.Deleted, task_id: taskId }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${preferences.apiKey}`,
|
||||
},
|
||||
}),
|
||||
),
|
||||
{
|
||||
optimisticUpdate(page) {
|
||||
if (page) {
|
||||
page.content = page.content.filter((n) => n.id !== notification.id);
|
||||
}
|
||||
console.log(`page(link): ${notification.id}`, page);
|
||||
return page;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
toast.style = Toast.Style.Success;
|
||||
toast.title = "Notification successfully linked to task";
|
||||
} catch (error) {
|
||||
toast.style = Toast.Style.Failure;
|
||||
toast.title = "Failed to link notification to task";
|
||||
toast.message = (error as Error).message;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
200
src/action/NotificationActions.tsx
Normal file
200
src/action/NotificationActions.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import { Notification, NotificationStatus, getNotificationHtmlUrl, isNotificationBuiltFromTask } from "../notification";
|
||||
import { Action, ActionPanel, Icon, getPreferenceValues, showToast, Toast } from "@raycast/api";
|
||||
import { CreateTaskFromNotification } from "./CreateTaskFromNotification";
|
||||
import { LinkNotificationToTask } from "./LinkNotificationToTask";
|
||||
import { Page, UniversalInboxPreferences } from "../types";
|
||||
import { MutatePromise } from "@raycast/utils";
|
||||
import { useMemo, ReactElement } from "react";
|
||||
import { handleErrors } from "../api";
|
||||
import { TaskStatus } from "../task";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import fetch from "node-fetch";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
dayjs.extend(utc);
|
||||
|
||||
interface NotificationActionsProps {
|
||||
notification: Notification;
|
||||
detailsTarget: ReactElement;
|
||||
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||
}
|
||||
|
||||
export function NotificationActions({ notification, detailsTarget, mutate }: NotificationActionsProps) {
|
||||
const notification_html_url = useMemo(() => {
|
||||
return getNotificationHtmlUrl(notification);
|
||||
}, [notification]);
|
||||
|
||||
return (
|
||||
<ActionPanel>
|
||||
<Action.OpenInBrowser url={notification_html_url} />
|
||||
<Action.Push title="Show Details" target={detailsTarget} />
|
||||
<Action
|
||||
title="Delete Notification"
|
||||
icon={Icon.Trash}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "d" }}
|
||||
onAction={() => deleteNotification(notification, mutate)}
|
||||
/>
|
||||
<Action
|
||||
title="Unsubscribe From Notification"
|
||||
icon={Icon.BellDisabled}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "u" }}
|
||||
onAction={() => unsubscribeFromNotification(notification, mutate)}
|
||||
/>
|
||||
<Action
|
||||
title="Snooze"
|
||||
icon={Icon.Clock}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "s" }}
|
||||
onAction={() => snoozeNotification(notification, mutate)}
|
||||
/>
|
||||
<Action.Push
|
||||
title="Create Task"
|
||||
icon={Icon.Calendar}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "p" }}
|
||||
target={<CreateTaskFromNotification notification={notification} mutate={mutate} />}
|
||||
/>
|
||||
<Action.Push
|
||||
title="Link to Task"
|
||||
icon={Icon.Link}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "l" }}
|
||||
target={<LinkNotificationToTask notification={notification} mutate={mutate} />}
|
||||
/>
|
||||
</ActionPanel>
|
||||
);
|
||||
}
|
||||
|
||||
async function deleteNotification(notification: Notification, mutate: MutatePromise<Page<Notification> | undefined>) {
|
||||
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||
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}`,
|
||||
},
|
||||
}),
|
||||
{
|
||||
optimisticUpdate(page) {
|
||||
if (page) {
|
||||
page.content = page.content.filter((n) => n.id !== notification.id);
|
||||
}
|
||||
return page;
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await mutate(
|
||||
handleErrors(
|
||||
fetch(`${preferences.universalInboxBaseUrl}/api/notifications/${notification.id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status: NotificationStatus.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;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
toast.style = Toast.Style.Success;
|
||||
toast.title = "Notification successfully deleted";
|
||||
} catch (error) {
|
||||
toast.style = Toast.Style.Failure;
|
||||
toast.title = "Failed to delete notification";
|
||||
toast.message = (error as Error).message;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function unsubscribeFromNotification(
|
||||
notification: Notification,
|
||||
mutate: MutatePromise<Page<Notification> | undefined>,
|
||||
) {
|
||||
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||
const toast = await showToast({ style: Toast.Style.Animated, title: "Unsubscribing from notification" });
|
||||
try {
|
||||
await mutate(
|
||||
handleErrors(
|
||||
fetch(`${preferences.universalInboxBaseUrl}/api/notifications/${notification.id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status: NotificationStatus.Unsubscribed }),
|
||||
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 = "Notification successfully unsubscribed";
|
||||
} catch (error) {
|
||||
toast.style = Toast.Style.Failure;
|
||||
toast.title = "Failed to unsubscribe from notification";
|
||||
toast.message = (error as Error).message;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function snoozeNotification(notification: Notification, mutate: MutatePromise<Page<Notification> | undefined>) {
|
||||
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||
const toast = await showToast({ style: Toast.Style.Animated, title: "Snoozing notification" });
|
||||
try {
|
||||
const snoozeTime = computeSnoozedUntil(new Date(), 1, 6);
|
||||
await mutate(
|
||||
handleErrors(
|
||||
fetch(`${preferences.universalInboxBaseUrl}/api/notifications/${notification.id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ snoozed_until: snoozeTime }),
|
||||
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 = "Notification successfully snoozed";
|
||||
} catch (error) {
|
||||
toast.style = Toast.Style.Failure;
|
||||
toast.title = "Failed to snooze notification";
|
||||
toast.message = (error as Error).message;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function computeSnoozedUntil(fromDate: Date, daysOffset: number, resetHour: number): Date {
|
||||
const result = dayjs(fromDate)
|
||||
.utc()
|
||||
.add(fromDate.getHours() < resetHour ? daysOffset - 1 : daysOffset, "day");
|
||||
return result.hour(resetHour).minute(0).second(0).millisecond(0).toDate();
|
||||
}
|
||||
63
src/action/NotificationTaskActions.tsx
Normal file
63
src/action/NotificationTaskActions.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Notification, getNotificationHtmlUrl } from "../notification";
|
||||
import { Action, ActionPanel, Icon } from "@raycast/api";
|
||||
import { MutatePromise } from "@raycast/utils";
|
||||
import { useMemo, ReactElement } from "react";
|
||||
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}`);
|
||||
}
|
||||
|
||||
interface NotificationTaskActionsProps {
|
||||
notification: Notification;
|
||||
detailsTarget: ReactElement;
|
||||
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||
}
|
||||
export function NotificationTaskActions({ notification, detailsTarget }: NotificationTaskActionsProps) {
|
||||
const notification_html_url = useMemo(() => {
|
||||
return getNotificationHtmlUrl(notification);
|
||||
}, [notification]);
|
||||
|
||||
return (
|
||||
<ActionPanel>
|
||||
<Action.OpenInBrowser url={notification_html_url} />
|
||||
<Action.Push title="Show Details" target={detailsTarget} />
|
||||
<Action
|
||||
title="Delete Notification"
|
||||
icon={Icon.Trash}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "d" }}
|
||||
onAction={() => deleteNotification(notification)}
|
||||
/>
|
||||
<Action
|
||||
title="Unsubscribe From Notification"
|
||||
icon={Icon.BellDisabled}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "u" }}
|
||||
onAction={() => unsubscribeFromNotification(notification)}
|
||||
/>
|
||||
<Action
|
||||
title="Snooze"
|
||||
icon={Icon.Clock}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "s" }}
|
||||
onAction={() => snoozeNotification(notification)}
|
||||
/>
|
||||
<Action
|
||||
title="Complete Task"
|
||||
icon={Icon.Calendar}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "c" }}
|
||||
onAction={() => completeTask(notification)}
|
||||
/>
|
||||
</ActionPanel>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user