aboutsummaryrefslogtreecommitdiffstats
path: root/apps/settings/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'apps/settings/src/components')
-rw-r--r--apps/settings/src/components/AdminAI.vue131
-rw-r--r--apps/settings/src/components/AdminDelegating.vue6
-rw-r--r--apps/settings/src/components/AdminDelegation/GroupSelect.vue6
-rw-r--r--apps/settings/src/components/AdminSettingsSharingForm.vue148
-rw-r--r--apps/settings/src/components/AdminTwoFactor.vue15
-rw-r--r--apps/settings/src/components/AppAPI/DaemonSelectionDialog.vue41
-rw-r--r--apps/settings/src/components/AppAPI/DaemonSelectionEntry.vue77
-rw-r--r--apps/settings/src/components/AppAPI/DaemonSelectionList.vue77
-rw-r--r--apps/settings/src/components/AppList.vue79
-rw-r--r--apps/settings/src/components/AppList/AppDaemonBadge.vue37
-rw-r--r--apps/settings/src/components/AppList/AppItem.vue105
-rw-r--r--apps/settings/src/components/AppList/AppLevelBadge.vue29
-rw-r--r--apps/settings/src/components/AppList/AppScore.vue26
-rw-r--r--apps/settings/src/components/AppNavigationGroupList.vue220
-rw-r--r--apps/settings/src/components/AppStoreDiscover/AppLink.vue31
-rw-r--r--apps/settings/src/components/AppStoreDiscover/AppStoreDiscoverSection.vue14
-rw-r--r--apps/settings/src/components/AppStoreDiscover/AppType.vue23
-rw-r--r--apps/settings/src/components/AppStoreDiscover/CarouselType.vue12
-rw-r--r--apps/settings/src/components/AppStoreDiscover/PostType.vue35
-rw-r--r--apps/settings/src/components/AppStoreDiscover/ShowcaseType.vue23
-rw-r--r--apps/settings/src/components/AppStoreDiscover/common.ts21
-rw-r--r--apps/settings/src/components/AppStoreSidebar/AppDeployDaemonTab.vue50
-rw-r--r--apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue320
-rw-r--r--apps/settings/src/components/AppStoreSidebar/AppDescriptionTab.vue27
-rw-r--r--apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue143
-rw-r--r--apps/settings/src/components/AppStoreSidebar/AppReleasesTab.vue28
-rw-r--r--apps/settings/src/components/AuthToken.vue45
-rw-r--r--apps/settings/src/components/AuthTokenList.vue23
-rw-r--r--apps/settings/src/components/AuthTokenSection.vue23
-rw-r--r--apps/settings/src/components/AuthTokenSetup.vue31
-rw-r--r--apps/settings/src/components/AuthTokenSetupDialog.vue29
-rw-r--r--apps/settings/src/components/BasicSettings/BackgroundJob.vue38
-rw-r--r--apps/settings/src/components/BasicSettings/ProfileSettings.vue26
-rw-r--r--apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue95
-rw-r--r--apps/settings/src/components/Encryption.vue209
-rw-r--r--apps/settings/src/components/Encryption/EncryptionSettings.vue197
-rw-r--r--apps/settings/src/components/Encryption/EncryptionWarningDialog.vue91
-rw-r--r--apps/settings/src/components/Encryption/sharedTexts.ts7
-rw-r--r--apps/settings/src/components/GroupListItem.vue54
-rw-r--r--apps/settings/src/components/Markdown.cy.ts58
-rw-r--r--apps/settings/src/components/Markdown.vue98
-rw-r--r--apps/settings/src/components/PasswordSection.vue26
-rw-r--r--apps/settings/src/components/PersonalInfo/AvatarSection.vue32
-rw-r--r--apps/settings/src/components/PersonalInfo/BiographySection.vue23
-rw-r--r--apps/settings/src/components/PersonalInfo/BirthdaySection.vue132
-rw-r--r--apps/settings/src/components/PersonalInfo/BlueskySection.vue64
-rw-r--r--apps/settings/src/components/PersonalInfo/DetailsSection.vue38
-rw-r--r--apps/settings/src/components/PersonalInfo/DisplayNameSection.vue21
-rw-r--r--apps/settings/src/components/PersonalInfo/EmailSection/Email.vue45
-rw-r--r--apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue30
-rw-r--r--apps/settings/src/components/PersonalInfo/FediverseSection.vue74
-rw-r--r--apps/settings/src/components/PersonalInfo/FirstDayOfWeekSection.vue126
-rw-r--r--apps/settings/src/components/PersonalInfo/HeadlineSection.vue21
-rw-r--r--apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue25
-rw-r--r--apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue21
-rw-r--r--apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue25
-rw-r--r--apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue21
-rw-r--r--apps/settings/src/components/PersonalInfo/LocationSection.vue21
-rw-r--r--apps/settings/src/components/PersonalInfo/OrganisationSection.vue21
-rw-r--r--apps/settings/src/components/PersonalInfo/PhoneSection.vue25
-rw-r--r--apps/settings/src/components/PersonalInfo/ProfileSection/EditProfileAnchorLink.vue23
-rw-r--r--apps/settings/src/components/PersonalInfo/ProfileSection/ProfileCheckbox.vue25
-rw-r--r--apps/settings/src/components/PersonalInfo/ProfileSection/ProfilePreviewCard.vue35
-rw-r--r--apps/settings/src/components/PersonalInfo/ProfileSection/ProfileSection.vue23
-rw-r--r--apps/settings/src/components/PersonalInfo/ProfileVisibilitySection/ProfileVisibilitySection.vue23
-rw-r--r--apps/settings/src/components/PersonalInfo/ProfileVisibilitySection/VisibilityDropdown.vue27
-rw-r--r--apps/settings/src/components/PersonalInfo/PronounsSection.vue47
-rw-r--r--apps/settings/src/components/PersonalInfo/RoleSection.vue21
-rw-r--r--apps/settings/src/components/PersonalInfo/TwitterSection.vue56
-rw-r--r--apps/settings/src/components/PersonalInfo/WebsiteSection.vue21
-rw-r--r--apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue36
-rw-r--r--apps/settings/src/components/PersonalInfo/shared/FederationControl.vue33
-rw-r--r--apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue34
-rw-r--r--apps/settings/src/components/SelectSharingPermissions.vue23
-rw-r--r--apps/settings/src/components/SvgFilterMixin.vue23
-rw-r--r--apps/settings/src/components/UserList.vue84
-rw-r--r--apps/settings/src/components/Users/NewUserDialog.vue (renamed from apps/settings/src/components/Users/NewUserModal.vue)270
-rw-r--r--apps/settings/src/components/Users/UserListFooter.vue43
-rw-r--r--apps/settings/src/components/Users/UserListHeader.vue54
-rw-r--r--apps/settings/src/components/Users/UserRow.vue324
-rw-r--r--apps/settings/src/components/Users/UserRowActions.vue51
-rw-r--r--apps/settings/src/components/Users/UserSettingsDialog.vue93
-rw-r--r--apps/settings/src/components/Users/VirtualList.vue26
-rw-r--r--apps/settings/src/components/Users/shared/styles.scss35
-rw-r--r--apps/settings/src/components/WebAuthn/AddDevice.vue49
-rw-r--r--apps/settings/src/components/WebAuthn/Device.vue26
-rw-r--r--apps/settings/src/components/WebAuthn/Section.vue27
87 files changed, 2900 insertions, 2171 deletions
diff --git a/apps/settings/src/components/AdminAI.vue b/apps/settings/src/components/AdminAI.vue
index ea444fc25cf..0d3e9154bb9 100644
--- a/apps/settings/src/components/AdminAI.vue
+++ b/apps/settings/src/components/AdminAI.vue
@@ -1,5 +1,47 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <div>
+ <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>
+ <h3>{{ t('settings', 'Provider for Task types') }}</h3>
+ <template v-for="type in taskProcessingTaskTypes">
+ <div :key="type" class="tasktype-item">
+ <p class="tasktype-name">
+ {{ type.name }}
+ </p>
+ <NcCheckboxRadioSwitch v-model="settings['ai.taskprocessing_type_preferences'][type.id]"
+ type="switch"
+ @update:modelValue="saveChanges">
+ {{ t('settings', 'Enable') }}
+ </NcCheckboxRadioSwitch><NcSelect v-model="settings['ai.taskprocessing_provider_preferences'][type.id]"
+ class="provider-select"
+ :clearable="false"
+ :disabled="!settings['ai.taskprocessing_type_preferences'][type.id]"
+ :options="taskProcessingProviders.filter(p => p.taskType === type.id).map(p => p.id)"
+ @input="saveChanges">
+ <template #option="{label}">
+ {{ taskProcessingProviders.find(p => p.id === label)?.name }}
+ </template>
+ <template #selected-option="{label}">
+ {{ taskProcessingProviders.find(p => p.id === label)?.name }}
+ </template>
+ </NcSelect>
+ </div>
+ </template>
+ <template v-if="!hasTaskProcessing">
+ <NcNoteCard type="info">
+ {{ t('settings', 'None of your currently installed apps provide Task processing functionality') }}
+ </NcNoteCard>
+ </template>
+ </NcSettingsSection>
<NcSettingsSection :name="t('settings', 'Machine translation')"
:description="t('settings', 'Machine translation can be implemented by different apps. Here you can define the precedence of the machine translation apps you have installed at the moment.')">
<draggable v-model="settings['ai.translation_provider_preferences']" @change="saveChanges">
@@ -18,24 +60,6 @@
</div>
</draggable>
</NcSettingsSection>
- <NcSettingsSection :name="t('settings', 'Speech-To-Text')"
- :description="t('settings', 'Speech-To-Text can be implemented by different apps. Here you can set which app should be used.')">
- <template v-for="provider in sttProviders">
- <NcCheckboxRadioSwitch :key="provider.class"
- :checked.sync="settings['ai.stt_provider']"
- :value="provider.class"
- name="stt_provider"
- type="radio"
- @update:checked="saveChanges">
- {{ provider.name }}
- </NcCheckboxRadioSwitch>
- </template>
- <template v-if="!hasStt">
- <NcNoteCard type="info">
- {{ t('settings', 'None of your currently installed apps provide Speech-To-Text functionality') }}
- </NcNoteCard>
- </template>
- </NcSettingsSection>
<NcSettingsSection :name="t('settings', 'Image generation')"
:description="t('settings', 'Image generation can be implemented by different apps. Here you can set which app should be used.')">
<template v-for="provider in text2imageProviders">
@@ -58,10 +82,11 @@
:description="t('settings', 'Text processing tasks can be implemented by different apps. Here you can set which app should be used for which task.')">
<template v-for="type in tpTaskTypes">
<div :key="type">
- <h3>{{ t('settings', 'Task:') }} {{ getTaskType(type).name }}</h3>
- <p>{{ getTaskType(type).description }}</p>
+ <h3>{{ t('settings', 'Task:') }} {{ getTextProcessingTaskType(type).name }}</h3>
+ <p>{{ getTextProcessingTaskType(type).description }}</p>
<p>&nbsp;</p>
<NcSelect v-model="settings['ai.textprocessing_provider_preferences'][type]"
+ class="provider-select"
:clearable="false"
:options="textProcessingProviders.filter(p => p.taskType === type).map(p => p.class)"
@input="saveChanges">
@@ -75,9 +100,10 @@
<p>&nbsp;</p>
</div>
</template>
- <template v-if="!hasTextProcessing">
+ <template v-if="tpTaskTypes.length === 0">
<NcNoteCard type="info">
- {{ t('settings', 'None of your currently installed apps provide Text processing functionality') }}
+ <!-- TRANSLATORS Text processing is the name of a Nextcloud-internal API -->
+ {{ t('settings', 'None of your currently installed apps provide text processing functionality using the Text Processing API.') }}
</NcNoteCard>
</template>
</NcSettingsSection>
@@ -86,17 +112,17 @@
<script>
import axios from '@nextcloud/axios'
-import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
-import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
-import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
+import NcSelect from '@nextcloud/vue/components/NcSelect'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import draggable from 'vuedraggable'
import DragVerticalIcon from 'vue-material-design-icons/DragVertical.vue'
import ArrowDownIcon from 'vue-material-design-icons/ArrowDown.vue'
import ArrowUpIcon from 'vue-material-design-icons/ArrowUp.vue'
import { loadState } from '@nextcloud/initial-state'
-
+import { nextTick } from 'vue'
import { generateUrl } from '@nextcloud/router'
export default {
@@ -123,22 +149,32 @@ export default {
textProcessingProviders: loadState('settings', 'ai-text-processing-providers'),
textProcessingTaskTypes: loadState('settings', 'ai-text-processing-task-types'),
text2imageProviders: loadState('settings', 'ai-text2image-providers'),
+ taskProcessingProviders: loadState('settings', 'ai-task-processing-providers'),
+ taskProcessingTaskTypes: loadState('settings', 'ai-task-processing-task-types'),
settings: loadState('settings', 'ai-settings'),
}
},
computed: {
- hasStt() {
- return this.sttProviders.length > 0
- },
hasTextProcessing() {
return Object.keys(this.settings['ai.textprocessing_provider_preferences']).length > 0 && Array.isArray(this.textProcessingTaskTypes)
},
tpTaskTypes() {
- return Object.keys(this.settings['ai.textprocessing_provider_preferences']).filter(type => !!this.getTaskType(type))
+ const builtinTextProcessingTypes = [
+ 'OCP\\TextProcessing\\FreePromptTaskType',
+ 'OCP\\TextProcessing\\HeadlineTaskType',
+ 'OCP\\TextProcessing\\SummaryTaskType',
+ 'OCP\\TextProcessing\\TopicsTaskType',
+ ]
+ return Object.keys(this.settings['ai.textprocessing_provider_preferences'])
+ .filter(type => !!this.getTextProcessingTaskType(type))
+ .filter(type => !builtinTextProcessingTypes.includes(type))
},
hasText2ImageProviders() {
return this.text2imageProviders.length > 0
},
+ hasTaskProcessing() {
+ return Object.keys(this.settings['ai.taskprocessing_provider_preferences']).length > 0 && Array.isArray(this.taskProcessingTaskTypes)
+ },
},
methods: {
moveUp(i) {
@@ -159,6 +195,7 @@ export default {
},
async saveChanges() {
this.loading = true
+ await nextTick()
const data = { settings: this.settings }
try {
await axios.put(generateUrl('/settings/api/admin/ai'), data)
@@ -167,7 +204,7 @@ export default {
}
this.loading = false
},
- getTaskType(type) {
+ getTextProcessingTaskType(type) {
if (!Array.isArray(this.textProcessingTaskTypes)) {
return null
}
@@ -190,13 +227,31 @@ export default {
.draggable__number {
border-radius: 20px;
- border: 2px solid var(--color-primary-default);
- color: var(--color-primary-default);
- padding: 0px 7px;
- margin-right: 3px;
+ border: 2px solid var(--color-primary-element);
+ color: var(--color-primary-element);
+ padding: 0px 7px;
+ margin-inline-end: 3px;
}
.drag-vertical-icon {
float: left;
}
+
+.ai-settings h3 {
+ font-size: 16px; /* to offset against the 20px section heading */
+}
+
+.provider-select {
+ min-width: 350px !important;
+}
+
+.tasktype-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ .tasktype-name {
+ flex: 1;
+ margin: 0;
+ }
+}
</style>
diff --git a/apps/settings/src/components/AdminDelegating.vue b/apps/settings/src/components/AdminDelegating.vue
index c614b2bd2f4..521ff8f0155 100644
--- a/apps/settings/src/components/AdminDelegating.vue
+++ b/apps/settings/src/components/AdminDelegating.vue
@@ -1,3 +1,7 @@
+<!--
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<NcSettingsSection :name="t('settings', 'Administration privileges')"
:description="t('settings', 'Here you can decide which group can access certain sections of the administration settings.')"
@@ -13,7 +17,7 @@
<script>
import GroupSelect from './AdminDelegation/GroupSelect.vue'
-import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
+import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
import { loadState } from '@nextcloud/initial-state'
export default {
diff --git a/apps/settings/src/components/AdminDelegation/GroupSelect.vue b/apps/settings/src/components/AdminDelegation/GroupSelect.vue
index d77b68bb538..28d3deb0afa 100644
--- a/apps/settings/src/components/AdminDelegation/GroupSelect.vue
+++ b/apps/settings/src/components/AdminDelegation/GroupSelect.vue
@@ -1,3 +1,7 @@
+<!--
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<NcSelect v-model="selected"
:input-id="setting.id"
@@ -10,7 +14,7 @@
</template>
<script>
-import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
+import NcSelect from '@nextcloud/vue/components/NcSelect'
import { generateUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
diff --git a/apps/settings/src/components/AdminSettingsSharingForm.vue b/apps/settings/src/components/AdminSettingsSharingForm.vue
index 122b3dfc642..b0e142d8480 100644
--- a/apps/settings/src/components/AdminSettingsSharingForm.vue
+++ b/apps/settings/src/components/AdminSettingsSharingForm.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @author Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<form class="sharing">
@@ -44,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">
@@ -56,15 +48,19 @@
<NcCheckboxRadioSwitch :checked.sync="settings.allowPublicUpload">
{{ t('settings', 'Allow public uploads') }}
</NcCheckboxRadioSwitch>
+ <NcCheckboxRadioSwitch v-model="settings.allowFederationOnPublicShares">
+ {{ t('settings', 'Allow public shares to be added to other clouds by federation.') }}
+ {{ t('settings', 'This will add share permissions to all newly created link shares.') }}
+ </NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked.sync="settings.enableLinkPasswordByDefault">
{{ t('settings', 'Always ask for a password') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked.sync="settings.enforceLinksPassword" :disabled="!settings.enableLinkPasswordByDefault">
{{ t('settings', 'Enforce password protection') }}
</NcCheckboxRadioSwitch>
- <label v-if="settings.passwordExcludedGroupsFeatureEnabled" class="sharing__labeled-entry sharing__input">
+ <label v-if="settings.enforceLinksPasswordExcludedGroupsEnabled" class="sharing__labeled-entry sharing__input">
<span>{{ t('settings', 'Exclude groups from password requirements') }}</span>
- <NcSettingsSelectGroup v-model="settings.passwordExcludedGroups"
+ <NcSettingsSelectGroup v-model="settings.enforceLinksPasswordExcludedGroups"
style="width: 100%"
:disabled="!settings.enforceLinksPassword || !settings.enableLinkPasswordByDefault" />
</label>
@@ -76,21 +72,45 @@
</label>
</fieldset>
+ <NcCheckboxRadioSwitch type="switch"
+ aria-describedby="settings-sharing-custom-token-disable-hint settings-sharing-custom-token-access-hint"
+ :checked.sync="settings.allowCustomTokens">
+ {{ t('settings', 'Allow users to set custom share link tokens') }}
+ </NcCheckboxRadioSwitch>
+ <div class="sharing__sub-section">
+ <NcNoteCard id="settings-sharing-custom-token-disable-hint"
+ class="sharing__note"
+ type="info">
+ {{ t('settings', 'Shares with custom tokens will continue to be accessible after this setting has been disabled') }}
+ </NcNoteCard>
+ <NcNoteCard id="settings-sharing-custom-token-access-hint"
+ class="sharing__note"
+ type="warning">
+ {{ t('settings', 'Shares with guessable tokens may be accessed easily') }}
+ </NcNoteCard>
+ </div>
+
<label>{{ t('settings', 'Limit sharing based on groups') }}</label>
<div class="sharing__sub-section">
<NcCheckboxRadioSwitch :checked.sync="settings.excludeGroups"
- name="excludeGroups" value="no"
- type="radio" @update:checked="onUpdateExcludeGroups">
+ name="excludeGroups"
+ value="no"
+ type="radio"
+ @update:checked="onUpdateExcludeGroups">
{{ t('settings', 'Allow sharing for everyone (default)') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked.sync="settings.excludeGroups"
- name="excludeGroups" value="yes"
- type="radio" @update:checked="onUpdateExcludeGroups">
+ name="excludeGroups"
+ value="yes"
+ type="radio"
+ @update:checked="onUpdateExcludeGroups">
{{ t('settings', 'Exclude some groups from sharing') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked.sync="settings.excludeGroups"
- name="excludeGroups" value="allow"
- type="radio" @update:checked="onUpdateExcludeGroups">
+ name="excludeGroups"
+ value="allow"
+ type="radio"
+ @update:checked="onUpdateExcludeGroups">
{{ t('settings', 'Limit sharing to some groups') }}
</NcCheckboxRadioSwitch>
<div v-show="settings.excludeGroups !== 'no'" class="sharing__labeled-entry sharing__input">
@@ -107,7 +127,7 @@
<NcCheckboxRadioSwitch type="switch"
aria-controls="settings-sharing-api-expiration"
:checked.sync="settings.defaultInternalExpireDate">
- {{ t('settings', 'Set default expiration date for shares') }}
+ {{ t('settings', 'Set default expiration date for internal shares') }}
</NcCheckboxRadioSwitch>
<fieldset v-show="settings.defaultInternalExpireDate" id="settings-sharing-api-expiration" class="sharing__sub-section">
<NcCheckboxRadioSwitch :checked.sync="settings.enforceInternalExpireDate">
@@ -144,7 +164,7 @@
</NcCheckboxRadioSwitch>
<fieldset v-show="settings.allowLinks && settings.defaultExpireDate" id="settings-sharing-api-api-expiration" class="sharing__sub-section">
<NcCheckboxRadioSwitch :checked.sync="settings.enforceExpireDate">
- {{ t('settings', 'Enforce expiration date for remote shares') }}
+ {{ t('settings', 'Enforce expiration date for link or mail shares') }}
</NcCheckboxRadioSwitch>
<NcTextField type="number"
class="sharing__input"
@@ -160,17 +180,17 @@
<NcCheckboxRadioSwitch type="switch"
aria-controls="settings-sharing-privacy-user-enumeration"
:checked.sync="settings.allowShareDialogUserEnumeration">
- {{ t('settings', 'Allow username autocompletion in share dialog and allow access to the system address book') }}
+ {{ t('settings', 'Allow account name autocompletion in share dialog and allow access to the system address book') }}
</NcCheckboxRadioSwitch>
<fieldset v-show="settings.allowShareDialogUserEnumeration" id="settings-sharing-privacy-user-enumeration" class="sharing__sub-section">
<em>
{{ t('settings', 'If autocompletion "same group" and "phone number integration" are enabled a match in either is enough to show the user.') }}
</em>
<NcCheckboxRadioSwitch :checked.sync="settings.restrictUserEnumerationToGroup">
- {{ t('settings', 'Allow username autocompletion to users within the same groups and limit system address books to users in the same groups') }}
+ {{ t('settings', 'Restrict account name autocompletion and system address book access to users within the same groups') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch :checked.sync="settings.restrictUserEnumerationToPhone">
- {{ t('settings', 'Allow username autocompletion to users based on phone number integration') }}
+ {{ t('settings', 'Restrict account name autocompletion to users based on phone number integration') }}
</NcCheckboxRadioSwitch>
</fieldset>
@@ -181,7 +201,7 @@
<NcCheckboxRadioSwitch type="switch" :checked.sync="publicShareDisclaimerEnabled">
{{ t('settings', 'Show disclaimer text on the public link upload page (only shown when the file list is hidden)') }}
</NcCheckboxRadioSwitch>
- <div v-if="typeof settings.publicShareDisclaimerText === 'string'"
+ <div v-if="publicShareDisclaimerEnabled"
aria-describedby="settings-sharing-privary-related-disclaimer-hint"
class="sharing__sub-section">
<NcTextArea class="sharing__input"
@@ -203,19 +223,19 @@
</template>
<script lang="ts">
-import {
- NcCheckboxRadioSwitch,
- NcSettingsSelectGroup,
- NcTextArea,
- NcTextField,
-} from '@nextcloud/vue'
import { showError, showSuccess } from '@nextcloud/dialogs'
-import { translate as t } from '@nextcloud/l10n'
import { loadState } from '@nextcloud/initial-state'
+import { t } from '@nextcloud/l10n'
+import { snakeCase } from 'lodash'
import { defineComponent } from 'vue'
+import debounce from 'debounce'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+import NcSettingsSelectGroup from '@nextcloud/vue/components/NcSettingsSelectGroup'
+import NcTextArea from '@nextcloud/vue/components/NcTextArea'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
import SelectSharingPermissions from './SelectSharingPermissions.vue'
-import { snakeCase, debounce } from 'lodash'
interface IShareSettings {
enabled: boolean
@@ -225,6 +245,7 @@ interface IShareSettings {
allowPublicUpload: boolean
allowResharing: boolean
allowShareDialogUserEnumeration: boolean
+ allowFederationOnPublicShares: boolean
restrictUserEnumerationToGroup: boolean
restrictUserEnumerationToPhone: boolean
restrictUserEnumerationFullMatch: boolean
@@ -232,8 +253,8 @@ interface IShareSettings {
restrictUserEnumerationFullMatchEmail: boolean
restrictUserEnumerationFullMatchIgnoreSecondDN: boolean
enforceLinksPassword: boolean
- passwordExcludedGroups: string[]
- passwordExcludedGroupsFeatureEnabled: boolean
+ enforceLinksPasswordExcludedGroups: string[]
+ enforceLinksPasswordExcludedGroupsEnabled: boolean
onlyShareWithGroupMembers: boolean
onlyShareWithGroupMembersExcludeGroupList: string[]
defaultExpireDate: boolean
@@ -241,7 +262,7 @@ interface IShareSettings {
enforceExpireDate: boolean
excludeGroups: string
excludeGroupsList: string[]
- publicShareDisclaimerText?: string
+ publicShareDisclaimerText: string
enableLinkPasswordByDefault: boolean
defaultPermissions: number
defaultInternalExpireDate: boolean
@@ -250,6 +271,8 @@ interface IShareSettings {
defaultRemoteExpireDate: boolean
remoteExpireAfterNDays: string
enforceRemoteExpireDate: boolean
+ allowCustomTokens: boolean
+ allowViewWithoutDownload: boolean
}
export default defineComponent({
@@ -257,13 +280,16 @@ export default defineComponent({
components: {
NcCheckboxRadioSwitch,
NcSettingsSelectGroup,
+ NcNoteCard,
NcTextArea,
NcTextField,
SelectSharingPermissions,
},
data() {
+ const settingsData = loadState<IShareSettings>('settings', 'sharingSettings')
return {
- settingsData: loadState<IShareSettings>('settings', 'sharingSettings'),
+ settingsData,
+ publicShareDisclaimerEnabled: settingsData.publicShareDisclaimerText !== '',
}
},
computed: {
@@ -282,26 +308,24 @@ export default defineComponent({
},
})
},
- publicShareDisclaimerEnabled: {
- get() {
- return typeof this.settingsData.publicShareDisclaimerText === 'string'
- },
- set(value) {
- if (value) {
- this.settingsData.publicShareDisclaimerText = ''
- } else {
- this.onUpdateDisclaimer()
- }
- },
+ },
+
+ watch: {
+ publicShareDisclaimerEnabled() {
+ // When disabled we just remove the disclaimer content
+ if (this.publicShareDisclaimerEnabled === false) {
+ this.onUpdateDisclaimer('')
+ }
},
},
+
methods: {
t,
- onUpdateDisclaimer: debounce(function(value?: string) {
+ onUpdateDisclaimer: debounce(function(value: string) {
const options = {
success() {
- if (value) {
+ if (value !== '') {
showSuccess(t('settings', 'Changed disclaimer text'))
} else {
showSuccess(t('settings', 'Deleted disclaimer text'))
@@ -311,7 +335,7 @@ export default defineComponent({
showError(t('settings', 'Could not set disclaimer text'))
},
}
- if (!value) {
+ if (value === '') {
window.OCP.AppConfig.deleteKey('core', 'shareapi_public_link_disclaimertext', options)
} else {
window.OCP.AppConfig.setValue('core', 'shareapi_public_link_disclaimertext', value, options)
@@ -321,7 +345,7 @@ export default defineComponent({
onUpdateExcludeGroups: debounce(function(value: string) {
window.OCP.AppConfig.setValue('core', 'excludeGroups', value)
this.settings.excludeGroups = value
- }, 500) as (v?: string) => void
+ }, 500) as (v?: string) => void,
},
})
</script>
@@ -364,6 +388,10 @@ export default defineComponent({
width: 100%;
}
}
+
+ & &__note {
+ margin: 2px 0;
+ }
}
@media only screen and (max-width: 350px) {
diff --git a/apps/settings/src/components/AdminTwoFactor.vue b/apps/settings/src/components/AdminTwoFactor.vue
index f2572be9c90..e24bee02593 100644
--- a/apps/settings/src/components/AdminTwoFactor.vue
+++ b/apps/settings/src/components/AdminTwoFactor.vue
@@ -1,3 +1,7 @@
+<!--
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<NcSettingsSection :name="t('settings', 'Two-Factor Authentication')"
:description="t('settings', 'Two-factor authentication can be enforced for all accounts and specific groups. If they do not have a two-factor provider configured, they will be unable to log into the system.')"
@@ -67,10 +71,10 @@
<script>
import axios from '@nextcloud/axios'
-import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
-import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
+import NcSelect from '@nextcloud/vue/components/NcSelect'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
import { loadState } from '@nextcloud/initial-state'
import sortedUniq from 'lodash/sortedUniq.js'
@@ -171,8 +175,7 @@ export default {
.two-factor-loading {
display: inline-block;
vertical-align: sub;
- margin-left: -2px;
- margin-right: 1px;
+ margin-inline: -2px 1px;
}
.top-margin {
diff --git a/apps/settings/src/components/AppAPI/DaemonSelectionDialog.vue b/apps/settings/src/components/AppAPI/DaemonSelectionDialog.vue
new file mode 100644
index 00000000000..696c77d19ce
--- /dev/null
+++ b/apps/settings/src/components/AppAPI/DaemonSelectionDialog.vue
@@ -0,0 +1,41 @@
+<!--
+ - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <NcDialog :open="show"
+ :name="t('settings', 'Choose Deploy Daemon for {appName}', {appName: app.name })"
+ size="normal"
+ @update:open="closeModal">
+ <DaemonSelectionList :app="app"
+ :deploy-options="deployOptions"
+ @close="closeModal" />
+ </NcDialog>
+</template>
+
+<script setup>
+import { defineProps, defineEmits } from 'vue'
+import NcDialog from '@nextcloud/vue/components/NcDialog'
+import DaemonSelectionList from './DaemonSelectionList.vue'
+
+defineProps({
+ show: {
+ type: Boolean,
+ required: true,
+ },
+ app: {
+ type: Object,
+ required: true,
+ },
+ deployOptions: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+})
+
+const emit = defineEmits(['update:show'])
+const closeModal = () => {
+ emit('update:show', false)
+}
+</script>
diff --git a/apps/settings/src/components/AppAPI/DaemonSelectionEntry.vue b/apps/settings/src/components/AppAPI/DaemonSelectionEntry.vue
new file mode 100644
index 00000000000..6b1cefde032
--- /dev/null
+++ b/apps/settings/src/components/AppAPI/DaemonSelectionEntry.vue
@@ -0,0 +1,77 @@
+<!--
+ - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <NcListItem :name="itemTitle"
+ :details="isDefault ? t('settings', 'Default') : ''"
+ :force-display-actions="true"
+ :counter-number="daemon.exAppsCount"
+ :active="isDefault"
+ counter-type="highlighted"
+ @click.stop="selectDaemonAndInstall">
+ <template #subname>
+ {{ daemon.accepts_deploy_id }}
+ </template>
+ </NcListItem>
+</template>
+
+<script>
+import NcListItem from '@nextcloud/vue/components/NcListItem'
+import AppManagement from '../../mixins/AppManagement.js'
+import { useAppsStore } from '../../store/apps-store'
+import { useAppApiStore } from '../../store/app-api-store'
+
+export default {
+ name: 'DaemonSelectionEntry',
+ components: {
+ NcListItem,
+ },
+ mixins: [AppManagement], // TODO: Convert to Composition API when AppManagement is refactored
+ props: {
+ daemon: {
+ type: Object,
+ required: true,
+ },
+ isDefault: {
+ type: Boolean,
+ required: true,
+ },
+ app: {
+ type: Object,
+ required: true,
+ },
+ deployOptions: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ setup() {
+ const store = useAppsStore()
+ const appApiStore = useAppApiStore()
+
+ return {
+ store,
+ appApiStore,
+ }
+ },
+ computed: {
+ itemTitle() {
+ return this.daemon.name + ' - ' + this.daemon.display_name
+ },
+ daemons() {
+ return this.appApiStore.dockerDaemons
+ },
+ },
+ methods: {
+ closeModal() {
+ this.$emit('close')
+ },
+ selectDaemonAndInstall() {
+ this.closeModal()
+ this.enable(this.app.id, this.daemon, this.deployOptions)
+ },
+ },
+}
+</script>
diff --git a/apps/settings/src/components/AppAPI/DaemonSelectionList.vue b/apps/settings/src/components/AppAPI/DaemonSelectionList.vue
new file mode 100644
index 00000000000..701a17dbe24
--- /dev/null
+++ b/apps/settings/src/components/AppAPI/DaemonSelectionList.vue
@@ -0,0 +1,77 @@
+<!--
+ - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <div class="daemon-selection-list">
+ <ul v-if="dockerDaemons.length > 0"
+ :aria-label="t('settings', 'Registered Deploy daemons list')">
+ <DaemonSelectionEntry v-for="daemon in dockerDaemons"
+ :key="daemon.id"
+ :daemon="daemon"
+ :is-default="defaultDaemon.name === daemon.name"
+ :app="app"
+ :deploy-options="deployOptions"
+ @close="closeModal" />
+ </ul>
+ <NcEmptyContent v-else
+ class="daemon-selection-list__empty-content"
+ :name="t('settings', 'No Deploy daemons configured')"
+ :description="t('settings', 'Register a custom one or setup from available templates')">
+ <template #icon>
+ <FormatListBullet :size="20" />
+ </template>
+ <template #action>
+ <NcButton :href="appApiAdminPage">
+ {{ t('settings', 'Manage Deploy daemons') }}
+ </NcButton>
+ </template>
+ </NcEmptyContent>
+ </div>
+</template>
+
+<script setup>
+import { computed, defineProps } from 'vue'
+import { generateUrl } from '@nextcloud/router'
+
+import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import FormatListBullet from 'vue-material-design-icons/FormatListBulleted.vue'
+import DaemonSelectionEntry from './DaemonSelectionEntry.vue'
+import { useAppApiStore } from '../../store/app-api-store.ts'
+
+defineProps({
+ app: {
+ type: Object,
+ required: true,
+ },
+ deployOptions: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+})
+
+const appApiStore = useAppApiStore()
+
+const dockerDaemons = computed(() => appApiStore.dockerDaemons)
+const defaultDaemon = computed(() => appApiStore.defaultDaemon)
+const appApiAdminPage = computed(() => generateUrl('/settings/admin/app_api'))
+const emit = defineEmits(['close'])
+const closeModal = () => {
+ emit('close')
+}
+</script>
+
+<style scoped lang="scss">
+.daemon-selection-list {
+ max-height: 350px;
+ overflow-y: scroll;
+ padding: 2rem;
+
+ &__empty-content {
+ margin-top: 0;
+ text-align: center;
+ }
+}
+</style>
diff --git a/apps/settings/src/components/AppList.vue b/apps/settings/src/components/AppList.vue
index bcd9a23112c..3e40e08b257 100644
--- a/apps/settings/src/components/AppList.vue
+++ b/apps/settings/src/components/AppList.vue
@@ -1,24 +1,7 @@
<!--
- - @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
- -
- - @author Julius Härtl <jus@bitgrid.net>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<div id="app-content-inner">
@@ -157,9 +140,12 @@
<script>
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
-import AppItem from './AppList/AppItem.vue'
import pLimit from 'p-limit'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import AppItem from './AppList/AppItem.vue'
+import AppManagement from '../mixins/AppManagement'
+import { useAppApiStore } from '../store/app-api-store'
+import { useAppsStore } from '../store/apps-store'
export default {
name: 'AppList',
@@ -168,6 +154,8 @@ export default {
NcButton,
},
+ mixins: [AppManagement],
+
props: {
category: {
type: String,
@@ -175,6 +163,16 @@ export default {
},
},
+ setup() {
+ const appApiStore = useAppApiStore()
+ const store = useAppsStore()
+
+ return {
+ appApiStore,
+ store,
+ }
+ },
+
data() {
return {
search: '',
@@ -185,7 +183,10 @@ export default {
return this.apps.filter(app => app.update).length
},
loading() {
- return this.$store.getters.loading('list')
+ if (!this.$store.getters['appApiApps/isAppApiEnabled']) {
+ return this.$store.getters.loading('list')
+ }
+ return this.$store.getters.loading('list') || this.appApiStore.getLoading('list')
},
hasPendingUpdate() {
return this.apps.filter(app => app.update).length > 0
@@ -194,12 +195,18 @@ export default {
return this.hasPendingUpdate && this.useListView
},
apps() {
- const apps = this.$store.getters.getAllApps
+ // Exclude ExApps from the list if AppAPI is disabled
+ const exApps = this.$store.getters.isAppApiEnabled ? this.appApiStore.getAllApps : []
+ 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') {
@@ -247,7 +254,8 @@ export default {
if (this.search === '') {
return []
}
- return this.$store.getters.getAllApps
+ const exApps = this.$store.getters.isAppApiEnabled ? this.appApiStore.getAllApps : []
+ return [...this.$store.getters.getAllApps, ...exApps]
.filter(app => {
if (app.name.toLowerCase().search(this.search.toLowerCase()) !== -1) {
return (!this.apps.find(_app => _app.id === app.id))
@@ -321,8 +329,9 @@ export default {
const limit = pLimit(1)
this.apps
.filter(app => app.update)
- .map(app => limit(() => this.$store.dispatch('updateApp', { appId: app.id })),
- )
+ .map((app) => limit(() => {
+ this.update(app.id)
+ }))
},
},
}
@@ -343,14 +352,14 @@ $toolbar-height: 44px + $toolbar-padding * 2;
}
#app-list-update-all {
- margin-left: 10px;
+ margin-inline-start: 10px;
}
&__toolbar {
height: $toolbar-height;
padding: $toolbar-padding;
// Leave room for app-navigation-toggle
- padding-left: $toolbar-height;
+ padding-inline-start: $toolbar-height;
width: 100%;
background-color: var(--color-main-background);
position: sticky;
@@ -378,11 +387,13 @@ $toolbar-height: 44px + $toolbar-padding * 2;
&__bundle-heading {
display: flex;
align-items: center;
- margin: 20px 10px 20px 0;
+ margin-block: 20px;
+ margin-inline: 0 10px;
}
&__bundle-header {
- margin: 0 10px 0 50px;
+ margin-block: 0;
+ margin-inline: 50px 10px;
font-weight: bold;
font-size: 20px;
line-height: 30px;
diff --git a/apps/settings/src/components/AppList/AppDaemonBadge.vue b/apps/settings/src/components/AppList/AppDaemonBadge.vue
new file mode 100644
index 00000000000..ca81e7fab0b
--- /dev/null
+++ b/apps/settings/src/components/AppList/AppDaemonBadge.vue
@@ -0,0 +1,37 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <span v-if="daemon"
+ class="app-daemon-badge"
+ :title="daemon.name">
+ <NcIconSvgWrapper :path="mdiFileChart" :size="20" inline />
+ {{ daemon.display_name }}
+ </span>
+</template>
+
+<script setup lang="ts">
+import type { IDeployDaemon } from '../../app-types.ts'
+import { mdiFileChart } from '@mdi/js'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+
+defineProps<{
+ daemon?: IDeployDaemon
+}>()
+</script>
+
+<style scoped lang="scss">
+.app-daemon-badge {
+ color: var(--color-text-maxcontrast);
+ background-color: transparent;
+ border: 1px solid var(--color-text-maxcontrast);
+ border-radius: var(--border-radius);
+
+ display: flex;
+ flex-direction: row;
+ gap: 6px;
+ padding: 3px 6px;
+ width: fit-content;
+}
+</style>
diff --git a/apps/settings/src/components/AppList/AppItem.vue b/apps/settings/src/components/AppList/AppItem.vue
index 51d0997c007..95a98a93cde 100644
--- a/apps/settings/src/components/AppList/AppItem.vue
+++ b/apps/settings/src/components/AppList/AppItem.vue
@@ -1,25 +1,7 @@
<!--
- - @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
- -
- - @author Julius Härtl <jus@bitgrid.net>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
-
+ - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<component :is="listView ? 'tr' : (inline ? 'article' : 'li')"
class="app-item"
@@ -32,9 +14,13 @@
<component :is="dataItemTag"
class="app-image app-image-icon"
:headers="getDataItemHeaders(`app-table-col-icon`)">
- <div v-if="(listView && !app.preview) || (!listView && !screenshotLoaded)" class="icon-settings-dark" />
+ <div v-if="!app?.app_api && shouldDisplayDefaultIcon" class="icon-settings-dark" />
+ <NcIconSvgWrapper v-else-if="app.app_api && shouldDisplayDefaultIcon"
+ :path="mdiCogOutline"
+ :size="listView ? 24 : 48"
+ style="min-width: auto; min-height: auto; height: 100%;" />
- <svg v-else-if="listView && app.preview"
+ <svg v-else-if="listView && app.preview && !app.app_api"
width="32"
height="32"
viewBox="0 0 32 32">
@@ -89,10 +75,11 @@
<div v-if="app.error" class="warning">
{{ app.error }}
</div>
- <div v-if="isLoading" class="icon icon-loading-small" />
+ <div v-if="isLoading || isInitializing" class="icon icon-loading-small" />
<NcButton v-if="app.update"
type="primary"
- :disabled="installing || isLoading"
+ :disabled="installing || isLoading || !defaultDeployDaemonAccessible || isManualInstall"
+ :title="updateButtonText"
@click.stop="update(app.id)">
{{ t('settings', 'Update to {update}', {update:app.update}) }}
</NcButton>
@@ -104,36 +91,46 @@
{{ t('settings', 'Remove') }}
</NcButton>
<NcButton v-if="app.active"
- :disabled="installing || isLoading"
+ :disabled="installing || isLoading || isInitializing || isDeploying"
@click.stop="disable(app.id)">
- {{ t('settings','Disable') }}
+ {{ disableButtonText }}
</NcButton>
<NcButton v-if="!app.active && (app.canInstall || app.isCompatible)"
:title="enableButtonTooltip"
:aria-label="enableButtonTooltip"
type="primary"
- :disabled="!app.canInstall || installing || isLoading"
- @click.stop="enable(app.id)">
+ :disabled="!app.canInstall || installing || isLoading || !defaultDeployDaemonAccessible || isInitializing || isDeploying"
+ @click.stop="enableButtonAction">
{{ enableButtonText }}
</NcButton>
<NcButton v-else-if="!app.active"
:title="forceEnableButtonTooltip"
:aria-label="forceEnableButtonTooltip"
type="secondary"
- :disabled="installing || isLoading"
+ :disabled="installing || isLoading || !defaultDeployDaemonAccessible"
@click.stop="forceEnable(app.id)">
{{ forceEnableButtonText }}
</NcButton>
+
+ <DaemonSelectionDialog v-if="app?.app_api && showSelectDaemonModal"
+ :show.sync="showSelectDaemonModal"
+ :app="app" />
</component>
</component>
</template>
<script>
+import { useAppsStore } from '../../store/apps-store.js'
+
import AppScore from './AppScore.vue'
import AppLevelBadge from './AppLevelBadge.vue'
import AppManagement from '../../mixins/AppManagement.js'
import SvgFilterMixin from '../SvgFilterMixin.vue'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import { mdiCogOutline } from '@mdi/js'
+import { useAppApiStore } from '../../store/app-api-store.ts'
+import DaemonSelectionDialog from '../AppAPI/DaemonSelectionDialog.vue'
export default {
name: 'AppItem',
@@ -141,6 +138,8 @@ export default {
AppLevelBadge,
AppScore,
NcButton,
+ NcIconSvgWrapper,
+ DaemonSelectionDialog,
},
mixins: [AppManagement, SvgFilterMixin],
props: {
@@ -169,11 +168,22 @@ export default {
default: false,
},
},
+ setup() {
+ const store = useAppsStore()
+ const appApiStore = useAppApiStore()
+
+ return {
+ store,
+ appApiStore,
+ mdiCogOutline,
+ }
+ },
data() {
return {
isSelected: false,
scrolled: false,
screenshotLoaded: false,
+ showSelectDaemonModal: false,
}
},
computed: {
@@ -186,6 +196,9 @@ export default {
withSidebar() {
return !!this.$route.params.id
},
+ shouldDisplayDefaultIcon() {
+ return (this.listView && !this.app.preview) || (!this.listView && !this.screenshotLoaded)
+ },
},
watch: {
'$route.params.id'(id) {
@@ -213,6 +226,23 @@ export default {
getDataItemHeaders(columnName) {
return this.useBundleView ? [this.headers, columnName].join(' ') : null
},
+ showSelectionModal() {
+ this.showSelectDaemonModal = true
+ },
+ async enableButtonAction() {
+ if (!this.app?.app_api) {
+ this.enable(this.app.id)
+ return
+ }
+ await this.appApiStore.fetchDockerDaemons()
+ if (this.appApiStore.dockerDaemons.length === 1 && this.app.needsDownload) {
+ this.enable(this.app.id, this.appApiStore.dockerDaemons[0])
+ } else if (this.app.needsDownload) {
+ this.showSelectionModal()
+ } else {
+ this.enable(this.app.id, this.app.daemon)
+ }
+ },
},
}
</script>
@@ -246,7 +276,7 @@ export default {
.app-image {
width: var(--default-clickable-area);
height: auto;
- text-align: right;
+ text-align: end;
}
.app-image-icon svg,
@@ -275,8 +305,7 @@ export default {
.app-name--link::after {
content: '';
position: absolute;
- left: 0;
- right: 0;
+ inset-inline: 0;
height: var(--app-item-height);
}
@@ -289,7 +318,7 @@ export default {
.icon-loading-small {
display: inline-block;
top: 4px;
- margin-right: 10px;
+ margin-inline-end: 10px;
}
}
@@ -335,10 +364,8 @@ export default {
.app-name--link::after {
content: '';
position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
+ inset-block: 0;
+ inset-inline: 0;
}
.app-actions {
diff --git a/apps/settings/src/components/AppList/AppLevelBadge.vue b/apps/settings/src/components/AppList/AppLevelBadge.vue
index f5bda5856ef..8461f5eb6b9 100644
--- a/apps/settings/src/components/AppList/AppLevelBadge.vue
+++ b/apps/settings/src/components/AppList/AppLevelBadge.vue
@@ -1,24 +1,7 @@
<!--
- - @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @author Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<span v-if="isSupported || isFeatured"
class="app-level-badge"
@@ -30,9 +13,9 @@
</template>
<script setup lang="ts">
-import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
-import { mdiCheck, mdiStarShooting } from '@mdi/js'
+import { mdiCheck, mdiStarShootingOutline } from '@mdi/js'
import { translate as t } from '@nextcloud/l10n'
import { computed } from 'vue'
@@ -45,7 +28,7 @@ const props = defineProps<{
const isSupported = computed(() => props.level === 300)
const isFeatured = computed(() => props.level === 200)
-const badgeIcon = computed(() => isSupported.value ? mdiStarShooting : mdiCheck)
+const badgeIcon = computed(() => isSupported.value ? mdiStarShootingOutline : mdiCheck)
const badgeText = computed(() => isSupported.value ? t('settings', 'Supported') : t('settings', 'Featured'))
const badgeTitle = computed(() => isSupported.value
? t('settings', 'This app is supported via your current Nextcloud subscription.')
diff --git a/apps/settings/src/components/AppList/AppScore.vue b/apps/settings/src/components/AppList/AppScore.vue
index f576a471ef6..a1dd4c03842 100644
--- a/apps/settings/src/components/AppList/AppScore.vue
+++ b/apps/settings/src/components/AppList/AppScore.vue
@@ -1,25 +1,7 @@
<!--
- - @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
- -
- - @author Julius Härtl <jus@bitgrid.net>
- - @author Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<span role="img"
@@ -38,7 +20,7 @@
</span>
</template>
<script lang="ts">
-import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import { mdiStar, mdiStarHalfFull, mdiStarOutline } from '@mdi/js'
import { translate as t } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
diff --git a/apps/settings/src/components/AppNavigationGroupList.vue b/apps/settings/src/components/AppNavigationGroupList.vue
new file mode 100644
index 00000000000..8f21d18d695
--- /dev/null
+++ b/apps/settings/src/components/AppNavigationGroupList.vue
@@ -0,0 +1,220 @@
+<!--
+ - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <Fragment>
+ <NcAppNavigationCaption :name="t('settings', 'Groups')"
+ :disabled="loadingAddGroup"
+ :aria-label="loadingAddGroup ? t('settings', 'Creating group…') : t('settings', 'Create group')"
+ force-menu
+ is-heading
+ :open.sync="isAddGroupOpen">
+ <template v-if="isAdminOrDelegatedAdmin" #actionsTriggerIcon>
+ <NcLoadingIcon v-if="loadingAddGroup" />
+ <NcIconSvgWrapper v-else :path="mdiPlus" />
+ </template>
+ <template v-if="isAdminOrDelegatedAdmin" #actions>
+ <NcActionText>
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiAccountGroupOutline" />
+ </template>
+ {{ t('settings', 'Create group') }}
+ </NcActionText>
+ <NcActionInput :label="t('settings', 'Group name')"
+ data-cy-users-settings-new-group-name
+ :label-outside="false"
+ :disabled="loadingAddGroup"
+ :value.sync="newGroupName"
+ :error="hasAddGroupError"
+ :helper-text="hasAddGroupError ? t('settings', 'Please enter a valid group name') : ''"
+ @submit="createGroup" />
+ </template>
+ </NcAppNavigationCaption>
+
+ <NcAppNavigationSearch v-model="groupsSearchQuery"
+ :label="t('settings', 'Search groups…')" />
+
+ <p id="group-list-desc" class="hidden-visually">
+ {{ t('settings', 'List of groups. This list is not fully populated for performance reasons. The groups will be loaded as you navigate or search through the list.') }}
+ </p>
+ <NcAppNavigationList class="account-management__group-list"
+ aria-describedby="group-list-desc"
+ data-cy-users-settings-navigation-groups="custom">
+ <GroupListItem v-for="group in filteredGroups"
+ :id="group.id"
+ ref="groupListItems"
+ :key="group.id"
+ :active="selectedGroupDecoded === group.id"
+ :name="group.title"
+ :count="group.count" />
+ <div v-if="loadingGroups" role="note">
+ <NcLoadingIcon :name="t('settings', 'Loading groups…')" />
+ </div>
+ </NcAppNavigationList>
+ </Fragment>
+</template>
+
+<script setup lang="ts">
+import type CancelablePromise from 'cancelable-promise'
+import type { IGroup } from '../views/user-types.d.ts'
+
+import { mdiAccountGroupOutline, 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 NcActionInput from '@nextcloud/vue/components/NcActionInput'
+import NcActionText from '@nextcloud/vue/components/NcActionText'
+import NcAppNavigationCaption from '@nextcloud/vue/components/NcAppNavigationCaption'
+import NcAppNavigationList from '@nextcloud/vue/components/NcAppNavigationList'
+import NcAppNavigationSearch from '@nextcloud/vue/components/NcAppNavigationSearch'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+
+import GroupListItem from './GroupListItem.vue'
+
+import { useFormatGroups } from '../composables/useGroupsNavigation.ts'
+import { useStore } from '../store'
+import { searchGroups } from '../service/groups.ts'
+import logger from '../logger.ts'
+
+const store = useStore()
+const route = useRoute()
+const router = useRouter()
+
+onBeforeMount(async () => {
+ await loadGroups()
+})
+
+/** Current active group in the view - this is URL encoded */
+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(() => {
+ return isAdminOrDelegatedAdmin.value
+ ? store.getters.getSortedGroups
+ : store.getters.getSubAdminGroups
+})
+/** User groups */
+const { userGroups } = useFormatGroups(groups)
+/** Server settings for current user */
+const settings = computed(() => store.getters.getServerData)
+/** True if the current user is a (delegated) admin */
+const isAdminOrDelegatedAdmin = computed(() => settings.value.isAdmin || settings.value.isDelegatedAdmin)
+
+/** True if the 'add-group' dialog is open - needed to be able to close it when the group is created */
+const isAddGroupOpen = ref(false)
+/** True if the group creation is in progress to show loading spinner and disable adding another one */
+const loadingAddGroup = ref(false)
+/** Error state for creating a new group */
+const hasAddGroupError = ref(false)
+/** Name of the group to create (used in the group creation dialog) */
+const newGroupName = ref('')
+
+/** True if groups are loading */
+const loadingGroups = ref(false)
+/** Search offset */
+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(() => {
+ return groupListItems.value
+ .findLast(component => component?.$vnode?.key === userGroups.value?.at(-1)?.id) // Order of refs is not guaranteed to match source array order
+ ?.$refs?.listItem?.$el
+})
+const isLastGroupVisible = useElementVisibility(lastGroupListItem)
+watch(isLastGroupVisible, async () => {
+ if (!isLastGroupVisible.value) {
+ return
+ }
+ await loadGroups()
+})
+
+watch(groupsSearchQuery, async () => {
+ store.commit('resetGroups')
+ offset.value = 0
+ await loadGroups()
+})
+
+/** Cancelable promise for search groups request */
+const promise = ref<CancelablePromise<IGroup[]>>()
+
+/**
+ * Load groups
+ */
+async function loadGroups() {
+ if (!isAdminOrDelegatedAdmin.value) {
+ return
+ }
+
+ if (promise.value) {
+ promise.value.cancel()
+ }
+ loadingGroups.value = true
+ try {
+ promise.value = searchGroups({
+ search: groupsSearchQuery.value,
+ offset: offset.value,
+ limit: 25,
+ })
+ const groups = await promise.value
+ if (groups.length > 0) {
+ offset.value += 25
+ }
+ for (const group of groups) {
+ store.commit('addGroup', group)
+ }
+ } catch (error) {
+ logger.error(t('settings', 'Failed to load groups'), { error })
+ }
+ promise.value = undefined
+ loadingGroups.value = false
+}
+
+/**
+ * Create a new group
+ */
+async function createGroup() {
+ hasAddGroupError.value = false
+ const groupId = newGroupName.value.trim()
+ if (groupId === '') {
+ hasAddGroupError.value = true
+ return
+ }
+
+ isAddGroupOpen.value = false
+ loadingAddGroup.value = true
+
+ try {
+ await store.dispatch('addGroup', groupId)
+ await router.push({
+ name: 'group',
+ params: {
+ selectedGroup: encodeURIComponent(groupId),
+ },
+ })
+ const newGroupListItem = groupListItems.value.findLast(component => component?.$vnode?.key === groupId)
+ newGroupListItem?.$refs?.listItem?.$el?.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
+ newGroupName.value = ''
+ } catch {
+ showError(t('settings', 'Failed to create group'))
+ }
+ loadingAddGroup.value = false
+}
+</script>
diff --git a/apps/settings/src/components/AppStoreDiscover/AppLink.vue b/apps/settings/src/components/AppStoreDiscover/AppLink.vue
index 44776f37ffb..703adb9f041 100644
--- a/apps/settings/src/components/AppStoreDiscover/AppLink.vue
+++ b/apps/settings/src/components/AppStoreDiscover/AppLink.vue
@@ -1,24 +1,7 @@
<!--
- - @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @author Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<a v-if="linkProps" v-bind="linkProps">
<slot />
@@ -35,12 +18,10 @@ import { loadState } from '@nextcloud/initial-state'
import { generateUrl } from '@nextcloud/router'
import { defineComponent } from 'vue'
import { RouterLink } from 'vue-router'
+import type { INavigationEntry } from '../../../../../core/src/types/navigation'
-const knownRoutes = Object.fromEntries(
- Object.entries(
- loadState<Record<string, { app?: string, href: string }>>('core', 'apps'),
- ).map(([k, v]) => [v.app ?? k, v.href]),
-)
+const apps = loadState<INavigationEntry[]>('core', 'apps')
+const knownRoutes = Object.fromEntries(apps.map((app) => [app.app ?? app.id, app.href]))
/**
* This component either shows a native link to the installed app or external size - or a router link to the appstore page of the app if not installed
diff --git a/apps/settings/src/components/AppStoreDiscover/AppStoreDiscoverSection.vue b/apps/settings/src/components/AppStoreDiscover/AppStoreDiscoverSection.vue
index 4e20a55bcde..bb91940c763 100644
--- a/apps/settings/src/components/AppStoreDiscover/AppStoreDiscoverSection.vue
+++ b/apps/settings/src/components/AppStoreDiscover/AppStoreDiscoverSection.vue
@@ -1,10 +1,14 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<div class="app-discover">
<NcEmptyContent v-if="hasError"
:name="t('settings', 'Nothing to show')"
:description="t('settings', 'Could not load section content from app store.')">
<template #icon>
- <NcIconSvgWrapper :path="mdiEyeOff" :size="64" />
+ <NcIconSvgWrapper :path="mdiEyeOffOutline" :size="64" />
</template>
</NcEmptyContent>
<NcEmptyContent v-else-if="elements.length === 0"
@@ -26,16 +30,16 @@
<script setup lang="ts">
import type { IAppDiscoverElements } from '../../constants/AppDiscoverTypes.ts'
-import { mdiEyeOff } from '@mdi/js'
+import { mdiEyeOffOutline } from '@mdi/js'
import { showError } from '@nextcloud/dialogs'
import { translate as t } from '@nextcloud/l10n'
import { generateUrl } from '@nextcloud/router'
import { defineAsyncComponent, defineComponent, onBeforeMount, ref } from 'vue'
import axios from '@nextcloud/axios'
-import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
-import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
-import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
+import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import logger from '../../logger'
import { parseApiResponse, filterElements } from '../../utils/appDiscoverParser.ts'
diff --git a/apps/settings/src/components/AppStoreDiscover/AppType.vue b/apps/settings/src/components/AppStoreDiscover/AppType.vue
index badb560e684..7263dc71041 100644
--- a/apps/settings/src/components/AppStoreDiscover/AppType.vue
+++ b/apps/settings/src/components/AppStoreDiscover/AppType.vue
@@ -1,24 +1,7 @@
<!--
- - @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @author Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<AppItem v-if="app"
:app="app"
diff --git a/apps/settings/src/components/AppStoreDiscover/CarouselType.vue b/apps/settings/src/components/AppStoreDiscover/CarouselType.vue
index 42493d24f31..69393176835 100644
--- a/apps/settings/src/components/AppStoreDiscover/CarouselType.vue
+++ b/apps/settings/src/components/AppStoreDiscover/CarouselType.vue
@@ -1,3 +1,7 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<section :aria-roledescription="t('settings', 'Carousel')" :aria-labelledby="headingId ? `${headingId}` : undefined">
<h3 v-if="headline" :id="headingId">
@@ -65,8 +69,8 @@ import { computed, defineComponent, nextTick, ref, watch } from 'vue'
import { commonAppDiscoverProps } from './common.ts'
import { useLocalizedValue } from '../../composables/useGetLocalizedValue.ts'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import PostType from './PostType.vue'
export default defineComponent({
@@ -161,10 +165,10 @@ h3 {
// See padding of discover section
&--next {
- right: -54px;
+ inset-inline-end: -54px;
}
&--previous {
- left: -54px;
+ inset-inline-start: -54px;
}
}
diff --git a/apps/settings/src/components/AppStoreDiscover/PostType.vue b/apps/settings/src/components/AppStoreDiscover/PostType.vue
index c03cac4feaf..090e9dee577 100644
--- a/apps/settings/src/components/AppStoreDiscover/PostType.vue
+++ b/apps/settings/src/components/AppStoreDiscover/PostType.vue
@@ -1,24 +1,7 @@
<!--
- - @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @author Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<article :id="domId"
ref="container"
@@ -82,7 +65,7 @@ import { computed, defineComponent, ref, watchEffect } from 'vue'
import { commonAppDiscoverProps } from './common'
import { useLocalizedValue } from '../../composables/useGetLocalizedValue'
-import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import AppLink from './AppLink.vue'
export default defineComponent({
@@ -269,15 +252,15 @@ export default defineComponent({
}
&__play-icon {
+ position: absolute;
+ top: -46px; // half of the icon height
+ inset-inline-end: -46px; // half of the icon width
+
&-wrapper {
position: relative;
top: -50%;
- left: -50%;
+ inset-inline-start: -50%;
}
-
- position: absolute;
- top: -46px; // half of the icon height
- right: -46px; // half of the icon width
}
}
diff --git a/apps/settings/src/components/AppStoreDiscover/ShowcaseType.vue b/apps/settings/src/components/AppStoreDiscover/ShowcaseType.vue
index cb4d118dd83..ac057b9ab7d 100644
--- a/apps/settings/src/components/AppStoreDiscover/ShowcaseType.vue
+++ b/apps/settings/src/components/AppStoreDiscover/ShowcaseType.vue
@@ -1,24 +1,7 @@
<!--
- - @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @author Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<section ref="container"
class="app-discover-showcase"
diff --git a/apps/settings/src/components/AppStoreDiscover/common.ts b/apps/settings/src/components/AppStoreDiscover/common.ts
index 39ac3d6801a..277d4910e49 100644
--- a/apps/settings/src/components/AppStoreDiscover/common.ts
+++ b/apps/settings/src/components/AppStoreDiscover/common.ts
@@ -1,23 +1,6 @@
/**
- * @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
- *
- * @author Ferdinand Thiessen <opensource@fthiessen.de>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { PropType } from 'vue'
import type { IAppDiscoverElement } from '../../constants/AppDiscoverTypes.ts'
diff --git a/apps/settings/src/components/AppStoreSidebar/AppDeployDaemonTab.vue b/apps/settings/src/components/AppStoreSidebar/AppDeployDaemonTab.vue
new file mode 100644
index 00000000000..7c0b8ea4421
--- /dev/null
+++ b/apps/settings/src/components/AppStoreSidebar/AppDeployDaemonTab.vue
@@ -0,0 +1,50 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <NcAppSidebarTab v-if="app?.daemon"
+ id="daemon"
+ :name="t('settings', 'Daemon')"
+ :order="3">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiFileChart" :size="24" />
+ </template>
+ <div class="daemon">
+ <h4>{{ t('settings', 'Deploy Daemon') }}</h4>
+ <p><b>{{ t('settings', 'Type') }}</b>: {{ app?.daemon.accepts_deploy_id }}</p>
+ <p><b>{{ t('settings', 'Name') }}</b>: {{ app?.daemon.name }}</p>
+ <p><b>{{ t('settings', 'Display Name') }}</b>: {{ app?.daemon.display_name }}</p>
+ <p><b>{{ t('settings', 'GPUs support') }}</b>: {{ gpuSupport }}</p>
+ <p><b>{{ t('settings', 'Compute device') }}</b>: {{ app?.daemon?.deploy_config?.computeDevice?.label }}</p>
+ </div>
+ </NcAppSidebarTab>
+</template>
+
+<script setup lang="ts">
+import type { IAppstoreExApp } from '../../app-types'
+
+import NcAppSidebarTab from '@nextcloud/vue/components/NcAppSidebarTab'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+
+import { mdiFileChart } from '@mdi/js'
+import { ref } from 'vue'
+
+const props = defineProps<{
+ app: IAppstoreExApp,
+}>()
+
+const gpuSupport = ref(props.app?.daemon?.deploy_config?.computeDevice?.id !== 'cpu' || false)
+</script>
+
+<style scoped lang="scss">
+.daemon {
+ padding: 20px;
+
+ h4 {
+ font-weight: bold;
+ margin: 10px auto;
+ }
+}
+</style>
diff --git a/apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue b/apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue
new file mode 100644
index 00000000000..0544c3848be
--- /dev/null
+++ b/apps/settings/src/components/AppStoreSidebar/AppDeployOptionsModal.vue
@@ -0,0 +1,320 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <NcDialog :open="show"
+ size="normal"
+ :name="t('settings', 'Advanced deploy options')"
+ @update:open="$emit('update:show', $event)">
+ <div class="modal__content">
+ <p class="deploy-option__hint">
+ {{ configuredDeployOptions === null ? t('settings', 'Edit ExApp deploy options before installation') : t('settings', 'Configured ExApp deploy options. Can be set only during installation') }}.
+ <a v-if="deployOptionsDocsUrl" :href="deployOptionsDocsUrl">
+ {{ t('settings', 'Learn more') }}
+ </a>
+ </p>
+ <h3 v-if="environmentVariables.length > 0 || (configuredDeployOptions !== null && configuredDeployOptions.environment_variables.length > 0)">
+ {{ t('settings', 'Environment variables') }}
+ </h3>
+ <template v-if="configuredDeployOptions === null">
+ <div v-for="envVar in environmentVariables"
+ :key="envVar.envName"
+ class="deploy-option">
+ <NcTextField :label="envVar.displayName" :value.sync="deployOptions.environment_variables[envVar.envName]" />
+ <p class="deploy-option__hint">
+ {{ envVar.description }}
+ </p>
+ </div>
+ </template>
+ <fieldset v-else-if="Object.keys(configuredDeployOptions).length > 0"
+ class="envs">
+ <legend class="deploy-option__hint">
+ {{ t('settings', 'ExApp container environment variables') }}
+ </legend>
+ <NcTextField v-for="(value, key) in configuredDeployOptions.environment_variables"
+ :key="key"
+ :label="value.displayName ?? key"
+ :helper-text="value.description"
+ :value="value.value"
+ readonly />
+ </fieldset>
+ <template v-else>
+ <p class="deploy-option__hint">
+ {{ t('settings', 'No environment variables defined') }}
+ </p>
+ </template>
+
+ <h3>{{ t('settings', 'Mounts') }}</h3>
+ <template v-if="configuredDeployOptions === null">
+ <p class="deploy-option__hint">
+ {{ t('settings', 'Define host folder mounts to bind to the ExApp container') }}
+ </p>
+ <NcNoteCard type="info" :text="t('settings', 'Must exist on the Deploy daemon host prior to installing the ExApp')" />
+ <div v-for="mount in deployOptions.mounts"
+ :key="mount.hostPath"
+ class="deploy-option"
+ style="display: flex; align-items: center; justify-content: space-between; flex-direction: row;">
+ <NcTextField :label="t('settings', 'Host path')" :value.sync="mount.hostPath" />
+ <NcTextField :label="t('settings', 'Container path')" :value.sync="mount.containerPath" />
+ <NcCheckboxRadioSwitch :checked.sync="mount.readonly">
+ {{ t('settings', 'Read-only') }}
+ </NcCheckboxRadioSwitch>
+ <NcButton :aria-label="t('settings', 'Remove mount')"
+ style="margin-top: 6px;"
+ @click="removeMount(mount)">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiDeleteOutline" />
+ </template>
+ </NcButton>
+ </div>
+ <div v-if="addingMount" class="deploy-option">
+ <h4>
+ {{ t('settings', 'New mount') }}
+ </h4>
+ <div style="display: flex; align-items: center; justify-content: space-between; flex-direction: row;">
+ <NcTextField ref="newMountHostPath"
+ :label="t('settings', 'Host path')"
+ :aria-label="t('settings', 'Enter path to host folder')"
+ :value.sync="newMountPoint.hostPath" />
+ <NcTextField :label="t('settings', 'Container path')"
+ :aria-label="t('settings', 'Enter path to container folder')"
+ :value.sync="newMountPoint.containerPath" />
+ <NcCheckboxRadioSwitch :checked.sync="newMountPoint.readonly"
+ :aria-label="t('settings', 'Toggle read-only mode')">
+ {{ t('settings', 'Read-only') }}
+ </NcCheckboxRadioSwitch>
+ </div>
+ <div style="display: flex; align-items: center; margin-top: 4px;">
+ <NcButton :aria-label="t('settings', 'Confirm adding new mount')"
+ @click="addMountPoint">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiCheck" />
+ </template>
+ {{ t('settings', 'Confirm') }}
+ </NcButton>
+ <NcButton :aria-label="t('settings', 'Cancel adding mount')"
+ style="margin-left: 4px;"
+ @click="cancelAddMountPoint">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiClose" />
+ </template>
+ {{ t('settings', 'Cancel') }}
+ </NcButton>
+ </div>
+ </div>
+ <NcButton v-if="!addingMount"
+ :aria-label="t('settings', 'Add mount')"
+ style="margin-top: 5px;"
+ @click="startAddingMount">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiPlus" />
+ </template>
+ {{ t('settings', 'Add mount') }}
+ </NcButton>
+ </template>
+ <template v-else-if="configuredDeployOptions.mounts.length > 0">
+ <p class="deploy-option__hint">
+ {{ t('settings', 'ExApp container mounts') }}
+ </p>
+ <div v-for="mount in configuredDeployOptions.mounts"
+ :key="mount.hostPath"
+ class="deploy-option"
+ style="display: flex; align-items: center; justify-content: space-between; flex-direction: row;">
+ <NcTextField :label="t('settings', 'Host path')" :value.sync="mount.hostPath" readonly />
+ <NcTextField :label="t('settings', 'Container path')" :value.sync="mount.containerPath" readonly />
+ <NcCheckboxRadioSwitch :checked.sync="mount.readonly" disabled>
+ {{ t('settings', 'Read-only') }}
+ </NcCheckboxRadioSwitch>
+ </div>
+ </template>
+ <p v-else class="deploy-option__hint">
+ {{ t('settings', 'No mounts defined') }}
+ </p>
+ </div>
+
+ <template v-if="!app.active && (app.canInstall || app.isCompatible) && configuredDeployOptions === null" #actions>
+ <NcButton :title="enableButtonTooltip"
+ :aria-label="enableButtonTooltip"
+ type="primary"
+ :disabled="!app.canInstall || installing || isLoading || !defaultDeployDaemonAccessible || isInitializing || isDeploying"
+ @click.stop="submitDeployOptions">
+ {{ enableButtonText }}
+ </NcButton>
+ </template>
+ </NcDialog>
+</template>
+
+<script>
+import { computed, ref } from 'vue'
+
+import axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
+import { loadState } from '@nextcloud/initial-state'
+import { emit } from '@nextcloud/event-bus'
+
+import NcDialog from '@nextcloud/vue/components/NcDialog'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+
+import { mdiPlus, mdiCheck, mdiClose, mdiDeleteOutline } from '@mdi/js'
+
+import { useAppApiStore } from '../../store/app-api-store.ts'
+import { useAppsStore } from '../../store/apps-store.ts'
+
+import AppManagement from '../../mixins/AppManagement.js'
+
+export default {
+ name: 'AppDeployOptionsModal',
+ components: {
+ NcDialog,
+ NcTextField,
+ NcButton,
+ NcNoteCard,
+ NcCheckboxRadioSwitch,
+ NcIconSvgWrapper,
+ },
+ mixins: [AppManagement],
+ props: {
+ app: {
+ type: Object,
+ required: true,
+ },
+ show: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ setup(props) {
+ // for AppManagement mixin
+ const store = useAppsStore()
+ const appApiStore = useAppApiStore()
+
+ const environmentVariables = computed(() => {
+ if (props.app?.releases?.length === 1) {
+ return props.app?.releases[0]?.environmentVariables || []
+ }
+ return []
+ })
+
+ const deployOptions = ref({
+ environment_variables: environmentVariables.value.reduce((acc, envVar) => {
+ acc[envVar.envName] = envVar.default || ''
+ return acc
+ }, {}),
+ mounts: [],
+ })
+
+ return {
+ environmentVariables,
+ deployOptions,
+ store,
+ appApiStore,
+ mdiPlus,
+ mdiCheck,
+ mdiClose,
+ mdiDeleteOutline,
+ }
+ },
+ data() {
+ return {
+ addingMount: false,
+ newMountPoint: {
+ hostPath: '',
+ containerPath: '',
+ readonly: false,
+ },
+ addingPortBinding: false,
+ configuredDeployOptions: null,
+ deployOptionsDocsUrl: loadState('settings', 'deployOptionsDocsUrl', null),
+ }
+ },
+ watch: {
+ show(newShow) {
+ if (newShow) {
+ this.fetchExAppDeployOptions()
+ } else {
+ this.configuredDeployOptions = null
+ }
+ },
+ },
+ methods: {
+ startAddingMount() {
+ this.addingMount = true
+ this.$nextTick(() => {
+ this.$refs.newMountHostPath.focus()
+ })
+ },
+ addMountPoint() {
+ this.deployOptions.mounts.push(this.newMountPoint)
+ this.newMountPoint = {
+ hostPath: '',
+ containerPath: '',
+ readonly: false,
+ }
+ this.addingMount = false
+ },
+ cancelAddMountPoint() {
+ this.newMountPoint = {
+ hostPath: '',
+ containerPath: '',
+ readonly: false,
+ }
+ this.addingMount = false
+ },
+ removeMount(mountToRemove) {
+ this.deployOptions.mounts = this.deployOptions.mounts.filter(mount => mount !== mountToRemove)
+ },
+ async fetchExAppDeployOptions() {
+ return axios.get(generateUrl(`/apps/app_api/apps/deploy-options/${this.app.id}`))
+ .then(response => {
+ this.configuredDeployOptions = response.data
+ })
+ .catch(() => {
+ this.configuredDeployOptions = null
+ })
+ },
+ async submitDeployOptions() {
+ await this.appApiStore.fetchDockerDaemons()
+ if (this.appApiStore.dockerDaemons.length === 1 && this.app.needsDownload) {
+ this.enable(this.app.id, this.appApiStore.dockerDaemons[0], this.deployOptions)
+ } else if (this.app.needsDownload) {
+ emit('showDaemonSelectionModal', this.deployOptions)
+ } else {
+ this.enable(this.app.id, this.app.daemon, this.deployOptions)
+ }
+ this.$emit('update:show', false)
+ },
+ },
+}
+</script>
+
+<style scoped>
+.deploy-option {
+ margin: calc(var(--default-grid-baseline) * 4) 0;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+
+ &__hint {
+ margin-top: 4px;
+ font-size: 0.8em;
+ color: var(--color-text-maxcontrast);
+ }
+}
+
+.envs {
+ width: 100%;
+ overflow: auto;
+ height: 100%;
+ max-height: 300px;
+
+ li {
+ margin: 10px 0;
+ }
+}
+</style>
diff --git a/apps/settings/src/components/AppStoreSidebar/AppDescriptionTab.vue b/apps/settings/src/components/AppStoreSidebar/AppDescriptionTab.vue
index 0466ddf4f4c..299d084ef9e 100644
--- a/apps/settings/src/components/AppStoreSidebar/AppDescriptionTab.vue
+++ b/apps/settings/src/components/AppStoreSidebar/AppDescriptionTab.vue
@@ -1,24 +1,7 @@
<!--
- - @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
- -
- - @author Julius Härtl <jus@bitgrid.net>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<NcAppSidebarTab id="desc"
@@ -39,8 +22,8 @@ import type { IAppstoreApp } from '../../app-types'
import { mdiTextShort } from '@mdi/js'
import { translate as t } from '@nextcloud/l10n'
-import NcAppSidebarTab from '@nextcloud/vue/dist/Components/NcAppSidebarTab.js'
-import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
+import NcAppSidebarTab from '@nextcloud/vue/components/NcAppSidebarTab'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import Markdown from '../Markdown.vue'
defineProps<{
diff --git a/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue b/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue
index 24387581cdd..eb66d8f3e3a 100644
--- a/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue
+++ b/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue
@@ -1,31 +1,14 @@
<!--
- - @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
- -
- - @author Julius Härtl <jus@bitgrid.net>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<NcAppSidebarTab id="details"
:name="t('settings', 'Details')"
:order="1">
<template #icon>
- <NcIconSvgWrapper :path="mdiTextBox" />
+ <NcIconSvgWrapper :path="mdiTextBoxOutline" />
</template>
<div class="app-details">
<div class="app-details__actions">
@@ -64,19 +47,19 @@
class="update primary"
type="button"
:value="t('settings', 'Update to {version}', { version: app.update })"
- :disabled="installing || isLoading"
+ :disabled="installing || isLoading || isManualInstall"
@click="update(app.id)">
<input v-if="app.canUnInstall"
class="uninstall"
type="button"
:value="t('settings', 'Remove')"
:disabled="installing || isLoading"
- @click="remove(app.id)">
+ @click="remove(app.id, removeData)">
<input v-if="app.active"
class="enable"
type="button"
- :value="t('settings','Disable')"
- :disabled="installing || isLoading"
+ :value="disableButtonText"
+ :disabled="installing || isLoading || isInitializing || isDeploying"
@click="disable(app.id)">
<input v-if="!app.active && (app.canInstall || app.isCompatible)"
:title="enableButtonTooltip"
@@ -84,8 +67,8 @@
class="enable primary"
type="button"
:value="enableButtonText"
- :disabled="!app.canInstall || installing || isLoading"
- @click="enable(app.id)">
+ :disabled="!app.canInstall || installing || isLoading || !defaultDeployDaemonAccessible || isInitializing || isDeploying"
+ @click="enableButtonAction">
<input v-else-if="!app.active && !app.canInstall"
:title="forceEnableButtonTooltip"
:aria-label="forceEnableButtonTooltip"
@@ -94,7 +77,25 @@
:value="forceEnableButtonText"
:disabled="installing || isLoading"
@click="forceEnable(app.id)">
+ <NcButton v-if="app?.app_api && (app.canInstall || app.isCompatible)"
+ :aria-label="t('settings', 'Advanced deploy options')"
+ type="secondary"
+ @click="() => showDeployOptionsModal = true">
+ <template #icon>
+ <NcIconSvgWrapper :path="mdiToyBrickPlusOutline" />
+ </template>
+ {{ t('settings', 'Deploy options') }}
+ </NcButton>
</div>
+ <p v-if="!defaultDeployDaemonAccessible" class="warning">
+ {{ t('settings', 'Default Deploy daemon is not accessible') }}
+ </p>
+ <NcCheckboxRadioSwitch v-if="app.canUnInstall"
+ :checked="removeData"
+ :disabled="installing || isLoading || !defaultDeployDaemonAccessible"
+ @update:checked="toggleRemoveData">
+ {{ t('settings', 'Delete data on remove') }}
+ </NcCheckboxRadioSwitch>
</div>
<ul class="app-details__dependencies">
@@ -114,7 +115,7 @@
</li>
</ul>
- <div v-if="lastModified" class="app-details__section">
+ <div v-if="lastModified && !app.shipped" class="app-details__section">
<h4>
{{ t('settings', 'Latest updated') }}
</h4>
@@ -161,7 +162,7 @@
:aria-label="t('settings', 'Report a bug')"
:title="t('settings', 'Report a bug')">
<template #icon>
- <NcIconSvgWrapper :path="mdiBug" />
+ <NcIconSvgWrapper :path="mdiBugOutline" />
</template>
</NcButton>
<NcButton :disabled="!app.bugs"
@@ -169,7 +170,7 @@
:aria-label="t('settings', 'Request feature')"
:title="t('settings', 'Request feature')">
<template #icon>
- <NcIconSvgWrapper :path="mdiFeatureSearch" />
+ <NcIconSvgWrapper :path="mdiFeatureSearchOutline" />
</template>
</NcButton>
<NcButton v-if="app.appstoreData?.discussion"
@@ -177,7 +178,7 @@
:aria-label="t('settings', 'Ask questions or discuss')"
:title="t('settings', 'Ask questions or discuss')">
<template #icon>
- <NcIconSvgWrapper :path="mdiTooltipQuestion" />
+ <NcIconSvgWrapper :path="mdiTooltipQuestionOutline" />
</template>
</NcButton>
<NcButton v-if="!app.internal"
@@ -190,20 +191,33 @@
</NcButton>
</div>
</div>
+
+ <AppDeployOptionsModal v-if="app?.app_api"
+ :show.sync="showDeployOptionsModal"
+ :app="app" />
+ <DaemonSelectionDialog v-if="app?.app_api"
+ :show.sync="showSelectDaemonModal"
+ :app="app"
+ :deploy-options="deployOptions" />
</div>
</NcAppSidebarTab>
</template>
<script>
-import NcAppSidebarTab from '@nextcloud/vue/dist/Components/NcAppSidebarTab.js'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcDateTime from '@nextcloud/vue/dist/Components/NcDateTime.js'
-import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
-import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
+import { subscribe, unsubscribe } from '@nextcloud/event-bus'
+import NcAppSidebarTab from '@nextcloud/vue/components/NcAppSidebarTab'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcDateTime from '@nextcloud/vue/components/NcDateTime'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import NcSelect from '@nextcloud/vue/components/NcSelect'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import AppDeployOptionsModal from './AppDeployOptionsModal.vue'
+import DaemonSelectionDialog from '../AppAPI/DaemonSelectionDialog.vue'
import AppManagement from '../../mixins/AppManagement.js'
-import { mdiBug, mdiFeatureSearch, mdiStar, mdiTextBox, mdiTooltipQuestion } from '@mdi/js'
+import { mdiBugOutline, mdiFeatureSearchOutline, mdiStar, mdiTextBoxOutline, mdiTooltipQuestionOutline, mdiToyBrickPlusOutline } from '@mdi/js'
import { useAppsStore } from '../../store/apps-store'
+import { useAppApiStore } from '../../store/app-api-store'
export default {
name: 'AppDetailsTab',
@@ -214,6 +228,9 @@ export default {
NcDateTime,
NcIconSvgWrapper,
NcSelect,
+ NcCheckboxRadioSwitch,
+ AppDeployOptionsModal,
+ DaemonSelectionDialog,
},
mixins: [AppManagement],
@@ -226,21 +243,28 @@ export default {
setup() {
const store = useAppsStore()
+ const appApiStore = useAppApiStore()
return {
store,
+ appApiStore,
- mdiBug,
- mdiFeatureSearch,
+ mdiBugOutline,
+ mdiFeatureSearchOutline,
mdiStar,
- mdiTextBox,
- mdiTooltipQuestion,
+ mdiTextBoxOutline,
+ mdiTooltipQuestionOutline,
+ mdiToyBrickPlusOutline,
}
},
data() {
return {
groupCheckedAppsData: false,
+ removeData: false,
+ showDeployOptionsModal: false,
+ showSelectDaemonModal: false,
+ deployOptions: null,
}
},
@@ -345,10 +369,45 @@ export default {
.sort((a, b) => a.name.localeCompare(b.name))
},
},
+ watch: {
+ 'app.id'() {
+ this.removeData = false
+ },
+ },
+ beforeUnmount() {
+ this.deployOptions = null
+ unsubscribe('showDaemonSelectionModal')
+ },
mounted() {
if (this.app.groups.length > 0) {
this.groupCheckedAppsData = true
}
+ subscribe('showDaemonSelectionModal', (deployOptions) => {
+ this.showSelectionModal(deployOptions)
+ })
+ },
+ methods: {
+ toggleRemoveData() {
+ this.removeData = !this.removeData
+ },
+ showSelectionModal(deployOptions = null) {
+ this.deployOptions = deployOptions
+ this.showSelectDaemonModal = true
+ },
+ async enableButtonAction() {
+ if (!this.app?.app_api) {
+ this.enable(this.app.id)
+ return
+ }
+ await this.appApiStore.fetchDockerDaemons()
+ if (this.appApiStore.dockerDaemons.length === 1 && this.app.needsDownload) {
+ this.enable(this.app.id, this.appApiStore.dockerDaemons[0])
+ } else if (this.app.needsDownload) {
+ this.showSelectionModal()
+ } else {
+ this.enable(this.app.id, this.app.daemon)
+ }
+ },
},
}
</script>
@@ -362,6 +421,7 @@ export default {
&-manage {
// if too many, shrink them and ellipsis
display: flex;
+ align-items: center;
input {
flex: 0 1 auto;
min-width: 0;
@@ -419,6 +479,7 @@ export default {
border-color: var(--color-error);
background: var(--color-main-background);
}
+
.force:hover,
.force:active {
color: var(--color-main-background);
diff --git a/apps/settings/src/components/AppStoreSidebar/AppReleasesTab.vue b/apps/settings/src/components/AppStoreSidebar/AppReleasesTab.vue
index 13e6ecff28f..e65df0341db 100644
--- a/apps/settings/src/components/AppStoreSidebar/AppReleasesTab.vue
+++ b/apps/settings/src/components/AppStoreSidebar/AppReleasesTab.vue
@@ -1,25 +1,7 @@
<!--
- - @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
- -
- - @author Julius Härtl <jus@bitgrid.net>
- - @author Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<NcAppSidebarTab v-if="hasChangelog"
id="changelog"
@@ -43,8 +25,8 @@ import { mdiClockFast } from '@mdi/js'
import { getLanguage, translate as t } from '@nextcloud/l10n'
import { computed } from 'vue'
-import NcAppSidebarTab from '@nextcloud/vue/dist/Components/NcAppSidebarTab.js'
-import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
+import NcAppSidebarTab from '@nextcloud/vue/components/NcAppSidebarTab'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import Markdown from '../Markdown.vue'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
diff --git a/apps/settings/src/components/AuthToken.vue b/apps/settings/src/components/AuthToken.vue
index 12d801d0db1..15286adb135 100644
--- a/apps/settings/src/components/AuthToken.vue
+++ b/apps/settings/src/components/AuthToken.vue
@@ -1,24 +1,7 @@
<!--
- - @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- -
- - @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- - @author Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -->
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<tr :class="['auth-token', { 'auth-token--wiping': wiping }]" :data-id="token.id">
@@ -97,18 +80,18 @@
import type { PropType } from 'vue'
import type { IToken } from '../store/authtoken'
-import { mdiCheck, mdiCellphone, mdiTablet, mdiMonitor, mdiWeb, mdiKey, mdiMicrosoftEdge, mdiFirefox, mdiGoogleChrome, mdiAppleSafari, mdiAndroid, mdiAppleIos } from '@mdi/js'
+import { mdiCheck, mdiCellphone, mdiTablet, mdiMonitor, mdiWeb, mdiKeyOutline, mdiMicrosoftEdge, mdiFirefox, mdiGoogleChrome, mdiAppleSafari, mdiAndroid, mdiAppleIos } from '@mdi/js'
import { translate as t } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import { TokenType, useAuthTokenStore } from '../store/authtoken.ts'
-import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
-import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
-import NcActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox.js'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcDateTime from '@nextcloud/vue/dist/Components/NcDateTime.js'
-import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
+import NcActions from '@nextcloud/vue/components/NcActions'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcActionCheckbox from '@nextcloud/vue/components/NcActionCheckbox'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcDateTime from '@nextcloud/vue/components/NcDateTime'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
// When using capture groups the following parts are extracted the first is used as the version number, the second as the OS
const userAgentMap = {
@@ -192,8 +175,8 @@ export default defineComponent({
return this.token.type === TokenType.PERMANENT_TOKEN
},
/**
- * Object ob the current user agend used by the token
- * @return Either an object containing user agent information or null if unknown
+ * Object ob the current user agent used by the token
+ * This either returns an object containing user agent information or `null` if unknown
*/
client() {
// pretty format sync client user agent
@@ -232,7 +215,7 @@ export default defineComponent({
tokenIcon() {
// For custom created app tokens / app passwords
if (this.token.type === TokenType.PERMANENT_TOKEN) {
- return mdiKey
+ return mdiKeyOutline
}
switch (this.client?.id) {
diff --git a/apps/settings/src/components/AuthTokenList.vue b/apps/settings/src/components/AuthTokenList.vue
index e4759adba8d..dbe3b9596d8 100644
--- a/apps/settings/src/components/AuthTokenList.vue
+++ b/apps/settings/src/components/AuthTokenList.vue
@@ -1,24 +1,7 @@
<!--
- - @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- -
- - @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- - @author Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -->
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<table id="app-tokens-table" class="token-list">
diff --git a/apps/settings/src/components/AuthTokenSection.vue b/apps/settings/src/components/AuthTokenSection.vue
index a1689846130..3a216f6407f 100644
--- a/apps/settings/src/components/AuthTokenSection.vue
+++ b/apps/settings/src/components/AuthTokenSection.vue
@@ -1,24 +1,7 @@
<!--
- - @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- -
- - @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- - @author Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -->
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<div id="security" class="section">
diff --git a/apps/settings/src/components/AuthTokenSetup.vue b/apps/settings/src/components/AuthTokenSetup.vue
index 9e709397362..b93086c9e88 100644
--- a/apps/settings/src/components/AuthTokenSetup.vue
+++ b/apps/settings/src/components/AuthTokenSetup.vue
@@ -1,24 +1,7 @@
<!--
- - @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- -
- - @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
- - @author Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -->
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<form id="generate-app-token-section"
@@ -48,8 +31,8 @@ import { translate as t } from '@nextcloud/l10n'
import { defineComponent } from 'vue'
import { useAuthTokenStore, type ITokenResponse } from '../store/authtoken'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
import AuthTokenSetupDialog from './AuthTokenSetupDialog.vue'
import logger from '../logger'
@@ -98,8 +81,8 @@ export default defineComponent({
<style lang="scss" scoped>
.app-name-text-field {
height: 44px !important;
- padding-left: 12px;
- margin-right: 12px;
+ padding-inline-start: 12px;
+ margin-inline-end: 12px;
width: 200px;
}
diff --git a/apps/settings/src/components/AuthTokenSetupDialog.vue b/apps/settings/src/components/AuthTokenSetupDialog.vue
index 20e3b29d0f1..3b8fac8dc1d 100644
--- a/apps/settings/src/components/AuthTokenSetupDialog.vue
+++ b/apps/settings/src/components/AuthTokenSetupDialog.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @author Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<NcDialog :open.sync="open"
@@ -70,10 +53,10 @@ import { getRootUrl } from '@nextcloud/router'
import { defineComponent, type PropType } from 'vue'
import QR from '@chenfengyuan/vue-qrcode'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
-import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcDialog from '@nextcloud/vue/components/NcDialog'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
import logger from '../logger'
diff --git a/apps/settings/src/components/BasicSettings/BackgroundJob.vue b/apps/settings/src/components/BasicSettings/BackgroundJob.vue
index c19eac47889..a9a3cbb9cef 100644
--- a/apps/settings/src/components/BasicSettings/BackgroundJob.vue
+++ b/apps/settings/src/components/BasicSettings/BackgroundJob.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2022 Carl Schwan <carl@carlschwan.eu>
- -
- - @author Carl Schwan <carl@carlschwan.eu>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -73,6 +56,7 @@
@update:checked="onBackgroundJobModeChanged">
{{ t('settings', 'Cron (Recommended)') }}
</NcCheckboxRadioSwitch>
+ <!-- eslint-disable-next-line vue/no-v-html The translation is sanitized-->
<em v-html="cronLabel" />
</NcSettingsSection>
</template>
@@ -80,13 +64,15 @@
<script>
import { loadState } from '@nextcloud/initial-state'
import { showError } from '@nextcloud/dialogs'
-import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
-import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
-import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
-import moment from '@nextcloud/moment'
-import axios from '@nextcloud/axios'
import { generateOcsUrl } from '@nextcloud/router'
import { confirmPassword } from '@nextcloud/password-confirmation'
+import axios from '@nextcloud/axios'
+import moment from '@nextcloud/moment'
+
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+
import '@nextcloud/password-confirmation/dist/style.css'
const lastCron = loadState('settings', 'lastCron')
@@ -126,7 +112,7 @@ export default {
desc += '<br>' + t('settings', 'The PHP POSIX extension is required. See {linkstart}PHP documentation{linkend} for more details.', {
linkstart: '<a target="_blank" rel="noreferrer nofollow" class="external" href="https://www.php.net/manual/en/book.posix.php">',
linkend: '</a>',
- }, undefined, { escape: false, sanitize: false })
+ }, undefined, { escape: false })
}
return desc
},
@@ -199,6 +185,7 @@ export default {
background-color: var(--color-error);
width: initial;
}
+
.warning {
margin-top: 8px;
padding: 5px;
@@ -207,6 +194,7 @@ export default {
background-color: var(--color-warning);
width: initial;
}
+
.ajaxSwitch {
margin-top: 1rem;
}
diff --git a/apps/settings/src/components/BasicSettings/ProfileSettings.vue b/apps/settings/src/components/BasicSettings/ProfileSettings.vue
index b64adb7bcf2..276448cd97b 100644
--- a/apps/settings/src/components/BasicSettings/ProfileSettings.vue
+++ b/apps/settings/src/components/BasicSettings/ProfileSettings.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2022 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -47,7 +30,7 @@ import { saveProfileDefault } from '../../service/ProfileService.js'
import { validateBoolean } from '../../utils/validate.js'
import logger from '../../logger.ts'
-import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
const profileEnabledByDefault = loadState('settings', 'profileEnabledByDefault', true)
@@ -97,6 +80,3 @@ export default {
},
}
</script>
-
-<style lang="scss" scoped>
-</style>
diff --git a/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue b/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue
index 879eb8e62d8..9ee1680516e 100644
--- a/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue
+++ b/apps/settings/src/components/DeclarativeSettings/DeclarativeSection.vue
@@ -1,12 +1,15 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <NcSettingsSection
- class="declarative-settings-section"
+ <NcSettingsSection class="declarative-settings-section"
:name="t(formApp, form.title)"
:description="t(formApp, form.description)"
:doc-url="form.doc_url || ''">
<div v-for="formField in formFields"
- :key="formField.id"
- class="declarative-form-field"
+ :key="formField.id"
+ class="declarative-form-field"
:aria-label="t('settings', '{app}\'s declarative setting field: {name}', { app: formApp, name: t(formApp, formField.title) })"
:class="{
'declarative-form-field-text': isTextFormField(formField),
@@ -16,39 +19,35 @@
'declarative-form-field-multi_checkbox': formField.type === 'multi-checkbox',
'declarative-form-field-radio': formField.type === 'radio'
}">
-
<template v-if="isTextFormField(formField)">
<div class="input-wrapper">
- <NcInputField
- :type="formField.type"
+ <NcInputField :type="formField.type"
:label="t(formApp, formField.title)"
:value.sync="formFieldsData[formField.id].value"
:placeholder="t(formApp, formField.placeholder)"
@update:value="onChangeDebounced(formField)"
- @submit="updateDeclarativeSettingsValue(formField)"/>
+ @submit="updateDeclarativeSettingsValue(formField)" />
</div>
- <span class="hint">{{ t(formApp, formField.description) }}</span>
+ <span v-if="formField.description" class="hint">{{ t(formApp, formField.description) }}</span>
</template>
<template v-if="formField.type === 'select'">
<label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label>
<div class="input-wrapper">
- <NcSelect
- :id="formField.id + '_field'"
+ <NcSelect :id="formField.id + '_field'"
:options="formField.options"
:placeholder="t(formApp, formField.placeholder)"
:label-outside="true"
:value="formFieldsData[formField.id].value"
- @input="(value) => updateFormFieldDataValue(value, formField, true)"/>
+ @input="(value) => updateFormFieldDataValue(value, formField, true)" />
</div>
- <span class="hint">{{ t(formApp, formField.description) }}</span>
+ <span v-if="formField.description" class="hint">{{ t(formApp, formField.description) }}</span>
</template>
<template v-if="formField.type === 'multi-select'">
<label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label>
<div class="input-wrapper">
- <NcSelect
- :id="formField.id + '_field'"
+ <NcSelect :id="formField.id + '_field'"
:options="formField.options"
:placeholder="t(formApp, formField.placeholder)"
:multiple="true"
@@ -58,30 +57,29 @@
formFieldsData[formField.id].value = value
updateDeclarativeSettingsValue(formField, JSON.stringify(formFieldsData[formField.id].value))
}
- "/>
+ " />
</div>
- <span class="hint">{{ t(formApp, formField.description) }}</span>
+ <span v-if="formField.description" class="hint">{{ t(formApp, formField.description) }}</span>
</template>
<template v-if="formField.type === 'checkbox'">
- <label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label>
- <NcCheckboxRadioSwitch
- :id="formField.id + '_field'"
+ <label v-if="formField.label" :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label>
+ <NcCheckboxRadioSwitch :id="formField.id + '_field'"
:checked="Boolean(formFieldsData[formField.id].value)"
+ type="switch"
@update:checked="(value) => {
formField.value = value
updateFormFieldDataValue(+value, formField, true)
}
- ">
- {{ t(formApp, formField.label) }}
+ ">
+ {{ t(formApp, formField.label ?? formField.title) }}
</NcCheckboxRadioSwitch>
- <span class="hint">{{ t(formApp, formField.description) }}</span>
+ <span v-if="formField.description" class="hint">{{ t(formApp, formField.description) }}</span>
</template>
<template v-if="formField.type === 'multi-checkbox'">
<label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label>
- <NcCheckboxRadioSwitch
- v-for="option in formField.options"
+ <NcCheckboxRadioSwitch v-for="option in formField.options"
:id="formField.id + '_field_' + option.value"
:key="option.value"
:checked="formFieldsData[formField.id].value[option.value]"
@@ -90,16 +88,15 @@
// Update without re-generating initial formFieldsData.value object as the link to components are lost
updateDeclarativeSettingsValue(formField, JSON.stringify(formFieldsData[formField.id].value))
}
- ">
+ ">
{{ t(formApp, option.name) }}
</NcCheckboxRadioSwitch>
- <span class="hint">{{ t(formApp, formField.description) }}</span>
+ <span v-if="formField.description" class="hint">{{ t(formApp, formField.description) }}</span>
</template>
<template v-if="formField.type === 'radio'">
<label :for="formField.id + '_field'">{{ t(formApp, formField.title) }}</label>
- <NcCheckboxRadioSwitch
- v-for="option in formField.options"
+ <NcCheckboxRadioSwitch v-for="option in formField.options"
:key="option.value"
:value="option.value"
type="radio"
@@ -107,7 +104,7 @@
@update:checked="(value) => updateFormFieldDataValue(value, formField, true)">
{{ t(formApp, option.name) }}
</NcCheckboxRadioSwitch>
- <span class="hint">{{ t(formApp, formField.description) }}</span>
+ <span v-if="formField.description" class="hint">{{ t(formApp, formField.description) }}</span>
</template>
</div>
</NcSettingsSection>
@@ -118,10 +115,11 @@ import axios from '@nextcloud/axios'
import { generateOcsUrl } from '@nextcloud/router'
import { showError } from '@nextcloud/dialogs'
import debounce from 'debounce'
-import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
-import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js'
-import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
-import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
+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',
@@ -142,9 +140,6 @@ export default {
formFieldsData: {},
}
},
- beforeMount() {
- this.initFormFieldsData()
- },
computed: {
formApp() {
return this.form.app || ''
@@ -153,6 +148,9 @@ export default {
return this.form.fields || []
},
},
+ beforeMount() {
+ this.initFormFieldsData()
+ },
methods: {
initFormFieldsData() {
this.form.fields.forEach((formField) => {
@@ -171,7 +169,7 @@ export default {
this.$set(formField, 'value', JSON.parse(formField.value))
// Merge possible new options
formField.options.forEach(option => {
- if (!formField.value.hasOwnProperty(option.value)) {
+ if (!Object.prototype.hasOwnProperty.call(formField.value, option.value)) {
this.$set(formField.value, option.value, false)
}
})
@@ -205,14 +203,24 @@ 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,
value: value === null ? this.formFieldsData[formField.id].value : value,
- });
+ })
} catch (err) {
console.debug(err)
showError(t('settings', 'Failed to save setting'))
@@ -232,7 +240,6 @@ export default {
<style lang="scss" scoped>
.declarative-form-field {
- margin: 20px 0;
padding: 10px 0;
.input-wrapper {
@@ -247,8 +254,8 @@ export default {
.hint {
display: inline-block;
color: var(--color-text-maxcontrast);
- margin-left: 8px;
- padding-top: 5px;
+ margin-inline-start: 8px;
+ padding-block-start: 5px;
}
&-radio, &-multi_checkbox {
diff --git a/apps/settings/src/components/Encryption.vue b/apps/settings/src/components/Encryption.vue
deleted file mode 100644
index 0953d22661f..00000000000
--- a/apps/settings/src/components/Encryption.vue
+++ /dev/null
@@ -1,209 +0,0 @@
-<!--
- - @copyright 2022 Carl Schwan <carl@carlschwan.eu>
- -
- - @author Carl Schwan <carl@carlschwan.eu>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
--->
-
-<template>
- <NcSettingsSection :name="t('settings', 'Server-side encryption')"
- :description="t('settings', 'Server-side encryption makes it possible to encrypt files which are uploaded to this server. This comes with limitations like a performance penalty, so enable this only if needed.')"
- :doc-url="encryptionAdminDoc">
- <NcCheckboxRadioSwitch :checked="encryptionEnabled || shouldDisplayWarning"
- :disabled="encryptionEnabled"
- type="switch"
- @update:checked="displayWarning">
- {{ t('settings', 'Enable server-side encryption') }}
- </NcCheckboxRadioSwitch>
-
- <div v-if="shouldDisplayWarning && !encryptionEnabled" class="notecard warning" role="alert">
- <p>{{ t('settings', 'Please read carefully before activating server-side encryption:') }}</p>
- <ul>
- <li>{{ t('settings', 'Once encryption is enabled, all files uploaded to the server from that point forward will be encrypted at rest on the server. It will only be possible to disable encryption at a later date if the active encryption module supports that function, and all pre-conditions (e.g. setting a recover key) are met.') }}</li>
- <li>{{ t('settings', 'Encryption alone does not guarantee security of the system. Please see documentation for more information about how the encryption app works, and the supported use cases.') }}</li>
- <li>{{ t('settings', 'Be aware that encryption always increases the file size.') }}</li>
- <li>{{ t('settings', 'It is always good to create regular backups of your data, in case of encryption make sure to backup the encryption keys along with your data.') }}</li>
- </ul>
-
- <p class="margin-bottom">
- {{ t('settings', 'This is the final warning: Do you really want to enable encryption?') }}
- </p>
- <NcButton type="primary"
- @click="enableEncryption()">
- {{ t('settings', "Enable encryption") }}
- </NcButton>
- </div>
-
- <div v-if="encryptionEnabled">
- <div v-if="encryptionReady">
- <p v-if="encryptionModules.length === 0">
- {{ t('settings', 'No encryption module loaded, please enable an encryption module in the app menu.') }}
- </p>
- <template v-else>
- <h3>{{ t('settings', 'Select default encryption module:') }}</h3>
- <fieldset>
- <NcCheckboxRadioSwitch v-for="(module, id) in encryptionModules"
- :key="id"
- :checked.sync="defaultCheckedModule"
- :value="id"
- type="radio"
- name="default_encryption_module"
- @update:checked="checkDefaultModule">
- {{ module.displayName }}
- </NcCheckboxRadioSwitch>
- </fieldset>
- </template>
- </div>
-
- <div v-else-if="externalBackendsEnabled" v-html="migrationMessage" />
- </div>
- </NcSettingsSection>
-</template>
-
-<script>
-import axios from '@nextcloud/axios'
-import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
-import { loadState } from '@nextcloud/initial-state'
-import { getLoggerBuilder } from '@nextcloud/logger'
-
-import { generateOcsUrl } from '@nextcloud/router'
-import { confirmPassword } from '@nextcloud/password-confirmation'
-import '@nextcloud/password-confirmation/dist/style.css'
-import { showError } from '@nextcloud/dialogs'
-
-const logger = getLoggerBuilder()
- .setApp('settings')
- .detectUser()
- .build()
-
-export default {
- name: 'Encryption',
- components: {
- NcCheckboxRadioSwitch,
- NcSettingsSection,
- NcButton,
- },
- data() {
- const encryptionModules = loadState('settings', 'encryption-modules')
- return {
- encryptionReady: loadState('settings', 'encryption-ready'),
- encryptionEnabled: loadState('settings', 'encryption-enabled'),
- externalBackendsEnabled: loadState('settings', 'external-backends-enabled'),
- encryptionAdminDoc: loadState('settings', 'encryption-admin-doc'),
- encryptionModules,
- shouldDisplayWarning: false,
- migrating: false,
- defaultCheckedModule: Object.entries(encryptionModules).find((module) => module[1].default)[0],
- }
- },
- computed: {
- migrationMessage() {
- return t('settings', 'You need to migrate your encryption keys from the old encryption (ownCloud <= 8.0) to the new one. Please enable the "Default encryption module" and run {command}', {
- command: '"occ encryption:migrate"',
- })
- },
- },
- methods: {
- displayWarning() {
- if (!this.encryptionEnabled) {
- this.shouldDisplayWarning = !this.shouldDisplayWarning
- } else {
- this.encryptionEnabled = false
- this.shouldDisplayWarning = false
- }
- },
- async update(key, value) {
- await confirmPassword()
-
- const url = generateOcsUrl('/apps/provisioning_api/api/v1/config/apps/{appId}/{key}', {
- appId: 'core',
- key,
- })
-
- try {
- const { data } = await axios.post(url, {
- value: value,
- })
- this.handleResponse({
- status: data.ocs?.meta?.status,
- })
- } catch (e) {
- this.handleResponse({
- errorMessage: t('settings', 'Unable to update server side encryption config'),
- error: e,
- })
- }
- },
- async checkDefaultModule() {
- await this.update('default_encryption_module', this.defaultCheckedModule)
- },
- async enableEncryption() {
- this.encryptionEnabled = true
- await this.update('encryption_enabled', 'yes')
- },
- async handleResponse({ status, errorMessage, error }) {
- if (status !== 'ok') {
- showError(errorMessage)
- logger.error(errorMessage, { error })
- }
- },
- },
-}
-</script>
-
-<style lang="scss" scoped>
-
-.notecard.success {
- --note-background: rgba(var(--color-success-rgb), 0.2);
- --note-theme: var(--color-success);
-}
-
-.notecard.error {
- --note-background: rgba(var(--color-error-rgb), 0.2);
- --note-theme: var(--color-error);
-}
-
-.notecard.warning {
- --note-background: rgba(var(--color-warning-rgb), 0.2);
- --note-theme: var(--color-warning);
-}
-
-#body-settings .notecard {
- color: var(--color-text-light);
- background-color: var(--note-background);
- border: 1px solid var(--color-border);
- border-left: 4px solid var(--note-theme);
- border-radius: var(--border-radius);
- box-shadow: rgba(43, 42, 51, 0.05) 0px 1px 2px 0px;
- margin: 1rem 0;
- margin-top: 1rem;
- padding: 1rem;
-}
-
-li {
- list-style-type: initial;
- margin-left: 1rem;
- padding: 0.25rem 0;
-}
-
-.margin-bottom {
- margin-bottom: 0.75rem;
-}
-</style>
diff --git a/apps/settings/src/components/Encryption/EncryptionSettings.vue b/apps/settings/src/components/Encryption/EncryptionSettings.vue
new file mode 100644
index 00000000000..f4db63ce53c
--- /dev/null
+++ b/apps/settings/src/components/Encryption/EncryptionSettings.vue
@@ -0,0 +1,197 @@
+<!--
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<script setup lang="ts">
+import type { OCSResponse } from '@nextcloud/typings/ocs'
+import { showError, spawnDialog } from '@nextcloud/dialogs'
+import { loadState } from '@nextcloud/initial-state'
+import { t } from '@nextcloud/l10n'
+import { confirmPassword } from '@nextcloud/password-confirmation'
+import { generateOcsUrl } from '@nextcloud/router'
+import { ref } from 'vue'
+import { textExistingFilesNotEncrypted } from './sharedTexts.ts'
+
+import axios from '@nextcloud/axios'
+import logger from '../../logger.ts'
+
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
+import EncryptionWarningDialog from './EncryptionWarningDialog.vue'
+
+interface EncryptionModule {
+ default?: boolean
+ displayName: string
+}
+
+const allEncryptionModules = loadState<never[]|Record<string, EncryptionModule>>('settings', 'encryption-modules')
+/** Available encryption modules on the backend */
+const encryptionModules = Array.isArray(allEncryptionModules) ? [] : Object.entries(allEncryptionModules).map(([id, module]) => ({ ...module, id }))
+/** ID of the default encryption module */
+const defaultCheckedModule = encryptionModules.find((module) => module.default)?.id
+
+/** Is the server side encryptio ready to be enabled */
+const encryptionReady = loadState<boolean>('settings', 'encryption-ready')
+/** Are external backends enabled (legacy ownCloud stuff) */
+const externalBackendsEnabled = loadState<boolean>('settings', 'external-backends-enabled')
+/** URL to the admin docs */
+const encryptionAdminDoc = loadState<string>('settings', 'encryption-admin-doc')
+
+/** Is the encryption enabled */
+const encryptionEnabled = ref(loadState<boolean>('settings', 'encryption-enabled'))
+
+/** Loading state while enabling encryption (e.g. because the confirmation dialog is open) */
+const loadingEncryptionState = ref(false)
+
+/**
+ * Open the encryption-enabling warning (spawns a dialog)
+ * @param enabled The enabled state of encryption
+ */
+function displayWarning(enabled: boolean) {
+ if (loadingEncryptionState.value || enabled === false) {
+ return
+ }
+
+ loadingEncryptionState.value = true
+ spawnDialog(EncryptionWarningDialog, {}, async (confirmed) => {
+ try {
+ if (confirmed) {
+ await enableEncryption()
+ }
+ } finally {
+ loadingEncryptionState.value = false
+ }
+ })
+}
+
+/**
+ * Update an encryption setting on the backend
+ * @param key The setting to update
+ * @param value The new value
+ */
+async function update(key: string, value: string) {
+ await confirmPassword()
+
+ const url = generateOcsUrl('/apps/provisioning_api/api/v1/config/apps/{appId}/{key}', {
+ appId: 'core',
+ key,
+ })
+
+ try {
+ const { data } = await axios.post<OCSResponse>(url, {
+ value,
+ })
+ if (data.ocs.meta.status !== 'ok') {
+ throw new Error('Unsuccessful OCS response', { cause: data.ocs })
+ }
+ } catch (error) {
+ showError(t('settings', 'Unable to update server side encryption config'))
+ logger.error('Unable to update server side encryption config', { error })
+ return false
+ }
+ return true
+}
+
+/**
+ * Choose the default encryption module
+ */
+async function checkDefaultModule(): Promise<void> {
+ if (defaultCheckedModule) {
+ await update('default_encryption_module', defaultCheckedModule)
+ }
+}
+
+/**
+ * Enable encryption - sends an async POST request
+ */
+async function enableEncryption(): Promise<void> {
+ encryptionEnabled.value = await update('encryption_enabled', 'yes')
+}
+</script>
+
+<template>
+ <NcSettingsSection :name="t('settings', 'Server-side encryption')"
+ :description="t('settings', 'Server-side encryption makes it possible to encrypt files which are uploaded to this server. This comes with limitations like a performance penalty, so enable this only if needed.')"
+ :doc-url="encryptionAdminDoc">
+ <NcNoteCard v-if="encryptionEnabled" type="info">
+ <p>
+ {{ textExistingFilesNotEncrypted }}
+ {{ t('settings', 'To encrypt all existing files run this OCC command:') }}
+ </p>
+ <code>
+ <pre>occ encryption:encrypt-all</pre>
+ </code>
+ </NcNoteCard>
+
+ <NcCheckboxRadioSwitch :class="{ disabled: encryptionEnabled }"
+ :checked="encryptionEnabled"
+ :aria-disabled="encryptionEnabled ? 'true' : undefined"
+ :aria-describedby="encryptionEnabled ? 'server-side-encryption-disable-hint' : undefined"
+ :loading="loadingEncryptionState"
+ type="switch"
+ @update:checked="displayWarning">
+ {{ t('settings', 'Enable server-side encryption') }}
+ </NcCheckboxRadioSwitch>
+ <p v-if="encryptionEnabled" id="server-side-encryption-disable-hint" class="disable-hint">
+ {{ t('settings', 'Disabling server side encryption is only possible using OCC, please refer to the documentation.') }}
+ </p>
+
+ <NcNoteCard v-if="encryptionModules.length === 0"
+ type="warning"
+ :text="t('settings', 'No encryption module loaded, please enable an encryption module in the app menu.')" />
+
+ <template v-else-if="encryptionEnabled">
+ <div v-if="encryptionReady && encryptionModules.length > 0">
+ <h3>{{ t('settings', 'Select default encryption module:') }}</h3>
+ <fieldset>
+ <NcCheckboxRadioSwitch v-for="module in encryptionModules"
+ :key="module.id"
+ :checked.sync="defaultCheckedModule"
+ :value="module.id"
+ type="radio"
+ name="default_encryption_module"
+ @update:checked="checkDefaultModule">
+ {{ module.displayName }}
+ </NcCheckboxRadioSwitch>
+ </fieldset>
+ </div>
+
+ <div v-else-if="externalBackendsEnabled">
+ {{
+ t(
+ 'settings',
+ 'You need to migrate your encryption keys from the old encryption (ownCloud <= 8.0) to the new one. Please enable the "Default encryption module" and run {command}',
+ { command: '"occ encryption:migrate"' },
+ )
+ }}
+ </div>
+ </template>
+ </NcSettingsSection>
+</template>
+
+<style scoped>
+code {
+ background-color: var(--color-background-dark);
+ color: var(--color-main-text);
+
+ display: block;
+ margin-block-start: 0.5rem;
+ padding: .25lh .5lh;
+ width: fit-content;
+}
+
+.disabled {
+ opacity: .75;
+}
+
+.disabled :deep(*) {
+ cursor: not-allowed !important;
+}
+
+.disable-hint {
+ color: var(--color-text-maxcontrast);
+ padding-inline-start: 10px;
+}
+</style>
diff --git a/apps/settings/src/components/Encryption/EncryptionWarningDialog.vue b/apps/settings/src/components/Encryption/EncryptionWarningDialog.vue
new file mode 100644
index 00000000000..f229544a7d9
--- /dev/null
+++ b/apps/settings/src/components/Encryption/EncryptionWarningDialog.vue
@@ -0,0 +1,91 @@
+<!--
+ - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<script setup lang="ts">
+import type { IDialogButton } from '@nextcloud/dialogs'
+
+import { t } from '@nextcloud/l10n'
+import { textExistingFilesNotEncrypted } from './sharedTexts.ts'
+import NcDialog from '@nextcloud/vue/components/NcDialog'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+
+const emit = defineEmits<{
+ (e: 'close', encrypt: boolean): void
+}>()
+
+const buttons: IDialogButton[] = [
+ {
+ label: t('settings', 'Cancel encryption'),
+ // @ts-expect-error Needs to be fixed in the dialogs library - value is allowed but missing from the types
+ type: 'tertiary',
+ callback: () => emit('close', false),
+ },
+ {
+ label: t('settings', 'Enable encryption'),
+ type: 'error',
+ callback: () => emit('close', true),
+ },
+]
+
+/**
+ * When closed we need to emit the close event
+ * @param isOpen open state of the dialog
+ */
+function onUpdateOpen(isOpen: boolean) {
+ if (!isOpen) {
+ emit('close', false)
+ }
+}
+</script>
+
+<template>
+ <NcDialog :buttons="buttons"
+ :name="t('settings', 'Confirm enabling encryption')"
+ size="normal"
+ @update:open="onUpdateOpen">
+ <NcNoteCard type="warning">
+ <p>
+ {{ t('settings', 'Please read carefully before activating server-side encryption:') }}
+ <ul>
+ <li>
+ {{ t('settings', 'Once encryption is enabled, all files uploaded to the server from that point forward will be encrypted at rest on the server. It will only be possible to disable encryption at a later date if the active encryption module supports that function, and all pre-conditions (e.g. setting a recover key) are met.') }}
+ </li>
+ <li>
+ {{ t('settings', 'By default a master key for the whole instance will be generated. Please check if that level of access is compliant with your needs.') }}
+ </li>
+ <li>
+ {{ t('settings', 'Encryption alone does not guarantee security of the system. Please see documentation for more information about how the encryption app works, and the supported use cases.') }}
+ </li>
+ <li>
+ {{ t('settings', 'Be aware that encryption always increases the file size.') }}
+ </li>
+ <li>
+ {{ t('settings', 'It is always good to create regular backups of your data, in case of encryption make sure to backup the encryption keys along with your data.') }}
+ </li>
+ <li>
+ {{ textExistingFilesNotEncrypted }}
+ {{ t('settings', 'Refer to the admin documentation on how to manually also encrypt existing files.') }}
+ </li>
+ </ul>
+ </p>
+ </NcNoteCard>
+ <p>
+ {{ t('settings', 'This is the final warning: Do you really want to enable encryption?') }}
+ </p>
+ </NcDialog>
+</template>
+
+<style scoped>
+li {
+ list-style-type: initial;
+ margin-inline-start: 1rem;
+ padding: 0.25rem 0;
+}
+
+p + p,
+div + p {
+ margin-block: 0.75rem;
+}
+</style>
diff --git a/apps/settings/src/components/Encryption/sharedTexts.ts b/apps/settings/src/components/Encryption/sharedTexts.ts
new file mode 100644
index 00000000000..94d23be07f2
--- /dev/null
+++ b/apps/settings/src/components/Encryption/sharedTexts.ts
@@ -0,0 +1,7 @@
+/*!
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { t } from '@nextcloud/l10n'
+
+export const textExistingFilesNotEncrypted = t('settings', 'For performance reasons, when you enable encryption on a Nextcloud server only new and changed files are encrypted.')
diff --git a/apps/settings/src/components/GroupListItem.vue b/apps/settings/src/components/GroupListItem.vue
index ca50b38b200..69bb8a3f575 100644
--- a/apps/settings/src/components/GroupListItem.vue
+++ b/apps/settings/src/components/GroupListItem.vue
@@ -1,24 +1,7 @@
<!--
- - @copyright Copyright (c) 2021 Martin Jänel <spammemore@posteo.de>
- -
- - @author Martin Jänel <spammemore@posteo.de>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<Fragment>
@@ -30,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"
@@ -46,6 +29,7 @@
</NcModal>
<NcAppNavigationItem :key="id"
+ ref="listItem"
:exact="true"
:name="name"
:to="{ name: 'group', params: { selectedGroup: encodeURIComponent(id) } }"
@@ -62,7 +46,7 @@
</NcCounterBubble>
</template>
<template #actions>
- <NcActionInput v-if="id !== 'admin' && id !== 'disabled' && settings.isAdmin"
+ <NcActionInput v-if="id !== 'admin' && id !== 'disabled' && (settings.isAdmin || settings.isDelegatedAdmin)"
ref="displayNameInput"
:trailing-button-label="t('settings', 'Submit')"
type="text"
@@ -73,12 +57,12 @@
<Pencil :size="20" />
</template>
</NcActionInput>
- <NcActionButton v-if="id !== 'admin' && id !== 'disabled' && settings.isAdmin"
+ <NcActionButton v-if="id !== 'admin' && id !== 'disabled' && (settings.isAdmin || settings.isDelegatedAdmin)"
@click="showRemoveGroupModal = true">
<template #icon>
<Delete :size="20" />
</template>
- {{ t('settings', 'Remove group') }}
+ {{ t('settings', 'Delete group') }}
</NcActionButton>
</template>
</NcAppNavigationItem>
@@ -88,17 +72,17 @@
<script>
import { Fragment } from 'vue-frag'
-import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
-import NcActionInput from '@nextcloud/vue/dist/Components/NcActionInput.js'
-import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcCounterBubble from '@nextcloud/vue/dist/Components/NcCounterBubble.js'
-import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
-import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcActionInput from '@nextcloud/vue/components/NcActionInput'
+import NcAppNavigationItem from '@nextcloud/vue/components/NcAppNavigationItem'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcCounterBubble from '@nextcloud/vue/components/NcCounterBubble'
+import NcModal from '@nextcloud/vue/components/NcModal'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
-import AccountGroup from 'vue-material-design-icons/AccountGroup.vue'
-import Delete from 'vue-material-design-icons/Delete.vue'
-import Pencil from 'vue-material-design-icons/Pencil.vue'
+import AccountGroup from 'vue-material-design-icons/AccountGroupOutline.vue'
+import Delete from 'vue-material-design-icons/DeleteOutline.vue'
+import Pencil from 'vue-material-design-icons/PencilOutline.vue'
import { showError } from '@nextcloud/dialogs'
@@ -195,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/Markdown.cy.ts b/apps/settings/src/components/Markdown.cy.ts
new file mode 100644
index 00000000000..ccdf43c26df
--- /dev/null
+++ b/apps/settings/src/components/Markdown.cy.ts
@@ -0,0 +1,58 @@
+/*!
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import Markdown from './Markdown.vue'
+
+describe('Markdown component', () => {
+ it('renders links', () => {
+ cy.mount(Markdown, {
+ propsData: {
+ text: 'This is [a link](http://example.com)!',
+ },
+ })
+
+ cy.contains('This is')
+ .find('a')
+ .should('exist')
+ .and('have.attr', 'href', 'http://example.com')
+ .and('contain.text', 'a link')
+ })
+
+ it('renders headings', () => {
+ cy.mount(Markdown, {
+ propsData: {
+ text: '# level 1\nText\n## level 2\nText\n### level 3\nText\n#### level 4\nText\n##### level 5\nText\n###### level 6\nText\n',
+ },
+ })
+
+ for (let level = 1; level <= 6; level++) {
+ cy.contains(`h${level}`, `level ${level}`)
+ .should('be.visible')
+ }
+ })
+
+ it('can limit headings', () => {
+ cy.mount(Markdown, {
+ propsData: {
+ text: '# level 1\nText\n## level 2\nText\n### level 3\nText\n#### level 4\nText\n##### level 5\nText\n###### level 6\nText\n',
+ minHeading: 4,
+ },
+ })
+
+ cy.get('h1').should('not.exist')
+ cy.get('h2').should('not.exist')
+ cy.get('h3').should('not.exist')
+ cy.get('h4')
+ .should('exist')
+ .and('contain.text', 'level 1')
+ cy.get('h5')
+ .should('exist')
+ .and('contain.text', 'level 2')
+ cy.contains('h6', 'level 3').should('exist')
+ cy.contains('h6', 'level 4').should('exist')
+ cy.contains('h6', 'level 5').should('exist')
+ cy.contains('h6', 'level 6').should('exist')
+ })
+})
diff --git a/apps/settings/src/components/Markdown.vue b/apps/settings/src/components/Markdown.vue
index 0e5c666f1c0..36535e46763 100644
--- a/apps/settings/src/components/Markdown.vue
+++ b/apps/settings/src/components/Markdown.vue
@@ -1,26 +1,10 @@
<!--
- - @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net>
- -
- - @author Julius Härtl <jus@bitgrid.net>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
+ <!-- eslint-disable-next-line vue/no-v-html This is rendered markdown so should be "safe" -->
<div class="settings-markdown" v-html="renderMarkdown" />
</template>
@@ -43,7 +27,7 @@ export default {
computed: {
renderMarkdown() {
const renderer = new marked.Renderer()
- renderer.link = function(href, title, text) {
+ renderer.link = function({ href, title, text }) {
let prot
try {
prot = decodeURIComponent(unescape(href))
@@ -64,18 +48,18 @@ export default {
out += '>' + text + '</a>'
return out
}
- renderer.heading = (text, level) => {
- level = Math.min(6, level + (this.minHeading - 1))
- return `<h${level}>${text}</h${level}>`
+ renderer.heading = ({ text, depth }) => {
+ depth = Math.min(6, depth + (this.minHeading - 1))
+ return `<h${depth}>${text}</h${depth}>`
}
- renderer.image = function(href, title, text) {
+ renderer.image = ({ title, text }) => {
if (text) {
return text
}
return title
}
- renderer.blockquote = function(quote) {
- return quote
+ renderer.blockquote = ({ text }) => {
+ return `<blockquote>${text}</blockquote>`
}
return dompurify.sanitize(
marked(this.text.trim(), {
@@ -116,45 +100,13 @@ export default {
</script>
<style scoped lang="scss">
- .settings-markdown::v-deep {
-
- h1,
- h2,
- h3,
- h4,
- h5,
- h6 {
- font-weight: 600;
- line-height: 120%;
- margin-top: 24px;
- margin-bottom: 12px;
- color: var(--color-main-text);
- }
-
- h1 {
- font-size: 36px;
- margin-top: 48px;
- }
-
- h2 {
- font-size: 28px;
- margin-top: 48px;
- }
-
- h3 {
- font-size: 24px;
- }
-
- h4 {
- font-size: 21px;
- }
-
- h5 {
- font-size: 17px;
- }
-
- h6 {
- font-size: var(--default-font-size);
+.settings-markdown :deep {
+ a {
+ text-decoration: underline;
+ &::after {
+ content: '↗';
+ padding-inline: calc(var(--default-grid-baseline) / 2);
+ }
}
pre {
@@ -177,8 +129,8 @@ export default {
}
ul, ol {
- padding-left: 10px;
- margin-left: 10px;
+ padding-inline-start: 10px;
+ margin-inline-start: 10px;
}
ul li {
@@ -194,12 +146,10 @@ export default {
}
blockquote {
- padding-left: 1em;
- border-left: 4px solid var(--color-primary-element);
+ padding-inline-start: 1em;
+ border-inline-start: 4px solid var(--color-primary-element);
color: var(--color-text-maxcontrast);
- margin-left: 0;
- margin-right: 0;
- }
-
+ margin-inline: 0;
}
+}
</style>
diff --git a/apps/settings/src/components/PasswordSection.vue b/apps/settings/src/components/PasswordSection.vue
index 931a0f007fe..44845c51ff4 100644
--- a/apps/settings/src/components/PasswordSection.vue
+++ b/apps/settings/src/components/PasswordSection.vue
@@ -1,21 +1,7 @@
<!--
- - @copyright 2022 Carl Schwan <carl@carlschwan.eu>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -->
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<NcSettingsSection :name="t('settings', 'Password')">
<form id="passwordform" method="POST" @submit.prevent="changePassword">
@@ -46,9 +32,9 @@
</template>
<script>
-import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcPasswordField from '@nextcloud/vue/dist/Components/NcPasswordField.js'
+import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import { showSuccess, showError } from '@nextcloud/dialogs'
diff --git a/apps/settings/src/components/PersonalInfo/AvatarSection.vue b/apps/settings/src/components/PersonalInfo/AvatarSection.vue
index 1137d4e1767..a99f228668c 100644
--- a/apps/settings/src/components/PersonalInfo/AvatarSection.vue
+++ b/apps/settings/src/components/PersonalInfo/AvatarSection.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2022 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -97,15 +80,15 @@ import { getCurrentUser } from '@nextcloud/auth'
import { getFilePickerBuilder, showError } from '@nextcloud/dialogs'
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
-import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
+import NcAvatar from '@nextcloud/vue/components/NcAvatar'
+import NcButton from '@nextcloud/vue/components/NcButton'
import VueCropper from 'vue-cropperjs'
// eslint-disable-next-line n/no-extraneous-import
import 'cropperjs/dist/cropper.css'
import Upload from 'vue-material-design-icons/Upload.vue'
import Folder from 'vue-material-design-icons/Folder.vue'
-import Delete from 'vue-material-design-icons/Delete.vue'
+import Delete from 'vue-material-design-icons/DeleteOutline.vue'
import HeaderBar from './shared/HeaderBar.vue'
import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js'
@@ -274,9 +257,10 @@ section {
grid-row: 1/3;
padding: 10px 10px;
}
+
.avatar {
&__container {
- margin: 0 auto;
+ margin: calc(var(--default-grid-baseline) * 2) auto 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
@@ -313,7 +297,7 @@ section {
justify-content: space-between;
}
- &::v-deep .cropper-view-box {
+ :deep(.cropper-view-box) {
border-radius: 50%;
}
}
diff --git a/apps/settings/src/components/PersonalInfo/BiographySection.vue b/apps/settings/src/components/PersonalInfo/BiographySection.vue
index 30c240dee1a..bbfb25e25cc 100644
--- a/apps/settings/src/components/PersonalInfo/BiographySection.vue
+++ b/apps/settings/src/components/PersonalInfo/BiographySection.vue
@@ -1,28 +1,11 @@
<!--
- - @copyright 2022 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<AccountPropertySection v-bind.sync="biography"
- :placeholder="t('settings', 'Your biography')"
+ :placeholder="t('settings', 'Your biography. Markdown is supported.')"
:multi-line="true" />
</template>
diff --git a/apps/settings/src/components/PersonalInfo/BirthdaySection.vue b/apps/settings/src/components/PersonalInfo/BirthdaySection.vue
new file mode 100644
index 00000000000..f55f09c95e5
--- /dev/null
+++ b/apps/settings/src/components/PersonalInfo/BirthdaySection.vue
@@ -0,0 +1,132 @@
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<template>
+ <section>
+ <HeaderBar :scope="birthdate.scope"
+ :input-id="inputId"
+ :readable="birthdate.readable" />
+
+ <NcDateTimePickerNative :id="inputId"
+ type="date"
+ label=""
+ :value="value"
+ @input="onInput" />
+
+ <p class="property__helper-text-message">
+ {{ t('settings', 'Enter your date of birth') }}
+ </p>
+ </section>
+</template>
+
+<script>
+import { loadState } from '@nextcloud/initial-state'
+import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js'
+import { savePrimaryAccountProperty } from '../../service/PersonalInfo/PersonalInfoService'
+import { handleError } from '../../utils/handlers'
+
+import debounce from 'debounce'
+
+import NcDateTimePickerNative from '@nextcloud/vue/components/NcDateTimePickerNative'
+import HeaderBar from './shared/HeaderBar.vue'
+
+const { birthdate } = loadState('settings', 'personalInfoParameters', {})
+
+export default {
+ name: 'BirthdaySection',
+
+ components: {
+ NcDateTimePickerNative,
+ HeaderBar,
+ },
+
+ data() {
+ let initialValue = null
+ if (birthdate.value) {
+ initialValue = new Date(birthdate.value)
+ }
+
+ return {
+ birthdate: {
+ ...birthdate,
+ readable: NAME_READABLE_ENUM[birthdate.name],
+ },
+ initialValue,
+ }
+ },
+
+ computed: {
+ inputId() {
+ return `account-property-${birthdate.name}`
+ },
+ value: {
+ get() {
+ return new Date(this.birthdate.value)
+ },
+ /** @param {Date} value The date to set */
+ set(value) {
+ const day = value.getDate().toString().padStart(2, '0')
+ const month = (value.getMonth() + 1).toString().padStart(2, '0')
+ const year = value.getFullYear()
+ this.birthdate.value = `${year}-${month}-${day}`
+ },
+ },
+ },
+
+ methods: {
+ onInput(e) {
+ this.value = e
+ this.debouncePropertyChange(this.value)
+ },
+
+ debouncePropertyChange: debounce(async function(value) {
+ await this.updateProperty(value)
+ }, 500),
+
+ async updateProperty(value) {
+ try {
+ const responseData = await savePrimaryAccountProperty(
+ this.birthdate.name,
+ value,
+ )
+ this.handleResponse({
+ value,
+ status: responseData.ocs?.meta?.status,
+ })
+ } catch (error) {
+ this.handleResponse({
+ errorMessage: t('settings', 'Unable to update date of birth'),
+ error,
+ })
+ }
+ },
+
+ handleResponse({ value, status, errorMessage, error }) {
+ if (status === 'ok') {
+ this.initialValue = value
+ } else {
+ this.$emit('update:value', this.initialValue)
+ handleError(error, errorMessage)
+ }
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+section {
+ padding: 10px 10px;
+
+ :deep(button:disabled) {
+ cursor: default;
+ }
+
+ .property__helper-text-message {
+ color: var(--color-text-maxcontrast);
+ padding: 4px 0;
+ display: flex;
+ align-items: center;
+ }
+}
+</style>
diff --git a/apps/settings/src/components/PersonalInfo/BlueskySection.vue b/apps/settings/src/components/PersonalInfo/BlueskySection.vue
new file mode 100644
index 00000000000..65223d1ab53
--- /dev/null
+++ b/apps/settings/src/components/PersonalInfo/BlueskySection.vue
@@ -0,0 +1,64 @@
+<!--
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <AccountPropertySection v-bind.sync="value"
+ :readable="readable"
+ :on-validate="onValidate"
+ :placeholder="t('settings', 'Bluesky handle')" />
+</template>
+
+<script setup lang="ts">
+import type { AccountProperties } from '../../constants/AccountPropertyConstants.js'
+
+import { loadState } from '@nextcloud/initial-state'
+import { t } from '@nextcloud/l10n'
+import { ref } from 'vue'
+import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.ts'
+import AccountPropertySection from './shared/AccountPropertySection.vue'
+
+const { bluesky } = loadState<AccountProperties>('settings', 'personalInfoParameters')
+
+const value = ref({ ...bluesky })
+const readable = NAME_READABLE_ENUM[bluesky.name]
+
+/**
+ * Validate that the text might be a bluesky handle
+ * @param text The potential bluesky handle
+ */
+function onValidate(text: string): boolean {
+ if (text === '') return true
+
+ const lowerText = text.toLowerCase()
+
+ if (lowerText === 'bsky.social') {
+ // Standalone bsky.social is invalid
+ return false
+ }
+
+ if (lowerText.endsWith('.bsky.social')) {
+ // Enforce format: exactly one label + '.bsky.social'
+ const parts = lowerText.split('.')
+
+ // Must be in form: [username, 'bsky', 'social']
+ if (parts.length !== 3 || parts[1] !== 'bsky' || parts[2] !== 'social') {
+ return false
+ }
+
+ const username = parts[0]
+ const validateRegex = /^[a-z0-9][a-z0-9-]{2,17}$/
+ return validateRegex.test(username)
+ }
+
+ // Else, treat as a custom domain
+ try {
+ const url = new URL(`https://${text}`)
+ // Ensure the parsed host matches exactly (case-insensitive already)
+ return url.host === lowerText
+ } catch {
+ return false
+ }
+}
+</script>
diff --git a/apps/settings/src/components/PersonalInfo/DetailsSection.vue b/apps/settings/src/components/PersonalInfo/DetailsSection.vue
index 2774f2c1c8e..d4bb0ce16ec 100644
--- a/apps/settings/src/components/PersonalInfo/DetailsSection.vue
+++ b/apps/settings/src/components/PersonalInfo/DetailsSection.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2022 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -37,6 +20,7 @@
<div class="details__quota">
<CircleSlice :size="20" />
<div class="details__quota-info">
+ <!-- eslint-disable-next-line vue/no-v-html -->
<p class="details__quota-text" v-html="quotaText" />
<NcProgressBar size="medium"
:value="usageRelative"
@@ -49,9 +33,10 @@
<script>
import { loadState } from '@nextcloud/initial-state'
-import NcProgressBar from '@nextcloud/vue/dist/Components/NcProgressBar.js'
+import { t } from '@nextcloud/l10n'
-import Account from 'vue-material-design-icons/Account.vue'
+import NcProgressBar from '@nextcloud/vue/components/NcProgressBar'
+import Account from 'vue-material-design-icons/AccountOutline.vue'
import CircleSlice from 'vue-material-design-icons/CircleSlice3.vue'
import HeaderBar from './shared/HeaderBar.vue'
@@ -81,12 +66,14 @@ export default {
computed: {
quotaText() {
if (quota === SPACE_UNLIMITED) {
- return t('settings', 'You are using <strong>{usage}</strong>', { usage })
+ return t('settings', 'You are using {s}{usage}{/s}', { usage, s: '<strong>', '/s': '</strong>' }, undefined, { escape: false })
}
return t(
'settings',
- 'You are using <strong>{usage}</strong> of <strong>{totalSpace}</strong> (<strong>{usageRelative}%</strong>)',
- { usage, totalSpace, usageRelative },
+ 'You are using {s}{usage}{/s} of {s}{totalSpace}{/s} ({s}{usageRelative}%{/s})',
+ { usage, totalSpace, usageRelative, s: '<strong>', '/s': '</strong>' },
+ undefined,
+ { escape: false },
)
},
},
@@ -97,7 +84,8 @@ export default {
.details {
display: flex;
flex-direction: column;
- margin: 10px 32px 10px 0;
+ margin-block: 10px;
+ margin-inline: 0 32px;
gap: 16px 0;
color: var(--color-text-maxcontrast);
diff --git a/apps/settings/src/components/PersonalInfo/DisplayNameSection.vue b/apps/settings/src/components/PersonalInfo/DisplayNameSection.vue
index 00a629c4d54..431dfbecc9a 100644
--- a/apps/settings/src/components/PersonalInfo/DisplayNameSection.vue
+++ b/apps/settings/src/components/PersonalInfo/DisplayNameSection.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2022 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
diff --git a/apps/settings/src/components/PersonalInfo/EmailSection/Email.vue b/apps/settings/src/components/PersonalInfo/EmailSection/Email.vue
index e1e0bc625ba..6a6baef8817 100644
--- a/apps/settings/src/components/PersonalInfo/EmailSection/Email.vue
+++ b/apps/settings/src/components/PersonalInfo/EmailSection/Email.vue
@@ -1,24 +1,6 @@
<!--
- - @copyright 2021, Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- - @author Grigorii K. Shartsev <me@shgk.me>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -66,7 +48,7 @@
:disabled="deleteDisabled"
@click="deleteEmail">
<template #icon>
- <NcIconSvgWrapper :path="mdiTrashCan" />
+ <NcIconSvgWrapper :path="mdiTrashCanOutline" />
</template>
{{ deleteEmailLabel }}
</NcActionButton>
@@ -82,17 +64,17 @@
</template>
<script>
-import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
-import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
-import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
+import NcActions from '@nextcloud/vue/components/NcActions'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
import debounce from 'debounce'
-import { mdiArrowLeft, mdiLock, mdiStar, mdiStarOutline, mdiTrashCan } from '@mdi/js'
+import { mdiArrowLeft, mdiLockOutline, mdiStar, mdiStarOutline, mdiTrashCanOutline } from '@mdi/js'
import FederationControl from '../shared/FederationControl.vue'
-import { handleError } from '../../../utils/handlers.js'
+import { handleError } from '../../../utils/handlers.ts'
import { ACCOUNT_PROPERTY_READABLE_ENUM, VERIFICATION_ENUM } from '../../../constants/AccountPropertyConstants.js'
import {
@@ -151,10 +133,10 @@ export default {
setup() {
return {
mdiArrowLeft,
- mdiLock,
+ mdiLockOutline,
mdiStar,
mdiStarOutline,
- mdiTrashCan,
+ mdiTrashCanOutline,
saveAdditionalEmailScope,
}
},
@@ -280,7 +262,7 @@ export default {
}
}
}
- }, 500),
+ }, 1000),
async deleteEmail() {
if (this.primary) {
@@ -374,6 +356,9 @@ export default {
handleDeleteAdditionalEmail(status) {
if (status === 'ok') {
this.$emit('delete-additional-email')
+ if (this.isNotificationEmail) {
+ this.$emit('update:notification-email', '')
+ }
} else {
this.handleResponse({
errorMessage: t('settings', 'Unable to delete additional email address'),
diff --git a/apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue b/apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue
index 55b68fa5933..f9674a3163b 100644
--- a/apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue
+++ b/apps/settings/src/components/PersonalInfo/EmailSection/EmailSection.vue
@@ -1,24 +1,6 @@
<!--
- - @copyright 2021, Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- - @author Grigorii K. Shartsev <me@shgk.me>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -31,7 +13,7 @@
:scope.sync="primaryEmail.scope"
@add-additional="onAddAdditionalEmail" />
- <template v-if="displayNameChangeSupported">
+ <template v-if="emailChangeSupported">
<Email :input-id="inputId"
:primary="true"
:scope.sync="primaryEmail.scope"
@@ -71,10 +53,10 @@ import HeaderBar from '../shared/HeaderBar.vue'
import { ACCOUNT_PROPERTY_READABLE_ENUM, DEFAULT_ADDITIONAL_EMAIL_SCOPE, NAME_READABLE_ENUM } from '../../../constants/AccountPropertyConstants.js'
import { savePrimaryEmail, removeAdditionalEmail } from '../../../service/PersonalInfo/EmailService.js'
import { validateEmail } from '../../../utils/validate.js'
-import { handleError } from '../../../utils/handlers.js'
+import { handleError } from '../../../utils/handlers.ts'
const { emailMap: { additionalEmails, primaryEmail, notificationEmail } } = loadState('settings', 'personalInfoParameters', {})
-const { displayNameChangeSupported } = loadState('settings', 'accountParameters', {})
+const { emailChangeSupported } = loadState('settings', 'accountParameters', {})
export default {
name: 'EmailSection',
@@ -88,7 +70,7 @@ export default {
return {
accountProperty: ACCOUNT_PROPERTY_READABLE_ENUM.EMAIL,
additionalEmails: additionalEmails.map(properties => ({ ...properties, key: this.generateUniqueKey() })),
- displayNameChangeSupported,
+ emailChangeSupported,
primaryEmail: { ...primaryEmail, readable: NAME_READABLE_ENUM[primaryEmail.name] },
notificationEmail,
}
diff --git a/apps/settings/src/components/PersonalInfo/FediverseSection.vue b/apps/settings/src/components/PersonalInfo/FediverseSection.vue
index 3975308d587..043fa6e64b9 100644
--- a/apps/settings/src/components/PersonalInfo/FediverseSection.vue
+++ b/apps/settings/src/components/PersonalInfo/FediverseSection.vue
@@ -1,50 +1,50 @@
<!--
- - @copyright 2022 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
- <AccountPropertySection v-bind.sync="fediverse"
+ <AccountPropertySection v-bind.sync="value"
+ :readable="readable"
+ :on-validate="onValidate"
:placeholder="t('settings', 'Your handle')" />
</template>
-<script>
+<script setup lang="ts">
+import type { AccountProperties } from '../../constants/AccountPropertyConstants.js'
import { loadState } from '@nextcloud/initial-state'
-
-import AccountPropertySection from './shared/AccountPropertySection.vue'
-
+import { t } from '@nextcloud/l10n'
+import { ref } from 'vue'
import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js'
-const { fediverse } = loadState('settings', 'personalInfoParameters', {})
-
-export default {
- name: 'FediverseSection',
-
- components: {
- AccountPropertySection,
- },
+import AccountPropertySection from './shared/AccountPropertySection.vue'
- data() {
- return {
- fediverse: { ...fediverse, readable: NAME_READABLE_ENUM[fediverse.name] },
- }
- },
+const { fediverse } = loadState<AccountProperties>('settings', 'personalInfoParameters')
+
+const value = ref({ ...fediverse })
+const readable = NAME_READABLE_ENUM[fediverse.name]
+
+/**
+ * Validate a fediverse handle
+ * @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 {
+ return false
+ }
}
</script>
diff --git a/apps/settings/src/components/PersonalInfo/FirstDayOfWeekSection.vue b/apps/settings/src/components/PersonalInfo/FirstDayOfWeekSection.vue
new file mode 100644
index 00000000000..98501db7ccc
--- /dev/null
+++ b/apps/settings/src/components/PersonalInfo/FirstDayOfWeekSection.vue
@@ -0,0 +1,126 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+ -->
+
+<template>
+ <section class="fdow-section">
+ <HeaderBar :input-id="inputId"
+ :readable="propertyReadable" />
+
+ <NcSelect :aria-label-listbox="t('settings', 'Day to use as the first day of week')"
+ class="fdow-section__day-select"
+ :clearable="false"
+ :input-id="inputId"
+ label="label"
+ label-outside
+ :options="dayOptions"
+ :value="valueOption"
+ @option:selected="updateFirstDayOfWeek" />
+ </section>
+</template>
+
+<script lang="ts">
+import HeaderBar from './shared/HeaderBar.vue'
+import NcSelect from '@nextcloud/vue/components/NcSelect'
+import {
+ ACCOUNT_SETTING_PROPERTY_ENUM,
+ ACCOUNT_SETTING_PROPERTY_READABLE_ENUM,
+} from '../../constants/AccountPropertyConstants'
+import { getDayNames, getFirstDay } from '@nextcloud/l10n'
+import { savePrimaryAccountProperty } from '../../service/PersonalInfo/PersonalInfoService'
+import { handleError } from '../../utils/handlers.ts'
+import { loadState } from '@nextcloud/initial-state'
+
+interface DayOption {
+ value: number,
+ label: string,
+}
+
+const { firstDayOfWeek } = loadState<{firstDayOfWeek?: string}>(
+ 'settings',
+ 'personalInfoParameters',
+ {},
+)
+
+export default {
+ name: 'FirstDayOfWeekSection',
+ components: {
+ HeaderBar,
+ NcSelect,
+ },
+ data() {
+ let firstDay = -1
+ if (firstDayOfWeek) {
+ firstDay = parseInt(firstDayOfWeek)
+ }
+
+ return {
+ firstDay,
+ }
+ },
+ computed: {
+ inputId(): string {
+ return 'account-property-fdow'
+ },
+ propertyReadable(): string {
+ return ACCOUNT_SETTING_PROPERTY_READABLE_ENUM.FIRST_DAY_OF_WEEK
+ },
+ dayOptions(): DayOption[] {
+ const options = [{
+ value: -1,
+ label: t('settings', 'Derived from your locale ({weekDayName})', {
+ weekDayName: getDayNames()[getFirstDay()],
+ }),
+ }]
+ for (const [index, dayName] of getDayNames().entries()) {
+ options.push({ value: index, label: dayName })
+ }
+ return options
+ },
+ valueOption(): DayOption | undefined {
+ return this.dayOptions.find((option) => option.value === this.firstDay)
+ },
+ },
+ methods: {
+ async updateFirstDayOfWeek(option: DayOption): Promise<void> {
+ try {
+ const responseData = await savePrimaryAccountProperty(
+ ACCOUNT_SETTING_PROPERTY_ENUM.FIRST_DAY_OF_WEEK,
+ option.value.toString(),
+ )
+ this.handleResponse({
+ value: option.value,
+ status: responseData.ocs?.meta?.status,
+ })
+ window.location.reload()
+ } catch (e) {
+ this.handleResponse({
+ errorMessage: t('settings', 'Unable to update first day of week'),
+ error: e,
+ })
+ }
+ },
+
+ handleResponse({ value, status, errorMessage, error }): void {
+ if (status === 'ok') {
+ this.firstDay = value
+ } else {
+ this.$emit('update:value', this.firstDay)
+ handleError(error, errorMessage)
+ }
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.fdow-section {
+ padding: 10px;
+
+ &__day-select {
+ width: 100%;
+ margin-top: 6px; // align with other inputs
+ }
+}
+</style>
diff --git a/apps/settings/src/components/PersonalInfo/HeadlineSection.vue b/apps/settings/src/components/PersonalInfo/HeadlineSection.vue
index 0275c1df80b..25fbde5b2f5 100644
--- a/apps/settings/src/components/PersonalInfo/HeadlineSection.vue
+++ b/apps/settings/src/components/PersonalInfo/HeadlineSection.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2022 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
diff --git a/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue b/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue
index 83611574ee5..8f42b2771c0 100644
--- a/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue
+++ b/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2021, Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -44,9 +27,9 @@
import { ACCOUNT_SETTING_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants.js'
import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/PersonalInfoService.js'
import { validateLanguage } from '../../../utils/validate.js'
-import { handleError } from '../../../utils/handlers.js'
+import { handleError } from '../../../utils/handlers.ts'
-import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
+import NcSelect from '@nextcloud/vue/components/NcSelect'
export default {
name: 'Language',
diff --git a/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue b/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue
index 23fc7850546..4e92436fd63 100644
--- a/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue
+++ b/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2021, Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
diff --git a/apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue b/apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue
index 185b06785d8..73300756472 100644
--- a/apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue
+++ b/apps/settings/src/components/PersonalInfo/LocaleSection/Locale.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2022 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -49,12 +32,12 @@
<script>
import moment from '@nextcloud/moment'
-import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
+import NcSelect from '@nextcloud/vue/components/NcSelect'
import MapClock from 'vue-material-design-icons/MapClock.vue'
import { ACCOUNT_SETTING_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants.js'
import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/PersonalInfoService.js'
-import { handleError } from '../../../utils/handlers.js'
+import { handleError } from '../../../utils/handlers.ts'
export default {
name: 'Locale',
diff --git a/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue b/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue
index 614a3e4bcf1..d4488e77efd 100644
--- a/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue
+++ b/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2022 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
diff --git a/apps/settings/src/components/PersonalInfo/LocationSection.vue b/apps/settings/src/components/PersonalInfo/LocationSection.vue
index 57811ddf3b0..a32f86b3442 100644
--- a/apps/settings/src/components/PersonalInfo/LocationSection.vue
+++ b/apps/settings/src/components/PersonalInfo/LocationSection.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2022 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
diff --git a/apps/settings/src/components/PersonalInfo/OrganisationSection.vue b/apps/settings/src/components/PersonalInfo/OrganisationSection.vue
index b8ae3d846e5..b951b938919 100644
--- a/apps/settings/src/components/PersonalInfo/OrganisationSection.vue
+++ b/apps/settings/src/components/PersonalInfo/OrganisationSection.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2022 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
diff --git a/apps/settings/src/components/PersonalInfo/PhoneSection.vue b/apps/settings/src/components/PersonalInfo/PhoneSection.vue
index 3a156bd9403..8ddeada960e 100644
--- a/apps/settings/src/components/PersonalInfo/PhoneSection.vue
+++ b/apps/settings/src/components/PersonalInfo/PhoneSection.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2022 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -56,6 +39,10 @@ export default {
methods: {
onValidate(value) {
+ if (value === '') {
+ return true
+ }
+
if (defaultPhoneRegion) {
return isValidPhoneNumber(value, defaultPhoneRegion)
}
diff --git a/apps/settings/src/components/PersonalInfo/ProfileSection/EditProfileAnchorLink.vue b/apps/settings/src/components/PersonalInfo/ProfileSection/EditProfileAnchorLink.vue
index a47936ba7ae..3deb5340751 100644
--- a/apps/settings/src/components/PersonalInfo/ProfileSection/EditProfileAnchorLink.vue
+++ b/apps/settings/src/components/PersonalInfo/ProfileSection/EditProfileAnchorLink.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2021 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -83,7 +66,7 @@ a {
display: inline-block;
vertical-align: middle;
margin-top: 6px;
- margin-right: 8px;
+ margin-inline-end: 8px;
}
&:hover,
diff --git a/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileCheckbox.vue b/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileCheckbox.vue
index 821cb981bb8..6eb7cf8c34c 100644
--- a/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileCheckbox.vue
+++ b/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileCheckbox.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2021, Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -36,8 +19,8 @@ import { emit } from '@nextcloud/event-bus'
import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/PersonalInfoService.js'
import { ACCOUNT_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants.js'
-import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
-import { handleError } from '../../../utils/handlers.js'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import { handleError } from '../../../utils/handlers.ts'
export default {
name: 'ProfileCheckbox',
diff --git a/apps/settings/src/components/PersonalInfo/ProfileSection/ProfilePreviewCard.vue b/apps/settings/src/components/PersonalInfo/ProfileSection/ProfilePreviewCard.vue
index 5ef6a00b1f2..47894f64f34 100644
--- a/apps/settings/src/components/PersonalInfo/ProfileSection/ProfilePreviewCard.vue
+++ b/apps/settings/src/components/PersonalInfo/ProfileSection/ProfilePreviewCard.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2021, Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -44,7 +27,7 @@
import { getCurrentUser } from '@nextcloud/auth'
import { generateUrl } from '@nextcloud/router'
-import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
+import NcAvatar from '@nextcloud/vue/components/NcAvatar'
export default {
name: 'ProfilePreviewCard',
@@ -121,7 +104,7 @@ export default {
box-shadow: 0 0 3px var(--color-box-shadow);
& *,
- &::v-deep * {
+ &:deep(*) {
cursor: default;
}
}
@@ -130,7 +113,7 @@ export default {
// Override Avatar component position to fix positioning on rerender
position: absolute !important;
top: 40px;
- left: 18px;
+ inset-inline-start: 18px;
z-index: 1;
&:not(.avatardiv--unknown) {
@@ -145,7 +128,7 @@ export default {
span {
position: absolute;
- left: 78px;
+ inset-inline-start: 78px;
overflow: hidden;
text-overflow: ellipsis;
overflow-wrap: anywhere;
@@ -168,7 +151,8 @@ export default {
color: var(--color-primary-element-text);
font-size: 18px;
font-weight: bold;
- margin: 0 4px 8px 0;
+ margin-block: 0 8px;
+ margin-inline: 0 4px;
}
}
@@ -180,7 +164,8 @@ export default {
color: var(--color-text-maxcontrast);
font-size: 14px;
font-weight: normal;
- margin: 4px 4px 0 0;
+ margin-block: 4px 0;
+ margin-inline: 0 4px;
line-height: 1.3;
}
}
diff --git a/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileSection.vue b/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileSection.vue
index 6b775092c6c..22c03f72697 100644
--- a/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileSection.vue
+++ b/apps/settings/src/components/PersonalInfo/ProfileSection/ProfileSection.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2021, Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -99,7 +82,7 @@ export default {
section {
padding: 10px 10px;
- &::v-deep button:disabled {
+ &:deep(button:disabled) {
cursor: default;
}
}
diff --git a/apps/settings/src/components/PersonalInfo/ProfileVisibilitySection/ProfileVisibilitySection.vue b/apps/settings/src/components/PersonalInfo/ProfileVisibilitySection/ProfileVisibilitySection.vue
index af45359d5a5..8acec883842 100644
--- a/apps/settings/src/components/PersonalInfo/ProfileVisibilitySection/ProfileVisibilitySection.vue
+++ b/apps/settings/src/components/PersonalInfo/ProfileVisibilitySection/ProfileVisibilitySection.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2021 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -135,7 +118,7 @@ section {
pointer-events: none;
& *,
- &::v-deep * {
+ &:deep(*) {
cursor: default;
pointer-events: none;
}
diff --git a/apps/settings/src/components/PersonalInfo/ProfileVisibilitySection/VisibilityDropdown.vue b/apps/settings/src/components/PersonalInfo/ProfileVisibilitySection/VisibilityDropdown.vue
index 0c01368c149..aaa13e63e92 100644
--- a/apps/settings/src/components/PersonalInfo/ProfileVisibilitySection/VisibilityDropdown.vue
+++ b/apps/settings/src/components/PersonalInfo/ProfileVisibilitySection/VisibilityDropdown.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2021 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -40,11 +23,11 @@
import { loadState } from '@nextcloud/initial-state'
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
-import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
+import NcSelect from '@nextcloud/vue/components/NcSelect'
import { saveProfileParameterVisibility } from '../../../service/ProfileService.js'
import { VISIBILITY_PROPERTY_ENUM } from '../../../constants/ProfileConstants.js'
-import { handleError } from '../../../utils/handlers.js'
+import { handleError } from '../../../utils/handlers.ts'
const { profileEnabled } = loadState('settings', 'personalInfoParameters', false)
@@ -159,7 +142,7 @@ export default {
pointer-events: none;
& *,
- &::v-deep * {
+ &:deep(*) {
cursor: default;
pointer-events: none;
}
diff --git a/apps/settings/src/components/PersonalInfo/PronounsSection.vue b/apps/settings/src/components/PersonalInfo/PronounsSection.vue
new file mode 100644
index 00000000000..e345cb8e225
--- /dev/null
+++ b/apps/settings/src/components/PersonalInfo/PronounsSection.vue
@@ -0,0 +1,47 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <AccountPropertySection v-bind.sync="pronouns"
+ :placeholder="randomPronounsPlaceholder" />
+</template>
+
+<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'
+
+const { pronouns } = loadState<{ pronouns: IAccountProperty }>('settings', 'personalInfoParameters')
+
+export default defineComponent({
+ name: 'PronounsSection',
+
+ components: {
+ AccountPropertySection,
+ },
+
+ data() {
+ return {
+ pronouns: { ...pronouns, readable: NAME_READABLE_ENUM[pronouns.name] },
+ }
+ },
+
+ computed: {
+ randomPronounsPlaceholder() {
+ const pronouns = [
+ t('settings', 'she/her'),
+ t('settings', 'he/him'),
+ t('settings', 'they/them'),
+ ]
+ const pronounsExample = pronouns[Math.floor(Math.random() * pronouns.length)]
+ return t('settings', 'Your pronouns. E.g. {pronounsExample}', { pronounsExample })
+ },
+ },
+})
+</script>
diff --git a/apps/settings/src/components/PersonalInfo/RoleSection.vue b/apps/settings/src/components/PersonalInfo/RoleSection.vue
index 82cb034600d..3581112fe1b 100644
--- a/apps/settings/src/components/PersonalInfo/RoleSection.vue
+++ b/apps/settings/src/components/PersonalInfo/RoleSection.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2022 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
diff --git a/apps/settings/src/components/PersonalInfo/TwitterSection.vue b/apps/settings/src/components/PersonalInfo/TwitterSection.vue
index dda773a0179..43d08f81e3f 100644
--- a/apps/settings/src/components/PersonalInfo/TwitterSection.vue
+++ b/apps/settings/src/components/PersonalInfo/TwitterSection.vue
@@ -1,50 +1,34 @@
<!--
- - @copyright 2022 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
- <AccountPropertySection v-bind.sync="twitter"
+ <AccountPropertySection v-bind.sync="value"
+ :readable="readable"
+ :on-validate="onValidate"
:placeholder="t('settings', 'Your X (formerly Twitter) handle')" />
</template>
-<script>
-import { loadState } from '@nextcloud/initial-state'
+<script setup lang="ts">
+import type { AccountProperties } from '../../constants/AccountPropertyConstants.js'
+import { loadState } from '@nextcloud/initial-state'
+import { t } from '@nextcloud/l10n'
+import { ref } from 'vue'
+import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.ts'
import AccountPropertySection from './shared/AccountPropertySection.vue'
-import { NAME_READABLE_ENUM } from '../../constants/AccountPropertyConstants.js'
-
-const { twitter } = loadState('settings', 'personalInfoParameters', {})
-
-export default {
- name: 'TwitterSection',
+const { twitter } = loadState<AccountProperties>('settings', 'personalInfoParameters')
- components: {
- AccountPropertySection,
- },
+const value = ref({ ...twitter })
+const readable = NAME_READABLE_ENUM[twitter.name]
- data() {
- return {
- twitter: { ...twitter, readable: NAME_READABLE_ENUM[twitter.name] },
- }
- },
+/**
+ * Validate that the text might be a twitter handle
+ * @param text The potential twitter handle
+ */
+function onValidate(text: string): boolean {
+ return text === '' || text.match(/^@?([a-zA-Z0-9_]{2,15})$/) !== null
}
</script>
diff --git a/apps/settings/src/components/PersonalInfo/WebsiteSection.vue b/apps/settings/src/components/PersonalInfo/WebsiteSection.vue
index 79e7a90de00..762909139dd 100644
--- a/apps/settings/src/components/PersonalInfo/WebsiteSection.vue
+++ b/apps/settings/src/components/PersonalInfo/WebsiteSection.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2022 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
diff --git a/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue b/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue
index 15ec0191921..d039641ec72 100644
--- a/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue
+++ b/apps/settings/src/components/PersonalInfo/shared/AccountPropertySection.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2022 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -63,13 +46,13 @@
<script>
import debounce from 'debounce'
-import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js'
-import NcTextArea from '@nextcloud/vue/dist/Components/NcTextArea.js'
+import NcInputField from '@nextcloud/vue/components/NcInputField'
+import NcTextArea from '@nextcloud/vue/components/NcTextArea'
import HeaderBar from './HeaderBar.vue'
import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/PersonalInfoService.js'
-import { handleError } from '../../../utils/handlers.js'
+import { handleError } from '../../../utils/handlers.ts'
export default {
name: 'AccountPropertySection',
@@ -165,13 +148,14 @@ export default {
return
}
await this.updateProperty(value)
- }, 500)
+ }, 1000)
},
},
methods: {
async updateProperty(value) {
try {
+ this.hasError = false
const responseData = await savePrimaryAccountProperty(
this.name,
value,
@@ -197,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)
}
},
},
@@ -224,7 +206,7 @@ section {
display: flex;
gap: 0 2px;
- margin-right: 5px;
+ margin-inline-end: 5px;
margin-bottom: 5px;
}
}
@@ -235,7 +217,7 @@ section {
align-items: center;
&__icon {
- margin-right: 8px;
+ margin-inline-end: 8px;
align-self: start;
margin-top: 4px;
}
diff --git a/apps/settings/src/components/PersonalInfo/shared/FederationControl.vue b/apps/settings/src/components/PersonalInfo/shared/FederationControl.vue
index b8dd7f18737..e55a50056d3 100644
--- a/apps/settings/src/components/PersonalInfo/shared/FederationControl.vue
+++ b/apps/settings/src/components/PersonalInfo/shared/FederationControl.vue
@@ -1,25 +1,6 @@
<!--
- - @copyright 2021, Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- - @author Ferdinand Thiessen <opensource@fthiessen.de>
- - @author Grigorii K. Shartsev <me@shgk.me>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -49,9 +30,9 @@
</template>
<script>
-import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
-import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
-import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
+import NcActions from '@nextcloud/vue/components/NcActions'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import { loadState } from '@nextcloud/initial-state'
import {
@@ -65,7 +46,7 @@ import {
UNPUBLISHED_READABLE_PROPERTIES,
} from '../../../constants/AccountPropertyConstants.js'
import { savePrimaryAccountPropertyScope } from '../../../service/PersonalInfo/PersonalInfoService.js'
-import { handleError } from '../../../utils/handlers.js'
+import { handleError } from '../../../utils/handlers.ts'
const {
federationEnabled,
@@ -165,7 +146,7 @@ export default {
}
// TODO: provide focus method from NcActions
- this.$refs.federationActions.$refs.menuButton.$el.focus()
+ this.$refs.federationActions.$refs?.triggerButton?.$el?.focus?.()
},
async updatePrimaryScope(scope) {
diff --git a/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue b/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue
index 18ae30e1dd5..7c95c2b8f4c 100644
--- a/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue
+++ b/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue
@@ -1,28 +1,11 @@
<!--
- - @copyright 2021, Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="headerbar-label" :class="{ 'setting-property': isSettingProperty, 'profile-property': isProfileProperty }">
- <h3 v-if="isHeading">
+ <h3 v-if="isHeading" class="headerbar__heading">
<!-- Already translated as required by prop validator -->
{{ readable }}
</h3>
@@ -53,7 +36,7 @@
</template>
<script>
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
+import NcButton from '@nextcloud/vue/components/NcButton'
import Plus from 'vue-material-design-icons/Plus.vue'
import FederationControl from './FederationControl.vue'
@@ -147,7 +130,7 @@ export default {
}
&.setting-property {
- height: 44px;
+ height: 34px;
}
label {
@@ -155,11 +138,16 @@ export default {
}
}
+ .headerbar__heading {
+ margin: 0;
+ }
+
.federation-control {
margin: 0;
}
.button-vue {
- margin: 0 0 0 auto !important;
+ margin: 0 !important;
+ margin-inline-start: auto !important;
}
</style>
diff --git a/apps/settings/src/components/SelectSharingPermissions.vue b/apps/settings/src/components/SelectSharingPermissions.vue
index 278b7b623df..ef24bcda026 100644
--- a/apps/settings/src/components/SelectSharingPermissions.vue
+++ b/apps/settings/src/components/SelectSharingPermissions.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @author Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<fieldset class="permissions-select">
@@ -38,8 +21,8 @@
<script lang="ts">
import { translate } from '@nextcloud/l10n'
-import { NcCheckboxRadioSwitch } from '@nextcloud/vue'
import { defineComponent } from 'vue'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
export default defineComponent({
name: 'SelectSharingPermissions',
diff --git a/apps/settings/src/components/SvgFilterMixin.vue b/apps/settings/src/components/SvgFilterMixin.vue
index 15713514436..004ab7b1857 100644
--- a/apps/settings/src/components/SvgFilterMixin.vue
+++ b/apps/settings/src/components/SvgFilterMixin.vue
@@ -1,24 +1,7 @@
<!--
- - @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
- -
- - @author Julius Härtl <jus@bitgrid.net>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<script>
export default {
diff --git a/apps/settings/src/components/UserList.vue b/apps/settings/src/components/UserList.vue
index 3ddb617dda5..459548fad26 100644
--- a/apps/settings/src/components/UserList.vue
+++ b/apps/settings/src/components/UserList.vue
@@ -1,33 +1,16 @@
<!--
- - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @author John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<Fragment>
- <NewUserModal v-if="showConfig.showNewUserForm"
+ <NewUserDialog v-if="showConfig.showNewUserForm"
:loading="loading"
:new-user="newUser"
:quota-options="quotaOptions"
@reset="resetForm"
- @close="closeModal" />
+ @closing="closeDialog" />
<NcEmptyContent v-if="filteredUsers.length === 0"
class="empty"
@@ -36,7 +19,7 @@
<NcLoadingIcon v-if="isInitialLoad && loading.users"
:name="t('settings', 'Loading accounts …')"
:size="64" />
- <NcIconSvgWrapper v-else :path="mdiAccountGroup" :size="64" />
+ <NcIconSvgWrapper v-else :path="mdiAccountGroupOutline" :size="64" />
</template>
</NcEmptyContent>
@@ -51,8 +34,6 @@
users,
settings,
hasObfuscated,
- groups,
- subAdminsGroups,
quotaOptions,
languages,
externalActions,
@@ -77,18 +58,18 @@
</template>
<script>
-import { mdiAccountGroup } from '@mdi/js'
+import { mdiAccountGroupOutline } from '@mdi/js'
import { showError } from '@nextcloud/dialogs'
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
import { Fragment } from 'vue-frag'
import Vue from 'vue'
-import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
-import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
-import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
+import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import VirtualList from './Users/VirtualList.vue'
-import NewUserModal from './Users/NewUserModal.vue'
+import NewUserDialog from './Users/NewUserDialog.vue'
import UserListFooter from './Users/UserListFooter.vue'
import UserListHeader from './Users/UserListHeader.vue'
import UserRow from './Users/UserRow.vue'
@@ -119,7 +100,7 @@ export default {
NcEmptyContent,
NcIconSvgWrapper,
NcLoadingIcon,
- NewUserModal,
+ NewUserDialog,
UserListFooter,
UserListHeader,
VirtualList,
@@ -139,7 +120,7 @@ export default {
setup() {
// non reactive properties
return {
- mdiAccountGroup,
+ mdiAccountGroupOutline,
rowHeight: 55,
UserRow,
@@ -186,23 +167,12 @@ export default {
if (this.selectedGroup === 'disabled') {
return this.users.filter(user => user.enabled === false)
}
- if (!this.settings.isAdmin) {
- // we don't want subadmins to edit themselves
- return this.users.filter(user => user.enabled !== false)
- }
return this.users.filter(user => user.enabled !== false)
},
groups() {
- // data provided php side + remove the disabled group
- return this.$store.getters.getGroups
- .filter(group => group.id !== 'disabled')
- .sort((a, b) => a.name.localeCompare(b.name))
- },
-
- subAdminsGroups() {
- // data provided php side
- return this.$store.getters.getSubadminGroups
+ return this.$store.getters.getSortedGroups
+ .filter(group => group.id !== '__nc_internal_recent' && group.id !== 'disabled')
},
quotaOptions() {
@@ -313,6 +283,13 @@ export default {
await this.$store.dispatch('getDisabledUsers', {
offset: this.disabledUsersOffset,
limit: this.disabledUsersLimit,
+ search: this.searchQuery,
+ })
+ } else if (this.selectedGroup === '__nc_internal_recent') {
+ await this.$store.dispatch('getRecentUsers', {
+ offset: this.usersOffset,
+ limit: this.usersLimit,
+ search: this.searchQuery,
})
} else {
await this.$store.dispatch('getUsers', {
@@ -331,7 +308,7 @@ export default {
this.isInitialLoad = false
},
- closeModal() {
+ closeDialog() {
this.$store.commit('setShowConfig', {
key: 'showNewUserForm',
value: false,
@@ -371,7 +348,18 @@ export default {
},
setNewUserDefaultGroup(value) {
- if (value && value.length > 0) {
+ // 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 (groups.length === 1) {
+ this.newUser.groups = [...groups]
+ }
+ return
+ }
+
+ if (value) {
// setting new account default group to the current selected one
const currentGroup = this.groups.find(group => group.id === value)
if (currentGroup) {
@@ -403,7 +391,7 @@ export default {
</script>
<style lang="scss" scoped>
-@import './Users/shared/styles.scss';
+@use './Users/shared/styles' as *;
.empty {
:deep {
diff --git a/apps/settings/src/components/Users/NewUserModal.vue b/apps/settings/src/components/Users/NewUserDialog.vue
index 236bc6db7d8..ef401b565fa 100644
--- a/apps/settings/src/components/Users/NewUserModal.vue
+++ b/apps/settings/src/components/Users/NewUserDialog.vue
@@ -1,36 +1,21 @@
<!--
- - @copyright 2023 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
- <NcModal class="modal"
+ <NcDialog class="dialog"
size="small"
+ :name="t('settings', 'New account')"
+ out-transition
v-on="$listeners">
- <form class="modal__form"
+ <form id="new-user-form"
+ class="dialog__form"
data-test="form"
:disabled="loading.all"
@submit.prevent="createUser">
- <h2>{{ t('settings', 'New user') }}</h2>
<NcTextField ref="username"
- class="modal__item"
+ class="dialog__item"
data-test="username"
:value.sync="newUser.id"
:disabled="settings.newUserGenerateUserID"
@@ -40,7 +25,7 @@
spellcheck="false"
pattern="[a-zA-Z0-9 _\.@\-']+"
required />
- <NcTextField class="modal__item"
+ <NcTextField class="dialog__item"
data-test="displayName"
:value.sync="newUser.displayName"
:label="t('settings', 'Display name')"
@@ -49,11 +34,11 @@
spellcheck="false" />
<span v-if="!settings.newUserRequireEmail"
id="password-email-hint"
- class="modal__hint">
+ class="dialog__hint">
{{ t('settings', 'Either password or email is required') }}
</span>
<NcPasswordField ref="password"
- class="modal__item"
+ class="dialog__item"
data-test="password"
:value.sync="newUser.password"
:minlength="minPasswordLength"
@@ -64,7 +49,7 @@
autocomplete="new-password"
spellcheck="false"
:required="newUser.mailAddress === ''" />
- <NcTextField class="modal__item"
+ <NcTextField class="dialog__item"
data-test="email"
type="email"
:value.sync="newUser.mailAddress"
@@ -74,66 +59,54 @@
autocomplete="off"
spellcheck="false"
:required="newUser.password === '' || settings.newUserRequireEmail" />
- <div class="modal__item">
- <label class="modal__label"
- for="new-user-groups">
- {{ !settings.isAdmin ? t('settings', 'Groups (required)') : t('settings', 'Groups') }}
- </label>
- <NcSelect class="modal__select"
- input-id="new-user-groups"
- :placeholder="t('settings', 'Set user groups')"
+ <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"
- :options="canAddGroups"
+ :options="availableGroups"
:value="newUser.groups"
label="name"
:close-on-select="false"
:multiple="true"
- :taggable="true"
- :required="!settings.isAdmin"
- @input="handleGroupInput"
- @option:created="createGroup" />
- <!-- If user is not admin, he is a subadmin.
+ :taggable="settings.isAdmin || settings.isDelegatedAdmin"
+ :required="!settings.isAdmin && !settings.isDelegatedAdmin"
+ :create-option="(value) => ({ id: value, name: value, isCreating: true })"
+ @search="searchGroups"
+ @option:created="createGroup"
+ @option:selected="options => addGroup(options.at(-1))" />
+ <!-- If user is not admin, they are a subadmin.
Subadmins can't create users outside their groups
Therefore, empty select is forbidden -->
</div>
- <div v-if="subAdminsGroups.length > 0"
- class="modal__item">
- <label class="modal__label"
- for="new-user-sub-admin">
- {{ t('settings', 'Administered groups') }}
- </label>
+ <div class="dialog__item">
<NcSelect v-model="newUser.subAdminsGroups"
- class="modal__select"
- input-id="new-user-sub-admin"
- :placeholder="t('settings', 'Set user as admin for …')"
- :options="subAdminsGroups"
+ class="dialog__select"
+ :input-label="t('settings', 'Admin of the following groups')"
+ :placeholder="t('settings', 'Set account as admin for …')"
+ :disabled="loading.groups || loading.all"
+ :options="availableGroups"
:close-on-select="false"
:multiple="true"
- label="name" />
+ label="name"
+ @search="searchGroups" />
</div>
- <div class="modal__item">
- <label class="modal__label"
- for="new-user-quota">
- {{ t('settings', 'Quota') }}
- </label>
+ <div class="dialog__item">
<NcSelect v-model="newUser.quota"
- class="modal__select"
- input-id="new-user-quota"
- :placeholder="t('settings', 'Set user quota')"
+ class="dialog__select"
+ :input-label="t('settings', 'Quota')"
+ :placeholder="t('settings', 'Set account quota')"
:options="quotaOptions"
:clearable="false"
:taggable="true"
:create-option="validateQuota" />
</div>
<div v-if="showConfig.showLanguages"
- class="modal__item">
- <label class="modal__label"
- for="new-user-language">
- {{ t('settings', 'Language') }}
- </label>
- <NcSelect v-model="newUser.language"
- class="modal__select"
- input-id="new-user-language"
+ class="dialog__item">
+ <NcSelect v-model="newUser.language"
+ class="dialog__select"
+ :input-label="t('settings', 'Language')"
:placeholder="t('settings', 'Set default language')"
:clearable="false"
:selectable="option => !option.languages"
@@ -141,44 +114,47 @@
:options="languages"
label="name" />
</div>
- <div :class="['modal__item managers', { 'icon-loading-small': loading.manager }]">
- <label class="modal__label"
- for="new-user-manager">
- <!-- TRANSLATORS This string describes a manager in the context of an organization -->
- {{ t('settings', 'Manager') }}
- </label>
+ <div :class="['dialog__item dialog__managers', { 'icon-loading-small': loading.manager }]">
<NcSelect v-model="newUser.manager"
- class="modal__select"
- input-id="new-user-manager"
+ class="dialog__select"
+ :input-label="managerInputLabel"
:placeholder="managerLabel"
:options="possibleManagers"
:user-select="true"
label="displayname"
@search="searchUserManager" />
</div>
- <NcButton class="modal__submit"
+ </form>
+
+ <template #actions>
+ <NcButton class="dialog__submit"
data-test="submit"
+ form="new-user-form"
type="primary"
native-type="submit">
- {{ t('settings', 'Add new user') }}
+ {{ t('settings', 'Add new account') }}
</NcButton>
- </form>
- </NcModal>
+ </template>
+ </NcDialog>
</template>
<script>
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcModal from '@nextcloud/vue/dist/Components/NcModal.js'
-import NcPasswordField from '@nextcloud/vue/dist/Components/NcPasswordField.js'
-import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
+import { formatFileSize, parseFileSize } from '@nextcloud/files'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcDialog from '@nextcloud/vue/components/NcDialog'
+import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
+import NcSelect from '@nextcloud/vue/components/NcSelect'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
+
+import { searchGroups } from '../../service/groups.ts'
+import logger from '../../logger.ts'
export default {
- name: 'NewUserModal',
+ name: 'NewUserDialog',
components: {
NcButton,
- NcModal,
+ NcDialog,
NcPasswordField,
NcSelect,
NcTextField,
@@ -205,7 +181,11 @@ export default {
return {
possibleManagers: [],
// TRANSLATORS This string describes a manager in the context of an organization
- managerLabel: t('settings', 'Set user manager'),
+ managerInputLabel: t('settings', 'Manager'),
+ // TRANSLATORS This string describes a manager in the context of an organization
+ managerLabel: t('settings', 'Set line manager'),
+ // Cancelable promise for search groups request
+ promise: null,
}
},
@@ -220,36 +200,21 @@ export default {
usernameLabel() {
if (this.settings.newUserGenerateUserID) {
- return t('settings', 'Username will be autogenerated')
+ return t('settings', 'Account name will be autogenerated')
}
- return t('settings', 'Username (required)')
+ return t('settings', 'Account name (required)')
},
minPasswordLength() {
return this.$store.getters.getPasswordPolicyMinLength
},
- groups() {
- // data provided php side + remove the disabled group
- return this.$store.getters.getGroups
- .filter(group => group.id !== 'disabled')
- .sort((a, b) => a.name.localeCompare(b.name))
- },
+ availableGroups() {
+ const groups = (this.settings.isAdmin || this.settings.isDelegatedAdmin)
+ ? this.$store.getters.getSortedGroups
+ : this.$store.getters.getSubAdminGroups
- subAdminsGroups() {
- // data provided php side
- return this.$store.getters.getSubadminGroups
- },
-
- canAddGroups() {
- // disabled if no permission to add new users to group
- return this.groups.map(group => {
- // clone object because we don't want
- // to edit the original groups
- group = Object.assign({}, group)
- group.$isDisabled = group.canAdd === false
- return group
- })
+ return groups.filter(group => group.id !== '__nc_internal_recent' && group.id !== 'disabled')
},
languages() {
@@ -272,6 +237,10 @@ export default {
await this.searchUserManager()
},
+ mounted() {
+ this.$refs.username?.focus?.()
+ },
+
methods: {
async createUser() {
this.loading.all = true
@@ -289,30 +258,49 @@ export default {
})
this.$emit('reset')
- this.$refs.username?.$refs?.inputField?.$refs?.input?.focus?.()
- this.$emit('close')
+ this.$refs.username?.focus?.()
+ this.$emit('closing')
} catch (error) {
this.loading.all = false
if (error.response && error.response.data && error.response.data.ocs && error.response.data.ocs.meta) {
const statuscode = error.response.data.ocs.meta.statuscode
if (statuscode === 102) {
// wrong username
- this.$refs.username?.$refs?.inputField?.$refs?.input?.focus?.()
+ this.$refs.username?.focus?.()
} else if (statuscode === 107) {
// wrong password
- this.$refs.password?.$refs?.inputField?.$refs?.input?.focus?.()
+ this.$refs.password?.focus?.()
}
}
}
},
- handleGroupInput(groups) {
- /**
- * Filter out groups with no id to prevent duplicate selected options
- *
- * Created groups are added programmatically by `createGroup()`
- */
- this.newUser.groups = groups.filter(group => Boolean(group.id))
+ async searchGroups(query, toggleLoading) {
+ if (!this.settings.isAdmin && !this.settings.isDelegatedAdmin) {
+ // managers cannot search for groups
+ return
+ }
+
+ if (this.promise) {
+ this.promise.cancel()
+ }
+ toggleLoading(true)
+ try {
+ this.promise = searchGroups({
+ search: query,
+ offset: 0,
+ limit: 25,
+ })
+ const groups = await this.promise
+ // 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 })
+ }
+ this.promise = null
+ toggleLoading(false)
},
/**
@@ -325,11 +313,26 @@ export default {
this.loading.groups = true
try {
await this.$store.dispatch('addGroup', gid)
- this.newUser.groups.push(this.groups.find(group => group.id === gid))
- this.loading.groups = false
+ this.newUser.groups.push({ id: gid, name: gid })
} catch (error) {
- this.loading.groups = false
+ logger.error(t('settings', 'Failed to create group'), { error })
+ }
+ this.loading.groups = false
+ },
+
+ /**
+ * Add user to group
+ *
+ * @param {object} group Group object
+ */
+ async addGroup(group) {
+ if (group.isCreating) {
+ return
}
+ if (group.canAdd === false) {
+ return
+ }
+ this.newUser.groups.push(group)
},
/**
@@ -343,7 +346,7 @@ export default {
const validQuota = OC.Util.computerFileSize(quota)
if (validQuota !== null && validQuota >= 0) {
// unify format output
- quota = OC.Util.humanFileSize(OC.Util.computerFileSize(quota))
+ quota = formatFileSize(parseFileSize(quota, true))
this.newUser.quota = { id: quota, label: quota }
return this.newUser.quota
}
@@ -383,12 +386,12 @@ export default {
</script>
<style lang="scss" scoped>
-.modal {
+.dialog {
&__form {
display: flex;
flex-direction: column;
align-items: center;
- padding: 20px;
+ padding: 0 8px;
gap: 4px 0;
}
@@ -415,8 +418,19 @@ export default {
width: 100%;
}
+ &__managers {
+ margin-bottom: 12px;
+ }
+
&__submit {
- margin-top: 20px;
+ margin-top: 4px;
+ margin-bottom: 8px;
+ }
+
+ :deep {
+ .dialog__actions {
+ margin: auto;
+ }
}
}
</style>
diff --git a/apps/settings/src/components/Users/UserListFooter.vue b/apps/settings/src/components/Users/UserListFooter.vue
index d8974658354..bf9aa43b6d3 100644
--- a/apps/settings/src/components/Users/UserListFooter.vue
+++ b/apps/settings/src/components/Users/UserListFooter.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2023 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -28,7 +11,7 @@
</th>
<td class="footer__cell footer__cell--loading">
<NcLoadingIcon v-if="loading"
- :title="t('settings', 'Loading users …')"
+ :title="t('settings', 'Loading accounts …')"
:size="32" />
</td>
<td class="footer__cell footer__cell--count footer__cell--multiline">
@@ -43,7 +26,7 @@
<script lang="ts">
import Vue from 'vue'
-import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import {
translate as t,
@@ -73,8 +56,8 @@ export default Vue.extend({
if (this.loading) {
return this.n(
'settings',
- '{userCount} user …',
- '{userCount} users …',
+ '{userCount} account …',
+ '{userCount} accounts …',
this.filteredUsers.length,
{
userCount: this.filteredUsers.length,
@@ -83,8 +66,8 @@ export default Vue.extend({
}
return this.n(
'settings',
- '{userCount} user',
- '{userCount} users',
+ '{userCount} account',
+ '{userCount} accounts',
this.filteredUsers.length,
{
userCount: this.filteredUsers.length,
@@ -101,18 +84,18 @@ export default Vue.extend({
</script>
<style lang="scss" scoped>
-@import './shared/styles.scss';
+@use './shared/styles';
.footer {
- @include row;
- @include cell;
+ @include styles.row;
+ @include styles.cell;
&__cell {
position: sticky;
color: var(--color-text-maxcontrast);
&--loading {
- left: 0;
+ inset-inline-start: 0;
min-width: var(--avatar-cell-width);
width: var(--avatar-cell-width);
align-items: center;
@@ -120,7 +103,7 @@ export default Vue.extend({
}
&--count {
- left: var(--avatar-cell-width);
+ inset-inline-start: var(--avatar-cell-width);
min-width: var(--cell-width);
width: var(--cell-width);
}
diff --git a/apps/settings/src/components/Users/UserListHeader.vue b/apps/settings/src/components/Users/UserListHeader.vue
index e314bcb6a73..a85306d84d3 100644
--- a/apps/settings/src/components/Users/UserListHeader.vue
+++ b/apps/settings/src/components/Users/UserListHeader.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2023 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -35,8 +18,12 @@
<strong>
{{ t('settings', 'Display name') }}
</strong>
- <span class="header__subtitle">
- {{ t('settings', 'Username') }}
+ </th>
+ <th class="header__cell header__cell--username"
+ data-cy-user-list-header-username
+ scope="col">
+ <span>
+ {{ t('settings', 'Account name') }}
</span>
</th>
<th class="header__cell"
@@ -55,7 +42,7 @@
scope="col">
<span>{{ t('settings', 'Groups') }}</span>
</th>
- <th v-if="subAdminsGroups.length > 0 && settings.isAdmin"
+ <th v-if="settings.isAdmin || settings.isDelegatedAdmin"
class="header__cell header__cell--large"
data-cy-user-list-header-subadmins
scope="col">
@@ -77,13 +64,19 @@
data-cy-user-list-header-storage-location
scope="col">
<span v-if="showConfig.showUserBackend">
- {{ t('settings', 'User backend') }}
+ {{ t('settings', 'Account backend') }}
</span>
<span v-if="showConfig.showStoragePath"
class="header__subtitle">
{{ t('settings', 'Storage location') }}
</span>
</th>
+ <th v-if="showConfig.showFirstLogin"
+ class="header__cell"
+ data-cy-user-list-header-first-login
+ scope="col">
+ <span>{{ t('settings', 'First login') }}</span>
+ </th>
<th v-if="showConfig.showLastLogin"
class="header__cell"
data-cy-user-list-header-last-login
@@ -100,7 +93,7 @@
data-cy-user-list-header-actions
scope="col">
<span class="hidden-visually">
- {{ t('settings', 'User actions') }}
+ {{ t('settings', 'Account actions') }}
</span>
</th>
</tr>
@@ -132,11 +125,6 @@ export default Vue.extend({
return this.$store.getters.getServerData
},
- subAdminsGroups() {
- // @ts-expect-error: allow untyped $store
- return this.$store.getters.getSubadminGroups
- },
-
passwordLabel(): string {
if (this.hasObfuscated) {
// TRANSLATORS This string is for a column header labelling either a password or a message that the current user has insufficient permissions
@@ -153,12 +141,12 @@ export default Vue.extend({
</script>
<style lang="scss" scoped>
-@import './shared/styles.scss';
+@use './shared/styles';
.header {
- @include row;
- @include cell;
-
border-bottom: 1px solid var(--color-border);
+
+ @include styles.row;
+ @include styles.cell;
}
</style>
diff --git a/apps/settings/src/components/Users/UserRow.vue b/apps/settings/src/components/Users/UserRow.vue
index 3586a7702b1..43668725972 100644
--- a/apps/settings/src/components/Users/UserRow.vue
+++ b/apps/settings/src/components/Users/UserRow.vue
@@ -1,27 +1,7 @@
<!--
- - @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev>
- - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- - @author Gary Kim <gary@garykim.dev>
- - @author John Molakvoæ <skjnldsv@protonmail.com>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
- -->
+ - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<tr class="user-list__row"
@@ -54,13 +34,14 @@
spellcheck="false"
@trailing-button-click="updateDisplayName" />
</template>
- <template v-else>
- <strong v-if="!isObfuscated"
- :title="user.displayname?.length > 20 ? user.displayname : null">
- {{ user.displayname }}
- </strong>
- <span class="row__subtitle">{{ user.id }}</span>
- </template>
+ <strong v-else-if="!isObfuscated"
+ :title="user.displayname?.length > 20 ? user.displayname : null">
+ {{ user.displayname }}
+ </strong>
+ </td>
+
+ <td class="row__cell row__cell--username" data-cy-user-list-cell-username>
+ <span class="row__subtitle">{{ user.id }}</span>
</td>
<td data-cy-user-list-cell-password
@@ -119,23 +100,24 @@
<template v-if="editing">
<label class="hidden-visually"
:for="'groups' + uniqueId">
- {{ t('settings', 'Add user to group') }}
+ {{ t('settings', 'Add account to group') }}
</label>
<NcSelect data-cy-user-list-input-groups
:data-loading="loading.groups || undefined"
:input-id="'groups' + uniqueId"
:close-on-select="false"
- :disabled="isLoadingField"
+ :disabled="isLoadingField || loading.groupsDetails"
:loading="loading.groups"
:multiple="true"
:append-to-body="false"
:options="availableGroups"
:placeholder="t('settings', 'Add account to group')"
- :taggable="settings.isAdmin"
+ :taggable="settings.isAdmin || settings.isDelegatedAdmin"
:value="userGroups"
label="name"
:no-wrap="true"
- :create-option="(value) => ({ name: value, isCreating: true })"
+ :create-option="(value) => ({ id: value, name: value, isCreating: true })"
+ @search="searchGroups"
@option:created="createGroup"
@option:selected="options => addUserGroup(options.at(-1))"
@option:deselected="removeUserGroup" />
@@ -146,10 +128,10 @@
</span>
</td>
- <td v-if="subAdminsGroups.length > 0 && settings.isAdmin"
+ <td v-if="settings.isAdmin || settings.isDelegatedAdmin"
data-cy-user-list-cell-subadmins
class="row__cell row__cell--large row__cell--multiline">
- <template v-if="editing && settings.isAdmin && subAdminsGroups.length > 0">
+ <template v-if="editing && (settings.isAdmin || settings.isDelegatedAdmin)">
<label class="hidden-visually"
:for="'subadmins' + uniqueId">
{{ t('settings', 'Set account as admin for') }}
@@ -158,21 +140,22 @@
:data-loading="loading.subadmins || undefined"
:input-id="'subadmins' + uniqueId"
:close-on-select="false"
- :disabled="isLoadingField"
+ :disabled="isLoadingField || loading.subAdminGroupsDetails"
:loading="loading.subadmins"
label="name"
:append-to-body="false"
:multiple="true"
:no-wrap="true"
- :options="subAdminsGroups"
+ :options="availableSubAdminGroups"
:placeholder="t('settings', 'Set account as admin for')"
- :value="userSubAdminsGroups"
+ :value="userSubAdminGroups"
+ @search="searchGroups"
@option:deselected="removeUserSubAdmin"
@option:selected="options => addUserSubAdmin(options.at(-1))" />
</template>
<span v-else-if="!isObfuscated"
- :title="userSubAdminsGroupsLabels?.length > 40 ? userSubAdminsGroupsLabels : null">
- {{ userSubAdminsGroupsLabels }}
+ :title="userSubAdminGroupsLabels?.length > 40 ? userSubAdminGroupsLabels : null">
+ {{ userSubAdminGroupsLabels }}
</span>
</td>
@@ -248,6 +231,12 @@
</template>
</td>
+ <td v-if="showConfig.showFirstLogin"
+ class="row__cell"
+ data-cy-user-list-cell-first-login>
+ <span v-if="!isObfuscated">{{ userFirstLogin }}</span>
+ </td>
+
<td v-if="showConfig.showLastLogin"
:title="userLastLoginTooltip"
class="row__cell"
@@ -266,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 }}
@@ -297,17 +287,20 @@
import { formatFileSize, parseFileSize } from '@nextcloud/files'
import { getCurrentUser } from '@nextcloud/auth'
import { showSuccess, showError } from '@nextcloud/dialogs'
+import { confirmPassword } from '@nextcloud/password-confirmation'
-import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
-import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
-import NcProgressBar from '@nextcloud/vue/dist/Components/NcProgressBar.js'
-import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
+import NcAvatar from '@nextcloud/vue/components/NcAvatar'
+import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
+import NcProgressBar from '@nextcloud/vue/components/NcProgressBar'
+import NcSelect from '@nextcloud/vue/components/NcSelect'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
import UserRowActions from './UserRowActions.vue'
import UserRowMixin from '../../mixins/UserRowMixin.js'
-import { isObfuscated, unlimitedQuota } from '../../utils/userUtils.ts';
+import { isObfuscated, unlimitedQuota } from '../../utils/userUtils.ts'
+import { searchGroups, loadUserGroups, loadUserSubAdminGroups } from '../../service/groups.ts'
+import logger from '../../logger.ts'
export default {
name: 'UserRow',
@@ -342,14 +335,6 @@ export default {
type: Boolean,
required: true,
},
- groups: {
- type: Array,
- default: () => [],
- },
- subAdminsGroups: {
- type: Array,
- required: true,
- },
quotaOptions: {
type: Array,
required: true,
@@ -382,6 +367,8 @@ export default {
password: false,
mailAddress: false,
groups: false,
+ groupsDetails: false,
+ subAdminGroupsDetails: false,
subadmins: false,
quota: false,
delete: false,
@@ -393,6 +380,8 @@ export default {
editedDisplayName: this.user.displayname,
editedPassword: '',
editedMail: this.user.email ?? '',
+ // Cancelable promise for search groups request
+ promise: null,
}
},
@@ -422,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)
+ .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(', ')
},
- userSubAdminsGroupsLabels() {
- return this.userSubAdminsGroups
- .map(group => group.name)
+ userSubAdminGroupsLabels() {
+ return this.userSubAdminGroups
+ .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(', ')
},
@@ -442,7 +451,7 @@ export default {
},
canEdit() {
- return getCurrentUser().uid !== this.user.id || this.settings.isAdmin
+ return getCurrentUser().uid !== this.user.id || this.settings.isAdmin || this.settings.isDelegatedAdmin
},
userQuota() {
@@ -514,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)
@@ -522,8 +530,9 @@ export default {
},
methods: {
- wipeUserDevices() {
+ async wipeUserDevices() {
const userid = this.user.id
+ await confirmPassword()
OC.dialogs.confirmDestructive(
t('settings', 'In case of lost device or exiting the organization, this can remotely wipe the Nextcloud data from all devices associated with {userid}. Only works if the devices are connected to the internet.', { userid }),
t('settings', 'Remote wipe of devices'),
@@ -565,6 +574,66 @@ export default {
this.loadingPossibleManagers = false
},
+ async loadGroupsDetails() {
+ this.loading.groups = true
+ this.loading.groupsDetails = true
+ try {
+ const groups = await loadUserGroups({ userId: this.user.id })
+ // 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 })
+ }
+ this.loading.groups = false
+ this.loading.groupsDetails = false
+ },
+
+ async loadSubAdminGroupsDetails() {
+ this.loading.subadmins = true
+ this.loading.subAdminGroupsDetails = true
+ try {
+ const groups = await loadUserSubAdminGroups({ userId: this.user.id })
+ // 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 })
+ }
+ this.loading.subadmins = false
+ this.loading.subAdminGroupsDetails = false
+ },
+
+ async searchGroups(query, toggleLoading) {
+ if (query === '') {
+ return // Prevent unexpected search behaviour e.g. on option:created
+ }
+ if (this.promise) {
+ this.promise.cancel()
+ }
+ toggleLoading(true)
+ try {
+ this.promise = await searchGroups({
+ search: query,
+ offset: 0,
+ limit: 25,
+ })
+ const groups = await this.promise
+ // 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 })
+ }
+ this.promise = null
+ toggleLoading(false)
+ },
+
async searchUserManager(query) {
await this.$store.dispatch('searchUsers', { offset: 0, limit: 10, search: query }).then(response => {
const users = response?.data ? this.filterManagers(Object.values(response?.data.ocs.data.users)) : []
@@ -574,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,
@@ -587,15 +657,19 @@ export default {
})
} catch (error) {
// TRANSLATORS This string describes a line manager in the context of an organization
- showError(t('setting', 'Failed to update line manager'))
- console.error(error)
+ showError(t('settings', 'Failed to update line manager'))
+ 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
}
},
- deleteUser() {
+ async deleteUser() {
const userid = this.user.id
+ await confirmPassword()
OC.dialogs.confirmDestructive(
t('settings', 'Fully delete {userid}\'s account including all their personal files, app data, etc.', { userid }),
t('settings', 'Account deletion'),
@@ -637,68 +711,70 @@ export default {
/**
* Set user displayName
- *
- * @param {string} displayName The display name
*/
- updateDisplayName() {
+ async updateDisplayName() {
this.loading.displayName = true
- this.$store.dispatch('setUserData', {
- userid: this.user.id,
- key: 'displayname',
- value: this.editedDisplayName,
- }).then(() => {
- this.loading.displayName = false
+ try {
+ await this.$store.dispatch('setUserData', {
+ userid: this.user.id,
+ key: 'displayname',
+ value: this.editedDisplayName,
+ })
+
if (this.editedDisplayName === this.user.displayname) {
- showSuccess(t('setting', 'Display name was successfully changed'))
+ showSuccess(t('settings', 'Display name was successfully changed'))
}
- })
+ } finally {
+ this.loading.displayName = false
+ }
},
/**
* Set user password
- *
- * @param {string} password The email address
*/
- updatePassword() {
+ async updatePassword() {
this.loading.password = true
if (this.editedPassword.length === 0) {
- showError(t('setting', "Password can't be empty"))
+ showError(t('settings', "Password can't be empty"))
this.loading.password = false
} else {
- this.$store.dispatch('setUserData', {
- userid: this.user.id,
- key: 'password',
- value: this.editedPassword,
- }).then(() => {
- this.loading.password = false
+ try {
+ await this.$store.dispatch('setUserData', {
+ userid: this.user.id,
+ key: 'password',
+ value: this.editedPassword,
+ })
this.editedPassword = ''
- showSuccess(t('setting', 'Password was successfully changed'))
- })
+ showSuccess(t('settings', 'Password was successfully changed'))
+ } finally {
+ this.loading.password = false
+ }
}
},
/**
* Set user mailAddress
- *
- * @param {string} mailAddress The email address
*/
- updateEmail() {
+ async updateEmail() {
this.loading.mailAddress = true
if (this.editedMail === '') {
- showError(t('setting', "Email can't be empty"))
+ showError(t('settings', "Email can't be empty"))
this.loading.mailAddress = false
this.editedMail = this.user.email
} else {
- this.$store.dispatch('setUserData', {
- userid: this.user.id,
- key: 'email',
- value: this.editedMail,
- }).then(() => {
- this.loading.mailAddress = false
+ try {
+ await this.$store.dispatch('setUserData', {
+ userid: this.user.id,
+ key: 'email',
+ value: this.editedMail,
+ })
+
if (this.editedMail === this.user.email) {
- showSuccess(t('setting', 'Email was successfully changed'))
+ showSuccess(t('settings', 'Email was successfully changed'))
}
- })
+ } finally {
+ this.loading.mailAddress = false
+ }
}
},
@@ -708,17 +784,16 @@ export default {
* @param {string} gid Group id
*/
async createGroup({ name: gid }) {
- this.loading = { groups: true, subadmins: true }
+ this.loading.groups = true
try {
await this.$store.dispatch('addGroup', gid)
const userid = this.user.id
await this.$store.dispatch('addUserGroup', { userid, gid })
+ this.userGroups.push({ id: gid, name: gid })
} catch (error) {
- console.error(error)
- } finally {
- this.loading = { groups: false, subadmins: false }
+ logger.error(t('settings', 'Failed to create group'), { error })
}
- return this.$store.getters.getGroups[this.groups.length]
+ this.loading.groups = false
},
/**
@@ -732,19 +807,19 @@ export default {
// Ignore
return
}
- this.loading.groups = true
const userid = this.user.id
const gid = group.id
if (group.canAdd === false) {
- return false
+ return
}
+ this.loading.groups = true
try {
await this.$store.dispatch('addUserGroup', { userid, gid })
+ this.userGroups.push(group)
} catch (error) {
console.error(error)
- } finally {
- this.loading.groups = false
}
+ this.loading.groups = false
},
/**
@@ -764,6 +839,7 @@ export default {
userid,
gid,
})
+ this.userGroups = this.userGroups.filter(group => group.id !== gid)
this.loading.groups = false
// remove user from current list if current list is the removed group
if (this.$route.params.selectedGroup === gid) {
@@ -788,10 +864,11 @@ export default {
userid,
gid,
})
- this.loading.subadmins = false
+ this.userSubAdminGroups.push(group)
} catch (error) {
console.error(error)
}
+ this.loading.subadmins = false
},
/**
@@ -809,6 +886,7 @@ export default {
userid,
gid,
})
+ this.userSubAdminGroups = this.userSubAdminGroups.filter(group => group.id !== gid)
} catch (error) {
console.error(error)
} finally {
@@ -898,7 +976,7 @@ export default {
sendWelcomeMail() {
this.loading.all = true
this.$store.dispatch('sendWelcomeMail', this.user.id)
- .then(() => showSuccess(t('setting', 'Welcome mail sent!'), { timeout: 2000 }))
+ .then(() => showSuccess(t('settings', 'Welcome mail sent!'), { timeout: 2000 }))
.finally(() => {
this.loading.all = false
})
@@ -909,6 +987,8 @@ export default {
if (this.editing) {
await this.$nextTick()
this.$refs.displayNameField?.$refs?.inputField?.$refs?.input?.focus()
+ this.loadGroupsDetails()
+ this.loadSubAdminGroupsDetails()
}
if (this.editedDisplayName !== this.user.displayname) {
this.editedDisplayName = this.user.displayname
@@ -921,10 +1001,10 @@ export default {
</script>
<style lang="scss" scoped>
-@import './shared/styles.scss';
+@use './shared/styles';
.user-list__row {
- @include row;
+ @include styles.row;
&:hover {
background-color: var(--color-background-hover);
@@ -941,7 +1021,7 @@ export default {
}
.row {
- @include cell;
+ @include styles.cell;
&__cell {
border-bottom: 1px solid var(--color-border);
diff --git a/apps/settings/src/components/Users/UserRowActions.vue b/apps/settings/src/components/Users/UserRowActions.vue
index a01bb868c7a..efd70d879a7 100644
--- a/apps/settings/src/components/Users/UserRowActions.vue
+++ b/apps/settings/src/components/Users/UserRowActions.vue
@@ -1,24 +1,6 @@
<!--
- - @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- - @author Ferdinand Thiessen <opensource@fthiessen.de>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -33,13 +15,17 @@
<NcIconSvgWrapper :key="editSvg" :svg="editSvg" aria-hidden="true" />
</template>
</NcActionButton>
- <NcActionButton v-for="({ action, icon, text }, index) in actions"
+ <NcActionButton v-for="({ action, icon, text }, index) in enabledActions"
:key="index"
:disabled="disabled"
:aria-label="text"
:icon="icon"
+ close-after-click
@click="(event) => action(event, { ...user })">
{{ text }}
+ <template v-if="isSvg(icon)" #icon>
+ <NcIconSvgWrapper :svg="icon" aria-hidden="true" />
+ </template>
</NcActionButton>
</NcActions>
</template>
@@ -47,17 +33,19 @@
<script lang="ts">
import type { PropType } from 'vue'
import { defineComponent } from 'vue'
+import isSvg from 'is-svg'
-import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
-import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
-import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcActions from '@nextcloud/vue/components/NcActions'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
import SvgCheck from '@mdi/svg/svg/check.svg?raw'
-import SvgPencil from '@mdi/svg/svg/pencil.svg?raw'
+import SvgPencil from '@mdi/svg/svg/pencil-outline.svg?raw'
interface UserAction {
action: (event: MouseEvent, user: Record<string, unknown>) => void,
+ enabled?: (user: Record<string, unknown>) => boolean,
icon: string,
- text: string
+ text: string,
}
export default defineComponent({
@@ -105,12 +93,21 @@ export default defineComponent({
/**
* Current MDI logo to show for edit toggle
*/
- editSvg() {
+ editSvg(): string {
return this.edit ? SvgCheck : SvgPencil
},
+
+ /**
+ * Enabled user row actions
+ */
+ enabledActions(): UserAction[] {
+ return this.actions.filter(action => typeof action.enabled === 'function' ? action.enabled(this.user) : true)
+ },
},
methods: {
+ isSvg,
+
/**
* Toggle edit mode by emitting the update event
*/
diff --git a/apps/settings/src/components/Users/UserSettingsDialog.vue b/apps/settings/src/components/Users/UserSettingsDialog.vue
index d8db67514c4..94c77d320dd 100644
--- a/apps/settings/src/components/Users/UserSettingsDialog.vue
+++ b/apps/settings/src/components/Users/UserSettingsDialog.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2023 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -42,6 +25,11 @@
{{ t('settings', 'Show storage path') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch type="switch"
+ data-test="showFirstLogin"
+ :checked.sync="showFirstLogin">
+ {{ t('settings', 'Show first login') }}
+ </NcCheckboxRadioSwitch>
+ <NcCheckboxRadioSwitch type="switch"
data-test="showLastLogin"
:checked.sync="showLastLogin">
{{ t('settings', 'Show last login') }}
@@ -55,6 +43,9 @@
</NcNoteCard>
<fieldset>
<legend>{{ t('settings', 'Group list sorting') }}</legend>
+ <NcNoteCard class="dialog__note"
+ type="info"
+ :text="t('settings', 'Sorting only applies to the currently loaded groups for performance reasons. Groups will be loaded as you navigate or search through the list.')" />
<NcCheckboxRadioSwitch type="radio"
:checked.sync="groupSorting"
data-test="sortGroupsByMemberCount"
@@ -87,13 +78,14 @@
<NcAppSettingsSection id="default-settings"
:name="t('settings', 'Defaults')">
<NcSelect v-model="defaultQuota"
+ :clearable="false"
+ :create-option="validateQuota"
+ :filter-by="filterQuotas"
:input-label="t('settings', 'Default quota')"
- placement="top"
- :taggable="true"
:options="quotaOptions"
- :create-option="validateQuota"
+ placement="top"
:placeholder="t('settings', 'Select default quota')"
- :clearable="false"
+ taggable
@option:selected="setDefaultQuota" />
</NcAppSettingsSection>
</NcAppSettingsDialog>
@@ -104,14 +96,15 @@ import { formatFileSize, parseFileSize } from '@nextcloud/files'
import { generateUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'
-import NcAppSettingsDialog from '@nextcloud/vue/dist/Components/NcAppSettingsDialog.js'
-import NcAppSettingsSection from '@nextcloud/vue/dist/Components/NcAppSettingsSection.js'
-import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
-import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
-import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
+import NcAppSettingsDialog from '@nextcloud/vue/components/NcAppSettingsDialog'
+import NcAppSettingsSection from '@nextcloud/vue/components/NcAppSettingsSection'
+import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
+import NcSelect from '@nextcloud/vue/components/NcSelect'
import { GroupSorting } from '../../constants/GroupManagement.ts'
import { unlimitedQuota } from '../../utils/userUtils.ts'
+import logger from '../../logger.ts'
export default {
name: 'UserSettingsDialog',
@@ -181,6 +174,15 @@ export default {
},
},
+ showFirstLogin: {
+ get() {
+ return this.showConfig.showFirstLogin
+ },
+ set(status) {
+ this.setShowConfig('showFirstLogin', status)
+ },
+ },
+
showLastLogin: {
get() {
return this.showConfig.showLastLogin
@@ -246,8 +248,8 @@ export default {
newUserSendEmail: value,
})
await axios.post(generateUrl('/settings/users/preferences/newUser.sendEmail'), { value: value ? 'yes' : 'no' })
- } catch (e) {
- console.error('could not update newUser.sendEmail preference: ' + e.message, e)
+ } catch (error) {
+ logger.error('Could not update newUser.sendEmail preference', { error })
} finally {
this.loadingSendMail = false
}
@@ -256,6 +258,22 @@ export default {
},
methods: {
+ /**
+ * Check if a quota matches the current search.
+ * This is a custom filter function to allow to map "1GB" to the label "1 GB" (ignoring whitespaces).
+ *
+ * @param option The quota to check
+ * @param label The label of the quota
+ * @param search The search string
+ */
+ filterQuotas(option, label, search) {
+ const searchValue = search.toLocaleLowerCase().replaceAll(/\s/g, '')
+ return (label || '')
+ .toLocaleLowerCase()
+ .replaceAll(/\s/g, '')
+ .indexOf(searchValue) > -1
+ },
+
setShowConfig(key, status) {
this.$store.commit('setShowConfig', { key, value: status })
},
@@ -271,14 +289,13 @@ export default {
quota = quota?.id || quota.label
}
// only used for new presets sent through @Tag
- const validQuota = parseFileSize(quota)
+ const validQuota = parseFileSize(quota, true)
if (validQuota === null) {
return unlimitedQuota
- } else {
- // unify format output
- quota = formatFileSize(parseFileSize(quota))
- return { id: quota, label: quota }
}
+ // unify format output
+ quota = formatFileSize(validQuota)
+ return { id: quota, label: quota }
},
/**
@@ -308,6 +325,12 @@ export default {
</script>
<style scoped lang="scss">
+.dialog {
+ &__note {
+ font-weight: normal;
+ }
+}
+
fieldset {
font-weight: bold;
}
diff --git a/apps/settings/src/components/Users/VirtualList.vue b/apps/settings/src/components/Users/VirtualList.vue
index 4ccc3fc60d6..20dc70ef830 100644
--- a/apps/settings/src/components/Users/VirtualList.vue
+++ b/apps/settings/src/components/Users/VirtualList.vue
@@ -1,23 +1,6 @@
<!--
- - @copyright 2023 Christopher Ng <chrng8@gmail.com>
- -
- - @author Christopher Ng <chrng8@gmail.com>
- -
- - @license AGPL-3.0-or-later
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
@@ -52,7 +35,7 @@
<script lang="ts">
import Vue from 'vue'
import { vElementVisibility } from '@vueuse/components'
-import { debounce } from 'debounce'
+import debounce from 'debounce'
import logger from '../../logger.ts'
@@ -174,6 +157,7 @@ export default Vue.extend({
display: block;
overflow: auto;
height: 100%;
+ will-change: scroll-position;
&__header,
&__footer {
@@ -188,7 +172,7 @@ export default Vue.extend({
}
&__footer {
- left: 0;
+ inset-inline-start: 0;
}
&__body {
diff --git a/apps/settings/src/components/Users/shared/styles.scss b/apps/settings/src/components/Users/shared/styles.scss
index a2ddcd8c8be..4dfdd58af6d 100644
--- a/apps/settings/src/components/Users/shared/styles.scss
+++ b/apps/settings/src/components/Users/shared/styles.scss
@@ -1,23 +1,6 @@
/**
- * @copyright 2023 Christopher Ng <chrng8@gmail.com>
- *
- * @author Christopher Ng <chrng8@gmail.com>
- *
- * @license AGPL-3.0-or-later
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
@mixin row {
@@ -57,15 +40,19 @@
}
&--avatar {
- left: 0;
+ inset-inline-start: 0;
}
&--displayname {
- left: var(--avatar-cell-width);
- border-right: 1px solid var(--color-border);
+ inset-inline-start: var(--avatar-cell-width);
+ border-inline-end: 1px solid var(--color-border);
}
}
+ &--username {
+ padding-inline-start: calc(var(--default-grid-baseline) * 3);
+ }
+
&--avatar {
min-width: var(--avatar-cell-width);
width: var(--avatar-cell-width);
@@ -105,7 +92,7 @@
&--actions {
position: sticky;
- right: 0;
+ inset-inline-end: 0;
z-index: var(--sticky-column-z-index);
display: flex;
flex-direction: row;
@@ -113,7 +100,7 @@
min-width: 110px;
width: 110px;
background-color: var(--color-main-background);
- border-left: 1px solid var(--color-border);
+ border-inline-start: 1px solid var(--color-border);
}
}
diff --git a/apps/settings/src/components/WebAuthn/AddDevice.vue b/apps/settings/src/components/WebAuthn/AddDevice.vue
index 72077003cdd..db00bae451a 100644
--- a/apps/settings/src/components/WebAuthn/AddDevice.vue
+++ b/apps/settings/src/components/WebAuthn/AddDevice.vue
@@ -1,23 +1,7 @@
<!--
- - @copyright 2020, Roeland Jago Douma <roeland@famdouma.nl>
- -
- - @author Roeland Jago Douma <roeland@famdouma.nl>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -->
+ - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<div v-if="!isHttps && !isLocalhost">
@@ -39,14 +23,16 @@
<div v-else-if="step === RegistrationSteps.NAMING"
class="new-webauthn-device">
<span class="icon-loading-small webauthn-loading" />
- <NcTextField ref="nameInput"
- class="new-webauthn-device__name"
- :label="t('settings', 'Device name')"
- :value.sync="name"
- show-trailing-button
- :trailing-button-label="t('settings', 'Add')"
- trailing-button-icon="arrowRight"
- @trailing-button-click="submit" />
+ <form @submit.prevent="submit">
+ <NcTextField ref="nameInput"
+ class="new-webauthn-device__name"
+ :label="t('settings', 'Device name')"
+ :value.sync="name"
+ show-trailing-button
+ :trailing-button-label="t('settings', 'Add')"
+ trailing-button-icon="arrowRight"
+ @trailing-button-click="submit" />
+ </form>
</div>
<div v-else-if="step === RegistrationSteps.PERSIST"
@@ -64,8 +50,8 @@
<script>
import { showError } from '@nextcloud/dialogs'
import { confirmPassword } from '@nextcloud/password-confirmation'
-import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
-import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
import logger from '../../logger.ts'
import {
@@ -73,6 +59,8 @@ import {
finishRegistration,
} from '../../service/WebAuthnRegistrationSerice.ts'
+import '@nextcloud/password-confirmation/dist/style.css'
+
const logAndPass = (text) => (data) => {
logger.debug(text)
return data
@@ -191,8 +179,7 @@ export default {
.webauthn-loading {
display: inline-block;
vertical-align: sub;
- margin-left: 2px;
- margin-right: 2px;
+ margin-inline: 2px;
}
.new-webauthn-device {
diff --git a/apps/settings/src/components/WebAuthn/Device.vue b/apps/settings/src/components/WebAuthn/Device.vue
index 319c99c3184..4e10c1f234d 100644
--- a/apps/settings/src/components/WebAuthn/Device.vue
+++ b/apps/settings/src/components/WebAuthn/Device.vue
@@ -1,23 +1,7 @@
<!--
- - @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
- -
- - @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -->
+ - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<li class="webauthn-device">
@@ -32,8 +16,8 @@
</template>
<script>
-import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
-import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
+import NcActions from '@nextcloud/vue/components/NcActions'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
export default {
name: 'Device',
diff --git a/apps/settings/src/components/WebAuthn/Section.vue b/apps/settings/src/components/WebAuthn/Section.vue
index 7e9c75b9264..fa818c24355 100644
--- a/apps/settings/src/components/WebAuthn/Section.vue
+++ b/apps/settings/src/components/WebAuthn/Section.vue
@@ -1,23 +1,7 @@
<!--
- - @copyright 2020, Roeland Jago Douma <roeland@famdouma.nl>
- -
- - @author Roeland Jago Douma <roeland@famdouma.nl>
- -
- - @license GNU AGPL version 3 or any later version
- -
- - This program is free software: you can redistribute it and/or modify
- - it under the terms of the GNU Affero General Public License as
- - published by the Free Software Foundation, either version 3 of the
- - License, or (at your option) any later version.
- -
- - This program is distributed in the hope that it will be useful,
- - but WITHOUT ANY WARRANTY; without even the implied warranty of
- - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- - GNU Affero General Public License for more details.
- -
- - You should have received a copy of the GNU Affero General Public License
- - along with this program. If not, see <http://www.gnu.org/licenses/>.
- -->
+ - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
<div id="security-webauthn" class="section">
@@ -53,8 +37,7 @@
<script>
import { browserSupportsWebAuthn } from '@simplewebauthn/browser'
import { confirmPassword } from '@nextcloud/password-confirmation'
-import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
-import '@nextcloud/password-confirmation/dist/style.css'
+import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import sortBy from 'lodash/fp/sortBy.js'
import AddDevice from './AddDevice.vue'
@@ -62,6 +45,8 @@ import Device from './Device.vue'
import logger from '../../logger.ts'
import { removeRegistration } from '../../service/WebAuthnRegistrationSerice.js'
+import '@nextcloud/password-confirmation/dist/style.css'
+
const sortByName = sortBy('name')
export default {