feat(apps): enhance multi-select app addition with modifier keys and helper tip

This commit is contained in:
Thomas Camlong committed 2026-08-21 00:31:19 +02:00
1 parent 1615f88a22
commit 7a7d4449c1
2 files changed
+48 -21

No files matched your search

@@ -41,6 +41,14 @@ const AddMenu = () => {
options: { appId: app.id },
});
},
onSelectMany: (apps) => {
for (const app of apps) {
createItem({
kind: "app",
options: { appId: app.id },
});
}
},
withCreate: session?.user.permissions.includes("app-create") ?? false,
});
};
@@ -51,7 +51,15 @@ export const AppSelectModal = createModal<AppSelectModalProps>(({ actions, inner
const selectedApps = useMemo(() => apps.filter((app) => selectedAppIds.has(app.id)), [apps, selectedAppIds]);
const handleSelect = (app: SelectableApp) => {
const handleSelect = (app: SelectableApp, event?: React.MouseEvent) => {
const isModifierPressed = Boolean(event?.shiftKey || event?.ctrlKey || event?.metaKey);
if (innerProps.onSelect && !isModifierPressed && selectedAppIds.size === 0) {
innerProps.onSelect(app);
actions.closeModal();
return;
}
if (multiSelect) {
setSelectedAppIds((current) => {
const next = new Set(current);
@@ -59,10 +67,7 @@ export const AppSelectModal = createModal<AppSelectModalProps>(({ actions, inner
else next.add(app.id);
return next;
});
return;
}
innerProps.onSelect?.(app);
actions.closeModal();
};
const handleAddNewApp = () => {
@@ -85,20 +90,34 @@ export const AppSelectModal = createModal<AppSelectModalProps>(({ actions, inner
return (
<Stack gap="md">
{/* Top Search Input */}
<Input
value={search}
onChange={(event) => setSearch(event.currentTarget.value)}
leftSection={<IconSearch size={16} />}
placeholder={`${t("app.action.select.search")}...`}
aria-label={t("app.action.select.search")}
data-autofocus
onKeyDown={(event) => {
if (event.key === "Enter" && filteredApps.length === 1 && filteredApps[0]) {
handleSelect(filteredApps[0]);
}
}}
/>
{/* Top Search Input & Multi-Select Tip */}
<Stack gap={6}>
<Input
value={search}
onChange={(event) => setSearch(event.currentTarget.value)}
leftSection={<IconSearch size={16} />}
placeholder={`${t("app.action.select.search")}...`}
aria-label={t("app.action.select.search")}
data-autofocus
onKeyDown={(event) => {
if (event.key === "Enter" && filteredApps.length === 1 && filteredApps[0]) {
handleSelect(filteredApps[0]);
}
}}
/>
{multiSelect && (
<Group justify="space-between" px={2}>
<Text size="xs" c="dimmed">
Tip: Hold Shift or Ctrl / ⌘ to select multiple apps at once
</Text>
{selectedAppIds.size > 0 && (
<Badge variant="light" color="primaryColor" size="xs">
{selectedAppIds.size} selected
</Badge>
)}
</Group>
)}
</Stack>
{/* Scrollable Container with App Cards */}
<ScrollArea.Autosize mah="70vh" offsetScrollbars>
@@ -179,13 +198,13 @@ const AppCard = ({
app: SelectableApp;
isSelected: boolean;
multiSelect: boolean;
onSelect: (app: SelectableApp) => void;
onSelect: (app: SelectableApp, event?: React.MouseEvent) => void;
}) => {
const t = useI18n();
return (
<SelectableCard
onClick={() => onSelect(app)}
onClick={(event) => onSelect(app, event)}
aria-label={app.name}
selected={isSelected}
icon={<Image src={app.iconUrl} alt={app.name} w={28} h={28} fit="contain" style={{ flexShrink: 0 }} />}
@@ -200,7 +219,7 @@ const AppCard = ({
description={app.description}
footerLeft={
<Text size="xs" c="dimmed">
{multiSelect ? (isSelected ? t("app.action.select.selected") : t("app.action.select.toggle")) : "Application"}
{multiSelect && isSelected ? t("app.action.select.selected") : "Application"}
</Text>
}
/>