diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f322c81..1977719c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] ### Added -* *Nothing* +* [#535](https://github.com/shlinkio/shlink-web-client/pull/535) Allowed editing default domain redirects when consuming Shlink 2.10 or newer. ### Changed * [#534](https://github.com/shlinkio/shlink-web-client/pull/534) Updated axios. diff --git a/src/api/services/ShlinkApiClient.ts b/src/api/services/ShlinkApiClient.ts index 6c783531..35ebebad 100644 --- a/src/api/services/ShlinkApiClient.ts +++ b/src/api/services/ShlinkApiClient.ts @@ -11,7 +11,6 @@ import { ShlinkVisits, ShlinkVisitsParams, ShlinkShortUrlData, - ShlinkDomain, ShlinkDomainsResponse, ShlinkVisitsOverview, ShlinkEditDomainRedirects, @@ -107,8 +106,8 @@ export default class ShlinkApiClient { this.performRequest('/mercure-info', 'GET') .then((resp) => resp.data); - public readonly listDomains = async (): Promise => - this.performRequest<{ domains: ShlinkDomainsResponse }>('/domains', 'GET').then(({ data }) => data.domains.data); + public readonly listDomains = async (): Promise => + this.performRequest<{ domains: ShlinkDomainsResponse }>('/domains', 'GET').then(({ data }) => data.domains); public readonly editDomainRedirects = async ( domainRedirects: ShlinkEditDomainRedirects, diff --git a/src/api/types/index.ts b/src/api/types/index.ts index 478194fc..c5d11a19 100644 --- a/src/api/types/index.ts +++ b/src/api/types/index.ts @@ -84,6 +84,7 @@ export interface ShlinkDomain { export interface ShlinkDomainsResponse { data: ShlinkDomain[]; + defaultRedirects?: ShlinkDomainRedirects; // Optional only for Shlink older than 2.10 } export interface ShlinkShortUrlsListParams { diff --git a/src/domains/DomainRow.tsx b/src/domains/DomainRow.tsx index 2c6bdef9..1a2f67ea 100644 --- a/src/domains/DomainRow.tsx +++ b/src/domains/DomainRow.tsx @@ -9,12 +9,15 @@ import { import { ShlinkDomain, ShlinkDomainRedirects } from '../api/types'; import { useToggle } from '../utils/helpers/hooks'; import { OptionalString } from '../utils/utils'; +import { SelectedServer } from '../servers/data'; +import { supportsDefaultDomainRedirectsEdition } from '../utils/helpers/features'; import { EditDomainRedirectsModal } from './helpers/EditDomainRedirectsModal'; interface DomainRowProps { domain: ShlinkDomain; defaultRedirects?: ShlinkDomainRedirects; editDomainRedirects: (domain: string, redirects: Partial) => Promise; + selectedServer: SelectedServer; } const Nr: FC<{ fallback: OptionalString }> = ({ fallback }) => ( @@ -30,9 +33,10 @@ const DefaultDomain: FC = () => ( ); -export const DomainRow: FC = ({ domain, editDomainRedirects, defaultRedirects }) => { +export const DomainRow: FC = ({ domain, editDomainRedirects, defaultRedirects, selectedServer }) => { const [ isOpen, toggle ] = useToggle(); const { domain: authority, isDefault, redirects } = domain; + const canEditDomain = !isDefault || supportsDefaultDomainRedirectsEdition(selectedServer); return ( @@ -48,12 +52,12 @@ export const DomainRow: FC = ({ domain, editDomainRedirects, def {redirects?.invalidShortUrlRedirect ?? } - - - {isDefault && ( + {!canEditDomain && ( Redirects for default domain cannot be edited here.
diff --git a/src/domains/ManageDomains.tsx b/src/domains/ManageDomains.tsx index 69f67a30..1466a7a2 100644 --- a/src/domains/ManageDomains.tsx +++ b/src/domains/ManageDomains.tsx @@ -5,6 +5,7 @@ import { ShlinkApiError } from '../api/ShlinkApiError'; import { SimpleCard } from '../utils/SimpleCard'; import SearchField from '../utils/SearchField'; import { ShlinkDomainRedirects } from '../api/types'; +import { SelectedServer } from '../servers/data'; import { DomainsList } from './reducers/domainsList'; import { DomainRow } from './DomainRow'; @@ -13,15 +14,16 @@ interface ManageDomainsProps { filterDomains: (searchTerm: string) => void; editDomainRedirects: (domain: string, redirects: Partial) => Promise; domainsList: DomainsList; + selectedServer: SelectedServer; } const headers = [ '', 'Domain', 'Base path redirect', 'Regular 404 redirect', 'Invalid short URL redirect', '' ]; export const ManageDomains: FC = ( - { listDomains, domainsList, filterDomains, editDomainRedirects }, + { listDomains, domainsList, filterDomains, editDomainRedirects, selectedServer }, ) => { - const { filteredDomains: domains, loading, error, errorData } = domainsList; - const defaultRedirects = domains.find(({ isDefault }) => isDefault)?.redirects; + const { filteredDomains: domains, defaultRedirects, loading, error, errorData } = domainsList; + const resolvedDefaultRedirects = defaultRedirects ?? domains.find(({ isDefault }) => isDefault)?.redirects; useEffect(() => { listDomains(); @@ -53,7 +55,8 @@ export const ManageDomains: FC = ( key={domain.domain} domain={domain} editDomainRedirects={editDomainRedirects} - defaultRedirects={defaultRedirects} + defaultRedirects={resolvedDefaultRedirects} + selectedServer={selectedServer} /> ))} diff --git a/src/domains/reducers/domainsList.ts b/src/domains/reducers/domainsList.ts index c8ee513d..6ade3fb3 100644 --- a/src/domains/reducers/domainsList.ts +++ b/src/domains/reducers/domainsList.ts @@ -17,6 +17,7 @@ export const FILTER_DOMAINS = 'shlink/domainsList/FILTER_DOMAINS'; export interface DomainsList { domains: ShlinkDomain[]; filteredDomains: ShlinkDomain[]; + defaultRedirects?: ShlinkDomainRedirects; loading: boolean; error: boolean; errorData?: ProblemDetailsError; @@ -24,6 +25,7 @@ export interface DomainsList { export interface ListDomainsAction extends Action { domains: ShlinkDomain[]; + defaultRedirects?: ShlinkDomainRedirects; } interface FilterDomainsAction extends Action { @@ -48,7 +50,8 @@ export const replaceRedirectsOnDomain = (domain: string, redirects: ShlinkDomain export default buildReducer({ [LIST_DOMAINS_START]: () => ({ ...initialState, loading: true }), [LIST_DOMAINS_ERROR]: ({ errorData }) => ({ ...initialState, error: true, errorData }), - [LIST_DOMAINS]: (_, { domains }) => ({ ...initialState, domains, filteredDomains: domains }), + [LIST_DOMAINS]: (_, { domains, defaultRedirects }) => + ({ ...initialState, domains, filteredDomains: domains, defaultRedirects }), [FILTER_DOMAINS]: (state, { searchTerm }) => ({ ...state, filteredDomains: state.domains.filter(({ domain }) => domain.toLowerCase().match(searchTerm)), @@ -68,9 +71,9 @@ export const listDomains = (buildShlinkApiClient: ShlinkApiClientBuilder) => () const { listDomains } = buildShlinkApiClient(getState); try { - const domains = await listDomains(); + const { data: domains, defaultRedirects } = await listDomains(); - dispatch({ type: LIST_DOMAINS, domains }); + dispatch({ type: LIST_DOMAINS, domains, defaultRedirects }); } catch (e: any) { dispatch({ type: LIST_DOMAINS_ERROR, errorData: parseApiError(e) }); } diff --git a/src/domains/services/provideServices.ts b/src/domains/services/provideServices.ts index e6f01b1b..55331860 100644 --- a/src/domains/services/provideServices.ts +++ b/src/domains/services/provideServices.ts @@ -12,7 +12,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => { bottle.serviceFactory('ManageDomains', () => ManageDomains); bottle.decorator('ManageDomains', connect( - [ 'domainsList' ], + [ 'domainsList', 'selectedServer' ], [ 'listDomains', 'filterDomains', 'editDomainRedirects' ], )); diff --git a/src/utils/helpers/features.ts b/src/utils/helpers/features.ts index d424e512..8706a0eb 100644 --- a/src/utils/helpers/features.ts +++ b/src/utils/helpers/features.ts @@ -23,3 +23,5 @@ export const supportsQrErrorCorrection = serverMatchesVersions({ minVersion: '2. export const supportsDomainRedirects = supportsQrErrorCorrection; export const supportsForwardQuery = serverMatchesVersions({ minVersion: '2.9.0' }); + +export const supportsDefaultDomainRedirectsEdition = serverMatchesVersions({ minVersion: '2.10.0' }); diff --git a/test/api/services/ShlinkApiClient.test.ts b/test/api/services/ShlinkApiClient.test.ts index 95bf899f..82efa569 100644 --- a/test/api/services/ShlinkApiClient.test.ts +++ b/test/api/services/ShlinkApiClient.test.ts @@ -256,10 +256,8 @@ describe('ShlinkApiClient', () => { describe('listDomains', () => { it('returns domains', async () => { - const expectedData = [ Mock.all(), Mock.all() ]; - const resp = { - domains: { data: expectedData }, - }; + const expectedData = { data: [ Mock.all(), Mock.all() ] }; + const resp = { domains: expectedData }; const axiosSpy = createAxiosMock({ data: resp }); const { listDomains } = new ShlinkApiClient(axiosSpy, '', ''); diff --git a/test/domains/DomainRow.test.tsx b/test/domains/DomainRow.test.tsx index 98232aa9..2034ad7e 100644 --- a/test/domains/DomainRow.test.tsx +++ b/test/domains/DomainRow.test.tsx @@ -5,11 +5,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faBan as forbiddenIcon, faEdit as editIcon } from '@fortawesome/free-solid-svg-icons'; import { ShlinkDomain, ShlinkDomainRedirects } from '../../src/api/types'; import { DomainRow } from '../../src/domains/DomainRow'; +import { ReachableServer, SelectedServer } from '../../src/servers/data'; describe('', () => { let wrapper: ShallowWrapper; - const createWrapper = (domain: ShlinkDomain) => { - wrapper = shallow(); + const createWrapper = (domain: ShlinkDomain, selectedServer = Mock.all()) => { + wrapper = shallow(); return wrapper; }; @@ -17,28 +18,60 @@ describe('', () => { afterEach(() => wrapper?.unmount()); it.each([ - [ Mock.of({ domain: '', isDefault: true }), 1, 'defaultDomainBtn' ], - [ Mock.of({ domain: '', isDefault: false }), 0, undefined ], - [ Mock.of({ domain: 'foo.com', isDefault: true }), 1, 'defaultDomainBtn' ], - [ Mock.of({ domain: 'foo.bar.com', isDefault: true }), 1, 'defaultDomainBtn' ], - [ Mock.of({ domain: 'foo.baz', isDefault: false }), 0, undefined ], - ])('shows proper components based on the fact that provided domain is default or not', ( + [ Mock.of({ domain: '', isDefault: true }), undefined, 1, 1, 'defaultDomainBtn' ], + [ Mock.of({ domain: '', isDefault: false }), undefined, 0, 0, undefined ], + [ Mock.of({ domain: 'foo.com', isDefault: true }), undefined, 1, 1, 'defaultDomainBtn' ], + [ Mock.of({ domain: 'foo.bar.com', isDefault: true }), undefined, 1, 1, 'defaultDomainBtn' ], + [ Mock.of({ domain: 'foo.baz', isDefault: false }), undefined, 0, 0, undefined ], + [ + Mock.of({ domain: 'foo.baz', isDefault: true }), + Mock.of({ version: '2.10.0' }), + 1, + 0, + undefined, + ], + [ + Mock.of({ domain: 'foo.baz', isDefault: true }), + Mock.of({ version: '2.9.0' }), + 1, + 1, + 'defaultDomainBtn', + ], + [ + Mock.of({ domain: 'foo.baz', isDefault: false }), + Mock.of({ version: '2.9.0' }), + 0, + 0, + undefined, + ], + [ + Mock.of({ domain: 'foo.baz', isDefault: false }), + Mock.of({ version: '2.10.0' }), + 0, + 0, + undefined, + ], + ])('shows proper components based on provided domain and selectedServer', ( domain, - expectedComps, + selectedServer, + expectedDefaultDomainIcons, + expectedDisabledComps, expectedDomainId, ) => { - const wrapper = createWrapper(domain); + const wrapper = createWrapper(domain, selectedServer); const defaultDomainComp = wrapper.find('td').first().find('DefaultDomain'); + const disabledBtn = wrapper.find(Button).findWhere((btn) => !!btn.prop('disabled')); const tooltip = wrapper.find(UncontrolledTooltip); const button = wrapper.find(Button); const icon = wrapper.find(FontAwesomeIcon); - expect(defaultDomainComp).toHaveLength(expectedComps); - expect(button.prop('disabled')).toEqual(domain.isDefault); - expect(icon.prop('icon')).toEqual(domain.isDefault ? forbiddenIcon : editIcon); - expect(tooltip).toHaveLength(expectedComps); + expect(defaultDomainComp).toHaveLength(expectedDefaultDomainIcons); + expect(disabledBtn).toHaveLength(expectedDisabledComps); + expect(button.prop('disabled')).toEqual(expectedDisabledComps > 0); + expect(icon.prop('icon')).toEqual(expectedDisabledComps > 0 ? forbiddenIcon : editIcon); + expect(tooltip).toHaveLength(expectedDisabledComps); - if (expectedComps > 0) { + if (expectedDisabledComps > 0) { expect(tooltip.prop('target')).toEqual(expectedDomainId); } }); diff --git a/test/domains/ManageDomains.test.tsx b/test/domains/ManageDomains.test.tsx index d5006935..6c5180e8 100644 --- a/test/domains/ManageDomains.test.tsx +++ b/test/domains/ManageDomains.test.tsx @@ -8,6 +8,7 @@ import SearchField from '../../src/utils/SearchField'; import { ProblemDetailsError, ShlinkDomain } from '../../src/api/types'; import { ShlinkApiError } from '../../src/api/ShlinkApiError'; import { DomainRow } from '../../src/domains/DomainRow'; +import { SelectedServer } from '../../src/servers/data'; describe('', () => { const listDomains = jest.fn(); @@ -21,6 +22,7 @@ describe('', () => { filterDomains={filterDomains} editDomainRedirects={editDomainRedirects} domainsList={domainsList} + selectedServer={Mock.all()} />, ); diff --git a/test/domains/reducers/domainsList.test.ts b/test/domains/reducers/domainsList.test.ts index 5bc1eb9c..83845197 100644 --- a/test/domains/reducers/domainsList.test.ts +++ b/test/domains/reducers/domainsList.test.ts @@ -14,7 +14,7 @@ import { EDIT_DOMAIN_REDIRECTS } from '../../../src/domains/reducers/domainRedir import { ShlinkDomain, ShlinkDomainRedirects } from '../../../src/api/types'; import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient'; -describe('domainsList', () => { +describe('domainsListReducer', () => { const filteredDomains = [ Mock.of({ domain: 'foo' }), Mock.of({ domain: 'boo' }) ]; const domains = [ ...filteredDomains, Mock.of({ domain: 'bar' }) ]; @@ -88,13 +88,13 @@ describe('domainsList', () => { }); it('dispatches domains once loaded', async () => { - listDomains.mockResolvedValue(domains); + listDomains.mockResolvedValue({ data: domains }); await listDomainsAction(buildShlinkApiClient)()(dispatch, getState); expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch).toHaveBeenNthCalledWith(1, { type: LIST_DOMAINS_START }); - expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_DOMAINS, domains }); + expect(dispatch).toHaveBeenNthCalledWith(2, { type: LIST_DOMAINS, domains, defaultRedirects: undefined }); expect(listDomains).toHaveBeenCalledTimes(1); }); });