🚧 Add search engine to user preferences
This commit is contained in:
@@ -78,6 +78,7 @@ model UserSettings {
|
|||||||
colorScheme String @default("environment") // environment, light, dark
|
colorScheme String @default("environment") // environment, light, dark
|
||||||
language String @default("en")
|
language String @default("en")
|
||||||
defaultBoard String @default("default")
|
defaultBoard String @default("default")
|
||||||
|
firstDayOfWeek String @default("monday") // monday, saturnday, sunday
|
||||||
searchTemplate String @default("https://google.com/search?q=%s")
|
searchTemplate String @default("https://google.com/search?q=%s")
|
||||||
openSearchInNewTab Boolean @default(true)
|
openSearchInNewTab Boolean @default(true)
|
||||||
disablePingPulse Boolean @default(false)
|
disablePingPulse Boolean @default(false)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"accessibility": {
|
"accessibility": {
|
||||||
|
"title": "Accessibility",
|
||||||
"disablePulse": {
|
"disablePulse": {
|
||||||
"label": "Disable ping pulse",
|
"label": "Disable ping pulse",
|
||||||
"description": "By default, ping indicators in Homarr will pulse. This may be irritating. This slider will deactivate the animation"
|
"description": "By default, ping indicators in Homarr will pulse. This may be irritating. This slider will deactivate the animation"
|
||||||
@@ -22,5 +23,16 @@
|
|||||||
"firstDayOfWeek": {
|
"firstDayOfWeek": {
|
||||||
"label": "First day of the week"
|
"label": "First day of the week"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"searchEngine": {
|
||||||
|
"title": "Search engine",
|
||||||
|
"custom": "Custom",
|
||||||
|
"newTab": {
|
||||||
|
"label": "Open search results in a new tab"
|
||||||
|
},
|
||||||
|
"template": {
|
||||||
|
"label": "Query URL",
|
||||||
|
"description": "Use %s as a placeholder for the query"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Stack, Switch } from '@mantine/core';
|
import { Stack, Switch } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useFormContext } from '~/pages/user/preferences';
|
import { useUserPreferencesFormContext } from '~/pages/user/preferences';
|
||||||
|
|
||||||
export const AccessibilitySettings = () => {
|
export const AccessibilitySettings = () => {
|
||||||
const { t } = useTranslation('user/preferences');
|
const { t } = useTranslation('user/preferences');
|
||||||
|
|
||||||
const form = useFormContext();
|
const form = useUserPreferencesFormContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
import { Alert, Paper, SegmentedControl, Space, Stack, TextInput, Title } from '@mantine/core';
|
|
||||||
import { IconInfoCircle } from '@tabler/icons-react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { ChangeEventHandler, useState } from 'react';
|
|
||||||
|
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
|
||||||
import { useConfigStore } from '../../../../config/store';
|
|
||||||
import {
|
|
||||||
CommonSearchEngineCommonSettingsType,
|
|
||||||
SearchEngineCommonSettingsType,
|
|
||||||
} from '../../../../types/settings';
|
|
||||||
import { SearchNewTabSwitch } from './SearchNewTabSwitch';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
searchEngine: SearchEngineCommonSettingsType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SearchEngineSelector = ({ searchEngine }: Props) => {
|
|
||||||
const { t } = useTranslation(['settings/general/search-engine']);
|
|
||||||
const { updateSearchEngineConfig } = useUpdateSearchEngineConfig();
|
|
||||||
|
|
||||||
const [engine, setEngine] = useState(searchEngine.type);
|
|
||||||
const [searchUrl, setSearchUrl] = useState(
|
|
||||||
searchEngine.type === 'custom' ? searchEngine.properties.template : searchUrls.google
|
|
||||||
);
|
|
||||||
|
|
||||||
const onEngineChange = (value: EngineType) => {
|
|
||||||
setEngine(value);
|
|
||||||
updateSearchEngineConfig(value, searchUrl);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSearchUrlChange: ChangeEventHandler<HTMLInputElement> = (ev) => {
|
|
||||||
const url = ev.currentTarget.value;
|
|
||||||
setSearchUrl(url);
|
|
||||||
updateSearchEngineConfig(engine, url);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack spacing={0} mt="xs">
|
|
||||||
<Title order={5} mb="xs">
|
|
||||||
{t('title')}
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
<SegmentedControl
|
|
||||||
fullWidth
|
|
||||||
mb="sm"
|
|
||||||
title={t('title') ?? undefined}
|
|
||||||
value={engine}
|
|
||||||
onChange={onEngineChange}
|
|
||||||
data={searchEngineOptions}
|
|
||||||
/>
|
|
||||||
<Paper p="md" py="sm" mb="md" withBorder>
|
|
||||||
<Title order={6} mb={0}>
|
|
||||||
{t('configurationName')}
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
<SearchNewTabSwitch defaultValue={searchEngine.properties.openInNewTab} />
|
|
||||||
|
|
||||||
{engine === 'custom' && (
|
|
||||||
<>
|
|
||||||
<Space mb="md" />
|
|
||||||
<TextInput
|
|
||||||
label={t('customEngine.label')}
|
|
||||||
name={t('configurationName') ?? undefined}
|
|
||||||
description={t('tips.placeholderTip')}
|
|
||||||
placeholder={t('customEngine.placeholder') ?? undefined}
|
|
||||||
value={searchUrl}
|
|
||||||
onChange={onSearchUrlChange}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
<Alert icon={<IconInfoCircle />} color="blue">
|
|
||||||
{t('tips.generalTip')}
|
|
||||||
</Alert>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const searchEngineOptions: { label: string; value: EngineType }[] = [
|
|
||||||
{ label: 'Google', value: 'google' },
|
|
||||||
{ label: 'DuckDuckGo', value: 'duckDuckGo' },
|
|
||||||
{ label: 'Bing', value: 'bing' },
|
|
||||||
{ label: 'Custom', value: 'custom' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const searchUrls: { [key in CommonSearchEngineCommonSettingsType['type']]: string } = {
|
|
||||||
google: 'https://google.com/search?q=',
|
|
||||||
duckDuckGo: 'https://duckduckgo.com/?q=',
|
|
||||||
bing: 'https://bing.com/search?q=',
|
|
||||||
};
|
|
||||||
|
|
||||||
type EngineType = SearchEngineCommonSettingsType['type'];
|
|
||||||
|
|
||||||
const useUpdateSearchEngineConfig = () => {
|
|
||||||
const { name: configName } = useConfigContext();
|
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
|
||||||
|
|
||||||
if (!configName) {
|
|
||||||
return {
|
|
||||||
updateSearchEngineConfig: () => {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSearchEngineConfig = (engine: EngineType, searchUrl: string) => {
|
|
||||||
updateConfig(configName, (prev) => ({
|
|
||||||
...prev,
|
|
||||||
settings: {
|
|
||||||
...prev.settings,
|
|
||||||
common: {
|
|
||||||
...prev.settings.common,
|
|
||||||
searchEngine:
|
|
||||||
engine === 'custom'
|
|
||||||
? {
|
|
||||||
type: engine,
|
|
||||||
properties: {
|
|
||||||
...prev.settings.common.searchEngine.properties,
|
|
||||||
template: searchUrl,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
type: engine,
|
|
||||||
properties: {
|
|
||||||
openInNewTab: prev.settings.common.searchEngine.properties.openInNewTab,
|
|
||||||
enabled: prev.settings.common.searchEngine.properties.enabled,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
updateSearchEngineConfig,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { Switch } from '@mantine/core';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
|
||||||
import { useConfigStore } from '../../../../config/store';
|
|
||||||
import { SearchEngineCommonSettingsType } from '../../../../types/settings';
|
|
||||||
|
|
||||||
interface SearchNewTabSwitchProps {
|
|
||||||
defaultValue: boolean | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SearchNewTabSwitch({ defaultValue }: SearchNewTabSwitchProps) {
|
|
||||||
const { t } = useTranslation('settings/general/search-engine');
|
|
||||||
const { name: configName } = useConfigContext();
|
|
||||||
const updateConfig = useConfigStore((x) => x.updateConfig);
|
|
||||||
|
|
||||||
const [openInNewTab, setOpenInNewTab] = useState<boolean>(defaultValue ?? true);
|
|
||||||
|
|
||||||
if (!configName) return null;
|
|
||||||
|
|
||||||
const toggleOpenInNewTab = () => {
|
|
||||||
setOpenInNewTab(!openInNewTab);
|
|
||||||
updateConfig(configName, (prev) => ({
|
|
||||||
...prev,
|
|
||||||
settings: {
|
|
||||||
...prev.settings,
|
|
||||||
common: {
|
|
||||||
...prev.settings.common,
|
|
||||||
searchEngine: {
|
|
||||||
...prev.settings.common.searchEngine,
|
|
||||||
properties: {
|
|
||||||
...prev.settings.common.searchEngine.properties,
|
|
||||||
openInNewTab: !openInNewTab,
|
|
||||||
},
|
|
||||||
} as SearchEngineCommonSettingsType,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch checked={openInNewTab} onChange={toggleOpenInNewTab} label={t('searchNewTab.label')} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
60
src/components/User/Preferences/SearchEngineSelector.tsx
Normal file
60
src/components/User/Preferences/SearchEngineSelector.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { Paper, SegmentedControl, Stack, Switch, TextInput } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useUserPreferencesFormContext } from '~/pages/user/preferences';
|
||||||
|
|
||||||
|
const searchEngineOptions = [
|
||||||
|
{ label: 'Google', value: 'https://google.com/search?q=%s' },
|
||||||
|
{ label: 'DuckDuckGo', value: 'https://duckduckgo.com/?q=%s' },
|
||||||
|
{ label: 'Bing', value: 'https://bing.com/search?q=%s' },
|
||||||
|
{ value: 'custom' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const useSegmentData = () => {
|
||||||
|
const { t } = useTranslation('user/preferences');
|
||||||
|
return searchEngineOptions.map((option) => ({
|
||||||
|
label: option.value === 'custom' ? t('searchEngine.custom') : option.label,
|
||||||
|
value: option.value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SearchEngineSelector = () => {
|
||||||
|
const { t } = useTranslation('user/preferences');
|
||||||
|
const form = useUserPreferencesFormContext();
|
||||||
|
const segmentData = useSegmentData();
|
||||||
|
const segmentValue = useMemo(
|
||||||
|
() =>
|
||||||
|
searchEngineOptions.find((x) => x.value === form.values.searchTemplate)?.value ?? 'custom',
|
||||||
|
[form.values.searchTemplate]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<SegmentedControl
|
||||||
|
fullWidth
|
||||||
|
data={segmentData}
|
||||||
|
value={segmentValue}
|
||||||
|
onChange={(v: typeof segmentValue) => {
|
||||||
|
v === 'custom'
|
||||||
|
? form.setFieldValue('searchTemplate', '')
|
||||||
|
: form.setFieldValue('searchTemplate', v);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Paper p="md" py="sm" mb="md" withBorder>
|
||||||
|
<Stack spacing="sm">
|
||||||
|
<Switch
|
||||||
|
label={t('searchEngine.newTab.label')}
|
||||||
|
{...form.getInputProps('openSearchInNewTab', { type: 'checkbox' })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label={t('searchEngine.template.label')}
|
||||||
|
description={t('searchEngine.template.description')}
|
||||||
|
inputWrapperOrder={['label', 'input', 'description', 'error']}
|
||||||
|
{...form.getInputProps('searchTemplate')}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Autocomplete, Group, Kbd, Modal, Text, Tooltip, useMantineTheme } from '@mantine/core';
|
import { Autocomplete, Group, Text, useMantineTheme } from '@mantine/core';
|
||||||
import { useDisclosure, useHotkeys, useMediaQuery } from '@mantine/hooks';
|
import { useDisclosure, useHotkeys } from '@mantine/hooks';
|
||||||
import {
|
import {
|
||||||
IconBrandYoutube,
|
IconBrandYoutube,
|
||||||
IconDownload,
|
IconDownload,
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { ReactNode, forwardRef, useEffect, useMemo, useRef, useState } from 'react';
|
import { ReactNode, forwardRef, useMemo, useRef, useState } from 'react';
|
||||||
import { useConfigContext } from '~/config/provider';
|
import { useConfigContext } from '~/config/provider';
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
import { Button, Group, LoadingOverlay, Select, Stack, Text, Title } from '@mantine/core';
|
import {
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Group,
|
||||||
|
LoadingOverlay,
|
||||||
|
Paper,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
import { createFormContext } from '@mantine/form';
|
import { createFormContext } from '@mantine/form';
|
||||||
import type { InferGetServerSidePropsType } from 'next';
|
import type { InferGetServerSidePropsType } from 'next';
|
||||||
import { GetServerSidePropsContext } from 'next';
|
import { GetServerSidePropsContext } from 'next';
|
||||||
@@ -7,7 +17,8 @@ import { forwardRef } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { AccessibilitySettings } from '~/components/User/Preferences/AccessibilitySettings';
|
import { AccessibilitySettings } from '~/components/User/Preferences/AccessibilitySettings';
|
||||||
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
import { SearchEngineSelector } from '~/components/User/Preferences/SearchEngineSelector';
|
||||||
|
import { MainLayout } from '~/components/layout/Templates/MainLayout';
|
||||||
import { sleep } from '~/tools/client/time';
|
import { sleep } from '~/tools/client/time';
|
||||||
import { languages } from '~/tools/language';
|
import { languages } from '~/tools/language';
|
||||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||||
@@ -21,18 +32,24 @@ const PreferencesPage = ({ locale }: InferGetServerSidePropsType<typeof getServe
|
|||||||
const { data: boardsData } = api.boards.all.useQuery();
|
const { data: boardsData } = api.boards.all.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ManageLayout>
|
<MainLayout>
|
||||||
<Head>
|
<Container>
|
||||||
<title>Preferences • Homarr</title>
|
<Paper p="xl" mih="100%" withBorder>
|
||||||
</Head>
|
<Head>
|
||||||
<Title mb="xl">Preferences</Title>
|
<title>Preferences • Homarr</title>
|
||||||
|
</Head>
|
||||||
|
<Title mb="xl">Preferences</Title>
|
||||||
|
|
||||||
{data && boardsData && <SettingsComponent settings={data.settings} boardsData={boardsData} />}
|
{data && boardsData && (
|
||||||
</ManageLayout>
|
<SettingsComponent settings={data.settings} boardsData={boardsData} />
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
</MainLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const [FormProvider, useFormContext, useForm] =
|
export const [FormProvider, useUserPreferencesFormContext, useForm] =
|
||||||
createFormContext<z.infer<typeof updateSettingsValidationSchema>>();
|
createFormContext<z.infer<typeof updateSettingsValidationSchema>>();
|
||||||
|
|
||||||
const SettingsComponent = ({
|
const SettingsComponent = ({
|
||||||
@@ -56,10 +73,13 @@ const SettingsComponent = ({
|
|||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
|
defaultBoard: settings.defaultBoard,
|
||||||
|
language: settings.language,
|
||||||
|
firstDayOfWeek: settings.firstDayOfWeek,
|
||||||
disablePingPulse: settings.disablePingPulse,
|
disablePingPulse: settings.disablePingPulse,
|
||||||
replaceDotsWithIcons: settings.replacePingWithIcons,
|
replaceDotsWithIcons: settings.replacePingWithIcons,
|
||||||
language: settings.language,
|
searchTemplate: settings.searchTemplate,
|
||||||
defaultBoard: settings.defaultBoard,
|
openSearchInNewTab: settings.openSearchInNewTab,
|
||||||
},
|
},
|
||||||
validate: i18nZodResolver(updateSettingsValidationSchema),
|
validate: i18nZodResolver(updateSettingsValidationSchema),
|
||||||
validateInputOnBlur: true,
|
validateInputOnBlur: true,
|
||||||
@@ -70,13 +90,12 @@ const SettingsComponent = ({
|
|||||||
const { mutate, isLoading } = api.user.updateSettings.useMutation({
|
const { mutate, isLoading } = api.user.updateSettings.useMutation({
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
void context.boards.all.invalidate();
|
void context.boards.all.invalidate();
|
||||||
}
|
void context.user.withSettings.invalidate();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = (values: z.infer<typeof updateSettingsValidationSchema>) => {
|
||||||
sleep(500).then(() => {
|
mutate(values);
|
||||||
mutate(form.values);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -130,11 +149,17 @@ const SettingsComponent = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Title order={2} size="lg" mt="lg" mb="md">
|
<Title order={2} size="lg" mt="lg" mb="md">
|
||||||
Accessibility
|
{t('accessibility.title')}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<AccessibilitySettings />
|
<AccessibilitySettings />
|
||||||
|
|
||||||
|
<Title order={2} size="lg" mt="lg" mb="md">
|
||||||
|
{t('searchEngine.title')}
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<SearchEngineSelector />
|
||||||
|
|
||||||
<Button type="submit" fullWidth mt="md">
|
<Button type="submit" fullWidth mt="md">
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -146,7 +146,12 @@ export const userRouter = createTRPCRouter({
|
|||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
settings: user.settings,
|
settings: {
|
||||||
|
...user.settings,
|
||||||
|
firstDayOfWeek: z
|
||||||
|
.enum(['monday', 'saturday', 'sunday'])
|
||||||
|
.parse(user.settings.firstDayOfWeek),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -164,6 +169,9 @@ export const userRouter = createTRPCRouter({
|
|||||||
replacePingWithIcons: input.replaceDotsWithIcons,
|
replacePingWithIcons: input.replaceDotsWithIcons,
|
||||||
defaultBoard: input.defaultBoard,
|
defaultBoard: input.defaultBoard,
|
||||||
language: input.language,
|
language: input.language,
|
||||||
|
firstDayOfWeek: input.firstDayOfWeek,
|
||||||
|
searchTemplate: input.searchTemplate,
|
||||||
|
openSearchInNewTab: input.openSearchInNewTab,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { CustomErrorParams } from '~/utils/i18n-zod-resolver';
|
import { CustomErrorParams } from '~/utils/i18n-zod-resolver';
|
||||||
|
|
||||||
export const passwordSchema = z
|
export const passwordSchema = z
|
||||||
@@ -41,8 +40,11 @@ export const colorSchemeParser = z
|
|||||||
.catch('environment');
|
.catch('environment');
|
||||||
|
|
||||||
export const updateSettingsValidationSchema = z.object({
|
export const updateSettingsValidationSchema = z.object({
|
||||||
|
defaultBoard: z.string(),
|
||||||
|
language: z.string(),
|
||||||
|
firstDayOfWeek: z.enum(['monday', 'saturday', 'sunday']),
|
||||||
disablePingPulse: z.boolean(),
|
disablePingPulse: z.boolean(),
|
||||||
replaceDotsWithIcons: z.boolean(),
|
replaceDotsWithIcons: z.boolean(),
|
||||||
language: z.string(),
|
searchTemplate: z.string().nonempty().max(256),
|
||||||
defaultBoard: z.string()
|
openSearchInNewTab: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user