Task #613: replace per-app hide toggles with single Dock visibility switch
The per-app hidden feature shipped in #609 was rejected — user wanted one toggle that hides/shows the entire bottom AppDock bar, leaving Home apps untouched. Removed: - lib/db/src/schema/user-hidden-apps.ts (and index.ts export); table dropped via `pnpm --filter @workspace/db run push` - GET /me/apps and PUT /me/apps/:appId/hidden routes - getHiddenAppIdsForUser + getVisibleNonHiddenAppsForUser helpers - MyApp + UpdateMyAppHiddenBody OpenAPI schemas - MyAppsBody settings UI + related i18n keys - Regenerated api-client-react + api-zod from the trimmed spec - Reverted GET /apps back to getVisibleAppsForUser Added: - artifacts/tx-os/src/hooks/use-dock-visible.ts — localStorage-backed preference with a custom window event for in-tab sync and the native `storage` event for cross-tab sync. Default = true. - DockBody in settings-panel: single ToggleRow under a new "App dock" section ("settingsPanel.section.dock" / "settingsPanel.dock.show") - AppDock now returns null when the preference is off and clears `--app-dock-height` so page padding doesn't stay reserved. Code review: PASS (architect). No remaining references to the removed infra. Typecheck shows only pre-existing errors in executive-meetings.ts and push.ts unrelated to this change. Follow-up: publish to Gitea + redeploy on Mac (proposed as a follow-up task).
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
logAppOpen,
|
||||
type App,
|
||||
} from "@workspace/api-client-react";
|
||||
import { useDockVisible } from "@/hooks/use-dock-visible";
|
||||
|
||||
type IconName = keyof typeof LucideIcons;
|
||||
function isIconName(x: string): x is IconName {
|
||||
@@ -34,6 +35,7 @@ export function AppDock({ currentSlug }: AppDockProps) {
|
||||
const lang = i18n.language;
|
||||
const { data: apps } = useListApps({ query: { queryKey: getListAppsQueryKey() } });
|
||||
const dockRef = useRef<HTMLDivElement | null>(null);
|
||||
const [dockEnabled] = useDockVisible();
|
||||
|
||||
// Match Home's source-of-truth contract: same `useListApps` query
|
||||
// key and the same `isActive` filter Home applies in its
|
||||
@@ -45,9 +47,10 @@ export function AppDock({ currentSlug }: AppDockProps) {
|
||||
(a) => a.isActive && a.slug !== currentSlug,
|
||||
);
|
||||
|
||||
// Skip the degenerate single-button dock: if the user only has one
|
||||
// other app to switch to, the dock adds no real value.
|
||||
const visible = others.length > 1;
|
||||
// Hide the dock when:
|
||||
// - the user disabled it in Settings (dockEnabled === false), or
|
||||
// - it would be degenerate (≤1 other app to switch to).
|
||||
const visible = dockEnabled && others.length > 1;
|
||||
|
||||
// Publish the dock's measured outer height as `--app-dock-height` on
|
||||
// the document root so the global `body { padding-bottom }` rule in
|
||||
|
||||
@@ -4,18 +4,12 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
useUpdateLanguage,
|
||||
getGetMeQueryKey,
|
||||
useListMyApps,
|
||||
useUpdateMyAppHidden,
|
||||
getListMyAppsQueryKey,
|
||||
getListAppsQueryKey,
|
||||
type MyApp,
|
||||
} from "@workspace/api-client-react";
|
||||
import * as LucideIcons from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
Settings as SettingsIcon,
|
||||
Volume2,
|
||||
VolumeX,
|
||||
LayoutGrid,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Sheet,
|
||||
@@ -41,6 +35,7 @@ import {
|
||||
} from "@/components/notification-settings";
|
||||
import { ClockStyleContent } from "@/components/clock-style-picker";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { useDockVisible } from "@/hooks/use-dock-visible";
|
||||
import i18n from "@/i18n";
|
||||
|
||||
function ToggleRow({
|
||||
@@ -141,107 +136,26 @@ function LanguageBody() {
|
||||
);
|
||||
}
|
||||
|
||||
type IconName = keyof typeof LucideIcons;
|
||||
function isIconName(x: string): x is IconName {
|
||||
return x in LucideIcons;
|
||||
}
|
||||
function resolveAppIcon(name: string): LucideIcon {
|
||||
if (isIconName(name)) {
|
||||
const Candidate = LucideIcons[name] as unknown;
|
||||
if (typeof Candidate === "function" || typeof Candidate === "object") {
|
||||
return Candidate as LucideIcon;
|
||||
}
|
||||
}
|
||||
return LucideIcons.Grid2X2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-user Home/Dock app visibility. Each row is a Switch that toggles a
|
||||
* row in `user_hidden_apps` for the current user. The mutation invalidates
|
||||
* both this list and the GET /apps query that backs Home + Dock so the
|
||||
* change reflects everywhere immediately without a reload.
|
||||
* Single toggle to hide/show the bottom AppDock bar.
|
||||
* Preference is per-device (localStorage) and the AppDock listens to
|
||||
* the same hook so the change is instant without a reload.
|
||||
*/
|
||||
function MyAppsBody() {
|
||||
const { t, i18n: i18nInstance } = useTranslation();
|
||||
const lang = i18nInstance.language;
|
||||
const queryClient = useQueryClient();
|
||||
const { data: apps, isLoading } = useListMyApps({
|
||||
query: { queryKey: getListMyAppsQueryKey() },
|
||||
});
|
||||
const update = useUpdateMyAppHidden({
|
||||
mutation: {
|
||||
onMutate: async ({ appId, data }) => {
|
||||
const key = getListMyAppsQueryKey();
|
||||
await queryClient.cancelQueries({ queryKey: key });
|
||||
const previous = queryClient.getQueryData<MyApp[]>(key);
|
||||
if (previous) {
|
||||
queryClient.setQueryData<MyApp[]>(
|
||||
key,
|
||||
previous.map((a) =>
|
||||
a.id === appId ? { ...a, hidden: data.hidden } : a,
|
||||
),
|
||||
);
|
||||
}
|
||||
return { previous };
|
||||
},
|
||||
onError: (_err, _vars, ctx) => {
|
||||
if (ctx?.previous) {
|
||||
queryClient.setQueryData(getListMyAppsQueryKey(), ctx.previous);
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: getListMyAppsQueryKey() });
|
||||
queryClient.invalidateQueries({ queryKey: getListAppsQueryKey() });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="px-3 py-4 text-sm text-muted-foreground">
|
||||
{t("settingsPanel.myApps.loading")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!apps || apps.length === 0) {
|
||||
return (
|
||||
<div className="px-3 py-4 text-sm text-muted-foreground">
|
||||
{t("settingsPanel.myApps.empty")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DockBody() {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useDockVisible();
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{apps.map((app) => {
|
||||
const Icon = resolveAppIcon(app.iconName);
|
||||
const name = lang === "ar" ? app.nameAr : app.nameEn;
|
||||
const visible = !app.hidden;
|
||||
const ariaLabel = visible
|
||||
? t("settingsPanel.myApps.hide", { name })
|
||||
: t("settingsPanel.myApps.show", { name });
|
||||
return (
|
||||
<ToggleRow
|
||||
key={app.id}
|
||||
active={visible}
|
||||
onClick={() =>
|
||||
update.mutate({ appId: app.id, data: { hidden: visible } })
|
||||
}
|
||||
label={name}
|
||||
ariaLabel={ariaLabel}
|
||||
leftIcon={
|
||||
<span
|
||||
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md"
|
||||
style={{ backgroundColor: `${app.color}20`, color: app.color }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Icon size={14} />
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<ToggleRow
|
||||
active={visible}
|
||||
onClick={() => setVisible(!visible)}
|
||||
label={t("settingsPanel.dock.show")}
|
||||
leftIcon={
|
||||
<LayoutGrid
|
||||
size={16}
|
||||
className={visible ? "text-primary shrink-0" : "text-muted-foreground shrink-0"}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -267,7 +181,7 @@ function SoundBody() {
|
||||
);
|
||||
}
|
||||
|
||||
type GroupId = "myApps" | "language" | "notifications" | "sound" | "clock";
|
||||
type GroupId = "dock" | "language" | "notifications" | "sound" | "clock";
|
||||
|
||||
/**
|
||||
* Section wrapper that styles each accordion item like the existing
|
||||
@@ -332,8 +246,8 @@ function SettingsPanelBody({
|
||||
onValueChange={(v) => onOpenChange(v as GroupId[])}
|
||||
className="space-y-2"
|
||||
>
|
||||
<GroupItem id="myApps" title={t("settingsPanel.section.myApps")}>
|
||||
<MyAppsBody />
|
||||
<GroupItem id="dock" title={t("settingsPanel.section.dock")}>
|
||||
<DockBody />
|
||||
</GroupItem>
|
||||
<GroupItem id="language" title={t("settingsPanel.section.language")}>
|
||||
<LanguageBody />
|
||||
|
||||
Reference in New Issue
Block a user