feat: add notes for creation of apps and integrations in widget edit modal (#1297)

* feat: add notes for creation of apps and integrations in widget edit modal

* fix: unit test failing when with-description flag missing
This commit is contained in:
Meier Lukas
2024-10-16 21:44:28 +02:00
committed by GitHub
parent a0cc31da70
commit e99fd64882
4 changed files with 59 additions and 9 deletions

View File

@@ -576,6 +576,7 @@ export default {
tryAgain: "Try again", tryAgain: "Try again",
loading: "Loading", loading: "Loading",
}, },
here: "here",
iconPicker: { iconPicker: {
label: "Icon URL", label: "Icon URL",
header: "Type name or objects to filter for icons... Homarr will search through {countIcons} icons for you.", header: "Type name or objects to filter for icons... Homarr will search through {countIcons} icons for you.",
@@ -1157,6 +1158,14 @@ export default {
}, },
}, },
}, },
integration: {
noData: "No integration found",
description: "Click {here} to create a new integration",
},
app: {
noData: "No app found",
description: "Click {here} to create a new app",
},
error: { error: {
action: { action: {
logs: "Check logs for more details", logs: "Check logs for more details",

View File

@@ -1,19 +1,22 @@
"use client"; "use client";
import { memo, useMemo } from "react"; import { memo, useMemo } from "react";
import Link from "next/link";
import type { SelectProps } from "@mantine/core"; import type { SelectProps } from "@mantine/core";
import { Group, Loader, Select } from "@mantine/core"; import { Anchor, Group, Loader, Select, Text } from "@mantine/core";
import { IconCheck } from "@tabler/icons-react"; import { IconCheck } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useI18n } from "@homarr/translation/client";
import type { CommonWidgetInputProps } from "./common"; import type { CommonWidgetInputProps } from "./common";
import { useWidgetInputTranslation } from "./common"; import { useWidgetInputTranslation } from "./common";
import { useFormContext } from "./form"; import { useFormContext } from "./form";
export const WidgetAppInput = ({ property, kind, options }: CommonWidgetInputProps<"app">) => { export const WidgetAppInput = ({ property, kind }: CommonWidgetInputProps<"app">) => {
const t = useWidgetInputTranslation(kind, property); const t = useI18n();
const tInput = useWidgetInputTranslation(kind, property);
const form = useFormContext(); const form = useFormContext();
const { data: apps, isPending } = clientApi.app.selectable.useQuery(); const { data: apps, isPending } = clientApi.app.selectable.useQuery();
@@ -24,10 +27,11 @@ export const WidgetAppInput = ({ property, kind, options }: CommonWidgetInputPro
return ( return (
<Select <Select
label={t("label")} label={tInput("label")}
searchable searchable
limit={10} limit={10}
leftSection={<MemoizedLeftSection isPending={isPending} currentApp={currentApp} />} leftSection={<MemoizedLeftSection isPending={isPending} currentApp={currentApp} />}
nothingFoundMessage={t("widget.common.app.noData")}
renderOption={renderSelectOption} renderOption={renderSelectOption}
data={ data={
apps?.map((app) => ({ apps?.map((app) => ({
@@ -36,7 +40,18 @@ export const WidgetAppInput = ({ property, kind, options }: CommonWidgetInputPro
iconUrl: app.iconUrl, iconUrl: app.iconUrl,
})) ?? [] })) ?? []
} }
description={options.withDescription ? t("description") : undefined} inputWrapperOrder={["label", "input", "description", "error"]}
description={
<Text size="xs">
{t("widget.common.app.description", {
here: (
<Anchor size="xs" component={Link} target="_blank" href="/manage/apps/new">
{t("common.here")}
</Anchor>
),
})}
</Text>
}
{...form.getInputProps(`options.${property}`)} {...form.getInputProps(`options.${property}`)}
/> />
); );

View File

@@ -104,10 +104,10 @@ const optionsFactory = {
values: [] as string[], values: [] as string[],
validate: input?.validate, validate: input?.validate,
}), }),
app: (input?: Omit<CommonInput<string>, "defaultValue">) => ({ app: () => ({
type: "app" as const, type: "app" as const,
defaultValue: "", defaultValue: "",
withDescription: input?.withDescription ?? false, withDescription: false,
}), }),
}; };

View File

@@ -1,7 +1,9 @@
"use client"; "use client";
import type { FocusEventHandler } from "react"; import type { FocusEventHandler } from "react";
import Link from "next/link";
import { import {
Anchor,
Avatar, Avatar,
CheckIcon, CheckIcon,
CloseButton, CloseButton,
@@ -86,7 +88,23 @@ export const WidgetIntegrationSelect = ({
return ( return (
<Combobox store={combobox} onOptionSubmit={handleValueSelect} withinPortal={false}> <Combobox store={combobox} onOptionSubmit={handleValueSelect} withinPortal={false}>
<Combobox.DropdownTarget> <Combobox.DropdownTarget>
<PillsInput pointer onClick={() => combobox.toggleDropdown()} {...props}> <PillsInput
inputWrapperOrder={["label", "input", "description", "error"]}
description={
<Text size="xs">
{t("widget.common.integration.description", {
here: (
<Anchor size="xs" component={Link} target="_blank" href="/manage/integrations">
{t("common.here")}
</Anchor>
),
})}
</Text>
}
pointer
onClick={() => combobox.toggleDropdown()}
{...props}
>
<Pill.Group> <Pill.Group>
{values.length > 0 ? values : <Input.Placeholder>{t("common.multiSelect.placeholder")}</Input.Placeholder>} {values.length > 0 ? values : <Input.Placeholder>{t("common.multiSelect.placeholder")}</Input.Placeholder>}
@@ -108,7 +126,15 @@ export const WidgetIntegrationSelect = ({
</Combobox.DropdownTarget> </Combobox.DropdownTarget>
<Combobox.Dropdown> <Combobox.Dropdown>
<Combobox.Options>{options}</Combobox.Options> <Combobox.Options>
{options.length >= 1 ? (
options
) : (
<Text p={4} size="sm" ta="center" c="var(--mantine-color-dimmed)">
{t("widget.common.integration.noData")}
</Text>
)}
</Combobox.Options>
</Combobox.Dropdown> </Combobox.Dropdown>
</Combobox> </Combobox>
); );