Skip to content

Commit

Permalink
first pass on populating a users form (#951)
Browse files Browse the repository at this point in the history
* 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
pjaudiomv and jbraswell authored Jul 5, 2024
1 parent ddc049f commit a3218da
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 117 deletions.
178 changes: 178 additions & 0 deletions src/resources/js/components/UsersForm.svelte
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>
157 changes: 41 additions & 116 deletions src/resources/js/routes/Users.svelte
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>
2 changes: 1 addition & 1 deletion src/resources/js/stores/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const strings = new LocalizedStrings({
serviceBodiesTitle: 'Service Bodies',
serviceBodyAdminTitle: 'Service Body Administrator',
signOutTitle: 'Sign Out',
userIsATitle: 'User Is A:',
userTypeTitle: 'User Type:',
userTitle: 'User',
usernameTitle: 'Username',
usersTitle: 'Users'
Expand Down

0 comments on commit a3218da

Please sign in to comment.