Skip to content

Commit

Permalink
feat: Consolidate quest editing UI, add formFields table (#315)
Browse files Browse the repository at this point in the history
  • Loading branch information
evadecker authored Dec 28, 2024
1 parent c0ce152 commit d362596
Show file tree
Hide file tree
Showing 52 changed files with 1,400 additions and 1,309 deletions.
5 changes: 5 additions & 0 deletions .changeset/selfish-waves-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"namesake": minor
---

Allow editing all quest details directly from quest page
2 changes: 2 additions & 0 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type * as documents from "../documents.js";
import type * as errors from "../errors.js";
import type * as faqTopics from "../faqTopics.js";
import type * as faqs from "../faqs.js";
import type * as formFields from "../formFields.js";
import type * as formPages from "../formPages.js";
import type * as forms from "../forms.js";
import type * as helpers from "../helpers.js";
Expand Down Expand Up @@ -47,6 +48,7 @@ declare const fullApi: ApiFromModules<{
errors: typeof errors;
faqTopics: typeof faqTopics;
faqs: typeof faqs;
formFields: typeof formFields;
formPages: typeof formPages;
forms: typeof forms;
helpers: typeof helpers;
Expand Down
3 changes: 3 additions & 0 deletions convex/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ type FormFieldDetails = {
label: string;
description: string;
icon: LucideIcon;
hasOptions?: boolean;
};

export type FormField =
Expand All @@ -366,6 +367,7 @@ export const FORM_FIELDS: Record<FormField, FormFieldDetails> = {
description:
"A group of checkboxes which allows a user to select one or more options.",
icon: ListChecks,
hasOptions: true,
},
email: {
label: "Email",
Expand All @@ -392,6 +394,7 @@ export const FORM_FIELDS: Record<FormField, FormFieldDetails> = {
description:
"A group of radio buttons which allows a user to select one option.",
icon: LayoutList,
hasOptions: true,
},
shortText: {
label: "Short Text",
Expand Down
6 changes: 0 additions & 6 deletions convex/documents.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { userMutation } from "./helpers";
import { jurisdiction } from "./validators";

// TODO: Add `returns` value validation
// https://docs.convex.dev/functions/validation

export const getAll = query({
args: {},
Expand Down Expand Up @@ -82,15 +78,13 @@ export const create = userMutation({
args: {
title: v.string(),
code: v.optional(v.string()),
jurisdiction: jurisdiction,
file: v.optional(v.id("_storage")),
questId: v.id("quests"),
},
handler: async (ctx, args) => {
return await ctx.db.insert("documents", {
title: args.title,
code: args.code,
jurisdiction: args.jurisdiction,
file: args.file,
questId: args.questId,
creationUser: ctx.userId,
Expand Down
215 changes: 215 additions & 0 deletions convex/formFields.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { convexTest } from "convex-test";
import { describe, expect, it } from "vitest";
import { api } from "./_generated/api";
import schema from "./schema";
import { modules } from "./test.setup";

describe("formFields", () => {
describe("getById", () => {
it("should return a form field by its ID", async () => {
const t = convexTest(schema, modules);

const userId = await t.run(async (ctx) => {
return await ctx.db.insert("users", {
email: "test@example.com",
role: "user",
});
});

const asUser = t.withIdentity({ subject: userId });

const fieldId = await t.run(async (ctx) => {
return await ctx.db.insert("formFields", {
type: "shortText",
label: "First Name",
name: "firstName",
required: true,
});
});

const field = await asUser.query(api.formFields.getById, { fieldId });
expect(field).toBeTruthy();
expect(field?.label).toBe("First Name");
expect(field?.name).toBe("firstName");
expect(field?.type).toBe("shortText");
expect(field?.required).toBe(true);
});
});

describe("getByIds", () => {
it("should return multiple form fields by their IDs", async () => {
const t = convexTest(schema, modules);

const userId = await t.run(async (ctx) => {
return await ctx.db.insert("users", {
email: "test@example.com",
role: "user",
});
});

const asUser = t.withIdentity({ subject: userId });

const fieldIds = await t.run(async (ctx) => {
const field1Id = await ctx.db.insert("formFields", {
type: "shortText",
label: "First Name",
name: "firstName",
required: true,
});

const field2Id = await ctx.db.insert("formFields", {
type: "email",
label: "Email",
name: "email",
required: true,
});

return [field1Id, field2Id];
});

const fields = await asUser.query(api.formFields.getByIds, { fieldIds });
expect(fields).toHaveLength(2);
expect(fields.map((f) => f.label)).toContain("First Name");
expect(fields.map((f) => f.label)).toContain("Email");
});
});

describe("getAll", () => {
it("should return all form fields", async () => {
const t = convexTest(schema, modules);

const userId = await t.run(async (ctx) => {
return await ctx.db.insert("users", {
email: "test@example.com",
role: "user",
});
});

const asUser = t.withIdentity({ subject: userId });

await t.run(async (ctx) => {
await ctx.db.insert("formFields", {
type: "shortText",
label: "First Name",
name: "firstName",
required: true,
});

await ctx.db.insert("formFields", {
type: "email",
label: "Email",
name: "email",
required: true,
});
});

const fields = await asUser.query(api.formFields.getAll);
expect(fields.length).toBeGreaterThanOrEqual(2);
expect(fields.some((f) => f.label === "First Name")).toBeTruthy();
expect(fields.some((f) => f.label === "Email")).toBeTruthy();
});
});

describe("create", () => {
it("should create a new form field", async () => {
const t = convexTest(schema, modules);

const userId = await t.run(async (ctx) => {
return await ctx.db.insert("users", {
email: "test@example.com",
role: "user",
});
});

const asUser = t.withIdentity({ subject: userId });

const fieldId = await asUser.mutation(api.formFields.create, {
type: "shortText",
label: "Phone Number",
name: "phoneNumber",
required: false,
});

await t.run(async (ctx) => {
const field = await ctx.db.get(fieldId);
expect(field).toBeTruthy();
expect(field?.label).toBe("Phone Number");
expect(field?.name).toBe("phoneNumber");
expect(field?.type).toBe("shortText");
expect(field?.required).toBe(false);
});
});
});

describe("update", () => {
it("should update an existing form field", async () => {
const t = convexTest(schema, modules);

const userId = await t.run(async (ctx) => {
return await ctx.db.insert("users", {
email: "test@example.com",
role: "user",
});
});

const asUser = t.withIdentity({ subject: userId });

const fieldId = await t.run(async (ctx) => {
return await ctx.db.insert("formFields", {
type: "shortText",
label: "Original Label",
name: "originalName",
required: false,
});
});

await asUser.mutation(api.formFields.update, {
fieldId,
type: "email",
label: "Updated Label",
name: "updatedName",
required: true,
});

await t.run(async (ctx) => {
const field = await ctx.db.get(fieldId);
expect(field).toBeTruthy();
expect(field?.label).toBe("Updated Label");
expect(field?.name).toBe("updatedName");
expect(field?.type).toBe("email");
expect(field?.required).toBe(true);
});
});
});

describe("remove", () => {
it("should remove a form field", async () => {
const t = convexTest(schema, modules);

const userId = await t.run(async (ctx) => {
return await ctx.db.insert("users", {
email: "test@example.com",
role: "user",
});
});

const asUser = t.withIdentity({ subject: userId });

const fieldId = await t.run(async (ctx) => {
return await ctx.db.insert("formFields", {
type: "shortText",
label: "To Be Deleted",
name: "toBeDeleted",
required: false,
});
});

await asUser.mutation(api.formFields.remove, { fieldId });

await t.run(async (ctx) => {
const field = await ctx.db.get(fieldId);
expect(field).toBeNull();
});
});
});
});
69 changes: 69 additions & 0 deletions convex/formFields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { v } from "convex/values";
import { query } from "./_generated/server";
import { userMutation } from "./helpers";
import { formField } from "./validators";

export const getById = query({
args: { fieldId: v.id("formFields") },
handler: async (ctx, { fieldId }) => {
return await ctx.db.get(fieldId);
},
});

export const getByIds = query({
args: { fieldIds: v.array(v.id("formFields")) },
handler: async (ctx, { fieldIds }) => {
return await ctx.db
.query("formFields")
.filter((q) => fieldIds.some((id) => q.eq(q.field("_id"), id)))
.collect();
},
});

export const getAll = query({
handler: async (ctx) => {
return await ctx.db.query("formFields").collect();
},
});

export const create = userMutation({
args: {
type: formField,
label: v.string(),
name: v.string(),
required: v.optional(v.boolean()),
},
handler: async (ctx, args) => {
return await ctx.db.insert("formFields", {
type: args.type,
label: args.label,
name: args.name,
required: args.required,
});
},
});

export const update = userMutation({
args: {
fieldId: v.id("formFields"),
type: formField,
label: v.string(),
name: v.string(),
required: v.optional(v.boolean()),
},
handler: async (ctx, args) => {
return await ctx.db.patch(args.fieldId, {
type: args.type,
label: args.label,
name: args.name,
required: args.required,
});
},
});

export const remove = userMutation({
args: { fieldId: v.id("formFields") },
handler: async (ctx, { fieldId }) => {
return await ctx.db.delete(fieldId);
},
});
Loading

0 comments on commit d362596

Please sign in to comment.