diff options
Diffstat (limited to 'apps/theming/src/components/admin')
-rw-r--r-- | apps/theming/src/components/admin/AppMenuSection.vue | 126 | ||||
-rw-r--r-- | apps/theming/src/components/admin/CheckboxField.vue | 85 | ||||
-rw-r--r-- | apps/theming/src/components/admin/ColorPickerField.vue | 157 | ||||
-rw-r--r-- | apps/theming/src/components/admin/FileInputField.vue | 241 | ||||
-rw-r--r-- | apps/theming/src/components/admin/TextField.vue | 78 | ||||
-rw-r--r-- | apps/theming/src/components/admin/shared/field.scss | 15 |
6 files changed, 702 insertions, 0 deletions
diff --git a/apps/theming/src/components/admin/AppMenuSection.vue b/apps/theming/src/components/admin/AppMenuSection.vue new file mode 100644 index 00000000000..bf229f15df4 --- /dev/null +++ b/apps/theming/src/components/admin/AppMenuSection.vue @@ -0,0 +1,126 @@ +<!-- + - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <NcSettingsSection :name="t('theming', 'Navigation bar settings')"> + <h3>{{ t('theming', 'Default app') }}</h3> + <p class="info-note"> + {{ t('theming', 'The default app is the app that is e.g. opened after login or when the logo in the menu is clicked.') }} + </p> + + <NcCheckboxRadioSwitch :checked.sync="hasCustomDefaultApp" type="switch" data-cy-switch-default-app=""> + {{ t('theming', 'Use custom default app') }} + </NcCheckboxRadioSwitch> + + <template v-if="hasCustomDefaultApp"> + <h4>{{ t('theming', 'Global default app') }}</h4> + <NcSelect v-model="selectedApps" + :close-on-select="false" + :placeholder="t('theming', 'Global default apps')" + :options="allApps" + :multiple="true" /> + <h5>{{ t('theming', 'Default app priority') }}</h5> + <p class="info-note"> + {{ t('theming', 'If an app is not enabled for a user, the next app with lower priority is used.') }} + </p> + <AppOrderSelector :value.sync="selectedApps" /> + </template> + </NcSettingsSection> +</template> + +<script lang="ts"> +import type { INavigationEntry } from '../../../../../core/src/types/navigation' + +import { showError } from '@nextcloud/dialogs' +import { loadState } from '@nextcloud/initial-state' +import { translate as t } from '@nextcloud/l10n' +import { generateUrl } from '@nextcloud/router' +import { computed, defineComponent } from 'vue' + +import axios from '@nextcloud/axios' + +import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' +import NcSelect from '@nextcloud/vue/components/NcSelect' +import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection' +import AppOrderSelector from '../AppOrderSelector.vue' + +export default defineComponent({ + name: 'AppMenuSection', + components: { + AppOrderSelector, + NcCheckboxRadioSwitch, + NcSelect, + NcSettingsSection, + }, + props: { + defaultApps: { + type: Array, + required: true, + }, + }, + emits: { + 'update:defaultApps': (value: string[]) => Array.isArray(value) && value.every((id) => typeof id === 'string'), + }, + setup(props, { emit }) { + const hasCustomDefaultApp = computed({ + get: () => props.defaultApps.length > 0, + set: (checked: boolean) => { + if (checked) { + emit('update:defaultApps', ['dashboard', 'files']) + } else { + selectedApps.value = [] + } + }, + }) + + /** + * All enabled apps which can be navigated + */ + const allApps = loadState<INavigationEntry[]>('core', 'apps') + .map(({ id, name, icon }) => ({ label: name, id, icon })) + + /** + * Currently selected app, wrapps the setter + */ + const selectedApps = computed({ + get: () => props.defaultApps.map((id) => allApps.filter(app => app.id === id)[0]), + set(value) { + saveSetting('defaultApps', value.map(app => app.id)) + .then(() => emit('update:defaultApps', value.map(app => app.id))) + .catch(() => showError(t('theming', 'Could not set global default apps'))) + }, + }) + + const saveSetting = async (key: string, value: unknown) => { + const url = generateUrl('/apps/theming/ajax/updateAppMenu') + return await axios.put(url, { + setting: key, + value, + }) + } + + return { + allApps, + selectedApps, + hasCustomDefaultApp, + + t, + } + }, +}) +</script> + +<style scoped lang="scss"> +h3, h4 { + font-weight: bold; +} + +h4, h5 { + margin-block-start: 12px; +} + +.info-note { + color: var(--color-text-maxcontrast); +} +</style> diff --git a/apps/theming/src/components/admin/CheckboxField.vue b/apps/theming/src/components/admin/CheckboxField.vue new file mode 100644 index 00000000000..42d86ded4e7 --- /dev/null +++ b/apps/theming/src/components/admin/CheckboxField.vue @@ -0,0 +1,85 @@ +<!-- + - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <div class="field"> + <label :for="id">{{ displayName }}</label> + <div class="field__row"> + <NcCheckboxRadioSwitch :id="id" + type="switch" + :checked.sync="localValue" + @update:checked="save"> + {{ label }} + </NcCheckboxRadioSwitch> + </div> + + <p class="field__description"> + {{ description }} + </p> + + <NcNoteCard v-if="errorMessage" + type="error" + :show-alert="true"> + <p>{{ errorMessage }}</p> + </NcNoteCard> + </div> +</template> + +<script> +import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' +import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' + +import TextValueMixin from '../../mixins/admin/TextValueMixin.js' + +export default { + name: 'CheckboxField', + + components: { + NcCheckboxRadioSwitch, + NcNoteCard, + }, + + mixins: [ + TextValueMixin, + ], + + props: { + name: { + type: String, + required: true, + }, + value: { + type: Boolean, + required: true, + }, + defaultValue: { + type: Boolean, + required: true, + }, + displayName: { + type: String, + required: true, + }, + label: { + type: String, + required: true, + }, + description: { + type: String, + required: true, + }, + }, +} +</script> + +<style lang="scss" scoped> +@use './shared/field' as *; + +.field { + &__description { + color: var(--color-text-maxcontrast); + } +} +</style> diff --git a/apps/theming/src/components/admin/ColorPickerField.vue b/apps/theming/src/components/admin/ColorPickerField.vue new file mode 100644 index 00000000000..4ec6d47fef6 --- /dev/null +++ b/apps/theming/src/components/admin/ColorPickerField.vue @@ -0,0 +1,157 @@ +<!-- + - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <div class="field"> + <label :for="id">{{ displayName }}</label> + <div class="field__row"> + <NcColorPicker :value.sync="localValue" + :advanced-fields="true" + @update:value="debounceSave"> + <NcButton :id="id" + class="field__button" + type="primary" + :aria-label="t('theming', 'Select a custom color')" + data-admin-theming-setting-color-picker> + <template #icon> + <NcLoadingIcon v-if="loading" + :appearance="calculatedTextColor === '#ffffff' ? 'light' : 'dark'" + :size="20" /> + <Palette v-else :size="20" /> + </template> + {{ value }} + </NcButton> + </NcColorPicker> + <div class="field__color-preview" data-admin-theming-setting-color /> + <NcButton v-if="value !== defaultValue" + type="tertiary" + :aria-label="t('theming', 'Reset to default')" + data-admin-theming-setting-color-reset + @click="undo"> + <template #icon> + <Undo :size="20" /> + </template> + </NcButton> + </div> + <div v-if="description" class="description"> + {{ description }} + </div> + + <NcNoteCard v-if="errorMessage" + type="error" + :show-alert="true"> + <p>{{ errorMessage }}</p> + </NcNoteCard> + </div> +</template> + +<script> +import { colord } from 'colord' +import debounce from 'debounce' + +import NcButton from '@nextcloud/vue/components/NcButton' +import NcColorPicker from '@nextcloud/vue/components/NcColorPicker' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' +import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' +import Undo from 'vue-material-design-icons/UndoVariant.vue' +import Palette from 'vue-material-design-icons/Palette.vue' + +import TextValueMixin from '../../mixins/admin/TextValueMixin.js' + +export default { + name: 'ColorPickerField', + + components: { + NcButton, + NcColorPicker, + NcLoadingIcon, + NcNoteCard, + Undo, + Palette, + }, + + mixins: [ + TextValueMixin, + ], + + props: { + name: { + type: String, + required: true, + }, + description: { + type: String, + default: '', + }, + value: { + type: String, + required: true, + }, + textColor: { + type: String, + default: null, + }, + defaultValue: { + type: String, + required: true, + }, + displayName: { + type: String, + required: true, + }, + }, + + emits: ['update:theming'], + + data() { + return { + loading: false, + } + }, + + computed: { + calculatedTextColor() { + const color = colord(this.value) + return color.isLight() ? '#000000' : '#ffffff' + }, + usedTextColor() { + if (this.textColor) { + return this.textColor + } + return this.calculatedTextColor + }, + }, + + methods: { + debounceSave: debounce(async function() { + this.loading = true + await this.save() + this.$emit('update:theming') + this.loading = false + }, 200), + }, +} +</script> + +<style lang="scss" scoped> +@use './shared/field' as *; + +.description { + color: var(--color-text-maxcontrast); +} + +.field { + &__button { + background-color: v-bind('value') !important; + color: v-bind('usedTextColor') !important; + } + + &__color-preview { + width: var(--default-clickable-area); + border-radius: var(--border-radius-large); + background-color: v-bind('value'); + } +} +</style> diff --git a/apps/theming/src/components/admin/FileInputField.vue b/apps/theming/src/components/admin/FileInputField.vue new file mode 100644 index 00000000000..d5e0052f5bd --- /dev/null +++ b/apps/theming/src/components/admin/FileInputField.vue @@ -0,0 +1,241 @@ +<!-- + - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <div class="field"> + <label :for="id">{{ displayName }}</label> + <div class="field__row"> + <NcButton :id="id" + type="secondary" + :aria-label="ariaLabel" + data-admin-theming-setting-file-picker + @click="activateLocalFilePicker"> + <template #icon> + <Upload :size="20" /> + </template> + {{ t('theming', 'Upload') }} + </NcButton> + <NcButton v-if="showReset" + type="tertiary" + :aria-label="t('theming', 'Reset to default')" + data-admin-theming-setting-file-reset + @click="undo"> + <template #icon> + <Undo :size="20" /> + </template> + </NcButton> + <NcButton v-if="showRemove" + type="tertiary" + :aria-label="t('theming', 'Remove background image')" + data-admin-theming-setting-file-remove + @click="removeBackground"> + <template #icon> + <Delete :size="20" /> + </template> + </NcButton> + <NcLoadingIcon v-if="showLoading" + class="field__loading-icon" + :size="20" /> + </div> + + <div v-if="(name === 'logoheader' || name === 'favicon') && mimeValue !== defaultMimeValue" + class="field__preview" + :class="{ + 'field__preview--logoheader': name === 'logoheader', + 'field__preview--favicon': name === 'favicon', + }" /> + + <NcNoteCard v-if="errorMessage" + type="error" + :show-alert="true"> + <p>{{ errorMessage }}</p> + </NcNoteCard> + + <input ref="input" + :accept="acceptMime" + type="file" + @change="onChange"> + </div> +</template> + +<script> +import axios from '@nextcloud/axios' +import { generateUrl } from '@nextcloud/router' +import { loadState } from '@nextcloud/initial-state' + +import NcButton from '@nextcloud/vue/components/NcButton' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' +import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' +import Delete from 'vue-material-design-icons/DeleteOutline.vue' +import Undo from 'vue-material-design-icons/UndoVariant.vue' +import Upload from 'vue-material-design-icons/Upload.vue' + +import FieldMixin from '../../mixins/admin/FieldMixin.js' + +const { + allowedMimeTypes, +} = loadState('theming', 'adminThemingParameters', {}) + +export default { + name: 'FileInputField', + + components: { + Delete, + NcButton, + NcLoadingIcon, + NcNoteCard, + Undo, + Upload, + }, + + mixins: [ + FieldMixin, + ], + + props: { + name: { + type: String, + required: true, + }, + mimeName: { + type: String, + required: true, + }, + mimeValue: { + type: String, + required: true, + }, + defaultMimeValue: { + type: String, + default: '', + }, + displayName: { + type: String, + required: true, + }, + ariaLabel: { + type: String, + required: true, + }, + }, + + data() { + return { + showLoading: false, + acceptMime: (allowedMimeTypes[this.name] + || ['image/jpeg', 'image/png', 'image/gif', 'image/webp']).join(','), + } + }, + + computed: { + showReset() { + return this.mimeValue !== this.defaultMimeValue + }, + + showRemove() { + if (this.name === 'background') { + if (this.mimeValue.startsWith('image/')) { + return true + } + if (this.mimeValue === this.defaultMimeValue) { + return true + } + } + return false + }, + }, + + methods: { + activateLocalFilePicker() { + this.reset() + // Set to null so that selecting the same file will trigger the change event + this.$refs.input.value = null + this.$refs.input.click() + }, + + async onChange(e) { + const file = e.target.files[0] + + const formData = new FormData() + formData.append('key', this.name) + formData.append('image', file) + + const url = generateUrl('/apps/theming/ajax/uploadImage') + try { + this.showLoading = true + const { data } = await axios.post(url, formData) + this.showLoading = false + this.$emit('update:mime-value', file.type) + this.$emit('uploaded', data.data.url) + this.handleSuccess() + } catch (e) { + this.showLoading = false + this.errorMessage = e.response.data.data?.message + } + }, + + async undo() { + this.reset() + const url = generateUrl('/apps/theming/ajax/undoChanges') + try { + await axios.post(url, { + setting: this.mimeName, + }) + this.$emit('update:mime-value', this.defaultMimeValue) + this.handleSuccess() + } catch (e) { + this.errorMessage = e.response.data.data?.message + } + }, + + async removeBackground() { + this.reset() + const url = generateUrl('/apps/theming/ajax/updateStylesheet') + try { + await axios.post(url, { + setting: this.mimeName, + value: 'backgroundColor', + }) + this.$emit('update:mime-value', 'backgroundColor') + this.handleSuccess() + } catch (e) { + this.errorMessage = e.response.data.data?.message + } + }, + }, +} +</script> + +<style lang="scss" scoped> +@use './shared/field' as *; + +.field { + &__loading-icon { + width: 44px; + height: 44px; + } + + &__preview { + width: 70px; + height: 70px; + background-size: contain; + background-position: center; + background-repeat: no-repeat; + margin: 10px 0; + + &--logoheader { + background-image: var(--image-logoheader); + } + + &--favicon { + background-image: var(--image-favicon); + } + } +} + +input[type="file"] { + display: none; +} +</style> diff --git a/apps/theming/src/components/admin/TextField.vue b/apps/theming/src/components/admin/TextField.vue new file mode 100644 index 00000000000..6ec52733aed --- /dev/null +++ b/apps/theming/src/components/admin/TextField.vue @@ -0,0 +1,78 @@ +<!-- + - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <div class="field"> + <NcTextField :value.sync="localValue" + :label="displayName" + :placeholder="placeholder" + :type="type" + :maxlength="maxlength" + :spellcheck="false" + :success="showSuccess" + :error="Boolean(errorMessage)" + :helper-text="errorMessage" + :show-trailing-button="value !== defaultValue" + trailing-button-icon="undo" + @trailing-button-click="undo" + @keydown.enter="save" + @blur="save" /> + </div> +</template> + +<script> +import NcTextField from '@nextcloud/vue/components/NcTextField' + +import TextValueMixin from '../../mixins/admin/TextValueMixin.js' + +export default { + name: 'TextField', + + components: { + NcTextField, + }, + + mixins: [ + TextValueMixin, + ], + + props: { + name: { + type: String, + required: true, + }, + value: { + type: String, + required: true, + }, + defaultValue: { + type: String, + required: true, + }, + type: { + type: String, + required: true, + }, + displayName: { + type: String, + required: true, + }, + placeholder: { + type: String, + required: true, + }, + maxlength: { + type: Number, + required: true, + }, + }, +} +</script> + +<style lang="scss" scoped> +.field { + max-width: 400px; +} +</style> diff --git a/apps/theming/src/components/admin/shared/field.scss b/apps/theming/src/components/admin/shared/field.scss new file mode 100644 index 00000000000..2347f31f7c5 --- /dev/null +++ b/apps/theming/src/components/admin/shared/field.scss @@ -0,0 +1,15 @@ +/*! + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +.field { + display: flex; + flex-direction: column; + gap: 4px 0; + + &__row { + display: flex; + gap: 0 4px; + } +} |