Merge branch 'dev' into feature/add-basic-authentication

This commit is contained in:
Meier Lukas
2023-08-06 15:02:57 +02:00
10 changed files with 1228 additions and 382 deletions

View File

@@ -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",

View File

@@ -35,5 +35,6 @@
"small": "small", "small": "small",
"medium": "medium", "medium": "medium",
"large": "large" "large": "large"
} },
"seeMore": "See more..."
} }

View File

@@ -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"

View File

@@ -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">
<Title order={5}>{t(`modules/${widgetId}:descriptor.settings.${key}.label`)}</Title> <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"> <Group noWrap align="end">
<TextInput <TextInput

View File

@@ -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 (
<Switch <Group align="center" spacing="sm">
label={t(`descriptor.settings.${key}.label`)} <Switch
checked={value as boolean} label={t(`descriptor.settings.${key}.label`)}
onChange={(ev) => handleChange(key, ev.currentTarget.checked)} 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': case 'text':
return ( return (
<TextInput <Stack spacing={0}>
color={primaryColor} <Group align="center" spacing="sm">
label={t(`descriptor.settings.${key}.label`)} <Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text>
value={value as string} {info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
onChange={(ev) => handleChange(key, ev.currentTarget.value)} </Group>
/> <TextInput
value={value as string}
onChange={(ev) => handleChange(key, ev.currentTarget.value)}
{...option.inputProps}
/>
</Stack>
); );
case 'multi-select': case 'multi-select':
return ( return (
<MultiSelect <Stack spacing={0}>
color={primaryColor} <Group align="center" spacing="sm">
data={option.data} <Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text>
label={t(`descriptor.settings.${key}.label`)} {info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
value={value as string[]} </Group>
defaultValue={option.defaultValue} <MultiSelect
onChange={(v) => handleChange(key, v)} data={option.data}
/> value={value as string[]}
defaultValue={option.defaultValue}
onChange={(v) => handleChange(key, v)}
withinPortal
{...option.inputProps}
/>
</Stack>
); );
case 'select': case 'select':
return ( return (
<Select <Stack spacing={0}>
color={primaryColor} <Group align="center" spacing="sm">
defaultValue={option.defaultValue} <Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text>
data={option.data} {info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
label={t(`descriptor.settings.${key}.label`)} </Group>
value={value as string} <Select
onChange={(v) => handleChange(key, v ?? option.defaultValue)} defaultValue={option.defaultValue}
/> data={option.data}
value={value as string}
onChange={(v) => handleChange(key, v ?? option.defaultValue)}
withinPortal
{...option.inputProps}
/>
</Stack>
); );
case 'number': case 'number':
return ( return (
<NumberInput <Stack spacing={0}>
color={primaryColor} <Group align="center" spacing="sm">
label={t(`descriptor.settings.${key}.label`)} <Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text>
value={value as number} {info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
onChange={(v) => handleChange(key, v!)} </Group>
{...option.inputProps} <NumberInput
/> value={value as number}
onChange={(v) => handleChange(key, v!)}
{...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">
<Text>{t(`descriptor.settings.${key}.label`)}</Text> <Group align="center" spacing="sm">
<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,28 +296,36 @@ const WidgetOptionTypeSwitch: FC<{
); );
case 'multiple-text': case 'multiple-text':
return ( return (
<MultiSelect <Stack spacing={0}>
data={value.map((name: any) => ({ value: name, label: name }))} <Group align="center" spacing="sm">
label={t(`descriptor.settings.${key}.label`)} <Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text>
description={t(`descriptor.settings.${key}.description`)} {info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>}
defaultValue={value as string[]} </Group>
withinPortal <MultiSelect
searchable data={value.map((name: any) => ({ value: name, label: name }))}
creatable description={t(`descriptor.settings.${key}.description`)}
getCreateLabel={(query) => t('common:createItem', { item: query })} defaultValue={value as string[]}
onChange={(values) => withinPortal
handleChange( searchable
key, creatable
values.map((item: string) => item) getCreateLabel={(query) => t('common:createItem', { item: query })}
) onChange={(values) =>
} handleChange(
/> key,
values.map((item: string) => item)
)
}
/>
</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">
<Text>{t(`descriptor.settings.${key}.label`)}</Text> <Group align="center" spacing="sm">
<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,

View 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>
);
};

View File

@@ -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;
} }

View File

@@ -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',

View File

@@ -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 };

1359
yarn.lock

File diff suppressed because it is too large Load Diff