From ffe72598026e5e27588b18feeaaf7d30065d5536 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sat, 15 Feb 2025 10:08:06 +0100 Subject: [PATCH] feat(groups): add home board settings (#2321) --- apps/nextjs/package.json | 4 + .../_components/board-settings-form.tsx | 42 +- .../_components/_change-home-board.tsx | 17 +- .../manage/users/[userId]/general/page.tsx | 5 +- .../manage/users/groups/[id]/layout.tsx | 7 +- .../[id]/settings/_group-home-boards.tsx | 81 + .../users/groups/[id]/settings/page.tsx | 41 + .../manage/users/groups/_add-group.tsx | 24 - .../[locale]/manage/users/groups/_client.tsx | 65 + .../manage/users/groups/_groups-table.tsx | 277 +++ .../manage/users/groups/groups.module.css | 7 + .../app/[locale]/manage/users/groups/page.tsx | 88 +- .../src/components/board/board-select.tsx | 33 + e2e/shared/actions/onboarding-actions.ts | 1 + packages/api/src/router/board.ts | 87 +- packages/api/src/router/group.ts | 70 +- packages/api/src/router/test/board.spec.ts | 3 + packages/api/src/router/test/group.spec.ts | 24 + packages/api/src/router/user.ts | 3 + .../integration-query-permissions.spec.ts | 6 +- .../providers/test/ldap-authorization.spec.ts | 1 + packages/auth/test/callbacks.spec.ts | 3 + packages/auth/test/events.spec.ts | 1 + .../0025_add-group-home-board-settings.sql | 25 + .../migrations/mysql/meta/0025_snapshot.json | 1811 +++++++++++++++++ .../db/migrations/mysql/meta/_journal.json | 7 + packages/db/migrations/seed.ts | 1 + .../0025_add-group-home-board-settings.sql | 33 + .../migrations/sqlite/meta/0025_snapshot.json | 1736 ++++++++++++++++ .../db/migrations/sqlite/meta/_journal.json | 7 + packages/db/queries/group.ts | 11 + packages/db/queries/index.ts | 1 + packages/db/schema/mysql.ts | 24 + packages/db/schema/sqlite.ts | 23 + .../src/import/collections/user-collection.ts | 1 + packages/translation/src/lang/en.json | 46 +- packages/ui/src/components/index.tsx | 1 + .../components/select-with-custom-items.tsx | 13 +- packages/validation/src/group.ts | 17 + pnpm-lock.yaml | 35 +- 40 files changed, 4536 insertions(+), 146 deletions(-) create mode 100644 apps/nextjs/src/app/[locale]/manage/users/groups/[id]/settings/_group-home-boards.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/users/groups/[id]/settings/page.tsx delete mode 100644 apps/nextjs/src/app/[locale]/manage/users/groups/_add-group.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/users/groups/_client.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/users/groups/_groups-table.tsx create mode 100644 apps/nextjs/src/app/[locale]/manage/users/groups/groups.module.css create mode 100644 apps/nextjs/src/components/board/board-select.tsx create mode 100644 packages/db/migrations/mysql/0025_add-group-home-board-settings.sql create mode 100644 packages/db/migrations/mysql/meta/0025_snapshot.json create mode 100644 packages/db/migrations/sqlite/0025_add-group-home-board-settings.sql create mode 100644 packages/db/migrations/sqlite/meta/0025_snapshot.json create mode 100644 packages/db/queries/group.ts diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index eb325613a..7e0f5e0e9 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -15,6 +15,10 @@ }, "prettier": "@homarr/prettier-config", "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/modifiers": "^9.0.0", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@homarr/analytics": "workspace:^0.1.0", "@homarr/api": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0", 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 3a06cf8a3..615c6ba38 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,13 +1,12 @@ "use client"; -import { Group, Switch, Text } from "@mantine/core"; -import { IconLayoutDashboard } from "@tabler/icons-react"; +import { Switch, Text } from "@mantine/core"; import { clientApi } from "@homarr/api/client"; import type { ServerSettings } from "@homarr/server-settings"; import { useScopedI18n } from "@homarr/translation/client"; -import { SelectWithCustomItems } from "@homarr/ui"; +import { BoardSelect } from "~/components/board/board-select"; import { CommonSettingsForm } from "./common-form"; export const BoardSettingsForm = ({ defaultValues }: { defaultValues: ServerSettings["board"] }) => { @@ -18,42 +17,19 @@ export const BoardSettingsForm = ({ defaultValues }: { defaultValues: ServerSett {(form) => ( <> - ({ - value: board.id, - label: board.name, - image: board.logoImageUrl, - }))} - SelectOption={({ label, image }: { value: string; label: string; image: string | null }) => ( - - {/* eslint-disable-next-line @next/next/no-img-element */} - {image ? {label} : } - - {label} - - - )} + clearable + boards={selectableBoards} {...form.getInputProps("homeBoardId")} /> - ({ - value: board.id, - label: board.name, - image: board.logoImageUrl, - }))} - SelectOption={({ label, image }: { value: string; label: string; image: string | null }) => ( - - {/* eslint-disable-next-line @next/next/no-img-element */} - {image ? {label} : } - - {label} - - - )} + clearable + boards={selectableBoards} {...form.getInputProps("mobileHomeBoardId")} /> diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-home-board.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-home-board.tsx index 9dc3a6491..d544a3c8d 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-home-board.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-home-board.tsx @@ -1,6 +1,6 @@ "use client"; -import { Button, Group, Select, Stack } from "@mantine/core"; +import { Button, Group, Stack } from "@mantine/core"; import type { z } from "zod"; import type { RouterOutputs } from "@homarr/api"; @@ -11,9 +11,12 @@ import { showErrorNotification, showSuccessNotification } from "@homarr/notifica import { useI18n } from "@homarr/translation/client"; import { validation } from "@homarr/validation"; +import type { Board } from "~/app/[locale]/boards/_types"; +import { BoardSelect } from "~/components/board/board-select"; + interface ChangeHomeBoardFormProps { user: RouterOutputs["user"]["getById"]; - boardsData: { value: string; label: string }[]; + boardsData: Pick[]; } export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormProps) => { @@ -54,16 +57,18 @@ export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormPro return (
- diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx index f77e77233..48a2e8aa3 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx @@ -95,8 +95,9 @@ export default async function EditUserPage(props: Props) { ({ - value: board.id, - label: board.name, + id: board.id, + name: board.name, + logoImageUrl: board.logoImageUrl, }))} /> diff --git a/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/layout.tsx b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/layout.tsx index c10037abd..d78a53fe5 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/layout.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/layout.tsx @@ -1,7 +1,7 @@ import type { PropsWithChildren } from "react"; import Link from "next/link"; import { Button, Grid, GridCol, Group, Stack, Text, Title } from "@mantine/core"; -import { IconLock, IconSettings, IconUsersGroup } from "@tabler/icons-react"; +import { IconId, IconLock, IconSettings, IconUsersGroup } from "@tabler/icons-react"; import { api } from "@homarr/api/server"; import { getI18n, getScopedI18n } from "@homarr/translation/server"; @@ -42,6 +42,11 @@ export default async function Layout(props: PropsWithChildren) { } + /> + } /> { + const t = useI18n(); + const [availableBoards] = clientApi.board.getBoardsForGroup.useSuspenseQuery({ groupId }); + const form = useZodForm(validation.group.settings.pick({ homeBoardId: true, mobileHomeBoardId: true }), { + initialValues: { + homeBoardId, + mobileHomeBoardId, + }, + }); + const { mutateAsync, isPending } = clientApi.group.savePartialSettings.useMutation(); + + const handleSubmit = form.onSubmit(async (values) => { + await mutateAsync( + { + id: groupId, + settings: values, + }, + { + onSuccess() { + form.setInitialValues(values); + showSuccessNotification({ + title: t("group.action.settings.board.notification.success.title"), + message: t("group.action.settings.board.notification.success.message"), + }); + }, + onError() { + showErrorNotification({ + title: t("group.action.settings.board.notification.error.title"), + message: t("group.action.settings.board.notification.error.message"), + }); + }, + }, + ); + }); + + return ( + + + + + + + + + + + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/settings/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/settings/page.tsx new file mode 100644 index 000000000..a8b17717b --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/users/groups/[id]/settings/page.tsx @@ -0,0 +1,41 @@ +import { notFound } from "next/navigation"; +import { Alert, Stack, Title } from "@mantine/core"; +import { IconExclamationCircle } from "@tabler/icons-react"; + +import { api } from "@homarr/api/server"; +import { auth } from "@homarr/auth/next"; +import { getI18n } from "@homarr/translation/server"; + +import { GroupHomeBoards } from "./_group-home-boards"; + +interface GroupSettingsPageProps { + params: Promise<{ + id: string; + }>; +} + +export default async function GroupPermissionsPage(props: GroupSettingsPageProps) { + const params = await props.params; + const session = await auth(); + + if (!session?.user.permissions.includes("admin")) { + notFound(); + } + + const group = await api.group.getById({ id: params.id }); + const t = await getI18n(); + + return ( + + {t("management.page.group.setting.setting.title")} + + }> + {t("management.page.group.setting.setting.alert")} + + + {t("management.page.group.setting.setting.board.title")} + + + + ); +} diff --git a/apps/nextjs/src/app/[locale]/manage/users/groups/_add-group.tsx b/apps/nextjs/src/app/[locale]/manage/users/groups/_add-group.tsx deleted file mode 100644 index f3febd49c..000000000 --- a/apps/nextjs/src/app/[locale]/manage/users/groups/_add-group.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -import { useCallback } from "react"; - -import { useModalAction } from "@homarr/modals"; -import { AddGroupModal } from "@homarr/modals-collection"; -import { useI18n } from "@homarr/translation/client"; - -import { MobileAffixButton } from "~/components/manage/mobile-affix-button"; - -export const AddGroup = () => { - const t = useI18n(); - const { openModal } = useModalAction(AddGroupModal); - - const handleAddGroup = useCallback(() => { - openModal(); - }, [openModal]); - - return ( - - {t("group.action.create.label")} - - ); -}; diff --git a/apps/nextjs/src/app/[locale]/manage/users/groups/_client.tsx b/apps/nextjs/src/app/[locale]/manage/users/groups/_client.tsx new file mode 100644 index 000000000..0c46cf7fc --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/users/groups/_client.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { useCallback, useMemo, useState } from "react"; +import { Group, TextInput } from "@mantine/core"; +import { IconSearch } from "@tabler/icons-react"; + +import type { RouterOutputs } from "@homarr/api"; +import { useModalAction } from "@homarr/modals"; +import { AddGroupModal } from "@homarr/modals-collection"; +import { useI18n } from "@homarr/translation/client"; + +import { MobileAffixButton } from "~/components/manage/mobile-affix-button"; +import { GroupsTable } from "./_groups-table"; + +interface GroupsListProps { + groups: RouterOutputs["group"]["getAll"]; +} + +export const GroupsList = ({ groups }: GroupsListProps) => { + const [search, setSearch] = useState(""); + const initialGroupIds = useMemo( + () => groups.sort((groupA, groupB) => groupA.position - groupB.position).map((group) => group.id), + [groups], + ); + const filteredGroups = useMemo( + () => + groups + .filter((group) => group.name.toLowerCase().includes(search.toLowerCase())) + .sort((groupA, groupB) => groupA.position - groupB.position), + [groups, search], + ); + const t = useI18n(); + + return ( + <> + + } + value={search} + onChange={(event) => setSearch(event.currentTarget.value)} + placeholder={`${t("group.search")}...`} + style={{ flex: 1 }} + /> + + + + + + ); +}; + +const AddGroup = () => { + const t = useI18n(); + const { openModal } = useModalAction(AddGroupModal); + + const handleAddGroup = useCallback(() => { + openModal(); + }, [openModal]); + + return ( + + {t("group.action.create.label")} + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/manage/users/groups/_groups-table.tsx b/apps/nextjs/src/app/[locale]/manage/users/groups/_groups-table.tsx new file mode 100644 index 000000000..d81b902d3 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/users/groups/_groups-table.tsx @@ -0,0 +1,277 @@ +"use client"; + +import type { ReactNode } from "react"; +import { useMemo, useState } from "react"; +import Link from "next/link"; +import type { DragEndEvent, DraggableAttributes, DragStartEvent } from "@dnd-kit/core"; +import { + closestCenter, + DndContext, + DragOverlay, + KeyboardSensor, + MouseSensor, + TouchSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"; +import { restrictToVerticalAxis } from "@dnd-kit/modifiers"; +import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { + Anchor, + Box, + Button, + Card, + Flex, + Group, + Table, + TableTbody, + TableTd, + TableTh, + TableThead, + TableTr, + Text, + Transition, +} from "@mantine/core"; +import { IconGripVertical } from "@tabler/icons-react"; + +import type { RouterOutputs } from "@homarr/api"; +import { clientApi } from "@homarr/api/client"; +import { revalidatePathActionAsync } from "@homarr/common/client"; +import { showSuccessNotification } from "@homarr/notifications"; +import { useI18n } from "@homarr/translation/client"; +import { UserAvatarGroup } from "@homarr/ui"; + +interface GroupsTableProps { + initialGroupIds: string[]; + groups: RouterOutputs["group"]["getAll"]; + hasFilter: boolean; +} + +export const GroupsTable = ({ groups, initialGroupIds, hasFilter }: GroupsTableProps) => { + const t = useI18n(); + const [activeId, setActiveId] = useState(null); + const [groupIds, setGroupIds] = useState(groups.map((group) => group.id)); + const isDirty = useMemo( + () => initialGroupIds.some((groupId, index) => groupIds.indexOf(groupId) !== index), + [groupIds, initialGroupIds], + ); + const { mutateAsync, isPending } = clientApi.group.savePositions.useMutation(); + const handleSavePositionsAsync = async () => { + await mutateAsync( + { positions: groupIds }, + { + async onSuccess() { + showSuccessNotification({ + message: t("group.action.changePosition.notification.success.message"), + }); + await revalidatePathActionAsync("/manage/users/groups"); + }, + onError() { + showSuccessNotification({ + message: t("group.action.changePosition.notification.error.message"), + }); + }, + }, + ); + }; + + const sensors = useSensors(useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {})); + + const handleDragStart = (event: DragStartEvent) => { + setActiveId(event.active.id as string); + }; + + function handleDragEnd(event: DragEndEvent) { + const { active, over } = event; + if (!over || active.id === over.id) { + setActiveId(null); + return; + } + + setGroupIds((groupIds) => { + const oldIndex = groupIds.indexOf(active.id as string); + const newIndex = groupIds.indexOf(over.id as string); + return arrayMove(groupIds, oldIndex, newIndex); + }); + } + + function handleDragCancel() { + setActiveId(null); + } + + const selectedRow = useMemo(() => { + if (!activeId) return null; + + const current = groups.find((group) => group.id === activeId); + if (!current) return null; + + return } />; + }, [activeId, groups]); + + return ( + <> + + + + + {t("group.field.name")} + {t("group.field.members")} + + + + + {groupIds.map((groupId) => { + const group = groups.find(({ id }) => id === groupId); + if (!group) return null; + + return ; + })} + + +
+ + + {activeId && ( + + {selectedRow} +
+ )} +
+
+ setGroupIds(initialGroupIds)} + isPending={isPending} + onSave={handleSavePositionsAsync} + /> + + ); +}; + +interface DraggableRowProps { + group: RouterOutputs["group"]["getAll"][number]; + disabled?: boolean; +} + +const DraggableRow = ({ group, disabled }: DraggableRowProps) => { + const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({ + id: group.id, + }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + if (isDragging) { + return ( + +   + + ); + } + + return ( + } + /> + ); +}; + +interface RowProps { + group: RouterOutputs["group"]["getAll"][number]; + handle?: ReactNode; + setNodeRef?: (node: HTMLElement | null) => void; + style?: React.CSSProperties; +} + +const Row = ({ group, handle, setNodeRef, style }: RowProps) => { + return ( + + + + {handle} + + {group.name} + + + + + + + + ); +}; + +interface DragHandleProps { + attributes: DraggableAttributes | undefined; + listeners: SyntheticListenerMap | undefined; + active: boolean; + disabled?: boolean; +} + +const DragHandle = ({ attributes, listeners, active, disabled }: DragHandleProps) => { + if (disabled) { + return ; + } + + return ( + + + + ); +}; + +interface SaveAffixProps { + visible: boolean; + isPending: boolean; + onDiscard: () => void; + onSave: () => void; +} + +const SaveAffix = ({ visible, isPending, onDiscard, onSave }: SaveAffixProps) => { + const t = useI18n(); + + return ( +
+ + {(transitionStyles) => ( + + + {t("common.unsavedChanges")} + + + + + + + )} + +
+ ); +}; diff --git a/apps/nextjs/src/app/[locale]/manage/users/groups/groups.module.css b/apps/nextjs/src/app/[locale]/manage/users/groups/groups.module.css new file mode 100644 index 000000000..0e05fd3e2 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/users/groups/groups.module.css @@ -0,0 +1,7 @@ +.everyoneGroup { + background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6)); +} + +.everyoneGroup:hover { + background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); +} diff --git a/apps/nextjs/src/app/[locale]/manage/users/groups/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/groups/page.tsx index 7fbbafc54..abd348739 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/groups/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/groups/page.tsx @@ -1,30 +1,19 @@ import Link from "next/link"; import { notFound } from "next/navigation"; -import { Anchor, Group, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from "@mantine/core"; -import { z } from "zod"; +import { Card, Group, Stack, Text, ThemeIcon, Title, UnstyledButton } from "@mantine/core"; +import { IconChevronRight, IconUsersGroup } from "@tabler/icons-react"; -import type { RouterOutputs } from "@homarr/api"; import { api } from "@homarr/api/server"; import { auth } from "@homarr/auth/next"; -import type { inferSearchParamsFromSchema } from "@homarr/common/types"; +import { everyoneGroup } from "@homarr/definitions"; import { getI18n } from "@homarr/translation/server"; -import { SearchInput, TablePagination, UserAvatarGroup } from "@homarr/ui"; import { ManageContainer } from "~/components/manage/manage-container"; import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; -import { AddGroup } from "./_add-group"; +import { GroupsList } from "./_client"; +import classes from "./groups.module.css"; -const searchParamsSchema = z.object({ - search: z.string().optional(), - pageSize: z.string().regex(/\d+/).transform(Number).catch(10), - page: z.string().regex(/\d+/).transform(Number).catch(1), -}); - -interface GroupsListPageProps { - searchParams: Promise>; -} - -export default async function GroupsListPage(props: GroupsListPageProps) { +export default async function GroupsListPage() { const session = await auth(); if (!session?.user.permissions.includes("admin")) { @@ -32,55 +21,38 @@ export default async function GroupsListPage(props: GroupsListPageProps) { } const t = await getI18n(); - const searchParams = searchParamsSchema.parse(await props.searchParams); - const { items: groups, totalCount } = await api.group.getPaginated(searchParams); + const groups = await api.group.getAll(); + const dbEveryoneGroup = groups.find((group) => group.name === everyoneGroup); + const groupsWithoutEveryone = groups.filter((group) => group.name !== everyoneGroup); return ( {t("group.title")} - - - - - - - - {t("group.field.name")} - {t("group.field.members")} - - - - {groups.map((group) => ( - - ))} - -
- - - + {dbEveryoneGroup && ( + + + + + + + + + {t("group.defaultGroup.name")} + + {t("group.defaultGroup.description", { name: everyoneGroup })} + + + + + + + )} + +
); } - -interface RowProps { - group: RouterOutputs["group"]["getPaginated"]["items"][number]; -} - -const Row = ({ group }: RowProps) => { - return ( - - - - {group.name} - - - - - - - ); -}; diff --git a/apps/nextjs/src/components/board/board-select.tsx b/apps/nextjs/src/components/board/board-select.tsx new file mode 100644 index 000000000..3785dbd0e --- /dev/null +++ b/apps/nextjs/src/components/board/board-select.tsx @@ -0,0 +1,33 @@ +import { Group, Text } from "@mantine/core"; +import { IconLayoutDashboard } from "@tabler/icons-react"; + +import type { SelectWithCustomItemsProps } from "@homarr/ui"; +import { SelectWithCustomItems } from "@homarr/ui"; + +import type { Board } from "~/app/[locale]/boards/_types"; + +interface BoardSelectProps extends Omit, "data"> { + boards: Pick[]; +} + +export const BoardSelect = ({ boards, ...props }: BoardSelectProps) => { + return ( + ({ + value: board.id, + label: board.name, + image: board.logoImageUrl, + }))} + SelectOption={({ label, image }: { value: string; label: string; image: string | null }) => ( + + {/* eslint-disable-next-line @next/next/no-img-element */} + {image ? {label} : } + + {label} + + + )} + /> + ); +}; diff --git a/e2e/shared/actions/onboarding-actions.ts b/e2e/shared/actions/onboarding-actions.ts index 8362a46a5..182203fcf 100644 --- a/e2e/shared/actions/onboarding-actions.ts +++ b/e2e/shared/actions/onboarding-actions.ts @@ -22,6 +22,7 @@ export class OnboardingActions { await this.db.insert(sqliteSchema.groups).values({ id: createId(), name: input.group, + position: 1, }); } } diff --git a/packages/api/src/router/board.ts b/packages/api/src/router/board.ts index b4f9b598a..788f7953a 100644 --- a/packages/api/src/router/board.ts +++ b/packages/api/src/router/board.ts @@ -5,7 +5,7 @@ import { z } from "zod"; import { constructBoardPermissions } from "@homarr/auth/shared"; import type { DeviceType } from "@homarr/common/server"; import type { Database, InferInsertModel, InferSelectModel, SQL } from "@homarr/db"; -import { and, createId, eq, handleTransactionsAsync, inArray, like, or } from "@homarr/db"; +import { and, asc, createId, eq, handleTransactionsAsync, inArray, isNull, like, not, or } from "@homarr/db"; import { getServerSettingByKeyAsync } from "@homarr/db/queries"; import { boardGroupPermissions, @@ -13,6 +13,7 @@ import { boardUserPermissions, groupMembers, groupPermissions, + groups, integrationGroupPermissions, integrationItems, integrationUserPermissions, @@ -22,7 +23,7 @@ import { users, } from "@homarr/db/schema"; import type { WidgetKind } from "@homarr/definitions"; -import { getPermissionsWithParents, widgetKinds } from "@homarr/definitions"; +import { everyoneGroup, getPermissionsWithChildren, getPermissionsWithParents, widgetKinds } from "@homarr/definitions"; import { importOldmarrAsync } from "@homarr/old-import"; import { importJsonFileSchema } from "@homarr/old-import/shared"; import { oldmarrConfigSchema } from "@homarr/old-schema"; @@ -57,6 +58,37 @@ export const boardRouter = createTRPCRouter({ where: eq(boards.isPublic, true), }); }), + getBoardsForGroup: permissionRequiredProcedure + .requiresPermission("admin") + .input(z.object({ groupId: z.string() })) + .query(async ({ ctx, input }) => { + const dbEveryoneAndCurrentGroup = await ctx.db.query.groups.findMany({ + where: or(eq(groups.name, everyoneGroup), eq(groups.id, input.groupId)), + with: { + boardPermissions: true, + permissions: true, + }, + }); + + const distinctPermissions = new Set( + dbEveryoneAndCurrentGroup.flatMap((group) => group.permissions.map(({ permission }) => permission)), + ); + const canViewAllBoards = getPermissionsWithChildren([...distinctPermissions]).includes("board-view-all"); + + const boardIds = dbEveryoneAndCurrentGroup.flatMap((group) => + group.boardPermissions.map(({ boardId }) => boardId), + ); + const boardWhere = canViewAllBoards ? undefined : or(eq(boards.isPublic, true), inArray(boards.id, boardIds)); + + return await ctx.db.query.boards.findMany({ + columns: { + id: true, + name: true, + logoImageUrl: true, + }, + where: boardWhere, + }); + }), getAllBoards: publicProcedure.query(async ({ ctx }) => { const userId = ctx.session?.user.id; const permissionsOfCurrentUserWhenPresent = await ctx.db.query.boardUserPermissions.findMany({ @@ -89,6 +121,7 @@ export const boardRouter = createTRPCRouter({ columns: { id: true, name: true, + logoImageUrl: true, isPublic: true, }, with: { @@ -975,9 +1008,13 @@ export const boardRouter = createTRPCRouter({ * For an example of a user with deviceType = 'mobile' it would go through the following order: * 1. user.mobileHomeBoardId * 2. user.homeBoardId - * 3. serverSettings.mobileHomeBoardId - * 4. serverSettings.homeBoardId - * 5. show NOT_FOUND error + * 3. group.mobileHomeBoardId of the lowest positions group + * 4. group.homeBoardId of the lowest positions group + * 5. everyoneGroup.mobileHomeBoardId + * 6. everyoneGroup.homeBoardId + * 7. serverSettings.mobileHomeBoardId + * 8. serverSettings.homeBoardId + * 9. show NOT_FOUND error */ const getHomeIdBoardAsync = async ( db: Database, @@ -985,12 +1022,46 @@ const getHomeIdBoardAsync = async ( deviceType: DeviceType, ) => { const settingKey = deviceType === "mobile" ? "mobileHomeBoardId" : "homeBoardId"; - if (user?.[settingKey] || user?.homeBoardId) { - return user[settingKey] ?? user.homeBoardId; - } else { + + if (!user) { const boardSettings = await getServerSettingByKeyAsync(db, "board"); return boardSettings[settingKey] ?? boardSettings.homeBoardId; } + + if (user[settingKey]) return user[settingKey]; + if (user.homeBoardId) return user.homeBoardId; + + const lowestGroupExceptEveryone = await db + .select({ + homeBoardId: groups.homeBoardId, + mobileHomeBoardId: groups.mobileHomeBoardId, + }) + .from(groups) + .leftJoin(groupMembers, eq(groups.id, groupMembers.groupId)) + .where( + and( + eq(groupMembers.userId, user.id), + not(eq(groups.name, everyoneGroup)), + not(isNull(groups[settingKey])), + not(isNull(groups.homeBoardId)), + ), + ) + .orderBy(asc(groups.position)) + .limit(1) + .then((result) => result[0]); + + if (lowestGroupExceptEveryone?.[settingKey]) return lowestGroupExceptEveryone[settingKey]; + if (lowestGroupExceptEveryone?.homeBoardId) return lowestGroupExceptEveryone.homeBoardId; + + const dbEveryoneGroup = await db.query.groups.findFirst({ + where: eq(groups.name, everyoneGroup), + }); + + if (dbEveryoneGroup?.[settingKey]) return dbEveryoneGroup[settingKey]; + if (dbEveryoneGroup?.homeBoardId) return dbEveryoneGroup.homeBoardId; + + const boardSettings = await getServerSettingByKeyAsync(db, "board"); + return boardSettings[settingKey] ?? boardSettings.homeBoardId; }; const noBoardWithSimilarNameAsync = async (db: Database, name: string, ignoredIds: string[] = []) => { diff --git a/packages/api/src/router/group.ts b/packages/api/src/router/group.ts index 86d289fd9..37dde6e16 100644 --- a/packages/api/src/router/group.ts +++ b/packages/api/src/router/group.ts @@ -2,7 +2,8 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; import type { Database } from "@homarr/db"; -import { and, createId, eq, like, not, sql } from "@homarr/db"; +import { and, createId, eq, handleTransactionsAsync, like, not, sql } from "@homarr/db"; +import { getMaxGroupPositionAsync } from "@homarr/db/queries"; import { groupMembers, groupPermissions, groups } from "@homarr/db/schema"; import { everyoneGroup } from "@homarr/definitions"; import { validation } from "@homarr/validation"; @@ -12,6 +13,30 @@ import { throwIfCredentialsDisabled } from "./invite/checks"; import { nextOnboardingStepAsync } from "./onboard/onboard-queries"; export const groupRouter = createTRPCRouter({ + getAll: permissionRequiredProcedure.requiresPermission("admin").query(async ({ ctx }) => { + const dbGroups = await ctx.db.query.groups.findMany({ + with: { + members: { + with: { + user: { + columns: { + id: true, + name: true, + email: true, + image: true, + }, + }, + }, + }, + }, + }); + + return dbGroups.map((group) => ({ + ...group, + members: group.members.map((member) => member.user), + })); + }), + getPaginated: permissionRequiredProcedure .requiresPermission("admin") .input(validation.common.paginated) @@ -153,10 +178,13 @@ export const groupRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { await checkSimilarNameAndThrowAsync(ctx.db, input.name); + const maxPosition = await getMaxGroupPositionAsync(ctx.db); + const groupId = createId(); await ctx.db.insert(groups).values({ id: groupId, name: input.name, + position: maxPosition + 1, }); await ctx.db.insert(groupPermissions).values({ @@ -172,10 +200,13 @@ export const groupRouter = createTRPCRouter({ .mutation(async ({ input, ctx }) => { await checkSimilarNameAndThrowAsync(ctx.db, input.name); + const maxPosition = await getMaxGroupPositionAsync(ctx.db); + const id = createId(); await ctx.db.insert(groups).values({ id, name: input.name, + position: maxPosition + 1, ownerId: ctx.session.user.id, }); @@ -197,6 +228,43 @@ export const groupRouter = createTRPCRouter({ }) .where(eq(groups.id, input.id)); }), + savePartialSettings: permissionRequiredProcedure + .requiresPermission("admin") + .input(validation.group.savePartialSettings) + .mutation(async ({ input, ctx }) => { + await throwIfGroupNotFoundAsync(ctx.db, input.id); + + await ctx.db + .update(groups) + .set({ + homeBoardId: input.settings.homeBoardId, + mobileHomeBoardId: input.settings.mobileHomeBoardId, + }) + .where(eq(groups.id, input.id)); + }), + savePositions: permissionRequiredProcedure + .requiresPermission("admin") + .input(validation.group.savePositions) + .mutation(async ({ input, ctx }) => { + const positions = input.positions.map((id, index) => ({ id, position: index + 1 })); + + await handleTransactionsAsync(ctx.db, { + handleAsync: async (db, schema) => { + await db.transaction(async (trx) => { + for (const { id, position } of positions) { + await trx.update(schema.groups).set({ position }).where(eq(groups.id, id)); + } + }); + }, + handleSync: (db) => { + db.transaction((trx) => { + for (const { id, position } of positions) { + trx.update(groups).set({ position }).where(eq(groups.id, id)).run(); + } + }); + }, + }); + }), savePermissions: permissionRequiredProcedure .requiresPermission("admin") .input(validation.group.savePermissions) diff --git a/packages/api/src/router/test/board.spec.ts b/packages/api/src/router/test/board.spec.ts index 3ea52d6ac..5c80ad2c0 100644 --- a/packages/api/src/router/test/board.spec.ts +++ b/packages/api/src/router/test/board.spec.ts @@ -205,6 +205,7 @@ describe("getAllBoards should return all boards accessable to the current user", await db.insert(groups).values({ id: groupId, name: "group1", + position: 1, }); await db.insert(groupMembers).values({ @@ -1166,6 +1167,7 @@ describe("getBoardPermissions should return board permissions", () => { await db.insert(groups).values({ id: groupId, name: "group1", + position: 1, }); await db.insert(boardGroupPermissions).values({ @@ -1260,6 +1262,7 @@ describe("saveGroupBoardPermissions should save group board permissions", () => await db.insert(groups).values({ id: groupId, name: "group1", + position: 1, }); const boardId = createId(); diff --git a/packages/api/src/router/test/group.spec.ts b/packages/api/src/router/test/group.spec.ts index f4d879005..6b880b27a 100644 --- a/packages/api/src/router/test/group.spec.ts +++ b/packages/api/src/router/test/group.spec.ts @@ -43,6 +43,7 @@ describe("paginated should return a list of groups with pagination", () => { [1, 2, 3, 4, 5].map((number) => ({ id: number.toString(), name: `Group ${number}`, + position: number, })), ); @@ -66,6 +67,7 @@ describe("paginated should return a list of groups with pagination", () => { [1, 2, 3, 4, 5].map((number) => ({ id: number.toString(), name: `Group ${number}`, + position: number, })), ); @@ -89,6 +91,7 @@ describe("paginated should return a list of groups with pagination", () => { await db.insert(groups).values({ id: groupId, name: "Group", + position: 1, }); await db.insert(groupMembers).values({ groupId, @@ -123,6 +126,7 @@ describe("paginated should return a list of groups with pagination", () => { ["first", "second", "third", "forth", "fifth"].map((key, index) => ({ id: index.toString(), name: key, + position: index + 1, })), ); @@ -163,10 +167,12 @@ describe("byId should return group by id including members and permissions", () { id: groupId, name: "Group", + position: 1, }, { id: createId(), name: "Another group", + position: 2, }, ]); await db.insert(groupMembers).values({ @@ -202,6 +208,7 @@ describe("byId should return group by id including members and permissions", () await db.insert(groups).values({ id: "2", name: "Group", + position: 1, }); // Act @@ -278,6 +285,7 @@ describe("create should create group in database", () => { await db.insert(groups).values({ id: createId(), name: similarName, + position: 1, }); // Act @@ -314,10 +322,12 @@ describe("update should update name with value that is no duplicate", () => { { id: groupId, name: initialValue, + position: 1, }, { id: createId(), name: "Third", + position: 2, }, ]); @@ -347,10 +357,12 @@ describe("update should update name with value that is no duplicate", () => { { id: groupId, name: "Something", + position: 1, }, { id: createId(), name: initialDuplicate, + position: 2, }, ]); @@ -373,6 +385,7 @@ describe("update should update name with value that is no duplicate", () => { await db.insert(groups).values({ id: createId(), name: "something", + position: 1, }); // Act @@ -413,6 +426,7 @@ describe("savePermissions should save permissions for group", () => { await db.insert(groups).values({ id: groupId, name: "Group", + position: 1, }); await db.insert(groupPermissions).values({ groupId, @@ -442,6 +456,7 @@ describe("savePermissions should save permissions for group", () => { await db.insert(groups).values({ id: createId(), name: "Group", + position: 1, }); // Act @@ -494,6 +509,7 @@ describe("transferOwnership should transfer ownership of group", () => { id: groupId, name: "Group", ownerId: defaultOwnerId, + position: 1, }); // Act @@ -518,6 +534,7 @@ describe("transferOwnership should transfer ownership of group", () => { await db.insert(groups).values({ id: createId(), name: "Group", + position: 1, }); // Act @@ -559,10 +576,12 @@ describe("deleteGroup should delete group", () => { { id: groupId, name: "Group", + position: 1, }, { id: createId(), name: "Another group", + position: 2, }, ]); @@ -586,6 +605,7 @@ describe("deleteGroup should delete group", () => { await db.insert(groups).values({ id: createId(), name: "Group", + position: 1, }); // Act @@ -638,6 +658,7 @@ describe("addMember should add member to group", () => { id: groupId, name: "Group", ownerId: defaultOwnerId, + position: 1, }); // Act @@ -715,6 +736,7 @@ describe("addMember should add member to group", () => { id: groupId, name: "Group", ownerId: defaultOwnerId, + position: 1, }); // Act @@ -753,6 +775,7 @@ describe("removeMember should remove member from group", () => { id: groupId, name: "Group", ownerId: defaultOwnerId, + position: 1, }); await db.insert(groupMembers).values({ groupId, @@ -833,6 +856,7 @@ describe("removeMember should remove member from group", () => { id: groupId, name: "Group", ownerId: defaultOwnerId, + position: 1, }); await db.insert(groupMembers).values({ groupId, diff --git a/packages/api/src/router/user.ts b/packages/api/src/router/user.ts index 1ce4e1690..9aefff564 100644 --- a/packages/api/src/router/user.ts +++ b/packages/api/src/router/user.ts @@ -4,6 +4,7 @@ import { z } from "zod"; import { createSaltAsync, hashPasswordAsync } from "@homarr/auth"; import type { Database } from "@homarr/db"; import { and, createId, eq, like } from "@homarr/db"; +import { getMaxGroupPositionAsync } from "@homarr/db/queries"; import { boards, groupMembers, groupPermissions, groups, invites, users } from "@homarr/db/schema"; import { selectUserSchema } from "@homarr/db/validationSchemas"; import { credentialsAdminGroup } from "@homarr/definitions"; @@ -31,12 +32,14 @@ export const userRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { throwIfCredentialsDisabled(); + const maxPosition = await getMaxGroupPositionAsync(ctx.db); const userId = await createUserAsync(ctx.db, input); const groupId = createId(); await ctx.db.insert(groups).values({ id: groupId, name: credentialsAdminGroup, ownerId: userId, + position: maxPosition + 1, }); await ctx.db.insert(groupPermissions).values({ groupId, diff --git a/packages/auth/permissions/test/integration-query-permissions.spec.ts b/packages/auth/permissions/test/integration-query-permissions.spec.ts index a1d9f52ed..fd2c1dca3 100644 --- a/packages/auth/permissions/test/integration-query-permissions.spec.ts +++ b/packages/auth/permissions/test/integration-query-permissions.spec.ts @@ -272,7 +272,7 @@ describe("hasQueryAccessToIntegrationsAsync should check if the user has query a }, ]; await db.insert(boards).values(createMockBoard({ id: "1" })); - await db.insert(groups).values({ id: "1", name: "" }); + await db.insert(groups).values({ id: "1", name: "", position: 1 }); await db.insert(groupMembers).values({ userId: session.user.id, groupId: "1" }); await db.insert(boardGroupPermissions).values({ groupId: "1", boardId: "1", permission: "view" }); @@ -325,7 +325,7 @@ describe("hasQueryAccessToIntegrationsAsync should check if the user has query a }, ]; await db.insert(boards).values(createMockBoard({ id: "1" })); - await db.insert(groups).values({ id: "1", name: "" }); + await db.insert(groups).values({ id: "1", name: "", position: 1 }); await db.insert(groupMembers).values({ userId: session.user.id, groupId: "1" }); await db.insert(boardGroupPermissions).values({ groupId: "1", boardId: "1", permission: "view" }); @@ -379,7 +379,7 @@ describe("hasQueryAccessToIntegrationsAsync should check if the user has query a ]; await db.insert(boards).values(createMockBoard({ id: "1" })); await db.insert(boards).values(createMockBoard({ id: "2" })); - await db.insert(groups).values({ id: "1", name: "" }); + await db.insert(groups).values({ id: "1", name: "", position: 1 }); await db.insert(groupMembers).values({ userId: session.user.id, groupId: "1" }); await db.insert(boardGroupPermissions).values({ groupId: "1", boardId: "2", permission: "view" }); await db.insert(boardUserPermissions).values({ userId: session.user.id, boardId: "1", permission: "view" }); diff --git a/packages/auth/providers/test/ldap-authorization.spec.ts b/packages/auth/providers/test/ldap-authorization.spec.ts index 50f35399e..2ea43034a 100644 --- a/packages/auth/providers/test/ldap-authorization.spec.ts +++ b/packages/auth/providers/test/ldap-authorization.spec.ts @@ -301,6 +301,7 @@ describe("authorizeWithLdapCredentials", () => { await db.insert(groups).values({ id: groupId, name: "homarr_example", + position: 1, }); // Act diff --git a/packages/auth/test/callbacks.spec.ts b/packages/auth/test/callbacks.spec.ts index 6a1f52e2b..6709d1bb0 100644 --- a/packages/auth/test/callbacks.spec.ts +++ b/packages/auth/test/callbacks.spec.ts @@ -25,6 +25,7 @@ describe("getCurrentUserPermissions", () => { await db.insert(groups).values({ id: "2", name: "test", + position: 1, }); await db.insert(groupPermissions).values({ groupId: "2", @@ -51,6 +52,7 @@ describe("getCurrentUserPermissions", () => { await db.insert(groups).values({ id: "2", name: "test", + position: 1, }); await db.insert(groupPermissions).values({ groupId: "2", @@ -81,6 +83,7 @@ describe("getCurrentUserPermissions", () => { await db.insert(groups).values({ id: mockId, name: "test", + position: 1, }); await db.insert(groupMembers).values({ userId: mockId, diff --git a/packages/auth/test/events.spec.ts b/packages/auth/test/events.spec.ts index a2783bad7..0fe01ddf1 100644 --- a/packages/auth/test/events.spec.ts +++ b/packages/auth/test/events.spec.ts @@ -259,4 +259,5 @@ const createGroupAsync = async (db: Database, name = "test") => await db.insert(groups).values({ id: "1", name, + position: 1, }); diff --git a/packages/db/migrations/mysql/0025_add-group-home-board-settings.sql b/packages/db/migrations/mysql/0025_add-group-home-board-settings.sql new file mode 100644 index 000000000..979e23bea --- /dev/null +++ b/packages/db/migrations/mysql/0025_add-group-home-board-settings.sql @@ -0,0 +1,25 @@ +ALTER TABLE `group` ADD `home_board_id` varchar(64); +--> statement-breakpoint +ALTER TABLE `group` ADD `mobile_home_board_id` varchar(64); +--> statement-breakpoint +ALTER TABLE `group` ADD `position` smallint; +--> statement-breakpoint +CREATE TABLE `temp_group` ( + `id` varchar(64) NOT NULL, + `name` varchar(255) NOT NULL, + `position` smallint NOT NULL +); +--> statement-breakpoint +INSERT INTO `temp_group`(`id`, `name`, `position`) SELECT `id`, `name`, ROW_NUMBER() OVER(ORDER BY `name`) FROM `group` WHERE `name` != 'everyone'; +--> statement-breakpoint +UPDATE `group` SET `position`=(SELECT `position` FROM `temp_group` WHERE `temp_group`.`id`=`group`.`id`); +--> statement-breakpoint +DROP TABLE `temp_group`; +--> statement-breakpoint +UPDATE `group` SET `position` = -1 WHERE `name` = 'everyone'; +--> statement-breakpoint +ALTER TABLE `group` MODIFY `position` smallint NOT NULL; +--> statement-breakpoint +ALTER TABLE `group` ADD CONSTRAINT `group_home_board_id_board_id_fk` FOREIGN KEY (`home_board_id`) REFERENCES `board`(`id`) ON DELETE set null ON UPDATE no action; +--> statement-breakpoint +ALTER TABLE `group` ADD CONSTRAINT `group_mobile_home_board_id_board_id_fk` FOREIGN KEY (`mobile_home_board_id`) REFERENCES `board`(`id`) ON DELETE set null ON UPDATE no action; \ No newline at end of file diff --git a/packages/db/migrations/mysql/meta/0025_snapshot.json b/packages/db/migrations/mysql/meta/0025_snapshot.json new file mode 100644 index 000000000..3451bad5f --- /dev/null +++ b/packages/db/migrations/mysql/meta/0025_snapshot.json @@ -0,0 +1,1811 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "9bdd2fb3-f81f-45cb-aac4-9f1bbddbf2f4", + "prevId": "f670b3a4-69ef-4ef8-9f1b-26a92dda2858", + "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 + }, + "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 + }, + "position": { + "name": "position", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "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" + }, + "group_home_board_id_board_id_fk": { + "name": "group_home_board_id_board_id_fk", + "tableFrom": "group", + "tableTo": "board", + "columnsFrom": ["home_board_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "group_mobile_home_board_id_board_id_fk": { + "name": "group_mobile_home_board_id_board_id_fk", + "tableFrom": "group", + "tableTo": "board", + "columnsFrom": ["mobile_home_board_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 036387804..2124ea88d 100644 --- a/packages/db/migrations/mysql/meta/_journal.json +++ b/packages/db/migrations/mysql/meta/_journal.json @@ -176,6 +176,13 @@ "when": 1738961147412, "tag": "0024_mean_vin_gonzales", "breakpoints": true + }, + { + "idx": 25, + "version": "5", + "when": 1739469710187, + "tag": "0025_add-group-home-board-settings", + "breakpoints": true } ] } diff --git a/packages/db/migrations/seed.ts b/packages/db/migrations/seed.ts index 105147e20..fa86e77a3 100644 --- a/packages/db/migrations/seed.ts +++ b/packages/db/migrations/seed.ts @@ -28,6 +28,7 @@ const seedEveryoneGroupAsync = async (db: Database) => { await db.insert(groups).values({ id: createId(), name: everyoneGroup, + position: -1, }); console.log("Created group 'everyone' through seed"); }; diff --git a/packages/db/migrations/sqlite/0025_add-group-home-board-settings.sql b/packages/db/migrations/sqlite/0025_add-group-home-board-settings.sql new file mode 100644 index 000000000..f27f2e8f2 --- /dev/null +++ b/packages/db/migrations/sqlite/0025_add-group-home-board-settings.sql @@ -0,0 +1,33 @@ +COMMIT TRANSACTION; +--> statement-breakpoint +PRAGMA foreign_keys = OFF; +--> statement-breakpoint +BEGIN TRANSACTION; +--> statement-breakpoint +CREATE TABLE `__new_group` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `owner_id` text, + `home_board_id` text, + `mobile_home_board_id` text, + `position` integer NOT NULL, + FOREIGN KEY (`owner_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`home_board_id`) REFERENCES `board`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`mobile_home_board_id`) REFERENCES `board`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +INSERT INTO `__new_group`("id", "name", "owner_id", "position") SELECT "id", "name", "owner_id", -1 FROM `group` WHERE "name" = 'everyone'; +--> statement-breakpoint +INSERT INTO `__new_group`("id", "name", "owner_id", "position") SELECT "id", "name", "owner_id", ROW_NUMBER() OVER(ORDER BY "name") FROM `group` WHERE "name" != 'everyone'; +--> statement-breakpoint +DROP TABLE `group`; +--> statement-breakpoint +ALTER TABLE `__new_group` RENAME TO `group`; +--> statement-breakpoint +CREATE UNIQUE INDEX `group_name_unique` ON `group` (`name`); +--> statement-breakpoint +COMMIT TRANSACTION; +--> statement-breakpoint +PRAGMA foreign_keys = ON; +--> statement-breakpoint +BEGIN TRANSACTION; \ No newline at end of file diff --git a/packages/db/migrations/sqlite/meta/0025_snapshot.json b/packages/db/migrations/sqlite/meta/0025_snapshot.json new file mode 100644 index 000000000..0b39a4b22 --- /dev/null +++ b/packages/db/migrations/sqlite/meta/0025_snapshot.json @@ -0,0 +1,1736 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "5f61b281-d2b7-405b-8933-826fdafaa793", + "prevId": "7111fcfa-dd13-42d5-b3f1-6dd094628858", + "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 + }, + "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 + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "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" + }, + "group_home_board_id_board_id_fk": { + "name": "group_home_board_id_board_id_fk", + "tableFrom": "group", + "tableTo": "board", + "columnsFrom": ["home_board_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "group_mobile_home_board_id_board_id_fk": { + "name": "group_mobile_home_board_id_board_id_fk", + "tableFrom": "group", + "tableTo": "board", + "columnsFrom": ["mobile_home_board_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 d82b86fb8..df5c7ef6d 100644 --- a/packages/db/migrations/sqlite/meta/_journal.json +++ b/packages/db/migrations/sqlite/meta/_journal.json @@ -176,6 +176,13 @@ "when": 1738961178990, "tag": "0024_bitter_scrambler", "breakpoints": true + }, + { + "idx": 25, + "version": "6", + "when": 1739468826756, + "tag": "0025_add-group-home-board-settings", + "breakpoints": true } ] } diff --git a/packages/db/queries/group.ts b/packages/db/queries/group.ts new file mode 100644 index 000000000..5c1f33451 --- /dev/null +++ b/packages/db/queries/group.ts @@ -0,0 +1,11 @@ +import { max } from "drizzle-orm"; + +import type { HomarrDatabase } from "../driver"; +import { groups } from "../schema"; + +export const getMaxGroupPositionAsync = async (db: HomarrDatabase) => { + return await db + .select({ value: max(groups.position) }) + .from(groups) + .then((result) => result[0]?.value ?? 1); +}; diff --git a/packages/db/queries/index.ts b/packages/db/queries/index.ts index 76efc434a..13e90d6eb 100644 --- a/packages/db/queries/index.ts +++ b/packages/db/queries/index.ts @@ -1,2 +1,3 @@ export * from "./item"; export * from "./server-setting"; +export * from "./group"; diff --git a/packages/db/schema/mysql.ts b/packages/db/schema/mysql.ts index 870d7f851..0b0c54e3f 100644 --- a/packages/db/schema/mysql.ts +++ b/packages/db/schema/mysql.ts @@ -9,6 +9,7 @@ import { int, mysqlTable, primaryKey, + smallint, text, timestamp, tinyint, @@ -150,6 +151,13 @@ export const groups = mysqlTable("group", { ownerId: varchar({ length: 64 }).references(() => users.id, { onDelete: "set null", }), + homeBoardId: varchar({ length: 64 }).references(() => boards.id, { + onDelete: "set null", + }), + mobileHomeBoardId: varchar({ length: 64 }).references(() => boards.id, { + onDelete: "set null", + }), + position: smallint().notNull(), }); export const groupPermissions = mysqlTable("groupPermission", { @@ -499,6 +507,16 @@ export const groupRelations = relations(groups, ({ one, many }) => ({ fields: [groups.ownerId], references: [users.id], }), + homeBoard: one(boards, { + fields: [groups.homeBoardId], + references: [boards.id], + relationName: "groupRelations__board__homeBoardId", + }), + mobileHomeBoard: one(boards, { + fields: [groups.mobileHomeBoardId], + references: [boards.id], + relationName: "groupRelations__board__mobileHomeBoardId", + }), })); export const groupPermissionRelations = relations(groupPermissions, ({ one }) => ({ @@ -574,6 +592,12 @@ export const boardRelations = relations(boards, ({ many, one }) => ({ }), userPermissions: many(boardUserPermissions), groupPermissions: many(boardGroupPermissions), + groupHomes: many(groups, { + relationName: "groupRelations__board__homeBoardId", + }), + mobileHomeBoard: many(groups, { + relationName: "groupRelations__board__mobileHomeBoardId", + }), })); export const sectionRelations = relations(sections, ({ many, one }) => ({ diff --git a/packages/db/schema/sqlite.ts b/packages/db/schema/sqlite.ts index 44436c84b..a33bf79b4 100644 --- a/packages/db/schema/sqlite.ts +++ b/packages/db/schema/sqlite.ts @@ -133,6 +133,13 @@ export const groups = sqliteTable("group", { ownerId: text().references(() => users.id, { onDelete: "set null", }), + homeBoardId: text().references(() => boards.id, { + onDelete: "set null", + }), + mobileHomeBoardId: text().references(() => boards.id, { + onDelete: "set null", + }), + position: int().notNull(), }); export const groupPermissions = sqliteTable("groupPermission", { @@ -486,6 +493,16 @@ export const groupRelations = relations(groups, ({ one, many }) => ({ fields: [groups.ownerId], references: [users.id], }), + homeBoard: one(boards, { + fields: [groups.homeBoardId], + references: [boards.id], + relationName: "groupRelations__board__homeBoardId", + }), + mobileHomeBoard: one(boards, { + fields: [groups.mobileHomeBoardId], + references: [boards.id], + relationName: "groupRelations__board__mobileHomeBoardId", + }), })); export const groupPermissionRelations = relations(groupPermissions, ({ one }) => ({ @@ -561,6 +578,12 @@ export const boardRelations = relations(boards, ({ many, one }) => ({ }), userPermissions: many(boardUserPermissions), groupPermissions: many(boardGroupPermissions), + groupHomes: many(groups, { + relationName: "groupRelations__board__homeBoardId", + }), + mobileHomeBoard: many(groups, { + relationName: "groupRelations__board__mobileHomeBoardId", + }), })); export const sectionRelations = relations(sections, ({ many, one }) => ({ diff --git a/packages/old-import/src/import/collections/user-collection.ts b/packages/old-import/src/import/collections/user-collection.ts index e98307590..f006e7a57 100644 --- a/packages/old-import/src/import/collections/user-collection.ts +++ b/packages/old-import/src/import/collections/user-collection.ts @@ -36,6 +36,7 @@ export const createUserInsertCollection = ( insertCollection.groups.push({ id: adminGroupId, name: credentialsAdminGroup, + position: 1, }); insertCollection.groupPermissions.push({ diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index f3ad7d017..94da2f675 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -305,7 +305,15 @@ "search": "Find a group", "field": { "name": "Name", - "members": "Members" + "members": "Members", + "homeBoard": { + "label": "Home board", + "description": "Only boards accessible to the group can be selected" + }, + "mobileBoard": { + "label": "Mobile board", + "description": "Only boards accessible to the group can be selected" + } }, "permission": { "admin": { @@ -501,7 +509,35 @@ "select": { "label": "Select group", "notFound": "No group found" + }, + "settings": { + "board": { + "notification": { + "success": { + "title": "Settings saved", + "message": "Board settings saved successfully" + }, + "error": { + "title": "Failed to save settings", + "message": "Unable to save board settings" + } + } + } + }, + "changePosition": { + "notification": { + "success": { + "message": "Position changed successfully" + }, + "error": { + "message": "Unable to change position" + } + } } + }, + "defaultGroup": { + "name": "Default group", + "description": "{name} - All signed in users" } }, "app": { @@ -888,6 +924,7 @@ }, "dangerZone": "Danger zone", "noResults": "No results found", + "unsavedChanges": "You have unsaved changes!", "preview": { "show": "Show preview", "hide": "Hide preview" @@ -2414,6 +2451,13 @@ "ownerOfGroup": "Owner of this group", "ownerOfGroupDeleted": "The owner of this group was deleted. It currently has no owner." }, + "setting": { + "title": "Settings", + "alert": "Group settings are prioritized by the order of groups in the list. The top settings overwrite the bottom settings.", + "board": { + "title": "Boards" + } + }, "members": { "title": "Members", "search": "Find a member", diff --git a/packages/ui/src/components/index.tsx b/packages/ui/src/components/index.tsx index 93b9cec0e..7493529c9 100644 --- a/packages/ui/src/components/index.tsx +++ b/packages/ui/src/components/index.tsx @@ -4,6 +4,7 @@ export { SearchInput } from "./search-input"; export * from "./select-with-description"; export * from "./select-with-description-and-badge"; export { SelectWithCustomItems } from "./select-with-custom-items"; +export type { SelectWithCustomItemsProps } from "./select-with-custom-items"; export { TablePagination } from "./table-pagination"; export { TextMultiSelect } from "./text-multi-select"; export { UserAvatar } from "./user-avatar"; diff --git a/packages/ui/src/components/select-with-custom-items.tsx b/packages/ui/src/components/select-with-custom-items.tsx index bbed3cd1f..d3ff3dff8 100644 --- a/packages/ui/src/components/select-with-custom-items.tsx +++ b/packages/ui/src/components/select-with-custom-items.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo } from "react"; import type { SelectProps } from "@mantine/core"; -import { Combobox, Input, InputBase, useCombobox } from "@mantine/core"; +import { Combobox, ComboboxClearButton, Input, InputBase, useCombobox } from "@mantine/core"; import { useUncontrolled } from "@mantine/hooks"; interface BaseSelectItem { @@ -11,7 +11,7 @@ interface BaseSelectItem { } export interface SelectWithCustomItemsProps - extends Pick { + extends Pick { data: TSelectItem[]; description?: string; withAsterisk?: boolean; @@ -32,6 +32,7 @@ export const SelectWithCustomItems = ({ placeholder, SelectOption, w, + clearable, ...props }: Props) => { const combobox = useCombobox({ @@ -65,6 +66,8 @@ export const SelectWithCustomItems = ({ [setValue, data, combobox], ); + const _clearable = clearable && Boolean(_value); + return ( @@ -73,9 +76,11 @@ export const SelectWithCustomItems = ({ component="button" type="button" pointer - rightSection={} + __clearSection={ setValue(null, null)} />} + __clearable={_clearable} + __defaultRightSection={} onClick={toggle} - rightSectionPointerEvents="none" + rightSectionPointerEvents={_clearable ? "all" : "none"} multiline w={w} > diff --git a/packages/validation/src/group.ts b/packages/validation/src/group.ts index a48642caf..25e1fa4ed 100644 --- a/packages/validation/src/group.ts +++ b/packages/validation/src/group.ts @@ -18,11 +18,25 @@ const createSchema = z.object({ const updateSchema = createSchema.merge(byIdSchema); +const settingsSchema = z.object({ + homeBoardId: z.string().nullable(), + mobileHomeBoardId: z.string().nullable(), +}); + +const savePartialSettingsSchema = z.object({ + id: z.string(), + settings: settingsSchema.partial(), +}); + const savePermissionsSchema = z.object({ groupId: z.string(), permissions: z.array(zodEnumFromArray(groupPermissionKeys)), }); +const savePositionsSchema = z.object({ + positions: z.array(z.string()), +}); + const groupUserSchema = z.object({ groupId: z.string(), userId: z.string() }); export const groupSchemas = { @@ -30,4 +44,7 @@ export const groupSchemas = { update: updateSchema, savePermissions: savePermissionsSchema, groupUser: groupUserSchema, + savePartialSettings: savePartialSettingsSchema, + settings: settingsSchema, + savePositions: savePositionsSchema, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 442e9f28e..f0636d1bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,6 +82,18 @@ importers: apps/nextjs: dependencies: + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@dnd-kit/modifiers': + specifier: ^9.0.0 + version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@19.0.0) '@homarr/analytics': specifier: workspace:^0.1.0 version: link:../../packages/analytics @@ -2351,6 +2363,12 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + '@dnd-kit/modifiers@9.0.0': + resolution: {integrity: sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + '@dnd-kit/sortable@10.0.0': resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} peerDependencies: @@ -10324,7 +10342,7 @@ snapshots: '@dnd-kit/accessibility@3.1.1(react@19.0.0)': dependencies: react: 19.0.0 - tslib: 2.7.0 + tslib: 2.8.1 '@dnd-kit/core@6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: @@ -10332,19 +10350,26 @@ snapshots: '@dnd-kit/utilities': 3.2.2(react@19.0.0) react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - tslib: 2.7.0 + tslib: 2.8.1 + + '@dnd-kit/modifiers@9.0.0(@dnd-kit/core@6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@dnd-kit/utilities': 3.2.2(react@19.0.0) + react: 19.0.0 + tslib: 2.8.1 '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': dependencies: '@dnd-kit/core': 6.3.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@dnd-kit/utilities': 3.2.2(react@19.0.0) react: 19.0.0 - tslib: 2.7.0 + tslib: 2.8.1 '@dnd-kit/utilities@3.2.2(react@19.0.0)': dependencies: react: 19.0.0 - tslib: 2.7.0 + tslib: 2.8.1 '@drizzle-team/brocli@0.10.2': {} @@ -10815,7 +10840,7 @@ snapshots: '@formatjs/intl-localematcher@0.5.5': dependencies: - tslib: 2.7.0 + tslib: 2.8.1 '@grpc/grpc-js@1.12.5': dependencies: