mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-11 16:05:47 +01:00
Merge branch 'dev' into feature/add-basic-authentication
This commit is contained in:
@@ -42,6 +42,7 @@
|
|||||||
"@mantine/modals": "^6.0.0",
|
"@mantine/modals": "^6.0.0",
|
||||||
"@mantine/next": "^6.0.0",
|
"@mantine/next": "^6.0.0",
|
||||||
"@mantine/notifications": "^6.0.0",
|
"@mantine/notifications": "^6.0.0",
|
||||||
|
"@mantine/tiptap": "^6.0.17",
|
||||||
"@next-auth/prisma-adapter": "^1.0.5",
|
"@next-auth/prisma-adapter": "^1.0.5",
|
||||||
"@nivo/core": "^0.83.0",
|
"@nivo/core": "^0.83.0",
|
||||||
"@nivo/line": "^0.83.0",
|
"@nivo/line": "^0.83.0",
|
||||||
@@ -54,6 +55,10 @@
|
|||||||
"@tanstack/react-query": "^4.2.1",
|
"@tanstack/react-query": "^4.2.1",
|
||||||
"@tanstack/react-query-devtools": "^4.24.4",
|
"@tanstack/react-query-devtools": "^4.24.4",
|
||||||
"@tanstack/react-query-persist-client": "^4.28.0",
|
"@tanstack/react-query-persist-client": "^4.28.0",
|
||||||
|
"@tiptap/extension-link": "^2.0.4",
|
||||||
|
"@tiptap/pm": "^2.0.4",
|
||||||
|
"@tiptap/react": "^2.0.4",
|
||||||
|
"@tiptap/starter-kit": "^2.0.4",
|
||||||
"@trpc/client": "^10.29.1",
|
"@trpc/client": "^10.29.1",
|
||||||
"@trpc/next": "^10.29.1",
|
"@trpc/next": "^10.29.1",
|
||||||
"@trpc/react-query": "^10.29.1",
|
"@trpc/react-query": "^10.29.1",
|
||||||
|
|||||||
@@ -35,5 +35,6 @@
|
|||||||
"small": "small",
|
"small": "small",
|
||||||
"medium": "medium",
|
"medium": "medium",
|
||||||
"large": "large"
|
"large": "large"
|
||||||
}
|
},
|
||||||
|
"seeMore": "See more..."
|
||||||
}
|
}
|
||||||
@@ -6,9 +6,7 @@
|
|||||||
"title": "Bookmark settings",
|
"title": "Bookmark settings",
|
||||||
"name": {
|
"name": {
|
||||||
"label": "Widget Title",
|
"label": "Widget Title",
|
||||||
"placeholder": {
|
"info": "Leave empty to keep the title hidden."
|
||||||
"label" : "Leave empty to keep the title hidden"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"items": {
|
"items": {
|
||||||
"label": "Items"
|
"label": "Items"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Center,
|
Center,
|
||||||
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
Modal,
|
Modal,
|
||||||
@@ -19,6 +20,7 @@ import { useDisclosure } from '@mantine/hooks';
|
|||||||
import { IconAlertTriangle, IconClick, IconListSearch } from '@tabler/icons-react';
|
import { IconAlertTriangle, IconClick, IconListSearch } from '@tabler/icons-react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { InfoCard } from '~/components/InfoCard/InfoCard';
|
||||||
import { City } from '~/server/api/routers/weather';
|
import { City } from '~/server/api/routers/weather';
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
@@ -29,6 +31,8 @@ type LocationSelectionProps = {
|
|||||||
propName: string;
|
propName: string;
|
||||||
value: any;
|
value: any;
|
||||||
handleChange: (key: string, value: IntegrationOptionsValueType) => void;
|
handleChange: (key: string, value: IntegrationOptionsValueType) => void;
|
||||||
|
info?: boolean;
|
||||||
|
infoLink?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LocationSelection = ({
|
export const LocationSelection = ({
|
||||||
@@ -36,6 +40,8 @@ export const LocationSelection = ({
|
|||||||
propName: key,
|
propName: key,
|
||||||
value,
|
value,
|
||||||
handleChange,
|
handleChange,
|
||||||
|
info,
|
||||||
|
infoLink,
|
||||||
}: LocationSelectionProps) => {
|
}: LocationSelectionProps) => {
|
||||||
const { t } = useTranslation('widgets/location');
|
const { t } = useTranslation('widgets/location');
|
||||||
const [query, setQuery] = useState(value.name ?? '');
|
const [query, setQuery] = useState(value.name ?? '');
|
||||||
@@ -57,7 +63,15 @@ export const LocationSelection = ({
|
|||||||
<>
|
<>
|
||||||
<Card>
|
<Card>
|
||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
|
<Flex direction="row" justify="space-between" wrap="nowrap">
|
||||||
<Title order={5}>{t(`modules/${widgetId}:descriptor.settings.${key}.label`)}</Title>
|
<Title order={5}>{t(`modules/${widgetId}:descriptor.settings.${key}.label`)}</Title>
|
||||||
|
{info && (
|
||||||
|
<InfoCard
|
||||||
|
message={t(`modules/${widgetId}:descriptor.settings.${key}.info`)}
|
||||||
|
link={infoLink}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<Group noWrap align="end">
|
<Group noWrap align="end">
|
||||||
<TextInput
|
<TextInput
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ import { IconAlertTriangle, IconPlaylistX, IconPlus } from '@tabler/icons-react'
|
|||||||
import { Trans, useTranslation } from 'next-i18next';
|
import { Trans, useTranslation } from 'next-i18next';
|
||||||
import { FC, useState } from 'react';
|
import { FC, useState } from 'react';
|
||||||
|
|
||||||
|
import { InfoCard } from '../../../InfoCard/InfoCard';
|
||||||
import { useConfigContext } from '../../../../config/provider';
|
import { useConfigContext } from '../../../../config/provider';
|
||||||
import { useConfigStore } from '../../../../config/store';
|
import { useConfigStore } from '../../../../config/store';
|
||||||
import { mapObject } from '../../../../tools/client/objects';
|
import { mapObject } from '../../../../tools/client/objects';
|
||||||
import { useColorTheme } from '../../../../tools/color';
|
|
||||||
import Widgets from '../../../../widgets';
|
import Widgets from '../../../../widgets';
|
||||||
import type { IDraggableListInputValue, IWidgetOptionValue } from '../../../../widgets/widgets';
|
import type { IDraggableListInputValue, IWidgetOptionValue } from '../../../../widgets/widgets';
|
||||||
import { IWidget } from '../../../../widgets/widgets';
|
import { IWidget } from '../../../../widgets/widgets';
|
||||||
@@ -135,70 +135,99 @@ const WidgetOptionTypeSwitch: FC<{
|
|||||||
handleChange: (key: string, value: IntegrationOptionsValueType) => void;
|
handleChange: (key: string, value: IntegrationOptionsValueType) => void;
|
||||||
}> = ({ option, widgetId, propName: key, value, handleChange }) => {
|
}> = ({ option, widgetId, propName: key, value, handleChange }) => {
|
||||||
const { t } = useTranslation([`modules/${widgetId}`, 'common']);
|
const { t } = useTranslation([`modules/${widgetId}`, 'common']);
|
||||||
const { primaryColor } = useColorTheme();
|
const info = option.info ?? false;
|
||||||
|
const link = option.infoLink ?? undefined;
|
||||||
|
|
||||||
switch (option.type) {
|
switch (option.type) {
|
||||||
case 'switch':
|
case 'switch':
|
||||||
return (
|
return (
|
||||||
|
<Group align="center" spacing="sm">
|
||||||
<Switch
|
<Switch
|
||||||
label={t(`descriptor.settings.${key}.label`)}
|
label={t(`descriptor.settings.${key}.label`)}
|
||||||
checked={value as boolean}
|
checked={value as boolean}
|
||||||
onChange={(ev) => handleChange(key, ev.currentTarget.checked)}
|
onChange={(ev) => handleChange(key, ev.currentTarget.checked)}
|
||||||
|
{...option.inputProps}
|
||||||
/>
|
/>
|
||||||
|
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
|
||||||
|
</Group>
|
||||||
);
|
);
|
||||||
case 'text':
|
case 'text':
|
||||||
return (
|
return (
|
||||||
|
<Stack spacing={0}>
|
||||||
|
<Group align="center" spacing="sm">
|
||||||
|
<Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text>
|
||||||
|
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
|
||||||
|
</Group>
|
||||||
<TextInput
|
<TextInput
|
||||||
color={primaryColor}
|
|
||||||
label={t(`descriptor.settings.${key}.label`)}
|
|
||||||
value={value as string}
|
value={value as string}
|
||||||
onChange={(ev) => handleChange(key, ev.currentTarget.value)}
|
onChange={(ev) => handleChange(key, ev.currentTarget.value)}
|
||||||
|
{...option.inputProps}
|
||||||
/>
|
/>
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
case 'multi-select':
|
case 'multi-select':
|
||||||
return (
|
return (
|
||||||
|
<Stack spacing={0}>
|
||||||
|
<Group align="center" spacing="sm">
|
||||||
|
<Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text>
|
||||||
|
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
|
||||||
|
</Group>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
color={primaryColor}
|
|
||||||
data={option.data}
|
data={option.data}
|
||||||
label={t(`descriptor.settings.${key}.label`)}
|
|
||||||
value={value as string[]}
|
value={value as string[]}
|
||||||
defaultValue={option.defaultValue}
|
defaultValue={option.defaultValue}
|
||||||
onChange={(v) => handleChange(key, v)}
|
onChange={(v) => handleChange(key, v)}
|
||||||
|
withinPortal
|
||||||
|
{...option.inputProps}
|
||||||
/>
|
/>
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
case 'select':
|
case 'select':
|
||||||
return (
|
return (
|
||||||
|
<Stack spacing={0}>
|
||||||
|
<Group align="center" spacing="sm">
|
||||||
|
<Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text>
|
||||||
|
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
|
||||||
|
</Group>
|
||||||
<Select
|
<Select
|
||||||
color={primaryColor}
|
|
||||||
defaultValue={option.defaultValue}
|
defaultValue={option.defaultValue}
|
||||||
data={option.data}
|
data={option.data}
|
||||||
label={t(`descriptor.settings.${key}.label`)}
|
|
||||||
value={value as string}
|
value={value as string}
|
||||||
onChange={(v) => handleChange(key, v ?? option.defaultValue)}
|
onChange={(v) => handleChange(key, v ?? option.defaultValue)}
|
||||||
|
withinPortal
|
||||||
|
{...option.inputProps}
|
||||||
/>
|
/>
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
case 'number':
|
case 'number':
|
||||||
return (
|
return (
|
||||||
|
<Stack spacing={0}>
|
||||||
|
<Group align="center" spacing="sm">
|
||||||
|
<Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text>
|
||||||
|
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
|
||||||
|
</Group>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
color={primaryColor}
|
|
||||||
label={t(`descriptor.settings.${key}.label`)}
|
|
||||||
value={value as number}
|
value={value as number}
|
||||||
onChange={(v) => handleChange(key, v!)}
|
onChange={(v) => handleChange(key, v!)}
|
||||||
{...option.inputProps}
|
{...option.inputProps}
|
||||||
/>
|
/>
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
case 'slider':
|
case 'slider':
|
||||||
return (
|
return (
|
||||||
<Stack spacing="xs">
|
<Stack spacing={0}>
|
||||||
<Text>{t(`descriptor.settings.${key}.label`)}</Text>
|
<Group align="center" spacing="sm">
|
||||||
|
<Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text>
|
||||||
|
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
|
||||||
|
</Group>
|
||||||
<Slider
|
<Slider
|
||||||
color={primaryColor}
|
|
||||||
label={value}
|
label={value}
|
||||||
value={value as number}
|
value={value as number}
|
||||||
min={option.min}
|
min={option.min}
|
||||||
max={option.max}
|
max={option.max}
|
||||||
step={option.step}
|
step={option.step}
|
||||||
onChange={(v) => handleChange(key, v)}
|
onChange={(v) => handleChange(key, v)}
|
||||||
|
{...option.inputProps}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
@@ -209,6 +238,8 @@ const WidgetOptionTypeSwitch: FC<{
|
|||||||
value={value}
|
value={value}
|
||||||
handleChange={handleChange}
|
handleChange={handleChange}
|
||||||
widgetId={widgetId}
|
widgetId={widgetId}
|
||||||
|
info={info}
|
||||||
|
infoLink={link}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -237,7 +268,10 @@ const WidgetOptionTypeSwitch: FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
|
<Group align="center" spacing="sm">
|
||||||
<Text>{t(`descriptor.settings.${key}.label`)}</Text>
|
<Text>{t(`descriptor.settings.${key}.label`)}</Text>
|
||||||
|
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
|
||||||
|
</Group>
|
||||||
<StaticDraggableList
|
<StaticDraggableList
|
||||||
value={typedVal}
|
value={typedVal}
|
||||||
onChange={(v) => handleChange(key, v)}
|
onChange={(v) => handleChange(key, v)}
|
||||||
@@ -262,9 +296,13 @@ const WidgetOptionTypeSwitch: FC<{
|
|||||||
);
|
);
|
||||||
case 'multiple-text':
|
case 'multiple-text':
|
||||||
return (
|
return (
|
||||||
|
<Stack spacing={0}>
|
||||||
|
<Group align="center" spacing="sm">
|
||||||
|
<Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text>
|
||||||
|
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
|
||||||
|
</Group>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
data={value.map((name: any) => ({ value: name, label: name }))}
|
data={value.map((name: any) => ({ value: name, label: name }))}
|
||||||
label={t(`descriptor.settings.${key}.label`)}
|
|
||||||
description={t(`descriptor.settings.${key}.description`)}
|
description={t(`descriptor.settings.${key}.description`)}
|
||||||
defaultValue={value as string[]}
|
defaultValue={value as string[]}
|
||||||
withinPortal
|
withinPortal
|
||||||
@@ -278,12 +316,16 @@ const WidgetOptionTypeSwitch: FC<{
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
case 'draggable-editable-list':
|
case 'draggable-editable-list':
|
||||||
const { t: translateDraggableList } = useTranslation('widgets/draggable-list');
|
const { t: translateDraggableList } = useTranslation('widgets/draggable-list');
|
||||||
return (
|
return (
|
||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
|
<Group align="center" spacing="sm">
|
||||||
<Text>{t(`descriptor.settings.${key}.label`)}</Text>
|
<Text>{t(`descriptor.settings.${key}.label`)}</Text>
|
||||||
|
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
|
||||||
|
</Group>
|
||||||
<DraggableList
|
<DraggableList
|
||||||
items={Array.from(value).map((v: any) => ({
|
items={Array.from(value).map((v: any) => ({
|
||||||
data: v,
|
data: v,
|
||||||
|
|||||||
51
src/components/InfoCard/InfoCard.tsx
Normal file
51
src/components/InfoCard/InfoCard.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
DefaultMantineColor,
|
||||||
|
HoverCard,
|
||||||
|
HoverCardProps,
|
||||||
|
SystemProp,
|
||||||
|
useMantineTheme,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { Link, RichTextEditor, RichTextEditorProps } from '@mantine/tiptap';
|
||||||
|
import { IconInfoCircle } from '@tabler/icons-react';
|
||||||
|
import { useEditor } from '@tiptap/react';
|
||||||
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
|
interface InfoCardProps {
|
||||||
|
bg?: SystemProp<DefaultMantineColor>;
|
||||||
|
cardProp?: Partial<RichTextEditorProps>;
|
||||||
|
message: string;
|
||||||
|
link?: string;
|
||||||
|
hoverProp?: Partial<HoverCardProps>;
|
||||||
|
position?: HoverCardProps['position'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InfoCard = ({ bg, cardProp, message, link, hoverProp, position }: InfoCardProps) => {
|
||||||
|
const { colorScheme } = useMantineTheme();
|
||||||
|
const { t } = useTranslation('common');
|
||||||
|
const content = link? message + ` <a href=\"${link}\" target=\"_blank\">${t('seeMore')}</a>` : message;
|
||||||
|
const editor = useEditor({
|
||||||
|
content,
|
||||||
|
editable: false,
|
||||||
|
editorProps: { attributes: { style: 'padding: 0;' } },
|
||||||
|
extensions: [StarterKit, Link],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HoverCard position={position ?? 'top'} radius="md" withArrow withinPortal {...hoverProp}>
|
||||||
|
<HoverCard.Target>
|
||||||
|
<IconInfoCircle size="1.25rem" style={{ display: 'block', opacity: 0.5 }} />
|
||||||
|
</HoverCard.Target>
|
||||||
|
<HoverCard.Dropdown
|
||||||
|
bg={bg ?? colorScheme === 'light' ? 'gray.2' : 'dark.8'}
|
||||||
|
maw={400}
|
||||||
|
px="10px"
|
||||||
|
py="5px"
|
||||||
|
>
|
||||||
|
<RichTextEditor editor={editor} style={{ border: '0' }} {...cardProp}>
|
||||||
|
<RichTextEditor.Content bg="transparent" />
|
||||||
|
</RichTextEditor>
|
||||||
|
</HoverCard.Dropdown>
|
||||||
|
</HoverCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -104,6 +104,8 @@ export class PlexClient {
|
|||||||
return 'movie';
|
return 'movie';
|
||||||
case 'episode':
|
case 'episode':
|
||||||
return 'video';
|
return 'video';
|
||||||
|
case 'track':
|
||||||
|
return 'audio';
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
createStyles,
|
createStyles,
|
||||||
useMantineTheme,
|
useMantineTheme,
|
||||||
InputProps,
|
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import {
|
import {
|
||||||
@@ -54,6 +53,8 @@ const definition = defineWidget({
|
|||||||
name: {
|
name: {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
|
info: true,
|
||||||
|
infoLink: "https://homarr.dev/docs/widgets/bookmarks/",
|
||||||
},
|
},
|
||||||
items: {
|
items: {
|
||||||
type: 'draggable-editable-list',
|
type: 'draggable-editable-list',
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export type IWidget<TKey extends string, TDefinition extends IWidgetDefinition>
|
|||||||
type MakeLessSpecific<T> = T extends boolean ? boolean : T;
|
type MakeLessSpecific<T> = T extends boolean ? boolean : T;
|
||||||
|
|
||||||
// Types of options that can be specified for the widget edit modal
|
// Types of options that can be specified for the widget edit modal
|
||||||
export type IWidgetOptionValue =
|
export type IWidgetOptionValue = (
|
||||||
| IMultiSelectOptionValue
|
| IMultiSelectOptionValue
|
||||||
| ISwitchOptionValue
|
| ISwitchOptionValue
|
||||||
| ITextInputOptionValue
|
| ITextInputOptionValue
|
||||||
@@ -41,7 +41,8 @@ export type IWidgetOptionValue =
|
|||||||
| IDraggableListInputValue
|
| IDraggableListInputValue
|
||||||
| IDraggableEditableListInputValue<any>
|
| IDraggableEditableListInputValue<any>
|
||||||
| IMultipleTextInputOptionValue
|
| IMultipleTextInputOptionValue
|
||||||
| ILocationOptionValue;
|
| ILocationOptionValue
|
||||||
|
) & ICommonWidgetOptions;
|
||||||
|
|
||||||
// Interface for data type
|
// Interface for data type
|
||||||
interface DataType {
|
interface DataType {
|
||||||
@@ -49,6 +50,11 @@ interface DataType {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ICommonWidgetOptions {
|
||||||
|
info?: boolean;
|
||||||
|
infoLink?: string;
|
||||||
|
};
|
||||||
|
|
||||||
// will show a multi-select with specified data
|
// will show a multi-select with specified data
|
||||||
export type IMultiSelectOptionValue = {
|
export type IMultiSelectOptionValue = {
|
||||||
type: 'multi-select';
|
type: 'multi-select';
|
||||||
@@ -96,6 +102,7 @@ export type ISliderInputOptionValue = {
|
|||||||
inputProps?: Partial<SliderProps>;
|
inputProps?: Partial<SliderProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// will show a custom location selector
|
||||||
type ILocationOptionValue = {
|
type ILocationOptionValue = {
|
||||||
type: 'location';
|
type: 'location';
|
||||||
defaultValue: { latitude: number; longitude: number };
|
defaultValue: { latitude: number; longitude: number };
|
||||||
|
|||||||
Reference in New Issue
Block a user