mirror of
https://github.com/nextcloud/server.git
synced 2026-06-10 17:23:59 -04:00
Merge pull request #54470 from nextcloud/feat/central-timezone-setting
feat: allow to set your local timezone in settings and provide it to clients
This commit is contained in:
commit
2fb1cfeb10
32 changed files with 359 additions and 117 deletions
|
|
@ -4,45 +4,36 @@
|
|||
-->
|
||||
<template>
|
||||
<div>
|
||||
<div class="time-zone">
|
||||
<label :for="`vs${timeZonePickerId}__combobox`" class="time-zone__heading">
|
||||
{{ $t('dav', 'Time zone:') }}
|
||||
</label>
|
||||
<span class="time-zone-text">
|
||||
<NcTimezonePicker v-model="timezone" :uid="timeZonePickerId" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<CalendarAvailability :slots.sync="slots"
|
||||
:loading="loading"
|
||||
:l10n-to="$t('dav', 'to')"
|
||||
:l10n-delete-slot="$t('dav', 'Delete slot')"
|
||||
:l10n-empty-day="$t('dav', 'No working hours set')"
|
||||
:l10n-add-slot="$t('dav', 'Add slot')"
|
||||
:l10n-week-day-list-label="$t('dav', 'Weekdays')"
|
||||
:l10n-monday="$t('dav', 'Monday')"
|
||||
:l10n-tuesday="$t('dav', 'Tuesday')"
|
||||
:l10n-wednesday="$t('dav', 'Wednesday')"
|
||||
:l10n-thursday="$t('dav', 'Thursday')"
|
||||
:l10n-friday="$t('dav', 'Friday')"
|
||||
:l10n-saturday="$t('dav', 'Saturday')"
|
||||
:l10n-sunday="$t('dav', 'Sunday')"
|
||||
:l10n-start-picker-label="(dayName) => $t('dav', 'Pick a start time for {dayName}', { dayName })"
|
||||
:l10n-end-picker-label="(dayName) => $t('dav', 'Pick a end time for {dayName}', { dayName })" />
|
||||
:l10n-to="t('dav', 'to')"
|
||||
:l10n-delete-slot="t('dav', 'Delete slot')"
|
||||
:l10n-empty-day="t('dav', 'No working hours set')"
|
||||
:l10n-add-slot="t('dav', 'Add slot')"
|
||||
:l10n-week-day-list-label="t('dav', 'Weekdays')"
|
||||
:l10n-monday="t('dav', 'Monday')"
|
||||
:l10n-tuesday="t('dav', 'Tuesday')"
|
||||
:l10n-wednesday="t('dav', 'Wednesday')"
|
||||
:l10n-thursday="t('dav', 'Thursday')"
|
||||
:l10n-friday="t('dav', 'Friday')"
|
||||
:l10n-saturday="t('dav', 'Saturday')"
|
||||
:l10n-sunday="t('dav', 'Sunday')"
|
||||
:l10n-start-picker-label="(dayName) => t('dav', 'Pick a start time for {dayName}', { dayName })"
|
||||
:l10n-end-picker-label="(dayName) => t('dav', 'Pick a end time for {dayName}', { dayName })" />
|
||||
|
||||
<NcCheckboxRadioSwitch :checked.sync="automated">
|
||||
{{ $t('dav', 'Automatically set user status to "Do not disturb" outside of availability to mute all notifications.') }}
|
||||
<NcCheckboxRadioSwitch v-model="automated">
|
||||
{{ t('dav', 'Automatically set user status to "Do not disturb" outside of availability to mute all notifications.') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<NcButton :disabled="loading || saving"
|
||||
type="primary"
|
||||
variant="primary"
|
||||
@click="save">
|
||||
{{ $t('dav', 'Save') }}
|
||||
{{ t('dav', 'Save') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { CalendarAvailability } from '@nextcloud/calendar-availability-vue'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import {
|
||||
|
|
@ -60,77 +51,57 @@ import {
|
|||
} from '../service/PreferenceService.js'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||
import NcTimezonePicker from '@nextcloud/vue/components/NcTimezonePicker'
|
||||
import { getCapabilities } from '@nextcloud/capabilities'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import logger from '../service/logger.js'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
|
||||
export default {
|
||||
name: 'AvailabilityForm',
|
||||
components: {
|
||||
NcButton,
|
||||
NcCheckboxRadioSwitch,
|
||||
CalendarAvailability,
|
||||
NcTimezonePicker,
|
||||
},
|
||||
data() {
|
||||
// Try to determine the current timezone, and fall back to UTC otherwise
|
||||
const defaultTimezoneId = (new Intl.DateTimeFormat())?.resolvedOptions()?.timeZone ?? 'UTC'
|
||||
// @ts-expect-error capabilities is missing the capability to type it...
|
||||
const timezone = getCapabilities().core.user?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
|
||||
return {
|
||||
loading: true,
|
||||
saving: false,
|
||||
timezone: defaultTimezoneId,
|
||||
slots: getEmptySlots(),
|
||||
automated: loadState('dav', 'user_status_automation') === 'yes',
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const slots = ref(getEmptySlots())
|
||||
const automated = ref(loadState('dav', 'user_status_automation') === 'yes')
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const slotData = await findScheduleInboxAvailability()
|
||||
if (!slotData) {
|
||||
logger.debug('no availability is set')
|
||||
} else {
|
||||
slots.value = slotData.slots
|
||||
logger.debug('availability loaded', { slots: slots.value })
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
timeZonePickerId() {
|
||||
return `tz-${(Math.random() + 1).toString(36).substring(7)}`
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
try {
|
||||
const slotData = await findScheduleInboxAvailability()
|
||||
if (!slotData) {
|
||||
console.info('no availability is set')
|
||||
this.slots = getEmptySlots()
|
||||
} else {
|
||||
const { slots, timezoneId } = slotData
|
||||
this.slots = slots
|
||||
if (timezoneId) {
|
||||
this.timezone = timezoneId
|
||||
}
|
||||
console.info('availability loaded', this.slots, this.timezoneId)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('could not load existing availability', e)
|
||||
} catch (error) {
|
||||
logger.error('could not load existing availability', { error })
|
||||
showError(t('dav', 'Failed to load availability'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
showError(t('dav', 'Failed to load availability'))
|
||||
} finally {
|
||||
this.loading = false
|
||||
/**
|
||||
* Save current slots on the server
|
||||
*/
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
await saveScheduleInboxAvailability(slots.value, timezone)
|
||||
if (automated.value) {
|
||||
await enableUserStatusAutomation()
|
||||
} else {
|
||||
await disableUserStatusAutomation()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async save() {
|
||||
try {
|
||||
this.saving = true
|
||||
|
||||
await saveScheduleInboxAvailability(this.slots, this.timezone)
|
||||
if (this.automated) {
|
||||
await enableUserStatusAutomation()
|
||||
} else {
|
||||
await disableUserStatusAutomation()
|
||||
}
|
||||
showSuccess(t('dav', 'Saved availability'))
|
||||
} catch (e) {
|
||||
console.error('could not save availability', e)
|
||||
|
||||
showSuccess(t('dav', 'Saved availability'))
|
||||
} catch (e) {
|
||||
console.error('could not save availability', e)
|
||||
|
||||
showError(t('dav', 'Failed to save availability'))
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
},
|
||||
showError(t('dav', 'Failed to save availability'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -165,11 +136,6 @@ export default {
|
|||
width: 97px;
|
||||
}
|
||||
|
||||
:deep(.multiselect) {
|
||||
border: 1px solid var(--color-border-dark);
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.time-zone {
|
||||
padding-block: 32px 12px;
|
||||
padding-inline: 0 12px;
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ abstract class AUserDataOCSController extends OCSController {
|
|||
public const USER_FIELD_DISPLAYNAME = 'display';
|
||||
public const USER_FIELD_LANGUAGE = 'language';
|
||||
public const USER_FIELD_LOCALE = 'locale';
|
||||
public const USER_FIELD_TIMEZONE = 'timezone';
|
||||
public const USER_FIELD_FIRST_DAY_OF_WEEK = 'first_day_of_week';
|
||||
public const USER_FIELD_PASSWORD = 'password';
|
||||
public const USER_FIELD_QUOTA = 'quota';
|
||||
|
|
@ -187,6 +188,7 @@ abstract class AUserDataOCSController extends OCSController {
|
|||
$data['groups'] = $gids;
|
||||
$data[self::USER_FIELD_LANGUAGE] = $this->l10nFactory->getUserLanguage($targetUserObject);
|
||||
$data[self::USER_FIELD_LOCALE] = $this->config->getUserValue($targetUserObject->getUID(), 'core', 'locale');
|
||||
$data[self::USER_FIELD_TIMEZONE] = $this->config->getUserValue($targetUserObject->getUID(), 'core', 'timezone');
|
||||
$data[self::USER_FIELD_NOTIFICATION_EMAIL] = $targetUserObject->getPrimaryEMailAddress();
|
||||
|
||||
$backend = $targetUserObject->getBackend();
|
||||
|
|
|
|||
|
|
@ -954,6 +954,7 @@ class UsersController extends AUserDataOCSController {
|
|||
|
||||
$permittedFields[] = self::USER_FIELD_PASSWORD;
|
||||
$permittedFields[] = self::USER_FIELD_NOTIFICATION_EMAIL;
|
||||
$permittedFields[] = self::USER_FIELD_TIMEZONE;
|
||||
if (
|
||||
$this->config->getSystemValue('force_language', false) === false
|
||||
|| $this->groupManager->isAdmin($currentLoggedInUser->getUID())
|
||||
|
|
@ -1028,6 +1029,7 @@ class UsersController extends AUserDataOCSController {
|
|||
$permittedFields[] = self::USER_FIELD_PASSWORD;
|
||||
$permittedFields[] = self::USER_FIELD_LANGUAGE;
|
||||
$permittedFields[] = self::USER_FIELD_LOCALE;
|
||||
$permittedFields[] = self::USER_FIELD_TIMEZONE;
|
||||
$permittedFields[] = self::USER_FIELD_FIRST_DAY_OF_WEEK;
|
||||
$permittedFields[] = IAccountManager::PROPERTY_PHONE;
|
||||
$permittedFields[] = IAccountManager::PROPERTY_ADDRESS;
|
||||
|
|
@ -1122,6 +1124,12 @@ class UsersController extends AUserDataOCSController {
|
|||
}
|
||||
$this->config->setUserValue($targetUser->getUID(), 'core', 'locale', $value);
|
||||
break;
|
||||
case self::USER_FIELD_TIMEZONE:
|
||||
if (!in_array($value, \DateTimeZone::listIdentifiers())) {
|
||||
throw new OCSException($this->l10n->t('Invalid timezone'), 101);
|
||||
}
|
||||
$this->config->setUserValue($targetUser->getUID(), 'core', 'timezone', $value);
|
||||
break;
|
||||
case self::USER_FIELD_FIRST_DAY_OF_WEEK:
|
||||
$intValue = (int)$value;
|
||||
if ($intValue < -1 || $intValue > 6) {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ namespace OCA\Provisioning_API;
|
|||
* roleScope?: Provisioning_APIUserDetailsScope,
|
||||
* storageLocation?: string,
|
||||
* subadmin: list<string>,
|
||||
* timezone: string,
|
||||
* twitter: string,
|
||||
* twitterScope?: Provisioning_APIUserDetailsScope,
|
||||
* bluesky: string,
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@
|
|||
"quota",
|
||||
"role",
|
||||
"subadmin",
|
||||
"timezone",
|
||||
"twitter",
|
||||
"bluesky",
|
||||
"website"
|
||||
|
|
@ -262,6 +263,9 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
},
|
||||
"twitter": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@
|
|||
"quota",
|
||||
"role",
|
||||
"subadmin",
|
||||
"timezone",
|
||||
"twitter",
|
||||
"bluesky",
|
||||
"website"
|
||||
|
|
@ -309,6 +310,9 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
},
|
||||
"twitter": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@
|
|||
"quota",
|
||||
"role",
|
||||
"subadmin",
|
||||
"timezone",
|
||||
"twitter",
|
||||
"bluesky",
|
||||
"website"
|
||||
|
|
@ -309,6 +310,9 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
},
|
||||
"twitter": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1225,6 +1225,7 @@ class UsersControllerTest extends TestCase {
|
|||
'groups' => ['group0', 'group1', 'group2'],
|
||||
'language' => 'de',
|
||||
'locale' => null,
|
||||
'timezone' => null,
|
||||
'backendCapabilities' => [
|
||||
'setDisplayName' => true,
|
||||
'setPassword' => true,
|
||||
|
|
@ -1372,6 +1373,7 @@ class UsersControllerTest extends TestCase {
|
|||
'groups' => [],
|
||||
'language' => 'da',
|
||||
'locale' => null,
|
||||
'timezone' => null,
|
||||
'backendCapabilities' => [
|
||||
'setDisplayName' => true,
|
||||
'setPassword' => true,
|
||||
|
|
@ -1557,6 +1559,7 @@ class UsersControllerTest extends TestCase {
|
|||
'groups' => [],
|
||||
'language' => 'ru',
|
||||
'locale' => null,
|
||||
'timezone' => null,
|
||||
'backendCapabilities' => [
|
||||
'setDisplayName' => false,
|
||||
'setPassword' => false,
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ class PersonalInfo implements ISettings {
|
|||
'biography' => $this->getProperty($account, IAccountManager::PROPERTY_BIOGRAPHY),
|
||||
'birthdate' => $this->getProperty($account, IAccountManager::PROPERTY_BIRTHDATE),
|
||||
'firstDayOfWeek' => $this->config->getUserValue($uid, 'core', AUserDataOCSController::USER_FIELD_FIRST_DAY_OF_WEEK),
|
||||
'timezone' => $this->config->getUserValue($uid, 'core', 'timezone', ''),
|
||||
'pronouns' => $this->getProperty($account, IAccountManager::PROPERTY_PRONOUNS),
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
<!--
|
||||
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { ref, watch } from 'vue'
|
||||
import NcTimezonePicker from '@nextcloud/vue/components/NcTimezonePicker'
|
||||
import HeaderBar from './shared/HeaderBar.vue'
|
||||
import { savePrimaryAccountProperty } from '../../service/PersonalInfo/PersonalInfoService.js'
|
||||
|
||||
const { timezone: currentTimezone } = loadState<{ timezone: string }>('settings', 'personalInfoParameters')
|
||||
|
||||
const inputId = 'account-property-timezone'
|
||||
const timezone = ref(currentTimezone)
|
||||
watch(timezone, () => {
|
||||
savePrimaryAccountProperty('timezone', timezone.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="timezone-section">
|
||||
<HeaderBar :input-id="inputId"
|
||||
:readable="t('settings', 'Timezone')" />
|
||||
|
||||
<NcTimezonePicker v-model="timezone"
|
||||
class="timezone-section__picker"
|
||||
:input-id="inputId" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.timezone-section {
|
||||
padding: 10px;
|
||||
|
||||
&__picker {
|
||||
margin-top: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -108,6 +108,7 @@ export const ACCOUNT_SETTING_PROPERTY_ENUM = Object.freeze({
|
|||
LANGUAGE: 'language',
|
||||
LOCALE: 'locale',
|
||||
FIRST_DAY_OF_WEEK: 'first_day_of_week',
|
||||
TIMEZONE: 'timezone',
|
||||
})
|
||||
|
||||
/** Enum of account setting properties to human readable setting properties */
|
||||
|
|
@ -115,6 +116,7 @@ export const ACCOUNT_SETTING_PROPERTY_READABLE_ENUM = Object.freeze({
|
|||
LANGUAGE: t('settings', 'Language'),
|
||||
LOCALE: t('settings', 'Locale'),
|
||||
FIRST_DAY_OF_WEEK: t('settings', 'First day of week'),
|
||||
TIMEZONE: t('settings', 'timezone'),
|
||||
})
|
||||
|
||||
/** Enum of scopes */
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import ProfileSection from './components/PersonalInfo/ProfileSection/ProfileSect
|
|||
import ProfileVisibilitySection from './components/PersonalInfo/ProfileVisibilitySection/ProfileVisibilitySection.vue'
|
||||
import PronounsSection from './components/PersonalInfo/PronounsSection.vue'
|
||||
import RoleSection from './components/PersonalInfo/RoleSection.vue'
|
||||
import TimezoneSection from './components/PersonalInfo/TimezoneSection.vue'
|
||||
import TwitterSection from './components/PersonalInfo/TwitterSection.vue'
|
||||
import BlueskySection from './components/PersonalInfo/BlueskySection.vue'
|
||||
import WebsiteSection from './components/PersonalInfo/WebsiteSection.vue'
|
||||
|
|
@ -47,6 +48,7 @@ const DisplayNameView = Vue.extend(DisplayNameSection)
|
|||
const EmailView = Vue.extend(EmailSection)
|
||||
const FediverseView = Vue.extend(FediverseSection)
|
||||
const FirstDayOfWeekView = Vue.extend(FirstDayOfWeekSection)
|
||||
const TimezoneView = Vue.extend(TimezoneSection)
|
||||
const LanguageView = Vue.extend(LanguageSection)
|
||||
const LocaleView = Vue.extend(LocaleSection)
|
||||
const LocationView = Vue.extend(LocationSection)
|
||||
|
|
@ -69,6 +71,7 @@ new FediverseView().$mount('#vue-fediverse-section')
|
|||
new LanguageView().$mount('#vue-language-section')
|
||||
new LocaleView().$mount('#vue-locale-section')
|
||||
new FirstDayOfWeekView().$mount('#vue-fdow-section')
|
||||
new TimezoneView().$mount('#vue-timezone-section')
|
||||
new BirthdayView().$mount('#vue-birthday-section')
|
||||
new PronounsView().$mount('#vue-pronouns-section')
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,9 @@ script('settings', [
|
|||
<div class="personal-settings-setting-box">
|
||||
<div id="vue-fdow-section"></div>
|
||||
</div>
|
||||
<div class="personal-settings-setting-box">
|
||||
<div id="vue-timezone-section"></div>
|
||||
</div>
|
||||
<div class="personal-settings-setting-box">
|
||||
<div id="vue-website-section"></div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ class Application extends App implements IBootstrap {
|
|||
|
||||
// config lexicon
|
||||
$context->registerConfigLexicon(ConfigLexicon::class);
|
||||
|
||||
$context->registerCapability(Capabilities::class);
|
||||
}
|
||||
|
||||
public function boot(IBootContext $context): void {
|
||||
|
|
|
|||
49
core/AppInfo/Capabilities.php
Normal file
49
core/AppInfo/Capabilities.php
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OC\Core\AppInfo;
|
||||
|
||||
use OCP\Capabilities\ICapability;
|
||||
use OCP\Config\IUserConfig;
|
||||
use OCP\IDateTimeZone;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IUserSession;
|
||||
|
||||
class Capabilities implements ICapability {
|
||||
|
||||
public function __construct(
|
||||
private IUserSession $session,
|
||||
private IUserConfig $userConfig,
|
||||
private IGroupManager $groupManager,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the core capabilities
|
||||
*
|
||||
* @return array{core: array{'user'?: array{language: string, locale: string, timezone: string} } }
|
||||
*/
|
||||
public function getCapabilities(): array {
|
||||
$capabilities = [];
|
||||
|
||||
$user = $this->session->getUser();
|
||||
if ($user !== null) {
|
||||
$timezone = \OCP\Server::get(IDateTimeZone::class)->getTimeZone();
|
||||
|
||||
$capabilities['user'] = [
|
||||
'language' => $this->userConfig->getValueString($user->getUID(), Application::APP_ID, ConfigLexicon::USER_LANGUAGE),
|
||||
'locale' => $this->userConfig->getValueString($user->getUID(), Application::APP_ID, ConfigLexicon::USER_LOCALE),
|
||||
'timezone' => $timezone->getName(),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'core' => $capabilities,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,9 @@ class ConfigLexicon implements ILexicon {
|
|||
public const SHARE_LINK_PASSWORD_ENFORCED = 'shareapi_enforce_links_password';
|
||||
|
||||
public const USER_LANGUAGE = 'lang';
|
||||
public const USER_LOCALE = 'locale';
|
||||
public const USER_TIMEZONE = 'timezone';
|
||||
|
||||
public const LASTCRON_TIMESTAMP = 'lastcron';
|
||||
|
||||
public function getStrictness(): Strictness {
|
||||
|
|
@ -68,7 +71,9 @@ class ConfigLexicon implements ILexicon {
|
|||
|
||||
public function getUserConfigs(): array {
|
||||
return [
|
||||
new Entry(self::USER_LANGUAGE, ValueType::STRING, null, 'language'),
|
||||
new Entry(self::USER_LANGUAGE, ValueType::STRING, definition: 'language'),
|
||||
new Entry(self::USER_LOCALE, ValueType::STRING, definition: 'locale'),
|
||||
new Entry(self::USER_TIMEZONE, ValueType::STRING, definition: 'timezone'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,25 @@
|
|||
},
|
||||
"mod-rewrite-working": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"user": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"language",
|
||||
"locale",
|
||||
"timezone"
|
||||
],
|
||||
"properties": {
|
||||
"language": {
|
||||
"type": "string"
|
||||
},
|
||||
"locale": {
|
||||
"type": "string"
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,25 @@
|
|||
},
|
||||
"mod-rewrite-working": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"user": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"language",
|
||||
"locale",
|
||||
"timezone"
|
||||
],
|
||||
"properties": {
|
||||
"language": {
|
||||
"type": "string"
|
||||
},
|
||||
"locale": {
|
||||
"type": "string"
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,6 +117,25 @@
|
|||
},
|
||||
"mod-rewrite-working": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"user": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"language",
|
||||
"locale",
|
||||
"timezone"
|
||||
],
|
||||
"properties": {
|
||||
"language": {
|
||||
"type": "string"
|
||||
},
|
||||
"locale": {
|
||||
"type": "string"
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,6 +117,25 @@
|
|||
},
|
||||
"mod-rewrite-working": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"user": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"language",
|
||||
"locale",
|
||||
"timezone"
|
||||
],
|
||||
"properties": {
|
||||
"language": {
|
||||
"type": "string"
|
||||
},
|
||||
"locale": {
|
||||
"type": "string"
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
dist/dav-settings-personal-availability.js
vendored
4
dist/dav-settings-personal-availability.js
vendored
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
4
dist/settings-vue-settings-personal-info.js
vendored
4
dist/settings-vue-settings-personal-info.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1,3 +1,4 @@
|
|||
SPDX-License-Identifier: MPL-2.0
|
||||
SPDX-License-Identifier: MIT
|
||||
SPDX-License-Identifier: ISC
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
|
@ -17,6 +18,7 @@ SPDX-FileCopyrightText: T. Jameson Little <t.jameson.little@gmail.com>
|
|||
SPDX-FileCopyrightText: Roman Shtylman <shtylman@gmail.com>
|
||||
SPDX-FileCopyrightText: Roeland Jago Douma
|
||||
SPDX-FileCopyrightText: Rob Cresswell <robcresswell@pm.me>
|
||||
SPDX-FileCopyrightText: Philipp Kewisch
|
||||
SPDX-FileCopyrightText: Paul Vorbach <paul@vorba.ch> (http://paul.vorba.ch)
|
||||
SPDX-FileCopyrightText: Paul Vorbach <paul@vorb.de> (http://vorb.de)
|
||||
SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
|
||||
|
|
@ -145,6 +147,9 @@ This file is generated from multiple sources. Included packages:
|
|||
- focus-trap
|
||||
- version: 7.6.5
|
||||
- license: MIT
|
||||
- ical.js
|
||||
- version: 2.1.0
|
||||
- license: MPL-2.0
|
||||
- ieee754
|
||||
- version: 1.2.1
|
||||
- license: BSD-3-Clause
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1237,6 +1237,7 @@ return array(
|
|||
'OC\\Contacts\\ContactsMenu\\Providers\\ProfileProvider' => $baseDir . '/lib/private/Contacts/ContactsMenu/Providers/ProfileProvider.php',
|
||||
'OC\\ContextChat\\ContentManager' => $baseDir . '/lib/private/ContextChat/ContentManager.php',
|
||||
'OC\\Core\\AppInfo\\Application' => $baseDir . '/core/AppInfo/Application.php',
|
||||
'OC\\Core\\AppInfo\\Capabilities' => $baseDir . '/core/AppInfo/Capabilities.php',
|
||||
'OC\\Core\\AppInfo\\ConfigLexicon' => $baseDir . '/core/AppInfo/ConfigLexicon.php',
|
||||
'OC\\Core\\BackgroundJobs\\BackgroundCleanupUpdaterBackupsJob' => $baseDir . '/core/BackgroundJobs/BackgroundCleanupUpdaterBackupsJob.php',
|
||||
'OC\\Core\\BackgroundJobs\\CheckForUserCertificates' => $baseDir . '/core/BackgroundJobs/CheckForUserCertificates.php',
|
||||
|
|
|
|||
|
|
@ -1278,6 +1278,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
|
|||
'OC\\Contacts\\ContactsMenu\\Providers\\ProfileProvider' => __DIR__ . '/../../..' . '/lib/private/Contacts/ContactsMenu/Providers/ProfileProvider.php',
|
||||
'OC\\ContextChat\\ContentManager' => __DIR__ . '/../../..' . '/lib/private/ContextChat/ContentManager.php',
|
||||
'OC\\Core\\AppInfo\\Application' => __DIR__ . '/../../..' . '/core/AppInfo/Application.php',
|
||||
'OC\\Core\\AppInfo\\Capabilities' => __DIR__ . '/../../..' . '/core/AppInfo/Capabilities.php',
|
||||
'OC\\Core\\AppInfo\\ConfigLexicon' => __DIR__ . '/../../..' . '/core/AppInfo/ConfigLexicon.php',
|
||||
'OC\\Core\\BackgroundJobs\\BackgroundCleanupUpdaterBackupsJob' => __DIR__ . '/../../..' . '/core/BackgroundJobs/BackgroundCleanupUpdaterBackupsJob.php',
|
||||
'OC\\Core\\BackgroundJobs\\CheckForUserCertificates' => __DIR__ . '/../../..' . '/core/BackgroundJobs/CheckForUserCertificates.php',
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace OC\Authentication\Login;
|
||||
|
||||
use OC\Core\AppInfo\Application;
|
||||
use OC\Core\AppInfo\ConfigLexicon;
|
||||
use OCP\IConfig;
|
||||
use OCP\ISession;
|
||||
|
||||
|
|
@ -26,12 +28,10 @@ class SetUserTimezoneCommand extends ALoginCommand {
|
|||
|
||||
public function process(LoginData $loginData): LoginResult {
|
||||
if ($loginData->getTimeZoneOffset() !== '' && $this->isValidTimezone($loginData->getTimeZone())) {
|
||||
$this->config->setUserValue(
|
||||
$loginData->getUser()->getUID(),
|
||||
'core',
|
||||
'timezone',
|
||||
$loginData->getTimeZone()
|
||||
);
|
||||
$userId = $loginData->getUser()->getUID();
|
||||
if ($this->config->getUserValue($userId, Application::APP_ID, ConfigLexicon::USER_TIMEZONE, '') === '') {
|
||||
$this->config->setUserValue($userId, Application::APP_ID, ConfigLexicon::USER_TIMEZONE, $loginData->getTimeZone());
|
||||
}
|
||||
$this->session->set(
|
||||
'timezone',
|
||||
$loginData->getTimeZoneOffset()
|
||||
|
|
|
|||
23
openapi.json
23
openapi.json
|
|
@ -155,6 +155,25 @@
|
|||
},
|
||||
"mod-rewrite-working": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"user": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"language",
|
||||
"locale",
|
||||
"timezone"
|
||||
],
|
||||
"properties": {
|
||||
"language": {
|
||||
"type": "string"
|
||||
},
|
||||
"locale": {
|
||||
"type": "string"
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3564,6 +3583,7 @@
|
|||
"quota",
|
||||
"role",
|
||||
"subadmin",
|
||||
"timezone",
|
||||
"twitter",
|
||||
"bluesky",
|
||||
"website"
|
||||
|
|
@ -3721,6 +3741,9 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
},
|
||||
"twitter": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,11 +15,10 @@ use OCP\ISession;
|
|||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
||||
class SetUserTimezoneCommandTest extends ALoginTestCommand {
|
||||
/** @var IConfig|MockObject */
|
||||
private $config;
|
||||
|
||||
/** @var ISession|MockObject */
|
||||
private $session;
|
||||
private IConfig&MockObject $config;
|
||||
|
||||
private ISession&MockObject $session;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
|
@ -50,6 +49,15 @@ class SetUserTimezoneCommandTest extends ALoginTestCommand {
|
|||
$this->user->expects($this->once())
|
||||
->method('getUID')
|
||||
->willReturn($this->username);
|
||||
$this->config->expects($this->once())
|
||||
->method('getUserValue')
|
||||
->with(
|
||||
$this->username,
|
||||
'core',
|
||||
'timezone',
|
||||
''
|
||||
)
|
||||
->willReturn('');
|
||||
$this->config->expects($this->once())
|
||||
->method('setUserValue')
|
||||
->with(
|
||||
|
|
@ -69,4 +77,32 @@ class SetUserTimezoneCommandTest extends ALoginTestCommand {
|
|||
|
||||
$this->assertTrue($result->isSuccess());
|
||||
}
|
||||
|
||||
public function testProcessAlreadySet(): void {
|
||||
$data = $this->getLoggedInLoginDataWithTimezone();
|
||||
$this->user->expects($this->once())
|
||||
->method('getUID')
|
||||
->willReturn($this->username);
|
||||
$this->config->expects($this->once())
|
||||
->method('getUserValue')
|
||||
->with(
|
||||
$this->username,
|
||||
'core',
|
||||
'timezone',
|
||||
'',
|
||||
)
|
||||
->willReturn('Europe/Berlin');
|
||||
$this->config->expects($this->never())
|
||||
->method('setUserValue');
|
||||
$this->session->expects($this->once())
|
||||
->method('set')
|
||||
->with(
|
||||
'timezone',
|
||||
$this->timeZoneOffset
|
||||
);
|
||||
|
||||
$result = $this->cmd->process($data);
|
||||
|
||||
$this->assertTrue($result->isSuccess());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue