mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-27 00:40:58 +01:00
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:
@@ -2072,6 +2072,10 @@
|
||||
"showDetails": {
|
||||
"label": "Show Details"
|
||||
},
|
||||
"topReleases": {
|
||||
"label": "Top Releases",
|
||||
"description": "The max number of latest releases to show. Zero means no limit."
|
||||
},
|
||||
"repositories": {
|
||||
"label": "Repositories",
|
||||
"addRRepository": {
|
||||
@@ -2084,6 +2088,9 @@
|
||||
"label": "Identifier",
|
||||
"placeholder": "Name or Owner/Name"
|
||||
},
|
||||
"name": {
|
||||
"label": "Name"
|
||||
},
|
||||
"versionFilter": {
|
||||
"label": "Version Filter",
|
||||
"prefix": {
|
||||
|
||||
@@ -40,6 +40,7 @@ export const WidgetMultiReleasesRepositoriesInput = ({
|
||||
(repository: ReleasesRepository, index: number): FormValidation => {
|
||||
form.setFieldValue(`options.${property}.${index}.providerKey`, repository.providerKey);
|
||||
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}.iconUrl`, repository.iconUrl);
|
||||
|
||||
@@ -123,7 +124,8 @@ export const WidgetMultiReleasesRepositoriesInput = ({
|
||||
|
||||
<Group justify="space-between" align="center" style={{ flex: 1 }} gap={5}>
|
||||
<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 c="dimmed" size="xs" ta="end" style={{ flex: 1, whiteSpace: "nowrap" }}>
|
||||
@@ -178,6 +180,11 @@ const formatVersionFilterRegex = (versionFilter: ReleasesVersionFilter | undefin
|
||||
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 {
|
||||
fieldPath: string;
|
||||
repository: ReleasesRepository;
|
||||
@@ -209,7 +216,7 @@ const ReleaseEditModal = createModal<ReleaseEditProps>(({ innerProps, actions })
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Group align="center">
|
||||
<Group align="center" wrap="nowrap">
|
||||
<Select
|
||||
withAsterisk
|
||||
label={tRepository("provider.label")}
|
||||
@@ -224,6 +231,7 @@ const ReleaseEditModal = createModal<ReleaseEditProps>(({ innerProps, actions })
|
||||
handleChange({ providerKey: value });
|
||||
}
|
||||
}}
|
||||
style={{ flex: 1, flexBasis: "40%" }}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
@@ -231,10 +239,38 @@ const ReleaseEditModal = createModal<ReleaseEditProps>(({ innerProps, actions })
|
||||
label={tRepository("identifier.label")}
|
||||
value={tempRepository.identifier}
|
||||
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`]}
|
||||
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>
|
||||
|
||||
@@ -298,13 +334,6 @@ const ReleaseEditModal = createModal<ReleaseEditProps>(({ innerProps, actions })
|
||||
</Text>
|
||||
</Fieldset>
|
||||
|
||||
<IconPicker
|
||||
withAsterisk={false}
|
||||
value={tempRepository.iconUrl}
|
||||
onChange={(url) => handleChange({ iconUrl: url })}
|
||||
error={formErrors[`${innerProps.fieldPath}.iconUrl`] as string}
|
||||
/>
|
||||
|
||||
<Divider my={"sm"} />
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={actions.closeModal} color="gray.5">
|
||||
|
||||
@@ -62,7 +62,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
|
||||
);
|
||||
|
||||
const repositories = useMemo(() => {
|
||||
return results
|
||||
const formattedResults = results
|
||||
.flat()
|
||||
.map(({ data }) => {
|
||||
if (data === undefined) return undefined;
|
||||
@@ -74,8 +74,8 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
|
||||
if (repository === undefined) return undefined;
|
||||
|
||||
return {
|
||||
...repository,
|
||||
...data,
|
||||
iconUrl: repository.iconUrl,
|
||||
isNewRelease:
|
||||
relativeDateOptions.newReleaseWithin !== "" && data.latestReleaseAt
|
||||
? isDateWithin(data.latestReleaseAt, relativeDateOptions.newReleaseWithin)
|
||||
@@ -99,10 +99,17 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
|
||||
if (repoB?.latestReleaseAt === undefined) return -1;
|
||||
return repoA.latestReleaseAt > repoB.latestReleaseAt ? -1 : 1;
|
||||
}) as ReleasesRepositoryResponse[];
|
||||
|
||||
if (typeof options.topReleases !== "string" && options.topReleases > 0) {
|
||||
return formattedResults.slice(0, options.topReleases);
|
||||
}
|
||||
|
||||
return formattedResults;
|
||||
}, [
|
||||
results,
|
||||
options.repositories,
|
||||
options.showOnlyHighlighted,
|
||||
options.topReleases,
|
||||
relativeDateOptions.newReleaseWithin,
|
||||
relativeDateOptions.staleReleaseWithin,
|
||||
]);
|
||||
@@ -152,7 +159,8 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
|
||||
/>
|
||||
|
||||
<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
|
||||
withArrow
|
||||
@@ -346,6 +354,11 @@ const ExpandedDisplay = ({ repository, hasIconColor }: ExtendedDisplayProps) =>
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Text size="xs" c="iconColor" ff="monospace">
|
||||
{repository.identifier}
|
||||
</Text>
|
||||
|
||||
{(repository.releaseUrl ?? repository.projectUrl) && (
|
||||
<>
|
||||
<Divider my={10} mx="30%" />
|
||||
|
||||
@@ -30,12 +30,18 @@ export const { definition, componentLoader } = createWidgetDefinition("releases"
|
||||
showDetails: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
topReleases: factory.number({
|
||||
withDescription: true,
|
||||
defaultValue: 0,
|
||||
validate: z.number().min(0),
|
||||
}),
|
||||
repositories: factory.multiReleasesRepositories({
|
||||
defaultValue: [],
|
||||
validate: z.array(
|
||||
z.object({
|
||||
providerKey: z.string().min(1),
|
||||
identifier: z.string().min(1),
|
||||
name: z.string().optional(),
|
||||
versionFilter: z
|
||||
.object({
|
||||
prefix: z.string().optional(),
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface ReleasesVersionFilter {
|
||||
export interface ReleasesRepository {
|
||||
providerKey: string;
|
||||
identifier: string;
|
||||
name?: string;
|
||||
versionFilter?: ReleasesVersionFilter;
|
||||
iconUrl?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user