diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_edit-actions.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_edit-actions.tsx index 3723ea9d3..b74e7f9de 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_edit-actions.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_edit-actions.tsx @@ -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, }); }; diff --git a/packages/modals-collection/src/apps/app-select-modal.tsx b/packages/modals-collection/src/apps/app-select-modal.tsx index 9c59d7817..abddb9b77 100644 --- a/packages/modals-collection/src/apps/app-select-modal.tsx +++ b/packages/modals-collection/src/apps/app-select-modal.tsx @@ -51,7 +51,15 @@ export const AppSelectModal = createModal(({ 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(({ 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(({ actions, inner return ( - {/* Top Search Input */} - setSearch(event.currentTarget.value)} - leftSection={} - 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 */} + + setSearch(event.currentTarget.value)} + leftSection={} + 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 && ( + + + Tip: Hold Shift or Ctrl / ⌘ to select multiple apps at once + + {selectedAppIds.size > 0 && ( + + {selectedAppIds.size} selected + + )} + + )} + {/* Scrollable Container with App Cards */} @@ -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 ( onSelect(app)} + onClick={(event) => onSelect(app, event)} aria-label={app.name} selected={isSelected} icon={{app.name}} @@ -200,7 +219,7 @@ const AppCard = ({ description={app.description} footerLeft={ - {multiSelect ? (isSelected ? t("app.action.select.selected") : t("app.action.select.toggle")) : "Application"} + {multiSelect && isSelected ? t("app.action.select.selected") : "Application"} } />