@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.
- package/CHANGELOG.md +524 -0
- package/README.md +9 -6
- package/app/index.ts +9 -3
- package/app/src/App.svelte +114 -77
- package/app/src/nymph.ts +1 -1
- package/app/src/routes/GroupEdit.svelte +1061 -0
- package/app/src/routes/Groups.svelte +203 -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 +1193 -0
- package/app/src/routes/Users.svelte +203 -0
- package/app/theme/_smui-theme.scss +5 -5
- package/dist/app/index.css +105 -0
- package/dist/app/index.js +48610 -3
- package/dist/app/smui.css +1 -29
- package/dist/index.d.ts +5 -3
- package/dist/index.js +22 -22
- package/dist/index.js.map +1 -1
- package/jest.config.js +31 -5
- package/package.json +72 -61
- package/rollup.config.mjs +36 -0
- package/src/index.ts +29 -23
- package/static/index.html +4 -1
- package/test.mjs +13 -12
- package/tsconfig.json +13 -1
- package/tsconfig.server.json +6 -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,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>
|