From 7372be0398fc2f9e7793169f74bae4e376e94407 Mon Sep 17 00:00:00 2001 From: Gabriel Stein Date: Tue, 10 Oct 2023 16:58:49 -0400 Subject: [PATCH 01/27] add all evaluation fields --- .../prisma/exampleCommunitySeeds/unjournal.ts | 198 ++++++++++++++++-- 1 file changed, 180 insertions(+), 18 deletions(-) diff --git a/core/prisma/exampleCommunitySeeds/unjournal.ts b/core/prisma/exampleCommunitySeeds/unjournal.ts index dffde8d07d..aa9518b76b 100644 --- a/core/prisma/exampleCommunitySeeds/unjournal.ts +++ b/core/prisma/exampleCommunitySeeds/unjournal.ts @@ -54,8 +54,9 @@ export default async function main(prisma: PrismaClient, communityUUID: string) namespace: "unjournal", schema: { $id: "unjournal:metrics", - title: "Metrics and Predictions", - description: "Responses will be public. See here for details on the categories.", + title: "Metrics", + description: + "Responses will be public. See here for details on the categories.", type: "object", properties: { assessment: { @@ -74,7 +75,7 @@ export default async function main(prisma: PrismaClient, communityUUID: string) properties: confidenceObject, }, logic: { - title: "Logic & Communication", + title: "Logic & communication", type: "object", properties: confidenceObject, }, @@ -89,7 +90,7 @@ export default async function main(prisma: PrismaClient, communityUUID: string) properties: confidenceObject, }, relevance: { - title: "Engaging with real-world, impact quantification; practice, realism, and relevance", + title: "Relevance to global priorities", type: "object", properties: confidenceObject, }, @@ -112,35 +113,190 @@ export default async function main(prisma: PrismaClient, communityUUID: string) }, }); - const fieldIds = [...Array(12)].map(() => uuidv4()); + const predictionsSchema = await prisma.pubFieldSchema.create({ + data: { + name: "predictions", + namespace: "unjournal", + schema: { + $id: "unjournal:predictions", + title: "Prediction metric", + description: + "Responses will be public. See here for details on the metrics.", + type: "object", + properties: { + qualityJournal: { + title: "What 'quality journal' do you expect this work will this be published in?", + type: "object", + properties: confidenceObject, + }, + qualityLevel: { + title: "Overall assessment on 'scale of journals'; i.e., quality-level of journal it should be published in.", + type: "object", + properties: confidenceObject, + }, + }, + }, + }, + }); + + const confidentialCommentsSchema = await prisma.pubFieldSchema.create({ + data: { + name: "confidential-comments", + namespace: "unjournal", + schema: { + $id: "unjournal:confidential-comments", + title: "Please write confidential comments here", + description: + "Response will not be public or seen by authors, please use this section only for comments that are personal/sensitivity in nature and place most of your evaluation in the public section).", + type: "string", + }, + }, + }); + + const surveySchema = await prisma.pubFieldSchema.create({ + data: { + name: "survey", + namespace: "unjournal", + schema: { + $id: "unjournal:survey", + title: "Survey questions", + description: "Responses will be public unless you ask us to keep them private.", + type: "object", + properties: { + field: { + title: "How long have you been in this field?", + type: "string", + }, + papers: { + title: "How many proposals, papers, and projects have you evaluated/reviewed (for journals, grants, or other peer-review)?", + type: "string", + }, + }, + }, + }, + }); + + const feedbackSchema = await prisma.pubFieldSchema.create({ + data: { + name: "feedback", + namespace: "unjournal", + schema: { + $id: "unjournal:feedback", + title: "Feedback", + description: "Responses will not be public or seen by authors.", + type: "object", + properties: { + rating: { + title: "How would you rate this template and process?", + type: "string", + }, + suggestions: { + title: "Do you have any suggestions or questions about this process or the Unjournal? (We will try to respond, and incorporate your suggestions.)", + type: "string", + }, + time: { + title: "Approximately how long did you spend completing this evaluation?", + type: "string", + }, + revision: { + title: "Would you be willing to consider evaluating a revised version of this work?", + type: "boolean", + }, + }, + }, + }, + }); + + const anonymitySchema = await prisma.pubFieldSchema.create({ + data: { + name: "anonymity", + namespace: "unjournal", + schema: { + $id: "unjournal:anonymity", + title: "Would you like to publicly sign your review?", + description: + "If no, the public sections of your review will be published anonymously.", + type: "boolean", + }, + }, + }); + + const evaluationSchema = await prisma.pubFieldSchema.create({ + data: { + name: "evaluation", + namespace: "unjournal", + schema: { + $id: "unjournal:evaluation", + title: "Please write your evaluation here", + description: + "Remember that your responses will be made public. Please consult our criteria. We are essentially asking for a 'standard high-quality referee report' here, with some specific considerations (mentioned in the above link). We welcome detail, elaboration, and technical discussion. If you prefer to link or submit your evaluation content in a different format, please link it here or send it to the corresponding/managing editor. Length and time spent: This is of course, up to you. The Econometrics society recommends a 2-3 page referee report. In a recent survey (Charness et al, 2022), economists report spending (median and mean) about one day per report, with substantial shares reporting ‘half a day’ and ‘two days’. We expect that that reviewers tend to spend more time on papers for high-status journals, and when reviewing work closely tied to their own agenda.", + type: "boolean", + }, + }, + }); + + const fieldIds = [...Array(15)].map(() => uuidv4()); await prisma.pubField.createMany({ data: [ { id: fieldIds[0], name: "Title", slug: "unjournal:title" }, { id: fieldIds[1], name: "Description", slug: "unjournal:description" }, { id: fieldIds[2], name: "Manager's Notes", slug: "unjournal:managers-notes" }, - { id: fieldIds[3], name: "Anonymity", slug: "unjournal:anonymity" }, - { id: fieldIds[4], name: "Metrics", slug: "unjournal:metrics" }, - { id: fieldIds[5], name: "Content", slug: "unjournal:content" }, + { + id: fieldIds[3], + name: "Anonymity", + pubFieldSchemaId: anonymitySchema.id, + slug: "unjournal:anonymity", + }, + { + id: fieldIds[4], + name: "Please enter your 'salted hashtag' here if you know it. Otherwise please enter an anonymous psuedonym here", + slug: "unjournal:hashtag", + }, + { + id: fieldIds[5], + name: "Evaluation", + pubFieldSchemaId: evaluationSchema.id, + slug: "unjournal:evaluation", + }, { id: fieldIds[6], name: "Evaluated Paper", slug: "unjournal:evaluated-paper" }, { id: fieldIds[7], name: "Tags", slug: "unjournal:tags" }, { id: fieldIds[8], name: "DOI", slug: "unjournal:doi" }, { id: fieldIds[9], - name: "Submission Evaluator", - pubFieldSchemaId: evaluator.id, - slug: "unjournal:evaluator", + name: "Metrics", + pubFieldSchemaId: metricsSchema.id, + slug: "unjournal:metrics", }, { id: fieldIds[10], - name: "Metrics and Predictions", - pubFieldSchemaId: metricsSchema.id, - slug: "unjournal:metrics-predictions", + name: "Predictions", + slug: "unjournal:predictions", + pubFieldSchemaId: predictionsSchema.id, }, { id: fieldIds[11], + name: "Confidential Comments", + slug: "unjournal:confidential-comments", + pubFieldSchemaId: confidentialCommentsSchema.id, + }, + { + id: fieldIds[12], name: "Survey Questions", - slug: "unjournal:survey-questions", + slug: "unjournal:survey", + pubFieldSchemaId: surveySchema.id, + }, + { + id: fieldIds[13], + name: "Feedback", + slug: "unjournal:feedback", + pubFieldSchemaId: feedbackSchema.id, + }, + { + id: fieldIds[14], + name: "Submission Evaluator", + pubFieldSchemaId: evaluator.id, + slug: "unjournal:evaluator", }, ], }); @@ -189,9 +345,15 @@ export default async function main(prisma: PrismaClient, communityUUID: string) fields: { connect: [ { id: fieldIds[0] }, - { id: fieldIds[1] }, - { id: fieldIds[9] }, // Submission Evaluator - { id: fieldIds[10] }, // Metrics and Predictions + { id: fieldIds[3] }, + { id: fieldIds[4] }, + { id: fieldIds[5] }, + { id: fieldIds[9] }, + { id: fieldIds[10] }, + { id: fieldIds[11] }, + { id: fieldIds[12] }, + { id: fieldIds[13] }, + { id: fieldIds[14] }, // evaluator ], }, }, From ea7316cc9d8c9af972489555ee5de67b83ae0a57 Mon Sep 17 00:00:00 2001 From: Gabriel Stein Date: Tue, 17 Oct 2023 17:26:08 -0400 Subject: [PATCH 02/27] add checkbox and fix seed fiel --- .../prisma/exampleCommunitySeeds/unjournal.ts | 2 +- packages/sdk/src/react/generateFormFields.tsx | 11 +++- packages/ui/package.json | 1 + packages/ui/src/checkbox.tsx | 30 +++++++++++ packages/ui/src/index.tsx | 1 + pnpm-lock.yaml | 50 +++++++++++++++++-- 6 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 packages/ui/src/checkbox.tsx diff --git a/core/prisma/exampleCommunitySeeds/unjournal.ts b/core/prisma/exampleCommunitySeeds/unjournal.ts index aa9518b76b..2d7070b0b5 100644 --- a/core/prisma/exampleCommunitySeeds/unjournal.ts +++ b/core/prisma/exampleCommunitySeeds/unjournal.ts @@ -230,7 +230,7 @@ export default async function main(prisma: PrismaClient, communityUUID: string) title: "Please write your evaluation here", description: "Remember that your responses will be made public. Please consult our criteria. We are essentially asking for a 'standard high-quality referee report' here, with some specific considerations (mentioned in the above link). We welcome detail, elaboration, and technical discussion. If you prefer to link or submit your evaluation content in a different format, please link it here or send it to the corresponding/managing editor. Length and time spent: This is of course, up to you. The Econometrics society recommends a 2-3 page referee report. In a recent survey (Charness et al, 2022), economists report spending (median and mean) about one day per report, with substantial shares reporting ‘half a day’ and ‘two days’. We expect that that reviewers tend to spend more time on papers for high-status journals, and when reviewing work closely tied to their own agenda.", - type: "boolean", + type: "string", }, }, }); diff --git a/packages/sdk/src/react/generateFormFields.tsx b/packages/sdk/src/react/generateFormFields.tsx index 0736452db9..452d68a073 100644 --- a/packages/sdk/src/react/generateFormFields.tsx +++ b/packages/sdk/src/react/generateFormFields.tsx @@ -9,6 +9,7 @@ import { CardDescription, CardHeader, CardTitle, + Checkbox, FormControl, FormDescription, FormField, @@ -18,6 +19,7 @@ import { Input, } from "ui"; import { cn } from "utils"; +import { Check } from "ui/src/icon"; // a bit of a hack, but allows us to use AJV's JSON schema type type AnySchema = {}; @@ -49,7 +51,10 @@ export const buildFormSchemaFromFields = ( }; // todo: array, and more complex types that we might want to handle -export const getFormField = (schemaType: "string" | "number", field: ControllerRenderProps) => { +export const getFormField = ( + schemaType: "string" | "number" | "boolean", + field: ControllerRenderProps +) => { switch (schemaType) { case "number": return ( @@ -59,6 +64,8 @@ export const getFormField = (schemaType: "string" | "number", field: ControllerR onChange={(event) => field.onChange(+event.target.value)} /> ); + case "boolean": + return ; default: return ; } @@ -79,8 +86,8 @@ const ScalarField = (props: ScalarFieldProps) => { render={({ field }) => ( {props.schema.title} - {getFormField(props.schema.type, field)} {props.schema.description} + {getFormField(props.schema.type, field)} )} diff --git a/packages/ui/package.json b/packages/ui/package.json index 774af13cea..59d69248f7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -14,6 +14,7 @@ "dependencies": { "@hookform/resolvers": "^3.3.1", "@radix-ui/react-avatar": "^1.0.3", + "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.5", diff --git a/packages/ui/src/checkbox.tsx b/packages/ui/src/checkbox.tsx new file mode 100644 index 0000000000..d03b1c7552 --- /dev/null +++ b/packages/ui/src/checkbox.tsx @@ -0,0 +1,30 @@ +"use client"; + +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { Check } from "lucide-react"; + +import { cn } from "utils"; + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export { Checkbox }; diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index d0c3a95369..d31755cceb 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -6,6 +6,7 @@ export * from "./badge"; export * from "./button"; export * from "./card"; export * from "./collapsible"; +export * from "./checkbox"; export * from "./dialog"; export * from "./dropdown-menu"; export * from "./form"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b086f2a567..65aeff647b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -413,6 +413,9 @@ importers: '@radix-ui/react-avatar': specifier: ^1.0.3 version: 1.0.3(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-checkbox': + specifier: ^1.0.4 + version: 1.0.4(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-collapsible': specifier: ^1.0.3 version: 1.0.3(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) @@ -457,7 +460,7 @@ importers: version: 3.3.3(ts-node@10.9.1) tailwindcss-animate: specifier: ^1.0.6 - version: 1.0.6(tailwindcss@3.3.3) + version: 1.0.6 utils: specifier: workspace:* version: link:../utils @@ -2536,6 +2539,33 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-checkbox@1.0.4(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.10 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.12)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.12)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.12)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.12)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.12)(react@18.2.0) + '@types/react': 18.2.12 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-collapsible@1.0.3(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==} peerDependencies: @@ -3114,6 +3144,20 @@ packages: react: 18.2.0 dev: false + /@radix-ui/react-use-previous@1.0.1(@types/react@18.2.12)(react@18.2.0): + resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.22.10 + '@types/react': 18.2.12 + react: 18.2.0 + dev: false + /@radix-ui/react-use-rect@1.0.1(@types/react@18.2.12)(react@18.2.0): resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==} peerDependencies: @@ -8825,12 +8869,10 @@ packages: resolution: {integrity: sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==} dev: false - /tailwindcss-animate@1.0.6(tailwindcss@3.3.3): + /tailwindcss-animate@1.0.6: resolution: {integrity: sha512-4WigSGMvbl3gCCact62ZvOngA+PRqhAn7si3TQ3/ZuPuQZcIEtVap+ENSXbzWhpojKB8CpvnIsrwBu8/RnHtuw==} peerDependencies: tailwindcss: '>=3.0.0 || insiders' - dependencies: - tailwindcss: 3.3.3(ts-node@10.9.1) dev: false /tailwindcss@3.3.3(ts-node@10.9.1): From 1045b09ced03248998c3aff400e421d5dc2ec752 Mon Sep 17 00:00:00 2001 From: Gabriel Stein Date: Tue, 17 Oct 2023 18:21:19 -0400 Subject: [PATCH 03/27] style and use boolean checkbox --- packages/sdk/src/react/generateFormFields.tsx | 66 +++++++++++++------ 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/packages/sdk/src/react/generateFormFields.tsx b/packages/sdk/src/react/generateFormFields.tsx index 452d68a073..2fac4b6e03 100644 --- a/packages/sdk/src/react/generateFormFields.tsx +++ b/packages/sdk/src/react/generateFormFields.tsx @@ -51,23 +51,56 @@ export const buildFormSchemaFromFields = ( }; // todo: array, and more complex types that we might want to handle -export const getFormField = ( - schemaType: "string" | "number" | "boolean", - field: ControllerRenderProps -) => { - switch (schemaType) { +export const getFormField = (schema: JSONSchemaType, field: ControllerRenderProps) => { + const { title, description, type } = schema; + const descriptionComponentWithHtml = ( + + ); + switch (type) { case "number": return ( - field.onChange(+event.target.value)} - /> + + {title} + {descriptionComponentWithHtml} + + field.onChange(+event.target.value)} + /> + + + ); case "boolean": - return ; + return ( + + + { + field.onChange(checked); + }} + /> + +
+ {title} + {descriptionComponentWithHtml} + +
+
+ ); default: - return ; + return ( + + {schema.title} + {descriptionComponentWithHtml} + + + + + + ); } }; @@ -83,14 +116,7 @@ const ScalarField = (props: ScalarFieldProps) => { control={props.control} name={props.title} defaultValue={props.schema.default ?? ""} - render={({ field }) => ( - - {props.schema.title} - {props.schema.description} - {getFormField(props.schema.type, field)} - - - )} + render={({ field }) => getFormField(props.schema, field)} /> ); }; From 745f3a82cca82124c8325331c0023df0d405300a Mon Sep 17 00:00:00 2001 From: Gabriel Stein Date: Wed, 18 Oct 2023 10:52:20 -0400 Subject: [PATCH 04/27] html and remake confidence schema --- .../prisma/exampleCommunitySeeds/unjournal.ts | 44 ++++++------------- packages/sdk/src/react/generateFormFields.tsx | 5 ++- 2 files changed, 17 insertions(+), 32 deletions(-) diff --git a/core/prisma/exampleCommunitySeeds/unjournal.ts b/core/prisma/exampleCommunitySeeds/unjournal.ts index 2d7070b0b5..199a464731 100644 --- a/core/prisma/exampleCommunitySeeds/unjournal.ts +++ b/core/prisma/exampleCommunitySeeds/unjournal.ts @@ -13,38 +13,20 @@ export default async function main(prisma: PrismaClient, communityUUID: string) }); const confidenceObject = { - rating: { - title: "Rating", - type: "number", - minimum: 0, - maximum: 100, - default: 0, - }, confidence: { - title: "90% Confidence Interval", - description: "E.g. for a 50 rating, you might give a CI of 42, 61", - type: "object", - properties: { - low: { - title: "Low", - type: "number", - minimum: 0, - maximum: 100, - default: 0, - }, - high: { - title: "High", - type: "number", - minimum: 0, - maximum: 100, - default: 0, - }, - comments: { - title: "Additional Comments", - type: "string", - minLength: 0, - }, - }, + id: "unjournal:confidence", + title: "90% Confidence Interval Rating", + description: + "Provide three numbers: your rating, then the 90% confidence bounds for your rating. E.g. for a 50 rating, you might give bounds of 42 and 61.", + type: "array", + maxItems: 3, + minItems: 3, + items: { type: "integer" }, + }, + supComments: { + title: "Additional Comments", + type: "string", + minLength: 0, }, }; diff --git a/packages/sdk/src/react/generateFormFields.tsx b/packages/sdk/src/react/generateFormFields.tsx index 2fac4b6e03..8a43849cee 100644 --- a/packages/sdk/src/react/generateFormFields.tsx +++ b/packages/sdk/src/react/generateFormFields.tsx @@ -133,6 +133,7 @@ export const buildFormFieldsFromSchema = ( path?: string ) => { const fields: React.ReactNode[] = []; + console.log(schema); if (isObjectSchema(schema)) { for (const [fieldKey, fieldSchema] of Object.entries(schema.properties)) { const fieldPath = path ? `${path}.${fieldKey}` : fieldKey; @@ -140,7 +141,9 @@ export const buildFormFieldsFromSchema = ( {fieldSchema.title} - {fieldSchema.description} + {buildFormFieldsFromSchema(fieldSchema, control, fieldPath)} From 14d0437e6f2a259da43297a164d2048c94e5a12d Mon Sep 17 00:00:00 2001 From: Gabriel Stein Date: Wed, 18 Oct 2023 12:20:53 -0400 Subject: [PATCH 05/27] make confidence into def' --- .../prisma/exampleCommunitySeeds/unjournal.ts | 87 ++++++++++++++----- packages/sdk/src/react/generateFormFields.tsx | 1 - 2 files changed, 66 insertions(+), 22 deletions(-) diff --git a/core/prisma/exampleCommunitySeeds/unjournal.ts b/core/prisma/exampleCommunitySeeds/unjournal.ts index 199a464731..9507d63d01 100644 --- a/core/prisma/exampleCommunitySeeds/unjournal.ts +++ b/core/prisma/exampleCommunitySeeds/unjournal.ts @@ -12,24 +12,36 @@ export default async function main(prisma: PrismaClient, communityUUID: string) }, }); - const confidenceObject = { - confidence: { - id: "unjournal:confidence", - title: "90% Confidence Interval Rating", - description: - "Provide three numbers: your rating, then the 90% confidence bounds for your rating. E.g. for a 50 rating, you might give bounds of 42 and 61.", - type: "array", - maxItems: 3, - minItems: 3, - items: { type: "integer" }, - }, - supComments: { + const commentsObject = { + comments: { title: "Additional Comments", type: "string", minLength: 0, }, }; + const HundredConfidenceDef = { + $id: "unjournal:100confidence", + title: "90% Confidence Interval Rating", + description: + "Provide three numbers: your rating, then the 90% confidence bounds for your rating. E.g. for a 50 rating, you might give bounds of 42 and 61.", + type: "array", + maxItems: 3, + minItems: 3, + items: { type: "integer", minimum: 0, maximum: 100 }, + }; + + const FiveConfidenceDef = { + $id: "unjournal:5confidence", + title: "90% Confidence Interval Rating", + description: + "Provide three numbers: your rating, then the 90% confidence bounds for your rating. E.g. for a 50 rating, you might give bounds of 42 and 61.", + type: "array", + maxItems: 3, + minItems: 3, + items: { type: "number", minimum: 0, maximum: 5 }, + }; + const metricsSchema = await prisma.pubFieldSchema.create({ data: { name: "metrics", @@ -40,41 +52,65 @@ export default async function main(prisma: PrismaClient, communityUUID: string) description: "Responses will be public. See here for details on the categories.", type: "object", + $defs: { + confidence: HundredConfidenceDef, + }, properties: { assessment: { title: "Overall assessment", type: "object", - properties: confidenceObject, + properties: { + confidence: { $ref: "#/$defs/confidence" }, + ...commentsObject, + }, }, advancing: { title: "Advancing knowledge and practice", type: "object", - properties: confidenceObject, + properties: { + confidence: { $ref: "#/$defs/confidence" }, + ...commentsObject, + }, }, methods: { title: "Methods: Justification, reasonableness, validity, robustness", type: "object", - properties: confidenceObject, + properties: { + confidence: { $ref: "#/$defs/confidence" }, + ...commentsObject, + }, }, logic: { title: "Logic & communication", type: "object", - properties: confidenceObject, + properties: { + confidence: { $ref: "#/$defs/confidence" }, + ...commentsObject, + }, }, open: { title: "Open, collaborative, replicable", type: "object", - properties: confidenceObject, + properties: { + confidence: { $ref: "#/$defs/confidence" }, + ...commentsObject, + }, }, real: { title: "Engaging with real-world, impact quantification; practice, realism, and relevance", type: "object", - properties: confidenceObject, + properties: { + confidence: { $ref: "#/$defs/confidence" }, + ...commentsObject, + }, }, relevance: { title: "Relevance to global priorities", type: "object", - properties: confidenceObject, + properties: { + confidence: { $ref: "#/$defs/confidence" }, + ...commentsObject, + }, }, }, }, @@ -105,16 +141,25 @@ export default async function main(prisma: PrismaClient, communityUUID: string) description: "Responses will be public. See here for details on the metrics.", type: "object", + $defs: { + confidence: FiveConfidenceDef, + }, properties: { qualityJournal: { title: "What 'quality journal' do you expect this work will this be published in?", type: "object", - properties: confidenceObject, + properties: { + confidence: { $ref: "#/$defs/confidence" }, + ...commentsObject, + }, }, qualityLevel: { title: "Overall assessment on 'scale of journals'; i.e., quality-level of journal it should be published in.", type: "object", - properties: confidenceObject, + properties: { + confidence: { $ref: "#/$defs/confidence" }, + ...commentsObject, + }, }, }, }, diff --git a/packages/sdk/src/react/generateFormFields.tsx b/packages/sdk/src/react/generateFormFields.tsx index 8a43849cee..47f53a6f76 100644 --- a/packages/sdk/src/react/generateFormFields.tsx +++ b/packages/sdk/src/react/generateFormFields.tsx @@ -133,7 +133,6 @@ export const buildFormFieldsFromSchema = ( path?: string ) => { const fields: React.ReactNode[] = []; - console.log(schema); if (isObjectSchema(schema)) { for (const [fieldKey, fieldSchema] of Object.entries(schema.properties)) { const fieldPath = path ? `${path}.${fieldKey}` : fieldKey; From fe78d4cec80ebd658276d71884f76e38bcad6dcd Mon Sep 17 00:00:00 2001 From: Gabriel Stein Date: Wed, 18 Oct 2023 20:18:19 -0400 Subject: [PATCH 06/27] deref local stuff --- .../app/actions/evaluate/evaluate.tsx | 7 +- packages/sdk/src/react/generateFormFields.tsx | 73 ++++++++++++++++--- pnpm-lock.yaml | 6 +- 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/integrations/evaluations/app/actions/evaluate/evaluate.tsx b/integrations/evaluations/app/actions/evaluate/evaluate.tsx index 424ff15132..a2211e93fa 100644 --- a/integrations/evaluations/app/actions/evaluate/evaluate.tsx +++ b/integrations/evaluations/app/actions/evaluate/evaluate.tsx @@ -1,4 +1,5 @@ "use client"; +import Ajv from "ajv"; import { ajvResolver } from "@hookform/resolvers/ajv"; import { GetPubResponseBody, GetPubTypeResponseBody, PubValues } from "@pubpub/sdk"; import { buildFormFieldsFromSchema, buildFormSchemaFromFields } from "@pubpub/sdk/react"; @@ -30,7 +31,11 @@ export function Evaluate(props: Props) { const { pub, pubType } = props; const { toast } = useToast(); + // we need to use an uncompiled schema for validation, but compiled for building the form + // we could return a compiled schema, but ajvResolver complains about, ironically, a resolved schema from ajv const generatedSchema = buildFormSchemaFromFields(pubType); + const ajv = new Ajv(); + const compiledSchema = ajv.addSchema(generatedSchema, "schema"); const form = useForm({ mode: "onChange", @@ -71,7 +76,7 @@ export function Evaluate(props: Props) { }, [values]); const formFieldsFromSchema = useMemo( - () => buildFormFieldsFromSchema(generatedSchema, form.control), + () => buildFormFieldsFromSchema(compiledSchema, form.control), [form.control] ); diff --git a/packages/sdk/src/react/generateFormFields.tsx b/packages/sdk/src/react/generateFormFields.tsx index 47f53a6f76..2856f0a7fb 100644 --- a/packages/sdk/src/react/generateFormFields.tsx +++ b/packages/sdk/src/react/generateFormFields.tsx @@ -1,6 +1,6 @@ import * as React from "react"; // this import causes a cyclic dependency in pnpm but here we are -import { JSONSchemaType } from "ajv"; +import Ajv, { JSONSchemaType } from "ajv"; import { GetPubTypeResponseBody } from "contracts"; import { Control, ControllerRenderProps } from "react-hook-form"; @@ -19,7 +19,6 @@ import { Input, } from "ui"; import { cn } from "utils"; -import { Check } from "ui/src/icon"; // a bit of a hack, but allows us to use AJV's JSON schema type type AnySchema = {}; @@ -127,15 +126,56 @@ const isObjectSchema = ( return schema.properties && Object.keys(schema.properties).length > 0; }; -export const buildFormFieldsFromSchema = ( +const hasRef = (schema: JSONSchemaType) => { + return schema.$ref; +}; + +const getResolvedSchema = (schema: JSONSchemaType, compiledSchema: Ajv) => {}; + +const getDereferencedSchema = ( schema: JSONSchemaType, - control: Control, + compiledSchema: Ajv, path?: string ) => { - const fields: React.ReactNode[] = []; if (isObjectSchema(schema)) { for (const [fieldKey, fieldSchema] of Object.entries(schema.properties)) { + const fieldPath = path + ? schema.$id + ? `${path}/${schema.$id}` + : path + : `${schema.$id}#/properties`; + const dereffedField = getDereferencedSchema(fieldSchema, compiledSchema, fieldPath); + } + } else { + if (schema.$ref) { + const fieldPath = path + schema.$ref.split("#")[1]; + return compiledSchema.getSchema(fieldPath)!.schema; + } + } +}; + +export const buildFormFieldsFromSchema = ( + schema: Ajv, + control: Control, + path?: string, + fieldSchema?: JSONSchemaType, + schemaPath?: string +) => { + const fields: React.ReactNode[] = []; + const resolvedSchema = fieldSchema + ? fieldSchema + : (schema.getSchema("schema")!.schema as JSONSchemaType); + if (isObjectSchema(resolvedSchema)) { + for (const [fieldKey, fieldSchema] of Object.entries(resolvedSchema.properties)) { const fieldPath = path ? `${path}.${fieldKey}` : fieldKey; + + // for querying the compiled schema later + const fieldSchemaPath = schemaPath + ? resolvedSchema.$id + ? `${schemaPath}/${resolvedSchema.$id}` + : schemaPath + : `${resolvedSchema.$id}#/properties`; + const fieldContent = isObjectSchema(fieldSchema) ? ( @@ -144,19 +184,32 @@ export const buildFormFieldsFromSchema = ( dangerouslySetInnerHTML={{ __html: fieldSchema.description }} /> - {buildFormFieldsFromSchema(fieldSchema, control, fieldPath)} + {buildFormFieldsFromSchema( + schema, + control, + fieldPath, + fieldSchema, + fieldSchemaPath + )} ) : ( - buildFormFieldsFromSchema(fieldSchema, control, fieldPath) + buildFormFieldsFromSchema(schema, control, fieldPath, fieldSchema, fieldSchemaPath) ); fields.push(fieldContent); } } else { + const scalarSchema = hasRef(resolvedSchema) + ? (schema.getSchema(`${schemaPath}${resolvedSchema.$ref!.split("#")[1]}`)! + .schema as JSONSchemaType) + : resolvedSchema; fields.push( - + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 65aeff647b..747ebcb49f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -460,7 +460,7 @@ importers: version: 3.3.3(ts-node@10.9.1) tailwindcss-animate: specifier: ^1.0.6 - version: 1.0.6 + version: 1.0.6(tailwindcss@3.3.3) utils: specifier: workspace:* version: link:../utils @@ -8869,10 +8869,12 @@ packages: resolution: {integrity: sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==} dev: false - /tailwindcss-animate@1.0.6: + /tailwindcss-animate@1.0.6(tailwindcss@3.3.3): resolution: {integrity: sha512-4WigSGMvbl3gCCact62ZvOngA+PRqhAn7si3TQ3/ZuPuQZcIEtVap+ENSXbzWhpojKB8CpvnIsrwBu8/RnHtuw==} peerDependencies: tailwindcss: '>=3.0.0 || insiders' + dependencies: + tailwindcss: 3.3.3(ts-node@10.9.1) dev: false /tailwindcss@3.3.3(ts-node@10.9.1): From ae465c72650ea1ec8d26d3cd6925a854bc1697e3 Mon Sep 17 00:00:00 2001 From: Gabriel Stein Date: Thu, 19 Oct 2023 11:01:00 -0400 Subject: [PATCH 07/27] support custom rendering --- .../app/actions/evaluate/evaluate.tsx | 7 +- packages/sdk/src/react/generateFormFields.tsx | 98 ++++++++++++++----- 2 files changed, 80 insertions(+), 25 deletions(-) diff --git a/integrations/evaluations/app/actions/evaluate/evaluate.tsx b/integrations/evaluations/app/actions/evaluate/evaluate.tsx index a2211e93fa..7f5c41c1c3 100644 --- a/integrations/evaluations/app/actions/evaluate/evaluate.tsx +++ b/integrations/evaluations/app/actions/evaluate/evaluate.tsx @@ -32,10 +32,11 @@ export function Evaluate(props: Props) { const { toast } = useToast(); // we need to use an uncompiled schema for validation, but compiled for building the form - // we could return a compiled schema, but ajvResolver complains about, ironically, a resolved schema from ajv + // "Schema" is a key later used to retrieve this schema (we could later pass multiple for dereferencing, for example) const generatedSchema = buildFormSchemaFromFields(pubType); const ajv = new Ajv(); - const compiledSchema = ajv.addSchema(generatedSchema, "schema"); + const schemaKey = "schema"; + const compiledSchema = ajv.addSchema(generatedSchema, schemaKey); const form = useForm({ mode: "onChange", @@ -76,7 +77,7 @@ export function Evaluate(props: Props) { }, [values]); const formFieldsFromSchema = useMemo( - () => buildFormFieldsFromSchema(compiledSchema, form.control), + () => buildFormFieldsFromSchema(compiledSchema, schemaKey, form.control), [form.control] ); diff --git a/packages/sdk/src/react/generateFormFields.tsx b/packages/sdk/src/react/generateFormFields.tsx index 2856f0a7fb..aed11ee154 100644 --- a/packages/sdk/src/react/generateFormFields.tsx +++ b/packages/sdk/src/react/generateFormFields.tsx @@ -49,6 +49,40 @@ export const buildFormSchemaFromFields = ( return schema; }; +const customScalars = ["unjournal:100confidence", "unjournal:5confidence"]; + +const hasCustomRenderer = (id: string) => { + return customScalars.includes(id); +}; + +const getCustomRenderer = ( + path: string | undefined, + control: Control, + fieldSchema: JSONSchemaType, + parentSchema: JSONSchemaType +) => { + if (fieldSchema.$id === "unjournal:100confidence") { + return ( + +

100

+
+ ); + } + if (fieldSchema.$id === "unjournal:5confidence") { + return ( + +

5

+
+ ); + } +}; + // todo: array, and more complex types that we might want to handle export const getFormField = (schema: JSONSchemaType, field: ControllerRenderProps) => { const { title, description, type } = schema; @@ -130,7 +164,10 @@ const hasRef = (schema: JSONSchemaType) => { return schema.$ref; }; -const getResolvedSchema = (schema: JSONSchemaType, compiledSchema: Ajv) => {}; +const hasResolvedSchema = (compiledSchema: Ajv, schemaKey: string) => { + const resolvedSchema = compiledSchema.getSchema(schemaKey); + return resolvedSchema && resolvedSchema.schema; +}; const getDereferencedSchema = ( schema: JSONSchemaType, @@ -155,21 +192,26 @@ const getDereferencedSchema = ( }; export const buildFormFieldsFromSchema = ( - schema: Ajv, + compiledSchema: Ajv, + compiledSchemaKey: string, control: Control, path?: string, fieldSchema?: JSONSchemaType, schemaPath?: string ) => { const fields: React.ReactNode[] = []; + + // probably should refactor into function and throw an error if the schema can't be resolved from the compiled schema const resolvedSchema = fieldSchema ? fieldSchema - : (schema.getSchema("schema")!.schema as JSONSchemaType); + : (compiledSchema.getSchema("schema")!.schema as JSONSchemaType); + if (isObjectSchema(resolvedSchema)) { for (const [fieldKey, fieldSchema] of Object.entries(resolvedSchema.properties)) { const fieldPath = path ? `${path}.${fieldKey}` : fieldKey; - // for querying the compiled schema later + // for querying the compiled schema later -- pretty robust, but does assume defs are not at top level + // may be better way to query just at last schema id, for example const fieldSchemaPath = schemaPath ? resolvedSchema.$id ? `${schemaPath}/${resolvedSchema.$id}` @@ -185,7 +227,8 @@ export const buildFormFieldsFromSchema = ( /> {buildFormFieldsFromSchema( - schema, + compiledSchema, + compiledSchemaKey, control, fieldPath, fieldSchema, @@ -193,27 +236,38 @@ export const buildFormFieldsFromSchema = ( )}
) : ( - buildFormFieldsFromSchema(schema, control, fieldPath, fieldSchema, fieldSchemaPath) + buildFormFieldsFromSchema( + compiledSchema, + compiledSchemaKey, + control, + fieldPath, + fieldSchema, + fieldSchemaPath + ) ); fields.push(fieldContent); } } else { - const scalarSchema = hasRef(resolvedSchema) - ? (schema.getSchema(`${schemaPath}${resolvedSchema.$ref!.split("#")[1]}`)! - .schema as JSONSchemaType) - : resolvedSchema; - fields.push( - - - - ); + const scalarSchema = + hasRef(resolvedSchema) && hasResolvedSchema(compiledSchema, compiledSchemaKey) + ? (compiledSchema.getSchema(`${schemaPath}${resolvedSchema.$ref!.split("#")[1]}`)! + .schema as JSONSchemaType) + : resolvedSchema; + + scalarSchema.$id && hasCustomRenderer(scalarSchema.$id) + ? fields.push(getCustomRenderer(path, control, scalarSchema, resolvedSchema)) + : fields.push( + + + + ); } return fields; }; From 6ffee07ca7fe6f8ca5c51280882e9ceb94c693ab Mon Sep 17 00:00:00 2001 From: Gabriel Stein Date: Thu, 19 Oct 2023 12:28:31 -0400 Subject: [PATCH 08/27] sliders but not working minmax --- .../prisma/exampleCommunitySeeds/unjournal.ts | 2 + packages/sdk/src/react/generateFormFields.tsx | 84 +++++++++++-------- packages/ui/package.json | 1 + packages/ui/src/index.tsx | 3 + pnpm-lock.yaml | 45 +++++++++- 5 files changed, 97 insertions(+), 38 deletions(-) diff --git a/core/prisma/exampleCommunitySeeds/unjournal.ts b/core/prisma/exampleCommunitySeeds/unjournal.ts index 9507d63d01..e389f24bf3 100644 --- a/core/prisma/exampleCommunitySeeds/unjournal.ts +++ b/core/prisma/exampleCommunitySeeds/unjournal.ts @@ -28,6 +28,7 @@ export default async function main(prisma: PrismaClient, communityUUID: string) type: "array", maxItems: 3, minItems: 3, + default: [20, 30, 40], items: { type: "integer", minimum: 0, maximum: 100 }, }; @@ -39,6 +40,7 @@ export default async function main(prisma: PrismaClient, communityUUID: string) type: "array", maxItems: 3, minItems: 3, + default: [2, 3, 4], items: { type: "number", minimum: 0, maximum: 5 }, }; diff --git a/packages/sdk/src/react/generateFormFields.tsx b/packages/sdk/src/react/generateFormFields.tsx index aed11ee154..f273b12cc8 100644 --- a/packages/sdk/src/react/generateFormFields.tsx +++ b/packages/sdk/src/react/generateFormFields.tsx @@ -17,6 +17,7 @@ import { FormLabel, FormMessage, Input, + Confidence, } from "ui"; import { cn } from "utils"; @@ -49,40 +50,6 @@ export const buildFormSchemaFromFields = ( return schema; }; -const customScalars = ["unjournal:100confidence", "unjournal:5confidence"]; - -const hasCustomRenderer = (id: string) => { - return customScalars.includes(id); -}; - -const getCustomRenderer = ( - path: string | undefined, - control: Control, - fieldSchema: JSONSchemaType, - parentSchema: JSONSchemaType -) => { - if (fieldSchema.$id === "unjournal:100confidence") { - return ( - -

100

-
- ); - } - if (fieldSchema.$id === "unjournal:5confidence") { - return ( - -

5

-
- ); - } -}; - // todo: array, and more complex types that we might want to handle export const getFormField = (schema: JSONSchemaType, field: ControllerRenderProps) => { const { title, description, type } = schema; @@ -154,6 +121,55 @@ const ScalarField = (props: ScalarFieldProps) => { ); }; +const customScalars = ["unjournal:100confidence", "unjournal:5confidence"]; + +const hasCustomRenderer = (id: string) => { + return customScalars.includes(id); +}; + +// todo: don't just use if statements, make more dynamic +const getCustomRenderer = ( + path: string | undefined, + control: Control, + fieldSchema: JSONSchemaType, + parentSchema: JSONSchemaType +) => { + if ( + fieldSchema.$id === "unjournal:100confidence" || + fieldSchema.$id === "unjournal:5confidence" + ) { + return ( + + ( + + {fieldSchema.title} + + + field.onChange(event)} + /> + + + + )} + /> + + ); + } +}; + const isObjectSchema = ( schema: JSONSchemaType ): schema is JSONSchemaType & { properties: JSONSchemaType[] } => { diff --git a/packages/ui/package.json b/packages/ui/package.json index 59d69248f7..f5383b60db 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -21,6 +21,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.6", + "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.4", "@radix-ui/react-tooltip": "^1.0.6", diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index d31755cceb..69b5c7fa79 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -19,5 +19,8 @@ export * from "./toaster"; export * from "./tooltip"; export * from "./use-toast"; +/* Renderers */ +export * from "./customRenderers/confidence"; + /* Hooks */ export * from "./hooks"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 747ebcb49f..a6c10a3e86 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -434,6 +434,9 @@ importers: '@radix-ui/react-popover': specifier: ^1.0.6 version: 1.0.6(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slider': + specifier: ^1.1.2 + version: 1.1.2(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.2.12)(react@18.2.0) @@ -460,7 +463,7 @@ importers: version: 3.3.3(ts-node@10.9.1) tailwindcss-animate: specifier: ^1.0.6 - version: 1.0.6(tailwindcss@3.3.3) + version: 1.0.6 utils: specifier: workspace:* version: link:../utils @@ -2490,6 +2493,12 @@ packages: resolution: {integrity: sha512-dT7FOLUCdZmq+AunLqB1Iz+ZH/IIS1Fz2THmKZQ6aFONrQD/BQ5ecJ7g2wGS2OgyUFf4OaLam6/bxmgdOBDqig==} requiresBuild: true + /@radix-ui/number@1.0.1: + resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} + dependencies: + '@babel/runtime': 7.22.10 + dev: false + /@radix-ui/primitive@1.0.1: resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} dependencies: @@ -3009,6 +3018,36 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-slider@1.1.2(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-NKs15MJylfzVsCagVSWKhGGLNR1W9qWs+HtgbmjjVUB3B9+lb3PYoXxVju3kOrpf0VKyVCtZp+iTwVoqpa1Chw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.10 + '@radix-ui/number': 1.0.1 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.12)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.12)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.12)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.12)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.12)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.12)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.12)(react@18.2.0) + '@types/react': 18.2.12 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-slot@1.0.2(@types/react@18.2.12)(react@18.2.0): resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: @@ -8869,12 +8908,10 @@ packages: resolution: {integrity: sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==} dev: false - /tailwindcss-animate@1.0.6(tailwindcss@3.3.3): + /tailwindcss-animate@1.0.6: resolution: {integrity: sha512-4WigSGMvbl3gCCact62ZvOngA+PRqhAn7si3TQ3/ZuPuQZcIEtVap+ENSXbzWhpojKB8CpvnIsrwBu8/RnHtuw==} peerDependencies: tailwindcss: '>=3.0.0 || insiders' - dependencies: - tailwindcss: 3.3.3(ts-node@10.9.1) dev: false /tailwindcss@3.3.3(ts-node@10.9.1): From 62e50c398f3fd3fd9a912c858db56cb75dd0489f Mon Sep 17 00:00:00 2001 From: Gabriel Stein Date: Thu, 19 Oct 2023 12:31:36 -0400 Subject: [PATCH 09/27] boom working slider --- packages/sdk/src/react/generateFormFields.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/react/generateFormFields.tsx b/packages/sdk/src/react/generateFormFields.tsx index f273b12cc8..7731019e9e 100644 --- a/packages/sdk/src/react/generateFormFields.tsx +++ b/packages/sdk/src/react/generateFormFields.tsx @@ -138,6 +138,9 @@ const getCustomRenderer = ( fieldSchema.$id === "unjournal:100confidence" || fieldSchema.$id === "unjournal:5confidence" ) { + // not sure why, but these need to be set outside of the render in FormField? + const min = fieldSchema.items.minimum; + const max = fieldSchema.items.maximum; return ( field.onChange(event)} /> From 9ed77d165861c8ec20ad765fecfdb0b0fbac9551 Mon Sep 17 00:00:00 2001 From: Gabriel Stein Date: Thu, 19 Oct 2023 12:59:57 -0400 Subject: [PATCH 10/27] add renderer files and title --- .../app/actions/evaluate/evaluate.tsx | 86 +++++++++++++------ packages/sdk/src/react/generateFormFields.tsx | 3 +- packages/ui/package.json | 1 + .../ui/src/customRenderers/confidence.css | 6 ++ .../ui/src/customRenderers/confidence.tsx | 29 +++++++ packages/ui/src/index.tsx | 1 + packages/ui/src/separator.tsx | 26 ++++++ pnpm-lock.yaml | 23 +++++ 8 files changed, 146 insertions(+), 29 deletions(-) create mode 100644 packages/ui/src/customRenderers/confidence.css create mode 100644 packages/ui/src/customRenderers/confidence.tsx create mode 100644 packages/ui/src/separator.tsx diff --git a/integrations/evaluations/app/actions/evaluate/evaluate.tsx b/integrations/evaluations/app/actions/evaluate/evaluate.tsx index 7f5c41c1c3..75a5db80d6 100644 --- a/integrations/evaluations/app/actions/evaluate/evaluate.tsx +++ b/integrations/evaluations/app/actions/evaluate/evaluate.tsx @@ -15,6 +15,7 @@ import { CardTitle, Form, Icon, + Separator, useLocalStorage, useToast, } from "ui"; @@ -82,33 +83,62 @@ export function Evaluate(props: Props) { ); return ( -
- - - - {pubType.name} - {pubType.description} - - {formFieldsFromSchema} - - - - - -
- + <> + + + + Thanks for your interest in evaluating research for the Unjournal! Your + evaluation will be made public and given a DOI, but you have the option to + remain anonymous or 'sign your review' and take credit. You will be + compensated a minimum of $250 for your evaluation work, and will be eligible + for financial 'most informative evaluation' prizes. See the full guidelines + on our wiki. + + +

To evaluate:

+

{`${pub.values["unjournal:title"]}`}

+

+ {pub.values["unjournal:description"] && + `${pub.values["unjournal:description"]}`} +

+

+ View Article +

+

Manager Notes:

+

+ {pub.values["unjournal:managers-notes"] && + `${pub.values["unjournal:managers-notes"]}`} +

+
+
+
+ + + + {pubType.name} + {pubType.description} + + {formFieldsFromSchema} + + + + + +
+ + ); } diff --git a/packages/sdk/src/react/generateFormFields.tsx b/packages/sdk/src/react/generateFormFields.tsx index 7731019e9e..9a3611b664 100644 --- a/packages/sdk/src/react/generateFormFields.tsx +++ b/packages/sdk/src/react/generateFormFields.tsx @@ -10,6 +10,7 @@ import { CardHeader, CardTitle, Checkbox, + Confidence, FormControl, FormDescription, FormField, @@ -17,7 +18,7 @@ import { FormLabel, FormMessage, Input, - Confidence, + Separator, } from "ui"; import { cn } from "utils"; diff --git a/packages/ui/package.json b/packages/ui/package.json index f5383b60db..61870a002a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -21,6 +21,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.6", + "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.4", diff --git a/packages/ui/src/customRenderers/confidence.css b/packages/ui/src/customRenderers/confidence.css new file mode 100644 index 0000000000..4ae64eac2e --- /dev/null +++ b/packages/ui/src/customRenderers/confidence.css @@ -0,0 +1,6 @@ +.slider-thumb:after { + position: absolute; + top: 1.5rem; + content: attr(aria-valuenow); + font-size: 0.8rem; +} diff --git a/packages/ui/src/customRenderers/confidence.tsx b/packages/ui/src/customRenderers/confidence.tsx new file mode 100644 index 0000000000..60a5fa9f75 --- /dev/null +++ b/packages/ui/src/customRenderers/confidence.tsx @@ -0,0 +1,29 @@ +"use client"; + +import * as React from "react"; +import * as SliderPrimitive from "@radix-ui/react-slider"; + +import "./confidence.css"; + +import { cn } from "utils"; + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + + + +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider as Confidence }; diff --git a/packages/ui/src/index.tsx b/packages/ui/src/index.tsx index 69b5c7fa79..44c79715fb 100644 --- a/packages/ui/src/index.tsx +++ b/packages/ui/src/index.tsx @@ -13,6 +13,7 @@ export * from "./form"; export * from "./input"; export * from "./label"; export * from "./popover"; +export * from "./separator"; export * from "./textarea"; export * from "./toast"; export * from "./toaster"; diff --git a/packages/ui/src/separator.tsx b/packages/ui/src/separator.tsx new file mode 100644 index 0000000000..f3f40f2e6b --- /dev/null +++ b/packages/ui/src/separator.tsx @@ -0,0 +1,26 @@ +"use client"; + +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; + +import { cn } from "utils"; + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => ( + +)); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6c10a3e86..95c17ef8c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -434,6 +434,9 @@ importers: '@radix-ui/react-popover': specifier: ^1.0.6 version: 1.0.6(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-separator': + specifier: ^1.0.3 + version: 1.0.3(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-slider': specifier: ^1.1.2 version: 1.1.2(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) @@ -3018,6 +3021,26 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-separator@1.0.3(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.22.10 + '@radix-ui/react-primitive': 1.0.3(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.12 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-slider@1.1.2(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-NKs15MJylfzVsCagVSWKhGGLNR1W9qWs+HtgbmjjVUB3B9+lb3PYoXxVju3kOrpf0VKyVCtZp+iTwVoqpa1Chw==} peerDependencies: From 8cc3f528d008c93d77b2e5904832757ff8cd9b1f Mon Sep 17 00:00:00 2001 From: Gabriel Stein Date: Thu, 19 Oct 2023 14:11:55 -0400 Subject: [PATCH 11/27] styling --- packages/sdk/src/react/generateFormFields.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/sdk/src/react/generateFormFields.tsx b/packages/sdk/src/react/generateFormFields.tsx index 9a3611b664..70140acde5 100644 --- a/packages/sdk/src/react/generateFormFields.tsx +++ b/packages/sdk/src/react/generateFormFields.tsx @@ -239,9 +239,10 @@ export const buildFormFieldsFromSchema = ( : `${resolvedSchema.$id}#/properties`; const fieldContent = isObjectSchema(fieldSchema) ? ( - +
+ {!path && } - {fieldSchema.title} + {fieldSchema.title} @@ -254,7 +255,7 @@ export const buildFormFieldsFromSchema = ( fieldSchema, fieldSchemaPath )} - +
) : ( buildFormFieldsFromSchema( compiledSchema, From 587309c240bd6237ac26613354bcf18ded6179fc Mon Sep 17 00:00:00 2001 From: Gabriel Stein Date: Thu, 19 Oct 2023 14:21:05 -0400 Subject: [PATCH 12/27] exclude shit --- .../app/actions/evaluate/evaluate.tsx | 3 ++- packages/sdk/src/react/generateFormFields.tsx | 24 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/integrations/evaluations/app/actions/evaluate/evaluate.tsx b/integrations/evaluations/app/actions/evaluate/evaluate.tsx index 75a5db80d6..986cf07328 100644 --- a/integrations/evaluations/app/actions/evaluate/evaluate.tsx +++ b/integrations/evaluations/app/actions/evaluate/evaluate.tsx @@ -34,7 +34,8 @@ export function Evaluate(props: Props) { // we need to use an uncompiled schema for validation, but compiled for building the form // "Schema" is a key later used to retrieve this schema (we could later pass multiple for dereferencing, for example) - const generatedSchema = buildFormSchemaFromFields(pubType); + const exclude = ["unjournal:title", "unjournal:evaluator"]; + const generatedSchema = buildFormSchemaFromFields(pubType, exclude); const ajv = new Ajv(); const schemaKey = "schema"; const compiledSchema = ajv.addSchema(generatedSchema, schemaKey); diff --git a/packages/sdk/src/react/generateFormFields.tsx b/packages/sdk/src/react/generateFormFields.tsx index 70140acde5..2a88fed984 100644 --- a/packages/sdk/src/react/generateFormFields.tsx +++ b/packages/sdk/src/react/generateFormFields.tsx @@ -26,7 +26,8 @@ import { cn } from "utils"; type AnySchema = {}; export const buildFormSchemaFromFields = ( - pubType: GetPubTypeResponseBody + pubType: GetPubTypeResponseBody, + exclude: String[] ): JSONSchemaType => { const schema = { $id: `urn:uuid:${pubType.id}`, @@ -36,15 +37,18 @@ export const buildFormSchemaFromFields = ( } as JSONSchemaType; if (pubType.fields) { for (const field of pubType.fields) { - if (field.schema) { - schema.properties[field.slug] = field.schema.schema as JSONSchemaType; - } else { - schema.properties[field.slug] = { - type: "string", - title: `${field.name}`, - $id: `urn:uuid:${field.id}`, - default: "", - }; + if (!exclude.includes(field.slug)) { + if (field.schema) { + schema.properties[field.slug] = field.schema + .schema as JSONSchemaType; + } else { + schema.properties[field.slug] = { + type: "string", + title: `${field.name}`, + $id: `urn:uuid:${field.id}`, + default: "", + }; + } } } } From de504c19457afed467df43d65a6df8a26f228172 Mon Sep 17 00:00:00 2001 From: Gabriel Stein Date: Thu, 19 Oct 2023 14:39:35 -0400 Subject: [PATCH 13/27] add default value to checkboxes --- core/prisma/exampleCommunitySeeds/unjournal.ts | 2 ++ packages/sdk/src/react/generateFormFields.tsx | 1 + 2 files changed, 3 insertions(+) diff --git a/core/prisma/exampleCommunitySeeds/unjournal.ts b/core/prisma/exampleCommunitySeeds/unjournal.ts index e389f24bf3..4d74652097 100644 --- a/core/prisma/exampleCommunitySeeds/unjournal.ts +++ b/core/prisma/exampleCommunitySeeds/unjournal.ts @@ -230,6 +230,7 @@ export default async function main(prisma: PrismaClient, communityUUID: string) revision: { title: "Would you be willing to consider evaluating a revised version of this work?", type: "boolean", + default: false, }, }, }, @@ -246,6 +247,7 @@ export default async function main(prisma: PrismaClient, communityUUID: string) description: "If no, the public sections of your review will be published anonymously.", type: "boolean", + default: false, }, }, }); diff --git a/packages/sdk/src/react/generateFormFields.tsx b/packages/sdk/src/react/generateFormFields.tsx index 2a88fed984..8a1fea14bc 100644 --- a/packages/sdk/src/react/generateFormFields.tsx +++ b/packages/sdk/src/react/generateFormFields.tsx @@ -83,6 +83,7 @@ export const getFormField = (schema: JSONSchemaType, field: Controlle { field.onChange(checked); }} From bfab398d7e465a2e9e1df1269b808114748b4cc6 Mon Sep 17 00:00:00 2001 From: Gabriel Stein Date: Thu, 19 Oct 2023 14:54:46 -0400 Subject: [PATCH 14/27] style tweaks --- packages/sdk/src/react/generateFormFields.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/sdk/src/react/generateFormFields.tsx b/packages/sdk/src/react/generateFormFields.tsx index 8a1fea14bc..2c96406425 100644 --- a/packages/sdk/src/react/generateFormFields.tsx +++ b/packages/sdk/src/react/generateFormFields.tsx @@ -247,10 +247,14 @@ export const buildFormFieldsFromSchema = (
{!path && } - {fieldSchema.title} - + + {fieldSchema.title} + + {fieldSchema.description && ( + + )} {buildFormFieldsFromSchema( compiledSchema, From 7e32ef4858750f3f4ddbf35e7e704bc3f0cba7ad Mon Sep 17 00:00:00 2001 From: Gabriel Stein Date: Thu, 26 Oct 2023 15:07:01 -0400 Subject: [PATCH 15/27] use component and simplify ternary --- packages/sdk/src/react/generateFormFields.tsx | 56 ++++++++++--------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/packages/sdk/src/react/generateFormFields.tsx b/packages/sdk/src/react/generateFormFields.tsx index 2c96406425..94e7d04eeb 100644 --- a/packages/sdk/src/react/generateFormFields.tsx +++ b/packages/sdk/src/react/generateFormFields.tsx @@ -133,13 +133,14 @@ const hasCustomRenderer = (id: string) => { return customScalars.includes(id); }; +type CustomRendererProps = { + control: Control; + fieldSchema: JSONSchemaType; + fieldName: string; +}; // todo: don't just use if statements, make more dynamic -const getCustomRenderer = ( - path: string | undefined, - control: Control, - fieldSchema: JSONSchemaType, - parentSchema: JSONSchemaType -) => { +const CustomRenderer = (props: CustomRendererProps) => { + const { control, fieldSchema, fieldName } = props; if ( fieldSchema.$id === "unjournal:100confidence" || fieldSchema.$id === "unjournal:5confidence" @@ -148,13 +149,10 @@ const getCustomRenderer = ( const min = fieldSchema.items.minimum; const max = fieldSchema.items.maximum; return ( - + ( @@ -283,21 +281,27 @@ export const buildFormFieldsFromSchema = ( ? (compiledSchema.getSchema(`${schemaPath}${resolvedSchema.$ref!.split("#")[1]}`)! .schema as JSONSchemaType) : resolvedSchema; - - scalarSchema.$id && hasCustomRenderer(scalarSchema.$id) - ? fields.push(getCustomRenderer(path, control, scalarSchema, resolvedSchema)) - : fields.push( - - - - ); + fields.push( + scalarSchema.$id && hasCustomRenderer(scalarSchema.$id) ? ( + + ) : ( + + + + ) + ); } return fields; }; From 8f709c8fb38d53a80b645521ed6ac2b44eda4fe4 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Thu, 5 Oct 2023 08:13:15 -0500 Subject: [PATCH 16/27] Disable changessets action --- .github/workflows/changesets.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/changesets.yml b/.github/workflows/changesets.yml index ea8c27c17c..ebe5ce53ec 100644 --- a/.github/workflows/changesets.yml +++ b/.github/workflows/changesets.yml @@ -1,8 +1,8 @@ name: changesets -on: - push: - branches: - - main +# on: +# push: +# branches: +# - main env: CI: true PNPM_CACHE_FOLDER: .pnpm-store From ec95a656c22e9874bcf60562b1aa14ea063af489 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Wed, 11 Oct 2023 10:06:32 -0500 Subject: [PATCH 17/27] Allow users with db records to claim supabase accounts --- core/app/api/user/route.ts | 55 ++++++++++++++----- core/lib/auth/loginId.ts | 1 - .../migration.sql | 11 ++++ core/prisma/schema.prisma | 19 ++++--- 4 files changed, 62 insertions(+), 24 deletions(-) create mode 100644 core/prisma/migrations/20231010215802_add_supabase_id/migration.sql diff --git a/core/app/api/user/route.ts b/core/app/api/user/route.ts index e49ae216e7..ea6d36effd 100644 --- a/core/app/api/user/route.ts +++ b/core/app/api/user/route.ts @@ -5,6 +5,7 @@ import { getServerSupabase } from "lib/supabaseServer"; import { generateHash, getSlugSuffix, slugifyString } from "lib/string"; import { getLoginId } from "lib/auth/loginId"; import { BadRequestError, ForbiddenError, UnauthorizedError, handleErrors } from "~/lib/server"; +import { captureException } from "@sentry/nextjs"; export type UserPostBody = { firstName: string; @@ -23,6 +24,17 @@ export async function POST(req: NextRequest) { const submittedData: UserPostBody = await req.json(); const { firstName, lastName, email, password } = submittedData; const supabase = getServerSupabase(); + + const existingUser = await prisma.user.findUnique({ + where: { + email + } + }) + + if (existingUser?.supabaseId) { + throw new ForbiddenError("User already exists") + } + const { data, error } = await supabase.auth.signUp({ email, password, @@ -48,24 +60,39 @@ export async function POST(req: NextRequest) { session: null } */ + + console.log("Supabase user", data); if (error || !data.user) { - console.error("Supabase createUser error:"); - console.error(error); + console.error("Supabase createUser error: ", error); + captureException(error); return NextResponse.json({ message: "Supabase createUser error" }, { status: 500 }); } - await prisma.user.create({ - data: { - id: data.user.id, - slug: `${slugifyString(firstName)}${ - lastName ? `-${slugifyString(lastName)}` : "" - }-${generateHash(4, "0123456789")}`, - firstName, - lastName: lastName || undefined, - email, - }, - }); - return NextResponse.json({}, { status: 201 }); + if (existingUser) { + await prisma.user.update({ + where: { + email + }, + data: { + supabaseId: data.user.id + } + }) + return NextResponse.json({ message: "Existing account claimed" }, { status: 200 }) + } else { + const newUser = await prisma.user.create({ + data: { + id: data.user.id, + supabaseId: data.user.id, + slug: `${slugifyString(firstName)}${ + lastName ? `-${slugifyString(lastName)}` : "" + }-${generateHash(4, "0123456789")}`, + firstName, + lastName: lastName || undefined, + email, + }, + }); + return NextResponse.json({}, { status: 201 }); + } }); } diff --git a/core/lib/auth/loginId.ts b/core/lib/auth/loginId.ts index 48d6b11b42..cbd0c5a4fb 100644 --- a/core/lib/auth/loginId.ts +++ b/core/lib/auth/loginId.ts @@ -5,7 +5,6 @@ import { getRefreshCookie, getTokenCookie } from "~/lib/auth/cookies"; import { getServerSupabase } from "~/lib/supabaseServer"; const JWT_SECRET: string = process.env.JWT_SECRET || ""; -const DATABASE_URL: string = process.env.DATABASE_URL || ""; /* This is only called from API calls */ /* When rendering server components, use getLoginData from loginData.ts */ diff --git a/core/prisma/migrations/20231010215802_add_supabase_id/migration.sql b/core/prisma/migrations/20231010215802_add_supabase_id/migration.sql new file mode 100644 index 0000000000..3ce72bec82 --- /dev/null +++ b/core/prisma/migrations/20231010215802_add_supabase_id/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - A unique constraint covering the columns `[supabaseId]` on the table `users` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "supabaseId" TEXT; + +-- CreateIndex +CREATE UNIQUE INDEX "users_supabaseId_key" ON "users"("supabaseId"); diff --git a/core/prisma/schema.prisma b/core/prisma/schema.prisma index 357e757c21..198c964d77 100644 --- a/core/prisma/schema.prisma +++ b/core/prisma/schema.prisma @@ -12,15 +12,16 @@ generator client { } model User { - id String @id @default(uuid()) - slug String @unique - email String @unique - firstName String - lastName String? - orcid String? - avatar String? - createdAt DateTime @default(now()) @map(name: "created_at") - updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at") + id String @id @default(uuid()) + supabaseId String? @unique + slug String @unique + email String @unique + firstName String + lastName String? + orcid String? + avatar String? + createdAt DateTime @default(now()) @map(name: "created_at") + updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at") claims ActionClaim[] moves ActionMove[] From ba4aa6e08187c321462cc43ee0916a166308d1d8 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Fri, 20 Oct 2023 17:39:55 -0400 Subject: [PATCH 18/27] Allow users to be created in supabase This commit creates local db accounts for any user that successfully authenticates with supabase. It also attempts to use the supabase user_metadata to find the user's name. --- README.md | 2 +- core/app/(user)/forgot/ForgotForm.tsx | 2 +- core/app/(user)/reset/ResetForm.tsx | 10 +-- core/app/(user)/settings/SettingsForm.tsx | 2 +- core/app/api/supabase-webhooks/user/route.ts | 2 +- core/app/api/user/route.ts | 38 +++++----- core/lib/auth/loginData.ts | 43 +++++++++-- core/lib/auth/loginId.ts | 75 +++++++++++++------- core/lib/supabase.ts | 4 +- 9 files changed, 118 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 1f3dcf16bc..feb8c883b6 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ root └── ... ``` -- `core` holds the primary app that is hosted on `www.pubpub.org`. +- `core` holds the primary app that is hosted on `v7.pubpub.org`. - `integrations` holds the integrations developed by the PubPub team. 3rd party integrations may be developed and hosted elsewhere. - `packages` holds libraries and npm packages that are used by `core`, `integrations`, and 3rd party integration developers. diff --git a/core/app/(user)/forgot/ForgotForm.tsx b/core/app/(user)/forgot/ForgotForm.tsx index d2d7554dc8..3e0ed6cd7a 100644 --- a/core/app/(user)/forgot/ForgotForm.tsx +++ b/core/app/(user)/forgot/ForgotForm.tsx @@ -14,7 +14,7 @@ export default function ForgotForm() { setIsLoading(true); setFailure(false); const { error } = await supabase.auth.resetPasswordForEmail(email, { - redirectTo: "https://www.pubpub.org/reset", + redirectTo: `${process.env.NEXT_PUBLIC_PUBPUB_URL}/reset`, }); if (error) { console.error(error); diff --git a/core/app/(user)/reset/ResetForm.tsx b/core/app/(user)/reset/ResetForm.tsx index 662d82b01c..fd6144cf82 100644 --- a/core/app/(user)/reset/ResetForm.tsx +++ b/core/app/(user)/reset/ResetForm.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useState, FormEvent } from "react"; import { Button } from "ui"; -import { supabase } from "lib/supabase"; +import { formatSupabaseError, supabase } from "lib/supabase"; import { useRouter } from "next/navigation"; export default function ResetForm() { @@ -9,18 +9,18 @@ export default function ResetForm() { const [password, setPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); const [success, setSuccess] = useState(false); - const [failure, setFailure] = useState(false); + const [error, setError] = useState(""); const handleSubmit = async (evt: FormEvent) => { setIsLoading(true); - setFailure(false); + setError(""); evt.preventDefault(); const { data, error } = await supabase.auth.updateUser({ password, }); if (error) { setIsLoading(false); - setFailure(true); + setError(formatSupabaseError(error)); } else if (data) { setIsLoading(false); setSuccess(true); @@ -46,7 +46,7 @@ export default function ResetForm() { - {failure && ( + {error && (
Error reseting password
)} diff --git a/core/app/(user)/settings/SettingsForm.tsx b/core/app/(user)/settings/SettingsForm.tsx index 1c6ba5cf0a..20fa7247db 100644 --- a/core/app/(user)/settings/SettingsForm.tsx +++ b/core/app/(user)/settings/SettingsForm.tsx @@ -88,7 +88,7 @@ export default function SettingsForm({ const resetPassword = async () => { setResetIsLoading(true); const { error } = await supabase.auth.resetPasswordForEmail(initEmail, { - redirectTo: "https://www.pubpub.org/reset", + redirectTo: `${process.env.NEXT_PUBLIC_PUBPUB_URL}/reset`, }); if (error) { console.error(error); diff --git a/core/app/api/supabase-webhooks/user/route.ts b/core/app/api/supabase-webhooks/user/route.ts index 2eb3437821..a5a41ab536 100644 --- a/core/app/api/supabase-webhooks/user/route.ts +++ b/core/app/api/supabase-webhooks/user/route.ts @@ -31,7 +31,7 @@ export async function POST(req: NextRequest) { try { await prisma.user.update({ where: { - id: body.record.id + supabaseId: body.record.id }, data: { email: newEmail diff --git a/core/app/api/user/route.ts b/core/app/api/user/route.ts index ea6d36effd..5f8105f0dc 100644 --- a/core/app/api/user/route.ts +++ b/core/app/api/user/route.ts @@ -3,7 +3,7 @@ import { NextRequest, NextResponse } from "next/server"; import prisma from "prisma/db"; import { getServerSupabase } from "lib/supabaseServer"; import { generateHash, getSlugSuffix, slugifyString } from "lib/string"; -import { getLoginId } from "lib/auth/loginId"; +import { getSupabaseId } from "lib/auth/loginId"; import { BadRequestError, ForbiddenError, UnauthorizedError, handleErrors } from "~/lib/server"; import { captureException } from "@sentry/nextjs"; @@ -27,19 +27,19 @@ export async function POST(req: NextRequest) { const existingUser = await prisma.user.findUnique({ where: { - email - } - }) + email, + }, + }); if (existingUser?.supabaseId) { - throw new ForbiddenError("User already exists") + throw new ForbiddenError("User already exists"); } const { data, error } = await supabase.auth.signUp({ email, password, options: { - emailRedirectTo: "https://www.pubpub.org/confirm", + emailRedirectTo: `${process.env.NEXT_PUBLIC_PUBPUB_URL}/login`, }, }); /* Supabase returns: @@ -71,13 +71,13 @@ export async function POST(req: NextRequest) { if (existingUser) { await prisma.user.update({ where: { - email + email, }, data: { - supabaseId: data.user.id - } - }) - return NextResponse.json({ message: "Existing account claimed" }, { status: 200 }) + supabaseId: data.user.id, + }, + }); + return NextResponse.json({ message: "Existing account claimed" }, { status: 200 }); } else { const newUser = await prisma.user.create({ data: { @@ -91,30 +91,28 @@ export async function POST(req: NextRequest) { email, }, }); - return NextResponse.json({}, { status: 201 }); + return NextResponse.json({message: "New user created"}, { status: 201 }); } }); } export async function PUT(req: NextRequest) { return await handleErrors(async () => { - const loginId = await getLoginId(req); - if (!loginId) { + const supabaseId = await getSupabaseId(req); + if (!supabaseId) { throw new UnauthorizedError(); } const submittedData: UserPutBody = await req.json(); const { firstName, lastName } = submittedData; const currentData = await prisma.user.findUnique({ - where: { id: loginId }, + where: { supabaseId }, }); if (!currentData) { throw new BadRequestError("Unable to find user"); } const slugSuffix = getSlugSuffix(currentData.slug); await prisma.user.update({ - where: { - id: loginId, - }, + where: { supabaseId }, data: { slug: `${slugifyString(firstName)}-${slugSuffix}`, firstName, @@ -128,8 +126,8 @@ export async function PUT(req: NextRequest) { // Used to determine if an email is available when a user attempts to change theirs export async function GET(req: NextRequest) { return await handleErrors(async () => { - const loginId = await getLoginId(req); - if (!loginId) { + const supabaseId = await getSupabaseId(req); + if (!supabaseId) { throw new UnauthorizedError(); } diff --git a/core/lib/auth/loginData.ts b/core/lib/auth/loginData.ts index a0deada984..c467b3cb76 100644 --- a/core/lib/auth/loginData.ts +++ b/core/lib/auth/loginData.ts @@ -3,7 +3,8 @@ import { cache } from "react"; import { cookies } from "next/headers"; import prisma from "~/prisma/db"; import { REFRESH_NAME, TOKEN_NAME } from "~/lib/auth/cookies"; -import { getIdFromJWT } from "~/lib/auth/loginId"; +import { getUserInfoFromJWT } from "~/lib/auth/loginId"; +import { generateHash, slugifyString } from "../string"; /* This is only called from Server Component functions */ /* When in the API, use getLoginId from loginId.ts */ @@ -11,11 +12,43 @@ export const getLoginData = cache(async () => { const nextCookies = cookies(); const sessionJWTCookie = nextCookies.get(TOKEN_NAME) || { value: "" }; const sessionRefreshCookie = nextCookies.get(REFRESH_NAME) || { value: "" }; - const loginId = await getIdFromJWT(sessionJWTCookie.value, sessionRefreshCookie.value); - if (!loginId) { + const supabaseUser = await getUserInfoFromJWT( + sessionJWTCookie.value, + sessionRefreshCookie.value + ); + if (!supabaseUser?.id) { return undefined; } - return prisma.user.findUnique({ - where: { id: loginId }, + let user = await prisma.user.findUnique({ + where: { supabaseId: supabaseUser.id }, }); + + if (!user) { + // They successfully logged in via supabase, but no corresponding record was found in the + // app database + + if (!supabaseUser.email) { + throw new Error( + `Unable to create corresponding local record for supabase user ${supabaseUser.id}` + ); + } + + // TODO: Instead of this, we should force invited users to visit the settings screen and set + // a name before progressing + const firstName = supabaseUser.user_metadata.firstName ?? ""; + const lastName = supabaseUser.user_metadata.lastName ?? null; + + user = await prisma.user.create({ + data: { + email: supabaseUser.email, + supabaseId: supabaseUser.id, + firstName, + lastName, + slug: `${slugifyString(firstName)}${ + lastName ? `-${slugifyString(lastName)}` : "" + }-${generateHash(4, "0123456789")}`, + }, + }); + } + return user; }); diff --git a/core/lib/auth/loginId.ts b/core/lib/auth/loginId.ts index cbd0c5a4fb..60e79263af 100644 --- a/core/lib/auth/loginId.ts +++ b/core/lib/auth/loginId.ts @@ -3,46 +3,71 @@ import jwt from "jsonwebtoken"; import { getRefreshCookie, getTokenCookie } from "~/lib/auth/cookies"; import { getServerSupabase } from "~/lib/supabaseServer"; +import type { UserAppMetadata, UserMetadata } from "@supabase/supabase-js"; const JWT_SECRET: string = process.env.JWT_SECRET || ""; /* This is only called from API calls */ /* When rendering server components, use getLoginData from loginData.ts */ -export async function getLoginId(req: NextRequest): Promise { +export async function getSupabaseId(req: NextRequest): Promise { const sessionJWT = getTokenCookie(req); if (!sessionJWT) { return "" } const refreshToken = getRefreshCookie(req); - return await getIdFromJWT(sessionJWT, refreshToken); + return await getSupabaseIdFromJWT(sessionJWT, refreshToken); } -export async function getIdFromJWT(sessionJWT?: string, refreshToken?: string): Promise { +export async function getSupabaseIdFromJWT(sessionJWT?: string, refreshToken?: string) { if (!sessionJWT) { return ""; } - try { - const { sub: userId } = await jwt.verify(sessionJWT, JWT_SECRET); - if (typeof userId !== "string") { - throw new Error("userId is not a string"); - } - return userId; - } catch (jwtError) { - console.error("In getLoginSession", jwtError); - /* We may get a jwtError if it has expired. In which case, */ - /* we try to use the refreshToken to sign back in before */ - /* waiting for the client to that after initial page load. */ - const supabase = getServerSupabase(); - const { data, error } = await supabase.auth.refreshSession({ - refresh_token: refreshToken || "", - }); - if (error) { - console.error("Error refreshing session:", error.message) - return ""; + + const user = await getUserInfoFromJWT(sessionJWT, refreshToken) + + if (!user?.id) { + return "" + } + + return user.id; +} + +type jwtUser = { + id: string, + email?: string, + aud: string, + app_metadata: UserAppMetadata, + user_metadata: UserMetadata, + role?: string, +} + +export async function getUserInfoFromJWT(sessionJWT: string, refreshToken?: string): Promise { + try { + const decoded = await jwt.verify(sessionJWT, JWT_SECRET); + if (typeof decoded === 'string' || !decoded.sub) { + throw new Error('Invalid jwt payload') + } + // TODO: actually validate the JWT payload! + // Rename `sub` to `id` for a consistent return type with the User that the + // refreshSession method below returns + return { id: decoded.sub, ...decoded} as jwtUser } - if (!data?.user?.id) { - return ""; + catch (jwtError) { + console.error("Error verifying jwt", jwtError); + /* We may get a jwtError if it has expired. In which case, */ + /* we try to use the refreshToken to sign back in before */ + /* waiting for the client to that after initial page load. */ + const supabase = getServerSupabase(); + const { data, error } = await supabase.auth.refreshSession({ + refresh_token: refreshToken || "", + }); + if (error) { + console.error("Error refreshing session:", error.message) + return null; + } + if (!data.user?.id) { + return null; + } + return data.user; } - return data.user.id; - } } diff --git a/core/lib/supabase.ts b/core/lib/supabase.ts index 9386a73a28..9c1bfa0f1b 100644 --- a/core/lib/supabase.ts +++ b/core/lib/supabase.ts @@ -1,4 +1,4 @@ -import { createClient, SupabaseClient } from "@supabase/supabase-js"; +import { AuthError, createClient, SupabaseClient } from "@supabase/supabase-js"; export let supabase: SupabaseClient; @@ -13,3 +13,5 @@ export const createBrowserSupabase = () => { }, }); }; + +export const formatSupabaseError = (error: AuthError) => `${error.name} ${error.status}: ${error.message}` \ No newline at end of file From e25af39d58b7f7de208d10944b1f5b583135aa3c Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Fri, 20 Oct 2023 17:44:46 -0400 Subject: [PATCH 19/27] Add script to invite users from csv --- core/package.json | 7 ++- core/scripts/invite.ts | 73 ++++++++++++++++++++++ core/tsconfig.json | 5 ++ pnpm-lock.yaml | 139 ++++++++++++++++++++++++++++++----------- 4 files changed, 185 insertions(+), 39 deletions(-) create mode 100644 core/scripts/invite.ts diff --git a/core/package.json b/core/package.json index 89ff0c3ba5..edb8a137a2 100644 --- a/core/package.json +++ b/core/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "next dev -p 3000", "build": "next build", + "invite-users": "dotenv -e .env.local ts-node scripts/invite.ts", "migrate-dev": "dotenv -e .env.local prisma migrate dev", "migrate-deploy": "dotenv -e .env.local prisma migrate deploy", "prisma-studio": "dotenv -e .env.local prisma studio", @@ -15,7 +16,7 @@ "type-check-watch": "tsc --watch" }, "prisma": { - "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" + "seed": "ts-node prisma/seed.ts" }, "dependencies": { "@faker-js/faker": "^8.0.2", @@ -56,6 +57,7 @@ "@types/react-dom": "18.2.5", "@types/uuid": "^9.0.2", "autoprefixer": "^10.4.14", + "csv-parse": "^5.5.2", "dotenv-cli": "^7.2.1", "postcss": "^8.4.27", "prisma": "^5.2.0", @@ -63,6 +65,7 @@ "tailwindcss": "^3.3.3", "ts-node": "^10.9.1", "tsconfig": "workspace:*", - "typescript": "5.1.3" + "typescript": "5.1.3", + "yargs": "^17.7.2" } } diff --git a/core/scripts/invite.ts b/core/scripts/invite.ts new file mode 100644 index 0000000000..aaa34b2a2b --- /dev/null +++ b/core/scripts/invite.ts @@ -0,0 +1,73 @@ +import { parse } from "csv-parse"; +import fs from "fs"; +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; +import { formatSupabaseError } from "../lib/supabase"; +import { createClient } from "@supabase/supabase-js"; +import { randomUUID } from "crypto"; + +const getServerSupabase = () => { + const url = process.env.NEXT_PUBLIC_SUPABASE_URL; + const key = process.env.SUPABASE_SERVICE_ROLE_KEY; + if (!url || !key) { + throw new Error("Missing Supabase parameters"); + } + return createClient(url, key, { + auth: { + persistSession: false, + }, + }); +}; +const client = getServerSupabase(); + +const inviteUser = async (email, firstName, lastName) => { + const { error: createError } = await client.auth.admin.createUser({email, + password: randomUUID(), + user_metadata: { + firstName, + lastName, + }, + email_confirm: true, + }); + if (createError) { + throw new Error(formatSupabaseError(createError)); + } + + const { error: resetError } = await client.auth.resetPasswordForEmail(email, { + redirectTo: `${process.env.NEXT_PUBLIC_PUBPUB_URL}/reset` + }) + if (resetError) { + throw new Error(formatSupabaseError(resetError)); + } +}; + +const inviteUsersFromCsv = async (path) => { + const parser = fs.createReadStream(path).pipe(parse({ columns: true })); + for await (const row of parser) { + if (!row.firstName || !row.email) { + console.log("Unable to invite user without firstname or email: ", row); + continue; + } + try { + await inviteUser(row.email, row.firstName, row.lastName); + } catch (err) { + console.log(err); + console.log(`Failed to invite ${row.firstName} ${row.lastName} ${row.email}`); + continue; + } + console.log(`Invited ${row.firstName} ${row.lastName} ${row.email}`); + } +}; + +const usage = () => { + console.log("Usage:\npnpm --filter core invite-users "); + process.exit(); +}; + +const { _: args} = yargs(hideBin(process.argv)).argv; +if (args.length !== 1) { + console.log(args); + usage(); +} + +inviteUsersFromCsv(args[0]); diff --git a/core/tsconfig.json b/core/tsconfig.json index 96e887d424..1b50f2e372 100644 --- a/core/tsconfig.json +++ b/core/tsconfig.json @@ -15,6 +15,11 @@ }, "strictNullChecks": true }, + "ts-node": { + "compilerOptions": { + "module": "CommonJS" + } + }, "include": [ "next-env.d.ts", "**/*.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95c17ef8c4..23b03c15cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,7 +40,7 @@ importers: version: 0.1.11(prettier@2.8.8) turbo: specifier: latest - version: 1.10.15 + version: 1.10.13 core: dependencies: @@ -55,7 +55,7 @@ importers: version: 7.72.0(next@13.5.2)(react@18.2.0) '@stoplight/elements': specifier: ^7.12.2 - version: 7.12.2(@babel/core@7.22.17)(react-dom@18.2.0)(react@18.2.0) + version: 7.12.2(react-dom@18.2.0)(react@18.2.0) '@supabase/supabase-js': specifier: ^2.33.2 version: 2.33.2 @@ -88,7 +88,7 @@ importers: version: 9.0.0 next: specifier: 13.5.2 - version: 13.5.2(@babel/core@7.22.17)(react-dom@18.2.0)(react@18.2.0) + version: 13.5.2(react-dom@18.2.0)(react@18.2.0) next-connect: specifier: ^1.0.0 version: 1.0.0 @@ -153,6 +153,9 @@ importers: autoprefixer: specifier: ^10.4.14 version: 10.4.14(postcss@8.4.27) + csv-parse: + specifier: ^5.5.2 + version: 5.5.2 dotenv-cli: specifier: ^7.2.1 version: 7.2.1 @@ -177,6 +180,9 @@ importers: typescript: specifier: 5.1.3 version: 5.1.3 + yargs: + specifier: ^17.7.2 + version: 17.7.2 integrations/evaluations: dependencies: @@ -3692,7 +3698,7 @@ packages: '@sentry/vercel-edge': 7.72.0 '@sentry/webpack-plugin': 1.20.0 chalk: 3.0.0 - next: 13.5.2(@babel/core@7.22.17)(react-dom@18.2.0)(react@18.2.0) + next: 13.5.2(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 rollup: 2.78.0 stacktrace-parser: 0.1.10 @@ -3803,7 +3809,7 @@ packages: - supports-color dev: false - /@stoplight/elements-core@7.12.3(@babel/core@7.22.17)(react-dom@18.2.0)(react@18.2.0): + /@stoplight/elements-core@7.12.3(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-SMhV9rCJk8FIOBuSZrcCa9vVfz9Bgjbj0Btxuv/E7pieIVP+7tkTfXILvTHG3n+D/abpK5rJ2VYGv7vH6MlL+w==} engines: {node: '>=14.13'} peerDependencies: @@ -3814,7 +3820,7 @@ packages: '@stoplight/json': 3.21.0 '@stoplight/json-schema-ref-parser': 9.2.7 '@stoplight/json-schema-sampler': 0.2.3 - '@stoplight/json-schema-viewer': 4.12.1(@babel/core@7.22.17)(@stoplight/markdown-viewer@5.6.0)(@stoplight/mosaic-code-viewer@1.44.3)(@stoplight/mosaic@1.44.3)(react-dom@18.2.0)(react@18.2.0) + '@stoplight/json-schema-viewer': 4.12.1(@stoplight/markdown-viewer@5.6.0)(@stoplight/mosaic-code-viewer@1.44.3)(@stoplight/mosaic@1.44.3)(react-dom@18.2.0)(react@18.2.0) '@stoplight/markdown-viewer': 5.6.0(@stoplight/mosaic-code-viewer@1.44.3)(@stoplight/mosaic@1.44.3)(react-dom@18.2.0)(react@18.2.0) '@stoplight/mosaic': 1.44.3(react-dom@18.2.0)(react@18.2.0) '@stoplight/mosaic-code-editor': 1.44.3(react-dom@18.2.0)(react@18.2.0) @@ -3825,7 +3831,7 @@ packages: '@stoplight/yaml': 4.2.3 classnames: 2.3.2 httpsnippet-lite: 3.0.5 - jotai: 1.3.9(@babel/core@7.22.17)(react-query@3.39.3)(react@18.2.0) + jotai: 1.3.9(react-query@3.39.3)(react@18.2.0) json-schema: 0.4.0 lodash: 4.17.21 nanoid: 3.3.6 @@ -3862,14 +3868,14 @@ packages: - xstate dev: false - /@stoplight/elements@7.12.2(@babel/core@7.22.17)(react-dom@18.2.0)(react@18.2.0): + /@stoplight/elements@7.12.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-lyzY3wnQp8LoEX7BM+ygvLn1WlaoQTuKzVxWq7HgvU21Gt2ZD7Tc/tLAs7bbyXVBbn11A+MyqfOTZzAkiEnIUw==} engines: {node: '>=14.13'} peerDependencies: react: '>=16.8' react-dom: '>=16.8' dependencies: - '@stoplight/elements-core': 7.12.3(@babel/core@7.22.17)(react-dom@18.2.0)(react@18.2.0) + '@stoplight/elements-core': 7.12.3(react-dom@18.2.0)(react@18.2.0) '@stoplight/http-spec': 5.13.0 '@stoplight/json': 3.21.0 '@stoplight/mosaic': 1.44.3(react-dom@18.2.0)(react@18.2.0) @@ -3980,7 +3986,7 @@ packages: magic-error: 0.0.1 dev: false - /@stoplight/json-schema-viewer@4.12.1(@babel/core@7.22.17)(@stoplight/markdown-viewer@5.6.0)(@stoplight/mosaic-code-viewer@1.44.3)(@stoplight/mosaic@1.44.3)(react-dom@18.2.0)(react@18.2.0): + /@stoplight/json-schema-viewer@4.12.1(@stoplight/markdown-viewer@5.6.0)(@stoplight/mosaic-code-viewer@1.44.3)(@stoplight/mosaic@1.44.3)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-oMx0WrggjDPbIUcoqO7gx8dx1QA9bs7Y2jYNGAWnDe1wmsGcOOevkJ5LaruoCbuYAvsoGsBfqyJv8Ec8ATHBGQ==} engines: {node: '>=16'} peerDependencies: @@ -3999,7 +4005,7 @@ packages: '@types/json-schema': 7.0.12 classnames: 2.3.2 fnv-plus: 1.3.1 - jotai: 1.13.1(@babel/core@7.22.17)(react@18.2.0) + jotai: 1.13.1(react@18.2.0) lodash: 4.17.21 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -4347,7 +4353,7 @@ packages: optional: true dependencies: '@ts-rest/core': 3.28.0(zod@3.21.4) - next: 13.5.2(@babel/core@7.22.17)(react-dom@18.2.0)(react@18.2.0) + next: 13.5.2(react-dom@18.2.0)(react@18.2.0) zod: 3.21.4 dev: false @@ -5154,7 +5160,7 @@ packages: dev: false /concat-map@0.0.1: - resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} /convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} @@ -5249,6 +5255,10 @@ packages: resolution: {integrity: sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==} dev: true + /csv-parse@5.5.2: + resolution: {integrity: sha512-YRVtvdtUNXZCMyK5zd5Wty1W6dNTpGKdqQd4EQ8tl/c6KW1aMBB1Kg1ppky5FONKmEqGJ/8WjLlTNLPne4ioVA==} + dev: true + /csv-stringify@5.6.5: resolution: {integrity: sha512-PjiQ659aQ+fUTQqSrd1XEDnOr52jh30RBurfzkscaE2tPaFsDH5wOAHJiw8XAHphRknCwMUE9KRayc4K/NbO8A==} dev: true @@ -6525,7 +6535,7 @@ packages: resolution: {integrity: sha512-oVhqoRDaBXf7sjkll95LHVS6Myyyb1zaunVwk4Z0+WPSW4gjS0pl01zYKHScTuyEhQsFxV5L4DR5r+YqSyqyyg==} hasBin: true - /jotai@1.13.1(@babel/core@7.22.17)(react@18.2.0): + /jotai@1.13.1(react@18.2.0): resolution: {integrity: sha512-RUmH1S4vLsG3V6fbGlKzGJnLrDcC/HNb5gH2AeA9DzuJknoVxSGvvg8OBB7lke+gDc4oXmdVsaKn/xDUhWZ0vw==} engines: {node: '>=12.20.0'} peerDependencies: @@ -6565,11 +6575,10 @@ packages: jotai-zustand: optional: true dependencies: - '@babel/core': 7.22.17 react: 18.2.0 dev: false - /jotai@1.3.9(@babel/core@7.22.17)(react-query@3.39.3)(react@18.2.0): + /jotai@1.3.9(react-query@3.39.3)(react@18.2.0): resolution: {integrity: sha512-b6DvH9gf+7TfjaboCO54g+C0yhaakIaUBtjLf0dk1p15FWCzNw/93sezdXy9cCaZ8qcEdMLJcjBwQlORmIq29g==} engines: {node: '>=12.7.0'} peerDependencies: @@ -6603,7 +6612,6 @@ packages: xstate: optional: true dependencies: - '@babel/core': 7.22.17 react: 18.2.0 react-query: 3.39.3(react-dom@18.2.0)(react@18.2.0) dev: false @@ -7389,6 +7397,46 @@ packages: - babel-plugin-macros dev: false + /next@13.5.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-vog4UhUaMYAzeqfiAAmgB/QWLW7p01/sg+2vn6bqc/CxHFYizMzLv6gjxKzl31EVFkfl/F+GbxlKizlkTE9RdA==} + engines: {node: '>=16.14.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + sass: + optional: true + dependencies: + '@next/env': 13.5.2 + '@swc/helpers': 0.5.2 + busboy: 1.6.0 + caniuse-lite: 1.0.30001519 + postcss: 8.4.14 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + styled-jsx: 5.1.1(react@18.2.0) + watchpack: 2.4.0 + zod: 3.21.4 + optionalDependencies: + '@next/swc-darwin-arm64': 13.5.2 + '@next/swc-darwin-x64': 13.5.2 + '@next/swc-linux-arm64-gnu': 13.5.2 + '@next/swc-linux-arm64-musl': 13.5.2 + '@next/swc-linux-x64-gnu': 13.5.2 + '@next/swc-linux-x64-musl': 13.5.2 + '@next/swc-win32-arm64-msvc': 13.5.2 + '@next/swc-win32-ia32-msvc': 13.5.2 + '@next/swc-win32-x64-msvc': 13.5.2 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: false + /node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} dev: false @@ -8883,6 +8931,23 @@ packages: react: 18.2.0 dev: false + /styled-jsx@5.1.1(react@18.2.0): + resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + dependencies: + client-only: 0.0.1 + react: 18.2.0 + dev: false + /stylis@4.3.0: resolution: {integrity: sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==} dev: false @@ -9135,64 +9200,64 @@ packages: yargs: 17.7.2 dev: true - /turbo-darwin-64@1.10.15: - resolution: {integrity: sha512-Sik5uogjkRTe1XVP9TC2GryEMOJCaKE2pM/O9uLn4koQDnWKGcLQv+mDU+H+9DXvKLnJnKCD18OVRkwK5tdpoA==} + /turbo-darwin-64@1.10.13: + resolution: {integrity: sha512-vmngGfa2dlYvX7UFVncsNDMuT4X2KPyPJ2Jj+xvf5nvQnZR/3IeDEGleGVuMi/hRzdinoxwXqgk9flEmAYp0Xw==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@1.10.15: - resolution: {integrity: sha512-xwqyFDYUcl2xwXyGPmHkmgnNm4Cy0oNzMpMOBGRr5x64SErS7QQLR4VHb0ubiR+VAb8M+ECPklU6vD1Gm+wekg==} + /turbo-darwin-arm64@1.10.13: + resolution: {integrity: sha512-eMoJC+k7gIS4i2qL6rKmrIQGP6Wr9nN4odzzgHFngLTMimok2cGLK3qbJs5O5F/XAtEeRAmuxeRnzQwTl/iuAw==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@1.10.15: - resolution: {integrity: sha512-dM07SiO3RMAJ09Z+uB2LNUSkPp3I1IMF8goH5eLj+d8Kkwoxd/+qbUZOj9RvInyxU/IhlnO9w3PGd3Hp14m/nA==} + /turbo-linux-64@1.10.13: + resolution: {integrity: sha512-0CyYmnKTs6kcx7+JRH3nPEqCnzWduM0hj8GP/aodhaIkLNSAGAa+RiYZz6C7IXN+xUVh5rrWTnU2f1SkIy7Gdg==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@1.10.15: - resolution: {integrity: sha512-MkzKLkKYKyrz4lwfjNXH8aTny5+Hmiu4SFBZbx+5C0vOlyp6fV5jZANDBvLXWiDDL4DSEAuCEK/2cmN6FVH1ow==} + /turbo-linux-arm64@1.10.13: + resolution: {integrity: sha512-0iBKviSGQQlh2OjZgBsGjkPXoxvRIxrrLLbLObwJo3sOjIH0loGmVIimGS5E323soMfi/o+sidjk2wU1kFfD7Q==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@1.10.15: - resolution: {integrity: sha512-3TdVU+WEH9ThvQGwV3ieX/XHebtYNHv9HARHauPwmVj3kakoALkpGxLclkHFBLdLKkqDvmHmXtcsfs6cXXRHJg==} + /turbo-windows-64@1.10.13: + resolution: {integrity: sha512-S5XySRfW2AmnTeY1IT+Jdr6Goq7mxWganVFfrmqU+qqq3Om/nr0GkcUX+KTIo9mPrN0D3p5QViBRzulwB5iuUQ==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@1.10.15: - resolution: {integrity: sha512-l+7UOBCbfadvPMYsX08hyLD+UIoAkg6ojfH+E8aud3gcA1padpjCJTh9gMpm3QdMbKwZteT5uUM+wyi6Rbbyww==} + /turbo-windows-arm64@1.10.13: + resolution: {integrity: sha512-nKol6+CyiExJIuoIc3exUQPIBjP9nIq5SkMJgJuxsot2hkgGrafAg/izVDRDrRduQcXj2s8LdtxJHvvnbI8hEQ==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@1.10.15: - resolution: {integrity: sha512-mKKkqsuDAQy1wCCIjCdG+jOCwUflhckDMSRoeBPcIL/CnCl7c5yRDFe7SyaXloUUkt4tUR0rvNIhVCcT7YeQpg==} + /turbo@1.10.13: + resolution: {integrity: sha512-vOF5IPytgQPIsgGtT0n2uGZizR2N3kKuPIn4b5p5DdeLoI0BV7uNiydT7eSzdkPRpdXNnO8UwS658VaI4+YSzQ==} hasBin: true optionalDependencies: - turbo-darwin-64: 1.10.15 - turbo-darwin-arm64: 1.10.15 - turbo-linux-64: 1.10.15 - turbo-linux-arm64: 1.10.15 - turbo-windows-64: 1.10.15 - turbo-windows-arm64: 1.10.15 + turbo-darwin-64: 1.10.13 + turbo-darwin-arm64: 1.10.13 + turbo-linux-64: 1.10.13 + turbo-linux-arm64: 1.10.13 + turbo-windows-64: 1.10.13 + turbo-windows-arm64: 1.10.13 dev: true /type-fest@0.13.1: From 784df8d08881c2371ef0c58bd9e4e99d9b65a519 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Fri, 20 Oct 2023 18:10:31 -0400 Subject: [PATCH 20/27] Create memberships for users --- core/lib/auth/loginData.ts | 8 ++++++++ core/prisma/seed.ts | 3 ++- core/scripts/invite.ts | 12 ++++++++---- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/core/lib/auth/loginData.ts b/core/lib/auth/loginData.ts index c467b3cb76..f39c5a5f78 100644 --- a/core/lib/auth/loginData.ts +++ b/core/lib/auth/loginData.ts @@ -37,6 +37,8 @@ export const getLoginData = cache(async () => { // a name before progressing const firstName = supabaseUser.user_metadata.firstName ?? ""; const lastName = supabaseUser.user_metadata.lastName ?? null; + const communityId = supabaseUser.user_metadata.communityId; + const canAdmin = supabaseUser.user_metadata.canAdmin ?? false; user = await prisma.user.create({ data: { @@ -47,6 +49,12 @@ export const getLoginData = cache(async () => { slug: `${slugifyString(firstName)}${ lastName ? `-${slugifyString(lastName)}` : "" }-${generateHash(4, "0123456789")}`, + memberships: { + create: { + communityId, + canAdmin, + } + } }, }); } diff --git a/core/prisma/seed.ts b/core/prisma/seed.ts index 035d7c0d16..fac363cfd9 100644 --- a/core/prisma/seed.ts +++ b/core/prisma/seed.ts @@ -2,6 +2,8 @@ import { PrismaClient } from "@prisma/client"; import { SupabaseClient } from "@supabase/supabase-js"; import buildUnjournal from "./exampleCommunitySeeds/unjournal"; +export const unJournalId = "03e7a5fd-bdca-4682-9221-3a69992c1f3b"; + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; const prisma = new PrismaClient(); @@ -47,7 +49,6 @@ async function createUserMembers( } async function main() { - const unJournalId = "03e7a5fd-bdca-4682-9221-3a69992c1f3b"; const prismaCommunityIds = [{ communityId: unJournalId, canAdmin: true }]; await buildUnjournal(prisma, unJournalId); diff --git a/core/scripts/invite.ts b/core/scripts/invite.ts index aaa34b2a2b..8b96535588 100644 --- a/core/scripts/invite.ts +++ b/core/scripts/invite.ts @@ -5,6 +5,7 @@ import { hideBin } from "yargs/helpers"; import { formatSupabaseError } from "../lib/supabase"; import { createClient } from "@supabase/supabase-js"; import { randomUUID } from "crypto"; +import { unJournalId } from "../prisma/seed"; const getServerSupabase = () => { const url = process.env.NEXT_PUBLIC_SUPABASE_URL; @@ -21,11 +22,14 @@ const getServerSupabase = () => { const client = getServerSupabase(); const inviteUser = async (email, firstName, lastName) => { - const { error: createError } = await client.auth.admin.createUser({email, + const { error: createError } = await client.auth.admin.createUser({ + email, password: randomUUID(), user_metadata: { firstName, lastName, + communityId: unJournalId, + canAdmin: true, }, email_confirm: true, }); @@ -34,8 +38,8 @@ const inviteUser = async (email, firstName, lastName) => { } const { error: resetError } = await client.auth.resetPasswordForEmail(email, { - redirectTo: `${process.env.NEXT_PUBLIC_PUBPUB_URL}/reset` - }) + redirectTo: `${process.env.NEXT_PUBLIC_PUBPUB_URL}/reset`, + }); if (resetError) { throw new Error(formatSupabaseError(resetError)); } @@ -64,7 +68,7 @@ const usage = () => { process.exit(); }; -const { _: args} = yargs(hideBin(process.argv)).argv; +const { _: args } = yargs(hideBin(process.argv)).argv; if (args.length !== 1) { console.log(args); usage(); From 4303788337d2d1de8ebd38a2d43fa5c9c0b5274c Mon Sep 17 00:00:00 2001 From: Gabriel Stein Date: Thu, 2 Nov 2023 16:00:13 -0400 Subject: [PATCH 21/27] rebase, memo, order fields --- .../prisma/exampleCommunitySeeds/unjournal.ts | 50 +++++++++---------- .../app/actions/evaluate/evaluate.tsx | 23 +++++---- packages/sdk/src/react/generateFormFields.tsx | 3 +- 3 files changed, 39 insertions(+), 37 deletions(-) diff --git a/core/prisma/exampleCommunitySeeds/unjournal.ts b/core/prisma/exampleCommunitySeeds/unjournal.ts index 4d74652097..639df37957 100644 --- a/core/prisma/exampleCommunitySeeds/unjournal.ts +++ b/core/prisma/exampleCommunitySeeds/unjournal.ts @@ -12,8 +12,8 @@ export default async function main(prisma: PrismaClient, communityUUID: string) }, }); - const commentsObject = { - comments: { + const confidenceCommentsObject = { + confidence2: { title: "Additional Comments", type: "string", minLength: 0, @@ -58,60 +58,60 @@ export default async function main(prisma: PrismaClient, communityUUID: string) confidence: HundredConfidenceDef, }, properties: { - assessment: { + metrics1: { title: "Overall assessment", type: "object", properties: { - confidence: { $ref: "#/$defs/confidence" }, - ...commentsObject, + confidence1: { $ref: "#/$defs/confidence" }, + ...confidenceCommentsObject, }, }, - advancing: { + metrics2: { title: "Advancing knowledge and practice", type: "object", properties: { - confidence: { $ref: "#/$defs/confidence" }, - ...commentsObject, + confidence1: { $ref: "#/$defs/confidence" }, + ...confidenceCommentsObject, }, }, - methods: { + metrics3: { title: "Methods: Justification, reasonableness, validity, robustness", type: "object", properties: { - confidence: { $ref: "#/$defs/confidence" }, - ...commentsObject, + confidence1: { $ref: "#/$defs/confidence" }, + ...confidenceCommentsObject, }, }, - logic: { + metrics4: { title: "Logic & communication", type: "object", properties: { - confidence: { $ref: "#/$defs/confidence" }, - ...commentsObject, + confidence1: { $ref: "#/$defs/confidence" }, + ...confidenceCommentsObject, }, }, - open: { + metrics5: { title: "Open, collaborative, replicable", type: "object", properties: { - confidence: { $ref: "#/$defs/confidence" }, - ...commentsObject, + confidence1: { $ref: "#/$defs/confidence" }, + ...confidenceCommentsObject, }, }, - real: { + metrics6: { title: "Engaging with real-world, impact quantification; practice, realism, and relevance", type: "object", properties: { - confidence: { $ref: "#/$defs/confidence" }, - ...commentsObject, + confidence1: { $ref: "#/$defs/confidence" }, + ...confidenceCommentsObject, }, }, - relevance: { + metrics7: { title: "Relevance to global priorities", type: "object", properties: { - confidence: { $ref: "#/$defs/confidence" }, - ...commentsObject, + confidence1: { $ref: "#/$defs/confidence" }, + ...confidenceCommentsObject, }, }, }, @@ -152,7 +152,7 @@ export default async function main(prisma: PrismaClient, communityUUID: string) type: "object", properties: { confidence: { $ref: "#/$defs/confidence" }, - ...commentsObject, + ...confidenceCommentsObject, }, }, qualityLevel: { @@ -160,7 +160,7 @@ export default async function main(prisma: PrismaClient, communityUUID: string) type: "object", properties: { confidence: { $ref: "#/$defs/confidence" }, - ...commentsObject, + ...confidenceCommentsObject, }, }, }, diff --git a/integrations/evaluations/app/actions/evaluate/evaluate.tsx b/integrations/evaluations/app/actions/evaluate/evaluate.tsx index 986cf07328..fdd1a0ce57 100644 --- a/integrations/evaluations/app/actions/evaluate/evaluate.tsx +++ b/integrations/evaluations/app/actions/evaluate/evaluate.tsx @@ -32,13 +32,10 @@ export function Evaluate(props: Props) { const { pub, pubType } = props; const { toast } = useToast(); - // we need to use an uncompiled schema for validation, but compiled for building the form - // "Schema" is a key later used to retrieve this schema (we could later pass multiple for dereferencing, for example) - const exclude = ["unjournal:title", "unjournal:evaluator"]; - const generatedSchema = buildFormSchemaFromFields(pubType, exclude); - const ajv = new Ajv(); - const schemaKey = "schema"; - const compiledSchema = ajv.addSchema(generatedSchema, schemaKey); + const generatedSchema = useMemo(() => { + const exclude = ["unjournal:title", "unjournal:evaluator"]; + return buildFormSchemaFromFields(pubType, exclude); + }, [pubType]); const form = useForm({ mode: "onChange", @@ -78,10 +75,14 @@ export function Evaluate(props: Props) { persist(values); }, [values]); - const formFieldsFromSchema = useMemo( - () => buildFormFieldsFromSchema(compiledSchema, schemaKey, form.control), - [form.control] - ); + const formFieldsFromSchema = useMemo(() => { + // we need to use an uncompiled schema for validation, but compiled for building the form + // "Schema" is a key later used to retrieve this schema (we could later pass multiple for dereferencing, for example) + const ajv = new Ajv(); + const schemaKey = "schema"; + const compiledSchema = ajv.addSchema(generatedSchema, schemaKey); + return buildFormFieldsFromSchema(compiledSchema, schemaKey, form.control); + }, [form.control, pubType]); return ( <> diff --git a/packages/sdk/src/react/generateFormFields.tsx b/packages/sdk/src/react/generateFormFields.tsx index 94e7d04eeb..a443dba130 100644 --- a/packages/sdk/src/react/generateFormFields.tsx +++ b/packages/sdk/src/react/generateFormFields.tsx @@ -155,7 +155,7 @@ const CustomRenderer = (props: CustomRendererProps) => { name={fieldName} defaultValue={fieldSchema.default ?? [0, 0, 0]} render={({ field }) => ( - + {fieldSchema.title} { min={min} max={max} onValueChange={(event) => field.onChange(event)} + className="confidence" /> From 2421331adf029e82b09b12149346d5060678b285 Mon Sep 17 00:00:00 2001 From: Gabriel Stein Date: Thu, 2 Nov 2023 16:02:14 -0400 Subject: [PATCH 22/27] generatedSchema dependency --- integrations/evaluations/app/actions/evaluate/evaluate.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/evaluations/app/actions/evaluate/evaluate.tsx b/integrations/evaluations/app/actions/evaluate/evaluate.tsx index fdd1a0ce57..02dd7739fd 100644 --- a/integrations/evaluations/app/actions/evaluate/evaluate.tsx +++ b/integrations/evaluations/app/actions/evaluate/evaluate.tsx @@ -82,7 +82,7 @@ export function Evaluate(props: Props) { const schemaKey = "schema"; const compiledSchema = ajv.addSchema(generatedSchema, schemaKey); return buildFormFieldsFromSchema(compiledSchema, schemaKey, form.control); - }, [form.control, pubType]); + }, [form.control, pubType, generatedSchema]); return ( <> From 0643386b01d1b013726a6ee68eee14a55ac62a73 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Mon, 30 Oct 2023 16:12:33 -0500 Subject: [PATCH 23/27] Avoid accidentally running seed script --- core/prisma/exampleCommunitySeeds/unjournal.ts | 2 ++ core/prisma/seed.ts | 4 +--- core/scripts/invite.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/prisma/exampleCommunitySeeds/unjournal.ts b/core/prisma/exampleCommunitySeeds/unjournal.ts index 639df37957..9c9109e8df 100644 --- a/core/prisma/exampleCommunitySeeds/unjournal.ts +++ b/core/prisma/exampleCommunitySeeds/unjournal.ts @@ -2,6 +2,8 @@ import { PrismaClient } from "@prisma/client"; import { v4 as uuidv4 } from "uuid"; import { faker } from "@faker-js/faker"; +export const unJournalId = "03e7a5fd-bdca-4682-9221-3a69992c1f3b"; + export default async function main(prisma: PrismaClient, communityUUID: string) { await prisma.community.create({ data: { diff --git a/core/prisma/seed.ts b/core/prisma/seed.ts index fac363cfd9..94adffbb11 100644 --- a/core/prisma/seed.ts +++ b/core/prisma/seed.ts @@ -1,8 +1,6 @@ import { PrismaClient } from "@prisma/client"; import { SupabaseClient } from "@supabase/supabase-js"; -import buildUnjournal from "./exampleCommunitySeeds/unjournal"; - -export const unJournalId = "03e7a5fd-bdca-4682-9221-3a69992c1f3b"; +import { default as buildUnjournal, unJournalId } from "./exampleCommunitySeeds/unjournal"; const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; diff --git a/core/scripts/invite.ts b/core/scripts/invite.ts index 8b96535588..8897d3a512 100644 --- a/core/scripts/invite.ts +++ b/core/scripts/invite.ts @@ -5,7 +5,7 @@ import { hideBin } from "yargs/helpers"; import { formatSupabaseError } from "../lib/supabase"; import { createClient } from "@supabase/supabase-js"; import { randomUUID } from "crypto"; -import { unJournalId } from "../prisma/seed"; +import { unJournalId } from "../prisma/exampleCommunitySeeds/unjournal"; const getServerSupabase = () => { const url = process.env.NEXT_PUBLIC_SUPABASE_URL; From ee55f9463992e783da5472abd881ac55bb2b2523 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Mon, 30 Oct 2023 16:12:56 -0500 Subject: [PATCH 24/27] Use signup instead of create + reset --- core/scripts/invite.ts | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/core/scripts/invite.ts b/core/scripts/invite.ts index 8897d3a512..7c099825ca 100644 --- a/core/scripts/invite.ts +++ b/core/scripts/invite.ts @@ -22,26 +22,21 @@ const getServerSupabase = () => { const client = getServerSupabase(); const inviteUser = async (email, firstName, lastName) => { - const { error: createError } = await client.auth.admin.createUser({ + const { error } = await client.auth.signUp({ email, password: randomUUID(), - user_metadata: { - firstName, - lastName, - communityId: unJournalId, - canAdmin: true, + options: { + emailRedirectTo: `${process.env.NEXT_PUBLIC_PUBPUB_URL}/reset`, + data: { + firstName, + lastName, + communityId: unJournalId, + canAdmin: true, + }, }, - email_confirm: true, }); - if (createError) { - throw new Error(formatSupabaseError(createError)); - } - - const { error: resetError } = await client.auth.resetPasswordForEmail(email, { - redirectTo: `${process.env.NEXT_PUBLIC_PUBPUB_URL}/reset`, - }); - if (resetError) { - throw new Error(formatSupabaseError(resetError)); + if (error) { + throw new Error(formatSupabaseError(error)); } }; From 86962948d39d2600685ec8d106ec1c020b87d372 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Thu, 2 Nov 2023 15:28:13 -0500 Subject: [PATCH 25/27] Disable claiming accounts --- core/app/api/user/route.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/core/app/api/user/route.ts b/core/app/api/user/route.ts index 5f8105f0dc..f40c72b629 100644 --- a/core/app/api/user/route.ts +++ b/core/app/api/user/route.ts @@ -69,14 +69,15 @@ export async function POST(req: NextRequest) { } if (existingUser) { - await prisma.user.update({ - where: { - email, - }, - data: { - supabaseId: data.user.id, - }, - }); + // TODO: create community membership here, update name, slug etc. + // await prisma.user.update({ + // where: { + // email, + // }, + // data: { + // supabaseId: data.user.id, + // }, + // }); return NextResponse.json({ message: "Existing account claimed" }, { status: 200 }); } else { const newUser = await prisma.user.create({ From e3e33b317d22a66ab755ae26a5cab10cd5851147 Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Thu, 2 Nov 2023 15:28:47 -0500 Subject: [PATCH 26/27] Make sure we're setting supabase_id rather than id on user creation --- core/app/api/user/route.ts | 1 - core/prisma/seed.ts | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/app/api/user/route.ts b/core/app/api/user/route.ts index f40c72b629..75cfa99c09 100644 --- a/core/app/api/user/route.ts +++ b/core/app/api/user/route.ts @@ -82,7 +82,6 @@ export async function POST(req: NextRequest) { } else { const newUser = await prisma.user.create({ data: { - id: data.user.id, supabaseId: data.user.id, slug: `${slugifyString(firstName)}${ lastName ? `-${slugifyString(lastName)}` : "" diff --git a/core/prisma/seed.ts b/core/prisma/seed.ts index 94adffbb11..c7553113ce 100644 --- a/core/prisma/seed.ts +++ b/core/prisma/seed.ts @@ -33,11 +33,12 @@ async function createUserMembers( } else { user = data.user; } + await prisma.user.create({ data: { - id: user ? user.id : undefined, slug, email: user ? user.email : email, + supabaseId: user.id, firstName, lastName, avatar: "/demo/person.png", From c7c3273eca92b270ab5450fa0056eafab4c609bd Mon Sep 17 00:00:00 2001 From: Kalil Smith-Nuevelle Date: Thu, 2 Nov 2023 17:10:20 -0500 Subject: [PATCH 27/27] Display reset error to user --- core/app/(user)/reset/ResetForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/app/(user)/reset/ResetForm.tsx b/core/app/(user)/reset/ResetForm.tsx index fd6144cf82..ed252cfda9 100644 --- a/core/app/(user)/reset/ResetForm.tsx +++ b/core/app/(user)/reset/ResetForm.tsx @@ -47,7 +47,7 @@ export default function ResetForm() { Set new password {error && ( -
Error reseting password
+
Error resetting password: {error}
)}