Skip to content

Commit 55a517c

Browse files
authored
Merge pull request #277 from code0-tech/feat/#38
Invite user page
2 parents 4c15fc5 + f363e84 commit 55a517c

7 files changed

Lines changed: 572 additions & 6 deletions

File tree

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export default function Page() {
2-
return null
3-
}
1+
import {UserInvitePage} from "@edition/user/pages/UserInvitePage";
2+
3+
export default UserInvitePage
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
"use client"
2+
3+
import React from "react";
4+
import {
5+
Button,
6+
ButtonGroup,
7+
Card, Col,
8+
Dialog,
9+
DialogClose,
10+
DialogContent,
11+
DialogOverlay,
12+
DialogPortal,
13+
EmailInput,
14+
emailValidation,
15+
Flex,
16+
PasswordInput,
17+
passwordValidation, Row,
18+
Spacing,
19+
SwitchInput,
20+
Text,
21+
TextAreaInput,
22+
TextInput,
23+
useForm,
24+
useService,
25+
useStore
26+
} from "@code0-tech/pictor";
27+
import CardSection from "@code0-tech/pictor/dist/components/card/CardSection";
28+
import {Tab, TabContent, TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab";
29+
import {User, UsersUpdateInput} from "@code0-tech/sagittarius-graphql-types";
30+
import {UserService} from "@edition/user/services/User.service";
31+
import {
32+
addIslandErrorNotification,
33+
addIslandSuccessNotification
34+
} from "@code0-tech/pictor/dist/components/island/Island.hook";
35+
import {IconAt, IconLock, IconMail, IconUser, IconX} from "@tabler/icons-react";
36+
import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout";
37+
38+
export interface UserEditDialogComponentProps {
39+
userId?: User['id']
40+
open?: boolean
41+
onOpenChange?: (open: boolean) => void
42+
}
43+
44+
export const UserEditDialogComponent: React.FC<UserEditDialogComponentProps> = (props) => {
45+
46+
const {userId, open, onOpenChange} = props
47+
48+
const userService = useService(UserService)
49+
const userStore = useStore(UserService)
50+
const [, startTransition] = React.useTransition()
51+
52+
const user = React.useMemo(
53+
() => userService.getById(userId),
54+
[userStore, userId]
55+
)
56+
57+
const initialValues = React.useMemo(() => ({
58+
firstname: user?.firstname ?? null,
59+
lastname: user?.lastname ?? null,
60+
email: user?.email ?? null,
61+
username: user?.username ?? null,
62+
readme: user?.readme ?? null,
63+
admin: user?.admin ?? false,
64+
password: null,
65+
repeatPassword: null,
66+
}), [user])
67+
68+
const [inputs, validate] = useForm<{
69+
firstname: string | null,
70+
lastname: string | null,
71+
email: string | null,
72+
username: string | null,
73+
readme: string | null,
74+
admin: boolean | null,
75+
password: string | null,
76+
repeatPassword: string | null,
77+
}>({
78+
useInitialValidation: false,
79+
initialValues: initialValues,
80+
validate: {
81+
email: (value) => {
82+
if (!value) return "Email is required"
83+
if (!emailValidation(value)) return "Please provide a valid email"
84+
return null
85+
},
86+
username: (value) => {
87+
if (!value) return "Username is required"
88+
return null
89+
},
90+
password: (value) => {
91+
if (!value) return null
92+
return passwordValidation(value)
93+
},
94+
repeatPassword: (value, values) => {
95+
if (!values?.password) return null
96+
if (passwordValidation(value) != null) return passwordValidation(value)
97+
if (value != values?.password) return "Passwords do not match"
98+
return null
99+
}
100+
},
101+
onSubmit: (values) => {
102+
if (!userId) return
103+
if (!values.email || !values.username) return
104+
105+
const payload: UsersUpdateInput = {userId: userId}
106+
if (values.email !== (user?.email ?? null)) payload.email = values.email
107+
if (values.username !== (user?.username ?? null)) payload.username = values.username
108+
if (values.firstname !== (user?.firstname ?? null)) payload.firstname = values.firstname
109+
if (values.lastname !== (user?.lastname ?? null)) payload.lastname = values.lastname
110+
if (values.readme !== (user?.readme ?? null)) payload.readme = values.readme
111+
if (values.admin !== (user?.admin ?? false)) payload.admin = values.admin
112+
if (values.password) {
113+
payload.password = values.password
114+
payload.passwordRepeat = values.repeatPassword
115+
}
116+
117+
// nothing but the userId changed, so there is nothing to update
118+
if (Object.keys(payload).length <= 1) {
119+
onOpenChange?.(false)
120+
return
121+
}
122+
123+
startTransition(async () => {
124+
await userService.usersUpdate(payload).then(payload => {
125+
if (payload?.user && (payload?.errors?.length ?? 0) <= 0) {
126+
onOpenChange?.(false)
127+
addIslandSuccessNotification({
128+
message: "Updated user",
129+
})
130+
}
131+
})
132+
})
133+
}
134+
})
135+
136+
return <Dialog open={open} onOpenChange={(open) => onOpenChange?.(open)}>
137+
<DialogPortal>
138+
<DialogOverlay/>
139+
<DialogContent
140+
style={{padding: "2px"}}
141+
w={"75%"} h={"75%"}>
142+
<Tab orientation={"vertical"} defaultValue={"general"} w={"100%"} h={"100%"}>
143+
<Layout layoutGap={0} showLayoutSplitter={false}
144+
leftContent={<div style={{maxWidth: "200px", padding: "0.7rem", marginTop: "0.55rem"}}>
145+
<Text size={"lg"} hierarchy={"primary"}>
146+
Settings of @{user?.username ?? ""}
147+
</Text>
148+
<Spacing spacing={"xxs"}/>
149+
<Text size={"sm"} hierarchy={"tertiary"} style={{textWrap: "wrap"}}>
150+
Edit the general settings, permissions and security for user
151+
@{user?.username ?? ""}
152+
</Text>
153+
<Spacing spacing={"md"}/>
154+
<TabList>
155+
<TabTrigger value={"general"} w={"100%"} asChild>
156+
<Button paddingSize={"xxs"} variant={"none"} justify={"start"}>
157+
<Text size={"md"}>General</Text>
158+
</Button>
159+
</TabTrigger>
160+
<TabTrigger value={"permissions"} w={"100%"} asChild>
161+
<Button paddingSize={"xxs"} variant={"none"} justify={"start"}>
162+
<Text size={"md"}>Permissions</Text>
163+
</Button>
164+
</TabTrigger>
165+
<TabTrigger value={"security"} w={"100%"} asChild>
166+
<Button paddingSize={"xxs"} variant={"none"} justify={"start"}>
167+
<Text size={"md"}>Security</Text>
168+
</Button>
169+
</TabTrigger>
170+
</TabList>
171+
</div>}>
172+
<Card color={"primary"} paddingSize={"md"} h={"100%"} w={"100%"}>
173+
<TabContent value={"general"} style={{overflow: "hidden"}}>
174+
<Flex justify={"space-between"} align={"center"}>
175+
<Text size={"lg"} hierarchy={"primary"} display={"block"}>General</Text>
176+
<ButtonGroup>
177+
<Button paddingSize={"xxs"} color={"success"} variant={"none"}
178+
onClick={validate}>
179+
Save changes
180+
</Button>
181+
<DialogClose asChild>
182+
<Button variant={"none"} color={"tertiary"} paddingSize={"xxs"}>
183+
<IconX size={13}/>
184+
</Button>
185+
</DialogClose>
186+
</ButtonGroup>
187+
188+
</Flex>
189+
<Spacing spacing={"md"}/>
190+
<Row>
191+
<Col xs={6}>
192+
<TextInput placeholder={"Firstname"}
193+
title={"Firstname"}
194+
description={"The user's given name."}
195+
left={<IconUser size={16}/>} leftType={"icon"}
196+
{...inputs.getInputProps("firstname")}/>
197+
</Col>
198+
<Col xs={6}>
199+
<TextInput placeholder={"Lastname"}
200+
title={"Lastname"}
201+
description={"The user's family name."}
202+
left={<IconUser size={16}/>} leftType={"icon"}
203+
{...inputs.getInputProps("lastname")}/>
204+
</Col>
205+
</Row>
206+
207+
<Spacing spacing={"md"}/>
208+
209+
<EmailInput w={"100%"}
210+
placeholder={"Email"}
211+
title={"Email"}
212+
description={"The email address used to sign in and receive notifications."}
213+
left={<IconMail size={16}/>} leftType={"icon"}
214+
{...inputs.getInputProps("email")}/>
215+
<Spacing spacing={"md"}/>
216+
<TextInput w={"100%"}
217+
placeholder={"Username"}
218+
title={"Username"}
219+
description={"The unique handle used to identify the user."}
220+
left={<IconAt size={16}/>} leftType={"icon"}
221+
{...inputs.getInputProps("username")}/>
222+
<Spacing spacing={"md"}/>
223+
<TextAreaInput w={"100%"} placeholder={"Readme"}
224+
title={"Readme"}
225+
description={"A short bio or notes shown on the user's profile."}
226+
{...inputs.getInputProps("readme")}/>
227+
</TabContent>
228+
<TabContent value={"permissions"} style={{overflow: "hidden"}}>
229+
<Flex justify={"space-between"} align={"center"}>
230+
<Text size={"lg"} hierarchy={"primary"} display={"block"}>Permissions</Text>
231+
<ButtonGroup>
232+
<Button paddingSize={"xxs"} color={"success"} variant={"none"}
233+
onClick={validate}>
234+
Save changes
235+
</Button>
236+
<DialogClose asChild>
237+
<Button variant={"none"} color={"tertiary"} paddingSize={"xxs"}>
238+
<IconX size={13}/>
239+
</Button>
240+
</DialogClose>
241+
</ButtonGroup>
242+
243+
</Flex>
244+
<Spacing spacing={"md"}/>
245+
<Card color={"secondary"}>
246+
<CardSection border>
247+
<Flex justify={"space-between"} align={"center"}>
248+
<Flex style={{gap: ".35rem", flexDirection: "column"}}>
249+
<Text size={"md"} hierarchy={"primary"}>Admin</Text>
250+
<Text size={"md"} hierarchy={"tertiary"}>
251+
Grant this user administrator privileges.
252+
</Text>
253+
</Flex>
254+
<SwitchInput w={"40px"} {...inputs.getInputProps("admin")}/>
255+
</Flex>
256+
</CardSection>
257+
</Card>
258+
</TabContent>
259+
<TabContent value={"security"} style={{overflow: "hidden"}}>
260+
<Flex justify={"space-between"} align={"center"}>
261+
<Text size={"lg"} hierarchy={"primary"} display={"block"}>Security</Text>
262+
<ButtonGroup>
263+
<Button paddingSize={"xxs"} color={"success"} variant={"none"}
264+
onClick={validate}>
265+
Save changes
266+
</Button>
267+
<DialogClose asChild>
268+
<Button variant={"none"} color={"tertiary"} paddingSize={"xxs"}>
269+
<IconX size={13}/>
270+
</Button>
271+
</DialogClose>
272+
</ButtonGroup>
273+
274+
</Flex>
275+
<Spacing spacing={"md"}/>
276+
<PasswordInput w={"100%"} placeholder={"New password"}
277+
title={"New password"}
278+
description={"Set a new password for the user. Leave blank to keep the current one."}
279+
left={<IconLock size={16}/>} leftType={"icon"}
280+
onChange={() => validate("password")}
281+
{...inputs.getInputProps("password")}/>
282+
<Spacing spacing={"md"}/>
283+
<PasswordInput w={"100%"} placeholder={"Repeat new password"}
284+
title={"Repeat new password"}
285+
description={"Re-enter the new password to check for typos."}
286+
left={<IconLock size={16}/>} leftType={"icon"}
287+
onChange={() => validate("repeatPassword")}
288+
{...inputs.getInputProps("repeatPassword")}/>
289+
</TabContent>
290+
</Card>
291+
</Layout>
292+
</Tab>
293+
</DialogContent>
294+
</DialogPortal>
295+
</Dialog>
296+
}

0 commit comments

Comments
 (0)