feat(releases-widget): limit number of displayed releases and custom name for repositories (#2974)

Co-authored-by: Andre Silva <asilva01@acuitysso.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
Andre Silva
2025-05-10 21:06:14 +01:00
committed by GitHub
parent b8bdf1836a
commit 8c1b365733
5 changed files with 70 additions and 14 deletions

View File

@@ -2072,6 +2072,10 @@
"showDetails": { "showDetails": {
"label": "Show Details" "label": "Show Details"
}, },
"topReleases": {
"label": "Top Releases",
"description": "The max number of latest releases to show. Zero means no limit."
},
"repositories": { "repositories": {
"label": "Repositories", "label": "Repositories",
"addRRepository": { "addRRepository": {
@@ -2084,6 +2088,9 @@
"label": "Identifier", "label": "Identifier",
"placeholder": "Name or Owner/Name" "placeholder": "Name or Owner/Name"
}, },
"name": {
"label": "Name"
},
"versionFilter": { "versionFilter": {
"label": "Version Filter", "label": "Version Filter",
"prefix": { "prefix": {

View File

@@ -40,6 +40,7 @@ export const WidgetMultiReleasesRepositoriesInput = ({
(repository: ReleasesRepository, index: number): FormValidation => { (repository: ReleasesRepository, index: number): FormValidation => {
form.setFieldValue(`options.${property}.${index}.providerKey`, repository.providerKey); form.setFieldValue(`options.${property}.${index}.providerKey`, repository.providerKey);
form.setFieldValue(`options.${property}.${index}.identifier`, repository.identifier); form.setFieldValue(`options.${property}.${index}.identifier`, repository.identifier);
form.setFieldValue(`options.${property}.${index}.name`, repository.name);
form.setFieldValue(`options.${property}.${index}.versionFilter`, repository.versionFilter); form.setFieldValue(`options.${property}.${index}.versionFilter`, repository.versionFilter);
form.setFieldValue(`options.${property}.${index}.iconUrl`, repository.iconUrl); form.setFieldValue(`options.${property}.${index}.iconUrl`, repository.iconUrl);
@@ -123,7 +124,8 @@ export const WidgetMultiReleasesRepositoriesInput = ({
<Group justify="space-between" align="center" style={{ flex: 1 }} gap={5}> <Group justify="space-between" align="center" style={{ flex: 1 }} gap={5}>
<Text size="sm" style={{ flex: 1, whiteSpace: "nowrap" }}> <Text size="sm" style={{ flex: 1, whiteSpace: "nowrap" }}>
{repository.identifier} {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */}
{repository.name || repository.identifier}
</Text> </Text>
<Text c="dimmed" size="xs" ta="end" style={{ flex: 1, whiteSpace: "nowrap" }}> <Text c="dimmed" size="xs" ta="end" style={{ flex: 1, whiteSpace: "nowrap" }}>
@@ -178,6 +180,11 @@ const formatVersionFilterRegex = (versionFilter: ReleasesVersionFilter | undefin
return `^${escapedPrefix}${precision}${escapedSuffix}$`; return `^${escapedPrefix}${precision}${escapedSuffix}$`;
}; };
const formatIdentifierName = (identifier: string) => {
const unformattedName = identifier.split("/").pop();
return unformattedName?.replace(/[-_]/g, " ").replace(/(?:^\w|[A-Z]|\b\w)/g, (char) => char.toUpperCase()) ?? "";
};
interface ReleaseEditProps { interface ReleaseEditProps {
fieldPath: string; fieldPath: string;
repository: ReleasesRepository; repository: ReleasesRepository;
@@ -209,7 +216,7 @@ const ReleaseEditModal = createModal<ReleaseEditProps>(({ innerProps, actions })
return ( return (
<Stack> <Stack>
<Group align="center"> <Group align="center" wrap="nowrap">
<Select <Select
withAsterisk withAsterisk
label={tRepository("provider.label")} label={tRepository("provider.label")}
@@ -224,6 +231,7 @@ const ReleaseEditModal = createModal<ReleaseEditProps>(({ innerProps, actions })
handleChange({ providerKey: value }); handleChange({ providerKey: value });
} }
}} }}
style={{ flex: 1, flexBasis: "40%" }}
/> />
<TextInput <TextInput
@@ -231,10 +239,38 @@ const ReleaseEditModal = createModal<ReleaseEditProps>(({ innerProps, actions })
label={tRepository("identifier.label")} label={tRepository("identifier.label")}
value={tempRepository.identifier} value={tempRepository.identifier}
onChange={(event) => { onChange={(event) => {
handleChange({ identifier: event.currentTarget.value }); const name =
tempRepository.name === undefined ||
formatIdentifierName(tempRepository.identifier) === tempRepository.name
? formatIdentifierName(event.currentTarget.value)
: tempRepository.name;
handleChange({
identifier: event.currentTarget.value,
name,
});
}} }}
error={formErrors[`${innerProps.fieldPath}.identifier`]} error={formErrors[`${innerProps.fieldPath}.identifier`]}
style={{ flex: 1 }} w="100%"
/>
</Group>
<Group align="center" wrap="nowrap">
<TextInput
label={tRepository("name.label")}
value={tempRepository.name ?? ""}
onChange={(event) => {
handleChange({ name: event.currentTarget.value });
}}
error={formErrors[`${innerProps.fieldPath}.name`]}
style={{ flex: 1, flexBasis: "40%" }}
/>
<IconPicker
withAsterisk={false}
value={tempRepository.iconUrl}
onChange={(url) => handleChange({ iconUrl: url })}
error={formErrors[`${innerProps.fieldPath}.iconUrl`] as string}
/> />
</Group> </Group>
@@ -298,13 +334,6 @@ const ReleaseEditModal = createModal<ReleaseEditProps>(({ innerProps, actions })
</Text> </Text>
</Fieldset> </Fieldset>
<IconPicker
withAsterisk={false}
value={tempRepository.iconUrl}
onChange={(url) => handleChange({ iconUrl: url })}
error={formErrors[`${innerProps.fieldPath}.iconUrl`] as string}
/>
<Divider my={"sm"} /> <Divider my={"sm"} />
<Group justify="flex-end"> <Group justify="flex-end">
<Button variant="default" onClick={actions.closeModal} color="gray.5"> <Button variant="default" onClick={actions.closeModal} color="gray.5">

View File

@@ -62,7 +62,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
); );
const repositories = useMemo(() => { const repositories = useMemo(() => {
return results const formattedResults = results
.flat() .flat()
.map(({ data }) => { .map(({ data }) => {
if (data === undefined) return undefined; if (data === undefined) return undefined;
@@ -74,8 +74,8 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
if (repository === undefined) return undefined; if (repository === undefined) return undefined;
return { return {
...repository,
...data, ...data,
iconUrl: repository.iconUrl,
isNewRelease: isNewRelease:
relativeDateOptions.newReleaseWithin !== "" && data.latestReleaseAt relativeDateOptions.newReleaseWithin !== "" && data.latestReleaseAt
? isDateWithin(data.latestReleaseAt, relativeDateOptions.newReleaseWithin) ? isDateWithin(data.latestReleaseAt, relativeDateOptions.newReleaseWithin)
@@ -99,10 +99,17 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
if (repoB?.latestReleaseAt === undefined) return -1; if (repoB?.latestReleaseAt === undefined) return -1;
return repoA.latestReleaseAt > repoB.latestReleaseAt ? -1 : 1; return repoA.latestReleaseAt > repoB.latestReleaseAt ? -1 : 1;
}) as ReleasesRepositoryResponse[]; }) as ReleasesRepositoryResponse[];
if (typeof options.topReleases !== "string" && options.topReleases > 0) {
return formattedResults.slice(0, options.topReleases);
}
return formattedResults;
}, [ }, [
results, results,
options.repositories, options.repositories,
options.showOnlyHighlighted, options.showOnlyHighlighted,
options.topReleases,
relativeDateOptions.newReleaseWithin, relativeDateOptions.newReleaseWithin,
relativeDateOptions.staleReleaseWithin, relativeDateOptions.staleReleaseWithin,
]); ]);
@@ -152,7 +159,8 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
/> />
<Group gap={5} justify="space-between" style={{ flex: 1 }}> <Group gap={5} justify="space-between" style={{ flex: 1 }}>
<Text size="xs">{repository.identifier}</Text> {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */}
<Text size="xs">{repository.name || repository.identifier}</Text>
<Tooltip <Tooltip
withArrow withArrow
@@ -346,6 +354,11 @@ const ExpandedDisplay = ({ repository, hasIconColor }: ExtendedDisplayProps) =>
</Text> </Text>
)} )}
</Group> </Group>
<Text size="xs" c="iconColor" ff="monospace">
{repository.identifier}
</Text>
{(repository.releaseUrl ?? repository.projectUrl) && ( {(repository.releaseUrl ?? repository.projectUrl) && (
<> <>
<Divider my={10} mx="30%" /> <Divider my={10} mx="30%" />

View File

@@ -30,12 +30,18 @@ export const { definition, componentLoader } = createWidgetDefinition("releases"
showDetails: factory.switch({ showDetails: factory.switch({
defaultValue: true, defaultValue: true,
}), }),
topReleases: factory.number({
withDescription: true,
defaultValue: 0,
validate: z.number().min(0),
}),
repositories: factory.multiReleasesRepositories({ repositories: factory.multiReleasesRepositories({
defaultValue: [], defaultValue: [],
validate: z.array( validate: z.array(
z.object({ z.object({
providerKey: z.string().min(1), providerKey: z.string().min(1),
identifier: z.string().min(1), identifier: z.string().min(1),
name: z.string().optional(),
versionFilter: z versionFilter: z
.object({ .object({
prefix: z.string().optional(), prefix: z.string().optional(),

View File

@@ -7,6 +7,7 @@ export interface ReleasesVersionFilter {
export interface ReleasesRepository { export interface ReleasesRepository {
providerKey: string; providerKey: string;
identifier: string; identifier: string;
name?: string;
versionFilter?: ReleasesVersionFilter; versionFilter?: ReleasesVersionFilter;
iconUrl?: string; iconUrl?: string;
} }