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/next": "^6.0.0",
|
||||
"@mantine/notifications": "^6.0.0",
|
||||
"@mantine/tiptap": "^6.0.17",
|
||||
"@next-auth/prisma-adapter": "^1.0.5",
|
||||
"@nivo/core": "^0.83.0",
|
||||
"@nivo/line": "^0.83.0",
|
||||
@@ -54,6 +55,10 @@
|
||||
"@tanstack/react-query": "^4.2.1",
|
||||
"@tanstack/react-query-devtools": "^4.24.4",
|
||||
"@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/next": "^10.29.1",
|
||||
"@trpc/react-query": "^10.29.1",
|
||||
|
||||
@@ -35,5 +35,6 @@
|
||||
"small": "small",
|
||||
"medium": "medium",
|
||||
"large": "large"
|
||||
}
|
||||
},
|
||||
"seeMore": "See more..."
|
||||
}
|
||||
@@ -6,9 +6,7 @@
|
||||
"title": "Bookmark settings",
|
||||
"name": {
|
||||
"label": "Widget Title",
|
||||
"placeholder": {
|
||||
"label" : "Leave empty to keep the title hidden"
|
||||
}
|
||||
"info": "Leave empty to keep the title hidden."
|
||||
},
|
||||
"items": {
|
||||
"label": "Items"
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
Center,
|
||||
Flex,
|
||||
Group,
|
||||
Loader,
|
||||
Modal,
|
||||
@@ -19,6 +20,7 @@ import { useDisclosure } from '@mantine/hooks';
|
||||
import { IconAlertTriangle, IconClick, IconListSearch } from '@tabler/icons-react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
import { InfoCard } from '~/components/InfoCard/InfoCard';
|
||||
import { City } from '~/server/api/routers/weather';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
@@ -29,6 +31,8 @@ type LocationSelectionProps = {
|
||||
propName: string;
|
||||
value: any;
|
||||
handleChange: (key: string, value: IntegrationOptionsValueType) => void;
|
||||
info?: boolean;
|
||||
infoLink?: string;
|
||||
};
|
||||
|
||||
export const LocationSelection = ({
|
||||
@@ -36,6 +40,8 @@ export const LocationSelection = ({
|
||||
propName: key,
|
||||
value,
|
||||
handleChange,
|
||||
info,
|
||||
infoLink,
|
||||
}: LocationSelectionProps) => {
|
||||
const { t } = useTranslation('widgets/location');
|
||||
const [query, setQuery] = useState(value.name ?? '');
|
||||
@@ -57,7 +63,15 @@ export const LocationSelection = ({
|
||||
<>
|
||||
<Card>
|
||||
<Stack spacing="xs">
|
||||
<Flex direction="row" justify="space-between" wrap="nowrap">
|
||||
<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">
|
||||
<TextInput
|
||||
|
||||
@@ -19,10 +19,10 @@ import { IconAlertTriangle, IconPlaylistX, IconPlus } from '@tabler/icons-react'
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { FC, useState } from 'react';
|
||||
|
||||
import { InfoCard } from '../../../InfoCard/InfoCard';
|
||||
import { useConfigContext } from '../../../../config/provider';
|
||||
import { useConfigStore } from '../../../../config/store';
|
||||
import { mapObject } from '../../../../tools/client/objects';
|
||||
import { useColorTheme } from '../../../../tools/color';
|
||||
import Widgets from '../../../../widgets';
|
||||
import type { IDraggableListInputValue, IWidgetOptionValue } from '../../../../widgets/widgets';
|
||||
import { IWidget } from '../../../../widgets/widgets';
|
||||
@@ -135,70 +135,99 @@ const WidgetOptionTypeSwitch: FC<{
|
||||
handleChange: (key: string, value: IntegrationOptionsValueType) => void;
|
||||
}> = ({ option, widgetId, propName: key, value, handleChange }) => {
|
||||
const { t } = useTranslation([`modules/${widgetId}`, 'common']);
|
||||
const { primaryColor } = useColorTheme();
|
||||
const info = option.info ?? false;
|
||||
const link = option.infoLink ?? undefined;
|
||||
|
||||
switch (option.type) {
|
||||
case 'switch':
|
||||
return (
|
||||
<Group align="center" spacing="sm">
|
||||
<Switch
|
||||
label={t(`descriptor.settings.${key}.label`)}
|
||||
checked={value as boolean}
|
||||
onChange={(ev) => handleChange(key, ev.currentTarget.checked)}
|
||||
{...option.inputProps}
|
||||
/>
|
||||
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
|
||||
</Group>
|
||||
);
|
||||
case 'text':
|
||||
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
|
||||
color={primaryColor}
|
||||
label={t(`descriptor.settings.${key}.label`)}
|
||||
value={value as string}
|
||||
onChange={(ev) => handleChange(key, ev.currentTarget.value)}
|
||||
{...option.inputProps}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
case 'multi-select':
|
||||
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
|
||||
color={primaryColor}
|
||||
data={option.data}
|
||||
label={t(`descriptor.settings.${key}.label`)}
|
||||
value={value as string[]}
|
||||
defaultValue={option.defaultValue}
|
||||
onChange={(v) => handleChange(key, v)}
|
||||
withinPortal
|
||||
{...option.inputProps}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
case 'select':
|
||||
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
|
||||
color={primaryColor}
|
||||
defaultValue={option.defaultValue}
|
||||
data={option.data}
|
||||
label={t(`descriptor.settings.${key}.label`)}
|
||||
value={value as string}
|
||||
onChange={(v) => handleChange(key, v ?? option.defaultValue)}
|
||||
withinPortal
|
||||
{...option.inputProps}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
case 'number':
|
||||
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
|
||||
color={primaryColor}
|
||||
label={t(`descriptor.settings.${key}.label`)}
|
||||
value={value as number}
|
||||
onChange={(v) => handleChange(key, v!)}
|
||||
{...option.inputProps}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
case 'slider':
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Text>{t(`descriptor.settings.${key}.label`)}</Text>
|
||||
<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>
|
||||
<Slider
|
||||
color={primaryColor}
|
||||
label={value}
|
||||
value={value as number}
|
||||
min={option.min}
|
||||
max={option.max}
|
||||
step={option.step}
|
||||
onChange={(v) => handleChange(key, v)}
|
||||
{...option.inputProps}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
@@ -209,6 +238,8 @@ const WidgetOptionTypeSwitch: FC<{
|
||||
value={value}
|
||||
handleChange={handleChange}
|
||||
widgetId={widgetId}
|
||||
info={info}
|
||||
infoLink={link}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -237,7 +268,10 @@ const WidgetOptionTypeSwitch: FC<{
|
||||
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Group align="center" spacing="sm">
|
||||
<Text>{t(`descriptor.settings.${key}.label`)}</Text>
|
||||
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
|
||||
</Group>
|
||||
<StaticDraggableList
|
||||
value={typedVal}
|
||||
onChange={(v) => handleChange(key, v)}
|
||||
@@ -262,9 +296,13 @@ const WidgetOptionTypeSwitch: FC<{
|
||||
);
|
||||
case 'multiple-text':
|
||||
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
|
||||
data={value.map((name: any) => ({ value: name, label: name }))}
|
||||
label={t(`descriptor.settings.${key}.label`)}
|
||||
description={t(`descriptor.settings.${key}.description`)}
|
||||
defaultValue={value as string[]}
|
||||
withinPortal
|
||||
@@ -278,12 +316,16 @@ const WidgetOptionTypeSwitch: FC<{
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
case 'draggable-editable-list':
|
||||
const { t: translateDraggableList } = useTranslation('widgets/draggable-list');
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Group align="center" spacing="sm">
|
||||
<Text>{t(`descriptor.settings.${key}.label`)}</Text>
|
||||
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
|
||||
</Group>
|
||||
<DraggableList
|
||||
items={Array.from(value).map((v: any) => ({
|
||||
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';
|
||||
case 'episode':
|
||||
return 'video';
|
||||
case 'track':
|
||||
return 'audio';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
Title,
|
||||
createStyles,
|
||||
useMantineTheme,
|
||||
InputProps,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import {
|
||||
@@ -54,6 +53,8 @@ const definition = defineWidget({
|
||||
name: {
|
||||
type: 'text',
|
||||
defaultValue: '',
|
||||
info: true,
|
||||
infoLink: "https://homarr.dev/docs/widgets/bookmarks/",
|
||||
},
|
||||
items: {
|
||||
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;
|
||||
|
||||
// Types of options that can be specified for the widget edit modal
|
||||
export type IWidgetOptionValue =
|
||||
export type IWidgetOptionValue = (
|
||||
| IMultiSelectOptionValue
|
||||
| ISwitchOptionValue
|
||||
| ITextInputOptionValue
|
||||
@@ -41,7 +41,8 @@ export type IWidgetOptionValue =
|
||||
| IDraggableListInputValue
|
||||
| IDraggableEditableListInputValue<any>
|
||||
| IMultipleTextInputOptionValue
|
||||
| ILocationOptionValue;
|
||||
| ILocationOptionValue
|
||||
) & ICommonWidgetOptions;
|
||||
|
||||
// Interface for data type
|
||||
interface DataType {
|
||||
@@ -49,6 +50,11 @@ interface DataType {
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface ICommonWidgetOptions {
|
||||
info?: boolean;
|
||||
infoLink?: string;
|
||||
};
|
||||
|
||||
// will show a multi-select with specified data
|
||||
export type IMultiSelectOptionValue = {
|
||||
type: 'multi-select';
|
||||
@@ -96,6 +102,7 @@ export type ISliderInputOptionValue = {
|
||||
inputProps?: Partial<SliderProps>;
|
||||
};
|
||||
|
||||
// will show a custom location selector
|
||||
type ILocationOptionValue = {
|
||||
type: 'location';
|
||||
defaultValue: { latitude: number; longitude: number };
|
||||
|
||||
Reference in New Issue
Block a user