Compare commits

...
Author SHA1 Message Date
Meier Lukas 6143e25686 feat: add recommended badge 2025-11-30 17:33:33 +01:00
Meier Lukas 384d83ef76 fix: export apply auto layout from client export instead 2025-11-30 17:33:15 +01:00
Meier Lukas 998161b1d9 fix: import 2025-11-30 17:00:13 +01:00
Meier Lukas a6e060265a fix: typecheck issue 2025-11-30 16:57:24 +01:00
Meier Lukas d8b278d716 fix: adjust grid-algorithm to reposition from less columns 2025-11-29 23:59:26 +01:00
Meier Lukas 813a91cb01 feat: add layout fields to board settings 2025-11-29 18:52:30 +01:00
Meier Lukas 990452a877 wip: load board with automatic size and position for non base layouts 2025-11-29 15:14:29 +01:00
Meier Lukas 0165e2e415 Merge branch 'dev' into add-auto-layout-mode 2025-11-29 13:30:04 +01:00
homarr-crowdin[bot] ed31e32150 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-11-29 00:50:15 +00:00
Releases Homarr d07730b5f8 chore(release): 1.45.0 [skip ci] 2025-11-28 19:15:49 +00:00
homarr-releases[bot] 681236ec3b chore(release): automatic release v1.45.0 2025-11-28 19:14:48 +00:00
Meier Lukas 524e1a485b fix(security): restrict modification of entrypoint script (#4555) 2025-11-28 18:34:51 +01:00
homarr-renovate[bot] 2bc6964f7e fix(deps): update dependency next-intl to v4.5.5 (#4542)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-28 17:27:04 +00:00
Meier Lukas 21de817dba chore(deps): security hardening (#4551) 2025-11-28 18:09:10 +01:00
homarr-update-contributors[bot] b15d96e8dd chore: update contributors (#4554)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2025-11-28 12:36:13 +00:00
Meier Lukas 514c6edc8f feat(api): use fields in board creation 2025-11-27 17:30:02 +01:00
Noah Croghan 99949fe095 fix: Add fallback when no icon in Minecraft Server widget (#4544) (#4545) 2025-11-27 17:06:31 +01:00
Meier Lukas fdcfb775fa feat(boards): add columns for base layout and layout mode 2025-11-26 22:14:06 +01:00
Meier Lukas 0843ccda99 feat(boards): add form logic and translations to board create modal 2025-11-26 18:38:32 +01:00
Meier Lukas 2b6eb88a53 feat(boards): add layout mode selection to new board modal 2025-11-26 18:22:21 +01:00
Meier Lukas e65c3cd801 feat(ui): add reusable radio card from mantine
see https://mantine.dev/core/radio/#radiocard-component
2025-11-26 18:21:07 +01:00
homarr-crowdin[bot] 85d4931538 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-11-26 00:52:18 +00:00
homarr-renovate[bot] 77a86ead41 fix(deps): update dependency next-intl to v4.5.4 (#4540)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-24 13:02:10 +00:00
homarr-renovate[bot] 27ca124d2a chore(deps): update dependency rollup@&gt;&#x3d;4.0.0 &lt;4.22.4 to >=4.53.3 (#4537)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-24 09:51:55 +00:00
homarr-renovate[bot] 3ca6cb18eb chore(deps): update dependency @types/dockerode to ^3.3.47 (#4539)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-24 09:39:01 +00:00
homarr-crowdin[bot] 245bab7cdd chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-11-24 00:56:25 +00:00
homarr-renovate[bot] 18a6bfd69d fix(deps): update dependency glob to v12 (#4530)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-23 20:07:39 +01:00
homarr-renovate[bot] c4e6528e6c chore(deps): update dependency @types/react to v19.2.6 (#4532)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-23 04:47:11 +00:00
homarr-renovate[bot] 24c985424e fix(deps): update dependency glob to ^11.1.0 (#4529)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-23 04:06:56 +00:00
homarr-renovate[bot] 292e36fec5 fix(deps): update dependency sass to ^1.94.1 (#4531)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-23 02:19:13 +00:00
homarr-renovate[bot] 7eee48ae88 chore(deps): update vitest monorepo to ^4.0.10 (#4528)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-22 13:34:54 +00:00
homarr-renovate[bot] 1b09b588f3 chore(deps): update dependency form-data@&gt;&#x3d;4.0.0 &lt;4.0.4 to >=4.0.5 (#4525)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-22 10:42:15 +00:00
ManuelandCopilot Autofix powered by AI 22fb696f25 ci(security): missing permissions for workflow (#4474)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-11-22 10:39:45 +00:00
generalmat82andMeier Lukas 60ab98ae36 feat(docker): hide containers with label homarr.hide (#4506)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-11-22 11:32:31 +01:00
homarr-renovate[bot] 9a3feb404c fix(deps): update dependency zod-validation-error to v5 (#4437)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-22 10:35:43 +01:00
homarr-renovate[bot] 9c5c4f33ef chore(deps): update node.js to v24.11.1 (#4502)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-22 10:34:17 +01:00
homarr-crowdin[bot] 3ec0c4097a chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-11-22 00:49:38 +00:00
homarr-renovate[bot] e87170ae6e fix(deps): update dependency @tanstack/react-query to ^5.90.10 (#4523)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-11-21 21:29:15 +00:00
Meier Lukas 30fedcb6f1 chore: update bug-report template 2025-11-21 22:14:37 +01:00
88 changed files with 7449 additions and 655 deletions

No files matched your search

+1
View File
@@ -33,6 +33,7 @@ body:
options: options:
# The below comment is used to insert a new version with on-release.yml # The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION# #NEXT_VERSION#
- 1.44.0
- 1.43.1 - 1.43.1
- 1.43.0 - 1.43.0
- 1.42.1 - 1.42.1
+15
View File
@@ -15,6 +15,21 @@
matchUpdateTypes: ["minor", "patch", "pin", "digest"], matchUpdateTypes: ["minor", "patch", "pin", "digest"],
automerge: true, automerge: true,
}, },
{
// disallow automerge for onlyBuiltDependencies
matchPackageNames: [
"@tree-sitter-grammars/tree-sitter-yaml",
"bcrypt",
"better-sqlite3",
"cpu-features",
"esbuild",
"sharp",
"ssh2",
"tree-sitter",
"tree-sitter-json",
],
automerge: false,
},
], ],
updateInternalDeps: true, updateInternalDeps: true,
rangeStrategy: "bump", rangeStrategy: "bump",
+2
View File
@@ -1,4 +1,6 @@
name: "[Quality] Code Analysis" name: "[Quality] Code Analysis"
permissions:
contents: read
on: on:
pull_request: pull_request:
+2
View File
@@ -1,4 +1,6 @@
name: "[Crowdin] Upload translations" name: "[Crowdin] Upload translations"
permissions:
contents: read
on: on:
workflow_dispatch: workflow_dispatch:
@@ -9,6 +9,8 @@ env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
CROWDIN_TOKEN: "${{ secrets.CROWDIN_UPDATE_CONTRIBUTORS_TOKEN }}" CROWDIN_TOKEN: "${{ secrets.CROWDIN_UPDATE_CONTRIBUTORS_TOKEN }}"
permissions:
contents: read
jobs: jobs:
update-contributors: update-contributors:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+1
View File
@@ -3,3 +3,4 @@
# @link https://docs.expo.dev/guides/monorepos/#common-issues # @link https://docs.expo.dev/guides/monorepos/#common-issues
node-linker=hoisted node-linker=hoisted
strict-peer-dependencies=false strict-peer-dependencies=false
engine-strict=true
+18
View File
@@ -1,3 +1,21 @@
## [1.45.0](https://github.com/homarr-labs/homarr/compare/v1.44.0...v1.45.0) (2025-11-28)
### Features
* **docker:** hide containers with label homarr.hide ([#4506](https://github.com/homarr-labs/homarr/issues/4506)) ([60ab98a](https://github.com/homarr-labs/homarr/commit/60ab98ae36abe712c825f7828394d3e3e98c888a))
### Bug Fixes
* Add fallback when no icon in Minecraft Server widget ([#4544](https://github.com/homarr-labs/homarr/issues/4544)) ([#4545](https://github.com/homarr-labs/homarr/issues/4545)) ([99949fe](https://github.com/homarr-labs/homarr/commit/99949fe095ac5a3616c8e9c1964d7eb253c9f826))
* **deps:** update dependency @tanstack/react-query to ^5.90.10 ([#4523](https://github.com/homarr-labs/homarr/issues/4523)) ([e87170a](https://github.com/homarr-labs/homarr/commit/e87170ae6e56cdbeef52e943353060329c39bdc9))
* **deps:** update dependency glob to ^11.1.0 ([#4529](https://github.com/homarr-labs/homarr/issues/4529)) ([24c9854](https://github.com/homarr-labs/homarr/commit/24c985424ecc795eb96b56881a9b00c0c37386c1))
* **deps:** update dependency glob to v12 ([#4530](https://github.com/homarr-labs/homarr/issues/4530)) ([18a6bfd](https://github.com/homarr-labs/homarr/commit/18a6bfd69d86de8c58dbfe6dc8b8fab78b2b5ee6))
* **deps:** update dependency next-intl to v4.5.4 ([#4540](https://github.com/homarr-labs/homarr/issues/4540)) ([77a86ea](https://github.com/homarr-labs/homarr/commit/77a86ead41c0d098987561249203667ac4d88177))
* **deps:** update dependency next-intl to v4.5.5 ([#4542](https://github.com/homarr-labs/homarr/issues/4542)) ([2bc6964](https://github.com/homarr-labs/homarr/commit/2bc6964f7efb5203f626d2f07b847dffd53191f0))
* **deps:** update dependency sass to ^1.94.1 ([#4531](https://github.com/homarr-labs/homarr/issues/4531)) ([292e36f](https://github.com/homarr-labs/homarr/commit/292e36fec5acff9b1d143f19e18ea170c87fa0d0))
* **deps:** update dependency zod-validation-error to v5 ([#4437](https://github.com/homarr-labs/homarr/issues/4437)) ([9a3feb4](https://github.com/homarr-labs/homarr/commit/9a3feb404c0c284b4f33fce12d87a469741a6ebd))
* **security:** restrict modification of entrypoint script ([#4555](https://github.com/homarr-labs/homarr/issues/4555)) ([524e1a4](https://github.com/homarr-labs/homarr/commit/524e1a485be56dbebf8f71d4546bc65672737f30))
## [1.44.0](https://github.com/homarr-labs/homarr/compare/v1.43.3...v1.44.0) (2025-11-21) ## [1.44.0](https://github.com/homarr-labs/homarr/compare/v1.43.3...v1.44.0) (2025-11-21)
### Features ### Features
+2 -2
View File
@@ -1,4 +1,4 @@
FROM node:24.11.0-alpine AS base FROM node:24.11.1-alpine AS base
FROM base AS builder FROM base AS builder
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
@@ -55,7 +55,7 @@ COPY --from=builder /app/apps/nextjs/.next/standalone ./
COPY --from=builder /app/apps/nextjs/.next/static ./apps/nextjs/.next/static COPY --from=builder /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
COPY --from=builder /app/apps/nextjs/public ./apps/nextjs/public COPY --from=builder /app/apps/nextjs/public ./apps/nextjs/public
COPY scripts/run.sh ./run.sh COPY scripts/run.sh ./run.sh
COPY --chmod=777 scripts/entrypoint.sh ./entrypoint.sh COPY --chmod=755 scripts/entrypoint.sh ./entrypoint.sh
COPY packages/redis/redis.conf /app/redis.conf COPY packages/redis/redis.conf /app/redis.conf
COPY nginx.conf /etc/nginx/templates/nginx.conf COPY nginx.conf /etc/nginx/templates/nginx.conf
+4 -4
View File
@@ -58,7 +58,7 @@
"@mantine/tiptap": "^8.3.8", "@mantine/tiptap": "^8.3.8",
"@million/lint": "1.0.14", "@million/lint": "1.0.14",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"@tanstack/react-query": "^5.90.9", "@tanstack/react-query": "^5.90.10",
"@tanstack/react-query-devtools": "^5.90.2", "@tanstack/react-query-devtools": "^5.90.2",
"@tanstack/react-query-next-experimental": "^5.91.0", "@tanstack/react-query-next-experimental": "^5.91.0",
"@trpc/client": "^11.7.1", "@trpc/client": "^11.7.1",
@@ -74,7 +74,7 @@
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"flag-icons": "^7.5.0", "flag-icons": "^7.5.0",
"glob": "^11.0.3", "glob": "^12.0.0",
"isomorphic-dompurify": "^2.32.0", "isomorphic-dompurify": "^2.32.0",
"jotai": "^2.15.1", "jotai": "^2.15.1",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
@@ -85,7 +85,7 @@
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-simple-code-editor": "^0.14.1", "react-simple-code-editor": "^0.14.1",
"sass": "^1.94.0", "sass": "^1.94.1",
"superjson": "2.2.5", "superjson": "2.2.5",
"swagger-ui-react": "^5.30.2", "swagger-ui-react": "^5.30.2",
"use-deep-compare-effect": "^1.8.1", "use-deep-compare-effect": "^1.8.1",
@@ -98,7 +98,7 @@
"@types/chroma-js": "3.1.2", "@types/chroma-js": "3.1.2",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"@types/react": "19.2.5", "@types/react": "19.2.6",
"@types/react-dom": "19.2.3", "@types/react-dom": "19.2.3",
"@types/swagger-ui-react": "^5.18.0", "@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
@@ -17,7 +17,7 @@ import {
IconSettings, IconSettings,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client"; import { applyAutoLayoutToBoard, clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client"; import { useSession } from "@homarr/auth/client";
import { useRequiredBoard } from "@homarr/boards/context"; import { useRequiredBoard } from "@homarr/boards/context";
import { useEditMode } from "@homarr/boards/edit-mode"; import { useEditMode } from "@homarr/boards/edit-mode";
@@ -165,7 +165,7 @@ const EditModeMenu = () => {
}); });
const toggle = useCallback(() => { const toggle = useCallback(() => {
if (isEditMode) return saveBoard(board); if (isEditMode) return saveBoard(applyAutoLayoutToBoard(board));
open(); open();
}, [board, isEditMode, saveBoard, open]); }, [board, isEditMode, saveBoard, open]);
@@ -1,11 +1,31 @@
"use client"; "use client";
import { Button, Fieldset, Grid, Group, Input, NumberInput, Slider, Stack, Text, TextInput } from "@mantine/core"; import {
Badge,
Box,
Button,
Chip,
ChipGroup,
Fieldset,
Grid,
Group,
Input,
InputWrapper,
NumberInput,
RadioGroup,
SimpleGrid,
Slider,
Stack,
Text,
TextInput,
} from "@mantine/core";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { createId } from "@homarr/common"; import { createId } from "@homarr/common";
import { boardLayoutModes } from "@homarr/definitions";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { CustomRadioCard } from "@homarr/ui";
import { boardSaveLayoutsSchema } from "@homarr/validation/board"; import { boardSaveLayoutsSchema } from "@homarr/validation/board";
import type { Board } from "../../_types"; import type { Board } from "../../_types";
@@ -25,6 +45,8 @@ export const LayoutSettingsContent = ({ board }: Props) => {
const form = useZodForm(boardSaveLayoutsSchema.omit({ id: true }).required(), { const form = useZodForm(boardSaveLayoutsSchema.omit({ id: true }).required(), {
initialValues: { initialValues: {
layouts: board.layouts, layouts: board.layouts,
layoutMode: board.layoutMode,
baseLayoutId: board.baseLayoutId,
}, },
}); });
@@ -38,6 +60,53 @@ export const LayoutSettingsContent = ({ board }: Props) => {
})} })}
> >
<Stack> <Stack>
<RadioGroup {...form.getInputProps("layoutMode")} label={t("board.field.layoutMode.label")}>
<SimpleGrid cols={{ sm: 2, xs: 1 }} spacing="md">
{boardLayoutModes.values.map((layoutMode) => (
<Box w="100%" key={layoutMode} pos="relative">
<CustomRadioCard
value={layoutMode}
label={t(`board.field.layoutMode.option.${layoutMode}.label`)}
description={t(`board.field.layoutMode.option.${layoutMode}.description`)}
/>
{layoutMode === boardLayoutModes.defaultValue && (
<Badge pos="absolute" top="8px" right="8px" variant="light" size="sm">
{t("common.select.badge.recommended")}
</Badge>
)}
</Box>
))}
</SimpleGrid>
</RadioGroup>
{form.values.layoutMode === "auto" && (
<InputWrapper label={t("board.field.baseLayout.label")} description={t("board.field.baseLayout.description")}>
<ChipGroup {...form.getInputProps("baseLayoutId")} multiple={false}>
<SimpleGrid
cols={{
lg: Math.min(6, form.values.layouts.length),
sm: Math.min(4, form.values.layouts.length),
xs: Math.min(2, form.values.layouts.length),
}}
spacing="md"
mt="sm"
>
{form.values.layouts.map((layout) => (
<Chip
key={layout.id}
value={layout.id}
w="100%"
size="sm"
styles={{ label: { width: "100%", justifyContent: "center" } }}
>
{layout.name}
</Chip>
))}
</SimpleGrid>
</ChipGroup>
</InputWrapper>
)}
<Stack gap="sm"> <Stack gap="sm">
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
<Text fw={500}>{t("board.setting.section.layout.responsive.title")}</Text> <Text fw={500}>{t("board.setting.section.layout.responsive.title")}</Text>
@@ -82,18 +151,21 @@ export const LayoutSettingsContent = ({ board }: Props) => {
/> />
</Grid.Col> </Grid.Col>
</Grid> </Grid>
{form.values.layouts.length >= 2 && ( {form.values.layouts.length >= 2 && form.values.baseLayoutId !== layout.id && (
<Group justify="end"> <Group justify="end">
<Button <Button
variant="subtle" variant="subtle"
onClick={() => { onClick={() => {
form.setValues((previous) => form.setValues((previous) => {
previous.layouts !== undefined && previous.layouts.length >= 2 if (!previous.layouts) return previous;
? { if (previous.layouts.length < 2) return previous;
layouts: form.values.layouts.filter((filteredLayout) => filteredLayout.id !== layout.id), if (previous.baseLayoutId === layout.id) return previous;
}
: previous, return {
); ...previous,
layouts: previous.layouts.filter((filteredLayout) => filteredLayout.id !== layout.id),
};
});
}} }}
> >
{t("common.action.remove")} {t("common.action.remove")}
@@ -10,6 +10,7 @@ export class BoardMockBuilder {
private readonly board: Board; private readonly board: Board;
constructor(board?: Partial<Omit<Board, "groupPermissions" | "userPermissions" | "sections" | "items" | "layouts">>) { constructor(board?: Partial<Omit<Board, "groupPermissions" | "userPermissions" | "sections" | "items" | "layouts">>) {
const baseLayoutId = createId();
this.board = { this.board = {
id: createId(), id: createId(),
backgroundImageRepeat: "no-repeat", backgroundImageRepeat: "no-repeat",
@@ -41,12 +42,14 @@ export class BoardMockBuilder {
items: [], items: [],
layouts: [ layouts: [
{ {
id: createId(), id: baseLayoutId,
name: "Base", name: "Base",
columnCount: 12, columnCount: 12,
breakpoint: 0, breakpoint: 0,
}, },
], ],
baseLayoutId,
layoutMode: "auto",
...board, ...board,
}; };
} }
+9 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "homarr", "name": "homarr",
"version": "1.44.0", "version": "1.45.0",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "cross-env CI=true turbo build", "build": "cross-env CI=true turbo build",
@@ -45,22 +45,24 @@
"@testcontainers/redis": "^11.8.1", "@testcontainers/redis": "^11.8.1",
"@turbo/gen": "^2.6.1", "@turbo/gen": "^2.6.1",
"@vitejs/plugin-react": "^5.1.1", "@vitejs/plugin-react": "^5.1.1",
"@vitest/coverage-v8": "^4.0.9", "@vitest/coverage-v8": "^4.0.10",
"@vitest/ui": "^4.0.9", "@vitest/ui": "^4.0.10",
"conventional-changelog-conventionalcommits": "^9.1.0", "conventional-changelog-conventionalcommits": "^9.1.0",
"cross-env": "^10.1.0", "cross-env": "^10.1.0",
"jsdom": "^27.2.0", "jsdom": "^27.2.0",
"json5": "^2.2.3",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"semantic-release": "^25.0.2", "semantic-release": "^25.0.2",
"testcontainers": "^11.8.1", "testcontainers": "^11.8.1",
"turbo": "^2.6.1", "turbo": "^2.6.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^4.0.9" "vitest": "^4.0.10"
}, },
"packageManager": "pnpm@10.22.0", "packageManager": "pnpm@10.22.0",
"engines": { "engines": {
"node": ">=24.11.1" "node": ">=24.11.1",
"pnpm": ">=10.22.0"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
@@ -81,14 +83,14 @@
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1", "brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1", "brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
"esbuild@<=0.24.2": ">=0.27.0", "esbuild@<=0.24.2": ">=0.27.0",
"form-data@>=4.0.0 <4.0.4": ">=4.0.4", "form-data@>=4.0.0 <4.0.4": ">=4.0.5",
"hono@<4.6.5": ">=4.10.6", "hono@<4.6.5": ">=4.10.6",
"linkifyjs@<4.3.2": ">=4.3.2", "linkifyjs@<4.3.2": ">=4.3.2",
"nanoid@>=4.0.0 <5.0.9": ">=5.1.6", "nanoid@>=4.0.0 <5.0.9": ">=5.1.6",
"prismjs@<1.30.0": ">=1.30.0", "prismjs@<1.30.0": ">=1.30.0",
"proxmox-api>undici": "7.16.0", "proxmox-api>undici": "7.16.0",
"react-is": "^19.2.0", "react-is": "^19.2.0",
"rollup@>=4.0.0 <4.22.4": ">=4.53.2", "rollup@>=4.0.0 <4.22.4": ">=4.53.3",
"sha.js@<=2.4.11": ">=2.4.12", "sha.js@<=2.4.11": ">=2.4.12",
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1", "tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.1", "tar-fs@>=2.0.0 <2.1.3": ">=3.1.1",
+1 -1
View File
@@ -43,7 +43,7 @@
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.4.0", "@kubernetes/client-node": "^1.4.0",
"@tanstack/react-query": "^5.90.9", "@tanstack/react-query": "^5.90.10",
"@trpc/client": "^11.7.1", "@trpc/client": "^11.7.1",
"@trpc/react-query": "^11.7.1", "@trpc/react-query": "^11.7.1",
"@trpc/server": "^11.7.1", "@trpc/server": "^11.7.1",
+2
View File
@@ -17,3 +17,5 @@ export const fetchApi = createTRPCClient<AppRouter>({
}), }),
], ],
}); });
export { applyAutoLayoutToBoard } from "./router/board/board-layout";
+41 -249
View File
@@ -5,7 +5,7 @@ import { z } from "zod/v4";
import { constructBoardPermissions } from "@homarr/auth/shared"; import { constructBoardPermissions } from "@homarr/auth/shared";
import { createId } from "@homarr/common"; import { createId } from "@homarr/common";
import type { DeviceType } from "@homarr/common/server"; import type { DeviceType } from "@homarr/common/server";
import type { Database, InferInsertModel, InferSelectModel, SQL } from "@homarr/db"; import type { Database, InferInsertModel, InferSelectModel } from "@homarr/db";
import { and, asc, eq, handleTransactionsAsync, inArray, isNull, like, not, or, sql } from "@homarr/db"; import { and, asc, eq, handleTransactionsAsync, inArray, isNull, like, not, or, sql } from "@homarr/db";
import { createDbInsertCollectionWithoutTransaction } from "@homarr/db/collection"; import { createDbInsertCollectionWithoutTransaction } from "@homarr/db/collection";
import { getServerSettingByKeyAsync } from "@homarr/db/queries"; import { getServerSettingByKeyAsync } from "@homarr/db/queries";
@@ -27,13 +27,11 @@ import {
sections, sections,
users, users,
} from "@homarr/db/schema"; } from "@homarr/db/schema";
import type { WidgetKind } from "@homarr/definitions";
import { import {
emptySuperJSON, emptySuperJSON,
everyoneGroup, everyoneGroup,
getPermissionsWithChildren, getPermissionsWithChildren,
getPermissionsWithParents, getPermissionsWithParents,
widgetKinds,
} from "@homarr/definitions"; } from "@homarr/definitions";
import { importOldmarrAsync } from "@homarr/old-import"; import { importOldmarrAsync } from "@homarr/old-import";
import { importJsonFileSchema } from "@homarr/old-import/shared"; import { importJsonFileSchema } from "@homarr/old-import/shared";
@@ -50,13 +48,11 @@ import {
boardSaveSchema, boardSaveSchema,
} from "@homarr/validation/board"; } from "@homarr/validation/board";
import { byIdSchema } from "@homarr/validation/common"; import { byIdSchema } from "@homarr/validation/common";
import { zodUnionFromArray } from "@homarr/validation/enums";
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
import { sectionSchema, sharedItemSchema } from "@homarr/validation/shared";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc";
import { throwIfActionForbiddenAsync } from "./board/board-access"; import { throwIfActionForbiddenAsync } from "./board/board-access";
import { generateResponsiveGridFor } from "./board/grid-algorithm"; import { applyAutoLayoutToBoard, getUpdatedBoardLayout } from "./board/board-layout";
import { getFullBoardWithWhereAsync } from "./board/full-board-query";
export const boardRouter = createTRPCRouter({ export const boardRouter = createTRPCRouter({
exists: permissionRequiredProcedure exists: permissionRequiredProcedure
@@ -279,6 +275,7 @@ export const boardRouter = createTRPCRouter({
name: input.name, name: input.name,
isPublic: input.isPublic, isPublic: input.isPublic,
creatorId: ctx.session.user.id, creatorId: ctx.session.user.id,
layoutMode: input.layoutMode,
}); });
createBoardCollection.sections.push({ createBoardCollection.sections.push({
id: createId(), id: createId(),
@@ -287,8 +284,9 @@ export const boardRouter = createTRPCRouter({
yOffset: 0, yOffset: 0,
boardId, boardId,
}); });
const baseLayoutId = createId();
createBoardCollection.layouts.push({ createBoardCollection.layouts.push({
id: createId(), id: baseLayoutId,
name: "Base", name: "Base",
columnCount: input.columnCount, columnCount: input.columnCount,
breakpoint: 0, breakpoint: 0,
@@ -296,6 +294,10 @@ export const boardRouter = createTRPCRouter({
}); });
await createBoardCollection.insertAllAsync(ctx.db); await createBoardCollection.insertAllAsync(ctx.db);
// Define the base layout (only for automatic layout mode)
if (input.layoutMode === "auto") {
await ctx.db.update(boards).set({ baseLayoutId }).where(eq(boards.id, boardId));
}
if (!user?.homeBoardId) { if (!user?.homeBoardId) {
await ctx.db.update(users).set({ homeBoardId: boardId }).where(eq(users.id, ctx.session.user.id)); await ctx.db.update(users).set({ homeBoardId: boardId }).where(eq(users.id, ctx.session.user.id));
@@ -582,13 +584,15 @@ export const boardRouter = createTRPCRouter({
await throwIfActionForbiddenAsync(ctx, boardWhere, "view"); await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null); const board = await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
return applyAutoLayoutToBoard(board);
}), }),
getBoardByName: publicProcedure.input(boardByNameSchema).query(async ({ input, ctx }) => { getBoardByName: publicProcedure.input(boardByNameSchema).query(async ({ input, ctx }) => {
const boardWhere = eq(sql`UPPER(${boards.name})`, input.name.toUpperCase()); const boardWhere = eq(sql`UPPER(${boards.name})`, input.name.toUpperCase());
await throwIfActionForbiddenAsync(ctx, boardWhere, "view"); await throwIfActionForbiddenAsync(ctx, boardWhere, "view");
return await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null); const board = await getFullBoardWithWhereAsync(ctx.db, boardWhere, ctx.session?.user.id ?? null);
return applyAutoLayoutToBoard(board);
}), }),
saveLayouts: protectedProcedure.input(boardSaveLayoutsSchema).mutation(async ({ ctx, input }) => { saveLayouts: protectedProcedure.input(boardSaveLayoutsSchema).mutation(async ({ ctx, input }) => {
await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify"); await throwIfActionForbiddenAsync(ctx, eq(boards.id, input.id), "modify");
@@ -596,13 +600,32 @@ export const boardRouter = createTRPCRouter({
const board = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id); const board = await getFullBoardWithWhereAsync(ctx.db, eq(boards.id, input.id), ctx.session.user.id);
const addedLayouts = filterAddedItems(input.layouts, board.layouts); const addedLayouts = filterAddedItems(input.layouts, board.layouts);
const removedLayouts = filterRemovedItems(input.layouts, board.layouts);
if (input.layoutMode === "auto" && input.baseLayoutId === null) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Base layout must be set when using automatic layout mode",
});
}
if (removedLayouts.some((layout) => layout.id === input.baseLayoutId)) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Cannot remove base layout",
});
}
const layoutsToInsert: InferInsertModel<typeof layouts>[] = []; const layoutsToInsert: InferInsertModel<typeof layouts>[] = [];
const itemSectionLayoutsToInsert: InferInsertModel<typeof itemLayouts>[] = []; const itemSectionLayoutsToInsert: InferInsertModel<typeof itemLayouts>[] = [];
const sectionLayoutsToInsert: InferInsertModel<typeof sectionLayouts>[] = []; const sectionLayoutsToInsert: InferInsertModel<typeof sectionLayouts>[] = [];
let baseLayoutId = input.baseLayoutId;
for (const addedLayout of addedLayouts) { for (const addedLayout of addedLayouts) {
const layoutId = createId(); const layoutId = createId();
if (addedLayout.id === input.baseLayoutId) {
baseLayoutId = layoutId;
}
layoutsToInsert.push({ layoutsToInsert.push({
id: layoutId, id: layoutId,
@@ -709,11 +732,18 @@ export const boardRouter = createTRPCRouter({
.where(eq(layouts.id, updatedLayout.id)); .where(eq(layouts.id, updatedLayout.id));
} }
const removedLayouts = filterRemovedItems(input.layouts, board.layouts);
const removedLayoutIds = removedLayouts.map((layout) => layout.id); const removedLayoutIds = removedLayouts.map((layout) => layout.id);
if (removedLayoutIds.length > 0) { if (removedLayoutIds.length > 0) {
await ctx.db.delete(layouts).where(inArray(layouts.id, removedLayoutIds)); await ctx.db.delete(layouts).where(inArray(layouts.id, removedLayoutIds));
} }
await ctx.db
.update(boards)
.set({
baseLayoutId,
layoutMode: input.layoutMode,
})
.where(eq(boards.id, input.id));
}), }),
savePartialBoardSettings: protectedProcedure savePartialBoardSettings: protectedProcedure
.input(boardSavePartialSettingsSchema.and(z.object({ id: z.string() }))) .input(boardSavePartialSettingsSchema.and(z.object({ id: z.string() })))
@@ -1408,244 +1438,6 @@ const noBoardWithSimilarNameAsync = async (db: Database, name: string, ignoredId
} }
}; };
const getUpdatedBoardLayout = (
board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>,
options: {
previous: {
layoutId: string;
columnCount: number;
};
current: {
layoutId: string;
columnCount: number;
};
},
) => {
const itemSectionLayoutsCollection: InferInsertModel<typeof itemLayouts>[] = [];
const sectionLayoutsCollection: InferInsertModel<typeof sectionLayouts>[] = [];
const elements = getElementsForLayout(board, options.previous.layoutId);
const rootSections = board.sections.filter((section) => section.kind !== "dynamic");
for (const rootSection of rootSections) {
const result = generateResponsiveGridFor({
items: elements,
previousWidth: options.previous.columnCount,
width: options.current.columnCount,
sectionId: rootSection.id,
});
itemSectionLayoutsCollection.push(
...board.items
.map((item): InferInsertModel<typeof itemLayouts> | null => {
const currentElement = result.items.find((element) => element.type === "item" && element.id === item.id);
if (!currentElement) {
return null;
}
return {
itemId: item.id,
layoutId: options.current.layoutId,
sectionId: currentElement.sectionId,
height: currentElement.height,
width: currentElement.width,
xOffset: currentElement.xOffset,
yOffset: currentElement.yOffset,
};
})
.filter((item) => item !== null),
);
sectionLayoutsCollection.push(
...board.sections
.filter((section) => section.kind === "dynamic")
.map((section): InferInsertModel<typeof sectionLayouts> | null => {
const currentElement = result.items.find(
(element) => element.type === "section" && element.id === section.id,
);
if (!currentElement) {
return null;
}
return {
layoutId: options.current.layoutId,
sectionId: section.id,
parentSectionId: currentElement.sectionId,
height: currentElement.height,
width: currentElement.width,
xOffset: currentElement.xOffset,
yOffset: currentElement.yOffset,
};
})
.filter((section) => section !== null),
);
}
return {
itemSectionLayouts: itemSectionLayoutsCollection,
sectionLayouts: sectionLayoutsCollection,
};
};
const getElementsForLayout = (board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>, layoutId: string) => {
const sectionElements = board.sections
.filter((section) => section.kind === "dynamic")
.map((section) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const clonedLayout = section.layouts.find((sectionLayout) => sectionLayout.layoutId === layoutId)!;
return {
id: section.id,
type: "section" as const,
height: clonedLayout.height,
width: clonedLayout.width,
xOffset: clonedLayout.xOffset,
yOffset: clonedLayout.yOffset,
sectionId: clonedLayout.parentSectionId,
};
});
const itemElements = board.items.map((item) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const clonedLayout = item.layouts.find((itemLayout) => itemLayout.layoutId === layoutId)!;
return {
id: item.id,
type: "item" as const,
height: clonedLayout.height,
width: clonedLayout.width,
xOffset: clonedLayout.xOffset,
yOffset: clonedLayout.yOffset,
sectionId: clonedLayout.sectionId,
};
});
return [...itemElements, ...sectionElements];
};
const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, userId: string | null) => {
const groupsOfCurrentUser = await db.query.groupMembers.findMany({
where: eq(groupMembers.userId, userId ?? ""),
});
const board = await db.query.boards.findFirst({
where,
with: {
creator: {
columns: {
id: true,
name: true,
image: true,
},
},
sections: {
with: {
collapseStates: {
where: eq(sectionCollapseStates.userId, userId ?? ""),
},
layouts: true,
},
},
items: {
with: {
integrations: {
with: {
integration: true,
},
},
layouts: true,
},
},
layouts: true,
userPermissions: {
where: eq(boardUserPermissions.userId, userId ?? ""),
columns: {
permission: true,
},
},
groupPermissions: {
where: inArray(boardGroupPermissions.groupId, groupsOfCurrentUser.map((group) => group.groupId).concat("")),
},
},
});
if (!board) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Board not found",
});
}
const { sections, items, layouts, ...otherBoardProperties } = board;
return {
...otherBoardProperties,
layouts: layouts
.map(({ boardId: _, ...layout }) => layout)
.sort((layoutA, layoutB) => layoutA.breakpoint - layoutB.breakpoint),
sections: sections.map(({ collapseStates, ...section }) =>
parseSection({
...section,
xOffset: section.xOffset,
yOffset: section.yOffset,
options: superjson.parse(section.options ?? emptySuperJSON),
layouts: section.layouts.map((layout) => ({
xOffset: layout.xOffset,
yOffset: layout.yOffset,
width: layout.width,
height: layout.height,
parentSectionId: layout.parentSectionId,
layoutId: layout.layoutId,
})),
collapsed: collapseStates.at(0)?.collapsed ?? false,
}),
),
items: items.map(({ integrations: itemIntegrations, ...item }) =>
parseItem({
...item,
layouts: item.layouts.map((layout) => ({
xOffset: layout.xOffset,
yOffset: layout.yOffset,
width: layout.width,
height: layout.height,
layoutId: layout.layoutId,
sectionId: layout.sectionId,
})),
integrationIds: itemIntegrations.map((item) => item.integration.id),
advancedOptions: superjson.parse<BoardItemAdvancedOptions>(item.advancedOptions),
options: superjson.parse<Record<string, unknown>>(item.options),
}),
),
};
};
const forKind = <T extends WidgetKind>(kind: T) =>
z.object({
kind: z.literal(kind),
options: z.record(z.string(), z.unknown()),
});
const outputItemSchema = zodUnionFromArray(widgetKinds.map((kind) => forKind(kind))).and(sharedItemSchema);
const parseItem = (item: unknown) => {
const result = outputItemSchema.safeParse(item);
if (!result.success) {
throw new Error(result.error.message);
}
return result.data;
};
const parseSection = (section: unknown) => {
const result = sectionSchema.safeParse(section);
if (!result.success) {
throw new Error(result.error.message);
}
return result.data;
};
const filterAddedItems = <TInput extends { id: string }>(inputArray: TInput[], dbArray: TInput[]) => const filterAddedItems = <TInput extends { id: string }>(inputArray: TInput[], dbArray: TInput[]) =>
inputArray.filter((inputItem) => !dbArray.some((dbItem) => dbItem.id === inputItem.id)); inputArray.filter((inputItem) => !dbArray.some((dbItem) => dbItem.id === inputItem.id));
@@ -0,0 +1,186 @@
import type { Modify } from "@homarr/common/types";
import type { InferInsertModel } from "@homarr/db";
import type { itemLayouts, sectionLayouts } from "@homarr/db/schema";
import type { getFullBoardWithWhereAsync } from "./full-board-query";
import { generateResponsiveGridFor } from "./grid-algorithm";
type DynamicSectionLayout = Modify<InferInsertModel<typeof sectionLayouts>, { parentSectionId: string }>;
export const getUpdatedBoardLayout = (
board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>,
options: {
previous: {
layoutId: string;
columnCount: number;
};
current: {
layoutId: string;
columnCount: number;
};
},
) => {
const itemSectionLayoutsCollection: InferInsertModel<typeof itemLayouts>[] = [];
const sectionLayoutsCollection: DynamicSectionLayout[] = [];
const elements = getElementsForLayout(board, options.previous.layoutId);
const rootSections = board.sections.filter((section) => section.kind !== "dynamic");
for (const rootSection of rootSections) {
const result = generateResponsiveGridFor({
items: elements,
previousWidth: options.previous.columnCount,
width: options.current.columnCount,
sectionId: rootSection.id,
});
itemSectionLayoutsCollection.push(
...board.items
.map((item): InferInsertModel<typeof itemLayouts> | null => {
const currentElement = result.items.find((element) => element.type === "item" && element.id === item.id);
if (!currentElement) {
return null;
}
return {
itemId: item.id,
layoutId: options.current.layoutId,
sectionId: currentElement.sectionId,
height: currentElement.height,
width: currentElement.width,
xOffset: currentElement.xOffset,
yOffset: currentElement.yOffset,
};
})
.filter((item) => item !== null),
);
sectionLayoutsCollection.push(
...board.sections
.filter((section) => section.kind === "dynamic")
.map((section): DynamicSectionLayout | null => {
const currentElement = result.items.find(
(element) => element.type === "section" && element.id === section.id,
);
if (!currentElement) {
return null;
}
return {
layoutId: options.current.layoutId,
sectionId: section.id,
parentSectionId: currentElement.sectionId,
height: currentElement.height,
width: currentElement.width,
xOffset: currentElement.xOffset,
yOffset: currentElement.yOffset,
};
})
.filter((section) => section !== null),
);
}
return {
itemSectionLayouts: itemSectionLayoutsCollection,
sectionLayouts: sectionLayoutsCollection,
};
};
const getElementsForLayout = (board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>, layoutId: string) => {
const sectionElements = board.sections
.filter((section) => section.kind === "dynamic")
.map((section) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const clonedLayout = section.layouts.find((sectionLayout) => sectionLayout.layoutId === layoutId)!;
return {
id: section.id,
type: "section" as const,
height: clonedLayout.height,
width: clonedLayout.width,
xOffset: clonedLayout.xOffset,
yOffset: clonedLayout.yOffset,
sectionId: clonedLayout.parentSectionId,
};
});
const itemElements = board.items.map((item) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const clonedLayout = item.layouts.find((itemLayout) => itemLayout.layoutId === layoutId)!;
return {
id: item.id,
type: "item" as const,
height: clonedLayout.height,
width: clonedLayout.width,
xOffset: clonedLayout.xOffset,
yOffset: clonedLayout.yOffset,
sectionId: clonedLayout.sectionId,
};
});
return [...itemElements, ...sectionElements];
};
export const applyAutoLayoutToBoard = (
board: Awaited<ReturnType<typeof getFullBoardWithWhereAsync>>,
): Awaited<ReturnType<typeof getFullBoardWithWhereAsync>> => {
if (board.layoutMode === "custom") return board;
const baseLayout = board.layouts.find((layout) => layout.id === board.baseLayoutId) ?? null;
if (!baseLayout) return board;
const updatedLayoutMap = new Map<string, ReturnType<typeof getUpdatedBoardLayout>>();
for (const layout of board.layouts) {
if (layout.id === baseLayout.id) continue;
updatedLayoutMap.set(
layout.id,
getUpdatedBoardLayout(board, {
previous: {
layoutId: baseLayout.id,
columnCount: baseLayout.columnCount,
},
current: {
layoutId: layout.id,
columnCount: layout.columnCount,
},
}),
);
}
return {
...board,
items: board.items.map((item) => ({
...item,
layouts: item.layouts.map(
(layoutSection) =>
updatedLayoutMap
.get(layoutSection.layoutId)
?.itemSectionLayouts.find(
(updatedLayoutSection) =>
updatedLayoutSection.itemId === item.id && updatedLayoutSection.layoutId === layoutSection.layoutId,
) ?? layoutSection,
),
})),
sections: board.sections.map((section) =>
"layouts" in section
? {
...section,
layouts: section.layouts.map(
(sectionLayout) =>
updatedLayoutMap
.get(sectionLayout.layoutId)
?.sectionLayouts.find(
(updatedSectionLayout) =>
updatedSectionLayout.sectionId === section.id &&
updatedSectionLayout.layoutId === sectionLayout.layoutId,
) ?? sectionLayout,
),
}
: section,
),
};
};
@@ -0,0 +1,133 @@
import { TRPCError } from "@trpc/server";
import superjson from "superjson";
import z from "zod";
import type { Database, SQL } from "@homarr/db";
import { eq, inArray } from "@homarr/db";
import { boardGroupPermissions, boardUserPermissions, groupMembers, sectionCollapseStates } from "@homarr/db/schema";
import type { WidgetKind } from "@homarr/definitions";
import { emptySuperJSON, widgetKinds } from "@homarr/definitions";
import { zodUnionFromArray } from "@homarr/validation/enums";
import { sectionSchema, sharedItemSchema } from "@homarr/validation/shared";
import type { BoardItemAdvancedOptions } from "@homarr/validation/shared";
export const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, userId: string | null) => {
const groupsOfCurrentUser = await db.query.groupMembers.findMany({
where: eq(groupMembers.userId, userId ?? ""),
});
const board = await db.query.boards.findFirst({
where,
with: {
creator: {
columns: {
id: true,
name: true,
image: true,
},
},
sections: {
with: {
collapseStates: {
where: eq(sectionCollapseStates.userId, userId ?? ""),
},
layouts: true,
},
},
items: {
with: {
integrations: {
with: {
integration: true,
},
},
layouts: true,
},
},
layouts: true,
userPermissions: {
where: eq(boardUserPermissions.userId, userId ?? ""),
columns: {
permission: true,
},
},
groupPermissions: {
where: inArray(boardGroupPermissions.groupId, groupsOfCurrentUser.map((group) => group.groupId).concat("")),
},
},
});
if (!board) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Board not found",
});
}
const { sections, items, layouts, ...otherBoardProperties } = board;
return {
...otherBoardProperties,
layouts: layouts
.map(({ boardId: _, ...layout }) => layout)
.sort((layoutA, layoutB) => layoutA.breakpoint - layoutB.breakpoint),
sections: sections.map(({ collapseStates, ...section }) =>
parseSection({
...section,
xOffset: section.xOffset,
yOffset: section.yOffset,
options: superjson.parse(section.options ?? emptySuperJSON),
layouts: section.layouts.map((layout) => ({
xOffset: layout.xOffset,
yOffset: layout.yOffset,
width: layout.width,
height: layout.height,
parentSectionId: layout.parentSectionId,
layoutId: layout.layoutId,
})),
collapsed: collapseStates.at(0)?.collapsed ?? false,
}),
),
items: items.map(({ integrations: itemIntegrations, ...item }) =>
parseItem({
...item,
layouts: item.layouts.map((layout) => ({
xOffset: layout.xOffset,
yOffset: layout.yOffset,
width: layout.width,
height: layout.height,
layoutId: layout.layoutId,
sectionId: layout.sectionId,
})),
integrationIds: itemIntegrations.map((item) => item.integration.id),
advancedOptions: superjson.parse<BoardItemAdvancedOptions>(item.advancedOptions),
options: superjson.parse<Record<string, unknown>>(item.options),
}),
),
};
};
const forKind = <T extends WidgetKind>(kind: T) =>
z.object({
kind: z.literal(kind),
options: z.record(z.string(), z.unknown()),
});
const outputItemSchema = zodUnionFromArray(widgetKinds.map((kind) => forKind(kind))).and(sharedItemSchema);
const parseItem = (item: unknown) => {
const result = outputItemSchema.safeParse(item);
if (!result.success) {
throw new Error(result.error.message);
}
return result.data;
};
const parseSection = (section: unknown) => {
const result = sectionSchema.safeParse(section);
if (!result.success) {
throw new Error(result.error.message);
}
return result.data;
};
@@ -56,8 +56,7 @@ export const generateResponsiveGridFor = ({
dynamicSectionHeightMap.set(dynamicSection.id, result.height); dynamicSectionHeightMap.set(dynamicSection.id, result.height);
} }
// Return same positions for items in the current section if (width === previousWidth) {
if (width >= previousWidth) {
return { return {
height: Math.max(...itemsOfCurrentSection.map((item) => item.yOffset + item.height)), height: Math.max(...itemsOfCurrentSection.map((item) => item.yOffset + item.height)),
items: newItems.concat(normalizedItems), items: newItems.concat(normalizedItems),
@@ -99,11 +99,12 @@ qlllmmmmmnnrrrst
ulllmmmmmnnrrrvw ulllmmmmmnnrrrvw
xyz äö`; xyz äö`;
// Just add two empty columns to the right const eighteenColumns = `
const eighteenColumns = sixteenColumns abbccccddddeeefghi
.split("\n") jbbccccddddeeeklll
.map((line, index) => (index === 0 ? line : `${line} `)) mmmmmnnopqseeetlll
.join("\n"); mmmmmnnrrruvwxylll
mmmmmnnrrrzäö `;
const tenColumns = ` const tenColumns = `
abbcccceee abbcccceee
+8 -1
View File
@@ -7,6 +7,7 @@ import { usePathname } from "next/navigation";
import type { RouterOutputs } from "@homarr/api"; import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useEditMode } from "./edit-mode";
import { updateBoardName } from "./updater"; import { updateBoardName } from "./updater";
const BoardContext = createContext<{ const BoardContext = createContext<{
@@ -83,10 +84,12 @@ export const getCurrentLayout = (board: RouterOutputs["board"]["getBoardByName"]
export const useCurrentLayout = () => { export const useCurrentLayout = () => {
const board = useRequiredBoard(); const board = useRequiredBoard();
const [currentLayout, setCurrentLayout] = useState(getCurrentLayout(board)); const [currentLayout, setCurrentLayout] = useState(getCurrentLayout(board));
const [isEditMode] = useEditMode();
const onResize = useCallback(() => { const onResize = useCallback(() => {
if (isEditMode && board.layoutMode === "auto") return;
setCurrentLayout(getCurrentLayout(board)); setCurrentLayout(getCurrentLayout(board));
}, [board]); }, [board, isEditMode]);
useEffect(() => { useEffect(() => {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
@@ -97,6 +100,10 @@ export const useCurrentLayout = () => {
}; };
}, [onResize]); }, [onResize]);
if (board.layoutMode === "auto" && isEditMode && board.baseLayoutId !== null) {
return board.baseLayoutId;
}
return currentLayout; return currentLayout;
}; };
+1 -1
View File
@@ -38,7 +38,7 @@
"react-dom": "19.2.0", "react-dom": "19.2.0",
"undici": "7.16.0", "undici": "7.16.0",
"zod": "^4.1.12", "zod": "^4.1.12",
"zod-validation-error": "^4.0.2" "zod-validation-error": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
+2 -2
View File
@@ -29,7 +29,7 @@
"@homarr/core": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0", "@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"@tanstack/react-query": "^5.90.9", "@tanstack/react-query": "^5.90.10",
"@trpc/client": "^11.7.1", "@trpc/client": "^11.7.1",
"@trpc/server": "^11.7.1", "@trpc/server": "^11.7.1",
"@trpc/tanstack-react-query": "^11.7.1", "@trpc/tanstack-react-query": "^11.7.1",
@@ -42,7 +42,7 @@
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"@types/react": "19.2.5", "@types/react": "19.2.6",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
@@ -0,0 +1,3 @@
ALTER TABLE `board` ADD `layout_mode` varchar(32) DEFAULT 'custom' NOT NULL;--> statement-breakpoint
ALTER TABLE `board` ADD `base_layout_id` varchar(64);--> statement-breakpoint
ALTER TABLE `board` ADD CONSTRAINT `board_base_layout_id_layout_id_fk` FOREIGN KEY (`base_layout_id`) REFERENCES `layout`(`id`) ON DELETE set null ON UPDATE no action;
File diff suppressed because it is too large. Load diff
@@ -260,6 +260,13 @@
"when": 1760968518445, "when": 1760968518445,
"tag": "0036_add_app_reference_to_integration", "tag": "0036_add_app_reference_to_integration",
"breakpoints": true "breakpoints": true
},
{
"idx": 37,
"version": "5",
"when": 1764190331048,
"tag": "0037_add_auto_layout_mode",
"breakpoints": true
} }
] ]
} }
@@ -0,0 +1,3 @@
ALTER TABLE "board" ADD COLUMN "layout_mode" varchar(32) DEFAULT 'custom' NOT NULL;--> statement-breakpoint
ALTER TABLE "board" ADD COLUMN "base_layout_id" varchar(64);--> statement-breakpoint
ALTER TABLE "board" ADD CONSTRAINT "board_base_layout_id_layout_id_fk" FOREIGN KEY ("base_layout_id") REFERENCES "public"."layout"("id") ON DELETE set null ON UPDATE no action;
File diff suppressed because it is too large. Load diff
@@ -22,6 +22,13 @@
"when": 1760968530084, "when": 1760968530084,
"tag": "0002_add_app_reference_to_integration", "tag": "0002_add_app_reference_to_integration",
"breakpoints": true "breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1764190350662,
"tag": "0003_add_auto_layout_mode",
"breakpoints": true
} }
] ]
} }
@@ -0,0 +1,2 @@
ALTER TABLE `board` ADD `layout_mode` text DEFAULT 'custom' NOT NULL;--> statement-breakpoint
ALTER TABLE `board` ADD `base_layout_id` text REFERENCES layout(id);
File diff suppressed because it is too large. Load diff
@@ -246,6 +246,13 @@
"when": 1760968503571, "when": 1760968503571,
"tag": "0034_add_app_reference_to_integration", "tag": "0034_add_app_reference_to_integration",
"breakpoints": true "breakpoints": true
},
{
"idx": 35,
"version": "6",
"when": 1764190313293,
"tag": "0035_add_auto_layout_mode",
"breakpoints": true
} }
] ]
} }
+16 -6
View File
@@ -17,16 +17,11 @@ import {
varchar, varchar,
} from "drizzle-orm/mysql-core"; } from "drizzle-orm/mysql-core";
import {
backgroundImageAttachments,
backgroundImageRepeats,
backgroundImageSizes,
emptySuperJSON,
} from "@homarr/definitions";
import type { import type {
BackgroundImageAttachment, BackgroundImageAttachment,
BackgroundImageRepeat, BackgroundImageRepeat,
BackgroundImageSize, BackgroundImageSize,
BoardLayoutMode,
BoardPermission, BoardPermission,
ColorScheme, ColorScheme,
GroupPermissionKey, GroupPermissionKey,
@@ -39,6 +34,12 @@ import type {
SupportedAuthProvider, SupportedAuthProvider,
WidgetKind, WidgetKind,
} from "@homarr/definitions"; } from "@homarr/definitions";
import {
backgroundImageAttachments,
backgroundImageRepeats,
backgroundImageSizes,
emptySuperJSON,
} from "@homarr/definitions";
const customBlob = customType<{ data: Buffer }>({ const customBlob = customType<{ data: Buffer }>({
dataType() { dataType() {
@@ -289,6 +290,11 @@ export const boards = mysqlTable("board", {
iconColor: text(), iconColor: text(),
itemRadius: text().$type<MantineSize>().default("lg").notNull(), itemRadius: text().$type<MantineSize>().default("lg").notNull(),
disableStatus: boolean().default(false).notNull(), disableStatus: boolean().default(false).notNull(),
// This has to be custom as the previous behaviour was custom
layoutMode: varchar({ length: 32 }).$type<BoardLayoutMode>().default("custom").notNull(),
baseLayoutId: varchar({ length: 64 }).references((): AnyMySqlColumn => layouts.id, {
onDelete: "set null",
}),
}); });
export const boardUserPermissions = mysqlTable( export const boardUserPermissions = mysqlTable(
@@ -678,6 +684,10 @@ export const boardRelations = relations(boards, ({ many, one }) => ({
userPermissions: many(boardUserPermissions), userPermissions: many(boardUserPermissions),
groupPermissions: many(boardGroupPermissions), groupPermissions: many(boardGroupPermissions),
layouts: many(layouts), layouts: many(layouts),
baseLayout: one(layouts, {
fields: [boards.baseLayoutId],
references: [layouts.id],
}),
groupHomes: many(groups, { groupHomes: many(groups, {
relationName: "groupRelations__board__homeBoardId", relationName: "groupRelations__board__homeBoardId",
}), }),
+16 -6
View File
@@ -16,16 +16,11 @@ import {
varchar, varchar,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import {
backgroundImageAttachments,
backgroundImageRepeats,
backgroundImageSizes,
emptySuperJSON,
} from "@homarr/definitions";
import type { import type {
BackgroundImageAttachment, BackgroundImageAttachment,
BackgroundImageRepeat, BackgroundImageRepeat,
BackgroundImageSize, BackgroundImageSize,
BoardLayoutMode,
BoardPermission, BoardPermission,
ColorScheme, ColorScheme,
GroupPermissionKey, GroupPermissionKey,
@@ -38,6 +33,12 @@ import type {
SupportedAuthProvider, SupportedAuthProvider,
WidgetKind, WidgetKind,
} from "@homarr/definitions"; } from "@homarr/definitions";
import {
backgroundImageAttachments,
backgroundImageRepeats,
backgroundImageSizes,
emptySuperJSON,
} from "@homarr/definitions";
const customBlob = customType<{ data: Buffer }>({ const customBlob = customType<{ data: Buffer }>({
dataType() { dataType() {
@@ -288,6 +289,11 @@ export const boards = pgTable("board", {
iconColor: text(), iconColor: text(),
itemRadius: text().$type<MantineSize>().default("lg").notNull(), itemRadius: text().$type<MantineSize>().default("lg").notNull(),
disableStatus: boolean().default(false).notNull(), disableStatus: boolean().default(false).notNull(),
// This has to be custom as the previous behaviour was custom
layoutMode: varchar({ length: 32 }).$type<BoardLayoutMode>().default("custom").notNull(),
baseLayoutId: varchar({ length: 64 }).references((): AnyPgColumn => layouts.id, {
onDelete: "set null",
}),
}); });
export const boardUserPermissions = pgTable( export const boardUserPermissions = pgTable(
@@ -677,6 +683,10 @@ export const boardRelations = relations(boards, ({ many, one }) => ({
userPermissions: many(boardUserPermissions), userPermissions: many(boardUserPermissions),
groupPermissions: many(boardGroupPermissions), groupPermissions: many(boardGroupPermissions),
layouts: many(layouts), layouts: many(layouts),
baseLayout: one(layouts, {
fields: [boards.baseLayoutId],
references: [layouts.id],
}),
groupHomes: many(groups, { groupHomes: many(groups, {
relationName: "groupRelations__board__homeBoardId", relationName: "groupRelations__board__homeBoardId",
}), }),
+16 -6
View File
@@ -5,16 +5,11 @@ import { relations, sql } from "drizzle-orm";
import type { AnySQLiteColumn } from "drizzle-orm/sqlite-core"; import type { AnySQLiteColumn } from "drizzle-orm/sqlite-core";
import { blob, index, int, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core"; import { blob, index, int, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
import {
backgroundImageAttachments,
backgroundImageRepeats,
backgroundImageSizes,
emptySuperJSON,
} from "@homarr/definitions";
import type { import type {
BackgroundImageAttachment, BackgroundImageAttachment,
BackgroundImageRepeat, BackgroundImageRepeat,
BackgroundImageSize, BackgroundImageSize,
BoardLayoutMode,
BoardPermission, BoardPermission,
ColorScheme, ColorScheme,
GroupPermissionKey, GroupPermissionKey,
@@ -27,6 +22,12 @@ import type {
SupportedAuthProvider, SupportedAuthProvider,
WidgetKind, WidgetKind,
} from "@homarr/definitions"; } from "@homarr/definitions";
import {
backgroundImageAttachments,
backgroundImageRepeats,
backgroundImageSizes,
emptySuperJSON,
} from "@homarr/definitions";
export const apiKeys = sqliteTable("apiKey", { export const apiKeys = sqliteTable("apiKey", {
id: text().notNull().primaryKey(), id: text().notNull().primaryKey(),
@@ -274,6 +275,11 @@ export const boards = sqliteTable("board", {
iconColor: text(), iconColor: text(),
itemRadius: text().$type<MantineSize>().default("lg").notNull(), itemRadius: text().$type<MantineSize>().default("lg").notNull(),
disableStatus: int({ mode: "boolean" }).default(false).notNull(), disableStatus: int({ mode: "boolean" }).default(false).notNull(),
// This has to be custom as the previous behaviour was custom
layoutMode: text().$type<BoardLayoutMode>().default("custom").notNull(),
baseLayoutId: text().references((): AnySQLiteColumn => layouts.id, {
onDelete: "set null",
}),
}); });
export const boardUserPermissions = sqliteTable( export const boardUserPermissions = sqliteTable(
@@ -663,6 +669,10 @@ export const boardRelations = relations(boards, ({ many, one }) => ({
userPermissions: many(boardUserPermissions), userPermissions: many(boardUserPermissions),
groupPermissions: many(boardGroupPermissions), groupPermissions: many(boardGroupPermissions),
layouts: many(layouts), layouts: many(layouts),
baseLayout: one(layouts, {
fields: [boards.baseLayoutId],
references: [layouts.id],
}),
groupHomes: many(groups, { groupHomes: many(groups, {
relationName: "groupRelations__board__homeBoardId", relationName: "groupRelations__board__homeBoardId",
}), }),
+2
View File
@@ -8,7 +8,9 @@ export const backgroundImageRepeats = createDefinition(["repeat", "repeat-x", "r
export const backgroundImageSizes = createDefinition(["cover", "contain"], { export const backgroundImageSizes = createDefinition(["cover", "contain"], {
defaultValue: "cover", defaultValue: "cover",
}); });
export const boardLayoutModes = createDefinition(["auto", "custom"], { defaultValue: "auto" });
export type BackgroundImageAttachment = inferDefinitionType<typeof backgroundImageAttachments>; export type BackgroundImageAttachment = inferDefinitionType<typeof backgroundImageAttachments>;
export type BackgroundImageRepeat = inferDefinitionType<typeof backgroundImageRepeats>; export type BackgroundImageRepeat = inferDefinitionType<typeof backgroundImageRepeats>;
export type BackgroundImageSize = inferDefinitionType<typeof backgroundImageSizes>; export type BackgroundImageSize = inferDefinitionType<typeof backgroundImageSizes>;
export type BoardLayoutMode = inferDefinitionType<typeof boardLayoutModes>;
+1 -1
View File
@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "^3.3.46", "@types/dockerode": "^3.3.47",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
+1
View File
@@ -8,3 +8,4 @@ export type { Docker };
export const containerStates = ["created", "running", "paused", "restarting", "exited", "removing", "dead"] as const; export const containerStates = ["created", "running", "paused", "restarting", "exited", "removing", "dead"] as const;
export type ContainerState = (typeof containerStates)[number]; export type ContainerState = (typeof containerStates)[number];
export * from "./labels";
+4
View File
@@ -0,0 +1,4 @@
export const dockerLabels = {
// Label to hide a container from Homarrs docker integration
hide: "homarr.hide",
} as const;
+1
View File
@@ -24,6 +24,7 @@
"dependencies": { "dependencies": {
"@homarr/api": "workspace:^0.1.0", "@homarr/api": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/form": "workspace:^0.1.0", "@homarr/form": "workspace:^0.1.0",
"@homarr/forms-collection": "workspace:^0.1.0", "@homarr/forms-collection": "workspace:^0.1.0",
"@homarr/modals": "workspace:^0.1.0", "@homarr/modals": "workspace:^0.1.0",
@@ -1,13 +1,28 @@
import { Button, Group, InputWrapper, Slider, Stack, Switch, TextInput } from "@mantine/core"; import {
Badge,
Box,
Button,
Fieldset,
Group,
InputWrapper,
RadioGroup,
SimpleGrid,
Slider,
Stack,
Switch,
TextInput,
} from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks"; import { useDebouncedValue } from "@mantine/hooks";
import { IconAlertTriangle, IconCircleCheck } from "@tabler/icons-react"; import { IconAlertTriangle, IconCircleCheck } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client"; import { revalidatePathActionAsync } from "@homarr/common/client";
import { boardLayoutModes } from "@homarr/definitions";
import { useZodForm } from "@homarr/form"; import { useZodForm } from "@homarr/form";
import { createModal } from "@homarr/modals"; import { createModal } from "@homarr/modals";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client"; import { useI18n } from "@homarr/translation/client";
import { CustomRadioCard } from "@homarr/ui";
import { boardColumnCountSchema, boardCreateSchema, boardNameSchema } from "@homarr/validation/board"; import { boardColumnCountSchema, boardCreateSchema, boardNameSchema } from "@homarr/validation/board";
export const AddBoardModal = createModal(({ actions }) => { export const AddBoardModal = createModal(({ actions }) => {
@@ -18,6 +33,7 @@ export const AddBoardModal = createModal(({ actions }) => {
name: "", name: "",
columnCount: 10, columnCount: 10,
isPublic: false, isPublic: false,
layoutMode: boardLayoutModes.defaultValue,
}, },
}); });
const { mutate, isPending } = clientApi.board.createBoard.useMutation({ const { mutate, isPending } = clientApi.board.createBoard.useMutation({
@@ -64,14 +80,38 @@ export const AddBoardModal = createModal(({ actions }) => {
) : null ) : null
} }
/> />
<InputWrapper label={t("board.field.columnCount.label")} {...form.getInputProps("columnCount")}>
<Slider <Fieldset legend={t("board.setting.section.layout.title")}>
min={boardColumnCountSchema.minValue ?? undefined} <Stack gap="sm">
max={boardColumnCountSchema.maxValue ?? undefined} <RadioGroup {...form.getInputProps("layoutMode")}>
step={1} <SimpleGrid cols={{ sm: 2, xs: 1 }} spacing="md">
{...form.getInputProps("columnCount")} {boardLayoutModes.values.map((mode) => (
/> <Box w="100%" key={mode} pos="relative">
</InputWrapper> <CustomRadioCard
value={mode}
label={t(`board.field.layoutMode.option.${mode}.label`)}
description={t(`board.field.layoutMode.option.${mode}.description`)}
/>
{mode === boardLayoutModes.defaultValue && (
<Badge pos="absolute" top="8px" right="8px" variant="light" size="sm">
{t("common.select.badge.recommended")}
</Badge>
)}
</Box>
))}
</SimpleGrid>
</RadioGroup>
<InputWrapper label={t("board.field.columnCount.label")} {...form.getInputProps("columnCount")}>
<Slider
min={boardColumnCountSchema.minValue ?? undefined}
max={boardColumnCountSchema.maxValue ?? undefined}
step={1}
{...form.getInputProps("columnCount")}
/>
</InputWrapper>
</Stack>
</Fieldset>
<Switch <Switch
label={t("board.field.isPublic.label")} label={t("board.field.isPublic.label")}
@@ -92,6 +132,7 @@ export const AddBoardModal = createModal(({ actions }) => {
); );
}).withOptions({ }).withOptions({
defaultTitle: (t) => t("management.page.board.action.new.label"), defaultTitle: (t) => t("management.page.board.action.new.label"),
size: "lg",
}); });
export const useBoardNameStatus = (name: string) => { export const useBoardNameStatus = (name: string) => {
+1
View File
@@ -26,6 +26,7 @@
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0",
"@homarr/docker": "workspace:^0.1.0",
"@homarr/integrations": "workspace:^0.1.0", "@homarr/integrations": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"@homarr/redis": "workspace:^0.1.0", "@homarr/redis": "workspace:^0.1.0",
+5 -7
View File
@@ -3,9 +3,9 @@ import type { ContainerInfo, ContainerStats } from "dockerode";
import { db, like, or } from "@homarr/db"; import { db, like, or } from "@homarr/db";
import { icons } from "@homarr/db/schema"; import { icons } from "@homarr/db/schema";
import type { ContainerState } from "@homarr/docker";
import { dockerLabels, DockerSingleton } from "@homarr/docker";
import type { ContainerState } from "../../docker/src";
import { DockerSingleton } from "../../docker/src";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler"; import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
export const dockerContainersRequestHandler = createCachedWidgetRequestHandler({ export const dockerContainersRequestHandler = createCachedWidgetRequestHandler({
@@ -27,13 +27,11 @@ async function getContainersWithStatsAsync() {
const containers = await Promise.all( const containers = await Promise.all(
dockerInstances.map(async ({ instance, host }) => { dockerInstances.map(async ({ instance, host }) => {
const instanceContainers = await instance.listContainers({ all: true }); const instanceContainers = await instance.listContainers({ all: true });
return instanceContainers.map((container) => ({ return instanceContainers
...container, .filter((container) => dockerLabels.hide in container.Labels === false)
instance: host, .map((container) => ({ ...container, instance: host }));
}));
}), }),
).then((res) => res.flat()); ).then((res) => res.flat());
const likeQueries = containers.map((container) => like(icons.name, `%${extractImage(container)}%`)); const likeQueries = containers.map((container) => like(icons.name, `%${extractImage(container)}%`));
const dbIcons = const dbIcons =
+1 -1
View File
@@ -33,7 +33,7 @@
"deepmerge": "4.3.1", "deepmerge": "4.3.1",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "16.0.3", "next": "16.0.3",
"next-intl": "4.5.3", "next-intl": "4.5.5",
"react": "19.2.0", "react": "19.2.0",
"react-dom": "19.2.0" "react-dom": "19.2.0"
}, },
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "", "unknown": "",
"pending": "", "pending": "",
"processing": "", "processing": "",
"requested": "",
"partiallyAvailable": "", "partiallyAvailable": "",
"available": "", "available": "",
"blacklisted": "", "blacklisted": "",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "未知", "unknown": "未知",
"pending": "等待处理", "pending": "等待处理",
"processing": "处理中", "processing": "处理中",
"requested": "",
"partiallyAvailable": "部分", "partiallyAvailable": "部分",
"available": "可用", "available": "可用",
"blacklisted": "已列入黑名单", "blacklisted": "已列入黑名单",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "crwdns1314:0crwdne1314:0", "unknown": "crwdns1314:0crwdne1314:0",
"pending": "crwdns1316:0crwdne1316:0", "pending": "crwdns1316:0crwdne1316:0",
"processing": "crwdns1318:0crwdne1318:0", "processing": "crwdns1318:0crwdne1318:0",
"requested": "crwdns3806:0crwdne3806:0",
"partiallyAvailable": "crwdns1320:0crwdne1320:0", "partiallyAvailable": "crwdns1320:0crwdne1320:0",
"available": "crwdns1322:0crwdne1322:0", "available": "crwdns1322:0crwdne1322:0",
"blacklisted": "crwdns3468:0crwdne3468:0", "blacklisted": "crwdns3468:0crwdne3468:0",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Neznámý", "unknown": "Neznámý",
"pending": "", "pending": "",
"processing": "", "processing": "",
"requested": "",
"partiallyAvailable": "Částečně dostupné", "partiallyAvailable": "Částečně dostupné",
"available": "K dispozici", "available": "K dispozici",
"blacklisted": "", "blacklisted": "",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Ukendt", "unknown": "Ukendt",
"pending": "Afventende", "pending": "Afventende",
"processing": "Behandler", "processing": "Behandler",
"requested": "",
"partiallyAvailable": "Delvis", "partiallyAvailable": "Delvis",
"available": "Tilgængelig", "available": "Tilgængelig",
"blacklisted": "Blacklistet", "blacklisted": "Blacklistet",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Unbekannt", "unknown": "Unbekannt",
"pending": "Ausstehend", "pending": "Ausstehend",
"processing": "In Bearbeitung", "processing": "In Bearbeitung",
"requested": "",
"partiallyAvailable": "Teilweise", "partiallyAvailable": "Teilweise",
"available": "Verfügbar", "available": "Verfügbar",
"blacklisted": "", "blacklisted": "",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Unbekannt", "unknown": "Unbekannt",
"pending": "Ausstehend", "pending": "Ausstehend",
"processing": "In Bearbeitung", "processing": "In Bearbeitung",
"requested": "",
"partiallyAvailable": "Teilweise", "partiallyAvailable": "Teilweise",
"available": "Verfügbar", "available": "Verfügbar",
"blacklisted": "Gesperrt", "blacklisted": "Gesperrt",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Άγνωστο", "unknown": "Άγνωστο",
"pending": "", "pending": "",
"processing": "", "processing": "",
"requested": "",
"partiallyAvailable": "Μερικώς", "partiallyAvailable": "Μερικώς",
"available": "Διαθέσιμο", "available": "Διαθέσιμο",
"blacklisted": "", "blacklisted": "",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "", "unknown": "",
"pending": "", "pending": "",
"processing": "", "processing": "",
"requested": "",
"partiallyAvailable": "", "partiallyAvailable": "",
"available": "", "available": "",
"blacklisted": "", "blacklisted": "",
+17
View File
@@ -2793,6 +2793,23 @@
"label": "Disable app status", "label": "Disable app status",
"description": "Disables the status check for all apps on this board" "description": "Disables the status check for all apps on this board"
}, },
"layoutMode": {
"label": "Layout mode",
"option": {
"auto": {
"label": "Auto",
"description": "Position and size of all layouts are calculated from the base layout."
},
"custom": {
"label": "Custom",
"description": "Position and size must be configured for all layouts separately."
}
}
},
"baseLayout": {
"label": "Base layout",
"description": "The layout will be used to calculate positions and sizes for other layouts."
},
"columnCount": { "columnCount": {
"label": "Column count" "label": "Column count"
}, },
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Desconocido", "unknown": "Desconocido",
"pending": "Pendiente", "pending": "Pendiente",
"processing": "Procesando", "processing": "Procesando",
"requested": "",
"partiallyAvailable": "Parcial", "partiallyAvailable": "Parcial",
"available": "Disponible", "available": "Disponible",
"blacklisted": "Lista negra", "blacklisted": "Lista negra",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "", "unknown": "",
"pending": "", "pending": "",
"processing": "", "processing": "",
"requested": "",
"partiallyAvailable": "", "partiallyAvailable": "",
"available": "", "available": "",
"blacklisted": "", "blacklisted": "",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Inconnu", "unknown": "Inconnu",
"pending": "En attente", "pending": "En attente",
"processing": "Traitement en cours", "processing": "Traitement en cours",
"requested": "",
"partiallyAvailable": "Partiel", "partiallyAvailable": "Partiel",
"available": "Disponible", "available": "Disponible",
"blacklisted": "Sur la liste noire", "blacklisted": "Sur la liste noire",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "לא ידוע", "unknown": "לא ידוע",
"pending": "בהמתנה", "pending": "בהמתנה",
"processing": "מעבד", "processing": "מעבד",
"requested": "",
"partiallyAvailable": "חלקי", "partiallyAvailable": "חלקי",
"available": "זמין", "available": "זמין",
"blacklisted": "ברשימה השחורה", "blacklisted": "ברשימה השחורה",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Nepoznato", "unknown": "Nepoznato",
"pending": "", "pending": "",
"processing": "", "processing": "",
"requested": "",
"partiallyAvailable": "", "partiallyAvailable": "",
"available": "", "available": "",
"blacklisted": "", "blacklisted": "",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Ismeretlen", "unknown": "Ismeretlen",
"pending": "", "pending": "",
"processing": "Feldolgozás", "processing": "Feldolgozás",
"requested": "",
"partiallyAvailable": "Részleges", "partiallyAvailable": "Részleges",
"available": "Elérhető", "available": "Elérhető",
"blacklisted": "", "blacklisted": "",
+65 -64
View File
@@ -730,7 +730,7 @@
"error": { "error": {
"common": { "common": {
"cause": { "cause": {
"title": "" "title": "Causa con più dettagli"
} }
}, },
"unknown": { "unknown": {
@@ -738,7 +738,7 @@
"description": "Si è verificato un errore sconosciuto, apri la causa sottostante per vedere maggiori dettagli" "description": "Si è verificato un errore sconosciuto, apri la causa sottostante per vedere maggiori dettagli"
}, },
"parse": { "parse": {
"title": "", "title": "Errore di analisi",
"description": "La risposta non può essere analizzata. Si prega di verificare che l'URL stia puntando all'URL di base del servizio." "description": "La risposta non può essere analizzata. Si prega di verificare che l'URL stia puntando all'URL di base del servizio."
}, },
"authorization": { "authorization": {
@@ -959,8 +959,8 @@
"newLabel": "Nuovo ID token" "newLabel": "Nuovo ID token"
}, },
"realm": { "realm": {
"label": "", "label": "Dominio",
"newLabel": "" "newLabel": "Nuovo dominio"
}, },
"personalAccessToken": { "personalAccessToken": {
"label": "Token d'Accesso Personale", "label": "Token d'Accesso Personale",
@@ -1053,7 +1053,7 @@
"backToOverview": "Torna alla panoramica", "backToOverview": "Torna alla panoramica",
"change": "Cambia", "change": "Cambia",
"create": "Crea", "create": "Crea",
"createAnother": "", "createAnother": "Crea e ricomincia",
"edit": "Modifica", "edit": "Modifica",
"import": "Importa", "import": "Importa",
"insert": "Inserisci", "insert": "Inserisci",
@@ -1389,8 +1389,8 @@
} }
}, },
"dnsHoleSummary": { "dnsHoleSummary": {
"name": "", "name": "Riepilogo del filtraggio DNS",
"description": "", "description": "Mostra il riepilogo del tuo filtro DNS",
"option": { "option": {
"layout": { "layout": {
"label": "Layout", "label": "Layout",
@@ -1411,7 +1411,7 @@
} }
}, },
"error": { "error": {
"internalServerError": "", "internalServerError": "Impossibile recuperare il riepilogo del filtro DNS",
"integrationsDisconnected": "Nessun dato disponibile, tutte le integrazioni disconnesse" "integrationsDisconnected": "Nessun dato disponibile, tutte le integrazioni disconnesse"
}, },
"data": { "data": {
@@ -1423,7 +1423,7 @@
"domainsTooltip": "A causa di più integrazioni, Homarr non può calcolare il numero esatto di domini bloccati" "domainsTooltip": "A causa di più integrazioni, Homarr non può calcolare il numero esatto di domini bloccati"
}, },
"dnsHoleControls": { "dnsHoleControls": {
"name": "", "name": "Controlli del filtro DNS",
"description": "Controlla PiHole o AdGuard dalla tua dashboard", "description": "Controlla PiHole o AdGuard dalla tua dashboard",
"option": { "option": {
"layout": { "layout": {
@@ -1445,7 +1445,7 @@
} }
}, },
"error": { "error": {
"internalServerError": "" "internalServerError": "Impossibile controllare il DNS Hole"
}, },
"controls": { "controls": {
"enableAll": "Abilita tutto", "enableAll": "Abilita tutto",
@@ -1514,7 +1514,7 @@
"label": "Indirizzo del server" "label": "Indirizzo del server"
}, },
"isBedrockServer": { "isBedrockServer": {
"label": "" "label": "Server Bedrock"
} }
}, },
"status": { "status": {
@@ -1625,7 +1625,7 @@
} }
}, },
"error": { "error": {
"noUrl": "", "noUrl": "Nessun URL iFrame fornito",
"unsupportedProtocol": "L'URL fornito sta usando un protocollo non supportato. Si prega di utilizzare uno di ({supportedProtocols})", "unsupportedProtocol": "L'URL fornito sta usando un protocollo non supportato. Si prega di utilizzare uno di ({supportedProtocols})",
"noBrowerSupport": "Il tuo browser non supporta iframes. Aggiorna il tuo browser." "noBrowerSupport": "Il tuo browser non supporta iframes. Aggiorna il tuo browser."
} }
@@ -1664,7 +1664,7 @@
} }
}, },
"stockPrice": { "stockPrice": {
"name": "", "name": "Prezzo delle azioni",
"description": "Visualizza il prezzo attuale delle azioni di una società", "description": "Visualizza il prezzo attuale delle azioni di una società",
"option": { "option": {
"stock": { "stock": {
@@ -1835,7 +1835,7 @@
"title": "Index manager", "title": "Index manager",
"testAll": "Prova tutto", "testAll": "Prova tutto",
"error": { "error": {
"internalServerError": "" "internalServerError": "Impossibile recuperare lo stato degli indicizzatori"
} }
}, },
"healthMonitoring": { "healthMonitoring": {
@@ -2020,7 +2020,7 @@
"name": "Nome", "name": "Nome",
"id": "Id", "id": "Id",
"metadata": { "metadata": {
"title": "", "title": "Statistiche per nerd",
"video": { "video": {
"title": "Video", "title": "Video",
"resolution": "Risoluzione" "resolution": "Risoluzione"
@@ -2034,7 +2034,7 @@
"title": "Transcodifica", "title": "Transcodifica",
"container": "Container", "container": "Container",
"resolution": "Risoluzione", "resolution": "Risoluzione",
"target": "" "target": "Codec di destinazione"
} }
} }
} }
@@ -2098,7 +2098,7 @@
"detailsTitle": "Categorie (Informazioni aggiuntive)" "detailsTitle": "Categorie (Informazioni aggiuntive)"
}, },
"downSpeed": { "downSpeed": {
"columnTitle": "", "columnTitle": "Giù",
"detailsTitle": "Velocità Di Download" "detailsTitle": "Velocità Di Download"
}, },
"index": { "index": {
@@ -2112,22 +2112,22 @@
"columnTitle": "Integrazione" "columnTitle": "Integrazione"
}, },
"name": { "name": {
"columnTitle": "" "columnTitle": "Nome del job"
}, },
"progress": { "progress": {
"columnTitle": "Avanzamento", "columnTitle": "Avanzamento",
"detailsTitle": "" "detailsTitle": "Avanzamento del download"
}, },
"ratio": { "ratio": {
"columnTitle": "Rapporto", "columnTitle": "Rapporto",
"detailsTitle": "Rapporto torrent (ricevuti/inviati)" "detailsTitle": "Rapporto torrent (ricevuti/inviati)"
}, },
"received": { "received": {
"columnTitle": "", "columnTitle": "Totale fuori servizio",
"detailsTitle": "Totale scaricati" "detailsTitle": "Totale scaricati"
}, },
"sent": { "sent": {
"columnTitle": "", "columnTitle": "Totale operativo",
"detailsTitle": "Totale Caricati" "detailsTitle": "Totale Caricati"
}, },
"size": { "size": {
@@ -2136,7 +2136,7 @@
}, },
"state": { "state": {
"columnTitle": "Stato", "columnTitle": "Stato",
"detailsTitle": "" "detailsTitle": "Stato job"
}, },
"time": { "time": {
"columnTitle": "Ora di fine", "columnTitle": "Ora di fine",
@@ -2158,7 +2158,7 @@
"completed": "Completato", "completed": "Completato",
"failed": "Non riuscito", "failed": "Non riuscito",
"processing": "In lavorazione", "processing": "In lavorazione",
"leeching": "", "leeching": "Leeching",
"stalled": "In stallo", "stalled": "In stallo",
"unknown": "Sconosciuto", "unknown": "Sconosciuto",
"seeding": "Seeding" "seeding": "Seeding"
@@ -2205,7 +2205,7 @@
"label": "Mostra suggerimento descrizione" "label": "Mostra suggerimento descrizione"
}, },
"showType": { "showType": {
"label": "" "label": "Mostra il badge del tipo di media"
}, },
"showSource": { "showSource": {
"label": "Mostra sorgente integrazione" "label": "Mostra sorgente integrazione"
@@ -2216,7 +2216,7 @@
} }
}, },
"mediaRequests-requestList": { "mediaRequests-requestList": {
"name": "", "name": "Elenco Richieste Media",
"description": "Visualizza un elenco di tutte le richieste media dalla tua istanza Overseerr o Jellyseerr", "description": "Visualizza un elenco di tutte le richieste media dalla tua istanza Overseerr o Jellyseerr",
"option": { "option": {
"linksTargetNewTab": { "linksTargetNewTab": {
@@ -2232,9 +2232,10 @@
"unknown": "Sconosciuto", "unknown": "Sconosciuto",
"pending": "In sospeso", "pending": "In sospeso",
"processing": "In lavorazione", "processing": "In lavorazione",
"requested": "Richiesto",
"partiallyAvailable": "Parziale", "partiallyAvailable": "Parziale",
"available": "Disponibile", "available": "Disponibile",
"blacklisted": "", "blacklisted": "In blacklist",
"deleted": "Eliminato" "deleted": "Eliminato"
}, },
"status": { "status": {
@@ -2247,7 +2248,7 @@
"toBeDetermined": "TBD" "toBeDetermined": "TBD"
}, },
"mediaRequests-requestStats": { "mediaRequests-requestStats": {
"name": "", "name": "Statistiche Richiesta Media",
"description": "Statistiche sulle richieste dei media", "description": "Statistiche sulle richieste dei media",
"option": {}, "option": {},
"titles": { "titles": {
@@ -2338,7 +2339,7 @@
"label": "Abilita RTL" "label": "Abilita RTL"
}, },
"textLinesClamp": { "textLinesClamp": {
"label": "" "label": "Limita le righe della descrizione"
}, },
"maximumAmountPosts": { "maximumAmountPosts": {
"label": "Limite di post" "label": "Limite di post"
@@ -2353,11 +2354,11 @@
"description": "Visualizza un elenco della versione corrente dei repository dati con la versione regex specificata.", "description": "Visualizza un elenco della versione corrente dei repository dati con la versione regex specificata.",
"option": { "option": {
"newReleaseWithin": { "newReleaseWithin": {
"label": "", "label": "Nuova versione entro",
"description": "Esempio di utilizzo: 1w (1 settimana), 10M (10 mesi). Tipi di unità accettati h (ore), d (giorni), w (settimane), M (mesi), y (anni). Lasciare vuoto per non evidenziare le nuove versioni." "description": "Esempio di utilizzo: 1w (1 settimana), 10M (10 mesi). Tipi di unità accettati h (ore), d (giorni), w (settimane), M (mesi), y (anni). Lasciare vuoto per non evidenziare le nuove versioni."
}, },
"staleReleaseWithin": { "staleReleaseWithin": {
"label": "", "label": "Rilascio obsoleto entro",
"description": "Esempio di utilizzo: 1w (1 settimana), 10M (10 mesi). Tipi di unità accettati h (ore), d (giorni), w (settimane), M (mesi), y (anni). Lasciare vuoto per non evidenziare le release obsolete." "description": "Esempio di utilizzo: 1w (1 settimana), 10M (10 mesi). Tipi di unità accettati h (ore), d (giorni), w (settimane), M (mesi), y (anni). Lasciare vuoto per non evidenziare le release obsolete."
}, },
"showOnlyHighlighted": { "showOnlyHighlighted": {
@@ -2702,7 +2703,7 @@
"label": "Titolo della pagina" "label": "Titolo della pagina"
}, },
"metaTitle": { "metaTitle": {
"label": "" "label": "Meta title"
}, },
"logoImageUrl": { "logoImageUrl": {
"label": "URL immagine del logo" "label": "URL immagine del logo"
@@ -2948,7 +2949,7 @@
"label": "Numero di colonne" "label": "Numero di colonne"
}, },
"breakpoint": { "breakpoint": {
"label": "", "label": "Breakpoint",
"description": "Il layout verrà utilizzato su tutti gli schermi più grandi di questo breakpoint fino al breakpoint più grande successivo." "description": "Il layout verrà utilizzato su tutti gli schermi più grandi di questo breakpoint fino al breakpoint più grande successivo."
} }
} }
@@ -3322,7 +3323,7 @@
"label": "Analytics" "label": "Analytics"
}, },
"smartHomeEntityState": { "smartHomeEntityState": {
"label": "" "label": "Stato dell'entità Smart Home"
}, },
"ping": { "ping": {
"label": "Ping" "label": "Ping"
@@ -3337,10 +3338,10 @@
"label": "Scaricamenti" "label": "Scaricamenti"
}, },
"mediaRequestStats": { "mediaRequestStats": {
"label": "" "label": "Statistiche richieste media"
}, },
"mediaRequestList": { "mediaRequestList": {
"label": "" "label": "Elenco richieste media"
}, },
"rssFeeds": { "rssFeeds": {
"label": "Feeds RSS" "label": "Feeds RSS"
@@ -3349,10 +3350,10 @@
"label": "Gestione Indicizzatori" "label": "Gestione Indicizzatori"
}, },
"healthMonitoring": { "healthMonitoring": {
"label": "" "label": "Monitoraggio integrità del sistema"
}, },
"dnsHole": { "dnsHole": {
"label": "" "label": "Dati di filtraggio DNS"
}, },
"updateChecker": { "updateChecker": {
"label": "Controllo aggiornamenti" "label": "Controllo aggiornamenti"
@@ -3435,10 +3436,10 @@
}, },
"trigger": { "trigger": {
"success": { "success": {
"message": "" "message": "Attività attivata con successo"
}, },
"error": { "error": {
"message": "" "message": "Impossibile attivare l'attività"
} }
}, },
"enable": { "enable": {
@@ -3453,7 +3454,7 @@
}, },
"toggle": { "toggle": {
"error": { "error": {
"message": "" "message": "Impossibile attivare lo stato dell'attività"
} }
} }
}, },
@@ -3517,12 +3518,12 @@
"action": "Azione" "action": "Azione"
}, },
"action": { "action": {
"toggleBoardEdit": "", "toggleBoardEdit": "Attiva la modalità di modifica della board",
"toggleColorScheme": "", "toggleColorScheme": "Attiva tema chiaro/scuro",
"saveNotebook": "", "saveNotebook": "Salva notebook (solo nel widget notebook)",
"openSpotlight": "Apri ricerca" "openSpotlight": "Apri ricerca"
}, },
"note": "" "note": "Suggerimento: 'Mod' si riferisce sia al tasto Ctrl che al tasto ⌘ su macOS"
} }
} }
} }
@@ -3534,7 +3535,7 @@
"updated": "Aggiornato {when}", "updated": "Aggiornato {when}",
"search": "Cerca {count} container", "search": "Cerca {count} container",
"selected": "{selectCount} di {totalCount} containers selezionati", "selected": "{selectCount} di {totalCount} containers selezionati",
"footer": "" "footer": "Totale container {count}"
}, },
"field": { "field": {
"name": { "name": {
@@ -3672,11 +3673,11 @@
"resources": { "resources": {
"title": "Risorse", "title": "Risorse",
"nodes": "Nodi", "nodes": "Nodi",
"namespaces": "", "namespaces": "Spazi dei nomi",
"ingresses": "Ingressi", "ingresses": "Ingressi",
"services": "Servizi", "services": "Servizi",
"pods": "Pods", "pods": "Pods",
"configmaps": "", "configmaps": "ConfigMaps",
"secrets": "Secrets", "secrets": "Secrets",
"volumes": "Volumi" "volumes": "Volumi"
} }
@@ -3721,7 +3722,7 @@
} }
}, },
"namespaces": { "namespaces": {
"label": "", "label": "Spazi dei nomi",
"field": { "field": {
"name": { "name": {
"label": "Nome" "label": "Nome"
@@ -3730,7 +3731,7 @@
"label": "Stato", "label": "Stato",
"option": { "option": {
"active": "Attivo", "active": "Attivo",
"terminating": "" "terminating": "Terminare"
} }
}, },
"creationTimestamp": { "creationTimestamp": {
@@ -3738,7 +3739,7 @@
} }
}, },
"table": { "table": {
"search": "" "search": "Cerca {count} spazi dei nomi"
} }
}, },
"ingresses": { "ingresses": {
@@ -3761,7 +3762,7 @@
} }
}, },
"table": { "table": {
"search": "" "search": "Cerca {count} ingresses"
} }
}, },
"services": { "services": {
@@ -3771,7 +3772,7 @@
"label": "Nome" "label": "Nome"
}, },
"namespace": { "namespace": {
"label": "" "label": "Spazio del nome"
}, },
"type": { "type": {
"label": "Tipo" "label": "Tipo"
@@ -3800,7 +3801,7 @@
"label": "Nome" "label": "Nome"
}, },
"namespace": { "namespace": {
"label": "" "label": "Spazio del nome"
}, },
"image": { "image": {
"label": "Immagine" "label": "Immagine"
@@ -3826,7 +3827,7 @@
"label": "Nome" "label": "Nome"
}, },
"namespace": { "namespace": {
"label": "" "label": "spazio del nome"
}, },
"type": { "type": {
"label": "tipo" "label": "tipo"
@@ -3836,24 +3837,24 @@
} }
}, },
"table": { "table": {
"search": "" "search": "Cerca {count} segreti"
} }
}, },
"configmaps": { "configmaps": {
"label": "", "label": "ConfigMaps",
"field": { "field": {
"name": { "name": {
"label": "Nome" "label": "Nome"
}, },
"namespace": { "namespace": {
"label": "" "label": "namespace"
}, },
"creationTimestamp": { "creationTimestamp": {
"label": "Creato" "label": "Creato"
} }
}, },
"table": { "table": {
"search": "" "search": "Cerca {count} ConfigMaps"
} }
}, },
"volumes": { "volumes": {
@@ -3863,7 +3864,7 @@
"label": "Nome" "label": "Nome"
}, },
"namespace": { "namespace": {
"label": "" "label": "Namespace"
}, },
"accessModes": { "accessModes": {
"label": "Modalità Di Accesso" "label": "Modalità Di Accesso"
@@ -3872,7 +3873,7 @@
"label": "Spazio di archiviazione" "label": "Spazio di archiviazione"
}, },
"storageClassName": { "storageClassName": {
"label": "" "label": "Nome Classe Archivio"
}, },
"volumeMode": { "volumeMode": {
"label": "Modalità Volume" "label": "Modalità Volume"
@@ -3888,7 +3889,7 @@
} }
}, },
"table": { "table": {
"search": "" "search": "Cerca {count} volumi"
} }
}, },
"error": { "error": {
@@ -3989,7 +3990,7 @@
"label": "Nodi" "label": "Nodi"
}, },
"namespaces": { "namespaces": {
"label": "" "label": "Namespaces"
}, },
"ingresses": { "ingresses": {
"label": "Ingressi" "label": "Ingressi"
@@ -4001,7 +4002,7 @@
"label": "pods" "label": "pods"
}, },
"configmaps": { "configmaps": {
"label": "" "label": "ConfigMaps"
}, },
"secrets": { "secrets": {
"label": "Secrets" "label": "Secrets"
@@ -4092,7 +4093,7 @@
"option": { "option": {
"colorScheme": { "colorScheme": {
"light": "Passa al tema chiaro", "light": "Passa al tema chiaro",
"dark": "Passa alla modalità scura" "dark": "Passa al tema scuro"
}, },
"language": { "language": {
"label": "Cambia lingua", "label": "Cambia lingua",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "不明", "unknown": "不明",
"pending": "保留中", "pending": "保留中",
"processing": "処理中", "processing": "処理中",
"requested": "",
"partiallyAvailable": "一部", "partiallyAvailable": "一部",
"available": "利用可能", "available": "利用可能",
"blacklisted": "ブラックリスト", "blacklisted": "ブラックリスト",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "알 수 없음", "unknown": "알 수 없음",
"pending": "", "pending": "",
"processing": "", "processing": "",
"requested": "",
"partiallyAvailable": "", "partiallyAvailable": "",
"available": "", "available": "",
"blacklisted": "", "blacklisted": "",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Nežinoma", "unknown": "Nežinoma",
"pending": "", "pending": "",
"processing": "", "processing": "",
"requested": "",
"partiallyAvailable": "Dalis", "partiallyAvailable": "Dalis",
"available": "Galima", "available": "Galima",
"blacklisted": "", "blacklisted": "",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Nezināms", "unknown": "Nezināms",
"pending": "", "pending": "",
"processing": "", "processing": "",
"requested": "",
"partiallyAvailable": "Daļējs", "partiallyAvailable": "Daļējs",
"available": "Pieejams", "available": "Pieejams",
"blacklisted": "", "blacklisted": "",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Onbekend", "unknown": "Onbekend",
"pending": "In afwachting", "pending": "In afwachting",
"processing": "Verwerken", "processing": "Verwerken",
"requested": "",
"partiallyAvailable": "Gedeeltelijk", "partiallyAvailable": "Gedeeltelijk",
"available": "Beschikbaar", "available": "Beschikbaar",
"blacklisted": "Geblacklist", "blacklisted": "Geblacklist",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Ukjent", "unknown": "Ukjent",
"pending": "Pågår", "pending": "Pågår",
"processing": "Prosesserer", "processing": "Prosesserer",
"requested": "",
"partiallyAvailable": "Delvis", "partiallyAvailable": "Delvis",
"available": "Tilgjengelig", "available": "Tilgjengelig",
"blacklisted": "", "blacklisted": "",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Nieznany", "unknown": "Nieznany",
"pending": "Oczekujący", "pending": "Oczekujący",
"processing": "Przetwarzanie", "processing": "Przetwarzanie",
"requested": "Żądanie",
"partiallyAvailable": "Częściowy", "partiallyAvailable": "Częściowy",
"available": "Dostępne", "available": "Dostępne",
"blacklisted": "Zablokowany", "blacklisted": "Zablokowany",
+38 -37
View File
@@ -7,7 +7,7 @@
"description": "Para começar, por favor selecione como você deseja configurar sua instância Homarr.", "description": "Para começar, por favor selecione como você deseja configurar sua instância Homarr.",
"action": { "action": {
"scratch": "Começar do zero", "scratch": "Começar do zero",
"importOldmarr": "Importe de uma instância Homarr anterior a 1,0" "importOldmarr": "Importe de uma instância Homarr anterior a 1.0"
} }
}, },
"import": { "import": {
@@ -307,12 +307,12 @@
"name": "Nome", "name": "Nome",
"members": "Membros", "members": "Membros",
"homeBoard": { "homeBoard": {
"label": "", "label": "Quadro inicial",
"description": "" "description": "Somente quadros acessíveis ao grupo podem ser selecionados"
}, },
"mobileBoard": { "mobileBoard": {
"label": "", "label": "Quadro para celular",
"description": "" "description": "Somente quadros acessíveis ao grupo podem ser selecionados"
} }
}, },
"permission": { "permission": {
@@ -320,8 +320,8 @@
"title": "Administrador", "title": "Administrador",
"item": { "item": {
"admin": { "admin": {
"label": "", "label": "Administrador",
"description": "" "description": "Membros com essa permissão têm acesso total a todos os recursos e configurações"
} }
} }
}, },
@@ -329,15 +329,15 @@
"title": "Aplicativos", "title": "Aplicativos",
"item": { "item": {
"create": { "create": {
"label": "", "label": "Criar aplicativo",
"description": "" "description": "Permitir que membros criem aplicativos"
}, },
"use-all": { "use-all": {
"label": "", "label": "Usar todas aplicações",
"description": "" "description": "Permitir que os membros adicionem qualquer aplicativo aos seus quadros"
}, },
"modify-all": { "modify-all": {
"label": "", "label": "Modificar todos os aplicativos",
"description": "" "description": ""
}, },
"full-all": { "full-all": {
@@ -410,7 +410,7 @@
"item": { "item": {
"view-logs": { "view-logs": {
"label": "", "label": "",
"description": "" "description": "Permitir que os membros visualizem os registros"
} }
} }
}, },
@@ -419,7 +419,7 @@
"item": { "item": {
"create": { "create": {
"label": "", "label": "",
"description": "" "description": "Permitir que os membros criem mecanismos de busca"
}, },
"modify-all": { "modify-all": {
"label": "", "label": "",
@@ -547,19 +547,19 @@
"title": "Aplicativos", "title": "Aplicativos",
"noResults": { "noResults": {
"title": "", "title": "",
"action": "" "action": "Crie seu primeiro aplicativo"
} }
}, },
"create": { "create": {
"title": "", "title": "Novo Aplicativo",
"notification": { "notification": {
"success": { "success": {
"title": "", "title": "Criado com sucesso",
"message": "" "message": "O aplicativo foi criado com sucesso"
}, },
"error": { "error": {
"title": "", "title": "Falha ao criar",
"message": "" "message": "Não foi possível criar o aplicativo"
} }
} }
}, },
@@ -618,9 +618,9 @@
"title": "" "title": ""
}, },
"create": { "create": {
"title": "", "title": "Criar aplicativo",
"description": "", "description": "Criar um aplicativo",
"action": "" "action": "Abrir criação de aplicativo"
}, },
"add": "" "add": ""
} }
@@ -649,14 +649,14 @@
"app": { "app": {
"option": { "option": {
"existing": { "existing": {
"title": "", "title": "Existente",
"label": "" "label": "Selecione um aplicativo existente"
}, },
"new": { "new": {
"title": "", "title": "Novo",
"url": { "url": {
"label": "", "label": "URL da aplicação",
"description": "" "description": "A URL do aplicativo será aberta quando acessada a partir do painel de controle"
} }
} }
} }
@@ -712,8 +712,8 @@
"sectionTitle": "" "sectionTitle": ""
}, },
"createApp": { "createApp": {
"label": "", "label": "Criar aplicativo",
"description": "" "description": "Crie um aplicativo com o mesmo nome e ícone da integração. Deixe o campo de entrada abaixo vazio para criar o aplicativo com a URL de integração."
}, },
"appHref": { "appHref": {
"placeholder": "" "placeholder": ""
@@ -1973,8 +1973,8 @@
}, },
"app": { "app": {
"noData": "", "noData": "",
"description": "", "description": "Clique <here></here> para criar um aplicativo",
"quickCreate": "" "quickCreate": "Criar aplicativo em tempo real"
}, },
"error": { "error": {
"noIntegration": "", "noIntegration": "",
@@ -2232,6 +2232,7 @@
"unknown": "Desconhecido", "unknown": "Desconhecido",
"pending": "", "pending": "",
"processing": "", "processing": "",
"requested": "",
"partiallyAvailable": "Parcial", "partiallyAvailable": "Parcial",
"available": "Disponível", "available": "Disponível",
"blacklisted": "", "blacklisted": "",
@@ -2659,7 +2660,7 @@
}, },
"onlyImportApps": { "onlyImportApps": {
"label": "", "label": "",
"description": "" "description": "Só adiciona os aplicativos, o quadro precisa ser recriado manualmente"
} }
}, },
"name": { "name": {
@@ -2692,8 +2693,8 @@
}, },
"quickCreateApp": { "quickCreateApp": {
"modal": { "modal": {
"title": "", "title": "Criar aplicativo em tempo real",
"createAndUse": "" "createAndUse": "Criar e utilizar"
} }
} }
}, },
@@ -2911,7 +2912,7 @@
"title": "", "title": "",
"description": "", "description": "",
"link": "", "link": "",
"notice": "" "notice": "Para fazer esta página desaparecer, crie um quadro e defina-o como quadro inicial"
}, },
"notFound": { "notFound": {
"title": "", "title": "",
@@ -4109,7 +4110,7 @@
"label": "" "label": ""
}, },
"newApp": { "newApp": {
"label": "" "label": "Criar aplicativo"
}, },
"newIntegration": { "newIntegration": {
"label": "", "label": "",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Necunoscut", "unknown": "Necunoscut",
"pending": "", "pending": "",
"processing": "", "processing": "",
"requested": "",
"partiallyAvailable": "Parțial", "partiallyAvailable": "Parțial",
"available": "Disponibil", "available": "Disponibil",
"blacklisted": "", "blacklisted": "",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Неизвестно", "unknown": "Неизвестно",
"pending": "В ожидании", "pending": "В ожидании",
"processing": "Обработка", "processing": "Обработка",
"requested": "",
"partiallyAvailable": "Частично доступно", "partiallyAvailable": "Частично доступно",
"available": "Доступно", "available": "Доступно",
"blacklisted": "", "blacklisted": "",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Neznámy", "unknown": "Neznámy",
"pending": "Čakajúce", "pending": "Čakajúce",
"processing": "Spracovanie", "processing": "Spracovanie",
"requested": "",
"partiallyAvailable": "Čiastočný", "partiallyAvailable": "Čiastočný",
"available": "K dispozícii", "available": "K dispozícii",
"blacklisted": "", "blacklisted": "",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Neznano", "unknown": "Neznano",
"pending": "", "pending": "",
"processing": "", "processing": "",
"requested": "",
"partiallyAvailable": "", "partiallyAvailable": "",
"available": "", "available": "",
"blacklisted": "", "blacklisted": "",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Okänd", "unknown": "Okänd",
"pending": "", "pending": "",
"processing": "", "processing": "",
"requested": "",
"partiallyAvailable": "Delmängd", "partiallyAvailable": "Delmängd",
"available": "Tillgänglig", "available": "Tillgänglig",
"blacklisted": "", "blacklisted": "",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Bilinmeyen", "unknown": "Bilinmeyen",
"pending": "Bekleyen", "pending": "Bekleyen",
"processing": "İşlemde", "processing": "İşlemde",
"requested": "",
"partiallyAvailable": "Kısmi", "partiallyAvailable": "Kısmi",
"available": "Mevcut", "available": "Mevcut",
"blacklisted": "Engellenenler", "blacklisted": "Engellenenler",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Невідомо", "unknown": "Невідомо",
"pending": "В очікуванні", "pending": "В очікуванні",
"processing": "В обробці", "processing": "В обробці",
"requested": "",
"partiallyAvailable": "Частково доступно", "partiallyAvailable": "Частково доступно",
"available": "Доступно", "available": "Доступно",
"blacklisted": "", "blacklisted": "",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "Không rõ", "unknown": "Không rõ",
"pending": "", "pending": "",
"processing": "", "processing": "",
"requested": "",
"partiallyAvailable": "Một phần", "partiallyAvailable": "Một phần",
"available": "Khả dụng", "available": "Khả dụng",
"blacklisted": "", "blacklisted": "",
+1
View File
@@ -2232,6 +2232,7 @@
"unknown": "未知", "unknown": "未知",
"pending": "等待處理中", "pending": "等待處理中",
"processing": "處理中", "processing": "處理中",
"requested": "",
"partiallyAvailable": "部分", "partiallyAvailable": "部分",
"available": "待定", "available": "待定",
"blacklisted": "", "blacklisted": "",
+1
View File
@@ -16,3 +16,4 @@ export { MaskedImage } from "./masked-image";
export { MaskedOrNormalImage } from "./masked-or-normal-image"; export { MaskedOrNormalImage } from "./masked-or-normal-image";
export { LanguageIcon } from "./language-icon"; export { LanguageIcon } from "./language-icon";
export { Link } from "./link"; export { Link } from "./link";
export * from "./radio";
@@ -0,0 +1,33 @@
.root {
position: relative;
padding: var(--mantine-spacing-md);
transition: border-color 150ms ease;
&[data-checked] {
border-color: var(--mantine-primary-color-filled);
}
@mixin hover {
@mixin light {
background-color: var(--mantine-color-gray-0);
}
@mixin dark {
background-color: var(--mantine-color-dark-6);
}
}
}
.label {
font-family: var(--mantine-font-family-monospace);
font-weight: bold;
font-size: var(--mantine-font-size-md);
line-height: 1.3;
color: var(--mantine-color-bright);
}
.description {
margin-top: 8px;
color: var(--mantine-color-dimmed);
font-size: var(--mantine-font-size-xs);
}
+24
View File
@@ -0,0 +1,24 @@
import type { RadioCardProps } from "@mantine/core";
import { Group, RadioCard, RadioIndicator, Text } from "@mantine/core";
import clsx from "clsx";
import classes from "./radio.module.css";
interface CustomRadioCardProps extends RadioCardProps {
label: string;
description: string;
}
export const CustomRadioCard = (props: CustomRadioCardProps) => {
return (
<RadioCard className={clsx(classes.root, props.className)} {...props}>
<Group wrap="nowrap" align="flex-start">
<RadioIndicator />
<div>
<Text className={classes.label}>{props.label}</Text>
<Text className={classes.description}>{props.description}</Text>
</div>
</Group>
</RadioCard>
);
};
+5
View File
@@ -4,6 +4,7 @@ import {
backgroundImageAttachments, backgroundImageAttachments,
backgroundImageRepeats, backgroundImageRepeats,
backgroundImageSizes, backgroundImageSizes,
boardLayoutModes,
boardPermissions, boardPermissions,
} from "@homarr/definitions"; } from "@homarr/definitions";
@@ -79,6 +80,9 @@ export const boardSaveLayoutsSchema = z.object({
breakpoint: z.number().min(0).max(32767), breakpoint: z.number().min(0).max(32767),
}), }),
), ),
// If not send it should fallback to the legacy behavior which is "custom"
layoutMode: z.enum(boardLayoutModes.values).default("custom"),
baseLayoutId: z.string().nullish(),
}); });
export const boardSaveSchema = z.object({ export const boardSaveSchema = z.object({
@@ -90,6 +94,7 @@ export const boardSaveSchema = z.object({
export const boardCreateSchema = z.object({ export const boardCreateSchema = z.object({
name: boardNameSchema, name: boardNameSchema,
columnCount: boardColumnCountSchema, columnCount: boardColumnCountSchema,
layoutMode: z.enum(boardLayoutModes.values).default(boardLayoutModes.defaultValue),
isPublic: z.boolean(), isPublic: z.boolean(),
}); });
@@ -1,7 +1,7 @@
"use client"; "use client";
import { Box, Flex, Group, Text, Tooltip } from "@mantine/core"; import { Box, Flex, Group, Text, Tooltip } from "@mantine/core";
import { IconUsersGroup } from "@tabler/icons-react"; import { IconCube, IconUsersGroup } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { formatNumber } from "@homarr/common"; import { formatNumber } from "@homarr/common";
@@ -44,13 +44,25 @@ export default function MinecraftServerStatusWidget({ options }: WidgetComponent
</Group> </Group>
{data.online && ( {data.online && (
<> <>
{!options.isBedrockServer && ( {!options.isBedrockServer &&
<img (data.icon ? (
style={{ flex: 1, transform: "scale(0.8)", objectFit: "contain" }} <img
alt={`minecraft icon ${options.domain}`} style={{ flex: 1, transform: "scale(0.8)", objectFit: "contain" }}
src={data.icon} alt={`minecraft icon ${options.domain}`}
/> src={data.icon}
)} />
) : (
<Box
style={{
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<IconCube size="3rem" color="var(--mantine-color-gray-5)" />
</Box>
))}
<Group gap={5} c="gray.6" align="center"> <Group gap={5} c="gray.6" align="center">
<IconUsersGroup size="1rem" /> <IconUsersGroup size="1rem" />
<Text size="md"> <Text size="md">
+309 -222
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -2,3 +2,5 @@ packages:
- apps/* - apps/*
- packages/* - packages/*
- tooling/* - tooling/*
minimumReleaseAge: 4320 # Only download deps if they are at least 3 days old (in minutes)
+1 -1
View File
@@ -1 +1 @@
[{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"tancak","avatar_url":"https://avatars.githubusercontent.com/u/11760749?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"VinnyVynce","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontent.com/u/47Line truncated [{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"tancak","avatar_url":"https://avatars.githubusercontent.com/u/11760749?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"alotlikebeans","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontent.com/uLine truncated
+1 -1
View File
@@ -1 +1 @@
[{"username":"carlchina","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/6697/medium/d22bbe7797bbeb30dbdc73a5648d329a_default.png"},{"username":"zielmann","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png"},{"username":"Thalyn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12494028/medium/5faf5acc945a874ed116c4567edaa09f_default.png"},{"username":"magnushj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12503488/medium/68f32e33ea7b891960055c9f831f7344_default.png"},{"username":"almontegil","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12580457/medium/f4136cacbdfdb4c28ae7f85dc5f840db_default.png"},{"username":"Skick","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12619811/medium/0331f1de413a645b4516bc4e037f31bb.jpg"},{"username":"andibing","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12664938/medium/b8be63e4dcb2e791ced1ffc9e3a049a5.jpg"},{"username":"gronare","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12680911/medium/c6eb14d1504156f34a158e507f73aa3d_default.png"},{"username":"SmartPhoneLover","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12701640/medium/ec95486662ec875cda080e778c3ff702.jpg"},{"username":"marcelotk_15","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12773227/medium/ecf6bea16ab0c873c131df2154ee9718_default.png"},{"username":"S3OD177","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13051544/medium/11dd990837f61f7e8f7d36b041a7fddb_default.png"},{"username":"marcg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13153301/medium/ebcf60fbf27deb148ec40a659566c6ff_default.png"},{"username":"BeersTeddy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13185230/medium/2f1f4e1effe74a23422b195cbefb2a95_default.png"},{"username":"jamesmcmahon0","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13245578/medium/586aa873b4abddbd9abc6f3de99ab70e.jpeg"},{"username":"b1nar10","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13286752/medium/249b02e3368752cb175446082015737c_default.png"},{"username":"vannCN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13330448/medium/9b8a9ee3611e51e951e22d5fd4eb7d8d.jpg"},{"username":"binswm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13343482/medium/45dde0e6097b9b72705d2eba9dbbc276_default.png"},{"username":"SkewRam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13496556/medium/188f5c2deb7938eda51eb786cc4539ca.jpeg"},{"username":"raphcatarino","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13547726/medium/b003511e67df13a4b4b5689488fa8099.jpg"},{"username":"shillos5","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13600557/medium/b17650b2b84f829d54fa548c44c1ab27.jpeg"},{"username":"wolong98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13641407/medium/f4634edc58c7857a357e5293543c15cf.jpg"},{"username":"spair0039","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14012333/medium/72430e96027c09c19141cac38eae4617.png"},{"username":"MajorMarcin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14072183/medium/0a197e64bb92b6ea94cecbb607512c45.jpg"},{"username":"KosmoMoustache","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14103535/medium/51c438cdd9f86e8811c75f4869de479c.png"},{"username":"Soochaehwa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14118689/medium/496a1ce63111547bf455a1e0a7ac75f1_default.png"},{"username":"garryfield","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14187035/medium/520c964706676045f6eeec1b08edc880.jpg"},{"username":"droidenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14444264/medium/4aa3a8e824f72bc9e5ec0d8de307095e.jpeg"},{"username":"maathis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14489950/medium/6eeddf13a757ee35f78f1763f94d95e1.jpeg"},{"username":"bukvam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14546502/medium/a819dc40a13c57395a17dc46e847d3ce.jpg"},{"username":"gm.cinalli","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14670666/medium/2d466a0fdbda40764526be86c97c0ab4.jpeg"},{"username":"antoine2tt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14722148/medium/f88d926900862dd59007ea4b3419cb9d.png"},{"username":"cretzen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14799754/medium/3ba5c1805ec20c000d5810c07985afe9.jpg"},{"username":"ktKongTong","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14817246/medium/97cfc3c028dbdaf85ebd1102da71e58c.jpeg"},{"username":"marinkaberg","avatarUrl":"https://crowLine truncated [{"username":"carlchina","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/6697/medium/d22bbe7797bbeb30dbdc73a5648d329a_default.png"},{"username":"zielmann","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png"},{"username":"Thalyn","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12494028/medium/5faf5acc945a874ed116c4567edaa09f_default.png"},{"username":"magnushj","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12503488/medium/68f32e33ea7b891960055c9f831f7344_default.png"},{"username":"almontegil","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12580457/medium/f4136cacbdfdb4c28ae7f85dc5f840db_default.png"},{"username":"Skick","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12619811/medium/0331f1de413a645b4516bc4e037f31bb.jpg"},{"username":"andibing","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12664938/medium/b8be63e4dcb2e791ced1ffc9e3a049a5.jpg"},{"username":"gronare","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12680911/medium/c6eb14d1504156f34a158e507f73aa3d_default.png"},{"username":"SmartPhoneLover","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12701640/medium/ec95486662ec875cda080e778c3ff702.jpg"},{"username":"marcelotk_15","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/12773227/medium/ecf6bea16ab0c873c131df2154ee9718_default.png"},{"username":"S3OD177","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13051544/medium/11dd990837f61f7e8f7d36b041a7fddb_default.png"},{"username":"marcg","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13153301/medium/ebcf60fbf27deb148ec40a659566c6ff_default.png"},{"username":"BeersTeddy","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13185230/medium/2f1f4e1effe74a23422b195cbefb2a95_default.png"},{"username":"jamesmcmahon0","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13245578/medium/586aa873b4abddbd9abc6f3de99ab70e.jpeg"},{"username":"b1nar10","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13286752/medium/249b02e3368752cb175446082015737c_default.png"},{"username":"vannCN","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13330448/medium/9b8a9ee3611e51e951e22d5fd4eb7d8d.jpg"},{"username":"binswm","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13343482/medium/45dde0e6097b9b72705d2eba9dbbc276_default.png"},{"username":"SkewRam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13496556/medium/188f5c2deb7938eda51eb786cc4539ca.jpeg"},{"username":"raphcatarino","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13547726/medium/b003511e67df13a4b4b5689488fa8099.jpg"},{"username":"shillos5","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13600557/medium/b17650b2b84f829d54fa548c44c1ab27.jpeg"},{"username":"wolong98","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/13641407/medium/f4634edc58c7857a357e5293543c15cf.jpg"},{"username":"spair0039","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14012333/medium/72430e96027c09c19141cac38eae4617.png"},{"username":"MajorMarcin","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14072183/medium/0a197e64bb92b6ea94cecbb607512c45.jpg"},{"username":"KosmoMoustache","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14103535/medium/51c438cdd9f86e8811c75f4869de479c.png"},{"username":"Soochaehwa","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14118689/medium/496a1ce63111547bf455a1e0a7ac75f1_default.png"},{"username":"garryfield","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14187035/medium/520c964706676045f6eeec1b08edc880.jpg"},{"username":"droidenko","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14444264/medium/4aa3a8e824f72bc9e5ec0d8de307095e.jpeg"},{"username":"maathis","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14489950/medium/6eeddf13a757ee35f78f1763f94d95e1.jpeg"},{"username":"bukvam","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14546502/medium/a819dc40a13c57395a17dc46e847d3ce.jpg"},{"username":"gm.cinalli","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14670666/medium/2d466a0fdbda40764526be86c97c0ab4.jpeg"},{"username":"antoine2tt","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14722148/medium/f88d926900862dd59007ea4b3419cb9d.png"},{"username":"cretzen","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14799754/medium/3ba5c1805ec20c000d5810c07985afe9.jpg"},{"username":"ktKongTong","avatarUrl":"https://crowdin-static.cf-downloads.crowdin.com/avatar/14817246/medium/97cfc3c028dbdaf85ebd1102da71e58c.jpeg"},{"username":"marinkaberg","avatarUrl":"https://crowLine truncated
+23
View File
@@ -0,0 +1,23 @@
import fs from "fs/promises";
import { join } from "path";
import json5 from "json5";
import { describe, test } from "vitest";
describe("Renovate configuration tests", () => {
test("automerge should be disabled for onlyBuiltDependencies", async () => {
const packageJson = await import("../package.json");
const renovateConfig = await fs.readFile(join(__dirname, "../.github/renovate.json5"), "utf-8").then(json5.parse);
const onlyBuiltDependencies = packageJson.pnpm.onlyBuiltDependencies;
const automergeDisabledDeps = renovateConfig.packageRules
.filter((rule: any) => rule.automerge === false)
.flatMap((rule: any) => rule.matchPackageNames || []);
const missingDeps = onlyBuiltDependencies.filter((dep: string) => !automergeDisabledDeps.includes(dep));
if (missingDeps.length > 0) {
throw new Error(
`The following onlyBuiltDependencies are missing automerge disable rules in renovate.json5: ${missingDeps.join(", ")}`,
);
}
});
});