@nymphjs/tilmeld-setup 1.0.0-beta.13 → 1.0.0-beta.131

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.
@@ -0,0 +1,1061 @@
1
+ {#if $clientConfig == null || $user == null || loading}
2
+ <section>
3
+ <div style="display: flex; justify-content: center; align-items: center;">
4
+ <CircularProgress style="height: 45px; width: 45px;" indeterminate />
5
+ </div>
6
+ </section>
7
+ {:else}
8
+ <div style="display: flex; align-items: center; padding: 12px;">
9
+ <IconButton title="Back" onclick={() => pop()}>
10
+ <Icon tag="svg" viewBox="0 0 24 24">
11
+ <path fill="currentColor" d={mdiArrowLeft} />
12
+ </Icon>
13
+ </IconButton>
14
+ <h2 style="margin: 0px 12px 0px;" class="mdc-typography--headline5">
15
+ Editing {$entity.guid ? ($entity.name ?? $entity.groupname) : 'New Group'}
16
+ </h2>
17
+ </div>
18
+
19
+ {#if $entity.user}
20
+ {#await $entity.user.$wake() then _user}
21
+ <div style="padding: 12px;" class="mdc-typography--subtitle1">
22
+ Generated primary group for <a
23
+ href="/users/edit/{encodeURIComponent($entity.user.guid || '')}"
24
+ use:link
25
+ >{$clientConfig.userFields.includes('name')
26
+ ? $entity.user.name + ' (' + $entity.user.username + ')'
27
+ : $entity.user.username}</a
28
+ >
29
+ </div>
30
+ {/await}
31
+ {/if}
32
+
33
+ <TabBar
34
+ tabs={['General', 'Parent', 'Children', 'Abilities', 'Users']}
35
+ bind:active={activeTab}
36
+ >
37
+ {#snippet tab(tab)}
38
+ <Tab {tab}>
39
+ <Label>{tab}</Label>
40
+ </Tab>
41
+ {/snippet}
42
+ </TabBar>
43
+
44
+ <section>
45
+ {#if activeTab === 'General'}
46
+ <LayoutGrid style="padding: 0;">
47
+ {#if $entity.user != null}
48
+ <LayoutCell span={12}>
49
+ Some of these fields are not editable, since this group inherits the
50
+ values from its user.
51
+ </LayoutCell>
52
+ {/if}
53
+ <LayoutCell span={4}>
54
+ <div class="mdc-typography--headline6">GUID</div>
55
+ <code>{$entity.guid}</code>
56
+ </LayoutCell>
57
+ <LayoutCell span={4}>
58
+ <FormField>
59
+ <Checkbox bind:checked={$entity.enabled} />
60
+ {#snippet label()}
61
+ Enabled (Able to give abilities)
62
+ {/snippet}
63
+ </FormField>
64
+ </LayoutCell>
65
+ <LayoutCell span={4} style="text-align: end;">
66
+ <a href="https://en.gravatar.com/" target="_blank" rel="noreferrer">
67
+ <img src={avatar} alt="Avatar" title="Avatar by Gravatar" />
68
+ </a>
69
+ </LayoutCell>
70
+ {#if !$clientConfig.emailUsernames}
71
+ <LayoutCell
72
+ span={$clientConfig.userFields.includes('email') ? 6 : 12}
73
+ >
74
+ <Textfield
75
+ bind:value={$entity.groupname}
76
+ label="Groupname"
77
+ type="text"
78
+ style="width: 100%;"
79
+ helperLine$style="width: 100%;"
80
+ invalid={groupnameVerified === false}
81
+ input$autocomplete="off"
82
+ input$autocapitalize="off"
83
+ input$spellcheck="false"
84
+ disabled={$entity.user != null}
85
+ >
86
+ {#snippet helper()}
87
+ <HelperText persistent>
88
+ {groupnameVerifiedMessage ?? ''}
89
+ </HelperText>
90
+ {/snippet}
91
+ </Textfield>
92
+ </LayoutCell>
93
+ {/if}
94
+ {#if $clientConfig.userFields.includes('email')}
95
+ <LayoutCell span={$clientConfig.emailUsernames ? 12 : 6}>
96
+ <Textfield
97
+ bind:value={$entity.email}
98
+ label="Email"
99
+ type="email"
100
+ style="width: 100%;"
101
+ helperLine$style="width: 100%;"
102
+ invalid={emailVerified === false}
103
+ input$autocomplete="off"
104
+ input$autocapitalize="off"
105
+ input$spellcheck="false"
106
+ disabled={$entity.user != null}
107
+ >
108
+ {#snippet helper()}
109
+ <HelperText persistent>
110
+ {emailVerifiedMessage ?? ''}
111
+ </HelperText>
112
+ {/snippet}
113
+ </Textfield>
114
+ </LayoutCell>
115
+ {/if}
116
+ {#if $clientConfig.userFields.includes('name')}
117
+ <LayoutCell span={12}>
118
+ <Textfield
119
+ bind:value={$entity.name}
120
+ label="Display Name"
121
+ type="text"
122
+ style="width: 100%;"
123
+ input$autocomplete="off"
124
+ disabled={$entity.user != null}
125
+ />
126
+ </LayoutCell>
127
+ {/if}
128
+ <LayoutCell span={$clientConfig.userFields.includes('phone') ? 8 : 12}>
129
+ <Textfield
130
+ bind:value={$entity.avatar}
131
+ label="Avatar"
132
+ type="text"
133
+ style="width: 100%;"
134
+ input$autocomplete="off"
135
+ disabled={$entity.user != null}
136
+ />
137
+ </LayoutCell>
138
+ {#if $clientConfig.userFields.includes('phone')}
139
+ <LayoutCell span={4}>
140
+ <Textfield
141
+ bind:value={$entity.phone}
142
+ label="Phone"
143
+ type="tel"
144
+ style="width: 100%;"
145
+ input$autocomplete="off"
146
+ disabled={$entity.user != null}
147
+ />
148
+ </LayoutCell>
149
+ {/if}
150
+ <LayoutCell span={12}>
151
+ <FormField>
152
+ <Checkbox bind:checked={$entity.defaultPrimary} />
153
+ {#snippet label()}
154
+ Default primary group for newly registered users. <small
155
+ class="form-text text-muted"
156
+ >Setting this will unset any current default primary group.</small
157
+ >
158
+ {/snippet}
159
+ </FormField>
160
+ </LayoutCell>
161
+ <LayoutCell span={12}>
162
+ <FormField>
163
+ <Checkbox bind:checked={$entity.defaultSecondary} />
164
+ {#snippet label()}
165
+ Default secondary group for newly registered{$clientConfig?.userFields.includes(
166
+ 'email',
167
+ ) &&
168
+ $clientConfig.verifyEmail &&
169
+ $clientConfig.unverifiedAccess
170
+ ? ', verified'
171
+ : ''} users.
172
+ {/snippet}
173
+ </FormField>
174
+ </LayoutCell>
175
+ {#if $clientConfig.userFields.includes('email') && $clientConfig.verifyEmail && $clientConfig.unverifiedAccess}
176
+ <LayoutCell span={12}>
177
+ <FormField>
178
+ <Checkbox bind:checked={$entity.unverifiedSecondary} />
179
+ {#snippet label()}
180
+ Default secondary group for newly registered, unverified users.
181
+ {/snippet}
182
+ </FormField>
183
+ </LayoutCell>
184
+ {/if}
185
+ </LayoutGrid>
186
+ {/if}
187
+
188
+ {#if activeTab === 'Parent'}
189
+ <h5 style="margin-top: 0;">Parent</h5>
190
+
191
+ <Paper
192
+ style="display: flex; justify-content: space-between; align-items: center;"
193
+ >
194
+ {#if !$entity.parent}
195
+ No parent
196
+ {:else}
197
+ <a
198
+ href="/groups/edit/{encodeURIComponent($entity.parent.guid || '')}"
199
+ use:link
200
+ >{$clientConfig.userFields.includes('name')
201
+ ? $entity.parent.name + ' (' + $entity.parent.groupname + ')'
202
+ : $entity.parent.groupname}</a
203
+ >
204
+
205
+ <IconButton
206
+ onclick={() => {
207
+ delete $entity.parent;
208
+ $entity = $entity;
209
+ }}
210
+ >
211
+ <Icon tag="svg" viewBox="0 0 24 24">
212
+ <path fill="currentColor" d={mdiMinus} />
213
+ </Icon>
214
+ </IconButton>
215
+ {/if}
216
+ </Paper>
217
+
218
+ <h6>Change Parent</h6>
219
+
220
+ <div class="solo-search-container solo-container">
221
+ <Paper class="solo-paper" elevation={1}>
222
+ <Icon class="solo-icon" tag="svg" viewBox="0 0 24 24">
223
+ <path fill="currentColor" d={mdiMagnify} />
224
+ </Icon>
225
+ <Input
226
+ bind:value={parentSearch}
227
+ onkeydown={parentSearchKeyDown}
228
+ placeholder="Parent Search"
229
+ class="solo-input"
230
+ />
231
+ </Paper>
232
+ <IconButton
233
+ onclick={searchParents}
234
+ disabled={parentSearch === ''}
235
+ class="solo-fab"
236
+ title="Search"
237
+ >
238
+ <Icon tag="svg" viewBox="0 0 24 24">
239
+ <path fill="currentColor" d={mdiArrowRight} />
240
+ </Icon>
241
+ </IconButton>
242
+ </div>
243
+
244
+ {#if parentsSearching}
245
+ <div
246
+ style="display: flex; justify-content: center; align-items: center;"
247
+ >
248
+ <CircularProgress style="height: 32px; width: 32px;" indeterminate />
249
+ </div>
250
+ {:else if parents != null}
251
+ <DataTable table$aria-label="Parent list" style="width: 100%;">
252
+ <Head>
253
+ <Row>
254
+ {#if !$clientConfig.emailUsernames}
255
+ <Cell>Groupname</Cell>
256
+ {/if}
257
+ {#if $clientConfig.userFields.includes('name')}
258
+ <Cell>Name</Cell>
259
+ {/if}
260
+ {#if $clientConfig.userFields.includes('email')}
261
+ <Cell>Email</Cell>
262
+ {/if}
263
+ <Cell>Enabled</Cell>
264
+ </Row>
265
+ </Head>
266
+ <Body>
267
+ <!-- Purposefully not making these links. -->
268
+ {#each parents as curEntity (curEntity.guid)}
269
+ <Row
270
+ onclick={() => ($entity.parent = curEntity)}
271
+ style="cursor: pointer;"
272
+ >
273
+ {#if !$clientConfig.emailUsernames}
274
+ <Cell>{curEntity.groupname}</Cell>
275
+ {/if}
276
+ {#if $clientConfig.userFields.includes('name')}
277
+ <Cell>{curEntity.name}</Cell>
278
+ {/if}
279
+ {#if $clientConfig.userFields.includes('email')}
280
+ <Cell>{curEntity.email}</Cell>
281
+ {/if}
282
+ <Cell>{curEntity.enabled ? 'Yes' : 'No'}</Cell>
283
+ </Row>
284
+ {/each}
285
+ </Body>
286
+ </DataTable>
287
+ {/if}
288
+ {/if}
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
+
428
+ {#if activeTab === 'Abilities'}
429
+ <h5 style="margin-top: 0;">Abilities</h5>
430
+
431
+ <List nonInteractive>
432
+ {#each $entity.abilities || [] as ability, index (ability)}
433
+ <Item>
434
+ <Text>
435
+ {ability}
436
+ </Text>
437
+ <Meta>
438
+ <IconButton
439
+ onclick={() => {
440
+ $entity.abilities?.splice(index, 1);
441
+ $entity = $entity;
442
+ }}
443
+ >
444
+ <Icon tag="svg" viewBox="0 0 24 24">
445
+ <path fill="currentColor" d={mdiMinus} />
446
+ </Icon>
447
+ </IconButton>
448
+ </Meta>
449
+ </Item>
450
+ {:else}
451
+ <Item>
452
+ <Text>No abilities</Text>
453
+ </Item>
454
+ {/each}
455
+ </List>
456
+
457
+ <h6>Add Ability</h6>
458
+
459
+ <div style="display: flex; align-items: center; flex-wrap: wrap;">
460
+ <Textfield
461
+ bind:value={ability}
462
+ label="Ability"
463
+ type="text"
464
+ style="width: 250px; max-width: 100%;"
465
+ onkeydown={abilityKeyDown}
466
+ />
467
+ <IconButton onclick={addAbility}>
468
+ <Icon tag="svg" viewBox="0 0 24 24">
469
+ <path fill="currentColor" d={mdiPlus} />
470
+ </Icon>
471
+ </IconButton>
472
+ </div>
473
+ {/if}
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
+
617
+ {#if failureMessage}
618
+ <div class="tilmeld-failure">
619
+ {failureMessage}
620
+ </div>
621
+ {/if}
622
+
623
+ <div style="margin-top: 36px;">
624
+ <Button variant="raised" onclick={saveEntity} disabled={saving}>
625
+ <Label>Save Group</Label>
626
+ </Button>
627
+ {#if $entity.guid}
628
+ <Button onclick={deleteEntity} disabled={saving}>
629
+ <Label>Delete</Label>
630
+ </Button>
631
+ {/if}
632
+ {#if success}
633
+ <span>Successfully saved!</span>
634
+ {/if}
635
+ </div>
636
+ </section>
637
+ {/if}
638
+
639
+ <script lang="ts">
640
+ import type { Writable } from 'svelte/store';
641
+ import { writable } from 'svelte/store';
642
+ import { getContext } from 'svelte';
643
+ import { link, pop, replace } from 'svelte-spa-router';
644
+ import type {
645
+ AdminGroupData,
646
+ ClientConfig,
647
+ CurrentUserData,
648
+ } from '@nymphjs/tilmeld-client';
649
+ import type {
650
+ Group as GroupClass,
651
+ User as UserClass,
652
+ AdminUserData,
653
+ } from '@nymphjs/tilmeld-client';
654
+ import queryParser from '@nymphjs/query-parser';
655
+ import {
656
+ mdiArrowLeft,
657
+ mdiArrowRight,
658
+ mdiMagnify,
659
+ mdiMinus,
660
+ mdiPlus,
661
+ } from '@mdi/js';
662
+ import CircularProgress from '@smui/circular-progress';
663
+ import Tab from '@smui/tab';
664
+ import TabBar from '@smui/tab-bar';
665
+ import LayoutGrid, { Cell as LayoutCell } from '@smui/layout-grid';
666
+ import FormField from '@smui/form-field';
667
+ import Checkbox from '@smui/checkbox';
668
+ import List, { Item, Text, Meta } from '@smui/list';
669
+ import Paper from '@smui/paper';
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';
679
+ import Textfield, { Input } from '@smui/textfield';
680
+ import HelperText from '@smui/textfield/helper-text';
681
+ import IconButton from '@smui/icon-button';
682
+ import Button from '@smui/button';
683
+ import LinearProgress from '@smui/linear-progress';
684
+ import { Icon, Label } from '@smui/common';
685
+
686
+ import { nymph, Group, User } from '../nymph.js';
687
+
688
+ let { params }: { params: { guid: string } } = $props();
689
+
690
+ let entity: Writable<GroupClass & AdminGroupData> = writable(
691
+ Group.factorySync(),
692
+ );
693
+ let activeTab: 'General' | 'Parent' | 'Children' | 'Abilities' | 'Users' =
694
+ $state('General');
695
+ let parentSearch = $state('');
696
+ let ability = $state('');
697
+ let avatar = $state('https://secure.gravatar.com/avatar/?d=mm&s=40');
698
+ let failureMessage: string | undefined = $state();
699
+ let groupnameTimer: number | undefined = undefined;
700
+ let groupnameVerified: boolean | undefined = $state();
701
+ let groupnameVerifiedMessage: string | undefined = $state();
702
+ let emailTimer: number | undefined = undefined;
703
+ let emailVerified: boolean | undefined = $state();
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);
733
+ let saving = $state(false);
734
+ let success: boolean | undefined = $state();
735
+ let loading = $state(true);
736
+
737
+ const clientConfig =
738
+ getContext<Writable<ClientConfig | undefined>>('clientConfigStore');
739
+ const user =
740
+ getContext<Writable<(UserClass & CurrentUserData) | null | undefined>>(
741
+ 'userStore',
742
+ );
743
+
744
+ $effect(() => {
745
+ if (params) {
746
+ handleGuidParam();
747
+ }
748
+ });
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
+
762
+ async function handleGuidParam() {
763
+ loading = true;
764
+ failureMessage = undefined;
765
+ try {
766
+ $entity =
767
+ params.guid === '+' || params.guid === ' ' || params.guid === '%20'
768
+ ? await Group.factory()
769
+ : await Group.factory(params.guid);
770
+ oldGroupname = $entity.groupname;
771
+ oldEmail = $entity.email;
772
+ await readyEntity();
773
+ } catch (e: any) {
774
+ failureMessage = e.message;
775
+ }
776
+ loading = false;
777
+ }
778
+
779
+ async function readyEntity() {
780
+ // Make sure all fields are defined.
781
+ if ($entity.enabled == null) {
782
+ $entity.enabled = false;
783
+ }
784
+ if ($entity.groupname == null) {
785
+ $entity.groupname = '';
786
+ }
787
+ if ($entity.email == null) {
788
+ $entity.email = '';
789
+ }
790
+ if ($entity.name == null) {
791
+ $entity.name = '';
792
+ }
793
+ if ($entity.avatar == null) {
794
+ $entity.avatar = '';
795
+ }
796
+ if ($entity.phone == null) {
797
+ $entity.phone = '';
798
+ }
799
+ if ($entity.defaultPrimary == null) {
800
+ $entity.defaultPrimary = false;
801
+ }
802
+ if ($entity.defaultSecondary == null) {
803
+ $entity.defaultSecondary = false;
804
+ }
805
+ if ($entity.unverifiedSecondary == null) {
806
+ $entity.unverifiedSecondary = false;
807
+ }
808
+ avatar = await $entity.$getAvatar();
809
+ await $entity.$wakeAll(1);
810
+ $entity = $entity;
811
+
812
+ await fillUsers();
813
+ await fillChildren();
814
+ }
815
+
816
+ let parentsSearching = $state(false);
817
+ let parents: (GroupClass & AdminGroupData)[] | undefined = $state();
818
+ async function searchParents() {
819
+ parentsSearching = true;
820
+ failureMessage = undefined;
821
+ if (parentSearch.trim() == '') {
822
+ return;
823
+ }
824
+ try {
825
+ const query = queryParser({
826
+ query: parentSearch,
827
+ entityClass: Group,
828
+ defaultFields: ['groupname', 'name', 'email'],
829
+ qrefMap: {
830
+ User: {
831
+ class: User,
832
+ defaultFields: ['username', 'name', 'email'],
833
+ },
834
+ Group: {
835
+ class: Group,
836
+ defaultFields: ['groupname', 'name', 'email'],
837
+ },
838
+ },
839
+ });
840
+ parents = (await nymph.getEntities(...query)).filter((group) => {
841
+ return !group.$is($entity) && !group.$is($entity.parent);
842
+ });
843
+ } catch (e: any) {
844
+ failureMessage = e?.message;
845
+ }
846
+ parentsSearching = false;
847
+ }
848
+ function parentSearchKeyDown(event: CustomEvent | KeyboardEvent) {
849
+ event = event as KeyboardEvent;
850
+ if (event.key === 'Enter') searchParents();
851
+ }
852
+
853
+ let oldGroupname: string | undefined = undefined;
854
+ $effect(() => {
855
+ if ($entity && $entity.groupname !== oldGroupname) {
856
+ if (groupnameTimer) {
857
+ clearTimeout(groupnameTimer);
858
+ }
859
+ groupnameTimer = setTimeout(async () => {
860
+ if ($entity.groupname === '') {
861
+ groupnameVerified = undefined;
862
+ groupnameVerifiedMessage = undefined;
863
+ return;
864
+ }
865
+ try {
866
+ const data = await $entity.$checkGroupname();
867
+ groupnameVerified = data.result;
868
+ groupnameVerifiedMessage = data.message;
869
+ } catch (e: any) {
870
+ groupnameVerified = false;
871
+ groupnameVerifiedMessage = e?.message;
872
+ }
873
+ }, 400);
874
+ oldGroupname = $entity.groupname;
875
+ }
876
+ });
877
+
878
+ let oldEmail: string | undefined = undefined;
879
+ $effect(() => {
880
+ if ($entity && $entity.email !== oldEmail) {
881
+ if (emailTimer) {
882
+ clearTimeout(emailTimer);
883
+ }
884
+ emailTimer = setTimeout(async () => {
885
+ if ($entity.email === '') {
886
+ emailVerified = undefined;
887
+ emailVerifiedMessage = undefined;
888
+ return;
889
+ }
890
+ try {
891
+ const data = await $entity.$checkEmail();
892
+ emailVerified = data.result;
893
+ emailVerifiedMessage = data.message;
894
+ } catch (e: any) {
895
+ emailVerified = false;
896
+ emailVerifiedMessage = e?.message;
897
+ }
898
+ }, 400);
899
+ oldEmail = $entity.email;
900
+ }
901
+ });
902
+
903
+ function addAbility() {
904
+ if (ability === '') {
905
+ return;
906
+ }
907
+ failureMessage = undefined;
908
+ if (ability === 'system/admin') {
909
+ failureMessage = "Groups aren't allowed to be system admins.";
910
+ return;
911
+ }
912
+ if (ability === 'tilmeld/admin') {
913
+ failureMessage = "Groups aren't allowed to be Tilmeld admins.";
914
+ return;
915
+ }
916
+ if (ability === 'tilmeld/switch') {
917
+ failureMessage = "Groups aren't allowed to have switch user ability.";
918
+ return;
919
+ }
920
+ $entity.abilities?.push(ability);
921
+ $entity = $entity;
922
+ ability = '';
923
+ }
924
+ function abilityKeyDown(event: CustomEvent | KeyboardEvent) {
925
+ event = event as KeyboardEvent;
926
+ if (event.key === 'Enter') addAbility();
927
+ }
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
+
1023
+ async function saveEntity() {
1024
+ saving = true;
1025
+ failureMessage = undefined;
1026
+ const newEntity = $entity.guid == null;
1027
+ try {
1028
+ await $entity.$save();
1029
+ await readyEntity();
1030
+ success = true;
1031
+ if (newEntity) {
1032
+ replace(`/groups/edit/${encodeURIComponent($entity.guid || '')}`);
1033
+ }
1034
+ setTimeout(() => {
1035
+ success = undefined;
1036
+ }, 1000);
1037
+ } catch (e: any) {
1038
+ console.log('error:', e);
1039
+ failureMessage = e?.message;
1040
+ }
1041
+ saving = false;
1042
+ }
1043
+
1044
+ async function deleteEntity() {
1045
+ failureMessage = undefined;
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
+ ) {
1051
+ saving = true;
1052
+ try {
1053
+ await $entity.$delete();
1054
+ pop();
1055
+ } catch (e: any) {
1056
+ failureMessage = e?.message;
1057
+ }
1058
+ saving = false;
1059
+ }
1060
+ }
1061
+ </script>