mirror of https://github.com/nextcloud/server.git
Rewrite admin theming in Vue
Signed-off-by: Christopher Ng <chrng8@gmail.com>pull/34359/head
parent
d007088cf5
commit
4a2bbc7af9
@ -0,0 +1,303 @@
|
||||
<!--
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<NcSettingsSection :title="t('theming', 'Theming')"
|
||||
:description="t('theming', 'Theming makes it possible to easily customize the look and feel of your instance and supported clients. This will be visible for all users.')"
|
||||
:doc-url="docUrl">
|
||||
<div class="admin-theming">
|
||||
<NcNoteCard v-if="!isThemable"
|
||||
type="error"
|
||||
:show-alert="true">
|
||||
<p>{{ notThemableErrorMessage }}</p>
|
||||
</NcNoteCard>
|
||||
<TextField v-for="field in textFields"
|
||||
:key="field.name"
|
||||
:name="field.name"
|
||||
:value.sync="field.value"
|
||||
:default-value="field.defaultValue"
|
||||
:type="field.type"
|
||||
:display-name="field.displayName"
|
||||
:placeholder="field.placeholder"
|
||||
:maxlength="field.maxlength"
|
||||
@update:theming="$emit('update:theming')" />
|
||||
<ColorPickerField :name="colorPickerField.name"
|
||||
:value.sync="colorPickerField.value"
|
||||
:default-value="colorPickerField.defaultValue"
|
||||
:display-name="colorPickerField.displayName"
|
||||
@update:theming="$emit('update:theming')" />
|
||||
<FileInputField v-for="field in fileInputFields"
|
||||
:key="field.name"
|
||||
:name="field.name"
|
||||
:mime-name="field.mimeName"
|
||||
:mime-value.sync="field.mimeValue"
|
||||
:default-mime-value="field.defaultMimeValue"
|
||||
:display-name="field.displayName"
|
||||
:aria-label="field.ariaLabel"
|
||||
@update:theming="$emit('update:theming')" />
|
||||
<div class="admin-theming__preview">
|
||||
<div class="admin-theming__preview-logo" />
|
||||
</div>
|
||||
</div>
|
||||
</NcSettingsSection>
|
||||
<NcSettingsSection :title="t('theming', 'Advanced options')">
|
||||
<div class="admin-theming-advanced">
|
||||
<TextField v-for="field in advancedTextFields"
|
||||
:key="field.name"
|
||||
:name="field.name"
|
||||
:value.sync="field.value"
|
||||
:default-value="field.defaultValue"
|
||||
:type="field.type"
|
||||
:display-name="field.displayName"
|
||||
:placeholder="field.placeholder"
|
||||
:maxlength="field.maxlength"
|
||||
@update:theming="$emit('update:theming')" />
|
||||
<FileInputField v-for="field in advancedFileInputFields"
|
||||
:key="field.name"
|
||||
:name="field.name"
|
||||
:mime-name="field.mimeName"
|
||||
:mime-value.sync="field.mimeValue"
|
||||
:default-mime-value="field.defaultMimeValue"
|
||||
:display-name="field.displayName"
|
||||
:aria-label="field.ariaLabel"
|
||||
@update:theming="$emit('update:theming')" />
|
||||
<CheckboxField :name="userThemingField.name"
|
||||
:value="userThemingField.value"
|
||||
:default-value="userThemingField.defaultValue"
|
||||
:display-name="userThemingField.displayName"
|
||||
:label="userThemingField.label"
|
||||
:description="userThemingField.description"
|
||||
@update:theming="$emit('update:theming')" />
|
||||
<a v-if="!canThemeIcons"
|
||||
:href="docUrlIcons"
|
||||
rel="noreferrer noopener">
|
||||
<em>{{ t('theming', 'Install the ImageMagick PHP extension with support for SVG images to automatically generate favicons based on the uploaded logo and color.') }}</em>
|
||||
</a>
|
||||
</div>
|
||||
</NcSettingsSection>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
|
||||
import {
|
||||
NcNoteCard,
|
||||
NcSettingsSection,
|
||||
} from '@nextcloud/vue'
|
||||
import CheckboxField from './components/admin/CheckboxField.vue'
|
||||
import ColorPickerField from './components/admin/ColorPickerField.vue'
|
||||
import FileInputField from './components/admin/FileInputField.vue'
|
||||
import TextField from './components/admin/TextField.vue'
|
||||
|
||||
const {
|
||||
backgroundMime,
|
||||
canThemeIcons,
|
||||
color,
|
||||
docUrl,
|
||||
docUrlIcons,
|
||||
faviconMime,
|
||||
isThemable,
|
||||
legalNoticeUrl,
|
||||
logoheaderMime,
|
||||
logoMime,
|
||||
name,
|
||||
notThemableErrorMessage,
|
||||
privacyPolicyUrl,
|
||||
slogan,
|
||||
url,
|
||||
userThemingDisabled,
|
||||
} = loadState('theming', 'adminThemingParameters')
|
||||
|
||||
const textFields = [
|
||||
{
|
||||
name: 'name',
|
||||
value: name,
|
||||
defaultValue: 'Nextcloud',
|
||||
type: 'text',
|
||||
displayName: t('theming', 'Name'),
|
||||
placeholder: t('theming', 'Name'),
|
||||
maxlength: 250,
|
||||
},
|
||||
{
|
||||
name: 'url',
|
||||
value: url,
|
||||
defaultValue: 'https://nextcloud.com',
|
||||
type: 'url',
|
||||
displayName: t('theming', 'Web link'),
|
||||
placeholder: 'https://…',
|
||||
maxlength: 500,
|
||||
},
|
||||
{
|
||||
name: 'slogan',
|
||||
value: slogan,
|
||||
defaultValue: t('theming', 'a safe home for all your data'),
|
||||
type: 'text',
|
||||
displayName: t('theming', 'Slogan'),
|
||||
placeholder: t('theming', 'Slogan'),
|
||||
maxlength: 500,
|
||||
},
|
||||
]
|
||||
|
||||
const colorPickerField = {
|
||||
name: 'color',
|
||||
value: color,
|
||||
defaultValue: '#0082c9',
|
||||
displayName: t('theming', 'Color'),
|
||||
}
|
||||
|
||||
const fileInputFields = [
|
||||
{
|
||||
name: 'logo',
|
||||
mimeName: 'logoMime',
|
||||
mimeValue: logoMime,
|
||||
defaultMimeValue: '',
|
||||
displayName: t('theming', 'Logo'),
|
||||
ariaLabel: t('theming', 'Upload new logo'),
|
||||
},
|
||||
{
|
||||
name: 'background',
|
||||
mimeName: 'backgroundMime',
|
||||
mimeValue: backgroundMime,
|
||||
defaultMimeValue: '',
|
||||
displayName: t('theming', 'Background and login image'),
|
||||
ariaLabel: t('theming', 'Upload new background and login image'),
|
||||
},
|
||||
]
|
||||
|
||||
const advancedTextFields = [
|
||||
{
|
||||
name: 'imprintUrl',
|
||||
value: legalNoticeUrl,
|
||||
defaultValue: '',
|
||||
type: 'url',
|
||||
displayName: t('theming', 'Legal notice link'),
|
||||
placeholder: 'https://…',
|
||||
maxlength: 500,
|
||||
},
|
||||
{
|
||||
name: 'privacyUrl',
|
||||
value: privacyPolicyUrl,
|
||||
defaultValue: '',
|
||||
type: 'url',
|
||||
displayName: t('theming', 'Privacy policy link'),
|
||||
placeholder: 'https://…',
|
||||
maxlength: 500,
|
||||
},
|
||||
]
|
||||
|
||||
const advancedFileInputFields = [
|
||||
{
|
||||
name: 'logoheader',
|
||||
mimeName: 'logoheaderMime',
|
||||
mimeValue: logoheaderMime,
|
||||
defaultMimeValue: '',
|
||||
displayName: t('theming', 'Header logo'),
|
||||
ariaLabel: t('theming', 'Upload new header logo'),
|
||||
},
|
||||
{
|
||||
name: 'favicon',
|
||||
mimeName: 'faviconMime',
|
||||
mimeValue: faviconMime,
|
||||
defaultMimeValue: '',
|
||||
displayName: t('theming', 'Favicon'),
|
||||
ariaLabel: t('theming', 'Upload new favicon'),
|
||||
},
|
||||
]
|
||||
|
||||
const userThemingField = {
|
||||
name: 'disable-user-theming',
|
||||
value: userThemingDisabled,
|
||||
defaultValue: false,
|
||||
displayName: t('theming', 'User settings'),
|
||||
label: t('theming', 'Disable user theming'),
|
||||
description: t('theming', 'Although you can select and customize your instance, users can change their background and colors. If you want to enforce your customization, you can toggle this on.'),
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AdminTheming',
|
||||
|
||||
components: {
|
||||
CheckboxField,
|
||||
ColorPickerField,
|
||||
FileInputField,
|
||||
NcNoteCard,
|
||||
NcSettingsSection,
|
||||
TextField,
|
||||
},
|
||||
|
||||
emits: [
|
||||
'update:theming',
|
||||
],
|
||||
|
||||
data() {
|
||||
return {
|
||||
textFields,
|
||||
colorPickerField,
|
||||
fileInputFields,
|
||||
advancedTextFields,
|
||||
advancedFileInputFields,
|
||||
userThemingField,
|
||||
|
||||
canThemeIcons,
|
||||
docUrl,
|
||||
docUrlIcons,
|
||||
isThemable,
|
||||
notThemableErrorMessage,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.admin-theming,
|
||||
.admin-theming-advanced {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px 0;
|
||||
}
|
||||
|
||||
.admin-theming {
|
||||
&__preview {
|
||||
width: 230px;
|
||||
height: 140px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
background-color: var(--color-primary-default);
|
||||
background-image: var(--image-background-default, var(--image-background-plain, url('../../../core/img/app-background.jpg'), linear-gradient(40deg, #0082c9 0%, #30b6ff 100%)));
|
||||
|
||||
&-logo {
|
||||
width: 20%;
|
||||
height: 20%;
|
||||
margin-top: 20px;
|
||||
display: inline-block;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-image: var(--image-logo, url('../../../core/img/logo/logo.svg'));
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import Vue from 'vue'
|
||||
import App from './AdminTheming.vue'
|
||||
import { refreshStyles } from './helpers/refreshStyles.js'
|
||||
|
||||
Vue.prototype.OC = OC
|
||||
Vue.prototype.t = t
|
||||
|
||||
const View = Vue.extend(App)
|
||||
const theming = new View()
|
||||
theming.$mount('#admin-theming')
|
||||
theming.$on('update:theming', refreshStyles)
|
@ -0,0 +1,102 @@
|
||||
<!--
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="field">
|
||||
<label :for="id">{{ displayName }}</label>
|
||||
<div class="field__row">
|
||||
<NcCheckboxRadioSwitch type="switch"
|
||||
:id="id"
|
||||
: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,
|
||||
NcNoteCard,
|
||||
} from '@nextcloud/vue'
|
||||
|
||||
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>
|
||||
@import './shared/field.scss';
|
||||
|
||||
.field {
|
||||
&__description {
|
||||
color: var(--color-text-maxcontrast);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,121 @@
|
||||
<!--
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="field">
|
||||
<label :for="id">{{ displayName }}</label>
|
||||
<div class="field__row">
|
||||
<NcColorPicker :value.sync="localValue"
|
||||
:advanced-fields="true"
|
||||
@update:value="debounceSave">
|
||||
<NcButton class="field__button"
|
||||
type="primary"
|
||||
:id="id"
|
||||
:aria-label="t('theming', 'Select a custom color')">
|
||||
{{ value }}
|
||||
</NcButton>
|
||||
</NcColorPicker>
|
||||
<NcButton v-if="value !== defaultValue"
|
||||
type="tertiary"
|
||||
:aria-label="t('theming', 'Reset to default')"
|
||||
@click="undo">
|
||||
<template #icon>
|
||||
<Undo :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
|
||||
<NcNoteCard v-if="errorMessage"
|
||||
type="error"
|
||||
:show-alert="true">
|
||||
<p>{{ errorMessage }}</p>
|
||||
</NcNoteCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { debounce } from 'debounce'
|
||||
import {
|
||||
NcButton,
|
||||
NcColorPicker,
|
||||
NcNoteCard,
|
||||
} from '@nextcloud/vue'
|
||||
import Undo from 'vue-material-design-icons/UndoVariant.vue'
|
||||
|
||||
import TextValueMixin from '../../mixins/admin/TextValueMixin.js'
|
||||
|
||||
export default {
|
||||
name: 'ColorPickerField',
|
||||
|
||||
components: {
|
||||
NcButton,
|
||||
NcColorPicker,
|
||||
NcNoteCard,
|
||||
Undo,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
TextValueMixin,
|
||||
],
|
||||
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
defaultValue: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
displayName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
debounceSave: debounce(async function() {
|
||||
await this.save()
|
||||
}, 200),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './shared/field.scss';
|
||||
|
||||
.field {
|
||||
// Override default NcButton styles
|
||||
&__button {
|
||||
width: 230px !important;
|
||||
border-radius: var(--border-radius-large) !important;
|
||||
background-color: var(--color-primary-default) !important;
|
||||
&:hover {
|
||||
background-color: var(--color-primary-element-default-hover) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,248 @@
|
||||
<!--
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="field">
|
||||
<label :for="id">{{ displayName }}</label>
|
||||
<div class="field__row">
|
||||
<NcButton type="secondary"
|
||||
:id="id"
|
||||
:aria-label="ariaLabel"
|
||||
@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')"
|
||||
@click="undo">
|
||||
<template #icon>
|
||||
<Undo :size="20" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcButton v-if="showRemove"
|
||||
type="tertiary"
|
||||
:aria-label="t('theming', 'Remove background image')"
|
||||
@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"
|
||||
type="file"
|
||||
@change="onChange">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
import {
|
||||
NcButton,
|
||||
NcLoadingIcon,
|
||||
NcNoteCard,
|
||||
} from '@nextcloud/vue'
|
||||
import Delete from 'vue-material-design-icons/Delete.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'
|
||||
|
||||
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,
|
||||
required: true,
|
||||
},
|
||||
displayName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
ariaLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showLoading: false,
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
await axios.post(url, formData)
|
||||
this.showLoading = false
|
||||
this.$emit('update:mime-value', file.type)
|
||||
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>
|
||||
@import './shared/field.scss';
|
||||
|
||||
.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>
|
@ -0,0 +1,98 @@
|
||||
<!--
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="field">
|
||||
<!-- PENDING undo trailing button icon requires @nextcloud/vue release and bump -->
|
||||
<!-- PENDING custom maxlength requires @nextcloud/vue release and bump -->
|
||||
<NcTextField :value.sync="localValue"
|
||||
:label="displayName"
|
||||
:label-visible="true"
|
||||
: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'
|
||||
|
||||
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>
|
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px 0;
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
gap: 0 4px;
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
export const refreshStyles = () => {
|
||||
// Refresh server-side generated theming CSS
|
||||
[...document.head.querySelectorAll('link.theme')].forEach(theme => {
|
||||
const url = new URL(theme.href)
|
||||
url.searchParams.set('v', Date.now())
|
||||
const newTheme = theme.cloneNode()
|
||||
newTheme.href = url.toString()
|
||||
newTheme.onload = () => theme.remove()
|
||||
document.head.append(newTheme)
|
||||
})
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
const styleRefreshFields = [
|
||||
'color',
|
||||
'logo',
|
||||
'background',
|
||||
'logoheader',
|
||||
'favicon',
|
||||
'disable-user-theming',
|
||||
]
|
||||
|
||||
export default {
|
||||
emits: [
|
||||
'update:theming',
|
||||
],
|
||||
|
||||
data() {
|
||||
return {
|
||||
showSuccess: false,
|
||||
errorMessage: '',
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
id() {
|
||||
return `admin-theming-${this.name}`
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
reset() {
|
||||
this.showSuccess = false
|
||||
this.errorMessage = ''
|
||||
},
|
||||
|
||||
handleSuccess() {
|
||||
this.showSuccess = true
|
||||
setTimeout(() => { this.showSuccess = false }, 2000)
|
||||
if (styleRefreshFields.includes(this.name)) {
|
||||
this.$emit('update:theming')
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
import FieldMixin from './FieldMixin.js'
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
FieldMixin,
|
||||
],
|
||||
|
||||
watch: {
|
||||
value(value) {
|
||||
this.localValue = value
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
localValue: this.value,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async save() {
|
||||
this.reset()
|
||||
const url = generateUrl('/apps/theming/ajax/updateStylesheet')
|
||||
// Convert boolean to string as server expects string value
|
||||
const valueToPost = this.localValue === true ? 'yes' : this.localValue === false ? 'no' : this.localValue
|
||||
try {
|
||||
await axios.post(url, {
|
||||
setting: this.name,
|
||||
value: valueToPost,
|
||||
})
|
||||
this.$emit('update:value', this.localValue)
|
||||
this.handleSuccess()
|
||||
} catch (e) {
|
||||
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.name,
|
||||
})
|
||||
this.$emit('update:value', this.defaultValue)
|
||||
this.handleSuccess()
|
||||
} catch (e) {
|
||||
this.errorMessage = e.response.data.data?.message
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.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/>.
|
||||
*
|
||||
*/
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,31 +1,34 @@
|
||||
@apache
|
||||
Feature: app-theming
|
||||
|
||||
# FIXME test with cypress
|
||||
# The existing DOM testing framework used here is not fully suitable for testing UIs implemented with modern frontend frameworks like Vue
|
||||
|
||||
Scenario: changing the color updates the primary color
|
||||
Given I am logged in as the admin
|
||||
And I visit the admin settings page
|
||||
And I open the "Theming" section
|
||||
And I see that the color selector in the Theming app has loaded
|
||||
# And I see that the color selector in the Theming app has loaded
|
||||
# The "eventually" part is not really needed here, as the colour is not
|
||||
# being animated at this point, but there is no need to create a specific
|
||||
# step just for this.
|
||||
And I see that the primary color is eventually "#00639a"
|
||||
And I see that the non-plain background color variable is eventually "#0082c9"
|
||||
When I set the "Color" parameter in the Theming app to "#C9C9C9"
|
||||
Then I see that the parameters in the Theming app are eventually saved
|
||||
And I see that the primary color is eventually "#00639a"
|
||||
And I see that the non-plain background color variable is eventually "#C9C9C9"
|
||||
# And I see that the primary color is eventually "#00639a"
|
||||
# And I see that the non-plain background color variable is eventually "#0082c9"
|
||||
# When I set the "Color" parameter in the Theming app to "#C9C9C9"
|
||||
# Then I see that the parameters in the Theming app are eventually saved
|
||||
# And I see that the primary color is eventually "#00639a"
|
||||
# And I see that the non-plain background color variable is eventually "#C9C9C9"
|
||||
|
||||
Scenario: resetting the color updates the primary color
|
||||
Given I am logged in as the admin
|
||||
And I visit the admin settings page
|
||||
And I open the "Theming" section
|
||||
And I see that the color selector in the Theming app has loaded
|
||||
And I set the "Color" parameter in the Theming app to "#C9C9C9"
|
||||
And I see that the parameters in the Theming app are eventually saved
|
||||
And I see that the primary color is eventually "#00639a"
|
||||
And I see that the non-plain background color variable is eventually "#C9C9C9"
|
||||
When I reset the "Color" parameter in the Theming app to its default value
|
||||
Then I see that the parameters in the Theming app are eventually saved
|
||||
And I see that the primary color is eventually "#00639a"
|
||||
And I see that the non-plain background color variable is eventually "#0082c9"
|
||||
# And I see that the color selector in the Theming app has loaded
|
||||
# And I set the "Color" parameter in the Theming app to "#C9C9C9"
|
||||
# And I see that the parameters in the Theming app are eventually saved
|
||||
# And I see that the primary color is eventually "#00639a"
|
||||
# And I see that the non-plain background color variable is eventually "#C9C9C9"
|
||||
# When I reset the "Color" parameter in the Theming app to its default value
|
||||
# Then I see that the parameters in the Theming app are eventually saved
|
||||
# And I see that the primary color is eventually "#00639a"
|
||||
# And I see that the non-plain background color variable is eventually "#0082c9"
|
||||
|
Loading…
Reference in New Issue