Skip to content

Commit

Permalink
[ALS-6325] Add New/Edit/View actions to users page (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
srpiatt authored Apr 29, 2024
1 parent 7d83921 commit 0552582
Show file tree
Hide file tree
Showing 29 changed files with 639 additions and 74 deletions.
4 changes: 1 addition & 3 deletions src/lib/components/Navigation.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
<script lang="ts">
import { AppBar, getToastStore, popup, type PopupSettings } from '@skeletonlabs/skeleton';
import { AppBar, popup, type PopupSettings } from '@skeletonlabs/skeleton';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import logo from '$lib/assets/app-logo.png';
import { user, logout } from '$lib/stores/User';
import { PicsurePrivileges } from '$lib/models/Privilege';
import { routes } from '$lib/configuration';
const toastStore = getToastStore();
let allowedRoutes = routes.filter((route) => !route.privilege);
function getUsersRoutes() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { textInput } from '$lib/utilities/Validation';
import type { Privilege } from '$lib/models/Privileges';
import type { Privilege } from '$lib/models/Privilege';
import PrivilegesStore from '$lib/stores/Privileges';
const { addPrivilege, updatePrivilege } = PrivilegesStore;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
export let data = { cell: '', row: { name: '' } };
function deleteModal() {
const name = data.row.name;
modalStore.trigger({
type: 'confirm',
title: 'Delete Privilege?',
body: `Are you sure you want to delete privilege '${data.row.name}'?`,
body: `Are you sure you want to delete privilege '${name}'?`,
buttonTextConfirm: 'Yes',
buttonTextCancel: 'No',
response: async (confirm: boolean) => {
Expand All @@ -21,13 +22,13 @@
try {
await deletePrivilege(data.cell);
toastStore.trigger({
message: `Successfully deleted privilege '${data.row.name}'`,
message: `Successfully deleted privilege '${name}'`,
background: 'variant-filled-success',
});
} catch (error) {
console.error(error);
toastStore.trigger({
message: `An error occured while deleting privilege '${data.row.name}'`,
message: `An error occured while deleting privilege '${name}'`,
background: 'variant-filled-error',
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
export let data = { cell: '', row: { name: '' } };
function deleteModal() {
const name = data.row.name;
modalStore.trigger({
type: 'confirm',
title: 'Delete Role?',
body: `Are you sure you want to delete role '${data.row.name}'?`,
body: `Are you sure you want to delete role '${name}'?`,
buttonTextConfirm: 'Yes',
buttonTextCancel: 'No',
response: async (confirm: boolean) => {
Expand All @@ -21,13 +22,13 @@
try {
await deleteRole(data.cell);
toastStore.trigger({
message: `Successfully deleted role '${data.row.name}'`,
message: `Successfully deleted role '${name}'`,
background: 'variant-filled-success',
});
} catch (error) {
console.error(error);
toastStore.trigger({
message: `An error occured while deleting role '${data.row.name}'`,
message: `An error occured while deleting role '${name}'`,
background: 'variant-filled-error',
});
}
Expand Down
120 changes: 120 additions & 0 deletions src/lib/components/admin/user/UserForm.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { getToastStore } from '@skeletonlabs/skeleton';
const toastStore = getToastStore();
import { type ExtendedUser } from '$lib/models/User';
import { type Connection } from '$lib/models/Connection';
import UsersStore from '$lib/stores/Users';
import ConnectionStore from '$lib/stores/Connections';
import RoleStore from '$lib/stores/Roles';
import PrivilegesStore from '$lib/stores/Privileges';
const { addUser, updateUser } = UsersStore;
const { getConnection } = ConnectionStore;
const { getRole } = RoleStore;
const { getPrivilege } = PrivilegesStore;
export let user: ExtendedUser | undefined = undefined;
export let roleList: string[][];
export let connections: Connection[];
let email = user ? user.email : '';
let connection = user ? user.connection : '';
let active = user ? user.active : true;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let roles = roleList.map(([_name, uuid]) => ({
uuid,
checked: user ? user.roles.includes(uuid) : false,
}));
async function saveUser() {
const generalMetadata = JSON.parse(user?.generalMetadata || '{"email":""}');
generalMetadata.email = email;
let newUser = {
email,
connection: await getConnection(connection),
generalMetadata: JSON.stringify(generalMetadata),
active,
roles: await Promise.all(
roles
.filter((role) => role.checked)
.map((role) =>
getRole(role.uuid).then((role) => ({
...role,
privileges: role.privileges.map((uuid: string) => getPrivilege(uuid)),
})),
),
),
};
try {
if (user) {
await updateUser({ ...newUser, uuid: user.uuid });
} else {
await addUser(newUser);
}
toastStore.trigger({
message: `Successfully saved ${newUser ? 'new user' : 'user'} '${email}'`,
background: 'variant-filled-success',
});
goto('/admin/users');
} catch (error) {
console.error(error);
toastStore.trigger({
message: `An error occured while saving ${newUser ? 'new user' : 'user'} '${email}'`,
background: 'variant-filled-error',
});
}
}
</script>

<form on:submit|preventDefault={saveUser}>
<label class="flex items-center space-x-2">
<input class="checkbox" type="checkbox" bind:checked={active} />
<p>Active</p>
</label>

<label class="label required">
<span>Email:</span>
<input type="email" bind:value={email} class="input" required minlength="1" maxlength="255" />
</label>

<label class="label required">
<span>Connection:</span>
<select class="select" bind:value={connection} required>
<option selected={!user || !user.connection} disabled value>none</option>
{#each connections as connection}
<option value={connection.uuid} selected={user && user.connection === connection.uuid}
>{connection.label}</option
>
{/each}
</select>
</label>

<fieldset data-testid="privilege-checkboxes">
<legend>Roles:</legend>
{#each roleList as [name], index}
<label class="flex items-center space-x-2">
<input class="checkbox" type="checkbox" bind:checked={roles[index].checked} />
<p>{name}</p>
</label>
{/each}
</fieldset>

<button type="submit" class="btn variant-ghost-primary hover:variant-filled-primary">
Save
</button>
<a href="/admin/users" class="btn variant-ghost-secondary hover:variant-filled-secondary">
Cancel
</a>
</form>

<style>
label,
fieldset {
margin: 0.5em 0;
}
fieldset label {
margin: 0;
}
</style>
100 changes: 100 additions & 0 deletions src/lib/components/admin/user/cell/Actions.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<script lang="ts">
import { getModalStore, getToastStore } from '@skeletonlabs/skeleton';
const modalStore = getModalStore();
const toastStore = getToastStore();
import UsersStore from '$lib/stores/Users';
import ConnectionStore from '$lib/stores/Connections';
import RoleStore from '$lib/stores/Roles';
import PrivilegesStore from '$lib/stores/Privileges';
const { getUser, updateUser } = UsersStore;
const { getConnection } = ConnectionStore;
const { getRole } = RoleStore;
const { getPrivilege } = PrivilegesStore;
export let data = { cell: '', row: { status: '', email: '' } };
async function userActivation(activate: boolean) {
const user = await getUser(data.cell);
modalStore.trigger({
type: 'confirm',
title: `${activate ? 'R' : 'D'}eactivate User?`,
body: `Are you sure you want to ${activate ? 'r' : 'd'}eactiveate user '${user.email}'?`,
buttonTextConfirm: activate ? 'Reactivate' : 'Deactivate',
response: async (confirm: boolean) => {
if (!confirm) return;
let newUser = {
...user,
active: activate,
connection: await getConnection(user.connection),
roles: await Promise.all(
user.roles.map((uuid: string) =>
getRole(uuid).then((role) => ({
...role,
privileges: role.privileges.map((uuid: string) => getPrivilege(uuid)),
})),
),
),
};
try {
await updateUser(newUser);
toastStore.trigger({
message: `Successfully ${activate ? 'activated' : 'deactivated'} user '${user.email}'`,
background: 'variant-filled-success',
});
} catch (error) {
console.error(error);
toastStore.trigger({
message: `An error occured while ${activate ? 'activating' : 'deactivating'} user '${
user.email
}'`,
background: 'variant-filled-error',
});
}
},
});
}
</script>

<a
data-testid={`user-view-btn-${data.cell}`}
href={`/admin/users/${data.cell}`}
class="text-secondary-600 hover:text-primary-600"
>
<i class="fa-solid fa-circle-info fa-xl"></i>
<span class="sr-only">View User</span>
</a>

{#if data.row.status === 'Active'}
<a
data-testid={`user-edit-btn-${data.cell}`}
href={`/admin/users/${data.cell}/edit`}
class="text-secondary-600 hover:text-primary-600"
>
<i class="fa-solid fa-pen-to-square fa-xl"></i>
<span class="sr-only">Edit</span>
</a>
<button
data-testid={`user-deactivate-btn-${data.cell}`}
type="button"
title="Deactivate user"
class="bg-initial text-secondary-600 hover:text-primary-600"
on:click={() => userActivation(false)}
>
<i class="fa-solid fa-circle-xmark fa-xl"></i>
<span class="sr-only">Deactivate user</span>
</button>
{:else}
<button
data-testid={`user-activate-btn-${data.cell}`}
type="button"
title="Reactivate user"
class="bg-initial text-secondary-600 hover:text-primary-600"
on:click={() => userActivation(true)}
>
<i class="fa-solid fa-circle-check fa-xl"></i>
<span class="sr-only">Reactivate user</span>
</button>
{/if}
File renamed without changes.
36 changes: 0 additions & 36 deletions src/lib/components/user/cell/Actions.svelte

This file was deleted.

3 changes: 2 additions & 1 deletion src/lib/models/User.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export interface User {
uuid: string;
uuid?: string;
email?: string;
userId?: string;
privileges?: string[];
Expand All @@ -10,6 +10,7 @@ export interface User {
export interface ExtendedUser extends User {
subject?: string;
connection: string;
generalMetadata: string;
active: boolean;
roles: string[];
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/stores/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const APP_URL = 'psama/application';

const loaded = writable(false);
const applications: Writable<Application[]> = writable([]);
const applicationList = derived(applications, ($a) => $a.map((a) => [a.name, a.uuid]));
const applicationList = derived(applications, ($a) => $a.map((a) => [a.name, a.uuid]), []);

async function loadApplications() {
if (get(loaded)) return;
Expand Down
8 changes: 7 additions & 1 deletion src/lib/stores/Connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ export async function loadConnections() {

async function getConnection(uuid: string) {
const store = get(connections);
return store.find((c) => c.uuid === uuid);
const connection = store.find((r) => r.uuid === uuid);
if (connection) {
return connection;
}

const res = await api.get(`${CONN_URL}/${uuid}`);
return res;
}

export default {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/stores/Privileges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const PRIV_PATH = 'psama/privilege';

const loaded = writable(false);
const privileges: Writable<Privilege[]> = writable([]);
const privilegeList = derived(privileges, ($p) => $p.map((p) => [p.name, p.uuid]));
const privilegeList = derived(privileges, ($p) => $p.map((p) => [p.name, p.uuid || '']), []);

async function loadPrivileges() {
if (get(loaded)) return;
Expand Down
Loading

0 comments on commit 0552582

Please sign in to comment.