Skip to content

Commit 7ae5b47

Browse files
committed
user creation and revamp edit dialog to support restrictions
1 parent 4848305 commit 7ae5b47

File tree

9 files changed

+305
-51
lines changed

9 files changed

+305
-51
lines changed

next-ui/src/colada/mutations/update-user.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@ import {defineMutation, useMutation, useQueryCache} from '@pinia/colada'
22
import {komgaClient} from '@/api/komga-client'
33
import type {components} from '@/generated/openapi/komga'
44

5+
export const useCreateUser = defineMutation(() => {
6+
const queryCache = useQueryCache()
7+
return useMutation({
8+
mutation: (user: components['schemas']['UserCreationDto']) =>
9+
komgaClient.POST('/api/v2/users', {
10+
body: user,
11+
}),
12+
onSuccess: () => {
13+
void queryCache.invalidateQueries({key: ['users']})
14+
},
15+
onError: (error) => {
16+
console.log('create user error', error)
17+
},
18+
})
19+
})
520
export const useUpdateUser = defineMutation(() => {
621
const queryCache = useQueryCache()
722
return useMutation({
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {defineQuery, useQuery} from '@pinia/colada'
2+
import {komgaClient} from '@/api/komga-client'
3+
4+
export const useLibraries = defineQuery(() => {
5+
return useQuery({
6+
key: () => ['libraries'],
7+
query: () => komgaClient.GET('/api/v1/libraries')
8+
// unwrap the openapi-fetch structure on success
9+
.then((res) => res.data),
10+
// 1 hour
11+
staleTime: 60 * 60 * 1000,
12+
gcTime: false,
13+
})
14+
})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {defineQuery, useQuery} from '@pinia/colada'
2+
import {komgaClient} from '@/api/komga-client'
3+
4+
export const useSharingLabels = defineQuery(() => {
5+
return useQuery({
6+
key: () => ['sharing-labels'],
7+
query: () => komgaClient.GET('/api/v1/sharing-labels')
8+
// unwrap the openapi-fetch structure on success
9+
.then((res) => res.data),
10+
// 1 hour
11+
staleTime: 60 * 60 * 1000,
12+
gcTime: false,
13+
})
14+
})

next-ui/src/components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ declare module 'vue' {
2424
DialogConfirm: typeof import('./components/dialogs/DialogConfirm.vue')['default']
2525
DialogConfirmEdit: typeof import('./components/dialogs/DialogConfirmEdit.vue')['default']
2626
FormUserChangePassword: typeof import('./components/forms/user/FormUserChangePassword.vue')['default']
27+
FormUserEdit: typeof import('./components/forms/user/FormUserEdit.vue')['default']
2728
FormUserRoles: typeof import('./components/forms/user/FormUserRoles.vue')['default']
2829
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
2930
LoginForm: typeof import('./components/LoginForm.vue')['default']

next-ui/src/components/forms/user/FormUserChangePassword.vue

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@
44
:rules="[rules.required()]"
55
label="New password"
66
autocomplete="off"
7+
autofocus
78
:type="showPassword ? 'text' : 'password'"
8-
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
9-
@click:append="showPassword = !showPassword"
9+
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
10+
@click:append-inner="showPassword = !showPassword"
1011
/>
1112
<v-text-field
1213
v-model="confirmPassword"
14+
class="mt-2"
1315
:rules="[rules.sameAs(newPassword)]"
1416
label="Confirm password"
1517
autocomplete="off"
1618
:type="showPassword ? 'text' : 'password'"
17-
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
18-
@click:append="showPassword = !showPassword"
19+
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
20+
@click:append-inner="showPassword = !showPassword"
1921
/>
2022
</template>
2123

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
<template>
2+
<template v-if="!user.id">
3+
<v-text-field
4+
v-model="user!.email"
5+
autofocus
6+
:rules="[rules.required(), rules.email()]"
7+
label="Email"
8+
prepend-icon="mdi-account"
9+
/>
10+
<v-text-field
11+
v-model="user.password"
12+
class="mt-1"
13+
:rules="[rules.required()]"
14+
label="Password"
15+
autocomplete="off"
16+
:type="showPassword ? 'text' : 'password'"
17+
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
18+
prepend-icon="mdi-none"
19+
@click:append-inner="showPassword = !showPassword"
20+
/>
21+
</template>
22+
23+
<!-- Roles -->
24+
<v-select
25+
v-model="user.roles"
26+
chips
27+
closable-chips
28+
multiple
29+
label="Roles"
30+
prepend-icon="mdi-key-chain"
31+
:items="userRoles"
32+
/>
33+
34+
<!-- Shared libraries -->
35+
<v-select
36+
v-model="user.sharedLibraries!.libraryIds"
37+
multiple
38+
label="Shared Libraries"
39+
:items="libraries"
40+
item-title="name"
41+
item-value="id"
42+
prepend-icon="mdi-book-multiple"
43+
>
44+
<!-- Workaround for the lack of a slot to override the whole selection -->
45+
<template #prepend-inner>
46+
<!-- Show an All Libraries chip instead of the selection -->
47+
<v-chip
48+
v-if="user.sharedLibraries?.all"
49+
text="All libraries"
50+
size="small"
51+
/>
52+
</template>
53+
54+
<template #selection="{ item }">
55+
<!-- Show the selection only if 'all' is false -->
56+
<v-chip
57+
v-if="!user.sharedLibraries?.all"
58+
size="small"
59+
:text="item.title"
60+
/>
61+
</template>
62+
63+
<template #prepend-item>
64+
<v-list-item
65+
title="All libraries"
66+
@click="selectAllLibraries"
67+
>
68+
<template #prepend>
69+
<v-checkbox-btn :model-value="user.sharedLibraries?.all" />
70+
</template>
71+
</v-list-item>
72+
</template>
73+
74+
<template #item="{ props: itemProps }">
75+
<v-list-item
76+
:disabled="user.sharedLibraries?.all"
77+
v-bind="itemProps"
78+
>
79+
<template #prepend="{isSelected}">
80+
<v-checkbox-btn :model-value="isSelected" />
81+
</template>
82+
</v-list-item>
83+
</template>
84+
</v-select>
85+
86+
<!-- Age restriction -->
87+
<v-row>
88+
<v-col>
89+
<v-select
90+
v-model="user.ageRestriction!.restriction"
91+
label="Age restriction"
92+
:items="ageRestrictions"
93+
prepend-icon="mdi-folder-lock"
94+
/>
95+
</v-col>
96+
<v-col>
97+
<v-number-input
98+
v-model="user.ageRestriction!.age"
99+
:disabled="user.ageRestriction?.restriction?.toString() === 'NONE'"
100+
label="Age"
101+
:min="0"
102+
:rules="[rules.required()]"
103+
/>
104+
</v-col>
105+
</v-row>
106+
107+
<!-- Allow labels -->
108+
<v-combobox
109+
v-model="user.labelsAllow"
110+
label="Allow only labels"
111+
chips
112+
closable-chips
113+
multiple
114+
:items="sharingLabels"
115+
prepend-icon="mdi-none"
116+
>
117+
<template #prepend-item>
118+
<v-list-item>
119+
<span class="font-weight-medium">Select an item or create one</span>
120+
</v-list-item>
121+
</template>
122+
</v-combobox>
123+
124+
<!-- Exclude labels -->
125+
<v-combobox
126+
v-model="user.labelsExclude"
127+
label="Exclude labels"
128+
chips
129+
closable-chips
130+
multiple
131+
:items="sharingLabels"
132+
prepend-icon="mdi-none"
133+
>
134+
<template #prepend-item>
135+
<v-list-item>
136+
<span class="font-weight-medium">Select an item or create one</span>
137+
</v-list-item>
138+
</template>
139+
</v-combobox>
140+
</template>
141+
142+
<script setup lang="ts">
143+
import {UserRoles} from '@/types/UserRoles.ts'
144+
import type {components} from '@/generated/openapi/komga'
145+
import { useRules } from 'vuetify/labs/rules'
146+
import {useLibraries} from '@/colada/queries/libraries.ts'
147+
import {useSharingLabels} from '@/colada/queries/referential.ts'
148+
149+
const rules = useRules()
150+
151+
interface UserExtend {
152+
id?: string,
153+
email: string,
154+
password?: string,
155+
}
156+
type UserCreation = components["schemas"]["UserCreationDto"] & UserExtend
157+
type UserUpdate = components["schemas"]["UserUpdateDto"] & UserExtend
158+
const user = defineModel<UserCreation | UserUpdate>({required: true})
159+
160+
const showPassword = ref<boolean>(false)
161+
162+
const {data: libraries} = useLibraries()
163+
const {data: sharingLabels} = useSharingLabels()
164+
165+
function selectAllLibraries() {
166+
user.value.sharedLibraries!.all = !user.value.sharedLibraries?.all
167+
user.value.sharedLibraries!.libraryIds = libraries.value?.map(x => x.id) || []
168+
}
169+
170+
const userRoles = computed(() => Object.keys(UserRoles).map(x => ({
171+
title: x,
172+
value: x,
173+
})))
174+
175+
const ageRestrictions = [
176+
{title: 'No restriction', value: 'NONE'},
177+
{title: 'Allow only under', value: 'ALLOW_ONLY'},
178+
{title: 'Exclude over', value: 'EXCLUDE'},
179+
]
180+
</script>

next-ui/src/components/forms/user/FormUserRoles.vue

Lines changed: 0 additions & 22 deletions
This file was deleted.

next-ui/src/generated/openapi/komga.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2669,7 +2669,7 @@ export interface components {
26692669
/** Format: int32 */
26702670
age: number;
26712671
/** @enum {string} */
2672-
restriction: "ALLOW_ONLY" | "EXCLUDE";
2672+
restriction: "ALLOW_ONLY" | "EXCLUDE" | "NONE";
26732673
};
26742674
AllOfBook: components["schemas"]["Book"] & {
26752675
allOf: (components["schemas"]["AllOfBook"] | components["schemas"]["AnyOfBook"] | components["schemas"]["Author"] | components["schemas"]["Deleted"] | components["schemas"]["LibraryId"] | components["schemas"]["MediaProfile"] | components["schemas"]["MediaStatus"] | components["schemas"]["NumberSort"] | components["schemas"]["OneShot"] | components["schemas"]["Poster"] | components["schemas"]["ReadListId"] | components["schemas"]["ReadStatus"] | components["schemas"]["ReleaseDate"] | components["schemas"]["SeriesId"] | components["schemas"]["Tag"] | components["schemas"]["Title"])[];
@@ -3885,9 +3885,13 @@ export interface components {
38853885
url: string;
38863886
};
38873887
UserCreationDto: {
3888+
ageRestriction?: components["schemas"]["AgeRestrictionUpdateDto"];
38883889
email: string;
3890+
labelsAllow?: string[];
3891+
labelsExclude?: string[];
38893892
password: string;
38903893
roles: string[];
3894+
sharedLibraries?: components["schemas"]["SharedLibrariesUpdateDto"];
38913895
};
38923896
UserDto: {
38933897
ageRestriction?: components["schemas"]["AgeRestrictionDto"];

0 commit comments

Comments
 (0)