@nymphjs/tilmeld-setup 1.0.0-beta.122 → 1.0.0-beta.124

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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,20 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [1.0.0-beta.124](https://forge.sciactive.com/sciactive/nymphjs/compare/v1.0.0-beta.123...v1.0.0-beta.124) (2026-09-24)
7
+
8
+ **Note:** Version bump only for package @nymphjs/tilmeld-setup
9
+
10
+ # [1.0.0-beta.123](https://forge.sciactive.com/sciactive/nymphjs/compare/v1.0.0-beta.122...v1.0.0-beta.123) (2026-09-23)
11
+
12
+ ### Bug Fixes
13
+
14
+ - reload router to reinitialize components after location change in tilmeld setup ([09c1206](https://forge.sciactive.com/sciactive/nymphjs/commits/09c12061418637c589a3db7e182a52c7ca33dca6))
15
+
16
+ ### Features
17
+
18
+ - add children and users tables to group edit in tilmeld setup ([3ffa18e](https://forge.sciactive.com/sciactive/nymphjs/commits/3ffa18ec7027b424df472f1883d643eefbb3cfcb))
19
+
6
20
  # [1.0.0-beta.122](https://forge.sciactive.com/sciactive/nymphjs/compare/v1.0.0-beta.121...v1.0.0-beta.122) (2026-09-22)
7
21
 
8
22
  ### Features
@@ -107,7 +107,9 @@
107
107
  <main class="tilmeld-main-content" bind:this={mainContent}>
108
108
  {#if tilmeldAdmin}
109
109
  {#if $clientConfig}
110
- <Router {routes} {onRouteLoaded} />
110
+ {#key currentLocation}
111
+ <Router {routes} {onRouteLoaded} />
112
+ {/key}
111
113
  {:else}
112
114
  Loading...
113
115
  {/if}
@@ -187,6 +189,7 @@
187
189
  });
188
190
 
189
191
  let currentRoute = $state('/');
192
+ let currentLocation = $state('/');
190
193
 
191
194
  const routes = {
192
195
  '/': Intro,
@@ -204,6 +207,7 @@
204
207
  }
205
208
 
206
209
  currentRoute = `${detail?.route ?? '/'}`;
210
+ currentLocation = `${detail?.location ?? '/'}`;
207
211
  }
208
212
 
209
213
  const sections: (
@@ -30,7 +30,10 @@
30
30
  {/await}
31
31
  {/if}
32
32
 
33
- <TabBar tabs={['General', 'Parent', 'Abilities']} bind:active={activeTab}>
33
+ <TabBar
34
+ tabs={['General', 'Parent', 'Children', 'Abilities', 'Users']}
35
+ bind:active={activeTab}
36
+ >
34
37
  {#snippet tab(tab)}
35
38
  <Tab {tab}>
36
39
  <Label>{tab}</Label>
@@ -284,6 +287,144 @@
284
287
  {/if}
285
288
  {/if}
286
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
+
287
428
  {#if activeTab === 'Abilities'}
288
429
  <h5 style="margin-top: 0;">Abilities</h5>
289
430
 
@@ -331,6 +472,148 @@
331
472
  </div>
332
473
  {/if}
333
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
+
334
617
  {#if failureMessage}
335
618
  <div class="tilmeld-failure">
336
619
  {failureMessage}
@@ -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,11 +667,20 @@
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
686
  import { nymph, Group, User } from '../nymph.js';
@@ -397,7 +690,8 @@
397
690
  let entity: Writable<GroupClass & AdminGroupData> = writable(
398
691
  Group.factorySync(),
399
692
  );
400
- let activeTab: 'General' | 'Parent' | 'Abilities' = $state('General');
693
+ let activeTab: 'General' | 'Parent' | 'Children' | 'Abilities' | 'Users' =
694
+ $state('General');
401
695
  let parentSearch = $state('');
402
696
  let ability = $state('');
403
697
  let avatar = $state('https://secure.gravatar.com/avatar/?d=mm&s=40');
@@ -408,6 +702,34 @@
408
702
  let emailTimer: number | undefined = undefined;
409
703
  let emailVerified: boolean | undefined = $state();
410
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);
411
733
  let saving = $state(false);
412
734
  let success: boolean | undefined = $state();
413
735
  let loading = $state(true);
@@ -425,6 +747,18 @@
425
747
  }
426
748
  });
427
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
+
428
762
  async function handleGuidParam() {
429
763
  loading = true;
430
764
  failureMessage = undefined;
@@ -474,6 +808,9 @@
474
808
  avatar = await $entity.$getAvatar();
475
809
  await $entity.$wakeAll(1);
476
810
  $entity = $entity;
811
+
812
+ await fillUsers();
813
+ await fillChildren();
477
814
  }
478
815
 
479
816
  let parentsSearching = $state(false);
@@ -589,6 +926,100 @@
589
926
  if (event.key === 'Enter') addAbility();
590
927
  }
591
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
+
592
1023
  async function saveEntity() {
593
1024
  saving = true;
594
1025
  failureMessage = undefined;
@@ -612,7 +1043,11 @@
612
1043
 
613
1044
  async function deleteEntity() {
614
1045
  failureMessage = undefined;
615
- 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
+ ) {
616
1051
  saving = true;
617
1052
  try {
618
1053
  await $entity.$delete();