From 7e96f51ff52eb77c198504f85ac14c5162950ed4 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Mon, 24 Feb 2025 19:38:27 -0600 Subject: [PATCH 01/38] Add ranks to form elements --- core/package.json | 2 + .../20250213201642_add_rank/migration.sql | 50 +++++++++++++++++++ core/prisma/schema/schema.dbml | 1 + core/prisma/schema/schema.prisma | 1 + pnpm-lock.yaml | 17 ++++++- 5 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 core/prisma/migrations/20250213201642_add_rank/migration.sql diff --git a/core/package.json b/core/package.json index 337f94f78..01a85178b 100644 --- a/core/package.json +++ b/core/package.json @@ -105,6 +105,7 @@ "lucia": "^3.2.2", "lucide-react": "^0.469.0", "micromark-extension-directive": "^3.0.0", + "mudder": "^2.1.1", "next": "catalog:", "next-connect": "^1.0.0", "nodemailer": "^6.9.5", @@ -158,6 +159,7 @@ "@types/jsonwebtoken": "^9.0.2", "@types/lodash.partition": "^4.6.9", "@types/mdast": "^4.0.4", + "@types/mudder": "^2.1.3", "@types/node": "catalog:", "@types/nodemailer": "^6.4.9", "@types/pg": "^8.11.6", diff --git a/core/prisma/migrations/20250213201642_add_rank/migration.sql b/core/prisma/migrations/20250213201642_add_rank/migration.sql new file mode 100644 index 000000000..88ee3da0e --- /dev/null +++ b/core/prisma/migrations/20250213201642_add_rank/migration.sql @@ -0,0 +1,50 @@ +BEGIN; + ALTER TABLE "form_elements" ADD COLUMN "rank" TEXT; + + /* Convert numeric order to alphanumeric (base62) string + * + * These values are the first 100 of a 1000 item division of the 0-z space, generated with mudder: + * mudder.base62.mudder('','', 100, undefined, 1000).map((rank, index) => `(${index}, '${rank}')`).join(", ") + * + * We intentionally generate ranks closer to the beginning of the space because new form elements + * are always added to the end. + */ + WITH "mudder_ranks" AS ( + SELECT "m".* FROM ( + VALUES (0, '03'), (1, '07'), (2, '0B'), (3, '0F'), (4, '0J'), (5, '0N'), (6, '0Q'), (7, '0U'), (8, '0Y'), + (9, '0c'), (10, '0g'), (11, '0k'), (12, '0n'), (13, '0r'), (14, '0v'), (15, '0z'), (16, '1'), (17, '17'), + (18, '1B'), (19, '1E'), (20, '1I'), (21, '1M'), (22, '1Q'), (23, '1U'), (24, '1Y'), (25, '1b'), (26, '1f'), + (27, '1j'), (28, '1n'), (29, '1r'), (30, '1v'), (31, '1z'), (32, '2'), (33, '26'), (34, '2A'), (35, '2E'), + (36, '2I'), (37, '2M'), (38, '2P'), (39, '2T'), (40, '2X'), (41, '2b'), (42, '2f'), (43, '2j'), (44, '2m'), + (45, '2q'), (46, '2u'), (47, '2y'), (48, '3'), (49, '36'), (50, '3A'), (51, '3D'), (52, '3H'), (53, '3L'), + (54, '3P'), (55, '3T'), (56, '3X'), (57, '3a'), (58, '3e'), (59, '3i'), (60, '3m'), (61, '3q'), (62, '3u'), + (63, '3y'), (64, '4'), (65, '45'), (66, '49'), (67, '4D'), (68, '4H'), (69, '4L'), (70, '4O'), (71, '4S'), + (72, '4W'), (73, '4a'), (74, '4e'), (75, '4i'), (76, '4l'), (77, '4p'), (78, '4t'), (79, '4x'), (80, '5'), + (81, '55'), (82, '59'), (83, '5C'), (84, '5G'), (85, '5K'), (86, '5O'), (87, '5S'), (88, '5W'), (89, '5Z'), + (90, '5d'), (91, '5h'), (92, '5l'), (93, '5p'), (94, '5t'), (95, '5x'), (96, '6'), (97, '64'), (98, '68'), + (99, '6C') + ) AS "m"("index", "rank") + ) + UPDATE "form_elements" + SET "rank" = "mudder_ranks"."rank" + FROM "mudder_ranks" + WHERE + "form_elements"."order" IS NOT NULL + AND "form_elements"."order" = "mudder_ranks"."index"; + + -- Set a rank for submit buttons, all the way at the end (z, zz, zzz etc.) + WITH "buttons" AS ( + -- Assign a somewhat arbitrary numeric order to the buttons + -- Since some have order = null, the non-null ordered ones will come first + SELECT "id", "formId", ROW_NUMBER() OVER (PARTITION BY "formId" ORDER BY "order") AS "rank" + FROM "form_elements" + WHERE "form_elements"."type" = 'button'::"ElementType" + ) + UPDATE "form_elements" + SET "rank" = REPEAT('z'::text, "buttons"."rank"::int) + FROM "buttons" + WHERE "form_elements"."type" = 'button'::"ElementType" + AND "form_elements"."id" = "buttons"."id"; + + ALTER TABLE "form_elements" ALTER COLUMN "rank" SET NOT NULL; +COMMIT; \ No newline at end of file diff --git a/core/prisma/schema/schema.dbml b/core/prisma/schema/schema.dbml index a649027fd..d2cb5a1fc 100644 --- a/core/prisma/schema/schema.dbml +++ b/core/prisma/schema/schema.dbml @@ -384,6 +384,7 @@ Table form_elements { field pub_fields formId String [not null] order Int + rank String [not null] label String element StructuralFormElement component InputComponent diff --git a/core/prisma/schema/schema.prisma b/core/prisma/schema/schema.prisma index 55db04e39..38ed2c3db 100644 --- a/core/prisma/schema/schema.prisma +++ b/core/prisma/schema/schema.prisma @@ -532,6 +532,7 @@ model FormElement { field PubField? @relation(fields: [fieldId], references: [id], onDelete: Cascade) formId String order Int? + rank String // label is only used by elements with type: ElementType.button. Pubfield inputs put everything in config label String? element StructuralFormElement? diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3731f983..52c4c2f82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -385,6 +385,9 @@ importers: micromark-extension-directive: specifier: ^3.0.0 version: 3.0.1 + mudder: + specifier: ^2.1.1 + version: 2.1.1 next: specifier: 'catalog:' version: 15.1.4(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.48.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -539,6 +542,9 @@ importers: '@types/mdast': specifier: ^4.0.4 version: 4.0.4 + '@types/mudder': + specifier: ^2.1.3 + version: 2.1.3 '@types/node': specifier: 'catalog:' version: 20.16.5 @@ -6231,6 +6237,9 @@ packages: '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + '@types/mudder@2.1.3': + resolution: {integrity: sha512-t4Sacwmm/xj5Ml74q8cHhwTNJPL5I30hG5iqFrlpIOla1ygdDMdzaYZzQ0+OT56kHlP5Ar+G6XKLWxgNBjq+MQ==} + '@types/mysql@2.15.26': resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} @@ -8988,7 +8997,6 @@ packages: resolution: {integrity: sha512-t0etAxTUk1w5MYdNOkZBZ8rvYYN5iL+2dHCCx/DpkFm/bW28M6y5nUS83D4XdZiHy35Fpaw6LBb+F88fHZnVCw==} engines: {node: '>=8.17.0'} hasBin: true - bundledDependencies: [] jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} @@ -9522,6 +9530,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mudder@2.1.1: + resolution: {integrity: sha512-0/F//kjoRlefsazFcGxa7FAuwRNDoX3ALal7W9uOZgE9QKxKatFM1NKu3tkmxMAFvUXoIHN2b/PlIt5B+hJirQ==} + mute-stream@0.0.8: resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} @@ -18339,6 +18350,8 @@ snapshots: '@types/ms@0.7.34': {} + '@types/mudder@2.1.3': {} + '@types/mysql@2.15.26': dependencies: '@types/node': 20.17.12 @@ -22391,6 +22404,8 @@ snapshots: ms@2.1.3: {} + mudder@2.1.1: {} + mute-stream@0.0.8: {} mz@2.7.0: From 6e01fce0bb5267abd01b50709c96fd93d5229ff2 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Tue, 25 Feb 2025 00:16:58 -0600 Subject: [PATCH 02/38] Add rank to pub values --- .../migration.sql | 1 + .../migration.sql | 64 +++++++++++++++++++ core/prisma/schema/schema.dbml | 1 + core/prisma/schema/schema.prisma | 4 ++ packages/db/src/public/FormElements.ts | 5 ++ packages/db/src/public/PubValues.ts | 5 ++ packages/db/src/table-names.ts | 16 +++++ 7 files changed, 96 insertions(+) rename core/prisma/migrations/{20250213201642_add_rank => 20250213201642_add_rank_to_form_elements}/migration.sql (99%) create mode 100644 core/prisma/migrations/20250225034631_add_rank_to_pub_values/migration.sql diff --git a/core/prisma/migrations/20250213201642_add_rank/migration.sql b/core/prisma/migrations/20250213201642_add_rank_to_form_elements/migration.sql similarity index 99% rename from core/prisma/migrations/20250213201642_add_rank/migration.sql rename to core/prisma/migrations/20250213201642_add_rank_to_form_elements/migration.sql index 88ee3da0e..56b5fcc2b 100644 --- a/core/prisma/migrations/20250213201642_add_rank/migration.sql +++ b/core/prisma/migrations/20250213201642_add_rank_to_form_elements/migration.sql @@ -47,4 +47,5 @@ BEGIN; AND "form_elements"."id" = "buttons"."id"; ALTER TABLE "form_elements" ALTER COLUMN "rank" SET NOT NULL; + COMMIT; \ No newline at end of file diff --git a/core/prisma/migrations/20250225034631_add_rank_to_pub_values/migration.sql b/core/prisma/migrations/20250225034631_add_rank_to_pub_values/migration.sql new file mode 100644 index 000000000..d1a1cb280 --- /dev/null +++ b/core/prisma/migrations/20250225034631_add_rank_to_pub_values/migration.sql @@ -0,0 +1,64 @@ +ALTER TABLE "pub_values" ADD COLUMN "rank" TEXT; + +WITH "mudder_ranks" AS ( + SELECT "m".* FROM ( + VALUES (0,'01'), (1,'03'), (2,'05'), (3,'07'), (4,'09'), (5,'0B'), (6,'0D'), + (7,'0F'), (8,'0H'), (9,'0J'), (10,'0L'), (11,'0N'), (12,'0O'), (13,'0Q'), + (14,'0S'), (15,'0U'), (16,'0W'), (17,'0Y'), (18,'0a'), (19,'0c'), (20,'0e'), + (21,'0g'), (22,'0i'), (23,'0k'), (24,'0m'), (25,'0n'), (26,'0p'), (27,'0r'), + (28,'0t'), (29,'0v'), (30,'0x'), (31,'0z'), (32,'1'), (33,'13'), (34,'15'), + (35,'17'), (36,'19'), (37,'1B'), (38,'1C'), (39,'1E'), (40,'1G'), (41,'1I'), + (42,'1K'), (43,'1M'), (44,'1O'), (45,'1Q'), (46,'1S'), (47,'1U'), (48,'1W'), + (49,'1Y'), (50,'1a'), (51,'1b'), (52,'1d'), (53,'1f'), (54,'1h'), (55,'1j'), + (56,'1l'), (57,'1n'), (58,'1p'), (59,'1r'), (60,'1t'), (61,'1v'), (62,'1x'), + (63,'1z'), (64,'2'), (65,'22'), (66,'24'), (67,'26'), (68,'28'), (69,'2A'), + (70,'2C'), (71,'2E'), (72,'2G'), (73,'2I'), (74,'2K'), (75,'2M'), (76,'2N'), + (77,'2P'), (78,'2R'), (79,'2T'), (80,'2V'), (81,'2X'), (82,'2Z'), (83,'2b'), + (84,'2d'), (85,'2f'), (86,'2h'), (87,'2j'), (88,'2l'), (89,'2m'), (90,'2o'), + (91,'2q'), (92,'2s'), (93,'2u'), (94,'2w'), (95,'2y'), (96,'3'), (97,'32'), + (98,'34'), (99,'36'), (100,'38'), (101,'3A'), (102,'3B'), (103,'3D'), + (104,'3F'), (105,'3H'), (106,'3J'), (107,'3L'), (108,'3N'), (109,'3P'), + (110,'3R'), (111,'3T'), (112,'3V'), (113,'3X'), (114,'3Z'), (115,'3a'), + (116,'3c'), (117,'3e'), (118,'3g'), (119,'3i'), (120,'3k'), (121,'3m'), + (122,'3o'), (123,'3q'), (124,'3s'), (125,'3u'), (126,'3w'), (127,'3y'), + (128,'3z'), (129,'4'), (130,'43'), (131,'45'), (132,'47'), (133,'49'), + (134,'4B'), (135,'4D'), (136,'4F'), (137,'4H'), (138,'4J'), (139,'4L'), + (140,'4N'), (141,'4O'), (142,'4Q'), (143,'4S'), (144,'4U'), (145,'4W'), + (146,'4Y'), (147,'4a'), (148,'4c'), (149,'4e'), (150,'4g'), (151,'4i'), + (152,'4k'), (153,'4l'), (154,'4n'), (155,'4p'), (156,'4r'), (157,'4t'), + (158,'4v'), (159,'4x'), (160,'4z'), (161,'5'), (162,'53'), (163,'55'), + (164,'57'), (165,'59'), (166,'5A'), (167,'5C'), (168,'5E'), (169,'5G'), + (170,'5I'), (171,'5K'), (172,'5M'), (173,'5O'), (174,'5Q'), (175,'5S'), + (176,'5U'), (177,'5W'), (178,'5Y'), (179,'5Z'), (180,'5b'), (181,'5d'), + (182,'5f'), (183,'5h'), (184,'5j'), (185,'5l'), (186,'5n'), (187,'5p'), + (188,'5r'), (189,'5t'), (190,'5v'), (191,'5x'), (192,'5y'), (193,'6'), + (194,'62'), (195,'64'), (196,'66'), (197,'68'), (198,'6A'), (199,'6C') + ) AS "m"("index", "rank") +), +"related_pubs" AS ( + SELECT "pubId", "fieldId" + FROM "pub_values" + WHERE "relatedPubId" IS NOT NULL + GROUP BY "pubId", "fieldId" + HAVING COUNT("pubId") > 1 +), +"row_numbers" AS ( + SELECT + "pub_values"."id", + ROW_NUMBER() OVER ( + PARTITION BY "pub_values"."pubId", "pub_values"."fieldId" + ORDER BY "pub_values"."updatedAt" + ) as "r" + FROM "pub_values" + JOIN "related_pubs" ON + "related_pubs"."pubId" = "pub_values"."pubId" + AND "related_pubs"."fieldId" = "pub_values"."fieldId" +) +UPDATE "pub_values" +SET + "rank" = "mudder_ranks"."rank", + "lastModifiedBy" = 'system|' || FLOOR(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000) +FROM "mudder_ranks", "row_numbers" +WHERE + "mudder_ranks"."index" = "row_numbers"."r" + AND "row_numbers"."id" = "pub_values"."id"; \ No newline at end of file diff --git a/core/prisma/schema/schema.dbml b/core/prisma/schema/schema.dbml index d2cb5a1fc..d42a960cf 100644 --- a/core/prisma/schema/schema.dbml +++ b/core/prisma/schema/schema.dbml @@ -149,6 +149,7 @@ Table pub_values { updatedAt DateTime [default: `now()`, not null] relatedPub pubs relatedPubId String + rank String } Table pub_types { diff --git a/core/prisma/schema/schema.prisma b/core/prisma/schema/schema.prisma index 38ed2c3db..f99071877 100644 --- a/core/prisma/schema/schema.prisma +++ b/core/prisma/schema/schema.prisma @@ -207,6 +207,10 @@ model PubValue { relatedPub Pub? @relation(fields: [relatedPubId], references: [id], onDelete: Cascade, name: "related_pub") relatedPubId String? + // Rank is only used for ordering multiple related pubs for a single field, not for ordering + // values within a pub + rank String? + @@map(name: "pub_values") } diff --git a/packages/db/src/public/FormElements.ts b/packages/db/src/public/FormElements.ts index e0859337a..af7a3d8e1 100644 --- a/packages/db/src/public/FormElements.ts +++ b/packages/db/src/public/FormElements.ts @@ -54,6 +54,8 @@ export interface FormElementsTable { createdAt: ColumnType; updatedAt: ColumnType; + + rank: ColumnType; } export type FormElements = Selectable; @@ -79,6 +81,7 @@ export const formElementsSchema = z.object({ config: z.unknown().nullable(), createdAt: z.date(), updatedAt: z.date(), + rank: z.string(), }); export const formElementsInitializerSchema = z.object({ @@ -96,6 +99,7 @@ export const formElementsInitializerSchema = z.object({ config: z.unknown().optional().nullable(), createdAt: z.date().optional(), updatedAt: z.date().optional(), + rank: z.string(), }); export const formElementsMutatorSchema = z.object({ @@ -113,4 +117,5 @@ export const formElementsMutatorSchema = z.object({ config: z.unknown().optional().nullable(), createdAt: z.date().optional(), updatedAt: z.date().optional(), + rank: z.string().optional(), }); diff --git a/packages/db/src/public/PubValues.ts b/packages/db/src/public/PubValues.ts index 77fb38a05..853e765f3 100644 --- a/packages/db/src/public/PubValues.ts +++ b/packages/db/src/public/PubValues.ts @@ -32,6 +32,8 @@ export interface PubValuesTable { relatedPubId: ColumnType; lastModifiedBy: ColumnType; + + rank: ColumnType; } export type PubValues = Selectable; @@ -51,6 +53,7 @@ export const pubValuesSchema = z.object({ updatedAt: z.date(), relatedPubId: pubsIdSchema.nullable(), lastModifiedBy: modifiedByTypeSchema, + rank: z.string().nullable(), }); export const pubValuesInitializerSchema = z.object({ @@ -62,6 +65,7 @@ export const pubValuesInitializerSchema = z.object({ updatedAt: z.date().optional(), relatedPubId: pubsIdSchema.optional().nullable(), lastModifiedBy: modifiedByTypeSchema, + rank: z.string().optional().nullable(), }); export const pubValuesMutatorSchema = z.object({ @@ -73,4 +77,5 @@ export const pubValuesMutatorSchema = z.object({ updatedAt: z.date().optional(), relatedPubId: pubsIdSchema.optional().nullable(), lastModifiedBy: modifiedByTypeSchema.optional(), + rank: z.string().optional().nullable(), }); diff --git a/packages/db/src/table-names.ts b/packages/db/src/table-names.ts index 86242bf3d..f76be76e0 100644 --- a/packages/db/src/table-names.ts +++ b/packages/db/src/table-names.ts @@ -917,6 +917,14 @@ export const databaseTables = [ isAutoIncrementing: false, hasDefaultValue: true, }, + { + name: "rank", + dataType: "text", + dataTypeSchema: "pg_catalog", + isNullable: false, + isAutoIncrementing: false, + hasDefaultValue: false, + }, ], }, { @@ -1453,6 +1461,14 @@ export const databaseTables = [ hasDefaultValue: false, comment: "@type(LastModifiedBy, '../types', true, false, true)", }, + { + name: "rank", + dataType: "text", + dataTypeSchema: "pg_catalog", + isNullable: true, + isAutoIncrementing: false, + hasDefaultValue: false, + }, ], }, { From 027853ade6ae37239fde081eacec47790b79e27b Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Tue, 25 Feb 2025 00:19:45 -0600 Subject: [PATCH 03/38] Set rank for form elements with mudder when creating new forms --- core/app/c/[communitySlug]/forms/actions.ts | 4 +- core/app/c/[communitySlug]/types/actions.ts | 8 +- core/lib/server/form.ts | 104 +++++--------------- core/lib/server/pub.db.test.ts | 2 +- core/prisma/seed/seedCommunity.ts | 46 +++++---- 5 files changed, 62 insertions(+), 102 deletions(-) diff --git a/core/app/c/[communitySlug]/forms/actions.ts b/core/app/c/[communitySlug]/forms/actions.ts index 59574b483..17b5b49d0 100644 --- a/core/app/c/[communitySlug]/forms/actions.ts +++ b/core/app/c/[communitySlug]/forms/actions.ts @@ -7,6 +7,7 @@ import { assert } from "utils"; import { isUniqueConstraintError } from "~/kysely/errors"; import { getLoginData } from "~/lib/authentication/loginData"; +import { getPubType } from "~/lib/server"; import { autoRevalidate } from "~/lib/server/cache/autoRevalidate"; import { findCommunityBySlug } from "~/lib/server/community"; import { defineServerAction } from "~/lib/server/defineServerAction"; @@ -27,8 +28,9 @@ export const createForm = defineServerAction(async function createForm( } try { + const pubType = await getPubType(pubTypeId).executeTakeFirstOrThrow(); await autoRevalidate( - insertForm(pubTypeId, name, slug, communityId, false) + insertForm(pubType, name, slug, communityId, false) ).executeTakeFirstOrThrow(); } catch (error) { if (isUniqueConstraintError(error)) { diff --git a/core/app/c/[communitySlug]/types/actions.ts b/core/app/c/[communitySlug]/types/actions.ts index 0517658e9..b069df324 100644 --- a/core/app/c/[communitySlug]/types/actions.ts +++ b/core/app/c/[communitySlug]/types/actions.ts @@ -8,7 +8,7 @@ import { isUniqueConstraintError } from "~/kysely/errors"; import { getLoginData } from "~/lib/authentication/loginData"; import { userCan } from "~/lib/authorization/capabilities"; import { defaultFormName, defaultFormSlug } from "~/lib/form"; -import { ApiError } from "~/lib/server"; +import { ApiError, getPubType } from "~/lib/server"; import { autoRevalidate } from "~/lib/server/cache/autoRevalidate"; import { findCommunityBySlug } from "~/lib/server/community"; import { defineServerAction } from "~/lib/server/defineServerAction"; @@ -157,7 +157,7 @@ export const createPubType = defineServerAction(async function createPubType( } try { await db.transaction().execute(async (trx) => { - const pubType = await autoRevalidate( + const { id: pubTypeId } = await autoRevalidate( trx .with("newType", (db) => db @@ -180,9 +180,11 @@ export const createPubType = defineServerAction(async function createPubType( .returning("B as id") ).executeTakeFirstOrThrow(); + const pubType = await getPubType(pubTypeId).executeTakeFirstOrThrow(); + await autoRevalidate( insertForm( - pubType.id, + pubType, defaultFormName(name), defaultFormSlug(name), communityId, diff --git a/core/lib/server/form.ts b/core/lib/server/form.ts index 26998d5a7..34b8e0ad4 100644 --- a/core/lib/server/form.ts +++ b/core/lib/server/form.ts @@ -2,28 +2,21 @@ import type { QueryCreator } from "kysely"; import { sql } from "kysely"; import { jsonArrayFrom } from "kysely/helpers/postgres"; -import { componentsBySchema } from "schemas"; +import mudder from "mudder"; +import { defaultComponent } from "schemas"; -import type { - CommunitiesId, - CoreSchemaType, - FormsId, - InputComponent, - PublicSchema, - PubsId, - PubTypesId, - UsersId, -} from "db/public"; -import { AuthTokenType, ElementType, StructuralFormElement } from "db/public"; +import type { CommunitiesId, FormsId, PublicSchema, PubsId, PubTypesId, UsersId } from "db/public"; +import { AuthTokenType, ElementType, InputComponent, StructuralFormElement } from "db/public"; +import { logger } from "logger"; import type { XOR } from "../types"; +import type { GetPubTypesResult } from "./pubtype"; import type { FormElements } from "~/app/components/forms/types"; import { db } from "~/kysely/database"; import { createMagicLink } from "../authentication/createMagicLink"; import { autoCache } from "./cache/autoCache"; import { autoRevalidate } from "./cache/autoRevalidate"; import { getCommunitySlug } from "./cache/getCommunitySlug"; -import { _getPubFields } from "./pubFields"; import { getUser } from "./user"; /** @@ -215,61 +208,30 @@ export const createFormInviteLink = async (props: FormInviteLinkProps) => { return magicLink; }; -const componentsBySchemaTable = Object.entries(componentsBySchema) - .map(([schema, components]) => { - const component = components[0] - ? `'${components[0]}'::"InputComponent"` - : `null::"InputComponent"`; - return `('${schema}'::"CoreSchemaType", ${component})`; - }) - .join(", "); - export const insertForm = ( - pubTypeId: PubTypesId, + pubType: GetPubTypesResult[number], name: string, slug: string, communityId: CommunitiesId, isDefault: boolean, trx = db ) => { + logger.debug({ msg: "inserting form", pubType, name }); + const ranks = mudder.base62.mudder( + undefined, + undefined, + pubType.fields.length + 1, + undefined, + (pubType.fields.length + 1) * 20 + ); + return trx - .with("components", (db) => - // This lets us set an appropriate default component during creation by turning - // the js mapping from schemaName to InputComponent into a temporary table which - // can be used during the query. Without this, we would need to first query for - // the pubtype's fields (and their schemaNames), then determine the input - // components in js before inserting - db - .selectFrom( - sql<{ - schema: CoreSchemaType; - component: InputComponent; - }>`(values ${sql.raw(componentsBySchemaTable)})`.as<"c">( - sql`c(schema, component)` - ) - ) - .selectAll("c") - ) - .with("fields", () => - _getPubFields({ pubTypeId, communityId }) - .clearSelect() - .select((eb) => [ - eb.ref("f.id").as("fieldId"), - eb.ref("f.json", "->>").key("name").as("name"), - eb - .cast( - eb.ref("f.json", "->>").key("schemaName"), - sql.raw('"CoreSchemaType"') - ) - .as("schemaName"), - ]) - ) .with("form", (db) => db .insertInto("forms") .values({ name, - pubTypeId, + pubTypeId: pubType.id, slug, communityId, isDefault, @@ -282,31 +244,19 @@ export const insertForm = ( type: ElementType.structural, element: StructuralFormElement.p, content: '# :value{field="title"}', - order: 0, + rank: ranks[0], })) ) .insertInto("form_elements") - .columns(["fieldId", "formId", "label", "type", "order", "component"]) - .expression((eb) => - eb - .selectFrom("fields") - .innerJoin("form", (join) => join.onTrue()) - .select((eb) => [ - "fields.fieldId", - "form.id as formId", - "fields.name as label", - eb.val("pubfield").as("type"), - eb( - eb.fn.agg("ROW_NUMBER").over((o) => o.partitionBy("id")), - "+", - 1 // Offset order by 1 for the title element - ).as("order"), - eb - .selectFrom("components") - .select("component") - .whereRef("components.schema", "=", "fields.schemaName") - .as("component"), - ]) + .values((eb) => + pubType.fields.map((field, i) => ({ + fieldId: field.id, + label: field.name, + type: ElementType.pubfield, + component: defaultComponent(field.schemaName!) ?? InputComponent.relationBlock, + rank: ranks[i + 1], + formId: eb.selectFrom("form").select("form.id"), + })) ); }; export const FORM_NAME_UNIQUE_CONSTRAINT = "forms_name_communityId_key"; diff --git a/core/lib/server/pub.db.test.ts b/core/lib/server/pub.db.test.ts index a72dde2fc..22ab122f1 100644 --- a/core/lib/server/pub.db.test.ts +++ b/core/lib/server/pub.db.test.ts @@ -502,7 +502,7 @@ describe("getPubsWithRelatedValuesAndChildren", () => { expect(pub.pubType).toMatchObject({ id: pubTypes["Basic Pub"].id, - fields: Object.values(pubTypes["Basic Pub"].pubFields).map((f) => ({ + fields: Object.values(pubTypes["Basic Pub"].fields).map((f) => ({ id: f.id, slug: f.slug, })), diff --git a/core/prisma/seed/seedCommunity.ts b/core/prisma/seed/seedCommunity.ts index fb26048df..e2579caae 100644 --- a/core/prisma/seed/seedCommunity.ts +++ b/core/prisma/seed/seedCommunity.ts @@ -7,6 +7,7 @@ import type { import { faker } from "@faker-js/faker"; import { jsonArrayFrom } from "kysely/helpers/postgres"; +import mudder from "mudder"; import type { ProcessedPub } from "contracts"; import type { @@ -433,7 +434,7 @@ type PubFieldsByName = { }; type PubTypesByName = { - [K in keyof PT]: Omit & { name: K } & { pubFields: PubFieldsByName }; + [K in keyof PT]: Omit & { name: K } & { fields: PubFieldsByName }; }; type UsersBySlug = { @@ -796,25 +797,12 @@ export async function seedCommunity< .execute() : []; - await Promise.all( - createdPubTypes.map((type) => - insertForm( - type.id, - `${type.name} Editor (Default)`, - `${slugifyString(type.name)}-default-editor`, - communityId, - true, - trx - ).execute() - ) - ); - const pubTypesWithPubFieldsByName = Object.fromEntries( createdPubTypes.map((pubType) => [ pubType.name, { ...pubType, - pubFields: Object.fromEntries( + fields: Object.fromEntries( createdPubFieldToPubTypes .filter((pubFieldToPubType) => pubFieldToPubType.B === pubType.id) .map((pubFieldToPubType) => { @@ -832,6 +820,19 @@ export async function seedCommunity< ]) ) as PubTypesByName; + await Promise.all( + Object.values(pubTypesWithPubFieldsByName).map((pubType) => + insertForm( + { ...pubType, fields: Object.values(pubType.fields) }, + `${pubType.name} Editor (Default)`, + `${slugifyString(pubType.name)}-default-editor`, + communityId, + true, + trx + ).execute() + ) + ); + const userValues = await Promise.all( Object.entries(props.users ?? {}).map(async ([slug, userInfo]) => ({ slug: options?.randomSlug === false ? slug : `${slug}-${new Date().toISOString()}`, @@ -1054,8 +1055,13 @@ export async function seedCommunity< db .insertInto("form_elements") .values((eb) => - formList.flatMap(([formTitle, formInput], idx) => - formInput.elements.map((elementInput, elementIndex) => ({ + formList.flatMap(([formTitle, formInput], idx) => { + const ranks = mudder.base62.mudder( + "", + "", + formInput.elements.length + ); + return formInput.elements.map((elementInput, elementIndex) => ({ formId: eb .selectFrom("form") .select("form.id") @@ -1071,10 +1077,10 @@ export async function seedCommunity< label: elementInput.label, element: elementInput.element, component: elementInput.component, - order: elementIndex, + rank: ranks[elementIndex], config: elementInput.config, - })) - ) + })); + }) ) .returningAll() ) From 68406d17177d3bee61d7a5e1cf03949dd78e8f31 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Tue, 25 Feb 2025 14:24:21 -0600 Subject: [PATCH 04/38] Use mudder rank instead of order in form builder --- .../ElementPanel/ButtonConfigurationForm.tsx | 13 ++++++--- .../ElementPanel/SelectElement.tsx | 14 +++++---- .../components/FormBuilder/FormBuilder.tsx | 29 ++++++++++++++++--- core/app/components/FormBuilder/actions.ts | 3 +- core/app/components/FormBuilder/types.ts | 2 +- core/app/components/forms/types.ts | 6 ++-- core/app/components/pubs/PubEditor/helpers.ts | 3 +- core/lib/server/form.ts | 4 +-- 8 files changed, 52 insertions(+), 22 deletions(-) diff --git a/core/app/components/FormBuilder/ElementPanel/ButtonConfigurationForm.tsx b/core/app/components/FormBuilder/ElementPanel/ButtonConfigurationForm.tsx index 58ee072fa..ab24b55f7 100644 --- a/core/app/components/FormBuilder/ElementPanel/ButtonConfigurationForm.tsx +++ b/core/app/components/FormBuilder/ElementPanel/ButtonConfigurationForm.tsx @@ -1,5 +1,6 @@ import { useMemo } from "react"; import { zodResolver } from "@hookform/resolvers/zod"; +import mudder from "mudder"; import { useForm, useFormContext } from "react-hook-form"; import { z } from "zod"; @@ -27,7 +28,7 @@ import { ChevronDown } from "ui/icon"; import { Input } from "ui/input"; import { cn } from "utils"; -import type { ButtonElement, FormBuilderSchema } from "../types"; +import type { FormBuilderSchema } from "../types"; import { useCommunity } from "../../providers/CommunityProvider"; import { useFormBuilder } from "../FormBuilderContext"; import { ButtonOption } from "../SubmissionSettings"; @@ -48,7 +49,7 @@ export const ButtonConfigurationForm = ({ // This uses the parent's form context to get the most up to date version of 'elements' const { getValues } = useFormContext(); // Derive some initial values based on the state of the parent form when this panel was opened - const { button, buttonIndex, otherButtons, numElements } = useMemo(() => { + const { button, buttonIndex, otherButtons, numElements, elements } = useMemo(() => { const elements = getValues()["elements"]; // Because a button might not have an ID yet (if it wasn't saved to the db yet) fall back to its label as an identifier const buttonIndex = buttonIdentifier @@ -66,7 +67,7 @@ export const ButtonConfigurationForm = ({ e.elementId !== buttonIdentifier && e.label !== buttonIdentifier ); - return { button, buttonIndex, otherButtons, numElements: elements.length }; + return { button, buttonIndex, otherButtons, numElements: elements.length, elements }; }, []); const schema = z.object({ @@ -102,7 +103,11 @@ export const ButtonConfigurationForm = ({ const onSubmit = (values: z.infer) => { const index = buttonIndex === -1 ? numElements : buttonIndex; update(index, { - order: null, + rank: mudder.base62.mudder( + elements[index - 1]?.rank ?? "", + elements[index + 1]?.rank ?? "", + 1 + )[0], type: ElementType.button, elementId: button?.elementId, label: values.label, diff --git a/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx b/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx index 330913178..dcda0e3f5 100644 --- a/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx +++ b/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx @@ -1,9 +1,9 @@ +import mudder from "mudder"; import { useFormContext } from "react-hook-form"; -import { defaultComponent, SCHEMA_TYPES_WITH_ICONS } from "schemas"; +import { defaultComponent } from "schemas"; import { ElementType, StructuralFormElement } from "db/public"; import { Button } from "ui/button"; -import { Type } from "ui/icon"; import { Input } from "ui/input"; import { usePubFieldContext } from "ui/pubFields"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "ui/tabs"; @@ -18,9 +18,9 @@ export const SelectElement = ({ panelState }: { panelState: PanelState }) => { const { elementsCount, dispatch, addElement } = useFormBuilder(); const { getValues } = useFormContext(); + const elements: FormElementData[] = getValues()["elements"]; const fieldButtons = Object.values(fields).map((field) => { - const elements: FormElementData[] = getValues()["elements"]; const usedFields = elements.map((e) => e.fieldId); if ( usedFields.includes(field.id) || @@ -49,7 +49,7 @@ export const SelectElement = ({ panelState }: { panelState: PanelState }) => { fieldId: field.id, required: true, type: ElementType.pubfield, - order: elementsCount, + rank: mudder.base62.mudder(elements[elementsCount].rank, "", 1)[0], configured: false, label: field.name, component, @@ -130,7 +130,11 @@ export const SelectElement = ({ panelState }: { panelState: PanelState }) => { addElement({ element: elementType, type: ElementType.structural, - order: elementsCount, + rank: mudder.base62.mudder( + elements[elementsCount].rank, + "", + 1 + )[0], configured: false, }); dispatch({ diff --git a/core/app/components/FormBuilder/FormBuilder.tsx b/core/app/components/FormBuilder/FormBuilder.tsx index d3c4205bd..324006e15 100644 --- a/core/app/components/FormBuilder/FormBuilder.tsx +++ b/core/app/components/FormBuilder/FormBuilder.tsx @@ -7,15 +7,14 @@ import { DndContext } from "@dnd-kit/core"; import { restrictToParentElement, restrictToVerticalAxis } from "@dnd-kit/modifiers"; import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; import { zodResolver } from "@hookform/resolvers/zod"; -import { createPortal } from "react-dom"; +import mudder from "mudder"; import { useFieldArray, useForm } from "react-hook-form"; import type { Stages } from "db/public"; import { logger } from "logger"; -import { Button } from "ui/button"; import { Form, FormControl, FormField, FormItem } from "ui/form"; import { useUnsavedChangesWarning } from "ui/hooks"; -import { CircleCheck, X } from "ui/icon"; +import { CircleCheck } from "ui/icon"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "ui/tabs"; import { TokenProvider } from "ui/tokens"; import { toast } from "ui/use-toast"; @@ -27,7 +26,7 @@ import { didSucceed, useServerAction } from "~/lib/serverActions"; import { PanelHeader, PanelWrapper, SidePanel } from "../SidePanel"; import { saveForm } from "./actions"; import { ElementPanel } from "./ElementPanel"; -import { FormBuilderProvider, useFormBuilder } from "./FormBuilderContext"; +import { FormBuilderProvider } from "./FormBuilderContext"; import { FormElement } from "./FormElement"; import { formBuilderSchema, isButtonElement } from "./types"; @@ -266,6 +265,28 @@ export function FormBuilder({ pubForm, id, stages }: Props) { activeIndex !== undefined && overIndex !== undefined ) { + const activeElem = + elements[activeIndex]; + const aboveRank = + elements[overIndex + 1] + ?.rank ?? ""; + const belowRank = + elements[overIndex - 1] + ?.rank ?? ""; + const [rank] = + mudder.base62.mudder( + belowRank, + aboveRank, + 1 + ); + form.setValue( + `elements.${activeIndex}`, + { + ...activeElem, + rank, + updated: true, + } + ); move(activeIndex, overIndex); } } diff --git a/core/app/components/FormBuilder/actions.ts b/core/app/components/FormBuilder/actions.ts index 7c710a838..a328909d6 100644 --- a/core/app/components/FormBuilder/actions.ts +++ b/core/app/components/FormBuilder/actions.ts @@ -54,13 +54,12 @@ export const saveForm = defineServerAction(async function saveForm(form: FormBui } else if (!element.elementId) { // Newly created elements have no elementId acc.upserts.push(formElementsInitializerSchema.parse({ formId, ...element })); - } else if (element.updated || element.order !== index + 1) { + } else if (element.updated) { acc.upserts.push( formElementsInitializerSchema.parse({ ...element, formId, id: element.elementId, - order: index + 1, }) ); // TODO: only update changed columns } diff --git a/core/app/components/FormBuilder/types.ts b/core/app/components/FormBuilder/types.ts index ad314b731..c5fd14cce 100644 --- a/core/app/components/FormBuilder/types.ts +++ b/core/app/components/FormBuilder/types.ts @@ -13,7 +13,7 @@ import { const baseElementSchema = z.object({ id: z.string().optional(), // react-hook-form assigned ID, meaningless in our DB elementId: formElementsIdSchema.optional(), - order: z.number().int().nullable(), + rank: z.string(), deleted: z.boolean().default(false), updated: z.boolean().default(false), configured: z.boolean().default(true), diff --git a/core/app/components/forms/types.ts b/core/app/components/forms/types.ts index f349d817f..ade66e15c 100644 --- a/core/app/components/forms/types.ts +++ b/core/app/components/forms/types.ts @@ -30,7 +30,7 @@ type BasePubFieldElement = { required: boolean | null; stageId: null; element: null; - order: number | null; + rank: string; slug: string; isRelation: boolean; }; @@ -53,7 +53,7 @@ export type ButtonElement = { id: FormElementsId; type: ElementType.button; fieldId: null; - order: number | null; + rank: string; label: string | null; element: null; content: null; @@ -70,7 +70,7 @@ export type StructuralElement = { id: FormElementsId; type: ElementType.structural; fieldId: null; - order: number | null; + rank: string; label: string | null; element: StructuralFormElement | null; content: string | null; diff --git a/core/app/components/pubs/PubEditor/helpers.ts b/core/app/components/pubs/PubEditor/helpers.ts index fa9f2b305..26f3ca01f 100644 --- a/core/app/components/pubs/PubEditor/helpers.ts +++ b/core/app/components/pubs/PubEditor/helpers.ts @@ -1,3 +1,4 @@ +import mudder from "mudder"; import { defaultComponent } from "schemas"; import type { FormElementsId } from "db/public"; @@ -20,7 +21,7 @@ export function makeFormElementDefFromPubFields( slug: field.slug, schemaName: field.schemaName, type: ElementType.pubfield, - order: index + 1, + rank: mudder.base62.numberToString(index + 1), stageId: null, fieldId: field.id, label: field.name ?? null, diff --git a/core/lib/server/form.ts b/core/lib/server/form.ts index 34b8e0ad4..bdcfe6ebc 100644 --- a/core/lib/server/form.ts +++ b/core/lib/server/form.ts @@ -55,7 +55,7 @@ export const getForm = ( "form_elements.fieldId", "form_elements.component", eb.fn.coalesce("form_elements.config", sql`'{}'`).as("config"), - "form_elements.order", + "form_elements.rank", "form_elements.label", "form_elements.content", "form_elements.element", @@ -66,7 +66,7 @@ export const getForm = ( "pub_fields.isRelation", ]) .$narrowType() - .orderBy("form_elements.order") + .orderBy("form_elements.rank") ).as("elements") ) ); From 6571462564884c8a1d2d3e6e972ebb8f67dba0f7 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Tue, 25 Feb 2025 16:04:19 -0600 Subject: [PATCH 05/38] Fix test expectation --- core/prisma/seed/seedCommunity.db.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/prisma/seed/seedCommunity.db.test.ts b/core/prisma/seed/seedCommunity.db.test.ts index 8ac140e2a..2232ab57c 100644 --- a/core/prisma/seed/seedCommunity.db.test.ts +++ b/core/prisma/seed/seedCommunity.db.test.ts @@ -289,7 +289,7 @@ describe("seedCommunity", () => { element: "p", fieldId: null, label: null, - order: 0, + rank: "F", required: null, stageId: null, type: "structural", @@ -302,7 +302,7 @@ describe("seedCommunity", () => { content: null, element: null, label: null, - order: 1, + rank: "U", required: null, stageId: null, type: "pubfield", @@ -314,7 +314,7 @@ describe("seedCommunity", () => { element: null, fieldId: null, label: "Submit", - order: 2, + rank: "k", required: null, stageId: null, type: "button", From f43c2554918c07632943f5244405aad4f236704b Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Tue, 25 Feb 2025 16:44:42 -0600 Subject: [PATCH 06/38] Sort pub values by rank --- .../[communitySlug]/activity/actions/page.tsx | 1 + .../pubs/[pubId]/components/queries.ts | 2 +- core/lib/server/pub.ts | 115 +++++++++++++++++- 3 files changed, 113 insertions(+), 5 deletions(-) diff --git a/core/app/c/[communitySlug]/activity/actions/page.tsx b/core/app/c/[communitySlug]/activity/actions/page.tsx index 20fd39cde..82b72eab2 100644 --- a/core/app/c/[communitySlug]/activity/actions/page.tsx +++ b/core/app/c/[communitySlug]/activity/actions/page.tsx @@ -88,6 +88,7 @@ export default async function Page(props: { "pub_fields.slug as fieldSlug", ]) .whereRef("pub_values.pubId", "=", "pubs.id") + .orderBy(["pub_values.fieldId", "pub_values.rank"]) ).as("values") ) .select((eb) => pubType({ eb, pubTypeIdRef: "pubs.pubTypeId" })) diff --git a/core/app/c/[communitySlug]/pubs/[pubId]/components/queries.ts b/core/app/c/[communitySlug]/pubs/[pubId]/components/queries.ts index 0afa852e0..47fb67e25 100644 --- a/core/app/c/[communitySlug]/pubs/[pubId]/components/queries.ts +++ b/core/app/c/[communitySlug]/pubs/[pubId]/components/queries.ts @@ -53,7 +53,7 @@ const memberFields = (pubId: Expression) => .whereRef("pub_values.pubId", "=", pubId) .where("pub_fields.schemaName", "=", CoreSchemaType.MemberId) .distinctOn("pub_fields.id") - .orderBy(["pub_fields.id", "pub_values.createdAt desc"]) + .orderBy(["pub_fields.id", "pub_values.rank", "pub_values.createdAt desc"]) ); const pubType = (pubTypeId: Expression) => diff --git a/core/lib/server/pub.ts b/core/lib/server/pub.ts index ece9b0528..425ca4f98 100644 --- a/core/lib/server/pub.ts +++ b/core/lib/server/pub.ts @@ -10,6 +10,7 @@ import type { import { sql, Transaction } from "kysely"; import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres"; import partition from "lodash.partition"; +import mudder from "mudder"; import type { CreatePubRequestBodyWithNullsNew, @@ -41,7 +42,7 @@ import { Capabilities, CoreSchemaType, MemberRole, MembershipType, OperationType import { logger } from "logger"; import { assert, expect } from "utils"; -import type { MaybeHas, Prettify, XOR } from "../types"; +import type { DefinitelyHas, MaybeHas, Prettify, XOR } from "../types"; import type { SafeUser } from "./user"; import { db } from "~/kysely/database"; import { env } from "../env/env.mjs"; @@ -124,6 +125,7 @@ const pubValues = ( .as("fields"), (join) => join.onRef("fields.id", "=", "pub_values.fieldId") ) + .orderBy(["pub_values.fieldId", "pub_values.rank"]) .$if(!!pubId, (qb) => qb.where("pub_values.pubId", "=", pubId!)) .$if(!!pubIdRef, (qb) => qb.whereRef("pub_values.pubId", "=", ref(pubIdRef!))) .as(alias) @@ -550,17 +552,23 @@ export const createPubRecursiveNew = async ({ + rankedValues.map(({ fieldId, value, relatedPubId, rank }, index) => ({ fieldId, pubId: newPub.id, value: JSON.stringify(value), relatedPubId, + rank, lastModifiedBy, })) ) @@ -705,6 +713,7 @@ export const deletePub = async ({ const pubValues = await trx .selectFrom("pub_values") .where("pubId", "in", Array.isArray(pubId) ? pubId : [pubId]) + .orderBy(["pub_values.fieldId", "pub_values.rank"]) .selectAll() .execute(); @@ -1196,6 +1205,99 @@ export const updatePub = async ({ return result; }; +const getRankedValues = async ({ + pubId, + pubValues, + trx, +}: { + pubId: PubsId; + pubValues: { + /** + * specify this if you do not want to use the pubId provided in the input + */ + pubId?: PubsId; + fieldId: PubFieldsId; + relatedPubId?: PubsId; + value: unknown; + }[]; + trx: typeof db; +}) => { + const { relatedValues, plainValues } = Object.groupBy(pubValues, (v) => + v.relatedPubId === undefined ? "plainValues" : "relatedValues" + ); + const groupedValues: Record< + PubsId, + Record[]> + > = {}; + let rankedValues; + if (relatedValues?.length) { + const firstVal = relatedValues.shift()!; + + const valuesQuery = trx + .selectFrom("pub_values") + .select(["rank", "fieldId", "pubId"]) + .where("pubId", "=", firstVal.pubId ?? pubId) + .where("fieldId", "=", firstVal.fieldId) + .where("rank", "is not", null) + .orderBy("rank desc") + .limit(1); + + for (const value of relatedValues) { + const newValue = { ...value, pubId: value.pubId ?? pubId }; + if (!groupedValues[newValue.pubId]) { + groupedValues[newValue.pubId] = { [value.fieldId]: [newValue] }; + } + if (!groupedValues[newValue.pubId][value.fieldId]) { + groupedValues[newValue.pubId][value.fieldId] = [newValue]; + } + + // If we've already found the highest ranked value for this pubId + fieldId combination, + // continue without adding to the query + if ( + groupedValues[newValue.pubId] && + groupedValues[newValue.pubId][value.fieldId]?.length + ) { + groupedValues[newValue.pubId][value.fieldId].push(newValue); + continue; + } + + // Select the highest ranked value for the given pub + field, and append (UNION ALL) + // that single row to the output + valuesQuery.unionAll((eb) => + eb + .selectFrom("pub_values") + .select(["rank", "fieldId", "pubId"]) + .where("pubId", "=", newValue.pubId) + .where("fieldId", "=", value.fieldId) + .where("rank", "is not", null) + .orderBy("rank desc") + .limit(1) + ); + } + const highestRanks = await valuesQuery.execute(); + + rankedValues = Object.values(groupedValues).flatMap((valuesForPub) => + Object.values(valuesForPub).flatMap((valuesForField) => { + const highestRank = + highestRanks.find( + ({ pubId, fieldId }) => + valuesForField[0].pubId === pubId && + valuesForField[0].fieldId === fieldId + )?.rank ?? ""; + const ranks = mudder.base62.mudder(highestRank, "", valuesForField.length); + return valuesForField.map((value, i) => ({ ...value, rank: ranks[i] })); + }) + ); + } + + const allValues: ((typeof pubValues)[number] & { rank?: string })[] = [ + ...(plainValues || []), + ...(rankedValues || []), + ]; + + return allValues; +}; + export const upsertPubValues = async ({ pubId, pubValues, @@ -1218,17 +1320,19 @@ export const upsertPubValues = async ({ if (!pubValues.length) { return []; } + const rankedValues = await getRankedValues({ pubId, pubValues, trx }); return autoRevalidate( trx .insertInto("pub_values") .values( - pubValues.map((value) => ({ + rankedValues.map((value) => ({ pubId: value.pubId ?? pubId, fieldId: value.fieldId, value: JSON.stringify(value.value), lastModifiedBy, relatedPubId: value.relatedPubId, + rank: value.rank, })) ) .onConflict((oc) => @@ -1265,15 +1369,18 @@ export const upsertPubRelationValues = async ({ return []; } + const rankedValues = await getRankedValues({ pubId, pubValues: allRelationsToCreate, trx }); + return autoRevalidate( trx .insertInto("pub_values") .values( - allRelationsToCreate.map((value) => ({ + rankedValues.map((value) => ({ pubId: value.pubId ?? pubId, relatedPubId: value.relatedPubId, value: JSON.stringify(value.value), fieldId: value.fieldId, + rank: value.rank, lastModifiedBy, })) ) From 69f0f951fbecc88aab0716cec09b0c20480a0809 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Tue, 25 Feb 2025 17:53:53 -0600 Subject: [PATCH 07/38] Fix bugs in related value ranking --- core/lib/server/pub.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/lib/server/pub.ts b/core/lib/server/pub.ts index 425ca4f98..9e2a877e1 100644 --- a/core/lib/server/pub.ts +++ b/core/lib/server/pub.ts @@ -1212,9 +1212,6 @@ const getRankedValues = async ({ }: { pubId: PubsId; pubValues: { - /** - * specify this if you do not want to use the pubId provided in the input - */ pubId?: PubsId; fieldId: PubFieldsId; relatedPubId?: PubsId; @@ -1231,7 +1228,7 @@ const getRankedValues = async ({ > = {}; let rankedValues; if (relatedValues?.length) { - const firstVal = relatedValues.shift()!; + const firstVal = relatedValues[0]; const valuesQuery = trx .selectFrom("pub_values") @@ -1246,14 +1243,13 @@ const getRankedValues = async ({ const newValue = { ...value, pubId: value.pubId ?? pubId }; if (!groupedValues[newValue.pubId]) { groupedValues[newValue.pubId] = { [value.fieldId]: [newValue] }; - } - if (!groupedValues[newValue.pubId][value.fieldId]) { + } else if (!groupedValues[newValue.pubId][value.fieldId]) { groupedValues[newValue.pubId][value.fieldId] = [newValue]; } // If we've already found the highest ranked value for this pubId + fieldId combination, // continue without adding to the query - if ( + else if ( groupedValues[newValue.pubId] && groupedValues[newValue.pubId][value.fieldId]?.length ) { @@ -1261,6 +1257,10 @@ const getRankedValues = async ({ continue; } + if (value === firstVal) { + continue; + } + // Select the highest ranked value for the given pub + field, and append (UNION ALL) // that single row to the output valuesQuery.unionAll((eb) => From ee6700e7a9d45bc2f1e05089364b9c82eb4e7462 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Wed, 26 Feb 2025 01:45:23 -0600 Subject: [PATCH 08/38] Fix form builder sort bugs --- .../components/FormBuilder/FormBuilder.tsx | 30 ++- core/lib/server/form.ts | 3 +- core/package.json | 2 +- pnpm-lock.yaml | 230 ++++++++++-------- 4 files changed, 153 insertions(+), 112 deletions(-) diff --git a/core/app/components/FormBuilder/FormBuilder.tsx b/core/app/components/FormBuilder/FormBuilder.tsx index 324006e15..7ebbbc8e8 100644 --- a/core/app/components/FormBuilder/FormBuilder.tsx +++ b/core/app/components/FormBuilder/FormBuilder.tsx @@ -265,29 +265,35 @@ export function FormBuilder({ pubForm, id, stages }: Props) { activeIndex !== undefined && overIndex !== undefined ) { + const isMovedEarlier = + activeIndex > overIndex; const activeElem = elements[activeIndex]; const aboveRank = - elements[overIndex + 1] - ?.rank ?? ""; + elements[ + isMovedEarlier + ? overIndex + : overIndex + 1 + ]?.rank ?? ""; const belowRank = - elements[overIndex - 1] - ?.rank ?? ""; + elements[ + isMovedEarlier + ? overIndex - 1 + : overIndex + ]?.rank ?? ""; const [rank] = mudder.base62.mudder( belowRank, aboveRank, 1 ); - form.setValue( - `elements.${activeIndex}`, - { - ...activeElem, - rank, - updated: true, - } - ); + move(activeIndex, overIndex); + update(overIndex, { + ...activeElem, + rank, + updated: true, + }); } } }} diff --git a/core/lib/server/form.ts b/core/lib/server/form.ts index bdcfe6ebc..aff290a58 100644 --- a/core/lib/server/form.ts +++ b/core/lib/server/form.ts @@ -66,7 +66,8 @@ export const getForm = ( "pub_fields.isRelation", ]) .$narrowType() - .orderBy("form_elements.rank") + // Use "C" collation order to ensure uppercase letters sort before lowercase to match mudder + .orderBy(sql`"rank" collate "C"`) ).as("elements") ) ); diff --git a/core/package.json b/core/package.json index 011d4c2b2..8bf2131b5 100644 --- a/core/package.json +++ b/core/package.json @@ -101,7 +101,7 @@ "jsonpath-plus": "^10.2.0", "jsonwebtoken": "^9.0.0", "katex": "^0.16.18", - "kysely": "^0.27.3", + "kysely": "^0.27.5", "lodash.partition": "^4.6.0", "logger": "workspace:*", "lucia": "^3.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7848b5476..6c89e06af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -374,8 +374,8 @@ importers: specifier: ^0.16.18 version: 0.16.18 kysely: - specifier: ^0.27.3 - version: 0.27.4 + specifier: ^0.27.5 + version: 0.27.5 lodash.partition: specifier: ^4.6.0 version: 4.6.0 @@ -574,7 +574,7 @@ importers: version: 9.0.8 '@vitejs/plugin-react': specifier: ^4.2.1 - version: 4.3.1(vite@5.4.3(@types/node@20.16.5)(terser@5.37.0)) + version: 4.3.1(vite@5.4.3(@types/node@20.16.5)(terser@5.39.0)) autoprefixer: specifier: ^10.4.14 version: 10.4.20(postcss@8.4.47) @@ -622,10 +622,10 @@ importers: version: 5.7.2 vite-tsconfig-paths: specifier: ^5.0.1 - version: 5.0.1(typescript@5.7.2)(vite@5.4.3(@types/node@20.16.5)(terser@5.37.0)) + version: 5.0.1(typescript@5.7.2)(vite@5.4.3(@types/node@20.16.5)(terser@5.39.0)) vitest: specifier: 'catalog:' - version: 3.0.5(@types/debug@4.1.12)(@types/node@20.16.5)(jsdom@25.0.1)(terser@5.37.0) + version: 3.0.5(@types/debug@4.1.12)(@types/node@20.16.5)(jsdom@25.0.1)(terser@5.39.0) yargs: specifier: ^17.7.2 version: 17.7.2 @@ -780,7 +780,7 @@ importers: version: 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.4.7(prettier@3.4.2))(typescript@5.7.2) '@storybook/react-vite': specifier: ^8.4.7 - version: 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.21.2)(storybook@8.4.7(prettier@3.4.2))(typescript@5.7.2)(vite@5.4.3(@types/node@20.17.12)(terser@5.37.0))(webpack-sources@3.2.3) + version: 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.21.2)(storybook@8.4.7(prettier@3.4.2))(typescript@5.7.2)(vite@5.4.3(@types/node@20.17.12)(terser@5.39.0))(webpack-sources@3.2.3) '@storybook/test': specifier: ^8.4.7 version: 8.4.7(storybook@8.4.7(prettier@3.4.2)) @@ -810,7 +810,7 @@ importers: version: 2.0.0-alpha.27(@babel/runtime@7.25.6)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@vitejs/plugin-react': specifier: ^4.2.1 - version: 4.3.1(vite@5.4.3(@types/node@20.17.12)(terser@5.37.0)) + version: 4.3.1(vite@5.4.3(@types/node@20.17.12)(terser@5.39.0)) prosemirror-dev-tools: specifier: ^4.1.0 version: 4.1.0(@babel/core@7.25.2)(@babel/template@7.25.0)(@types/react@19.0.6)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -828,7 +828,7 @@ importers: version: 5.7.2 vitest: specifier: 'catalog:' - version: 3.0.5(@types/debug@4.1.12)(@types/node@20.17.12)(jsdom@25.0.1)(terser@5.37.0) + version: 3.0.5(@types/debug@4.1.12)(@types/node@20.17.12)(jsdom@25.0.1)(terser@5.39.0) packages/contracts: dependencies: @@ -878,7 +878,7 @@ importers: version: link:../../config/prettier '@ts-rest/core': specifier: 'catalog:' - version: 3.51.0(@types/node@22.13.0)(zod@3.23.8) + version: 3.51.0(@types/node@22.13.5)(zod@3.23.8) '@types/pg': specifier: ^8.11.6 version: 8.11.8 @@ -1007,7 +1007,7 @@ importers: version: 5.7.2 vitest: specifier: 'catalog:' - version: 3.0.5(@types/debug@4.1.12)(@types/node@22.13.0)(jsdom@25.0.1)(terser@5.37.0) + version: 3.0.5(@types/debug@4.1.12)(@types/node@22.13.5)(jsdom@25.0.1)(terser@5.39.0) packages/ui: dependencies: @@ -1121,7 +1121,7 @@ importers: version: 1.1.1(@types/react-dom@19.0.3(@types/react@19.0.6))(@types/react@19.0.6)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@tailwindcss/typography': specifier: ^0.5.10 - version: 0.5.15(tailwindcss@3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2))) + version: 0.5.15(tailwindcss@3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.5)(typescript@5.7.2))) '@tanstack/react-table': specifier: ^8.20.6 version: 8.20.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -1184,7 +1184,7 @@ importers: version: 2.5.2 tailwindcss-animate: specifier: ^1.0.6 - version: 1.0.7(tailwindcss@3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2))) + version: 1.0.7(tailwindcss@3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.5)(typescript@5.7.2))) utils: specifier: workspace:* version: link:../utils @@ -1209,7 +1209,7 @@ importers: version: 19.0.0 tailwindcss: specifier: 'catalog:' - version: 3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)) + version: 3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.5)(typescript@5.7.2)) tsconfig: specifier: workspace:* version: link:../../config/tsconfig @@ -1274,7 +1274,7 @@ importers: version: 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.4.7(prettier@3.4.2))(typescript@5.7.2) '@storybook/react-vite': specifier: ^8.4.7 - version: 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.21.2)(storybook@8.4.7(prettier@3.4.2))(typescript@5.7.2)(vite@5.4.3(@types/node@22.13.0)(terser@5.37.0))(webpack-sources@3.2.3) + version: 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.21.2)(storybook@8.4.7(prettier@3.4.2))(typescript@5.7.2)(vite@5.4.3(@types/node@22.13.5)(terser@5.39.0))(webpack-sources@3.2.3) '@storybook/test': specifier: ^8.4.7 version: 8.4.7(storybook@8.4.7(prettier@3.4.2)) @@ -1298,7 +1298,7 @@ importers: version: 8.4.7(prettier@3.4.2) tailwindcss: specifier: 'catalog:' - version: 3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)) + version: 3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.5)(typescript@5.7.2)) tsconfig: specifier: workspace:* version: link:../config/tsconfig @@ -6366,6 +6366,9 @@ packages: '@types/node@22.13.0': resolution: {integrity: sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==} + '@types/node@22.13.5': + resolution: {integrity: sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==} + '@types/nodemailer@6.4.15': resolution: {integrity: sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==} @@ -7086,8 +7089,8 @@ packages: caniuse-lite@1.0.30001667: resolution: {integrity: sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==} - caniuse-lite@1.0.30001696: - resolution: {integrity: sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==} + caniuse-lite@1.0.30001700: + resolution: {integrity: sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==} case-anything@2.1.13: resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==} @@ -7692,12 +7695,12 @@ packages: engines: {node: '>=14'} hasBin: true + electron-to-chromium@1.5.105: + resolution: {integrity: sha512-ccp7LocdXx3yBhwiG0qTQ7XFrK48Ua2pxIxBdJO8cbddp/MvbBtPFzvnTchtyHQTsgqqczO8cdmAIbpMa0u2+g==} + electron-to-chromium@1.5.18: resolution: {integrity: sha512-1OfuVACu+zKlmjsNdcJuVQuVE61sZOLbNM4JAQ1Rvh6EOj0/EUKhMJjRH73InPlXSh8HIJk1cVZ8pyOV/FMdUQ==} - electron-to-chromium@1.5.90: - resolution: {integrity: sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==} - emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -7715,8 +7718,8 @@ packages: resolution: {integrity: sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==} engines: {node: '>=10.2.0'} - enhanced-resolve@5.18.0: - resolution: {integrity: sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==} + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} engines: {node: '>=10.13.0'} enquirer@2.4.1: @@ -9150,6 +9153,10 @@ packages: resolution: {integrity: sha512-dyNKv2KRvYOQPLCAOCjjQuCk4YFd33BvGdf/o5bC7FiW+BB6snA81Zt+2wT9QDFzKqxKa5rrOmvlK/anehCcgA==} engines: {node: '>=14.0.0'} + kysely@0.27.5: + resolution: {integrity: sha512-s7hZHcQeSNKpzCkHRm8yA+0JPLjncSWnjb+2TIElwS2JAqYr+Kv3Ess+9KFfJS0C1xcQ1i9NkNHpWwCYpHMWsA==} + engines: {node: '>=14.0.0'} + language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -11305,8 +11312,8 @@ packages: engines: {node: '>=10'} hasBin: true - terser@5.37.0: - resolution: {integrity: sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==} + terser@5.39.0: + resolution: {integrity: sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==} engines: {node: '>=10'} hasBin: true @@ -14165,19 +14172,19 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@joshwooding/vite-plugin-react-docgen-typescript@0.4.2(typescript@5.7.2)(vite@5.4.3(@types/node@20.17.12)(terser@5.37.0))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.4.2(typescript@5.7.2)(vite@5.4.3(@types/node@20.17.12)(terser@5.39.0))': dependencies: magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.7.2) - vite: 5.4.3(@types/node@20.17.12)(terser@5.37.0) + vite: 5.4.3(@types/node@20.17.12)(terser@5.39.0) optionalDependencies: typescript: 5.7.2 - '@joshwooding/vite-plugin-react-docgen-typescript@0.4.2(typescript@5.7.2)(vite@5.4.3(@types/node@22.13.0)(terser@5.37.0))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.4.2(typescript@5.7.2)(vite@5.4.3(@types/node@22.13.5)(terser@5.39.0))': dependencies: magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.7.2) - vite: 5.4.3(@types/node@22.13.0)(terser@5.37.0) + vite: 5.4.3(@types/node@22.13.5)(terser@5.39.0) optionalDependencies: typescript: 5.7.2 @@ -17804,23 +17811,23 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - '@storybook/builder-vite@8.4.7(storybook@8.4.7(prettier@3.4.2))(vite@5.4.3(@types/node@20.17.12)(terser@5.37.0))(webpack-sources@3.2.3)': + '@storybook/builder-vite@8.4.7(storybook@8.4.7(prettier@3.4.2))(vite@5.4.3(@types/node@20.17.12)(terser@5.39.0))(webpack-sources@3.2.3)': dependencies: '@storybook/csf-plugin': 8.4.7(storybook@8.4.7(prettier@3.4.2))(webpack-sources@3.2.3) browser-assert: 1.2.1 storybook: 8.4.7(prettier@3.4.2) ts-dedent: 2.2.0 - vite: 5.4.3(@types/node@20.17.12)(terser@5.37.0) + vite: 5.4.3(@types/node@20.17.12)(terser@5.39.0) transitivePeerDependencies: - webpack-sources - '@storybook/builder-vite@8.4.7(storybook@8.4.7(prettier@3.4.2))(vite@5.4.3(@types/node@22.13.0)(terser@5.37.0))(webpack-sources@3.2.3)': + '@storybook/builder-vite@8.4.7(storybook@8.4.7(prettier@3.4.2))(vite@5.4.3(@types/node@22.13.5)(terser@5.39.0))(webpack-sources@3.2.3)': dependencies: '@storybook/csf-plugin': 8.4.7(storybook@8.4.7(prettier@3.4.2))(webpack-sources@3.2.3) browser-assert: 1.2.1 storybook: 8.4.7(prettier@3.4.2) ts-dedent: 2.2.0 - vite: 5.4.3(@types/node@22.13.0)(terser@5.37.0) + vite: 5.4.3(@types/node@22.13.5)(terser@5.39.0) transitivePeerDependencies: - webpack-sources @@ -17897,11 +17904,11 @@ snapshots: react-dom: 19.0.0(react@19.0.0) storybook: 8.4.7(prettier@3.4.2) - '@storybook/react-vite@8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.21.2)(storybook@8.4.7(prettier@3.4.2))(typescript@5.7.2)(vite@5.4.3(@types/node@20.17.12)(terser@5.37.0))(webpack-sources@3.2.3)': + '@storybook/react-vite@8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.21.2)(storybook@8.4.7(prettier@3.4.2))(typescript@5.7.2)(vite@5.4.3(@types/node@20.17.12)(terser@5.39.0))(webpack-sources@3.2.3)': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.4.2(typescript@5.7.2)(vite@5.4.3(@types/node@20.17.12)(terser@5.37.0)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.4.2(typescript@5.7.2)(vite@5.4.3(@types/node@20.17.12)(terser@5.39.0)) '@rollup/pluginutils': 5.1.0(rollup@4.21.2) - '@storybook/builder-vite': 8.4.7(storybook@8.4.7(prettier@3.4.2))(vite@5.4.3(@types/node@20.17.12)(terser@5.37.0))(webpack-sources@3.2.3) + '@storybook/builder-vite': 8.4.7(storybook@8.4.7(prettier@3.4.2))(vite@5.4.3(@types/node@20.17.12)(terser@5.39.0))(webpack-sources@3.2.3) '@storybook/react': 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.4.7(prettier@3.4.2))(typescript@5.7.2) find-up: 5.0.0 magic-string: 0.30.11 @@ -17911,7 +17918,7 @@ snapshots: resolve: 1.22.8 storybook: 8.4.7(prettier@3.4.2) tsconfig-paths: 4.2.0 - vite: 5.4.3(@types/node@20.17.12)(terser@5.37.0) + vite: 5.4.3(@types/node@20.17.12)(terser@5.39.0) transitivePeerDependencies: - '@storybook/test' - rollup @@ -17919,11 +17926,11 @@ snapshots: - typescript - webpack-sources - '@storybook/react-vite@8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.21.2)(storybook@8.4.7(prettier@3.4.2))(typescript@5.7.2)(vite@5.4.3(@types/node@22.13.0)(terser@5.37.0))(webpack-sources@3.2.3)': + '@storybook/react-vite@8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(rollup@4.21.2)(storybook@8.4.7(prettier@3.4.2))(typescript@5.7.2)(vite@5.4.3(@types/node@22.13.5)(terser@5.39.0))(webpack-sources@3.2.3)': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.4.2(typescript@5.7.2)(vite@5.4.3(@types/node@22.13.0)(terser@5.37.0)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.4.2(typescript@5.7.2)(vite@5.4.3(@types/node@22.13.5)(terser@5.39.0)) '@rollup/pluginutils': 5.1.0(rollup@4.21.2) - '@storybook/builder-vite': 8.4.7(storybook@8.4.7(prettier@3.4.2))(vite@5.4.3(@types/node@22.13.0)(terser@5.37.0))(webpack-sources@3.2.3) + '@storybook/builder-vite': 8.4.7(storybook@8.4.7(prettier@3.4.2))(vite@5.4.3(@types/node@22.13.5)(terser@5.39.0))(webpack-sources@3.2.3) '@storybook/react': 8.4.7(@storybook/test@8.4.7(storybook@8.4.7(prettier@3.4.2)))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(storybook@8.4.7(prettier@3.4.2))(typescript@5.7.2) find-up: 5.0.0 magic-string: 0.30.11 @@ -17933,7 +17940,7 @@ snapshots: resolve: 1.22.8 storybook: 8.4.7(prettier@3.4.2) tsconfig-paths: 4.2.0 - vite: 5.4.3(@types/node@22.13.0)(terser@5.37.0) + vite: 5.4.3(@types/node@22.13.5)(terser@5.39.0) transitivePeerDependencies: - '@storybook/test' - rollup @@ -18066,13 +18073,13 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)) - '@tailwindcss/typography@0.5.15(tailwindcss@3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)))': + '@tailwindcss/typography@0.5.15(tailwindcss@3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.5)(typescript@5.7.2)))': dependencies: lodash.castarray: 4.4.0 lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)) + tailwindcss: 3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.5)(typescript@5.7.2)) '@tanstack/query-core@5.60.6': {} @@ -18157,9 +18164,9 @@ snapshots: '@types/node': 20.16.5 zod: 3.23.8 - '@ts-rest/core@3.51.0(@types/node@22.13.0)(zod@3.23.8)': + '@ts-rest/core@3.51.0(@types/node@22.13.5)(zod@3.23.8)': optionalDependencies: - '@types/node': 22.13.0 + '@types/node': 22.13.5 zod: 3.23.8 '@ts-rest/next@3.51.0(@ts-rest/core@3.51.0(@types/node@20.16.5)(zod@3.23.8))(next@15.1.4(@babel/core@7.25.2)(@opentelemetry/api@1.9.0)(@playwright/test@1.48.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(zod@3.23.8)': @@ -18508,6 +18515,10 @@ snapshots: dependencies: undici-types: 6.20.0 + '@types/node@22.13.5': + dependencies: + undici-types: 6.20.0 + '@types/nodemailer@6.4.15': dependencies: '@types/node': 20.16.5 @@ -18774,25 +18785,25 @@ snapshots: lodash: 4.17.21 preact: 10.23.2 - '@vitejs/plugin-react@4.3.1(vite@5.4.3(@types/node@20.16.5)(terser@5.37.0))': + '@vitejs/plugin-react@4.3.1(vite@5.4.3(@types/node@20.16.5)(terser@5.39.0))': dependencies: '@babel/core': 7.25.2 '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.25.2) '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.25.2) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.3(@types/node@20.16.5)(terser@5.37.0) + vite: 5.4.3(@types/node@20.16.5)(terser@5.39.0) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.3.1(vite@5.4.3(@types/node@20.17.12)(terser@5.37.0))': + '@vitejs/plugin-react@4.3.1(vite@5.4.3(@types/node@20.17.12)(terser@5.39.0))': dependencies: '@babel/core': 7.25.2 '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.25.2) '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.25.2) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.3(@types/node@20.17.12)(terser@5.37.0) + vite: 5.4.3(@types/node@20.17.12)(terser@5.39.0) transitivePeerDependencies: - supports-color @@ -18810,29 +18821,29 @@ snapshots: chai: 5.1.2 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.5(vite@5.4.3(@types/node@20.16.5)(terser@5.37.0))': + '@vitest/mocker@3.0.5(vite@5.4.3(@types/node@20.16.5)(terser@5.39.0))': dependencies: '@vitest/spy': 3.0.5 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.4.3(@types/node@20.16.5)(terser@5.37.0) + vite: 5.4.3(@types/node@20.16.5)(terser@5.39.0) - '@vitest/mocker@3.0.5(vite@5.4.3(@types/node@20.17.12)(terser@5.37.0))': + '@vitest/mocker@3.0.5(vite@5.4.3(@types/node@20.17.12)(terser@5.39.0))': dependencies: '@vitest/spy': 3.0.5 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.4.3(@types/node@20.17.12)(terser@5.37.0) + vite: 5.4.3(@types/node@20.17.12)(terser@5.39.0) - '@vitest/mocker@3.0.5(vite@5.4.3(@types/node@22.13.0)(terser@5.37.0))': + '@vitest/mocker@3.0.5(vite@5.4.3(@types/node@22.13.5)(terser@5.39.0))': dependencies: '@vitest/spy': 3.0.5 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.4.3(@types/node@22.13.0)(terser@5.37.0) + vite: 5.4.3(@types/node@22.13.5)(terser@5.39.0) '@vitest/pretty-format@2.0.5': dependencies: @@ -19359,8 +19370,8 @@ snapshots: browserslist@4.24.4: dependencies: - caniuse-lite: 1.0.30001696 - electron-to-chromium: 1.5.90 + caniuse-lite: 1.0.30001700 + electron-to-chromium: 1.5.105 node-releases: 2.0.19 update-browserslist-db: 1.1.2(browserslist@4.24.4) @@ -19431,7 +19442,7 @@ snapshots: caniuse-lite@1.0.30001667: {} - caniuse-lite@1.0.30001696: {} + caniuse-lite@1.0.30001700: {} case-anything@2.1.13: {} @@ -20023,9 +20034,9 @@ snapshots: minimatch: 9.0.1 semver: 7.6.3 - electron-to-chromium@1.5.18: {} + electron-to-chromium@1.5.105: {} - electron-to-chromium@1.5.90: {} + electron-to-chromium@1.5.18: {} emoji-regex@8.0.0: {} @@ -20054,7 +20065,7 @@ snapshots: - supports-color - utf-8-validate - enhanced-resolve@5.18.0: + enhanced-resolve@5.18.1: dependencies: graceful-fs: 4.2.11 tapable: 2.2.1 @@ -21719,7 +21730,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 20.17.12 + '@types/node': 22.13.5 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -21936,6 +21947,8 @@ snapshots: kysely@0.27.4: {} + kysely@0.27.5: {} + language-subtag-registry@0.3.23: {} language-tags@1.0.9: @@ -23120,13 +23133,13 @@ snapshots: postcss: 8.4.49 ts-node: 10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2) - postcss-load-config@4.0.2(postcss@8.4.49)(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)): + postcss-load-config@4.0.2(postcss@8.4.49)(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.5)(typescript@5.7.2)): dependencies: lilconfig: 3.1.3 yaml: 2.5.1 optionalDependencies: postcss: 8.4.49 - ts-node: 10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2) + ts-node: 10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.5)(typescript@5.7.2) postcss-nested@6.2.0(postcss@8.4.49): dependencies: @@ -24488,9 +24501,9 @@ snapshots: tailwind-merge@2.5.2: {} - tailwindcss-animate@1.0.7(tailwindcss@3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2))): + tailwindcss-animate@1.0.7(tailwindcss@3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.5)(typescript@5.7.2))): dependencies: - tailwindcss: 3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)) + tailwindcss: 3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.5)(typescript@5.7.2)) tailwindcss@3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@20.16.5)(typescript@5.7.2)): dependencies: @@ -24546,7 +24559,7 @@ snapshots: transitivePeerDependencies: - ts-node - tailwindcss@3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)): + tailwindcss@3.4.13(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.5)(typescript@5.7.2)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -24565,7 +24578,7 @@ snapshots: postcss: 8.4.49 postcss-import: 15.1.0(postcss@8.4.49) postcss-js: 4.0.1(postcss@8.4.49) - postcss-load-config: 4.0.2(postcss@8.4.49)(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.0)(typescript@5.7.2)) + postcss-load-config: 4.0.2(postcss@8.4.49)(ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.5)(typescript@5.7.2)) postcss-nested: 6.2.0(postcss@8.4.49) postcss-selector-parser: 6.1.2 resolve: 1.22.8 @@ -24616,7 +24629,7 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.0 serialize-javascript: 6.0.2 - terser: 5.37.0 + terser: 5.39.0 webpack: 5.94.0(@swc/core@1.7.24(@swc/helpers@0.5.15)) optionalDependencies: '@swc/core': 1.7.24(@swc/helpers@0.5.15) @@ -24628,7 +24641,7 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 - terser@5.37.0: + terser@5.39.0: dependencies: '@jridgewell/source-map': 0.3.6 acorn: 8.14.0 @@ -24798,6 +24811,27 @@ snapshots: optionalDependencies: '@swc/core': 1.7.24(@swc/helpers@0.5.15) + ts-node@10.9.2(@swc/core@1.7.24(@swc/helpers@0.5.15))(@types/node@22.13.5)(typescript@5.7.2): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.13.5 + acorn: 8.12.1 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.7.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.7.24(@swc/helpers@0.5.15) + optional: true + ts-pattern@4.3.0: {} tsconfck@3.1.3(typescript@5.7.2): @@ -25179,13 +25213,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@3.0.5(@types/node@20.16.5)(terser@5.37.0): + vite-node@3.0.5(@types/node@20.16.5)(terser@5.39.0): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.2 - vite: 5.4.3(@types/node@20.16.5)(terser@5.37.0) + vite: 5.4.3(@types/node@20.16.5)(terser@5.39.0) transitivePeerDependencies: - '@types/node' - less @@ -25197,13 +25231,13 @@ snapshots: - supports-color - terser - vite-node@3.0.5(@types/node@20.17.12)(terser@5.37.0): + vite-node@3.0.5(@types/node@20.17.12)(terser@5.39.0): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.2 - vite: 5.4.3(@types/node@20.17.12)(terser@5.37.0) + vite: 5.4.3(@types/node@20.17.12)(terser@5.39.0) transitivePeerDependencies: - '@types/node' - less @@ -25215,13 +25249,13 @@ snapshots: - supports-color - terser - vite-node@3.0.5(@types/node@22.13.0)(terser@5.37.0): + vite-node@3.0.5(@types/node@22.13.5)(terser@5.39.0): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.2 - vite: 5.4.3(@types/node@22.13.0)(terser@5.37.0) + vite: 5.4.3(@types/node@22.13.5)(terser@5.39.0) transitivePeerDependencies: - '@types/node' - less @@ -25233,18 +25267,18 @@ snapshots: - supports-color - terser - vite-tsconfig-paths@5.0.1(typescript@5.7.2)(vite@5.4.3(@types/node@20.16.5)(terser@5.37.0)): + vite-tsconfig-paths@5.0.1(typescript@5.7.2)(vite@5.4.3(@types/node@20.16.5)(terser@5.39.0)): dependencies: debug: 4.3.7 globrex: 0.1.2 tsconfck: 3.1.3(typescript@5.7.2) optionalDependencies: - vite: 5.4.3(@types/node@20.16.5)(terser@5.37.0) + vite: 5.4.3(@types/node@20.16.5)(terser@5.39.0) transitivePeerDependencies: - supports-color - typescript - vite@5.4.3(@types/node@20.16.5)(terser@5.37.0): + vite@5.4.3(@types/node@20.16.5)(terser@5.39.0): dependencies: esbuild: 0.21.5 postcss: 8.4.49 @@ -25252,9 +25286,9 @@ snapshots: optionalDependencies: '@types/node': 20.16.5 fsevents: 2.3.3 - terser: 5.37.0 + terser: 5.39.0 - vite@5.4.3(@types/node@20.17.12)(terser@5.37.0): + vite@5.4.3(@types/node@20.17.12)(terser@5.39.0): dependencies: esbuild: 0.21.5 postcss: 8.4.49 @@ -25262,22 +25296,22 @@ snapshots: optionalDependencies: '@types/node': 20.17.12 fsevents: 2.3.3 - terser: 5.37.0 + terser: 5.39.0 - vite@5.4.3(@types/node@22.13.0)(terser@5.37.0): + vite@5.4.3(@types/node@22.13.5)(terser@5.39.0): dependencies: esbuild: 0.21.5 postcss: 8.4.49 rollup: 4.21.2 optionalDependencies: - '@types/node': 22.13.0 + '@types/node': 22.13.5 fsevents: 2.3.3 - terser: 5.37.0 + terser: 5.39.0 - vitest@3.0.5(@types/debug@4.1.12)(@types/node@20.16.5)(jsdom@25.0.1)(terser@5.37.0): + vitest@3.0.5(@types/debug@4.1.12)(@types/node@20.16.5)(jsdom@25.0.1)(terser@5.39.0): dependencies: '@vitest/expect': 3.0.5 - '@vitest/mocker': 3.0.5(vite@5.4.3(@types/node@20.16.5)(terser@5.37.0)) + '@vitest/mocker': 3.0.5(vite@5.4.3(@types/node@20.16.5)(terser@5.39.0)) '@vitest/pretty-format': 3.0.5 '@vitest/runner': 3.0.5 '@vitest/snapshot': 3.0.5 @@ -25293,8 +25327,8 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 5.4.3(@types/node@20.16.5)(terser@5.37.0) - vite-node: 3.0.5(@types/node@20.16.5)(terser@5.37.0) + vite: 5.4.3(@types/node@20.16.5)(terser@5.39.0) + vite-node: 3.0.5(@types/node@20.16.5)(terser@5.39.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -25311,10 +25345,10 @@ snapshots: - supports-color - terser - vitest@3.0.5(@types/debug@4.1.12)(@types/node@20.17.12)(jsdom@25.0.1)(terser@5.37.0): + vitest@3.0.5(@types/debug@4.1.12)(@types/node@20.17.12)(jsdom@25.0.1)(terser@5.39.0): dependencies: '@vitest/expect': 3.0.5 - '@vitest/mocker': 3.0.5(vite@5.4.3(@types/node@20.17.12)(terser@5.37.0)) + '@vitest/mocker': 3.0.5(vite@5.4.3(@types/node@20.17.12)(terser@5.39.0)) '@vitest/pretty-format': 3.0.5 '@vitest/runner': 3.0.5 '@vitest/snapshot': 3.0.5 @@ -25330,8 +25364,8 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 5.4.3(@types/node@20.17.12)(terser@5.37.0) - vite-node: 3.0.5(@types/node@20.17.12)(terser@5.37.0) + vite: 5.4.3(@types/node@20.17.12)(terser@5.39.0) + vite-node: 3.0.5(@types/node@20.17.12)(terser@5.39.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 @@ -25348,10 +25382,10 @@ snapshots: - supports-color - terser - vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.13.0)(jsdom@25.0.1)(terser@5.37.0): + vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.13.5)(jsdom@25.0.1)(terser@5.39.0): dependencies: '@vitest/expect': 3.0.5 - '@vitest/mocker': 3.0.5(vite@5.4.3(@types/node@22.13.0)(terser@5.37.0)) + '@vitest/mocker': 3.0.5(vite@5.4.3(@types/node@22.13.5)(terser@5.39.0)) '@vitest/pretty-format': 3.0.5 '@vitest/runner': 3.0.5 '@vitest/snapshot': 3.0.5 @@ -25367,12 +25401,12 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 5.4.3(@types/node@22.13.0)(terser@5.37.0) - vite-node: 3.0.5(@types/node@22.13.0)(terser@5.37.0) + vite: 5.4.3(@types/node@22.13.5)(terser@5.39.0) + vite-node: 3.0.5(@types/node@22.13.5)(terser@5.39.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 22.13.0 + '@types/node': 22.13.5 jsdom: 25.0.1 transitivePeerDependencies: - less @@ -25422,7 +25456,7 @@ snapshots: acorn-import-attributes: 1.9.5(acorn@8.14.0) browserslist: 4.24.4 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.0 + enhanced-resolve: 5.18.1 es-module-lexer: 1.6.0 eslint-scope: 5.1.1 events: 3.3.0 From aaf0f0a056d5fa5406e0377fd533475fecc94fa4 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Wed, 26 Feb 2025 10:46:52 -0600 Subject: [PATCH 09/38] Pass transaction to getPubType --- core/app/c/[communitySlug]/types/actions.ts | 2 +- core/lib/server/pubtype.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/app/c/[communitySlug]/types/actions.ts b/core/app/c/[communitySlug]/types/actions.ts index b069df324..5fe22f486 100644 --- a/core/app/c/[communitySlug]/types/actions.ts +++ b/core/app/c/[communitySlug]/types/actions.ts @@ -180,7 +180,7 @@ export const createPubType = defineServerAction(async function createPubType( .returning("B as id") ).executeTakeFirstOrThrow(); - const pubType = await getPubType(pubTypeId).executeTakeFirstOrThrow(); + const pubType = await getPubType(pubTypeId, trx).executeTakeFirstOrThrow(); await autoRevalidate( insertForm( diff --git a/core/lib/server/pubtype.ts b/core/lib/server/pubtype.ts index fad65499a..1afffa87b 100644 --- a/core/lib/server/pubtype.ts +++ b/core/lib/server/pubtype.ts @@ -52,8 +52,8 @@ export const getPubTypeBase = >( ).as("fields"), ]); -export const getPubType = (pubTypeId: PubTypesId) => - autoCache(getPubTypeBase().where("pub_types.id", "=", pubTypeId)); +export const getPubType = (pubTypeId: PubTypesId, trx = db) => + autoCache(getPubTypeBase(trx).where("pub_types.id", "=", pubTypeId)); export const getPubTypeForPubId = async (pubId: PubsId) => { return autoCache( From 1e0850bc90deb86e764126b4ce77eccb1da770cc Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Wed, 26 Feb 2025 10:47:09 -0600 Subject: [PATCH 10/38] Fix off by one error --- .../app/components/FormBuilder/ElementPanel/SelectElement.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx b/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx index dcda0e3f5..6dd5ae3d0 100644 --- a/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx +++ b/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx @@ -49,7 +49,7 @@ export const SelectElement = ({ panelState }: { panelState: PanelState }) => { fieldId: field.id, required: true, type: ElementType.pubfield, - rank: mudder.base62.mudder(elements[elementsCount].rank, "", 1)[0], + rank: mudder.base62.mudder(elements[elementsCount - 1].rank, "", 1)[0], configured: false, label: field.name, component, @@ -131,7 +131,7 @@ export const SelectElement = ({ panelState }: { panelState: PanelState }) => { element: elementType, type: ElementType.structural, rank: mudder.base62.mudder( - elements[elementsCount].rank, + elements[elementsCount - 1].rank, "", 1 )[0], From f3b06bf828710f9042d7e90da0205320f9ad8bd8 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Wed, 26 Feb 2025 16:58:35 -0600 Subject: [PATCH 11/38] Condense migrations --- .../migration.sql | 89 +++++++++++++++++++ .../migration.sql | 51 ----------- .../migration.sql | 64 ------------- 3 files changed, 89 insertions(+), 115 deletions(-) create mode 100644 core/prisma/migrations/20250213201642_add_mudder_ranks/migration.sql delete mode 100644 core/prisma/migrations/20250213201642_add_rank_to_form_elements/migration.sql delete mode 100644 core/prisma/migrations/20250225034631_add_rank_to_pub_values/migration.sql diff --git a/core/prisma/migrations/20250213201642_add_mudder_ranks/migration.sql b/core/prisma/migrations/20250213201642_add_mudder_ranks/migration.sql new file mode 100644 index 000000000..f22f8fec6 --- /dev/null +++ b/core/prisma/migrations/20250213201642_add_mudder_ranks/migration.sql @@ -0,0 +1,89 @@ +BEGIN; + CREATE TEMP TABLE "mudder_ranks" ( + index SERIAL PRIMARY KEY, + rank TEXT, + ); + /* + * This temp table holds 200 mudder generated keys which we use to assign initial ranks to existing + * form elements and related pubs in the migration. + * Generated with: mudder.base62.mudder(200).map((rank) => `('${rank}')`).join(", ") + */ + INSERT INTO "mudder_ranks" + VALUES ('0J'), ('0c'), ('0v'), ('1'), ('1X'), ('1q'), ('2'), ('2S'), ('2m'), ('3'), ('3O'), ('3h'), + ('4'), ('4J'), ('4c'), ('4v'), ('5'), ('5Y'), ('5r'), ('6'), ('6T'), ('6m'), ('7'), ('7O'), ('7i'), + ('8'), ('8K'), ('8d'), ('8w'), ('9'), ('9Y'), ('9r'), ('A'), ('AU'), ('An'), ('B'), ('BP'), ('Bi'), + ('C'), ('CK'), ('Ce'), ('Cx'), ('D'), ('DZ'), ('Ds'), ('E'), ('EU'), ('En'), ('F'), ('FQ'), ('Fj'), + ('G'), ('GL'), ('Ge'), ('Gx'), ('H'), ('Ha'), ('Ht'), ('I'), ('IV'), ('Io'), ('J'), ('JQ'), ('Jj'), + ('K'), ('KM'), ('Kf'), ('Ky'), ('L'), ('La'), ('Lt'), ('M'), ('MW'), ('Mp'), ('N'), ('NR'), ('Nk'), + ('O'), ('OM'), ('Of'), ('Oz'), ('P'), ('Pb'), ('Pu'), ('Q'), ('QW'), ('Qp'), ('R'), ('RS'), ('Rl'), + ('S'), ('SN'), ('Sg'), ('Sz'), ('T'), ('Tb'), ('Tv'), ('U'), ('UX'), ('Uq'), ('V'), ('VS'), ('Vl'), + ('W'), ('WO'), ('Wh'), ('X'), ('XJ'), ('Xc'), ('Xv'), ('Y'), ('YX'), ('Yr'), ('Z'), ('ZT'), ('Zm'), + ('a'), ('aO'), ('ah'), ('b'), ('bK'), ('bd'), ('bw'), ('c'), ('cY'), ('cr'), ('d'), ('dT'), ('dn'), + ('e'), ('eP'), ('ei'), ('f'), ('fK'), ('fd'), ('fw'), ('g'), ('gZ'), ('gs'), ('h'), ('hU'), ('hn'), + ('i'), ('iP'), ('ij'), ('j'), ('jL'), ('je'), ('jx'), ('k'), ('kZ'), ('ks'), ('l'), ('lV'), ('lo'), + ('m'), ('mQ'), ('mj'), ('n'), ('nL'), ('nf'), ('ny'), ('o'), ('oa'), ('ot'), ('p'), ('pV'), ('po'), + ('q'), ('qR'), ('qk'), ('r'), ('rM'), ('rf'), ('ry'), ('s'), ('sb'), ('su'), ('t'), ('tW'), ('tp'), + ('u'), ('uR'), ('uk'), ('v'), ('vN'), ('vg'), ('vz'), ('w'), ('wb'), ('wu'), ('x'), ('xX'), ('xq'), + ('y'), ('yS'), ('yl'), ('z'), ('zN'), ('zg'); + + -- Add rank to form_elements + ALTER TABLE "form_elements" ADD COLUMN "rank" TEXT; + + -- Set initial rank values for form elements based on 'order' + UPDATE "form_elements" + SET "rank" = "mudder_ranks"."rank" + FROM "mudder_ranks" + WHERE + "form_elements"."order" IS NOT NULL + AND "form_elements"."order" = "mudder_ranks"."index"; + + -- Set a rank for submit buttons which were previously unordered, near the end (zzzz...) + WITH "buttons" AS ( + -- Assign a somewhat arbitrary numeric order to the buttons + -- Since some have order = null, the non-null ordered ones will come first + SELECT "id", "formId", ROW_NUMBER() OVER (PARTITION BY "formId" ORDER BY "order") AS "index" + FROM "form_elements" + WHERE "form_elements"."type" = 'button'::"ElementType" + ) + UPDATE "form_elements" + SET "rank" = REPEAT('z'::text, "buttons"."index"::int + 10) + FROM "buttons" + WHERE "form_elements"."type" = 'button'::"ElementType" + AND "form_elements"."id" = "buttons"."id"; + + -- Now that there are ranks for all elements, add a not null constraint + ALTER TABLE "form_elements" ALTER COLUMN "rank" SET NOT NULL; + +-- Add rank to pub_values +-- This one is nullable for now + ALTER TABLE "pub_values" ADD COLUMN "rank" TEXT; + +-- Get all pub_values with multiple related pubs, then assign initial ranks ordered by updatedAt + WITH "related_pubs" AS ( + SELECT "pubId", "fieldId" + FROM "pub_values" + WHERE "relatedPubId" IS NOT NULL + GROUP BY "pubId", "fieldId" + HAVING COUNT("pubId") > 1 + ), + "row_numbers" AS ( + SELECT + "pub_values"."id", + ROW_NUMBER() OVER ( + PARTITION BY "pub_values"."pubId", "pub_values"."fieldId" + ORDER BY "pub_values"."updatedAt" + ) as "r" + FROM "pub_values" + JOIN "related_pubs" ON + "related_pubs"."pubId" = "pub_values"."pubId" + AND "related_pubs"."fieldId" = "pub_values"."fieldId" + ) + UPDATE "pub_values" + SET + "rank" = "mudder_ranks"."rank", + "lastModifiedBy" = 'system|' || FLOOR(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000) + FROM "mudder_ranks", "row_numbers" + WHERE + "mudder_ranks"."index" = "row_numbers"."r" + AND "row_numbers"."id" = "pub_values"."id"; +COMMIT; \ No newline at end of file diff --git a/core/prisma/migrations/20250213201642_add_rank_to_form_elements/migration.sql b/core/prisma/migrations/20250213201642_add_rank_to_form_elements/migration.sql deleted file mode 100644 index 56b5fcc2b..000000000 --- a/core/prisma/migrations/20250213201642_add_rank_to_form_elements/migration.sql +++ /dev/null @@ -1,51 +0,0 @@ -BEGIN; - ALTER TABLE "form_elements" ADD COLUMN "rank" TEXT; - - /* Convert numeric order to alphanumeric (base62) string - * - * These values are the first 100 of a 1000 item division of the 0-z space, generated with mudder: - * mudder.base62.mudder('','', 100, undefined, 1000).map((rank, index) => `(${index}, '${rank}')`).join(", ") - * - * We intentionally generate ranks closer to the beginning of the space because new form elements - * are always added to the end. - */ - WITH "mudder_ranks" AS ( - SELECT "m".* FROM ( - VALUES (0, '03'), (1, '07'), (2, '0B'), (3, '0F'), (4, '0J'), (5, '0N'), (6, '0Q'), (7, '0U'), (8, '0Y'), - (9, '0c'), (10, '0g'), (11, '0k'), (12, '0n'), (13, '0r'), (14, '0v'), (15, '0z'), (16, '1'), (17, '17'), - (18, '1B'), (19, '1E'), (20, '1I'), (21, '1M'), (22, '1Q'), (23, '1U'), (24, '1Y'), (25, '1b'), (26, '1f'), - (27, '1j'), (28, '1n'), (29, '1r'), (30, '1v'), (31, '1z'), (32, '2'), (33, '26'), (34, '2A'), (35, '2E'), - (36, '2I'), (37, '2M'), (38, '2P'), (39, '2T'), (40, '2X'), (41, '2b'), (42, '2f'), (43, '2j'), (44, '2m'), - (45, '2q'), (46, '2u'), (47, '2y'), (48, '3'), (49, '36'), (50, '3A'), (51, '3D'), (52, '3H'), (53, '3L'), - (54, '3P'), (55, '3T'), (56, '3X'), (57, '3a'), (58, '3e'), (59, '3i'), (60, '3m'), (61, '3q'), (62, '3u'), - (63, '3y'), (64, '4'), (65, '45'), (66, '49'), (67, '4D'), (68, '4H'), (69, '4L'), (70, '4O'), (71, '4S'), - (72, '4W'), (73, '4a'), (74, '4e'), (75, '4i'), (76, '4l'), (77, '4p'), (78, '4t'), (79, '4x'), (80, '5'), - (81, '55'), (82, '59'), (83, '5C'), (84, '5G'), (85, '5K'), (86, '5O'), (87, '5S'), (88, '5W'), (89, '5Z'), - (90, '5d'), (91, '5h'), (92, '5l'), (93, '5p'), (94, '5t'), (95, '5x'), (96, '6'), (97, '64'), (98, '68'), - (99, '6C') - ) AS "m"("index", "rank") - ) - UPDATE "form_elements" - SET "rank" = "mudder_ranks"."rank" - FROM "mudder_ranks" - WHERE - "form_elements"."order" IS NOT NULL - AND "form_elements"."order" = "mudder_ranks"."index"; - - -- Set a rank for submit buttons, all the way at the end (z, zz, zzz etc.) - WITH "buttons" AS ( - -- Assign a somewhat arbitrary numeric order to the buttons - -- Since some have order = null, the non-null ordered ones will come first - SELECT "id", "formId", ROW_NUMBER() OVER (PARTITION BY "formId" ORDER BY "order") AS "rank" - FROM "form_elements" - WHERE "form_elements"."type" = 'button'::"ElementType" - ) - UPDATE "form_elements" - SET "rank" = REPEAT('z'::text, "buttons"."rank"::int) - FROM "buttons" - WHERE "form_elements"."type" = 'button'::"ElementType" - AND "form_elements"."id" = "buttons"."id"; - - ALTER TABLE "form_elements" ALTER COLUMN "rank" SET NOT NULL; - -COMMIT; \ No newline at end of file diff --git a/core/prisma/migrations/20250225034631_add_rank_to_pub_values/migration.sql b/core/prisma/migrations/20250225034631_add_rank_to_pub_values/migration.sql deleted file mode 100644 index d1a1cb280..000000000 --- a/core/prisma/migrations/20250225034631_add_rank_to_pub_values/migration.sql +++ /dev/null @@ -1,64 +0,0 @@ -ALTER TABLE "pub_values" ADD COLUMN "rank" TEXT; - -WITH "mudder_ranks" AS ( - SELECT "m".* FROM ( - VALUES (0,'01'), (1,'03'), (2,'05'), (3,'07'), (4,'09'), (5,'0B'), (6,'0D'), - (7,'0F'), (8,'0H'), (9,'0J'), (10,'0L'), (11,'0N'), (12,'0O'), (13,'0Q'), - (14,'0S'), (15,'0U'), (16,'0W'), (17,'0Y'), (18,'0a'), (19,'0c'), (20,'0e'), - (21,'0g'), (22,'0i'), (23,'0k'), (24,'0m'), (25,'0n'), (26,'0p'), (27,'0r'), - (28,'0t'), (29,'0v'), (30,'0x'), (31,'0z'), (32,'1'), (33,'13'), (34,'15'), - (35,'17'), (36,'19'), (37,'1B'), (38,'1C'), (39,'1E'), (40,'1G'), (41,'1I'), - (42,'1K'), (43,'1M'), (44,'1O'), (45,'1Q'), (46,'1S'), (47,'1U'), (48,'1W'), - (49,'1Y'), (50,'1a'), (51,'1b'), (52,'1d'), (53,'1f'), (54,'1h'), (55,'1j'), - (56,'1l'), (57,'1n'), (58,'1p'), (59,'1r'), (60,'1t'), (61,'1v'), (62,'1x'), - (63,'1z'), (64,'2'), (65,'22'), (66,'24'), (67,'26'), (68,'28'), (69,'2A'), - (70,'2C'), (71,'2E'), (72,'2G'), (73,'2I'), (74,'2K'), (75,'2M'), (76,'2N'), - (77,'2P'), (78,'2R'), (79,'2T'), (80,'2V'), (81,'2X'), (82,'2Z'), (83,'2b'), - (84,'2d'), (85,'2f'), (86,'2h'), (87,'2j'), (88,'2l'), (89,'2m'), (90,'2o'), - (91,'2q'), (92,'2s'), (93,'2u'), (94,'2w'), (95,'2y'), (96,'3'), (97,'32'), - (98,'34'), (99,'36'), (100,'38'), (101,'3A'), (102,'3B'), (103,'3D'), - (104,'3F'), (105,'3H'), (106,'3J'), (107,'3L'), (108,'3N'), (109,'3P'), - (110,'3R'), (111,'3T'), (112,'3V'), (113,'3X'), (114,'3Z'), (115,'3a'), - (116,'3c'), (117,'3e'), (118,'3g'), (119,'3i'), (120,'3k'), (121,'3m'), - (122,'3o'), (123,'3q'), (124,'3s'), (125,'3u'), (126,'3w'), (127,'3y'), - (128,'3z'), (129,'4'), (130,'43'), (131,'45'), (132,'47'), (133,'49'), - (134,'4B'), (135,'4D'), (136,'4F'), (137,'4H'), (138,'4J'), (139,'4L'), - (140,'4N'), (141,'4O'), (142,'4Q'), (143,'4S'), (144,'4U'), (145,'4W'), - (146,'4Y'), (147,'4a'), (148,'4c'), (149,'4e'), (150,'4g'), (151,'4i'), - (152,'4k'), (153,'4l'), (154,'4n'), (155,'4p'), (156,'4r'), (157,'4t'), - (158,'4v'), (159,'4x'), (160,'4z'), (161,'5'), (162,'53'), (163,'55'), - (164,'57'), (165,'59'), (166,'5A'), (167,'5C'), (168,'5E'), (169,'5G'), - (170,'5I'), (171,'5K'), (172,'5M'), (173,'5O'), (174,'5Q'), (175,'5S'), - (176,'5U'), (177,'5W'), (178,'5Y'), (179,'5Z'), (180,'5b'), (181,'5d'), - (182,'5f'), (183,'5h'), (184,'5j'), (185,'5l'), (186,'5n'), (187,'5p'), - (188,'5r'), (189,'5t'), (190,'5v'), (191,'5x'), (192,'5y'), (193,'6'), - (194,'62'), (195,'64'), (196,'66'), (197,'68'), (198,'6A'), (199,'6C') - ) AS "m"("index", "rank") -), -"related_pubs" AS ( - SELECT "pubId", "fieldId" - FROM "pub_values" - WHERE "relatedPubId" IS NOT NULL - GROUP BY "pubId", "fieldId" - HAVING COUNT("pubId") > 1 -), -"row_numbers" AS ( - SELECT - "pub_values"."id", - ROW_NUMBER() OVER ( - PARTITION BY "pub_values"."pubId", "pub_values"."fieldId" - ORDER BY "pub_values"."updatedAt" - ) as "r" - FROM "pub_values" - JOIN "related_pubs" ON - "related_pubs"."pubId" = "pub_values"."pubId" - AND "related_pubs"."fieldId" = "pub_values"."fieldId" -) -UPDATE "pub_values" -SET - "rank" = "mudder_ranks"."rank", - "lastModifiedBy" = 'system|' || FLOOR(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000) -FROM "mudder_ranks", "row_numbers" -WHERE - "mudder_ranks"."index" = "row_numbers"."r" - AND "row_numbers"."id" = "pub_values"."id"; \ No newline at end of file From c021a3434dda43215d9c1b7d022427f78396b97f Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Wed, 26 Feb 2025 17:12:23 -0600 Subject: [PATCH 12/38] Set collation order at field level rather than on query --- core/lib/server/form.ts | 3 +-- .../20250213201642_add_mudder_ranks/migration.sql | 9 +++++---- core/prisma/schema/schema.prisma | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/core/lib/server/form.ts b/core/lib/server/form.ts index aff290a58..9bbfd2afd 100644 --- a/core/lib/server/form.ts +++ b/core/lib/server/form.ts @@ -66,8 +66,7 @@ export const getForm = ( "pub_fields.isRelation", ]) .$narrowType() - // Use "C" collation order to ensure uppercase letters sort before lowercase to match mudder - .orderBy(sql`"rank" collate "C"`) + .orderBy("rank") ).as("elements") ) ); diff --git a/core/prisma/migrations/20250213201642_add_mudder_ranks/migration.sql b/core/prisma/migrations/20250213201642_add_mudder_ranks/migration.sql index f22f8fec6..7d4ff7b61 100644 --- a/core/prisma/migrations/20250213201642_add_mudder_ranks/migration.sql +++ b/core/prisma/migrations/20250213201642_add_mudder_ranks/migration.sql @@ -1,14 +1,14 @@ BEGIN; CREATE TEMP TABLE "mudder_ranks" ( index SERIAL PRIMARY KEY, - rank TEXT, + rank TEXT ); /* * This temp table holds 200 mudder generated keys which we use to assign initial ranks to existing * form elements and related pubs in the migration. * Generated with: mudder.base62.mudder(200).map((rank) => `('${rank}')`).join(", ") */ - INSERT INTO "mudder_ranks" + INSERT INTO "mudder_ranks"("rank") VALUES ('0J'), ('0c'), ('0v'), ('1'), ('1X'), ('1q'), ('2'), ('2S'), ('2m'), ('3'), ('3O'), ('3h'), ('4'), ('4J'), ('4c'), ('4v'), ('5'), ('5Y'), ('5r'), ('6'), ('6T'), ('6m'), ('7'), ('7O'), ('7i'), ('8'), ('8K'), ('8d'), ('8w'), ('9'), ('9Y'), ('9r'), ('A'), ('AU'), ('An'), ('B'), ('BP'), ('Bi'), @@ -27,7 +27,8 @@ BEGIN; ('y'), ('yS'), ('yl'), ('z'), ('zN'), ('zg'); -- Add rank to form_elements - ALTER TABLE "form_elements" ADD COLUMN "rank" TEXT; + -- Uses "C" collation order to ensure uppercase letters sort before lowercase to match mudder + ALTER TABLE "form_elements" ADD COLUMN "rank" TEXT COLLATE "C"; -- Set initial rank values for form elements based on 'order' UPDATE "form_elements" @@ -56,7 +57,7 @@ BEGIN; -- Add rank to pub_values -- This one is nullable for now - ALTER TABLE "pub_values" ADD COLUMN "rank" TEXT; + ALTER TABLE "pub_values" ADD COLUMN "rank" TEXT COLLATE "C"; -- Get all pub_values with multiple related pubs, then assign initial ranks ordered by updatedAt WITH "related_pubs" AS ( diff --git a/core/prisma/schema/schema.prisma b/core/prisma/schema/schema.prisma index f99071877..4254e032c 100644 --- a/core/prisma/schema/schema.prisma +++ b/core/prisma/schema/schema.prisma @@ -209,7 +209,7 @@ model PubValue { // Rank is only used for ordering multiple related pubs for a single field, not for ordering // values within a pub - rank String? + rank String? // Uses "C" collation @@map(name: "pub_values") } @@ -536,7 +536,7 @@ model FormElement { field PubField? @relation(fields: [fieldId], references: [id], onDelete: Cascade) formId String order Int? - rank String + rank String // Uses "C" collation // label is only used by elements with type: ElementType.button. Pubfield inputs put everything in config label String? element StructuralFormElement? From 0ff5ea02f5ae6ab71438c6c4c4865270b0ac3b60 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Wed, 26 Feb 2025 17:40:23 -0600 Subject: [PATCH 13/38] Improve form builder readability --- .../components/FormBuilder/FormBuilder.tsx | 83 +++++++++---------- 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/core/app/components/FormBuilder/FormBuilder.tsx b/core/app/components/FormBuilder/FormBuilder.tsx index 7ebbbc8e8..676c2d54c 100644 --- a/core/app/components/FormBuilder/FormBuilder.tsx +++ b/core/app/components/FormBuilder/FormBuilder.tsx @@ -1,5 +1,7 @@ "use client"; +import type { DragEndEvent } from "@dnd-kit/core"; + import * as React from "react"; import { useCallback, useReducer, useRef } from "react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; @@ -197,6 +199,41 @@ export function FormBuilder({ pubForm, id, stages }: Props) { } }, [elements, remove, panelState.selectedElementIndex]); + // Update ranks and rhf field array position when elements are dragged + const handleDragEnd = useCallback( + (event: DragEndEvent) => { + const { active, over } = event; + if (over && active.id !== over?.id) { + // activeIndex is the position the element started at and over is where it was dropped + const activeIndex = active.data.current?.sortable?.index; + const overIndex = over.data.current?.sortable?.index; + if (activeIndex !== undefined && overIndex !== undefined) { + // "earlier" means towards the beginning of the list, or towards the top of the page + const isMovedEarlier = activeIndex > overIndex; + const activeElem = elements[activeIndex]; + + // When moving an element earlier in the array, find a rank between the rank of the + // element at the dropped position and the element before it. When moving an element + // later, instead find a rank between that element and the element after it + const aboveRank = + elements[isMovedEarlier ? overIndex : overIndex + 1]?.rank ?? ""; + const belowRank = + elements[isMovedEarlier ? overIndex - 1 : overIndex]?.rank ?? ""; + const [rank] = mudder.base62.mudder(belowRank, aboveRank, 1); + + // move doesn't trigger a rerender, so it's safe to chain these calls + move(activeIndex, overIndex); + update(overIndex, { + ...activeElem, + rank, + updated: true, + }); + } + } + }, + [elements] + ); + const tokens = { content: renderWithPubTokens }; return ( @@ -252,51 +289,7 @@ export function FormBuilder({ pubForm, id, stages }: Props) { restrictToVerticalAxis, restrictToParentElement, ]} - onDragEnd={(event) => { - const { active, over } = event; - if (over && active.id !== over?.id) { - const activeIndex = - active.data.current?.sortable - ?.index; - const overIndex = - over.data.current?.sortable - ?.index; - if ( - activeIndex !== undefined && - overIndex !== undefined - ) { - const isMovedEarlier = - activeIndex > overIndex; - const activeElem = - elements[activeIndex]; - const aboveRank = - elements[ - isMovedEarlier - ? overIndex - : overIndex + 1 - ]?.rank ?? ""; - const belowRank = - elements[ - isMovedEarlier - ? overIndex - 1 - : overIndex - ]?.rank ?? ""; - const [rank] = - mudder.base62.mudder( - belowRank, - aboveRank, - 1 - ); - - move(activeIndex, overIndex); - update(overIndex, { - ...activeElem, - rank, - updated: true, - }); - } - } - }} + onDragEnd={handleDragEnd} > Date: Wed, 26 Feb 2025 17:53:01 -0600 Subject: [PATCH 14/38] Cleanup --- core/lib/server/form.ts | 10 +--------- core/lib/server/pub.ts | 5 +++++ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/core/lib/server/form.ts b/core/lib/server/form.ts index 9bbfd2afd..585d18f4c 100644 --- a/core/lib/server/form.ts +++ b/core/lib/server/form.ts @@ -7,7 +7,6 @@ import { defaultComponent } from "schemas"; import type { CommunitiesId, FormsId, PublicSchema, PubsId, PubTypesId, UsersId } from "db/public"; import { AuthTokenType, ElementType, InputComponent, StructuralFormElement } from "db/public"; -import { logger } from "logger"; import type { XOR } from "../types"; import type { GetPubTypesResult } from "./pubtype"; @@ -216,14 +215,7 @@ export const insertForm = ( isDefault: boolean, trx = db ) => { - logger.debug({ msg: "inserting form", pubType, name }); - const ranks = mudder.base62.mudder( - undefined, - undefined, - pubType.fields.length + 1, - undefined, - (pubType.fields.length + 1) * 20 - ); + const ranks = mudder.base62.mudder(pubType.fields.length + 1); return trx .with("form", (db) => diff --git a/core/lib/server/pub.ts b/core/lib/server/pub.ts index 9e2a877e1..d313821b7 100644 --- a/core/lib/server/pub.ts +++ b/core/lib/server/pub.ts @@ -1205,6 +1205,11 @@ export const updatePub = async ({ return result; }; +/** + * Adds an appropriate "rank" attribute to each related pub value passed in, based on the highest + * existing rank on the relevant pub. Returns all the pub values passed in. + */ + const getRankedValues = async ({ pubId, pubValues, From 31e4822bf36b9a05473b9dff6ac3af33600df85a Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Sun, 2 Mar 2025 21:48:12 -0600 Subject: [PATCH 15/38] Make keyboard interactions work on sortable form elements --- .../app/components/FormBuilder/FormBuilder.tsx | 18 +++++++++++++++--- .../app/components/FormBuilder/FormElement.tsx | 14 ++++++++++---- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/core/app/components/FormBuilder/FormBuilder.tsx b/core/app/components/FormBuilder/FormBuilder.tsx index 676c2d54c..d89c96e70 100644 --- a/core/app/components/FormBuilder/FormBuilder.tsx +++ b/core/app/components/FormBuilder/FormBuilder.tsx @@ -5,9 +5,13 @@ import type { DragEndEvent } from "@dnd-kit/core"; import * as React from "react"; import { useCallback, useReducer, useRef } from "react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { DndContext } from "@dnd-kit/core"; +import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; import { restrictToParentElement, restrictToVerticalAxis } from "@dnd-kit/modifiers"; -import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; +import { + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; import { zodResolver } from "@hookform/resolvers/zod"; import mudder from "mudder"; import { useFieldArray, useForm } from "react-hook-form"; @@ -236,6 +240,13 @@ export function FormBuilder({ pubForm, id, stages }: Props) { const tokens = { content: renderWithPubTokens }; + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + return ( Builder Preview - +
); return ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions
-
+
{isFieldInput(element) && ( )} @@ -92,9 +95,10 @@ export const FormElement = ({ element, index, isEditing, isDisabled }: FormEleme {restoreRemoveButton} From 059d430528792d16486cff7dca8a213c6ef51b98 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Sun, 2 Mar 2025 21:48:45 -0600 Subject: [PATCH 16/38] Fix missing hover state on form elements delete/restore buttons --- core/app/components/FormBuilder/FormElement.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/app/components/FormBuilder/FormElement.tsx b/core/app/components/FormBuilder/FormElement.tsx index ca6b0c431..436dd6727 100644 --- a/core/app/components/FormBuilder/FormElement.tsx +++ b/core/app/components/FormBuilder/FormElement.tsx @@ -50,7 +50,10 @@ export const FormElement = ({ element, index, isEditing, isDisabled }: FormEleme restoreElement(index); }} > - + ) : ( @@ -64,7 +67,7 @@ export const FormElement = ({ element, index, isEditing, isDisabled }: FormEleme removeElement(index); }} > - + ); return ( From abfbdefad84cc4a5981e9abf5270f564773669c3 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Sun, 2 Mar 2025 23:04:33 -0600 Subject: [PATCH 17/38] Add test for form element sorting --- core/playwright/formBuilder.spec.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/core/playwright/formBuilder.spec.ts b/core/playwright/formBuilder.spec.ts index ebe273539..4781aadf2 100644 --- a/core/playwright/formBuilder.spec.ts +++ b/core/playwright/formBuilder.spec.ts @@ -238,3 +238,32 @@ test.describe("relationship fields", () => { await formEditPage.saveForm(); }); }); + +test.describe("reordering fields", async () => { + test("field order is persisted after saving", async () => { + const formEditPage = new FormsEditPage(page, COMMUNITY_SLUG, FORM_SLUG); + + await formEditPage.goto(); + + const elementsRegex = RegExp(`(Paragraph|${COMMUNITY_SLUG}).*`); + + const initialElements = await page + .getByRole("button", { name: elementsRegex }) + .allTextContents(); + await page.getByRole("button", { name: 'Paragraph :value{field="title' }).press(" "); + await page.keyboard.press("ArrowDown"); + await page.keyboard.press(" "); + + const changedElements = await page.getByRole("button", { name: elementsRegex }); + + // Make sure reordering worked on the client + expect(changedElements).not.toHaveText(initialElements); + + await formEditPage.saveForm(); + + // Make sure the form is returned in the same order it was saved in + await expect(page.getByRole("button", { name: elementsRegex })).toHaveText( + await changedElements.allTextContents() + ); + }); +}); From 58ce93e0dfad261fa3a06341e36a1c854100b3de Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Sun, 2 Mar 2025 23:08:25 -0600 Subject: [PATCH 18/38] Set config.label on element creation for proper default label --- core/lib/server/form.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/lib/server/form.ts b/core/lib/server/form.ts index 585d18f4c..bf369781a 100644 --- a/core/lib/server/form.ts +++ b/core/lib/server/form.ts @@ -243,7 +243,7 @@ export const insertForm = ( .values((eb) => pubType.fields.map((field, i) => ({ fieldId: field.id, - label: field.name, + config: { label: field.name }, type: ElementType.pubfield, component: defaultComponent(field.schemaName!) ?? InputComponent.relationBlock, rank: ranks[i + 1], From 9693c08d19587505ced93fb5b726cb548e99e096 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Sun, 2 Mar 2025 23:08:57 -0600 Subject: [PATCH 19/38] Actually show label changes in form builder --- core/app/components/FormBuilder/FormElement.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/app/components/FormBuilder/FormElement.tsx b/core/app/components/FormBuilder/FormElement.tsx index 436dd6727..ffc894215 100644 --- a/core/app/components/FormBuilder/FormElement.tsx +++ b/core/app/components/FormBuilder/FormElement.tsx @@ -147,7 +147,7 @@ export const FieldInputElement = ({ element, isEditing }: FieldInputElementProps
{field.slug}
- {element.label ?? field.name} + {(element.config as any)?.label ?? field.name}
From ebe6b10ac66c0ab160b1e5fd6c2872789c3d7a84 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Sun, 2 Mar 2025 23:29:26 -0600 Subject: [PATCH 20/38] Set default label correctly for relationship elements --- core/lib/server/form.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/lib/server/form.ts b/core/lib/server/form.ts index bf369781a..e312a388a 100644 --- a/core/lib/server/form.ts +++ b/core/lib/server/form.ts @@ -243,7 +243,9 @@ export const insertForm = ( .values((eb) => pubType.fields.map((field, i) => ({ fieldId: field.id, - config: { label: field.name }, + config: field.isRelation + ? { relationshipConfig: { label: field.name } } + : { label: field.name }, type: ElementType.pubfield, component: defaultComponent(field.schemaName!) ?? InputComponent.relationBlock, rank: ranks[i + 1], From 4c1903734ede59e812268ee31e924dd7eb327591 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Mon, 3 Mar 2025 00:13:33 -0600 Subject: [PATCH 21/38] Set correct input component for relationship fields on form creation --- core/lib/server/form.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/lib/server/form.ts b/core/lib/server/form.ts index e312a388a..0d1b842fe 100644 --- a/core/lib/server/form.ts +++ b/core/lib/server/form.ts @@ -247,7 +247,9 @@ export const insertForm = ( ? { relationshipConfig: { label: field.name } } : { label: field.name }, type: ElementType.pubfield, - component: defaultComponent(field.schemaName!) ?? InputComponent.relationBlock, + component: field.isRelation + ? InputComponent.relationBlock + : defaultComponent(field.schemaName!), rank: ranks[i + 1], formId: eb.selectFrom("form").select("form.id"), })) From 594105ef8c2b2277c59250e26e80916ad7c2a62d Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Mon, 3 Mar 2025 00:19:39 -0600 Subject: [PATCH 22/38] Set default config.label when adding a new element to a form --- .../app/components/FormBuilder/ElementPanel/SelectElement.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx b/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx index 6dd5ae3d0..62fa0eaa1 100644 --- a/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx +++ b/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx @@ -51,7 +51,9 @@ export const SelectElement = ({ panelState }: { panelState: PanelState }) => { type: ElementType.pubfield, rank: mudder.base62.mudder(elements[elementsCount - 1].rank, "", 1)[0], configured: false, - label: field.name, + config: field.isRelation + ? { relationshipConfig: { label: field.name } } + : { label: field.name }, component, schemaName, isRelation: field.isRelation, From b8096b89c635192253ad32dc7c9e69d89241583e Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Mon, 3 Mar 2025 10:17:08 -0600 Subject: [PATCH 23/38] Make sure config.relationshipConfig.component is set for relationship form elements --- .../FormBuilder/ElementPanel/SelectElement.tsx | 9 +++++++-- core/lib/server/form.ts | 11 +++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx b/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx index 62fa0eaa1..ddce27663 100644 --- a/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx +++ b/core/app/components/FormBuilder/ElementPanel/SelectElement.tsx @@ -2,7 +2,7 @@ import mudder from "mudder"; import { useFormContext } from "react-hook-form"; import { defaultComponent } from "schemas"; -import { ElementType, StructuralFormElement } from "db/public"; +import { ElementType, InputComponent, StructuralFormElement } from "db/public"; import { Button } from "ui/button"; import { Input } from "ui/input"; import { usePubFieldContext } from "ui/pubFields"; @@ -52,7 +52,12 @@ export const SelectElement = ({ panelState }: { panelState: PanelState }) => { rank: mudder.base62.mudder(elements[elementsCount - 1].rank, "", 1)[0], configured: false, config: field.isRelation - ? { relationshipConfig: { label: field.name } } + ? { + relationshipConfig: { + label: field.name, + component: InputComponent.relationBlock, + }, + } : { label: field.name }, component, schemaName, diff --git a/core/lib/server/form.ts b/core/lib/server/form.ts index 0d1b842fe..20f03aa42 100644 --- a/core/lib/server/form.ts +++ b/core/lib/server/form.ts @@ -244,12 +244,15 @@ export const insertForm = ( pubType.fields.map((field, i) => ({ fieldId: field.id, config: field.isRelation - ? { relationshipConfig: { label: field.name } } + ? { + relationshipConfig: { + component: InputComponent.relationBlock, + label: field.name, + }, + } : { label: field.name }, type: ElementType.pubfield, - component: field.isRelation - ? InputComponent.relationBlock - : defaultComponent(field.schemaName!), + component: defaultComponent(field.schemaName!), rank: ranks[i + 1], formId: eb.selectFrom("form").select("form.id"), })) From 4e486f7de411e6ccca7e264efe146a200aace1f8 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Mon, 3 Mar 2025 10:17:38 -0600 Subject: [PATCH 24/38] Update test now that label updates are actually reflected in the form builder --- core/playwright/formBuilder.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/playwright/formBuilder.spec.ts b/core/playwright/formBuilder.spec.ts index 4781aadf2..b94eee53e 100644 --- a/core/playwright/formBuilder.spec.ts +++ b/core/playwright/formBuilder.spec.ts @@ -175,7 +175,9 @@ test.describe("relationship fields", () => { if (request.method() === "POST" && request.url().includes(`forms/${formSlug}/edit`)) { const data = request.postDataJSON(); const { elements } = data[0]; - const authorElement = elements.find((e: PubFieldElement) => e.label === "author"); + const authorElement = elements.find( + (e: PubFieldElement) => "label" in e.config && e.config.label === "Role" + ); expect(authorElement.component).toEqual(InputComponent.textArea); expect(authorElement.config).toMatchObject({ relationshipConfig: { @@ -219,7 +221,9 @@ test.describe("relationship fields", () => { const data = request.postDataJSON(); const { elements } = data[0]; const authorElement = elements.find( - (e: PubFieldElement) => e.label === "author null" + (e: PubFieldElement) => + "relationshipConfig" in e.config && + e.config.relationshipConfig.label === "Authors" ); expect(authorElement.component).toBeNull(); expect(authorElement.config).toMatchObject({ From 5c0536a87133eea15d3c1fdec88f9dc4836f67f3 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Mon, 3 Mar 2025 11:47:26 -0600 Subject: [PATCH 25/38] Improve UX for adding relationship form element Clicking edit on a newly added (not saved to the db) field, then clicking cancel will no longer delete the element. That will only happen if you click cancel immediately after adding the new element. The save button in the form element panel will always be enabled when first configuring a new field input element. Previously the only way to add a new relationship field without making an unnecessary config edit was to click the x, since the Save button was disabled and the cancel button would remove the element from the form. --- .../ElementPanel/InputComponentConfigurationForm.tsx | 2 +- core/app/components/FormBuilder/FormBuilder.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/app/components/FormBuilder/ElementPanel/InputComponentConfigurationForm.tsx b/core/app/components/FormBuilder/ElementPanel/InputComponentConfigurationForm.tsx index 66e455ea1..cf8ba4905 100644 --- a/core/app/components/FormBuilder/ElementPanel/InputComponentConfigurationForm.tsx +++ b/core/app/components/FormBuilder/ElementPanel/InputComponentConfigurationForm.tsx @@ -414,7 +414,7 @@ export const InputComponentConfigurationForm = ({ index, fieldInputElement }: Pr data-testid="save-configuration-button" type="submit" className="bg-blue-500 hover:bg-blue-600" - disabled={!form.formState.isDirty} + disabled={!form.formState.isDirty && fieldInputElement.configured} > Save diff --git a/core/app/components/FormBuilder/FormBuilder.tsx b/core/app/components/FormBuilder/FormBuilder.tsx index d89c96e70..b2f7e4132 100644 --- a/core/app/components/FormBuilder/FormBuilder.tsx +++ b/core/app/components/FormBuilder/FormBuilder.tsx @@ -194,7 +194,7 @@ export function FormBuilder({ pubForm, id, stages }: Props) { [elements] ); const removeIfUnconfigured = useCallback(() => { - if (panelState.selectedElementIndex === null) { + if (panelState.selectedElementIndex === null || panelState.backButton !== "selecting") { return; } const element = elements[panelState.selectedElementIndex]; From cfa81e26ed77e54ee275dd8ca9fab8580ec4aba5 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Mon, 3 Mar 2025 11:52:39 -0600 Subject: [PATCH 26/38] Prevent fallback to bad slug value when rendering unconfigured relationship field --- core/app/components/forms/elements/RelatedPubsElement.tsx | 7 +++++-- core/app/components/pubs/PubEditor/PubEditor.tsx | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/core/app/components/forms/elements/RelatedPubsElement.tsx b/core/app/components/forms/elements/RelatedPubsElement.tsx index 3e6c18347..8678acc0f 100644 --- a/core/app/components/forms/elements/RelatedPubsElement.tsx +++ b/core/app/components/forms/elements/RelatedPubsElement.tsx @@ -94,8 +94,11 @@ export const ConfigureRelatedValue = ({ onBlur?: () => void; className?: string; }) => { - const configLabel = "label" in element.config ? element.config.label : undefined; - const label = configLabel || element.label || slug; + const configLabel = + "relationshipConfig" in element.config + ? element.config.relationshipConfig.label + : element.config.label; + const label = configLabel || element.label || element.slug; const { watch, formState } = useFormContext(); const [isPopoverOpen, setPopoverIsOpen] = useState(false); diff --git a/core/app/components/pubs/PubEditor/PubEditor.tsx b/core/app/components/pubs/PubEditor/PubEditor.tsx index ec2ecec59..1e24bbf5b 100644 --- a/core/app/components/pubs/PubEditor/PubEditor.tsx +++ b/core/app/components/pubs/PubEditor/PubEditor.tsx @@ -1,7 +1,7 @@ import { randomUUID } from "crypto"; import type { ProcessedPub } from "contracts"; -import type { CommunitiesId, PubFieldsId, PubsId, PubTypesId, StagesId } from "db/public"; +import type { CommunitiesId, PubsId, PubTypesId, StagesId } from "db/public"; import { expect } from "utils"; import type { FormElements, PubFieldElement } from "../../forms/types"; @@ -34,7 +34,10 @@ const RelatedPubValueElement = ({ fieldName: string; element: PubFieldElement; }) => { - const configLabel = "label" in element.config ? element.config.label : undefined; + const configLabel = + "relationshipConfig" in element.config + ? element.config.relationshipConfig.label + : element.config.label; const label = configLabel || element.label || element.slug; return ( From 72a7c8f95de7686045e6e790e01068c911049535 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Tue, 4 Mar 2025 14:12:44 -0600 Subject: [PATCH 27/38] Make sure mudder table in migrations is 0 indexed --- .../migrations/20250213201642_add_mudder_ranks/migration.sql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/prisma/migrations/20250213201642_add_mudder_ranks/migration.sql b/core/prisma/migrations/20250213201642_add_mudder_ranks/migration.sql index 7d4ff7b61..505a4014a 100644 --- a/core/prisma/migrations/20250213201642_add_mudder_ranks/migration.sql +++ b/core/prisma/migrations/20250213201642_add_mudder_ranks/migration.sql @@ -8,8 +8,10 @@ BEGIN; * form elements and related pubs in the migration. * Generated with: mudder.base62.mudder(200).map((rank) => `('${rank}')`).join(", ") */ + + INSERT INTO "mudder_ranks"("index","rank") VALUES (0,'0J'); INSERT INTO "mudder_ranks"("rank") - VALUES ('0J'), ('0c'), ('0v'), ('1'), ('1X'), ('1q'), ('2'), ('2S'), ('2m'), ('3'), ('3O'), ('3h'), + VALUES ('0c'), ('0v'), ('1'), ('1X'), ('1q'), ('2'), ('2S'), ('2m'), ('3'), ('3O'), ('3h'), ('4'), ('4J'), ('4c'), ('4v'), ('5'), ('5Y'), ('5r'), ('6'), ('6T'), ('6m'), ('7'), ('7O'), ('7i'), ('8'), ('8K'), ('8d'), ('8w'), ('9'), ('9Y'), ('9r'), ('A'), ('AU'), ('An'), ('B'), ('BP'), ('Bi'), ('C'), ('CK'), ('Ce'), ('Cx'), ('D'), ('DZ'), ('Ds'), ('E'), ('EU'), ('En'), ('F'), ('FQ'), ('Fj'), From e8ff6b95959e1bada8d134aa72cdfbd373fc4c84 Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Tue, 4 Mar 2025 18:46:05 -0500 Subject: [PATCH 28/38] chore: PR preview fixes (#1021) * chore: set log level=debug * chore: try removing healthcheck * chore: add healthcheck back * chore: attach false maybe * chore: really not sure * chore: try removing minio-init * chore: empty test commit --- .github/workflows/on_pr.yml | 10 +++++++--- docker-compose.preview.yml | 13 +++++++------ self-host/docker-compose.yml | 34 +++++++++++++++++----------------- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index bb0e58520..730dbe0a7 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -36,7 +36,11 @@ jobs: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} deploy-preview: - permissions: write-all + permissions: + contents: read + deployments: write + pull-requests: write + statuses: write runs-on: ubuntu-latest timeout-minutes: 30 needs: @@ -71,13 +75,13 @@ jobs: with: label: preview admins: 3mcd - cidrs: "0.0.0.0/0" compose_files: ./self-host/docker-compose.yml,docker-compose.preview.yml default_port: 443 instance_type: small - ports: 443,9001 + ports: 80,443,9001 registries: docker://AWS:${{steps.ecrtoken.outputs.value}}@246372085946.dkr.ecr.us-east-1.amazonaws.com env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_REGION: ${{ env.AWS_REGION }} + PULLPREVIEW_LOGGER_LEVEL: DEBUG diff --git a/docker-compose.preview.yml b/docker-compose.preview.yml index b5ea9b0cf..216926d31 100644 --- a/docker-compose.preview.yml +++ b/docker-compose.preview.yml @@ -10,12 +10,13 @@ services: ASSETS_UPLOAD_KEY: preview-different ASSETS_UPLOAD_SECRET_KEY: preview-different123 ASSETS_STORAGE_ENDPOINT: https://${PULLPREVIEW_PUBLIC_DNS}/assets - minio-init: - environment: - MINIO_ROOT_USER: preview - MINIO_ROOT_PASSWORD: preview123 - ASSETS_UPLOAD_KEY: preview-different - ASSETS_UPLOAD_SECRET_KEY: preview-different123 + # minio-init: + # restart: on-failure + # environment: + # MINIO_ROOT_USER: preview + # MINIO_ROOT_PASSWORD: preview123 + # ASSETS_UPLOAD_KEY: preview-different + # ASSETS_UPLOAD_SECRET_KEY: preview-different123 minio: environment: MINIO_ROOT_USER: preview diff --git a/self-host/docker-compose.yml b/self-host/docker-compose.yml index e68b0aece..c2cec5c3b 100644 --- a/self-host/docker-compose.yml +++ b/self-host/docker-compose.yml @@ -110,23 +110,23 @@ services: - app-network # initialize minio - minio-init: - depends_on: - minio: - condition: service_healthy - image: minio/mc:latest - env_file: .env - entrypoint: > - /bin/sh -c ' - /usr/bin/mc config host add myminio http://minio:9000 "$${MINIO_ROOT_USER}" "$${MINIO_ROOT_PASSWORD}"; - /usr/bin/mc mb --ignore-existing myminio/"$${ASSETS_BUCKET_NAME}"; - /usr/bin/mc anonymous set download myminio/"$${ASSETS_BUCKET_NAME}"; - /usr/bin/mc admin user add myminio "$${ASSETS_UPLOAD_KEY}" "$${ASSETS_UPLOAD_SECRET_KEY}"; - /usr/bin/mc admin policy attach myminio readwrite --user "$${ASSETS_UPLOAD_KEY}"; - exit 0; - ' - networks: - - app-network + # minio-init: + # depends_on: + # minio: + # condition: service_healthy + # image: minio/mc:latest + # env_file: .env + # entrypoint: > + # /bin/sh -c ' + # /usr/bin/mc config host add myminio http://minio:9000 "$${MINIO_ROOT_USER}" "$${MINIO_ROOT_PASSWORD}"; + # /usr/bin/mc mb --ignore-existing myminio/"$${ASSETS_BUCKET_NAME}"; + # /usr/bin/mc anonymous set download myminio/"$${ASSETS_BUCKET_NAME}"; + # /usr/bin/mc admin user add myminio "$${ASSETS_UPLOAD_KEY}" "$${ASSETS_UPLOAD_SECRET_KEY}"; + # /usr/bin/mc admin policy attach myminio readwrite --user "$${ASSETS_UPLOAD_KEY}"; + # exit 0; + # ' + # networks: + # - app-network volumes: caddy-data: From b3a3698f3d8b59110688b9537fc37a2d026a0f30 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Tue, 4 Mar 2025 17:51:15 -0600 Subject: [PATCH 29/38] Sort get pubs result in js --- core/lib/server/pub.ts | 10 +++++++++- core/prisma/exampleCommunitySeeds/arcadia.ts | 6 +++--- packages/contracts/src/resources/site.ts | 8 +++----- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/core/lib/server/pub.ts b/core/lib/server/pub.ts index d313821b7..c6cb19e45 100644 --- a/core/lib/server/pub.ts +++ b/core/lib/server/pub.ts @@ -1429,6 +1429,7 @@ export type UnprocessedPub = { schemaName: CoreSchemaType; fieldSlug: string; fieldName: string; + rank: string; }[]; children?: { id: PubsId }[]; }; @@ -2062,7 +2063,14 @@ function nestRelatedPubsAndChildren { + // Sort values by fieldId, rank + if (a.fieldId === b.fieldId && a.rank !== null && b.rank !== null) { + return a.rank > b.rank ? 1 : -1; + } + return a.fieldId.localeCompare(b.fieldId); + }) ?? [], children: processedChildren ?? undefined, } as ProcessedPub; diff --git a/core/prisma/exampleCommunitySeeds/arcadia.ts b/core/prisma/exampleCommunitySeeds/arcadia.ts index 5960283a5..7fe509b2d 100644 --- a/core/prisma/exampleCommunitySeeds/arcadia.ts +++ b/core/prisma/exampleCommunitySeeds/arcadia.ts @@ -128,7 +128,6 @@ export const seedArcadia = async (communityId?: CommunitiesId) => { } return { - value: null, alsoAsChild: true, pub, }; @@ -195,7 +194,7 @@ export const seedArcadia = async (communityId?: CommunitiesId) => { "Issue Number": { schemaName: CoreSchemaType.Number }, ISSN: { schemaName: CoreSchemaType.String }, Issues: { schemaName: CoreSchemaType.Null, relation: true }, - Articles: { schemaName: CoreSchemaType.Null, relation: true }, + Articles: { schemaName: CoreSchemaType.String, relation: true }, Journals: { schemaName: CoreSchemaType.Null, relation: true }, // site settings @@ -534,7 +533,6 @@ export const seedArcadia = async (communityId?: CommunitiesId) => { relatedPubs: { Articles: [ { - value: null, alsoAsChild: true, pub: { id: articleId, @@ -739,6 +737,7 @@ export const seedArcadia = async (communityId?: CommunitiesId) => { value: '"Edited"', fieldId: seed.pubFields.Articles.id, lastModifiedBy: createLastModifiedBy("system"), + rank: "0", }, { pubId: authorId as PubsId, @@ -746,6 +745,7 @@ export const seedArcadia = async (communityId?: CommunitiesId) => { relatedPubId: articleId2 as PubsId, fieldId: seed.pubFields.Articles.id, lastModifiedBy: createLastModifiedBy("system"), + rank: "1", }, ]) .execute(); diff --git a/packages/contracts/src/resources/site.ts b/packages/contracts/src/resources/site.ts index bf94bdb84..1849688b3 100644 --- a/packages/contracts/src/resources/site.ts +++ b/packages/contracts/src/resources/site.ts @@ -1,14 +1,11 @@ -import type { AppRouteResponse, ContractOtherResponse, Opaque } from "@ts-rest/core"; - import { initContract } from "@ts-rest/core"; -import { z, ZodNull } from "zod"; +import { z } from "zod"; import type { CommunitiesId, CoreSchemaType, MemberRole, PubFields, - PubFieldSchemaId, PubFieldsId, PubsId, PubTypes, @@ -21,7 +18,6 @@ import type { } from "db/public"; import { communitiesIdSchema, - communityMembershipsIdSchema, communityMembershipsSchema, coreSchemaTypeSchema, memberRoleSchema, @@ -225,6 +221,7 @@ type ValueBase = { schemaName: CoreSchemaType; fieldSlug: string; fieldName: string; + rank: string | null; }; type ProcessedPubBase = { @@ -289,6 +286,7 @@ const processedPubSchema: z.ZodType = z.object({ schemaName: coreSchemaTypeSchema, relatedPubId: pubsIdSchema.nullable(), relatedPub: z.lazy(() => processedPubSchema.nullish()), + rank: z.string().nullable(), }) ), createdAt: z.date(), From 328ef0df98b747b686c18ee355d697b418dae53d Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Tue, 4 Mar 2025 17:53:02 -0600 Subject: [PATCH 30/38] Fix merge mistake --- core/app/components/pubs/PubEditor/PubEditor.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/app/components/pubs/PubEditor/PubEditor.tsx b/core/app/components/pubs/PubEditor/PubEditor.tsx index b663bcee3..a0bc0a1d0 100644 --- a/core/app/components/pubs/PubEditor/PubEditor.tsx +++ b/core/app/components/pubs/PubEditor/PubEditor.tsx @@ -2,6 +2,7 @@ import { randomUUID } from "crypto"; import type { ProcessedPub } from "contracts"; import type { CommunitiesId, PubsId, PubTypesId, StagesId } from "db/public"; +import { CoreSchemaType } from "db/public"; import { expect } from "utils"; import type { FormElements, PubFieldElement } from "../../forms/types"; @@ -9,7 +10,7 @@ import type { RenderWithPubContext } from "~/lib/server/render/pub/renderWithPub import type { AutoReturnType, PubField } from "~/lib/types"; import { db } from "~/kysely/database"; import { getLoginData } from "~/lib/authentication/loginData"; -import { getPubTitle } from "~/lib/pubs"; +import { getPubTitle } from "~/lib/pubs"; import { getForm } from "~/lib/server/form"; import { getPubsWithRelatedValuesAndChildren } from "~/lib/server/pub"; import { getPubFields } from "~/lib/server/pubFields"; From 62fadeb08ec1af1037a89585ba43557d722707a6 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Mon, 3 Mar 2025 18:26:21 -0600 Subject: [PATCH 31/38] Add ranks to values in datacite test --- core/actions/datacite/run.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/actions/datacite/run.test.ts b/core/actions/datacite/run.test.ts index 2870ae09f..382701e67 100644 --- a/core/actions/datacite/run.test.ts +++ b/core/actions/datacite/run.test.ts @@ -75,6 +75,7 @@ const pub = { updatedAt: new Date(), schemaName: CoreSchemaType.String, relatedPubId: null, + rank: "a", }, { id: "" as PubValuesId, @@ -86,6 +87,7 @@ const pub = { updatedAt: new Date(), schemaName: CoreSchemaType.String, relatedPubId: null, + rank: "b", }, { id: "" as PubValuesId, @@ -97,6 +99,7 @@ const pub = { updatedAt: new Date(), schemaName: CoreSchemaType.URL, relatedPubId: null, + rank: "c", }, { id: "" as PubValuesId, @@ -108,6 +111,7 @@ const pub = { updatedAt: new Date(), schemaName: CoreSchemaType.DateTime, relatedPubId: null, + rank: "d", }, ], children: [], From 585e6cc347b275c63ac99245ae822885a6b52180 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Wed, 5 Mar 2025 07:58:30 -0600 Subject: [PATCH 32/38] Allow tabbing to form element buttons Co-authored-by: Thomas F. K. Jorna --- .../components/FormBuilder/FormElement.tsx | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/core/app/components/FormBuilder/FormElement.tsx b/core/app/components/FormBuilder/FormElement.tsx index ffc894215..927beeed0 100644 --- a/core/app/components/FormBuilder/FormElement.tsx +++ b/core/app/components/FormBuilder/FormElement.tsx @@ -1,4 +1,3 @@ -import type { KeyboardEventHandler } from "react"; import type { FieldArrayWithId } from "react-hook-form"; import { useSortable } from "@dnd-kit/sortable"; @@ -44,16 +43,13 @@ export const FormElement = ({ element, index, isEditing, isDisabled }: FormEleme type="button" disabled={isDisabled} variant="ghost" - className="invisible p-2 hover:bg-white group-hover:visible" + className="p-2 opacity-0 hover:bg-white group-focus-within:opacity-100 group-hover:opacity-100 [&_svg]:pointer-events-auto [&_svg]:hover:text-red-500" aria-label="Restore element" onClick={() => { restoreElement(index); }} > - + ) : ( @@ -61,22 +57,19 @@ export const FormElement = ({ element, index, isEditing, isDisabled }: FormEleme type="button" disabled={isDisabled} variant="ghost" - className="invisible p-2 hover:bg-white group-hover:visible" + className="p-2 opacity-0 hover:bg-white group-focus-within:opacity-100 group-hover:opacity-100 [&_svg]:pointer-events-auto [&_svg]:hover:text-red-500" aria-label="Delete element" onClick={() => { removeElement(index); }} > - + ); return ( - // eslint-disable-next-line jsx-a11y/no-static-element-interactions
{ openConfigPanel(index); }} @@ -113,9 +106,10 @@ export const FormElement = ({ element, index, isEditing, isDisabled }: FormEleme aria-label="Drag handle" disabled={isDisabled || element.deleted} variant="ghost" - className="invisible p-1.5 group-hover:visible group-focus:visible" + className="p-1.5 opacity-0 group-focus-within:opacity-100 group-hover:opacity-100" {...listeners} {...attributes} + tabIndex={0} > From 92072926d25883ab9381f9b3ad5fa5d64280dc49 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Wed, 5 Mar 2025 09:58:46 -0600 Subject: [PATCH 33/38] Fix arcadia seed --- core/prisma/exampleCommunitySeeds/arcadia.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/prisma/exampleCommunitySeeds/arcadia.ts b/core/prisma/exampleCommunitySeeds/arcadia.ts index 7fe509b2d..a2d03f1bc 100644 --- a/core/prisma/exampleCommunitySeeds/arcadia.ts +++ b/core/prisma/exampleCommunitySeeds/arcadia.ts @@ -128,6 +128,7 @@ export const seedArcadia = async (communityId?: CommunitiesId) => { } return { + value: "", alsoAsChild: true, pub, }; @@ -535,6 +536,7 @@ export const seedArcadia = async (communityId?: CommunitiesId) => { { alsoAsChild: true, pub: { + value: "", id: articleId, pubType: "Journal Article", stage: "Articles", From 2121ea893ec5233ffbf16c297e19ca2d80af5437 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Wed, 5 Mar 2025 13:33:52 -0600 Subject: [PATCH 34/38] Remove unnecessary sorting --- .../actions/getActionRunsTableColumns.tsx | 10 +++------- .../c/[communitySlug]/activity/actions/page.tsx | 17 +---------------- .../pubs/[pubId]/components/queries.ts | 5 +---- core/lib/server/pub.ts | 1 - 4 files changed, 5 insertions(+), 28 deletions(-) diff --git a/core/app/c/[communitySlug]/activity/actions/getActionRunsTableColumns.tsx b/core/app/c/[communitySlug]/activity/actions/getActionRunsTableColumns.tsx index ac429d979..d6a58c5dd 100644 --- a/core/app/c/[communitySlug]/activity/actions/getActionRunsTableColumns.tsx +++ b/core/app/c/[communitySlug]/activity/actions/getActionRunsTableColumns.tsx @@ -4,10 +4,12 @@ import type { ColumnDef } from "@tanstack/react-table"; import Link from "next/link"; +import type { PubsId } from "db/public"; import { Badge } from "ui/badge"; import { DataTableColumnHeader } from "ui/data-table"; import { HoverCard, HoverCardContent, HoverCardTrigger } from "ui/hover-card"; +import type { PubTitleProps } from "~/lib/pubs"; import { PubTitle } from "~/app/components/PubTitle"; export type ActionRun = { @@ -15,13 +17,7 @@ export type ActionRun = { createdAt: Date; actionInstance: { name: string; action: string } | null; stage: { id: string; name: string } | null; - pub: { - id: string; - values: { field: { slug: string }; value: unknown }[] | Record; - createdAt: Date; - pubType: { name: string }; - title: string | null; - } | null; + pub: PubTitleProps & { id: PubsId }; result: unknown; } & ( | { diff --git a/core/app/c/[communitySlug]/activity/actions/page.tsx b/core/app/c/[communitySlug]/activity/actions/page.tsx index 82b72eab2..2c6a99548 100644 --- a/core/app/c/[communitySlug]/activity/actions/page.tsx +++ b/core/app/c/[communitySlug]/activity/actions/page.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next"; import { notFound, redirect } from "next/navigation"; -import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres"; +import { jsonObjectFrom } from "kysely/helpers/postgres"; import { Capabilities, MembershipType } from "db/public"; @@ -76,21 +76,6 @@ export default async function Page(props: { .selectFrom("pubs") .select(["pubs.id", "pubs.createdAt", "pubs.title"]) .whereRef("pubs.id", "=", "action_runs.pubId") - .select((eb) => - jsonArrayFrom( - eb - .selectFrom("pub_values") - .leftJoin("pub_fields", "pub_values.fieldId", "pub_fields.id") - .select([ - "pub_values.value", - "pub_fields.name as fieldName", - "pub_fields.schemaName as schemaName", - "pub_fields.slug as fieldSlug", - ]) - .whereRef("pub_values.pubId", "=", "pubs.id") - .orderBy(["pub_values.fieldId", "pub_values.rank"]) - ).as("values") - ) .select((eb) => pubType({ eb, pubTypeIdRef: "pubs.pubTypeId" })) ).as("pub"), jsonObjectFrom( diff --git a/core/app/c/[communitySlug]/pubs/[pubId]/components/queries.ts b/core/app/c/[communitySlug]/pubs/[pubId]/components/queries.ts index 47fb67e25..98e67a647 100644 --- a/core/app/c/[communitySlug]/pubs/[pubId]/components/queries.ts +++ b/core/app/c/[communitySlug]/pubs/[pubId]/components/queries.ts @@ -53,12 +53,9 @@ const memberFields = (pubId: Expression) => .whereRef("pub_values.pubId", "=", pubId) .where("pub_fields.schemaName", "=", CoreSchemaType.MemberId) .distinctOn("pub_fields.id") - .orderBy(["pub_fields.id", "pub_values.rank", "pub_values.createdAt desc"]) + .orderBy(["pub_fields.id", "pub_values.updatedAt desc"]) ); -const pubType = (pubTypeId: Expression) => - jsonObjectFrom(getPubTypeBase().whereRef("pub_types.id", "=", pubTypeId)); - export const getPubChildrenTable = (parentId: PubsId, selectedPubTypeId?: PubTypesId) => { return autoCache( db diff --git a/core/lib/server/pub.ts b/core/lib/server/pub.ts index c6cb19e45..fc30d3841 100644 --- a/core/lib/server/pub.ts +++ b/core/lib/server/pub.ts @@ -713,7 +713,6 @@ export const deletePub = async ({ const pubValues = await trx .selectFrom("pub_values") .where("pubId", "in", Array.isArray(pubId) ? pubId : [pubId]) - .orderBy(["pub_values.fieldId", "pub_values.rank"]) .selectAll() .execute(); From d5fedc6860f00621021920224b3ec435efb61714 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Wed, 5 Mar 2025 13:42:47 -0600 Subject: [PATCH 35/38] Sort pub values in the database --- core/lib/server/pub.ts | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/core/lib/server/pub.ts b/core/lib/server/pub.ts index fc30d3841..d8df9bbd2 100644 --- a/core/lib/server/pub.ts +++ b/core/lib/server/pub.ts @@ -125,7 +125,13 @@ const pubValues = ( .as("fields"), (join) => join.onRef("fields.id", "=", "pub_values.fieldId") ) - .orderBy(["pub_values.fieldId", "pub_values.rank"]) + .orderBy([ + (eb) => + sql`${eb.fn + .max("pub_values.updatedAt") + .over((ob) => ob.partitionBy("pub_values.fieldId"))} desc`, + "pub_values.rank", + ]) .$if(!!pubId, (qb) => qb.where("pub_values.pubId", "=", pubId!)) .$if(!!pubIdRef, (qb) => qb.whereRef("pub_values.pubId", "=", ref(pubIdRef!))) .as(alias) @@ -1914,6 +1920,7 @@ export async function getPubsWithRelatedValuesAndChildren< "pv.id as id", "pv.fieldId", "pv.value", + "pv.rank", "pv.relatedPubId", "pv.createdAt as createdAt", "pv.updatedAt as updatedAt", @@ -1922,7 +1929,15 @@ export async function getPubsWithRelatedValuesAndChildren< "pub_fields.name as fieldName", ]) .whereRef("pv.pubId", "=", "pt.pubId") - .orderBy("pv.createdAt desc") + // Order by most recently updated value (grouped by pub field), then rank + .orderBy([ + (eb) => + // Equivalent to: max(pv."updatedAt") over(partition by pv."fieldId") desc + sql`${eb.fn + .max("pv.updatedAt") + .over((ob) => ob.partitionBy("pv.fieldId"))} desc`, + "pv.rank", + ]) ).as("values") ) ) @@ -2062,14 +2077,7 @@ function nestRelatedPubsAndChildren { - // Sort values by fieldId, rank - if (a.fieldId === b.fieldId && a.rank !== null && b.rank !== null) { - return a.rank > b.rank ? 1 : -1; - } - return a.fieldId.localeCompare(b.fieldId); - }) ?? [], + values: processedValues ?? [], children: processedChildren ?? undefined, } as ProcessedPub; From 8ba8fd6a76b87356f60aef919fc1a299ae623929 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Wed, 5 Mar 2025 14:02:21 -0600 Subject: [PATCH 36/38] Remove rank from returned values --- core/actions/datacite/run.test.ts | 4 ---- core/lib/server/pub.ts | 2 -- packages/contracts/src/resources/site.ts | 2 -- 3 files changed, 8 deletions(-) diff --git a/core/actions/datacite/run.test.ts b/core/actions/datacite/run.test.ts index 382701e67..2870ae09f 100644 --- a/core/actions/datacite/run.test.ts +++ b/core/actions/datacite/run.test.ts @@ -75,7 +75,6 @@ const pub = { updatedAt: new Date(), schemaName: CoreSchemaType.String, relatedPubId: null, - rank: "a", }, { id: "" as PubValuesId, @@ -87,7 +86,6 @@ const pub = { updatedAt: new Date(), schemaName: CoreSchemaType.String, relatedPubId: null, - rank: "b", }, { id: "" as PubValuesId, @@ -99,7 +97,6 @@ const pub = { updatedAt: new Date(), schemaName: CoreSchemaType.URL, relatedPubId: null, - rank: "c", }, { id: "" as PubValuesId, @@ -111,7 +108,6 @@ const pub = { updatedAt: new Date(), schemaName: CoreSchemaType.DateTime, relatedPubId: null, - rank: "d", }, ], children: [], diff --git a/core/lib/server/pub.ts b/core/lib/server/pub.ts index d8df9bbd2..837778cdb 100644 --- a/core/lib/server/pub.ts +++ b/core/lib/server/pub.ts @@ -1434,7 +1434,6 @@ export type UnprocessedPub = { schemaName: CoreSchemaType; fieldSlug: string; fieldName: string; - rank: string; }[]; children?: { id: PubsId }[]; }; @@ -1920,7 +1919,6 @@ export async function getPubsWithRelatedValuesAndChildren< "pv.id as id", "pv.fieldId", "pv.value", - "pv.rank", "pv.relatedPubId", "pv.createdAt as createdAt", "pv.updatedAt as updatedAt", diff --git a/packages/contracts/src/resources/site.ts b/packages/contracts/src/resources/site.ts index 1849688b3..3c3daefa8 100644 --- a/packages/contracts/src/resources/site.ts +++ b/packages/contracts/src/resources/site.ts @@ -221,7 +221,6 @@ type ValueBase = { schemaName: CoreSchemaType; fieldSlug: string; fieldName: string; - rank: string | null; }; type ProcessedPubBase = { @@ -286,7 +285,6 @@ const processedPubSchema: z.ZodType = z.object({ schemaName: coreSchemaTypeSchema, relatedPubId: pubsIdSchema.nullable(), relatedPub: z.lazy(() => processedPubSchema.nullish()), - rank: z.string().nullable(), }) ), createdAt: z.date(), From eb4aee69c89bcafc76a72d918446973874613be8 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Wed, 5 Mar 2025 14:05:25 -0600 Subject: [PATCH 37/38] Update tests to ignore return order of pub values --- core/lib/__tests__/matchers.ts | 18 +++++++++++------- core/lib/server/pub.db.test.ts | 33 +++++++++++++++------------------ 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/core/lib/__tests__/matchers.ts b/core/lib/__tests__/matchers.ts index 75f3d31c6..76bebc4fa 100644 --- a/core/lib/__tests__/matchers.ts +++ b/core/lib/__tests__/matchers.ts @@ -5,15 +5,19 @@ import type { PubsId } from "db/public"; import type { db } from "~/kysely/database"; -const deepSortValues = (pub: ProcessedPub): ProcessedPub => { - pub.values +const deepSortValues = ( + values: Partial[] +): Partial[] => { + values .sort((a, b) => (a.value as string).localeCompare(b.value as string)) .map((item) => ({ ...item, - relatedPub: item.relatedPub?.values ? deepSortValues(item.relatedPub) : item.relatedPub, + relatedPub: item.relatedPub?.values + ? deepSortValues(item.relatedPub.values) + : item.relatedPub, })); - return pub; + return values; }; expect.extend({ @@ -35,10 +39,10 @@ expect.extend({ toHaveValues(received: ProcessedPub, expected: Partial[]) { const pub = received; - const sortedPubValues = deepSortValues(pub); + const sortedPubValues = deepSortValues(pub.values); const expectedLength = expected.length; - const receivedLength = sortedPubValues.values.length; + const receivedLength = sortedPubValues.length; const isNot = this.isNot; if (!isNot && !this.equals(expectedLength, receivedLength)) { @@ -50,7 +54,7 @@ expect.extend({ } // equiv. to .toMatchObject - const pass = this.equals(sortedPubValues.values, expected, [ + const pass = this.equals(sortedPubValues, deepSortValues(expected), [ this.utils.iterableEquality, this.utils.subsetEquality, ]); diff --git a/core/lib/server/pub.db.test.ts b/core/lib/server/pub.db.test.ts index 462268377..472a43f53 100644 --- a/core/lib/server/pub.db.test.ts +++ b/core/lib/server/pub.db.test.ts @@ -1142,15 +1142,13 @@ describe("upsertPubRelations", () => { { depth: 10 } ); - expect(updatedPub).toMatchObject({ - values: [ - { value: "test title" }, - { - value: "test relation value", - relatedPub: { values: [{ value: "Some title" }] }, - }, - ], - }); + expect(updatedPub).toHaveValues([ + { value: "test title" }, + { + value: "test relation value", + relatedPub: { values: [{ value: "Some title" }] }, + }, + ]); }); it("should be able to create new pubs as relations", async () => { @@ -1198,15 +1196,14 @@ describe("upsertPubRelations", () => { { depth: 10 } ); - expect(updatedPub).toMatchObject({ - values: [ - { value: "test title" }, - { - value: "test relation value", - relatedPub: { values: [{ value: "new related pub" }] }, - }, - ], - }); + expect(updatedPub).toHaveValues([ + { fieldSlug: pubFields["Title"].slug, value: "test title" }, + { + fieldSlug: pubFields["Some relation"].slug, + value: "test relation value", + relatedPub: { values: [{ value: "new related pub" }] }, + }, + ]); }); it("should validate relation values against schema", async () => { From 1d0cda062f245bfb6a0cf6219a6781a7e9decfa4 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Wed, 5 Mar 2025 15:37:10 -0600 Subject: [PATCH 38/38] Update sort test to account for new drag handle setup --- .../components/FormBuilder/FormBuilder.tsx | 5 ++-- .../components/FormBuilder/FormElement.tsx | 27 +++++++++++++------ core/playwright/formBuilder.spec.ts | 19 +++++++------ 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/core/app/components/FormBuilder/FormBuilder.tsx b/core/app/components/FormBuilder/FormBuilder.tsx index b2f7e4132..a420cdf66 100644 --- a/core/app/components/FormBuilder/FormBuilder.tsx +++ b/core/app/components/FormBuilder/FormBuilder.tsx @@ -280,6 +280,7 @@ export function FormBuilder({ pubForm, id, stages }: Props) { logger.error({ msg: "unable to submit form", @@ -294,7 +295,7 @@ export function FormBuilder({ pubForm, id, stages }: Props) { name="elements" render={() => ( <> -
+
    -
+
Deleted on save
@@ -67,7 +70,8 @@ export const FormElement = ({ element, index, isEditing, isDisabled }: FormEleme ); return ( -
{isFieldInput(element) && ( - + )} {isStructuralElement(element) && ( - + )} {isEditing ? (
EDITING
@@ -116,15 +120,16 @@ export const FormElement = ({ element, index, isEditing, isDisabled }: FormEleme
)}
-
+ ); }; type FieldInputElementProps = { element: InputElement; isEditing: boolean; + labelId?: string; }; -export const FieldInputElement = ({ element, isEditing }: FieldInputElementProps) => { +export const FieldInputElement = ({ element, isEditing, labelId }: FieldInputElementProps) => { const pubFields = usePubFieldContext(); const field = pubFields[element.fieldId as PubFieldsId]; @@ -140,7 +145,10 @@ export const FieldInputElement = ({ element, isEditing }: FieldInputElementProps />
{field.slug}
-
+
{(element.config as any)?.label ?? field.name}
@@ -151,8 +159,9 @@ export const FieldInputElement = ({ element, isEditing }: FieldInputElementProps type StructuralElementProps = { element: StructuralElement; isEditing: boolean; + labelId?: string; }; -const StructuralElement = ({ element, isEditing }: StructuralElementProps) => { +const StructuralElement = ({ element, isEditing, labelId }: StructuralElementProps) => { const { Icon, name } = structuralElements[element.element]; return ( @@ -166,7 +175,9 @@ const StructuralElement = ({ element, isEditing }: StructuralElementProps) => { )} />
-
{name}
+
+ {name} +
{/* TODO: sanitize links, truncate, generally improve styles for rendered content*/} {element.content} diff --git a/core/playwright/formBuilder.spec.ts b/core/playwright/formBuilder.spec.ts index b94eee53e..d6150f129 100644 --- a/core/playwright/formBuilder.spec.ts +++ b/core/playwright/formBuilder.spec.ts @@ -249,25 +249,24 @@ test.describe("reordering fields", async () => { await formEditPage.goto(); - const elementsRegex = RegExp(`(Paragraph|${COMMUNITY_SLUG}).*`); + const elements = page.getByRole("form", { name: "Form builder" }).getByRole("listitem"); + const initialElements = await elements.allTextContents(); - const initialElements = await page - .getByRole("button", { name: elementsRegex }) - .allTextContents(); - await page.getByRole("button", { name: 'Paragraph :value{field="title' }).press(" "); + await page.getByRole("button", { name: "Drag handle" }).first().press(" "); await page.keyboard.press("ArrowDown"); await page.keyboard.press(" "); - const changedElements = await page.getByRole("button", { name: elementsRegex }); + await page.getByRole("button", { name: "Drag handle" }).last().press(" "); + await page.keyboard.press("ArrowUp"); + await page.keyboard.press(" "); // Make sure reordering worked on the client - expect(changedElements).not.toHaveText(initialElements); + await expect(elements).not.toHaveText(initialElements); + const changedElements = await elements.allTextContents(); await formEditPage.saveForm(); // Make sure the form is returned in the same order it was saved in - await expect(page.getByRole("button", { name: elementsRegex })).toHaveText( - await changedElements.allTextContents() - ); + await expect(elements).toHaveText(changedElements); }); });