Skip to content

Commit 0c96290

Browse files
committed
chore: reduce duplicate code of RoleTreeMultiSelect
1 parent 890ce44 commit 0c96290

File tree

5 files changed

+121
-222
lines changed

5 files changed

+121
-222
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<script lang="ts">
2+
import type { RoleWithChildren } from '$lib/roles.ts'
3+
import { SvelteSet } from 'svelte/reactivity'
4+
import RoleTreeCheckbox from './RoleTreeCheckbox.svelte'
5+
6+
let {
7+
role,
8+
selectedValues,
9+
depth = 0,
10+
onToggle
11+
}: {
12+
role: RoleWithChildren;
13+
selectedValues: SvelteSet<string>;
14+
depth?: number;
15+
onToggle: (id: string, checked: boolean) => void
16+
} = $props()
17+
</script>
18+
<div style="padding-left: {depth * 1.5}rem;">
19+
<label
20+
class="flex h-12 cursor-pointer items-center gap-3 rounded px-4 ring-1 ring-gray-200 transition hover:bg-gray-50 has-checked:ring-2 has-checked:ring-blue-500"
21+
>
22+
<input
23+
type="checkbox"
24+
name="roles"
25+
value={role.id}
26+
class="h-4 w-4 border-gray-300 text-blue-600 focus:ring-offset-0 focus:outline-none"
27+
28+
checked={selectedValues.has(role.id)}
29+
onchange={(e) => onToggle(role.id, e.currentTarget.checked)}
30+
/>
31+
<span class="text-sm">
32+
{#if depth > 0}
33+
<span class="mr-1 text-gray-400">└</span>
34+
{/if}
35+
{role.name}
36+
</span>
37+
</label>
38+
</div>
39+
40+
{#if role.children.length > 0}
41+
<div class="mt-2 flex flex-col gap-2">
42+
{#each role.children as child (child.id)}
43+
<RoleTreeCheckbox role={child} depth={depth + 1} {selectedValues} {onToggle} />
44+
{/each}
45+
</div>
46+
{/if}
Lines changed: 71 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,77 @@
11
<script lang="ts">
2+
import * as m from '$lib/paraglide/messages'
23
import type { RoleWithChildren } from '$lib/roles.ts'
3-
import { SvelteSet } from 'svelte/reactivity'
4-
import RoleTreeMultiSelect from './RoleTreeMultiSelect.svelte'
4+
import { SvelteMap, SvelteSet } from 'svelte/reactivity'
5+
import RoleTreeCheckbox from '$lib/components/RoleTreeCheckbox.svelte'
6+
7+
interface Props {
8+
value?: string[],
9+
roleTree: RoleWithChildren[];
10+
}
511
612
let {
7-
role,
8-
selectedValues,
9-
depth = 0,
10-
onToggle
11-
}: {
12-
role: RoleWithChildren;
13-
selectedValues: SvelteSet<string>;
14-
depth?: number;
15-
onToggle: (id: string, checked: boolean) => void
16-
} = $props()
13+
value = [],
14+
roleTree
15+
}: Props = $props()
16+
17+
// Source of truth for roles. Callbacks in the RoleTreeCheckboxes will update this.
18+
let roles = new SvelteSet(value)
19+
20+
let maps = $derived.by(() => {
21+
const parent = new SvelteMap<string, string>()
22+
const children = new SvelteMap<string, string[]>()
23+
24+
function traverse(nodes: RoleWithChildren[], parentId: string | null = null) {
25+
for (const node of nodes) {
26+
if (parentId) parent.set(node.id, parentId)
27+
children.set(node.id, node.children.map((c) => c.id))
28+
if (node.children.length > 0) traverse(node.children, node.id)
29+
}
30+
}
31+
32+
traverse(roleTree)
33+
return { parent, children }
34+
})
35+
36+
function handleToggle(toggledId: string, isChecked: boolean) {
37+
if (isChecked) {
38+
// Add self
39+
roles.add(toggledId)
40+
41+
// Cascade UP: Add all ancestors
42+
let currentId = toggledId
43+
while (maps.parent.has(currentId)) {
44+
const parentId = maps.parent.get(currentId)!
45+
roles.add(parentId)
46+
currentId = parentId
47+
}
48+
49+
} else {
50+
// Remove self
51+
roles.delete(toggledId)
52+
53+
// Cascade DOWN: Remove all descendants
54+
let queue = [toggledId]
55+
while (queue.length > 0) {
56+
const currentId = queue.pop()!
57+
const kids = maps.children.get(currentId) || []
58+
59+
for (const childId of kids) {
60+
if (roles.has(childId)) {
61+
roles.delete(childId)
62+
queue.push(childId)
63+
}
64+
}
65+
}
66+
}
67+
}
1768
</script>
1869

19-
<div style="padding-left: {depth * 1.5}rem;">
20-
<label
21-
class="flex h-12 cursor-pointer items-center gap-3 rounded px-4 ring-1 ring-gray-200 transition hover:bg-gray-50 has-[:checked]:ring-2 has-[:checked]:ring-blue-500"
22-
>
23-
<input
24-
type="checkbox"
25-
name="roles"
26-
value={role.id}
27-
class="h-4 w-4 border-gray-300 text-blue-600 focus:ring-offset-0 focus:outline-none"
28-
29-
checked={selectedValues.has(role.id)}
30-
onchange={(e) => onToggle(role.id, e.currentTarget.checked)}
31-
/>
32-
<span class="text-sm">
33-
{#if depth > 0}
34-
<span class="mr-1 text-gray-400">└</span>
35-
{/if}
36-
{role.name}
37-
</span>
38-
</label>
39-
</div>
40-
41-
{#if role.children.length > 0}
42-
<div class="mt-2 flex flex-col gap-2">
43-
{#each role.children as child (child.id)}
44-
<RoleTreeMultiSelect role={child} depth={depth + 1} {selectedValues} {onToggle} />
45-
{/each}
46-
</div>
47-
{/if}
70+
<div class="text-xs font-semibold">{m.roles()}</div>
71+
{#each roleTree as roleNode (roleNode.id)}
72+
<RoleTreeCheckbox
73+
role={roleNode}
74+
selectedValues={roles}
75+
onToggle={handleToggle}
76+
/>
77+
{/each}

src/routes/(admin)/posting/PostingForm.svelte

Lines changed: 2 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import * as m from '$lib/paraglide/messages'
88
import RoleTreeMultiSelect from '$lib/components/RoleTreeMultiSelect.svelte'
99
import type { RoleWithChildren } from '$lib/roles.ts'
10-
import { SvelteMap, SvelteSet } from 'svelte/reactivity'
1110
1211
interface PostingData {
1312
id?: string;
@@ -48,6 +47,7 @@
4847
let descriptionValue = $state(posting.description)
4948
let contentValue = $state(posting.content)
5049
let visibilityValue = $state(posting.visibility)
50+
let rolesValue = $state(posting.roles)
5151
5252
let uploading = $state(false)
5353
let uploadError = $state('')
@@ -98,58 +98,6 @@
9898
uploading = false
9999
}
100100
}
101-
102-
// Source of truth for roles. Callbacks in the SubscriberRoleOption will update this.
103-
let roles = new SvelteSet(posting.roles)
104-
105-
let maps = $derived.by(() => {
106-
const parent = new SvelteMap<string, string>()
107-
const children = new SvelteMap<string, string[]>()
108-
109-
function traverse(nodes: any[], parentId: string | null = null) {
110-
for (const node of nodes) {
111-
if (parentId) parent.set(node.id, parentId)
112-
children.set(node.id, node.children.map((c: any) => c.id))
113-
if (node.children.length > 0) traverse(node.children, node.id)
114-
}
115-
}
116-
117-
traverse(roleTree)
118-
return { parent, children }
119-
})
120-
121-
function handleToggle(toggledId: string, isChecked: boolean) {
122-
if (isChecked) {
123-
// Add self
124-
roles.add(toggledId)
125-
126-
// Cascade UP: Add all ancestors
127-
let currentId = toggledId
128-
while (maps.parent.has(currentId)) {
129-
const parentId = maps.parent.get(currentId)!
130-
roles.add(parentId)
131-
currentId = parentId
132-
}
133-
134-
} else {
135-
// Remove self
136-
roles.delete(toggledId)
137-
138-
// Cascade DOWN: Remove all descendants
139-
let queue = [toggledId]
140-
while (queue.length > 0) {
141-
const currentId = queue.pop()!
142-
const kids = maps.children.get(currentId) || []
143-
144-
for (const childId of kids) {
145-
if (roles.has(childId)) {
146-
roles.delete(childId)
147-
queue.push(childId)
148-
}
149-
}
150-
}
151-
}
152-
}
153101
</script>
154102

155103
<h1 class="mt-12 text-center text-2xl font-bold">{m.create_posting()}</h1>
@@ -251,14 +199,7 @@
251199
</div>
252200

253201
{#if visibilityValue === 'roles'}
254-
<div class="text-xs font-semibold">{m.roles()}</div>
255-
{#each roleTree as roleNode (roleNode.id)}
256-
<RoleTreeMultiSelect
257-
role={roleNode}
258-
selectedValues={roles}
259-
onToggle={handleToggle}
260-
/>
261-
{/each}
202+
<RoleTreeMultiSelect value={rolesValue} {roleTree} />
262203
{/if}
263204

264205
<div class="flex justify-between">

src/routes/(admin)/subscriber/SubscriberForm.svelte

Lines changed: 1 addition & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -28,57 +28,6 @@
2828
}: Props = $props()
2929
3030
let username = $state(subscriber.username)
31-
// Source of truth for roles. Callbacks in the SubscriberRoleOption will update this.
32-
let roles = new SvelteSet(subscriber.roles)
33-
34-
let maps = $derived.by(() => {
35-
const parent = new SvelteMap<string, string>()
36-
const children = new SvelteMap<string, string[]>()
37-
38-
function traverse(nodes: any[], parentId: string | null = null) {
39-
for (const node of nodes) {
40-
if (parentId) parent.set(node.id, parentId)
41-
children.set(node.id, node.children.map((c: any) => c.id))
42-
if (node.children.length > 0) traverse(node.children, node.id)
43-
}
44-
}
45-
46-
traverse(roleTree)
47-
return { parent, children }
48-
})
49-
50-
function handleToggle(toggledId: string, isChecked: boolean) {
51-
if (isChecked) {
52-
// Add self
53-
roles.add(toggledId)
54-
55-
// Cascade UP: Add all ancestors
56-
let currentId = toggledId
57-
while (maps.parent.has(currentId)) {
58-
const parentId = maps.parent.get(currentId)!
59-
roles.add(parentId)
60-
currentId = parentId
61-
}
62-
63-
} else {
64-
// Remove self
65-
roles.delete(toggledId)
66-
67-
// Cascade DOWN: Remove all descendants
68-
let queue = [toggledId]
69-
while (queue.length > 0) {
70-
const currentId = queue.pop()!
71-
const kids = maps.children.get(currentId) || []
72-
73-
for (const childId of kids) {
74-
if (roles.has(childId)) {
75-
roles.delete(childId)
76-
queue.push(childId)
77-
}
78-
}
79-
}
80-
}
81-
}
8231
</script>
8332

8433
<h1 class="mt-12 text-center text-2xl font-bold">{m.subscribers_create()}</h1>
@@ -97,14 +46,7 @@
9746
/>
9847
<label class="text-xs font-semibold" for="password">{m.subscribers_edit_password()}</label>
9948
<input type="password" name="password" />
100-
<div class="text-xs font-semibold">{m.subscribers_roles()}</div>
101-
{#each roleTree as roleNode (roleNode.id)}
102-
<RoleTreeMultiSelect
103-
role={roleNode}
104-
selectedValues={roles}
105-
onToggle={handleToggle}
106-
/>
107-
{/each}
49+
<RoleTreeMultiSelect value={subscriber.roles} {roleTree} />
10850
<div class="flex justify-between">
10951
<button
11052
class="h-12 w-64 rounded bg-blue-600 text-sm font-semibold text-blue-100 hover:bg-blue-700"

0 commit comments

Comments
 (0)