wip: user preferences in command palette
This commit is contained in:
24 files changed
+921
-116
No files matched your search
@@ -1,16 +1,17 @@
|
||||
import { Group, Stack, Text } from "@mantine/core";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
import { Group, Text } from "@mantine/core";
|
||||
|
||||
import { localeConfigurations, supportedLanguages } from "@homarr/translation";
|
||||
import { useChangeLocale, useCurrentLocale, useI18n } from "@homarr/translation/client";
|
||||
import { useUserPreference } from "../../../preferences/use-user-preference";
|
||||
import { LanguageIcon } from "@homarr/ui";
|
||||
|
||||
import { createChildrenOptions } from "../../../lib/children";
|
||||
import { createCheckmarkPreferenceAction, PreferenceDetailHeader } from "./preferences/action-row";
|
||||
|
||||
export const languageChildrenOptions = createChildrenOptions<Record<string, unknown>>({
|
||||
useActions: (_, query) => {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
const currentLocale = useCurrentLocale();
|
||||
const { value: currentLocale, setValue: setLocale, isPending } = useUserPreference("locale");
|
||||
|
||||
return supportedLanguages
|
||||
.map((localeKey) => ({ localeKey, configuration: localeConfigurations[localeKey] }))
|
||||
.filter(
|
||||
@@ -29,38 +30,25 @@ export const languageChildrenOptions = createChildrenOptions<Record<string, unkn
|
||||
languageB.configuration.translatedName.toLowerCase().indexOf(normalizedQuery),
|
||||
),
|
||||
)
|
||||
.map(({ localeKey, configuration }) => ({
|
||||
key: localeKey,
|
||||
Component() {
|
||||
return (
|
||||
<Group mx="md" my="sm" wrap="nowrap" justify="space-between" w="100%">
|
||||
<Group wrap="nowrap">
|
||||
<LanguageIcon icon={localeConfigurations[localeKey].icon} />
|
||||
<Group wrap="nowrap" gap="xs">
|
||||
<Text>{configuration.name}</Text>
|
||||
<Text size="xs" c="dimmed" inherit>
|
||||
({configuration.translatedName})
|
||||
</Text>
|
||||
</Group>
|
||||
.map(({ localeKey, configuration }) =>
|
||||
createCheckmarkPreferenceAction({
|
||||
key: localeKey,
|
||||
isSelected: localeKey === currentLocale,
|
||||
onSelect: () => setLocale(localeKey as never),
|
||||
isPending,
|
||||
labelContent: (
|
||||
<Group wrap="nowrap">
|
||||
<LanguageIcon icon={localeConfigurations[localeKey].icon} />
|
||||
<Group wrap="nowrap" gap="xs">
|
||||
<Text>{configuration.name}</Text>
|
||||
<Text size="xs" c="dimmed" inherit>
|
||||
({configuration.translatedName})
|
||||
</Text>
|
||||
</Group>
|
||||
{localeKey === currentLocale && <IconCheck color="currentColor" size={24} />}
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
useInteraction() {
|
||||
const { changeLocale } = useChangeLocale();
|
||||
|
||||
return { type: "javaScript", onSelect: () => changeLocale(localeKey) };
|
||||
},
|
||||
}));
|
||||
},
|
||||
DetailComponent: () => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<Stack mx="md" my="sm">
|
||||
<Text>{t("search.mode.command.group.globalCommand.option.language.children.detail.title")}</Text>
|
||||
</Stack>
|
||||
);
|
||||
),
|
||||
}),
|
||||
);
|
||||
},
|
||||
DetailComponent: () => <PreferenceDetailHeader titleKey="locale.children.detail.title" />,
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Center, Group, Loader, Stack, Text } from "@mantine/core";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { interaction } from "../../../../lib/interaction";
|
||||
|
||||
export const PreferenceDetailHeader = ({ titleKey }: { titleKey: string }) => {
|
||||
const tOption = useScopedI18n("search.mode.command.group.preferences.option");
|
||||
const t = useScopedI18n("search.mode.command.group.preferences");
|
||||
|
||||
return (
|
||||
<Stack mx="md" my="sm" gap="xs">
|
||||
<Text>{tOption(titleKey as never)}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("children.detail.backHint")}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const createCheckmarkPreferenceAction = ({
|
||||
key,
|
||||
label,
|
||||
labelContent,
|
||||
isSelected,
|
||||
onSelect,
|
||||
dimmed,
|
||||
isPending,
|
||||
}: {
|
||||
key: string;
|
||||
label?: string;
|
||||
labelContent?: ReactNode;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
dimmed?: boolean;
|
||||
isPending?: boolean;
|
||||
}) => ({
|
||||
key,
|
||||
Component() {
|
||||
return (
|
||||
<Group mx="md" my="sm" wrap="nowrap" justify="space-between" w="100%" opacity={isPending ? 0.5 : 1}>
|
||||
{labelContent ?? <Text c={dimmed ? "dimmed" : undefined}>{label}</Text>}
|
||||
{isSelected && <IconCheck color="currentColor" size={24} />}
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
useInteraction: interaction.javaScript(() => ({
|
||||
onSelect: isPending ? () => undefined : onSelect,
|
||||
closeSpotlightOnTrigger: false,
|
||||
})),
|
||||
});
|
||||
|
||||
export const createLoadingPreferenceAction = () => ({
|
||||
key: "loading",
|
||||
Component() {
|
||||
return (
|
||||
<Center mx="md" my="sm">
|
||||
<Loader size="sm" />
|
||||
</Center>
|
||||
);
|
||||
},
|
||||
useInteraction: interaction.javaScript(() => ({
|
||||
onSelect: () => undefined,
|
||||
closeSpotlightOnTrigger: false,
|
||||
})),
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useUserPreference } from "../../../../preferences/use-user-preference";
|
||||
import type { UserPreferenceKey } from "@homarr/settings";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
|
||||
import { createChildrenOptions } from "../../../../lib/children";
|
||||
import { createLoadingPreferenceAction, PreferenceDetailHeader } from "./action-row";
|
||||
import { createSelectablePreferenceActions } from "./selectable-actions";
|
||||
|
||||
interface BoardChildrenOptionsConfig {
|
||||
preferenceKey: Extract<UserPreferenceKey, "homeBoardId" | "mobileHomeBoardId">;
|
||||
}
|
||||
|
||||
const boardDetailTitleKeys = {
|
||||
homeBoardId: "homeBoardId.children.detail.title",
|
||||
mobileHomeBoardId: "mobileHomeBoardId.children.detail.title",
|
||||
} as const;
|
||||
|
||||
export const createBoardChildrenOptions = (config: BoardChildrenOptionsConfig) =>
|
||||
createChildrenOptions<Record<string, unknown>>({
|
||||
useActions: (_, query) => {
|
||||
const { value, setValue, isPending } = useUserPreference(config.preferenceKey);
|
||||
const currentValue = value as string | null;
|
||||
const boardsQuery = clientApi.board.getAllBoards.useQuery();
|
||||
const t = useScopedI18n("search.mode.command.group.preferences.option");
|
||||
|
||||
if (boardsQuery.isLoading) {
|
||||
return [createLoadingPreferenceAction()];
|
||||
}
|
||||
|
||||
return createSelectablePreferenceActions({
|
||||
query,
|
||||
currentValue,
|
||||
noneLabel: t("board.none"),
|
||||
unavailableLabel: t("board.unavailable"),
|
||||
items: (boardsQuery.data ?? []).map((board) => ({
|
||||
key: board.id,
|
||||
label: board.name,
|
||||
value: board.id,
|
||||
})),
|
||||
onSelect: (nextValue) => setValue(nextValue as never),
|
||||
isPending,
|
||||
});
|
||||
},
|
||||
DetailComponent: () => <PreferenceDetailHeader titleKey={boardDetailTitleKeys[config.preferenceKey]} />,
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { ColorScheme } from "@homarr/definitions";
|
||||
import { colorSchemes } from "@homarr/definitions";
|
||||
import { useUserPreference } from "../../../../preferences/use-user-preference";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { createChildrenOptions } from "../../../../lib/children";
|
||||
import { createCheckmarkPreferenceAction, PreferenceDetailHeader } from "./action-row";
|
||||
|
||||
export const colorSchemeChildrenOptions = createChildrenOptions<Record<string, unknown>>({
|
||||
useActions: () => {
|
||||
const tOptions = useScopedI18n("common.colorScheme.options");
|
||||
const { value, setValue, isPending } = useUserPreference("colorScheme");
|
||||
const currentValue = value as ColorScheme;
|
||||
|
||||
return colorSchemes.map((scheme) =>
|
||||
createCheckmarkPreferenceAction({
|
||||
key: scheme,
|
||||
label: tOptions(scheme),
|
||||
isSelected: currentValue === scheme,
|
||||
onSelect: () => setValue(scheme as never),
|
||||
isPending,
|
||||
}),
|
||||
);
|
||||
},
|
||||
DetailComponent: () => <PreferenceDetailHeader titleKey="colorScheme.children.detail.title" />,
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { DayOfWeek } from "@mantine/dates";
|
||||
import dayjs from "dayjs";
|
||||
import localeData from "dayjs/plugin/localeData";
|
||||
|
||||
import { useUserPreference } from "../../../../preferences/use-user-preference";
|
||||
|
||||
import { createChildrenOptions } from "../../../../lib/children";
|
||||
import { createCheckmarkPreferenceAction, PreferenceDetailHeader } from "./action-row";
|
||||
|
||||
dayjs.extend(localeData);
|
||||
|
||||
const firstDayOfWeekOptions: DayOfWeek[] = [1, 6, 0];
|
||||
const weekDays = dayjs.weekdays(false);
|
||||
|
||||
export const firstDayOfWeekChildrenOptions = createChildrenOptions<Record<string, unknown>>({
|
||||
useActions: () => {
|
||||
const { value, setValue, isPending } = useUserPreference("firstDayOfWeek");
|
||||
const currentValue = value as DayOfWeek;
|
||||
|
||||
return firstDayOfWeekOptions.map((day) =>
|
||||
createCheckmarkPreferenceAction({
|
||||
key: day.toString(),
|
||||
label: weekDays[day] ?? day.toString(),
|
||||
isSelected: currentValue === day,
|
||||
onSelect: () => setValue(day as never),
|
||||
isPending,
|
||||
}),
|
||||
);
|
||||
},
|
||||
DetailComponent: () => <PreferenceDetailHeader titleKey="firstDayOfWeek.children.detail.title" />,
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export { settingsChildrenOptions } from "./settings-children";
|
||||
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
IconActivity,
|
||||
IconCalendarWeek,
|
||||
IconDeviceDesktop,
|
||||
IconExternalLink,
|
||||
IconHome,
|
||||
IconLanguage,
|
||||
IconLayoutBoard,
|
||||
IconSearch,
|
||||
IconSettings,
|
||||
IconWorldWww,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import { userPreferenceDefinitions } from "@homarr/settings";
|
||||
import type { UserPreferenceKey } from "@homarr/settings";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
|
||||
import { createChildrenOptions } from "../../../../lib/children";
|
||||
import { languageChildrenOptions } from "../language";
|
||||
import { createBoardChildrenOptions } from "./board";
|
||||
import { colorSchemeChildrenOptions } from "./color-scheme";
|
||||
import { firstDayOfWeekChildrenOptions } from "./first-day-of-week";
|
||||
import { searchEngineChildrenOptions } from "./search-engine";
|
||||
|
||||
type ChildrenPreferenceKey = "colorScheme" | "locale" | "defaultSearchEngineId" | "firstDayOfWeek" | "homeBoardId" | "mobileHomeBoardId";
|
||||
|
||||
type PreferenceChildrenOptionsFactory = ReturnType<typeof createChildrenOptions<Record<string, unknown>>>;
|
||||
|
||||
export const preferenceIcons: Record<UserPreferenceKey, TablerIcon> = {
|
||||
colorScheme: IconDeviceDesktop,
|
||||
locale: IconLanguage,
|
||||
defaultSearchEngineId: IconSearch,
|
||||
openSearchInNewTab: IconExternalLink,
|
||||
ddgBangs: IconWorldWww,
|
||||
firstDayOfWeek: IconCalendarWeek,
|
||||
homeBoardId: IconHome,
|
||||
mobileHomeBoardId: IconLayoutBoard,
|
||||
pingIconsEnabled: IconActivity,
|
||||
fullPreferencesPage: IconSettings,
|
||||
};
|
||||
|
||||
export const preferenceChildrenOptionsByKey: Record<ChildrenPreferenceKey, PreferenceChildrenOptionsFactory> = {
|
||||
colorScheme: colorSchemeChildrenOptions,
|
||||
locale: languageChildrenOptions,
|
||||
defaultSearchEngineId: searchEngineChildrenOptions,
|
||||
firstDayOfWeek: firstDayOfWeekChildrenOptions,
|
||||
homeBoardId: createBoardChildrenOptions({ preferenceKey: "homeBoardId" }),
|
||||
mobileHomeBoardId: createBoardChildrenOptions({ preferenceKey: "mobileHomeBoardId" }),
|
||||
};
|
||||
|
||||
export const visiblePreferenceDefinitions = (isAuthenticated: boolean) =>
|
||||
userPreferenceDefinitions.filter((definition) => definition.guest || isAuthenticated);
|
||||
@@ -0,0 +1,35 @@
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useUserPreference } from "../../../../preferences/use-user-preference";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { createChildrenOptions } from "../../../../lib/children";
|
||||
import { createLoadingPreferenceAction, PreferenceDetailHeader } from "./action-row";
|
||||
import { createSelectablePreferenceActions } from "./selectable-actions";
|
||||
|
||||
export const searchEngineChildrenOptions = createChildrenOptions<Record<string, unknown>>({
|
||||
useActions: (_, query) => {
|
||||
const { value, setValue, isPending } = useUserPreference("defaultSearchEngineId");
|
||||
const currentValue = value as string | null;
|
||||
const searchEnginesQuery = clientApi.searchEngine.getSelectable.useQuery();
|
||||
const t = useScopedI18n("search.mode.command.group.preferences.option");
|
||||
|
||||
if (searchEnginesQuery.isLoading) {
|
||||
return [createLoadingPreferenceAction()];
|
||||
}
|
||||
|
||||
return createSelectablePreferenceActions({
|
||||
query,
|
||||
currentValue,
|
||||
noneLabel: t("searchEngine.none"),
|
||||
unavailableLabel: t("searchEngine.unavailable"),
|
||||
items: (searchEnginesQuery.data ?? []).map((engine) => ({
|
||||
key: engine.value,
|
||||
label: engine.label,
|
||||
value: engine.value,
|
||||
})),
|
||||
onSelect: (nextValue) => setValue(nextValue as never),
|
||||
isPending,
|
||||
});
|
||||
},
|
||||
DetailComponent: () => <PreferenceDetailHeader titleKey="defaultSearchEngineId.children.detail.title" />,
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { createCheckmarkPreferenceAction } from "./action-row";
|
||||
|
||||
interface SelectablePreferenceItem {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const createSelectablePreferenceActions = ({
|
||||
query,
|
||||
currentValue,
|
||||
noneLabel,
|
||||
unavailableLabel,
|
||||
items,
|
||||
onSelect,
|
||||
isPending,
|
||||
}: {
|
||||
query: string;
|
||||
currentValue: string | null;
|
||||
noneLabel: string;
|
||||
unavailableLabel: string;
|
||||
items: SelectablePreferenceItem[];
|
||||
onSelect: (value: string | null) => void;
|
||||
isPending?: boolean;
|
||||
}) => {
|
||||
const hasCurrentSelection = currentValue !== null && !items.some((item) => item.value === currentValue);
|
||||
const orphanedItem: SelectablePreferenceItem | null = hasCurrentSelection
|
||||
? { key: `orphaned-${currentValue}`, label: unavailableLabel, value: currentValue }
|
||||
: null;
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
const filteredItems = items.filter((item) => item.label.toLowerCase().includes(normalizedQuery));
|
||||
const visibleItems = orphanedItem ? [orphanedItem, ...filteredItems] : filteredItems;
|
||||
|
||||
const noneAction = createCheckmarkPreferenceAction({
|
||||
key: "none",
|
||||
label: noneLabel,
|
||||
isSelected: currentValue === null,
|
||||
onSelect: () => onSelect(null),
|
||||
dimmed: true,
|
||||
isPending,
|
||||
});
|
||||
|
||||
const itemActions = visibleItems.map((item) =>
|
||||
createCheckmarkPreferenceAction({
|
||||
key: item.key,
|
||||
label: item.label,
|
||||
isSelected: currentValue === item.value,
|
||||
onSelect: () => onSelect(item.value),
|
||||
dimmed: item.key.startsWith("orphaned-"),
|
||||
isPending,
|
||||
}),
|
||||
);
|
||||
|
||||
return [noneAction, ...itemActions];
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
"use client";
|
||||
|
||||
import { Group, Stack, Switch, Text } from "@mantine/core";
|
||||
import type { DayOfWeek } from "@mantine/dates";
|
||||
import dayjs from "dayjs";
|
||||
import localeData from "dayjs/plugin/localeData";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import type { ColorScheme } from "@homarr/definitions";
|
||||
import type { UserPreferenceKey } from "@homarr/settings";
|
||||
import { localeConfigurations } from "@homarr/translation";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
|
||||
import { createChildrenOptions } from "../../../../lib/children";
|
||||
import type { ChildrenAction } from "../../../../lib/children";
|
||||
import { interaction } from "../../../../lib/interaction";
|
||||
import { useUserPreference } from "../../../../preferences/use-user-preference";
|
||||
import { preferenceChildrenOptionsByKey, preferenceIcons, visiblePreferenceDefinitions } from "./registry";
|
||||
|
||||
dayjs.extend(localeData);
|
||||
|
||||
type SettingsAction = ChildrenAction<Record<string, unknown>>;
|
||||
|
||||
const weekDays = dayjs.weekdays(false);
|
||||
|
||||
const BooleanRow = ({ label, Icon, checked, isPending }: { label: string; Icon: TablerIcon; checked: boolean; isPending: boolean }) => (
|
||||
<Group mx="md" my="sm" wrap="nowrap" justify="space-between" w="100%">
|
||||
<Group wrap="nowrap" gap="sm">
|
||||
<Icon stroke={1.5} size={20} />
|
||||
<Text>{label}</Text>
|
||||
</Group>
|
||||
<Switch checked={checked} readOnly size="md" disabled={isPending} styles={{ track: { cursor: "pointer" } }} />
|
||||
</Group>
|
||||
);
|
||||
|
||||
const SelectRow = ({ label, Icon, valueLabel }: { label: string; Icon: TablerIcon; valueLabel: string }) => (
|
||||
<Group mx="md" my="sm" wrap="nowrap" justify="space-between" w="100%">
|
||||
<Group wrap="nowrap" gap="sm">
|
||||
<Icon stroke={1.5} size={20} />
|
||||
<Text>{label}</Text>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed">{valueLabel}</Text>
|
||||
</Group>
|
||||
);
|
||||
|
||||
const LinkRow = ({ label, Icon }: { label: string; Icon: TablerIcon }) => (
|
||||
<Group mx="md" my="sm" wrap="nowrap" gap="sm">
|
||||
<Icon stroke={1.5} size={20} />
|
||||
<Text>{label}</Text>
|
||||
</Group>
|
||||
);
|
||||
|
||||
const useSettingsActions = (_: Record<string, unknown>, query: string): SettingsAction[] => {
|
||||
const t = useScopedI18n("search.mode.command.group.preferences.option");
|
||||
const tColorScheme = useScopedI18n("common.colorScheme.options");
|
||||
const { data: session } = useSession();
|
||||
const isAuthenticated = Boolean(session?.user);
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
|
||||
const colorScheme = useUserPreference("colorScheme");
|
||||
const locale = useUserPreference("locale");
|
||||
const defaultSearchEngineId = useUserPreference("defaultSearchEngineId");
|
||||
const openSearchInNewTab = useUserPreference("openSearchInNewTab");
|
||||
const ddgBangs = useUserPreference("ddgBangs");
|
||||
const firstDayOfWeek = useUserPreference("firstDayOfWeek");
|
||||
const homeBoardId = useUserPreference("homeBoardId");
|
||||
const mobileHomeBoardId = useUserPreference("mobileHomeBoardId");
|
||||
const pingIconsEnabled = useUserPreference("pingIconsEnabled");
|
||||
|
||||
const boardsQuery = clientApi.board.getAllBoards.useQuery();
|
||||
const searchEnginesQuery = clientApi.searchEngine.getSelectable.useQuery();
|
||||
|
||||
const boards = boardsQuery.data ?? [];
|
||||
const engines = searchEnginesQuery.data ?? [];
|
||||
|
||||
const prefState: Record<Exclude<UserPreferenceKey, "fullPreferencesPage">, { value: unknown; setValue: (v: never) => void; isPending: boolean }> = {
|
||||
colorScheme,
|
||||
locale,
|
||||
defaultSearchEngineId,
|
||||
openSearchInNewTab,
|
||||
ddgBangs,
|
||||
firstDayOfWeek,
|
||||
homeBoardId,
|
||||
mobileHomeBoardId,
|
||||
pingIconsEnabled,
|
||||
};
|
||||
|
||||
const valueLabels: Record<Exclude<UserPreferenceKey, "fullPreferencesPage">, string> = {
|
||||
colorScheme: tColorScheme(colorScheme.value as ColorScheme),
|
||||
locale: localeConfigurations[locale.value as keyof typeof localeConfigurations]?.name ?? String(locale.value),
|
||||
defaultSearchEngineId: engines.find((e) => e.value === defaultSearchEngineId.value)?.label ?? t("searchEngine.none"),
|
||||
openSearchInNewTab: "",
|
||||
ddgBangs: "",
|
||||
firstDayOfWeek: weekDays[firstDayOfWeek.value as DayOfWeek] ?? String(firstDayOfWeek.value),
|
||||
homeBoardId: boards.find((b) => b.id === homeBoardId.value)?.name ?? t("board.none"),
|
||||
mobileHomeBoardId: boards.find((b) => b.id === mobileHomeBoardId.value)?.name ?? t("board.none"),
|
||||
pingIconsEnabled: "",
|
||||
};
|
||||
|
||||
const definitions = visiblePreferenceDefinitions(isAuthenticated);
|
||||
|
||||
return definitions.flatMap((definition): SettingsAction[] => {
|
||||
const label = t(`${definition.key}.label` as never);
|
||||
const Icon = preferenceIcons[definition.key];
|
||||
|
||||
if (!label.toLowerCase().includes(normalizedQuery)) return [];
|
||||
|
||||
if (definition.kind === "boolean") {
|
||||
const state = prefState[definition.key as keyof typeof prefState];
|
||||
const checked = state.value as boolean;
|
||||
return [{
|
||||
key: definition.key,
|
||||
Component: () => <BooleanRow label={label} Icon={Icon} checked={checked} isPending={state.isPending} />,
|
||||
useInteraction: interaction.javaScript(() => ({
|
||||
onSelect: () => state.setValue(!checked as never),
|
||||
closeSpotlightOnTrigger: false,
|
||||
})),
|
||||
}];
|
||||
}
|
||||
|
||||
if (definition.kind === "link") {
|
||||
return [{
|
||||
key: definition.key,
|
||||
Component: () => <LinkRow label={label} Icon={Icon} />,
|
||||
useInteraction: interaction.link(() => ({
|
||||
href: `/manage/users/${session?.user.id ?? ""}/general`,
|
||||
})),
|
||||
}];
|
||||
}
|
||||
|
||||
const childrenKey = definition.key as keyof typeof preferenceChildrenOptionsByKey;
|
||||
return [{
|
||||
key: definition.key,
|
||||
Component: () => <SelectRow label={label} Icon={Icon} valueLabel={valueLabels[definition.key as keyof typeof valueLabels]} />,
|
||||
useInteraction: interaction.children(preferenceChildrenOptionsByKey[childrenKey]),
|
||||
}];
|
||||
});
|
||||
};
|
||||
|
||||
export const settingsChildrenOptions = createChildrenOptions<Record<string, unknown>>({
|
||||
useActions: useSettingsActions,
|
||||
DetailComponent: () => {
|
||||
const t = useScopedI18n("search.mode.command.group.preferences");
|
||||
return (
|
||||
<Stack mx="md" my="sm" gap="xs">
|
||||
<Text>{t("title")}</Text>
|
||||
<Text size="xs" c="dimmed">{t("children.detail.backHint")}</Text>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { Group, Text, useComputedColorScheme, useMantineColorScheme } from "@mantine/core";
|
||||
import { Group, Text } from "@mantine/core";
|
||||
import {
|
||||
IconBox,
|
||||
IconCategoryPlus,
|
||||
IconDeviceDesktop,
|
||||
IconFileImport,
|
||||
IconLanguage,
|
||||
IconMailForward,
|
||||
IconMoon,
|
||||
IconPlug,
|
||||
IconSun,
|
||||
IconUserPlus,
|
||||
IconUsersGroup,
|
||||
} from "@tabler/icons-react";
|
||||
@@ -22,7 +18,6 @@ import type { TablerIcon } from "@homarr/ui";
|
||||
import { createGroup } from "../../lib/group";
|
||||
import type { inferSearchInteractionDefinition, SearchInteraction } from "../../lib/interaction";
|
||||
import { interaction } from "../../lib/interaction";
|
||||
import { languageChildrenOptions } from "./children/language";
|
||||
import { newIntegrationChildrenOptions } from "./children/new-integration";
|
||||
|
||||
// This has to be type so it can be interpreted as Record<string, unknown>.
|
||||
@@ -52,33 +47,9 @@ export const globalCommandGroup = createGroup<Command>({
|
||||
},
|
||||
useOptions() {
|
||||
const tOption = useScopedI18n("search.mode.command.group.globalCommand.option");
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const computedColorScheme = useComputedColorScheme("light");
|
||||
const { data: session } = useSession();
|
||||
|
||||
const commands: (Command & { hidden?: boolean })[] = [
|
||||
{
|
||||
commandKey: "colorScheme",
|
||||
icon: colorScheme === "auto" ? IconDeviceDesktop : computedColorScheme === "dark" ? IconSun : IconMoon,
|
||||
name:
|
||||
colorScheme === "auto"
|
||||
? tOption("colorScheme.auto")
|
||||
: tOption(`colorScheme.${computedColorScheme === "dark" ? "light" : "dark"}`),
|
||||
useInteraction: () => {
|
||||
const { toggleColorScheme } = useMantineColorScheme();
|
||||
|
||||
return {
|
||||
type: "javaScript",
|
||||
onSelect: toggleColorScheme,
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
commandKey: "language",
|
||||
icon: IconLanguage,
|
||||
name: tOption("language.label"),
|
||||
useInteraction: interaction.children(languageChildrenOptions),
|
||||
},
|
||||
{
|
||||
commandKey: "newBoard",
|
||||
icon: IconCategoryPlus,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { SearchMode } from "../../lib/mode";
|
||||
import { contextSpecificActionsSearchGroups } from "./context-specific-group";
|
||||
import { globalCommandGroup } from "./global-group";
|
||||
import { preferencesGroup } from "../preferences/groups";
|
||||
|
||||
export const commandMode = {
|
||||
modeKey: "command",
|
||||
character: ">",
|
||||
groups: [contextSpecificActionsSearchGroups, globalCommandGroup],
|
||||
groups: [contextSpecificActionsSearchGroups, preferencesGroup, globalCommandGroup],
|
||||
} satisfies SearchMode;
|
||||
Reference in new issue
Block a user