diff --git a/next-ui/src/components.d.ts b/next-ui/src/components.d.ts
index fbae7f4be..96d01c02a 100644
--- a/next-ui/src/components.d.ts
+++ b/next-ui/src/components.d.ts
@@ -20,6 +20,7 @@ declare module 'vue' {
FragmentSnackQueue: typeof import('./fragments/fragment/SnackQueue.vue')['default']
FragmentThemeSelector: typeof import('./fragments/fragment/ThemeSelector.vue')['default']
FragmentUserFormCreateEdit: typeof import('./fragments/fragment/user/form/CreateEdit.vue')['default']
+ FragmentUserTable: typeof import('./fragments/fragment/user/Table.vue')['default']
HelloWorld: typeof import('./components/HelloWorld.vue')['default']
LayoutAppBar: typeof import('./fragments/layout/app/Bar.vue')['default']
LayoutAppDrawer: typeof import('./fragments/layout/app/drawer/Drawer.vue')['default']
@@ -36,6 +37,5 @@ declare module 'vue' {
RouterView: typeof import('vue-router')['RouterView']
UserDeletionWarning: typeof import('./components/user/DeletionWarning.vue')['default']
UserFormChangePassword: typeof import('./components/user/form/ChangePassword.vue')['default']
- UserFormCreateEdit: typeof import('./components/user/form/CreateEdit.vue')['default']
}
}
diff --git a/next-ui/src/fragments/fragment/user/Table.stories.ts b/next-ui/src/fragments/fragment/user/Table.stories.ts
new file mode 100644
index 000000000..3483d3f8f
--- /dev/null
+++ b/next-ui/src/fragments/fragment/user/Table.stories.ts
@@ -0,0 +1,50 @@
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import Table from './Table.vue'
+import { users } from '@/mocks/api/handlers/users'
+import { fn } from 'storybook/test'
+
+const meta = {
+ component: Table,
+ render: (args: object) => ({
+ components: { Table },
+ setup() {
+ return { args }
+ },
+ template: '
',
+ }),
+ parameters: {
+ // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
+ },
+ args: {
+ onAddUser: fn(),
+ onEnterAddUser: fn(),
+ onEnterEditUser: fn(),
+ onEnterDeleteUser: fn(),
+ onEnterChangePassword: fn(),
+ onChangePassword: fn(),
+ onEditUser: fn(),
+ onDeleteUser: fn(),
+ },
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {
+ args: {
+ users: users,
+ },
+}
+
+export const Loading: Story = {
+ args: {
+ loading: true,
+ },
+}
+
+export const NoData: Story = {
+ args: {
+ users: [],
+ },
+}
diff --git a/next-ui/src/fragments/fragment/user/Table.vue b/next-ui/src/fragments/fragment/user/Table.vue
new file mode 100644
index 000000000..8ca423921
--- /dev/null
+++ b/next-ui/src/fragments/fragment/user/Table.vue
@@ -0,0 +1,209 @@
+
+
+
+
+
+
+ {{
+ $formatMessage({
+ description: 'Users table global header',
+ defaultMessage: 'Users',
+ id: 'c+hx0g',
+ })
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $formatDate(value, { dateStyle: 'medium', timeStyle: 'short' }) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/next-ui/src/mocks/api/handlers/users.ts b/next-ui/src/mocks/api/handlers/users.ts
index 1d7898d47..4ec94ce3b 100644
--- a/next-ui/src/mocks/api/handlers/users.ts
+++ b/next-ui/src/mocks/api/handlers/users.ts
@@ -1,45 +1,39 @@
import { httpTyped } from '@/mocks/api/httpTyped'
-export const users = [
- {
- id: '0JEDA00AV4Z7G',
- email: 'admin@example.org',
- roles: ['ADMIN', 'FILE_DOWNLOAD', 'KOBO_SYNC', 'KOREADER_SYNC', 'PAGE_STREAMING', 'USER'],
- sharedAllLibraries: true,
- sharedLibrariesIds: [],
- labelsAllow: [],
- labelsExclude: [],
- ageRestriction: null,
- },
- {
- id: '0JEDA00AZ4QXH',
- email: 'user@example.org',
- roles: ['KOBO_SYNC', 'PAGE_STREAMING', 'USER'],
- sharedAllLibraries: true,
- sharedLibrariesIds: [],
- labelsAllow: [],
- labelsExclude: ['book'],
- ageRestriction: null,
- },
-]
+export const userAdmin = {
+ id: '0JEDA00AV4Z7G',
+ email: 'admin@example.org',
+ roles: ['ADMIN', 'FILE_DOWNLOAD', 'KOBO_SYNC', 'KOREADER_SYNC', 'PAGE_STREAMING', 'USER'],
+ sharedAllLibraries: true,
+ sharedLibrariesIds: [],
+ labelsAllow: [],
+ labelsExclude: [],
+}
+export const userRegular = {
+ id: '0JEDA00AZ4QXH',
+ email: 'user@example.org',
+ roles: ['KOBO_SYNC', 'PAGE_STREAMING', 'USER'],
+ sharedAllLibraries: true,
+ sharedLibrariesIds: [],
+ labelsAllow: [],
+ labelsExclude: ['book'],
+}
+export const users = [userAdmin, userRegular]
const latestActivity = {
userId: '0JEDA00AV4Z7G',
email: 'admin@example.org',
- apiKeyId: null,
- apiKeyComment: null,
ip: '127.0.0.1',
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0',
success: true,
- error: null,
dateTime: new Date('2025-06-30T06:56:33Z'),
source: 'Password',
}
export const usersHandlers = [
- httpTyped.get('/api/v2/users/me', ({ response }) => response(200).json(users[0])),
+ httpTyped.get('/api/v2/users/me', ({ response }) => response(200).json(userAdmin)),
httpTyped.get('/api/v2/users', ({ response }) => response(200).json(users)),
- httpTyped.get('/api/v2/users/{userId}/authentication-activity/latest', ({ response }) =>
+ httpTyped.get('/api/v2/users/{id}/authentication-activity/latest', ({ response }) =>
response(200).json(latestActivity),
),
]
diff --git a/next-ui/src/pages/server/users.vue b/next-ui/src/pages/server/users.vue
index 5ad9dc1e9..0de44ef22 100644
--- a/next-ui/src/pages/server/users.vue
+++ b/next-ui/src/pages/server/users.vue
@@ -7,90 +7,25 @@
/>
-
-
-
-
-
- Users
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ $formatDate(value, { dateStyle: 'medium', timeStyle: 'short' }) }}
-
-
-
-
-
-
-
-
-
-
+ @add-user="showDialog(ACTION.ADD)"
+ @change-password="(user) => showDialog(ACTION.PASSWORD, user)"
+ @edit-user="(user) => showDialog(ACTION.EDIT, user)"
+ @delete-user="(user) => showDialog(ACTION.DELETE, user)"
+ @enter-add-user="(target) => (dialogConfirmEdit.activator = target)"
+ @enter-edit-user="(target) => (dialogConfirmEdit.activator = target)"
+ @enter-change-password="(target) => (dialogConfirmEdit.activator = target)"
+ @enter-delete-user="(target) => (dialogConfirm.activator = target)"
+ />