From 9b96560a17b4af243d9990c107c7de765a3a7a48 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sun, 12 Nov 2023 01:41:10 +0100 Subject: [PATCH] Feature/settings backup (#453) * Show loading indicator for "NumberSetting" * [i18n] Add lowercase interpolation format * Add required dependencies for time settings * Dynamically import dayjs locales for languages * Add backup settings --- package.json | 2 + src/components/settings/NumberSetting.tsx | 4 +- src/components/settings/TimeSetting.tsx | 133 +++++++++++++++ .../downloads/DownloadAheadSetting.tsx | 14 +- .../GlobalUpdateSettingsInterval.tsx | 12 +- src/i18n/index.ts | 8 + src/i18n/locale/en.json | 27 ++++ src/screens/settings/Backup.tsx | 92 +++++++++++ src/util/language.tsx | 152 +++++++++++------- yarn.lock | 41 +++++ 10 files changed, 417 insertions(+), 68 deletions(-) create mode 100644 src/components/settings/TimeSetting.tsx diff --git a/package.json b/package.json index fd68db12..24d5ef0f 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,11 @@ "@fontsource/roboto": "^5.0.8", "@mui/icons-material": "^5.14.16", "@mui/material": "^5.14.16", + "@mui/x-date-pickers": "^6.18.0", "@vitejs/plugin-react-swc": "^3.4.1", "apollo-upload-client": "^17.0.0", "axios": "^1.6.0", + "dayjs": "^1.11.10", "file-selector": "^0.6.0", "graphql-tag": "^2.12.6", "graphql-ws": "^5.14.2", diff --git a/src/components/settings/NumberSetting.tsx b/src/components/settings/NumberSetting.tsx index 96248e30..1e702d43 100644 --- a/src/components/settings/NumberSetting.tsx +++ b/src/components/settings/NumberSetting.tsx @@ -22,7 +22,7 @@ import Slider from '@mui/material/Slider'; type BaseProps = { settingTitle: string; - settingValue: string; + settingValue?: string; settingIcon?: React.ReactNode; value: number; defaultValue?: number; @@ -98,7 +98,7 @@ export const NumberSetting = ({ {settingIcon ? {settingIcon} : null} diff --git a/src/components/settings/TimeSetting.tsx b/src/components/settings/TimeSetting.tsx new file mode 100644 index 00000000..1d17b9a6 --- /dev/null +++ b/src/components/settings/TimeSetting.tsx @@ -0,0 +1,133 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { Button, Dialog, DialogTitle, ListItemText } from '@mui/material'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; +import ListItemButton from '@mui/material/ListItemButton'; +import { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { TimePicker } from '@mui/x-date-pickers/TimePicker'; +import dayjs from 'dayjs'; +import { loadDayJsLocale } from '@/util/language.tsx'; + +export const TimeSetting = ({ + settingName, + value, + defaultValue, + handleChange, +}: { + settingName: string; + value?: string; + defaultValue: string; + handleChange: (path: string) => void; +}) => { + const { t, i18n } = useTranslation(); + + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [dialogValue, setDialogValue] = useState(value ?? defaultValue); + + const [locale, setLocale] = useState('en'); + + const currentLocale = i18n.language; + + useEffect(() => { + loadDayJsLocale(currentLocale).then((wasLoaded) => setLocale(wasLoaded ? currentLocale : 'en')); + }, [currentLocale]); + + useEffect(() => { + if (!value) { + return; + } + + setDialogValue(value); + }, [value]); + + const closeDialog = useCallback( + (resetValue: boolean) => { + setIsDialogOpen(false); + + if (resetValue) { + setDialogValue(value ?? defaultValue); + } + }, + [value], + ); + + const closeDialogWithReset = useCallback(() => closeDialog(true), [closeDialog]); + + const updateSetting = useCallback( + (newValue: string, shouldCloseDialog: boolean = true) => { + if (shouldCloseDialog) { + closeDialog(false); + } + + const didValueChange = value !== newValue; + if (!didValueChange) { + return; + } + + handleChange(newValue); + }, + [value, handleChange, closeDialog], + ); + + return ( + <> + setIsDialogOpen(true)}> + + + + + + {settingName} + + setDialogValue(time?.format('HH:mm') ?? '00:00')} + /> + + + + {defaultValue !== undefined ? ( + + ) : null} + + + + + + ); +}; diff --git a/src/components/settings/downloads/DownloadAheadSetting.tsx b/src/components/settings/downloads/DownloadAheadSetting.tsx index 0bb460aa..679bf619 100644 --- a/src/components/settings/downloads/DownloadAheadSetting.tsx +++ b/src/components/settings/downloads/DownloadAheadSetting.tsx @@ -21,7 +21,7 @@ export const DownloadAheadSetting = () => { const { t } = useTranslation(); const { data } = requestManager.useGetServerSettings(); - const downloadAheadLimit = data?.settings.autoDownloadAheadLimit ?? 0; + const downloadAheadLimit = data?.settings.autoDownloadAheadLimit; const shouldDownloadAhead = !!downloadAheadLimit; const [mutateSettings] = requestManager.useUpdateServerSettings(); @@ -49,10 +49,14 @@ export const DownloadAheadSetting = () => { {shouldDownloadAhead ? ( { const { t } = useTranslation(); const { data } = requestManager.useGetServerSettings(); - const autoUpdateIntervalHours = data?.settings.globalUpdateInterval ?? 0; + const autoUpdateIntervalHours = data?.settings.globalUpdateInterval; const doAutoUpdates = !!autoUpdateIntervalHours; const [mutateSettings] = requestManager.useUpdateServerSettings(); @@ -45,9 +45,13 @@ export const GlobalUpdateSettingsInterval = () => { {doAutoUpdates ? ( { + switch (format) { + case 'lowercase': + return value.toLowerCase(); + default: + return value; + } + }, }, returnNull: false, debug: process.env.NODE_ENV !== 'production', diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index b1ac893d..1d72108d 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -243,10 +243,18 @@ }, "date": { "label": { + "day": "Day", + "day_one": "Day", + "day_other": "Days", "today": "Today", "today_at": "Today at {{timeString}}", "yesterday": "Yesterday", "yesterday_at": "Yesterday at {{timeString}}" + }, + "value": { + "label": { + "day": "{{days}} $t(global.date.label.day, lowercase)" + } } }, "error": { @@ -278,6 +286,7 @@ "display": "Display", "filter": "Filter", "loading": "Loading…", + "never": "Never", "none": "None", "sort": "Sort", "unknown": "Unknown" @@ -486,6 +495,24 @@ "title": "About" }, "backup": { + "automated": { + "cleanup": { + "label": { + "title": "Backup cleanup", + "value": "Delete backups that are older than $t(global.date.value.label.day)" + } + }, + "label": { + "interval": "Backup interval", + "time": "Backup time" + }, + "location": { + "label": { + "description": "The path to the directory on the server where automated backups should get saved in", + "title": "Backup location" + } + } + }, "label": { "backup_restore_failed": "Could not restore backup", "create_backup": "Create backup", diff --git a/src/screens/settings/Backup.tsx b/src/screens/settings/Backup.tsx index 44426c67..c0b93392 100644 --- a/src/screens/settings/Backup.tsx +++ b/src/screens/settings/Backup.tsx @@ -13,12 +13,39 @@ import { fromEvent } from 'file-selector'; import { useTranslation } from 'react-i18next'; import { ListItemButton } from '@mui/material'; import ListItemIcon from '@mui/material/ListItemIcon'; +import ListSubheader from '@mui/material/ListSubheader'; +import { t as translate } from 'i18next'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { makeToast } from '@/components/util/Toast'; import { ListItemLink } from '@/components/util/ListItemLink'; import { NavBarContext, useSetDefaultBackTo } from '@/components/context/NavbarContext'; import { BackupRestoreState } from '@/lib/graphql/generated/graphql.ts'; import { Progress } from '@/components/util/Progress.tsx'; +import { ServerDirSetting } from '@/components/settings/ServerDirSetting.tsx'; +import { NumberSetting } from '@/components/settings/NumberSetting.tsx'; +import { TimeSetting } from '@/components/settings/TimeSetting.tsx'; +import { ServerSettings } from '@/typings.ts'; + +type BackupSettingsType = Pick; + +const extractBackupSettings = (settings: ServerSettings): BackupSettingsType => ({ + backupPath: settings.backupPath, + backupTime: settings.backupTime, + backupInterval: settings.backupInterval, + backupTTL: settings.backupTTL, +}); + +const getBackupCleanupDisplayValue = (ttl?: number) => { + if (ttl === undefined) { + return undefined; + } + + if (ttl === 0) { + return translate('global.label.never'); + } + + return translate('settings.backup.automated.cleanup.label.value', { days: ttl, count: ttl }); +}; let backupRestoreId: string | undefined; @@ -32,6 +59,11 @@ export function Backup() { useSetDefaultBackTo('settings'); + const { data: settingsData } = requestManager.useGetServerSettings(); + const [mutateSettings] = requestManager.useUpdateServerSettings(); + + const backupSettings = settingsData ? extractBackupSettings(settingsData.settings) : undefined; + const { data } = requestManager.useGetBackupRestoreStatus(backupRestoreId ?? '', { skip: !backupRestoreId, pollInterval: 1000, @@ -48,6 +80,13 @@ export function Backup() { return Number.isNaN(progress) ? 0 : progress; })(); + const updateSetting = ( + setting: Setting, + value: BackupSettingsType[Setting], + ) => { + mutateSettings({ variables: { input: { settings: { [setting]: value } } } }); + }; + useEffect(() => { if (!data?.restoreStatus) { return; @@ -142,6 +181,59 @@ export function Backup() { ) : null} + + Automated backup + + } + > + updateSetting('backupPath', path)} + /> + updateSetting('backupTime', time)} + /> + updateSetting('backupInterval', interval)} + /> + updateSetting('backupTTL', ttl)} + /> + diff --git a/src/util/language.tsx b/src/util/language.tsx index 066cc579..45af69ae 100644 --- a/src/util/language.tsx +++ b/src/util/language.tsx @@ -14,67 +14,88 @@ export enum DefaultLanguage { LOCAL_SOURCE = 'localsourcelang', } +const loadDefaultDayJsLocale = () => import('dayjs/locale/en.js'); + export const ISOLanguages = [ // full list: https://github.com/meikidd/iso-639-1/blob/master/src/data.js - { code: 'en', name: 'English', nativeName: 'English' }, - { code: 'ca', name: 'Catalan; Valencian', nativeName: 'Català' }, - { code: 'de', name: 'German', nativeName: 'Deutsch' }, - { code: 'es', name: 'Spanish; Castilian', nativeName: 'Español' }, - { code: 'es-419', name: 'Spanish; Castilian', nativeName: 'Español (Latinoamérica)' }, - { code: 'fr', name: 'French', nativeName: 'Français' }, - { code: 'id', name: 'Indonesian', nativeName: 'Indonesia' }, - { code: 'it', name: 'Italian', nativeName: 'Italiano' }, - { code: 'pt', name: 'Portuguese', nativeName: 'Português' }, - { code: 'pt-pt', name: 'Portuguese', nativeName: 'Português (Portugal)' }, - { code: 'pt-br', name: 'Portuguese; Brasil', nativeName: 'Português (Brasil)' }, - { code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt' }, - { code: 'tr', name: 'Turkish', nativeName: 'Türkçe' }, - { code: 'ru', name: 'Russian', nativeName: 'русский' }, - { code: 'ar', name: 'Arabic', nativeName: 'العربية' }, - { code: 'hi', name: 'Hindi', nativeName: 'हिन्दी' }, - { code: 'th', name: 'Thai', nativeName: 'ไทย' }, - { code: 'zh', name: 'Chinese', nativeName: '中文' }, - { code: 'zh-hans', name: 'Chinese', nativeName: '中文 (HANS)' }, - { code: 'zh-hant', name: 'Chinese', nativeName: '中文 (HANT)' }, - { code: 'zh-rhk', name: 'Chinese', nativeName: '中文 (RHK)' }, - { code: 'zh-rtw', name: 'Chinese', nativeName: '中文 (RTW)' }, + { dayjsImport: loadDefaultDayJsLocale, code: 'en', name: 'English', nativeName: 'English' }, + { dayjsImport: () => import('dayjs/locale/ca.js'), code: 'ca', name: 'Catalan; Valencian', nativeName: 'Català' }, + { dayjsImport: () => import('dayjs/locale/de.js'), code: 'de', name: 'German', nativeName: 'Deutsch' }, + { dayjsImport: () => import('dayjs/locale/es.js'), code: 'es', name: 'Spanish; Castilian', nativeName: 'Español' }, + { + dayjsImport: () => import('dayjs/locale/es.js'), + code: 'es-419', + name: 'Spanish; Castilian', + nativeName: 'Español (Latinoamérica)', + }, + { dayjsImport: () => import('dayjs/locale/fr.js'), code: 'fr', name: 'French', nativeName: 'Français' }, + { dayjsImport: () => import('dayjs/locale/id.js'), code: 'id', name: 'Indonesian', nativeName: 'Indonesia' }, + { dayjsImport: () => import('dayjs/locale/it.js'), code: 'it', name: 'Italian', nativeName: 'Italiano' }, + { dayjsImport: () => import('dayjs/locale/pt.js'), code: 'pt', name: 'Portuguese', nativeName: 'Português' }, + { + dayjsImport: () => import('dayjs/locale/pt.js'), + code: 'pt-pt', + name: 'Portuguese', + nativeName: 'Português (Portugal)', + }, + { + dayjsImport: () => import('dayjs/locale/pt-br.js'), + code: 'pt-br', + name: 'Portuguese; Brasil', + nativeName: 'Português (Brasil)', + }, + { dayjsImport: () => import('dayjs/locale/vi.js'), code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt' }, + { dayjsImport: () => import('dayjs/locale/tr.js'), code: 'tr', name: 'Turkish', nativeName: 'Türkçe' }, + { dayjsImport: () => import('dayjs/locale/ru.js'), code: 'ru', name: 'Russian', nativeName: 'русский' }, + { dayjsImport: () => import('dayjs/locale/ar.js'), code: 'ar', name: 'Arabic', nativeName: 'العربية' }, + { dayjsImport: () => import('dayjs/locale/hi.js'), code: 'hi', name: 'Hindi', nativeName: 'हिन्दी' }, + { dayjsImport: () => import('dayjs/locale/th.js'), code: 'th', name: 'Thai', nativeName: 'ไทย' }, + { dayjsImport: () => import('dayjs/locale/zh.js'), code: 'zh', name: 'Chinese', nativeName: '中文' }, + { dayjsImport: () => import('dayjs/locale/zh-cn.js'), code: 'zh-hans', name: 'Chinese', nativeName: '中文 (HANS)' }, + { dayjsImport: () => import('dayjs/locale/zh-hk.js'), code: 'zh-hant', name: 'Chinese', nativeName: '中文 (HANT)' }, + { dayjsImport: () => import('dayjs/locale/zh-hk.js'), code: 'zh-rhk', name: 'Chinese', nativeName: '中文 (RHK)' }, + { dayjsImport: () => import('dayjs/locale/zh-tw.js'), code: 'zh-rtw', name: 'Chinese', nativeName: '中文 (RTW)' }, - { code: 'ja', name: 'Japanese', nativeName: '日本語' }, - { code: 'ko', name: 'Korean', nativeName: '한국어' }, - { code: 'zu', name: 'Zulu', nativeName: 'isiZulu' }, - { code: 'xh', name: 'Xhosa', nativeName: 'isiXhosa' }, - { code: 'uk', name: 'Ukrainian', nativeName: 'Українська' }, - { code: 'ro', name: 'Romanian', nativeName: 'Română' }, - { code: 'bg', name: 'Bulgarian', nativeName: 'български' }, - { code: 'cs', name: 'Czech', nativeName: 'čeština' }, - { code: 'pl', name: 'Polish', nativeName: 'polski' }, - { code: 'no', name: 'Norwegian', nativeName: 'Norsk' }, - { code: 'nl', name: 'Dutch', nativeName: 'Nederlands' }, - { code: 'my', name: 'Burmese', nativeName: 'ဗမာစာ' }, - { code: 'ms', name: 'Malay', nativeName: 'Malaysia' }, - { code: 'mn', name: 'Mongolian', nativeName: 'Монгол' }, - { code: 'ml', name: 'Malayalam', nativeName: 'മലയാളം' }, - { code: 'ku', name: 'Kurdish', nativeName: 'Kurdî' }, - { code: 'hu', name: 'Hungarian', nativeName: 'Magyar' }, - { code: 'hr', name: 'Croatian', nativeName: 'Hrvatski' }, - { code: 'he', name: 'Hebrew', nativeName: 'עברית' }, - { code: 'fil', name: 'Filipino', nativeName: 'Filipino' }, - { code: 'fi', name: 'Finnish', nativeName: 'suomi' }, - { code: 'fa', name: 'Persian', nativeName: 'فارسی' }, - { code: 'eu', name: 'Basque', nativeName: 'euskara' }, - { code: 'el', name: 'Greek', nativeName: 'Ελληνικά' }, - { code: 'da', name: 'Danish', nativeName: 'dansk' }, - { code: 'bn', name: 'Bengali', nativeName: 'বাংলা' }, - { code: 'lt', name: 'Lithuanian', nativeName: 'lietuvių kalba' }, - { code: 'sh', name: 'Serbo-Croatian', nativeName: 'srpskohrvatski' }, + { dayjsImport: () => import('dayjs/locale/ja.js'), code: 'ja', name: 'Japanese', nativeName: '日本語' }, + { dayjsImport: () => import('dayjs/locale/ko.js'), code: 'ko', name: 'Korean', nativeName: '한국어' }, + { dayjsImport: () => import('dayjs/locale/af.js'), code: 'zu', name: 'Zulu', nativeName: 'isiZulu' }, + { dayjsImport: () => import('dayjs/locale/af.js'), code: 'xh', name: 'Xhosa', nativeName: 'isiXhosa' }, + { dayjsImport: () => import('dayjs/locale/uk.js'), code: 'uk', name: 'Ukrainian', nativeName: 'Українська' }, + { dayjsImport: () => import('dayjs/locale/ro.js'), code: 'ro', name: 'Romanian', nativeName: 'Română' }, + { dayjsImport: () => import('dayjs/locale/bg.js'), code: 'bg', name: 'Bulgarian', nativeName: 'български' }, + { dayjsImport: () => import('dayjs/locale/cs.js'), code: 'cs', name: 'Czech', nativeName: 'čeština' }, + { dayjsImport: () => import('dayjs/locale/pl.js'), code: 'pl', name: 'Polish', nativeName: 'polski' }, + { dayjsImport: () => import('dayjs/locale/nb.js'), code: 'no', name: 'Norwegian', nativeName: 'Norsk' }, + { dayjsImport: () => import('dayjs/locale/nl.js'), code: 'nl', name: 'Dutch', nativeName: 'Nederlands' }, + { dayjsImport: () => import('dayjs/locale/my.js'), code: 'my', name: 'Burmese', nativeName: 'ဗမာစာ' }, + { dayjsImport: () => import('dayjs/locale/ms.js'), code: 'ms', name: 'Malay', nativeName: 'Malaysia' }, + { dayjsImport: () => import('dayjs/locale/mn.js'), code: 'mn', name: 'Mongolian', nativeName: 'Монгол' }, + { dayjsImport: () => import('dayjs/locale/ml.js'), code: 'ml', name: 'Malayalam', nativeName: 'മലയാളം' }, + { dayjsImport: () => import('dayjs/locale/ku.js'), code: 'ku', name: 'Kurdish', nativeName: 'Kurdî' }, + { dayjsImport: () => import('dayjs/locale/hu.js'), code: 'hu', name: 'Hungarian', nativeName: 'Magyar' }, + { dayjsImport: () => import('dayjs/locale/hr.js'), code: 'hr', name: 'Croatian', nativeName: 'Hrvatski' }, + { dayjsImport: () => import('dayjs/locale/he.js'), code: 'he', name: 'Hebrew', nativeName: 'עברית' }, + { dayjsImport: () => import('dayjs/locale/tl-ph'), code: 'fil', name: 'Filipino', nativeName: 'Filipino' }, + { dayjsImport: () => import('dayjs/locale/fi.js'), code: 'fi', name: 'Finnish', nativeName: 'suomi' }, + { dayjsImport: () => import('dayjs/locale/fa.js'), code: 'fa', name: 'Persian', nativeName: 'فارسی' }, + { dayjsImport: () => import('dayjs/locale/eu.js'), code: 'eu', name: 'Basque', nativeName: 'euskara' }, + { dayjsImport: () => import('dayjs/locale/el.js'), code: 'el', name: 'Greek', nativeName: 'Ελληνικά' }, + { dayjsImport: () => import('dayjs/locale/da.js'), code: 'da', name: 'Danish', nativeName: 'dansk' }, + { dayjsImport: () => import('dayjs/locale/bn.js'), code: 'bn', name: 'Bengali', nativeName: 'বাংলা' }, + { dayjsImport: () => import('dayjs/locale/lt.js'), code: 'lt', name: 'Lithuanian', nativeName: 'lietuvių kalba' }, + { + dayjsImport: loadDefaultDayJsLocale, + code: 'sh', + name: 'Serbo-Croatian', + nativeName: 'srpskohrvatski', + }, - { code: 'af', name: 'Afrikaans', nativeName: 'Afrikaans' }, - { code: 'am', name: 'Amharic', nativeName: 'አማርኛ' }, - { code: 'az', name: 'Azerbaijani', nativeName: 'Azərbaycan' }, - { code: 'be', name: 'Belarusian', nativeName: 'беларуская' }, - { code: 'bs', name: 'Bosnian', nativeName: 'bosanski' }, - { code: 'sv', name: 'Swedish', nativeName: 'svenska' }, - { code: 'sv', name: 'Swedish', nativeName: 'svenska' }, + { dayjsImport: () => import('dayjs/locale/af.js'), code: 'af', name: 'Afrikaans', nativeName: 'Afrikaans' }, + { dayjsImport: () => import('dayjs/locale/am.js'), code: 'am', name: 'Amharic', nativeName: 'አማርኛ' }, + { dayjsImport: () => import('dayjs/locale/az.js'), code: 'az', name: 'Azerbaijani', nativeName: 'Azərbaycan' }, + { dayjsImport: () => import('dayjs/locale/be.js'), code: 'be', name: 'Belarusian', nativeName: 'беларуская' }, + { dayjsImport: () => import('dayjs/locale/bs.js'), code: 'bs', name: 'Bosnian', nativeName: 'bosanski' }, + { dayjsImport: () => import('dayjs/locale/sv.js'), code: 'sv', name: 'Swedish', nativeName: 'svenska' }, ]; export function langCodeToName(code: string): string { @@ -120,3 +141,20 @@ export const langSortCmp = (a: string, b: string) => { return aLang > bLang ? 1 : -1; }; + +export const loadDayJsLocale = async (locale: string) => { + const lang = ISOLanguages.find(({ code }) => code.toLowerCase() === locale.toLowerCase()); + + try { + if (!lang) { + await loadDefaultDayJsLocale(); + return false; + } + + await lang.dayjsImport(); + return true; + } catch (e) { + return false; + // ignore - dayjs falls back to en anyway + } +}; diff --git a/yarn.lock b/yarn.lock index f8c8001c..fbc057bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1400,6 +1400,19 @@ clsx "^2.0.0" prop-types "^15.8.1" +"@mui/base@^5.0.0-beta.22": + version "5.0.0-beta.23" + resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.23.tgz#dd10dfc609d8937749521f940965f757fa3c0f2c" + integrity sha512-9L8SQUGAWtd/Qi7Qem26+oSSgpY7f2iQTuvcz/rsGpyZjSomMMO6lwYeQSA0CpWM7+aN7eGoSY/WV6wxJiIxXw== + dependencies: + "@babel/runtime" "^7.23.2" + "@floating-ui/react-dom" "^2.0.2" + "@mui/types" "^7.2.8" + "@mui/utils" "^5.14.17" + "@popperjs/core" "^2.11.8" + clsx "^2.0.0" + prop-types "^15.8.1" + "@mui/core-downloads-tracker@^5.14.16": version "5.14.16" resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.16.tgz#03ceb422d69a33e6c1cbd7e943cf60816878be2a" @@ -1478,6 +1491,29 @@ prop-types "^15.8.1" react-is "^18.2.0" +"@mui/utils@^5.14.17": + version "5.14.17" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.14.17.tgz#8e2e7ca58865119eec8c6bdb359f539c25aaf576" + integrity sha512-yxnWgSS4J6DMFPw2Dof85yBkG02VTbEiqsikymMsnZnXDurtVGTIhlNuV24GTmFTuJMzEyTTU9UF+O7zaL8LEQ== + dependencies: + "@babel/runtime" "^7.23.2" + "@types/prop-types" "^15.7.9" + prop-types "^15.8.1" + react-is "^18.2.0" + +"@mui/x-date-pickers@^6.18.0": + version "6.18.0" + resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-6.18.0.tgz#3bc439d246315e957858f08a8c4a3a8fbe95adfd" + integrity sha512-y4UlkHQXiNRfb6FWQ/GWir0sZ+9kL+GEEZssG+XWP3KJ+d3lONRteusl4AJkYJBdIAOh+5LnMV9RAQKq9Sl7yw== + dependencies: + "@babel/runtime" "^7.23.2" + "@mui/base" "^5.0.0-beta.22" + "@mui/utils" "^5.14.16" + "@types/react-transition-group" "^4.4.8" + clsx "^2.0.0" + prop-types "^15.8.1" + react-transition-group "^4.4.5" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -2634,6 +2670,11 @@ dataloader@^2.2.2: resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.2.2.tgz#216dc509b5abe39d43a9b9d97e6e5e473dfbe3e0" integrity sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g== +dayjs@^1.11.10: + version "1.11.10" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" + integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== + debounce@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"