From 2362716219c8238fe28fc47cc5554922c6258130 Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Thu, 7 Nov 2024 17:42:32 +0100 Subject: [PATCH 1/2] removed signed up at edit --- apps/dashboard/src/components/user-dialog.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/dashboard/src/components/user-dialog.tsx b/apps/dashboard/src/components/user-dialog.tsx index 13d967b78..24a2ba11b 100644 --- a/apps/dashboard/src/components/user-dialog.tsx +++ b/apps/dashboard/src/components/user-dialog.tsx @@ -27,7 +27,6 @@ export function UserDialog(props: { displayName: props.user.displayName || undefined, primaryEmail: props.user.primaryEmail || undefined, primaryEmailVerified: props.user.primaryEmailVerified, - signedUpAt: props.user.signedUpAt, clientMetadata: props.user.clientMetadata == null ? "" : JSON.stringify(props.user.clientMetadata, null, 2), clientReadOnlyMetadata: props.user.clientReadOnlyMetadata == null ? "" : JSON.stringify(props.user.clientReadOnlyMetadata, null, 2), serverMetadata: props.user.serverMetadata == null ? "" : JSON.stringify(props.user.serverMetadata, null, 2), @@ -35,9 +34,7 @@ export function UserDialog(props: { otpAuthEnabled: props.user.otpAuthEnabled, }; } else { - defaultValues = { - signedUpAt: new Date(), - }; + defaultValues = {}; } const formSchema = yup.object({ @@ -109,8 +106,6 @@ export function UserDialog(props: { - - {project.config.magicLinkEnabled && } {project.config.credentialEnabled && } {form.watch("passwordEnabled") ? : null} From ba5f32355ac17d09b39f1df5e2b84b0e1af2c062 Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Thu, 7 Nov 2024 18:17:47 +0100 Subject: [PATCH 2/2] restructured code --- .../projects/[projectId]/page-layout.tsx | 12 +- .../users/[userId]/page-client.tsx | 27 ++++ .../[projectId]/users/[userId]/page.tsx | 11 ++ .../[projectId]/users/page-client.tsx | 101 +++++++++++++- apps/dashboard/src/app/not-found.tsx | 3 - .../src/components/data-table/user-table.tsx | 7 +- apps/dashboard/src/components/user-dialog.tsx | 129 ------------------ 7 files changed, 148 insertions(+), 142 deletions(-) create mode 100644 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx create mode 100644 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page.tsx delete mode 100644 apps/dashboard/src/components/user-dialog.tsx diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/page-layout.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/page-layout.tsx index 8a1b54da6..4ab2c0e73 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/page-layout.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/page-layout.tsx @@ -1,14 +1,22 @@ -import { Typography } from "@stackframe/stack-ui"; +import { Typography, cn } from "@stackframe/stack-ui"; export function PageLayout(props: { children: React.ReactNode, title: string, description?: string, actions?: React.ReactNode, + width?: "full" | "lg" | "md" | "sm", }) { + const widthClass = { + full: "w-full", + lg: "max-w-[1250px] w-[1250px]", + md: "max-w-[800px] w-[800px]", + sm: "max-w-[600px] w-[600px]", + }[props.width ?? "lg"]; + return (
-
+
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx new file mode 100644 index 000000000..0a2c59d1d --- /dev/null +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx @@ -0,0 +1,27 @@ +"use client"; + +import NotFound from "@/app/not-found"; +import * as yup from "yup"; +import { PageLayout } from "../../page-layout"; +import { useAdminApp } from "../../use-admin-app"; + + +export default function PageClient(props: { userId: string }) { + const stackAdminApp = useAdminApp(); + + if (!yup.string().uuid().isValidSync(props.userId)) { + return ; + } + + const user = stackAdminApp.useUser(props.userId); + + if (!user) { + return ; + } + + return ( + + test + + ); +} diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page.tsx new file mode 100644 index 000000000..3feb144e8 --- /dev/null +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page.tsx @@ -0,0 +1,11 @@ +import PageClient from "./page-client"; + +export const metadata = { + title: "User Profile", +}; + +export default async function Page(props: { params: { userId: string } }) { + return ( + + ); +} diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/page-client.tsx index 00fac14b9..9d1cb2ce8 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/page-client.tsx @@ -2,13 +2,107 @@ import { UserTable } from "@/components/data-table/user-table"; import { FormDialog } from "@/components/form-dialog"; -import { InputField, SwitchField } from "@/components/form-fields"; +import { InputField, SwitchField, TextAreaField } from "@/components/form-fields"; import { StyledLink } from "@/components/link"; -import { Alert, Button } from "@stackframe/stack-ui"; +import { KnownErrors } from "@stackframe/stack-shared"; +import { jsonStringOrEmptySchema } from "@stackframe/stack-shared/dist/schema-fields"; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, Button, Typography, useToast } from "@stackframe/stack-ui"; import * as yup from "yup"; import { PageLayout } from "../page-layout"; import { useAdminApp } from "../use-admin-app"; -import { UserDialog } from "@/components/user-dialog"; + +export function UserDialog(props: { + open?: boolean, + onOpenChange?: (open: boolean) => void, + trigger?: React.ReactNode, +}) { + const { toast } = useToast(); + const adminApp = useAdminApp(); + const project = adminApp.useProject(); + + const formSchema = yup.object({ + primaryEmail: yup.string().email("Primary Email must be a valid email address").required("Primary email is required"), + displayName: yup.string().optional(), + signedUpAt: yup.date().required(), + clientMetadata: jsonStringOrEmptySchema.default("null"), + clientReadOnlyMetadata: jsonStringOrEmptySchema.default("null"), + serverMetadata: jsonStringOrEmptySchema.default("null"), + primaryEmailVerified: yup.boolean().optional(), + password: yup.string().optional(), + otpAuthEnabled: yup.boolean().test({ + name: 'otp-verified', + message: "Primary email must be verified if OTP/magic link sign-in is enabled", + test: (value, context) => { + return context.parent.primaryEmailVerified || !value; + }, + }).optional(), + passwordEnabled: yup.boolean().optional(), + }); + + async function handleSubmit(values: yup.InferType) { + const userValues = { + ...values, + primaryEmailAuthEnabled: true, + clientMetadata: values.clientMetadata ? JSON.parse(values.clientMetadata) : undefined, + clientReadOnlyMetadata: values.clientReadOnlyMetadata ? JSON.parse(values.clientReadOnlyMetadata) : undefined, + serverMetadata: values.serverMetadata ? JSON.parse(values.serverMetadata) : undefined + }; + + try { + await adminApp.createUser(userValues); + } catch (error) { + if (error instanceof KnownErrors.UserEmailAlreadyExists) { + toast({ + title: "Email already exists", + description: "Please choose a different email address", + variant: "destructive", + }); + return 'prevent-close'; + } + } + } + + return ( + <> +
+
+ +
+
+ +
+
+ + + + {project.config.magicLinkEnabled && } + {project.config.credentialEnabled && } + {form.watch("passwordEnabled") ? : null} + {!form.watch("primaryEmailVerified") && form.watch("otpAuthEnabled") && Primary email must be verified if OTP/magic link sign-in is enabled} + + + + Metadata + + + + + + + + + )} + onSubmit={handleSubmit} + cancelButton + />; +} export default function PageClient() { @@ -19,7 +113,6 @@ export default function PageClient() { Create User} />} > diff --git a/apps/dashboard/src/app/not-found.tsx b/apps/dashboard/src/app/not-found.tsx index 42378a6aa..396c345de 100644 --- a/apps/dashboard/src/app/not-found.tsx +++ b/apps/dashboard/src/app/not-found.tsx @@ -8,9 +8,6 @@ export default function NotFound() { title="Oh no! 404" description="Page not found." redirectUrl="/" - secondaryDescription={<> - Did you mean to log in? - } redirectText="Go to home" />; } diff --git a/apps/dashboard/src/components/data-table/user-table.tsx b/apps/dashboard/src/components/data-table/user-table.tsx index e6005fbe4..7a5f0e27c 100644 --- a/apps/dashboard/src/components/data-table/user-table.tsx +++ b/apps/dashboard/src/components/data-table/user-table.tsx @@ -4,8 +4,8 @@ import { ServerUser } from '@stackframe/stack'; import { deindent } from '@stackframe/stack-shared/dist/utils/strings'; import { ActionCell, ActionDialog, AvatarCell, BadgeCell, CopyField, DataTableColumnHeader, DataTableManualPagination, DateCell, SearchToolbarItem, SimpleTooltip, TextCell, Typography } from "@stackframe/stack-ui"; import { ColumnDef, ColumnFiltersState, Row, SortingState, Table } from "@tanstack/react-table"; +import { useRouter } from 'next/navigation'; import { useState } from "react"; -import { UserDialog } from '../user-dialog'; export type ExtendedServerUser = ServerUser & { authTypes: string[], @@ -61,14 +61,13 @@ function ImpersonateUserDialog(props: { } function UserActions({ row }: { row: Row }) { - const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [impersonateSnippet, setImpersonateSnippet] = useState(null); const app = useAdminApp(); + const router = useRouter(); return ( <> - setImpersonateSnippet(null)} /> }) { '-', { item: "Edit", - onClick: () => setIsEditModalOpen(true), + onClick: () => { router.push(`/projects/${app.projectId}/users/${row.original.id}`); }, }, ...row.original.isMultiFactorRequired ? [{ item: "Remove 2FA", diff --git a/apps/dashboard/src/components/user-dialog.tsx b/apps/dashboard/src/components/user-dialog.tsx deleted file mode 100644 index 24a2ba11b..000000000 --- a/apps/dashboard/src/components/user-dialog.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { useAdminApp } from "@/app/(main)/(protected)/projects/[projectId]/use-admin-app"; -import { ServerUser } from "@stackframe/stack"; -import { jsonStringOrEmptySchema } from "@stackframe/stack-shared/dist/schema-fields"; -import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Typography, useToast } from "@stackframe/stack-ui"; -import * as yup from "yup"; -import { FormDialog } from "./form-dialog"; -import { DateField, InputField, SwitchField, TextAreaField } from "./form-fields"; -import { KnownErrors } from "@stackframe/stack-shared"; - -export function UserDialog(props: { - open?: boolean, - onOpenChange?: (open: boolean) => void, - trigger?: React.ReactNode, -} & ({ - type: 'create', -} | { - type: 'edit', - user: ServerUser, -})) { - const { toast } = useToast(); - const adminApp = useAdminApp(); - const project = adminApp.useProject(); - - let defaultValues; - if (props.type === 'edit') { - defaultValues = { - displayName: props.user.displayName || undefined, - primaryEmail: props.user.primaryEmail || undefined, - primaryEmailVerified: props.user.primaryEmailVerified, - clientMetadata: props.user.clientMetadata == null ? "" : JSON.stringify(props.user.clientMetadata, null, 2), - clientReadOnlyMetadata: props.user.clientReadOnlyMetadata == null ? "" : JSON.stringify(props.user.clientReadOnlyMetadata, null, 2), - serverMetadata: props.user.serverMetadata == null ? "" : JSON.stringify(props.user.serverMetadata, null, 2), - passwordEnabled: props.user.hasPassword, - otpAuthEnabled: props.user.otpAuthEnabled, - }; - } else { - defaultValues = {}; - } - - const formSchema = yup.object({ - primaryEmail: yup.string().email("Primary Email must be a valid email address").required("Primary email is required"), - displayName: yup.string().optional(), - signedUpAt: yup.date().required(), - clientMetadata: jsonStringOrEmptySchema.default("null"), - clientReadOnlyMetadata: jsonStringOrEmptySchema.default("null"), - serverMetadata: jsonStringOrEmptySchema.default("null"), - primaryEmailVerified: yup.boolean().optional(), - password: yup.string().optional(), - otpAuthEnabled: yup.boolean().test({ - name: 'otp-verified', - message: "Primary email must be verified if OTP/magic link sign-in is enabled", - test: (value, context) => { - return context.parent.primaryEmailVerified || !value; - }, - }).optional(), - passwordEnabled: yup.boolean().optional(), - }); - - async function handleSubmit(values: yup.InferType) { - const userValues = { - ...values, - primaryEmailAuthEnabled: true, - clientMetadata: values.clientMetadata ? JSON.parse(values.clientMetadata) : undefined, - clientReadOnlyMetadata: values.clientReadOnlyMetadata ? JSON.parse(values.clientReadOnlyMetadata) : undefined, - serverMetadata: values.serverMetadata ? JSON.parse(values.serverMetadata) : undefined - }; - - try { - if (props.type === 'edit') { - await props.user.update(userValues); - } else { - await adminApp.createUser(userValues); - } - } catch (error) { - if (error instanceof KnownErrors.UserEmailAlreadyExists) { - toast({ - title: "Email already exists", - description: "Please choose a different email address", - variant: "destructive", - }); - return 'prevent-close'; - } - } - } - - return ( - <> - {props.type === 'edit' ? ID: {props.user.id} : null} - -
-
- -
-
- -
-
- - - - {project.config.magicLinkEnabled && } - {project.config.credentialEnabled && } - {form.watch("passwordEnabled") ? : null} - {!form.watch("primaryEmailVerified") && form.watch("otpAuthEnabled") && Primary email must be verified if OTP/magic link sign-in is enabled} - - - - Metadata - - - - - - - - - )} - onSubmit={handleSubmit} - cancelButton - />; -}