+
-
-
- User details
-
- Set an initial password that the user can replace after
- their first sign-in.
-
-
-
-
-
-
-
+
diff --git a/resources/js/pages/admin/users/Edit.vue b/resources/js/pages/admin/users/Edit.vue
index 7619318..36754f2 100644
--- a/resources/js/pages/admin/users/Edit.vue
+++ b/resources/js/pages/admin/users/Edit.vue
@@ -1,170 +1,60 @@
diff --git a/resources/js/pages/admin/users/Index.vue b/resources/js/pages/admin/users/Index.vue
index 754e0ae..6f8d820 100644
--- a/resources/js/pages/admin/users/Index.vue
+++ b/resources/js/pages/admin/users/Index.vue
@@ -1,267 +1,468 @@
-
-
+
+
-
-
-
-
-
- {{ status }}
-
-
-
-
- Users
-
- Global permissions are assigned directly to users and
- apply across the entire application.
-
-
-
-
-
+
- No users exist yet.
-
-
-
-
-
-
- | User |
- Email |
-
- Current access
- |
-
+
+
+
+ {{ user.name }}
+
+
- Action
- |
-
-
-
-
- |
-
-
- {{ user.name }}
-
- |
-
{{ user.email }}
- |
-
-
-
-
- Must change password
-
-
-
- Global admin
-
-
-
- Project create
-
-
- No direct global permissions
-
-
- |
-
-
-
+
+ {{ t('adminUsers.managedAccount') }}
+
+
-
-
-
- |
-
-
-
-
-
-
+
+
+
+ {{ t('adminUsers.permissions.action') }}
+
+
+
+
+ {{ t('adminUsers.edit.shortAction') }}
+
+
+
+
+
+
+
+
+
diff --git a/tests/Feature/Admin/UserManagementUiTest.php b/tests/Feature/Admin/UserManagementUiTest.php
new file mode 100644
index 0000000..7ba392b
--- /dev/null
+++ b/tests/Feature/Admin/UserManagementUiTest.php
@@ -0,0 +1,25 @@
+setTimeout(60);
+ $process->run();
+
+ $this->assertTrue(
+ $process->isSuccessful(),
+ $process->getOutput().$process->getErrorOutput(),
+ );
+ $this->assertStringContainsString(
+ 'User management rendering checks passed.',
+ $process->getOutput(),
+ );
+ }
+}
diff --git a/tests/Fixtures/user-management-preview.mjs b/tests/Fixtures/user-management-preview.mjs
new file mode 100644
index 0000000..31ba643
--- /dev/null
+++ b/tests/Fixtures/user-management-preview.mjs
@@ -0,0 +1,21 @@
+import { createApp, h } from 'vue';
+import { createInertiaApp } from '@inertiajs/vue3';
+import { createAppearance } from '@/composables/useAppearance';
+import { createLocalization } from '@/lib/localization';
+import Create from '@/pages/admin/users/Create.vue';
+import Edit from '@/pages/admin/users/Edit.vue';
+import Index from '@/pages/admin/users/Index.vue';
+import '@/../css/app.css';
+
+const components = { Create, Edit, Index };
+
+createInertiaApp({
+ resolve: (name) => components[name],
+ setup({ el, App, props, plugin }) {
+ createApp({ render: () => h(App, props) })
+ .use(plugin)
+ .use(createLocalization())
+ .use(createAppearance())
+ .mount(el);
+ },
+});
diff --git a/tests/Fixtures/user-management.mjs b/tests/Fixtures/user-management.mjs
new file mode 100644
index 0000000..5073685
--- /dev/null
+++ b/tests/Fixtures/user-management.mjs
@@ -0,0 +1,206 @@
+import assert from 'node:assert/strict';
+import process from 'node:process';
+import { fileURLToPath } from 'node:url';
+import vue from '@vitejs/plugin-vue';
+import tailwind from '@tailwindcss/vite';
+import { createServer } from 'vite';
+import { createSSRApp, h } from 'vue';
+import { renderToString } from 'vue/server-renderer';
+import { createInertiaApp } from '@inertiajs/vue3';
+
+const serve = process.argv.includes('--serve');
+const server = await createServer({
+ configFile: false,
+ cacheDir: 'node_modules/.vite/user-management-fixture',
+ plugins: [vue(), ...(serve ? [tailwind()] : [])],
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('../../resources/js', import.meta.url)),
+ },
+ },
+ server: serve
+ ? { host: '127.0.0.1', port: 4191, strictPort: true, hmr: false }
+ : { middlewareMode: true, hmr: false, watch: null },
+ appType: 'custom',
+});
+
+const admin = {
+ id: 1,
+ name: 'Alex Admin',
+ email: 'alex@example.test',
+ avatar: null,
+ avatar_color: '#10B981',
+ avatar_is_generated: true,
+ email_verified_at: null,
+ must_change_password_on_next_login: false,
+ created_at: '2026-01-01T00:00:00Z',
+ updated_at: '2026-01-01T00:00:00Z',
+};
+const member = {
+ ...admin,
+ id: 2,
+ name: 'Morgan Member',
+ email: 'morgan@example.test',
+ must_change_password_on_next_login: true,
+};
+const shared = {
+ name: 'KanbanGit',
+ errors: {},
+ sidebarOpen: true,
+ appearance: 'system',
+ auth: {
+ user: admin,
+ permissions: {
+ globalAdmin: true,
+ projectCreate: true,
+ canManageTeams: true,
+ },
+ },
+ navigation: { project: null },
+};
+const pages = {
+ index: {
+ component: 'Index',
+ url: '/admin/users',
+ props: {
+ ...shared,
+ directPermissions: {
+ 1: ['global:admin', 'project:create'],
+ 2: [],
+ },
+ permissionOptions: [
+ { value: 'global:admin', label: 'Global admin' },
+ { value: 'project:create', label: 'Create project' },
+ ],
+ users: [admin, member],
+ status: null,
+ },
+ },
+ create: {
+ component: 'Create',
+ url: '/admin/users/create',
+ props: shared,
+ },
+ edit: {
+ component: 'Edit',
+ url: '/admin/users/2/edit',
+ props: { ...shared, user: member },
+ },
+};
+
+function inertiaPage(page) {
+ return {
+ ...page,
+ version: null,
+ clearHistory: false,
+ encryptHistory: false,
+ };
+}
+
+async function render(path, page) {
+ const component = (await server.ssrLoadModule(path)).default;
+ const { createLocalization } = await server.ssrLoadModule(
+ '/resources/js/lib/localization.ts',
+ );
+ const { createAppearance } = await server.ssrLoadModule(
+ '/resources/js/composables/useAppearance.ts',
+ );
+
+ return (
+ await createInertiaApp({
+ page: inertiaPage(page),
+ resolve: () => component,
+ render: renderToString,
+ setup: ({ App, props, plugin }) =>
+ createSSRApp({ render: () => h(App, props) })
+ .use(plugin)
+ .use(createLocalization())
+ .use(createAppearance()),
+ })
+ ).body;
+}
+
+if (serve) {
+ server.middlewares.use(async (request, response, next) => {
+ if (!request.url.startsWith('/admin/users')) {
+ return next();
+ }
+
+ const pathname = new URL(request.url, 'http://localhost').pathname;
+ const page = pathname.endsWith('/create')
+ ? pages.create
+ : pathname.endsWith('/edit')
+ ? pages.edit
+ : pages.index;
+ const completePage = inertiaPage(page);
+ response.setHeader('Vary', 'X-Inertia');
+
+ if (request.headers['x-inertia']) {
+ response.setHeader('X-Inertia', 'true');
+ response.setHeader('Content-Type', 'application/json');
+ response.end(JSON.stringify(completePage));
+ return;
+ }
+
+ const escaped = JSON.stringify(completePage)
+ .replaceAll('&', '&')
+ .replaceAll('"', '"')
+ .replaceAll('<', '<');
+ response.setHeader('Content-Type', 'text/html');
+ response.end(
+ await server.transformIndexHtml(
+ request.url,
+ '
',
+ ),
+ );
+ });
+ await server.listen();
+ console.log('User management fixture ready.');
+} else {
+ try {
+ const indexHtml = await render(
+ '/resources/js/pages/admin/users/Index.vue',
+ pages.index,
+ );
+ assert.match(indexHtml, /Workspace administration/);
+ assert.match(indexHtml, /User management/);
+ assert.match(indexHtml, /Workspace users/);
+ assert.match(indexHtml, /Global administrators/);
+ assert.match(indexHtml, /Password changes pending/);
+ assert.equal(indexHtml.match(/data-managed-user/g)?.length, 2);
+ assert.match(indexHtml, /Alex Admin/);
+ assert.match(indexHtml, /Morgan Member/);
+ assert.match(indexHtml, /Manage access/);
+ assert.match(indexHtml, /Edit account details for Morgan Member/);
+ assert.match(indexHtml, /Password change required/);
+ assert.match(indexHtml, /No direct global permissions/);
+
+ const createHtml = await render(
+ '/resources/js/pages/admin/users/Create.vue',
+ pages.create,
+ );
+ assert.match(createHtml, /Create user/);
+ assert.match(createHtml, /Temporary password/);
+ assert.match(createHtml, /Confirm password/);
+ assert.match(createHtml, /Require a password change on next sign-in/);
+ assert.match(createHtml, /data-test="managed-user-submit"/);
+ assert.match(createHtml, /managed-user-password-confirmation-error/);
+
+ const editHtml = await render(
+ '/resources/js/pages/admin/users/Edit.vue',
+ pages.edit,
+ );
+ assert.match(editHtml, /Edit Morgan Member/);
+ assert.match(editHtml, /morgan@example\.test/);
+ assert.match(editHtml, /Leave both password fields empty/);
+ assert.match(editHtml, /Review changes/);
+ assert.match(editHtml, /value="Morgan Member"/);
+ assert.match(editHtml, /value="morgan@example\.test"/);
+
+ console.log('User management rendering checks passed.');
+ } finally {
+ await server.close();
+ }
+}