mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat: add timezone support for schedule and user runtime (#282)
This commit is contained in:
@@ -75,6 +75,8 @@
|
||||
"userID": "User ID",
|
||||
"displayName": "Display Name",
|
||||
"avatarUrl": "Avatar URL",
|
||||
"timezone": "Timezone",
|
||||
"timezonePlaceholder": "e.g. America/New_York",
|
||||
"saveProfile": "Save Profile",
|
||||
"profileUpdated": "Profile updated",
|
||||
"profileUpdateFailed": "Failed to update profile",
|
||||
@@ -511,10 +513,18 @@
|
||||
"displayNamePlaceholder": "Give your bot a name",
|
||||
"avatarUrl": "Avatar URL",
|
||||
"avatarUrlPlaceholder": "Enter avatar image URL",
|
||||
"timezone": "Timezone",
|
||||
"timezonePlaceholder": "e.g. America/New_York",
|
||||
"timezoneInherited": "Inherited from user/system",
|
||||
"timezoneInheritedHint": "Leave empty to inherit the user's timezone, or fall back to the system timezone.",
|
||||
"editAvatar": "Edit Avatar",
|
||||
"editAvatarDescription": "Set the image URL for the bot avatar.",
|
||||
"editTimezone": "Edit Timezone",
|
||||
"editTimezoneDescription": "Set a timezone override for this bot.",
|
||||
"avatarUpdateSuccess": "Avatar updated",
|
||||
"avatarUpdateFailed": "Failed to update avatar",
|
||||
"timezoneUpdateSuccess": "Timezone updated",
|
||||
"timezoneUpdateFailed": "Failed to update timezone",
|
||||
"typePlaceholder": "Select bot type",
|
||||
"types": {
|
||||
"personal": "Personal",
|
||||
|
||||
@@ -75,6 +75,8 @@
|
||||
"userID": "用户 ID",
|
||||
"displayName": "显示名称",
|
||||
"avatarUrl": "头像链接",
|
||||
"timezone": "时区",
|
||||
"timezonePlaceholder": "例如 Asia/Tokyo",
|
||||
"saveProfile": "保存资料",
|
||||
"profileUpdated": "用户资料已更新",
|
||||
"profileUpdateFailed": "用户资料更新失败",
|
||||
@@ -400,7 +402,7 @@
|
||||
"locale": "语言区域",
|
||||
"localePlaceholder": "例如 zh-CN",
|
||||
"timezoneId": "时区",
|
||||
"timezonePlaceholder": "例如 Asia/Shanghai",
|
||||
"timezonePlaceholder": "例如 Asia/Tokyo",
|
||||
"ignoreHTTPSErrors": "忽略 HTTPS 错误",
|
||||
"core": "浏览器内核",
|
||||
"chromium": "Chromium",
|
||||
@@ -507,10 +509,18 @@
|
||||
"displayNamePlaceholder": "给你的 Bot 起个名字",
|
||||
"avatarUrl": "头像链接",
|
||||
"avatarUrlPlaceholder": "输入头像图片地址",
|
||||
"timezone": "时区",
|
||||
"timezonePlaceholder": "例如 Asia/Shanghai",
|
||||
"timezoneInherited": "继承用户或系统时区",
|
||||
"timezoneInheritedHint": "留空则继承用户时区,若用户未设置则回退到系统时区。",
|
||||
"editAvatar": "编辑头像",
|
||||
"editAvatarDescription": "设置 Bot 头像图片的链接地址",
|
||||
"editTimezone": "编辑时区",
|
||||
"editTimezoneDescription": "为这个 Bot 设置独立时区。",
|
||||
"avatarUpdateSuccess": "头像已更新",
|
||||
"avatarUpdateFailed": "更新头像失败",
|
||||
"timezoneUpdateSuccess": "时区已更新",
|
||||
"timezoneUpdateFailed": "更新时区失败",
|
||||
"typePlaceholder": "选择 Bot 类型",
|
||||
"types": {
|
||||
"personal": "个人",
|
||||
|
||||
@@ -57,6 +57,42 @@
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="timezone"
|
||||
>
|
||||
<FormItem>
|
||||
<Label class="mb-2">
|
||||
{{ $t('bots.timezone') }}
|
||||
<span class="text-muted-foreground text-xs ml-1">({{ $t('common.optional') }})</span>
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Select
|
||||
:model-value="componentField.modelValue || emptyTimezoneValue"
|
||||
@update:model-value="(value) => componentField['onUpdate:modelValue'](value === emptyTimezoneValue ? '' : value)"
|
||||
>
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue :placeholder="$t('bots.timezonePlaceholder')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem :value="emptyTimezoneValue">
|
||||
{{ $t('bots.timezoneInherited') }}
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
v-for="timezoneOption in timezones"
|
||||
:key="timezoneOption"
|
||||
:value="timezoneOption"
|
||||
>
|
||||
{{ timezoneOption }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<div class="rounded-md border bg-muted/40 px-3 py-2 text-sm text-muted-foreground">
|
||||
{{ $t('bots.createBotWaitHint') }}
|
||||
</div>
|
||||
@@ -98,6 +134,12 @@ import {
|
||||
FormItem,
|
||||
Separator,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Spinner,
|
||||
} from '@memohai/ui'
|
||||
import { useForm } from 'vee-validate'
|
||||
@@ -108,6 +150,7 @@ import { useMutation, useQueryCache } from '@pinia/colada'
|
||||
import { postBotsMutation, getBotsQueryKey } from '@memohai/sdk/colada'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDialogMutation } from '@/composables/useDialogMutation'
|
||||
import { emptyTimezoneValue, timezones } from '@/utils/timezones'
|
||||
|
||||
const open = defineModel<boolean>('open', { default: false })
|
||||
const { t } = useI18n()
|
||||
@@ -116,6 +159,7 @@ const { run } = useDialogMutation()
|
||||
const formSchema = toTypedSchema(z.object({
|
||||
display_name: z.string().min(1),
|
||||
avatar_url: z.string().optional(),
|
||||
timezone: z.string().optional(),
|
||||
}))
|
||||
|
||||
const form = useForm({
|
||||
@@ -123,6 +167,7 @@ const form = useForm({
|
||||
initialValues: {
|
||||
display_name: '',
|
||||
avatar_url: '',
|
||||
timezone: '',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -138,6 +183,7 @@ watch(open, (val) => {
|
||||
values: {
|
||||
display_name: '',
|
||||
avatar_url: '',
|
||||
timezone: '',
|
||||
},
|
||||
})
|
||||
} else {
|
||||
@@ -151,6 +197,7 @@ const handleSubmit = form.handleSubmit(async (values) => {
|
||||
body: {
|
||||
display_name: values.display_name,
|
||||
avatar_url: values.avatar_url || undefined,
|
||||
timezone: values.timezone || undefined,
|
||||
is_active: true,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -93,6 +93,22 @@
|
||||
</Badge>
|
||||
<span v-if="bot?.type">{{ botTypeLabel }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="bot"
|
||||
class="mt-1 flex items-center gap-2 text-sm text-muted-foreground"
|
||||
>
|
||||
<span>{{ $t('bots.timezone') }}: {{ bot.timezone || $t('bots.timezoneInherited') }}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 px-2"
|
||||
:disabled="botLifecyclePending"
|
||||
@click="handleEditTimezone"
|
||||
>
|
||||
{{ $t('common.edit') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
@@ -193,6 +209,67 @@
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="timezoneDialogOpen">
|
||||
<DialogContent class="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ $t('bots.editTimezone') }}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{{ $t('bots.editTimezoneDescription') }}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="mt-4 flex flex-col gap-2">
|
||||
<Select
|
||||
:model-value="timezoneDraft || emptyTimezoneValue"
|
||||
@update:model-value="(value) => timezoneDraft = value === emptyTimezoneValue ? '' : String(value)"
|
||||
>
|
||||
<SelectTrigger
|
||||
class="w-full"
|
||||
:disabled="timezoneSaving"
|
||||
>
|
||||
<SelectValue :placeholder="$t('bots.timezonePlaceholder')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem :value="emptyTimezoneValue">
|
||||
{{ $t('bots.timezoneInherited') }}
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
v-for="timezoneOption in timezones"
|
||||
:key="timezoneOption"
|
||||
:value="timezoneOption"
|
||||
>
|
||||
{{ timezoneOption }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ $t('bots.timezoneInheritedHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter class="mt-6">
|
||||
<DialogClose as-child>
|
||||
<Button
|
||||
variant="outline"
|
||||
:disabled="timezoneSaving"
|
||||
>
|
||||
{{ $t('common.cancel') }}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
:disabled="timezoneSaving || !canConfirmTimezone"
|
||||
@click="handleConfirmTimezone"
|
||||
>
|
||||
<Spinner
|
||||
v-if="timezoneSaving"
|
||||
class="mr-1.5"
|
||||
/>
|
||||
{{ $t('common.confirm') }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -211,6 +288,12 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Separator,
|
||||
Spinner,
|
||||
ScrollArea,
|
||||
@@ -255,6 +338,7 @@ import { useAvatarInitials } from '@/composables/useAvatarInitials'
|
||||
import { useSyncedQueryParam } from '@/composables/useSyncedQueryParam'
|
||||
import { useBotStatusMeta } from '@/composables/useBotStatusMeta'
|
||||
import MasterDetailSidebarLayout from '@/components/master-detail-sidebar-layout/index.vue'
|
||||
import { emptyTimezoneValue, timezones } from '@/utils/timezones'
|
||||
type BotCheck = BotsBotCheck
|
||||
type BotContainerInfo = HandlersGetContainerResponse
|
||||
type BotContainerSnapshot = HandlersListSnapshotsResponse extends { snapshots?: (infer T)[] } ? T : never
|
||||
@@ -344,9 +428,12 @@ watch(bot, (val) => {
|
||||
const activeTab = useSyncedQueryParam('tab', 'overview')
|
||||
const avatarDialogOpen = ref(false)
|
||||
const avatarUrlDraft = ref('')
|
||||
const timezoneDialogOpen = ref(false)
|
||||
const timezoneDraft = ref('')
|
||||
const avatarFallback = useAvatarInitials(() => bot.value?.display_name || botId.value || '')
|
||||
const isSavingBotName = computed(() => updateBotLoading.value)
|
||||
const avatarSaving = computed(() => updateBotLoading.value)
|
||||
const timezoneSaving = computed(() => updateBotLoading.value)
|
||||
const canConfirmAvatar = computed(() => {
|
||||
if (!bot.value) return false
|
||||
const next = avatarUrlDraft.value.trim()
|
||||
@@ -359,6 +446,10 @@ const canConfirmBotName = computed(() => {
|
||||
if (!nextName) return false
|
||||
return nextName !== (bot.value.display_name || '').trim()
|
||||
})
|
||||
const canConfirmTimezone = computed(() => {
|
||||
if (!bot.value) return false
|
||||
return timezoneDraft.value.trim() !== (bot.value.timezone || '').trim()
|
||||
})
|
||||
const {
|
||||
hasIssue,
|
||||
isPending: botLifecyclePending,
|
||||
@@ -412,6 +503,12 @@ function handleEditAvatar() {
|
||||
avatarDialogOpen.value = true
|
||||
}
|
||||
|
||||
function handleEditTimezone() {
|
||||
if (!bot.value || botLifecyclePending.value) return
|
||||
timezoneDraft.value = bot.value.timezone || ''
|
||||
timezoneDialogOpen.value = true
|
||||
}
|
||||
|
||||
async function handleConfirmAvatar() {
|
||||
if (!bot.value || !canConfirmAvatar.value || avatarSaving.value) return
|
||||
const nextUrl = avatarUrlDraft.value.trim()
|
||||
@@ -427,6 +524,21 @@ async function handleConfirmAvatar() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfirmTimezone() {
|
||||
if (!bot.value || !canConfirmTimezone.value || timezoneSaving.value) return
|
||||
const nextTimezone = timezoneDraft.value.trim()
|
||||
try {
|
||||
await updateBot({
|
||||
id: bot.value.id as string,
|
||||
timezone: nextTimezone,
|
||||
})
|
||||
timezoneDialogOpen.value = false
|
||||
toast.success(t('bots.timezoneUpdateSuccess'))
|
||||
} catch (error) {
|
||||
toast.error(resolveErrorMessage(error, t('bots.timezoneUpdateFailed')))
|
||||
}
|
||||
}
|
||||
|
||||
function handleStartEditBotName() {
|
||||
if (!bot.value) return
|
||||
isEditingBotName.value = true
|
||||
@@ -517,4 +629,4 @@ async function loadSnapshots() {
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -155,11 +155,28 @@
|
||||
<FormItem>
|
||||
<Label>{{ $t('browserContext.timezoneId') }}</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
:placeholder="$t('browserContext.timezonePlaceholder')"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
<Select
|
||||
:model-value="componentField.modelValue || emptyTimezoneValue"
|
||||
@update:model-value="(value) => componentField['onUpdate:modelValue'](value === emptyTimezoneValue ? '' : value)"
|
||||
>
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue :placeholder="$t('browserContext.timezonePlaceholder')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem :value="emptyTimezoneValue">
|
||||
{{ $t('common.optional') }}
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
v-for="timezoneOption in timezones"
|
||||
:key="timezoneOption"
|
||||
:value="timezoneOption"
|
||||
>
|
||||
{{ timezoneOption }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
@@ -232,6 +249,12 @@ import {
|
||||
Label,
|
||||
Separator,
|
||||
Button,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Switch,
|
||||
} from '@memohai/ui'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
@@ -247,6 +270,7 @@ import { toast } from 'vue-sonner'
|
||||
import { useDialogMutation } from '@/composables/useDialogMutation'
|
||||
import ConfirmPopover from '@/components/confirm-popover/index.vue'
|
||||
import { resolveApiErrorMessage } from '@/utils/api-error'
|
||||
import { emptyTimezoneValue, timezones } from '@/utils/timezones'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { run } = useDialogMutation()
|
||||
|
||||
@@ -187,6 +187,7 @@ const login = form.handleSubmit(async (values) => {
|
||||
displayName: data.display_name ?? '',
|
||||
role: data.role ?? '',
|
||||
avatarUrl: data.avatar_url ?? '',
|
||||
timezone: data.timezone ?? 'UTC',
|
||||
}, data.access_token)
|
||||
} else {
|
||||
throw new Error(t('auth.loginFailed'))
|
||||
|
||||
@@ -46,6 +46,32 @@
|
||||
@update:model-value="onAvatarUrlChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="settings-timezone">{{ $t('settings.timezone') }}</Label>
|
||||
<Select
|
||||
:model-value="timezone"
|
||||
@update:model-value="onTimezoneChange"
|
||||
>
|
||||
<SelectTrigger
|
||||
id="settings-timezone"
|
||||
class="w-full"
|
||||
:aria-label="$t('settings.timezone')"
|
||||
>
|
||||
<SelectValue :placeholder="$t('settings.timezonePlaceholder')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem
|
||||
v-for="timezoneOption in timezones"
|
||||
:key="timezoneOption"
|
||||
:value="timezoneOption"
|
||||
>
|
||||
{{ timezoneOption }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
:disabled="saving || loading"
|
||||
@@ -60,13 +86,27 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Button, Input, Label, Separator, Spinner } from '@memohai/ui'
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
Separator,
|
||||
Spinner,
|
||||
} from '@memohai/ui'
|
||||
import { timezones } from '@/utils/timezones'
|
||||
|
||||
defineProps<{
|
||||
displayUserId: string
|
||||
displayUsername: string
|
||||
displayName: string
|
||||
avatarUrl: string
|
||||
timezone: string
|
||||
saving: boolean
|
||||
loading: boolean
|
||||
}>()
|
||||
@@ -74,6 +114,7 @@ defineProps<{
|
||||
const emit = defineEmits<{
|
||||
'update:displayName': [value: string]
|
||||
'update:avatarUrl': [value: string]
|
||||
'update:timezone': [value: string]
|
||||
save: []
|
||||
}>()
|
||||
|
||||
@@ -84,4 +125,8 @@ function onDisplayNameChange(value: string | number) {
|
||||
function onAvatarUrlChange(value: string | number) {
|
||||
emit('update:avatarUrl', String(value))
|
||||
}
|
||||
|
||||
function onTimezoneChange(value: string | number | undefined) {
|
||||
emit('update:timezone', String(value || 'UTC'))
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -106,10 +106,12 @@
|
||||
:display-username="displayUsername"
|
||||
:display-name="profileForm.display_name"
|
||||
:avatar-url="profileForm.avatar_url"
|
||||
:timezone="profileForm.timezone"
|
||||
:saving="savingProfile"
|
||||
:loading="loadingInitial"
|
||||
@update:display-name="profileForm.display_name = $event"
|
||||
@update:avatar-url="profileForm.avatar_url = $event"
|
||||
@update:timezone="profileForm.timezone = $event"
|
||||
@save="onSaveProfile"
|
||||
/>
|
||||
|
||||
@@ -263,6 +265,7 @@ const generatingBindCode = ref(false)
|
||||
const profileForm = reactive({
|
||||
display_name: '',
|
||||
avatar_url: '',
|
||||
timezone: '',
|
||||
})
|
||||
|
||||
const passwordForm = reactive({
|
||||
@@ -322,12 +325,14 @@ async function loadMyAccount() {
|
||||
account.value = data
|
||||
profileForm.display_name = data.display_name || ''
|
||||
profileForm.avatar_url = data.avatar_url || ''
|
||||
profileForm.timezone = data.timezone || 'UTC'
|
||||
patchUserInfo({
|
||||
id: data.id,
|
||||
username: data.username,
|
||||
role: data.role,
|
||||
displayName: data.display_name || '',
|
||||
avatarUrl: data.avatar_url || '',
|
||||
timezone: data.timezone || 'UTC',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -347,14 +352,17 @@ async function onSaveProfile() {
|
||||
const body: AccountsUpdateProfileRequest = {
|
||||
display_name: profileForm.display_name.trim(),
|
||||
avatar_url: profileForm.avatar_url.trim(),
|
||||
timezone: profileForm.timezone.trim(),
|
||||
}
|
||||
const { data } = await putUsersMe({ body, throwOnError: true })
|
||||
account.value = data
|
||||
profileForm.display_name = data.display_name || ''
|
||||
profileForm.avatar_url = data.avatar_url || ''
|
||||
profileForm.timezone = data.timezone || 'UTC'
|
||||
patchUserInfo({
|
||||
displayName: data.display_name || '',
|
||||
avatarUrl: data.avatar_url || '',
|
||||
timezone: data.timezone || 'UTC',
|
||||
})
|
||||
toast.success(t('settings.profileUpdated'))
|
||||
} catch (error) {
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface UserInfo {
|
||||
role: string;
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore(
|
||||
@@ -20,6 +21,7 @@ export const useUserStore = defineStore(
|
||||
role: '',
|
||||
displayName: '',
|
||||
avatarUrl: '',
|
||||
timezone: 'UTC',
|
||||
})
|
||||
|
||||
const localToken = useLocalStorage('token', '')
|
||||
@@ -43,7 +45,7 @@ export const useUserStore = defineStore(
|
||||
const exitLogin = () => {
|
||||
localToken.value = ''
|
||||
for (const key of Object.keys(userInfo) as (keyof UserInfo)[]) {
|
||||
userInfo[key as keyof UserInfo] = ''
|
||||
userInfo[key as keyof UserInfo] = key === 'timezone' ? 'UTC' : ''
|
||||
}
|
||||
}
|
||||
const router = useRouter()
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
const fallbackTimezones = ['UTC']
|
||||
|
||||
export const timezones = typeof Intl.supportedValuesOf === 'function'
|
||||
? Intl.supportedValuesOf('timeZone')
|
||||
: fallbackTimezones
|
||||
|
||||
export const emptyTimezoneValue = '__empty_timezone__'
|
||||
Reference in New Issue
Block a user