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";
|
||||
|
||||
// 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 = () => {
|
||||
return null;
|
||||
import "~/styles/prismjs.scss";
|
||||
|
||||
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} />
|
||||
</AccordionItemFor>
|
||||
<AccordionItemFor value="customCss" icon={IconFileTypeCss}>
|
||||
<CustomCssSettingsContent />
|
||||
<CustomCssSettingsContent board={board} />
|
||||
</AccordionItemFor>
|
||||
{hasFullAccess && (
|
||||
<>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ClientShell } from "~/components/layout/shell";
|
||||
import type { Board } from "./_types";
|
||||
import { BoardProvider } from "./(content)/_context";
|
||||
import type { Params } from "./(content)/_creator";
|
||||
import { CustomCss } from "./(content)/_custom-css";
|
||||
import { BoardMantineProvider } from "./(content)/_theme";
|
||||
|
||||
interface CreateBoardLayoutProps<TParams extends Params> {
|
||||
@@ -44,6 +45,7 @@ export const createBoardLayout = <TParams extends Params>({
|
||||
<GlobalItemServerDataRunner board={initialBoard} shouldRun={isBoardContentPage}>
|
||||
<BoardProvider initialBoard={initialBoard}>
|
||||
<BoardMantineProvider>
|
||||
<CustomCss />
|
||||
<ClientShell hasNavigation={false}>
|
||||
<MainHeader
|
||||
logo={<BoardLogoWithTitle size="md" hideTitleOnMobile />}
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { showSuccessNotification } from "@homarr/notifications";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { BoardItemIntegration } from "@homarr/validation";
|
||||
import type { BoardItemAdvancedOptions, BoardItemIntegration } from "@homarr/validation";
|
||||
import {
|
||||
loadWidgetDynamic,
|
||||
reduceWidgetOptionsWithDefaultValues,
|
||||
@@ -42,9 +42,13 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie
|
||||
const [state, setState] = useState<{
|
||||
options: Record<string, unknown>;
|
||||
integrations: BoardItemIntegration[];
|
||||
advancedOptions: BoardItemAdvancedOptions;
|
||||
}>({
|
||||
options: reduceWidgetOptionsWithDefaultValues(kind, {}),
|
||||
integrations: [],
|
||||
advancedOptions: {
|
||||
customCssClasses: [],
|
||||
},
|
||||
});
|
||||
|
||||
const handleOpenEditWidgetModal = useCallback(() => {
|
||||
|
||||
Reference in New Issue
Block a user