@nymphjs/tilmeld-setup 1.0.0-beta.121 → 1.0.0-beta.123

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,10 +6,7 @@
6
6
  </section>
7
7
  {:else}
8
8
  <div style="display: flex; align-items: center; padding: 12px;">
9
- <IconButton
10
- title="Back"
11
- onclick={() => router.navigate('', { historyAPIMethod: 'back' })}
12
- >
9
+ <IconButton title="Back" onclick={() => pop()}>
13
10
  <Icon tag="svg" viewBox="0 0 24 24">
14
11
  <path fill="currentColor" d={mdiArrowLeft} />
15
12
  </Icon>
@@ -23,7 +20,8 @@
23
20
  {#await $entity.user.$wake() then _user}
24
21
  <div style="padding: 12px;" class="mdc-typography--subtitle1">
25
22
  Generated primary group for <a
26
- href="#/users/edit/{encodeURIComponent($entity.user.guid || '')}"
23
+ href="/users/edit/{encodeURIComponent($entity.user.guid || '')}"
24
+ use:link
27
25
  >{$clientConfig.userFields.includes('name')
28
26
  ? $entity.user.name + ' (' + $entity.user.username + ')'
29
27
  : $entity.user.username}</a
@@ -32,7 +30,10 @@
32
30
  {/await}
33
31
  {/if}
34
32
 
35
- <TabBar tabs={['General', 'Parent', 'Abilities']} bind:active={activeTab}>
33
+ <TabBar
34
+ tabs={['General', 'Parent', 'Children', 'Abilities', 'Users']}
35
+ bind:active={activeTab}
36
+ >
36
37
  {#snippet tab(tab)}
37
38
  <Tab {tab}>
38
39
  <Label>{tab}</Label>
@@ -194,7 +195,8 @@
194
195
  No parent
195
196
  {:else}
196
197
  <a
197
- href="#/groups/edit/{encodeURIComponent($entity.parent.guid || '')}"
198
+ href="/groups/edit/{encodeURIComponent($entity.parent.guid || '')}"
199
+ use:link
198
200
  >{$clientConfig.userFields.includes('name')
199
201
  ? $entity.parent.name + ' (' + $entity.parent.groupname + ')'
200
202
  : $entity.parent.groupname}</a
@@ -285,6 +287,144 @@
285
287
  {/if}
286
288
  {/if}
287
289
 
290
+ {#if activeTab === 'Children'}
291
+ <h5 style="margin-top: 0;">Children</h5>
292
+
293
+ <DataTable
294
+ sortable
295
+ bind:sort={childrenSort}
296
+ bind:sortDirection={childrenSortDirection}
297
+ onSMUIDataTableSorted={handleChildrenSort}
298
+ table$aria-label="Children list"
299
+ style="width: 100%;"
300
+ >
301
+ <Head>
302
+ <Row>
303
+ {#if !$clientConfig.emailUsernames}
304
+ <Cell columnId="child-groupname">
305
+ <Label>Groupname</Label>
306
+ <IconButton>
307
+ <Icon class="material-icons">arrow_upward</Icon>
308
+ </IconButton>
309
+ </Cell>
310
+ {/if}
311
+ {#if $clientConfig.userFields.includes('name')}
312
+ <Cell columnId="child-name">
313
+ <Label>Name</Label>
314
+ <IconButton>
315
+ <Icon class="material-icons">arrow_upward</Icon>
316
+ </IconButton>
317
+ </Cell>
318
+ {/if}
319
+ {#if $clientConfig.userFields.includes('email')}
320
+ <Cell columnId="child-email">
321
+ <Label>Email</Label>
322
+ <IconButton>
323
+ <Icon class="material-icons">arrow_upward</Icon>
324
+ </IconButton>
325
+ </Cell>
326
+ {/if}
327
+ <Cell columnId="child-enabled">
328
+ <Label>Enabled</Label>
329
+ <IconButton>
330
+ <Icon class="material-icons">arrow_upward</Icon>
331
+ </IconButton>
332
+ </Cell>
333
+ </Row>
334
+ </Head>
335
+ <Body>
336
+ {#each $children as group (group.guid)}
337
+ <Row>
338
+ {#if !$clientConfig.emailUsernames}
339
+ <Cell
340
+ ><a
341
+ href="/groups/edit/{encodeURIComponent(group.guid || '')}"
342
+ use:link>{group.groupname}</a
343
+ ></Cell
344
+ >
345
+ {/if}
346
+ {#if $clientConfig.userFields.includes('name')}
347
+ <Cell
348
+ ><a
349
+ href="/groups/edit/{encodeURIComponent(group.guid || '')}"
350
+ use:link>{group.name}</a
351
+ ></Cell
352
+ >
353
+ {/if}
354
+ {#if $clientConfig.userFields.includes('email')}
355
+ <Cell
356
+ ><a
357
+ href="/groups/edit/{encodeURIComponent(group.guid || '')}"
358
+ use:link>{group.email}</a
359
+ ></Cell
360
+ >
361
+ {/if}
362
+ <Cell>{group.enabled ? 'Yes' : 'No'}</Cell>
363
+ </Row>
364
+ {:else}
365
+ {null}
366
+ {/each}
367
+ </Body>
368
+
369
+ {#snippet progress()}
370
+ <LinearProgress
371
+ indeterminate
372
+ closed={childrenLoaded}
373
+ aria-label="Data is being loaded..."
374
+ />
375
+ {/snippet}
376
+
377
+ {#snippet paginate()}
378
+ <Pagination>
379
+ {#snippet rowsPerPage()}
380
+ <Label>Rows Per Page</Label>
381
+ <Select variant="outlined" bind:value={childrenPerPage} noLabel>
382
+ <Option value={10}>10</Option>
383
+ <Option value={25}>25</Option>
384
+ <Option value={100}>100</Option>
385
+ </Select>
386
+ {/snippet}
387
+ {#snippet total()}
388
+ {childrenStart + 1}-{childrenEnd} of {childrenLength}
389
+ {/snippet}
390
+
391
+ <IconButton
392
+ action="first-page"
393
+ title="First page"
394
+ onclick={() => (childrenCurrentPage = 0)}
395
+ disabled={childrenCurrentPage === 0}
396
+ >
397
+ <Icon class="material-icons">first_page</Icon>
398
+ </IconButton>
399
+ <IconButton
400
+ action="prev-page"
401
+ title="Prev page"
402
+ onclick={() => childrenCurrentPage--}
403
+ disabled={childrenCurrentPage === 0}
404
+ >
405
+ <Icon class="material-icons">chevron_left</Icon>
406
+ </IconButton>
407
+ <IconButton
408
+ action="next-page"
409
+ title="Next page"
410
+ onclick={() => childrenCurrentPage++}
411
+ disabled={childrenCurrentPage === childrenLastPage}
412
+ >
413
+ <Icon class="material-icons">chevron_right</Icon>
414
+ </IconButton>
415
+ <IconButton
416
+ action="last-page"
417
+ title="Last page"
418
+ onclick={() => (childrenCurrentPage = childrenLastPage)}
419
+ disabled={childrenCurrentPage === childrenLastPage}
420
+ >
421
+ <Icon class="material-icons">last_page</Icon>
422
+ </IconButton>
423
+ </Pagination>
424
+ {/snippet}
425
+ </DataTable>
426
+ {/if}
427
+
288
428
  {#if activeTab === 'Abilities'}
289
429
  <h5 style="margin-top: 0;">Abilities</h5>
290
430
 
@@ -332,6 +472,148 @@
332
472
  </div>
333
473
  {/if}
334
474
 
475
+ {#if activeTab === 'Users'}
476
+ <h5 style="margin-top: 0;">Users</h5>
477
+
478
+ <DataTable
479
+ sortable
480
+ bind:sort={usersSort}
481
+ bind:sortDirection={usersSortDirection}
482
+ onSMUIDataTableSorted={handleUsersSort}
483
+ table$aria-label="User list"
484
+ style="width: 100%;"
485
+ >
486
+ <Head>
487
+ <Row>
488
+ {#if !$clientConfig.emailUsernames}
489
+ <Cell columnId="user-username">
490
+ <Label>Username</Label>
491
+ <IconButton>
492
+ <Icon class="material-icons">arrow_upward</Icon>
493
+ </IconButton>
494
+ </Cell>
495
+ {/if}
496
+ {#if $clientConfig.userFields.includes('name')}
497
+ <Cell columnId="user-name">
498
+ <Label>Name</Label>
499
+ <IconButton>
500
+ <Icon class="material-icons">arrow_upward</Icon>
501
+ </IconButton>
502
+ </Cell>
503
+ {/if}
504
+ {#if $clientConfig.userFields.includes('email')}
505
+ <Cell columnId="user-email">
506
+ <Label>Email</Label>
507
+ <IconButton>
508
+ <Icon class="material-icons">arrow_upward</Icon>
509
+ </IconButton>
510
+ </Cell>
511
+ {/if}
512
+ <Cell columnId="user-enabled">
513
+ <Label>Enabled</Label>
514
+ <IconButton>
515
+ <Icon class="material-icons">arrow_upward</Icon>
516
+ </IconButton>
517
+ </Cell>
518
+ <Cell columnId="user-primary" sortable={false}>
519
+ <Label>Primary</Label>
520
+ </Cell>
521
+ </Row>
522
+ </Head>
523
+ <Body>
524
+ {#each $users as user (user.guid)}
525
+ <Row>
526
+ {#if !$clientConfig.emailUsernames}
527
+ <Cell
528
+ ><a
529
+ href="/users/edit/{encodeURIComponent(user.guid || '')}"
530
+ use:link>{user.username}</a
531
+ ></Cell
532
+ >
533
+ {/if}
534
+ {#if $clientConfig.userFields.includes('name')}
535
+ <Cell
536
+ ><a
537
+ href="/users/edit/{encodeURIComponent(user.guid || '')}"
538
+ use:link>{user.name}</a
539
+ ></Cell
540
+ >
541
+ {/if}
542
+ {#if $clientConfig.userFields.includes('email')}
543
+ <Cell
544
+ ><a
545
+ href="/users/edit/{encodeURIComponent(user.guid || '')}"
546
+ use:link>{user.email}</a
547
+ ></Cell
548
+ >
549
+ {/if}
550
+ <Cell>{user.enabled ? 'Yes' : 'No'}</Cell>
551
+ <Cell>{$entity.$is(user.group) ? 'Yes' : 'No'}</Cell>
552
+ </Row>
553
+ {:else}
554
+ {null}
555
+ {/each}
556
+ </Body>
557
+
558
+ {#snippet progress()}
559
+ <LinearProgress
560
+ indeterminate
561
+ closed={usersLoaded}
562
+ aria-label="Data is being loaded..."
563
+ />
564
+ {/snippet}
565
+
566
+ {#snippet paginate()}
567
+ <Pagination>
568
+ {#snippet rowsPerPage()}
569
+ <Label>Rows Per Page</Label>
570
+ <Select variant="outlined" bind:value={usersPerPage} noLabel>
571
+ <Option value={10}>10</Option>
572
+ <Option value={25}>25</Option>
573
+ <Option value={100}>100</Option>
574
+ </Select>
575
+ {/snippet}
576
+ {#snippet total()}
577
+ {usersStart + 1}-{usersEnd} of {usersLength}
578
+ {/snippet}
579
+
580
+ <IconButton
581
+ action="first-page"
582
+ title="First page"
583
+ onclick={() => (usersCurrentPage = 0)}
584
+ disabled={usersCurrentPage === 0}
585
+ >
586
+ <Icon class="material-icons">first_page</Icon>
587
+ </IconButton>
588
+ <IconButton
589
+ action="prev-page"
590
+ title="Prev page"
591
+ onclick={() => usersCurrentPage--}
592
+ disabled={usersCurrentPage === 0}
593
+ >
594
+ <Icon class="material-icons">chevron_left</Icon>
595
+ </IconButton>
596
+ <IconButton
597
+ action="next-page"
598
+ title="Next page"
599
+ onclick={() => usersCurrentPage++}
600
+ disabled={usersCurrentPage === usersLastPage}
601
+ >
602
+ <Icon class="material-icons">chevron_right</Icon>
603
+ </IconButton>
604
+ <IconButton
605
+ action="last-page"
606
+ title="Last page"
607
+ onclick={() => (usersCurrentPage = usersLastPage)}
608
+ disabled={usersCurrentPage === usersLastPage}
609
+ >
610
+ <Icon class="material-icons">last_page</Icon>
611
+ </IconButton>
612
+ </Pagination>
613
+ {/snippet}
614
+ </DataTable>
615
+ {/if}
616
+
335
617
  {#if failureMessage}
336
618
  <div class="tilmeld-failure">
337
619
  {failureMessage}
@@ -357,7 +639,8 @@
357
639
  <script lang="ts">
358
640
  import type { Writable } from 'svelte/store';
359
641
  import { writable } from 'svelte/store';
360
- import type Navigo from 'navigo';
642
+ import { getContext } from 'svelte';
643
+ import { link, pop, replace } from 'svelte-spa-router';
361
644
  import type {
362
645
  AdminGroupData,
363
646
  ClientConfig,
@@ -366,6 +649,7 @@
366
649
  import type {
367
650
  Group as GroupClass,
368
651
  User as UserClass,
652
+ AdminUserData,
369
653
  } from '@nymphjs/tilmeld-client';
370
654
  import queryParser from '@nymphjs/query-parser';
371
655
  import {
@@ -383,31 +667,31 @@
383
667
  import Checkbox from '@smui/checkbox';
384
668
  import List, { Item, Text, Meta } from '@smui/list';
385
669
  import Paper from '@smui/paper';
386
- import DataTable, { Head, Body, Row, Cell } from '@smui/data-table';
670
+ import DataTable, {
671
+ Head,
672
+ Body,
673
+ Row,
674
+ Cell,
675
+ Pagination,
676
+ SortValue,
677
+ } from '@smui/data-table';
678
+ import Select, { Option } from '@smui/select';
387
679
  import Textfield, { Input } from '@smui/textfield';
388
680
  import HelperText from '@smui/textfield/helper-text';
389
681
  import IconButton from '@smui/icon-button';
390
682
  import Button from '@smui/button';
683
+ import LinearProgress from '@smui/linear-progress';
391
684
  import { Icon, Label } from '@smui/common';
392
685
 
393
- import { nymph, Group, User } from '../nymph';
686
+ import { nymph, Group, User } from '../nymph.js';
394
687
 
395
- let {
396
- router,
397
- params,
398
- clientConfig,
399
- user,
400
- }: {
401
- router: Navigo;
402
- params: { guid: string };
403
- clientConfig: Writable<ClientConfig | undefined>;
404
- user: Writable<(UserClass & CurrentUserData) | null | undefined>;
405
- } = $props();
688
+ let { params }: { params: { guid: string } } = $props();
406
689
 
407
690
  let entity: Writable<GroupClass & AdminGroupData> = writable(
408
691
  Group.factorySync(),
409
692
  );
410
- let activeTab: 'General' | 'Parent' | 'Abilities' = $state('General');
693
+ let activeTab: 'General' | 'Parent' | 'Children' | 'Abilities' | 'Users' =
694
+ $state('General');
411
695
  let parentSearch = $state('');
412
696
  let ability = $state('');
413
697
  let avatar = $state('https://secure.gravatar.com/avatar/?d=mm&s=40');
@@ -418,16 +702,63 @@
418
702
  let emailTimer: number | undefined = undefined;
419
703
  let emailVerified: boolean | undefined = $state();
420
704
  let emailVerifiedMessage: string | undefined = $state();
705
+ let children: Writable<(GroupClass & AdminGroupData)[]> = $state(
706
+ writable([]),
707
+ );
708
+ let childrenSort: `child-${keyof AdminGroupData}` = $state('child-groupname');
709
+ let childrenSortDirection: Lowercase<SortValue> = $state('ascending');
710
+ let childrenPerPage = $state(10);
711
+ let childrenCurrentPage = $state(0);
712
+ let childrenLength = $state(0);
713
+ const childrenStart = $derived(childrenCurrentPage * childrenPerPage);
714
+ const childrenEnd = $derived(
715
+ Math.min(childrenStart + childrenPerPage, childrenLength),
716
+ );
717
+ const childrenLastPage = $derived(
718
+ Math.max(Math.ceil(childrenLength / childrenPerPage) - 1, 0),
719
+ );
720
+ let childrenLoaded = $state(false);
721
+ let users: Writable<(UserClass & AdminUserData)[]> = $state(writable([]));
722
+ let usersSort: `user-${keyof AdminUserData}` = $state('user-username');
723
+ let usersSortDirection: Lowercase<SortValue> = $state('ascending');
724
+ let usersPerPage = $state(10);
725
+ let usersCurrentPage = $state(0);
726
+ let usersLength = $state(0);
727
+ const usersStart = $derived(usersCurrentPage * usersPerPage);
728
+ const usersEnd = $derived(Math.min(usersStart + usersPerPage, usersLength));
729
+ const usersLastPage = $derived(
730
+ Math.max(Math.ceil(usersLength / usersPerPage) - 1, 0),
731
+ );
732
+ let usersLoaded = $state(false);
421
733
  let saving = $state(false);
422
734
  let success: boolean | undefined = $state();
423
735
  let loading = $state(true);
424
736
 
737
+ const clientConfig =
738
+ getContext<Writable<ClientConfig | undefined>>('clientConfigStore');
739
+ const user =
740
+ getContext<Writable<(UserClass & CurrentUserData) | null | undefined>>(
741
+ 'userStore',
742
+ );
743
+
425
744
  $effect(() => {
426
745
  if (params) {
427
746
  handleGuidParam();
428
747
  }
429
748
  });
430
749
 
750
+ $effect(() => {
751
+ if (usersCurrentPage >= 0 && usersPerPage >= 0) {
752
+ fillUsers();
753
+ }
754
+ });
755
+
756
+ $effect(() => {
757
+ if (childrenCurrentPage >= 0 && childrenPerPage >= 0) {
758
+ fillChildren();
759
+ }
760
+ });
761
+
431
762
  async function handleGuidParam() {
432
763
  loading = true;
433
764
  failureMessage = undefined;
@@ -477,6 +808,9 @@
477
808
  avatar = await $entity.$getAvatar();
478
809
  await $entity.$wakeAll(1);
479
810
  $entity = $entity;
811
+
812
+ await fillUsers();
813
+ await fillChildren();
480
814
  }
481
815
 
482
816
  let parentsSearching = $state(false);
@@ -592,26 +926,114 @@
592
926
  if (event.key === 'Enter') addAbility();
593
927
  }
594
928
 
929
+ async function handleChildrenSort() {
930
+ await fillChildren();
931
+ }
932
+
933
+ async function fillChildren() {
934
+ childrenLoaded = false;
935
+ try {
936
+ if ($entity.guid == null) {
937
+ $children = [];
938
+ childrenLength = 0;
939
+ } else {
940
+ $children = await nymph.getEntities(
941
+ {
942
+ class: Group,
943
+ sort: childrenSort.slice('child-'.length),
944
+ reverse: childrenSortDirection === 'descending',
945
+ limit: childrenPerPage,
946
+ offset: childrenPerPage * childrenCurrentPage,
947
+ },
948
+ {
949
+ type: '&',
950
+ ref: ['parent', $entity],
951
+ },
952
+ );
953
+
954
+ if (childrenLength === 0) {
955
+ childrenLength = await nymph.getEntities(
956
+ {
957
+ class: Group,
958
+ return: 'count',
959
+ },
960
+ {
961
+ type: '&',
962
+ ref: ['parent', $entity],
963
+ },
964
+ );
965
+ }
966
+ }
967
+ } catch (e: any) {
968
+ failureMessage = e.message;
969
+ }
970
+ childrenLoaded = true;
971
+ }
972
+
973
+ async function handleUsersSort() {
974
+ await fillUsers();
975
+ }
976
+
977
+ async function fillUsers() {
978
+ usersLoaded = false;
979
+ try {
980
+ if ($entity.guid == null) {
981
+ $users = [];
982
+ usersLength = 0;
983
+ } else {
984
+ $users = await nymph.getEntities(
985
+ {
986
+ class: User,
987
+ sort: usersSort.slice('user-'.length),
988
+ reverse: usersSortDirection === 'descending',
989
+ limit: usersPerPage,
990
+ offset: usersPerPage * usersCurrentPage,
991
+ },
992
+ {
993
+ type: '|',
994
+ ref: [
995
+ ['group', $entity],
996
+ ['groups', $entity],
997
+ ],
998
+ },
999
+ );
1000
+
1001
+ if (usersLength === 0) {
1002
+ usersLength = await nymph.getEntities(
1003
+ {
1004
+ class: User,
1005
+ return: 'count',
1006
+ },
1007
+ {
1008
+ type: '|',
1009
+ ref: [
1010
+ ['group', $entity],
1011
+ ['groups', $entity],
1012
+ ],
1013
+ },
1014
+ );
1015
+ }
1016
+ }
1017
+ } catch (e: any) {
1018
+ failureMessage = e.message;
1019
+ }
1020
+ usersLoaded = true;
1021
+ }
1022
+
595
1023
  async function saveEntity() {
596
1024
  saving = true;
597
1025
  failureMessage = undefined;
598
1026
  const newEntity = $entity.guid == null;
599
1027
  try {
600
- if (await $entity.$save()) {
601
- await readyEntity();
602
- success = true;
603
- if (newEntity) {
604
- router.navigate(
605
- `/groups/edit/${encodeURIComponent($entity.guid || '')}`,
606
- { historyAPIMethod: 'replaceState' },
607
- );
608
- }
609
- setTimeout(() => {
610
- success = undefined;
611
- }, 1000);
612
- } else {
613
- failureMessage = 'Error saving group.';
1028
+ await $entity.$save();
1029
+ await readyEntity();
1030
+ success = true;
1031
+ if (newEntity) {
1032
+ replace(`/groups/edit/${encodeURIComponent($entity.guid || '')}`);
614
1033
  }
1034
+ setTimeout(() => {
1035
+ success = undefined;
1036
+ }, 1000);
615
1037
  } catch (e: any) {
616
1038
  console.log('error:', e);
617
1039
  failureMessage = e?.message;
@@ -621,14 +1043,15 @@
621
1043
 
622
1044
  async function deleteEntity() {
623
1045
  failureMessage = undefined;
624
- if (confirm('Are you sure you want to delete this?')) {
1046
+ if (
1047
+ confirm(
1048
+ 'Are you sure you want to delete this? All descendant groups will be deleted too, and users will be removed.',
1049
+ )
1050
+ ) {
625
1051
  saving = true;
626
1052
  try {
627
- if (await $entity.$delete()) {
628
- router.navigate('', { historyAPIMethod: 'back' });
629
- } else {
630
- failureMessage = 'An error occurred.';
631
- }
1053
+ await $entity.$delete();
1054
+ pop();
632
1055
  } catch (e: any) {
633
1056
  failureMessage = e?.message;
634
1057
  }