Remake locale saving with Vuetags/v25.0.0beta7
@@ -267,6 +267,12 @@ class UsersControllerTest extends TestCase { | |||
->method('isAdmin') | |||
->with('adminUser') | |||
->willReturn(true); | |||
$l10n = $this->createMock(IL10N::class); | |||
$this->l10nFactory | |||
->expects($this->once()) | |||
->method('get') | |||
->with('provisioning_api') | |||
->willReturn($l10n); | |||
$this->api->addUser('AlreadyExistingUser', 'password', '', '', []); | |||
} |
@@ -105,11 +105,6 @@ input#openid, input#webdav { | |||
grid-template-columns: 1fr; | |||
grid-template-rows: 1fr 1fr 1fr 2fr; | |||
} | |||
.profile-settings-container #locale h3 { | |||
height: 44px; | |||
display: flex; | |||
align-items: center; | |||
} | |||
.personal-show-container { | |||
width: 100%; | |||
@@ -125,7 +120,7 @@ input#openid, input#webdav { | |||
width: 100%; | |||
} | |||
select#timezone, select#languageinput, select#localeinput { | |||
select#timezone { | |||
width: 100%; | |||
} | |||
@@ -47,14 +47,6 @@ input { | |||
display: inline-grid; | |||
grid-template-columns: 1fr; | |||
grid-template-rows: 1fr 1fr 1fr 2fr; | |||
#locale { | |||
h3 { | |||
height: 44px; | |||
display: flex; | |||
align-items: center; | |||
} | |||
} | |||
} | |||
.personal-show-container { | |||
@@ -78,9 +70,7 @@ input { | |||
} | |||
select { | |||
&#timezone, | |||
&#languageinput, | |||
&#localeinput { | |||
&#timezone { | |||
width: 100%; | |||
} | |||
} |
@@ -108,62 +108,4 @@ window.addEventListener('DOMContentLoaded', function () { | |||
}); | |||
federationSettingsView.render(); | |||
var updateLanguage = function () { | |||
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) { | |||
OC.PasswordConfirmation.requirePasswordConfirmation(updateLanguage); | |||
return; | |||
} | |||
var selectedLang = $("#languageinput").val(), | |||
user = OC.getCurrentUser(); | |||
$.ajax({ | |||
url: OC.linkToOCS('cloud/users', 2) + user['uid'], | |||
method: 'PUT', | |||
data: { | |||
key: 'language', | |||
value: selectedLang | |||
}, | |||
success: function() { | |||
location.reload(); | |||
}, | |||
fail: function() { | |||
OC.Notification.showTemporary(t('settings', 'An error occurred while changing your language. Please reload the page and try again.')); | |||
} | |||
}); | |||
}; | |||
$("#languageinput").change(updateLanguage); | |||
var updateLocale = function () { | |||
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) { | |||
OC.PasswordConfirmation.requirePasswordConfirmation(updateLocale); | |||
return; | |||
} | |||
var selectedLocale = $("#localeinput").val(), | |||
user = OC.getCurrentUser(); | |||
$.ajax({ | |||
url: OC.linkToOCS('cloud/users', 2) + user.uid, | |||
method: 'PUT', | |||
data: { | |||
key: 'locale', | |||
value: selectedLocale | |||
}, | |||
success: function() { | |||
moment.locale(selectedLocale); | |||
}, | |||
fail: function() { | |||
OC.Notification.showTemporary(t('settings', 'An error occurred while changing your locale. Please reload the page and try again.')); | |||
} | |||
}); | |||
}; | |||
$("#localeinput").change(updateLocale); | |||
}); | |||
window.setInterval(function() { | |||
$('#localeexample-time').text(moment().format('LTS')) | |||
$('#localeexample-date').text(moment().format('L')) | |||
$('#localeexample-fdow').text(t('settings', 'Week starts on {fdow}', { fdow: dayNames[firstDay] })) | |||
}, 1000) |
@@ -135,7 +135,6 @@ class PersonalInfo implements ISettings { | |||
$totalSpace = \OC_Helper::humanFileSize($storageInfo['total']); | |||
} | |||
$localeParameters = $this->getLocales($user); | |||
$messageParameters = $this->getMessageParameters($account); | |||
$parameters = [ | |||
@@ -143,7 +142,7 @@ class PersonalInfo implements ISettings { | |||
'lookupServerUploadEnabled' => $lookupServerUploadEnabled, | |||
'isFairUseOfFreePushService' => $this->isFairUseOfFreePushService(), | |||
'profileEnabledGlobally' => $this->profileManager->isProfileEnabled(), | |||
] + $messageParameters + $localeParameters; | |||
] + $messageParameters; | |||
$personalInfoParameters = [ | |||
'userId' => $uid, | |||
@@ -160,6 +159,7 @@ class PersonalInfo implements ISettings { | |||
'website' => $this->getProperty($account, IAccountManager::PROPERTY_WEBSITE), | |||
'twitter' => $this->getProperty($account, IAccountManager::PROPERTY_TWITTER), | |||
'languageMap' => $this->getLanguageMap($user), | |||
'localeMap' => $this->getLocaleMap($user), | |||
'profileEnabledGlobally' => $this->profileManager->isProfileEnabled(), | |||
'profileEnabled' => $this->profileManager->isProfileEnabled($user), | |||
'organisation' => $this->getProperty($account, IAccountManager::PROPERTY_ORGANISATION), | |||
@@ -315,31 +315,24 @@ class PersonalInfo implements ISettings { | |||
); | |||
} | |||
private function getLocales(IUser $user): array { | |||
private function getLocaleMap(IUser $user): array { | |||
$forceLanguage = $this->config->getSystemValue('force_locale', false); | |||
if ($forceLanguage !== false) { | |||
return []; | |||
} | |||
$uid = $user->getUID(); | |||
$userLocaleString = $this->config->getUserValue($uid, 'core', 'locale', $this->l10nFactory->findLocale()); | |||
$userLang = $this->config->getUserValue($uid, 'core', 'lang', $this->l10nFactory->findLanguage()); | |||
$localeCodes = $this->l10nFactory->findAvailableLocales(); | |||
$userLocale = array_filter($localeCodes, function ($value) use ($userLocaleString) { | |||
return $userLocaleString === $value['code']; | |||
}); | |||
$userLocale = array_filter($localeCodes, fn ($value) => $userLocaleString === $value['code']); | |||
if (!empty($userLocale)) { | |||
$userLocale = reset($userLocale); | |||
} | |||
$localesForLanguage = array_filter($localeCodes, function ($localeCode) use ($userLang) { | |||
return 0 === strpos($localeCode['code'], $userLang); | |||
}); | |||
$localesForLanguage = array_values(array_filter($localeCodes, fn ($localeCode) => strpos($localeCode['code'], $userLang) === 0)); | |||
$otherLocales = array_values(array_filter($localeCodes, fn ($localeCode) => strpos($localeCode['code'], $userLang) !== 0)); | |||
if (!$userLocale) { | |||
$userLocale = [ | |||
@@ -349,10 +342,10 @@ class PersonalInfo implements ISettings { | |||
} | |||
return [ | |||
'activelocaleLang' => $userLocaleString, | |||
'activelocale' => $userLocale, | |||
'locales' => $localeCodes, | |||
'activeLocaleLang' => $userLocaleString, | |||
'activeLocale' => $userLocale, | |||
'localesForLanguage' => $localesForLanguage, | |||
'otherLocales' => $otherLocales, | |||
]; | |||
} | |||
@@ -0,0 +1,208 @@ | |||
<!-- | |||
- @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="locale"> | |||
<select :id="inputId" | |||
:placeholder="t('settings', 'Locale')" | |||
@change="onLocaleChange"> | |||
<option v-for="currentLocale in localesForLanguage" | |||
:key="currentLocale.code" | |||
:selected="locale.code === currentLocale.code" | |||
:value="currentLocale.code"> | |||
{{ currentLocale.name }} | |||
</option> | |||
<option disabled> | |||
────────── | |||
</option> | |||
<option v-for="currentLocale in otherLocales" | |||
:key="currentLocale.code" | |||
:selected="locale.code === currentLocale.code" | |||
:value="currentLocale.code"> | |||
{{ currentLocale.name }} | |||
</option> | |||
</select> | |||
<div class="example"> | |||
<Web :size="20" /> | |||
<div class="example__text"> | |||
<p> | |||
<span>{{ example.date }}</span> | |||
<span>{{ example.time }}</span> | |||
</p> | |||
<p> | |||
{{ t('settings', 'Week starts on {firstDayOfWeek}', { firstDayOfWeek: this.example.firstDayOfWeek }) }} | |||
</p> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import { showError } from '@nextcloud/dialogs' | |||
import moment from '@nextcloud/moment' | |||
import Web from 'vue-material-design-icons/Web' | |||
import { ACCOUNT_SETTING_PROPERTY_ENUM } from '../../../constants/AccountPropertyConstants.js' | |||
import { savePrimaryAccountProperty } from '../../../service/PersonalInfo/PersonalInfoService.js' | |||
import { validateLocale } from '../../../utils/validate.js' | |||
import logger from '../../../logger.js' | |||
export default { | |||
name: 'Locale', | |||
components: { | |||
Web, | |||
}, | |||
props: { | |||
inputId: { | |||
type: String, | |||
default: null, | |||
}, | |||
locale: { | |||
type: Object, | |||
required: true, | |||
}, | |||
localesForLanguage: { | |||
type: Array, | |||
required: true, | |||
}, | |||
otherLocales: { | |||
type: Array, | |||
required: true, | |||
}, | |||
}, | |||
data() { | |||
return { | |||
initialLocale: this.locale, | |||
example: { | |||
date: moment().format('L'), | |||
time: moment().format('LTS'), | |||
firstDayOfWeek: window.dayNames[window.firstDay], | |||
}, | |||
} | |||
}, | |||
computed: { | |||
allLocales() { | |||
return Object.freeze( | |||
[...this.localesForLanguage, ...this.otherLocales] | |||
.reduce((acc, { code, name }) => ({ ...acc, [code]: name }), {}) | |||
) | |||
}, | |||
}, | |||
created() { | |||
setInterval(this.refreshExample, 1000) | |||
}, | |||
methods: { | |||
async onLocaleChange(e) { | |||
const locale = this.constructLocale(e.target.value) | |||
this.$emit('update:locale', locale) | |||
if (validateLocale(locale)) { | |||
await this.updateLocale(locale) | |||
} | |||
}, | |||
async updateLocale(locale) { | |||
try { | |||
const responseData = await savePrimaryAccountProperty(ACCOUNT_SETTING_PROPERTY_ENUM.LOCALE, locale.code) | |||
this.handleResponse({ | |||
locale, | |||
status: responseData.ocs?.meta?.status, | |||
}) | |||
this.reloadPage() | |||
} catch (e) { | |||
this.handleResponse({ | |||
errorMessage: t('settings', 'Unable to update locale'), | |||
error: e, | |||
}) | |||
} | |||
}, | |||
constructLocale(localeCode) { | |||
return { | |||
code: localeCode, | |||
name: this.allLocales[localeCode], | |||
} | |||
}, | |||
handleResponse({ locale, status, errorMessage, error }) { | |||
if (status === 'ok') { | |||
this.initialLocale = locale | |||
} else { | |||
this.$emit('update:locale', this.initialLocale) | |||
showError(errorMessage) | |||
logger.error(errorMessage, error) | |||
} | |||
}, | |||
refreshExample() { | |||
this.example = { | |||
date: moment().format('L'), | |||
time: moment().format('LTS'), | |||
firstDayOfWeek: window.dayNames[window.firstDay], | |||
} | |||
}, | |||
reloadPage() { | |||
location.reload() | |||
}, | |||
}, | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
.locale { | |||
display: grid; | |||
select { | |||
width: 100%; | |||
height: 34px; | |||
margin: 3px 3px 3px 0; | |||
padding: 6px 16px; | |||
color: var(--color-main-text); | |||
border: 1px solid var(--color-border-dark); | |||
border-radius: var(--border-radius); | |||
background: var(--icon-triangle-s-dark) no-repeat right 4px center; | |||
font-family: var(--font-face); | |||
appearance: none; | |||
cursor: pointer; | |||
} | |||
} | |||
.example { | |||
margin: 10px 0; | |||
display: flex; | |||
gap: 0 10px; | |||
color: var(--color-text-lighter); | |||
&::v-deep .material-design-icon { | |||
align-self: flex-start; | |||
margin-top: 2px; | |||
} | |||
} | |||
</style> |
@@ -0,0 +1,88 @@ | |||
<!-- | |||
- @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> | |||
<HeaderBar :input-id="inputId" | |||
:readable="propertyReadable" /> | |||
<template v-if="isEditable"> | |||
<Locale :input-id="inputId" | |||
:locales-for-language="localesForLanguage" | |||
:other-locales="otherLocales" | |||
:locale.sync="locale" /> | |||
</template> | |||
<span v-else> | |||
{{ t('settings', 'No locale set') }} | |||
</span> | |||
</section> | |||
</template> | |||
<script> | |||
import { loadState } from '@nextcloud/initial-state' | |||
import Locale from './Locale.vue' | |||
import HeaderBar from '../shared/HeaderBar.vue' | |||
import { ACCOUNT_SETTING_PROPERTY_ENUM, ACCOUNT_SETTING_PROPERTY_READABLE_ENUM } from '../../../constants/AccountPropertyConstants.js' | |||
const { localeMap: { activeLocale, localesForLanguage, otherLocales } } = loadState('settings', 'personalInfoParameters', {}) | |||
export default { | |||
name: 'LocaleSection', | |||
components: { | |||
Locale, | |||
HeaderBar, | |||
}, | |||
data() { | |||
return { | |||
propertyReadable: ACCOUNT_SETTING_PROPERTY_READABLE_ENUM.LOCALE, | |||
localesForLanguage, | |||
otherLocales, | |||
locale: activeLocale, | |||
} | |||
}, | |||
computed: { | |||
inputId() { | |||
return `account-setting-${ACCOUNT_SETTING_PROPERTY_ENUM.LOCALE}` | |||
}, | |||
isEditable() { | |||
return Boolean(this.locale) | |||
}, | |||
}, | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
section { | |||
padding: 10px 10px; | |||
&::v-deep button:disabled { | |||
cursor: default; | |||
} | |||
} | |||
</style> |
@@ -106,11 +106,13 @@ export const PROPERTY_READABLE_KEYS_ENUM = Object.freeze({ | |||
*/ | |||
export const ACCOUNT_SETTING_PROPERTY_ENUM = Object.freeze({ | |||
LANGUAGE: 'language', | |||
LOCALE: 'locale', | |||
}) | |||
/** Enum of account setting properties to human readable setting properties */ | |||
export const ACCOUNT_SETTING_PROPERTY_READABLE_ENUM = Object.freeze({ | |||
LANGUAGE: t('settings', 'Language'), | |||
LOCALE: t('settings', 'Locale'), | |||
}) | |||
/** Enum of scopes */ |
@@ -35,6 +35,7 @@ import LocationSection from './components/PersonalInfo/LocationSection.vue' | |||
import WebsiteSection from './components/PersonalInfo/WebsiteSection.vue' | |||
import TwitterSection from './components/PersonalInfo/TwitterSection.vue' | |||
import LanguageSection from './components/PersonalInfo/LanguageSection/LanguageSection.vue' | |||
import LocaleSection from './components/PersonalInfo/LocaleSection/LocaleSection.vue' | |||
import ProfileSection from './components/PersonalInfo/ProfileSection/ProfileSection.vue' | |||
import OrganisationSection from './components/PersonalInfo/OrganisationSection.vue' | |||
import RoleSection from './components/PersonalInfo/RoleSection.vue' | |||
@@ -61,6 +62,7 @@ const LocationView = Vue.extend(LocationSection) | |||
const WebsiteView = Vue.extend(WebsiteSection) | |||
const TwitterView = Vue.extend(TwitterSection) | |||
const LanguageView = Vue.extend(LanguageSection) | |||
const LocaleView = Vue.extend(LocaleSection) | |||
new AvatarView().$mount('#vue-avatar-section') | |||
new DetailsView().$mount('#vue-details-section') | |||
@@ -71,6 +73,7 @@ new LocationView().$mount('#vue-location-section') | |||
new WebsiteView().$mount('#vue-website-section') | |||
new TwitterView().$mount('#vue-twitter-section') | |||
new LanguageView().$mount('#vue-language-section') | |||
new LocaleView().$mount('#vue-locale-section') | |||
if (profileEnabledGlobally) { | |||
const ProfileView = Vue.extend(ProfileSection) |
@@ -74,6 +74,18 @@ export function validateLanguage(input) { | |||
&& input.name !== undefined | |||
} | |||
/** | |||
* Validate the locale input | |||
* | |||
* @param {object} input the input | |||
* @return {boolean} | |||
*/ | |||
export function validateLocale(input) { | |||
return input.code !== '' | |||
&& input.name !== '' | |||
&& input.name !== undefined | |||
} | |||
/** | |||
* Validate boolean input | |||
* |
@@ -98,39 +98,7 @@ script('settings', [ | |||
<div id="vue-language-section"></div> | |||
</div> | |||
<div class="personal-settings-setting-box personal-settings-locale-box"> | |||
<?php if (isset($_['activelocale'])) { ?> | |||
<form id="locale" class="section"> | |||
<h3> | |||
<label for="localeinput"><?php p($l->t('Locale')); ?></label> | |||
</h3> | |||
<select id="localeinput" name="lang" data-placeholder="<?php p($l->t('Locale')); ?>"> | |||
<option value="<?php p($_['activelocale']['code']); ?>"> | |||
<?php p($l->t($_['activelocale']['name'])); ?> | |||
</option> | |||
<optgroup label="––––––––––"></optgroup> | |||
<?php foreach ($_['localesForLanguage'] as $locale) : ?> | |||
<option value="<?php p($locale['code']); ?>"> | |||
<?php p($l->t($locale['name'])); ?> | |||
</option> | |||
<?php endforeach; ?> | |||
<optgroup label="––––––––––"></optgroup> | |||
<option value="<?php p($_['activelocale']['code']); ?>"> | |||
<?php p($l->t($_['activelocale']['name'])); ?> | |||
</option> | |||
<?php foreach ($_['locales'] as $locale) : ?> | |||
<option value="<?php p($locale['code']); ?>"> | |||
<?php p($l->t($locale['name'])); ?> | |||
</option> | |||
<?php endforeach; ?> | |||
</select> | |||
<div id="localeexample" class="personal-info icon-timezone"> | |||
<p> | |||
<span id="localeexample-date"></span> <span id="localeexample-time"></span> | |||
</p> | |||
<p id="localeexample-fdow"></p> | |||
</div> | |||
</form> | |||
<?php } ?> | |||
<div id="vue-locale-section"></div> | |||
</div> | |||
<span class="msg"></span> | |||
</div> |