Feature/installed and created packages (#545)

* multiple selection in modalhandler is ok?

* installed view updates

* created view updates

* workspace for package builder

* package builder

* all pickers in workspace

* use input-picker rather than property editor ui

* installed package view

* ui updates

* packageview

* update handlers

* update package views with migrations

* update backend api

* endpoints

* migration & language picker

* small update

* seperate migrations that doesnt belong to a packag

* rename NotificationContext

* filter out packages that have no name before they go in to the store

---------

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
This commit is contained in:
Lone Iversen
2023-03-02 11:27:06 +01:00
committed by GitHub
parent b81d1e2e33
commit ce5f2d7182
26 changed files with 1020 additions and 97 deletions

View File

@@ -6,6 +6,10 @@ export { CancelablePromise, CancelError } from './core/CancelablePromise';
export { OpenAPI } from './core/OpenAPI';
export type { OpenAPIConfig } from './core/OpenAPI';
export type { AuditLogBaseModel } from './models/AuditLogBaseModel';
export type { AuditLogResponseModel } from './models/AuditLogResponseModel';
export type { AuditLogWithUsernameResponseModel } from './models/AuditLogWithUsernameResponseModel';
export { AuditTypeModel } from './models/AuditTypeModel';
export type { ConsentLevelModel } from './models/ConsentLevelModel';
export { ContentStateModel } from './models/ContentStateModel';
export type { ContentTreeItemModel } from './models/ContentTreeItemModel';
@@ -74,6 +78,7 @@ export type { LanguageModel } from './models/LanguageModel';
export type { LanguageModelBaseModel } from './models/LanguageModelBaseModel';
export type { LanguageUpdateModel } from './models/LanguageUpdateModel';
export type { LoggerModel } from './models/LoggerModel';
export type { LogLevelCountsModel } from './models/LogLevelCountsModel';
export { LogLevelModel } from './models/LogLevelModel';
export type { LogMessageModel } from './models/LogMessageModel';
export type { LogMessagePropertyModel } from './models/LogMessagePropertyModel';
@@ -84,7 +89,13 @@ export type { OkResultModel } from './models/OkResultModel';
export { OperatorModel } from './models/OperatorModel';
export type { OutOfDateStatusModel } from './models/OutOfDateStatusModel';
export { OutOfDateTypeModel } from './models/OutOfDateTypeModel';
export type { PackageCreateModel } from './models/PackageCreateModel';
export type { PackageDefinitionModel } from './models/PackageDefinitionModel';
export type { PackageMigrationStatusModel } from './models/PackageMigrationStatusModel';
export type { PackageModelBaseModel } from './models/PackageModelBaseModel';
export type { PackageUpdateModel } from './models/PackageUpdateModel';
export type { PagedAuditLogResponseModel } from './models/PagedAuditLogResponseModel';
export type { PagedAuditLogWithUsernameResponseModel } from './models/PagedAuditLogWithUsernameResponseModel';
export type { PagedContentTreeItemModel } from './models/PagedContentTreeItemModel';
export type { PagedCultureModel } from './models/PagedCultureModel';
export type { PagedDictionaryOverviewModel } from './models/PagedDictionaryOverviewModel';
@@ -101,6 +112,7 @@ export type { PagedLanguageModel } from './models/PagedLanguageModel';
export type { PagedLoggerModel } from './models/PagedLoggerModel';
export type { PagedLogMessageModel } from './models/PagedLogMessageModel';
export type { PagedLogTemplateModel } from './models/PagedLogTemplateModel';
export type { PagedPackageDefinitionModel } from './models/PagedPackageDefinitionModel';
export type { PagedPackageMigrationStatusModel } from './models/PagedPackageMigrationStatusModel';
export type { PagedRecycleBinItemModel } from './models/PagedRecycleBinItemModel';
export type { PagedRedirectUrlModel } from './models/PagedRedirectUrlModel';
@@ -157,6 +169,7 @@ export type { ValueViewModelBaseModel } from './models/ValueViewModelBaseModel';
export type { VariantViewModelBaseModel } from './models/VariantViewModelBaseModel';
export type { VersionModel } from './models/VersionModel';
export { AuditLogResource } from './services/AuditLogResource';
export { CultureResource } from './services/CultureResource';
export { DataTypeResource } from './services/DataTypeResource';
export { DictionaryResource } from './services/DictionaryResource';

View File

@@ -0,0 +1,16 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { AuditTypeModel } from './AuditTypeModel';
export type AuditLogBaseModel = {
userKey?: string;
entityKey?: string | null;
timestamp?: string;
logType?: AuditTypeModel;
entityType?: string | null;
comment?: string | null;
parameters?: string | null;
};

View File

@@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { AuditLogBaseModel } from './AuditLogBaseModel';
export type AuditLogResponseModel = AuditLogBaseModel;

View File

@@ -0,0 +1,11 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { AuditLogBaseModel } from './AuditLogBaseModel';
export type AuditLogWithUsernameResponseModel = (AuditLogBaseModel & {
userName?: string | null;
userAvatars?: Array<string> | null;
});

View File

@@ -0,0 +1,30 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export enum AuditTypeModel {
NEW = 'New',
SAVE = 'Save',
SAVE_VARIANT = 'SaveVariant',
OPEN = 'Open',
DELETE = 'Delete',
PUBLISH = 'Publish',
PUBLISH_VARIANT = 'PublishVariant',
SEND_TO_PUBLISH = 'SendToPublish',
SEND_TO_PUBLISH_VARIANT = 'SendToPublishVariant',
UNPUBLISH = 'Unpublish',
UNPUBLISH_VARIANT = 'UnpublishVariant',
MOVE = 'Move',
COPY = 'Copy',
ASSIGN_DOMAIN = 'AssignDomain',
PUBLIC_ACCESS = 'PublicAccess',
SORT = 'Sort',
NOTIFY = 'Notify',
SYSTEM = 'System',
ROLL_BACK = 'RollBack',
PACKAGER_INSTALL = 'PackagerInstall',
PACKAGER_UNINSTALL = 'PackagerUninstall',
CUSTOM = 'Custom',
CONTENT_VERSION_PREVENT_CLEANUP = 'ContentVersionPreventCleanup',
CONTENT_VERSION_ENABLE_CLEANUP = 'ContentVersionEnableCleanup',
}

View File

@@ -0,0 +1,12 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type LogLevelCountsModel = {
information?: number;
debug?: number;
warning?: number;
error?: number;
fatal?: number;
};

View File

@@ -0,0 +1,11 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { AuditLogResponseModel } from './AuditLogResponseModel';
export type PagedAuditLogResponseModel = {
total: number;
items: Array<AuditLogResponseModel>;
};

View File

@@ -0,0 +1,11 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { AuditLogWithUsernameResponseModel } from './AuditLogWithUsernameResponseModel';
export type PagedAuditLogWithUsernameResponseModel = {
total: number;
items: Array<AuditLogWithUsernameResponseModel>;
};

View File

@@ -0,0 +1,103 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { AuditTypeModel } from '../models/AuditTypeModel';
import type { DirectionModel } from '../models/DirectionModel';
import type { PagedAuditLogResponseModel } from '../models/PagedAuditLogResponseModel';
import type { PagedAuditLogWithUsernameResponseModel } from '../models/PagedAuditLogWithUsernameResponseModel';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class AuditLogResource {
/**
* @returns PagedAuditLogWithUsernameResponseModel Success
* @throws ApiError
*/
public static getAuditLog({
orderDirection,
sinceDate,
skip,
take = 100,
}: {
orderDirection?: DirectionModel,
sinceDate?: string,
skip?: number,
take?: number,
}): CancelablePromise<PagedAuditLogWithUsernameResponseModel> {
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/audit-log',
query: {
'orderDirection': orderDirection,
'sinceDate': sinceDate,
'skip': skip,
'take': take,
},
});
}
/**
* @returns PagedAuditLogResponseModel Success
* @throws ApiError
*/
public static getAuditLogByKey({
key,
orderDirection,
sinceDate,
skip,
take = 100,
}: {
key: string,
orderDirection?: DirectionModel,
sinceDate?: string,
skip?: number,
take?: number,
}): CancelablePromise<PagedAuditLogResponseModel> {
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/audit-log/{key}',
path: {
'key': key,
},
query: {
'orderDirection': orderDirection,
'sinceDate': sinceDate,
'skip': skip,
'take': take,
},
});
}
/**
* @returns PagedAuditLogResponseModel Success
* @throws ApiError
*/
public static getAuditLogTypeByLogType({
logType,
sinceDate,
skip,
take = 100,
}: {
logType: AuditTypeModel,
sinceDate?: string,
skip?: number,
take?: number,
}): CancelablePromise<PagedAuditLogResponseModel> {
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/audit-log/type/{logType}',
path: {
'logType': logType,
},
query: {
'sinceDate': sinceDate,
'skip': skip,
'take': take,
},
});
}
}

View File

@@ -2,6 +2,7 @@
/* tslint:disable */
/* eslint-disable */
import type { DirectionModel } from '../models/DirectionModel';
import type { LogLevelCountsModel } from '../models/LogLevelCountsModel';
import type { LogLevelModel } from '../models/LogLevelModel';
import type { PagedLoggerModel } from '../models/PagedLoggerModel';
import type { PagedLogMessageModel } from '../models/PagedLogMessageModel';
@@ -46,7 +47,7 @@ export class LogViewerResource {
}: {
startDate?: string,
endDate?: string,
}): CancelablePromise<any> {
}): CancelablePromise<LogLevelCountsModel> {
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/log-viewer/level-count',

View File

@@ -20,4 +20,21 @@ export class ProfilingResource {
});
}
/**
* @returns any Success
* @throws ApiError
*/
public static putProfilingStatus({
requestBody,
}: {
requestBody?: ProfilingStatusModel,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'PUT',
url: '/umbraco/management/api/v1/profiling/status',
body: requestBody,
mediaType: 'application/json',
});
}
}

View File

@@ -46,7 +46,7 @@ export class TrackedReferenceResource {
parentKey,
skip,
take,
filterMustBeIsDependency,
filterMustBeIsDependency = true,
}: {
parentKey: string,
skip?: number,

View File

@@ -159,3 +159,7 @@ export type UmbPackage = {
};
export type PagedManifestsResponse = UmbPackage[];
export type UmbPackageWithMigrationStatus = UmbPackage & {
hasPendingMigrations: boolean;
};

View File

@@ -45,9 +45,9 @@ import {
} from './settings/languages/app-language-select/app-language.context';
import { UmbPackageStore } from './packages/repository/package.store';
import { UmbServerExtensionController } from './packages/repository/server-extension.controller';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api';
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/notification';
import { UmbLitElement } from '@umbraco-cms/element';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api';
import '@umbraco-cms/router';

View File

@@ -1,24 +1,295 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { UUIBooleanInputEvent, UUIInputElement, UUIInputEvent } from '@umbraco-ui/uui';
import { css, html, nothing } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { ifDefined } from 'lit-html/directives/if-defined.js';
import { UmbInputDocumentPickerElement } from '../../../shared/components/input-document-picker/input-document-picker.element';
import { UmbInputMediaPickerElement } from '../../../shared/components/input-media-picker/input-media-picker.element';
import { UmbInputLanguagePickerElement } from '../../../shared/components/input-language-picker/input-language-picker.element';
import { UmbLitElement } from '@umbraco-cms/element';
import { PackageDefinitionModel, PackageResource } from '@umbraco-cms/backend-api';
import { tryExecuteAndNotify } from '@umbraco-cms/resources';
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/notification';
@customElement('umb-workspace-package-builder')
export class UmbWorkspacePackageBuilderElement extends LitElement {
export class UmbWorkspacePackageBuilderElement extends UmbLitElement {
static styles = [
UUITextStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
}
.header {
margin: 0 var(--uui-size-layout-1);
display: flex;
gap: var(--uui-size-space-4);
}
uui-box {
margin: var(--uui-size-layout-1);
}
uui-checkbox {
margin-top: var(--uui-size-space-4);
}
`,
];
@property()
entityKey?: string;
@state()
private _package: PackageDefinitionModel = {};
@query('#package-name-input')
private _packageNameInput!: UUIInputElement;
private _notificationContext?: UmbNotificationContext;
constructor() {
super();
this.consumeContext(UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => {
this._notificationContext = instance;
});
}
connectedCallback(): void {
super.connectedCallback();
if (this.entityKey) this.#getPackageCreated();
}
async #getPackageCreated() {
if (!this.entityKey) return;
const { data } = await tryExecuteAndNotify(this, PackageResource.getPackageCreatedByKey({ key: this.entityKey }));
if (!data) return;
this._package = data as PackageDefinitionModel;
}
async #download() {
if (!this._package?.key) return;
const response = await tryExecuteAndNotify(
this,
PackageResource.getPackageCreatedByKeyDownload({ key: this._package.key })
);
}
#nameDefined() {
const valid = this._packageNameInput.checkValidity();
if (!valid) this._notificationContext?.peek('danger', { data: { message: 'Package missing a name' } });
return valid;
}
async #save() {
if (!this.#nameDefined()) return;
const response = await tryExecuteAndNotify(
this,
PackageResource.postPackageCreated({ requestBody: this._package })
);
if (!response.data || response.error) return;
this._package = response.data as PackageDefinitionModel;
this.#navigateBack();
}
async #update() {
if (!this.#nameDefined()) return;
if (!this._package?.key) return;
const response = await tryExecuteAndNotify(
this,
PackageResource.putPackageCreatedByKey({ key: this._package.key, requestBody: this._package })
);
if (response.error) return;
this.#navigateBack();
}
#navigateBack() {
window.history.pushState({}, '', '/section/packages/view/created');
}
render() {
return html`<umb-workspace-layout alias="Umb.Workspace.PackageBuilder"
>PACKAGE BUILDER</umb-workspace-layout
> `;
return html`
<umb-workspace-layout alias="Umb.Workspace.PackageBuilder">
${this.#renderHeader()}
<uui-box class="wrapper" headline="Package Content"> ${this.#renderEditors()} </uui-box>
${this.#renderActions()}
</umb-workspace-layout>
`;
}
#renderHeader() {
return html`<div class="header" slot="header">
<uui-button compact @click="${this.#navigateBack}" label="Back to created package overview">
<uui-icon name="umb:arrow-left"></uui-icon>
</uui-button>
<uui-input
required
id="package-name-input"
label="Name of the package"
placeholder="Enter a name"
value="${ifDefined(this._package?.name)}"
@change="${(e: UUIInputEvent) => (this._package.name = e.target.value as string)}"></uui-input>
</div>`;
}
#renderActions() {
return html`<div slot="actions">
${this._package?.key
? html`<uui-button @click="${this.#download}" color="" look="secondary" label="Download package">
Download
</uui-button>`
: nothing}
<uui-button
@click="${this._package.key ? this.#update : this.#save}"
color="positive"
look="primary"
label="Save changes to package">
Save
</uui-button>
</div>`;
}
#renderEditors() {
return html`<umb-workspace-property-layout label="Content" description="">
${this.#renderContentSection()}
</umb-workspace-property-layout>
<umb-workspace-property-layout label="Media" description=""
>${this.#renderMediaSection()}
</umb-workspace-property-layout>
<umb-workspace-property-layout label="Document Types" description="">
${this.#renderDocumentTypeSection()}
</umb-workspace-property-layout>
<umb-workspace-property-layout label="Media Types" description="">
${this.#renderMediaTypeSection()}
</umb-workspace-property-layout>
<umb-workspace-property-layout label="Languages" description="">
${this.#renderLanguageSection()}
</umb-workspace-property-layout>
<umb-workspace-property-layout label="Dictionary" description="">
${this.#renderDictionarySection()}
</umb-workspace-property-layout>
<umb-workspace-property-layout label="Data Types" description="">
${this.#renderDataTypeSection()}
</umb-workspace-property-layout>
<umb-workspace-property-layout label="Templates" description="">
${this.#renderTemplateSection()}
</umb-workspace-property-layout>
<umb-workspace-property-layout label="Stylesheets" description="">
${this.#renderStylesheetsSection()}
</umb-workspace-property-layout>
<umb-workspace-property-layout label="Scripts" description="">
${this.#renderScriptsSection()}
</umb-workspace-property-layout>
<umb-workspace-property-layout label="Partial Views" description="">
${this.#renderPartialViewSection()}
</umb-workspace-property-layout>`;
}
#renderContentSection() {
return html`
<div slot="editor">
<umb-input-document-picker
.value=${this._package.contentNodeId ?? ''}
max="1"
@change="${(e: CustomEvent) =>
(this._package.contentNodeId = (e.target as UmbInputDocumentPickerElement).selectedKeys[0])}">
</umb-input-document-picker>
<uui-checkbox
label="Include child nodes"
.checked="${this._package.contentLoadChildNodes ?? false}"
@change="${(e: UUIBooleanInputEvent) => (this._package.contentLoadChildNodes = e.target.checked)}">
Include child nodes
</uui-checkbox>
</div>
`;
}
#renderMediaSection() {
return html`
<div slot="editor">
<umb-input-media-picker
.selectedKeys=${this._package.mediaKeys ?? []}
@change="${(e: CustomEvent) =>
(this._package.mediaKeys = (
e.target as UmbInputMediaPickerElement
).selectedKeys)}"></umb-input-media-picker>
<uui-checkbox
label="Include child nodes"
.checked="${this._package.mediaLoadChildNodes ?? false}"
@change="${(e: UUIBooleanInputEvent) => (this._package.mediaLoadChildNodes = e.target.checked)}">
Include child nodes
</uui-checkbox>
</div>
`;
}
#renderDocumentTypeSection() {
return html`<div slot="editor">
<umb-input-checkbox-list></umb-input-checkbox-list>
</div>`;
}
#renderMediaTypeSection() {
return html`<div slot="editor">
<umb-input-checkbox-list></umb-input-checkbox-list>
</div>`;
}
#renderLanguageSection() {
return html`<div slot="editor">
<umb-input-language-picker
.value="${this._package.languages?.join(',') ?? ''}"
@change="${(e: CustomEvent) => {
this._package.languages = (e.target as UmbInputLanguagePickerElement).selectedIsoCodes;
}}"></umb-input-language-picker>
</div>`;
}
#renderDictionarySection() {
return html`<div slot="editor">
<umb-input-checkbox-list></umb-input-checkbox-list>
</div>`;
}
#renderDataTypeSection() {
return html`<div slot="editor">
<umb-input-checkbox-list></umb-input-checkbox-list>
</div>`;
}
#renderTemplateSection() {
return html`<div slot="editor">
<umb-input-checkbox-list></umb-input-checkbox-list>
</div>`;
}
#renderStylesheetsSection() {
return html`<div slot="editor">
<umb-input-checkbox-list></umb-input-checkbox-list>
</div>`;
}
#renderScriptsSection() {
return html`<div slot="editor">
<umb-input-checkbox-list></umb-input-checkbox-list>
</div>`;
}
#renderPartialViewSection() {
return html`<div slot="editor">
<umb-input-checkbox-list></umb-input-checkbox-list>
</div>`;
}
}

View File

@@ -1,22 +1,55 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { customElement, property, state } from 'lit/decorators.js';
import { path, stripSlash } from 'router-slot';
@customElement('umb-workspace-package')
export class UmbWorkspacePackageElement extends LitElement {
static styles = [
UUITextStyles,
css`
:host {
display: block;
width: 100%;
height: 100%;
.header {
display: flex;
font-size: var(--uui-type-h5-size);
}
`,
];
@property()
entityKey?: string;
@state()
_package?: any;
connectedCallback(): void {
super.connectedCallback();
if (this.entityKey) this._getPackageData();
}
private _getPackageData() {
//TODO
this._package = {
key: this.entityKey,
name: 'A created package',
};
}
private _navigateBack() {
window.history.pushState({}, '', '/section/packages/view/installed');
}
private _renderHeader() {
return html`<div class="header" slot="header">
<uui-button compact @click="${this._navigateBack}">
<uui-icon name="umb:arrow-left"></uui-icon>
${this._package.name ?? 'Package name'}
</uui-button>
</div>`;
}
render() {
return html`<umb-workspace-layout alias="Umb.Workspace.Package">PACKAGE Workspace</umb-workspace-layout> `;
return html`<umb-workspace-layout alias="Umb.Workspace.Package"> ${this._renderHeader()} </umb-workspace-layout> `;
}
}

View File

@@ -1,8 +1,8 @@
import { html } from 'lit';
import { css, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { IRoute, IRoutingInfo } from 'router-slot';
import type { ManifestWorkspace } from '@umbraco-cms/models';
import { createExtensionElement , umbExtensionsRegistry } from '@umbraco-cms/extensions-api';
import type { ManifestTree, ManifestWorkspace } from '@umbraco-cms/models';
import { createExtensionElement, umbExtensionsRegistry } from '@umbraco-cms/extensions-api';
import { UmbLitElement } from '@umbraco-cms/element';
@customElement('umb-created-packages-section-view')
@@ -12,9 +12,10 @@ export class UmbCreatedPackagesSectionViewElement extends UmbLitElement {
private _workspaces: Array<ManifestWorkspace> = [];
private _trees: Array<ManifestTree> = [];
constructor() {
super();
this.observe(umbExtensionsRegistry?.extensionsOfType('workspace'), (workspaceExtensions) => {
this._workspaces = workspaceExtensions;
this._createRoutes();
@@ -48,7 +49,7 @@ export class UmbCreatedPackagesSectionViewElement extends UmbLitElement {
routes.push({
path: '**',
redirectTo: 'section/packages/view/created/overview', //TODO: this should be dynamic
redirectTo: 'overview',
});
this._routes = routes;
}

View File

@@ -1,27 +0,0 @@
import { html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
@customElement('umb-packages-created-item')
export class UmbPackagesCreatedItem extends LitElement {
@property({ type: Object })
package!: any;
render() {
return html`
<uui-ref-node-package
name=${this.package.name}
version=${this.package.version}
@open=${this._onClick}></uui-ref-node-package>
`;
}
private _onClick() {
window.history.pushState({}, '', `/section/packages/view/created/packageBuilder/${this.package.key}`);
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-packages-created-item': UmbPackagesCreatedItem;
}
}

View File

@@ -1,35 +1,158 @@
import { html, LitElement } from 'lit';
import { html, css, nothing } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
import { customElement, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import './packages-created-item.element';
import { UUIPaginationEvent } from '@umbraco-ui/uui';
import { PackageDefinitionModel, PackageResource } from '@umbraco-cms/backend-api';
import { UmbLitElement } from '@umbraco-cms/element';
import { tryExecuteAndNotify } from '@umbraco-cms/resources';
import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal';
@customElement('umb-packages-created-overview')
export class UmbPackagesCreatedOverviewElement extends LitElement {
// TODO: implement call to backend
// TODO: add correct model for created packages
@state()
private _createdPackages: any[] = [
{
alias: 'my.package',
key: '2a0181ec-244b-4068-a1d7-2f95ed7e6da6',
name: 'A created package',
plans: [],
version: '1.0.0',
},
export class UmbPackagesCreatedOverviewElement extends UmbLitElement {
static styles = [
css`
:host {
display: block;
margin: var(--uui-size-layout-1);
}
uui-box {
margin: var(--uui-size-space-5) 0;
padding-bottom: var(--uui-size-space-1);
}
.no-packages {
display: flex;
justify-content: space-around;
}
uui-pagination {
display: inline-block;
}
.pagination,
.loading {
display: flex;
justify-content: center;
}
`,
];
private take = 20;
@state()
private _loading = true;
@state()
private _createdPackages: PackageDefinitionModel[] = [];
@state()
private _currentPage = 1;
@state()
private _total?: number;
private _modalContext?: UmbModalContext;
constructor() {
super();
}
connectedCallback(): void {
super.connectedCallback();
this.#getPackages();
this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => {
this._modalContext = instance;
});
}
async #getPackages() {
const skip = this._currentPage * this.take - this.take;
const { data } = await tryExecuteAndNotify(this, PackageResource.getPackageCreated({ skip, take: this.take }));
if (data) {
this._total = data.total;
this._createdPackages = data.items;
}
this._loading = false;
}
#createNewPackage() {
window.history.pushState({}, '', `/section/packages/view/created/package-builder`);
}
render() {
return html`<uui-box headline="Created packages">
return html`<uui-button look="primary" @click="${this.#createNewPackage}" label="Create package">
Create package
</uui-button>
${this._loading ? html`<uui-loader class="loading"></uui-loader>` : this.#renderCreatedPackages()}
${this.#renderPagination()}`;
}
#renderCreatedPackages() {
if (!this._createdPackages.length) return html`<h2 class="no-packages">No packages have been created yet</h2>`;
return html`<uui-box headline="Created packages" style="--uui-box-default-padding:0;">
<uui-ref-list>
${repeat(
this._createdPackages,
(item) => item.key,
(item) => html`<umb-packages-created-item .package=${item}></umb-packages-created-item>`
(item) => this.#renderPackageItem(item)
)}
</uui-ref-list>
</uui-box>`;
}
#renderPackageItem(p: PackageDefinitionModel) {
return html`<uui-ref-node-package name=${ifDefined(p.name)} @open="${() => this.#packageBuilder(p)}">
<uui-action-bar slot="actions">
<uui-button @click=${() => this.#deletePackage(p)} label="Delete package">
<uui-icon name="delete"></uui-icon>
</uui-button>
</uui-action-bar>
</uui-ref-node-package>`;
}
#packageBuilder(p: PackageDefinitionModel) {
if (!p.key) return;
window.history.pushState({}, '', `/section/packages/view/created/package-builder/${p.key}`);
}
#renderPagination() {
if (!this._total) return nothing;
const totalPages = Math.ceil(this._total / this.take);
if (totalPages <= 1) return nothing;
return html`<div class="pagination">
<uui-pagination .total="${totalPages}" @change="${this.#onPageChange}"></uui-pagination>
</div>`;
}
#onPageChange(event: UUIPaginationEvent) {
if (this._currentPage === event.target.current) return;
this._currentPage = event.target.current;
this.#getPackages();
}
async #deletePackage(p: PackageDefinitionModel) {
if (!p.key) return;
const modalHandler = this._modalContext?.confirm({
color: 'danger',
headline: `Remove ${p.name}?`,
content: 'Are you sure you want to delete this package',
confirmLabel: 'Delete',
});
const deleteConfirmed = await modalHandler?.onClose().then(({ confirmed }: any) => {
return confirmed;
});
if (!deleteConfirmed == true) return;
const { error } = await tryExecuteAndNotify(this, PackageResource.deletePackageCreatedByKey({ key: p.key }));
if (error) return;
const index = this._createdPackages.findIndex((x) => x.key === p.key);
this._createdPackages.splice(index, 1);
this.requestUpdate();
}
}
export default UmbPackagesCreatedOverviewElement;

View File

@@ -1,27 +1,54 @@
import { html, nothing } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { html, css, nothing } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
import { customElement, property, state } from 'lit/decorators.js';
import { firstValueFrom, map } from 'rxjs';
import { UUIButtonState } from '@umbraco-ui/uui';
import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '../../../../../core/modal';
import { createExtensionElement, umbExtensionsRegistry } from '@umbraco-cms/extensions-api';
import type { ManifestPackageView, UmbPackage } from '@umbraco-cms/models';
import type { ManifestPackageView } from '@umbraco-cms/models';
import { UmbLitElement } from '@umbraco-cms/element';
import { tryExecuteAndNotify } from '@umbraco-cms/resources';
import { PackageResource } from '@umbraco-cms/backend-api';
import { UmbNotificationContext, UMB_NOTIFICATION_CONTEXT_TOKEN } from '@umbraco-cms/notification';
@customElement('umb-installed-packages-section-view-item')
export class UmbInstalledPackagesSectionViewItemElement extends UmbLitElement {
@property({ type: Object })
package!: UmbPackage;
export class UmbInstalledPackagesSectionViewItem extends UmbLitElement {
static styles = css`
:host {
display: flex;
min-height: 47px;
}
`;
@property()
name?: string;
@property()
version?: string;
@property()
hasPendingMigrations = false;
@property()
customIcon?: string;
@state()
private _migrationButtonState?: UUIButtonState;
@state()
private _packageView?: ManifestPackageView;
private _notificationContext?: UmbNotificationContext;
private _modalContext?: UmbModalContext;
constructor() {
super();
this.consumeContext(UMB_NOTIFICATION_CONTEXT_TOKEN, (instance) => {
this._notificationContext = instance;
});
this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => {
this._modalContext = instance;
});
@@ -30,8 +57,8 @@ export class UmbInstalledPackagesSectionViewItemElement extends UmbLitElement {
connectedCallback(): void {
super.connectedCallback();
if (this.package.name?.length) {
this.findPackageView(this.package.name);
if (this.name?.length) {
this.findPackageView(this.name);
}
}
@@ -52,18 +79,51 @@ export class UmbInstalledPackagesSectionViewItemElement extends UmbLitElement {
this._packageView = views[0];
}
async _onMigration() {
if (!this.name) return;
const modalHandler = this._modalContext?.confirm({
color: 'positive',
headline: `Run migrations for ${this.name}?`,
content: `Do you want to start run migrations for ${this.name}`,
confirmLabel: 'Run migrations',
});
const migrationConfirmed = await modalHandler?.onClose().then(({ confirmed }: any) => {
return confirmed;
});
if (!migrationConfirmed == true) return;
this._migrationButtonState = 'waiting';
const { error } = await tryExecuteAndNotify(
this,
PackageResource.postPackageByNameRunMigration({ name: this.name })
);
if (error) return;
this._notificationContext?.peek('positive', { data: { message: 'Migrations completed' } });
this._migrationButtonState = 'success';
this.hasPendingMigrations = false;
}
render() {
return html`
<uui-ref-node-package name=${ifDefined(this.package.name)} version=${ifDefined(this.package.version)}>
<uui-action-bar slot="actions">
${this._packageView
<uui-ref-node-package
name=${ifDefined(this.name)}
version="${ifDefined(this.version)}"
@open=${this._onConfigure}
?disabled="${!this._packageView}">
${this.customIcon ? html`<uui-icon slot="icon" name="${this.customIcon}"></uui-icon>` : nothing}
<div slot="tag">
${this.hasPendingMigrations
? html`<uui-button
@click="${this._onMigration}"
.state=${this._migrationButtonState}
color="warning"
look="primary"
color="positive"
@click=${this._onConfigure}
label="Configure"></uui-button>`
label="Run pending package migrations">
Run pending migrations
</uui-button>`
: nothing}
</uui-action-bar>
</div>
</uui-ref-node-package>
`;
}
@@ -81,12 +141,16 @@ export class UmbInstalledPackagesSectionViewItemElement extends UmbLitElement {
return;
}
this._modalContext?.open(element, { data: this.package, size: 'small', type: 'sidebar' });
this._modalContext?.open(element, {
data: { name: this.name, version: this.version },
size: 'full',
type: 'sidebar',
});
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-installed-packages-section-view-item': UmbInstalledPackagesSectionViewItemElement;
'umb-installed-packages-section-view-item': UmbInstalledPackagesSectionViewItem;
}
}

View File

@@ -1,23 +1,56 @@
import { html } from 'lit';
import { html, css, nothing } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { combineLatest } from 'rxjs';
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { UmbPackageRepository } from '../../../repository/package.repository';
import type { UmbPackage } from '@umbraco-cms/models';
import { UmbLitElement } from '@umbraco-cms/element';
import type { UmbPackageWithMigrationStatus } from '@umbraco-cms/models';
import './installed-packages-section-view-item.element';
@customElement('umb-installed-packages-section-view')
export class UmbInstalledPackagesSectionView extends UmbLitElement {
@state()
private _installedPackages: UmbPackage[] = [];
static styles = [
UUITextStyles,
css`
:host {
display: block;
margin: var(--uui-size-layout-1);
}
uui-box {
margin-top: var(--uui-size-space-5);
padding-bottom: var(--uui-size-space-1);
}
private repository: UmbPackageRepository;
umb-installed-packages-section-view-item {
padding: var(--uui-size-space-3) 0 var(--uui-size-space-2);
}
umb-installed-packages-section-view-item:not(:first-child) {
border-top: 1px solid var(--uui-color-border, #d8d7d9);
}
.no-packages {
display: flex;
justify-content: space-around;
flex-direction: column;
align-items: center;
}
`,
];
@state()
private _installedPackages: UmbPackageWithMigrationStatus[] = [];
@state()
private _migrationPackages: UmbPackageWithMigrationStatus[] = [];
#packageRepository: UmbPackageRepository;
constructor() {
super();
this.repository = new UmbPackageRepository(this);
this.#packageRepository = new UmbPackageRepository(this);
}
firstUpdated() {
@@ -28,20 +61,77 @@ export class UmbInstalledPackagesSectionView extends UmbLitElement {
* Fetch the installed packages from the server
*/
private async _loadInstalledPackages() {
const package$ = await this.repository.rootItems();
package$.subscribe((packages) => {
this._installedPackages = packages.filter((p) => !!p.name);
const data = await Promise.all([this.#packageRepository.rootItems(), this.#packageRepository.migrations()]);
const [package$, migration$] = data;
combineLatest([package$, migration$]).subscribe(([packages, migrations]) => {
this._installedPackages = packages.map((p) => {
const migration = migrations.find((m) => m.packageName === p.name);
if (migration) {
// Remove that migration from the list
migrations = migrations.filter((m) => m.packageName !== p.name);
}
return {
...p,
hasPendingMigrations: migration?.hasPendingMigrations ?? false,
};
});
this._migrationPackages = [
...migrations.map((m) => ({
name: m.packageName,
hasPendingMigrations: m.hasPendingMigrations ?? false,
})),
];
/*this._installedPackages = [
...this._installedPackages,
...migrations.map((m) => ({
name: m.packageName,
hasPendingMigrations: m.hasPendingMigrations ?? false,
})),
];*/
});
}
render() {
return html`<uui-box headline="Installed packages">
if (this._installedPackages.length) return html`${this._renderCustomMigrations()} ${this._renderInstalled()} `;
return html`<div class="no-packages">
<h2><strong>No packages have been installed</strong></h2>
<p>
Browse through the available packages using the <strong>'Packages'</strong> icon in the top right of your screen
</p>
</div>`;
}
private _renderInstalled() {
return html`<uui-box headline="Installed packages" style="--uui-box-default-padding:0">
<uui-ref-list>
${repeat(
this._installedPackages,
(item) => item.name,
(item) =>
html`<umb-installed-packages-section-view-item .package=${item}></umb-installed-packages-section-view-item>`
(item) => html`<umb-installed-packages-section-view-item
.name=${item.name}
.version=${item.version}
.hasPendingMigrations=${item.hasPendingMigrations}></umb-installed-packages-section-view-item>`
)}
</uui-ref-list>
</uui-box>`;
}
private _renderCustomMigrations() {
if (!this._migrationPackages) return;
return html`<uui-box headline="Migrations" style="--uui-box-default-padding:0">
<uui-ref-list>
${repeat(
this._migrationPackages,
(item) => item.name,
(item) => html`<umb-installed-packages-section-view-item
.name=${item.name}
.version=${item.version}
.customIcon="${'umb:sync'}"
.hasPendingMigrations=${item.hasPendingMigrations}></umb-installed-packages-section-view-item>`
)}
</uui-ref-list>
</uui-box>`;

View File

@@ -39,7 +39,8 @@ export class UmbPackageRepository {
const { data: packages } = await this.#packageSource.getRootItems();
if (packages) {
store.appendItems(packages);
// Append packages to the store but only if they have a name
store.appendItems(packages.filter((p) => p.name?.length));
const extensions: ManifestBase[] = [];
packages.forEach((p) => {

View File

@@ -24,6 +24,7 @@ import { handlers as templateHandlers } from './domains/template.handlers';
import { handlers as languageHandlers } from './domains/language.handlers';
import { handlers as cultureHandlers } from './domains/culture.handlers';
import { handlers as redirectManagementHandlers } from './domains/redirect-management.handlers';
import { handlers as packageHandlers } from './domains/package.handlers';
const handlers = [
serverHandlers.serverVersionHandler,
@@ -51,6 +52,7 @@ const handlers = [
...languageHandlers,
...cultureHandlers,
...redirectManagementHandlers,
...packageHandlers,
];
switch (import.meta.env.VITE_UMBRACO_INSTALL_STATUS) {

View File

@@ -0,0 +1,126 @@
import { rest } from 'msw';
import { v4 as uuidv4 } from 'uuid';
import { umbracoPath } from '@umbraco-cms/utils';
import {
PackageCreateModel,
PackageDefinitionModel,
PagedPackageDefinitionModel,
PagedPackageMigrationStatusModel,
} from '@umbraco-cms/backend-api';
export const handlers = [
rest.get(umbracoPath('/package/migration-status'), (_req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json<PagedPackageMigrationStatusModel>({
total: 3,
items: [
{
hasPendingMigrations: true,
packageName: 'Named Package',
},
{
hasPendingMigrations: true,
packageName: 'My Custom Migration',
},
{
hasPendingMigrations: false,
packageName: 'Package with a view',
},
],
})
);
}),
rest.post(umbracoPath('/package/:name/run-migration'), async (_req, res, ctx) => {
const name = _req.params.name as string;
if (!name) return res(ctx.status(404));
return res(ctx.status(200));
}),
rest.get(umbracoPath('/package/created'), async (_req, res, ctx) => {
// read all
return res(
ctx.status(200),
ctx.json<PagedPackageDefinitionModel>({
total: packageArray.length,
items: packageArray,
})
);
}),
rest.post(umbracoPath('/package/created'), async (_req, res, ctx) => {
//save
const data: PackageCreateModel = await _req.json();
const newPackage: PackageDefinitionModel = { ...data, key: uuidv4() };
packageArray.push(newPackage);
return res(ctx.status(200), ctx.json<PackageDefinitionModel>(newPackage));
}),
rest.get(umbracoPath('/package/created/:key'), (_req, res, ctx) => {
//read 1
const key = _req.params.key as string;
if (!key) return res(ctx.status(404));
const found = packageArray.find((p) => p.key == key);
if (!found) return res(ctx.status(404));
return res(ctx.status(200), ctx.json<PackageDefinitionModel>(found));
}),
rest.put(umbracoPath('/package/created/:key'), async (_req, res, ctx) => {
//update
const data: PackageDefinitionModel = await _req.json();
if (!data.key) return;
const index = packageArray.findIndex((x) => x.key === data.key);
packageArray[index] = data;
return res(ctx.status(200));
}),
rest.delete(umbracoPath('/package/created/:key'), (_req, res, ctx) => {
//delete
const key = _req.params.key as string;
if (!key) return res(ctx.status(404));
const index = packageArray.findIndex((p) => p.key == key);
if (index <= -1) return res(ctx.status(404));
packageArray.splice(index, 1);
return res(ctx.status(200));
}),
rest.get(umbracoPath('/package/created/:key/download'), (_req, res, ctx) => {
//download
return res(ctx.status(200));
}),
];
const packageArray: PackageDefinitionModel[] = [
{
key: '2a0181ec-244b-4068-a1d7-2f95ed7e6da6',
packagePath: undefined,
name: 'My Package',
//contentNodeId?: string | null;
//contentLoadChildNodes?: boolean;
//mediaKeys?: Array<string>;
//mediaLoadChildNodes?: boolean;
//documentTypes?: Array<string>;
//mediaTypes?: Array<string>;
//dataTypes?: Array<string>;
//templates?: Array<string>;
//partialViews?: Array<string>;
//stylesheets?: Array<string>;
//scripts?: Array<string>;
//languages?: Array<string>;
//dictionaryItems?: Array<string>;
},
{
key: '2a0181ec-244b-4068-a1d7-2f95ed7e6da7',
packagePath: undefined,
name: 'My Second Package',
},
{
key: '2a0181ec-244b-4068-a1d7-2f95ed7e6da8',
packagePath: undefined,
name: 'My Third Package',
},
];

View File

@@ -13,6 +13,7 @@ import { handlers as profileHandlers } from './domains/performance-profiling.han
import { handlers as healthCheckHandlers } from './domains/health-check.handlers';
import { handlers as languageHandlers } from './domains/language.handlers';
import { handlers as redirectManagementHandlers } from './domains/redirect-management.handlers';
import { handlers as packageHandlers } from './domains/package.handlers';
export const handlers = [
serverHandlers.serverRunningHandler,
@@ -31,4 +32,5 @@ export const handlers = [
...healthCheckHandlers,
...languageHandlers,
...redirectManagementHandlers,
...packageHandlers,
];

View File

@@ -5,7 +5,7 @@ import { UmbModalLayoutElement } from '../modal-layout.element';
export interface UmbModalContentPickerData {
multiple?: boolean;
selection: Array<string>;
selection?: Array<string>;
}
import { UmbTreeElement } from '../../../../backoffice/shared/components/tree/tree.element';