mirror of
https://github.com/gotson/komga.git
synced 2025-12-09 10:05:10 +01:00
user creation and revamp edit dialog to support restrictions
This commit is contained in:
parent
f4be925fd9
commit
a4e92ef798
9 changed files with 305 additions and 51 deletions
|
|
@ -2,6 +2,21 @@ import {defineMutation, useMutation, useQueryCache} from '@pinia/colada'
|
|||
import {komgaClient} from '@/api/komga-client'
|
||||
import type {components} from '@/generated/openapi/komga'
|
||||
|
||||
export const useCreateUser = defineMutation(() => {
|
||||
const queryCache = useQueryCache()
|
||||
return useMutation({
|
||||
mutation: (user: components['schemas']['UserCreationDto']) =>
|
||||
komgaClient.POST('/api/v2/users', {
|
||||
body: user,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryCache.invalidateQueries({key: ['users']})
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log('create user error', error)
|
||||
},
|
||||
})
|
||||
})
|
||||
export const useUpdateUser = defineMutation(() => {
|
||||
const queryCache = useQueryCache()
|
||||
return useMutation({
|
||||
|
|
|
|||
14
next-ui/src/colada/queries/libraries.ts
Normal file
14
next-ui/src/colada/queries/libraries.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import {defineQuery, useQuery} from '@pinia/colada'
|
||||
import {komgaClient} from '@/api/komga-client'
|
||||
|
||||
export const useLibraries = defineQuery(() => {
|
||||
return useQuery({
|
||||
key: () => ['libraries'],
|
||||
query: () => komgaClient.GET('/api/v1/libraries')
|
||||
// unwrap the openapi-fetch structure on success
|
||||
.then((res) => res.data),
|
||||
// 1 hour
|
||||
staleTime: 60 * 60 * 1000,
|
||||
gcTime: false,
|
||||
})
|
||||
})
|
||||
14
next-ui/src/colada/queries/referential.ts
Normal file
14
next-ui/src/colada/queries/referential.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import {defineQuery, useQuery} from '@pinia/colada'
|
||||
import {komgaClient} from '@/api/komga-client'
|
||||
|
||||
export const useSharingLabels = defineQuery(() => {
|
||||
return useQuery({
|
||||
key: () => ['sharing-labels'],
|
||||
query: () => komgaClient.GET('/api/v1/sharing-labels')
|
||||
// unwrap the openapi-fetch structure on success
|
||||
.then((res) => res.data),
|
||||
// 1 hour
|
||||
staleTime: 60 * 60 * 1000,
|
||||
gcTime: false,
|
||||
})
|
||||
})
|
||||
1
next-ui/src/components.d.ts
vendored
1
next-ui/src/components.d.ts
vendored
|
|
@ -24,6 +24,7 @@ declare module 'vue' {
|
|||
DialogConfirm: typeof import('./components/dialogs/DialogConfirm.vue')['default']
|
||||
DialogConfirmEdit: typeof import('./components/dialogs/DialogConfirmEdit.vue')['default']
|
||||
FormUserChangePassword: typeof import('./components/forms/user/FormUserChangePassword.vue')['default']
|
||||
FormUserEdit: typeof import('./components/forms/user/FormUserEdit.vue')['default']
|
||||
FormUserRoles: typeof import('./components/forms/user/FormUserRoles.vue')['default']
|
||||
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
|
||||
LoginForm: typeof import('./components/LoginForm.vue')['default']
|
||||
|
|
|
|||
|
|
@ -4,18 +4,20 @@
|
|||
:rules="[rules.required()]"
|
||||
label="New password"
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="showPassword = !showPassword"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="confirmPassword"
|
||||
class="mt-2"
|
||||
:rules="[rules.sameAs(newPassword)]"
|
||||
label="Confirm password"
|
||||
autocomplete="off"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:append-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append="showPassword = !showPassword"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
180
next-ui/src/components/forms/user/FormUserEdit.vue
Normal file
180
next-ui/src/components/forms/user/FormUserEdit.vue
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<template>
|
||||
<template v-if="!user.id">
|
||||
<v-text-field
|
||||
v-model="user!.email"
|
||||
autofocus
|
||||
:rules="[rules.required(), rules.email()]"
|
||||
label="Email"
|
||||
prepend-icon="mdi-account"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="user.password"
|
||||
class="mt-1"
|
||||
:rules="[rules.required()]"
|
||||
label="Password"
|
||||
autocomplete="off"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
:append-inner-icon="showPassword ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
prepend-icon="mdi-none"
|
||||
@click:append-inner="showPassword = !showPassword"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Roles -->
|
||||
<v-select
|
||||
v-model="user.roles"
|
||||
chips
|
||||
closable-chips
|
||||
multiple
|
||||
label="Roles"
|
||||
prepend-icon="mdi-key-chain"
|
||||
:items="userRoles"
|
||||
/>
|
||||
|
||||
<!-- Shared libraries -->
|
||||
<v-select
|
||||
v-model="user.sharedLibraries!.libraryIds"
|
||||
multiple
|
||||
label="Shared Libraries"
|
||||
:items="libraries"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
prepend-icon="mdi-book-multiple"
|
||||
>
|
||||
<!-- Workaround for the lack of a slot to override the whole selection -->
|
||||
<template #prepend-inner>
|
||||
<!-- Show an All Libraries chip instead of the selection -->
|
||||
<v-chip
|
||||
v-if="user.sharedLibraries?.all"
|
||||
text="All libraries"
|
||||
size="small"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #selection="{ item }">
|
||||
<!-- Show the selection only if 'all' is false -->
|
||||
<v-chip
|
||||
v-if="!user.sharedLibraries?.all"
|
||||
size="small"
|
||||
:text="item.title"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #prepend-item>
|
||||
<v-list-item
|
||||
title="All libraries"
|
||||
@click="selectAllLibraries"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-checkbox-btn :model-value="user.sharedLibraries?.all" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<template #item="{ props: itemProps }">
|
||||
<v-list-item
|
||||
:disabled="user.sharedLibraries?.all"
|
||||
v-bind="itemProps"
|
||||
>
|
||||
<template #prepend="{isSelected}">
|
||||
<v-checkbox-btn :model-value="isSelected" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
|
||||
<!-- Age restriction -->
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-select
|
||||
v-model="user.ageRestriction!.restriction"
|
||||
label="Age restriction"
|
||||
:items="ageRestrictions"
|
||||
prepend-icon="mdi-folder-lock"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-number-input
|
||||
v-model="user.ageRestriction!.age"
|
||||
:disabled="user.ageRestriction?.restriction?.toString() === 'NONE'"
|
||||
label="Age"
|
||||
:min="0"
|
||||
:rules="[rules.required()]"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Allow labels -->
|
||||
<v-combobox
|
||||
v-model="user.labelsAllow"
|
||||
label="Allow only labels"
|
||||
chips
|
||||
closable-chips
|
||||
multiple
|
||||
:items="sharingLabels"
|
||||
prepend-icon="mdi-none"
|
||||
>
|
||||
<template #prepend-item>
|
||||
<v-list-item>
|
||||
<span class="font-weight-medium">Select an item or create one</span>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-combobox>
|
||||
|
||||
<!-- Exclude labels -->
|
||||
<v-combobox
|
||||
v-model="user.labelsExclude"
|
||||
label="Exclude labels"
|
||||
chips
|
||||
closable-chips
|
||||
multiple
|
||||
:items="sharingLabels"
|
||||
prepend-icon="mdi-none"
|
||||
>
|
||||
<template #prepend-item>
|
||||
<v-list-item>
|
||||
<span class="font-weight-medium">Select an item or create one</span>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-combobox>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {UserRoles} from '@/types/UserRoles.ts'
|
||||
import type {components} from '@/generated/openapi/komga'
|
||||
import { useRules } from 'vuetify/labs/rules'
|
||||
import {useLibraries} from '@/colada/queries/libraries.ts'
|
||||
import {useSharingLabels} from '@/colada/queries/referential.ts'
|
||||
|
||||
const rules = useRules()
|
||||
|
||||
interface UserExtend {
|
||||
id?: string,
|
||||
email: string,
|
||||
password?: string,
|
||||
}
|
||||
type UserCreation = components["schemas"]["UserCreationDto"] & UserExtend
|
||||
type UserUpdate = components["schemas"]["UserUpdateDto"] & UserExtend
|
||||
const user = defineModel<UserCreation | UserUpdate>({required: true})
|
||||
|
||||
const showPassword = ref<boolean>(false)
|
||||
|
||||
const {data: libraries} = useLibraries()
|
||||
const {data: sharingLabels} = useSharingLabels()
|
||||
|
||||
function selectAllLibraries() {
|
||||
user.value.sharedLibraries!.all = !user.value.sharedLibraries?.all
|
||||
user.value.sharedLibraries!.libraryIds = libraries.value?.map(x => x.id) || []
|
||||
}
|
||||
|
||||
const userRoles = computed(() => Object.keys(UserRoles).map(x => ({
|
||||
title: x,
|
||||
value: x,
|
||||
})))
|
||||
|
||||
const ageRestrictions = [
|
||||
{title: 'No restriction', value: 'NONE'},
|
||||
{title: 'Allow only under', value: 'ALLOW_ONLY'},
|
||||
{title: 'Exclude over', value: 'EXCLUDE'},
|
||||
]
|
||||
</script>
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
<template>
|
||||
<v-checkbox
|
||||
v-for="role in userRoles"
|
||||
:key="role.value"
|
||||
v-model="user.roles"
|
||||
:label="role.text"
|
||||
:value="role.value"
|
||||
hide-details
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {UserRoles} from '@/types/UserRoles.ts'
|
||||
import type {components} from '@/generated/openapi/komga'
|
||||
|
||||
const user = defineModel<components["schemas"]["UserDto"]>({required: true})
|
||||
|
||||
const userRoles = computed(() => Object.keys(UserRoles).map(x => ({
|
||||
text: x,
|
||||
value: x,
|
||||
})))
|
||||
</script>
|
||||
6
next-ui/src/generated/openapi/komga.d.ts
vendored
6
next-ui/src/generated/openapi/komga.d.ts
vendored
|
|
@ -2669,7 +2669,7 @@ export interface components {
|
|||
/** Format: int32 */
|
||||
age: number;
|
||||
/** @enum {string} */
|
||||
restriction: "ALLOW_ONLY" | "EXCLUDE";
|
||||
restriction: "ALLOW_ONLY" | "EXCLUDE" | "NONE";
|
||||
};
|
||||
AllOfBook: components["schemas"]["Book"] & {
|
||||
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 {
|
|||
url: string;
|
||||
};
|
||||
UserCreationDto: {
|
||||
ageRestriction?: components["schemas"]["AgeRestrictionUpdateDto"];
|
||||
email: string;
|
||||
labelsAllow?: string[];
|
||||
labelsExclude?: string[];
|
||||
password: string;
|
||||
roles: string[];
|
||||
sharedLibraries?: components["schemas"]["SharedLibrariesUpdateDto"];
|
||||
};
|
||||
UserDto: {
|
||||
ageRestriction?: components["schemas"]["AgeRestrictionDto"];
|
||||
|
|
|
|||
|
|
@ -14,6 +14,30 @@
|
|||
:headers="headers"
|
||||
:hide-default-footer="hideFooter"
|
||||
>
|
||||
<template #top>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>
|
||||
<v-icon
|
||||
color="medium-emphasis"
|
||||
icon="mdi-account-multiple"
|
||||
size="x-small"
|
||||
start
|
||||
/>
|
||||
Users
|
||||
</v-toolbar-title>
|
||||
|
||||
<v-btn
|
||||
class="me-2"
|
||||
prepend-icon="mdi-plus"
|
||||
rounded="lg"
|
||||
text="Add a User"
|
||||
border
|
||||
@click="showDialog(ACTION.ADD)"
|
||||
@mouseenter="activator = $event.currentTarget"
|
||||
/>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
|
||||
<template #[`item.roles`]="{ value }">
|
||||
<div class="d-flex ga-1">
|
||||
<v-chip
|
||||
|
|
@ -30,18 +54,11 @@
|
|||
<template #[`item.actions`]="{ item : user }">
|
||||
<div class="d-flex ga-1 justify-end">
|
||||
<v-icon-btn
|
||||
v-tooltip:bottom="'Reset password'"
|
||||
v-tooltip:bottom="'Change password'"
|
||||
icon="mdi-lock-reset"
|
||||
@click="showDialog(ACTION.PASSWORD, user)"
|
||||
@mouseenter="activator = $event.currentTarget"
|
||||
/>
|
||||
<v-icon-btn
|
||||
v-tooltip:bottom="'Edit restrictions'"
|
||||
icon="mdi-book-lock"
|
||||
:disabled="me?.id == user.id"
|
||||
@click="showDialog(ACTION.RESTRICTIONS, user)"
|
||||
@mouseenter="activator = $event.currentTarget"
|
||||
/>
|
||||
<v-icon-btn
|
||||
v-tooltip:bottom="'Edit user'"
|
||||
icon="mdi-pencil"
|
||||
|
|
@ -65,7 +82,7 @@
|
|||
:activator="activator"
|
||||
:title="dialogTitle"
|
||||
:subtitle="userRecord?.email"
|
||||
max-width="400"
|
||||
:max-width="currentAction === ACTION.PASSWORD ? 400 : 600"
|
||||
@update:record="handleDialogConfirmation()"
|
||||
>
|
||||
<template #text="{proxyModel}">
|
||||
|
|
@ -111,10 +128,11 @@ import {komgaClient} from '@/api/komga-client.ts'
|
|||
import type {components} from '@/generated/openapi/komga'
|
||||
import {useCurrentUser} from '@/colada/queries/current-user.ts'
|
||||
import {UserRoles} from '@/types/UserRoles.ts'
|
||||
import {useDeleteUser, useUpdateUser, useUpdateUserPassword} from '@/colada/mutations/update-user.ts'
|
||||
import {useCreateUser, useDeleteUser, useUpdateUser, useUpdateUserPassword} from '@/colada/mutations/update-user.ts'
|
||||
import FormUserChangePassword from '@/components/forms/user/FormUserChangePassword.vue'
|
||||
import FormUserRoles from '@/components/forms/user/FormUserRoles.vue'
|
||||
import FormUserEdit from '@/components/forms/user/FormUserEdit.vue'
|
||||
import type {Component} from 'vue'
|
||||
import {useLibraries} from '@/colada/queries/libraries.ts'
|
||||
|
||||
// API data
|
||||
const {data: users, error, isLoading, refetch: refetchUsers} = useUsers()
|
||||
|
|
@ -172,30 +190,57 @@ const dialogTitle = ref<string>()
|
|||
// dynamic component for the dialog's inner form
|
||||
const dialogComponent = shallowRef<Component>()
|
||||
|
||||
const {mutate: mutateCreateUser} = useCreateUser()
|
||||
const {mutate: mutateUser} = useUpdateUser()
|
||||
const {mutate: mutateUserPassword} = useUpdateUserPassword()
|
||||
const {mutate: mutateDeleteUser} = useDeleteUser()
|
||||
const {data: libraries} = useLibraries()
|
||||
|
||||
enum ACTION {
|
||||
EDIT, DELETE, RESTRICTIONS, PASSWORD
|
||||
ADD, EDIT, DELETE, PASSWORD
|
||||
}
|
||||
|
||||
function showDialog(action: ACTION, user: components["schemas"]["UserDto"]) {
|
||||
function showDialog(action: ACTION, user?: components["schemas"]["UserDto"]) {
|
||||
currentAction.value = action
|
||||
switch (action) {
|
||||
case ACTION.ADD:
|
||||
dialogTitle.value = 'Add User'
|
||||
dialogComponent.value = FormUserEdit
|
||||
dialogRecord.value = {
|
||||
email: '',
|
||||
password: '',
|
||||
roles: [UserRoles.PAGE_STREAMING, UserRoles.FILE_DOWNLOAD],
|
||||
sharedLibraries: {
|
||||
all: true,
|
||||
// we fill the array with all libraries for a nicer display in the edit dialog
|
||||
libraryIds: libraries.value?.map(x => x.id) || [],
|
||||
},
|
||||
ageRestriction: {
|
||||
age: 0,
|
||||
restriction: 'NONE',
|
||||
}
|
||||
} as components["schemas"]["UserCreationDto"]
|
||||
break;
|
||||
case ACTION.EDIT:
|
||||
dialogTitle.value = 'Edit Roles'
|
||||
dialogComponent.value = FormUserRoles
|
||||
dialogRecord.value = user
|
||||
dialogTitle.value = 'Edit User'
|
||||
dialogComponent.value = FormUserEdit
|
||||
dialogRecord.value = {
|
||||
...user,
|
||||
roles: user?.roles.filter(x => x !== 'USER'),
|
||||
sharedLibraries: {
|
||||
all: user?.sharedAllLibraries,
|
||||
// we fill the array with all libraries for a nicer display in the edit dialog
|
||||
libraryIds: user?.sharedAllLibraries ? libraries.value?.map(x => x.id) || [] : user?.sharedLibrariesIds,
|
||||
},
|
||||
ageRestriction: user?.ageRestriction || {
|
||||
age: 0,
|
||||
restriction: 'NONE',
|
||||
}
|
||||
} as components["schemas"]["UserUpdateDto"]
|
||||
break;
|
||||
case ACTION.DELETE:
|
||||
dialogTitle.value = 'Delete User'
|
||||
dialogComponent.value = FormUserRoles
|
||||
dialogRecord.value = user
|
||||
break;
|
||||
case ACTION.RESTRICTIONS:
|
||||
dialogTitle.value = 'Edit Restrictions'
|
||||
dialogComponent.value = FormUserRoles
|
||||
dialogComponent.value = FormUserEdit
|
||||
dialogRecord.value = user
|
||||
break;
|
||||
case ACTION.PASSWORD:
|
||||
|
|
@ -209,14 +254,15 @@ function showDialog(action: ACTION, user: components["schemas"]["UserDto"]) {
|
|||
|
||||
function handleDialogConfirmation() {
|
||||
switch (currentAction.value) {
|
||||
case ACTION.ADD:
|
||||
mutateCreateUser(dialogRecord.value as components["schemas"]["UserCreationDto"])
|
||||
break;
|
||||
case ACTION.EDIT:
|
||||
mutateUser(dialogRecord.value as components["schemas"]["UserDto"])
|
||||
break;
|
||||
case ACTION.DELETE:
|
||||
mutateDeleteUser(userRecord.value!.id)
|
||||
break;
|
||||
case ACTION.RESTRICTIONS:
|
||||
break;
|
||||
case ACTION.PASSWORD:
|
||||
mutateUserPassword({
|
||||
userId: userRecord.value!.id,
|
||||
|
|
|
|||
Loading…
Reference in a new issue