Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 69 additions & 103 deletions apps/dav/src/components/AvailabilityForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 })" />

<NcCheckboxRadioSwitch :checked.sync="automated">
{{ $t('dav', 'Automatically set user status to "Do not disturb" outside of availability to mute all notifications.') }}
: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 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 {
Expand All @@ -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'

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'

return {
loading: true,
saving: false,
timezone: defaultTimezoneId,
slots: getEmptySlots(),
automated: loadState('dav', 'user_status_automation') === 'yes',
import { getCapabilities } from '@nextcloud/capabilities'
import { onMounted, ref } from 'vue'
import logger from '../service/logger.js'
import { t } from '@nextcloud/l10n'

// @ts-expect-error capabilities is missing the capability to type it...
const timezone = getCapabilities().core.user?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone

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)

showError(t('dav', 'Failed to load availability'))
} finally {
this.loading = false
} catch (error) {
logger.error('could not load existing availability', { error })
showError(t('dav', 'Failed to load availability'))
} finally {
loading.value = 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)

showError(t('dav', 'Failed to save availability'))
} finally {
this.saving = false
}
},
},

showSuccess(t('dav', 'Saved availability'))
} catch (e) {
console.error('could not save availability', e)

showError(t('dav', 'Failed to save availability'))
} finally {
saving.value = false
}
}
</script>

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -187,6 +188,7 @@ protected function getUserData(string $userId, bool $includeScopes = false): ?ar
$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();
Expand Down
8 changes: 8 additions & 0 deletions apps/provisioning_api/lib/Controller/UsersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,7 @@ public function editUser(string $userId, string $key, string $value): DataRespon

$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())
Expand Down Expand Up @@ -1028,6 +1029,7 @@ public function editUser(string $userId, string $key, string $value): DataRespon
$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;
Expand Down Expand Up @@ -1122,6 +1124,12 @@ public function editUser(string $userId, string $key, string $value): DataRespon
}
$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) {
Expand Down
1 change: 1 addition & 0 deletions apps/provisioning_api/lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
* roleScope?: Provisioning_APIUserDetailsScope,
* storageLocation?: string,
* subadmin: list<string>,
* timezone: string,
* twitter: string,
* twitterScope?: Provisioning_APIUserDetailsScope,
* bluesky: string,
Expand Down
4 changes: 4 additions & 0 deletions apps/provisioning_api/openapi-administration.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"quota",
"role",
"subadmin",
"timezone",
"twitter",
"bluesky",
"website"
Expand Down Expand Up @@ -262,6 +263,9 @@
"type": "string"
}
},
"timezone": {
"type": "string"
},
"twitter": {
"type": "string"
},
Expand Down
4 changes: 4 additions & 0 deletions apps/provisioning_api/openapi-full.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
"quota",
"role",
"subadmin",
"timezone",
"twitter",
"bluesky",
"website"
Expand Down Expand Up @@ -309,6 +310,9 @@
"type": "string"
}
},
"timezone": {
"type": "string"
},
"twitter": {
"type": "string"
},
Expand Down
4 changes: 4 additions & 0 deletions apps/provisioning_api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@
"quota",
"role",
"subadmin",
"timezone",
"twitter",
"bluesky",
"website"
Expand Down Expand Up @@ -309,6 +310,9 @@
"type": "string"
}
},
"timezone": {
"type": "string"
},
"twitter": {
"type": "string"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1225,6 +1225,7 @@ public function testGetUserDataAsAdmin(): void {
'groups' => ['group0', 'group1', 'group2'],
'language' => 'de',
'locale' => null,
'timezone' => null,
'backendCapabilities' => [
'setDisplayName' => true,
'setPassword' => true,
Expand Down Expand Up @@ -1372,6 +1373,7 @@ public function testGetUserDataAsSubAdminAndUserIsAccessible(): void {
'groups' => [],
'language' => 'da',
'locale' => null,
'timezone' => null,
'backendCapabilities' => [
'setDisplayName' => true,
'setPassword' => true,
Expand Down Expand Up @@ -1557,6 +1559,7 @@ public function testGetUserDataAsSubAdminSelfLookup(): void {
'groups' => [],
'language' => 'ru',
'locale' => null,
'timezone' => null,
'backendCapabilities' => [
'setDisplayName' => false,
'setPassword' => false,
Expand Down
1 change: 1 addition & 0 deletions apps/settings/lib/Settings/Personal/PersonalInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ public function getForm(): TemplateResponse {
'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),
];

Expand Down
43 changes: 43 additions & 0 deletions apps/settings/src/components/PersonalInfo/TimezoneSection.vue
Original file line number Diff line number Diff line change
@@ -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>
Loading
Loading