diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.element.ts index 387d22bdf6..595da98b54 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.element.ts @@ -1,17 +1,330 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; -import { css, html, LitElement } from 'lit'; -import { customElement } from 'lit/decorators.js'; +import { css, html, nothing } from 'lit'; +import { customElement, state, query, property } from 'lit/decorators.js'; +import { UUIButtonState, UUIPaginationElement, UUIPaginationEvent } from '@umbraco-ui/uui'; +import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../../../../core/modal'; +import { UmbLitElement } from '@umbraco-cms/element'; +import { RedirectManagementResource, RedirectStatus, RedirectUrl } from '@umbraco-cms/backend-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; @customElement('umb-dashboard-redirect-management') -export class UmbDashboardRedirectManagementElement extends LitElement { - static styles = [UUITextStyles, css``]; +export class UmbDashboardRedirectManagementElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + .actions { + display: flex; + gap: 4px; + justify-content: space-between; + margin-bottom: 12px; + } + + .actions uui-icon { + transform: translateX(50%); + } + + uui-table { + table-layout: fixed; + } + + uui-table-head-cell:nth-child(2*n) { + width: 10%; + } + + uui-table-head-cell:last-child, + uui-table-cell:last-child { + text-align: right; + } + + uui-table uui-icon { + vertical-align: sub; + } + uui-pagination { + display: inline-block; + } + .pagination { + display: flex; + justify-content: center; + margin-top: var(--uui-size-space-5); + } + + .trackerDisabled { + position: relative; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + } + .trackerDisabled::after { + content: ''; + background-color: rgba(250, 250, 250, 0.7); + position: absolute; + border-radius: 2px; + left: 0; + right: 0; + top: 0; + bottom: 0; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; + } + + a { + color: var(--uui-color-interactive); + } + a:hover, + a:focus { + color: var(--uui-color-interactive-emphasis); + } + `, + ]; + + @property({ type: Number, attribute: 'items-per-page' }) + itemsPerPage = 20; + + @state() + private _redirectData?: RedirectUrl[]; + + @state() + private _trackerStatus = true; + + @state() + private _currentPage = 1; + + @state() + private _total?: number; + + @state() + private _buttonState: UUIButtonState; + + @state() + private _filter?: string; + + @query('#search-input') + private _searchField!: HTMLInputElement; + + @query('uui-pagination') + private _pagination?: UUIPaginationElement; + + private _modalService?: UmbModalService; + + constructor() { + super(); + this.consumeContext(UMB_MODAL_SERVICE_CONTEXT_TOKEN, (_instance) => { + this._modalService = _instance; + }); + } + + connectedCallback() { + super.connectedCallback(); + this._getTrackerStatus(); + this._getRedirectData(); + } + + private async _getTrackerStatus() { + const { data } = await tryExecuteAndNotify(this, RedirectManagementResource.getRedirectManagementStatus()); + if (data && data.status) this._trackerStatus = data.status === RedirectStatus.ENABLED ? true : false; + } + + private _removeRedirectHandler(data: RedirectUrl) { + const modalHandler = this._modalService?.confirm({ + headline: 'Delete', + content: html` +
+

This will remove the redirect

+ Original URL: ${data.originalUrl}
+ Redirected To: ${data.destinationUrl} +

Are you sure you want to delete?

+
+ `, + color: 'danger', + confirmLabel: 'Delete', + }); + modalHandler?.onClose().then(({ confirmed }) => { + if (confirmed) this._removeRedirect(data); + }); + } + + private async _removeRedirect(r: RedirectUrl) { + if (!r.key) return; + const res = await tryExecuteAndNotify( + this, + RedirectManagementResource.deleteRedirectManagementByKey({ key: r.key }) + ); + if (!res.error) { + // or just run a this._getRedirectData() again? + this.shadowRoot?.getElementById(`redirect-key-${r.key}`)?.remove(); + } + } + + private _disableRedirectHandler() { + const modalHandler = this._modalService?.confirm({ + headline: 'Disable URL tracker', + content: html`Are you sure you want to disable the URL tracker?`, + color: 'danger', + confirmLabel: 'Disable', + }); + modalHandler?.onClose().then(({ confirmed }) => { + if (confirmed) this._toggleRedirect(); + }); + } + + private async _toggleRedirect() { + const { error } = await tryExecuteAndNotify( + this, + RedirectManagementResource.postRedirectManagementStatus({ status: RedirectStatus.ENABLED }) + ); + + if (!error) { + this._trackerStatus = !this._trackerStatus; + } + } + + private _inputHandler(pressed: KeyboardEvent) { + if (pressed.key === 'Enter') this._searchHandler(); + } + + private async _searchHandler() { + this._filter = this._searchField.value; + if (this._pagination) this._pagination.current = 1; + this._currentPage = 1; + if (this._filter.length) { + this._buttonState = 'waiting'; + } + this._getRedirectData(); + } + + private _onPageChange(event: UUIPaginationEvent) { + if (this._currentPage === event.target.current) return; + this._currentPage = event.target.current; + this._getRedirectData(); + } + + private async _getRedirectData() { + const skip = this._currentPage * this.itemsPerPage - this.itemsPerPage; + const { data } = await tryExecuteAndNotify( + this, + RedirectManagementResource.getRedirectManagement({ filter: this._filter, take: this.itemsPerPage, skip }) + ); + if (data) { + this._total = data?.total; + this._redirectData = data?.items; + if (this._filter?.length) this._buttonState = 'success'; + } + } render() { - return html` - -

Redirect Management

+ return html`
+ ${this._trackerStatus + ? html`
+ + + + Search + +
+ + Disable URL tracker + ` + : html` + Enable URL tracker + `} +
+ + ${this._total && this._total > 0 + ? html`
+ ${this.renderTable()} +
` + : this._filter?.length + ? this._renderZeroResults() + : this.renderNoRedirects()} `; + } + + private _renderZeroResults() { + return html` + No redirects matching this search criteria +

Double check your search for any error or spelling mistakes.

+
`; + } + + private renderNoRedirects() { + return html` + No redirects have been made +

When a published page gets renamed or moved, a redirect will automatically be made to the new page.

+
`; + } + + private renderTable() { + return html` + + + Culture + Original URL + + Redirected To + Actions + + ${this._redirectData?.map((data) => { + return html` + ${data.culture || '*'} + + ${data.originalUrl} + + + + + + + ${data.destinationUrl} + + + + + + + + + + `; + })} + - `; + ${this._renderPagination()} + `; + } + + private _renderPagination() { + if (!this._total) return nothing; + + const totalPages = Math.ceil(this._total / this.itemsPerPage); + + if (totalPages <= 1) return nothing; + + return html``; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.test.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.test.ts new file mode 100644 index 0000000000..2d960a1757 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/dashboards/redirect-management/dashboard-redirect-management.test.ts @@ -0,0 +1,20 @@ +import { expect, fixture, html } from '@open-wc/testing'; + +import { UmbDashboardRedirectManagementElement } from './dashboard-redirect-management.element'; +import { defaultA11yConfig } from '@umbraco-cms/test-utils'; + +describe('UmbDashboardRedirectManagement', () => { + let element: UmbDashboardRedirectManagementElement; + + beforeEach(async () => { + element = await fixture(html``); + }); + + it('is defined with its own instance', () => { + expect(element).to.be.instanceOf(UmbDashboardRedirectManagementElement); + }); + + it('passes the a11y audit', async () => { + await expect(element).to.be.accessible(defaultA11yConfig); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-dashboards/section-dashboards.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-dashboards/section-dashboards.element.ts index 2bd975f3c7..3b20551708 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-dashboards/section-dashboards.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-dashboards/section-dashboards.element.ts @@ -4,12 +4,9 @@ import { customElement, state } from 'lit/decorators.js'; import { IRoutingInfo } from 'router-slot'; import { first, map } from 'rxjs'; import { UmbSectionContext, UMB_SECTION_CONTEXT_TOKEN } from '../section.context'; -import { createExtensionElement , umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; -import type { - ManifestDashboard, - ManifestDashboardCollection, - ManifestWithMeta, -} from '@umbraco-cms/models'; +import { createExtensionElement, umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; +import type { ManifestDashboard, ManifestDashboardCollection, ManifestWithMeta } from '@umbraco-cms/models'; + import { UmbLitElement } from '@umbraco-cms/element'; @customElement('umb-section-dashboards') @@ -29,7 +26,7 @@ export class UmbSectionDashboardsElement extends UmbLitElement { } #scroll-container { - flex:1; + flex: 1; } #router-slot { diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts index 347381db8c..c2cb2489d8 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/browser-handlers.ts @@ -19,6 +19,7 @@ import { handlers as dictionaryHandlers } from './domains/dictionary.handlers'; import { handlers as mediaTypeHandlers } from './domains/media-type.handlers'; import { handlers as memberGroupHandlers } from './domains/member-group.handlers'; import { handlers as memberTypeHandlers } from './domains/member-type.handlers'; +import { handlers as redirectManagement } from './domains/redirect-management.handlers'; const handlers = [ serverHandlers.serverVersionHandler, @@ -42,6 +43,7 @@ const handlers = [ ...healthCheckHandlers, ...profilingHandlers, ...dictionaryHandlers, + ...redirectManagement, ]; switch (import.meta.env.VITE_UMBRACO_INSTALL_STATUS) { diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/redirect-management.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/redirect-management.handlers.ts new file mode 100644 index 0000000000..b60e0430a4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/redirect-management.handlers.ts @@ -0,0 +1,173 @@ +import { rest } from 'msw'; +import { umbracoPath } from '@umbraco-cms/utils'; +import { PagedRedirectUrl, RedirectUrl, RedirectStatus, RedirectUrlStatus } from '@umbraco-cms/backend-api'; + +export const handlers = [ + rest.get(umbracoPath('/redirect-management'), (_req, res, ctx) => { + const filter = _req.url.searchParams.get('filter'); + const skip = parseInt(_req.url.searchParams.get('skip') ?? '0', 10); + const take = parseInt(_req.url.searchParams.get('take') ?? '20', 10); + + if (filter) { + const filtered: RedirectUrl[] = []; + + PagedRedirectUrlData.items.forEach((item) => { + if (item.originalUrl?.includes(filter)) filtered.push(item); + }); + const filteredPagedData: PagedRedirectUrl = { + total: filtered.length, + items: filtered.slice(skip, skip + take), + }; + return res(ctx.status(200), ctx.json(filteredPagedData)); + } else { + const items = PagedRedirectUrlData.items.slice(skip, skip + take); + + const PagedData: PagedRedirectUrl = { + total: PagedRedirectUrlData.total, + items, + }; + return res(ctx.status(200), ctx.json(PagedData)); + } + }), + + rest.get(umbracoPath('/redirect-management/:key'), async (_req, res, ctx) => { + const key = _req.params.key as string; + if (!key) return res(ctx.status(404)); + if (key === 'status') return res(ctx.status(200), ctx.json(UrlTracker)); + + const PagedRedirectUrlObject = _getRedirectUrlByKey(key); + + return res(ctx.status(200), ctx.json(PagedRedirectUrlObject)); + }), + + rest.delete(umbracoPath('/redirect-management/:key'), async (_req, res, ctx) => { + const key = _req.params.key as string; + if (!key) return res(ctx.status(404)); + + const PagedRedirectUrlObject = _deleteRedirectUrlByKey(key); + + return res(ctx.status(200), ctx.json(PagedRedirectUrlObject)); + }), + + /*rest.get(umbracoPath('/redirect-management/status'), (_req, res, ctx) => { + return res(ctx.status(200), ctx.json(UrlTracker)); + }),*/ + + rest.post(umbracoPath('/redirect-management/status'), async (_req, res, ctx) => { + UrlTracker.status = UrlTracker.status === RedirectStatus.ENABLED ? RedirectStatus.DISABLED : RedirectStatus.ENABLED; + return res(ctx.status(200), ctx.json(UrlTracker.status)); + }), +]; + +// Mock Data + +const UrlTracker: RedirectUrlStatus = { status: RedirectStatus.ENABLED, userIsAdmin: true }; + +const _getRedirectUrlByKey = (key: string) => { + const PagedResult: PagedRedirectUrl = { + total: 0, + items: [], + }; + RedirectUrlData.forEach((data) => { + if (data.key?.includes(key)) { + PagedResult.items.push(data); + PagedResult.total++; + } + }); + return PagedResult; +}; + +const _deleteRedirectUrlByKey = (key: string) => { + const index = RedirectUrlData.findIndex((data) => data.key === key); + if (index > -1) RedirectUrlData.splice(index, 1); + const PagedResult: PagedRedirectUrl = { + items: RedirectUrlData, + total: RedirectUrlData.length, + }; + return PagedResult; +}; + +const RedirectUrlData: RedirectUrl[] = [ + { + key: '1', + created: '2022-12-05T13:59:43.6827244', + destinationUrl: 'kitty.com', + originalUrl: 'kitty.dk', + contentKey: '7191c911-6747-4824-849e-5208e2b31d9f2', + }, + { + key: '2', + created: '2022-13-05T13:59:43.6827244', + destinationUrl: 'umbraco.com', + originalUrl: 'umbraco.dk', + contentKey: '7191c911-6747-4824-849e-5208e2b31d9f', + }, + { + key: '3', + created: '2022-12-05T13:59:43.6827244', + destinationUrl: 'uui.umbraco.com', + originalUrl: 'uui.umbraco.dk', + contentKey: '7191c911-6747-4824-849e-5208e2b31d9f23', + }, + { + key: '4', + created: '2022-13-05T13:59:43.6827244', + destinationUrl: 'umbracoffee.com', + originalUrl: 'umbracoffee.dk', + contentKey: '7191c911-6747-4824-849e-5208e2b31d9fdsaa', + }, + { + key: '5', + created: '2022-12-05T13:59:43.6827244', + destinationUrl: 'section/settings', + originalUrl: 'section/settings/123', + contentKey: '7191c911-6747-4824-849e-5208e2b31d9f2e23', + }, + { + key: '6', + created: '2022-13-05T13:59:43.6827244', + destinationUrl: 'dxp.com', + originalUrl: 'dxp.dk', + contentKey: '7191c911-6747-4824-849e-5208e2b31d9fsafsfd', + }, + { + key: '7', + created: '2022-12-05T13:59:43.6827244', + destinationUrl: 'google.com', + originalUrl: 'google.dk', + contentKey: '7191c911-6747-4824-849e-5208e2b31d9f2cxza', + }, + { + key: '8', + created: '2022-13-05T13:59:43.6827244', + destinationUrl: 'unicorns.com', + originalUrl: 'unicorns.dk', + contentKey: '7191c911-6747-4824-849e-5208e2b31d9fweds', + }, + { + key: '9', + created: '2022-12-05T13:59:43.6827244', + destinationUrl: 'h5yr.com', + originalUrl: 'h5yr.dk', + contentKey: '7191c911-6747-4824-849e-5208e2b31ddsfsdsfadsfdx9f2', + }, + { + key: '10', + created: '2022-13-05T13:59:43.6827244', + destinationUrl: 'our.umbraco.com', + originalUrl: 'our.umbraco.dk', + contentKey: '7191c911-6747-4824-849e-52dsacx08e2b31d9dsafdsff', + }, + { + key: '11', + created: '2022-13-05T13:59:43.6827244', + destinationUrl: 'your.umbraco.com', + originalUrl: 'your.umbraco.dk', + contentKey: '7191c911-6747-4824-849e-52dsacx08e2b31d9fsda', + }, +]; + +const PagedRedirectUrlData: PagedRedirectUrl = { + total: RedirectUrlData.length, + items: RedirectUrlData, +}; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/e2e-handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/e2e-handlers.ts index bc8c6fb0a5..4f3575a236 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/e2e-handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/e2e-handlers.ts @@ -11,6 +11,7 @@ import { handlers as examineManagementHandlers } from './domains/examine-managem import { handlers as modelsBuilderHandlers } from './domains/modelsbuilder.handlers'; import { handlers as profileHandlers } from './domains/performance-profiling.handlers'; import { handlers as healthCheckHandlers } from './domains/health-check.handlers'; +import { handlers as redirectManagementHandlers } from './domains/redirect-management.handlers'; export const handlers = [ serverHandlers.serverRunningHandler, @@ -28,4 +29,5 @@ export const handlers = [ ...modelsBuilderHandlers, ...profileHandlers, ...healthCheckHandlers, + ...redirectManagementHandlers, ];