diff options
Diffstat (limited to 'apps/settings/src/components')
13 files changed, 162 insertions, 60 deletions
diff --git a/apps/settings/src/components/AdminAI.vue b/apps/settings/src/components/AdminAI.vue index afa08cd676e..044ebd9183e 100644 --- a/apps/settings/src/components/AdminAI.vue +++ b/apps/settings/src/components/AdminAI.vue @@ -6,6 +6,11 @@ <div class="ai-settings"> <NcSettingsSection :name="t('settings', 'Unified task processing')" :description="t('settings', 'AI tasks can be implemented by different apps. Here you can set which app should be used for which task.')"> + <NcCheckboxRadioSwitch v-model="settings['ai.taskprocessing_guests']" + type="switch" + @update:modelValue="saveChanges"> + {{ t('settings', 'Allow AI usage for guest users') }} + </NcCheckboxRadioSwitch> <template v-for="type in taskProcessingTaskTypes"> <div :key="type"> <h3>{{ t('settings', 'Task:') }} {{ type.name }}</h3> diff --git a/apps/settings/src/components/AdminSettingsSharingForm.vue b/apps/settings/src/components/AdminSettingsSharingForm.vue index 25dcbb7972d..1973781edee 100644 --- a/apps/settings/src/components/AdminSettingsSharingForm.vue +++ b/apps/settings/src/components/AdminSettingsSharingForm.vue @@ -27,6 +27,15 @@ :label="t('settings', 'Ignore the following groups when checking group membership')" style="width: 100%" /> </div> + <NcCheckboxRadioSwitch :checked.sync="settings.allowViewWithoutDownload"> + {{ t('settings', 'Allow users to preview files even if download is disabled') }} + </NcCheckboxRadioSwitch> + <NcNoteCard v-show="settings.allowViewWithoutDownload" + id="settings-sharing-api-view-without-download-hint" + class="sharing__note" + type="warning"> + {{ t('settings', 'Users will still be able to screenshot or record the screen. This does not provide any definitive protection.') }} + </NcNoteCard> </div> <div v-show="settings.enabled" id="settings-sharing-api" class="sharing__section"> @@ -258,6 +267,7 @@ interface IShareSettings { remoteExpireAfterNDays: string enforceRemoteExpireDate: boolean allowCustomTokens: boolean + allowViewWithoutDownload: boolean } export default defineComponent({ diff --git a/apps/settings/src/components/AppList.vue b/apps/settings/src/components/AppList.vue index cfc778fe409..3e40e08b257 100644 --- a/apps/settings/src/components/AppList.vue +++ b/apps/settings/src/components/AppList.vue @@ -200,9 +200,13 @@ export default { const apps = [...this.$store.getters.getAllApps, ...exApps] .filter(app => app.name.toLowerCase().search(this.search.toLowerCase()) !== -1) .sort(function(a, b) { - const sortStringA = '' + (a.active ? 0 : 1) + (a.update ? 0 : 1) + a.name - const sortStringB = '' + (b.active ? 0 : 1) + (b.update ? 0 : 1) + b.name - return OC.Util.naturalSortCompare(sortStringA, sortStringB) + const natSortDiff = OC.Util.naturalSortCompare(a, b) + if (natSortDiff === 0) { + const sortStringA = '' + (a.active ? 0 : 1) + (a.update ? 0 : 1) + const sortStringB = '' + (b.active ? 0 : 1) + (b.update ? 0 : 1) + return Number(sortStringA) - Number(sortStringB) + } + return natSortDiff }) if (this.category === 'installed') { diff --git a/apps/settings/src/components/AppNavigationGroupList.vue b/apps/settings/src/components/AppNavigationGroupList.vue index b32a07bc9b8..2be7cce2f8b 100644 --- a/apps/settings/src/components/AppNavigationGroupList.vue +++ b/apps/settings/src/components/AppNavigationGroupList.vue @@ -42,7 +42,7 @@ <NcAppNavigationList class="account-management__group-list" aria-describedby="group-list-desc" data-cy-users-settings-navigation-groups="custom"> - <GroupListItem v-for="group in userGroups" + <GroupListItem v-for="group in filteredGroups" :id="group.id" ref="groupListItems" :key="group.id" @@ -57,12 +57,16 @@ </template> <script setup lang="ts"> +import type CancelablePromise from 'cancelable-promise' +import type { IGroup } from '../views/user-types.d.ts' + +import { mdiAccountGroup, mdiPlus } from '@mdi/js' +import { showError } from '@nextcloud/dialogs' +import { t } from '@nextcloud/l10n' +import { useElementVisibility } from '@vueuse/core' import { computed, ref, watch, onBeforeMount } from 'vue' import { Fragment } from 'vue-frag' import { useRoute, useRouter } from 'vue-router/composables' -import { useElementVisibility } from '@vueuse/core' -import { showError } from '@nextcloud/dialogs' -import { mdiAccountGroup, mdiPlus } from '@mdi/js' import NcActionInput from '@nextcloud/vue/components/NcActionInput' import NcActionText from '@nextcloud/vue/components/NcActionText' @@ -92,7 +96,11 @@ const selectedGroup = computed(() => route.params?.selectedGroup) /** Current active group - URL decoded */ const selectedGroupDecoded = computed(() => selectedGroup.value ? decodeURIComponent(selectedGroup.value) : null) /** All available groups */ -const groups = computed(() => store.getters.getSortedGroups) +const groups = computed(() => { + return isAdminOrDelegatedAdmin.value + ? store.getters.getSortedGroups + : store.getters.getSubAdminGroups +}) /** User groups */ const { userGroups } = useFormatGroups(groups) /** Server settings for current user */ @@ -115,6 +123,14 @@ const loadingGroups = ref(false) const offset = ref(0) /** Search query for groups */ const groupsSearchQuery = ref('') +const filteredGroups = computed(() => { + if (isAdminOrDelegatedAdmin.value) { + return userGroups.value + } + + const substring = groupsSearchQuery.value.toLowerCase() + return userGroups.value.filter(group => group.id.toLowerCase().search(substring) !== -1 || group.title.toLowerCase().search(substring) !== -1) +}) const groupListItems = ref([]) const lastGroupListItem = computed(() => { @@ -137,12 +153,16 @@ watch(groupsSearchQuery, async () => { }) /** Cancelable promise for search groups request */ -const promise = ref(null) +const promise = ref<CancelablePromise<IGroup[]>>() /** * Load groups */ async function loadGroups() { + if (!isAdminOrDelegatedAdmin.value) { + return + } + if (promise.value) { promise.value.cancel() } @@ -163,7 +183,7 @@ async function loadGroups() { } catch (error) { logger.error(t('settings', 'Failed to load groups'), { error }) } - promise.value = null + promise.value = undefined loadingGroups.value = false } diff --git a/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue b/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue index 334739337e1..9ee1680516e 100644 --- a/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue +++ b/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue @@ -119,6 +119,7 @@ import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection' import NcInputField from '@nextcloud/vue/components/NcInputField' import NcSelect from '@nextcloud/vue/components/NcSelect' import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' +import { confirmPassword } from '@nextcloud/password-confirmation' export default { name: 'DeclarativeSection', @@ -202,9 +203,19 @@ export default { } }, - updateDeclarativeSettingsValue(formField, value = null) { + async updateDeclarativeSettingsValue(formField, value = null) { try { - return axios.post(generateOcsUrl('settings/api/declarative/value'), { + let url = generateOcsUrl('settings/api/declarative/value') + if (formField?.sensitive === true) { + url = generateOcsUrl('settings/api/declarative/value-sensitive') + try { + await confirmPassword() + } catch (err) { + showError(t('settings', 'Password confirmation is required')) + return + } + } + return axios.post(url, { app: this.formApp, formId: this.form.id.replace(this.formApp + '_', ''), // Remove app prefix to send clean form id fieldId: formField.id, diff --git a/apps/settings/src/components/GroupListItem.vue b/apps/settings/src/components/GroupListItem.vue index 65d46136ec1..76088fa74db 100644 --- a/apps/settings/src/components/GroupListItem.vue +++ b/apps/settings/src/components/GroupListItem.vue @@ -13,7 +13,7 @@ </h2> <NcNoteCard type="warning" show-alert> - {{ t('settings', 'You are about to remove the group "{group}". The accounts will NOT be deleted.', { group: name }) }} + {{ t('settings', 'You are about to delete the group "{group}". The accounts will NOT be deleted.', { group: name }) }} </NcNoteCard> <div class="modal__button-row"> <NcButton type="secondary" @@ -62,7 +62,7 @@ <template #icon> <Delete :size="20" /> </template> - {{ t('settings', 'Remove group') }} + {{ t('settings', 'Delete group') }} </NcActionButton> </template> </NcAppNavigationItem> @@ -179,7 +179,7 @@ export default { await this.$store.dispatch('removeGroup', this.id) this.showRemoveGroupModal = false } catch (error) { - showError(t('settings', 'Failed to remove group "{group}"', { group: this.name })) + showError(t('settings', 'Failed to delete group "{group}"', { group: this.name })) } }, }, diff --git a/apps/settings/src/components/PersonalInfo/FediverseSection.vue b/apps/settings/src/components/PersonalInfo/FediverseSection.vue index 65400ce0e45..043fa6e64b9 100644 --- a/apps/settings/src/components/PersonalInfo/FediverseSection.vue +++ b/apps/settings/src/components/PersonalInfo/FediverseSection.vue @@ -19,7 +19,7 @@ import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js' import AccountPropertySection from './shared/AccountPropertySection.vue' -const { fediverse } = loadState<AccountProperties>('settings', 'personalInfoParameters', {}) +const { fediverse } = loadState<AccountProperties>('settings', 'personalInfoParameters') const value = ref({ ...fediverse }) const readable = NAME_READABLE_ENUM[fediverse.name] @@ -29,11 +29,18 @@ const readable = NAME_READABLE_ENUM[fediverse.name] * @param text The potential fediverse handle */ function onValidate(text: string): boolean { + // allow to clear the value + if (text === '') { + return true + } + + // check its in valid format const result = text.match(/^@?([^@/]+)@([^@/]+)$/) if (result === null) { return false } + // check its a valid URL try { return URL.parse(`https://${result[2]}/`) !== null } catch { diff --git a/apps/settings/src/components/PersonalInfo/PronounsSection.vue b/apps/settings/src/components/PersonalInfo/PronounsSection.vue index fb35b1800c5..e345cb8e225 100644 --- a/apps/settings/src/components/PersonalInfo/PronounsSection.vue +++ b/apps/settings/src/components/PersonalInfo/PronounsSection.vue @@ -8,16 +8,18 @@ :placeholder="randomPronounsPlaceholder" /> </template> -<script> -import { loadState } from '@nextcloud/initial-state' +<script lang="ts"> +import type { IAccountProperty } from '../../constants/AccountPropertyConstants.ts' +import { loadState } from '@nextcloud/initial-state' +import { t } from '@nextcloud/l10n' +import { defineComponent } from 'vue' import AccountPropertySection from './shared/AccountPropertySection.vue' +import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.ts' -import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js' - -const { pronouns } = loadState('settings', 'personalInfoParameters', {}) +const { pronouns } = loadState<{ pronouns: IAccountProperty }>('settings', 'personalInfoParameters') -export default { +export default defineComponent({ name: 'PronounsSection', components: { @@ -33,13 +35,13 @@ export default { computed: { randomPronounsPlaceholder() { const pronouns = [ - this.t('settings', 'she/her'), - this.t('settings', 'he/him'), - this.t('settings', 'they/them'), + t('settings', 'she/her'), + t('settings', 'he/him'), + t('settings', 'they/them'), ] const pronounsExample = pronouns[Math.floor(Math.random() * pronouns.length)] - return this.t('settings', `Your pronouns. E.g. ${pronounsExample}`, { pronounsExample }) + return t('settings', 'Your pronouns. E.g. {pronounsExample}', { pronounsExample }) }, }, -} +}) </script> diff --git a/apps/settings/src/components/PersonalInfo/TwitterSection.vue b/apps/settings/src/components/PersonalInfo/TwitterSection.vue index 802f96087c2..43d08f81e3f 100644 --- a/apps/settings/src/components/PersonalInfo/TwitterSection.vue +++ b/apps/settings/src/components/PersonalInfo/TwitterSection.vue @@ -19,7 +19,7 @@ import { ref } from 'vue' import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.ts' import AccountPropertySection from './shared/AccountPropertySection.vue' -const { twitter } = loadState<AccountProperties>('settings', 'personalInfoParameters', {}) +const { twitter } = loadState<AccountProperties>('settings', 'personalInfoParameters') const value = ref({ ...twitter }) const readable = NAME_READABLE_ENUM[twitter.name] @@ -29,6 +29,6 @@ const readable = NAME_READABLE_ENUM[twitter.name] * @param text The potential twitter handle */ function onValidate(text: string): boolean { - return text.match(/^@?([a-zA-Z0-9_]{2,15})$/) !== null + return text === '' || text.match(/^@?([a-zA-Z0-9_]{2,15})$/) !== null } </script> diff --git a/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue b/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue index 8cdde6b7d9a..d039641ec72 100644 --- a/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue +++ b/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue @@ -155,6 +155,7 @@ export default { methods: { async updateProperty(value) { try { + this.hasError = false const responseData = await savePrimaryAccountProperty( this.name, value, @@ -180,10 +181,8 @@ export default { this.isSuccess = true setTimeout(() => { this.isSuccess = false }, 2000) } else { - this.$emit('update:value', this.initialValue) handleError(error, errorMessage) this.hasError = true - setTimeout(() => { this.hasError = false }, 2000) } }, }, diff --git a/apps/settings/src/components/UserList.vue b/apps/settings/src/components/UserList.vue index 5d6bd5f04ee..84c204805cc 100644 --- a/apps/settings/src/components/UserList.vue +++ b/apps/settings/src/components/UserList.vue @@ -350,11 +350,13 @@ export default { setNewUserDefaultGroup(value) { // Is no value set, but user is a line manager we set their group as this is a requirement for line manager if (!value && !this.settings.isAdmin && !this.settings.isDelegatedAdmin) { + const groups = this.$store.getters.getSubAdminGroups // if there are multiple groups we do not know which to add, // so we cannot make the managers life easier by preselecting it. - if (this.groups.length === 1) { - value = this.groups[0].id + if (groups.length === 1) { + this.newUser.groups = [...groups] } + return } if (value) { diff --git a/apps/settings/src/components/Users/NewUserDialog.vue b/apps/settings/src/components/Users/NewUserDialog.vue index 3e50efc2072..ef401b565fa 100644 --- a/apps/settings/src/components/Users/NewUserDialog.vue +++ b/apps/settings/src/components/Users/NewUserDialog.vue @@ -61,6 +61,7 @@ :required="newUser.password === '' || settings.newUserRequireEmail" /> <div class="dialog__item"> <NcSelect class="dialog__select" + data-test="groups" :input-label="!settings.isAdmin && !settings.isDelegatedAdmin ? t('settings', 'Member of the following groups (required)') : t('settings', 'Member of the following groups')" :placeholder="t('settings', 'Set account groups')" :disabled="loading.groups || loading.all" @@ -69,7 +70,7 @@ label="name" :close-on-select="false" :multiple="true" - :taggable="true" + :taggable="settings.isAdmin || settings.isDelegatedAdmin" :required="!settings.isAdmin && !settings.isDelegatedAdmin" :create-option="(value) => ({ id: value, name: value, isCreating: true })" @search="searchGroups" @@ -85,7 +86,7 @@ :input-label="t('settings', 'Admin of the following groups')" :placeholder="t('settings', 'Set account as admin for …')" :disabled="loading.groups || loading.all" - :options="subAdminsGroups" + :options="availableGroups" :close-on-select="false" :multiple="true" label="name" @@ -178,7 +179,6 @@ export default { data() { return { - availableGroups: this.$store.getters.getSortedGroups.filter(group => group.id !== '__nc_internal_recent' && group.id !== 'disabled'), possibleManagers: [], // TRANSLATORS This string describes a manager in the context of an organization managerInputLabel: t('settings', 'Manager'), @@ -209,9 +209,12 @@ export default { return this.$store.getters.getPasswordPolicyMinLength }, - subAdminsGroups() { - // data provided php side - return this.availableGroups.filter(group => group.id !== 'admin' && group.id !== '__nc_internal_recent' && group.id !== 'disabled') + availableGroups() { + const groups = (this.settings.isAdmin || this.settings.isDelegatedAdmin) + ? this.$store.getters.getSortedGroups + : this.$store.getters.getSubAdminGroups + + return groups.filter(group => group.id !== '__nc_internal_recent' && group.id !== 'disabled') }, languages() { @@ -273,6 +276,11 @@ export default { }, async searchGroups(query, toggleLoading) { + if (!this.settings.isAdmin && !this.settings.isDelegatedAdmin) { + // managers cannot search for groups + return + } + if (this.promise) { this.promise.cancel() } @@ -284,7 +292,10 @@ export default { limit: 25, }) const groups = await this.promise - this.availableGroups = groups + // Populate store from server request + for (const group of groups) { + this.$store.commit('addGroup', group) + } } catch (error) { logger.error(t('settings', 'Failed to search groups'), { error }) } @@ -302,7 +313,6 @@ export default { this.loading.groups = true try { await this.$store.dispatch('addGroup', gid) - this.availableGroups.push({ id: gid, name: gid }) this.newUser.groups.push({ id: gid, name: gid }) } catch (error) { logger.error(t('settings', 'Failed to create group'), { error }) @@ -336,7 +346,7 @@ export default { const validQuota = OC.Util.computerFileSize(quota) if (validQuota !== null && validQuota >= 0) { // unify format output - quota = formatFileSize(parseFileSize(quota)) + quota = formatFileSize(parseFileSize(quota, true)) this.newUser.quota = { id: quota, label: quota } return this.newUser.quota } diff --git a/apps/settings/src/components/Users/UserRow.vue b/apps/settings/src/components/Users/UserRow.vue index 987cf84492a..43668725972 100644 --- a/apps/settings/src/components/Users/UserRow.vue +++ b/apps/settings/src/components/Users/UserRow.vue @@ -255,16 +255,17 @@ data-cy-user-list-input-manager :data-loading="loading.manager || undefined" :input-id="'manager' + uniqueId" - :close-on-select="true" :disabled="isLoadingField" - :append-to-body="false" :loading="loadingPossibleManagers || loading.manager" - label="displayname" :options="possibleManagers" :placeholder="managerLabel" + label="displayname" + :filterable="false" + :internal-search="false" + :clearable="true" @open="searchInitialUserManager" @search="searchUserManager" - @option:selected="updateUserManager" /> + @update:model-value="updateUserManager" /> </template> <span v-else-if="!isObfuscated"> {{ user.manager }} @@ -410,15 +411,35 @@ export default { return encodeURIComponent(this.user.id + this.rand) }, + availableGroups() { + const groups = (this.settings.isAdmin || this.settings.isDelegatedAdmin) + ? this.$store.getters.getSortedGroups + : this.$store.getters.getSubAdminGroups + + return groups.filter(group => group.id !== '__nc_internal_recent' && group.id !== 'disabled') + }, + + availableSubAdminGroups() { + return this.availableGroups.filter(group => group.id !== 'admin') + }, + userGroupsLabels() { return this.userGroups - .map(group => group.name ?? group.id) + .map(group => { + // Try to match with more extensive group data + const availableGroup = this.availableGroups.find(g => g.id === group.id) + return availableGroup?.name ?? group.name ?? group.id + }) .join(', ') }, userSubAdminGroupsLabels() { return this.userSubAdminGroups - .map(group => group.name ?? group.id) + .map(group => { + // Try to match with more extensive group data + const availableGroup = this.availableSubAdminGroups.find(g => g.id === group.id) + return availableGroup?.name ?? group.name ?? group.id + }) .join(', ') }, @@ -502,7 +523,6 @@ export default { return this.languages[0].languages.concat(this.languages[1].languages) }, }, - async beforeMount() { if (this.user.manager) { await this.initManager(this.user.manager) @@ -559,7 +579,11 @@ export default { this.loading.groupsDetails = true try { const groups = await loadUserGroups({ userId: this.user.id }) - this.availableGroups = this.availableGroups.map(availableGroup => groups.find(group => group.id === availableGroup.id) ?? availableGroup) + // Populate store from server request + for (const group of groups) { + this.$store.commit('addGroup', group) + } + this.selectedGroups = this.selectedGroups.map(selectedGroup => groups.find(group => group.id === selectedGroup.id) ?? selectedGroup) } catch (error) { logger.error(t('settings', 'Failed to load groups with details'), { error }) } @@ -572,7 +596,11 @@ export default { this.loading.subAdminGroupsDetails = true try { const groups = await loadUserSubAdminGroups({ userId: this.user.id }) - this.availableSubAdminGroups = this.availableSubAdminGroups.map(availableGroup => groups.find(group => group.id === availableGroup.id) ?? availableGroup) + // Populate store from server request + for (const group of groups) { + this.$store.commit('addGroup', group) + } + this.selectedSubAdminGroups = this.selectedSubAdminGroups.map(selectedGroup => groups.find(group => group.id === selectedGroup.id) ?? selectedGroup) } catch (error) { logger.error(t('settings', 'Failed to load sub admin groups with details'), { error }) } @@ -595,8 +623,10 @@ export default { limit: 25, }) const groups = await this.promise - this.availableGroups = groups - this.availableSubAdminGroups = groups.filter(group => group.id !== 'admin') + // Populate store from server request + for (const group of groups) { + this.$store.commit('addGroup', group) + } } catch (error) { logger.error(t('settings', 'Failed to search groups'), { error }) } @@ -613,11 +643,12 @@ export default { }) }, - async updateUserManager(manager) { - if (manager === null) { - this.currentManager = '' - } + async updateUserManager() { this.loading.manager = true + + // Store the current manager before making changes + const previousManager = this.user.manager + try { await this.$store.dispatch('setUserData', { userid: this.user.id, @@ -627,7 +658,10 @@ export default { } catch (error) { // TRANSLATORS This string describes a line manager in the context of an organization showError(t('settings', 'Failed to update line manager')) - console.error(error) + logger.error('Failed to update manager:', { error }) + + // Revert to the previous manager in the UI on error + this.currentManager = previousManager } finally { this.loading.manager = false } @@ -753,8 +787,6 @@ export default { this.loading.groups = true try { await this.$store.dispatch('addGroup', gid) - this.availableGroups.push({ id: gid, name: gid }) - this.availableSubAdminGroups.push({ id: gid, name: gid }) const userid = this.user.id await this.$store.dispatch('addUserGroup', { userid, gid }) this.userGroups.push({ id: gid, name: gid }) |