diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index 35aabdebe..74d863927 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -18,6 +18,7 @@ "@homarr/analytics": "workspace:^0.1.0", "@homarr/api": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0", + "@homarr/boards": "workspace:^0.1.0", "@homarr/certificates": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", "@homarr/cron-job-status": "workspace:^0.1.0", diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx index 25a550dca..0f3454653 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_client.tsx @@ -5,12 +5,13 @@ import { Box, LoadingOverlay, Stack } from "@mantine/core"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; +import { useRequiredBoard } from "@homarr/boards/context"; import { BoardCategorySection } from "~/components/board/sections/category-section"; import { BoardEmptySection } from "~/components/board/sections/empty-section"; import { BoardBackgroundVideo } from "~/components/layout/background"; import { fullHeightWithoutHeaderAndFooter } from "~/constants"; -import { useIsBoardReady, useRequiredBoard } from "./_context"; +import { useIsBoardReady } from "./_ready-context"; let boardName: string | null = null; diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_context.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_context.tsx deleted file mode 100644 index 808759af1..000000000 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_context.tsx +++ /dev/null @@ -1,118 +0,0 @@ -"use client"; - -import type { Dispatch, PropsWithChildren, SetStateAction } from "react"; -import { createContext, useCallback, useContext, useEffect, useState } from "react"; -import { usePathname } from "next/navigation"; - -import type { RouterOutputs } from "@homarr/api"; -import { clientApi } from "@homarr/api/client"; - -import { updateBoardName } from "./_client"; - -const BoardContext = createContext<{ - board: RouterOutputs["board"]["getHomeBoard"]; - isReady: boolean; - markAsReady: (id: string) => void; - isEditMode: boolean; - setEditMode: Dispatch>; -} | null>(null); - -export const BoardProvider = ({ - children, - initialBoard, -}: PropsWithChildren<{ - initialBoard: RouterOutputs["board"]["getBoardByName"]; -}>) => { - const pathname = usePathname(); - const utils = clientApi.useUtils(); - const [readySections, setReadySections] = useState([]); - const [isEditMode, setEditMode] = useState(false); - const { data } = clientApi.board.getBoardByName.useQuery( - { name: initialBoard.name }, - { - initialData: initialBoard, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - }, - ); - // Update the board name so it can be used within updateBoard method - updateBoardName(initialBoard.name); - - // Invalidate the board when the pathname changes - // This allows to refetch the board when it might have changed - e.g. if someone else added an item - useEffect(() => { - return () => { - setReadySections([]); - void utils.board.getBoardByName.invalidate({ name: initialBoard.name }); - }; - }, [pathname, utils, initialBoard.name]); - - useEffect(() => { - setReadySections((previous) => previous.filter((id) => data.sections.some((section) => section.id === id))); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [data.sections.length, setReadySections]); - - const markAsReady = useCallback((id: string) => { - setReadySections((previous) => (previous.includes(id) ? previous : [...previous, id])); - }, []); - - return ( - - {children} - - ); -}; - -export const useMarkSectionAsReady = () => { - const context = useContext(BoardContext); - - if (!context) { - throw new Error("Board is required"); - } - - return context.markAsReady; -}; - -export const useIsBoardReady = () => { - const context = useContext(BoardContext); - - if (!context) { - throw new Error("Board is required"); - } - - return context.isReady; -}; - -export const useRequiredBoard = () => { - const optionalBoard = useOptionalBoard(); - - if (!optionalBoard) { - throw new Error("Board is required"); - } - - return optionalBoard; -}; - -export const useOptionalBoard = () => { - const context = useContext(BoardContext); - - return context?.board; -}; - -export const useEditMode = () => { - const context = useContext(BoardContext); - - if (!context) { - throw new Error("Board is required"); - } - - return [context.isEditMode, context.setEditMode] as const; -}; diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_custom-css.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_custom-css.tsx index ab01aff39..4ef289946 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_custom-css.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_custom-css.tsx @@ -1,6 +1,6 @@ "use client"; -import { useRequiredBoard } from "./_context"; +import { useRequiredBoard } from "@homarr/boards/context"; export const CustomCss = () => { const board = useRequiredBoard(); diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx index 74c617d09..e4029bd3a 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx @@ -20,6 +20,8 @@ import { } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; +import { useRequiredBoard } from "@homarr/boards/context"; +import { useEditMode } from "@homarr/boards/edit-mode"; import { revalidatePathActionAsync } from "@homarr/common/client"; import { useConfirmModal, useModalAction } from "@homarr/modals"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; @@ -32,7 +34,6 @@ import { CategoryEditModal } from "~/components/board/sections/category/category import { useDynamicSectionActions } from "~/components/board/sections/dynamic/dynamic-actions"; import { HeaderButton } from "~/components/layout/header/button"; import { env } from "~/env"; -import { useEditMode, useRequiredBoard } from "./_context"; export const BoardContentHeaderActions = () => { const [isEditMode] = useEditMode(); @@ -119,7 +120,7 @@ const AddMenu = () => { }; const EditModeMenu = () => { - const [isEditMode, setEditMode] = useEditMode(); + const [isEditMode, { open, close }] = useEditMode(); const board = useRequiredBoard(); const utils = clientApi.useUtils(); const t = useScopedI18n("board.action.edit"); @@ -131,7 +132,7 @@ const EditModeMenu = () => { }); void utils.board.getBoardByName.invalidate({ name: board.name }); void revalidatePathActionAsync(`/boards/${board.name}`); - setEditMode(false); + close(); }, onError() { showErrorNotification({ @@ -143,8 +144,8 @@ const EditModeMenu = () => { const toggle = useCallback(() => { if (isEditMode) return saveBoard(board); - setEditMode(true); - }, [board, isEditMode, saveBoard, setEditMode]); + open(); + }, [board, isEditMode, saveBoard, open]); useHotkeys([["mod+e", toggle]]); usePreventLeaveWithDirty(isEditMode); diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_ready-context.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_ready-context.tsx new file mode 100644 index 000000000..dcd07a311 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_ready-context.tsx @@ -0,0 +1,67 @@ +"use client"; + +import type { PropsWithChildren } from "react"; +import { createContext, useCallback, useContext, useEffect, useState } from "react"; +import { usePathname } from "next/navigation"; + +import { clientApi } from "@homarr/api/client"; +import { useRequiredBoard } from "@homarr/boards/context"; + +const BoardReadyContext = createContext<{ + isReady: boolean; + markAsReady: (id: string) => void; +} | null>(null); + +export const BoardReadyProvider = ({ children }: PropsWithChildren) => { + const pathname = usePathname(); + const utils = clientApi.useUtils(); + const board = useRequiredBoard(); + const [readySections, setReadySections] = useState([]); + + // Reset sections required for ready state + useEffect(() => { + return () => { + setReadySections([]); + }; + }, [pathname, utils]); + + useEffect(() => { + setReadySections((previous) => previous.filter((id) => board.sections.some((section) => section.id === id))); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [board.sections.length, setReadySections]); + + const markAsReady = useCallback((id: string) => { + setReadySections((previous) => (previous.includes(id) ? previous : [...previous, id])); + }, []); + + return ( + + {children} + + ); +}; + +export const useMarkSectionAsReady = () => { + const context = useContext(BoardReadyContext); + + if (!context) { + throw new Error("BoardReadyProvider is required"); + } + + return context.markAsReady; +}; + +export const useIsBoardReady = () => { + const context = useContext(BoardReadyContext); + + if (!context) { + throw new Error("BoardReadyProvider is required"); + } + + return context.isReady; +}; diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_theme.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_theme.tsx index 755c0a42b..bde65aa42 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_theme.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_theme.tsx @@ -4,10 +4,10 @@ import type { PropsWithChildren } from "react"; import type { MantineColorsTuple } from "@mantine/core"; import { createTheme, darken, lighten, MantineProvider } from "@mantine/core"; +import { useRequiredBoard } from "@homarr/boards/context"; import type { ColorScheme } from "@homarr/definitions"; import { useColorSchemeManager } from "../../_client-providers/mantine"; -import { useRequiredBoard } from "./_context"; export const BoardMantineProvider = ({ children, diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_behavior.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_behavior.tsx new file mode 100644 index 000000000..fdfd0042b --- /dev/null +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_behavior.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { Button, Group, Stack, Switch } from "@mantine/core"; + +import { useForm } from "@homarr/form"; +import { useI18n } from "@homarr/translation/client"; + +import type { Board } from "../../_types"; +import { useSavePartialSettingsMutation } from "./_shared"; + +interface Props { + board: Board; +} + +export const BehaviorSettingsContent = ({ board }: Props) => { + const t = useI18n(); + const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board); + const form = useForm({ + initialValues: { + disableStatus: board.disableStatus, + }, + }); + + return ( +
{ + savePartialSettings({ + id: board.id, + ...values, + }); + })} + > + + + + + + + +
+ ); +}; diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_danger.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_danger.tsx index 115605279..699da6003 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_danger.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_danger.tsx @@ -5,11 +5,11 @@ import { useRouter } from "next/navigation"; import { Button, Divider, Group, Stack, Text } from "@mantine/core"; import { clientApi } from "@homarr/api/client"; +import { useRequiredBoard } from "@homarr/boards/context"; import { useConfirmModal, useModalAction } from "@homarr/modals"; import { useScopedI18n } from "@homarr/translation/client"; import { BoardRenameModal } from "~/components/board/modals/board-rename-modal"; -import { useRequiredBoard } from "../../(content)/_context"; import classes from "./danger.module.css"; export const DangerZoneSettingsContent = ({ hideVisibility }: { hideVisibility: boolean }) => { diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_general.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_general.tsx index 0303be98d..efa640f93 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_general.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_general.tsx @@ -5,13 +5,13 @@ import { Button, Grid, Group, Loader, Stack, TextInput, Tooltip } from "@mantine import { useDebouncedValue, useDocumentTitle, useFavicon } from "@mantine/hooks"; import { IconAlertTriangle } from "@tabler/icons-react"; +import { useUpdateBoard } from "@homarr/boards/updater"; import { useZodForm } from "@homarr/form"; import { useI18n } from "@homarr/translation/client"; import { validation } from "@homarr/validation"; import { createMetaTitle } from "~/metadata"; import type { Board } from "../../_types"; -import { useUpdateBoard } from "../../(content)/_client"; import { useSavePartialSettingsMutation } from "./_shared"; interface Props { diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/page.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/page.tsx index 1ffb892d1..4304f51da 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/page.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/page.tsx @@ -4,6 +4,7 @@ import { AccordionControl, AccordionItem, AccordionPanel, Container, Stack, Text import { IconAlertTriangle, IconBrush, + IconClick, IconFileTypeCss, IconLayout, IconPhoto, @@ -23,6 +24,7 @@ import type { TablerIcon } from "@homarr/ui"; import { getBoardPermissionsAsync } from "~/components/board/permissions/server"; import { ActiveTabAccordion } from "../../../../../components/active-tab-accordion"; import { BackgroundSettingsContent } from "./_background"; +import { BehaviorSettingsContent } from "./_behavior"; import { BoardAccessSettings } from "./_board-access"; import { ColorSettingsContent } from "./_colors"; import { CustomCssSettingsContent } from "./_customCss"; @@ -95,6 +97,9 @@ export default async function BoardSettingsPage(props: Props) { + + + {hasFullAccess && ( <> diff --git a/apps/nextjs/src/app/[locale]/boards/_header-actions.tsx b/apps/nextjs/src/app/[locale]/boards/_header-actions.tsx index 299bbb05f..c68be29e9 100644 --- a/apps/nextjs/src/app/[locale]/boards/_header-actions.tsx +++ b/apps/nextjs/src/app/[locale]/boards/_header-actions.tsx @@ -2,8 +2,9 @@ import { IconLayoutBoard } from "@tabler/icons-react"; +import { useRequiredBoard } from "@homarr/boards/context"; + import { HeaderButton } from "~/components/layout/header/button"; -import { useRequiredBoard } from "./(content)/_context"; export const BoardOtherHeaderActions = () => { const board = useRequiredBoard(); diff --git a/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx b/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx index 22a4e53c0..446709f34 100644 --- a/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx +++ b/apps/nextjs/src/app/[locale]/boards/_layout-creator.tsx @@ -3,6 +3,8 @@ import { notFound } from "next/navigation"; import { AppShellMain } from "@mantine/core"; import { TRPCError } from "@trpc/server"; +import { BoardProvider } from "@homarr/boards/context"; +import { EditModeProvider } from "@homarr/boards/edit-mode"; import { logger } from "@homarr/log"; import { MainHeader } from "~/components/layout/header"; @@ -10,9 +12,9 @@ import { BoardLogoWithTitle } from "~/components/layout/logo/board-logo"; import { ClientShell } from "~/components/layout/shell"; import { getCurrentColorSchemeAsync } from "~/theme/color-scheme"; import type { Board } from "./_types"; -import { BoardProvider } from "./(content)/_context"; import type { Params } from "./(content)/_creator"; import { CustomCss } from "./(content)/_custom-css"; +import { BoardReadyProvider } from "./(content)/_ready-context"; import { BoardMantineProvider } from "./(content)/_theme"; interface CreateBoardLayoutProps { @@ -42,17 +44,21 @@ export const createBoardLayout = ({ return ( - - - - } - actions={headerActions} - hasNavigation={false} - /> - {children} - - + + + + + + } + actions={headerActions} + hasNavigation={false} + /> + {children} + + + + ); }; diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx index 1a04d8c7e..b988647bc 100644 --- a/apps/nextjs/src/app/[locale]/layout.tsx +++ b/apps/nextjs/src/app/[locale]/layout.tsx @@ -94,6 +94,8 @@ export default async function Layout(props: { board: { homeBoardId: serverSettings.board.homeBoardId, mobileHomeBoardId: serverSettings.board.mobileHomeBoardId, + enableStatusByDefault: serverSettings.board.enableStatusByDefault, + forceDisableStatus: serverSettings.board.forceDisableStatus, }, search: { defaultSearchEngineId: serverSettings.search.defaultSearchEngineId }, }} diff --git a/apps/nextjs/src/app/[locale]/manage/settings/_components/board-settings-form.tsx b/apps/nextjs/src/app/[locale]/manage/settings/_components/board-settings-form.tsx index 69f2329b0..3a06cf8a3 100644 --- a/apps/nextjs/src/app/[locale]/manage/settings/_components/board-settings-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/settings/_components/board-settings-form.tsx @@ -1,6 +1,6 @@ "use client"; -import { Group, Text } from "@mantine/core"; +import { Group, Switch, Text } from "@mantine/core"; import { IconLayoutDashboard } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; @@ -56,6 +56,18 @@ export const BoardSettingsForm = ({ defaultValues }: { defaultValues: ServerSett )} {...form.getInputProps("mobileHomeBoardId")} /> + + {tBoard("status.title")} + + )} diff --git a/apps/nextjs/src/app/[locale]/widgets/[kind]/_content.tsx b/apps/nextjs/src/app/[locale]/widgets/[kind]/_content.tsx index d351c9238..d31f42e9e 100644 --- a/apps/nextjs/src/app/[locale]/widgets/[kind]/_content.tsx +++ b/apps/nextjs/src/app/[locale]/widgets/[kind]/_content.tsx @@ -9,6 +9,7 @@ import { ErrorBoundary } from "react-error-boundary"; import type { IntegrationKind, WidgetKind } from "@homarr/definitions"; import { useModalAction } from "@homarr/modals"; import { showSuccessNotification } from "@homarr/notifications"; +import { useSettings } from "@homarr/settings"; import { useScopedI18n } from "@homarr/translation/client"; import type { BoardItemAdvancedOptions } from "@homarr/validation"; import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets"; @@ -29,6 +30,7 @@ interface WidgetPreviewPageContentProps { } export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPreviewPageContentProps) => { + const settings = useSettings(); const t = useScopedI18n("widgetPreview"); const { openModal: openWidgetEditModal } = useModalAction(WidgetEditModal); const { openModal: openPreviewDimensionsModal } = useModalAction(PreviewDimensionsModal); @@ -43,7 +45,7 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie integrationIds: string[]; advancedOptions: BoardItemAdvancedOptions; }>({ - options: reduceWidgetOptionsWithDefaultValues(kind, {}), + options: reduceWidgetOptionsWithDefaultValues(kind, settings, {}), integrationIds: [], advancedOptions: { customCssClasses: [], @@ -63,8 +65,9 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie (currentDefinition.supportedIntegrations as string[]).some((kind) => kind === integration.kind), ), integrationSupport: "supportedIntegrations" in currentDefinition, + settings, }); - }, [currentDefinition, integrationData, kind, openWidgetEditModal, state]); + }, [currentDefinition, integrationData, kind, openWidgetEditModal, settings, state]); const Comp = loadWidgetDynamic(kind); diff --git a/apps/nextjs/src/components/board/items/item-actions.tsx b/apps/nextjs/src/components/board/items/item-actions.tsx index c8a93a590..bdda5793f 100644 --- a/apps/nextjs/src/components/board/items/item-actions.tsx +++ b/apps/nextjs/src/components/board/items/item-actions.tsx @@ -1,9 +1,9 @@ import { useCallback } from "react"; +import { useUpdateBoard } from "@homarr/boards/updater"; import type { BoardItemAdvancedOptions } from "@homarr/validation"; import type { Item } from "~/app/[locale]/boards/_types"; -import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client"; import type { CreateItemInput } from "./actions/create-item"; import { createItemCallback } from "./actions/create-item"; import type { DuplicateItemInput } from "./actions/duplicate-item"; diff --git a/apps/nextjs/src/components/board/items/item-content.tsx b/apps/nextjs/src/components/board/items/item-content.tsx index 6db4bb9d4..bcee538b0 100644 --- a/apps/nextjs/src/components/board/items/item-content.tsx +++ b/apps/nextjs/src/components/board/items/item-content.tsx @@ -5,11 +5,13 @@ import combineClasses from "clsx"; import { NoIntegrationSelectedError } from "node_modules/@homarr/widgets/src/errors"; import { ErrorBoundary } from "react-error-boundary"; +import { useRequiredBoard } from "@homarr/boards/context"; +import { useEditMode } from "@homarr/boards/edit-mode"; +import { useSettings } from "@homarr/settings"; import { loadWidgetDynamic, reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets"; import { WidgetError } from "@homarr/widgets/errors"; import type { Item } from "~/app/[locale]/boards/_types"; -import { useEditMode, useRequiredBoard } from "~/app/[locale]/boards/(content)/_context"; import classes from "../sections/item.module.css"; import { useItemActions } from "./item-actions"; import { BoardItemMenu } from "./item-menu"; @@ -53,11 +55,12 @@ interface InnerContentProps { } const InnerContent = ({ item, ...dimensions }: InnerContentProps) => { + const settings = useSettings(); const board = useRequiredBoard(); const [isEditMode] = useEditMode(); const Comp = loadWidgetDynamic(item.kind); const { definition } = widgetImports[item.kind]; - const options = reduceWidgetOptionsWithDefaultValues(item.kind, item.options); + const options = reduceWidgetOptionsWithDefaultValues(item.kind, settings, item.options); const newItem = { ...item, options }; const { updateItemOptions } = useItemActions(); const updateOptions = ({ newOptions }: { newOptions: Record }) => diff --git a/apps/nextjs/src/components/board/items/item-menu.tsx b/apps/nextjs/src/components/board/items/item-menu.tsx index d16ec2c7a..3d01f8aaf 100644 --- a/apps/nextjs/src/components/board/items/item-menu.tsx +++ b/apps/nextjs/src/components/board/items/item-menu.tsx @@ -3,13 +3,14 @@ import { ActionIcon, Menu } from "@mantine/core"; import { IconCopy, IconDotsVertical, IconLayoutKanban, IconPencil, IconTrash } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; +import { useEditMode } from "@homarr/boards/edit-mode"; import { useConfirmModal, useModalAction } from "@homarr/modals"; +import { useSettings } from "@homarr/settings"; import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { widgetImports } from "@homarr/widgets"; import { WidgetEditModal } from "@homarr/widgets/modals"; import type { Item } from "~/app/[locale]/boards/_types"; -import { useEditMode } from "~/app/[locale]/boards/(content)/_context"; import { useSectionContext } from "../sections/section-context"; import { useItemActions } from "./item-actions"; import { ItemMoveModal } from "./item-move-modal"; @@ -35,6 +36,7 @@ export const BoardItemMenu = ({ const { data: integrationData, isPending } = clientApi.integration.all.useQuery(); const currentDefinition = useMemo(() => widgetImports[item.kind].definition, [item.kind]); const { gridstack } = useSectionContext().refs; + const settings = useSettings(); // Reset error boundary on next render if item has been edited useEffect(() => { @@ -75,6 +77,7 @@ export const BoardItemMenu = ({ (currentDefinition.supportedIntegrations as string[]).some((kind) => kind === integration.kind), ), integrationSupport: "supportedIntegrations" in currentDefinition, + settings, }); }; diff --git a/apps/nextjs/src/components/board/sections/category-section.tsx b/apps/nextjs/src/components/board/sections/category-section.tsx index 294fd856a..c5180b032 100644 --- a/apps/nextjs/src/components/board/sections/category-section.tsx +++ b/apps/nextjs/src/components/board/sections/category-section.tsx @@ -3,9 +3,9 @@ import { useDisclosure } from "@mantine/hooks"; import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; +import { useRequiredBoard } from "@homarr/boards/context"; import type { CategorySection } from "~/app/[locale]/boards/_types"; -import { useRequiredBoard } from "~/app/[locale]/boards/(content)/_context"; import { CategoryMenu } from "./category/category-menu"; import { GridStack } from "./gridstack/gridstack"; import classes from "./item.module.css"; diff --git a/apps/nextjs/src/components/board/sections/category/category-actions.ts b/apps/nextjs/src/components/board/sections/category/category-actions.ts index 70298ceac..3007be754 100644 --- a/apps/nextjs/src/components/board/sections/category/category-actions.ts +++ b/apps/nextjs/src/components/board/sections/category/category-actions.ts @@ -1,9 +1,9 @@ import { useCallback } from "react"; +import { useUpdateBoard } from "@homarr/boards/updater"; import { createId } from "@homarr/db/client"; import type { CategorySection, EmptySection } from "~/app/[locale]/boards/_types"; -import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client"; import type { MoveCategoryInput } from "./actions/move-category"; import { moveCategoryCallback } from "./actions/move-category"; import type { RemoveCategoryInput } from "./actions/remove-category"; diff --git a/apps/nextjs/src/components/board/sections/category/category-menu-actions.tsx b/apps/nextjs/src/components/board/sections/category/category-menu-actions.tsx index 15de9c00c..6b89fb54c 100644 --- a/apps/nextjs/src/components/board/sections/category/category-menu-actions.tsx +++ b/apps/nextjs/src/components/board/sections/category/category-menu-actions.tsx @@ -3,6 +3,7 @@ import { useCallback } from "react"; import { fetchApi } from "@homarr/api/client"; import { createId } from "@homarr/db/client"; import { useConfirmModal, useModalAction } from "@homarr/modals"; +import { useSettings } from "@homarr/settings"; import { useI18n } from "@homarr/translation/client"; import type { CategorySection } from "~/app/[locale]/boards/_types"; @@ -99,8 +100,9 @@ export const useCategoryMenuActions = (category: CategorySection) => { ); }, [category, openModal, renameCategory, t]); + const settings = useSettings(); const openAllInNewTabs = useCallback(async () => { - const appIds = filterByItemKind(category.items, "app").map((item) => { + const appIds = filterByItemKind(category.items, settings, "app").map((item) => { return item.options.appId; }); @@ -119,7 +121,7 @@ export const useCategoryMenuActions = (category: CategorySection) => { }); break; } - }, [category, t, openConfirmModal]); + }, [category, t, openConfirmModal, settings]); return { addCategoryAbove, diff --git a/apps/nextjs/src/components/board/sections/category/category-menu.tsx b/apps/nextjs/src/components/board/sections/category/category-menu.tsx index 69c72a347..894db8949 100644 --- a/apps/nextjs/src/components/board/sections/category/category-menu.tsx +++ b/apps/nextjs/src/components/board/sections/category/category-menu.tsx @@ -13,12 +13,12 @@ import { IconTrash, } from "@tabler/icons-react"; +import { useEditMode } from "@homarr/boards/edit-mode"; import type { MaybePromise } from "@homarr/common/types"; import { useScopedI18n } from "@homarr/translation/client"; import type { TablerIcon } from "@homarr/ui"; import type { CategorySection } from "~/app/[locale]/boards/_types"; -import { useEditMode } from "~/app/[locale]/boards/(content)/_context"; import { useCategoryMenuActions } from "./category-menu-actions"; interface Props { diff --git a/apps/nextjs/src/components/board/sections/category/filter.ts b/apps/nextjs/src/components/board/sections/category/filter.ts index 87bc2c210..bdc59484c 100644 --- a/apps/nextjs/src/components/board/sections/category/filter.ts +++ b/apps/nextjs/src/components/board/sections/category/filter.ts @@ -1,14 +1,23 @@ import type { WidgetKind } from "@homarr/definitions"; +import type { SettingsContextProps } from "@homarr/settings"; import type { WidgetComponentProps } from "@homarr/widgets"; import { reduceWidgetOptionsWithDefaultValues } from "@homarr/widgets"; import type { Item } from "~/app/[locale]/boards/_types"; -export const filterByItemKind = (items: Item[], kind: TKind) => { +export const filterByItemKind = ( + items: Item[], + settings: SettingsContextProps, + kind: TKind, +) => { return items .filter((item) => item.kind === kind) .map((item) => ({ ...item, - options: reduceWidgetOptionsWithDefaultValues(kind, item.options) as WidgetComponentProps["options"], + options: reduceWidgetOptionsWithDefaultValues( + kind, + settings, + item.options, + ) as WidgetComponentProps["options"], })); }; diff --git a/apps/nextjs/src/components/board/sections/content.tsx b/apps/nextjs/src/components/board/sections/content.tsx index fd8b4b552..c51894342 100644 --- a/apps/nextjs/src/components/board/sections/content.tsx +++ b/apps/nextjs/src/components/board/sections/content.tsx @@ -1,7 +1,8 @@ import { useMemo } from "react"; +import { useRequiredBoard } from "@homarr/boards/context"; + import type { DynamicSection, Item, Section } from "~/app/[locale]/boards/_types"; -import { useRequiredBoard } from "~/app/[locale]/boards/(content)/_context"; import { BoardItemContent } from "../items/item-content"; import { BoardDynamicSection } from "./dynamic-section"; import { GridStackItem } from "./gridstack/gridstack-item"; diff --git a/apps/nextjs/src/components/board/sections/dynamic-section.tsx b/apps/nextjs/src/components/board/sections/dynamic-section.tsx index 6a858bd10..afb138555 100644 --- a/apps/nextjs/src/components/board/sections/dynamic-section.tsx +++ b/apps/nextjs/src/components/board/sections/dynamic-section.tsx @@ -1,7 +1,8 @@ import { Box, Card } from "@mantine/core"; +import { useRequiredBoard } from "@homarr/boards/context"; + import type { DynamicSection } from "~/app/[locale]/boards/_types"; -import { useRequiredBoard } from "~/app/[locale]/boards/(content)/_context"; import { BoardDynamicSectionMenu } from "./dynamic/dynamic-menu"; import { GridStack } from "./gridstack/gridstack"; import classes from "./item.module.css"; diff --git a/apps/nextjs/src/components/board/sections/dynamic/dynamic-actions.ts b/apps/nextjs/src/components/board/sections/dynamic/dynamic-actions.ts index dc8228b35..d27bd907e 100644 --- a/apps/nextjs/src/components/board/sections/dynamic/dynamic-actions.ts +++ b/apps/nextjs/src/components/board/sections/dynamic/dynamic-actions.ts @@ -1,9 +1,9 @@ import { useCallback } from "react"; +import { useUpdateBoard } from "@homarr/boards/updater"; import { createId } from "@homarr/db/client"; import type { DynamicSection, EmptySection } from "~/app/[locale]/boards/_types"; -import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client"; interface RemoveDynamicSection { id: string; diff --git a/apps/nextjs/src/components/board/sections/dynamic/dynamic-menu.tsx b/apps/nextjs/src/components/board/sections/dynamic/dynamic-menu.tsx index 1a07eb81e..e1910d363 100644 --- a/apps/nextjs/src/components/board/sections/dynamic/dynamic-menu.tsx +++ b/apps/nextjs/src/components/board/sections/dynamic/dynamic-menu.tsx @@ -1,11 +1,11 @@ import { ActionIcon, Menu } from "@mantine/core"; import { IconDotsVertical, IconTrash } from "@tabler/icons-react"; +import { useEditMode } from "@homarr/boards/edit-mode"; import { useConfirmModal } from "@homarr/modals"; import { useI18n, useScopedI18n } from "@homarr/translation/client"; import type { DynamicSection } from "~/app/[locale]/boards/_types"; -import { useEditMode } from "~/app/[locale]/boards/(content)/_context"; import { useDynamicSectionActions } from "./dynamic-actions"; export const BoardDynamicSectionMenu = ({ section }: { section: DynamicSection }) => { diff --git a/apps/nextjs/src/components/board/sections/empty-section.tsx b/apps/nextjs/src/components/board/sections/empty-section.tsx index 3245b4e41..de474760f 100644 --- a/apps/nextjs/src/components/board/sections/empty-section.tsx +++ b/apps/nextjs/src/components/board/sections/empty-section.tsx @@ -1,7 +1,8 @@ import combineClasses from "clsx"; +import { useEditMode } from "@homarr/boards/edit-mode"; + import type { EmptySection } from "~/app/[locale]/boards/_types"; -import { useEditMode } from "~/app/[locale]/boards/(content)/_context"; import { GridStack } from "./gridstack/gridstack"; import { useSectionItems } from "./use-section-items"; diff --git a/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts b/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts index afe3f0eb6..251bb1d42 100644 --- a/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts +++ b/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts @@ -2,10 +2,12 @@ import type { RefObject } from "react"; import { createRef, useCallback, useEffect, useRef } from "react"; import { useElementSize } from "@mantine/hooks"; +import { useRequiredBoard } from "@homarr/boards/context"; +import { useEditMode } from "@homarr/boards/edit-mode"; import type { GridHTMLElement, GridItemHTMLElement, GridStack, GridStackNode } from "@homarr/gridstack"; import type { Section } from "~/app/[locale]/boards/_types"; -import { useEditMode, useMarkSectionAsReady, useRequiredBoard } from "~/app/[locale]/boards/(content)/_context"; +import { useMarkSectionAsReady } from "~/app/[locale]/boards/(content)/_ready-context"; import { useItemActions } from "../../items/item-actions"; import { useSectionActions } from "../section-actions"; import { initializeGridstack } from "./init-gridstack"; diff --git a/apps/nextjs/src/components/board/sections/section-actions.tsx b/apps/nextjs/src/components/board/sections/section-actions.tsx index b79ef88c5..6403d2175 100644 --- a/apps/nextjs/src/components/board/sections/section-actions.tsx +++ b/apps/nextjs/src/components/board/sections/section-actions.tsx @@ -1,6 +1,6 @@ import { useCallback } from "react"; -import { useUpdateBoard } from "~/app/[locale]/boards/(content)/_client"; +import { useUpdateBoard } from "@homarr/boards/updater"; interface MoveAndResizeInnerSection { innerSectionId: string; diff --git a/apps/nextjs/src/components/board/sections/use-section-items.ts b/apps/nextjs/src/components/board/sections/use-section-items.ts index 9989e0f85..7e31f8643 100644 --- a/apps/nextjs/src/components/board/sections/use-section-items.ts +++ b/apps/nextjs/src/components/board/sections/use-section-items.ts @@ -1,5 +1,6 @@ +import { useRequiredBoard } from "@homarr/boards/context"; + import type { Section } from "~/app/[locale]/boards/_types"; -import { useRequiredBoard } from "~/app/[locale]/boards/(content)/_context"; export const useSectionItems = (section: Section) => { const board = useRequiredBoard(); diff --git a/apps/nextjs/src/components/layout/background.tsx b/apps/nextjs/src/components/layout/background.tsx index 08365e924..c7011408a 100644 --- a/apps/nextjs/src/components/layout/background.tsx +++ b/apps/nextjs/src/components/layout/background.tsx @@ -1,7 +1,7 @@ import { usePathname } from "next/navigation"; import type { AppShellProps } from "@mantine/core"; -import { useOptionalBoard } from "~/app/[locale]/boards/(content)/_context"; +import { useOptionalBoard } from "@homarr/boards/context"; const supportedVideoFormats = ["mp4", "webm", "ogg"]; const isVideo = (url: string) => supportedVideoFormats.some((format) => url.toLowerCase().endsWith(`.${format}`)); diff --git a/apps/nextjs/src/components/layout/logo/board-logo.tsx b/apps/nextjs/src/components/layout/logo/board-logo.tsx index 096709431..ad2f1ca2e 100644 --- a/apps/nextjs/src/components/layout/logo/board-logo.tsx +++ b/apps/nextjs/src/components/layout/logo/board-logo.tsx @@ -1,6 +1,7 @@ "use client"; -import { useRequiredBoard } from "~/app/[locale]/boards/(content)/_context"; +import { useRequiredBoard } from "@homarr/boards/context"; + import { homarrLogoPath, homarrPageTitle } from "./homarr-logo"; import type { LogoWithTitleProps } from "./logo"; import { Logo, LogoWithTitle } from "./logo"; diff --git a/packages/api/src/router/board.ts b/packages/api/src/router/board.ts index 2522809c2..b4f9b598a 100644 --- a/packages/api/src/router/board.ts +++ b/packages/api/src/router/board.ts @@ -484,6 +484,9 @@ export const boardRouter = createTRPCRouter({ // layout settings columnCount: input.columnCount, + + // Behavior settings + disableStatus: input.disableStatus, }) .where(eq(boards.id, input.id)); }), diff --git a/packages/api/src/router/widgets/index.ts b/packages/api/src/router/widgets/index.ts index ce39bf9f7..c2a8efc60 100644 --- a/packages/api/src/router/widgets/index.ts +++ b/packages/api/src/router/widgets/index.ts @@ -10,6 +10,7 @@ import { mediaServerRouter } from "./media-server"; import { mediaTranscodingRouter } from "./media-transcoding"; import { minecraftRouter } from "./minecraft"; import { notebookRouter } from "./notebook"; +import { optionsRouter } from "./options"; import { rssFeedRouter } from "./rssFeed"; import { smartHomeRouter } from "./smart-home"; import { weatherRouter } from "./weather"; @@ -29,4 +30,5 @@ export const widgetRouter = createTRPCRouter({ healthMonitoring: healthMonitoringRouter, mediaTranscoding: mediaTranscodingRouter, minecraft: minecraftRouter, + options: optionsRouter, }); diff --git a/packages/api/src/router/widgets/options.ts b/packages/api/src/router/widgets/options.ts new file mode 100644 index 000000000..aa7c5e5c6 --- /dev/null +++ b/packages/api/src/router/widgets/options.ts @@ -0,0 +1,19 @@ +import { getServerSettingsAsync } from "@homarr/db/queries"; + +import type { WidgetOptionsSettings } from "../../../../widgets/src"; +import { createTRPCRouter, publicProcedure } from "../../trpc"; + +export const optionsRouter = createTRPCRouter({ + getWidgetOptionSettings: publicProcedure.query(async ({ ctx }): Promise => { + const serverSettings = await getServerSettingsAsync(ctx.db); + + return { + server: { + board: { + enableStatusByDefault: serverSettings.board.enableStatusByDefault, + forceDisableStatus: serverSettings.board.forceDisableStatus, + }, + }, + }; + }), +}); diff --git a/packages/boards/eslint.config.js b/packages/boards/eslint.config.js new file mode 100644 index 000000000..5b19b6f8a --- /dev/null +++ b/packages/boards/eslint.config.js @@ -0,0 +1,9 @@ +import baseConfig from "@homarr/eslint-config/base"; + +/** @type {import('typescript-eslint').Config} */ +export default [ + { + ignores: [], + }, + ...baseConfig, +]; diff --git a/packages/boards/package.json b/packages/boards/package.json new file mode 100644 index 000000000..9784f6677 --- /dev/null +++ b/packages/boards/package.json @@ -0,0 +1,38 @@ +{ + "name": "@homarr/boards", + "version": "0.1.0", + "private": true, + "license": "MIT", + "type": "module", + "exports": { + "./context": "./src/context.tsx", + "./updater": "./src/updater.ts", + "./edit-mode": "./src/edit-mode.tsx" + }, + "typesVersions": { + "*": { + "*": [ + "src/*" + ] + } + }, + "scripts": { + "clean": "rm -rf .turbo node_modules", + "format": "prettier --check . --ignore-path ../../.gitignore", + "lint": "eslint", + "typecheck": "tsc --noEmit" + }, + "prettier": "@homarr/prettier-config", + "dependencies": { + "@homarr/api": "workspace:^0.1.0", + "react": "19.0.0", + "react-dom": "19.0.0" + }, + "devDependencies": { + "@homarr/eslint-config": "workspace:^0.2.0", + "@homarr/prettier-config": "workspace:^0.1.0", + "@homarr/tsconfig": "workspace:^0.1.0", + "eslint": "^9.19.0", + "typescript": "^5.7.3" + } +} diff --git a/packages/boards/src/context.tsx b/packages/boards/src/context.tsx new file mode 100644 index 000000000..ab2af5c74 --- /dev/null +++ b/packages/boards/src/context.tsx @@ -0,0 +1,70 @@ +"use client"; + +import type { PropsWithChildren } from "react"; +import { createContext, useContext, useEffect } from "react"; +import { usePathname } from "next/navigation"; + +import type { RouterOutputs } from "@homarr/api"; +import { clientApi } from "@homarr/api/client"; + +import { updateBoardName } from "./updater"; + +const BoardContext = createContext<{ + board: RouterOutputs["board"]["getHomeBoard"]; +} | null>(null); + +export const BoardProvider = ({ + children, + initialBoard, +}: PropsWithChildren<{ + initialBoard: RouterOutputs["board"]["getBoardByName"]; +}>) => { + const { data } = clientApi.board.getBoardByName.useQuery( + { name: initialBoard.name }, + { + initialData: initialBoard, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + }, + ); + + // Update the board name so it can be used within updateBoard method + updateBoardName(initialBoard.name); + + const pathname = usePathname(); + const utils = clientApi.useUtils(); + + // Invalidate the board when the pathname changes + // This allows to refetch the board when it might have changed - e.g. if someone else added an item + useEffect(() => { + return () => { + void utils.board.getBoardByName.invalidate({ name: initialBoard.name }); + }; + }, [pathname, utils, initialBoard.name]); + + return ( + + {children} + + ); +}; + +export const useRequiredBoard = () => { + const optionalBoard = useOptionalBoard(); + + if (!optionalBoard) { + throw new Error("Board is required"); + } + + return optionalBoard; +}; + +export const useOptionalBoard = () => { + const context = useContext(BoardContext); + + return context?.board ?? null; +}; diff --git a/packages/boards/src/edit-mode.tsx b/packages/boards/src/edit-mode.tsx new file mode 100644 index 000000000..9b813b032 --- /dev/null +++ b/packages/boards/src/edit-mode.tsx @@ -0,0 +1,23 @@ +"use client"; + +import type { PropsWithChildren } from "react"; +import { createContext, useContext } from "react"; +import { useDisclosure } from "@mantine/hooks"; + +const EditModeContext = createContext | null>(null); + +export const EditModeProvider = ({ children }: PropsWithChildren) => { + const editModeDisclosure = useDisclosure(false); + + return {children}; +}; + +export const useEditMode = () => { + const context = useContext(EditModeContext); + + if (!context) { + throw new Error("EditMode is required"); + } + + return context; +}; diff --git a/packages/boards/src/updater.ts b/packages/boards/src/updater.ts new file mode 100644 index 000000000..49a301a8c --- /dev/null +++ b/packages/boards/src/updater.ts @@ -0,0 +1,34 @@ +"use client"; + +import { useCallback } from "react"; + +import type { RouterOutputs } from "@homarr/api"; +import { clientApi } from "@homarr/api/client"; + +let boardName: string | null = null; + +export const updateBoardName = (name: string | null) => { + boardName = name; +}; + +type UpdateCallback = (prev: RouterOutputs["board"]["getHomeBoard"]) => RouterOutputs["board"]["getHomeBoard"]; + +export const useUpdateBoard = () => { + const utils = clientApi.useUtils(); + + const updateBoard = useCallback( + (updaterWithoutUndefined: UpdateCallback) => { + if (!boardName) { + throw new Error("Board name is not set"); + } + utils.board.getBoardByName.setData({ name: boardName }, (previous) => + previous ? updaterWithoutUndefined(previous) : previous, + ); + }, + [utils], + ); + + return { + updateBoard, + }; +}; diff --git a/packages/boards/tsconfig.json b/packages/boards/tsconfig.json new file mode 100644 index 000000000..cbe8483d9 --- /dev/null +++ b/packages/boards/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@homarr/tsconfig/base.json", + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["*.ts", "src"], + "exclude": ["node_modules"] +} diff --git a/packages/cron-jobs/src/jobs/ping.ts b/packages/cron-jobs/src/jobs/ping.ts index 0ad99ca91..318db80bd 100644 --- a/packages/cron-jobs/src/jobs/ping.ts +++ b/packages/cron-jobs/src/jobs/ping.ts @@ -1,4 +1,6 @@ import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions"; +import { db } from "@homarr/db"; +import { getServerSettingByKeyAsync } from "@homarr/db/queries"; import { logger } from "@homarr/log"; import { sendPingRequestAsync } from "@homarr/ping"; import { pingChannel, pingUrlChannel } from "@homarr/redis"; @@ -13,6 +15,13 @@ const resetPreviousUrlsAsync = async () => { export const pingJob = createCronJob("ping", EVERY_MINUTE, { beforeStart: resetPreviousUrlsAsync, }).withCallback(async () => { + const boardSettings = await getServerSettingByKeyAsync(db, "board"); + + if (boardSettings.forceDisableStatus) { + logger.debug("Simple ping is disabled by server settings"); + return; + } + const urls = await pingUrlChannel.getAllAsync(); await Promise.allSettled([...new Set(urls)].map(pingAsync)); diff --git a/packages/db/migrations/mysql/0024_mean_vin_gonzales.sql b/packages/db/migrations/mysql/0024_mean_vin_gonzales.sql new file mode 100644 index 000000000..1dbb67b0c --- /dev/null +++ b/packages/db/migrations/mysql/0024_mean_vin_gonzales.sql @@ -0,0 +1 @@ +ALTER TABLE `board` ADD `disable_status` boolean DEFAULT false NOT NULL; \ No newline at end of file diff --git a/packages/db/migrations/mysql/meta/0024_snapshot.json b/packages/db/migrations/mysql/meta/0024_snapshot.json new file mode 100644 index 000000000..d74c8cee0 --- /dev/null +++ b/packages/db/migrations/mysql/meta/0024_snapshot.json @@ -0,0 +1,1772 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "f670b3a4-69ef-4ef8-9f1b-26a92dda2858", + "prevId": "bb3e417c-c928-4840-be08-94b5562cddd5", + "tables": { + "account": { + "name": "account", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_provider_account_id_pk": { + "name": "account_provider_provider_account_id_pk", + "columns": ["provider", "provider_account_id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "apiKey_user_id_user_id_fk": { + "name": "apiKey_user_id_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "apiKey_id": { + "name": "apiKey_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "app": { + "name": "app", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "href": { + "name": "href", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "app_id": { + "name": "app_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "boardGroupPermission": { + "name": "boardGroupPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardGroupPermission_board_id_board_id_fk": { + "name": "boardGroupPermission_board_id_board_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardGroupPermission_group_id_group_id_fk": { + "name": "boardGroupPermission_group_id_group_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardGroupPermission_board_id_group_id_permission_pk": { + "name": "boardGroupPermission_board_id_group_id_permission_pk", + "columns": ["board_id", "group_id", "permission"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "boardUserPermission": { + "name": "boardUserPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardUserPermission_board_id_board_id_fk": { + "name": "boardUserPermission_board_id_board_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardUserPermission_user_id_user_id_fk": { + "name": "boardUserPermission_user_id_user_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardUserPermission_board_id_user_id_permission_pk": { + "name": "boardUserPermission_board_id_user_id_permission_pk", + "columns": ["board_id", "user_id", "permission"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "board": { + "name": "board", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_title": { + "name": "page_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta_title": { + "name": "meta_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_image_url": { + "name": "logo_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon_image_url": { + "name": "favicon_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_url": { + "name": "background_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_attachment": { + "name": "background_image_attachment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('fixed')" + }, + "background_image_repeat": { + "name": "background_image_repeat", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('no-repeat')" + }, + "background_image_size": { + "name": "background_image_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('cover')" + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('#fa5252')" + }, + "secondary_color": { + "name": "secondary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('#fd7e14')" + }, + "opacity": { + "name": "opacity", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 100 + }, + "custom_css": { + "name": "custom_css", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "column_count": { + "name": "column_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "disable_status": { + "name": "disable_status", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_creator_id_user_id_fk": { + "name": "board_creator_id_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "board_id": { + "name": "board_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "board_name_unique": { + "name": "board_name_unique", + "columns": ["name"] + } + }, + "checkConstraint": {} + }, + "groupMember": { + "name": "groupMember", + "columns": { + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupMember_group_id_group_id_fk": { + "name": "groupMember_group_id_group_id_fk", + "tableFrom": "groupMember", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "groupMember_user_id_user_id_fk": { + "name": "groupMember_user_id_user_id_fk", + "tableFrom": "groupMember", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "groupMember_group_id_user_id_pk": { + "name": "groupMember_group_id_user_id_pk", + "columns": ["group_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "groupPermission": { + "name": "groupPermission", + "columns": { + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupPermission_group_id_group_id_fk": { + "name": "groupPermission_group_id_group_id_fk", + "tableFrom": "groupPermission", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "group": { + "name": "group", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "group_owner_id_user_id_fk": { + "name": "group_owner_id_user_id_fk", + "tableFrom": "group", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "group_id": { + "name": "group_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "group_name_unique": { + "name": "group_name_unique", + "columns": ["name"] + } + }, + "checkConstraint": {} + }, + "iconRepository": { + "name": "iconRepository", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(150)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "iconRepository_id": { + "name": "iconRepository_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "icon": { + "name": "icon", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(250)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_repository_id": { + "name": "icon_repository_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "icon_icon_repository_id_iconRepository_id_fk": { + "name": "icon_icon_repository_id_iconRepository_id_fk", + "tableFrom": "icon", + "tableTo": "iconRepository", + "columnsFrom": ["icon_repository_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "icon_id": { + "name": "icon_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integrationGroupPermissions": { + "name": "integrationGroupPermissions", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationGroupPermissions_integration_id_integration_id_fk": { + "name": "integrationGroupPermissions_integration_id_integration_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationGroupPermissions_group_id_group_id_fk": { + "name": "integrationGroupPermissions_group_id_group_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_group_permission__pk": { + "name": "integration_group_permission__pk", + "columns": ["integration_id", "group_id", "permission"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integration_item": { + "name": "integration_item", + "columns": { + "item_id": { + "name": "item_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integration_item_item_id_item_id_fk": { + "name": "integration_item_item_id_item_id_fk", + "tableFrom": "integration_item", + "tableTo": "item", + "columnsFrom": ["item_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integration_item_integration_id_integration_id_fk": { + "name": "integration_item_integration_id_integration_id_fk", + "tableFrom": "integration_item", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_item_item_id_integration_id_pk": { + "name": "integration_item_item_id_integration_id_pk", + "columns": ["item_id", "integration_id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integrationSecret": { + "name": "integrationSecret", + "columns": { + "kind": { + "name": "kind", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration_secret__kind_idx": { + "name": "integration_secret__kind_idx", + "columns": ["kind"], + "isUnique": false + }, + "integration_secret__updated_at_idx": { + "name": "integration_secret__updated_at_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "integrationSecret_integration_id_integration_id_fk": { + "name": "integrationSecret_integration_id_integration_id_fk", + "tableFrom": "integrationSecret", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationSecret_integration_id_kind_pk": { + "name": "integrationSecret_integration_id_kind_pk", + "columns": ["integration_id", "kind"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integrationUserPermission": { + "name": "integrationUserPermission", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationUserPermission_integration_id_integration_id_fk": { + "name": "integrationUserPermission_integration_id_integration_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationUserPermission_user_id_user_id_fk": { + "name": "integrationUserPermission_user_id_user_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationUserPermission_integration_id_user_id_permission_pk": { + "name": "integrationUserPermission_integration_id_user_id_permission_pk", + "columns": ["integration_id", "user_id", "permission"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "integration": { + "name": "integration", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration__kind_idx": { + "name": "integration__kind_idx", + "columns": ["kind"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "integration_id": { + "name": "integration_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "invite": { + "name": "invite", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiration_date": { + "name": "expiration_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "invite_creator_id_user_id_fk": { + "name": "invite_creator_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "invite_id": { + "name": "invite_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "invite_token_unique": { + "name": "invite_token_unique", + "columns": ["token"] + } + }, + "checkConstraint": {} + }, + "item": { + "name": "item", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"json\": {}}')" + }, + "advanced_options": { + "name": "advanced_options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "('{\"json\": {}}')" + } + }, + "indexes": {}, + "foreignKeys": { + "item_section_id_section_id_fk": { + "name": "item_section_id_section_id_fk", + "tableFrom": "item", + "tableTo": "section", + "columnsFrom": ["section_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "item_id": { + "name": "item_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "media": { + "name": "media", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "BLOB", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "creator_id": { + "name": "creator_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "media_creator_id_user_id_fk": { + "name": "media_creator_id_user_id_fk", + "tableFrom": "media", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "media_id": { + "name": "media_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "onboarding": { + "name": "onboarding", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "step": { + "name": "step", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "previous_step": { + "name": "previous_step", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "onboarding_id": { + "name": "onboarding_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "search_engine": { + "name": "search_engine", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "short": { + "name": "short", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url_template": { + "name": "url_template", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'generic'" + }, + "integration_id": { + "name": "integration_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "search_engine_integration_id_integration_id_fk": { + "name": "search_engine_integration_id_integration_id_fk", + "tableFrom": "search_engine", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "search_engine_id": { + "name": "search_engine_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "search_engine_short_unique": { + "name": "search_engine_short_unique", + "columns": ["short"] + } + }, + "checkConstraint": {} + }, + "section_collapse_state": { + "name": "section_collapse_state", + "columns": { + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "collapsed": { + "name": "collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_collapse_state_user_id_user_id_fk": { + "name": "section_collapse_state_user_id_user_id_fk", + "tableFrom": "section_collapse_state", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_collapse_state_section_id_section_id_fk": { + "name": "section_collapse_state_section_id_section_id_fk", + "tableFrom": "section_collapse_state", + "tableTo": "section", + "columnsFrom": ["section_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_collapse_state_user_id_section_id_pk": { + "name": "section_collapse_state_user_id_section_id_pk", + "columns": ["user_id", "section_id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "section": { + "name": "section", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_section_id": { + "name": "parent_section_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_board_id_board_id_fk": { + "name": "section_board_id_board_id_fk", + "tableFrom": "section", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_parent_section_id_section_id_fk": { + "name": "section_parent_section_id_section_id_fk", + "tableFrom": "section", + "tableTo": "section", + "columnsFrom": ["parent_section_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_id": { + "name": "section_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "serverSetting": { + "name": "serverSetting", + "columns": { + "setting_key": { + "name": "setting_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_setting_key": { + "name": "serverSetting_setting_key", + "columns": ["setting_key"] + } + }, + "uniqueConstraints": { + "serverSetting_settingKey_unique": { + "name": "serverSetting_settingKey_unique", + "columns": ["setting_key"] + } + }, + "checkConstraint": {} + }, + "session": { + "name": "session", + "columns": { + "session_token": { + "name": "session_token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "session_session_token": { + "name": "session_session_token", + "columns": ["session_token"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credentials'" + }, + "home_board_id": { + "name": "home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mobile_home_board_id": { + "name": "mobile_home_board_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_search_engine_id": { + "name": "default_search_engine_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "open_search_in_new_tab": { + "name": "open_search_in_new_tab", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "color_scheme": { + "name": "color_scheme", + "type": "varchar(5)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'dark'" + }, + "first_day_of_week": { + "name": "first_day_of_week", + "type": "tinyint", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "ping_icons_enabled": { + "name": "ping_icons_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_home_board_id_board_id_fk": { + "name": "user_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": ["home_board_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_mobile_home_board_id_board_id_fk": { + "name": "user_mobile_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": ["mobile_home_board_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_default_search_engine_id_search_engine_id_fk": { + "name": "user_default_search_engine_id_search_engine_id_fk", + "tableFrom": "user", + "tableTo": "search_engine", + "columnsFrom": ["default_search_engine_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_id": { + "name": "user_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": ["identifier", "token"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} diff --git a/packages/db/migrations/mysql/meta/_journal.json b/packages/db/migrations/mysql/meta/_journal.json index 4dc8e48b9..036387804 100644 --- a/packages/db/migrations/mysql/meta/_journal.json +++ b/packages/db/migrations/mysql/meta/_journal.json @@ -169,6 +169,13 @@ "when": 1738687012272, "tag": "0023_fix_on_delete_actions", "breakpoints": true + }, + { + "idx": 24, + "version": "5", + "when": 1738961147412, + "tag": "0024_mean_vin_gonzales", + "breakpoints": true } ] } diff --git a/packages/db/migrations/sqlite/0024_bitter_scrambler.sql b/packages/db/migrations/sqlite/0024_bitter_scrambler.sql new file mode 100644 index 000000000..0ee55e417 --- /dev/null +++ b/packages/db/migrations/sqlite/0024_bitter_scrambler.sql @@ -0,0 +1 @@ +ALTER TABLE `board` ADD `disable_status` integer DEFAULT false NOT NULL; \ No newline at end of file diff --git a/packages/db/migrations/sqlite/meta/0024_snapshot.json b/packages/db/migrations/sqlite/meta/0024_snapshot.json new file mode 100644 index 000000000..6cbd102a7 --- /dev/null +++ b/packages/db/migrations/sqlite/meta/0024_snapshot.json @@ -0,0 +1,1697 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "7111fcfa-dd13-42d5-b3f1-6dd094628858", + "prevId": "f1f5327f-6803-4b9e-a006-cff157d77fc8", + "tables": { + "account": { + "name": "account", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_account_id": { + "name": "provider_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_provider_account_id_pk": { + "columns": ["provider", "provider_account_id"], + "name": "account_provider_provider_account_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "apiKey_user_id_user_id_fk": { + "name": "apiKey_user_id_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app": { + "name": "app", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "href": { + "name": "href", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "boardGroupPermission": { + "name": "boardGroupPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardGroupPermission_board_id_board_id_fk": { + "name": "boardGroupPermission_board_id_board_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardGroupPermission_group_id_group_id_fk": { + "name": "boardGroupPermission_group_id_group_id_fk", + "tableFrom": "boardGroupPermission", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardGroupPermission_board_id_group_id_permission_pk": { + "columns": ["board_id", "group_id", "permission"], + "name": "boardGroupPermission_board_id_group_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "boardUserPermission": { + "name": "boardUserPermission", + "columns": { + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "boardUserPermission_board_id_board_id_fk": { + "name": "boardUserPermission_board_id_board_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "boardUserPermission_user_id_user_id_fk": { + "name": "boardUserPermission_user_id_user_id_fk", + "tableFrom": "boardUserPermission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "boardUserPermission_board_id_user_id_permission_pk": { + "columns": ["board_id", "user_id", "permission"], + "name": "boardUserPermission_board_id_user_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "board": { + "name": "board", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_public": { + "name": "is_public", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "page_title": { + "name": "page_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta_title": { + "name": "meta_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_image_url": { + "name": "logo_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon_image_url": { + "name": "favicon_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_url": { + "name": "background_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_attachment": { + "name": "background_image_attachment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'fixed'" + }, + "background_image_repeat": { + "name": "background_image_repeat", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'no-repeat'" + }, + "background_image_size": { + "name": "background_image_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'cover'" + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#fa5252'" + }, + "secondary_color": { + "name": "secondary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'#fd7e14'" + }, + "opacity": { + "name": "opacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 100 + }, + "custom_css": { + "name": "custom_css", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "column_count": { + "name": "column_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "disable_status": { + "name": "disable_status", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "board_name_unique": { + "name": "board_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": { + "board_creator_id_user_id_fk": { + "name": "board_creator_id_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "groupMember": { + "name": "groupMember", + "columns": { + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupMember_group_id_group_id_fk": { + "name": "groupMember_group_id_group_id_fk", + "tableFrom": "groupMember", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "groupMember_user_id_user_id_fk": { + "name": "groupMember_user_id_user_id_fk", + "tableFrom": "groupMember", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "groupMember_group_id_user_id_pk": { + "columns": ["group_id", "user_id"], + "name": "groupMember_group_id_user_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "groupPermission": { + "name": "groupPermission", + "columns": { + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "groupPermission_group_id_group_id_fk": { + "name": "groupPermission_group_id_group_id_fk", + "tableFrom": "groupPermission", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "group": { + "name": "group", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "group_name_unique": { + "name": "group_name_unique", + "columns": ["name"], + "isUnique": true + } + }, + "foreignKeys": { + "group_owner_id_user_id_fk": { + "name": "group_owner_id_user_id_fk", + "tableFrom": "group", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "iconRepository": { + "name": "iconRepository", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "icon": { + "name": "icon", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon_repository_id": { + "name": "icon_repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "icon_icon_repository_id_iconRepository_id_fk": { + "name": "icon_icon_repository_id_iconRepository_id_fk", + "tableFrom": "icon", + "tableTo": "iconRepository", + "columnsFrom": ["icon_repository_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integrationGroupPermissions": { + "name": "integrationGroupPermissions", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationGroupPermissions_integration_id_integration_id_fk": { + "name": "integrationGroupPermissions_integration_id_integration_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationGroupPermissions_group_id_group_id_fk": { + "name": "integrationGroupPermissions_group_id_group_id_fk", + "tableFrom": "integrationGroupPermissions", + "tableTo": "group", + "columnsFrom": ["group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationGroupPermissions_integration_id_group_id_permission_pk": { + "columns": ["integration_id", "group_id", "permission"], + "name": "integrationGroupPermissions_integration_id_group_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integration_item": { + "name": "integration_item", + "columns": { + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integration_item_item_id_item_id_fk": { + "name": "integration_item_item_id_item_id_fk", + "tableFrom": "integration_item", + "tableTo": "item", + "columnsFrom": ["item_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integration_item_integration_id_integration_id_fk": { + "name": "integration_item_integration_id_integration_id_fk", + "tableFrom": "integration_item", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_item_item_id_integration_id_pk": { + "columns": ["item_id", "integration_id"], + "name": "integration_item_item_id_integration_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integrationSecret": { + "name": "integrationSecret", + "columns": { + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration_secret__kind_idx": { + "name": "integration_secret__kind_idx", + "columns": ["kind"], + "isUnique": false + }, + "integration_secret__updated_at_idx": { + "name": "integration_secret__updated_at_idx", + "columns": ["updated_at"], + "isUnique": false + } + }, + "foreignKeys": { + "integrationSecret_integration_id_integration_id_fk": { + "name": "integrationSecret_integration_id_integration_id_fk", + "tableFrom": "integrationSecret", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationSecret_integration_id_kind_pk": { + "columns": ["integration_id", "kind"], + "name": "integrationSecret_integration_id_kind_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integrationUserPermission": { + "name": "integrationUserPermission", + "columns": { + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permission": { + "name": "permission", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integrationUserPermission_integration_id_integration_id_fk": { + "name": "integrationUserPermission_integration_id_integration_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integrationUserPermission_user_id_user_id_fk": { + "name": "integrationUserPermission_user_id_user_id_fk", + "tableFrom": "integrationUserPermission", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integrationUserPermission_integration_id_user_id_permission_pk": { + "columns": ["integration_id", "user_id", "permission"], + "name": "integrationUserPermission_integration_id_user_id_permission_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "integration": { + "name": "integration", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "integration__kind_idx": { + "name": "integration__kind_idx", + "columns": ["kind"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite": { + "name": "invite", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expiration_date": { + "name": "expiration_date", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invite_token_unique": { + "name": "invite_token_unique", + "columns": ["token"], + "isUnique": true + } + }, + "foreignKeys": { + "invite_creator_id_user_id_fk": { + "name": "invite_creator_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "item": { + "name": "item", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "options": { + "name": "options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + }, + "advanced_options": { + "name": "advanced_options", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + } + }, + "indexes": {}, + "foreignKeys": { + "item_section_id_section_id_fk": { + "name": "item_section_id_section_id_fk", + "tableFrom": "item", + "tableTo": "section", + "columnsFrom": ["section_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "media": { + "name": "media", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "blob", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "media_creator_id_user_id_fk": { + "name": "media_creator_id_user_id_fk", + "tableFrom": "media", + "tableTo": "user", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "onboarding": { + "name": "onboarding", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "step": { + "name": "step", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "previous_step": { + "name": "previous_step", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "search_engine": { + "name": "search_engine", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "short": { + "name": "short", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "url_template": { + "name": "url_template", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'generic'" + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "search_engine_short_unique": { + "name": "search_engine_short_unique", + "columns": ["short"], + "isUnique": true + } + }, + "foreignKeys": { + "search_engine_integration_id_integration_id_fk": { + "name": "search_engine_integration_id_integration_id_fk", + "tableFrom": "search_engine", + "tableTo": "integration", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "section_collapse_state": { + "name": "section_collapse_state", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "collapsed": { + "name": "collapsed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_collapse_state_user_id_user_id_fk": { + "name": "section_collapse_state_user_id_user_id_fk", + "tableFrom": "section_collapse_state", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_collapse_state_section_id_section_id_fk": { + "name": "section_collapse_state_section_id_section_id_fk", + "tableFrom": "section_collapse_state", + "tableTo": "section", + "columnsFrom": ["section_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "section_collapse_state_user_id_section_id_pk": { + "columns": ["user_id", "section_id"], + "name": "section_collapse_state_user_id_section_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "section": { + "name": "section", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x_offset": { + "name": "x_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y_offset": { + "name": "y_offset", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_section_id": { + "name": "parent_section_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_board_id_board_id_fk": { + "name": "section_board_id_board_id_fk", + "tableFrom": "section", + "tableTo": "board", + "columnsFrom": ["board_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "section_parent_section_id_section_id_fk": { + "name": "section_parent_section_id_section_id_fk", + "tableFrom": "section", + "tableTo": "section", + "columnsFrom": ["parent_section_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "serverSetting": { + "name": "serverSetting", + "columns": { + "setting_key": { + "name": "setting_key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'{\"json\": {}}'" + } + }, + "indexes": { + "serverSetting_settingKey_unique": { + "name": "serverSetting_settingKey_unique", + "columns": ["setting_key"], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "session_token": { + "name": "session_token", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": ["user_id"], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'credentials'" + }, + "home_board_id": { + "name": "home_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mobile_home_board_id": { + "name": "mobile_home_board_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_search_engine_id": { + "name": "default_search_engine_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "open_search_in_new_tab": { + "name": "open_search_in_new_tab", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "color_scheme": { + "name": "color_scheme", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'dark'" + }, + "first_day_of_week": { + "name": "first_day_of_week", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "ping_icons_enabled": { + "name": "ping_icons_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_home_board_id_board_id_fk": { + "name": "user_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": ["home_board_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_mobile_home_board_id_board_id_fk": { + "name": "user_mobile_home_board_id_board_id_fk", + "tableFrom": "user", + "tableTo": "board", + "columnsFrom": ["mobile_home_board_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "user_default_search_engine_id_search_engine_id_fk": { + "name": "user_default_search_engine_id_search_engine_id_fk", + "tableFrom": "user", + "tableTo": "search_engine", + "columnsFrom": ["default_search_engine_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": ["identifier", "token"], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/packages/db/migrations/sqlite/meta/_journal.json b/packages/db/migrations/sqlite/meta/_journal.json index 396c5b215..d82b86fb8 100644 --- a/packages/db/migrations/sqlite/meta/_journal.json +++ b/packages/db/migrations/sqlite/meta/_journal.json @@ -169,6 +169,13 @@ "when": 1738686324915, "tag": "0023_fix_on_delete_actions", "breakpoints": true + }, + { + "idx": 24, + "version": "6", + "when": 1738961178990, + "tag": "0024_bitter_scrambler", + "breakpoints": true } ] } diff --git a/packages/db/schema/mysql.ts b/packages/db/schema/mysql.ts index c7406057a..870d7f851 100644 --- a/packages/db/schema/mysql.ts +++ b/packages/db/schema/mysql.ts @@ -272,6 +272,7 @@ export const boards = mysqlTable("board", { opacity: int().default(100).notNull(), customCss: text(), columnCount: int().default(10).notNull(), + disableStatus: boolean().default(false).notNull(), }); export const boardUserPermissions = mysqlTable( diff --git a/packages/db/schema/sqlite.ts b/packages/db/schema/sqlite.ts index 000fa9463..44436c84b 100644 --- a/packages/db/schema/sqlite.ts +++ b/packages/db/schema/sqlite.ts @@ -258,6 +258,7 @@ export const boards = sqliteTable("board", { opacity: int().default(100).notNull(), customCss: text(), columnCount: int().default(10).notNull(), + disableStatus: int({ mode: "boolean" }).default(false).notNull(), }); export const boardUserPermissions = sqliteTable( diff --git a/packages/request-handler/src/lib/cached-request-integration-job-handler.ts b/packages/request-handler/src/lib/cached-request-integration-job-handler.ts index c9e2efe9a..1c306f300 100644 --- a/packages/request-handler/src/lib/cached-request-integration-job-handler.ts +++ b/packages/request-handler/src/lib/cached-request-integration-job-handler.ts @@ -5,7 +5,7 @@ import { hashObjectBase64, Stopwatch } from "@homarr/common"; import { decryptSecret } from "@homarr/common/server"; import type { MaybeArray } from "@homarr/common/types"; import { db } from "@homarr/db"; -import { getItemsWithIntegrationsAsync } from "@homarr/db/queries"; +import { getItemsWithIntegrationsAsync, getServerSettingsAsync } from "@homarr/db/queries"; import type { WidgetKind } from "@homarr/definitions"; import { logger } from "@homarr/log"; @@ -33,6 +33,7 @@ export const createRequestIntegrationJobHandler = < }, ) => { return async () => { + const serverSettings = await getServerSettingsAsync(db); const itemsForIntegration = await getItemsWithIntegrationsAsync(db, { kinds: widgetKinds, }); @@ -52,7 +53,17 @@ export const createRequestIntegrationJobHandler = < const oneOrMultipleInputs = getInput[itemForIntegration.kind]( reduceWidgetOptionsWithDefaultValues( itemForIntegration.kind, - SuperJSON.parse(itemForIntegration.options), + { + defaultSearchEngineId: serverSettings.search.defaultSearchEngineId, + openSearchInNewTab: true, + firstDayOfWeek: 1, + homeBoardId: serverSettings.board.homeBoardId, + mobileHomeBoardId: serverSettings.board.mobileHomeBoardId, + pingIconsEnabled: true, + enableStatusByDefault: serverSettings.board.enableStatusByDefault, + forceDisableStatus: serverSettings.board.forceDisableStatus, + }, + SuperJSON.parse>(itemForIntegration.options), ) as never, ); for (const { integration } of itemForIntegration.integrations) { diff --git a/packages/server-settings/src/index.ts b/packages/server-settings/src/index.ts index 35f73699f..12c859165 100644 --- a/packages/server-settings/src/index.ts +++ b/packages/server-settings/src/index.ts @@ -28,6 +28,8 @@ export const defaultServerSettings = { board: { homeBoardId: null as string | null, mobileHomeBoardId: null as string | null, + enableStatusByDefault: true, + forceDisableStatus: false, }, appearance: { defaultColorScheme: "light" as ColorScheme, diff --git a/packages/settings/src/context.tsx b/packages/settings/src/context.tsx index a78fe3fcd..d7b71aa72 100644 --- a/packages/settings/src/context.tsx +++ b/packages/settings/src/context.tsx @@ -8,7 +8,7 @@ import type { RouterOutputs } from "@homarr/api"; import type { User } from "@homarr/db/schema"; import type { ServerSettings } from "@homarr/server-settings"; -type SettingsContextProps = Pick< +export type SettingsContextProps = Pick< User, | "firstDayOfWeek" | "defaultSearchEngineId" @@ -16,11 +16,15 @@ type SettingsContextProps = Pick< | "mobileHomeBoardId" | "openSearchInNewTab" | "pingIconsEnabled" ->; +> & + Pick; interface PublicServerSettings { search: Pick; - board: Pick; + board: Pick< + ServerSettings["board"], + "homeBoardId" | "mobileHomeBoardId" | "enableStatusByDefault" | "forceDisableStatus" + >; } const SettingsContext = createContext(null); @@ -39,6 +43,8 @@ export const SettingsProvider = ({ homeBoardId: user?.homeBoardId ?? serverSettings.board.homeBoardId, mobileHomeBoardId: user?.mobileHomeBoardId ?? serverSettings.board.mobileHomeBoardId, pingIconsEnabled: user?.pingIconsEnabled ?? false, + enableStatusByDefault: serverSettings.board.enableStatusByDefault, + forceDisableStatus: serverSettings.board.forceDisableStatus, }} > {children} diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 379fcd243..f3ad7d017 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -1048,7 +1048,7 @@ "label": "Show description tooltip" }, "pingEnabled": { - "label": "Enable simple ping" + "label": "Enable status check" } }, "error": { @@ -2052,6 +2052,10 @@ "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." } }, + "disableStatus": { + "label": "Disable app status", + "description": "Disables the status check for all apps on this board" + }, "columnCount": { "label": "Column count" }, @@ -2085,6 +2089,9 @@ "customCss": { "title": "Custom css" }, + "behavior": { + "title": "Behavior" + }, "access": { "title": "Access control", "permission": { @@ -2476,6 +2483,17 @@ "label": "Global home board", "mobileLabel": "Global mobile board", "description": "Only public boards are available for selection" + }, + "status": { + "title": "App status", + "enableStatusByDefault": { + "label": "Enable status by default", + "description": "When adding an app item, the status will be enabled by default" + }, + "forceDisableStatus": { + "label": "Force disable status", + "description": "Status for apps will be disabled for all users and can't be enabled" + } } }, "search": { diff --git a/packages/validation/src/board.ts b/packages/validation/src/board.ts index 1fd121a95..50b4113c2 100644 --- a/packages/validation/src/board.ts +++ b/packages/validation/src/board.ts @@ -58,6 +58,7 @@ const savePartialSettingsSchema = z opacity: z.number().min(0).max(100), customCss: z.string().max(16384), columnCount: z.number().min(1).max(24), + disableStatus: z.boolean(), }) .partial(); diff --git a/packages/widgets/package.json b/packages/widgets/package.json index 045d00927..c42989191 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -28,6 +28,7 @@ "@dnd-kit/sortable": "^10.0.0", "@homarr/api": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0", + "@homarr/boards": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", @@ -36,6 +37,7 @@ "@homarr/modals": "workspace:^0.1.0", "@homarr/notifications": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0", + "@homarr/server-settings": "workspace:^0.1.0", "@homarr/settings": "workspace:^0.1.0", "@homarr/spotlight": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", diff --git a/packages/widgets/src/app/component.tsx b/packages/widgets/src/app/component.tsx index 77039a7af..33e111619 100644 --- a/packages/widgets/src/app/component.tsx +++ b/packages/widgets/src/app/component.tsx @@ -7,6 +7,8 @@ import { IconLoader } from "@tabler/icons-react"; import combineClasses from "clsx"; import { clientApi } from "@homarr/api/client"; +import { useRequiredBoard } from "@homarr/boards/context"; +import { useSettings } from "@homarr/settings"; import { useRegisterSpotlightContextResults } from "@homarr/spotlight"; import { useI18n } from "@homarr/translation/client"; @@ -17,6 +19,8 @@ import { PingIndicator } from "./ping/ping-indicator"; export default function AppWidget({ options, isEditMode }: WidgetComponentProps<"app">) { const t = useI18n(); + const settings = useSettings(); + const board = useRequiredBoard(); const [app] = clientApi.app.byId.useSuspenseQuery( { id: options.appId, @@ -81,7 +85,7 @@ export default function AppWidget({ options, isEditMode }: WidgetComponentProps< {app.name} - {options.pingEnabled && app.href ? ( + {options.pingEnabled && !settings.forceDisableStatus && !board.disableStatus && app.href ? ( }> diff --git a/packages/widgets/src/app/index.ts b/packages/widgets/src/app/index.ts index 66d7a0ecb..463f7c00b 100644 --- a/packages/widgets/src/app/index.ts +++ b/packages/widgets/src/app/index.ts @@ -5,13 +5,24 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("app", { icon: IconApps, - options: optionsBuilder.from((factory) => ({ - appId: factory.app(), - openInNewTab: factory.switch({ defaultValue: true }), - showTitle: factory.switch({ defaultValue: true }), - showDescriptionTooltip: factory.switch({ defaultValue: false }), - pingEnabled: factory.switch({ defaultValue: false }), - })), + createOptions(settings) { + return optionsBuilder.from( + (factory) => ({ + appId: factory.app(), + openInNewTab: factory.switch({ defaultValue: true }), + showTitle: factory.switch({ defaultValue: true }), + showDescriptionTooltip: factory.switch({ defaultValue: false }), + pingEnabled: factory.switch({ defaultValue: settings.enableStatusByDefault }), + }), + { + pingEnabled: { + shouldHide() { + return settings.forceDisableStatus; + }, + }, + }, + ); + }, errors: { NOT_FOUND: { icon: IconDeviceDesktopX, diff --git a/packages/widgets/src/bookmarks/index.tsx b/packages/widgets/src/bookmarks/index.tsx index 959a8601d..763b0a3ad 100644 --- a/packages/widgets/src/bookmarks/index.tsx +++ b/packages/widgets/src/bookmarks/index.tsx @@ -10,50 +10,52 @@ import { BookmarkAddButton } from "./add-button"; export const { definition, componentLoader } = createWidgetDefinition("bookmarks", { icon: IconClock, - options: optionsBuilder.from((factory) => ({ - title: factory.text(), - layout: factory.select({ - options: (["grid", "row", "column"] as const).map((value) => ({ - value, - label: (t) => t(`widget.bookmarks.option.layout.option.${value}.label`), - })), - defaultValue: "column", - }), - hideIcon: factory.switch({ defaultValue: false }), - hideHostname: factory.switch({ defaultValue: false }), - openNewTab: factory.switch({ defaultValue: true }), - items: factory.sortableItemList({ - ItemComponent: ({ item, handle: Handle, removeItem, rootAttributes }) => { - return ( - - - + createOptions() { + return optionsBuilder.from((factory) => ({ + title: factory.text(), + layout: factory.select({ + options: (["grid", "row", "column"] as const).map((value) => ({ + value, + label: (t) => t(`widget.bookmarks.option.layout.option.${value}.label`), + })), + defaultValue: "column", + }), + hideIcon: factory.switch({ defaultValue: false }), + hideHostname: factory.switch({ defaultValue: false }), + openNewTab: factory.switch({ defaultValue: true }), + items: factory.sortableItemList({ + ItemComponent: ({ item, handle: Handle, removeItem, rootAttributes }) => { + return ( + + + - - - - {item.name} - + + + + {item.name} + + + + + + + ); + }, + AddButton: BookmarkAddButton, + uniqueIdentifier: (item) => item.id, + useData: (initialIds) => { + const { data, error, isLoading } = clientApi.app.byIds.useQuery(initialIds); - - - - - ); - }, - AddButton: BookmarkAddButton, - uniqueIdentifier: (item) => item.id, - useData: (initialIds) => { - const { data, error, isLoading } = clientApi.app.byIds.useQuery(initialIds); - - return { - data, - error, - isLoading, - }; - }, - }), - })), + return { + data, + error, + isLoading, + }; + }, + }), + })); + }, }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/calendar/index.ts b/packages/widgets/src/calendar/index.ts index 333d47a99..275b2d605 100644 --- a/packages/widgets/src/calendar/index.ts +++ b/packages/widgets/src/calendar/index.ts @@ -9,23 +9,25 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("calendar", { icon: IconCalendar, - options: optionsBuilder.from((factory) => ({ - releaseType: factory.multiSelect({ - defaultValue: ["inCinemas", "digitalRelease"], - options: radarrReleaseTypes.map((value) => ({ - value, - label: (t) => t(`widget.calendar.option.releaseType.options.${value}`), - })), - }), - filterPastMonths: factory.number({ - validate: z.number().min(2).max(9999), - defaultValue: 2, - }), - filterFutureMonths: factory.number({ - validate: z.number().min(2).max(9999), - defaultValue: 2, - }), - })), + createOptions() { + return optionsBuilder.from((factory) => ({ + releaseType: factory.multiSelect({ + defaultValue: ["inCinemas", "digitalRelease"], + options: radarrReleaseTypes.map((value) => ({ + value, + label: (t) => t(`widget.calendar.option.releaseType.options.${value}`), + })), + }), + filterPastMonths: factory.number({ + validate: z.number().min(2).max(9999), + defaultValue: 2, + }), + filterFutureMonths: factory.number({ + validate: z.number().min(2).max(9999), + defaultValue: 2, + }), + })); + }, supportedIntegrations: getIntegrationKindsByCategory("calendar"), integrationsRequired: false, }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/clock/index.ts b/packages/widgets/src/clock/index.ts index 47125609c..d80055a89 100644 --- a/packages/widgets/src/clock/index.ts +++ b/packages/widgets/src/clock/index.ts @@ -6,65 +6,67 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("clock", { icon: IconClock, - options: optionsBuilder.from( - (factory) => ({ - customTitleToggle: factory.switch({ - defaultValue: false, - withDescription: true, + createOptions() { + return optionsBuilder.from( + (factory) => ({ + customTitleToggle: factory.switch({ + defaultValue: false, + withDescription: true, + }), + customTitle: factory.text({ + defaultValue: "", + }), + is24HourFormat: factory.switch({ + defaultValue: true, + withDescription: true, + }), + showSeconds: factory.switch({ + defaultValue: false, + }), + useCustomTimezone: factory.switch({ defaultValue: false }), + timezone: factory.select({ + options: Intl.supportedValuesOf("timeZone").map((value) => value), + defaultValue: "Europe/London", + searchable: true, + withDescription: true, + }), + showDate: factory.switch({ + defaultValue: true, + }), + dateFormat: factory.select({ + options: [ + { value: "dddd, MMMM D", label: dayjs().format("dddd, MMMM D") }, + { value: "dddd, D MMMM", label: dayjs().format("dddd, D MMMM") }, + { value: "MMM D", label: dayjs().format("MMM D") }, + { value: "D MMM", label: dayjs().format("D MMM") }, + { value: "DD/MM/YYYY", label: dayjs().format("DD/MM/YYYY") }, + { value: "MM/DD/YYYY", label: dayjs().format("MM/DD/YYYY") }, + { value: "DD/MM", label: dayjs().format("DD/MM") }, + { value: "MM/DD", label: dayjs().format("MM/DD") }, + ], + defaultValue: "dddd, MMMM D", + withDescription: true, + }), + customTimeFormat: factory.text({ + defaultValue: "", + withDescription: true, + }), + customDateFormat: factory.text({ + defaultValue: "", + withDescription: true, + }), }), - customTitle: factory.text({ - defaultValue: "", - }), - is24HourFormat: factory.switch({ - defaultValue: true, - withDescription: true, - }), - showSeconds: factory.switch({ - defaultValue: false, - }), - useCustomTimezone: factory.switch({ defaultValue: false }), - timezone: factory.select({ - options: Intl.supportedValuesOf("timeZone").map((value) => value), - defaultValue: "Europe/London", - searchable: true, - withDescription: true, - }), - showDate: factory.switch({ - defaultValue: true, - }), - dateFormat: factory.select({ - options: [ - { value: "dddd, MMMM D", label: dayjs().format("dddd, MMMM D") }, - { value: "dddd, D MMMM", label: dayjs().format("dddd, D MMMM") }, - { value: "MMM D", label: dayjs().format("MMM D") }, - { value: "D MMM", label: dayjs().format("D MMM") }, - { value: "DD/MM/YYYY", label: dayjs().format("DD/MM/YYYY") }, - { value: "MM/DD/YYYY", label: dayjs().format("MM/DD/YYYY") }, - { value: "DD/MM", label: dayjs().format("DD/MM") }, - { value: "MM/DD", label: dayjs().format("MM/DD") }, - ], - defaultValue: "dddd, MMMM D", - withDescription: true, - }), - customTimeFormat: factory.text({ - defaultValue: "", - withDescription: true, - }), - customDateFormat: factory.text({ - defaultValue: "", - withDescription: true, - }), - }), - { - customTitle: { - shouldHide: (options) => !options.customTitleToggle, + { + customTitle: { + shouldHide: (options) => !options.customTitleToggle, + }, + timezone: { + shouldHide: (options) => !options.useCustomTimezone, + }, + dateFormat: { + shouldHide: (options) => !options.showDate, + }, }, - timezone: { - shouldHide: (options) => !options.useCustomTimezone, - }, - dateFormat: { - shouldHide: (options) => !options.showDate, - }, - }, - ), + ); + }, }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/definition.ts b/packages/widgets/src/definition.ts index 7ee07ed37..85e2de5ad 100644 --- a/packages/widgets/src/definition.ts +++ b/packages/widgets/src/definition.ts @@ -2,11 +2,13 @@ import type { LoaderComponent } from "next/dynamic"; import type { DefaultErrorData } from "@trpc/server/unstable-core-do-not-import"; import type { IntegrationKind, WidgetKind } from "@homarr/definitions"; +import type { ServerSettings } from "@homarr/server-settings"; +import type { SettingsContextProps } from "@homarr/settings"; import type { stringOrTranslation } from "@homarr/translation"; import type { TablerIcon } from "@homarr/ui"; import type { WidgetImports } from "."; -import type { inferOptionsFromDefinition, WidgetOptionsRecord } from "./options"; +import type { inferOptionsFromCreator, WidgetOptionsRecord } from "./options"; const createWithDynamicImport = (kind: TKind, definition: TDefinition) => @@ -30,7 +32,7 @@ export interface WidgetDefinition { icon: TablerIcon; supportedIntegrations?: IntegrationKind[]; integrationsRequired?: boolean; - options: WidgetOptionsRecord; + createOptions: (settings: SettingsContextProps) => WidgetOptionsRecord; errors?: Partial< Record< DefaultErrorData["code"], @@ -44,7 +46,7 @@ export interface WidgetDefinition { } export interface WidgetProps { - options: inferOptionsFromDefinition>; + options: inferOptionsFromCreator>; integrationIds: string[]; itemId: string | undefined; // undefined when in preview mode } @@ -52,13 +54,19 @@ export interface WidgetProps { export type WidgetComponentProps = WidgetProps & { boardId: string | undefined; // undefined when in preview mode isEditMode: boolean; - setOptions: ({ - newOptions, - }: { - newOptions: Partial>>; - }) => void; + setOptions: ({ newOptions }: { newOptions: Partial>> }) => void; width: number; height: number; }; -export type WidgetOptionsRecordOf = WidgetImports[TKind]["definition"]["options"]; +export type WidgetOptionsRecordOf = WidgetImports[TKind]["definition"]["createOptions"]; + +/** + * The following type should only include values that can be available for user (including anonymous). + * Because they need to be provided to the client to for example set certain default values. + */ +export interface WidgetOptionsSettings { + server: { + board: Pick; + }; +} diff --git a/packages/widgets/src/dns-hole/controls/index.ts b/packages/widgets/src/dns-hole/controls/index.ts index 27f126ee9..50a3c3c80 100644 --- a/packages/widgets/src/dns-hole/controls/index.ts +++ b/packages/widgets/src/dns-hole/controls/index.ts @@ -9,11 +9,13 @@ export const widgetKind = "dnsHoleControls"; export const { definition, componentLoader } = createWidgetDefinition(widgetKind, { icon: IconDeviceGamepad, - options: optionsBuilder.from((factory) => ({ - showToggleAllButtons: factory.switch({ - defaultValue: true, - }), - })), + createOptions() { + return optionsBuilder.from((factory) => ({ + showToggleAllButtons: factory.switch({ + defaultValue: true, + }), + })); + }, supportedIntegrations: getIntegrationKindsByCategory("dnsHole"), errors: { INTERNAL_SERVER_ERROR: { diff --git a/packages/widgets/src/dns-hole/summary/index.ts b/packages/widgets/src/dns-hole/summary/index.ts index 25246d23f..fe1c8b8a1 100644 --- a/packages/widgets/src/dns-hole/summary/index.ts +++ b/packages/widgets/src/dns-hole/summary/index.ts @@ -9,18 +9,20 @@ export const widgetKind = "dnsHoleSummary"; export const { definition, componentLoader } = createWidgetDefinition(widgetKind, { icon: IconAd, - options: optionsBuilder.from((factory) => ({ - usePiHoleColors: factory.switch({ - defaultValue: true, - }), - layout: factory.select({ - options: (["grid", "row", "column"] as const).map((value) => ({ - value, - label: (t) => t(`widget.dnsHoleSummary.option.layout.option.${value}.label`), - })), - defaultValue: "grid", - }), - })), + createOptions() { + return optionsBuilder.from((factory) => ({ + usePiHoleColors: factory.switch({ + defaultValue: true, + }), + layout: factory.select({ + options: (["grid", "row", "column"] as const).map((value) => ({ + value, + label: (t) => t(`widget.dnsHoleSummary.option.layout.option.${value}.label`), + })), + defaultValue: "grid", + }), + })); + }, supportedIntegrations: getIntegrationKindsByCategory("dnsHole"), errors: { INTERNAL_SERVER_ERROR: { diff --git a/packages/widgets/src/downloads/index.ts b/packages/widgets/src/downloads/index.ts index c5161463e..e4b8831b6 100644 --- a/packages/widgets/src/downloads/index.ts +++ b/packages/widgets/src/downloads/index.ts @@ -33,76 +33,78 @@ const columnsSort = columnsList.filter((column) => export const { definition, componentLoader } = createWidgetDefinition("downloads", { icon: IconDownload, - options: optionsBuilder.from( - (factory) => ({ - columns: factory.multiSelect({ - defaultValue: ["integration", "name", "progress", "time", "actions"], - options: columnsList.map((value) => ({ - value, - label: (t) => t(`widget.downloads.items.${value}.columnTitle`), - })), - searchable: true, + createOptions() { + return optionsBuilder.from( + (factory) => ({ + columns: factory.multiSelect({ + defaultValue: ["integration", "name", "progress", "time", "actions"], + options: columnsList.map((value) => ({ + value, + label: (t) => t(`widget.downloads.items.${value}.columnTitle`), + })), + searchable: true, + }), + enableRowSorting: factory.switch({ + defaultValue: false, + }), + defaultSort: factory.select({ + defaultValue: "type", + options: columnsSort.map((value) => ({ + value, + label: (t) => t(`widget.downloads.items.${value}.columnTitle`), + })), + }), + descendingDefaultSort: factory.switch({ + defaultValue: false, + }), + showCompletedUsenet: factory.switch({ + defaultValue: true, + }), + showCompletedTorrent: factory.switch({ + defaultValue: true, + }), + activeTorrentThreshold: factory.number({ + //in KiB/s + validate: z.number().min(0), + defaultValue: 0, + step: 1, + }), + categoryFilter: factory.multiText({ + defaultValue: [] as string[], + validate: z.string(), + }), + filterIsWhitelist: factory.switch({ + defaultValue: false, + }), + applyFilterToRatio: factory.switch({ + defaultValue: true, + }), }), - enableRowSorting: factory.switch({ - defaultValue: false, - }), - defaultSort: factory.select({ - defaultValue: "type", - options: columnsSort.map((value) => ({ - value, - label: (t) => t(`widget.downloads.items.${value}.columnTitle`), - })), - }), - descendingDefaultSort: factory.switch({ - defaultValue: false, - }), - showCompletedUsenet: factory.switch({ - defaultValue: true, - }), - showCompletedTorrent: factory.switch({ - defaultValue: true, - }), - activeTorrentThreshold: factory.number({ - //in KiB/s - validate: z.number().min(0), - defaultValue: 0, - step: 1, - }), - categoryFilter: factory.multiText({ - defaultValue: [] as string[], - validate: z.string(), - }), - filterIsWhitelist: factory.switch({ - defaultValue: false, - }), - applyFilterToRatio: factory.switch({ - defaultValue: true, - }), - }), - { - defaultSort: { - shouldHide: (options) => !options.enableRowSorting, + { + defaultSort: { + shouldHide: (options) => !options.enableRowSorting, + }, + descendingDefaultSort: { + shouldHide: (options) => !options.enableRowSorting, + }, + showCompletedUsenet: { + shouldHide: (_, integrationKinds) => + !getIntegrationKindsByCategory("usenet").some((kinds) => integrationKinds.includes(kinds)), + }, + showCompletedTorrent: { + shouldHide: (_, integrationKinds) => + !getIntegrationKindsByCategory("torrent").some((kinds) => integrationKinds.includes(kinds)), + }, + activeTorrentThreshold: { + shouldHide: (_, integrationKinds) => + !getIntegrationKindsByCategory("torrent").some((kinds) => integrationKinds.includes(kinds)), + }, + applyFilterToRatio: { + shouldHide: (_, integrationKinds) => + !getIntegrationKindsByCategory("torrent").some((kinds) => integrationKinds.includes(kinds)), + }, }, - descendingDefaultSort: { - shouldHide: (options) => !options.enableRowSorting, - }, - showCompletedUsenet: { - shouldHide: (_, integrationKinds) => - !getIntegrationKindsByCategory("usenet").some((kinds) => integrationKinds.includes(kinds)), - }, - showCompletedTorrent: { - shouldHide: (_, integrationKinds) => - !getIntegrationKindsByCategory("torrent").some((kinds) => integrationKinds.includes(kinds)), - }, - activeTorrentThreshold: { - shouldHide: (_, integrationKinds) => - !getIntegrationKindsByCategory("torrent").some((kinds) => integrationKinds.includes(kinds)), - }, - applyFilterToRatio: { - shouldHide: (_, integrationKinds) => - !getIntegrationKindsByCategory("torrent").some((kinds) => integrationKinds.includes(kinds)), - }, - }, - ), + ); + }, supportedIntegrations: getIntegrationKindsByCategory("downloadClient"), }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/health-monitoring/index.ts b/packages/widgets/src/health-monitoring/index.ts index 2d9c9e895..b3bc28980 100644 --- a/packages/widgets/src/health-monitoring/index.ts +++ b/packages/widgets/src/health-monitoring/index.ts @@ -7,34 +7,36 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("healthMonitoring", { icon: IconHeartRateMonitor, - options: optionsBuilder.from((factory) => ({ - fahrenheit: factory.switch({ - defaultValue: false, - }), - cpu: factory.switch({ - defaultValue: true, - }), - memory: factory.switch({ - defaultValue: true, - }), - fileSystem: factory.switch({ - defaultValue: true, - }), - defaultTab: factory.select({ - defaultValue: "system", - options: [ - { value: "system", label: "System" }, - { value: "cluster", label: "Cluster" }, - ] as const, - }), - sectionIndicatorRequirement: factory.select({ - defaultValue: "all", - options: [ - { value: "all", label: "All active" }, - { value: "any", label: "Any active" }, - ] as const, - }), - })), + createOptions() { + return optionsBuilder.from((factory) => ({ + fahrenheit: factory.switch({ + defaultValue: false, + }), + cpu: factory.switch({ + defaultValue: true, + }), + memory: factory.switch({ + defaultValue: true, + }), + fileSystem: factory.switch({ + defaultValue: true, + }), + defaultTab: factory.select({ + defaultValue: "system", + options: [ + { value: "system", label: "System" }, + { value: "cluster", label: "Cluster" }, + ] as const, + }), + sectionIndicatorRequirement: factory.select({ + defaultValue: "all", + options: [ + { value: "all", label: "All active" }, + { value: "any", label: "Any active" }, + ] as const, + }), + })); + }, supportedIntegrations: getIntegrationKindsByCategory("healthMonitoring"), errors: { INTERNAL_SERVER_ERROR: { diff --git a/packages/widgets/src/iframe/index.ts b/packages/widgets/src/iframe/index.ts index 97b277ff8..725e1b3fc 100644 --- a/packages/widgets/src/iframe/index.ts +++ b/packages/widgets/src/iframe/index.ts @@ -5,17 +5,19 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("iframe", { icon: IconBrowser, - options: optionsBuilder.from((factory) => ({ - embedUrl: factory.text(), - allowFullScreen: factory.switch(), - allowScrolling: factory.switch({ - defaultValue: true, - }), - allowTransparency: factory.switch(), - allowPayment: factory.switch(), - allowAutoPlay: factory.switch(), - allowMicrophone: factory.switch(), - allowCamera: factory.switch(), - allowGeolocation: factory.switch(), - })), + createOptions() { + return optionsBuilder.from((factory) => ({ + embedUrl: factory.text(), + allowFullScreen: factory.switch(), + allowScrolling: factory.switch({ + defaultValue: true, + }), + allowTransparency: factory.switch(), + allowPayment: factory.switch(), + allowAutoPlay: factory.switch(), + allowMicrophone: factory.switch(), + allowCamera: factory.switch(), + allowGeolocation: factory.switch(), + })); + }, }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/index.tsx b/packages/widgets/src/index.tsx index b041c11ba..e5ca8edf0 100644 --- a/packages/widgets/src/index.tsx +++ b/packages/widgets/src/index.tsx @@ -5,6 +5,7 @@ import { Center, Loader as UiLoader } from "@mantine/core"; import { objectEntries } from "@homarr/common"; import type { IntegrationKind, WidgetKind } from "@homarr/definitions"; +import type { SettingsContextProps } from "@homarr/settings"; import * as app from "./app"; import * as bookmarks from "./bookmarks"; @@ -31,7 +32,7 @@ import * as smartHomeExecuteAutomation from "./smart-home/execute-automation"; import * as video from "./video"; import * as weather from "./weather"; -export type { WidgetDefinition } from "./definition"; +export type { WidgetDefinition, WidgetOptionsSettings } from "./definition"; export type { WidgetComponentProps }; export const widgetImports = { @@ -94,9 +95,13 @@ export type inferSupportedIntegrationsStrict = (Widget ? WidgetImports[TKind]["definition"]["supportedIntegrations"] : never[])[number]; -export const reduceWidgetOptionsWithDefaultValues = (kind: WidgetKind, currentValue: Record = {}) => { +export const reduceWidgetOptionsWithDefaultValues = ( + kind: WidgetKind, + settings: SettingsContextProps, + currentValue: Record = {}, +) => { const definition = widgetImports[kind].definition; - const options = definition.options as Record; + const options = definition.createOptions(settings) as Record; return objectEntries(options).reduce( (prev, [key, value]) => ({ ...prev, diff --git a/packages/widgets/src/indexer-manager/index.ts b/packages/widgets/src/indexer-manager/index.ts index d0539194e..c2160999f 100644 --- a/packages/widgets/src/indexer-manager/index.ts +++ b/packages/widgets/src/indexer-manager/index.ts @@ -7,11 +7,13 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("indexerManager", { icon: IconReportSearch, - options: optionsBuilder.from((factory) => ({ - openIndexerSiteInNewTab: factory.switch({ - defaultValue: true, - }), - })), + createOptions() { + return optionsBuilder.from((factory) => ({ + openIndexerSiteInNewTab: factory.switch({ + defaultValue: true, + }), + })); + }, supportedIntegrations: getIntegrationKindsByCategory("indexerManager"), errors: { INTERNAL_SERVER_ERROR: { diff --git a/packages/widgets/src/media-requests/list/index.ts b/packages/widgets/src/media-requests/list/index.ts index 93243e0df..8f1a88575 100644 --- a/packages/widgets/src/media-requests/list/index.ts +++ b/packages/widgets/src/media-requests/list/index.ts @@ -7,10 +7,12 @@ import { optionsBuilder } from "../../options"; export const { componentLoader, definition } = createWidgetDefinition("mediaRequests-requestList", { icon: IconZoomQuestion, - options: optionsBuilder.from((factory) => ({ - linksTargetNewTab: factory.switch({ - defaultValue: true, - }), - })), + createOptions() { + return optionsBuilder.from((factory) => ({ + linksTargetNewTab: factory.switch({ + defaultValue: true, + }), + })); + }, supportedIntegrations: getIntegrationKindsByCategory("mediaRequest"), }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/media-requests/stats/index.ts b/packages/widgets/src/media-requests/stats/index.ts index b8504c7d2..25911c894 100644 --- a/packages/widgets/src/media-requests/stats/index.ts +++ b/packages/widgets/src/media-requests/stats/index.ts @@ -6,6 +6,8 @@ import { createWidgetDefinition } from "../../definition"; export const { componentLoader, definition } = createWidgetDefinition("mediaRequests-requestStats", { icon: IconChartBar, - options: {}, + createOptions() { + return {}; + }, supportedIntegrations: getIntegrationKindsByCategory("mediaRequest"), }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/media-server/index.ts b/packages/widgets/src/media-server/index.ts index f5cd3ad57..5e9fe1f3a 100644 --- a/packages/widgets/src/media-server/index.ts +++ b/packages/widgets/src/media-server/index.ts @@ -4,6 +4,8 @@ import { createWidgetDefinition } from "../definition"; export const { componentLoader, definition } = createWidgetDefinition("mediaServer", { icon: IconVideo, - options: {}, + createOptions() { + return {}; + }, supportedIntegrations: ["jellyfin", "plex"], }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/media-transcoding/index.ts b/packages/widgets/src/media-transcoding/index.ts index 53e523645..355f3d9d3 100644 --- a/packages/widgets/src/media-transcoding/index.ts +++ b/packages/widgets/src/media-transcoding/index.ts @@ -6,16 +6,18 @@ import { optionsBuilder } from "../options"; export const { componentLoader, definition } = createWidgetDefinition("mediaTranscoding", { icon: IconTransform, - options: optionsBuilder.from((factory) => ({ - defaultView: factory.select({ - defaultValue: "statistics", - options: [ - { label: "Workers", value: "workers" }, - { label: "Queue", value: "queue" }, - { label: "Statistics", value: "statistics" }, - ], - }), - queuePageSize: factory.number({ defaultValue: 10, validate: z.number().min(1).max(30) }), - })), + createOptions() { + return optionsBuilder.from((factory) => ({ + defaultView: factory.select({ + defaultValue: "statistics", + options: [ + { label: "Workers", value: "workers" }, + { label: "Queue", value: "queue" }, + { label: "Statistics", value: "statistics" }, + ], + }), + queuePageSize: factory.number({ defaultValue: 10, validate: z.number().min(1).max(30) }), + })); + }, supportedIntegrations: ["tdarr"], }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/minecraft/server-status/index.ts b/packages/widgets/src/minecraft/server-status/index.ts index d929f40da..1f258e75c 100644 --- a/packages/widgets/src/minecraft/server-status/index.ts +++ b/packages/widgets/src/minecraft/server-status/index.ts @@ -6,9 +6,11 @@ import { optionsBuilder } from "../../options"; export const { componentLoader, definition } = createWidgetDefinition("minecraftServerStatus", { icon: IconBrandMinecraft, - options: optionsBuilder.from((factory) => ({ - title: factory.text({ defaultValue: "" }), - domain: factory.text({ defaultValue: "hypixel.net", validate: z.string().nonempty() }), - isBedrockServer: factory.switch({ defaultValue: false }), - })), + createOptions() { + return optionsBuilder.from((factory) => ({ + title: factory.text({ defaultValue: "" }), + domain: factory.text({ defaultValue: "hypixel.net", validate: z.string().nonempty() }), + isBedrockServer: factory.switch({ defaultValue: false }), + })); + }, }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/modals/widget-edit-modal.tsx b/packages/widgets/src/modals/widget-edit-modal.tsx index f3d5f6f45..446d8c6c0 100644 --- a/packages/widgets/src/modals/widget-edit-modal.tsx +++ b/packages/widgets/src/modals/widget-edit-modal.tsx @@ -8,6 +8,7 @@ import { objectEntries } from "@homarr/common"; import type { WidgetKind } from "@homarr/definitions"; import { zodResolver } from "@homarr/form"; import { createModal, useModalAction } from "@homarr/modals"; +import type { SettingsContextProps } from "@homarr/settings"; import { useI18n } from "@homarr/translation/client"; import { zodErrorMap } from "@homarr/validation/form"; @@ -32,6 +33,7 @@ interface ModalProps { onSuccessfulEdit: (value: WidgetEditModalState) => void; integrationData: IntegrationSelectOption[]; integrationSupport: boolean; + settings: SettingsContextProps; } export const WidgetEditModal = createModal>(({ actions, innerProps }) => { @@ -40,13 +42,16 @@ export const WidgetEditModal = createModal>(({ actions, i // Translate the error messages z.setErrorMap(zodErrorMap(t)); + const { definition } = widgetImports[innerProps.kind]; + const options = definition.createOptions(innerProps.settings) as Record; + const form = useForm({ mode: "controlled", initialValues: innerProps.value, validate: zodResolver( z.object({ options: z.object( - objectEntries(widgetImports[innerProps.kind].definition.options).reduce( + objectEntries(options).reduce( (acc, [key, value]: [string, { type: string; validate?: z.ZodType }]) => { if (value.validate) { acc[key] = value.type === "multiText" ? z.array(value.validate).optional() : value.validate; @@ -68,8 +73,6 @@ export const WidgetEditModal = createModal>(({ actions, i }); const { openModal } = useModalAction(WidgetAdvancedOptionsModal); - const { definition } = widgetImports[innerProps.kind]; - return (
{ @@ -89,7 +92,7 @@ export const WidgetEditModal = createModal>(({ actions, i {...form.getInputProps("integrationIds")} /> )} - {Object.entries(definition.options).map(([key, value]: [string, OptionsBuilderResult[string]]) => { + {Object.entries(options).map(([key, value]) => { const Input = getInputForType(value.type); if ( diff --git a/packages/widgets/src/notebook/index.ts b/packages/widgets/src/notebook/index.ts index 5258ee78d..6b85f3e33 100644 --- a/packages/widgets/src/notebook/index.ts +++ b/packages/widgets/src/notebook/index.ts @@ -6,22 +6,24 @@ import { defaultContent } from "./default-content"; export const { definition, componentLoader } = createWidgetDefinition("notebook", { icon: IconNotes, - options: optionsBuilder.from( - (factory) => ({ - showToolbar: factory.switch({ - defaultValue: true, + createOptions() { + return optionsBuilder.from( + (factory) => ({ + showToolbar: factory.switch({ + defaultValue: true, + }), + allowReadOnlyCheck: factory.switch({ + defaultValue: true, + }), + content: factory.text({ + defaultValue: defaultContent, + }), }), - allowReadOnlyCheck: factory.switch({ - defaultValue: true, - }), - content: factory.text({ - defaultValue: defaultContent, - }), - }), - { - content: { - shouldHide: () => true, // Hide the content option as it can be modified in the editor + { + content: { + shouldHide: () => true, // Hide the content option as it can be modified in the editor + }, }, - }, - ), + ); + }, }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/options.ts b/packages/widgets/src/options.ts index 20a5330c2..4f74a13a7 100644 --- a/packages/widgets/src/options.ts +++ b/packages/widgets/src/options.ts @@ -149,6 +149,10 @@ export type WidgetOptionType = WidgetOptionDefinition["type"]; export type WidgetOptionOfType = Extract; type inferOptionFromDefinition = TDefinition["defaultValue"]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type inferOptionsFromCreator WidgetOptionsRecord> = + inferOptionsFromDefinition>; export type inferOptionsFromDefinition = { [key in keyof TOptions]: inferOptionFromDefinition; }; diff --git a/packages/widgets/src/rssFeed/index.ts b/packages/widgets/src/rssFeed/index.ts index 267547631..cd6a66c06 100644 --- a/packages/widgets/src/rssFeed/index.ts +++ b/packages/widgets/src/rssFeed/index.ts @@ -12,21 +12,23 @@ import { optionsBuilder } from "../options"; */ export const { definition, componentLoader } = createWidgetDefinition("rssFeed", { icon: IconRss, - options: optionsBuilder.from((factory) => ({ - feedUrls: factory.multiText({ - defaultValue: [], - validate: z.string().url(), - }), - enableRtl: factory.switch({ - defaultValue: false, - }), - textLinesClamp: factory.number({ - defaultValue: 5, - validate: z.number().min(1).max(50), - }), - maximumAmountPosts: factory.number({ - defaultValue: 100, - validate: z.number().min(1).max(9999), - }), - })), + createOptions() { + return optionsBuilder.from((factory) => ({ + feedUrls: factory.multiText({ + defaultValue: [], + validate: z.string().url(), + }), + enableRtl: factory.switch({ + defaultValue: false, + }), + textLinesClamp: factory.number({ + defaultValue: 5, + validate: z.number().min(1).max(50), + }), + maximumAmountPosts: factory.number({ + defaultValue: 100, + validate: z.number().min(1).max(9999), + }), + })); + }, }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/smart-home/entity-state/index.ts b/packages/widgets/src/smart-home/entity-state/index.ts index 4bfdaaaff..a4dd32921 100644 --- a/packages/widgets/src/smart-home/entity-state/index.ts +++ b/packages/widgets/src/smart-home/entity-state/index.ts @@ -7,15 +7,17 @@ import { optionsBuilder } from "../../options"; export const { definition, componentLoader } = createWidgetDefinition("smartHome-entityState", { icon: IconBinaryTree, - options: optionsBuilder.from((factory) => ({ - entityId: factory.text({ - defaultValue: "sun.sun", - }), - displayName: factory.text({ - defaultValue: "Sun", - }), - entityUnit: factory.text(), - clickable: factory.switch(), - })), + createOptions() { + return optionsBuilder.from((factory) => ({ + entityId: factory.text({ + defaultValue: "sun.sun", + }), + displayName: factory.text({ + defaultValue: "Sun", + }), + entityUnit: factory.text(), + clickable: factory.switch(), + })); + }, supportedIntegrations: getIntegrationKindsByCategory("smartHomeServer"), }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/smart-home/execute-automation/index.ts b/packages/widgets/src/smart-home/execute-automation/index.ts index 96c6f8394..ea957fab9 100644 --- a/packages/widgets/src/smart-home/execute-automation/index.ts +++ b/packages/widgets/src/smart-home/execute-automation/index.ts @@ -7,9 +7,11 @@ import { optionsBuilder } from "../../options"; export const { definition, componentLoader } = createWidgetDefinition("smartHome-executeAutomation", { icon: IconBinaryTree, - options: optionsBuilder.from((factory) => ({ - displayName: factory.text(), - automationId: factory.text(), - })), + createOptions() { + return optionsBuilder.from((factory) => ({ + displayName: factory.text(), + automationId: factory.text(), + })); + }, supportedIntegrations: getIntegrationKindsByCategory("smartHomeServer"), }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/test/translation.spec.ts b/packages/widgets/src/test/translation.spec.ts index 967c5de85..70a6721b8 100644 --- a/packages/widgets/src/test/translation.spec.ts +++ b/packages/widgets/src/test/translation.spec.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { objectEntries } from "@homarr/common"; +import type { SettingsContextProps } from "@homarr/settings"; import { createLanguageMapping } from "@homarr/translation"; import { widgetImports } from ".."; @@ -8,26 +9,25 @@ import { widgetImports } from ".."; describe("Widget properties with description should have matching translations", async () => { const enTranslation = await createLanguageMapping().en(); objectEntries(widgetImports).forEach(([key, value]) => { - Object.entries(value.definition.options).forEach( - ([optionKey, optionValue]: [string, { withDescription?: boolean }]) => { - it(`should have matching translations for ${optionKey} option description of ${key} widget`, () => { - const option = enTranslation.default.widget[key].option; - if (!(optionKey in option)) { - throw new Error(`Option ${optionKey} not found in translation`); - } - const value = option[optionKey as keyof typeof option]; + Object.entries(value.definition.createOptions({} as SettingsContextProps)).forEach(([optionKey, optionValue_]) => { + const optionValue = optionValue_ as { withDescription: boolean }; + it(`should have matching translations for ${optionKey} option description of ${key} widget`, () => { + const option = enTranslation.default.widget[key].option; + if (!(optionKey in option)) { + throw new Error(`Option ${optionKey} not found in translation`); + } + const value = option[optionKey as keyof typeof option]; - expect("description" in value).toBe(optionValue.withDescription); - }); - }, - ); + expect("description" in value).toBe(optionValue.withDescription); + }); + }); }); }); describe("Widget properties should have matching name translations", async () => { const enTranslation = await createLanguageMapping().en(); objectEntries(widgetImports).forEach(([key, value]) => { - Object.keys(value.definition.options).forEach((optionKey) => { + Object.keys(value.definition.createOptions({} as SettingsContextProps)).forEach((optionKey) => { it(`should have matching translations for ${optionKey} option name of ${key} widget`, () => { const option = enTranslation.default.widget[key].option; if (!(optionKey in option)) { diff --git a/packages/widgets/src/video/index.ts b/packages/widgets/src/video/index.ts index 616ca62ba..d35a83dd5 100644 --- a/packages/widgets/src/video/index.ts +++ b/packages/widgets/src/video/index.ts @@ -5,16 +5,18 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("video", { icon: IconDeviceCctv, - options: optionsBuilder.from((factory) => ({ - feedUrl: factory.text({ - defaultValue: "", - }), - hasAutoPlay: factory.switch({ - withDescription: true, - }), - isMuted: factory.switch({ - defaultValue: true, - }), - hasControls: factory.switch(), - })), + createOptions() { + return optionsBuilder.from((factory) => ({ + feedUrl: factory.text({ + defaultValue: "", + }), + hasAutoPlay: factory.switch({ + withDescription: true, + }), + isMuted: factory.switch({ + defaultValue: true, + }), + hasControls: factory.switch(), + })); + }, }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/weather/index.ts b/packages/widgets/src/weather/index.ts index 96bb83031..e120c5783 100644 --- a/packages/widgets/src/weather/index.ts +++ b/packages/widgets/src/weather/index.ts @@ -7,47 +7,49 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("weather", { icon: IconCloud, - options: optionsBuilder.from( - (factory) => ({ - isFormatFahrenheit: factory.switch(), - disableTemperatureDecimals: factory.switch(), - showCurrentWindSpeed: factory.switch({ withDescription: true }), - location: factory.location({ - defaultValue: { - name: "Paris", - latitude: 48.85341, - longitude: 2.3488, - }, + createOptions() { + return optionsBuilder.from( + (factory) => ({ + isFormatFahrenheit: factory.switch(), + disableTemperatureDecimals: factory.switch(), + showCurrentWindSpeed: factory.switch({ withDescription: true }), + location: factory.location({ + defaultValue: { + name: "Paris", + latitude: 48.85341, + longitude: 2.3488, + }, + }), + dateFormat: factory.select({ + options: [ + { value: "dddd, MMMM D", label: dayjs().format("dddd, MMMM D") }, + { value: "dddd, D MMMM", label: dayjs().format("dddd, D MMMM") }, + { value: "MMM D", label: dayjs().format("MMM D") }, + { value: "D MMM", label: dayjs().format("D MMM") }, + { value: "DD/MM/YYYY", label: dayjs().format("DD/MM/YYYY") }, + { value: "MM/DD/YYYY", label: dayjs().format("MM/DD/YYYY") }, + { value: "DD/MM", label: dayjs().format("DD/MM") }, + { value: "MM/DD", label: dayjs().format("MM/DD") }, + ], + defaultValue: "dddd, MMMM D", + withDescription: true, + }), + showCity: factory.switch(), + hasForecast: factory.switch(), + forecastDayCount: factory.slider({ + defaultValue: 5, + validate: z.number().min(1).max(7), + step: 1, + withDescription: true, + }), }), - dateFormat: factory.select({ - options: [ - { value: "dddd, MMMM D", label: dayjs().format("dddd, MMMM D") }, - { value: "dddd, D MMMM", label: dayjs().format("dddd, D MMMM") }, - { value: "MMM D", label: dayjs().format("MMM D") }, - { value: "D MMM", label: dayjs().format("D MMM") }, - { value: "DD/MM/YYYY", label: dayjs().format("DD/MM/YYYY") }, - { value: "MM/DD/YYYY", label: dayjs().format("MM/DD/YYYY") }, - { value: "DD/MM", label: dayjs().format("DD/MM") }, - { value: "MM/DD", label: dayjs().format("MM/DD") }, - ], - defaultValue: "dddd, MMMM D", - withDescription: true, - }), - showCity: factory.switch(), - hasForecast: factory.switch(), - forecastDayCount: factory.slider({ - defaultValue: 5, - validate: z.number().min(1).max(7), - step: 1, - withDescription: true, - }), - }), - { - forecastDayCount: { - shouldHide({ hasForecast }) { - return !hasForecast; + { + forecastDayCount: { + shouldHide({ hasForecast }) { + return !hasForecast; + }, }, }, - }, - ), + ); + }, }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/tsconfig.json b/packages/widgets/tsconfig.json index cbe8483d9..e205abceb 100644 --- a/packages/widgets/tsconfig.json +++ b/packages/widgets/tsconfig.json @@ -3,6 +3,6 @@ "compilerOptions": { "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" }, - "include": ["*.ts", "src"], + "include": ["*.ts", "src", "*.tsx"], "exclude": ["node_modules"] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e044fb4c0..e65d2bcd0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,6 +91,9 @@ importers: '@homarr/auth': specifier: workspace:^0.1.0 version: link:../../packages/auth + '@homarr/boards': + specifier: workspace:^0.1.0 + version: link:../../packages/boards '@homarr/certificates': specifier: workspace:^0.1.0 version: link:../../packages/certificates @@ -695,6 +698,34 @@ importers: specifier: ^5.7.3 version: 5.7.3 + packages/boards: + dependencies: + '@homarr/api': + specifier: workspace:^0.1.0 + version: link:../api + react: + specifier: 19.0.0 + version: 19.0.0 + react-dom: + specifier: 19.0.0 + version: 19.0.0(react@19.0.0) + devDependencies: + '@homarr/eslint-config': + specifier: workspace:^0.2.0 + version: link:../../tooling/eslint + '@homarr/prettier-config': + specifier: workspace:^0.1.0 + version: link:../../tooling/prettier + '@homarr/tsconfig': + specifier: workspace:^0.1.0 + version: link:../../tooling/typescript + eslint: + specifier: ^9.19.0 + version: 9.19.0 + typescript: + specifier: ^5.7.3 + version: 5.7.3 + packages/certificates: dependencies: '@homarr/common': @@ -1880,6 +1911,9 @@ importers: '@homarr/auth': specifier: workspace:^0.1.0 version: link:../auth + '@homarr/boards': + specifier: workspace:^0.1.0 + version: link:../boards '@homarr/common': specifier: workspace:^0.1.0 version: link:../common @@ -1904,6 +1938,9 @@ importers: '@homarr/redis': specifier: workspace:^0.1.0 version: link:../redis + '@homarr/server-settings': + specifier: workspace:^0.1.0 + version: link:../server-settings '@homarr/settings': specifier: workspace:^0.1.0 version: link:../settings diff --git a/turbo/generators/templates/package.json.hbs b/turbo/generators/templates/package.json.hbs index e288ab93c..3890eef64 100644 --- a/turbo/generators/templates/package.json.hbs +++ b/turbo/generators/templates/package.json.hbs @@ -24,7 +24,7 @@ "@homarr/eslint-config": "workspace:^0.2.0", "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "^9.18.0", + "eslint": "^9.19.0", "typescript": "^5.7.3" }, "prettier": "@homarr/prettier-config"