feat: add multi-select functionality across various pages

- Implemented checkbox selection for multiple items in DeploymentRulesPage, DomainsPage, EnvironmentsPage, ServicesPage, TargetsPage, TemplateCategoriesPage, and TemplatesPage.
- Added delete functionality for selected items with confirmation prompts.
- Updated state management to handle selected IDs for batch operations.
- Enhanced UI with a toolbar for actions related to selected items.
- Adjusted API calls to accommodate changes in data structure, including deployment requests and job details.
This commit is contained in:
Torsten Brendgen
2026-07-09 23:49:10 +02:00
parent 3083e81452
commit 1894f740b0
13 changed files with 911 additions and 118 deletions

View File

@@ -1,6 +1,7 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
Button,
Checkbox,
Combobox,
Dialog,
DialogActions,
@@ -161,6 +162,7 @@ export function TemplatesPage() {
const [jsonResources, setJsonResources] = useState("[]");
const [editorTab, setEditorTab] = useState<TemplateEditorTab>("parameters");
const [serviceFilterId, setServiceFilterId] = useState("all");
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [expandedServiceGroups, setExpandedServiceGroups] = useState<Record<string, boolean>>({});
const [expandedCategoryGroups, setExpandedCategoryGroups] = useState<Record<string, boolean>>({});
const { data, error, isLoading } = useQuery({
@@ -255,6 +257,15 @@ export function TemplatesPage() {
await queryClient.invalidateQueries({ queryKey: ["templates"] });
},
});
const deleteSelectedTemplates = useMutation({
mutationFn: async (ids: string[]) => {
await Promise.all(ids.map((id) => portalApi.deleteTemplate(id)));
},
onSuccess: async () => {
setSelectedIds([]);
await queryClient.invalidateQueries({ queryKey: ["templates"] });
},
});
const submitTemplate = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
@@ -326,6 +337,11 @@ export function TemplatesPage() {
.sort((a, b) => a.categoryName.localeCompare(b.categoryName)),
}))
.filter((group) => group.categories.length > 0);
const allSelected =
filteredTemplates.length > 0 && filteredTemplates.every((template) => selectedIds.includes(template.id));
const toggleSelected = (id: string, checked: boolean) => {
setSelectedIds((current) => (checked ? [...new Set([...current, id])] : current.filter((entry) => entry !== id)));
};
const toggleServiceGroup = (serviceId: string) => {
setExpandedServiceGroups((current) => ({
@@ -350,6 +366,18 @@ export function TemplatesPage() {
<Button appearance="primary" icon={<AddRegular />} onClick={openAddDialog}>
Template hinzufuegen
</Button>
<Button
appearance="secondary"
disabled={selectedIds.length === 0 || deleteSelectedTemplates.isPending}
icon={<DeleteRegular />}
onClick={() => {
if (window.confirm(`${selectedIds.length} Templates wirklich loeschen?`)) {
deleteSelectedTemplates.mutate(selectedIds);
}
}}
>
Auswahl loeschen
</Button>
</div>
<Field className={styles.filter} label="Service Filter">
<Combobox
@@ -487,12 +515,20 @@ export function TemplatesPage() {
<DataState
isLoading={isLoading || templateCategoriesLoading || servicesLoading}
error={error ?? templateCategoriesError ?? servicesError ?? deleteTemplate.error}
error={error ?? templateCategoriesError ?? servicesError ?? deleteTemplate.error ?? deleteSelectedTemplates.error}
/>
{data && (
<Table aria-label="Templates">
<TableHeader>
<TableRow>
<TableHeaderCell>
<Checkbox
checked={allSelected}
onChange={(_, checkboxData) =>
setSelectedIds(checkboxData.checked ? filteredTemplates.map((template) => template.id) : [])
}
/>
</TableHeaderCell>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Version</TableHeaderCell>
<TableHeaderCell>Template Category</TableHeaderCell>
@@ -504,7 +540,7 @@ export function TemplatesPage() {
{groupedTemplates.map((group) => (
<Fragment key={`group-${group.serviceId}`}>
<TableRow className={styles.groupRow} onClick={() => toggleServiceGroup(group.serviceId)}>
<TableCell colSpan={5}>
<TableCell colSpan={6}>
<div className={styles.groupHeader}>
{expandedServiceGroups[group.serviceId] !== false ? <ChevronDownRegular /> : <ChevronRightRegular />}
<strong>{group.serviceName}</strong>
@@ -517,7 +553,7 @@ export function TemplatesPage() {
className={styles.groupRow}
onClick={() => toggleCategoryGroup(group.serviceId, categoryGroup.categoryId)}
>
<TableCell className={styles.categoryHeaderCell} colSpan={5}>
<TableCell className={styles.categoryHeaderCell} colSpan={6}>
<div className={styles.groupHeader}>
{expandedCategoryGroups[`${group.serviceId}:${categoryGroup.categoryId}`] !== false ? (
<ChevronDownRegular />
@@ -531,6 +567,12 @@ export function TemplatesPage() {
{expandedCategoryGroups[`${group.serviceId}:${categoryGroup.categoryId}`] !== false &&
categoryGroup.items.map((template) => (
<TableRow key={template.id}>
<TableCell>
<Checkbox
checked={selectedIds.includes(template.id)}
onChange={(_, checkboxData) => toggleSelected(template.id, Boolean(checkboxData.checked))}
/>
</TableCell>
<TableCell className={styles.nestedLevel2Cell}>{template.name}</TableCell>
<TableCell>{template.version}</TableCell>
<TableCell>