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._removeRedirectHandler(data)}">
+
+
+
+
+ `;
+ })}
+
- `;
+ ${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,
];