Merge remote-tracking branch 'origin/main' into bugfix/block-grid-row-span-corrections

This commit is contained in:
Niels Lyngsø
2024-05-21 23:38:46 +02:00
207 changed files with 3029 additions and 666 deletions

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en-us" dir="ltr">
<head>
<base href="/" />
@@ -7,6 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Umbraco</title>
<script src="node_modules/msw/lib/iife/index.js"></script>
<link rel="stylesheet" href="src/css/user-defined.css" />
<link rel="stylesheet" href="node_modules/@umbraco-ui/uui-css/dist/uui-css.css" />
<link rel="stylesheet" href="src/css/umb-css.css" />
<script type="module" src="index.ts"></script>

View File

@@ -1,4 +1,9 @@
import { UMB_AUTH_CONTEXT, UMB_MODAL_APP_AUTH, type UmbUserLoginState } from '@umbraco-cms/backoffice/auth';
import {
UMB_AUTH_CONTEXT,
UMB_MODAL_APP_AUTH,
UMB_STORAGE_REDIRECT_URL,
type UmbUserLoginState,
} from '@umbraco-cms/backoffice/auth';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
@@ -65,6 +70,9 @@ export class UmbAppAuthController extends UmbControllerBase {
throw new Error('[Fatal] Auth context is not available');
}
// Save the current state
sessionStorage.setItem(UMB_STORAGE_REDIRECT_URL, window.location.href);
// Figure out which providers are available
const availableProviders = await firstValueFrom(this.#authContext.getAuthProviders(umbExtensionsRegistry));

View File

@@ -4,7 +4,7 @@ import { UmbAppContext } from './app.context.js';
import { UmbServerConnection } from './server-connection.js';
import { UmbAppAuthController } from './app-auth.controller.js';
import type { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
import { UmbAuthContext } from '@umbraco-cms/backoffice/auth';
import { UMB_STORAGE_REDIRECT_URL, UmbAuthContext } from '@umbraco-cms/backoffice/auth';
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UUIIconRegistryEssential } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -86,7 +86,14 @@ export class UmbAppElement extends UmbLitElement {
: this.localize.term('errors_externalLoginFailed');
this.observe(this.#authContext.authorizationSignal, () => {
history.replaceState(null, '', '');
// Redirect to the saved state or root
let currentRoute = '';
const savedRoute = sessionStorage.getItem(UMB_STORAGE_REDIRECT_URL);
if (savedRoute) {
sessionStorage.removeItem(UMB_STORAGE_REDIRECT_URL);
currentRoute = savedRoute.endsWith('logout') ? currentRoute : savedRoute;
}
history.replaceState(null, '', currentRoute);
});
}
@@ -99,6 +106,11 @@ export class UmbAppElement extends UmbLitElement {
component: () => import('../upgrader/upgrader.element.js'),
guards: [this.#isAuthorizedGuard()],
},
{
path: 'preview',
component: () => import('../preview/preview.element.js'),
guards: [this.#isAuthorizedGuard()],
},
{
path: 'logout',
resolve: () => {

View File

@@ -44,6 +44,7 @@ export class UmbBackofficeHeaderLogoElement extends UmbLitElement {
UmbTextStyles,
css`
#logo {
display: var(--umb-header-logo-display, inline);
--uui-button-padding-top-factor: 1;
--uui-button-padding-bottom-factor: 0.5;
margin-right: var(--uui-size-space-2);

View File

@@ -20,7 +20,7 @@ export class UmbBackofficeHeaderElement extends UmbLitElement {
}
#appHeader {
background-color: var(--uui-color-header-surface);
background-color: var(--umb-header-background-color, var(--uui-color-header-surface));
display: flex;
align-items: center;
justify-content: space-between;

View File

@@ -0,0 +1,32 @@
import type { ManifestPreviewAppProvider } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestPreviewAppProvider> = [
{
type: 'previewApp',
alias: 'Umb.PreviewApps.Device',
name: 'Preview: Device Switcher',
element: () => import('./preview-device.element.js'),
weight: 400,
},
{
type: 'previewApp',
alias: 'Umb.PreviewApps.Culture',
name: 'Preview: Culture Switcher',
element: () => import('./preview-culture.element.js'),
weight: 300,
},
{
type: 'previewApp',
alias: 'Umb.PreviewApps.OpenWebsite',
name: 'Preview: Open Website Button',
element: () => import('./preview-open-website.element.js'),
weight: 200,
},
{
type: 'previewApp',
alias: 'Umb.PreviewApps.Exit',
name: 'Preview: Exit Button',
element: () => import('./preview-exit.element.js'),
weight: 100,
},
];

View File

@@ -0,0 +1,103 @@
import { UMB_PREVIEW_CONTEXT } from '../preview.context.js';
import { css, customElement, html, nothing, repeat, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language';
import type { UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language';
const elementName = 'umb-preview-culture';
@customElement(elementName)
export class UmbPreviewCultureElement extends UmbLitElement {
#languageRepository = new UmbLanguageCollectionRepository(this);
@state()
private _culture?: UmbLanguageDetailModel;
@state()
private _cultures: Array<UmbLanguageDetailModel> = [];
connectedCallback() {
super.connectedCallback();
this.#getCultures();
}
async #getCultures() {
const { data: langauges } = await this.#languageRepository.requestCollection({ skip: 0, take: 100 });
this._cultures = langauges?.items ?? [];
const searchParams = new URLSearchParams(window.location.search);
const culture = searchParams.get('culture');
if (culture && culture !== this._culture?.unique) {
this._culture = this._cultures.find((c) => c.unique === culture);
}
}
async #onClick(culture: UmbLanguageDetailModel) {
if (this._culture === culture) return;
this._culture = culture;
const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT);
previewContext.updateIFrame({ culture: culture.unique });
}
render() {
if (this._cultures.length <= 1) return nothing;
return html`
<uui-button look="primary" popovertarget="cultures-popover">
<div>
<uui-icon name="icon-globe"></uui-icon>
<span>${this._culture?.name ?? this.localize.term('treeHeaders_languages')}</span>
</div>
</uui-button>
<uui-popover-container id="cultures-popover" placement="top-end">
<umb-popover-layout>
${repeat(
this._cultures,
(item) => item.unique,
(item) => html`
<uui-menu-item
label=${item.name}
?active=${item.unique === this._culture?.unique}
@click=${() => this.#onClick(item)}>
<uui-icon slot="icon" name="icon-globe"></uui-icon>
</uui-menu-item>
`,
)}
</umb-popover-layout>
</uui-popover-container>
`;
}
static styles = [
css`
:host {
display: flex;
border-left: 1px solid var(--uui-color-header-contrast);
--uui-button-font-weight: 400;
--uui-button-padding-left-factor: 3;
--uui-button-padding-right-factor: 3;
}
uui-button > div {
display: flex;
align-items: center;
gap: 5px;
}
umb-popover-layout {
--uui-color-surface: var(--uui-color-header-surface);
--uui-color-border: var(--uui-color-header-surface);
color: var(--uui-color-header-contrast);
}
`,
];
}
export { UmbPreviewCultureElement as element };
declare global {
interface HTMLElementTagNameMap {
[elementName]: UmbPreviewCultureElement;
}
}

View File

@@ -0,0 +1,149 @@
import { UMB_PREVIEW_CONTEXT } from '../preview.context.js';
import { css, customElement, html, property, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
export interface UmbPreviewDevice {
alias: string;
label: string;
css: string;
icon: string;
dimensions: { height: string; width: string };
}
const elementName = 'umb-preview-device';
@customElement(elementName)
export class UmbPreviewDeviceElement extends UmbLitElement {
#devices: Array<UmbPreviewDevice> = [
{
alias: 'fullsize',
label: 'Fit browser',
css: 'fullsize',
icon: 'icon-application-window-alt',
dimensions: { height: '100%', width: '100%' },
},
{
alias: 'desktop',
label: 'Desktop',
css: 'desktop shadow',
icon: 'icon-display',
dimensions: { height: '1080px', width: '1920px' },
},
{
alias: 'laptop',
label: 'Laptop',
css: 'laptop shadow',
icon: 'icon-laptop',
dimensions: { height: '768px', width: '1366px' },
},
{
alias: 'ipad-portrait',
label: 'Tablet portrait',
css: 'ipad-portrait shadow',
icon: 'icon-ipad',
dimensions: { height: '929px', width: '769px' },
},
{
alias: 'ipad-landscape',
label: 'Tablet landscape',
css: 'ipad-landscape shadow flip',
icon: 'icon-ipad',
dimensions: { height: '675px', width: '1024px' },
},
{
alias: 'smartphone-portrait',
label: 'Smartphone portrait',
css: 'smartphone-portrait shadow',
icon: 'icon-iphone',
dimensions: { height: '640px', width: '360px' },
},
{
alias: 'smartphone-landscape',
label: 'Smartphone landscape',
css: 'smartphone-landscape shadow flip',
icon: 'icon-iphone',
dimensions: { height: '360px', width: '640px' },
},
];
@property({ attribute: false, type: Object })
device = this.#devices[0];
connectedCallback() {
super.connectedCallback();
this.#changeDevice(this.device);
}
async #changeDevice(device: UmbPreviewDevice) {
if (device === this.device) return;
this.device = device;
const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT);
previewContext?.updateIFrame({
className: device.css,
height: device.dimensions.height,
width: device.dimensions.width,
});
}
render() {
return html`
<uui-button look="primary" popovertarget="devices-popover">
<div>
<uui-icon name=${this.device.icon} class=${this.device.css.includes('flip') ? 'flip' : ''}></uui-icon>
<span>${this.device.label}</span>
</div>
</uui-button>
<uui-popover-container id="devices-popover" placement="top-end">
<umb-popover-layout>
${repeat(
this.#devices,
(item) => item.alias,
(item) => html`
<uui-menu-item
label=${item.label}
?active=${item === this.device}
@click=${() => this.#changeDevice(item)}>
<uui-icon slot="icon" name=${item.icon} class=${item.css.includes('flip') ? 'flip' : ''}></uui-icon>
</uui-menu-item>
`,
)}
</umb-popover-layout>
</uui-popover-container>
`;
}
static styles = [
css`
:host {
display: flex;
border-left: 1px solid var(--uui-color-header-contrast);
--uui-button-font-weight: 400;
--uui-button-padding-left-factor: 3;
--uui-button-padding-right-factor: 3;
}
uui-button > div {
display: flex;
align-items: center;
gap: 5px;
}
umb-popover-layout {
--uui-color-surface: var(--uui-color-header-surface);
--uui-color-border: var(--uui-color-header-surface);
color: var(--uui-color-header-contrast);
}
`,
];
}
export { UmbPreviewDeviceElement as element };
declare global {
interface HTMLElementTagNameMap {
[elementName]: UmbPreviewDeviceElement;
}
}

View File

@@ -0,0 +1,49 @@
import { UMB_PREVIEW_CONTEXT } from '../preview.context.js';
import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
const elementName = 'umb-preview-exit';
@customElement(elementName)
export class UmbPreviewExitElement extends UmbLitElement {
async #onClick() {
const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT);
previewContext.exitPreview(0);
}
render() {
return html`
<uui-button look="primary" @click=${this.#onClick}>
<div>
<uui-icon name="icon-power"></uui-icon>
<span>${this.localize.term('preview_endLabel')}</span>
</div>
</uui-button>
`;
}
static styles = [
css`
:host {
display: flex;
border-left: 1px solid var(--uui-color-header-contrast);
--uui-button-font-weight: 400;
--uui-button-padding-left-factor: 3;
--uui-button-padding-right-factor: 3;
}
uui-button > div {
display: flex;
align-items: center;
gap: 5px;
}
`,
];
}
export { UmbPreviewExitElement as element };
declare global {
interface HTMLElementTagNameMap {
[elementName]: UmbPreviewExitElement;
}
}

View File

@@ -0,0 +1,49 @@
import { UMB_PREVIEW_CONTEXT } from '../preview.context.js';
import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
const elementName = 'umb-preview-open-website';
@customElement(elementName)
export class UmbPreviewOpenWebsiteElement extends UmbLitElement {
async #onClick() {
const previewContext = await this.getContext(UMB_PREVIEW_CONTEXT);
previewContext.openWebsite();
}
render() {
return html`
<uui-button look="primary" @click=${this.#onClick}>
<div>
<uui-icon name="icon-out"></uui-icon>
<span>${this.localize.term('preview_openWebsiteLabel')}</span>
</div>
</uui-button>
`;
}
static styles = [
css`
:host {
display: flex;
border-left: 1px solid var(--uui-color-header-contrast);
--uui-button-font-weight: 400;
--uui-button-padding-left-factor: 3;
--uui-button-padding-right-factor: 3;
}
uui-button > div {
display: flex;
align-items: center;
gap: 5px;
}
`,
];
}
export { UmbPreviewOpenWebsiteElement as element };
declare global {
interface HTMLElementTagNameMap {
[elementName]: UmbPreviewOpenWebsiteElement;
}
}

View File

@@ -0,0 +1,208 @@
import { UMB_APP_CONTEXT } from '../app/app.context.js';
import { UmbBooleanState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbDocumentPreviewRepository } from '@umbraco-cms/backoffice/document';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
const UMB_LOCALSTORAGE_SESSION_KEY = 'umb:previewSessions';
export class UmbPreviewContext extends UmbContextBase<UmbPreviewContext> {
#culture?: string | null;
#serverUrl: string = '';
#webSocket?: WebSocket;
#unique?: string | null;
#iframeReady = new UmbBooleanState(false);
public readonly iframeReady = this.#iframeReady.asObservable();
#previewUrl = new UmbStringState(undefined);
public readonly previewUrl = this.#previewUrl.asObservable();
#documentPreviewRepository = new UmbDocumentPreviewRepository(this);
constructor(host: UmbControllerHost) {
super(host, UMB_PREVIEW_CONTEXT);
this.#init();
}
async #init() {
const appContext = await this.getContext(UMB_APP_CONTEXT);
this.#serverUrl = appContext.getServerUrl();
const params = new URLSearchParams(window.location.search);
this.#culture = params.get('culture');
this.#unique = params.get('id');
if (!this.#unique) {
console.error('No unique ID found in query string.');
return;
}
this.#setPreviewUrl();
}
#configureWebSocket() {
if (this.#webSocket && this.#webSocket.readyState < 2) return;
const url = `${this.#serverUrl.replace('https://', 'wss://')}/umbraco/PreviewHub`;
this.#webSocket = new WebSocket(url);
this.#webSocket.addEventListener('open', () => {
// NOTE: SignalR protocol handshake; it requires a terminating control character.
const endChar = String.fromCharCode(30);
this.#webSocket?.send(`{"protocol":"json","version":1}${endChar}`);
});
this.#webSocket.addEventListener('message', (event: MessageEvent<string>) => {
if (!event?.data) return;
// NOTE: Strip the terminating control character, (from SignalR).
const data = event.data.substring(0, event.data.length - 1);
const json = JSON.parse(data) as { type: number; target: string; arguments: Array<string> };
if (json.type === 1 && json.target === 'refreshed') {
const pageId = json.arguments?.[0];
if (pageId === this.#unique) {
this.#setPreviewUrl({ rnd: Math.random() });
}
}
});
}
#getSessionCount(): number {
return Math.max(Number(localStorage.getItem(UMB_LOCALSTORAGE_SESSION_KEY)), 0) || 0;
}
#setPreviewUrl(args?: { serverUrl?: string; unique?: string | null; culture?: string | null; rnd?: number }) {
const host = args?.serverUrl || this.#serverUrl;
const path = args?.unique || this.#unique;
const params = new URLSearchParams();
const culture = args?.culture || this.#culture;
if (culture) params.set('culture', culture);
if (args?.rnd) params.set('rnd', args.rnd.toString());
this.#previewUrl.setValue(`${host}/${path}?${params}`);
}
#setSessionCount(sessions: number) {
localStorage.setItem(UMB_LOCALSTORAGE_SESSION_KEY, sessions.toString());
}
checkSession() {
const sessions = this.#getSessionCount();
if (sessions > 0) return;
umbConfirmModal(this._host, {
headline: `Preview website?`,
content: `You have ended preview mode, do you want to enable it again to view the latest saved version of your website?`,
cancelLabel: 'View published version',
confirmLabel: 'Preview latest version',
})
.then(() => {
this.restartSession();
})
.catch(() => {
this.exitSession();
});
}
async exitPreview(sessions: number = 0) {
this.#setSessionCount(sessions);
// We are good to end preview mode.
if (sessions <= 0) {
await this.#documentPreviewRepository.exit();
}
if (this.#webSocket) {
this.#webSocket.close();
this.#webSocket = undefined;
}
const url = this.#previewUrl.getValue() as string;
window.location.replace(url);
}
async exitSession() {
let sessions = this.#getSessionCount();
sessions--;
this.exitPreview(sessions);
}
iframeLoaded(iframe: HTMLIFrameElement) {
if (!iframe) return;
this.#configureWebSocket();
this.#iframeReady.setValue(true);
}
getIFrameWrapper(): HTMLElement | undefined {
return this.getHostElement().shadowRoot?.querySelector('#wrapper') as HTMLElement;
}
openWebsite() {
const url = this.#previewUrl.getValue() as string;
window.open(url, '_blank');
}
// TODO: [LK] Figure out how to make `iframe.contentDocument` works, as it's not from SameOrigin.
reloadIFrame(iframe: HTMLIFrameElement) {
const document = iframe.contentDocument;
if (!document) return;
document.location.reload();
}
async restartSession() {
await this.#documentPreviewRepository.enter();
this.startSession();
}
startSession() {
let sessions = this.#getSessionCount();
sessions++;
this.#setSessionCount(sessions);
}
async updateIFrame(args?: { culture?: string; className?: string; height?: string; width?: string }) {
if (!args) return;
const wrapper = this.getIFrameWrapper();
if (!wrapper) return;
const scaleIFrame = () => {
if (wrapper.className === 'fullsize') {
wrapper.style.transform = '';
} else {
const wScale = document.body.offsetWidth / (wrapper.offsetWidth + 30);
const hScale = document.body.offsetHeight / (wrapper.offsetHeight + 30);
const scale = Math.min(wScale, hScale, 1); // get the lowest ratio, but not higher than 1
wrapper.style.transform = `scale(${scale})`;
}
};
window.addEventListener('resize', scaleIFrame);
wrapper.addEventListener('transitionend', scaleIFrame);
if (args.culture) {
this.#iframeReady.setValue(false);
const params = new URLSearchParams(window.location.search);
params.set('culture', args.culture);
const newRelativePathQuery = window.location.pathname + '?' + params.toString();
history.pushState(null, '', newRelativePathQuery);
this.#setPreviewUrl({ culture: args.culture });
}
if (args.className) wrapper.className = args.className;
if (args.height) wrapper.style.height = args.height;
if (args.width) wrapper.style.width = args.width;
}
}
export const UMB_PREVIEW_CONTEXT = new UmbContextToken<UmbPreviewContext>('UmbPreviewContext');

View File

@@ -0,0 +1,204 @@
import { manifests as previewApps } from './apps/manifests.js';
import { UmbPreviewContext } from './preview.context.js';
import { css, customElement, html, nothing, state, when } from '@umbraco-cms/backoffice/external/lit';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
const elementName = 'umb-preview';
/**
* @element umb-preview
*/
@customElement(elementName)
export class UmbPreviewElement extends UmbLitElement {
#context = new UmbPreviewContext(this);
constructor() {
super();
if (previewApps?.length) {
umbExtensionsRegistry.registerMany(previewApps);
}
this.observe(this.#context.iframeReady, (iframeReady) => (this._iframeReady = iframeReady));
this.observe(this.#context.previewUrl, (previewUrl) => (this._previewUrl = previewUrl));
}
connectedCallback() {
super.connectedCallback();
this.addEventListener('visibilitychange', this.#onVisibilityChange);
window.addEventListener('beforeunload', () => this.#context.exitSession());
this.#context.startSession();
}
disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener('visibilitychange', this.#onVisibilityChange);
// NOTE: Unsure how we remove an anonymous function from 'beforeunload' event listener.
// The reason for the anonymous function is that if we used a named function,
// `this` would be the `window` and would not have context to the class instance. [LK]
//window.removeEventListener('beforeunload', () => this.#context.exitSession());
this.#context.exitSession();
}
@state()
private _iframeReady?: boolean;
@state()
private _previewUrl?: string;
#onIFrameLoad(event: Event & { target: HTMLIFrameElement }) {
this.#context.iframeLoaded(event.target);
}
#onVisibilityChange() {
this.#context.checkSession();
}
render() {
if (!this._previewUrl) return nothing;
return html`
${when(!this._iframeReady, () => html`<div id="loading"><uui-loader-circle></uui-loader-circle></div>`)}
<div id="wrapper">
<div id="container">
<iframe
src=${this._previewUrl}
title="Page preview"
@load=${this.#onIFrameLoad}
sandbox="allow-scripts"></iframe>
</div>
</div>
<div id="menu">
<h4>Preview Mode</h4>
<uui-button-group>
<umb-extension-slot id="apps" type="previewApp"></umb-extension-slot>
</uui-button-group>
</div>
`;
}
static styles = [
css`
:host {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding-bottom: 40px;
}
#loading {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
font-size: 6rem;
backdrop-filter: blur(5px);
}
#wrapper {
transition: all 240ms cubic-bezier(0.165, 0.84, 0.44, 1);
flex-shrink: 0;
height: 100%;
width: 100%;
}
#wrapper.fullsize {
margin: 0 auto;
overflow: hidden;
}
#wrapper.shadow {
margin: 10px auto;
background-color: white;
border-radius: 3px;
overflow: hidden;
opacity: 1;
box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.26);
}
#container {
width: 100%;
height: 100%;
margin: 0 auto;
overflow: hidden;
}
#menu {
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
bottom: 0;
left: 0;
right: 0;
background-color: var(--uui-color-header-surface);
height: 40px;
animation: menu-bar-animation 1.2s;
animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
}
#menu > h4 {
color: var(--uui-color-header-contrast-emphasis);
margin: 0;
padding: 0 15px;
}
#menu > uui-button-group {
height: 100%;
}
uui-icon.flip {
rotate: 90deg;
}
iframe {
border: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
overflow-x: hidden;
overflow-y: hidden;
}
@keyframes menu-bar-animation {
0% {
bottom: -50px;
}
40% {
bottom: -50px;
}
80% {
bottom: 0px;
}
}
`,
];
}
export default UmbPreviewElement;
declare global {
interface HTMLElementTagNameMap {
[elementName]: UmbPreviewElement;
}
}

View File

@@ -0,0 +1,10 @@
import type { Meta } from '@storybook/web-components';
import { html } from '@umbraco-cms/backoffice/external/lit';
export default {
title: 'Apps/Preview',
component: 'umb-preview',
id: 'umb-preview',
} satisfies Meta;
export const Preview = () => html`<umb-preview></umb-preview>`;

View File

@@ -0,0 +1,14 @@
import { expect, fixture, html } from '@open-wc/testing';
import { UmbPreviewElement } from './preview.element.js';
describe('UmbPreview', () => {
let element: UmbPreviewElement;
beforeEach(async () => {
element = await fixture(html`<umb-preview></umb-preview>`);
});
it('is defined with its own instance', () => {
expect(element).to.be.instanceOf(UmbPreviewElement);
});
});

View File

@@ -548,13 +548,15 @@ export default {
noIconsFound: 'Ingen ikoner blev fundet',
noMacroParams: 'Der er ingen parametre for denne makro',
noMacros: 'Der er ikke tilføjet nogen makroer',
externalLoginProviders: 'Eksterne login-udbydere',
externalLoginProviders: 'Eksternt login',
exceptionDetail: 'Undtagelsesdetaljer',
stacktrace: 'Stacktrace',
innerException: 'Indre undtagelse',
linkYour: 'Link dit',
unLinkYour: 'Fjern link fra dit',
account: 'konto',
linkYour: 'Link din {0} konto',
linkYourConfirm: 'For at linke dine Umbraco og {0} konti, vil du blive sendt til {0} for at bekræfte.',
unLinkYour: 'Fjern link fra din {0} konto',
unLinkYourConfirm: 'Du er ved at fjerne linket mellem dine Umbraco og {0} konti og du vil blive logget ud.',
linkedToService: 'Din konto er linket til denne service',
selectEditor: 'Vælg editor',
selectEditorConfiguration: 'Vælg konfiguration',
selectSnippet: 'Vælg snippet',

View File

@@ -559,13 +559,16 @@ export default {
noIconsFound: 'No icons were found',
noMacroParams: 'There are no parameters for this macro',
noMacros: 'There are no macros available to insert',
externalLoginProviders: 'External login providers',
externalLoginProviders: 'External logins',
exceptionDetail: 'Exception Details',
stacktrace: 'Stacktrace',
innerException: 'Inner Exception',
linkYour: 'Link your',
unLinkYour: 'Un-link your',
account: 'account',
linkYour: 'Link your {0} account',
linkYourConfirm:
'You are about to link your Umbraco and {0} accounts and you will be redirected to {0} to confirm.',
unLinkYour: 'Un-link your {0} account',
unLinkYourConfirm: 'You are about to un-link your Umbraco and {0} accounts and you will be logged out.',
linkedToService: 'Your account is linked to this service',
selectEditor: 'Select editor',
selectEditorConfiguration: 'Select configuration',
selectSnippet: 'Select snippet',

View File

@@ -568,13 +568,16 @@ export default {
noIconsFound: 'No icons were found',
noMacroParams: 'There are no parameters for this macro',
noMacros: 'There are no macros available to insert',
externalLoginProviders: 'External login providers',
externalLoginProviders: 'External logins',
exceptionDetail: 'Exception Details',
stacktrace: 'Stacktrace',
innerException: 'Inner Exception',
linkYour: 'Link your',
unLinkYour: 'Un-link your',
account: 'account',
linkYour: 'Link your {0} account',
linkYourConfirm:
'You are about to link your Umbraco and {0} accounts and you will be redirected to {0} to confirm.',
unLinkYour: 'Un-link your {0} account',
unLinkYourConfirm: 'You are about to un-link your Umbraco and {0} accounts and you will be logged out.',
linkedToService: 'Your account is linked to this service',
selectEditor: 'Select editor',
selectSnippet: 'Select snippet',
variantdeletewarning:

View File

@@ -0,0 +1 @@
/* This file can be overridden by placing a file with the same name in the /wwwroot/umbraco/backoffice/css folder of the website */

View File

@@ -1057,15 +1057,6 @@ fallbackIsoCode?: string | null
isoCode: string
};
export type LinkedLoginModel = {
providerName: string
providerKey: string
};
export type LinkedLoginsRequestModel = {
linkedLogins: Array<LinkedLoginModel>
};
export type LogLevelCountsReponseModel = {
information: number
debug: number
@@ -2585,6 +2576,7 @@ key: string
export type UserExternalLoginProviderModel = {
providerSchemeName: string
providerKey?: string | null
isLinkedOnUser: boolean
hasManualLinkingEnabled: boolean
};
@@ -5241,7 +5233,6 @@ PostUserUnlock: {
,PostUserCurrentChangePassword: string
,GetUserCurrentConfiguration: CurrenUserConfigurationResponseModel
,GetUserCurrentLoginProviders: Array<UserExternalLoginProviderModel>
,GetUserCurrentLogins: LinkedLoginsRequestModel
,GetUserCurrentPermissions: UserPermissionsResponseModel
,GetUserCurrentPermissionsDocument: Array<UserPermissionsResponseModel>
,GetUserCurrentPermissionsMedia: UserPermissionsResponseModel

View File

@@ -8701,21 +8701,6 @@ requestBody
});
}
/**
* @returns unknown Success
* @throws ApiError
*/
public static getUserCurrentLogins(): CancelablePromise<UserData['responses']['GetUserCurrentLogins']> {
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/user/current/logins',
errors: {
401: `The resource is protected and requires an authentication token`,
},
});
}
/**
* @returns unknown Success
* @throws ApiError

View File

@@ -831,7 +831,6 @@ export const data: Array<UmbMockDataTypeModel> = [
{ alias: 'icon', value: 'icon-layers' },
{ alias: 'tabName', value: 'Children' },
{ alias: 'showContentFirst', value: true },
{ alias: 'useInfiniteEditor', value: true },
],
},
{
@@ -876,7 +875,6 @@ export const data: Array<UmbMockDataTypeModel> = [
{ alias: 'icon', value: 'icon-layers' },
{ alias: 'tabName', value: 'Items' },
{ alias: 'showContentFirst', value: false },
{ alias: 'useInfiniteEditor', value: true },
],
},
{

View File

@@ -128,8 +128,4 @@ export const mfaLoginProviders: Array<UserTwoFactorProviderModel> = [
isEnabledOnUser: false,
providerName: 'sms',
},
{
isEnabledOnUser: true,
providerName: 'Email',
},
];

View File

@@ -76,6 +76,15 @@ const privateManifests: PackageManifestResponse = [
label: 'Setup SMS Verification',
},
},
{
type: 'mfaLoginProvider',
alias: 'My.MfaLoginProvider.Custom.Email',
name: 'My Custom Email MFA Provider',
forProviderName: 'email',
meta: {
label: 'Setup Email Verification',
},
},
],
},
{
@@ -92,20 +101,6 @@ const privateManifests: PackageManifestResponse = [
},
],
},
{
name: 'My MFA Package',
extensions: [
{
type: 'mfaLoginProvider',
alias: 'My.MfaLoginProvider.Custom',
name: 'My Custom MFA Provider',
forProviderName: 'sms',
meta: {
label: 'Setup SMS Verification',
},
},
],
},
];
const publicManifests: PackageManifestResponse = [

View File

@@ -1,7 +1,7 @@
const { rest } = window.MockServiceWorker;
import { umbUserMockDb } from '../../data/user/user.db.js';
import { UMB_SLUG } from './slug.js';
import type { LinkedLoginsRequestModel } from '@umbraco-cms/backoffice/external/backend-api';
import type { UserData } from '@umbraco-cms/backoffice/external/backend-api';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
@@ -9,19 +9,22 @@ export const handlers = [
const loggedInUser = umbUserMockDb.getCurrentUser();
return res(ctx.status(200), ctx.json(loggedInUser));
}),
rest.get<LinkedLoginsRequestModel>(umbracoPath(`${UMB_SLUG}/current/logins`), (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json<LinkedLoginsRequestModel>({
linkedLogins: [
rest.get<UserData['responses']['GetUserCurrentLoginProviders']>(
umbracoPath(`${UMB_SLUG}/current/login-providers`),
(_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json<UserData['responses']['GetUserCurrentLoginProviders']>([
{
hasManualLinkingEnabled: true,
isLinkedOnUser: true,
providerKey: 'google',
providerName: 'Umbraco.Google',
providerSchemeName: 'Umbraco.Google',
},
],
}),
);
}),
]),
);
},
),
rest.get(umbracoPath(`${UMB_SLUG}/current/2fa`), (_req, res, ctx) => {
const mfaLoginProviders = umbUserMockDb.getMfaLoginProviders();
return res(ctx.status(200), ctx.json(mfaLoginProviders));

View File

@@ -98,6 +98,11 @@ export class UmbAuthFlow {
// tokens
#tokenResponse?: TokenResponse;
// external login
#link_endpoint;
#link_key_endpoint;
#unlink_endpoint;
/**
* This signal will emit when the authorization flow is complete.
* @remark It will also emit if there is an error during the authorization flow.
@@ -125,6 +130,10 @@ export class UmbAuthFlow {
end_session_endpoint: `${openIdConnectUrl}/umbraco/management/api/v1/security/back-office/signout`,
});
this.#link_endpoint = `${openIdConnectUrl}/umbraco/management/api/v1/security/back-office/link-login`;
this.#link_key_endpoint = `${openIdConnectUrl}/umbraco/management/api/v1/security/back-office/link-login-key`;
this.#unlink_endpoint = `${openIdConnectUrl}/umbraco/management/api/v1/security/back-office/unlink-login`;
this.#notifier = new AuthorizationNotifier();
this.#tokenHandler = new BaseTokenRequestHandler(requestor);
this.#storageBackend = new LocalStorageBackend();
@@ -320,6 +329,55 @@ export class UmbAuthFlow {
: Promise.reject('Missing tokenResponse.');
}
/**
* This method will link the current user to the specified provider by redirecting the user to the link endpoint.
* @param provider The provider to link to.
*/
async linkLogin(provider: string): Promise<void> {
const linkKey = await this.#makeLinkTokenRequest(provider);
const form = document.createElement('form');
form.method = 'POST';
form.action = this.#link_endpoint;
form.style.display = 'none';
const providerInput = document.createElement('input');
providerInput.name = 'provider';
providerInput.value = provider;
form.appendChild(providerInput);
const linkKeyInput = document.createElement('input');
linkKeyInput.name = 'linkKey';
linkKeyInput.value = linkKey;
form.appendChild(linkKeyInput);
document.body.appendChild(form);
form.submit();
}
/**
* This method will unlink the current user from the specified provider.
*/
async unlinkLogin(loginProvider: string, providerKey: string): Promise<boolean> {
const token = await this.performWithFreshTokens();
const request = new Request(this.#unlink_endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ loginProvider, providerKey }),
});
const result = await fetch(request);
if (!result.ok) {
const error = await result.json();
throw error;
}
await this.signOut();
return true;
}
/**
* Save the current token response to local storage.
*/
@@ -384,4 +442,21 @@ export class UmbAuthFlow {
return false;
}
}
async #makeLinkTokenRequest(provider: string) {
const token = await this.performWithFreshTokens();
const request = await fetch(`${this.#link_key_endpoint}?provider=${provider}`, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!request.ok) {
throw new Error('Failed to link login');
}
return request.json();
}
}

View File

@@ -12,12 +12,8 @@ export class UmbAuthContext extends UmbContextBase<UmbAuthContext> {
#isAuthorized = new UmbBooleanState<boolean>(false);
// Timeout is different from `isAuthorized` because it can occur repeatedly
#isTimeout = new Subject<void>();
/**
* Observable that emits true when the auth context is initialized.
* @remark It will only emit once and then complete itself.
*/
#isInitialized = new ReplaySubject<void>(1);
#isBypassed = false;
#isBypassed;
#serverUrl;
#backofficePath;
#authFlow;
@@ -25,6 +21,12 @@ export class UmbAuthContext extends UmbContextBase<UmbAuthContext> {
#authWindowProxy?: WindowProxy | null;
#previousAuthUrl?: string;
/**
* Observable that emits true when the auth context is initialized.
* @remark It will only emit once and then complete itself.
*/
readonly isInitialized = this.#isInitialized.asObservable();
/**
* Observable that emits true if the user is authorized, otherwise false.
* @remark It will only emit when the authorization state changes.
@@ -254,22 +256,51 @@ export class UmbAuthContext extends UmbContextBase<UmbAuthContext> {
};
}
/**
* Sets the auth context as initialized, which means that the auth context is ready to be used.
* @remark This is used to let the app context know that the core module is ready, which means that the core auth providers are available.
*/
setInitialized() {
this.#isInitialized.next();
this.#isInitialized.complete();
}
/**
* Gets all registered auth providers.
*/
getAuthProviders(extensionsRegistry: UmbBackofficeExtensionRegistry) {
return this.#isInitialized.pipe(
switchMap(() => extensionsRegistry.byType<'authProvider', ManifestAuthProvider>('authProvider')),
);
}
/**
* Gets the authorized redirect url.
* @returns The redirect url, which is the backoffice path.
*/
getRedirectUrl() {
return `${window.location.origin}${this.#backofficePath}${this.#backofficePath.endsWith('/') ? '' : '/'}oauth_complete`;
}
/**
* Gets the post logout redirect url.
* @returns The post logout redirect url, which is the backoffice path with the logout path appended.
*/
getPostLogoutRedirectUrl() {
return `${window.location.origin}${this.#backofficePath}${this.#backofficePath.endsWith('/') ? '' : '/'}logout`;
}
/**
* @see UmbAuthFlow#linkLogin
*/
linkLogin(provider: string) {
return this.#authFlow.linkLogin(provider);
}
/**
* @see UmbAuthFlow#unlinkLogin
*/
unlinkLogin(providerName: string, providerKey: string) {
return this.#authFlow.unlinkLogin(providerName, providerKey);
}
}

View File

@@ -18,19 +18,25 @@ export class UmbAuthProviderDefaultElement extends UmbLitElement implements UmbA
this.setAttribute('part', 'auth-provider-default');
}
get #label() {
const label = this.manifest.meta?.label ?? this.manifest.forProviderName;
const labelLocalized = this.localize.string(label);
return this.localize.term('login_signInWith', labelLocalized);
}
render() {
return html`
<uui-button
type="button"
@click=${() => this.onSubmit(this.manifest)}
id="auth-provider-button"
.label=${this.manifest.meta?.label ?? this.manifest.forProviderName}
.label=${this.#label}
.look=${this.manifest.meta?.defaultView?.look ?? 'outline'}
.color=${this.manifest.meta?.defaultView?.color ?? 'default'}>
${this.manifest.meta?.defaultView?.icon
? html`<uui-icon .name=${this.manifest.meta?.defaultView?.icon}></uui-icon>`
? html`<uui-icon id="icon" .name=${this.manifest.meta?.defaultView?.icon}></uui-icon>`
: nothing}
${this.manifest.meta?.label ?? this.manifest.forProviderName}
${this.#label}
</uui-button>
`;
}
@@ -45,6 +51,10 @@ export class UmbAuthProviderDefaultElement extends UmbLitElement implements UmbA
#auth-provider-button {
width: 100%;
}
#icon {
margin-right: var(--uui-size-space-1);
}
`,
];
}

View File

@@ -8,7 +8,7 @@ export const manifests: Array<ManifestAuthProvider> = [
forProviderName: 'Umbraco',
weight: 1000,
meta: {
label: 'Sign in with Umbraco',
label: 'Umbraco',
defaultView: {
icon: 'icon-umbraco',
look: 'primary',

View File

@@ -1,4 +1,4 @@
import { UMB_COLLECTION_CONTEXT } from './default/collection-default.context.js';
import { UMB_COLLECTION_CONTEXT } from './default/index.js';
import type { CollectionAliasConditionConfig } from './collection-alias.manifest.js';
import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api';
import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry';

View File

@@ -1,4 +1,4 @@
import { UMB_COLLECTION_CONTEXT } from './default/collection-default.context.js';
import { UMB_COLLECTION_CONTEXT } from './default/index.js';
import type { CollectionBulkActionPermissionConditionConfig } from './collection-bulk-action-permission.manifest.js';
import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api';
import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry';

View File

@@ -1,9 +1,9 @@
import { UMB_COLLECTION_CONTEXT } from '../default/collection-default.context.js';
import type { ManifestEntityBulkAction, MetaEntityBulkAction } from '../../extension-registry/models/index.js';
import { UMB_COLLECTION_CONTEXT } from '../default/index.js';
import type { UmbActionExecutedEvent } from '@umbraco-cms/backoffice/event';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { ManifestEntityBulkAction, MetaEntityBulkAction } from '@umbraco-cms/backoffice/extension-registry';
function apiArgsMethod(manifest: ManifestEntityBulkAction<MetaEntityBulkAction>) {
return [{ meta: manifest.meta }] as unknown[];

View File

@@ -1,5 +1,5 @@
import type { UmbDefaultCollectionContext } from '../default/collection-default.context.js';
import { UMB_COLLECTION_CONTEXT } from '../default/collection-default.context.js';
import type { UmbDefaultCollectionContext } from '../default/index.js';
import { UMB_COLLECTION_CONTEXT } from '../default/index.js';
import type { UmbCollectionLayoutConfiguration } from '../types.js';
import { css, html, customElement, state, nothing, repeat, query } from '@umbraco-cms/backoffice/external/lit';
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';

View File

@@ -1,4 +1,4 @@
import { UMB_COLLECTION_CONTEXT } from '../../default/collection-default.context.js';
import { UMB_COLLECTION_CONTEXT } from '../../default/index.js';
import type { UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, nothing, state } from '@umbraco-cms/backoffice/external/lit';

View File

@@ -0,0 +1,4 @@
import type { UmbDefaultCollectionContext } from './collection-default.context.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export const UMB_COLLECTION_CONTEXT = new UmbContextToken<UmbDefaultCollectionContext>('UmbCollectionContext');

View File

@@ -6,23 +6,31 @@ import type {
UmbCollectionContext,
UmbCollectionLayoutConfiguration,
} from '../types.js';
import type { UmbCollectionFilterModel } from '../collection-filter-model.interface.js';
import type { UmbCollectionRepository } from '../repository/collection-repository.interface.js';
import { UMB_COLLECTION_CONTEXT } from './collection-default.context-token.js';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbArrayState, UmbNumberState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UmbSelectionManager, UmbPaginationManager } from '@umbraco-cms/backoffice/utils';
import type { ManifestCollection, ManifestRepository } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import type { UmbCollectionFilterModel, UmbCollectionRepository } from '@umbraco-cms/backoffice/collection';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import {
UmbRequestReloadChildrenOfEntityEvent,
UmbRequestReloadStructureForEntityEvent,
} from '@umbraco-cms/backoffice/entity-action';
import type { UmbActionEventContext } from '@umbraco-cms/backoffice/action';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity';
const LOCAL_STORAGE_KEY = 'umb-collection-view';
export class UmbDefaultCollectionContext<
CollectionItemType = any,
FilterModelType extends UmbCollectionFilterModel = any,
CollectionItemType extends { entityType: string; unique: string } = any,
FilterModelType extends UmbCollectionFilterModel = UmbCollectionFilterModel,
>
extends UmbContextBase<UmbDefaultCollectionContext>
implements UmbCollectionContext, UmbApi
@@ -34,7 +42,7 @@ export class UmbDefaultCollectionContext<
#loading = new UmbObjectState<boolean>(false);
public readonly loading = this.#loading.asObservable();
#items = new UmbArrayState<CollectionItemType>([], (x) => x);
#items = new UmbArrayState<CollectionItemType>([], (x) => x.unique);
public readonly items = this.#items.asObservable();
#totalItems = new UmbNumberState(0);
@@ -63,6 +71,8 @@ export class UmbDefaultCollectionContext<
this.#initialized ? resolve() : (this.#initResolver = resolve);
});
#actionEventContext: UmbActionEventContext | undefined;
constructor(host: UmbControllerHost, defaultViewAlias: string, defaultFilter: Partial<FilterModelType> = {}) {
super(host, UMB_COLLECTION_CONTEXT);
@@ -70,6 +80,33 @@ export class UmbDefaultCollectionContext<
this.#defaultFilter = defaultFilter;
this.pagination.addEventListener(UmbChangeEvent.TYPE, this.#onPageChange);
this.#listenToEntityEvents();
}
async #listenToEntityEvents() {
this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (context) => {
this.#actionEventContext = context;
context?.removeEventListener(
UmbRequestReloadStructureForEntityEvent.TYPE,
this.#onReloadStructureRequest as unknown as EventListener,
);
context?.removeEventListener(
UmbRequestReloadChildrenOfEntityEvent.TYPE,
this.#onReloadChildrenRequest as unknown as EventListener,
);
context?.addEventListener(
UmbRequestReloadStructureForEntityEvent.TYPE,
this.#onReloadStructureRequest as unknown as EventListener,
);
context?.addEventListener(
UmbRequestReloadChildrenOfEntityEvent.TYPE,
this.#onReloadChildrenRequest as unknown as EventListener,
);
});
}
#configured = false;
@@ -222,11 +259,37 @@ export class UmbDefaultCollectionContext<
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(layouts));
}
#onReloadStructureRequest = (event: UmbRequestReloadStructureForEntityEvent) => {
const items = this.#items.getValue();
const hasItem = items.some((item) => item.unique === event.getUnique());
if (hasItem) {
this.requestCollection();
}
};
#onReloadChildrenRequest = async (event: UmbRequestReloadChildrenOfEntityEvent) => {
// check if the collection is in the same context as the entity from the event
const entityContext = await this.getContext(UMB_ENTITY_CONTEXT);
const unique = entityContext.getUnique();
const entityType = entityContext.getEntityType();
if (unique === event.getUnique() && entityType === event.getEntityType()) {
this.requestCollection();
}
};
destroy(): void {
this.#actionEventContext?.removeEventListener(
UmbRequestReloadStructureForEntityEvent.TYPE,
this.#onReloadStructureRequest as unknown as EventListener,
);
this.#actionEventContext?.removeEventListener(
UmbRequestReloadChildrenOfEntityEvent.TYPE,
this.#onReloadChildrenRequest as unknown as EventListener,
);
super.destroy();
}
}
export const UMB_COLLECTION_CONTEXT = new UmbContextToken<UmbDefaultCollectionContext>('UmbCollectionContext');
/**
* @deprecated Use UMB_COLLECTION_CONTEXT instead.
*/
export { UMB_COLLECTION_CONTEXT as UMB_DEFAULT_COLLECTION_CONTEXT };

View File

@@ -1,9 +1,9 @@
import { UMB_COLLECTION_CONTEXT, UmbDefaultCollectionContext } from './collection-default.context.js';
import { UmbDefaultCollectionContext } from './collection-default.context.js';
import { UMB_COLLECTION_CONTEXT } from './collection-default.context-token.js';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbRoute } from '@umbraco-cms/backoffice/router';

View File

@@ -0,0 +1,2 @@
export { UMB_COLLECTION_CONTEXT } from './collection-default.context-token.js';
export { UmbDefaultCollectionContext } from './collection-default.context.js';

View File

@@ -7,6 +7,7 @@ export * from './collection.element.js';
export * from './components/index.js';
export * from './default/collection-default.context.js';
export * from './default/collection-default.context-token.js';
export * from './collection-filter-model.interface.js';
export { UMB_COLLECTION_ALIAS_CONDITION } from './collection-alias.manifest.js';

View File

@@ -1,6 +1,10 @@
import type { UmbDataSourceResponse } from '../../repository/index.js';
import type { UmbPagedModel } from '../../repository/types.js';
import type { UmbCollectionFilterModel } from '../collection-filter-model.interface.js';
export interface UmbCollectionDataSource<CollectionItemType, FilterType = unknown> {
export interface UmbCollectionDataSource<
CollectionItemType extends { entityType: string; unique: string } = any,
FilterType extends UmbCollectionFilterModel = UmbCollectionFilterModel,
> {
getCollection(filter: FilterType): Promise<UmbDataSourceResponse<UmbPagedModel<CollectionItemType>>>;
}

View File

@@ -1,5 +1,10 @@
import type { UmbCollectionFilterModel } from '../collection-filter-model.interface.js';
import type { UmbPagedModel, UmbRepositoryResponse } from '@umbraco-cms/backoffice/repository';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export interface UmbCollectionRepository extends UmbApi {
requestCollection(filter?: any): Promise<any>;
export interface UmbCollectionRepository<
CollectionItemType extends { entityType: string; unique: string } = any,
FilterType extends UmbCollectionFilterModel = UmbCollectionFilterModel,
> extends UmbApi {
requestCollection(filter?: FilterType): Promise<UmbRepositoryResponse<UmbPagedModel<CollectionItemType>>>;
}

View File

@@ -18,7 +18,6 @@ export interface UmbCollectionConfiguration {
orderBy?: string;
orderDirection?: string;
pageSize?: number;
useInfiniteEditor?: boolean;
userDefinedProperties?: Array<UmbCollectionColumnConfiguration>;
}

View File

@@ -1,11 +1,12 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit';
import { css, html, LitElement, customElement, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import type {
ManifestHeaderAppButtonKind,
UmbBackofficeManifestKind,
} from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
const manifest: UmbBackofficeManifestKind = {
type: 'kind',
@@ -21,7 +22,7 @@ const manifest: UmbBackofficeManifestKind = {
umbExtensionsRegistry.register(manifest);
@customElement('umb-header-app-button')
export class UmbHeaderAppButtonElement extends LitElement {
export class UmbHeaderAppButtonElement extends UmbLitElement {
public manifest?: ManifestHeaderAppButtonKind;
render() {
@@ -41,7 +42,11 @@ export class UmbHeaderAppButtonElement extends LitElement {
css`
uui-button {
font-size: 18px;
--uui-button-background-color: transparent;
--uui-button-background-color: var(--umb-header-app-button-background-color, transparent);
--uui-button-background-color-hover: var(
--umb-header-app-button-background-color-hover,
var(--uui-color-emphasis)
);
}
`,
];

View File

@@ -25,7 +25,7 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
this._src = value.src;
}
get value(): MediaValueType {
return !this.temporaryFile ? { src: this._src } : { temporaryFileId: this.temporaryFile.unique };
return !this.temporaryFile ? { src: this._src } : { temporaryFileId: this.temporaryFile.temporaryUnique };
}
/**
@@ -67,7 +67,7 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
async #onUpload(e: UUIFileDropzoneEvent) {
//Property Editor for Upload field will always only have one file.
const item: UmbTemporaryFileModel = {
unique: UmbId.new(),
temporaryUnique: UmbId.new(),
file: e.detail.files[0],
};
const upload = this.#manager.uploadOne(item);
@@ -80,7 +80,7 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
const uploaded = await upload;
if (uploaded.status === TemporaryFileStatus.SUCCESS) {
this.temporaryFile = { unique: item.unique, file: item.file };
this.temporaryFile = { temporaryUnique: item.temporaryUnique, file: item.file };
this.dispatchEvent(new UmbChangeEvent());
}
}
@@ -172,6 +172,9 @@ export class UmbInputUploadFieldElement extends UmbLitElement {
static styles = [
css`
:host {
position: relative;
}
uui-icon {
vertical-align: sub;
margin-right: var(--uui-size-space-4);

View File

@@ -1,5 +1,6 @@
import type { UmbPropertyEditorConfig } from '../../../property-editor/index.js';
import type { UmbPropertyTypeModel } from '../../types.js';
import { UmbContentPropertyContext } from '@umbraco-cms/backoffice/content';
import type { UmbDataTypeDetailModel } from '@umbraco-cms/backoffice/data-type';
import { UmbDataTypeDetailRepository } from '@umbraco-cms/backoffice/data-type';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
@@ -34,6 +35,8 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement {
private _dataTypeDetailRepository = new UmbDataTypeDetailRepository(this);
private _dataTypeObserver?: UmbObserverController<UmbDataTypeDetailModel | undefined>;
#contentPropertyContext = new UmbContentPropertyContext(this);
private async _observeDataType(dataTypeUnique?: string) {
this._dataTypeObserver?.destroy();
if (dataTypeUnique) {
@@ -42,6 +45,9 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement {
this._dataTypeObserver = this.observe(
await this._dataTypeDetailRepository.byUnique(dataTypeUnique),
(dataType) => {
const contextValue = dataType ? { unique: dataType.unique } : undefined;
this.#contentPropertyContext.setDataType(contextValue);
this._dataTypeData = dataType?.values;
this._propertyEditorUiAlias = dataType?.editorUiAlias || undefined;
// If there is no UI, we will look up the Property editor model to find the default UI alias:

View File

@@ -0,0 +1,4 @@
import type { UmbContentPropertyContext } from './content-property.context.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export const UMB_CONTENT_PROPERTY_CONTEXT = new UmbContextToken<UmbContentPropertyContext>('UmbContentPropertyContext');

View File

@@ -0,0 +1,18 @@
import type { UmbPropertyTypeModel } from '../content-type/types.js';
import { UMB_CONTENT_PROPERTY_CONTEXT } from './content-property.context-token.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
export class UmbContentPropertyContext extends UmbContextBase<UmbContentPropertyContext> {
#dataType = new UmbObjectState<UmbPropertyTypeModel['dataType'] | undefined>(undefined);
dataType = this.#dataType.asObservable();
constructor(host: UmbControllerHost) {
super(host, UMB_CONTENT_PROPERTY_CONTEXT);
}
setDataType(dataType: UmbPropertyTypeModel['dataType'] | undefined) {
this.#dataType.setValue(dataType);
}
}

View File

@@ -1 +1,4 @@
export * from './workspace/index.js';
export { UmbContentPropertyContext } from './content-property.context.js';
export { UMB_CONTENT_PROPERTY_CONTEXT } from './content-property.context-token.js';

View File

@@ -1,9 +1,9 @@
import { UmbEntityActionBase } from '../../entity-action-base.js';
import { UmbRequestReloadChildrenOfEntityEvent } from '../../request-reload-children-of-entity.event.js';
import { UMB_SORT_CHILDREN_OF_MODAL } from './modal/index.js';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import type { MetaEntityActionSortChildrenOfKind } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UmbRequestReloadTreeItemChildrenEvent } from '@umbraco-cms/backoffice/tree';
export class UmbSortChildrenOfEntityAction extends UmbEntityActionBase<MetaEntityActionSortChildrenOfKind> {
async execute() {
@@ -22,7 +22,7 @@ export class UmbSortChildrenOfEntityAction extends UmbEntityActionBase<MetaEntit
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
eventContext.dispatchEvent(
new UmbRequestReloadTreeItemChildrenEvent({
new UmbRequestReloadChildrenOfEntityEvent({
unique: this.args.unique,
entityType: this.args.entityType,
}),

View File

@@ -7,4 +7,5 @@ export * from './types.js';
export type * from './entity-action-element.interface.js';
export { UmbRequestReloadStructureForEntityEvent } from './request-reload-structure-for-entity.event.js';
export { UmbRequestReloadChildrenOfEntityEvent } from './request-reload-children-of-entity.event.js';
export { UMB_ENTITY_ACTION_DEFAULT_KIND_MANIFEST } from './default/default.action.kind.js';

View File

@@ -0,0 +1,10 @@
import type { UmbEntityActionEventArgs } from './entity-action.event.js';
import { UmbEntityActionEvent } from './entity-action.event.js';
export class UmbRequestReloadChildrenOfEntityEvent extends UmbEntityActionEvent {
static readonly TYPE = 'request-reload-children-of-entity';
constructor(args: UmbEntityActionEventArgs) {
super(UmbRequestReloadChildrenOfEntityEvent.TYPE, args);
}
}

View File

@@ -1,4 +1,5 @@
import { umbExtensionsRegistry } from '../registry.js';
import type { UmbExtensionCollectionFilterModel } from './types.js';
import { html, customElement, css } from '@umbraco-cms/backoffice/external/lit';
import { fromCamelCase } from '@umbraco-cms/backoffice/utils';
import { UMB_COLLECTION_CONTEXT, UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection';
@@ -7,7 +8,7 @@ import type { UUISelectEvent } from '@umbraco-cms/backoffice/external/uui';
@customElement('umb-extension-collection')
export class UmbExtensionCollectionElement extends UmbCollectionDefaultElement {
#collectionContext?: UmbDefaultCollectionContext;
#collectionContext?: UmbDefaultCollectionContext<any, UmbExtensionCollectionFilterModel>;
#inputTimer?: NodeJS.Timeout;
#inputTimerAmount = 500;
@@ -29,15 +30,15 @@ export class UmbExtensionCollectionElement extends UmbCollectionDefaultElement {
}
#onChange(event: UUISelectEvent) {
const extensionType = event.target.value;
const extensionType = event.target.value as string;
this.#collectionContext?.setFilter({ type: extensionType });
}
#onSearch(event: InputEvent) {
const target = event.target as HTMLInputElement;
const query = target.value || '';
const filter = target.value || '';
clearTimeout(this.#inputTimer);
this.#inputTimer = setTimeout(() => this.#collectionContext?.setFilter({ query }), this.#inputTimerAmount);
this.#inputTimer = setTimeout(() => this.#collectionContext?.setFilter({ filter }), this.#inputTimerAmount);
}
protected renderToolbar() {

View File

@@ -1,39 +1,45 @@
import { umbExtensionsRegistry } from '../../registry.js';
import type { ManifestTypes } from '../../models/index.js';
import type { UmbExtensionCollectionFilterModel, UmbExtensionDetailModel } from '../types.js';
import { UMB_EXTENSION_ENTITY_TYPE } from '../../entity.js';
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbCollectionRepository } from '@umbraco-cms/backoffice/collection';
export interface UmbExtensionCollectionFilter {
query?: string;
skip: number;
take: number;
type?: ManifestTypes['type'];
}
export class UmbExtensionCollectionRepository extends UmbRepositoryBase implements UmbCollectionRepository {
export class UmbExtensionCollectionRepository
extends UmbRepositoryBase
implements UmbCollectionRepository<UmbExtensionDetailModel, UmbExtensionCollectionFilterModel>
{
constructor(host: UmbControllerHost) {
super(host);
}
async requestCollection(filter: UmbExtensionCollectionFilter) {
let extensions = umbExtensionsRegistry.getAllExtensions();
async requestCollection(query: UmbExtensionCollectionFilterModel) {
let extensions: Array<UmbExtensionDetailModel> = umbExtensionsRegistry.getAllExtensions().map((manifest) => {
return {
...manifest,
unique: manifest.alias,
entityType: UMB_EXTENSION_ENTITY_TYPE,
};
});
if (filter.query) {
const query = filter.query.toLowerCase();
const skip = query.skip || 0;
const take = query.take || 100;
if (query.filter) {
const text = query.filter.toLowerCase();
extensions = extensions.filter(
(x) => x.name.toLowerCase().includes(query) || x.alias.toLowerCase().includes(query),
(x) => x.name.toLowerCase().includes(text) || x.alias.toLowerCase().includes(text),
);
}
if (filter.type) {
extensions = extensions.filter((x) => x.type === filter.type);
if (query.type) {
extensions = extensions.filter((x) => x.type === query.type);
}
extensions.sort((a, b) => a.type.localeCompare(b.type) || a.alias.localeCompare(b.alias));
const total = extensions.length;
const items = extensions.slice(filter.skip, filter.skip + filter.take);
const items = extensions.slice(skip, skip + take);
const data = { items, total };
return { data };
}
@@ -41,4 +47,4 @@ export class UmbExtensionCollectionRepository extends UmbRepositoryBase implemen
destroy(): void {}
}
export default UmbExtensionCollectionRepository;
export { UmbExtensionCollectionRepository as api };

View File

@@ -0,0 +1,12 @@
import type { UmbExtensionEntityType } from '../entity.js';
import type { UmbCollectionFilterModel } from '@umbraco-cms/backoffice/collection';
import type { ManifestBase } from '@umbraco-cms/backoffice/extension-api';
export interface UmbExtensionCollectionFilterModel extends UmbCollectionFilterModel {
type?: string;
}
export interface UmbExtensionDetailModel extends ManifestBase {
unique: string;
entityType: UmbExtensionEntityType;
}

View File

@@ -1,50 +0,0 @@
import { umbExtensionsRegistry } from '../../index.js';
import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection';
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import type { ManifestBase } from '@umbraco-cms/backoffice/extension-api';
import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection';
@customElement('umb-extension-table-action-column-layout')
export class UmbExtensionTableActionColumnLayoutElement extends UmbLitElement {
@property({ attribute: false })
value!: ManifestBase;
#collectionContext?: UmbDefaultCollectionContext<ManifestBase>;
constructor() {
super();
this.consumeContext(UMB_COLLECTION_CONTEXT, (instance) => {
this.#collectionContext = instance;
});
}
async #removeExtension() {
await umbConfirmModal(this, {
headline: 'Unload extension',
confirmLabel: 'Unload',
content: html`<p>Are you sure you want to unload the extension <strong>${this.value.alias}</strong>?</p>`,
color: 'danger',
});
umbExtensionsRegistry.unregister(this.value.alias);
this.#collectionContext?.requestCollection();
}
render() {
return html`
<uui-button label="Unload" color="danger" look="primary" @click=${this.#removeExtension}>
<uui-icon name="icon-trash"></uui-icon>
</uui-button>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-extension-table-action-column-layout': UmbExtensionTableActionColumnLayoutElement;
}
}

View File

@@ -1,12 +1,12 @@
import type { UmbExtensionCollectionFilterModel, UmbExtensionDetailModel } from '../../types.js';
import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection';
import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection';
import type { UmbTableColumn, UmbTableConfig, UmbTableItem } from '@umbraco-cms/backoffice/components';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { ManifestBase } from '@umbraco-cms/backoffice/extension-api';
import '../extension-table-action-column-layout.element.js';
import './extension-table-entity-actions-column-layout.element.js';
@customElement('umb-extension-table-collection-view')
export class UmbExtensionTableCollectionViewElement extends UmbLitElement {
@@ -36,14 +36,14 @@ export class UmbExtensionTableCollectionViewElement extends UmbLitElement {
{
name: '',
alias: 'extensionAction',
elementName: 'umb-extension-table-action-column-layout',
elementName: 'umb-extension-table-entity-actions-column-layout',
},
];
@state()
private _tableItems: Array<UmbTableItem> = [];
#collectionContext?: UmbDefaultCollectionContext<ManifestBase>;
#collectionContext?: UmbDefaultCollectionContext<UmbExtensionDetailModel, UmbExtensionCollectionFilterModel>;
constructor() {
super();
@@ -59,10 +59,10 @@ export class UmbExtensionTableCollectionViewElement extends UmbLitElement {
this.observe(this.#collectionContext.items, (items) => this.#createTableItems(items), 'umbCollectionItemsObserver');
}
#createTableItems(extensions: Array<ManifestBase>) {
#createTableItems(extensions: Array<UmbExtensionDetailModel>) {
this._tableItems = extensions.map((extension) => {
return {
id: extension.alias,
id: extension.unique,
data: [
{
columnAlias: 'extensionType',

View File

@@ -0,0 +1,35 @@
import type { UmbExtensionDetailModel } from '../../types.js';
import { html, customElement, property, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
const elementName = 'umb-extension-table-entity-actions-column-layout';
@customElement(elementName)
export class UmbExtensionTableEntityActionsColumnLayoutElement extends UmbLitElement {
@property({ attribute: false })
value!: UmbExtensionDetailModel;
@state()
_isOpen = false;
#onActionExecuted() {
this._isOpen = false;
}
render() {
return html`
<umb-dropdown .open=${this._isOpen} compact hide-expand>
<uui-symbol-more slot="label"></uui-symbol-more>
<umb-entity-action-list
@action-executed=${this.#onActionExecuted}
entity-type=${this.value.entityType}
unique=${ifDefined(this.value.unique)}></umb-entity-action-list>
</umb-dropdown>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
[elementName]: UmbExtensionTableEntityActionsColumnLayoutElement;
}
}

View File

@@ -0,0 +1,3 @@
import { manifests as unregisterManifests } from './unregister/manifests.js';
export const manifests = [...unregisterManifests];

View File

@@ -0,0 +1,16 @@
import { UMB_EXTENSION_ENTITY_TYPE } from '../../entity.js';
export const manifests = [
{
type: 'entityAction',
kind: 'default',
alias: 'Umb.EntityAction.Extension.Unregister',
name: 'Unregister Extension Entity Action',
api: () => import('./unregister-extension.action.js'),
forEntityTypes: [UMB_EXTENSION_ENTITY_TYPE],
meta: {
label: 'Unregister',
icon: 'icon-trash',
},
},
];

View File

@@ -0,0 +1,33 @@
import { umbExtensionsRegistry } from '../../registry.js';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UmbEntityActionBase, UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import { html } from '@umbraco-cms/backoffice/external/lit';
export class UmbUnregisterExtensionEntityAction extends UmbEntityActionBase<unknown> {
async execute() {
if (!this.args.unique) throw new Error('Cannot delete an item without a unique identifier.');
const extension = umbExtensionsRegistry.getByAlias(this.args.unique);
if (!extension) throw new Error('Extension not found');
await umbConfirmModal(this, {
headline: 'Unregister extension',
confirmLabel: 'Unregister',
content: html`<p>Are you sure you want to unregister the extension <strong>${extension.alias}</strong>?</p>`,
color: 'danger',
});
umbExtensionsRegistry.unregister(extension.alias);
const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadStructureForEntityEvent({
unique: this.args.unique,
entityType: this.args.entityType,
});
actionEventContext.dispatchEvent(event);
}
}
export { UmbUnregisterExtensionEntityAction as api };

View File

@@ -0,0 +1,3 @@
export const UMB_EXTENSION_ENTITY_TYPE = 'extension';
export type UmbExtensionEntityType = typeof UMB_EXTENSION_ENTITY_TYPE;

View File

@@ -2,6 +2,7 @@ import { manifests as conditionManifests } from './conditions/manifests.js';
import { manifests as menuItemManifests } from './menu-item/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js';
import { manifests as collectionManifests } from './collection/manifests.js';
import { manifests as entityActionManifests } from './entity-actions/manifests.js';
import type { ManifestTypes } from './models/index.js';
export const manifests: Array<ManifestTypes> = [
@@ -9,4 +10,5 @@ export const manifests: Array<ManifestTypes> = [
...menuItemManifests,
...workspaceManifests,
...collectionManifests,
...entityActionManifests,
];

View File

@@ -33,6 +33,7 @@ import type { ManifestMenu } from './menu.model.js';
import type { ManifestMenuItem, ManifestMenuItemTreeKind } from './menu-item.model.js';
import type { ManifestModal } from './modal.model.js';
import type { ManifestPackageView } from './package-view.model.js';
import type { ManifestPreviewAppProvider } from './preview-app.model.js';
import type { ManifestPropertyAction, ManifestPropertyActionDefaultKind } from './property-action.model.js';
import type { ManifestPropertyEditorUi, ManifestPropertyEditorSchema } from './property-editor.model.js';
import type { ManifestRepository } from './repository.model.js';
@@ -97,6 +98,7 @@ export type * from './mfa-login-provider.model.js';
export type * from './modal.model.js';
export type * from './monaco-markdown-editor-action.model.js';
export type * from './package-view.model.js';
export type * from './preview-app.model.js';
export type * from './property-action.model.js';
export type * from './property-editor.model.js';
export type * from './repository.model.js';
@@ -183,6 +185,7 @@ export type ManifestTypes =
| ManifestModal
| ManifestMonacoMarkdownEditorAction
| ManifestPackageView
| ManifestPreviewAppProvider
| ManifestPropertyActions
| ManifestPropertyEditorSchema
| ManifestPropertyEditorUi

View File

@@ -0,0 +1,8 @@
import type { ManifestElement } from '@umbraco-cms/backoffice/extension-api';
/**
* Preview apps are displayed in the menu of the preview window.
*/
export interface ManifestPreviewAppProvider extends ManifestElement {
type: 'previewApp';
}

View File

@@ -2404,6 +2404,10 @@
"name": "icon-window-popin",
"file": "square-arrow-down-left.svg"
},
{
"name": "icon-window-popout",
"file": "square-arrow-up-right.svg"
},
{
"name": "icon-window-sizes",
"file": "scaling.svg"

View File

@@ -0,0 +1,16 @@
export default `<!-- @license lucide-static v0.367.0 - ISC -->
<svg
class="lucide lucide-square-arrow-up-right"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.75"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M8 8h8v8" />
<path d="m8 16 8-8" />
</svg>
`;

View File

@@ -2055,6 +2055,10 @@ name: "icon-window-popin",
path: "./icons/icon-window-popin.js",
},{
name: "icon-window-popout",
path: "./icons/icon-window-popout.js",
},{
name: "icon-window-sizes",
path: "./icons/icon-window-sizes.js",

View File

@@ -104,7 +104,7 @@ export class UmbPropertyLayoutElement extends LitElement {
height: min-content;
}
/*@container (width > 600px) {*/
#headerColumn {
:host(:not([orientation='vertical'])) #headerColumn {
position: sticky;
top: calc(var(--uui-size-space-2) * -1);
}
@@ -128,7 +128,7 @@ export class UmbPropertyLayoutElement extends LitElement {
margin-top: var(--uui-size-space-3);
}
/*@container (width > 600px) {*/
#editorColumn {
:host(:not([orientation='vertical'])) #editorColumn {
margin-top: 0;
}
/*}*/

View File

@@ -235,7 +235,10 @@ export class UmbPropertyElement extends UmbLitElement {
this.#valueObserver = this.observe(
this.#propertyContext.value,
(value) => {
// Set the value on the element:
this._element!.value = value;
// Set the value on the context as well, to ensure that any default values are stored right away:
this.#propertyContext.setValue(value);
if (this.#validationMessageBinder) {
this.#validationMessageBinder.value = value;
}

View File

@@ -6,7 +6,7 @@ import {
type MetaEntityActionEmptyRecycleBinKind,
} from '@umbraco-cms/backoffice/extension-registry';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '@umbraco-cms/backoffice/tree';
import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action';
/**
* Entity action for emptying the recycle bin.
@@ -34,7 +34,7 @@ export class UmbEmptyRecycleBinEntityAction extends UmbEntityActionBase<MetaEnti
await recycleBinRepository.requestEmpty();
const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbReloadTreeItemChildrenRequestEntityActionEvent({
const event = new UmbRequestReloadChildrenOfEntityEvent({
unique: this.args.unique,
entityType: this.args.entityType,
});

View File

@@ -2,7 +2,6 @@ import { UmbTemporaryFileRepository } from './temporary-file.repository.js';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import { UmbId } from '@umbraco-cms/backoffice/id';
///export type TemporaryFileStatus = 'success' | 'waiting' | 'error';
@@ -14,7 +13,7 @@ export enum TemporaryFileStatus {
export interface UmbTemporaryFileModel {
file: File;
unique: string;
temporaryUnique: string;
status?: TemporaryFileStatus;
}
@@ -23,7 +22,7 @@ export class UmbTemporaryFileManager<
> extends UmbControllerBase {
#temporaryFileRepository;
#queue = new UmbArrayState<UploadableItem>([], (item) => item.unique);
#queue = new UmbArrayState<UploadableItem>([], (item) => item.temporaryUnique);
public readonly queue = this.#queue.asObservable();
constructor(host: UmbControllerHost) {
@@ -66,18 +65,18 @@ export class UmbTemporaryFileManager<
if (!queue.length) return filesCompleted;
for (const item of queue) {
if (!item.unique) throw new Error(`Unique is missing for item ${item}`);
if (!item.temporaryUnique) throw new Error(`Unique is missing for item ${item}`);
const { error } = await this.#temporaryFileRepository.upload(item.unique, item.file);
const { error } = await this.#temporaryFileRepository.upload(item.temporaryUnique, item.file);
//await new Promise((resolve) => setTimeout(resolve, (Math.random() + 0.5) * 1000)); // simulate small delay so that the upload badge is properly shown
let status: TemporaryFileStatus;
if (error) {
status = TemporaryFileStatus.ERROR;
this.#queue.updateOne(item.unique, { ...item, status });
this.#queue.updateOne(item.temporaryUnique, { ...item, status });
} else {
status = TemporaryFileStatus.SUCCESS;
this.#queue.updateOne(item.unique, { ...item, status });
this.#queue.updateOne(item.temporaryUnique, { ...item, status });
}
filesCompleted.push({ ...item, status });

View File

@@ -23,25 +23,30 @@ export interface UmbTreeDataSourceConstructor<TreeItemType extends UmbTreeItemMo
* @interface UmbTreeDataSource
* @template TreeItemType
*/
export interface UmbTreeDataSource<TreeItemType extends UmbTreeItemModel> {
export interface UmbTreeDataSource<
TreeItemType extends UmbTreeItemModel,
TreeRootItemsRequestArgsType extends UmbTreeRootItemsRequestArgs = UmbTreeRootItemsRequestArgs,
TreeChildrenOfRequestArgsType extends UmbTreeChildrenOfRequestArgs = UmbTreeChildrenOfRequestArgs,
TreeAncestorsOfRequestArgsType extends UmbTreeAncestorsOfRequestArgs = UmbTreeAncestorsOfRequestArgs,
> {
/**
* Gets the root items of the tree.
* @return {*} {Promise<UmbDataSourceResponse<UmbPagedModel<TreeItemType>>>}
* @memberof UmbTreeDataSource
*/
getRootItems(args: UmbTreeRootItemsRequestArgs): Promise<UmbDataSourceResponse<UmbPagedModel<TreeItemType>>>;
getRootItems(args: TreeRootItemsRequestArgsType): Promise<UmbDataSourceResponse<UmbPagedModel<TreeItemType>>>;
/**
* Gets the children of the given parent item.
* @return {*} {Promise<UmbDataSourceResponse<UmbPagedModel<TreeItemType>>}
* @memberof UmbTreeDataSource
*/
getChildrenOf(args: UmbTreeChildrenOfRequestArgs): Promise<UmbDataSourceResponse<UmbPagedModel<TreeItemType>>>;
getChildrenOf(args: TreeChildrenOfRequestArgsType): Promise<UmbDataSourceResponse<UmbPagedModel<TreeItemType>>>;
/**
* Gets the ancestors of the given item.
* @return {*} {Promise<UmbDataSourceResponse<Array<TreeItemType>>}
* @memberof UmbTreeDataSource
*/
getAncestorsOf(args: UmbTreeAncestorsOfRequestArgs): Promise<UmbDataSourceResponse<Array<TreeItemType>>>;
getAncestorsOf(args: TreeAncestorsOfRequestArgsType): Promise<UmbDataSourceResponse<Array<TreeItemType>>>;
}

View File

@@ -19,7 +19,7 @@ import type { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
* @abstract
* @class UmbTreeRepositoryBase
* @extends {UmbRepositoryBase}
* @implements {UmbTreeRepository<TreeItemType, TreeRootType>}
* @implements {UmbTreeRepository}
* @implements {UmbApi}
* @template TreeItemType
* @template TreeRootType
@@ -27,9 +27,20 @@ import type { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export abstract class UmbTreeRepositoryBase<
TreeItemType extends UmbTreeItemModel,
TreeRootType extends UmbTreeRootModel,
TreeRootItemsRequestArgsType extends UmbTreeRootItemsRequestArgs = UmbTreeRootItemsRequestArgs,
TreeChildrenOfRequestArgsType extends UmbTreeChildrenOfRequestArgs = UmbTreeChildrenOfRequestArgs,
TreeAncestorsOfRequestArgsType extends UmbTreeAncestorsOfRequestArgs = UmbTreeAncestorsOfRequestArgs,
>
extends UmbRepositoryBase
implements UmbTreeRepository<TreeItemType, TreeRootType>, UmbApi
implements
UmbTreeRepository<
TreeItemType,
TreeRootType,
TreeRootItemsRequestArgsType,
TreeChildrenOfRequestArgsType,
TreeAncestorsOfRequestArgsType
>,
UmbApi
{
protected _init: Promise<unknown>;
protected _treeStore?: UmbTreeStore<TreeItemType>;
@@ -67,7 +78,7 @@ export abstract class UmbTreeRepositoryBase<
* @return {*}
* @memberof UmbTreeRepositoryBase
*/
async requestRootTreeItems(args: UmbTreeRootItemsRequestArgs) {
async requestTreeRootItems(args: TreeRootItemsRequestArgsType) {
await this._init;
const { data, error: _error } = await this._treeSource.getRootItems(args);
@@ -85,7 +96,7 @@ export abstract class UmbTreeRepositoryBase<
* @return {*}
* @memberof UmbTreeRepositoryBase
*/
async requestTreeItemsOf(args: UmbTreeChildrenOfRequestArgs) {
async requestTreeItemsOf(args: TreeChildrenOfRequestArgsType) {
if (!args.parent) throw new Error('Parent is missing');
if (args.parent.unique === undefined) throw new Error('Parent unique is missing');
if (args.parent.entityType === null) throw new Error('Parent entity type is missing');
@@ -106,7 +117,7 @@ export abstract class UmbTreeRepositoryBase<
* @return {*}
* @memberof UmbTreeRepositoryBase
*/
async requestTreeItemAncestors(args: UmbTreeAncestorsOfRequestArgs) {
async requestTreeItemAncestors(args: TreeAncestorsOfRequestArgsType) {
if (args.treeItem.unique === undefined) throw new Error('Descendant unique is missing');
await this._init;

View File

@@ -20,6 +20,9 @@ import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export interface UmbTreeRepository<
TreeItemType extends UmbTreeItemModel = UmbTreeItemModel,
TreeRootType extends UmbTreeRootModel = UmbTreeRootModel,
TreeRootItemsRequestArgsType extends UmbTreeRootItemsRequestArgs = UmbTreeRootItemsRequestArgs,
TreeChildrenOfRequestArgsType extends UmbTreeChildrenOfRequestArgs = UmbTreeChildrenOfRequestArgs,
TreeAncestorsOfRequestArgsType extends UmbTreeAncestorsOfRequestArgs = UmbTreeAncestorsOfRequestArgs,
> extends UmbApi {
/**
* Requests the root of the tree.
@@ -35,7 +38,7 @@ export interface UmbTreeRepository<
* @param {UmbTreeRootItemsRequestArgs} args
* @memberof UmbTreeRepository
*/
requestRootTreeItems: (args: UmbTreeRootItemsRequestArgs) => Promise<{
requestTreeRootItems: (args: TreeRootItemsRequestArgsType) => Promise<{
data?: UmbPagedModel<TreeItemType>;
error?: ProblemDetails;
asObservable?: () => Observable<TreeItemType[]>;
@@ -46,7 +49,7 @@ export interface UmbTreeRepository<
* @param {UmbTreeChildrenOfRequestArgs} args
* @memberof UmbTreeRepository
*/
requestTreeItemsOf: (args: UmbTreeChildrenOfRequestArgs) => Promise<{
requestTreeItemsOf: (args: TreeChildrenOfRequestArgsType) => Promise<{
data?: UmbPagedModel<TreeItemType>;
error?: ProblemDetails;
asObservable?: () => Observable<TreeItemType[]>;
@@ -58,7 +61,7 @@ export interface UmbTreeRepository<
* @memberof UmbTreeRepository
*/
requestTreeItemAncestors: (
args: UmbTreeAncestorsOfRequestArgs,
args: TreeAncestorsOfRequestArgsType,
) => Promise<{ data?: TreeItemType[]; error?: ProblemDetails; asObservable?: () => Observable<TreeItemType[]> }>;
/**

View File

@@ -12,10 +12,13 @@ import type { UmbPagedModel } from '@umbraco-cms/backoffice/repository';
export interface UmbTreeServerDataSourceBaseArgs<
ServerTreeItemType extends { hasChildren: boolean },
ClientTreeItemType extends UmbTreeItemModelBase,
TreeRootItemsRequestArgsType extends UmbTreeRootItemsRequestArgs = UmbTreeRootItemsRequestArgs,
TreeChildrenOfRequestArgsType extends UmbTreeChildrenOfRequestArgs = UmbTreeChildrenOfRequestArgs,
TreeAncestorsOfRequestArgsType extends UmbTreeAncestorsOfRequestArgs = UmbTreeAncestorsOfRequestArgs,
> {
getRootItems: (args: UmbTreeRootItemsRequestArgs) => Promise<UmbPagedModel<ServerTreeItemType>>;
getChildrenOf: (args: UmbTreeChildrenOfRequestArgs) => Promise<UmbPagedModel<ServerTreeItemType>>;
getAncestorsOf: (args: UmbTreeAncestorsOfRequestArgs) => Promise<Array<ServerTreeItemType>>;
getRootItems: (args: TreeRootItemsRequestArgsType) => Promise<UmbPagedModel<ServerTreeItemType>>;
getChildrenOf: (args: TreeChildrenOfRequestArgsType) => Promise<UmbPagedModel<ServerTreeItemType>>;
getAncestorsOf: (args: TreeAncestorsOfRequestArgsType) => Promise<Array<ServerTreeItemType>>;
mapper: (item: ServerTreeItemType) => ClientTreeItemType;
}
@@ -28,7 +31,16 @@ export interface UmbTreeServerDataSourceBaseArgs<
export abstract class UmbTreeServerDataSourceBase<
ServerTreeItemType extends { hasChildren: boolean },
ClientTreeItemType extends UmbTreeItemModel,
> implements UmbTreeDataSource<ClientTreeItemType>
TreeRootItemsRequestArgsType extends UmbTreeRootItemsRequestArgs = UmbTreeRootItemsRequestArgs,
TreeChildrenOfRequestArgsType extends UmbTreeChildrenOfRequestArgs = UmbTreeChildrenOfRequestArgs,
TreeAncestorsOfRequestArgsType extends UmbTreeAncestorsOfRequestArgs = UmbTreeAncestorsOfRequestArgs,
> implements
UmbTreeDataSource<
ClientTreeItemType,
TreeRootItemsRequestArgsType,
TreeChildrenOfRequestArgsType,
TreeAncestorsOfRequestArgsType
>
{
#host;
#getRootItems;
@@ -41,7 +53,16 @@ export abstract class UmbTreeServerDataSourceBase<
* @param {UmbControllerHost} host
* @memberof UmbTreeServerDataSourceBase
*/
constructor(host: UmbControllerHost, args: UmbTreeServerDataSourceBaseArgs<ServerTreeItemType, ClientTreeItemType>) {
constructor(
host: UmbControllerHost,
args: UmbTreeServerDataSourceBaseArgs<
ServerTreeItemType,
ClientTreeItemType,
TreeRootItemsRequestArgsType,
TreeChildrenOfRequestArgsType,
TreeAncestorsOfRequestArgsType
>,
) {
this.#host = host;
this.#getRootItems = args.getRootItems;
this.#getChildrenOf = args.getChildrenOf;
@@ -55,7 +76,7 @@ export abstract class UmbTreeServerDataSourceBase<
* @return {*}
* @memberof UmbTreeServerDataSourceBase
*/
async getRootItems(args: UmbTreeRootItemsRequestArgs) {
async getRootItems(args: TreeRootItemsRequestArgsType) {
const { data, error } = await tryExecuteAndNotify(this.#host, this.#getRootItems(args));
if (data) {
@@ -72,7 +93,7 @@ export abstract class UmbTreeServerDataSourceBase<
* @return {*}
* @memberof UmbTreeServerDataSourceBase
*/
async getChildrenOf(args: UmbTreeChildrenOfRequestArgs) {
async getChildrenOf(args: TreeChildrenOfRequestArgsType) {
if (args.parent.unique === undefined) throw new Error('Parent unique is missing');
const { data, error } = await tryExecuteAndNotify(this.#host, this.#getChildrenOf(args));
@@ -91,7 +112,7 @@ export abstract class UmbTreeServerDataSourceBase<
* @return {*}
* @memberof UmbTreeServerDataSourceBase
*/
async getAncestorsOf(args: UmbTreeAncestorsOfRequestArgs) {
async getAncestorsOf(args: TreeAncestorsOfRequestArgsType) {
if (!args.treeItem.entityType) throw new Error('Parent unique is missing');
const { data, error } = await tryExecuteAndNotify(this.#host, this.#getAncestorsOf(args));

View File

@@ -0,0 +1,7 @@
import type { UmbTreeItemModel, UmbTreeRootModel } from '../types.js';
import type { UmbDefaultTreeContext } from './default-tree.context.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export const UMB_TREE_CONTEXT = new UmbContextToken<UmbDefaultTreeContext<UmbTreeItemModel, UmbTreeRootModel>>(
'UmbTreeContext',
);

View File

@@ -2,6 +2,8 @@ import { UmbRequestReloadTreeItemChildrenEvent } from '../reload-tree-item-child
import type { UmbTreeItemModel, UmbTreeRootModel, UmbTreeStartNode } from '../types.js';
import type { UmbTreeRepository } from '../data/tree-repository.interface.js';
import type { UmbTreeContext } from '../tree-context.interface.js';
import type { UmbTreeRootItemsRequestArgs } from '../data/types.js';
import { UMB_TREE_CONTEXT } from './default-tree.context-token.js';
import { type UmbActionEventContext, UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import {
type ManifestRepository,
@@ -12,15 +14,24 @@ import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UmbPaginationManager, UmbSelectionManager, debounce } from '@umbraco-cms/backoffice/utils';
import type { UmbEntityActionEvent } from '@umbraco-cms/backoffice/entity-action';
import {
UmbRequestReloadChildrenOfEntityEvent,
type UmbEntityActionEvent,
} from '@umbraco-cms/backoffice/entity-action';
import { UmbArrayState, UmbBooleanState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModel, TreeRootType extends UmbTreeRootModel>
extends UmbContextBase<UmbDefaultTreeContext<TreeItemType, TreeRootType>>
export class UmbDefaultTreeContext<
TreeItemType extends UmbTreeItemModel,
TreeRootType extends UmbTreeRootModel,
RequestArgsType extends UmbTreeRootItemsRequestArgs = UmbTreeRootItemsRequestArgs,
>
extends UmbContextBase<UmbDefaultTreeContext<TreeItemType, TreeRootType, RequestArgsType>>
implements UmbTreeContext
{
#additionalRequestArgs = new UmbObjectState<Partial<RequestArgsType> | object>({});
public readonly additionalRequestArgs = this.#additionalRequestArgs.asObservable();
#treeRoot = new UmbObjectState<TreeRootType | undefined>(undefined);
treeRoot = this.#treeRoot.asObservable();
@@ -57,7 +68,7 @@ export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModel, TreeRo
constructor(host: UmbControllerHost) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
super(host, UMB_DEFAULT_TREE_CONTEXT);
super(host, UMB_TREE_CONTEXT);
this.pagination.setPageSize(this.#paging.take);
this.#consumeContexts();
@@ -136,7 +147,7 @@ export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModel, TreeRo
public loadMore = () => this.#debouncedLoadTree(true);
#debouncedLoadTree(reload = false) {
if (this.getStartFrom()) {
if (this.getStartNode()) {
this.#loadRootItems(reload);
return;
}
@@ -166,10 +177,12 @@ export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModel, TreeRo
const take = loadMore ? this.#paging.take : this.pagination.getCurrentPageNumber() * this.#paging.take;
// If we have a start node get children of that instead of the root
const startNode = this.getStartFrom();
const startNode = this.getStartNode();
const additionalArgs = this.#additionalRequestArgs.getValue();
const { data } = startNode?.unique
? await this.#repository!.requestTreeItemsOf({
...additionalArgs,
parent: {
unique: startNode.unique,
entityType: startNode.entityType,
@@ -177,7 +190,8 @@ export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModel, TreeRo
skip,
take,
})
: await this.#repository!.requestRootTreeItems({
: await this.#repository!.requestTreeRootItems({
...additionalArgs,
skip,
take,
});
@@ -220,19 +234,32 @@ export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModel, TreeRo
* @param {UmbTreeStartNode} startNode
* @memberof UmbDefaultTreeContext
*/
setStartFrom(startNode: UmbTreeStartNode | undefined) {
setStartNode(startNode: UmbTreeStartNode | undefined) {
this.#startNode.setValue(startNode);
// we need to reset the tree if this config changes
this.#resetTree();
this.loadTree();
}
/**
* Updates the requestArgs config and reloads the tree.
*/
public updateAdditionalRequestArgs(args: Partial<RequestArgsType>) {
this.#additionalRequestArgs.setValue({ ...this.#additionalRequestArgs.getValue(), ...args });
this.#resetTree();
this.loadTree();
}
public getAdditionalRequestArgs() {
return this.#additionalRequestArgs.getValue();
}
/**
* Gets the startNode config
* @return {UmbTreeStartNode}
* @memberof UmbDefaultTreeContext
*/
getStartFrom() {
getStartNode() {
return this.#startNode.getValue();
}
@@ -245,14 +272,26 @@ export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModel, TreeRo
#consumeContexts() {
this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (instance) => {
this.#actionEventContext = instance;
this.#actionEventContext.removeEventListener(
UmbRequestReloadTreeItemChildrenEvent.TYPE,
this.#onReloadRequest as EventListener,
);
this.#actionEventContext.removeEventListener(
UmbRequestReloadChildrenOfEntityEvent.TYPE,
this.#onReloadRequest as EventListener,
);
this.#actionEventContext.addEventListener(
UmbRequestReloadTreeItemChildrenEvent.TYPE,
this.#onReloadRequest as EventListener,
);
this.#actionEventContext.addEventListener(
UmbRequestReloadChildrenOfEntityEvent.TYPE,
this.#onReloadRequest as EventListener,
);
});
}
@@ -291,12 +330,14 @@ export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModel, TreeRo
UmbRequestReloadTreeItemChildrenEvent.TYPE,
this.#onReloadRequest as EventListener,
);
this.#actionEventContext?.removeEventListener(
UmbRequestReloadChildrenOfEntityEvent.TYPE,
this.#onReloadRequest as EventListener,
);
super.destroy();
}
}
export default UmbDefaultTreeContext;
export const UMB_DEFAULT_TREE_CONTEXT = new UmbContextToken<UmbDefaultTreeContext<UmbTreeItemModel, UmbTreeRootModel>>(
'UmbTreeContext',
);
export { UmbDefaultTreeContext as api };

View File

@@ -6,7 +6,7 @@ import type {
UmbTreeStartNode,
} from '../types.js';
import type { UmbDefaultTreeContext } from './default-tree.context.js';
import { UMB_DEFAULT_TREE_CONTEXT } from './default-tree.context.js';
import { UMB_TREE_CONTEXT } from './default-tree.context-token.js';
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
import { html, nothing, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -57,7 +57,7 @@ export class UmbDefaultTreeElement extends UmbLitElement {
this.#init = Promise.all([
// TODO: Notice this can be retrieve via a api property. [NL]
this.consumeContext(UMB_DEFAULT_TREE_CONTEXT, (instance) => {
this.consumeContext(UMB_TREE_CONTEXT, (instance) => {
this.#treeContext = instance;
this.observe(this.#treeContext.treeRoot, (treeRoot) => (this._treeRoot = treeRoot));
this.observe(this.#treeContext.rootItems, (rootItems) => (this._rootItems = rootItems));
@@ -80,7 +80,7 @@ export class UmbDefaultTreeElement extends UmbLitElement {
}
if (_changedProperties.has('startNode')) {
this.#treeContext!.setStartFrom(this.startNode);
this.#treeContext!.setStartNode(this.startNode);
}
if (_changedProperties.has('hideTreeRoot')) {

View File

@@ -1,2 +1,3 @@
export { UmbDefaultTreeElement as UmbTreeDefaultElement } from './default-tree.element.js';
export { UmbDefaultTreeContext as UmbTreeDefaultContext } from './default-tree.context.js';
export { UmbDefaultTreeElement } from './default-tree.element.js';
export { UmbDefaultTreeContext } from './default-tree.context.js';
export { UMB_TREE_CONTEXT } from './default-tree.context-token.js';

View File

@@ -1,8 +1,8 @@
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import { UmbEntityActionBase, UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action';
import type { MetaEntityActionFolderKind } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { UMB_FOLDER_CREATE_MODAL, UmbRequestReloadTreeItemChildrenEvent } from '@umbraco-cms/backoffice/tree';
import { UMB_FOLDER_CREATE_MODAL } from '@umbraco-cms/backoffice/tree';
export class UmbCreateFolderEntityAction extends UmbEntityActionBase<MetaEntityActionFolderKind> {
async execute() {
@@ -20,7 +20,7 @@ export class UmbCreateFolderEntityAction extends UmbEntityActionBase<MetaEntityA
await modalContext.onSubmit();
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadTreeItemChildrenEvent({
const event = new UmbRequestReloadChildrenOfEntityEvent({
entityType: this.args.entityType,
unique: this.args.unique,
});

View File

@@ -1,5 +1,3 @@
import { UmbRequestReloadTreeItemChildrenEvent } from './reload-tree-item-children/index.js';
export * from './tree-item/index.js';
export * from './default/index.js';
export * from './data/index.js';
@@ -16,9 +14,3 @@ export type { UmbTreePickerModalData, UmbTreePickerModalValue } from './tree-pic
export { UMB_TREE_PICKER_MODAL, UMB_TREE_PICKER_MODAL_ALIAS } from './tree-picker/index.js';
export * from './types.js';
/*
* @deprecated Use UmbRequestReloadTreeItemChildrenEvent instead — Will be removed before RC.
* TODO: Delete before RC.
*/
export { UmbRequestReloadTreeItemChildrenEvent as UmbReloadTreeItemChildrenRequestEntityActionEvent };

View File

@@ -1,5 +1,8 @@
import { UmbEntityActionEvent, type UmbEntityActionEventArgs } from '@umbraco-cms/backoffice/entity-action';
/**
* @deprecated Use `UmbRequestReloadChildrenOfEntityEvent` instead.
*/
export class UmbRequestReloadTreeItemChildrenEvent extends UmbEntityActionEvent {
static readonly TYPE = 'request-reload-tree-item-children';

View File

@@ -1,6 +1,5 @@
import { UmbRequestReloadTreeItemChildrenEvent } from './reload-tree-item-children-request.event.js';
import type { UmbEntityActionArgs } from '@umbraco-cms/backoffice/entity-action';
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import { UmbEntityActionBase, UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import type { MetaEntityActionReloadTreeItemChildrenKind } from '@umbraco-cms/backoffice/extension-registry';
@@ -14,7 +13,7 @@ export class UmbReloadTreeItemChildrenEntityAction extends UmbEntityActionBase<M
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
eventContext.dispatchEvent(
new UmbRequestReloadTreeItemChildrenEvent({
new UmbRequestReloadChildrenOfEntityEvent({
unique: this.args.unique,
entityType: this.args.entityType,
}),

View File

@@ -1,5 +1,5 @@
import type { UmbTreeItemContext } from '../tree-item-context.interface.js';
import { UMB_DEFAULT_TREE_CONTEXT, type UmbDefaultTreeContext } from '../../default/default-tree.context.js';
import { UMB_TREE_CONTEXT, type UmbDefaultTreeContext } from '../../default/index.js';
import type { UmbTreeItemModel, UmbTreeRootModel } from '../../types.js';
import { UmbRequestReloadTreeItemChildrenEvent } from '../../reload-tree-item-children/index.js';
import { map } from '@umbraco-cms/backoffice/external/rxjs';
@@ -12,7 +12,10 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UMB_ACTION_EVENT_CONTEXT, type UmbActionEventContext } from '@umbraco-cms/backoffice/action';
import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
import {
UmbRequestReloadChildrenOfEntityEvent,
UmbRequestReloadStructureForEntityEvent,
} from '@umbraco-cms/backoffice/entity-action';
import type { UmbEntityActionEvent } from '@umbraco-cms/backoffice/entity-action';
import { UmbPaginationManager, debounce } from '@umbraco-cms/backoffice/utils';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
@@ -172,6 +175,7 @@ export abstract class UmbTreeItemContextBase<
const skip = loadMore ? this.#paging.skip : 0;
const take = loadMore ? this.#paging.take : this.pagination.getCurrentPageNumber() * this.#paging.take;
const additionalArgs = this.treeContext?.getAdditionalRequestArgs();
const { data } = await repository.requestTreeItemsOf({
parent: {
@@ -180,6 +184,7 @@ export abstract class UmbTreeItemContextBase<
},
skip,
take,
...additionalArgs,
});
if (data) {
@@ -229,7 +234,7 @@ export abstract class UmbTreeItemContextBase<
this.#sectionSidebarContext = instance;
});
this.consumeContext(UMB_DEFAULT_TREE_CONTEXT, (treeContext) => {
this.consumeContext(UMB_TREE_CONTEXT, (treeContext) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.treeContext = treeContext;
@@ -243,6 +248,11 @@ export abstract class UmbTreeItemContextBase<
this.#onReloadRequest as EventListener,
);
this.#actionEventContext?.removeEventListener(
UmbRequestReloadChildrenOfEntityEvent.TYPE,
this.#onReloadRequest as EventListener,
);
this.#actionEventContext?.removeEventListener(
UmbRequestReloadStructureForEntityEvent.TYPE,
this.#onReloadStructureRequest as unknown as EventListener,
@@ -255,6 +265,11 @@ export abstract class UmbTreeItemContextBase<
this.#onReloadRequest as EventListener,
);
this.#actionEventContext.addEventListener(
UmbRequestReloadChildrenOfEntityEvent.TYPE,
this.#onReloadRequest as EventListener,
);
this.#actionEventContext.addEventListener(
UmbRequestReloadStructureForEntityEvent.TYPE,
this.#onReloadStructureRequest as unknown as EventListener,
@@ -382,6 +397,12 @@ export abstract class UmbTreeItemContextBase<
UmbRequestReloadTreeItemChildrenEvent.TYPE,
this.#onReloadRequest as EventListener,
);
this.#actionEventContext?.removeEventListener(
UmbRequestReloadChildrenOfEntityEvent.TYPE,
this.#onReloadRequest as EventListener,
);
this.#actionEventContext?.removeEventListener(
UmbRequestReloadStructureForEntityEvent.TYPE,
this.#onReloadStructureRequest as unknown as EventListener,

View File

@@ -67,7 +67,6 @@ export class UmbWorkspaceViewCollectionElement extends UmbLitElement implements
orderBy: config?.getValueByAlias('orderBy') ?? 'updateDate',
orderDirection: config?.getValueByAlias('orderDirection') ?? 'asc',
pageSize: Number(config?.getValueByAlias('pageSize')) ?? 50,
useInfiniteEditor: config?.getValueByAlias('useInfiniteEditor') ?? false,
userDefinedProperties: config?.getValueByAlias('includeProperties'),
};
}

View File

@@ -27,8 +27,10 @@ import type {
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_PROPERTY_EDITOR_SCHEMA_ALIAS_DEFAULT } from '@umbraco-cms/backoffice/property-editor';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UmbRequestReloadTreeItemChildrenEvent } from '@umbraco-cms/backoffice/tree';
import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
import {
UmbRequestReloadChildrenOfEntityEvent,
UmbRequestReloadStructureForEntityEvent,
} from '@umbraco-cms/backoffice/entity-action';
type EntityType = UmbDataTypeDetailModel;
export class UmbDataTypeWorkspaceContext
@@ -347,7 +349,7 @@ export class UmbDataTypeWorkspaceContext
// TODO: this might not be the right place to alert the tree, but it works for now
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadTreeItemChildrenEvent({
const event = new UmbRequestReloadChildrenOfEntityEvent({
entityType: parent.entityType,
unique: parent.unique,
});

View File

@@ -10,9 +10,11 @@ import {
} from '@umbraco-cms/backoffice/workspace';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { UmbRequestReloadTreeItemChildrenEvent } from '@umbraco-cms/backoffice/tree';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
import {
UmbRequestReloadChildrenOfEntityEvent,
UmbRequestReloadStructureForEntityEvent,
} from '@umbraco-cms/backoffice/entity-action';
export class UmbDictionaryWorkspaceContext
extends UmbSubmittableWorkspaceContextBase<UmbDictionaryDetailModel>
@@ -146,7 +148,7 @@ export class UmbDictionaryWorkspaceContext
// TODO: this might not be the right place to alert the tree, but it works for now
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadTreeItemChildrenEvent({
const event = new UmbRequestReloadChildrenOfEntityEvent({
entityType: parent.entityType,
unique: parent.unique,
});

View File

@@ -25,8 +25,10 @@ import {
UmbDocumentTypeDetailRepository,
} from '@umbraco-cms/backoffice/document-type';
import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language';
import { UmbRequestReloadTreeItemChildrenEvent } from '@umbraco-cms/backoffice/tree';
import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
import {
UmbRequestReloadChildrenOfEntityEvent,
UmbRequestReloadStructureForEntityEvent,
} from '@umbraco-cms/backoffice/entity-action';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UMB_INVARIANT_CULTURE, UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
@@ -407,7 +409,7 @@ export class UmbDocumentBlueprintWorkspaceContext
// TODO: this might not be the right place to alert the tree, but it works for now
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadTreeItemChildrenEvent({
const event = new UmbRequestReloadChildrenOfEntityEvent({
entityType: parent.entityType,
unique: parent.unique,
});

View File

@@ -11,8 +11,10 @@ import {
import { UmbDocumentTypeWorkspaceEditorElement } from './document-type-workspace-editor.element.js';
import { UmbContentTypeStructureManager } from '@umbraco-cms/backoffice/content-type';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { UmbRequestReloadTreeItemChildrenEvent } from '@umbraco-cms/backoffice/tree';
import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
import {
UmbRequestReloadChildrenOfEntityEvent,
UmbRequestReloadStructureForEntityEvent,
} from '@umbraco-cms/backoffice/entity-action';
import {
UmbSubmittableWorkspaceContextBase,
UmbWorkspaceIsNewRedirectController,
@@ -295,7 +297,7 @@ export class UmbDocumentTypeWorkspaceContext
// TODO: this might not be the right place to alert the tree, but it works for now
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadTreeItemChildrenEvent({
const event = new UmbRequestReloadChildrenOfEntityEvent({
entityType: parent.entityType,
unique: parent.unique,
});

View File

@@ -1,7 +1,7 @@
import { UMB_DOCUMENT_COLLECTION_CONTEXT } from '../document-collection.context-token.js';
import { css, customElement, html, map, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbDocumentTypeStructureRepository } from '@umbraco-cms/backoffice/document-type';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection';
import {
UMB_CREATE_DOCUMENT_WORKSPACE_PATH_PATTERN,
UMB_DOCUMENT_ENTITY_TYPE,
@@ -35,9 +35,6 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement {
@state()
private _rootPathName?: string;
@state()
private _useInfiniteEditor = false;
@property({ attribute: false })
manifest?: ManifestCollectionAction;
@@ -64,16 +61,13 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement {
});
});
this.consumeContext(UMB_COLLECTION_CONTEXT, (collectionContext) => {
this.consumeContext(UMB_DOCUMENT_COLLECTION_CONTEXT, (collectionContext) => {
this.observe(collectionContext.view.currentView, (currentView) => {
this._currentView = currentView?.meta.pathName;
});
this.observe(collectionContext.view.rootPathName, (rootPathName) => {
this._rootPathName = rootPathName;
});
this.observe(collectionContext.filter, (filter) => {
this._useInfiniteEditor = filter.useInfiniteEditor == true;
});
});
}
@@ -99,22 +93,14 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement {
}
#getCreateUrl(item: UmbAllowedDocumentTypeModel) {
if (this._useInfiniteEditor) {
return (
this._createDocumentPath.replace(`${this._rootPathName}`, `${this._rootPathName}/${this._currentView}`) +
UMB_CREATE_DOCUMENT_WORKSPACE_PATH_PATTERN.generateLocal({
parentEntityType: this._documentUnique ? UMB_DOCUMENT_ENTITY_TYPE : UMB_DOCUMENT_ROOT_ENTITY_TYPE,
parentUnique: this._documentUnique ?? 'null',
documentTypeUnique: item.unique,
})
);
}
return UMB_CREATE_DOCUMENT_WORKSPACE_PATH_PATTERN.generateAbsolute({
parentEntityType: this._documentUnique ? UMB_DOCUMENT_ENTITY_TYPE : UMB_DOCUMENT_ROOT_ENTITY_TYPE,
parentUnique: this._documentUnique ?? 'null',
documentTypeUnique: item.unique,
});
return (
this._createDocumentPath.replace(`${this._rootPathName}`, `${this._rootPathName}/${this._currentView}`) +
UMB_CREATE_DOCUMENT_WORKSPACE_PATH_PATTERN.generateLocal({
parentEntityType: this._documentUnique ? UMB_DOCUMENT_ENTITY_TYPE : UMB_DOCUMENT_ROOT_ENTITY_TYPE,
parentUnique: this._documentUnique ?? 'null',
documentTypeUnique: item.unique,
})
);
}
render() {

View File

@@ -0,0 +1,6 @@
import type { UmbDocumentCollectionContext } from './document-collection.context.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
export const UMB_DOCUMENT_COLLECTION_CONTEXT = new UmbContextToken<UmbDocumentCollectionContext>(
'UmbCollectionContext',
);

View File

@@ -2,11 +2,11 @@ import { getPropertyValueByAlias } from '../index.js';
import { UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN } from '../../../paths.js';
import type { UmbCollectionColumnConfiguration } from '../../../../../core/collection/types.js';
import type { UmbDocumentCollectionFilterModel, UmbDocumentCollectionItemModel } from '../../types.js';
import { UMB_DOCUMENT_COLLECTION_CONTEXT } from '../../document-collection.context-token.js';
import { css, customElement, html, nothing, repeat, state, when } from '@umbraco-cms/backoffice/external/lit';
import { fromCamelCase } from '@umbraco-cms/backoffice/utils';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection';
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection';
import type { UUIInterfaceColor } from '@umbraco-cms/backoffice/external/uui';
@@ -33,7 +33,7 @@ export class UmbDocumentGridCollectionViewElement extends UmbLitElement {
constructor() {
super();
this.consumeContext(UMB_COLLECTION_CONTEXT, (collectionContext) => {
this.consumeContext(UMB_DOCUMENT_COLLECTION_CONTEXT, (collectionContext) => {
this.#collectionContext = collectionContext;
this.#observeCollectionContext();
});

View File

@@ -3,10 +3,10 @@ import { UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN } from '../../../paths.js';
import type { UmbCollectionColumnConfiguration } from '../../../../../core/collection/types.js';
import type { UmbDocumentCollectionItemModel } from '../../types.js';
import type { UmbDocumentCollectionContext } from '../../document-collection.context.js';
import { UMB_DOCUMENT_COLLECTION_CONTEXT } from '../../document-collection.context-token.js';
import { css, customElement, html, nothing, state, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection';
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/modal';
import type {
@@ -68,7 +68,7 @@ export class UmbDocumentTableCollectionViewElement extends UmbLitElement {
constructor() {
super();
this.consumeContext(UMB_COLLECTION_CONTEXT, (collectionContext) => {
this.consumeContext(UMB_DOCUMENT_COLLECTION_CONTEXT, (collectionContext) => {
this.#collectionContext = collectionContext;
});

View File

@@ -90,9 +90,6 @@ export class UmbInputDocumentElement extends UUIFormControlMixin(UmbLitElement,
@property({ type: Boolean })
showOpenButton?: boolean;
@property({ type: Boolean })
ignoreUserStartNodes?: boolean;
@property()
public set value(idsString: string) {
this.selection = splitStringToArray(idsString);
@@ -153,7 +150,6 @@ export class UmbInputDocumentElement extends UUIFormControlMixin(UmbLitElement,
};
#openPicker() {
// TODO: Configure the content picker, with `startNodeId` and `ignoreUserStartNodes` [LK]
this.#pickerContext.openPicker({
hideTreeRoot: true,
pickableFilter: this.#pickableFilter,

View File

@@ -3,10 +3,11 @@ import { UmbDocumentDetailRepository, UmbDocumentPublishingRepository } from '..
import type { UmbDocumentVariantOptionModel } from '../types.js';
import { UMB_APP_LANGUAGE_CONTEXT, UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language';
import type { UmbEntityActionArgs } from '@umbraco-cms/backoffice/entity-action';
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import { UmbEntityActionBase, UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/entity-action';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
export class UmbPublishDocumentEntityAction extends UmbEntityActionBase<never> {
constructor(host: UmbControllerHost, args: UmbEntityActionArgs<never>) {
@@ -44,11 +45,18 @@ export class UmbPublishDocumentEntityAction extends UmbEntityActionBase<never> {
}),
);
const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadStructureForEntityEvent({
unique: this.args.unique,
entityType: this.args.entityType,
});
// If the document has only one variant, we can skip the modal and publish directly:
if (options.length === 1) {
const variantId = UmbVariantId.Create(documentData.variants[0]);
const publishingRepository = new UmbDocumentPublishingRepository(this._host);
await publishingRepository.publish(this.args.unique, [{ variantId }]);
actionEventContext.dispatchEvent(event);
return;
}
@@ -84,6 +92,7 @@ export class UmbPublishDocumentEntityAction extends UmbEntityActionBase<never> {
this.args.unique,
variantIds.map((variantId) => ({ variantId })),
);
actionEventContext.dispatchEvent(event);
}
}
}

View File

@@ -2,10 +2,15 @@ import { UmbDocumentDetailRepository, UmbDocumentPublishingRepository } from '..
import type { UmbDocumentVariantOptionModel } from '../types.js';
import { UMB_DOCUMENT_UNPUBLISH_MODAL } from '../modals/index.js';
import { UMB_APP_LANGUAGE_CONTEXT, UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language';
import { type UmbEntityActionArgs, UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import {
type UmbEntityActionArgs,
UmbEntityActionBase,
UmbRequestReloadStructureForEntityEvent,
} from '@umbraco-cms/backoffice/entity-action';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
export class UmbUnpublishDocumentEntityAction extends UmbEntityActionBase<never> {
constructor(host: UmbControllerHost, args: UmbEntityActionArgs<never>) {
@@ -73,6 +78,14 @@ export class UmbUnpublishDocumentEntityAction extends UmbEntityActionBase<never>
if (variantIds.length) {
const publishingRepository = new UmbDocumentPublishingRepository(this._host);
await publishingRepository.unpublish(this.args.unique, variantIds);
const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadStructureForEntityEvent({
unique: this.args.unique,
entityType: this.args.entityType,
});
actionEventContext.dispatchEvent(event);
}
}
}

View File

@@ -8,9 +8,19 @@ import { UMB_APP_LANGUAGE_CONTEXT, UmbLanguageCollectionRepository } from '@umbr
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import { UMB_CONFIRM_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action';
export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase<object> {
async execute() {
const entityContext = await this.getContext(UMB_ENTITY_CONTEXT);
const entityType = entityContext.getEntityType();
const unique = entityContext.getUnique();
if (!entityType) throw new Error('Entity type not found');
if (unique === undefined) throw new Error('Entity unique not found');
// If there is only one selection, we can refer to the regular publish entity action:
if (this.selection.length === 1) {
const action = new UmbPublishDocumentEntityAction(this._host, {
@@ -43,6 +53,12 @@ export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase<
const modalManagerContext = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadChildrenOfEntityEvent({
entityType,
unique,
});
// If there is only one language available, we can skip the modal and publish directly:
if (options.length === 1) {
const localizationController = new UmbLocalizationController(this._host);
@@ -62,6 +78,7 @@ export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase<
const variantId = new UmbVariantId(options[0].language.unique, null);
const publishingRepository = new UmbDocumentPublishingRepository(this._host);
await publishingRepository.unpublish(this.selection[0], [variantId]);
eventContext.dispatchEvent(event);
}
return;
}
@@ -98,6 +115,7 @@ export class UmbDocumentPublishEntityBulkAction extends UmbEntityBulkActionBase<
unique,
variantIds.map((variantId) => ({ variantId })),
);
eventContext.dispatchEvent(event);
}
}
}

View File

@@ -8,9 +8,19 @@ import { UmbEntityBulkActionBase } from '@umbraco-cms/backoffice/entity-bulk-act
import { UMB_APP_LANGUAGE_CONTEXT, UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity';
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action';
export class UmbDocumentUnpublishEntityBulkAction extends UmbEntityBulkActionBase<object> {
async execute() {
const entityContext = await this.getContext(UMB_ENTITY_CONTEXT);
const entityType = entityContext.getEntityType();
const unique = entityContext.getUnique();
if (!entityType) throw new Error('Entity type not found');
if (unique === undefined) throw new Error('Entity unique not found');
// If there is only one selection, we can refer to the regular unpublish entity action:
if (this.selection.length === 1) {
const action = new UmbUnpublishDocumentEntityAction(this._host, {
@@ -43,6 +53,12 @@ export class UmbDocumentUnpublishEntityBulkAction extends UmbEntityBulkActionBas
const modalManagerContext = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);
const event = new UmbRequestReloadChildrenOfEntityEvent({
entityType,
unique,
});
// If there is only one language available, we can skip the modal and unpublish directly:
if (options.length === 1) {
const localizationController = new UmbLocalizationController(this._host);
@@ -62,6 +78,7 @@ export class UmbDocumentUnpublishEntityBulkAction extends UmbEntityBulkActionBas
const variantId = new UmbVariantId(options[0].language.unique, null);
const publishingRepository = new UmbDocumentPublishingRepository(this._host);
await publishingRepository.unpublish(this.selection[0], [variantId]);
eventContext.dispatchEvent(event);
}
return;
}
@@ -95,6 +112,7 @@ export class UmbDocumentUnpublishEntityBulkAction extends UmbEntityBulkActionBas
if (variantIds.length) {
for (const unique of this.selection) {
await repository.unpublish(unique, variantIds);
eventContext.dispatchEvent(event);
}
}
}

View File

@@ -274,7 +274,9 @@ export class UmbRollbackModalElement extends UmbModalBaseElement<UmbRollbackModa
get currentVersionHeader() {
return (
this.localize.date(this.currentVersion?.date || '', this.#localizeDateOptions) + ' - ' + this.currentVersion?.user
this.localize.date(this.currentVersion?.date ?? new Date(), this.#localizeDateOptions) +
' - ' +
this.currentVersion?.user
);
}

View File

@@ -22,7 +22,6 @@ export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement impl
this._max = minMax.max && minMax.max > 0 ? minMax.max : Infinity;
}
this._ignoreUserStartNodes = config.getValueByAlias('ignoreUserStartNodes') ?? false;
this._startNodeId = config.getValueByAlias('startNodeId');
this._showOpenButton = config.getValueByAlias('showOpenButton') ?? false;
}
@@ -39,9 +38,6 @@ export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement impl
@state()
private _showOpenButton?: boolean;
@state()
private _ignoreUserStartNodes?: boolean;
#onChange(event: CustomEvent & { target: UmbInputDocumentElement }) {
this.value = event.target.selection.join(',');
this.dispatchEvent(new UmbPropertyValueChangeEvent());
@@ -58,7 +54,6 @@ export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement impl
.max=${this._max}
.startNode=${startNode}
.value=${this.value ?? ''}
?ignoreUserStartNodes=${this._ignoreUserStartNodes}
?showOpenButton=${this._showOpenButton}
@change=${this.#onChange}>
</umb-input-document>

Some files were not shown because too many files have changed in this diff Show More