Skip to content

Commit 5de9123

Browse files
committed
refactor(settings): migrate UserList to script setup
Signed-off-by: Peter Ringelmann <peter.ringelmann@nextcloud.com>
1 parent 5f05048 commit 5de9123

12 files changed

Lines changed: 257 additions & 403 deletions

apps/settings/src/components/UserList.vue

Lines changed: 223 additions & 281 deletions
Large diffs are not rendered by default.

apps/settings/src/components/Users/EditUserDialog.vue

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,7 @@ import { formDataKey } from './injectionKeys.ts'
5959
import { diffPayload, userToFormData } from './userFormUtils.ts'
6060
6161
const props = defineProps<{
62-
/** The user being edited */
6362
user: IUser
64-
/** Quota preset options for the quota select */
6563
quotaOptions: QuotaOption[]
6664
}>()
6765
@@ -75,15 +73,13 @@ const allGroups = store.getters.getGroups
7573
const serverLanguages = store.getters.getServerData.languages
7674
const formData = userToFormData(props.user, allGroups, props.quotaOptions, serverLanguages)
7775
78-
/** Snapshot of initial state for diffing */
7976
const initialData = structuredClone(formData)
80-
// Children inject this reactive object and mutate its properties via v-model.
81-
// Do not reassign editedUser entirely, the injected reference would go stale.
77+
// Children inject and mutate this object's properties; never reassign it or the
78+
// injected reference goes stale.
8279
const editedUser = reactive(formData)
8380
const saving = ref(false)
8481
const fieldErrors = ref<Record<string, string>>({})
8582
86-
// Children inject editedUser and mutate its properties via v-model.
8783
provide(formDataKey, editedUser)
8884
8985
const settings = computed(() => store.getters.getServerData)
@@ -102,8 +98,7 @@ const fieldConfig = computed(() => ({
10298
}))
10399
104100
/**
105-
* Diff the form against its initial snapshot and submit only changed fields.
106-
* Maps a 422 response to per-field errors; closes the dialog on success or no-op.
101+
* Submit the changed fields; map a 422 to per-field errors, else close.
107102
*/
108103
async function save() {
109104
// Guard against re-submit while a request is already running. The

apps/settings/src/components/Users/NewUserDialog.vue

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,8 @@ import { useStore } from '../../store/index.js'
5454
import { formDataKey } from './injectionKeys.ts'
5555
5656
const props = defineProps<{
57-
/** Shared loading flags owned by UserList */
5857
loading: { all: boolean }
59-
/** The reactive new-user form state owned by UserList */
6058
newUser: FormData
61-
/** Quota preset options for the quota select */
6259
quotaOptions: QuotaOption[]
6360
}>()
6461
@@ -68,11 +65,10 @@ const emit = defineEmits<{
6865
6966
const store = useStore()
7067
71-
/** Template ref to UserFormFields, used to focus a field on mount / on error */
7268
const fields = ref<{ focusField: (name: 'username' | 'password') => void } | null>(null)
7369
74-
// Children inject this reactive object and mutate its properties via v-model.
75-
// Do not reassign newUser entirely, the injected reference would go stale.
70+
// Children inject and mutate this object's properties; never reassign it or the
71+
// injected reference goes stale.
7672
provide(formDataKey, props.newUser)
7773
7874
const settings = computed(() => store.getters.getServerData)
@@ -81,11 +77,6 @@ const usernameLabel = computed(() => settings.value.newUserGenerateUserID
8177
? t('settings', 'Account name will be autogenerated')
8278
: t('settings', 'Account name (required)'))
8379
84-
/**
85-
* Reactive field configuration passed to UserFormFields.
86-
* Controls visibility, labels, and required state for each field
87-
* based on the current form values and server settings.
88-
*/
8980
const fieldConfig = computed(() => ({
9081
username: {
9182
show: true,
@@ -116,8 +107,7 @@ const fieldConfig = computed(() => ({
116107
onMounted(() => fields.value?.focusField('username'))
117108
118109
/**
119-
* Create the account from the current form state. On a known 4xx, focus the
120-
* offending field (102 = username taken, 107 = password policy).
110+
* Create the account from the current form state.
121111
*/
122112
async function createUser() {
123113
// Guard against re-submit while a request is already running. The
@@ -146,8 +136,10 @@ async function createUser() {
146136
.response?.data?.ocs?.meta
147137
if (meta) {
148138
if (meta.statuscode === 102) {
139+
// Username already taken.
149140
fields.value?.focusField('username')
150141
} else if (meta.statuscode === 107) {
142+
// Password policy rejected.
151143
fields.value?.focusField('password')
152144
}
153145
}

apps/settings/src/components/Users/UserFormFields.vue

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -113,28 +113,16 @@ import UserFormQuota from './UserFormQuota.vue'
113113
import { useStore } from '../../store/index.js'
114114
import { formDataKey } from './injectionKeys.ts'
115115
116-
/** Per-field configuration for visibility, labels, and required state */
117116
interface FieldConfig {
118117
username?: { show?: boolean, disabled?: boolean, label?: string, required?: boolean }
119118
password?: { show?: boolean, label?: string, required?: boolean }
120119
email?: { label?: string, required?: boolean }
121120
showPasswordEmailHint?: boolean
122121
}
123122
124-
/**
125-
* Shared form fields for creating and editing user accounts.
126-
*
127-
* Injects a reactive `formData` object (provided by the parent dialog)
128-
* and binds directly to its properties via v-model. Complex field logic
129-
* (groups, quota, language, manager) is delegated to dedicated sub-components
130-
* that also inject the same formData.
131-
*/
132123
const props = withDefaults(defineProps<{
133-
/** Quota preset options for the quota select */
134124
quotaOptions: QuotaOption[]
135-
/** Per-field configuration; only fields differing from defaults need specifying */
136125
fieldConfig?: FieldConfig
137-
/** Per-field error messages from 422 validation (e.g. { email: 'Invalid' }) */
138126
errors?: Record<string, string>
139127
}>(), {
140128
fieldConfig: () => ({}),
@@ -143,24 +131,21 @@ const props = withDefaults(defineProps<{
143131
144132
const store = useStore()
145133
146-
/** Shared, reactive form state provided by the parent dialog */
147134
const formData = inject(formDataKey)!
148135
149-
/** Template refs used by the parent dialog to focus a field on error */
150136
const username = ref<{ focus?: () => void } | null>(null)
151137
const password = ref<{ focus?: () => void } | null>(null)
152138
153139
const minPasswordLength = computed(() => store.getters.getPasswordPolicyMinLength)
154140
155-
/** Errors not bound to a dedicated input, surfaced in the catch-all live region */
141+
// Errors not bound to a dedicated input, shown in the catch-all live region.
156142
const unhandledErrors = computed(() => {
157143
const handled = new Set(['displayName', 'password', 'email'])
158144
return Object.fromEntries(Object.entries(props.errors).filter(([key]) => !handled.has(key)))
159145
})
160146
161147
/**
162-
* Focus a named field. Called by the parent dialog (e.g. on 422 to focus the
163-
* offending input, or on mount to focus the username).
148+
* Focus a field. Exposed so the parent dialog can call it on mount or on a 422.
164149
*
165150
* @param name The field to focus
166151
*/

apps/settings/src/components/Users/UserFormGroups.vue

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,12 @@ import { isSelectableGroup } from './userFormUtils.ts'
5656
5757
const store = useStore()
5858
59-
/** Shared, reactive form state provided by the parent dialog */
6059
const formData = inject(formDataKey)!
6160
62-
/** True while a freshly tagged group is being created (disables the selects) */
6361
const creatingGroup = ref(false)
64-
/** In-flight group search, kept so a new search can cancel the previous one */
62+
// Kept so a new search can cancel the in-flight one.
6563
let promise: ReturnType<typeof searchGroupsApi> | null = null
6664
67-
/** Server settings for the current user (admin/delegated-admin flags) */
6865
const settings = computed(() => store.getters.getServerData)
6966
7067
const availableGroups = computed(() => {
@@ -85,7 +82,7 @@ const groupsLabel = computed(() => !settings.value.isAdmin && !settings.value.is
8582
* Search groups from the backend and add them to the store.
8683
*
8784
* @param query The current search string
88-
* @param toggleLoading NcSelect callback to toggle its loading spinner
85+
* @param toggleLoading NcSelect callback to toggle its spinner
8986
*/
9087
async function searchGroups(query: string, toggleLoading: (loading: boolean) => void) {
9188
if (!settings.value.isAdmin && !settings.value.isDelegatedAdmin) {
@@ -109,7 +106,7 @@ async function searchGroups(query: string, toggleLoading: (loading: boolean) =>
109106
}
110107
111108
/**
112-
* Create a new group from a tagged option and add it to the selection.
109+
* Create a tagged group and add it to the selection.
113110
*
114111
* @param option The created NcSelect option
115112
* @param option.name The new group id/name

apps/settings/src/components/Users/UserFormLanguage.vue

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,10 @@ import { languageFilterBy } from './userFormUtils.ts'
3030
3131
const store = useStore()
3232
33-
/** Shared, reactive form state provided by the parent dialog */
3433
const formData = inject(formDataKey)!
3534
36-
/** Per-admin UI flags from the store (controls language field visibility) */
3735
const showConfig = computed(() => store.getters.getShowConfig)
3836
39-
/** Grouped options: a section header followed by its languages, twice */
4037
const languages = computed(() => {
4138
const { commonLanguages, otherLanguages } = store.getters.getServerData.languages
4239
return [

apps/settings/src/components/Users/UserFormManager.vue

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,14 @@ import { formDataKey } from './injectionKeys.ts'
2929
3030
const store = useStore()
3131
32-
/** Shared, reactive form state provided by the parent dialog */
3332
const formData = inject(formDataKey)!
3433
3534
const possibleManagers = ref<Array<{ id: string, displayname?: string, email?: string }>>([])
3635
const loading = ref(false)
3736
let searchTimeout: ReturnType<typeof setTimeout> | undefined
3837
let managerModelCache: NcSelectUsersModel | undefined
3938
40-
/**
41-
* Map internal formData.manager to the NcSelectUsers model shape.
42-
* Cached to keep object identity stable across reads, so NcSelectUsers
43-
* doesn't see a fresh modelValue on every parent re-render.
44-
*/
39+
// Cache by value so NcSelectUsers keeps a stable modelValue reference across re-renders.
4540
const managerModel = computed<NcSelectUsersModel | undefined>(() => {
4641
const m = formData.manager
4742
if (!m) {
@@ -56,7 +51,6 @@ const managerModel = computed<NcSelectUsersModel | undefined>(() => {
5651
return managerModelCache
5752
})
5853
59-
/** Map API users to the NcSelectUsers model shape */
6054
const managerOptions = computed<NcSelectUsersModel[]>(() => possibleManagers.value.map((u) => ({
6155
id: u.id,
6256
displayName: u.displayname ?? u.id,
@@ -66,9 +60,9 @@ const managerOptions = computed<NcSelectUsersModel[]>(() => possibleManagers.val
6660
onBeforeUnmount(() => clearTimeout(searchTimeout))
6761
6862
/**
69-
* Map the NcSelectUsers model back to the internal formData shape
63+
* Write the selected manager back to formData.
7064
*
71-
* @param value The selected manager model, or null when cleared
65+
* @param value The selected model, or null when cleared
7266
*/
7367
function onManagerChange(value: NcSelectUsersModel | NcSelectUsersModel[] | null) {
7468
const manager = Array.isArray(value) ? value[0] : value
@@ -78,7 +72,7 @@ function onManagerChange(value: NcSelectUsersModel | NcSelectUsersModel[] | null
7872
}
7973
8074
/**
81-
* Debounce keystrokes so a 10-char query produces 1-2 requests, not 10.
75+
* Debounce the search so a 10-char query produces 1-2 requests, not 10.
8276
*
8377
* @param query The current search string
8478
*/
@@ -88,7 +82,7 @@ function searchUserManager(query: string) {
8882
}
8983
9084
/**
91-
* Fetch matching users from the store to populate the manager dropdown.
85+
* Fetch matching users to populate the manager dropdown.
9286
*
9387
* @param query The current search string
9488
*/

apps/settings/src/components/Users/UserFormQuota.vue

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,15 @@ import { formDataKey } from './injectionKeys.ts'
2727
import { validateQuota as validateQuotaOption } from './userFormUtils.ts'
2828
2929
const props = defineProps<{
30-
/** Quota preset options; the first entry is the fallback for invalid input */
3130
quotaOptions: QuotaOption[]
3231
}>()
3332
34-
/** Shared, reactive form state provided by the parent dialog */
3533
const formData = inject(formDataKey)!
3634
3735
/**
38-
* Wraps the pure validator so NcSelect's create-option callback receives the
39-
* preset fallback (first option) for unparseable quota strings.
36+
* Validate a typed quota, falling back to the first preset when unparseable.
4037
*
41-
* @param quota Raw quota string entered by the user
38+
* @param quota The raw quota string entered in the select
4239
*/
4340
function validateQuota(quota: string) {
4441
return validateQuotaOption(quota, props.quotaOptions[0])

apps/settings/src/components/Users/UserRow.vue

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -146,22 +146,15 @@ import { useStore } from '../../store/index.js'
146146
import { isObfuscated as isObfuscatedUser } from '../../utils/userUtils.ts'
147147
148148
const props = withDefaults(defineProps<{
149-
/** The user this row renders */
150149
user: IUser
151-
/** Whether the row is within the rendered viewport window */
152150
visible: boolean
153-
/** All loaded users (passed through from the list) */
154151
users: IUser[]
155-
/** Quota preset options */
156152
quotaOptions: QuotaOption[]
157-
/** Grouped language options */
158153
languages: { languages: LanguageOption[] }[]
159-
/** Server settings for the current admin (loose until the store is typed) */
154+
// settings is loose until the store is typed.
160155
// eslint-disable-next-line @typescript-eslint/no-explicit-any
161156
settings: Record<string, any>
162-
/** Extra row actions contributed by other apps */
163157
externalActions?: { icon: string, text: string, action: (...args: unknown[]) => void }[]
164-
/** Callback from UserList to open the edit dialog */
165158
onEditUser?: ((user: IUser) => void) | null
166159
}>(), {
167160
externalActions: () => [],
@@ -318,15 +311,17 @@ const userActions = computed(() => {
318311
return actions.concat(props.externalActions)
319312
})
320313
321-
/** Open the edit dialog for this user via the list-provided callback */
314+
/**
315+
* Open the edit dialog via the list-provided callback.
316+
*/
322317
function toggleEdit() {
323318
if (props.onEditUser) {
324319
props.onEditUser(props.user)
325320
}
326321
}
327322
328323
/**
329-
* Confirm and remotely wipe all devices associated with this account.
324+
* Confirm and remotely wipe the account's devices.
330325
*/
331326
async function wipeUserDevices() {
332327
const userid = props.user.id
@@ -361,7 +356,7 @@ async function wipeUserDevices() {
361356
}
362357
363358
/**
364-
* Confirm and fully delete this account and its data.
359+
* Confirm and fully delete the account and its data.
365360
*/
366361
async function deleteUser() {
367362
const userid = props.user.id
@@ -391,7 +386,7 @@ async function deleteUser() {
391386
}
392387
393388
/**
394-
* Toggle this account's enabled state.
389+
* Toggle the account's enabled state.
395390
*/
396391
function enableDisableUser() {
397392
loading.delete = true
@@ -409,7 +404,7 @@ function enableDisableUser() {
409404
}
410405
411406
/**
412-
* Resend the welcome email to this account.
407+
* Resend the welcome email to the account.
413408
*/
414409
function sendWelcomeMail() {
415410
loading.all = true

apps/settings/src/components/Users/UserRowActions.vue

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,14 @@ interface UserAction {
5050
}
5151
5252
const props = defineProps<{
53-
/** Row action descriptors; the optional `enabled` predicate filters them per user */
5453
actions: readonly UserAction[]
55-
/** Disables all actions (e.g. while a request is pending) */
5654
disabled: boolean
57-
/** The user the actions operate on */
5855
user: Record<string, unknown>
5956
}>()
6057
6158
defineEmits<{
6259
'update:edit': [value: boolean]
6360
}>()
6461
65-
/** Actions whose optional `enabled(user)` predicate passes for this user */
6662
const enabledActions = computed<UserAction[]>(() => props.actions.filter((action) => typeof action.enabled === 'function' ? action.enabled(props.user) : true))
6763
</script>

0 commit comments

Comments
 (0)