chore(custom-widgets): merge v2

This commit is contained in:
Thomas Camlong committed 2026-07-20 14:29:49 +02:00
commit aebc7016b1
36 files changed
+7351 -209

No files matched your search

+2 -15
View File
@@ -186,21 +186,8 @@ jobs:
path: e2e/test-results/
retention-days: 7
- name: Log in to GHCR for preview publication
if: |
!env.ACT &&
(
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
)
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push amd64 preview image by digest
id: preview-build
- name: Determine preview tag
id: preview
if: |
!env.ACT &&
(
+2
View File
@@ -1,3 +1,5 @@
## [1.71.0](https://github.com/homarr-labs/homarr/compare/v1.70.0...v1.71.0) (2026-07-17)
## [1.70.0](https://github.com/homarr-labs/homarr/compare/v1.69.2...v1.70.0) (2026-07-10)
## [1.69.2](https://github.com/homarr-labs/homarr/compare/v1.69.1...v1.69.2) (2026-07-06)
@@ -193,6 +193,10 @@ export function DockerTable({ initialData }: DockerTableProps) {
positionToolbarAlertBanner: "top",
enableTableFooter: false,
enableBottomToolbar: false,
// MRT's built-in full-screen mode does not stretch the table to fill the
// viewport, so with few containers it renders as a thin strip (#6230). Disable
// it, matching the other management tables (users, invites).
enableFullScreenToggle: false,
positionGlobalFilter: "right",
mantineSearchTextInputProps: {
placeholder: tDocker("table.search", { count: String(containers.length) }),
@@ -31,6 +31,7 @@ import {
userChangeHomeBoardsSchema,
userChangeSearchPreferencesSchema,
userEditProfileSchema,
userEnableRightClickOnWidgetsSchema,
userFirstDayOfWeekSchema,
userPingIconsEnabledSchema,
} from "@homarr/validation/user";
@@ -51,6 +52,7 @@ const userGeneralSettingsSchema = z.object({
ddgBangsEnabled: userChangeSearchPreferencesSchema.shape.ddgBangsEnabled,
firstDayOfWeek: userFirstDayOfWeekSchema.shape.firstDayOfWeek,
pingIconsEnabled: userPingIconsEnabledSchema.shape.pingIconsEnabled,
enableRightClickOnWidgets: userEnableRightClickOnWidgetsSchema.shape.enableRightClickOnWidgets,
});
type FormValues = z.infer<typeof userGeneralSettingsSchema>;
@@ -78,6 +80,7 @@ const buildInitialValues = (user: RouterOutputs["user"]["getById"]): FormValues
ddgBangsEnabled: user.ddgBangs,
firstDayOfWeek: user.firstDayOfWeek as DayOfWeek,
pingIconsEnabled: user.pingIconsEnabled,
enableRightClickOnWidgets: user.enableRightClickOnWidgets,
});
export const UserGeneralSettingsForm = ({
@@ -95,6 +98,7 @@ export const UserGeneralSettingsForm = ({
const changeSearchPreferencesMutation = clientApi.user.changeSearchPreferences.useMutation();
const changeFirstDayOfWeekMutation = clientApi.user.changeFirstDayOfWeek.useMutation();
const changePingIconsEnabledMutation = clientApi.user.changePingIconsEnabled.useMutation();
const changeEnableRightClickOnWidgetsMutation = clientApi.user.changeEnableRightClickOnWidgets.useMutation();
const initialValues = buildInitialValues(user);
const initialValuesRef = useRef(initialValues);
@@ -114,6 +118,7 @@ export const UserGeneralSettingsForm = ({
changeSearchPreferencesMutation,
changeFirstDayOfWeekMutation,
changePingIconsEnabledMutation,
changeEnableRightClickOnWidgetsMutation,
];
const isPending = mutations.some((m) => m.isPending);
@@ -169,6 +174,14 @@ export const UserGeneralSettingsForm = ({
action: () =>
changePingIconsEnabledMutation.mutateAsync({ id: user.id, pingIconsEnabled: parsed.data.pingIconsEnabled }),
},
{
when: changed("enableRightClickOnWidgets"),
action: () =>
changeEnableRightClickOnWidgetsMutation.mutateAsync({
id: user.id,
enableRightClickOnWidgets: parsed.data.enableRightClickOnWidgets,
}),
},
];
const promises = saveActions.filter((s) => s.when).map((s) => s.action());
@@ -305,6 +318,11 @@ export const UserGeneralSettingsForm = ({
label={t("user.field.pingIconsEnabled.label")}
{...form.getInputProps("pingIconsEnabled", { type: "checkbox" })}
/>
<Switch
label={t("user.field.enableRightClickOnWidgets.label")}
description={t("user.field.enableRightClickOnWidgets.description")}
{...form.getInputProps("enableRightClickOnWidgets", { type: "checkbox" })}
/>
</Stack>
</Card>
</SimpleGrid>
@@ -48,6 +48,7 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont
const tMenu = useScopedI18n("item.menu.label");
const t = useI18n();
const settings = useSettings();
const isRightClickEnabled = settings.enableRightClickOnWidgets;
const { openModal } = useModalAction(WidgetEditModal);
const { openModal: openMoveModal } = useModalAction(ItemMoveModal);
const { openConfirmModal } = useConfirmModal();
@@ -168,6 +169,7 @@ export const WidgetContextMenu = ({ item, widgetStateRef, children }: WidgetCont
);
if (!session) return <>{children}</>;
if (!isRightClickEnabled) return <>{children}</>;
const visibleWidgetActions = widgetContextActions.filter((a) => !a.hidden);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.70.0",
"version": "1.71.0",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -3,7 +3,7 @@ import { z } from "zod/v4";
import { createId } from "@homarr/common";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { asc, eq, like } from "@homarr/db";
import { and, asc, eq, like } from "@homarr/db";
import { getServerSettingByKeyAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
import { searchEngines, users } from "@homarr/db/schema";
import { byIdSchema, paginatedSchema, searchSchema } from "@homarr/validation/common";
@@ -130,9 +130,15 @@ export const searchEngineRouter = createTRPCRouter({
return null;
}),
search: protectedProcedure.input(searchSchema).query(async ({ ctx, input }) => {
search: publicProcedure.input(searchSchema).query(async ({ ctx, input }) => {
return await ctx.db.query.searchEngines.findMany({
where: like(searchEngines.short, `${input.query.toLowerCase().trim()}%`),
// Public dashboards have no session: restrict anonymous users to generic
// (non-integration) engines so custom search engines work there too (#4132),
// while integration-backed engines stay available only when signed in.
where: and(
like(searchEngines.short, `${input.query.toLowerCase().trim()}%`),
ctx.session?.user ? undefined : eq(searchEngines.type, "generic"),
),
with: {
integration: {
columns: {
+77
View File
@@ -357,6 +357,83 @@ describe("delete should delete user", () => {
});
});
describe("changeEnableRightClickOnWidgets should toggle the right-click preference", () => {
test("non-admin can toggle their own preference", async () => {
const db = createDb();
const caller = userRouter.createCaller({
db,
deviceType: undefined,
session: defaultSession,
});
await db.insert(users).values({
id: defaultOwnerId,
name: "owner",
});
await caller.changeEnableRightClickOnWidgets({ id: defaultOwnerId, enableRightClickOnWidgets: false });
const updated = await db.query.users.findFirst({
where: eq(users.id, defaultOwnerId),
columns: { enableRightClickOnWidgets: true },
});
expect(updated?.enableRightClickOnWidgets).toBe(false);
await caller.changeEnableRightClickOnWidgets({ id: defaultOwnerId, enableRightClickOnWidgets: true });
const restored = await db.query.users.findFirst({
where: eq(users.id, defaultOwnerId),
columns: { enableRightClickOnWidgets: true },
});
expect(restored?.enableRightClickOnWidgets).toBe(true);
});
test("non-admin cannot toggle another user's preference", async () => {
const db = createDb();
const caller = userRouter.createCaller({
db,
deviceType: undefined,
session: defaultSession,
});
const otherUserId = createId();
await db.insert(users).values({ id: defaultOwnerId });
await db.insert(users).values({ id: otherUserId, name: "other" });
await expect(
caller.changeEnableRightClickOnWidgets({ id: otherUserId, enableRightClickOnWidgets: false }),
).rejects.toThrow("User not found");
const other = await db.query.users.findFirst({
where: eq(users.id, otherUserId),
columns: { enableRightClickOnWidgets: true },
});
expect(other?.enableRightClickOnWidgets).toBe(true);
});
test("admin can toggle another user's preference", async () => {
const db = createDb();
const adminSession = createSession(["admin"]);
const caller = userRouter.createCaller({
db,
deviceType: undefined,
session: adminSession,
});
const targetUserId = createId();
await db.insert(users).values({ id: defaultOwnerId });
await db.insert(users).values({ id: targetUserId, name: "target" });
await caller.changeEnableRightClickOnWidgets({ id: targetUserId, enableRightClickOnWidgets: false });
const target = await db.query.users.findFirst({
where: eq(users.id, targetUserId),
columns: { enableRightClickOnWidgets: true },
});
expect(target?.enableRightClickOnWidgets).toBe(false);
});
});
const createOnboardingStepAsync = async (db: Database, step: OnboardingStep) => {
await db.insert(onboarding).values({
id: createId(),
+29
View File
@@ -23,6 +23,7 @@ import {
userCreateSchema,
userDdgBangsSchema,
userEditProfileSchema,
userEnableRightClickOnWidgetsSchema,
userFirstDayOfWeekSchema,
userInitSchema,
userPingIconsEnabledSchema,
@@ -345,6 +346,7 @@ export const userRouter = createTRPCRouter({
mobileHomeBoardId: true,
firstDayOfWeek: true,
pingIconsEnabled: true,
enableRightClickOnWidgets: true,
defaultSearchEngineId: true,
openSearchInNewTab: true,
ddgBangs: true,
@@ -381,6 +383,7 @@ export const userRouter = createTRPCRouter({
mobileHomeBoardId: true,
firstDayOfWeek: true,
pingIconsEnabled: true,
enableRightClickOnWidgets: true,
defaultSearchEngineId: true,
openSearchInNewTab: true,
ddgBangs: true,
@@ -651,6 +654,32 @@ export const userRouter = createTRPCRouter({
})
.where(eq(users.id, ctx.session.user.id));
}),
changeEnableRightClickOnWidgets: protectedProcedure
.meta({
openapi: {
method: "PATCH",
path: "/api/users/right-click-widgets",
tags: ["users"],
protect: true,
},
})
.input(convertIntersectionToZodObject(userEnableRightClickOnWidgetsSchema.and(byIdSchema)))
.output(z.void())
.mutation(async ({ input, ctx }) => {
if (!ctx.session.user.permissions.includes("admin") && ctx.session.user.id !== input.id) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
await ctx.db
.update(users)
.set({
enableRightClickOnWidgets: input.enableRightClickOnWidgets,
})
.where(eq(users.id, input.id));
}),
changePingIconsEnabled: protectedProcedure
.input(userPingIconsEnabledSchema.and(byIdSchema))
.mutation(async ({ input, ctx }) => {
@@ -0,0 +1 @@
ALTER TABLE `user` ADD `enable_right_click_on_widgets` boolean DEFAULT true NOT NULL;
@@ -1,7 +1,7 @@
{
"version": "5",
"dialect": "mysql",
"id": "1a10d79c-6c82-4036-8717-0283ebaece10",
"id": "073d408e-ff80-4a57-8007-f1f32da1c71c",
"prevId": "4df22d3b-b69e-4afc-9d82-ac73d3c6e100",
"tables": {
"account": {
@@ -565,48 +565,51 @@
"notNull": false,
"autoincrement": false
},
"sources": {
"name": "sources",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"requests": {
"name": "requests",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"options_schema": {
"name": "options_schema",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"default_options": {
"name": "default_options",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"template": {
"name": "template",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"enabled": {
"name": "enabled",
"type": "boolean",
"auth_type": {
"name": "auth_type",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
"default": "'none'"
},
"header_name": {
"name": "header_name",
"type": "varchar(256)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"method": {
"name": "method",
"type": "varchar(16)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'GET'"
},
"request_body": {
"name": "request_body",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"display_type": {
"name": "display_type",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'singleValue'"
},
"display_config": {
"name": "display_config",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "('{\"json\": {}}')"
},
"created_at": {
"name": "created_at",
@@ -630,6 +633,21 @@
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"enabled": {
"name": "enabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
}
},
"indexes": {},
@@ -656,13 +674,6 @@
"custom_widget_secret": {
"name": "custom_widget_secret",
"columns": {
"source_id": {
"name": "source_id",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"kind": {
"name": "kind",
"type": "varchar(64)",
@@ -670,8 +681,8 @@
"notNull": true,
"autoincrement": false
},
"encrypted_value": {
"name": "encrypted_value",
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
@@ -694,8 +705,8 @@
},
"indexes": {},
"foreignKeys": {
"custom_widget_secret_definition_id_custom_widget_definition_id_fk": {
"name": "custom_widget_secret_definition_id_custom_widget_definition_id_fk",
"cw_secret_definition_id_cw_definition_id_fk": {
"name": "cw_secret_definition_id_cw_definition_id_fk",
"tableFrom": "custom_widget_secret",
"tableTo": "custom_widget_definition",
"columnsFrom": ["definition_id"],
@@ -705,9 +716,9 @@
}
},
"compositePrimaryKeys": {
"custom_widget_secret_definition_id_source_id_kind_pk": {
"name": "custom_widget_secret_definition_id_source_id_kind_pk",
"columns": ["definition_id", "source_id", "kind"]
"custom_widget_secret_definition_id_kind_pk": {
"name": "custom_widget_secret_definition_id_kind_pk",
"columns": ["definition_id", "kind"]
}
},
"uniqueConstraints": {},
@@ -2043,42 +2054,6 @@
"uniqueConstraints": {},
"checkConstraint": {}
},
"trusted_certificate_hostname": {
"name": "trusted_certificate_hostname",
"columns": {
"hostname": {
"name": "hostname",
"type": "varchar(256)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"thumbprint": {
"name": "thumbprint",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"certificate": {
"name": "certificate",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"trusted_certificate_hostname_hostname_thumbprint_pk": {
"name": "trusted_certificate_hostname_hostname_thumbprint_pk",
"columns": ["hostname", "thumbprint"]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"user": {
"name": "user",
"columns": {
@@ -2193,6 +2168,14 @@
"autoincrement": false,
"default": false
},
"enable_right_click_on_widgets": {
"name": "enable_right_click_on_widgets",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"completed_manage_tour": {
"name": "completed_manage_tour",
"type": "boolean",
@@ -2285,6 +2268,42 @@
"uniqueConstraints": {},
"checkConstraint": {}
},
"trusted_certificate_hostname": {
"name": "trusted_certificate_hostname",
"columns": {
"hostname": {
"name": "hostname",
"type": "varchar(256)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"thumbprint": {
"name": "thumbprint",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"certificate": {
"name": "certificate",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"trusted_certificate_hostname_hostname_thumbprint_pk": {
"name": "trusted_certificate_hostname_hostname_thumbprint_pk",
"columns": ["hostname", "thumbprint"]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"widget_secret": {
"name": "widget_secret",
"columns": {
@@ -2331,8 +2350,8 @@
},
"compositePrimaryKeys": {
"widget_secret_item_id_kind_pk": {
"name": "widget_secret_item_id_kind_pk",
"columns": ["item_id", "kind"]
"columns": ["item_id", "kind"],
"name": "widget_secret_item_id_kind_pk"
}
},
"uniqueConstraints": {},
File diff suppressed because it is too large. Load diff
@@ -306,8 +306,15 @@
{
"idx": 43,
"version": "5",
"when": 1783852800000,
"tag": "0043_add_right_click_preference",
"breakpoints": true
},
{
"idx": 44,
"version": "5",
"when": 1784305537309,
"tag": "0043_custom_widget_v2_reset",
"tag": "0044_custom_widget_v2_reset",
"breakpoints": true
}
]
@@ -0,0 +1 @@
ALTER TABLE "user" ADD COLUMN "enable_right_click_on_widgets" boolean DEFAULT true NOT NULL;
@@ -1,5 +1,5 @@
{
"id": "f2cd8146-b478-4617-870e-f601fb437d15",
"id": "97bda61f-0a71-4e41-8e60-874aafe17115",
"prevId": "39cf38d0-0503-4c79-b7f4-632fa062fa2a",
"version": "7",
"dialect": "postgresql",
@@ -526,35 +526,51 @@
"primaryKey": false,
"notNull": false
},
"sources": {
"name": "sources",
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true
},
"requests": {
"name": "requests",
"type": "text",
"auth_type": {
"name": "auth_type",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true
"notNull": true,
"default": "'none'"
},
"options_schema": {
"name": "options_schema",
"type": "text",
"header_name": {
"name": "header_name",
"type": "varchar(256)",
"primaryKey": false,
"notNull": true
"notNull": false
},
"default_options": {
"name": "default_options",
"type": "text",
"method": {
"name": "method",
"type": "varchar(16)",
"primaryKey": false,
"notNull": true
"notNull": true,
"default": "'GET'"
},
"template": {
"name": "template",
"request_body": {
"name": "request_body",
"type": "text",
"primaryKey": false,
"notNull": true
"notNull": false
},
"display_type": {
"name": "display_type",
"type": "varchar(32)",
"primaryKey": false,
"notNull": true,
"default": "'singleValue'"
},
"display_config": {
"name": "display_config",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'{\"json\": {}}'"
},
"enabled": {
"name": "enabled",
@@ -606,20 +622,14 @@
"name": "custom_widget_secret",
"schema": "",
"columns": {
"source_id": {
"name": "source_id",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"kind": {
"name": "kind",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"encrypted_value": {
"name": "encrypted_value",
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true
@@ -650,9 +660,9 @@
}
},
"compositePrimaryKeys": {
"custom_widget_secret_definition_id_source_id_kind_pk": {
"name": "custom_widget_secret_definition_id_source_id_kind_pk",
"columns": ["definition_id", "source_id", "kind"]
"custom_widget_secret_definition_id_kind_pk": {
"name": "custom_widget_secret_definition_id_kind_pk",
"columns": ["definition_id", "kind"]
}
},
"uniqueConstraints": {},
@@ -1940,42 +1950,6 @@
"checkConstraints": {},
"isRLSEnabled": false
},
"public.trusted_certificate_hostname": {
"name": "trusted_certificate_hostname",
"schema": "",
"columns": {
"hostname": {
"name": "hostname",
"type": "varchar(256)",
"primaryKey": false,
"notNull": true
},
"thumbprint": {
"name": "thumbprint",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true
},
"certificate": {
"name": "certificate",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"trusted_certificate_hostname_hostname_thumbprint_pk": {
"name": "trusted_certificate_hostname_hostname_thumbprint_pk",
"columns": ["hostname", "thumbprint"]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.user": {
"name": "user",
"schema": "",
@@ -2076,6 +2050,13 @@
"notNull": true,
"default": false
},
"enable_right_click_on_widgets": {
"name": "enable_right_click_on_widgets",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": true
},
"completed_manage_tour": {
"name": "completed_manage_tour",
"type": "boolean",
@@ -2214,6 +2195,42 @@
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.trusted_certificate_hostname": {
"name": "trusted_certificate_hostname",
"schema": "",
"columns": {
"hostname": {
"name": "hostname",
"type": "varchar(256)",
"primaryKey": false,
"notNull": true
},
"thumbprint": {
"name": "thumbprint",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true
},
"certificate": {
"name": "certificate",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"trusted_certificate_hostname_hostname_thumbprint_pk": {
"name": "trusted_certificate_hostname_hostname_thumbprint_pk",
"columns": ["hostname", "thumbprint"]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
File diff suppressed because it is too large. Load diff
@@ -68,8 +68,15 @@
{
"idx": 9,
"version": "7",
"when": 1783786677702,
"tag": "0009_add_right_click_preference",
"breakpoints": true
},
{
"idx": 10,
"version": "7",
"when": 1784305412862,
"tag": "0009_custom_widget_v2_reset",
"tag": "0010_custom_widget_v2_reset",
"breakpoints": true
}
]
@@ -0,0 +1 @@
ALTER TABLE `user` ADD `enable_right_click_on_widgets` integer DEFAULT true NOT NULL;
@@ -1,7 +1,7 @@
{
"version": "6",
"dialect": "sqlite",
"id": "30bbf6f8-3a3c-4cb0-994e-9d8494e4fcd0",
"id": "852e049b-72cc-4d7f-8be9-931c042fe019",
"prevId": "d76abdce-ba49-450b-81f0-621bcf581b90",
"tables": {
"account": {
@@ -546,40 +546,58 @@
"notNull": false,
"autoincrement": false
},
"sources": {
"name": "sources",
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"requests": {
"name": "requests",
"auth_type": {
"name": "auth_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'none'"
},
"header_name": {
"name": "header_name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"options_schema": {
"name": "options_schema",
"method": {
"name": "method",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'GET'"
},
"request_body": {
"name": "request_body",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"default_options": {
"name": "default_options",
"display_type": {
"name": "display_type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
"autoincrement": false,
"default": "'singleValue'"
},
"template": {
"name": "template",
"display_config": {
"name": "display_config",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
"autoincrement": false,
"default": "'{\"json\": {}}'"
},
"enabled": {
"name": "enabled",
@@ -632,13 +650,6 @@
"custom_widget_secret": {
"name": "custom_widget_secret",
"columns": {
"source_id": {
"name": "source_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"kind": {
"name": "kind",
"type": "text",
@@ -646,8 +657,8 @@
"notNull": true,
"autoincrement": false
},
"encrypted_value": {
"name": "encrypted_value",
"value": {
"name": "value",
"type": "text",
"primaryKey": false,
"notNull": true,
@@ -681,9 +692,9 @@
}
},
"compositePrimaryKeys": {
"custom_widget_secret_definition_id_source_id_kind_pk": {
"columns": ["definition_id", "source_id", "kind"],
"name": "custom_widget_secret_definition_id_source_id_kind_pk"
"custom_widget_secret_definition_id_kind_pk": {
"columns": ["definition_id", "kind"],
"name": "custom_widget_secret_definition_id_kind_pk"
}
},
"uniqueConstraints": {},
@@ -2108,6 +2119,14 @@
"autoincrement": false,
"default": false
},
"enable_right_click_on_widgets": {
"name": "enable_right_click_on_widgets",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"completed_manage_tour": {
"name": "completed_manage_tour",
"type": "integer",
File diff suppressed because it is too large. Load diff
@@ -292,8 +292,15 @@
{
"idx": 41,
"version": "6",
"when": 1783786658943,
"tag": "0041_add_right_click_preference",
"breakpoints": true
},
{
"idx": 42,
"version": "6",
"when": 1784305283910,
"tag": "0041_custom_widget_v2_reset",
"tag": "0042_custom_widget_v2_reset",
"breakpoints": true
}
]
+1
View File
@@ -81,6 +81,7 @@ export const users = mysqlTable("user", {
colorScheme: varchar({ length: 5 }).$type<ColorScheme>().default("dark").notNull(),
firstDayOfWeek: tinyint().$type<DayOfWeek>().default(1).notNull(), // Defaults to Monday
pingIconsEnabled: boolean().default(false).notNull(),
enableRightClickOnWidgets: boolean().default(true).notNull(),
completedManageTour: boolean().default(false).notNull(),
completedBoardTour: boolean().default(false).notNull(),
});
+1
View File
@@ -80,6 +80,7 @@ export const users = pgTable("user", {
colorScheme: varchar({ length: 5 }).$type<ColorScheme>().default("dark").notNull(),
firstDayOfWeek: smallint().$type<DayOfWeek>().default(1).notNull(), // Defaults to Monday
pingIconsEnabled: boolean().default(false).notNull(),
enableRightClickOnWidgets: boolean().default(true).notNull(),
completedManageTour: boolean().default(false).notNull(),
completedBoardTour: boolean().default(false).notNull(),
});
+1
View File
@@ -63,6 +63,7 @@ export const users = sqliteTable("user", {
colorScheme: text().$type<ColorScheme>().default("dark").notNull(),
firstDayOfWeek: int().$type<DayOfWeek>().default(1).notNull(), // Defaults to Monday
pingIconsEnabled: int({ mode: "boolean" }).default(false).notNull(),
enableRightClickOnWidgets: int({ mode: "boolean" }).default(true).notNull(),
completedManageTour: int({ mode: "boolean" }).default(false).notNull(),
completedBoardTour: int({ mode: "boolean" }).default(false).notNull(),
});
@@ -745,6 +745,19 @@ const linuxServerIOReleasesSchema = z.object({
}),
});
// Parse a github.com project URL (as returned by the LSIO registry) into an
// "owner/repo" identifier, or null if it is not a github.com repository URL.
const parseGithubRepo = (url: string): string | null => {
try {
const parsed = new URL(url);
if (parsed.hostname !== "github.com") return null;
const [owner, repo] = parsed.pathname.replace(/^\/+/, "").split("/");
return owner && repo ? `${owner}/${repo}` : null;
} catch {
return null;
}
};
const getLinuxServerIOReleaseAsync = async (
baseUrl: string,
identifier: string,
@@ -774,6 +787,29 @@ const getLinuxServerIOReleaseAsync = async (
error: { code: "noReleasesFound", message: `Image "${parsed.name}" not found in LinuxServer.io registry` },
};
// LSIO's /api/v1/images only exposes the single latest version, so a version
// filter can never surface an older release (#4351). When the image has a GitHub
// repository, delegate to the GitHub release provider to fetch the real release
// history and apply the filter, while keeping LSIO's own image metadata. Any
// GitHub failure (no repo, no releases, rate limit) falls back to the LSIO latest.
const githubRepo = parseGithubRepo(release.github_url);
if (githubRepo) {
const githubResult = await getGithubReleaseAsync(getReleaseProviderDefaultUrl("github"), githubRepo, versionRegex);
if (githubResult.success) {
return {
success: true,
data: {
...githubResult.data,
projectUrl: release.github_url,
projectDescription: release.description,
isArchived: release.deprecated,
starsCount: release.stars,
createdAt: release.initial_date ?? githubResult.data.createdAt,
},
};
}
}
const regex = compileVersionRegex(versionRegex);
if (versionRegex && !regex) {
return {
+3
View File
@@ -10,6 +10,7 @@ export type SettingsContextProps = Pick<
| "openSearchInNewTab"
| "ddgBangs"
| "pingIconsEnabled"
| "enableRightClickOnWidgets"
> &
Pick<ServerSettings["board"], "enableStatusByDefault" | "forceDisableStatus"> &
Pick<ServerSettings["user"], "enableGravatar">;
@@ -32,6 +33,7 @@ export type UserSettings = Pick<
| "openSearchInNewTab"
| "ddgBangs"
| "pingIconsEnabled"
| "enableRightClickOnWidgets"
>;
export const createSettings = ({
@@ -48,6 +50,7 @@ export const createSettings = ({
homeBoardId: user?.homeBoardId ?? serverSettings.board.homeBoardId,
mobileHomeBoardId: user?.mobileHomeBoardId ?? serverSettings.board.mobileHomeBoardId,
pingIconsEnabled: user?.pingIconsEnabled ?? false,
enableRightClickOnWidgets: user?.enableRightClickOnWidgets ?? true,
enableStatusByDefault: serverSettings.board.enableStatusByDefault,
forceDisableStatus: serverSettings.board.forceDisableStatus,
enableGravatar: serverSettings.user.enableGravatar,
@@ -56,6 +56,13 @@ export const userPreferenceDefinitions = [
mutationGroup: "pingIconsEnabled",
aliases: ["ping", "icon", "status"],
},
{
key: "enableRightClickOnWidgets",
kind: "boolean",
guest: false,
mutationGroup: "enableRightClickOnWidgets",
aliases: ["right click", "context menu", "tile", "widget menu"],
},
{
key: "fullPreferencesPage",
kind: "link",
@@ -3,6 +3,7 @@
import {
IconActivity,
IconCalendarWeek,
IconClick,
IconDeviceDesktop,
IconExternalLink,
IconHome,
@@ -46,6 +47,7 @@ export const preferenceIcons: Record<UserPreferenceKey, TablerIcon> = {
homeBoardId: IconHome,
mobileHomeBoardId: IconLayoutBoard,
pingIconsEnabled: IconActivity,
enableRightClickOnWidgets: IconClick,
fullPreferencesPage: IconSettings,
};
@@ -93,6 +93,7 @@ export const useUserPreferences = () => {
const homeBoardsMutation = clientApi.user.changeHomeBoards.useMutation({ onSuccess: refresh });
const firstDayMutation = clientApi.user.changeFirstDayOfWeek.useMutation({ onSuccess: refresh });
const pingMutation = clientApi.user.changePingIconsEnabled.useMutation({ onSuccess: refresh });
const rightClickMutation = clientApi.user.changeEnableRightClickOnWidgets.useMutation({ onSuccess: refresh });
const getEffective = (key: UserPreferenceKey): unknown =>
key in optimisticRef.current ? optimisticRef.current[key] : settingsRef.current[key as keyof SettingsContextProps];
@@ -116,6 +117,8 @@ export const useUserPreferences = () => {
}),
firstDayOfWeek: (uid, _key, value) => firstDayMutation.mutateAsync({ id: uid, firstDayOfWeek: value as DayOfWeek }),
pingIconsEnabled: (uid, _key, value) => pingMutation.mutateAsync({ id: uid, pingIconsEnabled: value as boolean }),
enableRightClickOnWidgets: (uid, _key, value) =>
rightClickMutation.mutateAsync({ id: uid, enableRightClickOnWidgets: value as boolean }),
};
const groupPending: Record<string, boolean> = {
@@ -123,6 +126,7 @@ export const useUserPreferences = () => {
homeBoards: homeBoardsMutation.isPending,
firstDayOfWeek: firstDayMutation.isPending,
pingIconsEnabled: pingMutation.isPending,
enableRightClickOnWidgets: rightClickMutation.isPending,
};
const persistedValues = useMemo<Record<UserPreferenceKey, unknown>>(() => {
+12
View File
@@ -198,6 +198,10 @@
"pingIconsEnabled": {
"label": "Use icons for pings"
},
"enableRightClickOnWidgets": {
"label": "Enable right-click on widgets",
"description": "Show Homarr's context menu when right-clicking a widget. Disable to use the browser's default menu (e.g. \"Open link in new tab\")."
},
"defaultSearchEngine": {
"label": "Default search engine"
},
@@ -6133,6 +6137,14 @@
}
}
},
"enableRightClickOnWidgets": {
"label": "Enable right-click on widgets",
"children": {
"detail": {
"title": "Enable right-click on widgets"
}
}
},
"fullPreferencesPage": {
"label": "Open full preferences page"
}
+4
View File
@@ -130,6 +130,10 @@ export const userPingIconsEnabledSchema = z.object({
pingIconsEnabled: z.boolean(),
});
export const userEnableRightClickOnWidgetsSchema = z.object({
enableRightClickOnWidgets: z.boolean(),
});
export const userDdgBangsSchema = z.object({
ddgBangs: z.boolean(),
});
+4 -4
View File
@@ -19203,7 +19203,7 @@ snapshots:
react-dom: 19.2.7(react@19.2.7)
react-number-format: 5.4.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7)
type-fest: 5.7.0
type-fest: 5.6.0
transitivePeerDependencies:
- '@types/react'
@@ -20571,7 +20571,7 @@ snapshots:
dependencies:
'@scalar/helpers': 0.9.0
nanoid: 5.1.16
type-fest: 5.7.0
type-fest: 5.6.0
zod: 4.4.3
'@scalar/use-codemirror@0.14.12(@typescript/typescript6@6.0.2)':
@@ -28045,14 +28045,14 @@ snapshots:
dependencies:
find-up-simple: 1.0.1
read-pkg: 10.0.0
type-fest: 5.6.0
type-fest: 5.7.0
read-pkg@10.0.0:
dependencies:
'@types/normalize-package-data': 2.4.4
normalize-package-data: 8.0.0
parse-json: 8.3.0
type-fest: 5.6.0
type-fest: 5.7.0
unicorn-magic: 0.3.0
read-pkg@9.0.1:
+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":"marektancak0106","avatar_url":"https://avatars.githubusercontent.com/u/25422667?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":"beanbag420","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.githubusercontentLine 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":"marektancak0106","avatar_url":"https://avatars.githubusercontent.com/u/25422667?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":"beanbag420","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.githubusercontentLine truncated