@nymphjs/tilmeld-setup 1.0.0-beta.11 → 1.0.0-beta.111
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 +442 -0
- package/README.md +6 -5
- package/app/index.ts +9 -3
- package/app/src/App.svelte +143 -71
- package/app/src/nymph.ts +1 -1
- package/app/src/routes/GroupEdit.svelte +638 -0
- package/app/src/routes/Groups.svelte +201 -0
- package/app/src/{Intro.svelte → routes/Intro.svelte} +14 -1
- package/app/src/routes/NotFound.svelte +9 -0
- package/app/src/routes/UserEdit.svelte +1202 -0
- package/app/src/routes/Users.svelte +201 -0
- package/dist/app/index.css +105 -0
- package/dist/app/index.d.ts +1 -0
- package/dist/app/index.js +41288 -3
- package/dist/app/smui.css +1 -29
- package/dist/app/src/nymph.d.ts +6 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +19 -18
- package/dist/index.js.map +1 -1
- package/jest.config.js +11 -2
- package/package.json +58 -54
- package/rollup.config.mjs +36 -0
- package/src/index.ts +28 -21
- package/src/type/locutus.d.ts +3 -0
- package/static/index.html +4 -1
- package/test.mjs +13 -12
- package/tsconfig.json +11 -1
- package/tsconfig.server.json +5 -3
- package/typedoc.json +5 -0
- package/app/src/GroupEdit.svelte +0 -570
- package/app/src/Groups.svelte +0 -159
- package/app/src/UserEdit.svelte +0 -902
- package/app/src/Users.svelte +0 -159
- package/dist/app/index.js.LICENSE.txt +0 -114
- package/dist/app/index.js.map +0 -1
- package/webpack.config.js +0 -38
|
@@ -0,0 +1,638 @@
|
|
|
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
|
|
10
|
+
title="Back"
|
|
11
|
+
onclick={() => router.navigate('', { historyAPIMethod: 'back' })}
|
|
12
|
+
>
|
|
13
|
+
<Icon tag="svg" viewBox="0 0 24 24">
|
|
14
|
+
<path fill="currentColor" d={mdiArrowLeft} />
|
|
15
|
+
</Icon>
|
|
16
|
+
</IconButton>
|
|
17
|
+
<h2 style="margin: 0px 12px 0px;" class="mdc-typography--headline5">
|
|
18
|
+
Editing {$entity.guid ? ($entity.name ?? $entity.groupname) : 'New Group'}
|
|
19
|
+
</h2>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
{#if $entity.user}
|
|
23
|
+
{#await $entity.user.$wake() then _user}
|
|
24
|
+
<div style="padding: 12px;" class="mdc-typography--subtitle1">
|
|
25
|
+
Generated primary group for <a
|
|
26
|
+
href="#/users/edit/{encodeURIComponent($entity.user.guid || '')}"
|
|
27
|
+
>{$clientConfig.userFields.includes('name')
|
|
28
|
+
? $entity.user.name + ' (' + $entity.user.username + ')'
|
|
29
|
+
: $entity.user.username}</a
|
|
30
|
+
>
|
|
31
|
+
</div>
|
|
32
|
+
{/await}
|
|
33
|
+
{/if}
|
|
34
|
+
|
|
35
|
+
<TabBar tabs={['General', 'Parent', 'Abilities']} bind:active={activeTab}>
|
|
36
|
+
{#snippet tab(tab)}
|
|
37
|
+
<Tab {tab}>
|
|
38
|
+
<Label>{tab}</Label>
|
|
39
|
+
</Tab>
|
|
40
|
+
{/snippet}
|
|
41
|
+
</TabBar>
|
|
42
|
+
|
|
43
|
+
<section>
|
|
44
|
+
{#if activeTab === 'General'}
|
|
45
|
+
<LayoutGrid style="padding: 0;">
|
|
46
|
+
{#if $entity.user != null}
|
|
47
|
+
<LayoutCell span={12}>
|
|
48
|
+
Some of these fields are not editable, since this group inherits the
|
|
49
|
+
values from its user.
|
|
50
|
+
</LayoutCell>
|
|
51
|
+
{/if}
|
|
52
|
+
<LayoutCell span={4}>
|
|
53
|
+
<div class="mdc-typography--headline6">GUID</div>
|
|
54
|
+
<code>{$entity.guid}</code>
|
|
55
|
+
</LayoutCell>
|
|
56
|
+
<LayoutCell span={4}>
|
|
57
|
+
<FormField>
|
|
58
|
+
<Checkbox bind:checked={$entity.enabled} />
|
|
59
|
+
{#snippet label()}
|
|
60
|
+
Enabled (Able to give abilities)
|
|
61
|
+
{/snippet}
|
|
62
|
+
</FormField>
|
|
63
|
+
</LayoutCell>
|
|
64
|
+
<LayoutCell span={4} style="text-align: end;">
|
|
65
|
+
<a href="https://en.gravatar.com/" target="_blank" rel="noreferrer">
|
|
66
|
+
<img src={avatar} alt="Avatar" title="Avatar by Gravatar" />
|
|
67
|
+
</a>
|
|
68
|
+
</LayoutCell>
|
|
69
|
+
{#if !$clientConfig.emailUsernames}
|
|
70
|
+
<LayoutCell
|
|
71
|
+
span={$clientConfig.userFields.includes('email') ? 6 : 12}
|
|
72
|
+
>
|
|
73
|
+
<Textfield
|
|
74
|
+
bind:value={$entity.groupname}
|
|
75
|
+
label="Groupname"
|
|
76
|
+
type="text"
|
|
77
|
+
style="width: 100%;"
|
|
78
|
+
helperLine$style="width: 100%;"
|
|
79
|
+
invalid={groupnameVerified === false}
|
|
80
|
+
input$autocomplete="off"
|
|
81
|
+
input$autocapitalize="off"
|
|
82
|
+
input$spellcheck="false"
|
|
83
|
+
disabled={$entity.user != null}
|
|
84
|
+
>
|
|
85
|
+
{#snippet helper()}
|
|
86
|
+
<HelperText persistent>
|
|
87
|
+
{groupnameVerifiedMessage ?? ''}
|
|
88
|
+
</HelperText>
|
|
89
|
+
{/snippet}
|
|
90
|
+
</Textfield>
|
|
91
|
+
</LayoutCell>
|
|
92
|
+
{/if}
|
|
93
|
+
{#if $clientConfig.userFields.includes('email')}
|
|
94
|
+
<LayoutCell span={$clientConfig.emailUsernames ? 12 : 6}>
|
|
95
|
+
<Textfield
|
|
96
|
+
bind:value={$entity.email}
|
|
97
|
+
label="Email"
|
|
98
|
+
type="email"
|
|
99
|
+
style="width: 100%;"
|
|
100
|
+
helperLine$style="width: 100%;"
|
|
101
|
+
invalid={emailVerified === false}
|
|
102
|
+
input$autocomplete="off"
|
|
103
|
+
input$autocapitalize="off"
|
|
104
|
+
input$spellcheck="false"
|
|
105
|
+
disabled={$entity.user != null}
|
|
106
|
+
>
|
|
107
|
+
{#snippet helper()}
|
|
108
|
+
<HelperText persistent>
|
|
109
|
+
{emailVerifiedMessage ?? ''}
|
|
110
|
+
</HelperText>
|
|
111
|
+
{/snippet}
|
|
112
|
+
</Textfield>
|
|
113
|
+
</LayoutCell>
|
|
114
|
+
{/if}
|
|
115
|
+
{#if $clientConfig.userFields.includes('name')}
|
|
116
|
+
<LayoutCell span={12}>
|
|
117
|
+
<Textfield
|
|
118
|
+
bind:value={$entity.name}
|
|
119
|
+
label="Display Name"
|
|
120
|
+
type="text"
|
|
121
|
+
style="width: 100%;"
|
|
122
|
+
input$autocomplete="off"
|
|
123
|
+
disabled={$entity.user != null}
|
|
124
|
+
/>
|
|
125
|
+
</LayoutCell>
|
|
126
|
+
{/if}
|
|
127
|
+
<LayoutCell span={$clientConfig.userFields.includes('phone') ? 8 : 12}>
|
|
128
|
+
<Textfield
|
|
129
|
+
bind:value={$entity.avatar}
|
|
130
|
+
label="Avatar"
|
|
131
|
+
type="text"
|
|
132
|
+
style="width: 100%;"
|
|
133
|
+
input$autocomplete="off"
|
|
134
|
+
disabled={$entity.user != null}
|
|
135
|
+
/>
|
|
136
|
+
</LayoutCell>
|
|
137
|
+
{#if $clientConfig.userFields.includes('phone')}
|
|
138
|
+
<LayoutCell span={4}>
|
|
139
|
+
<Textfield
|
|
140
|
+
bind:value={$entity.phone}
|
|
141
|
+
label="Phone"
|
|
142
|
+
type="tel"
|
|
143
|
+
style="width: 100%;"
|
|
144
|
+
input$autocomplete="off"
|
|
145
|
+
disabled={$entity.user != null}
|
|
146
|
+
/>
|
|
147
|
+
</LayoutCell>
|
|
148
|
+
{/if}
|
|
149
|
+
<LayoutCell span={12}>
|
|
150
|
+
<FormField>
|
|
151
|
+
<Checkbox bind:checked={$entity.defaultPrimary} />
|
|
152
|
+
{#snippet label()}
|
|
153
|
+
Default primary group for newly registered users. <small
|
|
154
|
+
class="form-text text-muted"
|
|
155
|
+
>Setting this will unset any current default primary group.</small
|
|
156
|
+
>
|
|
157
|
+
{/snippet}
|
|
158
|
+
</FormField>
|
|
159
|
+
</LayoutCell>
|
|
160
|
+
<LayoutCell span={12}>
|
|
161
|
+
<FormField>
|
|
162
|
+
<Checkbox bind:checked={$entity.defaultSecondary} />
|
|
163
|
+
{#snippet label()}
|
|
164
|
+
Default secondary group for newly registered{$clientConfig?.userFields.includes(
|
|
165
|
+
'email',
|
|
166
|
+
) &&
|
|
167
|
+
$clientConfig.verifyEmail &&
|
|
168
|
+
$clientConfig.unverifiedAccess
|
|
169
|
+
? ', verified'
|
|
170
|
+
: ''} users.
|
|
171
|
+
{/snippet}
|
|
172
|
+
</FormField>
|
|
173
|
+
</LayoutCell>
|
|
174
|
+
{#if $clientConfig.userFields.includes('email') && $clientConfig.verifyEmail && $clientConfig.unverifiedAccess}
|
|
175
|
+
<LayoutCell span={12}>
|
|
176
|
+
<FormField>
|
|
177
|
+
<Checkbox bind:checked={$entity.unverifiedSecondary} />
|
|
178
|
+
{#snippet label()}
|
|
179
|
+
Default secondary group for newly registered, unverified users.
|
|
180
|
+
{/snippet}
|
|
181
|
+
</FormField>
|
|
182
|
+
</LayoutCell>
|
|
183
|
+
{/if}
|
|
184
|
+
</LayoutGrid>
|
|
185
|
+
{/if}
|
|
186
|
+
|
|
187
|
+
{#if activeTab === 'Parent'}
|
|
188
|
+
<h5 style="margin-top: 0;">Parent</h5>
|
|
189
|
+
|
|
190
|
+
<Paper
|
|
191
|
+
style="display: flex; justify-content: space-between; align-items: center;"
|
|
192
|
+
>
|
|
193
|
+
{#if !$entity.parent}
|
|
194
|
+
No parent
|
|
195
|
+
{:else}
|
|
196
|
+
<a
|
|
197
|
+
href="#/groups/edit/{encodeURIComponent($entity.parent.guid || '')}"
|
|
198
|
+
>{$clientConfig.userFields.includes('name')
|
|
199
|
+
? $entity.parent.name + ' (' + $entity.parent.groupname + ')'
|
|
200
|
+
: $entity.parent.groupname}</a
|
|
201
|
+
>
|
|
202
|
+
|
|
203
|
+
<IconButton
|
|
204
|
+
onclick={() => {
|
|
205
|
+
delete $entity.parent;
|
|
206
|
+
$entity = $entity;
|
|
207
|
+
}}
|
|
208
|
+
>
|
|
209
|
+
<Icon tag="svg" viewBox="0 0 24 24">
|
|
210
|
+
<path fill="currentColor" d={mdiMinus} />
|
|
211
|
+
</Icon>
|
|
212
|
+
</IconButton>
|
|
213
|
+
{/if}
|
|
214
|
+
</Paper>
|
|
215
|
+
|
|
216
|
+
<h6>Change Parent</h6>
|
|
217
|
+
|
|
218
|
+
<div class="solo-search-container solo-container">
|
|
219
|
+
<Paper class="solo-paper" elevation={1}>
|
|
220
|
+
<Icon class="solo-icon" tag="svg" viewBox="0 0 24 24">
|
|
221
|
+
<path fill="currentColor" d={mdiMagnify} />
|
|
222
|
+
</Icon>
|
|
223
|
+
<Input
|
|
224
|
+
bind:value={parentSearch}
|
|
225
|
+
onkeydown={parentSearchKeyDown}
|
|
226
|
+
placeholder="Parent Search"
|
|
227
|
+
class="solo-input"
|
|
228
|
+
/>
|
|
229
|
+
</Paper>
|
|
230
|
+
<IconButton
|
|
231
|
+
onclick={searchParents}
|
|
232
|
+
disabled={parentSearch === ''}
|
|
233
|
+
class="solo-fab"
|
|
234
|
+
title="Search"
|
|
235
|
+
>
|
|
236
|
+
<Icon tag="svg" viewBox="0 0 24 24">
|
|
237
|
+
<path fill="currentColor" d={mdiArrowRight} />
|
|
238
|
+
</Icon>
|
|
239
|
+
</IconButton>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
{#if parentsSearching}
|
|
243
|
+
<div
|
|
244
|
+
style="display: flex; justify-content: center; align-items: center;"
|
|
245
|
+
>
|
|
246
|
+
<CircularProgress style="height: 32px; width: 32px;" indeterminate />
|
|
247
|
+
</div>
|
|
248
|
+
{:else if parents != null}
|
|
249
|
+
<DataTable table$aria-label="Parent list" style="width: 100%;">
|
|
250
|
+
<Head>
|
|
251
|
+
<Row>
|
|
252
|
+
{#if !$clientConfig.emailUsernames}
|
|
253
|
+
<Cell>Groupname</Cell>
|
|
254
|
+
{/if}
|
|
255
|
+
{#if $clientConfig.userFields.includes('name')}
|
|
256
|
+
<Cell>Name</Cell>
|
|
257
|
+
{/if}
|
|
258
|
+
{#if $clientConfig.userFields.includes('email')}
|
|
259
|
+
<Cell>Email</Cell>
|
|
260
|
+
{/if}
|
|
261
|
+
<Cell>Enabled</Cell>
|
|
262
|
+
</Row>
|
|
263
|
+
</Head>
|
|
264
|
+
<Body>
|
|
265
|
+
<!-- Purposefully not making these links. -->
|
|
266
|
+
{#each parents as curEntity (curEntity.guid)}
|
|
267
|
+
<Row
|
|
268
|
+
onclick={() => ($entity.parent = curEntity)}
|
|
269
|
+
style="cursor: pointer;"
|
|
270
|
+
>
|
|
271
|
+
{#if !$clientConfig.emailUsernames}
|
|
272
|
+
<Cell>{curEntity.groupname}</Cell>
|
|
273
|
+
{/if}
|
|
274
|
+
{#if $clientConfig.userFields.includes('name')}
|
|
275
|
+
<Cell>{curEntity.name}</Cell>
|
|
276
|
+
{/if}
|
|
277
|
+
{#if $clientConfig.userFields.includes('email')}
|
|
278
|
+
<Cell>{curEntity.email}</Cell>
|
|
279
|
+
{/if}
|
|
280
|
+
<Cell>{curEntity.enabled ? 'Yes' : 'No'}</Cell>
|
|
281
|
+
</Row>
|
|
282
|
+
{/each}
|
|
283
|
+
</Body>
|
|
284
|
+
</DataTable>
|
|
285
|
+
{/if}
|
|
286
|
+
{/if}
|
|
287
|
+
|
|
288
|
+
{#if activeTab === 'Abilities'}
|
|
289
|
+
<h5 style="margin-top: 0;">Abilities</h5>
|
|
290
|
+
|
|
291
|
+
<List nonInteractive>
|
|
292
|
+
{#each $entity.abilities || [] as ability, index (ability)}
|
|
293
|
+
<Item>
|
|
294
|
+
<Text>
|
|
295
|
+
{ability}
|
|
296
|
+
</Text>
|
|
297
|
+
<Meta>
|
|
298
|
+
<IconButton
|
|
299
|
+
onclick={() => {
|
|
300
|
+
$entity.abilities?.splice(index, 1);
|
|
301
|
+
$entity = $entity;
|
|
302
|
+
}}
|
|
303
|
+
>
|
|
304
|
+
<Icon tag="svg" viewBox="0 0 24 24">
|
|
305
|
+
<path fill="currentColor" d={mdiMinus} />
|
|
306
|
+
</Icon>
|
|
307
|
+
</IconButton>
|
|
308
|
+
</Meta>
|
|
309
|
+
</Item>
|
|
310
|
+
{:else}
|
|
311
|
+
<Item>
|
|
312
|
+
<Text>No abilities</Text>
|
|
313
|
+
</Item>
|
|
314
|
+
{/each}
|
|
315
|
+
</List>
|
|
316
|
+
|
|
317
|
+
<h6>Add Ability</h6>
|
|
318
|
+
|
|
319
|
+
<div style="display: flex; align-items: center; flex-wrap: wrap;">
|
|
320
|
+
<Textfield
|
|
321
|
+
bind:value={ability}
|
|
322
|
+
label="Ability"
|
|
323
|
+
type="text"
|
|
324
|
+
style="width: 250px; max-width: 100%;"
|
|
325
|
+
onkeydown={abilityKeyDown}
|
|
326
|
+
/>
|
|
327
|
+
<IconButton onclick={addAbility}>
|
|
328
|
+
<Icon tag="svg" viewBox="0 0 24 24">
|
|
329
|
+
<path fill="currentColor" d={mdiPlus} />
|
|
330
|
+
</Icon>
|
|
331
|
+
</IconButton>
|
|
332
|
+
</div>
|
|
333
|
+
{/if}
|
|
334
|
+
|
|
335
|
+
{#if failureMessage}
|
|
336
|
+
<div class="tilmeld-failure">
|
|
337
|
+
{failureMessage}
|
|
338
|
+
</div>
|
|
339
|
+
{/if}
|
|
340
|
+
|
|
341
|
+
<div style="margin-top: 36px;">
|
|
342
|
+
<Button variant="raised" onclick={saveEntity} disabled={saving}>
|
|
343
|
+
<Label>Save Group</Label>
|
|
344
|
+
</Button>
|
|
345
|
+
{#if $entity.guid}
|
|
346
|
+
<Button onclick={deleteEntity} disabled={saving}>
|
|
347
|
+
<Label>Delete</Label>
|
|
348
|
+
</Button>
|
|
349
|
+
{/if}
|
|
350
|
+
{#if success}
|
|
351
|
+
<span>Successfully saved!</span>
|
|
352
|
+
{/if}
|
|
353
|
+
</div>
|
|
354
|
+
</section>
|
|
355
|
+
{/if}
|
|
356
|
+
|
|
357
|
+
<script lang="ts">
|
|
358
|
+
import type { Writable } from 'svelte/store';
|
|
359
|
+
import { writable } from 'svelte/store';
|
|
360
|
+
import type Navigo from 'navigo';
|
|
361
|
+
import type {
|
|
362
|
+
AdminGroupData,
|
|
363
|
+
ClientConfig,
|
|
364
|
+
CurrentUserData,
|
|
365
|
+
} from '@nymphjs/tilmeld-client';
|
|
366
|
+
import type {
|
|
367
|
+
Group as GroupClass,
|
|
368
|
+
User as UserClass,
|
|
369
|
+
} from '@nymphjs/tilmeld-client';
|
|
370
|
+
import queryParser from '@nymphjs/query-parser';
|
|
371
|
+
import {
|
|
372
|
+
mdiArrowLeft,
|
|
373
|
+
mdiArrowRight,
|
|
374
|
+
mdiMagnify,
|
|
375
|
+
mdiMinus,
|
|
376
|
+
mdiPlus,
|
|
377
|
+
} from '@mdi/js';
|
|
378
|
+
import CircularProgress from '@smui/circular-progress';
|
|
379
|
+
import Tab from '@smui/tab';
|
|
380
|
+
import TabBar from '@smui/tab-bar';
|
|
381
|
+
import LayoutGrid, { Cell as LayoutCell } from '@smui/layout-grid';
|
|
382
|
+
import FormField from '@smui/form-field';
|
|
383
|
+
import Checkbox from '@smui/checkbox';
|
|
384
|
+
import List, { Item, Text, Meta } from '@smui/list';
|
|
385
|
+
import Paper from '@smui/paper';
|
|
386
|
+
import DataTable, { Head, Body, Row, Cell } from '@smui/data-table';
|
|
387
|
+
import Textfield, { Input } from '@smui/textfield';
|
|
388
|
+
import HelperText from '@smui/textfield/helper-text';
|
|
389
|
+
import IconButton from '@smui/icon-button';
|
|
390
|
+
import Button from '@smui/button';
|
|
391
|
+
import { Icon, Label } from '@smui/common';
|
|
392
|
+
|
|
393
|
+
import { nymph, Group, User } from '../nymph';
|
|
394
|
+
|
|
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();
|
|
406
|
+
|
|
407
|
+
let entity: Writable<GroupClass & AdminGroupData> = writable(
|
|
408
|
+
Group.factorySync(),
|
|
409
|
+
);
|
|
410
|
+
let activeTab: 'General' | 'Parent' | 'Abilities' = $state('General');
|
|
411
|
+
let parentSearch = $state('');
|
|
412
|
+
let ability = $state('');
|
|
413
|
+
let avatar = $state('https://secure.gravatar.com/avatar/?d=mm&s=40');
|
|
414
|
+
let failureMessage: string | undefined = $state();
|
|
415
|
+
let groupnameTimer: NodeJS.Timeout | undefined = undefined;
|
|
416
|
+
let groupnameVerified: boolean | undefined = $state();
|
|
417
|
+
let groupnameVerifiedMessage: string | undefined = $state();
|
|
418
|
+
let emailTimer: NodeJS.Timeout | undefined = undefined;
|
|
419
|
+
let emailVerified: boolean | undefined = $state();
|
|
420
|
+
let emailVerifiedMessage: string | undefined = $state();
|
|
421
|
+
let saving = $state(false);
|
|
422
|
+
let success: boolean | undefined = $state();
|
|
423
|
+
let loading = $state(true);
|
|
424
|
+
|
|
425
|
+
$effect(() => {
|
|
426
|
+
if (params) {
|
|
427
|
+
handleGuidParam();
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
async function handleGuidParam() {
|
|
432
|
+
loading = true;
|
|
433
|
+
failureMessage = undefined;
|
|
434
|
+
try {
|
|
435
|
+
$entity =
|
|
436
|
+
params.guid === '+' || params.guid === ' ' || params.guid === '%20'
|
|
437
|
+
? await Group.factory()
|
|
438
|
+
: await Group.factory(params.guid);
|
|
439
|
+
oldGroupname = $entity.groupname;
|
|
440
|
+
oldEmail = $entity.email;
|
|
441
|
+
await readyEntity();
|
|
442
|
+
} catch (e: any) {
|
|
443
|
+
failureMessage = e.message;
|
|
444
|
+
}
|
|
445
|
+
loading = false;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function readyEntity() {
|
|
449
|
+
// Make sure all fields are defined.
|
|
450
|
+
if ($entity.enabled == null) {
|
|
451
|
+
$entity.enabled = false;
|
|
452
|
+
}
|
|
453
|
+
if ($entity.groupname == null) {
|
|
454
|
+
$entity.groupname = '';
|
|
455
|
+
}
|
|
456
|
+
if ($entity.email == null) {
|
|
457
|
+
$entity.email = '';
|
|
458
|
+
}
|
|
459
|
+
if ($entity.name == null) {
|
|
460
|
+
$entity.name = '';
|
|
461
|
+
}
|
|
462
|
+
if ($entity.avatar == null) {
|
|
463
|
+
$entity.avatar = '';
|
|
464
|
+
}
|
|
465
|
+
if ($entity.phone == null) {
|
|
466
|
+
$entity.phone = '';
|
|
467
|
+
}
|
|
468
|
+
if ($entity.defaultPrimary == null) {
|
|
469
|
+
$entity.defaultPrimary = false;
|
|
470
|
+
}
|
|
471
|
+
if ($entity.defaultSecondary == null) {
|
|
472
|
+
$entity.defaultSecondary = false;
|
|
473
|
+
}
|
|
474
|
+
if ($entity.unverifiedSecondary == null) {
|
|
475
|
+
$entity.unverifiedSecondary = false;
|
|
476
|
+
}
|
|
477
|
+
avatar = await $entity.$getAvatar();
|
|
478
|
+
await $entity.$wakeAll(1);
|
|
479
|
+
$entity = $entity;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let parentsSearching = $state(false);
|
|
483
|
+
let parents: (GroupClass & AdminGroupData)[] | undefined = $state();
|
|
484
|
+
async function searchParents() {
|
|
485
|
+
parentsSearching = true;
|
|
486
|
+
failureMessage = undefined;
|
|
487
|
+
if (parentSearch.trim() == '') {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
const query = queryParser({
|
|
492
|
+
query: parentSearch,
|
|
493
|
+
entityClass: Group,
|
|
494
|
+
defaultFields: ['groupname', 'name', 'email'],
|
|
495
|
+
qrefMap: {
|
|
496
|
+
User: {
|
|
497
|
+
class: User,
|
|
498
|
+
defaultFields: ['username', 'name', 'email'],
|
|
499
|
+
},
|
|
500
|
+
Group: {
|
|
501
|
+
class: Group,
|
|
502
|
+
defaultFields: ['groupname', 'name', 'email'],
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
parents = (await nymph.getEntities(...query)).filter((group) => {
|
|
507
|
+
return !group.$is($entity) && !group.$is($entity.parent);
|
|
508
|
+
});
|
|
509
|
+
} catch (e: any) {
|
|
510
|
+
failureMessage = e?.message;
|
|
511
|
+
}
|
|
512
|
+
parentsSearching = false;
|
|
513
|
+
}
|
|
514
|
+
function parentSearchKeyDown(event: CustomEvent | KeyboardEvent) {
|
|
515
|
+
event = event as KeyboardEvent;
|
|
516
|
+
if (event.key === 'Enter') searchParents();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
let oldGroupname: string | undefined = undefined;
|
|
520
|
+
$effect(() => {
|
|
521
|
+
if ($entity && $entity.groupname !== oldGroupname) {
|
|
522
|
+
if (groupnameTimer) {
|
|
523
|
+
clearTimeout(groupnameTimer);
|
|
524
|
+
}
|
|
525
|
+
groupnameTimer = setTimeout(async () => {
|
|
526
|
+
if ($entity.groupname === '') {
|
|
527
|
+
groupnameVerified = undefined;
|
|
528
|
+
groupnameVerifiedMessage = undefined;
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
const data = await $entity.$checkGroupname();
|
|
533
|
+
groupnameVerified = data.result;
|
|
534
|
+
groupnameVerifiedMessage = data.message;
|
|
535
|
+
} catch (e: any) {
|
|
536
|
+
groupnameVerified = false;
|
|
537
|
+
groupnameVerifiedMessage = e?.message;
|
|
538
|
+
}
|
|
539
|
+
}, 400);
|
|
540
|
+
oldGroupname = $entity.groupname;
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
let oldEmail: string | undefined = undefined;
|
|
545
|
+
$effect(() => {
|
|
546
|
+
if ($entity && $entity.email !== oldEmail) {
|
|
547
|
+
if (emailTimer) {
|
|
548
|
+
clearTimeout(emailTimer);
|
|
549
|
+
}
|
|
550
|
+
emailTimer = setTimeout(async () => {
|
|
551
|
+
if ($entity.email === '') {
|
|
552
|
+
emailVerified = undefined;
|
|
553
|
+
emailVerifiedMessage = undefined;
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
const data = await $entity.$checkEmail();
|
|
558
|
+
emailVerified = data.result;
|
|
559
|
+
emailVerifiedMessage = data.message;
|
|
560
|
+
} catch (e: any) {
|
|
561
|
+
emailVerified = false;
|
|
562
|
+
emailVerifiedMessage = e?.message;
|
|
563
|
+
}
|
|
564
|
+
}, 400);
|
|
565
|
+
oldEmail = $entity.email;
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
function addAbility() {
|
|
570
|
+
if (ability === '') {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
failureMessage = undefined;
|
|
574
|
+
if (ability === 'system/admin') {
|
|
575
|
+
failureMessage = "Groups aren't allowed to be system admins.";
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (ability === 'tilmeld/admin') {
|
|
579
|
+
failureMessage = "Groups aren't allowed to be Tilmeld admins.";
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (ability === 'tilmeld/switch') {
|
|
583
|
+
failureMessage = "Groups aren't allowed to have switch user ability.";
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
$entity.abilities?.push(ability);
|
|
587
|
+
$entity = $entity;
|
|
588
|
+
ability = '';
|
|
589
|
+
}
|
|
590
|
+
function abilityKeyDown(event: CustomEvent | KeyboardEvent) {
|
|
591
|
+
event = event as KeyboardEvent;
|
|
592
|
+
if (event.key === 'Enter') addAbility();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function saveEntity() {
|
|
596
|
+
saving = true;
|
|
597
|
+
failureMessage = undefined;
|
|
598
|
+
const newEntity = $entity.guid == null;
|
|
599
|
+
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.';
|
|
614
|
+
}
|
|
615
|
+
} catch (e: any) {
|
|
616
|
+
console.log('error:', e);
|
|
617
|
+
failureMessage = e?.message;
|
|
618
|
+
}
|
|
619
|
+
saving = false;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async function deleteEntity() {
|
|
623
|
+
failureMessage = undefined;
|
|
624
|
+
if (confirm('Are you sure you want to delete this?')) {
|
|
625
|
+
saving = true;
|
|
626
|
+
try {
|
|
627
|
+
if (await $entity.$delete()) {
|
|
628
|
+
router.navigate('', { historyAPIMethod: 'back' });
|
|
629
|
+
} else {
|
|
630
|
+
failureMessage = 'An error occurred.';
|
|
631
|
+
}
|
|
632
|
+
} catch (e: any) {
|
|
633
|
+
failureMessage = e?.message;
|
|
634
|
+
}
|
|
635
|
+
saving = false;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
</script>
|