From 278498e11a272c923a5f1822eb5670d7a362a641 Mon Sep 17 00:00:00 2001 From: evanaronson Date: Sat, 4 Jul 2026 15:55:02 +0200 Subject: [PATCH 1/2] make link text optional --- ...DistributorCreateCampaignActionDetails.tsx | 12 +- .../createProposalActionDetails.tsx | 12 +- ...ugeRegistrarRegisterGaugeActionDetails.tsx | 12 +- .../gaugeVoterCreateGaugeActionDetails.tsx | 12 +- ...eVoterUpdateGaugeMetadataActionDetails.tsx | 12 +- src/assets/locales/en.json | 2 +- .../aragonProfileDialog.tsx | 4 +- .../daoDashboardPageClient.tsx | 12 +- .../components/daoInfoAside/daoInfoAside.tsx | 12 +- .../daoProposalDetailsPageClient.test.tsx | 14 ++ .../daoProposalDetailsPageClient.tsx | 14 +- .../components/daoHierarchy/daoHierarchy.tsx | 11 +- .../daoPluginInfo/daoPluginInfoMetadata.tsx | 14 +- .../daoSettingsInfo/daoSettingsInfo.tsx | 11 +- .../capitalDistributorClaimDialogDetails.tsx | 11 +- .../capitalDistributorRewardsPageClient.tsx | 13 +- .../gaugeVoterGaugesPageClient.tsx | 13 +- .../resourcesInputItem.test.tsx | 134 ++++++++++++------ .../resourcesInput/resourcesInputItem.tsx | 39 +++-- src/shared/components/resourceLink/index.ts | 1 + .../resourceLink/resourceLink.test.tsx | 34 +++++ .../components/resourceLink/resourceLink.tsx | 29 ++++ 22 files changed, 274 insertions(+), 154 deletions(-) create mode 100644 src/shared/components/resourceLink/index.ts create mode 100644 src/shared/components/resourceLink/resourceLink.test.tsx create mode 100644 src/shared/components/resourceLink/resourceLink.tsx diff --git a/src/actions/capitalDistributor/components/capitalDistributorCreateCampaignActionDetails/capitalDistributorCreateCampaignActionDetails.tsx b/src/actions/capitalDistributor/components/capitalDistributorCreateCampaignActionDetails/capitalDistributorCreateCampaignActionDetails.tsx index ec829d2b25..225fa0e31f 100644 --- a/src/actions/capitalDistributor/components/capitalDistributorCreateCampaignActionDetails/capitalDistributorCreateCampaignActionDetails.tsx +++ b/src/actions/capitalDistributor/components/capitalDistributorCreateCampaignActionDetails/capitalDistributorCreateCampaignActionDetails.tsx @@ -10,12 +10,12 @@ import { formatterUtils, type IProposalAction, type IProposalActionComponentProps, - Link, NumberFormat, } from '@aragon/gov-ui-kit'; import { formatUnits, type Hex, zeroAddress, zeroHash } from 'viem'; import type { IProposalActionData } from '@/modules/governance/components/createProposalForm'; import { useDao } from '@/shared/api/daoService'; +import { ResourceLink } from '@/shared/components/resourceLink'; import { type TranslationFunction, useTranslations, @@ -147,14 +147,12 @@ export const CapitalDistributorCreateCampaignActionDetails: React.FC< >
{resources.map((link) => ( - - {link.name} - + name={link.name} + url={link.url} + /> ))}
diff --git a/src/actions/core/createProposal/createProposalActionDetails.tsx b/src/actions/core/createProposal/createProposalActionDetails.tsx index a3b67cb9a6..03d70972d8 100644 --- a/src/actions/core/createProposal/createProposalActionDetails.tsx +++ b/src/actions/core/createProposal/createProposalActionDetails.tsx @@ -8,10 +8,10 @@ import { InputContainer, type IProposalAction, type IProposalActionComponentProps, - Link, } from '@aragon/gov-ui-kit'; import type { IProposalActionData } from '@/modules/governance/components/createProposalForm'; import { NestedActionsList } from '@/modules/governance/components/nestedActionsList'; +import { ResourceLink } from '@/shared/components/resourceLink'; import { SafeDocumentParser } from '@/shared/components/SafeDocumentParser'; import { useTranslations } from '@/shared/components/translationsProvider'; import type { ICoreActionCreateProposal } from '../types/coreActionCreateProposal'; @@ -103,14 +103,12 @@ export const CreateProposalActionDetails: React.FC< >
{metadata.resources.map((resource) => ( - - {resource.name} - + name={resource.name} + url={resource.url} + /> ))}
diff --git a/src/actions/gaugeRegistrar/components/gaugeRegistrarRegisterGaugeActionDetails/gaugeRegistrarRegisterGaugeActionDetails.tsx b/src/actions/gaugeRegistrar/components/gaugeRegistrarRegisterGaugeActionDetails/gaugeRegistrarRegisterGaugeActionDetails.tsx index fcf7e35f11..f6565c8635 100644 --- a/src/actions/gaugeRegistrar/components/gaugeRegistrarRegisterGaugeActionDetails/gaugeRegistrarRegisterGaugeActionDetails.tsx +++ b/src/actions/gaugeRegistrar/components/gaugeRegistrarRegisterGaugeActionDetails/gaugeRegistrarRegisterGaugeActionDetails.tsx @@ -8,10 +8,10 @@ import { type IProposalAction, type IProposalActionComponentProps, type IProposalActionInputDataParameter, - Link, } from '@aragon/gov-ui-kit'; import type { IProposalActionData } from '@/modules/governance/components/createProposalForm'; import { useDao } from '@/shared/api/daoService'; +import { ResourceLink } from '@/shared/components/resourceLink'; import { useTranslations } from '@/shared/components/translationsProvider'; import { useDaoChain } from '@/shared/hooks/useDaoChain'; import { ipfsUtils } from '@/shared/utils/ipfsUtils'; @@ -103,14 +103,12 @@ export const GaugeRegistrarRegisterGaugeActionDetails: React.FC< >
{links.map((link) => ( - - {link.name} - + name={link.name} + url={link.url} + /> ))}
diff --git a/src/actions/gaugeVoter/components/gaugeVoterCreateGaugeActionDetails/gaugeVoterCreateGaugeActionDetails.tsx b/src/actions/gaugeVoter/components/gaugeVoterCreateGaugeActionDetails/gaugeVoterCreateGaugeActionDetails.tsx index be3a4d9a18..85033d9706 100644 --- a/src/actions/gaugeVoter/components/gaugeVoterCreateGaugeActionDetails/gaugeVoterCreateGaugeActionDetails.tsx +++ b/src/actions/gaugeVoter/components/gaugeVoterCreateGaugeActionDetails/gaugeVoterCreateGaugeActionDetails.tsx @@ -7,10 +7,10 @@ import { DefinitionList, type IProposalAction, type IProposalActionComponentProps, - Link, } from '@aragon/gov-ui-kit'; import type { IProposalActionData } from '@/modules/governance/components/createProposalForm'; import { useDao } from '@/shared/api/daoService'; +import { ResourceLink } from '@/shared/components/resourceLink'; import { useTranslations } from '@/shared/components/translationsProvider'; import { useDaoChain } from '@/shared/hooks/useDaoChain'; import { ipfsUtils } from '@/shared/utils/ipfsUtils'; @@ -86,14 +86,12 @@ export const GaugeVoterCreateGaugeActionDetails: React.FC< >
{links.map((link) => ( - - {link.name} - + name={link.name} + url={link.url} + /> ))}
diff --git a/src/actions/gaugeVoter/components/gaugeVoterUpdateGaugeMetadataActionDetails/gaugeVoterUpdateGaugeMetadataActionDetails.tsx b/src/actions/gaugeVoter/components/gaugeVoterUpdateGaugeMetadataActionDetails/gaugeVoterUpdateGaugeMetadataActionDetails.tsx index ebeef77f8f..68dc2d4476 100644 --- a/src/actions/gaugeVoter/components/gaugeVoterUpdateGaugeMetadataActionDetails/gaugeVoterUpdateGaugeMetadataActionDetails.tsx +++ b/src/actions/gaugeVoter/components/gaugeVoterUpdateGaugeMetadataActionDetails/gaugeVoterUpdateGaugeMetadataActionDetails.tsx @@ -7,10 +7,10 @@ import { DefinitionList, type IProposalAction, type IProposalActionComponentProps, - Link, } from '@aragon/gov-ui-kit'; import type { IProposalActionData } from '@/modules/governance/components/createProposalForm'; import { useDao } from '@/shared/api/daoService'; +import { ResourceLink } from '@/shared/components/resourceLink'; import { useTranslations } from '@/shared/components/translationsProvider'; import { useDaoChain } from '@/shared/hooks/useDaoChain'; import { ipfsUtils } from '@/shared/utils/ipfsUtils'; @@ -86,14 +86,12 @@ export const GaugeVoterUpdateGaugeMetadataActionDetails: React.FC< >
{links.map((link) => ( - - {link.name} - + name={link.name} + url={link.url} + /> ))}
diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index fabdd295a8..662b8f86df 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -580,7 +580,7 @@ }, "dangerZone": { "title": "Delete Aragon profile", - "description": "Once you delete your Aragon profile it's released and can be claimed by someone else. This can't be undone." + "description": "Once you delete your Aragon profile, the name is released and can be claimed by anyone else. This action cannot be undone." } }, "aragonProfileReleaseAlertDialog": { diff --git a/src/modules/application/dialogs/aragonProfileDialog/aragonProfileDialog.tsx b/src/modules/application/dialogs/aragonProfileDialog/aragonProfileDialog.tsx index 7cc0d6dd43..33b1177ddc 100644 --- a/src/modules/application/dialogs/aragonProfileDialog/aragonProfileDialog.tsx +++ b/src/modules/application/dialogs/aragonProfileDialog/aragonProfileDialog.tsx @@ -287,7 +287,7 @@ export const AragonProfileDialog: React.FC = ( className="w-fit" iconLeft={IconType.PEN} onClick={handleRenameAragonName} - size="md" + size="sm" variant="tertiary" > {t( @@ -384,7 +384,7 @@ export const AragonProfileDialog: React.FC = ( + + + ); + }; + + it('renders the URL field before the link text field', () => { render(createTestComponent()); + + const urlInput = screen.getByLabelText( + /resourcesInput.item.linkInput.title/, + ); + const labelInput = screen.getByLabelText( + /resourcesInput.item.labelInput.title/, + ); + expect( - screen.getByPlaceholderText( - /resourcesInput.item.linkInput.placeholder/, - ), - ).toBeInTheDocument(); + urlInput.compareDocumentPosition(labelInput) & + Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); + }); + + it('uses the URL placeholder without prefilling a value', () => { + render(createTestComponent()); + + const urlInput = screen.getByPlaceholderText( + /resourcesInput.item.linkInput.placeholder/, + ); + + expect(urlInput).toHaveValue(''); }); it('calls remove function when remove button is clicked', async () => { @@ -68,11 +90,11 @@ describe(' component', () => { it('accepts valid URL format in link input', async () => { render(createTestComponent()); - const linkInput = screen.getByPlaceholderText( + const urlInput = screen.getByPlaceholderText( /resourcesInput.item.linkInput.placeholder/, ); - await userEvent.type(linkInput, 'https://example.com'); + await userEvent.type(urlInput, 'https://example.com'); await userEvent.tab(); expect( @@ -81,31 +103,57 @@ describe(' component', () => { }); it('validates URL format in link input', async () => { - useFormFieldSpy.mockImplementationOnce((name, options) => ({ - name, - onChange: jest.fn(), - onBlur: jest.fn(), - value: '', - ref: jest.fn(), - variant: 'critical', - alert: { message: 'Invalid URL format', variant: 'critical' }, - label: options?.label, - })); - - render(createTestComponent()); + render(); - const linkInput = screen.getByPlaceholderText( + const urlInput = screen.getByPlaceholderText( /resourcesInput.item.linkInput.placeholder/, ); - await userEvent.type(linkInput, 'broken link'); + await userEvent.type(urlInput, 'broken link'); await userEvent.tab(); expect( - await screen.findByText('Invalid URL format'), + await screen.findByText(/formField.error.pattern/), ).toBeInTheDocument(); }); + it('submits with an empty link text when the URL is valid', async () => { + const onSubmit = jest.fn(); + render(); + + const urlInput = screen.getByPlaceholderText( + /resourcesInput.item.linkInput.placeholder/, + ); + + await userEvent.type(urlInput, 'https://example.com'); + await userEvent.click(screen.getByRole('button', { name: 'Submit' })); + + await waitFor(() => expect(onSubmit).toHaveBeenCalled()); + expect(onSubmit.mock.calls[0][0]).toEqual({ + resources: [{ name: '', url: 'https://example.com' }], + }); + }); + + it('mirrors the URL value as the link text placeholder', async () => { + render(createTestComponent()); + + const urlInput = screen.getByPlaceholderText( + /resourcesInput.item.linkInput.placeholder/, + ); + const labelInput = screen.getByLabelText( + /resourcesInput.item.labelInput.title/, + ); + + await userEvent.type(urlInput, 'https://example.com'); + + await waitFor(() => + expect(labelInput).toHaveAttribute( + 'placeholder', + 'https://example.com', + ), + ); + }); + it('sets a max length requirement for the resource label', () => { render(createTestComponent()); diff --git a/src/shared/components/forms/resourcesInput/resourcesInputItem.tsx b/src/shared/components/forms/resourcesInput/resourcesInputItem.tsx index 639ed3844d..1e07815fc7 100644 --- a/src/shared/components/forms/resourcesInput/resourcesInputItem.tsx +++ b/src/shared/components/forms/resourcesInput/resourcesInputItem.tsx @@ -5,6 +5,7 @@ import { IconType, InputText, } from '@aragon/gov-ui-kit'; +import { useWatch } from 'react-hook-form'; import { useTranslations } from '@/shared/components/translationsProvider'; import { useFormField } from '@/shared/hooks/useFormField'; @@ -32,17 +33,6 @@ export const ResourcesInputItem: React.FC = ( const { t } = useTranslations(); - const nameFieldName = `${name}.${index.toString()}.name`; - const nameField = useFormField< - ResourcesInputItemBaseForm, - typeof nameFieldName - >(nameFieldName, { - label: t('app.shared.resourcesInput.item.labelInput.title'), - rules: { required: true }, - defaultValue: '', - trimOnBlur: true, - }); - /** * URL Regex: * - Optional protocol (http:// or https://) @@ -55,6 +45,9 @@ export const ResourcesInputItem: React.FC = ( /^(https?:\/\/)?(([\da-zA-Z-]+\.)+[a-zA-Z]{2,})(\/[^\s?#]*)?(\?[^\s#]*)?(#[^\s]*)?$/; const urlFieldName = `${name}.${index.toString()}.url`; + const urlValue = useWatch({ + name: urlFieldName, + }); const urlField = useFormField< ResourcesInputItemBaseForm, typeof urlFieldName @@ -65,16 +58,36 @@ export const ResourcesInputItem: React.FC = ( trimOnBlur: true, }); + const nameFieldName = `${name}.${index.toString()}.name`; + const nameField = useFormField< + ResourcesInputItemBaseForm, + typeof nameFieldName + >(nameFieldName, { + label: t('app.shared.resourcesInput.item.labelInput.title'), + defaultValue: '', + trimOnBlur: true, + }); + + const namePlaceholder = + typeof urlValue === 'string' && urlValue.length > 0 + ? urlValue + : undefined; + return ( - - + +
component', () => { + const createTestComponent = (props?: Partial) => { + const completeProps: IResourceLinkProps = { + url: 'https://example.com', + isExternal: true, + ...props, + }; + + return ; + }; + + it('uses the URL once as link text when the name is empty', () => { + const url = 'https://example.com'; + render(createTestComponent({ name: '', url })); + + const link = screen.getByRole('link', { name: url }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', url); + expect(screen.getAllByText(url)).toHaveLength(1); + }); + + it('uses the name as link text and shows the URL when the name is present', () => { + const url = 'https://example.com'; + render(createTestComponent({ name: 'Example', url })); + + const link = screen.getByRole('link', { name: /Example/ }); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', url); + expect(screen.getByText(url)).toBeInTheDocument(); + }); +}); diff --git a/src/shared/components/resourceLink/resourceLink.tsx b/src/shared/components/resourceLink/resourceLink.tsx new file mode 100644 index 0000000000..c852dae578 --- /dev/null +++ b/src/shared/components/resourceLink/resourceLink.tsx @@ -0,0 +1,29 @@ +import { type ILinkProps, Link } from '@aragon/gov-ui-kit'; + +export interface IResourceLinkProps + extends Omit { + /** + * Optional custom text for the resource link. + */ + name?: string; + /** + * Resource URL. + */ + url: string; +} + +/** + * Renders an external resource link (gov-ui-kit Link) for user-authored resources: the name is used as link text with + * the URL shown below it, falling back to the URL alone when no name is set. Not to be confused with the shared Link + * component, which wraps the Next.js Link for internal navigation. + */ +export const ResourceLink: React.FC = (props) => { + const { name, url, ...otherProps } = props; + const hasName = Boolean(name); + + return ( + + {name || url} + + ); +}; From 9903984b600bd17777b8bb1c4daf66ef3e3a1aa3 Mon Sep 17 00:00:00 2001 From: evanaronson Date: Sat, 4 Jul 2026 15:55:18 +0200 Subject: [PATCH 2/2] changeset --- .changeset/mandatory-fields-irritate.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/mandatory-fields-irritate.md diff --git a/.changeset/mandatory-fields-irritate.md b/.changeset/mandatory-fields-irritate.md new file mode 100644 index 0000000000..7cad57efa4 --- /dev/null +++ b/.changeset/mandatory-fields-irritate.md @@ -0,0 +1,5 @@ +--- +"@aragon/app": patch +--- + +Allow resource links to be saved and rendered without custom link text.