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:
@@ -16,6 +16,7 @@ import {
|
||||
} from "@homarr/db/schema/sqlite";
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { getPermissionsWithParents, widgetKinds } from "@homarr/definitions";
|
||||
import type { BoardItemAdvancedOptions } from "@homarr/validation";
|
||||
import { createSectionSchema, sharedItemSchema, validation, z } from "@homarr/validation";
|
||||
|
||||
import { zodUnionFromArray } from "../../../validation/src/enums";
|
||||
@@ -229,6 +230,7 @@ export const boardRouter = createTRPCRouter({
|
||||
xOffset: item.xOffset,
|
||||
yOffset: item.yOffset,
|
||||
options: superjson.stringify(item.options),
|
||||
advancedOptions: superjson.stringify(item.advancedOptions),
|
||||
sectionId: item.sectionId,
|
||||
})),
|
||||
);
|
||||
@@ -515,6 +517,7 @@ const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, use
|
||||
items: section.items.map((item) => ({
|
||||
...item,
|
||||
integrations: item.integrations.map((item) => item.integration),
|
||||
advancedOptions: superjson.parse<BoardItemAdvancedOptions>(item.advancedOptions),
|
||||
options: superjson.parse<Record<string, unknown>>(item.options),
|
||||
})),
|
||||
}),
|
||||
|
||||
@@ -664,6 +664,7 @@ describe("saveBoard should save full board", () => {
|
||||
width: 1,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
advancedOptions: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -724,6 +725,7 @@ describe("saveBoard should save full board", () => {
|
||||
width: 1,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
advancedOptions: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -837,6 +839,7 @@ describe("saveBoard should save full board", () => {
|
||||
width: 1,
|
||||
xOffset: 3,
|
||||
yOffset: 2,
|
||||
advancedOptions: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -905,6 +908,7 @@ describe("saveBoard should save full board", () => {
|
||||
width: 1,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
advancedOptions: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1018,6 +1022,7 @@ describe("saveBoard should save full board", () => {
|
||||
width: 2,
|
||||
xOffset: 7,
|
||||
yOffset: 5,
|
||||
advancedOptions: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
1
packages/db/migrations/mysql/0002_flimsy_deathbird.sql
Normal file
1
packages/db/migrations/mysql/0002_flimsy_deathbird.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `item` ADD `advanced_options` text DEFAULT ('{"json": {}}') NOT NULL;
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "e7a373e1-9f36-4910-9f2b-ac6fd5e79145",
|
||||
"prevId": "ba2dd885-4e7f-4a45-99a0-7b45cbd0a5c2",
|
||||
"id": "4e382d0d-a432-4953-bd5e-04f3f33e26a4",
|
||||
"prevId": "fdeaf6eb-cd62-4fa5-9b38-d7f80a60db9f",
|
||||
"tables": {
|
||||
"account": {
|
||||
"name": "account",
|
||||
@@ -910,6 +910,14 @@
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "('{\"json\": {}}')"
|
||||
},
|
||||
"advanced_options": {
|
||||
"name": "advanced_options",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "('{\"json\": {}}')"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
@@ -991,40 +999,6 @@
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"serverSetting": {
|
||||
"name": "serverSetting",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "('{\"json\": {}}')"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"serverSetting_key": {
|
||||
"name": "serverSetting_key",
|
||||
"columns": ["key"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"serverSetting_key_unique": {
|
||||
"name": "serverSetting_key_unique",
|
||||
"columns": ["key"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"name": "session",
|
||||
"columns": {
|
||||
|
||||
1208
packages/db/migrations/mysql/meta/0003_snapshot.json
Normal file
1208
packages/db/migrations/mysql/meta/0003_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -19,8 +19,15 @@
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "5",
|
||||
"when": 1715980459023,
|
||||
"tag": "0002_flimsy_deathbird",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "5",
|
||||
"when": 1716148439439,
|
||||
"tag": "0002_freezing_black_panther",
|
||||
"tag": "0003_freezing_black_panther",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
1
packages/db/migrations/sqlite/0002_cooing_sumo.sql
Normal file
1
packages/db/migrations/sqlite/0002_cooing_sumo.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `item` ADD `advanced_options` text DEFAULT '{"json": {}}' NOT NULL;
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "b72fe407-31bc-4dd0-8c36-dbb8e42ef708",
|
||||
"prevId": "2ed0ffc3-8612-42e7-bd8e-f5f8f3338a39",
|
||||
"id": "5ad60251-8450-437d-9081-a456884120d2",
|
||||
"prevId": "0575873a-9e10-4480-8d7d-c47198622c22",
|
||||
"tables": {
|
||||
"account": {
|
||||
"name": "account",
|
||||
@@ -877,6 +877,14 @@
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'{\"json\": {}}'"
|
||||
},
|
||||
"advanced_options": {
|
||||
"name": "advanced_options",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'{\"json\": {}}'"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
@@ -948,36 +956,6 @@
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"serverSetting": {
|
||||
"name": "serverSetting",
|
||||
"columns": {
|
||||
"key": {
|
||||
"name": "key",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'{\"json\": {}}'"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"serverSetting_key_unique": {
|
||||
"name": "serverSetting_key_unique",
|
||||
"columns": ["key"],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"session": {
|
||||
"name": "session",
|
||||
"columns": {
|
||||
|
||||
1152
packages/db/migrations/sqlite/meta/0003_snapshot.json
Normal file
1152
packages/db/migrations/sqlite/meta/0003_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -19,8 +19,15 @@
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "6",
|
||||
"when": 1715973963014,
|
||||
"tag": "0002_cooing_sumo",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "6",
|
||||
"when": 1716148434186,
|
||||
"tag": "0002_adorable_raider",
|
||||
"tag": "0003_adorable_raider",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -243,6 +243,7 @@ export const items = mysqlTable("item", {
|
||||
width: int("width").notNull(),
|
||||
height: int("height").notNull(),
|
||||
options: text("options").default('{"json": {}}').notNull(), // empty superjson object
|
||||
advancedOptions: text("advanced_options").default('{"json": {}}').notNull(), // empty superjson object
|
||||
});
|
||||
|
||||
export const apps = mysqlTable("app", {
|
||||
|
||||
@@ -246,6 +246,7 @@ export const items = sqliteTable("item", {
|
||||
width: int("width").notNull(),
|
||||
height: int("height").notNull(),
|
||||
options: text("options").default('{"json": {}}').notNull(), // empty superjson object
|
||||
advancedOptions: text("advanced_options").default('{"json": {}}').notNull(), // empty superjson object
|
||||
});
|
||||
|
||||
export const apps = sqliteTable("app", {
|
||||
|
||||
@@ -103,7 +103,6 @@ const ActiveModal = ({ modal, state, handleCloseModal }: ActiveModalProps) => {
|
||||
<Modal
|
||||
key={modal.id}
|
||||
zIndex={getDefaultZIndex("modal") + 1}
|
||||
display={modal.id === state.current?.id ? undefined : "none"}
|
||||
style={{
|
||||
userSelect: modal.id === state.current?.id ? undefined : "none",
|
||||
}}
|
||||
@@ -112,6 +111,9 @@ const ActiveModal = ({ modal, state, handleCloseModal }: ActiveModalProps) => {
|
||||
fontSize: "1.25rem",
|
||||
fontWeight: 500,
|
||||
},
|
||||
inner: {
|
||||
display: modal.id === state.current?.id ? undefined : "none",
|
||||
},
|
||||
}}
|
||||
trapFocus={modal.id === state.current?.id}
|
||||
{...otherModalProps}
|
||||
|
||||
@@ -469,6 +469,10 @@ export default {
|
||||
multiSelect: {
|
||||
placeholder: "Pick one or more values",
|
||||
},
|
||||
multiText: {
|
||||
placeholder: "Add more values",
|
||||
addLabel: `Add {value}`,
|
||||
},
|
||||
select: {
|
||||
placeholder: "Pick value",
|
||||
badge: {
|
||||
@@ -594,10 +598,17 @@ export default {
|
||||
},
|
||||
edit: {
|
||||
title: "Edit item",
|
||||
advancedOptions: {
|
||||
label: "Advanced options",
|
||||
title: "Advanced item options",
|
||||
},
|
||||
field: {
|
||||
integrations: {
|
||||
label: "Integrations",
|
||||
},
|
||||
customCssClasses: {
|
||||
label: "Custom css classes",
|
||||
},
|
||||
},
|
||||
},
|
||||
remove: {
|
||||
@@ -944,7 +955,13 @@ export default {
|
||||
label: "Opacity",
|
||||
},
|
||||
customCss: {
|
||||
label: "Custom CSS",
|
||||
label: "Custom css for this board",
|
||||
description: "Further, customize your dashboard using CSS, only recommended for experienced users",
|
||||
customClassesAlert: {
|
||||
title: "Custom classes",
|
||||
description:
|
||||
"You can add custom classes to your board items in the advanced options of each item and use them in the custom CSS above.",
|
||||
},
|
||||
},
|
||||
columnCount: {
|
||||
label: "Column count",
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@homarr/log": "workspace:^0.1.0"
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
||||
@@ -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";
|
||||
|
||||
98
packages/ui/src/components/text-multi-select.tsx
Normal file
98
packages/ui/src/components/text-multi-select.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -18,4 +18,10 @@ export const validation = {
|
||||
icons: iconsSchemas,
|
||||
};
|
||||
|
||||
export { createSectionSchema, sharedItemSchema, type BoardItemIntegration } from "./shared";
|
||||
export {
|
||||
createSectionSchema,
|
||||
sharedItemSchema,
|
||||
itemAdvancedOptionsSchema,
|
||||
type BoardItemIntegration,
|
||||
type BoardItemAdvancedOptions,
|
||||
} from "./shared";
|
||||
|
||||
@@ -13,6 +13,12 @@ export const integrationSchema = z.object({
|
||||
|
||||
export type BoardItemIntegration = z.infer<typeof integrationSchema>;
|
||||
|
||||
export const itemAdvancedOptionsSchema = z.object({
|
||||
customCssClasses: z.array(z.string()).default([]),
|
||||
});
|
||||
|
||||
export type BoardItemAdvancedOptions = z.infer<typeof itemAdvancedOptionsSchema>;
|
||||
|
||||
export const sharedItemSchema = z.object({
|
||||
id: z.string(),
|
||||
xOffset: z.number(),
|
||||
@@ -20,6 +26,7 @@ export const sharedItemSchema = z.object({
|
||||
height: z.number(),
|
||||
width: z.number(),
|
||||
integrations: z.array(integrationSchema),
|
||||
advancedOptions: itemAdvancedOptionsSchema,
|
||||
});
|
||||
|
||||
export const commonItemSchema = z
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/prismjs": "^1.26.4",
|
||||
"@types/video.js": "^7.3.58",
|
||||
"eslint": "^8.57.0",
|
||||
"typescript": "^5.4.5"
|
||||
@@ -40,17 +41,15 @@
|
||||
"@homarr/form": "workspace:^0.1.0",
|
||||
"@homarr/modals": "workspace:^0.1.0",
|
||||
"@homarr/notifications": "workspace:^0.1.0",
|
||||
"@homarr/spotlight": "workspace:^0.1.0",
|
||||
"@homarr/redis": "workspace:^0.1.0",
|
||||
"@homarr/spotlight": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@tiptap/extension-link": "^2.4.0",
|
||||
"@tiptap/react": "^2.4.0",
|
||||
"@tiptap/starter-kit": "^2.4.0",
|
||||
"@tiptap/extension-color": "2.4.0",
|
||||
"@tiptap/extension-highlight": "2.4.0",
|
||||
"@tiptap/extension-image": "2.4.0",
|
||||
"@tiptap/extension-link": "^2.4.0",
|
||||
"@tiptap/extension-table": "2.4.0",
|
||||
"@tiptap/extension-table-cell": "2.4.0",
|
||||
"@tiptap/extension-table-header": "2.4.0",
|
||||
@@ -60,6 +59,10 @@
|
||||
"@tiptap/extension-text-align": "2.4.0",
|
||||
"@tiptap/extension-text-style": "2.4.0",
|
||||
"@tiptap/extension-underline": "2.4.0",
|
||||
"@tiptap/react": "^2.4.0",
|
||||
"@tiptap/starter-kit": "^2.4.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"react-simple-code-editor": "^0.13.1",
|
||||
"video.js": "^8.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@ import { createFormContext } from "@homarr/form";
|
||||
|
||||
import type { WidgetEditModalState } from "../modals/widget-edit-modal";
|
||||
|
||||
export const [FormProvider, useFormContext, useForm] = createFormContext<WidgetEditModalState>();
|
||||
export const [FormProvider, useFormContext, useForm] =
|
||||
createFormContext<Omit<WidgetEditModalState, "advancedOptions">>();
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Group, Stack } from "@mantine/core";
|
||||
|
||||
import { useForm } from "@homarr/form";
|
||||
import { createModal } from "@homarr/modals";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { TextMultiSelect } from "@homarr/ui";
|
||||
import type { BoardItemAdvancedOptions } from "@homarr/validation";
|
||||
|
||||
interface InnerProps {
|
||||
advancedOptions: BoardItemAdvancedOptions;
|
||||
onSuccess: (options: BoardItemAdvancedOptions) => void;
|
||||
}
|
||||
|
||||
export const WidgetAdvancedOptionsModal = createModal<InnerProps>(({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const form = useForm({
|
||||
initialValues: innerProps.advancedOptions,
|
||||
});
|
||||
const handleSubmit = (values: BoardItemAdvancedOptions) => {
|
||||
innerProps.onSuccess(values);
|
||||
actions.closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextMultiSelect
|
||||
label={t("item.edit.field.customCssClasses.label")}
|
||||
{...form.getInputProps("customCssClasses")}
|
||||
/>
|
||||
<Group justify="end">
|
||||
<Button onClick={actions.closeModal} variant="subtle" color="gray">
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" color="teal">
|
||||
{t("common.action.saveChanges")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
}).withOptions({
|
||||
defaultTitle(t) {
|
||||
return t("item.edit.advancedOptions.title");
|
||||
},
|
||||
size: "lg",
|
||||
transitionProps: {
|
||||
duration: 0,
|
||||
},
|
||||
});
|
||||
@@ -1,21 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button, Group, Stack } from "@mantine/core";
|
||||
|
||||
import type { WidgetKind } from "@homarr/definitions";
|
||||
import { createModal } from "@homarr/modals";
|
||||
import { createModal, useModalAction } from "@homarr/modals";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { BoardItemIntegration } from "@homarr/validation";
|
||||
|
||||
import { widgetImports } from "..";
|
||||
import { getInputForType } from "../_inputs";
|
||||
import { FormProvider, useForm } from "../_inputs/form";
|
||||
import type { BoardItemAdvancedOptions } from "../../../validation/src/shared";
|
||||
import type { OptionsBuilderResult } from "../options";
|
||||
import type { IntegrationSelectOption } from "../widget-integration-select";
|
||||
import { WidgetIntegrationSelect } from "../widget-integration-select";
|
||||
import { WidgetAdvancedOptionsModal } from "./widget-advanced-options-modal";
|
||||
|
||||
export interface WidgetEditModalState {
|
||||
options: Record<string, unknown>;
|
||||
advancedOptions: BoardItemAdvancedOptions;
|
||||
integrations: BoardItemIntegration[];
|
||||
}
|
||||
|
||||
@@ -29,16 +33,21 @@ interface ModalProps<TSort extends WidgetKind> {
|
||||
|
||||
export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const [advancedOptions, setAdvancedOptions] = useState<BoardItemAdvancedOptions>(innerProps.value.advancedOptions);
|
||||
const form = useForm({
|
||||
initialValues: innerProps.value,
|
||||
});
|
||||
const { openModal } = useModalAction(WidgetAdvancedOptionsModal);
|
||||
|
||||
const { definition } = widgetImports[innerProps.kind];
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
innerProps.onSuccessfulEdit(values);
|
||||
innerProps.onSuccessfulEdit({
|
||||
...values,
|
||||
advancedOptions,
|
||||
});
|
||||
actions.closeModal();
|
||||
})}
|
||||
>
|
||||
@@ -60,13 +69,32 @@ export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, i
|
||||
|
||||
return <Input key={key} kind={innerProps.kind} property={key} options={value as never} />;
|
||||
})}
|
||||
<Group justify="right">
|
||||
<Button onClick={actions.closeModal} variant="subtle" color="gray">
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" color="teal">
|
||||
{t("common.action.saveChanges")}
|
||||
<Group justify="space-between">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() =>
|
||||
openModal({
|
||||
advancedOptions,
|
||||
onSuccess(options) {
|
||||
setAdvancedOptions(options);
|
||||
innerProps.onSuccessfulEdit({
|
||||
...innerProps.value,
|
||||
advancedOptions: options,
|
||||
});
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("item.edit.advancedOptions.label")}
|
||||
</Button>
|
||||
<Group justify="end" w={{ base: "100%", xs: "auto" }}>
|
||||
<Button onClick={actions.closeModal} variant="subtle" color="gray">
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" color="teal">
|
||||
{t("common.action.saveChanges")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Stack>
|
||||
</FormProvider>
|
||||
@@ -74,4 +102,8 @@ export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, i
|
||||
);
|
||||
}).withOptions({
|
||||
keepMounted: true,
|
||||
defaultTitle(t) {
|
||||
return t("item.edit.title");
|
||||
},
|
||||
size: "lg",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user