Revert "feat: complete automatic mobile board UX"
This reverts commit 7959c20d88.
This commit is contained in:
1 parent
e93293f8a9
commit
be9c69f325
41 files changed
+270
-1225
No files matched your search
@@ -19,7 +19,6 @@ import { nextOnboardingStepAsync } from "./onboard/onboard-queries";
|
||||
const boardServerSettingsSchema = z.object({
|
||||
homeBoardId: z.string().nullable(),
|
||||
mobileHomeBoardId: z.string().nullable(),
|
||||
enableAutomaticMobileLayout: z.boolean(),
|
||||
enableStatusByDefault: z.boolean(),
|
||||
forceDisableStatus: z.boolean(),
|
||||
}) satisfies z.ZodType<ServerSettings["board"]>;
|
||||
@@ -40,7 +39,7 @@ export const serverSettingsRouter = createTRPCRouter({
|
||||
mcp: {
|
||||
enabled: true,
|
||||
description:
|
||||
"Get global board defaults, including desktop/mobile home board IDs, automatic mobile layout mode, and status behavior. Requires admin permission",
|
||||
"Get global board defaults, including desktop/mobile home board IDs and status behavior. Requires admin permission",
|
||||
},
|
||||
})
|
||||
.input(z.void())
|
||||
@@ -55,7 +54,7 @@ export const serverSettingsRouter = createTRPCRouter({
|
||||
mcp: {
|
||||
enabled: true,
|
||||
description:
|
||||
"Update global board defaults. Requires admin permission. Optional fields: homeBoardId, mobileHomeBoardId, enableAutomaticMobileLayout, enableStatusByDefault, forceDisableStatus. Home board IDs must reference public boards or be null",
|
||||
"Update global board defaults. Requires admin permission. Optional fields: homeBoardId, mobileHomeBoardId, enableStatusByDefault, forceDisableStatus. Home board IDs must reference public boards or be null",
|
||||
},
|
||||
})
|
||||
.input(boardServerSettingsUpdateSchema)
|
||||
@@ -104,19 +103,10 @@ export const serverSettingsRouter = createTRPCRouter({
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const current = await getServerSettingByKeyAsync(ctx.db, input.settingsKey);
|
||||
const next = {
|
||||
await updateServerSettingByKeyAsync(ctx.db, input.settingsKey, {
|
||||
...current,
|
||||
...input.value,
|
||||
} as ServerSettings[keyof ServerSettings];
|
||||
const existing = await ctx.db.query.serverSettings.findFirst({
|
||||
where: eq(serverSettings.settingKey, input.settingsKey),
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
await updateServerSettingByKeyAsync(ctx.db, input.settingsKey, next);
|
||||
} else {
|
||||
await insertServerSettingByKeyAsync(ctx.db, input.settingsKey, next);
|
||||
}
|
||||
} as ServerSettings[keyof ServerSettings]);
|
||||
}),
|
||||
initSettings: onboardingProcedure
|
||||
.requiresStep("settings")
|
||||
|
||||
@@ -56,33 +56,6 @@ describe("getAll server settings", () => {
|
||||
});
|
||||
|
||||
describe("saveSettings", () => {
|
||||
test("saveSettings should persist settings when defaults are not seeded", async () => {
|
||||
const db = createDb();
|
||||
const caller = serverSettingsRouter.createCaller({
|
||||
db,
|
||||
deviceType: undefined,
|
||||
session: defaultSession,
|
||||
});
|
||||
|
||||
await caller.saveSettings({
|
||||
settingsKey: "board",
|
||||
value: {
|
||||
enableAutomaticMobileLayout: false,
|
||||
},
|
||||
});
|
||||
|
||||
const dbSettings = await db.select().from(serverSettings);
|
||||
expect(dbSettings).toStrictEqual([
|
||||
{
|
||||
settingKey: "board",
|
||||
value: stringify({
|
||||
...defaultServerSettings.board,
|
||||
enableAutomaticMobileLayout: false,
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("saveSettings should update settings and return true when it updated only one", async () => {
|
||||
const db = createDb();
|
||||
const caller = serverSettingsRouter.createCaller({
|
||||
@@ -130,27 +103,6 @@ describe("board settings API", () => {
|
||||
await expect(caller.getBoardSettings()).resolves.toStrictEqual(defaultServerSettings.board);
|
||||
});
|
||||
|
||||
test("getBoardSettings should backfill the automatic layout default for legacy values", async () => {
|
||||
const db = createDb();
|
||||
const caller = serverSettingsRouter.createCaller({
|
||||
db,
|
||||
deviceType: undefined,
|
||||
session: defaultSession,
|
||||
});
|
||||
const legacyBoardSettings = {
|
||||
homeBoardId: defaultServerSettings.board.homeBoardId,
|
||||
mobileHomeBoardId: defaultServerSettings.board.mobileHomeBoardId,
|
||||
enableStatusByDefault: defaultServerSettings.board.enableStatusByDefault,
|
||||
forceDisableStatus: defaultServerSettings.board.forceDisableStatus,
|
||||
};
|
||||
await db.insert(serverSettings).values({
|
||||
settingKey: "board",
|
||||
value: stringify(legacyBoardSettings),
|
||||
});
|
||||
|
||||
await expect(caller.getBoardSettings()).resolves.toStrictEqual(defaultServerSettings.board);
|
||||
});
|
||||
|
||||
test("updateBoardSettings should insert settings when defaults are not persisted", async () => {
|
||||
const db = createDb();
|
||||
const caller = serverSettingsRouter.createCaller({
|
||||
@@ -193,7 +145,6 @@ describe("board settings API", () => {
|
||||
await caller.updateBoardSettings({ homeBoardId: boardId });
|
||||
const result = await caller.updateBoardSettings({
|
||||
mobileHomeBoardId: boardId,
|
||||
enableAutomaticMobileLayout: false,
|
||||
enableStatusByDefault: false,
|
||||
});
|
||||
|
||||
@@ -201,7 +152,6 @@ describe("board settings API", () => {
|
||||
...defaultServerSettings.board,
|
||||
homeBoardId: boardId,
|
||||
mobileHomeBoardId: boardId,
|
||||
enableAutomaticMobileLayout: false,
|
||||
enableStatusByDefault: false,
|
||||
});
|
||||
const dbSettings = await db.select().from(serverSettings);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { getIntegrationQueryProvenance, settleIntegrationQueries } from "./settle-integrations";
|
||||
import { settleIntegrationQueries } from "./settle-integrations";
|
||||
|
||||
const integrations = [
|
||||
{ id: "first", name: "First", kind: "mock" },
|
||||
@@ -76,19 +76,3 @@ describe("settleIntegrationQueries", () => {
|
||||
).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIntegrationQueryProvenance", () => {
|
||||
it("counts dropped and stale integration results independently", () => {
|
||||
expect(getIntegrationQueryProvenance(4, [{ isStale: false }, { isStale: true }, { isStale: true }])).toEqual({
|
||||
failedIntegrationCount: 1,
|
||||
staleIntegrationCount: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not report failures for complete current results", () => {
|
||||
expect(getIntegrationQueryProvenance(2, [{ isStale: false }, {}])).toEqual({
|
||||
failedIntegrationCount: 0,
|
||||
staleIntegrationCount: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -25,7 +25,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@homarr/api": "workspace:^0.1.0",
|
||||
"@homarr/settings": "workspace:^0.1.0",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:"
|
||||
},
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
|
||||
import { getBoardLayouts, getCurrentLayout, getDesktopLayout } from "./context";
|
||||
|
||||
const createBoard = () =>
|
||||
({
|
||||
layouts: [
|
||||
{ id: "small", name: "Small", columnCount: 4, breakpoint: 0 },
|
||||
{ id: "medium", name: "Medium", columnCount: 8, breakpoint: 768 },
|
||||
{ id: "large", name: "Large", columnCount: 12, breakpoint: 1200 },
|
||||
],
|
||||
}) as RouterOutputs["board"]["getBoardByName"];
|
||||
|
||||
describe("board layout selection", () => {
|
||||
test("automatic mode always uses the desktop layout", () => {
|
||||
const board = createBoard();
|
||||
|
||||
expect(getDesktopLayout(board).id).toBe("large");
|
||||
expect(getCurrentLayout(board, true, 390)).toBe("large");
|
||||
});
|
||||
|
||||
test.each([
|
||||
[390, "small"],
|
||||
[768, "medium"],
|
||||
[1024, "medium"],
|
||||
[1440, "large"],
|
||||
])("legacy mode selects the matching responsive layout at %spx", (viewportWidth, expectedLayoutId) => {
|
||||
expect(getCurrentLayout(createBoard(), false, viewportWidth)).toBe(expectedLayoutId);
|
||||
});
|
||||
|
||||
test("keeps every saved layout available for reversible mutations", () => {
|
||||
expect(getBoardLayouts(createBoard())).toEqual(["small", "medium", "large"]);
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import type { BoardEditAction, BoardEditActionEvent } from "./edit-mode";
|
||||
import { boardEditActionEventName, requestBoardEditAction } from "./edit-mode";
|
||||
|
||||
describe("requestBoardEditAction", () => {
|
||||
test("runs the action immediately when no edit guard is mounted", () => {
|
||||
const action = vi.fn();
|
||||
|
||||
requestBoardEditAction(action);
|
||||
|
||||
expect(action).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test("lets an edit guard defer the action until the user confirms", () => {
|
||||
const action = vi.fn();
|
||||
let deferredAction: BoardEditAction | undefined;
|
||||
const listener = (event: Event) => {
|
||||
event.preventDefault();
|
||||
deferredAction = (event as BoardEditActionEvent).detail.action;
|
||||
};
|
||||
document.addEventListener(boardEditActionEventName, listener, { once: true });
|
||||
|
||||
requestBoardEditAction(action);
|
||||
expect(action).not.toHaveBeenCalled();
|
||||
|
||||
void deferredAction?.();
|
||||
expect(action).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -6,28 +6,6 @@ import { useDisclosure } from "@mantine/hooks";
|
||||
|
||||
const EditModeContext = createContext<ReturnType<typeof useDisclosure> | null>(null);
|
||||
|
||||
export const boardEditActionEventName = "homarr:board-edit-action";
|
||||
|
||||
export type BoardEditActionResult = boolean | void;
|
||||
export type BoardEditAction = () => BoardEditActionResult | Promise<BoardEditActionResult>;
|
||||
export type BoardEditActionEvent = CustomEvent<{ action: BoardEditAction }>;
|
||||
|
||||
export const requestBoardEditAction = (action: BoardEditAction) => {
|
||||
if (typeof document === "undefined") {
|
||||
void action();
|
||||
return;
|
||||
}
|
||||
|
||||
const event: BoardEditActionEvent = new CustomEvent(boardEditActionEventName, {
|
||||
cancelable: true,
|
||||
detail: { action },
|
||||
});
|
||||
|
||||
if (document.dispatchEvent(event)) {
|
||||
void action();
|
||||
}
|
||||
};
|
||||
|
||||
const noop = () => undefined;
|
||||
const readOnlyEditMode = [false, { open: noop, close: noop, toggle: noop, set: noop }] satisfies ReturnType<
|
||||
typeof useDisclosure
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import { stringify } from "superjson";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { createId } from "@homarr/common";
|
||||
import { eq } from "@homarr/db";
|
||||
import {
|
||||
getServerSettingByKeyAsync,
|
||||
getServerSettingsAsync,
|
||||
shouldEnableAutomaticMobileLayoutForUpgrade,
|
||||
} from "@homarr/db/queries";
|
||||
import { boards, layouts, serverSettings } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import { defaultServerSettings } from "@homarr/server-settings";
|
||||
|
||||
import { seedServerSettingsAsync } from "./seed";
|
||||
|
||||
describe("automatic mobile layout upgrade default", () => {
|
||||
test("enables automatic mode for a new instance", () => {
|
||||
expect(shouldEnableAutomaticMobileLayoutForUpgrade([])).toBe(true);
|
||||
});
|
||||
|
||||
test("enables automatic mode when every existing board has only its canonical layout", () => {
|
||||
expect(
|
||||
shouldEnableAutomaticMobileLayoutForUpgrade([
|
||||
{ boardId: "first", name: "Base", breakpoint: 0 },
|
||||
{ boardId: "second", name: "Base", breakpoint: 0 },
|
||||
]),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("preserves legacy mode when a board has multiple responsive layouts", () => {
|
||||
expect(
|
||||
shouldEnableAutomaticMobileLayoutForUpgrade([
|
||||
{ boardId: "board", name: "Base", breakpoint: 0 },
|
||||
{ boardId: "board", name: "Mobile", breakpoint: 480 },
|
||||
]),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ boardId: "board", name: "Phone", breakpoint: 0 },
|
||||
{ boardId: "board", name: "Base", breakpoint: 480 },
|
||||
])("preserves legacy mode for a noncanonical saved layout", (existingLayout) => {
|
||||
expect(shouldEnableAutomaticMobileLayoutForUpgrade([existingLayout])).toBe(false);
|
||||
});
|
||||
|
||||
test("uses the upgrade heuristic when migrations and seeding are skipped", async () => {
|
||||
const db = createDb();
|
||||
const boardId = createId();
|
||||
await db.insert(boards).values({ id: boardId, name: "dashboard", isPublic: false });
|
||||
await db.insert(layouts).values([
|
||||
{ id: createId(), boardId, name: "Base", columnCount: 12, breakpoint: 0 },
|
||||
{ id: createId(), boardId, name: "Phone", columnCount: 4, breakpoint: 480 },
|
||||
]);
|
||||
|
||||
await expect(getServerSettingByKeyAsync(db, "board")).resolves.toMatchObject({
|
||||
enableAutomaticMobileLayout: false,
|
||||
});
|
||||
await expect(getServerSettingsAsync(db)).resolves.toMatchObject({
|
||||
board: {
|
||||
enableAutomaticMobileLayout: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("backfills legacy responsive instances once without overriding an explicit choice", async () => {
|
||||
const db = createDb();
|
||||
const boardId = createId();
|
||||
await db.insert(boards).values({ id: boardId, name: "dashboard", isPublic: false });
|
||||
await db.insert(layouts).values([
|
||||
{ id: createId(), boardId, name: "Base", columnCount: 12, breakpoint: 0 },
|
||||
{ id: createId(), boardId, name: "Phone", columnCount: 4, breakpoint: 480 },
|
||||
]);
|
||||
await db.insert(serverSettings).values({
|
||||
settingKey: "board",
|
||||
value: stringify({
|
||||
homeBoardId: null,
|
||||
mobileHomeBoardId: null,
|
||||
enableStatusByDefault: true,
|
||||
forceDisableStatus: false,
|
||||
}),
|
||||
});
|
||||
|
||||
await seedServerSettingsAsync(db);
|
||||
await seedServerSettingsAsync(db);
|
||||
|
||||
await expect(getServerSettingByKeyAsync(db, "board")).resolves.toStrictEqual({
|
||||
...defaultServerSettings.board,
|
||||
enableAutomaticMobileLayout: false,
|
||||
});
|
||||
|
||||
await db
|
||||
.update(serverSettings)
|
||||
.set({
|
||||
value: stringify({
|
||||
...defaultServerSettings.board,
|
||||
enableAutomaticMobileLayout: true,
|
||||
}),
|
||||
})
|
||||
.where(eq(serverSettings.settingKey, "board"));
|
||||
|
||||
await seedServerSettingsAsync(db);
|
||||
|
||||
await expect(getServerSettingByKeyAsync(db, "board")).resolves.toStrictEqual(defaultServerSettings.board);
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,6 @@ import { getMaxGroupPositionAsync, placeAllWidgetsAsync } from "../queries";
|
||||
import {
|
||||
getServerSettingByKeyAsync,
|
||||
insertServerSettingByKeyAsync,
|
||||
shouldEnableAutomaticMobileLayoutForUpgrade,
|
||||
updateServerSettingByKeyAsync,
|
||||
} from "../queries/server-setting";
|
||||
|
||||
@@ -156,33 +155,19 @@ const seedDefaultSearchEnginesAsync = async (db: Database) => {
|
||||
console.log("Set Homarr docs as the default search engine");
|
||||
};
|
||||
|
||||
export const seedServerSettingsAsync = async (db: Database) => {
|
||||
const seedServerSettingsAsync = async (db: Database) => {
|
||||
const serverSettingsData = await db.query.serverSettings.findMany();
|
||||
const existingLayouts = await db.query.layouts.findMany({
|
||||
columns: {
|
||||
boardId: true,
|
||||
name: true,
|
||||
breakpoint: true,
|
||||
},
|
||||
});
|
||||
const defaultsForThisInstance = {
|
||||
...defaultServerSettings,
|
||||
board: {
|
||||
...defaultServerSettings.board,
|
||||
enableAutomaticMobileLayout: shouldEnableAutomaticMobileLayoutForUpgrade(existingLayouts),
|
||||
},
|
||||
};
|
||||
|
||||
for (const settingsKey of defaultServerSettingsKeys) {
|
||||
const currentDbEntry = serverSettingsData.find((setting) => setting.settingKey === settingsKey);
|
||||
if (!currentDbEntry) {
|
||||
await insertServerSettingByKeyAsync(db, settingsKey, defaultsForThisInstance[settingsKey]);
|
||||
await insertServerSettingByKeyAsync(db, settingsKey, defaultServerSettings[settingsKey]);
|
||||
console.log(`Created serverSetting through seed key=${settingsKey}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentSettings = SuperJSON.parse<(typeof defaultServerSettings)[typeof settingsKey]>(currentDbEntry.value);
|
||||
const defaultSettings = defaultsForThisInstance[settingsKey];
|
||||
const currentSettings = await getServerSettingByKeyAsync(db, settingsKey);
|
||||
const defaultSettings = defaultServerSettings[settingsKey];
|
||||
const missingKeys = objectKeys(defaultSettings).filter((key) => !(key in currentSettings));
|
||||
|
||||
if (missingKeys.length === 0) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parse, stringify } from "superjson";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
import type { ServerSettings } from "@homarr/server-settings";
|
||||
import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings";
|
||||
@@ -7,71 +7,23 @@ import type { Database } from "..";
|
||||
import { eq } from "..";
|
||||
import { serverSettings } from "../schema";
|
||||
|
||||
interface ExistingBoardLayout {
|
||||
boardId: string;
|
||||
name: string;
|
||||
breakpoint: number;
|
||||
}
|
||||
|
||||
export const shouldEnableAutomaticMobileLayoutForUpgrade = (existingLayouts: ExistingBoardLayout[]) => {
|
||||
const layoutCounts = new Map<string, number>();
|
||||
|
||||
for (const layout of existingLayouts) {
|
||||
if (layout.name !== "Base" || layout.breakpoint !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const nextCount = (layoutCounts.get(layout.boardId) ?? 0) + 1;
|
||||
if (nextCount > 1) {
|
||||
return false;
|
||||
}
|
||||
layoutCounts.set(layout.boardId, nextCount);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const getAutomaticMobileLayoutUpgradeDefaultAsync = async (db: Database) => {
|
||||
const existingLayouts = await db.query.layouts.findMany({
|
||||
columns: {
|
||||
boardId: true,
|
||||
name: true,
|
||||
breakpoint: true,
|
||||
},
|
||||
});
|
||||
|
||||
return shouldEnableAutomaticMobileLayoutForUpgrade(existingLayouts);
|
||||
};
|
||||
|
||||
export const getServerSettingsAsync = async (db: Database) => {
|
||||
const settings = await db.query.serverSettings.findMany();
|
||||
|
||||
const mergedSettings = defaultServerSettingsKeys.reduce((acc, settingKey) => {
|
||||
const setting = settings.find((candidate) => candidate.settingKey === settingKey);
|
||||
return defaultServerSettingsKeys.reduce((acc, settingKey) => {
|
||||
const setting = settings.find((setting) => setting.settingKey === settingKey);
|
||||
if (!setting) {
|
||||
// Typescript is not happy because the key is a union and it does not know that they are the same
|
||||
acc[settingKey] = { ...defaultServerSettings[settingKey] } as never;
|
||||
acc[settingKey] = defaultServerSettings[settingKey] as never;
|
||||
return acc;
|
||||
}
|
||||
|
||||
const persistedSettings = parse<Record<string, unknown>>(setting.value);
|
||||
acc[settingKey] = {
|
||||
...defaultServerSettings[settingKey],
|
||||
...persistedSettings,
|
||||
} as never;
|
||||
...SuperJSON.parse(setting.value),
|
||||
};
|
||||
return acc;
|
||||
}, {} as ServerSettings);
|
||||
|
||||
const persistedBoardSettings = settings.find((setting) => setting.settingKey === "board");
|
||||
const parsedBoardSettings = persistedBoardSettings
|
||||
? parse<Partial<ServerSettings["board"]>>(persistedBoardSettings.value)
|
||||
: {};
|
||||
|
||||
if (typeof parsedBoardSettings.enableAutomaticMobileLayout !== "boolean") {
|
||||
mergedSettings.board.enableAutomaticMobileLayout = await getAutomaticMobileLayoutUpgradeDefaultAsync(db);
|
||||
}
|
||||
|
||||
return mergedSettings;
|
||||
};
|
||||
|
||||
export const getServerSettingByKeyAsync = async <TKey extends keyof ServerSettings>(db: Database, key: TKey) => {
|
||||
@@ -80,31 +32,10 @@ export const getServerSettingByKeyAsync = async <TKey extends keyof ServerSettin
|
||||
});
|
||||
|
||||
if (!dbSettings) {
|
||||
if (key === "board") {
|
||||
return {
|
||||
...defaultServerSettings.board,
|
||||
enableAutomaticMobileLayout: await getAutomaticMobileLayoutUpgradeDefaultAsync(db),
|
||||
} as ServerSettings[TKey];
|
||||
}
|
||||
|
||||
return defaultServerSettings[key];
|
||||
}
|
||||
|
||||
const persistedSettings = parse<Partial<ServerSettings[TKey]>>(dbSettings.value);
|
||||
const mergedSettings = {
|
||||
...defaultServerSettings[key],
|
||||
...persistedSettings,
|
||||
} as ServerSettings[TKey];
|
||||
|
||||
if (
|
||||
key === "board" &&
|
||||
typeof (persistedSettings as Partial<ServerSettings["board"]>).enableAutomaticMobileLayout !== "boolean"
|
||||
) {
|
||||
(mergedSettings as ServerSettings["board"]).enableAutomaticMobileLayout =
|
||||
await getAutomaticMobileLayoutUpgradeDefaultAsync(db);
|
||||
}
|
||||
|
||||
return mergedSettings;
|
||||
return SuperJSON.parse<ServerSettings[TKey]>(dbSettings.value);
|
||||
};
|
||||
|
||||
export const updateServerSettingByKeyAsync = async <TKey extends keyof ServerSettings>(
|
||||
@@ -115,7 +46,7 @@ export const updateServerSettingByKeyAsync = async <TKey extends keyof ServerSet
|
||||
await db
|
||||
.update(serverSettings)
|
||||
.set({
|
||||
value: stringify(value),
|
||||
value: SuperJSON.stringify(value),
|
||||
})
|
||||
.where(eq(serverSettings.settingKey, key));
|
||||
};
|
||||
@@ -127,6 +58,6 @@ export const insertServerSettingByKeyAsync = async <TKey extends keyof ServerSet
|
||||
) => {
|
||||
await db.insert(serverSettings).values({
|
||||
settingKey: key,
|
||||
value: stringify(value),
|
||||
value: SuperJSON.stringify(value),
|
||||
});
|
||||
};
|
||||
@@ -16,7 +16,6 @@ export interface ConfirmModalProps {
|
||||
children: ReactNode;
|
||||
onConfirm?: () => MaybePromise<void>;
|
||||
onCancel?: () => MaybePromise<void>;
|
||||
onClose?: () => void;
|
||||
closeOnConfirm?: boolean;
|
||||
closeOnCancel?: boolean;
|
||||
cancelProps?: ButtonProps & ComponentPropsWithoutRef<"button">;
|
||||
|
||||
@@ -128,7 +128,6 @@ interface OpenModalOptions {
|
||||
keepMounted?: boolean;
|
||||
onClose?: () => void;
|
||||
title?: stringOrTranslation;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const useModalAction = <TModal extends ModalDefinition>(modal: TModal) => {
|
||||
@@ -149,10 +148,6 @@ export const useConfirmModal = () => {
|
||||
const { openModal } = useModalAction(ConfirmModal);
|
||||
|
||||
return {
|
||||
openConfirmModal: (props: ConfirmModalProps) =>
|
||||
openModal(props, {
|
||||
title: props.title,
|
||||
onClose: props.onClose,
|
||||
}),
|
||||
openConfirmModal: (props: ConfirmModalProps) => openModal(props, { title: props.title }),
|
||||
};
|
||||
};
|
||||
@@ -27,7 +27,6 @@ export const defaultServerSettings = {
|
||||
board: {
|
||||
homeBoardId: null as string | null,
|
||||
mobileHomeBoardId: null as string | null,
|
||||
enableAutomaticMobileLayout: true,
|
||||
enableStatusByDefault: true,
|
||||
forceDisableStatus: false,
|
||||
},
|
||||
|
||||
@@ -12,14 +12,14 @@ export type SettingsContextProps = Pick<
|
||||
| "pingIconsEnabled"
|
||||
| "enableRightClickOnWidgets"
|
||||
> &
|
||||
Pick<ServerSettings["board"], "enableAutomaticMobileLayout" | "enableStatusByDefault" | "forceDisableStatus"> &
|
||||
Pick<ServerSettings["board"], "enableStatusByDefault" | "forceDisableStatus"> &
|
||||
Pick<ServerSettings["user"], "enableGravatar">;
|
||||
|
||||
export interface PublicServerSettings {
|
||||
search: Pick<ServerSettings["search"], "defaultSearchEngineId">;
|
||||
board: Pick<
|
||||
ServerSettings["board"],
|
||||
"homeBoardId" | "mobileHomeBoardId" | "enableAutomaticMobileLayout" | "enableStatusByDefault" | "forceDisableStatus"
|
||||
"homeBoardId" | "mobileHomeBoardId" | "enableStatusByDefault" | "forceDisableStatus"
|
||||
>;
|
||||
user: Pick<ServerSettings["user"], "enableGravatar">;
|
||||
}
|
||||
@@ -51,7 +51,6 @@ export const createSettings = ({
|
||||
mobileHomeBoardId: user?.mobileHomeBoardId ?? serverSettings.board.mobileHomeBoardId,
|
||||
pingIconsEnabled: user?.pingIconsEnabled ?? false,
|
||||
enableRightClickOnWidgets: user?.enableRightClickOnWidgets ?? true,
|
||||
enableAutomaticMobileLayout: serverSettings.board.enableAutomaticMobileLayout,
|
||||
enableStatusByDefault: serverSettings.board.enableStatusByDefault,
|
||||
forceDisableStatus: serverSettings.board.forceDisableStatus,
|
||||
enableGravatar: serverSettings.user.enableGravatar,
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"dependencies": {
|
||||
"@homarr/api": "workspace:^0.1.0",
|
||||
"@homarr/auth": "workspace:^0.1.0",
|
||||
"@homarr/boards": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
"@homarr/integrations": "workspace:^0.1.0",
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { DayOfWeek } from "@mantine/dates";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { requestBoardEditAction } from "@homarr/boards/edit-mode";
|
||||
import type { ColorScheme } from "@homarr/definitions";
|
||||
import { showErrorNotification } from "@homarr/notifications";
|
||||
import type { SettingsContextProps } from "@homarr/settings/creator";
|
||||
@@ -161,7 +160,7 @@ export const useUserPreferences = () => {
|
||||
|
||||
const clientSetters: Partial<Record<UserPreferenceKey, (value: unknown) => void>> = {
|
||||
colorScheme: (value) => setColorScheme(value as ColorScheme),
|
||||
locale: (value) => requestBoardEditAction(() => changeLocale(value as typeof currentLocale)),
|
||||
locale: (value) => changeLocale(value as typeof currentLocale),
|
||||
fullPreferencesPage: () => undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { Box, Center, Tooltip } from "@mantine/core";
|
||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
interface WidgetQueryProvenance {
|
||||
error?: unknown;
|
||||
failedIntegrationCount?: number;
|
||||
staleIntegrationCount?: number;
|
||||
expectedIntegrationCount?: number;
|
||||
receivedIntegrationCount?: number;
|
||||
}
|
||||
|
||||
export const hasWidgetDataWarning = ({
|
||||
error,
|
||||
failedIntegrationCount = 0,
|
||||
staleIntegrationCount = 0,
|
||||
expectedIntegrationCount,
|
||||
receivedIntegrationCount,
|
||||
}: WidgetQueryProvenance) =>
|
||||
Boolean(error) ||
|
||||
failedIntegrationCount > 0 ||
|
||||
staleIntegrationCount > 0 ||
|
||||
(expectedIntegrationCount !== undefined &&
|
||||
receivedIntegrationCount !== undefined &&
|
||||
receivedIntegrationCount < expectedIntegrationCount);
|
||||
|
||||
export const throwOnInitialQueryError = (error: unknown, hasData: boolean) => {
|
||||
if (error && !hasData) throw error;
|
||||
};
|
||||
|
||||
interface WidgetDataStateProps {
|
||||
children: ReactNode;
|
||||
hasWarning: boolean;
|
||||
}
|
||||
|
||||
export function WidgetDataState({ children, hasWarning }: WidgetDataStateProps) {
|
||||
const t = useI18n();
|
||||
const label = t("board.mobile.dataWarning");
|
||||
|
||||
return (
|
||||
<Box pos="relative" h="100%" w="100%">
|
||||
{hasWarning && (
|
||||
<Tooltip label={label} position="left" withArrow>
|
||||
<Center
|
||||
component="output"
|
||||
aria-label={label}
|
||||
pos="absolute"
|
||||
bottom={4}
|
||||
w={28}
|
||||
h={28}
|
||||
style={{
|
||||
insetInlineStart: 4,
|
||||
zIndex: 4,
|
||||
borderRadius: "var(--mantine-radius-sm)",
|
||||
background: "light-dark(var(--mantine-color-yellow-1), var(--mantine-color-dark-6))",
|
||||
}}
|
||||
>
|
||||
<IconAlertTriangle
|
||||
size={16}
|
||||
color="light-dark(var(--mantine-color-yellow-9), var(--mantine-color-yellow-3))"
|
||||
aria-hidden
|
||||
/>
|
||||
</Center>
|
||||
</Tooltip>
|
||||
)}
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("customApi", {
|
||||
icon: IconApi,
|
||||
mobile: { supportsDetailView: true, unmountWhenOffscreen: true },
|
||||
mobile: { supportsDetailView: true },
|
||||
queryKey: [["widget", "customApi"]],
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
|
||||
@@ -10,7 +10,7 @@ export const widgetKind = "dnsHoleControls";
|
||||
export const { definition, componentLoader } = createWidgetDefinition(widgetKind, {
|
||||
supportsAdvancedFocus: false,
|
||||
icon: IconDeviceGamepad,
|
||||
mobile: { supportsDetailView: true, unmountWhenOffscreen: true },
|
||||
mobile: { supportsDetailView: true },
|
||||
queryKey: [["widget", "dnsHole"]],
|
||||
refetchInterval: 5,
|
||||
createOptions() {
|
||||
|
||||
@@ -10,7 +10,6 @@ export const widgetKind = "dnsHoleSummary";
|
||||
export const { definition, componentLoader } = createWidgetDefinition(widgetKind, {
|
||||
supportsAdvancedFocus: false,
|
||||
icon: IconAd,
|
||||
mobile: { unmountWhenOffscreen: true },
|
||||
queryKey: [["widget", "dnsHole"]],
|
||||
refetchInterval: 5,
|
||||
createOptions() {
|
||||
|
||||
Reference in new issue
Block a user