diff options
5 files changed, 286 insertions, 39 deletions
diff --git a/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue b/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue new file mode 100644 index 00000000000..df847e87442 --- /dev/null +++ b/apps/settings/src/components/PersonalInfo/LanguageSection/Language.vue @@ -0,0 +1,151 @@ +<!-- + - @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/>. +--> + +<template> + <div class="language"> + <select + id="language" + ref="language" + name="language" + :placeholder="t('settings', 'Language')" + required + @input="onLanguageChange"> + <option v-for="commonLanguage in commonLanguages" + :key="commonLanguage.code" + :selected="language.code === commonLanguage.code" + :value="commonLanguage.code"> + {{ commonLanguage.name }} + </option> + <optgroup label="––––––––––" /> + <option v-for="otherLanguage in otherLanguages" + :key="otherLanguage.code" + :selected="language.code === otherLanguage.code" + :value="otherLanguage.code"> + {{ otherLanguage.name }} + </option> + </select> + + <a + href="https://www.transifex.com/nextcloud/nextcloud/" + target="_blank" + rel="noreferrer noopener"> + <em>{{ t('settings', 'Help translate') }}</em> + </a> + </div> +</template> + +<script> +import { showError } from '@nextcloud/dialogs' + +import { saveLanguage } from '../../../service/PersonalInfo/LanguageService' + +export default { + name: 'Language', + + props: { + commonLanguages: { + type: Array, + required: true, + }, + otherLanguages: { + type: Array, + required: true, + }, + language: { + type: Object, + required: true, + }, + }, + + data() { + return { + initialLanguage: this.language, + } + }, + + computed: { + allLanguages() { + return Object.freeze( + [...this.commonLanguages, ...this.otherLanguages] + .reduce((acc, { code, name }) => ({ ...acc, [code]: name }), {}) + ) + }, + }, + + methods: { + async onLanguageChange(e) { + const language = this.constructLanguage(e.target.value) + this.$emit('update:language', language) + + if (this.$refs.language?.checkValidity()) { + await this.updateLanguage(language) + } + }, + + async updateLanguage(language) { + try { + const responseData = await saveLanguage(language.code) + this.handleResponse({ + language, + status: responseData.ocs?.meta?.status, + }) + this.reloadPage() + } catch (e) { + this.handleResponse({ + errorMessage: 'Unable to update language', + error: e, + }) + } + }, + + constructLanguage(languageCode) { + return { + code: languageCode, + name: this.allLanguages[languageCode], + } + }, + + handleResponse({ language, status, errorMessage, error }) { + if (status === 'ok') { + // Ensure that local state reflects server state + this.initialLanguage = language + } else { + showError(t('settings', errorMessage)) + this.logger.error(errorMessage, error) + } + }, + + reloadPage() { + location.reload() + }, + }, +} +</script> + +<style lang="scss" scoped> +.language { + display: grid; + + a { + width: max-content; + } +} +</style> diff --git a/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue b/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue new file mode 100644 index 00000000000..1489b96a465 --- /dev/null +++ b/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue @@ -0,0 +1,102 @@ +<!-- + - @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/>. +--> + +<template> + <form + ref="form" + class="section" + @submit.stop.prevent="() => {}"> + <HeaderBar + :account-property="accountProperty" + label-for="language" + :is-valid-form="isValidForm" /> + + <template v-if="isEditable"> + <Language + :common-languages="commonLanguages" + :other-languages="otherLanguages" + :language.sync="language" + @update:language="onUpdateLanguage" /> + </template> + + <span v-else> + {{ t('settings', 'No language set') }} + </span> + </form> +</template> + +<script> +import { loadState } from '@nextcloud/initial-state' + +import Language from './Language' +import HeaderBar from '../shared/HeaderBar' + +import { SETTING_PROPERTY_READABLE_ENUM } from '../../../constants/AccountPropertyConstants' + +const { languages: { activeLanguage, commonLanguages, otherLanguages } } = loadState('settings', 'personalInfoParameters', {}) + +export default { + name: 'LanguageSection', + + components: { + Language, + HeaderBar, + }, + + data() { + return { + accountProperty: SETTING_PROPERTY_READABLE_ENUM.LANGUAGE, + isValidForm: true, + commonLanguages, + otherLanguages, + language: activeLanguage, + } + }, + + computed: { + isEditable() { + return Boolean(this.language) + }, + }, + + mounted() { + this.$nextTick(() => this.updateFormValidity()) + }, + + methods: { + onUpdateLanguage() { + this.$nextTick(() => this.updateFormValidity()) + }, + + updateFormValidity() { + this.isValidForm = this.$refs.form?.checkValidity() + }, + }, +} +</script> + +<style lang="scss" scoped> +form::v-deep button { + &:disabled { + cursor: default; + } +} +</style> diff --git a/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue b/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue index e4ff0846a07..3cb293c8a34 100644 --- a/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue +++ b/apps/settings/src/components/PersonalInfo/shared/HeaderBar.vue @@ -20,18 +20,21 @@ --> <template> - <h3> + <h3 + :class="{ 'setting-property': isSettingProperty }"> <label :for="labelFor"> <!-- Already translated as required by prop validator --> {{ accountProperty }} </label> - <FederationControl - class="federation-control" - :account-property="accountProperty" - :handle-scope-change="handleScopeChange" - :scope.sync="localScope" - @update:scope="onScopeChange" /> + <template v-if="scope && handleScopeChange"> + <FederationControl + class="federation-control" + :account-property="accountProperty" + :handle-scope-change="handleScopeChange" + :scope.sync="localScope" + @update:scope="onScopeChange" /> + </template> <template v-if="isEditable && isMultiValueSupported"> <AddButton @@ -46,7 +49,7 @@ import AddButton from './AddButton' import FederationControl from './FederationControl' -import { ACCOUNT_PROPERTY_READABLE_ENUM } from '../../../constants/AccountPropertyConstants' +import { ACCOUNT_PROPERTY_READABLE_ENUM, SETTING_PROPERTY_READABLE_ENUM } from '../../../constants/AccountPropertyConstants' export default { name: 'HeaderBar', @@ -60,11 +63,11 @@ export default { accountProperty: { type: String, required: true, - validator: (value) => Object.values(ACCOUNT_PROPERTY_READABLE_ENUM).includes(value), + validator: (value) => Object.values(ACCOUNT_PROPERTY_READABLE_ENUM).includes(value) || Object.values(SETTING_PROPERTY_READABLE_ENUM).includes(value), }, handleScopeChange: { type: Function, - required: true, + default: null, }, isEditable: { type: Boolean, @@ -84,7 +87,7 @@ export default { }, scope: { type: String, - required: true, + default: null, }, }, @@ -94,6 +97,12 @@ export default { } }, + computed: { + isSettingProperty() { + return Object.values(SETTING_PROPERTY_READABLE_ENUM).includes(this.accountProperty) + }, + }, + methods: { onAddAdditional() { this.$emit('add-additional') @@ -119,6 +128,15 @@ export default { } } + h3.setting-property { + width: 100%; + min-height: 38px; + display: inline-flex; + position: relative; + flex-wrap: nowrap; + justify-content: flex-start; + } + .federation-control { margin: -12px 0 0 8px; } diff --git a/apps/settings/src/main-personal-info.js b/apps/settings/src/main-personal-info.js index 72ed4c15230..78de03cf7cf 100644 --- a/apps/settings/src/main-personal-info.js +++ b/apps/settings/src/main-personal-info.js @@ -29,8 +29,8 @@ import logger from './logger' import DisplayNameSection from './components/PersonalInfo/DisplayNameSection/DisplayNameSection' import EmailSection from './components/PersonalInfo/EmailSection/EmailSection' +import LanguageSection from './components/PersonalInfo/LanguageSection/LanguageSection' -// eslint-disable-next-line camelcase __webpack_nonce__ = btoa(getRequestToken()) Vue.mixin({ @@ -44,6 +44,8 @@ Vue.mixin({ const DisplayNameView = Vue.extend(DisplayNameSection) const EmailView = Vue.extend(EmailSection) +const LanguageView = Vue.extend(LanguageSection) new DisplayNameView().$mount('#vue-displaynamesection') new EmailView().$mount('#vue-emailsection') +new LanguageView().$mount('#vue-languagesection') diff --git a/apps/settings/templates/settings/personal/personal.info.php b/apps/settings/templates/settings/personal/personal.info.php index 8b1e22f964d..7484870a936 100644 --- a/apps/settings/templates/settings/personal/personal.info.php +++ b/apps/settings/templates/settings/personal/personal.info.php @@ -245,33 +245,7 @@ script('settings', [ <div class="profile-settings-container"> <div class="personal-settings-setting-box personal-settings-language-box"> - <?php if (isset($_['activelanguage'])) { ?> - <form id="language" class="section"> - <h3> - <label for="languageinput"><?php p($l->t('Language'));?></label> - </h3> - <select id="languageinput" name="lang" data-placeholder="<?php p($l->t('Language'));?>"> - <option value="<?php p($_['activelanguage']['code']);?>"> - <?php p($_['activelanguage']['name']);?> - </option> - <?php foreach ($_['commonlanguages'] as $language):?> - <option value="<?php p($language['code']);?>"> - <?php p($language['name']);?> - </option> - <?php endforeach;?> - <optgroup label="––––––––––"></optgroup> - <?php foreach ($_['languages'] as $language):?> - <option value="<?php p($language['code']);?>"> - <?php p($language['name']);?> - </option> - <?php endforeach;?> - </select> - <a href="https://www.transifex.com/nextcloud/nextcloud/" - target="_blank" rel="noreferrer noopener"> - <em><?php p($l->t('Help translate'));?></em> - </a> - </form> - <?php } ?> + <div id="vue-languagesection" class="section"></div> </div> <div class="personal-settings-setting-box personal-settings-locale-box"> <?php if (isset($_['activelocale'])) { ?> |