223 lines
6.4 KiB
TypeScript
223 lines
6.4 KiB
TypeScript
"use client";
|
|
|
|
import type { FocusEventHandler } from "react";
|
|
import {
|
|
Anchor,
|
|
Avatar,
|
|
CheckIcon,
|
|
CloseButton,
|
|
Combobox,
|
|
Group,
|
|
Pill,
|
|
PillsInput,
|
|
Stack,
|
|
Text,
|
|
useCombobox,
|
|
} from "@mantine/core";
|
|
|
|
import type { IntegrationKind } from "@homarr/definitions";
|
|
import { getIconUrl } from "@homarr/definitions";
|
|
import { useI18n } from "@homarr/translation/client";
|
|
import { Link } from "@homarr/ui";
|
|
|
|
import classes from "./widget-integration-select.module.css";
|
|
|
|
interface WidgetIntegrationSelectProps {
|
|
label: string;
|
|
onChange: (value: string[]) => void;
|
|
value?: string[];
|
|
error?: string;
|
|
onFocus?: FocusEventHandler<HTMLInputElement>;
|
|
onBlur?: FocusEventHandler<HTMLInputElement>;
|
|
canSelectMultiple?: boolean;
|
|
data: IntegrationSelectOption[];
|
|
withAsterisk?: boolean;
|
|
onOpenNewIntegration?: () => void;
|
|
}
|
|
export const WidgetIntegrationSelect = ({
|
|
data,
|
|
onChange,
|
|
value: valueProp,
|
|
label,
|
|
canSelectMultiple = true,
|
|
withAsterisk = false,
|
|
onOpenNewIntegration,
|
|
...props
|
|
}: WidgetIntegrationSelectProps) => {
|
|
const tItem = useI18n("item.edit.field.integrations");
|
|
const tIntegration = useI18n("widget.common.integration");
|
|
const tCommon = useI18n("common");
|
|
const multiSelectValues = valueProp ?? [];
|
|
|
|
const combobox = useCombobox({
|
|
onDropdownClose: () => combobox.resetSelectedOption(),
|
|
onDropdownOpen: () => combobox.updateSelectedOptionIndex("active"),
|
|
});
|
|
|
|
const handleValueSelect = (selectedValue: string) => {
|
|
if (multiSelectValues.includes(selectedValue)) {
|
|
onChange(multiSelectValues.filter((value) => value !== selectedValue));
|
|
} else if (canSelectMultiple) {
|
|
onChange([...multiSelectValues, selectedValue]);
|
|
} else {
|
|
onChange([selectedValue]);
|
|
}
|
|
|
|
if (!canSelectMultiple) {
|
|
combobox.closeDropdown();
|
|
}
|
|
};
|
|
|
|
const handleValueRemove = (valueToRemove: string) =>
|
|
onChange(multiSelectValues.filter((value) => value !== valueToRemove));
|
|
|
|
const values = multiSelectValues.map((item) => {
|
|
const option = data.find((integration) => integration.id === item);
|
|
if (!option) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<IntegrationPill
|
|
key={item}
|
|
option={option}
|
|
onRemove={() => handleValueRemove(item)}
|
|
showRemoveButton={canSelectMultiple}
|
|
removeLabel={tItem("removeLabel", { name: option.name })}
|
|
/>
|
|
);
|
|
});
|
|
|
|
const options = data.map((item) => {
|
|
return (
|
|
<Combobox.Option value={item.id} key={item.id} active={multiSelectValues.includes(item.id)}>
|
|
<Group gap="sm" align="center">
|
|
{multiSelectValues.includes(item.id) ? <CheckIcon size="var(--mantine-font-size-xs)" /> : null}
|
|
<Group gap={7} align="center">
|
|
<Avatar src={getIconUrl(item.kind)} size="sm" />
|
|
<Stack gap={0}>
|
|
<span>{item.name}</span>
|
|
<Text size="xs" c="dimmed">
|
|
{item.url}
|
|
</Text>
|
|
</Stack>
|
|
</Group>
|
|
</Group>
|
|
</Combobox.Option>
|
|
);
|
|
});
|
|
|
|
return (
|
|
<Combobox store={combobox} onOptionSubmit={handleValueSelect}>
|
|
<Combobox.DropdownTarget>
|
|
<PillsInput
|
|
inputWrapperOrder={["label", "input", "description", "error"]}
|
|
description={
|
|
onOpenNewIntegration ? (
|
|
<Text size="xs" span>
|
|
<Anchor size="xs" component="button" type="button" onClick={onOpenNewIntegration}>
|
|
{tCommon("action.create")}
|
|
</Anchor>
|
|
</Text>
|
|
) : (
|
|
<Text size="xs" span>
|
|
<Anchor size="xs" component={Link} target="_blank" href="/manage/integrations">
|
|
{tIntegration("manage")}
|
|
</Anchor>
|
|
</Text>
|
|
)
|
|
}
|
|
pointer
|
|
onClick={() => combobox.openDropdown()}
|
|
label={label}
|
|
withAsterisk={withAsterisk}
|
|
{...props}
|
|
>
|
|
<Pill.Group>
|
|
{values}
|
|
|
|
<Combobox.EventsTarget>
|
|
<PillsInput.Field
|
|
readOnly
|
|
aria-label={label}
|
|
placeholder={values.length === 0 ? tCommon("multiSelect.placeholder") : undefined}
|
|
onFocus={() => combobox.openDropdown()}
|
|
onBlur={() => combobox.closeDropdown()}
|
|
onKeyDown={(event) => {
|
|
if (["ArrowDown", "Enter", " "].includes(event.key)) {
|
|
event.preventDefault();
|
|
combobox.openDropdown();
|
|
return;
|
|
}
|
|
if (event.key === "Escape") {
|
|
combobox.closeDropdown();
|
|
return;
|
|
}
|
|
if (event.key === "Backspace" && canSelectMultiple && multiSelectValues.length > 0) {
|
|
event.preventDefault();
|
|
const lastValue = multiSelectValues.at(-1);
|
|
if (lastValue) handleValueRemove(lastValue);
|
|
}
|
|
}}
|
|
/>
|
|
</Combobox.EventsTarget>
|
|
</Pill.Group>
|
|
</PillsInput>
|
|
</Combobox.DropdownTarget>
|
|
|
|
<Combobox.Dropdown>
|
|
<Combobox.Options>
|
|
{options.length >= 1 ? (
|
|
options
|
|
) : (
|
|
<Text p={4} size="sm" ta="center" c="var(--mantine-color-dimmed)">
|
|
{tIntegration("noData")}
|
|
</Text>
|
|
)}
|
|
</Combobox.Options>
|
|
</Combobox.Dropdown>
|
|
</Combobox>
|
|
);
|
|
};
|
|
|
|
export interface IntegrationSelectOption {
|
|
id: string;
|
|
name: string;
|
|
url: string;
|
|
kind: IntegrationKind;
|
|
permissions?: {
|
|
hasUseAccess: boolean;
|
|
hasInteractAccess: boolean;
|
|
hasFullAccess: boolean;
|
|
};
|
|
}
|
|
|
|
interface IntegrationPillProps {
|
|
option: IntegrationSelectOption;
|
|
onRemove: () => void;
|
|
showRemoveButton: boolean;
|
|
removeLabel: string;
|
|
}
|
|
|
|
const IntegrationPill = ({ option, onRemove, showRemoveButton, removeLabel }: IntegrationPillProps) => (
|
|
<Group align="center" wrap="nowrap" gap={0} className={classes.pill} mih={24} pr={!showRemoveButton ? 10 : undefined}>
|
|
<Avatar src={getIconUrl(option.kind)} size={14} mr={6} />
|
|
<Text span size="xs" lh={1} fw={500}>
|
|
{option.name}
|
|
</Text>
|
|
{showRemoveButton && (
|
|
<CloseButton
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
onRemove();
|
|
}}
|
|
aria-label={removeLabel}
|
|
variant="transparent"
|
|
color="gray"
|
|
size={24}
|
|
iconSize={14}
|
|
/>
|
|
)}
|
|
</Group>
|
|
);
|