-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
first pass on populating a users form (#951)
* user populate * refactor a bit * min/max, mo validation * make UsersForm component * no submit * various fixes * cannot change type or owner of self * format --------- Co-authored-by: Jonathan Braswell <10187286+jbraswell@users.noreply.github.com>
- Loading branch information
Showing
3 changed files
with
220 additions
and
117 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
<script lang="ts"> | ||
import { validator } from '@felte/validator-yup'; | ||
import { createForm } from 'felte'; | ||
import { Button, Helper, Input, Label, Select } from 'flowbite-svelte'; | ||
import * as yup from 'yup'; | ||
import { authenticatedUser } from '../stores/apiCredentials'; | ||
import { spinner } from '../stores/spinner'; | ||
import { translations } from '../stores/localization'; | ||
import type { User } from 'bmlt-root-server-client'; | ||
import RootServerApi from '../lib/RootServerApi'; | ||
export let selectedUserId: number; | ||
export let usersById: Record<number, User> = {}; | ||
let userItems = Object.values(usersById) | ||
.map((user) => ({ value: user.id, name: user.displayName })) | ||
.sort((a, b) => a.name.localeCompare(b.name)); | ||
const userTypeItems = [ | ||
{ value: 'deactivated', name: 'Deactivated' }, | ||
{ value: 'observer', name: 'Observer' }, | ||
{ value: 'serviceBodyAdmin', name: 'Service Body Administrator' } | ||
]; | ||
const { form, data, errors, setInitialValues, reset } = createForm({ | ||
initialValues: { | ||
type: '', | ||
ownedBy: -1, | ||
email: '', | ||
displayName: '', | ||
username: '', | ||
password: '', | ||
description: '' | ||
}, | ||
onSubmit: async (values) => { | ||
spinner.show(); | ||
console.log(values); | ||
// Rather than blindly casing values as a UserCreate object, we should | ||
// actually build a UserCreate object. That way we are explicit about | ||
// every field, including the funky ones like ownedBy and type. | ||
}, | ||
onError: async (error) => { | ||
console.log(error); | ||
await RootServerApi.handleErrors(error as Error, { | ||
handleValidationError: (error) => { | ||
console.log(error); | ||
// TODO validate that these fields match what is in the 422 error schema | ||
// from the openapi json spec, and actually try to force these errors to | ||
// test them. | ||
errors.set({ | ||
type: (error?.errors?.type ?? []).join(' '), | ||
ownedBy: (error?.errors?.ownedBy ?? []).join(' '), | ||
email: (error?.errors?.email ?? []).join(' '), | ||
displayName: (error?.errors?.displayName ?? []).join(' '), | ||
username: (error?.errors?.username ?? []).join(' '), | ||
password: (error?.errors?.password ?? []).join(' '), | ||
description: (error?.errors?.description ?? []).join(' ') | ||
}); | ||
} | ||
}); | ||
spinner.hide(); | ||
}, | ||
onSuccess: () => { | ||
spinner.hide(); | ||
}, | ||
extend: validator({ | ||
// TODO compare these required fields against what is required by the API | ||
schema: yup.object({ | ||
type: yup.string().required(), | ||
ownedBy: yup.number(), | ||
email: yup.string().max(255).email(), | ||
displayName: yup.string().max(255).required(), | ||
username: yup.string().max(255).required(), | ||
password: yup.string().test('password-valid', 'password must be between 12 and 255 characters', (password, context) => { | ||
if (!password) { | ||
// empty password means no change, which passes validation | ||
return true; | ||
} | ||
if (!password.trim()) { | ||
return context.createError({ message: 'password must contain non-whitespace characters' }); | ||
} | ||
return password.length >= 12 && password.length <= 255; | ||
}), | ||
description: yup.string().max(255, 'description cannot be longer than 255 characters') | ||
}) | ||
}) | ||
}); | ||
function populateForm() { | ||
const user = usersById[selectedUserId]; | ||
// The only reason we use setInitialValues and reesethere instead of setData is to make development | ||
// easier. It is super annoying that each time we save the file, hot module replacement causes the | ||
// values in the form fields to be replaced when the UsersForm is refreshed. | ||
setInitialValues({ | ||
type: user?.type ?? '', | ||
ownedBy: user?.ownerId ? parseInt(user.ownerId) : -1, | ||
email: user?.email ?? '', | ||
displayName: user?.displayName ?? '', | ||
username: user?.username ?? '', | ||
description: user?.description ?? '', | ||
password: '' | ||
}); | ||
reset(); | ||
} | ||
$: if (selectedUserId) { | ||
populateForm(); | ||
} | ||
</script> | ||
|
||
<form use:form> | ||
<div class="mb-6 grid gap-6 md:grid-cols-2"> | ||
<div> | ||
<Label for="type" class="mb-2">{$translations.userTypeTitle}</Label> | ||
<Select id="type" items={userTypeItems} name="type" disabled={selectedUserId === $authenticatedUser?.id} /> | ||
<Helper class="mt-2" color="red"> | ||
{#if $errors.type} | ||
{$errors.type} | ||
{/if} | ||
</Helper> | ||
</div> | ||
<div> | ||
<Label for="ownedBy" class="mb-2">{$translations.ownedByTitle}</Label> | ||
<Select id="ownedBy" items={userItems} name="ownedBy" disabled={selectedUserId === $authenticatedUser?.id || $data.type === 'admin'} /> | ||
<Helper class="mt-2" color="red"> | ||
{#if $errors.ownedBy} | ||
{$errors.ownedBy} | ||
{/if} | ||
</Helper> | ||
</div> | ||
</div> | ||
<div class="mb-6"> | ||
<Label for="email" class="mb-2">{$translations.emailTitle}</Label> | ||
<Input type="email" id="email" name="email" /> | ||
<Helper class="mt-2" color="red"> | ||
{#if $errors.email} | ||
{$errors.email} | ||
{/if} | ||
</Helper> | ||
</div> | ||
<div class="mb-6"> | ||
<Label for="displayName" class="mb-2">{$translations.nameTitle}</Label> | ||
<Input type="text" id="displayName" name="displayName" required /> | ||
<Helper class="mt-2" color="red"> | ||
{#if $errors.displayName} | ||
{$errors.displayName} | ||
{/if} | ||
</Helper> | ||
</div> | ||
<div class="mb-6"> | ||
<Label for="description" class="mb-2">{$translations.descriptionTitle}</Label> | ||
<Input type="text" id="description" name="description" /> | ||
<Helper class="mt-2" color="red"> | ||
{#if $errors.description} | ||
{$errors.description} | ||
{/if} | ||
</Helper> | ||
</div> | ||
<div class="mb-6"> | ||
<Label for="username" class="mb-2">{$translations.usernameTitle}</Label> | ||
<Input type="text" id="username" name="username" required /> | ||
<Helper class="mt-2" color="red"> | ||
{#if $errors.username} | ||
{$errors.username} | ||
{/if} | ||
</Helper> | ||
</div> | ||
<div class="mb-6"> | ||
<Label for="password" class="mb-2">{$translations.passwordTitle}</Label> | ||
<Input type="password" id="password" name="password" required /> | ||
<Helper class="mt-2" color="red"> | ||
{#if $errors.password} | ||
{$errors.password} | ||
{/if} | ||
</Helper> | ||
</div> | ||
<Button type="submit">{$translations.applyChangesTitle}</Button> | ||
</form> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,130 +1,55 @@ | ||
<script lang="ts"> | ||
import { validator } from '@felte/validator-yup'; | ||
import { createForm } from 'felte'; | ||
import { Button, Helper, Input, Label, Select } from 'flowbite-svelte'; | ||
import * as yup from 'yup'; | ||
import { Label, Select } from 'flowbite-svelte'; | ||
import Nav from '../components/NavBar.svelte'; | ||
import UsersForm from '../components/UsersForm.svelte'; | ||
import { authenticatedUser } from '../stores/apiCredentials'; | ||
import { spinner } from '../stores/spinner'; | ||
import { translations } from '../stores/localization'; | ||
import RootServerApi from '../lib/RootServerApi'; | ||
import { onMount } from 'svelte'; | ||
import type { User } from 'bmlt-root-server-client'; | ||
let usersById: Record<number, User> = {}; | ||
let userItems = [{ value: -1, name: '' }]; | ||
let selectedUserId = -1; | ||
const { form, data, errors } = createForm({ | ||
initialValues: { | ||
user: '', | ||
userIs: '', | ||
ownedBy: '', | ||
email: '', | ||
name: '', | ||
username: '', | ||
password: '', | ||
description: '' | ||
}, | ||
onSubmit: async (values) => { | ||
async function getUsers(): Promise<void> { | ||
try { | ||
spinner.show(); | ||
console.log(values); | ||
const users = await RootServerApi.getUsers(); | ||
const _usersById: Record<number, User> = {}; | ||
for (const user of users) { | ||
_usersById[user.id] = user; | ||
if ($authenticatedUser?.type === 'admin') { | ||
if (user.ownerId === null) { | ||
user.ownerId = $authenticatedUser.id.toString(); | ||
} | ||
} | ||
} | ||
usersById = _usersById; | ||
userItems = users.map((user) => ({ value: user.id, name: user.displayName })).sort((a, b) => a.name.localeCompare(b.name)); | ||
spinner.hide(); | ||
}, | ||
extend: validator({ | ||
schema: yup.object({ | ||
user: yup.string().required('User is required'), | ||
userIs: yup.string().required('User role is required'), | ||
ownedBy: yup.string().required('Owner is required'), | ||
email: yup.string().email('Invalid email').required('Email is required'), | ||
name: yup.string().required('Name is required'), | ||
username: yup.string().required('Username is required'), | ||
password: yup.string().required('Password is required'), | ||
description: yup.string() | ||
}) | ||
}) | ||
}); | ||
} catch (error: any) { | ||
RootServerApi.handleErrors(error); | ||
} | ||
} | ||
let users = [{ value: 'bronx_asc_admin', name: 'Bronx ASC Administrator' }]; | ||
let userIs = [{ value: 'service_body_admin', name: 'Service Body Administrator' }]; | ||
let ownedBy = [{ value: 'greater_ny_admin', name: 'Greater New York Regional Administrator' }]; | ||
onMount(getUsers); | ||
</script> | ||
|
||
<Nav /> | ||
|
||
{#if $authenticatedUser} | ||
<div class="mx-auto max-w-xl p-4"> | ||
<h2 class="mb-4 text-center text-xl font-semibold dark:text-white">{$translations.userTitle} {$translations.idTitle} #{$authenticatedUser.id}</h2> | ||
<form use:form> | ||
<div class="mb-6"> | ||
<Label for="user" class="mb-2">{$translations.userTitle}</Label> | ||
<Select id="user" items={users} name="user" bind:value={$data.user} /> | ||
<Helper class="mt-2" color="red"> | ||
{#if $errors.user} | ||
{$errors.user} | ||
{/if} | ||
</Helper> | ||
</div> | ||
<div class="mb-6 grid gap-6 md:grid-cols-2"> | ||
<div> | ||
<Label for="user-is" class="mb-2">{$translations.userIsATitle}</Label> | ||
<Select id="user-is" items={userIs} name="userIs" bind:value={$data.userIs} /> | ||
<Helper class="mt-2" color="red"> | ||
{#if $errors.userIs} | ||
{$errors.userIs} | ||
{/if} | ||
</Helper> | ||
</div> | ||
<div> | ||
<Label for="owned-by" class="mb-2">{$translations.ownedByTitle}</Label> | ||
<Select id="owned-by" items={ownedBy} name="ownedBy" bind:value={$data.ownedBy} /> | ||
<Helper class="mt-2" color="red"> | ||
{#if $errors.ownedBy} | ||
{$errors.ownedBy} | ||
{/if} | ||
</Helper> | ||
</div> | ||
</div> | ||
<div class="mb-6"> | ||
<Label for="email" class="mb-2">{$translations.emailTitle}</Label> | ||
<Input type="email" id="email" name="email" bind:value={$data.email} required /> | ||
<Helper class="mt-2" color="red"> | ||
{#if $errors.email} | ||
{$errors.email} | ||
{/if} | ||
</Helper> | ||
</div> | ||
<div class="mb-6"> | ||
<Label for="name" class="mb-2">{$translations.nameTitle}</Label> | ||
<Input type="text" id="name" name="name" bind:value={$data.name} required /> | ||
<Helper class="mt-2" color="red"> | ||
{#if $errors.name} | ||
{$errors.name} | ||
{/if} | ||
</Helper> | ||
</div> | ||
<div class="mb-6"> | ||
<Label for="description" class="mb-2">{$translations.descriptionTitle}</Label> | ||
<Input type="text" id="description" name="description" bind:value={$data.description} /> | ||
<Helper class="mt-2" color="red"> | ||
{#if $errors.description} | ||
{$errors.description} | ||
{/if} | ||
</Helper> | ||
</div> | ||
<div class="mb-6"> | ||
<Label for="username" class="mb-2">{$translations.usernameTitle}</Label> | ||
<Input type="text" id="username" name="username" bind:value={$data.username} required /> | ||
<Helper class="mt-2" color="red"> | ||
{#if $errors.username} | ||
{$errors.username} | ||
{/if} | ||
</Helper> | ||
</div> | ||
<div class="mb-6"> | ||
<Label for="password" class="mb-2">{$translations.passwordTitle}</Label> | ||
<Input type="password" id="password" name="password" bind:value={$data.password} required /> | ||
<Helper class="mt-2" color="red"> | ||
{#if $errors.password} | ||
{$errors.password} | ||
{/if} | ||
</Helper> | ||
</div> | ||
<Button type="submit">{$translations.applyChangesTitle}</Button> | ||
</form> | ||
<div class="mx-auto max-w-xl p-4"> | ||
<h2 class="mb-4 text-center text-xl font-semibold dark:text-white">{$translations.usersTitle}</h2> | ||
|
||
<div class="mb-6"> | ||
<Label for="user" class="mb-2">{$translations.userTitle}</Label> | ||
<Select id="user" items={userItems} name="user" bind:value={selectedUserId} /> | ||
</div> | ||
{/if} | ||
{#if selectedUserId !== -1} | ||
<UsersForm {selectedUserId} {usersById} /> | ||
{/if} | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters