chore(release): automatic release v1.19.1

This commit is contained in:
homarr-releases[bot]
2025-05-09 19:14:15 +00:00
committed by GitHub
61 changed files with 1952 additions and 1491 deletions

View File

@@ -31,6 +31,7 @@ body:
label: Version
description: What version of Homarr are you running?
options:
- 1.19.0
- 1.18.0
- 1.17.0
- 1.16.0

View File

@@ -48,17 +48,17 @@
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"@mantine/colors-generator": "^7.17.7",
"@mantine/core": "^7.17.7",
"@mantine/dropzone": "^7.17.7",
"@mantine/hooks": "^7.17.7",
"@mantine/modals": "^7.17.7",
"@mantine/tiptap": "^7.17.7",
"@mantine/colors-generator": "^8.0.0",
"@mantine/core": "^8.0.0",
"@mantine/dropzone": "^8.0.0",
"@mantine/hooks": "^8.0.0",
"@mantine/modals": "^8.0.0",
"@mantine/tiptap": "^8.0.0",
"@million/lint": "1.0.14",
"@tabler/icons-react": "^3.31.0",
"@tanstack/react-query": "^5.75.1",
"@tanstack/react-query-devtools": "^5.75.1",
"@tanstack/react-query-next-experimental": "^5.75.1",
"@tanstack/react-query": "^5.75.7",
"@tanstack/react-query-devtools": "^5.75.7",
"@tanstack/react-query-next-experimental": "^5.75.7",
"@trpc/client": "^11.1.2",
"@trpc/next": "^11.1.2",
"@trpc/react-query": "^11.1.2",
@@ -72,33 +72,33 @@
"dotenv": "^16.5.0",
"flag-icons": "^7.3.2",
"glob": "^11.0.2",
"jotai": "^2.12.3",
"jotai": "^2.12.4",
"mantine-react-table": "2.0.0-beta.9",
"next": "15.3.1",
"next": "15.3.2",
"postcss-preset-mantine": "^1.17.0",
"prismjs": "^1.30.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-error-boundary": "^5.0.0",
"react-error-boundary": "^6.0.0",
"react-simple-code-editor": "^0.14.1",
"sass": "^1.87.0",
"superjson": "2.2.2",
"swagger-ui-react": "^5.21.0",
"use-deep-compare-effect": "^1.8.1",
"zod": "^3.24.3"
"zod": "^3.24.4"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.1",
"@types/node": "^22.15.3",
"@types/node": "^22.15.17",
"@types/prismjs": "^1.26.5",
"@types/react": "19.1.2",
"@types/react": "19.1.3",
"@types/react-dom": "19.1.3",
"@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.1.2",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"node-loader": "^2.1.0",
"prettier": "^3.5.3",
"typescript": "^5.8.3"

View File

@@ -44,9 +44,9 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.15.3",
"@types/node": "^22.15.17",
"dotenv-cli": "^8.0.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"prettier": "^3.5.3",
"tsx": "4.19.4",
"typescript": "^5.8.3"

View File

@@ -27,14 +27,14 @@
"@homarr/validation": "workspace:^0.1.0",
"dotenv": "^16.5.0",
"tsx": "4.19.4",
"ws": "^8.18.1"
"ws": "^8.18.2"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.18.1",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"prettier": "^3.5.3",
"typescript": "^5.8.3"
}

View File

@@ -38,20 +38,20 @@
"@semantic-release/github": "^11.0.2",
"@semantic-release/npm": "^12.0.1",
"@semantic-release/release-notes-generator": "^14.0.3",
"@turbo/gen": "^2.5.2",
"@turbo/gen": "^2.5.3",
"@vitejs/plugin-react": "^4.4.1",
"@vitest/coverage-v8": "^3.1.2",
"@vitest/ui": "^3.1.2",
"@vitest/coverage-v8": "^3.1.3",
"@vitest/ui": "^3.1.3",
"conventional-changelog-conventionalcommits": "^8.0.0",
"cross-env": "^7.0.3",
"jsdom": "^26.1.0",
"prettier": "^3.5.3",
"semantic-release": "^24.2.3",
"testcontainers": "^10.25.0",
"turbo": "^2.5.2",
"turbo": "^2.5.3",
"typescript": "^5.8.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.2"
"vitest": "^3.1.3"
},
"packageManager": "pnpm@10.10.0",
"engines": {

View File

@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -40,25 +40,25 @@
"@homarr/request-handler": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.1.2",
"@tanstack/react-query": "^5.75.1",
"@kubernetes/client-node": "^1.2.0",
"@tanstack/react-query": "^5.75.7",
"@trpc/client": "^11.1.2",
"@trpc/react-query": "^11.1.2",
"@trpc/server": "^11.1.2",
"@trpc/tanstack-react-query": "^11.1.2",
"lodash.clonedeep": "^4.5.0",
"next": "15.3.1",
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"superjson": "2.2.2",
"trpc-to-openapi": "^2.2.0",
"zod": "^3.24.3"
"zod": "^3.24.4"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"prettier": "^3.5.3",
"typescript": "^5.8.3"
}

View File

@@ -5,7 +5,7 @@ import { releasesRequestHandler } from "@homarr/request-handler/releases";
import { createTRPCRouter, publicProcedure } from "../../trpc";
const formatVersionFilterRegex = (versionFilter: z.infer<typeof _releaseVersionFilterSchema> | undefined) => {
const formatVersionFilterRegex = (versionFilter: z.infer<typeof releaseVersionFilterSchema> | undefined) => {
if (!versionFilter) return undefined;
const escapedPrefix = versionFilter.prefix ? escapeForRegEx(versionFilter.prefix) : "";
@@ -15,7 +15,7 @@ const formatVersionFilterRegex = (versionFilter: z.infer<typeof _releaseVersionF
return `^${escapedPrefix}${precision}${escapedSuffix}$`;
};
const _releaseVersionFilterSchema = z.object({
const releaseVersionFilterSchema = z.object({
prefix: z.string().optional(),
precision: z.number(),
suffix: z.string().optional(),
@@ -29,7 +29,7 @@ export const releasesRouter = createTRPCRouter({
z.object({
providerKey: z.string(),
identifier: z.string(),
versionFilter: _releaseVersionFilterSchema.optional(),
versionFilter: releaseVersionFilterSchema.optional(),
}),
),
}),

View File

@@ -13,6 +13,7 @@ import { extractProfileName } from "./providers/oidc/oidc-provider";
export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["events"], undefined>["signIn"] => {
return async ({ user, profile }) => {
logger.debug(`SignIn EventHandler for user: ${JSON.stringify(user)} . profile: ${JSON.stringify(profile)}`);
if (!user.id) throw new Error("User ID is missing");
const dbUser = await db.query.users.findFirst({
@@ -28,11 +29,13 @@ export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["
const groupsKey = env.AUTH_OIDC_GROUPS_ATTRIBUTE;
// Groups from oidc provider are provided from the profile, it's not typed.
if (profile && groupsKey in profile && Array.isArray(profile[groupsKey])) {
logger.debug(`Using profile groups (${groupsKey}): ${JSON.stringify(profile[groupsKey])}`);
await synchronizeGroupsWithExternalForUserAsync(db, user.id, profile[groupsKey] as string[]);
}
// In ldap-authroization we return the groups from ldap, it's not typed.
if ("groups" in user && Array.isArray(user.groups)) {
logger.debug(`Using profile groups: ${JSON.stringify(user.groups)}`);
await synchronizeGroupsWithExternalForUserAsync(db, user.id, user.groups as string[]);
}
await addUserToEveryoneGroupIfNotMemberAsync(db, user.id);

View File

@@ -34,12 +34,12 @@
"@homarr/validation": "workspace:^0.1.0",
"bcrypt": "^5.1.1",
"cookies": "^0.9.1",
"ldapts": "7.4.0",
"next": "15.3.1",
"ldapts": "8.0.0",
"next": "15.3.2",
"next-auth": "5.0.0-beta.27",
"react": "19.1.0",
"react-dom": "19.1.0",
"zod": "^3.24.3"
"zod": "^3.24.4"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -47,7 +47,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "5.0.2",
"@types/cookies": "0.9.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"prettier": "^3.5.3",
"typescript": "^5.8.3"
}

View File

@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -34,7 +34,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -30,17 +30,17 @@
"@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"dayjs": "^1.11.13",
"next": "15.3.1",
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"undici": "7.8.0",
"zod": "^3.24.3"
"zod": "^3.24.4"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -5,3 +5,11 @@ export const splitToNChunks = <T>(array: T[], chunks: number): T[][] => {
}
return result;
};
export const splitToChunksWithNItems = <T>(array: T[], itemCount: number): T[][] => {
const result: T[][] = [];
for (let i = 0; i < array.length; i += itemCount) {
result.push(array.slice(i, i + itemCount));
}
return result;
};

View File

@@ -0,0 +1,26 @@
import dayjs from "dayjs";
import type { UnitTypeShort } from "dayjs";
import isBetween from "dayjs/plugin/isBetween";
dayjs.extend(isBetween);
const validUnits = ["h", "d", "w", "M", "y"] as UnitTypeShort[];
export const isDateWithin = (date: Date, relativeDate: string): boolean => {
if (relativeDate.length < 2) {
throw new Error("Relative date must be at least 2 characters long");
}
const amount = parseInt(relativeDate.slice(0, -1), 10);
if (isNaN(amount) || amount <= 0) {
throw new Error("Relative date must be a number greater than 0");
}
const unit = relativeDate.slice(-1) as dayjs.UnitTypeShort;
if (!validUnits.includes(unit)) {
throw new Error("Invalid relative time unit");
}
const startDate = dayjs().subtract(amount, unit);
return dayjs(date).isBetween(startDate, dayjs(), null, "[]");
};

View File

@@ -2,6 +2,7 @@ export * from "./object";
export * from "./string";
export * from "./cookie";
export * from "./array";
export * from "./date";
export * from "./stopwatch";
export * from "./hooks";
export * from "./url";

View File

@@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";
import { splitToChunksWithNItems, splitToNChunks } from "../array";
describe("splitToNChunks", () => {
it("should split an array into the specified number of chunks", () => {
const array = [1, 2, 3, 4, 5];
const chunks = 3;
const result = splitToNChunks(array, chunks);
expect(result).toEqual([[1, 2], [3, 4], [5]]);
});
it("should handle an empty array", () => {
const array: number[] = [];
const chunks = 3;
const result = splitToNChunks(array, chunks);
expect(result).toEqual([[], [], []]);
});
it("should handle more chunks than elements", () => {
const array = [1, 2];
const chunks = 5;
const result = splitToNChunks(array, chunks);
expect(result).toEqual([[1], [2], [], [], []]);
});
});
describe("splitToChunksWithNItems", () => {
it("should split an array into chunks with the specified number of items", () => {
const array = [1, 2, 3, 4, 5];
const items = 2;
const result = splitToChunksWithNItems(array, items);
expect(result).toEqual([[1, 2], [3, 4], [5]]);
});
it("should handle an empty array", () => {
const array: number[] = [];
const items = 2;
const result = splitToChunksWithNItems(array, items);
expect(result).toEqual([]);
});
it("should handle more items per chunk than elements", () => {
const array = [1, 2];
const items = 5;
const result = splitToChunksWithNItems(array, items);
expect(result).toEqual([[1, 2]]);
});
});

View File

@@ -0,0 +1,91 @@
import { describe, expect, it } from "vitest";
import { isDateWithin } from "../date";
describe("isDateWithin", () => {
it("should return true for a date within the specified hours", () => {
const date = new Date();
date.setHours(date.getHours() - 20);
expect(isDateWithin(date, "100h")).toBe(true);
});
it("should return false for a date outside the specified hours", () => {
const date = new Date();
date.setHours(date.getHours() - 101);
expect(isDateWithin(date, "100h")).toBe(false);
});
it("should return true for a date within the specified days", () => {
const date = new Date();
date.setDate(date.getDate() - 5);
expect(isDateWithin(date, "10d")).toBe(true);
});
it("should return false for a date outside the specified days", () => {
const date = new Date();
date.setDate(date.getDate() - 11);
expect(isDateWithin(date, "10d")).toBe(false);
});
it("should return true for a date within the specified weeks", () => {
const date = new Date();
date.setDate(date.getDate() - 10);
expect(isDateWithin(date, "7w")).toBe(true);
});
it("should return false for a date outside the specified weeks", () => {
const date = new Date();
date.setDate(date.getDate() - 50);
expect(isDateWithin(date, "7w")).toBe(false);
});
it("should return true for a date within the specified months", () => {
const date = new Date();
date.setMonth(date.getMonth() - 1);
expect(isDateWithin(date, "2M")).toBe(true);
});
it("should return false for a date outside the specified months", () => {
const date = new Date();
date.setMonth(date.getMonth() - 3);
expect(isDateWithin(date, "2M")).toBe(false);
});
it("should return true for a date within the specified years", () => {
const date = new Date();
date.setFullYear(date.getFullYear() - 1);
expect(isDateWithin(date, "2y")).toBe(true);
});
it("should return false for a date outside the specified years", () => {
const date = new Date();
date.setFullYear(date.getFullYear() - 3);
expect(isDateWithin(date, "2y")).toBe(false);
});
it("should return false for a date after the specified relative time", () => {
const date = new Date();
date.setDate(date.getDate() + 2);
expect(isDateWithin(date, "1d")).toBe(false);
});
it("should throw an error for an invalid unit", () => {
const date = new Date();
expect(() => isDateWithin(date, "2x")).toThrow("Invalid relative time unit");
});
it("should throw an error if relativeDate is less than 2 characters long", () => {
const date = new Date();
expect(() => isDateWithin(date, "h")).toThrow("Relative date must be at least 2 characters long");
});
it("should throw an error if relativeDate has an invalid number", () => {
const date = new Date();
expect(() => isDateWithin(date, "hh")).toThrow("Relative date must be a number greater than 0");
});
it("should throw an error if relativeDate is set to 0", () => {
const date = new Date();
expect(() => isDateWithin(date, "0y")).toThrow("Relative date must be a number greater than 0");
});
});

View File

@@ -33,7 +33,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -32,7 +32,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -44,7 +44,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -44,12 +44,12 @@
"@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^7.17.7",
"@mantine/core": "^8.0.0",
"@paralleldrive/cuid2": "^2.2.2",
"@testcontainers/mysql": "^10.25.0",
"better-sqlite3": "^11.9.1",
"better-sqlite3": "^11.10.0",
"dotenv": "^16.5.0",
"drizzle-kit": "^0.31.0",
"drizzle-kit": "^0.31.1",
"drizzle-orm": "^0.43.1",
"drizzle-zod": "^0.7.1",
"mysql2": "3.14.1"
@@ -60,7 +60,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/better-sqlite3": "7.6.13",
"dotenv-cli": "^8.0.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"prettier": "^3.5.3",
"tsx": "4.19.4",
"typescript": "^5.8.3"

View File

@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -32,7 +32,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "^3.3.38",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -24,13 +24,13 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@t3-oss/env-nextjs": "^0.13.4",
"zod": "^3.24.3"
"zod": "^3.24.4"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -26,14 +26,14 @@
"@homarr/common": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/form": "^7.17.7",
"zod": "^3.24.3"
"@mantine/form": "^8.0.0",
"zod": "^3.24.4"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -29,15 +29,15 @@
"@homarr/notifications": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^7.17.7",
"@mantine/core": "^8.0.0",
"react": "19.1.0",
"zod": "^3.24.3"
"zod": "^3.24.4"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -31,7 +31,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -43,7 +43,7 @@
"tsdav": "^2.1.4",
"undici": "7.8.0",
"xml2js": "^0.6.2",
"zod": "^3.24.3"
"zod": "^3.24.4"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -51,7 +51,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-unifi": "^2.5.1",
"@types/xml2js": "^0.4.14",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -27,13 +27,13 @@
"ioredis": "5.6.1",
"superjson": "2.2.2",
"winston": "3.17.0",
"zod": "^3.24.3"
"zod": "^3.24.4"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -22,6 +22,10 @@ export const formatErrorCause = (cause: unknown, iteration = 0): string => {
return `\ncaused by ${formatErrorTitle(cause)}\n${formatErrorStack(cause.stack)}${formatErrorCause(cause.cause, iteration + 1)}`;
}
if (cause instanceof Object) {
return `\ncaused by ${JSON.stringify(cause)}`;
}
return `\ncaused by ${cause as string}`;
};

View File

@@ -33,19 +33,19 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^7.17.7",
"@mantine/core": "^8.0.0",
"@tabler/icons-react": "^3.31.0",
"dayjs": "^1.11.13",
"next": "15.3.1",
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"zod": "^3.24.3"
"zod": "^3.24.4"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -24,15 +24,15 @@
"dependencies": {
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^7.17.7",
"@mantine/hooks": "^7.17.7",
"@mantine/core": "^8.0.0",
"@mantine/hooks": "^8.0.0",
"react": "19.1.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -24,14 +24,14 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/ui": "workspace:^0.1.0",
"@mantine/notifications": "^7.17.7",
"@mantine/notifications": "^8.0.0",
"@tabler/icons-react": "^3.31.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -37,14 +37,14 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^7.17.7",
"@mantine/hooks": "^7.17.7",
"@mantine/core": "^8.0.0",
"@mantine/hooks": "^8.0.0",
"adm-zip": "0.5.16",
"next": "15.3.1",
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"superjson": "2.2.2",
"zod": "^3.24.3",
"zod": "^3.24.4",
"zod-form-data": "^2.0.7"
},
"devDependencies": {
@@ -52,7 +52,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/adm-zip": "0.5.7",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -23,13 +23,13 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"zod": "^3.24.3"
"zod": "^3.24.4"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -30,7 +30,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -33,7 +33,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -22,7 +22,7 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@extractus/feed-extractor": "7.1.4",
"@extractus/feed-extractor": "7.1.5",
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
@@ -37,7 +37,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -41,12 +41,8 @@ export const Providers: ProvidersProps = {
.transform((resp) => ({
projectUrl: `https://hub.docker.com/r/${resp.namespace === "library" ? "_" : resp.namespace}/${resp.name}`,
projectDescription: resp.description,
isFork: false,
isArchived: false,
createdAt: resp.date_registered,
starsCount: resp.star_count,
openIssues: 0,
forksCount: 0,
}))
.safeParse(response);
},
@@ -67,12 +63,7 @@ export const Providers: ProvidersProps = {
),
})
.transform((resp) => {
return resp.results.map((release) => ({
...release,
releaseUrl: "",
releaseDescription: "",
isPreRelease: false,
}));
return resp.results;
})
.safeParse(response);
},
@@ -89,7 +80,7 @@ export const Providers: ProvidersProps = {
return z
.object({
html_url: z.string(),
description: z.string(),
description: z.string().nullable(),
fork: z.boolean(),
archived: z.boolean(),
created_at: z.string().transform((value) => new Date(value)),
@@ -99,7 +90,7 @@ export const Providers: ProvidersProps = {
})
.transform((resp) => ({
projectUrl: resp.html_url,
projectDescription: resp.description,
projectDescription: resp.description ?? undefined,
isFork: resp.fork,
isArchived: resp.archived,
createdAt: resp.created_at,
@@ -120,7 +111,7 @@ export const Providers: ProvidersProps = {
tag_name: z.string(),
published_at: z.string().transform((value) => new Date(value)),
html_url: z.string(),
body: z.string(),
body: z.string().nullable(),
prerelease: z.boolean(),
})
.transform((tag) => ({
@@ -128,7 +119,7 @@ export const Providers: ProvidersProps = {
latestRelease: tag.tag_name,
latestReleaseAt: tag.published_at,
releaseUrl: tag.html_url,
releaseDescription: tag.body,
releaseDescription: tag.body ?? undefined,
isPreRelease: tag.prerelease,
})),
)
@@ -144,17 +135,17 @@ export const Providers: ProvidersProps = {
.object({
web_url: z.string(),
description: z.string(),
forked_from_project: z.object({ id: z.number() }).nullable(),
archived: z.boolean(),
forked_from_project: z.object({ id: z.number() }).optional(),
archived: z.boolean().optional(),
created_at: z.string().transform((value) => new Date(value)),
star_count: z.number(),
open_issues_count: z.number(),
open_issues_count: z.number().optional(),
forks_count: z.number(),
})
.transform((resp) => ({
projectUrl: resp.web_url,
projectDescription: resp.description,
isFork: resp.forked_from_project !== null,
isFork: resp.forked_from_project !== undefined,
isArchived: resp.archived,
createdAt: resp.created_at,
starsCount: resp.star_count,
@@ -217,7 +208,6 @@ export const Providers: ProvidersProps = {
...release,
releaseUrl: `https://www.npmjs.com/package/${resp.name}/v/${release.latestRelease}`,
releaseDescription: resp.versions[release.latestRelease]?.description ?? "",
isPreRelease: false,
}));
})
.safeParse(response);
@@ -282,23 +272,31 @@ export const Providers: ProvidersProps = {
},
};
const _detailsSchema = z.object({
projectUrl: z.string(),
projectDescription: z.string(),
isFork: z.boolean(),
isArchived: z.boolean(),
createdAt: z.date(),
starsCount: z.number(),
openIssues: z.number(),
forksCount: z.number(),
});
const _detailsSchema = z
.object({
projectUrl: z.string().optional(),
projectDescription: z.string().optional(),
isFork: z.boolean().optional(),
isArchived: z.boolean().optional(),
createdAt: z.date().optional(),
starsCount: z.number().optional(),
openIssues: z.number().optional(),
forksCount: z.number().optional(),
})
.optional();
const _releasesSchema = z.object({
latestRelease: z.string(),
latestReleaseAt: z.date(),
releaseUrl: z.string(),
releaseDescription: z.string(),
isPreRelease: z.boolean(),
releaseUrl: z.string().optional(),
releaseDescription: z.string().optional(),
isPreRelease: z.boolean().optional(),
error: z
.object({
code: z.string().optional(),
message: z.string().optional(),
})
.optional(),
});
export type DetailsResponse = z.infer<typeof _detailsSchema>;

View File

@@ -8,22 +8,49 @@ import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-ha
import { Providers } from "./releases-providers";
import type { DetailsResponse } from "./releases-providers";
const errorSchema = z.object({
code: z.string().optional(),
message: z.string().optional(),
});
type ReleasesError = z.infer<typeof errorSchema>;
const _reponseSchema = z.object({
identifier: z.string(),
providerKey: z.string(),
latestRelease: z.string(),
latestReleaseAt: z.date(),
releaseUrl: z.string(),
releaseDescription: z.string(),
isPreRelease: z.boolean(),
projectUrl: z.string(),
projectDescription: z.string(),
isFork: z.boolean(),
isArchived: z.boolean(),
createdAt: z.date(),
starsCount: z.number(),
openIssues: z.number(),
forksCount: z.number(),
latestRelease: z.string().optional(),
latestReleaseAt: z.date().optional(),
releaseUrl: z.string().optional(),
releaseDescription: z.string().optional(),
isPreRelease: z.boolean().optional(),
projectUrl: z.string().optional(),
projectDescription: z.string().optional(),
isFork: z.boolean().optional(),
isArchived: z.boolean().optional(),
createdAt: z.date().optional(),
starsCount: z.number().optional(),
openIssues: z.number().optional(),
forksCount: z.number().optional(),
error: errorSchema.optional(),
});
const formatErrorRelease = (identifier: string, providerKey: string, error: ReleasesError) => ({
identifier,
providerKey,
latestRelease: undefined,
latestReleaseAt: undefined,
releaseUrl: undefined,
releaseDescription: undefined,
isPreRelease: undefined,
projectUrl: undefined,
projectDescription: undefined,
isFork: undefined,
isArchived: undefined,
createdAt: undefined,
starsCount: undefined,
openIssues: undefined,
forksCount: undefined,
error,
});
export const releasesRequestHandler = createCachedWidgetRequestHandler({
@@ -34,17 +61,7 @@ export const releasesRequestHandler = createCachedWidgetRequestHandler({
if (!provider) return undefined;
let detailsResult: DetailsResponse = {
projectUrl: "",
projectDescription: "",
isFork: false,
isArchived: false,
createdAt: new Date(0),
starsCount: 0,
openIssues: 0,
forksCount: 0,
};
let detailsResult: DetailsResponse;
const detailsUrl = provider.getDetailsUrl(input.identifier);
if (detailsUrl !== undefined) {
const detailsResponse = await fetchWithTimeout(detailsUrl);
@@ -53,7 +70,8 @@ export const releasesRequestHandler = createCachedWidgetRequestHandler({
if (parsedDetails?.success) {
detailsResult = parsedDetails.data;
} else {
logger.warn("Failed to parse details response", {
detailsResult = undefined;
logger.warn(`Failed to parse details response for ${input.identifier} on ${input.providerKey}`, {
provider: input.providerKey,
identifier: input.identifier,
detailsUrl,
@@ -63,43 +81,42 @@ export const releasesRequestHandler = createCachedWidgetRequestHandler({
}
const releasesResponse = await fetchWithTimeout(provider.getReleasesUrl(input.identifier));
const releasesResult = provider.parseReleasesResponse(await releasesResponse.json());
const releasesResponseJson: unknown = await releasesResponse.json();
const releasesResult = provider.parseReleasesResponse(releasesResponseJson);
if (!releasesResult.success) return undefined;
const latest: ResponseResponse = releasesResult.data
.filter((result) => (input.versionRegex ? new RegExp(input.versionRegex).test(result.latestRelease) : true))
.reduce(
(latest, result) => {
return {
...detailsResult,
...(result.latestReleaseAt > latest.latestReleaseAt ? result : latest),
identifier: input.identifier,
providerKey: input.providerKey,
};
},
{
identifier: "",
providerKey: "",
latestRelease: "",
latestReleaseAt: new Date(0),
releaseUrl: "",
releaseDescription: "",
isPreRelease: false,
projectUrl: "",
projectDescription: "",
isFork: false,
isArchived: false,
createdAt: new Date(0),
starsCount: 0,
openIssues: 0,
forksCount: 0,
},
if (!releasesResult.success) {
return formatErrorRelease(input.identifier, input.providerKey, {
message: releasesResponseJson ? JSON.stringify(releasesResponseJson, null, 2) : releasesResult.error.message,
});
} else {
const releases = releasesResult.data.filter((result) =>
input.versionRegex && result.latestRelease ? new RegExp(input.versionRegex).test(result.latestRelease) : true,
);
return latest;
const latest =
releases.length === 0
? formatErrorRelease(input.identifier, input.providerKey, { code: "noMatchingVersion" })
: releases.reduce(
(latest, result) => {
return {
...detailsResult,
...(result.latestReleaseAt > latest.latestReleaseAt ? result : latest),
identifier: input.identifier,
providerKey: input.providerKey,
};
},
{
identifier: "",
providerKey: "",
latestRelease: "",
latestReleaseAt: new Date(0),
},
);
return latest;
}
},
cacheDuration: dayjs.duration(5, "minutes"),
});
export type ResponseResponse = z.infer<typeof _reponseSchema>;
export type ReleaseResponse = z.infer<typeof _reponseSchema>;

View File

@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -26,8 +26,8 @@
"@homarr/api": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/dates": "^7.17.7",
"next": "15.3.1",
"@mantine/dates": "^8.0.0",
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0"
},
@@ -35,7 +35,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -33,12 +33,12 @@
"@homarr/settings": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^7.17.7",
"@mantine/hooks": "^7.17.7",
"@mantine/spotlight": "^7.17.7",
"@mantine/core": "^8.0.0",
"@mantine/hooks": "^8.0.0",
"@mantine/spotlight": "^8.0.0",
"@tabler/icons-react": "^3.31.0",
"jotai": "^2.12.3",
"next": "15.3.1",
"jotai": "^2.12.4",
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"use-deep-compare-effect": "^1.8.1"
@@ -47,7 +47,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -32,7 +32,7 @@
"dayjs": "^1.11.13",
"deepmerge": "4.3.1",
"mantine-react-table": "2.0.0-beta.9",
"next": "15.3.1",
"next": "15.3.2",
"next-intl": "4.1.0",
"react": "19.1.0",
"react-dom": "19.1.0"
@@ -41,7 +41,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -2059,11 +2059,11 @@
"option": {
"newReleaseWithin": {
"label": "New Release Within",
"description": "Usage example: 1w (1 week), 10m (10 months). Accepted unit types h (hours), d (days), w (weeks), m (months), y (years). Leave empty for no highlighting of new releases."
"description": "Usage example: 1w (1 week), 10M (10 months). Accepted unit types h (hours), d (days), w (weeks), M (months), y (years). Leave empty for no highlighting of new releases."
},
"staleReleaseWithin": {
"label": "Stale Release Within",
"description": "Usage example: 1w (1 week), 10m (10 months). Accepted unit types h (hours), d (days), w (weeks), m (months), y (years). Leave empty for no highlighting of stale releases."
"description": "Usage example: 1w (1 week), 10M (10 months). Accepted unit types h (hours), d (days), w (weeks), M (months), y (years). Leave empty for no highlighting of stale releases."
},
"showOnlyHighlighted": {
"label": "Show Only Highlighted",
@@ -2130,7 +2130,13 @@
"openProjectPage": "Open Project Page",
"openReleasePage": "Open Release Page",
"releaseDescription": "Release Description",
"created": "Created"
"created": "Created",
"error": {
"label": "Error",
"options": {
"noMatchingVersion": "No matching version found"
}
}
},
"networkControllerSummary": {
"option": {},

View File

@@ -258,7 +258,7 @@
},
"toLarge": {
"title": "Bilden är för stor",
"message": ""
"message": "Maximal bildstorlek är {size}"
}
}
},
@@ -612,17 +612,17 @@
"select": {
"label": "",
"notFound": "",
"search": "",
"search": "Sök efter en applikation",
"noResults": "",
"action": "",
"title": ""
"action": "Välj {app}",
"title": "Välj en applikation att lägga till på tavlan"
},
"create": {
"title": "Addera en ny applikation",
"description": "Addera en ny applikation ",
"action": ""
"action": "Addera applikation"
},
"add": ""
"add": "Lägg till en applikation"
}
},
"integration": {
@@ -954,7 +954,7 @@
"unsavedChanges": "",
"preview": {
"show": "Förhandsgranska",
"hide": ""
"hide": "Dölj förhandsgranskning"
},
"zod": {
"errors": {
@@ -990,7 +990,7 @@
"section": {
"dynamic": {
"action": {
"create": "",
"create": "Ny dynamisk sektion",
"remove": ""
},
"option": {
@@ -1013,7 +1013,7 @@
}
},
"action": {
"create": "",
"create": "Ny kategori",
"edit": "",
"remove": "",
"moveUp": "Flytta uppåt",
@@ -1024,7 +1024,7 @@
},
"create": {
"title": "",
"submit": ""
"submit": "Lägg till kategori"
},
"remove": {
"title": "",
@@ -1048,7 +1048,7 @@
},
"item": {
"action": {
"create": "",
"create": "Nytt objekt",
"import": "",
"edit": "Redigera objekt",
"moveResize": "",
@@ -1063,7 +1063,7 @@
"create": {
"title": "Välj objekt du vill lägga till",
"search": "",
"addToBoard": ""
"addToBoard": "Lägg till på tavlan"
},
"moveResize": {
"title": "",
@@ -1138,7 +1138,7 @@
},
"bookmarks": {
"name": "Bokmärken",
"description": "",
"description": "Visar länkar till flera applikationer",
"option": {
"title": {
"label": "Titel"
@@ -1252,7 +1252,7 @@
}
},
"clock": {
"name": "",
"name": "Datum och tid",
"description": "Visar aktuellt datum och tid.",
"option": {
"customTitleToggle": {
@@ -2298,16 +2298,16 @@
"label": "Namn på sidan"
},
"metaTitle": {
"label": ""
"label": "Metarubrik (visas i huvudet eller fliken i webbläsaren)"
},
"logoImageUrl": {
"label": "URL-adress till logo för tavlan"
},
"faviconImageUrl": {
"label": ""
"label": "URL-adress till bilden som visas som favoritbild"
},
"backgroundImageUrl": {
"label": "",
"label": "URL-adress till bakgrundsbilden",
"placeholder": "",
"group": {
"your": "",
@@ -2315,49 +2315,49 @@
}
},
"backgroundImageAttachment": {
"label": "Bilaga med bakgrundsbild",
"label": "Bakgrundsbildens beteende",
"option": {
"fixed": {
"label": "",
"description": ""
"label": "Fast",
"description": "Bakgrunden stannar i samma läge."
},
"scroll": {
"label": "",
"description": ""
"label": "Förflyttas",
"description": "Bakgrunden förflyttas med musens rörelse."
}
}
},
"backgroundImageRepeat": {
"label": "",
"label": "Upprepa bakgrundsbilden",
"option": {
"repeat": {
"label": "",
"description": ""
"label": "Upprepa",
"description": "Bilden kommer att upprepas så mycket som krävs för att täcka bakgrunden."
},
"no-repeat": {
"label": "",
"description": ""
"label": "Ingen upprepning",
"description": "Bilden upprepas inte och kommer eventuellt inte fylla hela bakgrunden."
},
"repeat-x": {
"label": "",
"description": ""
"label": "Upprepa horisontellt",
"description": "Samma sak om 'Upprepa' men endast horisontellt."
},
"repeat-y": {
"label": "",
"description": ""
"label": "Upprepa vertikalt",
"description": "Samma sak som 'Upprepa' men endast vertikalt."
}
}
},
"backgroundImageSize": {
"label": "Storlek på bakgrundsbild",
"label": "Storlek på bakgrundsbilden",
"option": {
"cover": {
"label": "",
"description": ""
"label": "Täck",
"description": "Gör bilden så liten som möjligt för att täcka hela tavlan genom att beskära överflödig den av bilden."
},
"contain": {
"label": "",
"description": ""
"label": "Maximera",
"description": "Gör bilden så stor som möjligt för att täcka hela tavlan utan att beskära eller sträcka ut bilden."
}
}
},
@@ -2411,7 +2411,7 @@
"metaTitle": ""
},
"setting": {
"title": "Inställningar för tavlan {boardName}",
"title": "Inställningar för tavlan \"{boardName}\"",
"section": {
"general": {
"title": "Generellt",
@@ -2430,7 +2430,7 @@
"title": "Bakgrund"
},
"appearance": {
"title": ""
"title": "Utseende"
},
"customCss": {
"title": ""
@@ -3388,11 +3388,11 @@
"label": "Grupp"
},
"permission": {
"label": ""
"label": "Behörighet"
}
},
"action": {
"saveUser": "",
"saveUser": "Spara behörighet",
"saveGroup": ""
}
},

View File

@@ -1950,7 +1950,7 @@
"approved": "Onaylandı",
"declined": "Reddedildi",
"failed": "Başarısız",
"completed": ""
"completed": "Tamamlandı"
},
"toBeDetermined": "-Yapım Aşamasında-"
},

View File

@@ -29,12 +29,12 @@
"@homarr/log": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^7.17.7",
"@mantine/dates": "^7.17.7",
"@mantine/hooks": "^7.17.7",
"@mantine/core": "^8.0.0",
"@mantine/dates": "^8.0.0",
"@mantine/hooks": "^8.0.0",
"@tabler/icons-react": "^3.31.0",
"mantine-react-table": "2.0.0-beta.9",
"next": "15.3.1",
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0"
},
@@ -43,7 +43,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/css-modules": "^1.0.5",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -24,14 +24,14 @@
"dependencies": {
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"zod": "^3.24.3",
"zod": "^3.24.4",
"zod-form-data": "^2.0.7"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -47,42 +47,42 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/charts": "^7.17.7",
"@mantine/core": "^7.17.7",
"@mantine/hooks": "^7.17.7",
"@mantine/charts": "^8.0.0",
"@mantine/core": "^8.0.0",
"@mantine/hooks": "^8.0.0",
"@tabler/icons-react": "^3.31.0",
"@tiptap/extension-color": "2.11.9",
"@tiptap/extension-highlight": "2.11.9",
"@tiptap/extension-image": "2.11.9",
"@tiptap/extension-link": "^2.11.9",
"@tiptap/extension-table": "2.11.9",
"@tiptap/extension-table-cell": "2.11.9",
"@tiptap/extension-table-header": "2.11.9",
"@tiptap/extension-table-row": "2.11.9",
"@tiptap/extension-task-item": "2.11.9",
"@tiptap/extension-task-list": "2.11.9",
"@tiptap/extension-text-align": "2.11.9",
"@tiptap/extension-text-style": "2.11.9",
"@tiptap/extension-underline": "2.11.9",
"@tiptap/react": "^2.11.9",
"@tiptap/starter-kit": "^2.11.9",
"@tiptap/extension-color": "2.12.0",
"@tiptap/extension-highlight": "2.12.0",
"@tiptap/extension-image": "2.12.0",
"@tiptap/extension-link": "^2.12.0",
"@tiptap/extension-table": "2.12.0",
"@tiptap/extension-table-cell": "2.12.0",
"@tiptap/extension-table-header": "2.12.0",
"@tiptap/extension-table-row": "2.12.0",
"@tiptap/extension-task-item": "2.12.0",
"@tiptap/extension-task-list": "2.12.0",
"@tiptap/extension-text-align": "2.12.0",
"@tiptap/extension-text-style": "2.12.0",
"@tiptap/extension-underline": "2.12.0",
"@tiptap/react": "^2.12.0",
"@tiptap/starter-kit": "^2.12.0",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"mantine-react-table": "2.0.0-beta.9",
"next": "15.3.1",
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-markdown": "^10.1.0",
"recharts": "^2.15.3",
"video.js": "^8.22.0",
"zod": "^3.24.3"
"zod": "^3.24.4"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/video.js": "^7.3.58",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -63,7 +63,7 @@ export const WidgetMultiReleasesRepositoriesInput = ({
const item = {
providerKey: "DockerHub",
identifier: "",
} as ReleasesRepository;
};
form.setValues((previous) => {
const previousValues = previous.options?.[property] as ReleasesRepository[];
@@ -98,7 +98,6 @@ export const WidgetMultiReleasesRepositoriesInput = ({
};
});
};
return (
<Fieldset legend={t("label")}>
<Stack gap="5">

View File

@@ -72,8 +72,8 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
return (
<Calendar
defaultDate={new Date()}
onPreviousMonth={setMonth}
onNextMonth={setMonth}
onPreviousMonth={(month) => setMonth(new Date(month))}
onNextMonth={(month) => setMonth(new Date(month))}
highlightToday
locale={locale}
hideWeekdays={false}
@@ -126,7 +126,7 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
.filter((event): event is CalendarEvent => Boolean(event.date));
return (
<CalendarDay
date={tileDate}
date={new Date(tileDate)}
events={eventsForDate}
disabled={isEditMode || eventsForDate.length === 0}
rootWidth={width}

View File

@@ -10,6 +10,7 @@ import {
IconGitFork,
IconProgressCheck,
IconStar,
IconTriangleFilled,
} from "@tabler/icons-react";
import combineClasses from "clsx";
import { useFormatter, useNow } from "next-intl";
@@ -17,118 +18,116 @@ import ReactMarkdown from "react-markdown";
import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import { isDateWithin, splitToChunksWithNItems } from "@homarr/common";
import { useScopedI18n } from "@homarr/translation/client";
import { MaskedOrNormalImage } from "@homarr/ui";
import type { WidgetComponentProps } from "../definition";
import classes from "./component.module.scss";
import { Providers } from "./releases-providers";
import type { ReleasesRepository } from "./releases-repository";
import type { ReleasesRepositoryResponse } from "./releases-repository";
function isDateWithin(date: Date, relativeDate: string): boolean {
const amount = parseInt(relativeDate.slice(0, -1), 10);
const unit = relativeDate.slice(-1);
const startTime = new Date().getTime();
const endTime = new Date(date).getTime();
const diffTime = Math.abs(endTime - startTime);
const diffHours = Math.ceil(diffTime / (1000 * 60 * 60));
switch (unit) {
case "h":
return diffHours < amount;
case "d":
return diffHours / 24 < amount;
case "w":
return diffHours / (24 * 7) < amount;
case "m":
return diffHours / (24 * 30) < amount;
case "y":
return diffHours / (24 * 365) < amount;
default:
throw new Error("Invalid unit");
}
}
const formatRelativeDate = (value: string): string => {
const isMonths = /\d+m/g.test(value);
const isOtherUnits = /\d+[HDWY]/g.test(value);
return isMonths ? value.toUpperCase() : isOtherUnits ? value.toLowerCase() : value;
};
export default function ReleasesWidget({ options }: WidgetComponentProps<"releases">) {
const t = useScopedI18n("widget.releases");
const now = useNow();
const formatter = useFormatter();
const board = useRequiredBoard();
const [expandedRepository, setExpandedRepository] = useState("");
const [expandedRepository, setExpandedRepository] = useState({ providerKey: "", identifier: "" });
const hasIconColor = useMemo(() => board.iconColor !== null, [board.iconColor]);
const relativeDateOptions = useMemo(
() => ({
newReleaseWithin: formatRelativeDate(options.newReleaseWithin),
staleReleaseWithin: formatRelativeDate(options.staleReleaseWithin),
}),
[options.newReleaseWithin, options.staleReleaseWithin],
);
const [results] = clientApi.widget.releases.getLatest.useSuspenseQuery(
{
repositories: options.repositories.map((repository) => ({
providerKey: repository.providerKey,
identifier: repository.identifier,
versionFilter: repository.versionFilter,
})),
},
{
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: false,
},
const batchedRepositories = useMemo(() => splitToChunksWithNItems(options.repositories, 5), [options.repositories]);
const [results] = clientApi.useSuspenseQueries((t) =>
batchedRepositories.flatMap((chunk) =>
t.widget.releases.getLatest({
repositories: chunk.map((repository) => ({
providerKey: repository.providerKey,
identifier: repository.identifier,
versionFilter: repository.versionFilter,
})),
}),
),
);
const repositories = useMemo(() => {
return results
.flat()
.map(({ data }) => {
if (data === undefined) return undefined;
const repository = options.repositories.find(
(repository: ReleasesRepository) =>
repository.providerKey === data.providerKey && repository.identifier === data.identifier,
(repository) => repository.providerKey === data.providerKey && repository.identifier === data.identifier,
);
if (repository === undefined) return undefined;
return {
...repository,
...data,
iconUrl: repository.iconUrl,
isNewRelease:
options.newReleaseWithin !== "" ? isDateWithin(data.latestReleaseAt, options.newReleaseWithin) : false,
relativeDateOptions.newReleaseWithin !== "" && data.latestReleaseAt
? isDateWithin(data.latestReleaseAt, relativeDateOptions.newReleaseWithin)
: false,
isStaleRelease:
options.staleReleaseWithin !== "" ? !isDateWithin(data.latestReleaseAt, options.staleReleaseWithin) : false,
relativeDateOptions.staleReleaseWithin !== "" && data.latestReleaseAt
? !isDateWithin(data.latestReleaseAt, relativeDateOptions.staleReleaseWithin)
: false,
};
})
.filter(
(repository) =>
repository !== undefined &&
(!options.showOnlyHighlighted || repository.isNewRelease || repository.isStaleRelease),
(repository.error !== undefined ||
!options.showOnlyHighlighted ||
repository.isNewRelease ||
repository.isStaleRelease),
)
.sort((repoA, repoB) => {
if (repoA?.latestReleaseAt === undefined) return 1;
if (repoB?.latestReleaseAt === undefined) return -1;
return repoA.latestReleaseAt > repoB.latestReleaseAt ? -1 : 1;
}) as ReleasesRepository[];
}) as ReleasesRepositoryResponse[];
}, [
results,
options.repositories,
options.showOnlyHighlighted,
options.newReleaseWithin,
options.staleReleaseWithin,
relativeDateOptions.newReleaseWithin,
relativeDateOptions.staleReleaseWithin,
]);
const toggleExpandedRepository = useCallback(
(identifier: string) => {
setExpandedRepository(expandedRepository === identifier ? "" : identifier);
(repository: ReleasesRepositoryResponse) => {
if (
expandedRepository.providerKey === repository.providerKey &&
expandedRepository.identifier === repository.identifier
) {
setExpandedRepository({ providerKey: "", identifier: "" });
} else {
setExpandedRepository({ providerKey: repository.providerKey, identifier: repository.identifier });
}
},
[expandedRepository],
);
return (
<Stack gap={0}>
{repositories.map((repository: ReleasesRepository) => {
const isActive = expandedRepository === repository.identifier;
{repositories.map((repository: ReleasesRepositoryResponse) => {
const isActive =
expandedRepository.providerKey === repository.providerKey &&
expandedRepository.identifier === repository.identifier;
const hasError = repository.error !== undefined;
return (
<Stack
@@ -141,7 +140,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
[classes.active ?? ""]: isActive,
})}
p="xs"
onClick={() => toggleExpandedRepository(repository.identifier)}
onClick={() => toggleExpandedRepository(repository)}
>
<MaskedOrNormalImage
imageUrl={repository.iconUrl ?? Providers[repository.providerKey]?.iconUrl}
@@ -155,9 +154,14 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
<Group gap={5} justify="space-between" style={{ flex: 1 }}>
<Text size="xs">{repository.identifier}</Text>
<Tooltip label={repository.latestRelease ?? t("not-found")}>
<Text size="xs" fw={700} truncate="end" style={{ flexShrink: 1 }}>
{repository.latestRelease ?? t("not-found")}
<Tooltip
withArrow
arrowSize={5}
label={repository.latestRelease}
events={{ hover: repository.latestRelease !== undefined, focus: false, touch: false }}
>
<Text size="xs" fw={700} truncate="end" c={hasError ? "red" : "text"} style={{ flexShrink: 1 }}>
{hasError ? t("error.label") : (repository.latestRelease ?? t("not-found"))}
</Text>
</Tooltip>
</Group>
@@ -168,20 +172,25 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
c={repository.isNewRelease ? "primaryColor" : repository.isStaleRelease ? "secondaryColor" : "dimmed"}
>
{repository.latestReleaseAt &&
!hasError &&
formatter.relativeTime(repository.latestReleaseAt, {
now,
style: "narrow",
})}
</Text>
{(repository.isNewRelease || repository.isStaleRelease) && (
<IconCircleFilled
size={10}
color={
repository.isNewRelease
? "var(--mantine-color-primaryColor-filled)"
: "var(--mantine-color-secondaryColor-filled)"
}
/>
{!hasError ? (
(repository.isNewRelease || repository.isStaleRelease) && (
<IconCircleFilled
size={10}
color={
repository.isNewRelease
? "var(--mantine-color-primaryColor-filled)"
: "var(--mantine-color-secondaryColor-filled)"
}
/>
)
) : (
<IconTriangleFilled size={10} color={"var(--mantine-color-red-filled)"} />
)}
</Group>
</Group>
@@ -198,8 +207,8 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
}
interface DetailsDisplayProps {
repository: ReleasesRepository;
toggleExpandedRepository: (identifier: string) => void;
repository: ReleasesRepositoryResponse;
toggleExpandedRepository: (repository: ReleasesRepositoryResponse) => void;
}
const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplayProps) => {
@@ -208,15 +217,15 @@ const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplay
return (
<>
<Divider onClick={() => toggleExpandedRepository(repository.identifier)} />
<Divider onClick={() => toggleExpandedRepository(repository)} />
<Group
className={classes.releasesRepositoryDetails}
justify="space-between"
p={5}
onClick={() => toggleExpandedRepository(repository.identifier)}
onClick={() => toggleExpandedRepository(repository)}
>
<Group>
<Tooltip label={t("pre-release")}>
<Tooltip label={t("pre-release")} withArrow arrowSize={5}>
<IconProgressCheck
size={13}
color={
@@ -225,14 +234,14 @@ const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplay
/>
</Tooltip>
<Tooltip label={t("archived")}>
<Tooltip label={t("archived")} withArrow arrowSize={5}>
<IconArchive
size={13}
color={repository.isArchived ? "var(--mantine-color-secondaryColor-text)" : "var(--mantine-color-dimmed)"}
/>
</Tooltip>
<Tooltip label={t("forked")}>
<Tooltip label={t("forked")} withArrow arrowSize={5}>
<IconGitFork
size={13}
color={repository.isFork ? "var(--mantine-color-secondaryColor-text)" : "var(--mantine-color-dimmed)"}
@@ -240,16 +249,16 @@ const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplay
</Tooltip>
</Group>
<Group>
<Tooltip label={t("starsCount")}>
<Tooltip label={t("starsCount")} withArrow arrowSize={5}>
<Group gap={5}>
<IconStar
size={12}
color={repository.starsCount === 0 ? "var(--mantine-color-dimmed)" : "var(--mantine-color-text)"}
color={!repository.starsCount ? "var(--mantine-color-dimmed)" : "var(--mantine-color-text)"}
/>
<Text size="xs" c={repository.starsCount === 0 ? "dimmed" : ""}>
{repository.starsCount === 0
<Text size="xs" c={!repository.starsCount ? "dimmed" : ""}>
{!repository.starsCount
? "-"
: formatter.number(repository.starsCount ?? 0, {
: formatter.number(repository.starsCount, {
notation: "compact",
maximumFractionDigits: 1,
})}
@@ -257,16 +266,16 @@ const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplay
</Group>
</Tooltip>
<Tooltip label={t("forksCount")}>
<Tooltip label={t("forksCount")} withArrow arrowSize={5}>
<Group gap={5}>
<IconGitFork
size={12}
color={repository.forksCount === 0 ? "var(--mantine-color-dimmed)" : "var(--mantine-color-text)"}
color={!repository.forksCount ? "var(--mantine-color-dimmed)" : "var(--mantine-color-text)"}
/>
<Text size="xs" c={repository.forksCount === 0 ? "dimmed" : ""}>
{repository.forksCount === 0
<Text size="xs" c={!repository.forksCount ? "dimmed" : ""}>
{!repository.forksCount
? "-"
: formatter.number(repository.forksCount ?? 0, {
: formatter.number(repository.forksCount, {
notation: "compact",
maximumFractionDigits: 1,
})}
@@ -274,16 +283,16 @@ const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplay
</Group>
</Tooltip>
<Tooltip label={t("issuesCount")}>
<Tooltip label={t("issuesCount")} withArrow arrowSize={5}>
<Group gap={5}>
<IconCircleDot
size={12}
color={repository.openIssues === 0 ? "var(--mantine-color-dimmed)" : "var(--mantine-color-text)"}
color={!repository.openIssues ? "var(--mantine-color-dimmed)" : "var(--mantine-color-text)"}
/>
<Text size="xs" c={repository.openIssues === 0 ? "dimmed" : ""}>
{repository.openIssues === 0
<Text size="xs" c={!repository.openIssues ? "dimmed" : ""}>
{!repository.openIssues
? "-"
: formatter.number(repository.openIssues ?? 0, {
: formatter.number(repository.openIssues, {
notation: "compact",
maximumFractionDigits: 1,
})}
@@ -297,7 +306,7 @@ const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplay
};
interface ExtendedDisplayProps {
repository: ReleasesRepository;
repository: ReleasesRepositoryResponse;
hasIconColor: boolean;
}
@@ -337,17 +346,32 @@ const ExpandedDisplay = ({ repository, hasIconColor }: ExtendedDisplayProps) =>
</Text>
)}
</Group>
<Divider my={10} mx="30%" />
<Button
variant="light"
component="a"
href={repository.releaseUrl ?? repository.projectUrl}
target="_blank"
rel="noreferrer"
>
<IconExternalLink />
{repository.releaseUrl ? t("openReleasePage") : t("openProjectPage")}
</Button>
{(repository.releaseUrl ?? repository.projectUrl) && (
<>
<Divider my={10} mx="30%" />
<Button
variant="light"
component="a"
href={repository.releaseUrl ?? repository.projectUrl}
target="_blank"
rel="noreferrer"
>
<IconExternalLink />
{repository.releaseUrl ? t("openReleasePage") : t("openProjectPage")}
</Button>
</>
)}
{repository.error && (
<>
<Divider my={10} mx="30%" />
<Title order={4} ta="center">
{t("error.label")}
</Title>
<Text size="xs" ff="monospace" c="red" style={{ whiteSpace: "pre-wrap" }}>
{repository.error.code ? t(`error.options.${repository.error.code}` as never) : repository.error.message}
</Text>
</>
)}
{repository.releaseDescription && (
<>
<Divider my={10} mx="30%" />

View File

@@ -4,6 +4,11 @@ import { z } from "zod";
import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options";
const relativeDateSchema = z
.string()
.regex(/^\d+[hdwmyHDWMY]$/)
.or(z.literal(""));
export const { definition, componentLoader } = createWidgetDefinition("releases", {
icon: IconRocket,
createOptions() {
@@ -11,18 +16,12 @@ export const { definition, componentLoader } = createWidgetDefinition("releases"
newReleaseWithin: factory.text({
defaultValue: "1w",
withDescription: true,
validate: z
.string()
.regex(/^\d+[hdwmy]$/)
.or(z.literal("")),
validate: relativeDateSchema,
}),
staleReleaseWithin: factory.text({
defaultValue: "6m",
defaultValue: "6M",
withDescription: true,
validate: z
.string()
.regex(/^\d+[hdwmy]$/)
.or(z.literal("")),
validate: relativeDateSchema,
}),
showOnlyHighlighted: factory.switch({
withDescription: true,

View File

@@ -9,7 +9,9 @@ export interface ReleasesRepository {
identifier: string;
versionFilter?: ReleasesVersionFilter;
iconUrl?: string;
}
export interface ReleasesRepositoryResponse extends ReleasesRepository {
latestRelease?: string;
latestReleaseAt?: Date;
isNewRelease: boolean;
@@ -27,4 +29,6 @@ export interface ReleasesRepository {
starsCount?: number;
forksCount?: number;
openIssues?: number;
error?: { code?: string; message?: string };
}

2441
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,19 +17,19 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@next/eslint-plugin-next": "15.3.1",
"eslint-config-prettier": "^10.1.2",
"eslint-config-turbo": "^2.5.2",
"@next/eslint-plugin-next": "15.3.2",
"eslint-config-prettier": "^10.1.5",
"eslint-config-turbo": "^2.5.3",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"typescript-eslint": "^8.31.1"
"typescript-eslint": "^8.32.0"
},
"devDependencies": {
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1",
"eslint": "^9.26.0",
"typescript": "^5.8.3"
}
}

View File

@@ -15,7 +15,7 @@
},
"devDependencies": {
"@homarr/tsconfig": "workspace:^0.1.0",
"prettier-plugin-packagejson": "^2.5.10",
"prettier-plugin-packagejson": "^2.5.11",
"typescript": "^5.8.3"
}
}