feat: add custom css for board and custom classes in advanced options for items (#512)

* feat: add custom css for board and custom classes in advanced options for items

* chore: add mysql migration

* fix: test not working

* fix: format issues

* fix: typecheck issue

* fix: build issue

* chore: add missing translations

* fix: merge issues related to migrations

* fix: format issues

* fix: merge issue with migration

* fix: format issue
This commit is contained in:
Meier Lukas
2024-05-19 23:01:26 +02:00
committed by GitHub
parent f1b1ec59ec
commit 26b1c4a319
35 changed files with 3080 additions and 97 deletions

View File

@@ -5,3 +5,4 @@ export { UserAvatar } from "./user-avatar";
export { UserAvatarGroup } from "./user-avatar-group";
export { TablePagination } from "./table-pagination";
export { SearchInput } from "./search-input";
export { TextMultiSelect } from "./text-multi-select";

View File

@@ -0,0 +1,98 @@
"use client";
import type { FocusEventHandler } from "react";
import { useState } from "react";
import { Combobox, Group, Pill, PillsInput, Text, useCombobox } from "@mantine/core";
import { IconPlus } from "@tabler/icons-react";
import { useI18n } from "@homarr/translation/client";
interface TextMultiSelectProps {
label: string;
value?: string[];
onChange: (value: string[]) => void;
onFocus?: FocusEventHandler;
onBlur?: FocusEventHandler;
error?: string;
}
export const TextMultiSelect = ({ label, value = [], onChange, onBlur, onFocus, error }: TextMultiSelectProps) => {
const t = useI18n();
const combobox = useCombobox({
onDropdownClose: () => combobox.resetSelectedOption(),
onDropdownOpen: () => combobox.updateSelectedOptionIndex("active"),
});
const [search, setSearch] = useState("");
const exactOptionMatch = value.some((item) => item === search);
const handleValueSelect = (selectedValue: string) => {
setSearch("");
if (selectedValue === "$create") {
onChange([...value, search]);
} else {
onChange(value.filter((filterValue) => filterValue !== selectedValue));
}
};
const handleValueRemove = (removedValue: string) =>
onChange(value.filter((filterValue) => filterValue !== removedValue));
const values = value.map((item) => (
<Pill key={item} withRemoveButton onRemove={() => handleValueRemove(item)}>
{item}
</Pill>
));
return (
<Combobox store={combobox} onOptionSubmit={handleValueSelect} withinPortal={false}>
<Combobox.DropdownTarget>
<PillsInput label={label} error={error} onClick={() => combobox.openDropdown()}>
<Pill.Group>
{values}
<Combobox.EventsTarget>
<PillsInput.Field
onFocus={(event) => {
onFocus?.(event);
combobox.openDropdown();
}}
onBlur={(event) => {
onBlur?.(event);
combobox.closeDropdown();
}}
value={search}
placeholder={t("common.multiText.placeholder")}
onChange={(event) => {
combobox.updateSelectedOptionIndex();
setSearch(event.currentTarget.value);
}}
onKeyDown={(event) => {
if (event.key === "Backspace" && search.length === 0) {
event.preventDefault();
handleValueRemove(value.at(-1)!);
}
}}
/>
</Combobox.EventsTarget>
</Pill.Group>
</PillsInput>
</Combobox.DropdownTarget>
{!exactOptionMatch && search.trim().length > 0 && (
<Combobox.Dropdown>
<Combobox.Options>
<Combobox.Option value="$create">
<Group>
<IconPlus size={12} />
<Text size="sm">{t("common.multiText.addLabel", { value: search })}</Text>
</Group>
</Combobox.Option>
</Combobox.Options>
</Combobox.Dropdown>
)}
</Combobox>
);
};