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:
@@ -0,0 +1,9 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRequiredBoard } from "./_context";
|
||||||
|
|
||||||
|
export const CustomCss = () => {
|
||||||
|
const board = useRequiredBoard();
|
||||||
|
|
||||||
|
return <style>{board.customCss}</style>;
|
||||||
|
};
|
||||||
@@ -1,7 +1,91 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
// TODO: add some sort of store (maybe directory on GitHub)
|
import { Alert, Button, Group, Input, Stack } from "@mantine/core";
|
||||||
|
import { highlight, languages } from "prismjs";
|
||||||
|
import Editor from "react-simple-code-editor";
|
||||||
|
|
||||||
export const CustomCssSettingsContent = () => {
|
import "~/styles/prismjs.scss";
|
||||||
return null;
|
|
||||||
|
import { IconInfoCircle } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
import { useForm } from "@homarr/form";
|
||||||
|
import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
|
import type { Board } from "../../_types";
|
||||||
|
import { useSavePartialSettingsMutation } from "./_shared";
|
||||||
|
import classes from "./customcss.module.css";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
board: Board;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomCssSettingsContent = ({ board }: Props) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const customCssT = useScopedI18n("board.field.customCss");
|
||||||
|
const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board);
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
customCss: board.customCss ?? "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit((values) => {
|
||||||
|
savePartialSettings({
|
||||||
|
id: board.id,
|
||||||
|
...values,
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<CustomCssInput {...form.getInputProps("customCss")} />
|
||||||
|
|
||||||
|
<Alert variant="light" color="cyan" title={customCssT("customClassesAlert.title")} icon={<IconInfoCircle />}>
|
||||||
|
{customCssT("customClassesAlert.description")}
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<Group justify="end">
|
||||||
|
<Button type="submit" loading={isPending} color="teal">
|
||||||
|
{t("common.action.saveChanges")}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CustomCssInputProps {
|
||||||
|
value?: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomCssInput = ({ value, onChange }: CustomCssInputProps) => {
|
||||||
|
const customCssT = useScopedI18n("board.field.customCss");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input.Wrapper
|
||||||
|
label={customCssT("label")}
|
||||||
|
labelProps={{
|
||||||
|
htmlFor: "custom-css",
|
||||||
|
}}
|
||||||
|
description={customCssT("description")}
|
||||||
|
inputWrapperOrder={["label", "description", "input", "error"]}
|
||||||
|
>
|
||||||
|
<div className={classes.codeEditorRoot}>
|
||||||
|
<Editor
|
||||||
|
textareaId="custom-css"
|
||||||
|
onValueChange={onChange}
|
||||||
|
value={value ?? ""}
|
||||||
|
highlight={(code) => highlight(code, languages.extend("css", {}), "css")}
|
||||||
|
padding={10}
|
||||||
|
style={{
|
||||||
|
fontFamily: '"Fira code", "Fira Mono", monospace',
|
||||||
|
fontSize: 12,
|
||||||
|
minHeight: 250,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Input.Wrapper>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
.codeEditorFooter {
|
||||||
|
border-bottom-left-radius: var(--mantine-radius-sm);
|
||||||
|
border-bottom-right-radius: var(--mantine-radius-sm);
|
||||||
|
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-7));
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeEditorRoot {
|
||||||
|
margin-top: 4px;
|
||||||
|
border-color: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4));
|
||||||
|
border-width: 1px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: var(--mantine-radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeEditor {
|
||||||
|
background-color: light-dark(white, var(--mantine-color-dark-6));
|
||||||
|
font-size: var(--mantine-font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.codeEditor ::placeholder {
|
||||||
|
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
|
||||||
|
}
|
||||||
@@ -84,7 +84,7 @@ export default async function BoardSettingsPage({ params, searchParams }: Props)
|
|||||||
<ColorSettingsContent board={board} />
|
<ColorSettingsContent board={board} />
|
||||||
</AccordionItemFor>
|
</AccordionItemFor>
|
||||||
<AccordionItemFor value="customCss" icon={IconFileTypeCss}>
|
<AccordionItemFor value="customCss" icon={IconFileTypeCss}>
|
||||||
<CustomCssSettingsContent />
|
<CustomCssSettingsContent board={board} />
|
||||||
</AccordionItemFor>
|
</AccordionItemFor>
|
||||||
{hasFullAccess && (
|
{hasFullAccess && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ClientShell } from "~/components/layout/shell";
|
|||||||
import type { Board } from "./_types";
|
import type { Board } from "./_types";
|
||||||
import { BoardProvider } from "./(content)/_context";
|
import { BoardProvider } from "./(content)/_context";
|
||||||
import type { Params } from "./(content)/_creator";
|
import type { Params } from "./(content)/_creator";
|
||||||
|
import { CustomCss } from "./(content)/_custom-css";
|
||||||
import { BoardMantineProvider } from "./(content)/_theme";
|
import { BoardMantineProvider } from "./(content)/_theme";
|
||||||
|
|
||||||
interface CreateBoardLayoutProps<TParams extends Params> {
|
interface CreateBoardLayoutProps<TParams extends Params> {
|
||||||
@@ -44,6 +45,7 @@ export const createBoardLayout = <TParams extends Params>({
|
|||||||
<GlobalItemServerDataRunner board={initialBoard} shouldRun={isBoardContentPage}>
|
<GlobalItemServerDataRunner board={initialBoard} shouldRun={isBoardContentPage}>
|
||||||
<BoardProvider initialBoard={initialBoard}>
|
<BoardProvider initialBoard={initialBoard}>
|
||||||
<BoardMantineProvider>
|
<BoardMantineProvider>
|
||||||
|
<CustomCss />
|
||||||
<ClientShell hasNavigation={false}>
|
<ClientShell hasNavigation={false}>
|
||||||
<MainHeader
|
<MainHeader
|
||||||
logo={<BoardLogoWithTitle size="md" hideTitleOnMobile />}
|
logo={<BoardLogoWithTitle size="md" hideTitleOnMobile />}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
|
|||||||
import { useModalAction } from "@homarr/modals";
|
import { useModalAction } from "@homarr/modals";
|
||||||
import { showSuccessNotification } from "@homarr/notifications";
|
import { showSuccessNotification } from "@homarr/notifications";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import type { BoardItemIntegration } from "@homarr/validation";
|
import type { BoardItemAdvancedOptions, BoardItemIntegration } from "@homarr/validation";
|
||||||
import {
|
import {
|
||||||
loadWidgetDynamic,
|
loadWidgetDynamic,
|
||||||
reduceWidgetOptionsWithDefaultValues,
|
reduceWidgetOptionsWithDefaultValues,
|
||||||
@@ -42,9 +42,13 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie
|
|||||||
const [state, setState] = useState<{
|
const [state, setState] = useState<{
|
||||||
options: Record<string, unknown>;
|
options: Record<string, unknown>;
|
||||||
integrations: BoardItemIntegration[];
|
integrations: BoardItemIntegration[];
|
||||||
|
advancedOptions: BoardItemAdvancedOptions;
|
||||||
}>({
|
}>({
|
||||||
options: reduceWidgetOptionsWithDefaultValues(kind, {}),
|
options: reduceWidgetOptionsWithDefaultValues(kind, {}),
|
||||||
integrations: [],
|
integrations: [],
|
||||||
|
advancedOptions: {
|
||||||
|
customCssClasses: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleOpenEditWidgetModal = useCallback(() => {
|
const handleOpenEditWidgetModal = useCallback(() => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCallback } from "react";
|
|||||||
|
|
||||||
import { createId } from "@homarr/db/client";
|
import { createId } from "@homarr/db/client";
|
||||||
import type { WidgetKind } from "@homarr/definitions";
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
import type { BoardItemIntegration } from "@homarr/validation";
|
import type { BoardItemAdvancedOptions, BoardItemIntegration } from "@homarr/validation";
|
||||||
|
|
||||||
import type { EmptySection, Item } from "~/app/[locale]/boards/_types";
|
import type { EmptySection, Item } from "~/app/[locale]/boards/_types";
|
||||||
import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client";
|
import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client";
|
||||||
@@ -31,6 +31,11 @@ interface UpdateItemOptions {
|
|||||||
newOptions: Record<string, unknown>;
|
newOptions: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface UpdateItemAdvancedOptions {
|
||||||
|
itemId: string;
|
||||||
|
newAdvancedOptions: BoardItemAdvancedOptions;
|
||||||
|
}
|
||||||
|
|
||||||
interface UpdateItemIntegrations {
|
interface UpdateItemIntegrations {
|
||||||
itemId: string;
|
itemId: string;
|
||||||
newIntegrations: BoardItemIntegration[];
|
newIntegrations: BoardItemIntegration[];
|
||||||
@@ -59,6 +64,9 @@ export const useItemActions = () => {
|
|||||||
width: 1,
|
width: 1,
|
||||||
height: 1,
|
height: 1,
|
||||||
integrations: [],
|
integrations: [],
|
||||||
|
advancedOptions: {
|
||||||
|
customCssClasses: [],
|
||||||
|
},
|
||||||
} satisfies Omit<Item, "kind" | "yOffset" | "xOffset"> & {
|
} satisfies Omit<Item, "kind" | "yOffset" | "xOffset"> & {
|
||||||
kind: WidgetKind;
|
kind: WidgetKind;
|
||||||
};
|
};
|
||||||
@@ -91,7 +99,7 @@ export const useItemActions = () => {
|
|||||||
return {
|
return {
|
||||||
...section,
|
...section,
|
||||||
items: section.items.map((item) => {
|
items: section.items.map((item) => {
|
||||||
// Return same item if item is not the one we're moving
|
// Return same item if item is not the one we're changing
|
||||||
if (item.id !== itemId) return item;
|
if (item.id !== itemId) return item;
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
@@ -106,6 +114,33 @@ export const useItemActions = () => {
|
|||||||
[updateBoard],
|
[updateBoard],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const updateItemAdvancedOptions = useCallback(
|
||||||
|
({ itemId, newAdvancedOptions }: UpdateItemAdvancedOptions) => {
|
||||||
|
updateBoard((previous) => {
|
||||||
|
if (!previous) return previous;
|
||||||
|
return {
|
||||||
|
...previous,
|
||||||
|
sections: previous.sections.map((section) => {
|
||||||
|
// Return same section if item is not in it
|
||||||
|
if (!section.items.some((item) => item.id === itemId)) return section;
|
||||||
|
return {
|
||||||
|
...section,
|
||||||
|
items: section.items.map((item) => {
|
||||||
|
// Return same item if item is not the one we're changing
|
||||||
|
if (item.id !== itemId) return item;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
advancedOptions: newAdvancedOptions,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[updateBoard],
|
||||||
|
);
|
||||||
|
|
||||||
const updateItemIntegrations = useCallback(
|
const updateItemIntegrations = useCallback(
|
||||||
({ itemId, newIntegrations }: UpdateItemIntegrations) => {
|
({ itemId, newIntegrations }: UpdateItemIntegrations) => {
|
||||||
updateBoard((previous) => {
|
updateBoard((previous) => {
|
||||||
@@ -224,6 +259,7 @@ export const useItemActions = () => {
|
|||||||
moveItemToSection,
|
moveItemToSection,
|
||||||
removeItem,
|
removeItem,
|
||||||
updateItemOptions,
|
updateItemOptions,
|
||||||
|
updateItemAdvancedOptions,
|
||||||
updateItemIntegrations,
|
updateItemIntegrations,
|
||||||
createItem,
|
createItem,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -70,7 +70,11 @@ const BoardItem = ({ refs, item, opacity }: ItemProps) => {
|
|||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={combineClasses(classes.itemCard, "grid-stack-item-content")}
|
className={combineClasses(
|
||||||
|
classes.itemCard,
|
||||||
|
"grid-stack-item-content",
|
||||||
|
item.advancedOptions.customCssClasses.join(" "),
|
||||||
|
)}
|
||||||
withBorder
|
withBorder
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
@@ -123,7 +127,7 @@ const ItemMenu = ({ offset, item }: { offset: number; item: Item }) => {
|
|||||||
const { openModal } = useModalAction(WidgetEditModal);
|
const { openModal } = useModalAction(WidgetEditModal);
|
||||||
const { openConfirmModal } = useConfirmModal();
|
const { openConfirmModal } = useConfirmModal();
|
||||||
const isEditMode = useAtomValue(editModeAtom);
|
const isEditMode = useAtomValue(editModeAtom);
|
||||||
const { updateItemOptions, updateItemIntegrations, removeItem } = useItemActions();
|
const { updateItemOptions, updateItemAdvancedOptions, updateItemIntegrations, removeItem } = useItemActions();
|
||||||
const { data: integrationData, isPending } = clientApi.integration.all.useQuery();
|
const { data: integrationData, isPending } = clientApi.integration.all.useQuery();
|
||||||
const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]);
|
const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]);
|
||||||
|
|
||||||
@@ -133,14 +137,19 @@ const ItemMenu = ({ offset, item }: { offset: number; item: Item }) => {
|
|||||||
openModal({
|
openModal({
|
||||||
kind: item.kind,
|
kind: item.kind,
|
||||||
value: {
|
value: {
|
||||||
|
advancedOptions: item.advancedOptions,
|
||||||
options: item.options,
|
options: item.options,
|
||||||
integrations: item.integrations,
|
integrations: item.integrations,
|
||||||
},
|
},
|
||||||
onSuccessfulEdit: ({ options, integrations }) => {
|
onSuccessfulEdit: ({ options, integrations, advancedOptions }) => {
|
||||||
updateItemOptions({
|
updateItemOptions({
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
newOptions: options,
|
newOptions: options,
|
||||||
});
|
});
|
||||||
|
updateItemAdvancedOptions({
|
||||||
|
itemId: item.id,
|
||||||
|
newAdvancedOptions: advancedOptions,
|
||||||
|
});
|
||||||
updateItemIntegrations({
|
updateItemIntegrations({
|
||||||
itemId: item.id,
|
itemId: item.id,
|
||||||
newIntegrations: integrations,
|
newIntegrations: integrations,
|
||||||
|
|||||||
225
apps/nextjs/src/styles/prismjs.scss
Normal file
225
apps/nextjs/src/styles/prismjs.scss
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
[data-mantine-color-scheme="light"] {
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
color: #000;
|
||||||
|
background: 0 0;
|
||||||
|
text-shadow: 0 1px #fff;
|
||||||
|
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
-webkit-hyphens: none;
|
||||||
|
-moz-hyphens: none;
|
||||||
|
-ms-hyphens: none;
|
||||||
|
hyphens: none;
|
||||||
|
}
|
||||||
|
code[class*="language-"] ::-moz-selection,
|
||||||
|
code[class*="language-"]::-moz-selection,
|
||||||
|
pre[class*="language-"] ::-moz-selection,
|
||||||
|
pre[class*="language-"]::-moz-selection {
|
||||||
|
text-shadow: none;
|
||||||
|
background: #b3d4fc;
|
||||||
|
}
|
||||||
|
code[class*="language-"] ::selection,
|
||||||
|
code[class*="language-"]::selection,
|
||||||
|
pre[class*="language-"] ::selection,
|
||||||
|
pre[class*="language-"]::selection {
|
||||||
|
text-shadow: none;
|
||||||
|
background: #b3d4fc;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pre[class*="language-"] {
|
||||||
|
padding: 1em;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
:not(pre) > code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
background: #f5f2f0;
|
||||||
|
}
|
||||||
|
:not(pre) > code[class*="language-"] {
|
||||||
|
padding: 0.1em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
.token.cdata,
|
||||||
|
.token.comment,
|
||||||
|
.token.doctype,
|
||||||
|
.token.prolog {
|
||||||
|
color: #708090;
|
||||||
|
}
|
||||||
|
.token.punctuation {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.token.namespace {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.token.boolean,
|
||||||
|
.token.constant,
|
||||||
|
.token.deleted,
|
||||||
|
.token.number,
|
||||||
|
.token.property,
|
||||||
|
.token.symbol,
|
||||||
|
.token.tag {
|
||||||
|
color: #905;
|
||||||
|
}
|
||||||
|
.token.attr-name,
|
||||||
|
.token.builtin,
|
||||||
|
.token.char,
|
||||||
|
.token.inserted,
|
||||||
|
.token.selector,
|
||||||
|
.token.string {
|
||||||
|
color: #690;
|
||||||
|
}
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string,
|
||||||
|
.token.entity,
|
||||||
|
.token.operator,
|
||||||
|
.token.url {
|
||||||
|
color: #9a6e3a;
|
||||||
|
background: hsla(0, 0%, 100%, 0.5);
|
||||||
|
}
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.keyword {
|
||||||
|
color: #07a;
|
||||||
|
}
|
||||||
|
.token.class-name,
|
||||||
|
.token.function {
|
||||||
|
color: #dd4a68;
|
||||||
|
}
|
||||||
|
.token.important,
|
||||||
|
.token.regex,
|
||||||
|
.token.variable {
|
||||||
|
color: #e90;
|
||||||
|
}
|
||||||
|
.token.bold,
|
||||||
|
.token.important {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.token.entity {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-mantine-color-scheme="dark"] {
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
color: #fff;
|
||||||
|
background: 0 0;
|
||||||
|
text-shadow: 0 -0.1em 0.2em #000;
|
||||||
|
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
-webkit-hyphens: none;
|
||||||
|
-moz-hyphens: none;
|
||||||
|
-ms-hyphens: none;
|
||||||
|
hyphens: none;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:not(pre) > code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
background: #4c3f33;
|
||||||
|
}
|
||||||
|
pre[class*="language-"] {
|
||||||
|
padding: 1em;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
overflow: auto;
|
||||||
|
border: 0.3em solid #7a6651;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
box-shadow: 1px 1px 0.5em #000 inset;
|
||||||
|
}
|
||||||
|
:not(pre) > code[class*="language-"] {
|
||||||
|
padding: 0.15em 0.2em 0.05em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
border: 0.13em solid #7a6651;
|
||||||
|
box-shadow: 1px 1px 0.3em -0.1em #000 inset;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
.token.cdata,
|
||||||
|
.token.comment,
|
||||||
|
.token.doctype,
|
||||||
|
.token.prolog {
|
||||||
|
color: #997f66;
|
||||||
|
}
|
||||||
|
.token.punctuation {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.token.namespace {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.token.boolean,
|
||||||
|
.token.constant,
|
||||||
|
.token.number,
|
||||||
|
.token.property,
|
||||||
|
.token.symbol,
|
||||||
|
.token.tag {
|
||||||
|
color: #d1939e;
|
||||||
|
}
|
||||||
|
.token.attr-name,
|
||||||
|
.token.builtin,
|
||||||
|
.token.char,
|
||||||
|
.token.inserted,
|
||||||
|
.token.selector,
|
||||||
|
.token.string {
|
||||||
|
color: #bce051;
|
||||||
|
}
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string,
|
||||||
|
.token.entity,
|
||||||
|
.token.operator,
|
||||||
|
.token.url,
|
||||||
|
.token.variable {
|
||||||
|
color: #f4b73d;
|
||||||
|
}
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.keyword {
|
||||||
|
color: #d1939e;
|
||||||
|
}
|
||||||
|
.token.important,
|
||||||
|
.token.regex {
|
||||||
|
color: #e90;
|
||||||
|
}
|
||||||
|
.token.bold,
|
||||||
|
.token.important {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.token.entity {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
.token.deleted {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
} from "@homarr/db/schema/sqlite";
|
} from "@homarr/db/schema/sqlite";
|
||||||
import type { WidgetKind } from "@homarr/definitions";
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
import { getPermissionsWithParents, widgetKinds } from "@homarr/definitions";
|
import { getPermissionsWithParents, widgetKinds } from "@homarr/definitions";
|
||||||
|
import type { BoardItemAdvancedOptions } from "@homarr/validation";
|
||||||
import { createSectionSchema, sharedItemSchema, validation, z } from "@homarr/validation";
|
import { createSectionSchema, sharedItemSchema, validation, z } from "@homarr/validation";
|
||||||
|
|
||||||
import { zodUnionFromArray } from "../../../validation/src/enums";
|
import { zodUnionFromArray } from "../../../validation/src/enums";
|
||||||
@@ -229,6 +230,7 @@ export const boardRouter = createTRPCRouter({
|
|||||||
xOffset: item.xOffset,
|
xOffset: item.xOffset,
|
||||||
yOffset: item.yOffset,
|
yOffset: item.yOffset,
|
||||||
options: superjson.stringify(item.options),
|
options: superjson.stringify(item.options),
|
||||||
|
advancedOptions: superjson.stringify(item.advancedOptions),
|
||||||
sectionId: item.sectionId,
|
sectionId: item.sectionId,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
@@ -515,6 +517,7 @@ const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, use
|
|||||||
items: section.items.map((item) => ({
|
items: section.items.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
integrations: item.integrations.map((item) => item.integration),
|
integrations: item.integrations.map((item) => item.integration),
|
||||||
|
advancedOptions: superjson.parse<BoardItemAdvancedOptions>(item.advancedOptions),
|
||||||
options: superjson.parse<Record<string, unknown>>(item.options),
|
options: superjson.parse<Record<string, unknown>>(item.options),
|
||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -664,6 +664,7 @@ describe("saveBoard should save full board", () => {
|
|||||||
width: 1,
|
width: 1,
|
||||||
xOffset: 0,
|
xOffset: 0,
|
||||||
yOffset: 0,
|
yOffset: 0,
|
||||||
|
advancedOptions: {},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -724,6 +725,7 @@ describe("saveBoard should save full board", () => {
|
|||||||
width: 1,
|
width: 1,
|
||||||
xOffset: 0,
|
xOffset: 0,
|
||||||
yOffset: 0,
|
yOffset: 0,
|
||||||
|
advancedOptions: {},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -837,6 +839,7 @@ describe("saveBoard should save full board", () => {
|
|||||||
width: 1,
|
width: 1,
|
||||||
xOffset: 3,
|
xOffset: 3,
|
||||||
yOffset: 2,
|
yOffset: 2,
|
||||||
|
advancedOptions: {},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -905,6 +908,7 @@ describe("saveBoard should save full board", () => {
|
|||||||
width: 1,
|
width: 1,
|
||||||
xOffset: 0,
|
xOffset: 0,
|
||||||
yOffset: 0,
|
yOffset: 0,
|
||||||
|
advancedOptions: {},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -1018,6 +1022,7 @@ describe("saveBoard should save full board", () => {
|
|||||||
width: 2,
|
width: 2,
|
||||||
xOffset: 7,
|
xOffset: 7,
|
||||||
yOffset: 5,
|
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",
|
"version": "5",
|
||||||
"dialect": "mysql",
|
"dialect": "mysql",
|
||||||
"id": "e7a373e1-9f36-4910-9f2b-ac6fd5e79145",
|
"id": "4e382d0d-a432-4953-bd5e-04f3f33e26a4",
|
||||||
"prevId": "ba2dd885-4e7f-4a45-99a0-7b45cbd0a5c2",
|
"prevId": "fdeaf6eb-cd62-4fa5-9b38-d7f80a60db9f",
|
||||||
"tables": {
|
"tables": {
|
||||||
"account": {
|
"account": {
|
||||||
"name": "account",
|
"name": "account",
|
||||||
@@ -910,6 +910,14 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"default": "('{\"json\": {}}')"
|
"default": "('{\"json\": {}}')"
|
||||||
|
},
|
||||||
|
"advanced_options": {
|
||||||
|
"name": "advanced_options",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "('{\"json\": {}}')"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
@@ -991,40 +999,6 @@
|
|||||||
},
|
},
|
||||||
"uniqueConstraints": {}
|
"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": {
|
"session": {
|
||||||
"name": "session",
|
"name": "session",
|
||||||
"columns": {
|
"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,
|
"idx": 2,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
|
"when": 1715980459023,
|
||||||
|
"tag": "0002_flimsy_deathbird",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "5",
|
||||||
"when": 1716148439439,
|
"when": 1716148439439,
|
||||||
"tag": "0002_freezing_black_panther",
|
"tag": "0003_freezing_black_panther",
|
||||||
"breakpoints": true
|
"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",
|
"version": "6",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "b72fe407-31bc-4dd0-8c36-dbb8e42ef708",
|
"id": "5ad60251-8450-437d-9081-a456884120d2",
|
||||||
"prevId": "2ed0ffc3-8612-42e7-bd8e-f5f8f3338a39",
|
"prevId": "0575873a-9e10-4480-8d7d-c47198622c22",
|
||||||
"tables": {
|
"tables": {
|
||||||
"account": {
|
"account": {
|
||||||
"name": "account",
|
"name": "account",
|
||||||
@@ -877,6 +877,14 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"default": "'{\"json\": {}}'"
|
"default": "'{\"json\": {}}'"
|
||||||
|
},
|
||||||
|
"advanced_options": {
|
||||||
|
"name": "advanced_options",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": "'{\"json\": {}}'"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
@@ -948,36 +956,6 @@
|
|||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {}
|
"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": {
|
"session": {
|
||||||
"name": "session",
|
"name": "session",
|
||||||
"columns": {
|
"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,
|
"idx": 2,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
|
"when": 1715973963014,
|
||||||
|
"tag": "0002_cooing_sumo",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "6",
|
||||||
"when": 1716148434186,
|
"when": 1716148434186,
|
||||||
"tag": "0002_adorable_raider",
|
"tag": "0003_adorable_raider",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ export const items = mysqlTable("item", {
|
|||||||
width: int("width").notNull(),
|
width: int("width").notNull(),
|
||||||
height: int("height").notNull(),
|
height: int("height").notNull(),
|
||||||
options: text("options").default('{"json": {}}').notNull(), // empty superjson object
|
options: text("options").default('{"json": {}}').notNull(), // empty superjson object
|
||||||
|
advancedOptions: text("advanced_options").default('{"json": {}}').notNull(), // empty superjson object
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apps = mysqlTable("app", {
|
export const apps = mysqlTable("app", {
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ export const items = sqliteTable("item", {
|
|||||||
width: int("width").notNull(),
|
width: int("width").notNull(),
|
||||||
height: int("height").notNull(),
|
height: int("height").notNull(),
|
||||||
options: text("options").default('{"json": {}}').notNull(), // empty superjson object
|
options: text("options").default('{"json": {}}').notNull(), // empty superjson object
|
||||||
|
advancedOptions: text("advanced_options").default('{"json": {}}').notNull(), // empty superjson object
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apps = sqliteTable("app", {
|
export const apps = sqliteTable("app", {
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ const ActiveModal = ({ modal, state, handleCloseModal }: ActiveModalProps) => {
|
|||||||
<Modal
|
<Modal
|
||||||
key={modal.id}
|
key={modal.id}
|
||||||
zIndex={getDefaultZIndex("modal") + 1}
|
zIndex={getDefaultZIndex("modal") + 1}
|
||||||
display={modal.id === state.current?.id ? undefined : "none"}
|
|
||||||
style={{
|
style={{
|
||||||
userSelect: modal.id === state.current?.id ? undefined : "none",
|
userSelect: modal.id === state.current?.id ? undefined : "none",
|
||||||
}}
|
}}
|
||||||
@@ -112,6 +111,9 @@ const ActiveModal = ({ modal, state, handleCloseModal }: ActiveModalProps) => {
|
|||||||
fontSize: "1.25rem",
|
fontSize: "1.25rem",
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
},
|
},
|
||||||
|
inner: {
|
||||||
|
display: modal.id === state.current?.id ? undefined : "none",
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
trapFocus={modal.id === state.current?.id}
|
trapFocus={modal.id === state.current?.id}
|
||||||
{...otherModalProps}
|
{...otherModalProps}
|
||||||
|
|||||||
@@ -469,6 +469,10 @@ export default {
|
|||||||
multiSelect: {
|
multiSelect: {
|
||||||
placeholder: "Pick one or more values",
|
placeholder: "Pick one or more values",
|
||||||
},
|
},
|
||||||
|
multiText: {
|
||||||
|
placeholder: "Add more values",
|
||||||
|
addLabel: `Add {value}`,
|
||||||
|
},
|
||||||
select: {
|
select: {
|
||||||
placeholder: "Pick value",
|
placeholder: "Pick value",
|
||||||
badge: {
|
badge: {
|
||||||
@@ -594,10 +598,17 @@ export default {
|
|||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
title: "Edit item",
|
title: "Edit item",
|
||||||
|
advancedOptions: {
|
||||||
|
label: "Advanced options",
|
||||||
|
title: "Advanced item options",
|
||||||
|
},
|
||||||
field: {
|
field: {
|
||||||
integrations: {
|
integrations: {
|
||||||
label: "Integrations",
|
label: "Integrations",
|
||||||
},
|
},
|
||||||
|
customCssClasses: {
|
||||||
|
label: "Custom css classes",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
remove: {
|
remove: {
|
||||||
@@ -944,7 +955,13 @@ export default {
|
|||||||
label: "Opacity",
|
label: "Opacity",
|
||||||
},
|
},
|
||||||
customCss: {
|
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: {
|
columnCount: {
|
||||||
label: "Column count",
|
label: "Column count",
|
||||||
|
|||||||
@@ -29,7 +29,8 @@
|
|||||||
"typescript": "^5.4.5"
|
"typescript": "^5.4.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@homarr/log": "workspace:^0.1.0"
|
"@homarr/log": "workspace:^0.1.0",
|
||||||
|
"@homarr/translation": "workspace:^0.1.0"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ export { UserAvatar } from "./user-avatar";
|
|||||||
export { UserAvatarGroup } from "./user-avatar-group";
|
export { UserAvatarGroup } from "./user-avatar-group";
|
||||||
export { TablePagination } from "./table-pagination";
|
export { TablePagination } from "./table-pagination";
|
||||||
export { SearchInput } from "./search-input";
|
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,
|
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 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({
|
export const sharedItemSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
xOffset: z.number(),
|
xOffset: z.number(),
|
||||||
@@ -20,6 +26,7 @@ export const sharedItemSchema = z.object({
|
|||||||
height: z.number(),
|
height: z.number(),
|
||||||
width: z.number(),
|
width: z.number(),
|
||||||
integrations: z.array(integrationSchema),
|
integrations: z.array(integrationSchema),
|
||||||
|
advancedOptions: itemAdvancedOptionsSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const commonItemSchema = z
|
export const commonItemSchema = z
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||||
|
"@types/prismjs": "^1.26.4",
|
||||||
"@types/video.js": "^7.3.58",
|
"@types/video.js": "^7.3.58",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"typescript": "^5.4.5"
|
"typescript": "^5.4.5"
|
||||||
@@ -40,17 +41,15 @@
|
|||||||
"@homarr/form": "workspace:^0.1.0",
|
"@homarr/form": "workspace:^0.1.0",
|
||||||
"@homarr/modals": "workspace:^0.1.0",
|
"@homarr/modals": "workspace:^0.1.0",
|
||||||
"@homarr/notifications": "workspace:^0.1.0",
|
"@homarr/notifications": "workspace:^0.1.0",
|
||||||
"@homarr/spotlight": "workspace:^0.1.0",
|
|
||||||
"@homarr/redis": "workspace:^0.1.0",
|
"@homarr/redis": "workspace:^0.1.0",
|
||||||
|
"@homarr/spotlight": "workspace:^0.1.0",
|
||||||
"@homarr/translation": "workspace:^0.1.0",
|
"@homarr/translation": "workspace:^0.1.0",
|
||||||
"@homarr/ui": "workspace:^0.1.0",
|
"@homarr/ui": "workspace:^0.1.0",
|
||||||
"@homarr/validation": "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-color": "2.4.0",
|
||||||
"@tiptap/extension-highlight": "2.4.0",
|
"@tiptap/extension-highlight": "2.4.0",
|
||||||
"@tiptap/extension-image": "2.4.0",
|
"@tiptap/extension-image": "2.4.0",
|
||||||
|
"@tiptap/extension-link": "^2.4.0",
|
||||||
"@tiptap/extension-table": "2.4.0",
|
"@tiptap/extension-table": "2.4.0",
|
||||||
"@tiptap/extension-table-cell": "2.4.0",
|
"@tiptap/extension-table-cell": "2.4.0",
|
||||||
"@tiptap/extension-table-header": "2.4.0",
|
"@tiptap/extension-table-header": "2.4.0",
|
||||||
@@ -60,6 +59,10 @@
|
|||||||
"@tiptap/extension-text-align": "2.4.0",
|
"@tiptap/extension-text-align": "2.4.0",
|
||||||
"@tiptap/extension-text-style": "2.4.0",
|
"@tiptap/extension-text-style": "2.4.0",
|
||||||
"@tiptap/extension-underline": "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"
|
"video.js": "^8.12.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ import { createFormContext } from "@homarr/form";
|
|||||||
|
|
||||||
import type { WidgetEditModalState } from "../modals/widget-edit-modal";
|
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";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import { Button, Group, Stack } from "@mantine/core";
|
import { Button, Group, Stack } from "@mantine/core";
|
||||||
|
|
||||||
import type { WidgetKind } from "@homarr/definitions";
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
import { createModal } from "@homarr/modals";
|
import { createModal, useModalAction } from "@homarr/modals";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import type { BoardItemIntegration } from "@homarr/validation";
|
import type { BoardItemIntegration } from "@homarr/validation";
|
||||||
|
|
||||||
import { widgetImports } from "..";
|
import { widgetImports } from "..";
|
||||||
import { getInputForType } from "../_inputs";
|
import { getInputForType } from "../_inputs";
|
||||||
import { FormProvider, useForm } from "../_inputs/form";
|
import { FormProvider, useForm } from "../_inputs/form";
|
||||||
|
import type { BoardItemAdvancedOptions } from "../../../validation/src/shared";
|
||||||
import type { OptionsBuilderResult } from "../options";
|
import type { OptionsBuilderResult } from "../options";
|
||||||
import type { IntegrationSelectOption } from "../widget-integration-select";
|
import type { IntegrationSelectOption } from "../widget-integration-select";
|
||||||
import { WidgetIntegrationSelect } from "../widget-integration-select";
|
import { WidgetIntegrationSelect } from "../widget-integration-select";
|
||||||
|
import { WidgetAdvancedOptionsModal } from "./widget-advanced-options-modal";
|
||||||
|
|
||||||
export interface WidgetEditModalState {
|
export interface WidgetEditModalState {
|
||||||
options: Record<string, unknown>;
|
options: Record<string, unknown>;
|
||||||
|
advancedOptions: BoardItemAdvancedOptions;
|
||||||
integrations: BoardItemIntegration[];
|
integrations: BoardItemIntegration[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,16 +33,21 @@ interface ModalProps<TSort extends WidgetKind> {
|
|||||||
|
|
||||||
export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, innerProps }) => {
|
export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, innerProps }) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
const [advancedOptions, setAdvancedOptions] = useState<BoardItemAdvancedOptions>(innerProps.value.advancedOptions);
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: innerProps.value,
|
initialValues: innerProps.value,
|
||||||
});
|
});
|
||||||
|
const { openModal } = useModalAction(WidgetAdvancedOptionsModal);
|
||||||
|
|
||||||
const { definition } = widgetImports[innerProps.kind];
|
const { definition } = widgetImports[innerProps.kind];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={form.onSubmit((values) => {
|
onSubmit={form.onSubmit((values) => {
|
||||||
innerProps.onSuccessfulEdit(values);
|
innerProps.onSuccessfulEdit({
|
||||||
|
...values,
|
||||||
|
advancedOptions,
|
||||||
|
});
|
||||||
actions.closeModal();
|
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} />;
|
return <Input key={key} kind={innerProps.kind} property={key} options={value as never} />;
|
||||||
})}
|
})}
|
||||||
<Group justify="right">
|
<Group justify="space-between">
|
||||||
<Button onClick={actions.closeModal} variant="subtle" color="gray">
|
<Button
|
||||||
{t("common.action.cancel")}
|
variant="subtle"
|
||||||
</Button>
|
onClick={() =>
|
||||||
<Button type="submit" color="teal">
|
openModal({
|
||||||
{t("common.action.saveChanges")}
|
advancedOptions,
|
||||||
|
onSuccess(options) {
|
||||||
|
setAdvancedOptions(options);
|
||||||
|
innerProps.onSuccessfulEdit({
|
||||||
|
...innerProps.value,
|
||||||
|
advancedOptions: options,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("item.edit.advancedOptions.label")}
|
||||||
</Button>
|
</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>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
@@ -74,4 +102,8 @@ export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, i
|
|||||||
);
|
);
|
||||||
}).withOptions({
|
}).withOptions({
|
||||||
keepMounted: true,
|
keepMounted: true,
|
||||||
|
defaultTitle(t) {
|
||||||
|
return t("item.edit.title");
|
||||||
|
},
|
||||||
|
size: "lg",
|
||||||
});
|
});
|
||||||
|
|||||||
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@@ -798,6 +798,9 @@ importers:
|
|||||||
'@homarr/log':
|
'@homarr/log':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../log
|
version: link:../log
|
||||||
|
'@homarr/translation':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../translation
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@homarr/eslint-config':
|
'@homarr/eslint-config':
|
||||||
specifier: workspace:^0.2.0
|
specifier: workspace:^0.2.0
|
||||||
@@ -926,6 +929,12 @@ importers:
|
|||||||
'@tiptap/starter-kit':
|
'@tiptap/starter-kit':
|
||||||
specifier: ^2.4.0
|
specifier: ^2.4.0
|
||||||
version: 2.4.0(@tiptap/pm@2.2.4)
|
version: 2.4.0(@tiptap/pm@2.2.4)
|
||||||
|
prismjs:
|
||||||
|
specifier: ^1.29.0
|
||||||
|
version: 1.29.0
|
||||||
|
react-simple-code-editor:
|
||||||
|
specifier: ^0.13.1
|
||||||
|
version: 0.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
video.js:
|
video.js:
|
||||||
specifier: ^8.12.0
|
specifier: ^8.12.0
|
||||||
version: 8.12.0
|
version: 8.12.0
|
||||||
@@ -939,6 +948,9 @@ importers:
|
|||||||
'@homarr/tsconfig':
|
'@homarr/tsconfig':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../../tooling/typescript
|
version: link:../../tooling/typescript
|
||||||
|
'@types/prismjs':
|
||||||
|
specifier: ^1.26.4
|
||||||
|
version: 1.26.4
|
||||||
'@types/video.js':
|
'@types/video.js':
|
||||||
specifier: ^7.3.58
|
specifier: ^7.3.58
|
||||||
version: 7.3.58
|
version: 7.3.58
|
||||||
@@ -2367,6 +2379,9 @@ packages:
|
|||||||
'@types/object.pick@1.3.4':
|
'@types/object.pick@1.3.4':
|
||||||
resolution: {integrity: sha512-5PjwB0uP2XDp3nt5u5NJAG2DORHIRClPzWT/TTZhJ2Ekwe8M5bA9tvPdi9NO/n2uvu2/ictat8kgqvLfcIE1SA==}
|
resolution: {integrity: sha512-5PjwB0uP2XDp3nt5u5NJAG2DORHIRClPzWT/TTZhJ2Ekwe8M5bA9tvPdi9NO/n2uvu2/ictat8kgqvLfcIE1SA==}
|
||||||
|
|
||||||
|
'@types/prismjs@1.26.4':
|
||||||
|
resolution: {integrity: sha512-rlAnzkW2sZOjbqZ743IHUhFcvzaGbqijwOu8QZnZCjfQzBqFE3s4lOTJEsxikImav9uzz/42I+O7YUs1mWgMlg==}
|
||||||
|
|
||||||
'@types/prop-types@15.7.11':
|
'@types/prop-types@15.7.11':
|
||||||
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
|
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
|
||||||
|
|
||||||
@@ -4647,6 +4662,10 @@ packages:
|
|||||||
pretty-format@3.8.0:
|
pretty-format@3.8.0:
|
||||||
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
|
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
|
||||||
|
|
||||||
|
prismjs@1.29.0:
|
||||||
|
resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
process@0.11.10:
|
process@0.11.10:
|
||||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||||
engines: {node: '>= 0.6.0'}
|
engines: {node: '>= 0.6.0'}
|
||||||
@@ -4784,6 +4803,12 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
react-simple-code-editor@0.13.1:
|
||||||
|
resolution: {integrity: sha512-XYeVwRZwgyKtjNIYcAEgg2FaQcCZwhbarnkJIV20U2wkCU9q/CPFBo8nRXrK4GXUz3AvbqZFsZRrpUTkqqEYyQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '*'
|
||||||
|
react-dom: '*'
|
||||||
|
|
||||||
react-style-singleton@2.2.1:
|
react-style-singleton@2.2.1:
|
||||||
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
|
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -6968,6 +6993,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/object.pick@1.3.4': {}
|
'@types/object.pick@1.3.4': {}
|
||||||
|
|
||||||
|
'@types/prismjs@1.26.4': {}
|
||||||
|
|
||||||
'@types/prop-types@15.7.11': {}
|
'@types/prop-types@15.7.11': {}
|
||||||
|
|
||||||
'@types/qs@6.9.11': {}
|
'@types/qs@6.9.11': {}
|
||||||
@@ -9614,6 +9641,8 @@ snapshots:
|
|||||||
|
|
||||||
pretty-format@3.8.0: {}
|
pretty-format@3.8.0: {}
|
||||||
|
|
||||||
|
prismjs@1.29.0: {}
|
||||||
|
|
||||||
process@0.11.10: {}
|
process@0.11.10: {}
|
||||||
|
|
||||||
prop-types@15.8.1:
|
prop-types@15.8.1:
|
||||||
@@ -9799,6 +9828,11 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 18.3.2
|
'@types/react': 18.3.2
|
||||||
|
|
||||||
|
react-simple-code-editor@0.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
|
||||||
react-style-singleton@2.2.1(@types/react@18.3.2)(react@18.3.1):
|
react-style-singleton@2.2.1(@types/react@18.3.2)(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
get-nonce: 1.0.1
|
get-nonce: 1.0.1
|
||||||
|
|||||||
Reference in New Issue
Block a user