Skip to content

Commit

Permalink
Merge pull request #50 from richardguerre/shortcuts
Browse files Browse the repository at this point in the history
Shortcuts
  • Loading branch information
richardguerre authored Nov 3, 2024
2 parents 9c91ed1 + 566398d commit c885220
Show file tree
Hide file tree
Showing 28 changed files with 1,124 additions and 295 deletions.
2 changes: 0 additions & 2 deletions apps/mobile-pwa/src/getPlugin/getPluginOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import {
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuTrigger,
Expand Down Expand Up @@ -167,7 +166,6 @@ export const getPluginOptions = (slug: string) => ({
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
Dialog,
DialogContent,
DialogDescription,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "Shortcut" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMPTZ NOT NULL,
"slug" TEXT NOT NULL,
"pluginSlug" TEXT NOT NULL,
"elementId" TEXT NOT NULL,
"trigger" TEXT[],
"enabled" BOOLEAN NOT NULL DEFAULT true,

CONSTRAINT "Shortcut_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "Shortcut_slug_pluginSlug_key" ON "Shortcut"("slug", "pluginSlug");
21 changes: 21 additions & 0 deletions apps/server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,27 @@ model Template {
routineStepId Int?
}

model Shortcut {
/// The id of the shortcut
id Int @id @default(autoincrement())
/// The date and time of creation of the shortcut
createdAt DateTime @default(now()) @db.Timestamptz()
/// The date and time the shortcut was last updated
updatedAt DateTime @updatedAt @db.Timestamptz()
/// The slug of the shortcut
slug String
/// The plugin slug of the shortcut
pluginSlug String
/// Focused element type
elementId String
/// How the shortcut is triggered (see type ShortcutTrigger in apps/web for more info)
trigger String[]
/// Whether the shortcut is enabled. This can be used to pause a shortcut.
enabled Boolean @default(true)
@@unique([slug, pluginSlug], name: "slug_pluginSlug_unique")
}

enum RepetitionPattern {
MONDAY
TUESDAY
Expand Down
11 changes: 10 additions & 1 deletion apps/server/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ await prisma.day.create({
data: [
{
status: "TODO",
title: "Make a new friend",
title: "Meet a friend",
durationInMinutes: 120,
itemId: item.id,
},
Expand Down Expand Up @@ -210,4 +210,13 @@ await prisma.routine.create({
},
});

await prisma.shortcut.create({
data: {
pluginSlug: "flow",
slug: "create-task",
elementId: "Day",
trigger: ["c"],
},
});

console.log("✅ Seeding complete!");
6 changes: 6 additions & 0 deletions apps/server/src/graphql/PrismaFilters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ export const IntFilter = builder.prismaFilter("Int", {
ops: ["equals", "lt", "lte", "gt", "gte", "in", "not", "notIn"],
});

export const StringFilter = builder.prismaFilter("String", {
name: "PrismaStringFilter",
description: "Filter input of String",
ops: ["equals", "in", "not", "notIn", "contains", "startsWith", "endsWith"],
});

export const JsonFilter = builder.prismaFilter("JSON", {
name: "PrismaJsonFilter",
description: "Filter input of JSON",
Expand Down
113 changes: 113 additions & 0 deletions apps/server/src/graphql/Shortcut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { prisma } from "../utils/prisma";
import { builder, u } from "./builder";
import { JsonFilter, StringFilter } from "./PrismaFilters";

export const ShortcutType = builder.prismaNode("Shortcut", {
id: { field: "id" },
fields: (t) => ({
createdAt: t.expose("createdAt", { type: "DateTime" }),
updatedAt: t.expose("updatedAt", { type: "DateTime" }),
slug: t.exposeString("slug"),
pluginSlug: t.exposeString("pluginSlug"),
enabled: t.exposeBoolean("enabled"),
elementId: t.exposeString("elementId"),
trigger: t.exposeStringList("trigger"),
}),
});

export const ShortcutWhereInputType = builder.prismaWhere("Shortcut", {
fields: {
pluginSlug: StringFilter,
slug: StringFilter,
trigger: JsonFilter,
AND: true,
OR: true,
NOT: true,
},
});

export const ShortcutOrderByType = builder.prismaOrderBy("Shortcut", {
fields: {
slug: true,
createdAt: true,
updatedAt: true,
},
});

builder.queryField("shortcuts", (t) =>
t.prismaConnection({
type: "Shortcut",
cursor: "id",
description: "Get keyboard shortcuts.",
args: {
where: t.arg({ type: ShortcutWhereInputType, required: false }),
orderBy: t.arg({ type: ShortcutOrderByType, required: false }),
},
resolve: async (query, _, args) => {
return prisma.shortcut.findMany({
...query,
where: args.where ?? undefined,
orderBy: args.orderBy ?? undefined,
});
},
}),
);

builder.mutationField("upsertShortcut", (t) =>
t.prismaFieldWithInput({
type: "Shortcut",
description: "Create a new shortcut.",
input: {
slug: t.input.string({ required: true }),
pluginSlug: t.input.string({ required: true }),
elementId: t.input.string({ required: true }),
trigger: t.input.stringList({ required: true }),
enabled: t.input.boolean({ required: false }),
},
resolve: async (_, __, args) => {
return prisma.shortcut.upsert({
where: {
slug_pluginSlug_unique: { slug: args.input.slug, pluginSlug: args.input.pluginSlug },
},
create: {
slug: args.input.slug,
pluginSlug: args.input.pluginSlug,
elementId: args.input.elementId,
trigger: args.input.trigger,
},
update: {
trigger: args.input.trigger,
enabled: u(args.input.enabled),
},
});
},
}),
);

builder.mutationField("disableShortcut", (t) =>
t.prismaField({
type: "Shortcut",
description: "Disable a shortcut.",
args: { id: t.arg.globalID({ required: true }) },
resolve: async (_, __, args) => {
return prisma.shortcut.update({
where: { id: parseInt(args.id.id) },
data: { enabled: false },
});
},
}),
);

builder.mutationField("enableShortcut", (t) =>
t.prismaField({
type: "Shortcut",
description: "Enable a shortcut.",
args: { id: t.arg.globalID({ required: true }) },
resolve: async (_, __, args) => {
return prisma.shortcut.update({
where: { id: parseInt(args.id.id) },
data: { enabled: true },
});
},
}),
);
83 changes: 83 additions & 0 deletions apps/server/src/graphql/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,89 @@ builder.mutationField("createTask", (t) =>
}),
);

builder.mutationField("cloneTask", (t) =>
t.prismaFieldWithInput({
type: [DayType],
description: `Clone an existing task into a new day.
It will clone the task, taskPluginDatas linked to it and link to the same item if there was one to the original task. The original task will be left intact.
If the new task is for today, it will have the same status as the original task.
If the new task is in the past, it will have the status \`DONE\` and the completedAt set to the end of the day.
If the new task is in the future, it will have the status \`TODO\` and the completedAt set to null.
Returns all the days that were updated (as a list in chronological order).`,
input: {
id: t.input.globalID({ required: true, description: "The Relay ID of the task to clone." }),
date: t.input.field({
type: "Date",
required: true,
description: "The date to clone the task into.",
}),
newTasksOrder: t.input.globalIDList({
required: true,
description: "The new order of the tasks in the day the task is cloned into.",
}),
},
resolve: async (query, _, args) => {
const newDate = args.input.date;
const task = await prisma.task.findUnique({
where: { id: parseInt(args.input.id.id) },
include: { day: { select: { date: true, tasksOrder: true } }, pluginDatas: true },
});
if (!task) {
throw new GraphQLError(`Task with ID ${args.input.id.id} not found.`, {
extensions: {
code: "TASK_NOT_FOUND",
userFriendlyMessage:
"The task was not found. Please try refreshing the page and try again.",
},
});
}
await prisma.$transaction(async (tx) => {
const newTasksOrder = args.input.newTasksOrder.map((id) => parseInt(id.id));
const isInPast = dayjs().startOf("day").isAfter(newDate);
const isToday = dayjs().isSame(newDate, "day");
const isInFuture = dayjs().endOf("day").isBefore(newDate);

await tx.task.create({
data: {
title: task.title,
status: isToday ? task.status : isInPast ? "DONE" : isInFuture ? "TODO" : "TODO",
completedAt: isInPast ? dayjs(newDate).endOf("day").toDate() : null,
durationInMinutes: task.durationInMinutes,
day: { connectOrCreate: { where: { date: newDate }, create: { date: newDate } } },
...(task.itemId ? { item: { connect: { id: task.itemId } } } : {}),
pluginDatas: {
createMany: {
data: task.pluginDatas.map((pluginData) => ({
pluginSlug: pluginData.pluginSlug,
originalId: pluginData.originalId,
min: pluginData.min ?? {},
full: pluginData.full ?? {},
})),
},
},
},
});

// update just one day as they are the same
await tx.day.update({
...query,
where: { date: newDate },
data: { tasksOrder: { set: newTasksOrder } },
});
});

return prisma.day.findMany({
...query,
where: { date: { in: [newDate, task.date] } },
orderBy: { date: "asc" },
});
},
}),
);

const TaskPluginDataInput = builder.inputType("TaskPluginDataInput", {
fields: (t) => ({
pluginSlug: t.string({ required: true }),
Expand Down
2 changes: 0 additions & 2 deletions apps/server/src/graphql/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,13 @@ export const builder = new SchemaBuilder<{
smartSubscriptions: {
subscribe: async ($name, context, callback) => {
const name = $name as PubSubKeys;
console.log("subscribing", name);
if (!context.subscriptions[name]) context.subscriptions[name] = pubsub.subscribe(name);
for await (const data of context.subscriptions[name]) {
callback(undefined, data);
}
},
unsubscribe: ($name, context) => {
const name = $name as PubSubKeys;
console.log("unsubscribing", name);
context.subscriptions[name]?.return?.();
delete context.subscriptions[name];
},
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/graphql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import "./TaskPluginData";
import "./TaskTag";
import "./Util.ts";
import "./Plugin";
import "./Shortcut.ts";
import { env } from "../env";

export const schema = builder.toSchema();
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/utils/getPluginOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export const getPluginOptions = (pluginSlug: string) => {
routine: prisma.routine,
routineStep: prisma.routineStep,
template: prisma.template,
shortcut: prisma.shortcut,
},
/**
* A key-value store plugins can use to store data, user preferences, settings, configurations, etc.
Expand Down
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
},
"devDependencies": {
"@swc/plugin-relay": "^1.5.36",
"@types/mousetrap": "^1.6.15",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"@vitejs/plugin-react": "4.1.0",
Expand Down
Loading

0 comments on commit c885220

Please sign in to comment.