|
41 | 41 | </NcDialog> |
42 | 42 | </template> |
43 | 43 |
|
44 | | -<script> |
| 44 | +<script setup lang="ts"> |
| 45 | +import type { IUser } from '../../views/user-types.d.ts' |
| 46 | +import type { QuotaOption } from './userFormUtils.ts' |
| 47 | +
|
45 | 48 | import { showError, showSuccess } from '@nextcloud/dialogs' |
| 49 | +import { translate as t } from '@nextcloud/l10n' |
46 | 50 | import { confirmPassword } from '@nextcloud/password-confirmation' |
| 51 | +import { computed, provide, reactive, ref } from 'vue' |
47 | 52 | import NcButton from '@nextcloud/vue/components/NcButton' |
48 | 53 | import NcDialog from '@nextcloud/vue/components/NcDialog' |
49 | 54 | import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' |
50 | 55 | import UserFormFields from './UserFormFields.vue' |
51 | 56 | import logger from '../../logger.ts' |
| 57 | +import { useStore } from '../../store/index.js' |
| 58 | +import { formDataKey } from './injectionKeys.ts' |
52 | 59 | import { diffPayload, userToFormData } from './userFormUtils.ts' |
53 | 60 |
|
54 | | -export default { |
55 | | - name: 'EditUserDialog', |
56 | | -
|
57 | | - components: { |
58 | | - NcButton, |
59 | | - NcDialog, |
60 | | - NcLoadingIcon, |
61 | | - UserFormFields, |
| 61 | +const props = defineProps<{ |
| 62 | + /** The user being edited */ |
| 63 | + user: IUser |
| 64 | + /** Quota preset options for the quota select */ |
| 65 | + quotaOptions: QuotaOption[] |
| 66 | +}>() |
| 67 | +
|
| 68 | +const emit = defineEmits<{ |
| 69 | + closing: [] |
| 70 | +}>() |
| 71 | +
|
| 72 | +const store = useStore() |
| 73 | +
|
| 74 | +const allGroups = store.getters.getGroups |
| 75 | +const serverLanguages = store.getters.getServerData.languages |
| 76 | +const formData = userToFormData(props.user, allGroups, props.quotaOptions, serverLanguages) |
| 77 | +
|
| 78 | +/** Snapshot of initial state for diffing */ |
| 79 | +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. |
| 82 | +const editedUser = reactive(formData) |
| 83 | +const saving = ref(false) |
| 84 | +const fieldErrors = ref<Record<string, string>>({}) |
| 85 | +
|
| 86 | +// Children inject editedUser and mutate its properties via v-model. |
| 87 | +provide(formDataKey, editedUser) |
| 88 | +
|
| 89 | +const settings = computed(() => store.getters.getServerData) |
| 90 | +
|
| 91 | +const fieldConfig = computed(() => ({ |
| 92 | + username: { |
| 93 | + show: true, |
| 94 | + disabled: true, |
| 95 | + label: t('settings', 'Account name'), |
62 | 96 | }, |
63 | 97 |
|
64 | | - // Children inject this reactive object and mutate its properties via v-model. |
65 | | - // Do not reassign editedUser entirely, the injected reference would go stale. |
66 | | - provide() { |
67 | | - return { |
68 | | - formData: this.editedUser, |
69 | | - } |
| 98 | + password: { |
| 99 | + show: settings.value.canChangePassword && props.user.backendCapabilities.setPassword, |
| 100 | + label: t('settings', 'New password'), |
70 | 101 | }, |
| 102 | +})) |
| 103 | +
|
| 104 | +/** |
| 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. |
| 107 | + */ |
| 108 | +async function save() { |
| 109 | + // Guard against re-submit while a request is already running. The |
| 110 | + // button is only aria-disabled (not disabled), so it can still fire. |
| 111 | + if (saving.value) { |
| 112 | + return |
| 113 | + } |
| 114 | + fieldErrors.value = {} |
71 | 115 |
|
72 | | - props: { |
73 | | - user: { |
74 | | - type: Object, |
75 | | - required: true, |
76 | | - }, |
77 | | -
|
78 | | - quotaOptions: { |
79 | | - type: Array, |
80 | | - required: true, |
81 | | - }, |
82 | | - }, |
| 116 | + const payload = diffPayload(initialData, editedUser) |
| 117 | + if (Object.keys(payload).length === 0) { |
| 118 | + emit('closing') |
| 119 | + return |
| 120 | + } |
83 | 121 |
|
84 | | - emits: ['closing'], |
85 | | -
|
86 | | - data() { |
87 | | - const allGroups = this.$store.getters.getGroups |
88 | | - const serverLanguages = this.$store.getters.getServerData.languages |
89 | | - const formData = userToFormData(this.user, allGroups, this.quotaOptions, serverLanguages) |
90 | | - return { |
91 | | - /** Snapshot of initial state for diffing */ |
92 | | - initialData: structuredClone(formData), |
93 | | - /** Mutable form state */ |
94 | | - editedUser: formData, |
95 | | - saving: false, |
96 | | - fieldErrors: {}, |
| 122 | + saving.value = true |
| 123 | + try { |
| 124 | + await confirmPassword() |
| 125 | + await store.dispatch('editUserMultiField', { |
| 126 | + userid: props.user.id, |
| 127 | + payload, |
| 128 | + }) |
| 129 | + showSuccess(t('settings', 'Account updated')) |
| 130 | + emit('closing') |
| 131 | + } catch (error) { |
| 132 | + const errors = (error as { response?: { data?: { ocs?: { data?: { errors?: Record<string, string> } } } } }) |
| 133 | + .response?.data?.ocs?.data?.errors |
| 134 | + if (errors && typeof errors === 'object') { |
| 135 | + fieldErrors.value = errors |
| 136 | + } else { |
| 137 | + logger.error('Failed to update account', { error }) |
| 138 | + showError(t('settings', 'Failed to update account')) |
97 | 139 | } |
98 | | - }, |
99 | | -
|
100 | | - computed: { |
101 | | - settings() { |
102 | | - return this.$store.getters.getServerData |
103 | | - }, |
104 | | -
|
105 | | - fieldConfig() { |
106 | | - return { |
107 | | - username: { |
108 | | - show: true, |
109 | | - disabled: true, |
110 | | - label: t('settings', 'Account name'), |
111 | | - }, |
112 | | -
|
113 | | - password: { |
114 | | - show: this.settings.canChangePassword && this.user.backendCapabilities.setPassword, |
115 | | - label: t('settings', 'New password'), |
116 | | - }, |
117 | | - } |
118 | | - }, |
119 | | - }, |
120 | | -
|
121 | | - methods: { |
122 | | - async save() { |
123 | | - // Guard against re-submit while a request is already running. The |
124 | | - // button is only aria-disabled (not disabled), so it can still fire. |
125 | | - if (this.saving) { |
126 | | - return |
127 | | - } |
128 | | - this.fieldErrors = {} |
129 | | -
|
130 | | - const payload = diffPayload(this.initialData, this.editedUser) |
131 | | - if (Object.keys(payload).length === 0) { |
132 | | - this.$emit('closing') |
133 | | - return |
134 | | - } |
135 | | -
|
136 | | - this.saving = true |
137 | | - try { |
138 | | - await confirmPassword() |
139 | | - await this.$store.dispatch('editUserMultiField', { |
140 | | - userid: this.user.id, |
141 | | - payload, |
142 | | - }) |
143 | | - showSuccess(t('settings', 'Account updated')) |
144 | | - this.$emit('closing') |
145 | | - } catch (error) { |
146 | | - const errors = error.response?.data?.ocs?.data?.errors |
147 | | - if (errors && typeof errors === 'object') { |
148 | | - this.fieldErrors = errors |
149 | | - } else { |
150 | | - logger.error('Failed to update account', { error }) |
151 | | - showError(t('settings', 'Failed to update account')) |
152 | | - } |
153 | | - } finally { |
154 | | - this.saving = false |
155 | | - } |
156 | | - }, |
157 | | - }, |
| 140 | + } finally { |
| 141 | + saving.value = false |
| 142 | + } |
158 | 143 | } |
159 | 144 | </script> |
160 | 145 |
|
|
0 commit comments