Merge branch 'feature/12-user-management' into dev

This commit is contained in:
Philipp Dormann 2021-01-11 21:17:12 +01:00
commit e0093480d9
5 changed files with 248 additions and 200 deletions

View File

@ -2,7 +2,7 @@
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { clickOutside } from "./outsideclick"; import { clickOutside } from "./outsideclick";
import { focusTrap } from "svelte-focus-trap"; import { focusTrap } from "svelte-focus-trap";
import { tracks as tracksstore } from "../store.js"; import { tracks as usersstore } from "../store.js";
import { UserService } from "@odit/lfk-client-js"; import { UserService } from "@odit/lfk-client-js";
import isEmail from "validator/es/lib/isEmail"; import isEmail from "validator/es/lib/isEmail";
import Toastify from "toastify-js"; import Toastify from "toastify-js";
@ -68,11 +68,11 @@
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)", backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast(); }).showToast();
let storeval = []; let storeval = [];
tracksstore.subscribe((val) => { usersstore.subscribe((val) => {
storeval = val; storeval = val;
}); });
storeval.push(result); storeval.push(result);
tracksstore.set(storeval); usersstore.set(storeval);
}) })
.catch((err) => { .catch((err) => {
// //

View File

@ -1,9 +1,12 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
const releaseinfo = document $: releaseinfo = "";
.getElementById("buildinfo") setTimeout(() => {
.textContent.replace("RELEASE_INFO-", "") releaseinfo = document
.replace("-RELEASE_INFO", ""); .getElementById("buildinfo")
.textContent.replace("RELEASE_INFO-", "")
.replace("-RELEASE_INFO", "");
}, 1500);
const year = new Date().getFullYear(); const year = new Date().getFullYear();
</script> </script>

View File

@ -1,28 +1,50 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import lodashIsEqual from "lodash.isequal"; import lodashIsEqual from "lodash.isequal";
import store from "../store";
import { UserService } from "@odit/lfk-client-js"; import { UserService } from "@odit/lfk-client-js";
import "gridjs/dist/theme/mermaid.css"; import "gridjs/dist/theme/mermaid.css";
import PromiseError from "./PromiseError.svelte"; import PromiseError from "./PromiseError.svelte";
export let params; export let params;
const user_promise = UserService.userControllerGetOne(params.userid); const user_promise = UserService.userControllerGetOne(params.userid);
let data_loaded = false; let data_loaded = false;
let original_data = undefined; $: delete_triggered = false;
$: original_data = {};
$: editable_userdata = undefined; $: editable_userdata = undefined;
user_promise.then((data) => { user_promise.then((data) => {
data_loaded = true; data_loaded = true;
original_data = data; original_data = Object.assign(original_data, data);
editable_userdata = data; editable_userdata = data;
}); });
// $: changes_performed = lodashIsEqual(original_data, editable_userdata); $: changes_performed = !lodashIsEqual(original_data, editable_userdata);
$: changes_performed = !lodashIsEqual({ test: 1 }, { test: 1 });
function submit() { function submit() {
if (data_loaded === true && changes_performed === true) { if (data_loaded === true && changes_performed === true) {
console.log("ok, submitting..."); console.log("ok, submitting...");
console.log(editable_userdata);
UserService.userControllerPut(original_data.id, editable_userdata)
.then((resp) => {
console.log(resp);
Object.assign(original_data, editable_userdata);
original_data = editable_userdata;
Object.assign(original_data, editable_userdata);
})
.catch((err) => {
console.log(err);
});
} else { } else {
console.log("no changes performed"); console.log("no changes performed");
} }
} }
function deleteUser() {
UserService.userControllerRemove(original_data.id, true)
.then((resp) => {
console.log(resp);
location.replace("./");
})
.catch((err) => {
console.log(err);
});
}
</script> </script>
{#await user_promise} {#await user_promise}
@ -63,36 +85,47 @@
<polyline points="12 5 19 12 12 19" /></svg> <polyline points="12 5 19 12 12 19" /></svg>
</li> </li>
<li class="flex items-center"> <li class="flex items-center">
<span class="mr-2">{user.firstname} <span class="mr-2">{original_data.firstname}
{user.middlename || ''} {original_data.middlename || ''}
{user.lastname}</span> {original_data.lastname}</span>
</li> </li>
</ol> </ol>
</nav> </nav>
</div> </div>
</div> </div>
<div class="mb-8 text-3xl font-extrabold leading-tight"> <div class="mb-8 text-3xl font-extrabold leading-tight">
{user.firstname} {original_data.firstname}
{user.middlename || ''} {original_data.middlename || ''}
{user.lastname} {original_data.lastname}
<span data-id="user_actions_${user.id}"> <span data-id="user_actions_${editable_userdata.id}">
<button {#if store.state.jwtinfo.userdetails.permissions.includes('USER:DELETE')}
class="hidden w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-green-400 text-base font-medium text-white sm:w-auto sm:text-sm" {#if delete_triggered}
data-userid="${user.id}" <button
onclick="user__delete_cancel()">{$_('cancel')}</button> on:click={deleteUser}
<button class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('confirm-delete')}</button>
class="hidden w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-500 text-base font-medium text-white sm:w-auto sm:text-sm" <button
data-userid="${user.id}" on:click={() => {
onclick="user__delete_confirm()">{$_('confirm-delete')}</button> delete_triggered = !delete_triggered;
<button }}
type="button" class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-400 text-base font-medium text-white sm:w-auto sm:text-sm">{$_('cancel')}</button>
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('delete-user')}</button> {/if}
<button {#if !delete_triggered}
disabled={!changes_performed} <button
class:opacity-50={!changes_performed} on:click={() => {
type="button" delete_triggered = true;
on:click={submit} }}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('save-changes')}</button> type="button"
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('delete-user')}</button>
{/if}
{/if}
{#if !delete_triggered}
<button
disabled={!changes_performed}
class:opacity-50={!changes_performed}
type="button"
on:click={submit}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('save-changes')}</button>
{/if}
</span> </span>
</div> </div>
@ -101,7 +134,7 @@
<img <img
alt={$_('profile-picture')} alt={$_('profile-picture')}
class="inline-block h-20 w-20 rounded-full overflow-hidden bg-gray-100" class="inline-block h-20 w-20 rounded-full overflow-hidden bg-gray-100"
src={user.profilePic} /> src={editable_userdata.profilePic} />
<!-- <span <!-- <span
class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100"><svg class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100"><svg
class="h-full w-full text-gray-300" class="h-full w-full text-gray-300"

View File

@ -1,23 +1,30 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import store from "../store";
import AddUserModal from "./AddUserModal.svelte"; import AddUserModal from "./AddUserModal.svelte";
export let modal_open = false; export let modal_open = false;
import UsersOverview from "./UsersOverview.svelte"; import UsersOverview from "./UsersOverview.svelte";
console.log(store.state.jwtinfo.userdetails.permissions);
</script> </script>
<section class="container p-5"> <section class="container p-5">
<span class="mb-1 text-3xl font-extrabold leading-tight"> <span class="mb-1 text-3xl font-extrabold leading-tight">
{$_('users')} {$_('users')}
<button {#if store.state.jwtinfo.userdetails.permissions.includes('USER:CREATE')}
on:click={() => { <button
modal_open = true; on:click={() => {
}} modal_open = true;
type="button" }}
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm"> type="button"
{$_('create-user')} class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
</button> {$_('create-user')}
</button>
{/if}
</span> </span>
<p class="mb-8 text-lg text-gray-500">{$_('manage-admin-users')}</p> <p class="mb-8 text-lg text-gray-500">{$_('manage-admin-users')}</p>
<UsersOverview /> <UsersOverview />
</section> </section>
<AddUserModal bind:modal_open />
{#if store.state.jwtinfo.userdetails.permissions.includes('USER:CREATE')}
<AddUserModal bind:modal_open />
{/if}

View File

@ -5,6 +5,7 @@
const users_promise = UserService.userControllerGetAll(); const users_promise = UserService.userControllerGetAll();
import "gridjs/dist/theme/mermaid.css"; import "gridjs/dist/theme/mermaid.css";
import { users as usersstore } from "../store.js"; import { users as usersstore } from "../store.js";
import store from "../store";
import UsersEmptyState from "./UsersEmptyState.svelte"; import UsersEmptyState from "./UsersEmptyState.svelte";
$: searchvalue = ""; $: searchvalue = "";
$: active_deletes = []; $: active_deletes = [];
@ -19,163 +20,167 @@
}); });
</script> </script>
{#await users_promise} {#if store.state.jwtinfo.userdetails.permissions.includes('USER:GET')}
<div {#await users_promise}
class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2"
role="alert">
<p class="font-bold">users are being loaded...</p>
<p class="text-sm">{$_('this-might-take-a-moment')}</p>
</div>
{:then users}
{#if userscache.length === 0}
<UsersEmptyState />
{:else}
{#if advanced_search}
advanced search
{:else}
<input
type="search"
bind:value={searchvalue}
placeholder={$_('datatable.search')}
aria-label={$_('datatable.search')}
class="gridjs-input gridjs-search-input mb-4" />
{/if}
<button
on:click={() => {
advanced_search = !advanced_search;
}}
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-gray-600 text-base font-medium text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 sm:ml-3 sm:w-auto sm:text-sm">
{#if advanced_search}
toggle simple search
{:else}toggle advanced search{/if}
</button>
<div <div
class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll"> class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2"
<table class="divide-y divide-gray-200 w-full"> role="alert">
<thead class="bg-gray-50"> <p class="font-bold">users are being loaded...</p>
<tr> <p class="text-sm">{$_('this-might-take-a-moment')}</p>
<th </div>
scope="col" {:then users}
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> {#if userscache.length === 0}
Name <UsersEmptyState />
</th> {:else}
<th {#if advanced_search}
scope="col" advanced search
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> {:else}
Status <input
</th> type="search"
<th bind:value={searchvalue}
scope="col" placeholder={$_('datatable.search')}
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> aria-label={$_('datatable.search')}
Groups class="gridjs-input gridjs-search-input mb-4" />
</th> {/if}
<th scope="col" class="relative px-6 py-3"> <button
<span class="sr-only">Action</span> on:click={() => {
</th> advanced_search = !advanced_search;
</tr> }}
</thead> type="button"
<tbody class="divide-y divide-gray-200"> class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-gray-600 text-base font-medium text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 sm:ml-3 sm:w-auto sm:text-sm">
{#each users as u} {#if advanced_search}
{#if Object.values(u) toggle simple search
.toString() {:else}toggle advanced search{/if}
.toLowerCase() </button>
.includes(searchvalue)} <div
<tr> class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll">
<td class="px-6 py-4 whitespace-nowrap"> <table class="divide-y divide-gray-200 w-full">
<div class="flex items-center"> <thead class="bg-gray-50">
{#if u.profilePic} <tr>
<div class="flex-shrink-0 h-10 w-10"> <th
<img scope="col"
class="h-10 w-10 rounded-full" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
src={u.profilePic} Name
alt="" /> </th>
</div> <th
{/if} scope="col"
<div class="ml-4"> class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<div Status
class="text-sm font-medium text-gray-900 dark:text-gray-100"> </th>
{u.firstname} <th
{u.middlename || ''} scope="col"
{u.lastname} class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</div> Groups
<div class="text-sm text-gray-500"> </th>
{u.email || u.username} <th scope="col" class="relative px-6 py-3">
<span class="sr-only">Action</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{#each users as u}
{#if Object.values(u)
.toString()
.toLowerCase()
.includes(searchvalue)}
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
{#if u.profilePic}
<div class="flex-shrink-0 h-10 w-10">
<img
class="h-10 w-10 rounded-full"
src={u.profilePic}
alt="" />
</div>
{/if}
<div class="ml-4">
<div
class="text-sm font-medium text-gray-900 dark:text-gray-100">
{u.firstname}
{u.middlename || ''}
{u.lastname}
</div>
<div class="text-sm text-gray-500">
{u.email || u.username}
</div>
</div> </div>
</div> </div>
</div> </td>
</td> <td class="px-6 py-4 whitespace-nowrap">
<td class="px-6 py-4 whitespace-nowrap"> {#if u.enabled}
{#if u.enabled} <span
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Active</span>
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Active</span> {:else}
{:else} <span
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">Inactive</span>
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">Inactive</span> {/if}
{/if} </td>
</td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> {#each u.groups as g}
{#each u.groups as g} <a
<a href="../groups/{g.id}"
href="../groups/{g.id}" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">{g.name}</a>
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">{g.name}</a> {/each}
{/each} </td>
</td> {#if active_deletes[u.id] === true}
{#if active_deletes[u.id] === true} <td
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <button
<button on:click={() => {
on:click={() => { active_deletes[u.id] = false;
active_deletes[u.id] = false; }}
}} tabindex="0"
tabindex="0" class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer">Cancel
class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer">Cancel Delete</button>
Delete</button> <button
<button on:click={() => {
on:click={() => { UserService.userControllerRemove(u.id, true)
UserService.userControllerRemove(u.id, true) .then((resp) => {
.then((resp) => { console.log(resp);
console.log(resp); // user deleted
// user deleted users_promise.then((data) => {
users_promise.then((data) => { console.log(data);
console.log(data); usersstore.set(data);
usersstore.set(data); });
})
.catch((err) => {
// error deleting user
}); });
}) }}
.catch((err) => { tabindex="0"
// error deleting user class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">Confirm
}); Delete</button>
}} </td>
tabindex="0" {:else}
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">Confirm <td
Delete</button> class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
</td> <a
{:else} href="./{u.id}"
<td class="text-indigo-600 hover:text-indigo-900">Edit</a>
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> {#if store.state.jwtinfo.userdetails.permissions.includes('USER:DELETE')}
<a <button
href="./{u.id}" on:click={() => {
class="text-indigo-600 hover:text-indigo-900">Edit</a> active_deletes[u.id] = true;
<button }}
on:click={() => { tabindex="0"
active_deletes[u.id] = true; class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">Delete</button>
}} {/if}
tabindex="0" </td>
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">Delete</button> {/if}
</td> </tr>
{/if} {/if}
</tr> {/each}
{/if} </tbody>
{/each} </table>
</tbody> </div>
</table> {/if}
{:catch error}
<div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
<span class="inline-block align-middle mr-8">
<b class="capitalize">{$_('general_promise_error')}</b>
{error}
</span>
</div> </div>
{/if} {/await}
{:catch error} {/if}
<div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
<span class="inline-block align-middle mr-8">
<b class="capitalize">{$_('general_promise_error')}</b>
{error}
</span>
</div>
{/await}