feat(releases-widget): add Select/Deselect All to import from docker screen (#3674)

This commit is contained in:
Andre Silva
2025-08-01 10:12:56 +01:00
committed by GitHub
parent 6a819e38ed
commit 949c7a40d9
2 changed files with 94 additions and 55 deletions

View File

@@ -2273,7 +2273,9 @@
"listFoundImages": "List of found images", "listFoundImages": "List of found images",
"listAlreadyImportedImages": "List of already imported images", "listAlreadyImportedImages": "List of already imported images",
"allImagesAlreadyImported": "All images already imported", "allImagesAlreadyImported": "All images already imported",
"onlyAdminCanImport": "Only administrators can import from docker" "onlyAdminCanImport": "Only administrators can import from docker",
"selectAll": "Select all",
"deselectAll": "Deselect all"
}, },
"provider": { "provider": {
"label": "Provider" "label": "Provider"

View File

@@ -19,18 +19,19 @@ import {
Title, Title,
Tooltip, Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import type { CheckboxProps } from "@mantine/core";
import type { FormErrors } from "@mantine/form"; import type { FormErrors } from "@mantine/form";
import { useDebouncedValue } from "@mantine/hooks"; import { useDebouncedValue } from "@mantine/hooks";
import { import {
IconAlertTriangleFilled, IconAlertTriangleFilled,
IconBrandDocker, IconBrandDocker,
IconCopy,
IconCopyCheckFilled,
IconEdit, IconEdit,
IconPackageImport,
IconPlus, IconPlus,
IconSquare,
IconSquareCheck,
IconTrash, IconTrash,
IconTriangleFilled, IconTriangleFilled,
IconZoomScan,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { escapeForRegEx } from "@tiptap/react"; import { escapeForRegEx } from "@tiptap/react";
@@ -511,33 +512,37 @@ interface ReleasesRepositoryImport extends ReleasesRepository {
interface ImportRepositorySelectProps { interface ImportRepositorySelectProps {
repository: ReleasesRepositoryImport; repository: ReleasesRepositoryImport;
checked: boolean;
integration?: Integration; integration?: Integration;
versionFilterPrecisionOptions: string[]; versionFilterPrecisionOptions: string[];
disabled: boolean;
onImageSelectionChanged?: (isSelected: boolean) => void; onImageSelectionChanged?: (isSelected: boolean) => void;
} }
const ImportRepositorySelect = ({ const ImportRepositorySelect = ({
repository, repository,
checked,
integration, integration,
versionFilterPrecisionOptions, versionFilterPrecisionOptions,
onImageSelectionChanged, disabled = false,
onImageSelectionChanged = undefined,
}: ImportRepositorySelectProps) => { }: ImportRepositorySelectProps) => {
const tRepository = useScopedI18n("widget.releases.option.repositories"); const tRepository = useScopedI18n("widget.releases.option.repositories");
const checkBoxProps: CheckboxProps = !onImageSelectionChanged
? {
disabled: true,
checked: true,
}
: {
onChange: (event) => onImageSelectionChanged(event.currentTarget.checked),
};
return ( return (
<Group gap="xl" justify="space-between"> <Group gap="xl" justify="space-between">
<Group gap="md"> <Group gap="md" align="center">
<Checkbox <Checkbox
checked={checked}
disabled={disabled}
readOnly={disabled}
onChange={() => {
if (onImageSelectionChanged) {
onImageSelectionChanged(!checked);
}
}}
label={ label={
<Group> <Group align="center">
<Image <Image
src={repository.iconUrl} src={repository.iconUrl}
style={{ style={{
@@ -548,7 +553,6 @@ const ImportRepositorySelect = ({
<Text>{repository.identifier}</Text> <Text>{repository.identifier}</Text>
</Group> </Group>
} }
{...checkBoxProps}
/> />
{repository.versionFilter && ( {repository.versionFilter && (
@@ -693,7 +697,7 @@ const RepositoryImportModal = createModal<RepositoryImportProps>(({ innerProps,
<Stack> <Stack>
<Accordion defaultValue={!allImagesImported ? "foundImages" : anyImagesImported ? "alreadyImported" : ""}> <Accordion defaultValue={!allImagesImported ? "foundImages" : anyImagesImported ? "alreadyImported" : ""}>
<Accordion.Item value="foundImages"> <Accordion.Item value="foundImages">
<Accordion.Control disabled={allImagesImported} icon={<IconSquare stroke={1.25} />}> <Accordion.Control disabled={allImagesImported} icon={<IconZoomScan />}>
<Group> <Group>
{tRepository("importRepositories.listFoundImages")} {tRepository("importRepositories.listFoundImages")}
{allImagesImported && ( {allImagesImported && (
@@ -704,52 +708,85 @@ const RepositoryImportModal = createModal<RepositoryImportProps>(({ innerProps,
</Group> </Group>
</Accordion.Control> </Accordion.Control>
<Accordion.Panel> <Accordion.Panel>
{!allImagesImported && {!allImagesImported && (
importRepositories <Stack justify="center" gap="xs">
.filter((repository) => !repository.alreadyImported) <Group>
.map((repository) => { <Button
const integration = repository.providerIntegrationId leftSection={<IconCopyCheckFilled size="1em" />}
? innerProps.integrations[repository.providerIntegrationId] onClick={() =>
: undefined; setSelectedImages(importRepositories.filter((repository) => !repository.alreadyImported))
}
size="xs"
>
{tRepository("importRepositories.selectAll")}
</Button>
<Button
leftSection={<IconCopy size="1em" />}
onClick={() => setSelectedImages([])}
size="xs"
variant="default"
color="gray.5"
>
{tRepository("importRepositories.deselectAll")}
</Button>
</Group>
return ( <Divider />
<ImportRepositorySelect
key={repository.id} {importRepositories
repository={repository} .filter((repository) => !repository.alreadyImported)
integration={integration} .map((repository) => {
versionFilterPrecisionOptions={innerProps.versionFilterPrecisionOptions} const integration = repository.providerIntegrationId
onImageSelectionChanged={(isSelected) => ? innerProps.integrations[repository.providerIntegrationId]
isSelected : undefined;
? setSelectedImages([...selectedImages, repository])
: setSelectedImages(selectedImages.filter((img) => img !== repository)) return (
} <ImportRepositorySelect
/> key={repository.id}
); repository={repository}
})} checked={selectedImages.includes(repository)}
integration={integration}
versionFilterPrecisionOptions={innerProps.versionFilterPrecisionOptions}
disabled={false}
onImageSelectionChanged={(isSelected) =>
isSelected
? setSelectedImages([...selectedImages, repository])
: setSelectedImages(selectedImages.filter((img) => img !== repository))
}
/>
);
})}
</Stack>
)}
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
<Accordion.Item value="alreadyImported"> <Accordion.Item value="alreadyImported">
<Accordion.Control disabled={!anyImagesImported} icon={<IconSquareCheck stroke={1.25} />}> <Accordion.Control disabled={!anyImagesImported} icon={<IconPackageImport />}>
{tRepository("importRepositories.listAlreadyImportedImages")} {tRepository("importRepositories.listAlreadyImportedImages")}
</Accordion.Control> </Accordion.Control>
<Accordion.Panel> <Accordion.Panel>
{anyImagesImported && {anyImagesImported && (
importRepositories <Stack justify="center" gap="xs">
.filter((repository) => repository.alreadyImported) {importRepositories
.map((repository) => { .filter((repository) => repository.alreadyImported)
const integration = repository.providerIntegrationId .map((repository) => {
? innerProps.integrations[repository.providerIntegrationId] const integration = repository.providerIntegrationId
: undefined; ? innerProps.integrations[repository.providerIntegrationId]
: undefined;
return ( return (
<ImportRepositorySelect <ImportRepositorySelect
key={repository.id} key={repository.id}
repository={repository} repository={repository}
integration={integration} integration={integration}
versionFilterPrecisionOptions={innerProps.versionFilterPrecisionOptions} versionFilterPrecisionOptions={innerProps.versionFilterPrecisionOptions}
/> checked
); disabled
})} />
);
})}
</Stack>
)}
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
</Accordion> </Accordion>