Merge remote-tracking branch 'origin/main' into feature/media-section

This commit is contained in:
Jesper Møller Jensen
2022-12-14 12:55:45 +01:00
83 changed files with 7789 additions and 6027 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -64,7 +64,7 @@
"lodash": "^4.17.21",
"openapi-typescript-fetch": "^1.1.3",
"router-slot": "^1.5.5",
"rxjs": "^7.5.7",
"rxjs": "^7.6.0",
"uuid": "^9.0.0"
},
"devDependencies": {
@@ -74,7 +74,7 @@
"@playwright/test": "^1.28.1",
"@storybook/addon-a11y": "^6.5.14",
"@storybook/addon-actions": "^6.5.14",
"@storybook/addon-essentials": "^6.5.13",
"@storybook/addon-essentials": "^6.5.14",
"@storybook/addon-links": "^6.5.13",
"@storybook/builder-vite": "^0.2.5",
"@storybook/mdx2-csf": "^0.0.3",
@@ -83,33 +83,33 @@
"@types/lodash-es": "^4.17.6",
"@types/mocha": "^10.0.0",
"@types/uuid": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"@typescript-eslint/eslint-plugin": "^5.46.1",
"@typescript-eslint/parser": "^5.46.1",
"@web/dev-server-esbuild": "^0.3.3",
"@web/dev-server-import-maps": "^0.0.7",
"@web/test-runner": "^0.15.0",
"@web/test-runner-playwright": "^0.9.0",
"babel-loader": "^9.1.0",
"eslint": "^8.28.0",
"eslint": "^8.29.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-lit": "^1.6.1",
"eslint-plugin-lit": "^1.7.0",
"eslint-plugin-lit-a11y": "^2.3.0",
"eslint-plugin-local-rules": "^1.3.2",
"eslint-plugin-storybook": "^0.6.7",
"eslint-plugin-storybook": "^0.6.8",
"lit-html": "^2.4.0",
"msw": "^0.49.1",
"msw-storybook-addon": "^1.6.3",
"openapi-typescript-codegen": "^0.23.0",
"playwright-msw": "^2.0.2",
"playwright-msw": "^2.1.0",
"plop": "^3.1.1",
"prettier": "2.8.0",
"prettier": "2.8.1",
"tiny-glob": "^0.2.9",
"typescript": "^4.9.3",
"vite": "^3.2.4",
"vite-plugin-static-copy": "^0.12.0",
"vite-tsconfig-paths": "^3.6.0",
"vite-tsconfig-paths": "^4.0.1",
"web-component-analyzer": "^2.0.0-next.4"
},
"msw": {

View File

@@ -1,4 +1,6 @@
//TODO: we need to figure out what components should be available for extensions and load them upfront
import './editors/shared/editor-entity-layout/editor-entity-layout.element';
import './components/ref-property-editor-ui/ref-property-editor-ui.element';
import './components/backoffice-header.element';
import './components/backoffice-main.element';
import './components/backoffice-modal-container.element';
@@ -34,7 +36,7 @@ import { manifests as editorManifests } from './editors/manifests';
import { manifests as propertyActionManifests } from './property-actions/manifests';
import { UmbContextConsumerMixin, UmbContextProviderMixin } from '@umbraco-cms/context-api';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import type { ManifestTypes, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestTypes } from '@umbraco-cms/models';
@defineElement('umb-backoffice')
export class UmbBackofficeElement extends UmbContextConsumerMixin(UmbContextProviderMixin(LitElement)) {
@@ -79,7 +81,7 @@ export class UmbBackofficeElement extends UmbContextConsumerMixin(UmbContextProv
this.provideContext('umbSectionStore', new UmbSectionStore());
}
private _registerExtensions(manifests: Array<ManifestWithLoader<ManifestTypes>> | Array<ManifestTypes>) {
private _registerExtensions(manifests: Array<ManifestTypes> | Array<ManifestTypes>) {
manifests.forEach((manifest) => {
if (umbExtensionsRegistry.isRegistered(manifest.alias)) return;
umbExtensionsRegistry.register(manifest);

View File

@@ -0,0 +1,78 @@
import { html, LitElement } from 'lit';
import { property } from 'lit/decorators.js';
import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar';
import { UmbPickerData } from '../../../core/services/modal/layouts/modal-layout-picker-base';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
//TODO: These should probably be imported dynamically.
import '../../../core/services/modal/layouts/picker-section/picker-layout-section.element';
import '../../../core/services/modal/layouts/picker-user-group/picker-layout-user-group.element';
import '../../../core/services/modal/layouts/picker-user/picker-layout-user.element';
import { UmbModalService, UmbModalType } from '@umbraco-cms/services';
/** TODO: Make use of UUI FORM Mixin, to make it easily take part of a form. */
export class UmbInputListBase extends UmbContextConsumerMixin(LitElement) {
@property({ type: Array })
public value: Array<string> = [];
@property({ type: Boolean })
public multiple = true;
@property({ type: String })
public modalType: UmbModalType = 'sidebar';
@property({ type: String })
public modalSize: UUIModalSidebarSize = 'small';
protected pickerLayout?: string;
private _modalService?: UmbModalService;
constructor() {
super();
this.consumeContext('umbModalService', (modalService: UmbModalService) => {
this._modalService = modalService;
});
}
private _openPicker() {
if (!this.pickerLayout) return;
const modalHandler = this._modalService?.open(this.pickerLayout, {
type: this.modalType,
size: this.modalSize,
data: {
multiple: this.multiple,
selection: this.value,
},
});
modalHandler?.onClose().then((data: UmbPickerData<string>) => {
if (data) {
this.value = data.selection;
this.selectionUpdated();
}
});
}
protected removeFromSelection(key: string) {
this.value = this.value.filter((k) => k !== key);
this.selectionUpdated();
}
protected selectionUpdated() {
// override this method to react to selection changes
}
protected renderButton() {
return html`<uui-button id="add-button" look="placeholder" @click=${this._openPicker} label="open">
Add
</uui-button>`;
}
protected renderContent() {
return html``;
}
render() {
return html`${this.renderContent()}${this.renderButton()}`;
}
}

View File

@@ -0,0 +1,90 @@
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { css, html, nothing } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UmbInputListBase } from '../input-list-base/input-list-base';
import type { ManifestSection } from '@umbraco-cms/models';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
@customElement('umb-input-section')
export class UmbInputPickerSectionElement extends UmbInputListBase {
static styles = [
UUITextStyles,
css`
:host {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
#user-group-list {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
.user-group {
display: flex;
align-items: center;
gap: var(--uui-size-space-2);
}
.user-group div {
display: flex;
align-items: center;
gap: var(--uui-size-4);
}
.user-group uui-button {
margin-left: auto;
}
`,
];
@state()
private _sections: Array<ManifestSection> = [];
connectedCallback(): void {
super.connectedCallback();
this.pickerLayout = 'umb-picker-layout-section';
this._observeSections();
}
private _observeSections() {
if (this.value.length > 0) {
umbExtensionsRegistry.extensionsOfType('section').subscribe((sections: Array<ManifestSection>) => {
this._sections = sections.filter((section) => this.value.includes(section.alias));
});
} else {
this._sections = [];
}
}
selectionUpdated() {
this._observeSections();
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
}
renderContent() {
if (this._sections.length === 0) return html`${nothing}`;
return html`
<div id="user-list">
${this._sections.map(
(section) => html`
<div class="user-group">
<div>
<span>${section.meta.label}</span>
</div>
<uui-button
@click=${() => this.removeFromSelection(section.alias)}
label="remove"
color="danger"></uui-button>
</div>
`
)}
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-input-section': UmbInputPickerSectionElement;
}
}

View File

@@ -0,0 +1,19 @@
import { expect, fixture, html } from '@open-wc/testing';
//TODO: Test has been commented out while we figure out how to setup import maps for the test environment
// import { UmbPickerSectionElement } from './picker-section.element';
// import { defaultA11yConfig } from '@umbraco-cms/test-utils';
// describe('UmbPickerSectionElement', () => {
// let element: UmbPickerSectionElement;
// beforeEach(async () => {
// element = await fixture(html`<umb-input-section></umb-input-section>`);
// });
// it('is defined with its own instance', () => {
// expect(element).to.be.instanceOf(UmbPickerSectionElement);
// });
// it('passes the a11y audit', async () => {
// await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
// });
// });

View File

@@ -0,0 +1,100 @@
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { css, html, nothing } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UmbInputListBase } from '../input-list-base/input-list-base';
import type { UserGroupEntity } from '@umbraco-cms/models';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbUserGroupStore } from '@umbraco-cms/stores/user/user-group.store';
@customElement('umb-input-user-group')
export class UmbInputPickerUserGroupElement extends UmbObserverMixin(UmbInputListBase) {
static styles = [
UUITextStyles,
css`
:host {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
#user-group-list {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
.user-group {
display: flex;
align-items: center;
gap: var(--uui-size-space-2);
}
.user-group div {
display: flex;
align-items: center;
gap: var(--uui-size-4);
}
.user-group uui-button {
margin-left: auto;
}
`,
];
@state()
private _userGroups: Array<UserGroupEntity> = [];
private _userGroupStore?: UmbUserGroupStore;
connectedCallback(): void {
super.connectedCallback();
this.pickerLayout = 'umb-picker-layout-user-group';
this.consumeContext('umbUserGroupStore', (usersContext: UmbUserGroupStore) => {
this._userGroupStore = usersContext;
this._observeUserGroups();
});
}
private _observeUserGroups() {
if (this.value.length > 0 && this._userGroupStore) {
this.observe<Array<UserGroupEntity>>(
this._userGroupStore.getByKeys(this.value),
(userGroups) => (this._userGroups = userGroups)
);
} else {
this._userGroups = [];
}
}
selectionUpdated() {
this._observeUserGroups();
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
}
private _renderUserGroupList() {
if (this._userGroups.length === 0) return nothing;
return html`<div id="user-list">
${this._userGroups.map(
(userGroup) => html`
<div class="user-group">
<div>
<uui-icon .name=${userGroup.icon}></uui-icon>
<span>${userGroup.name}</span>
</div>
<uui-button
@click=${() => this.removeFromSelection(userGroup.key)}
label="remove"
color="danger"></uui-button>
</div>
`
)}
</div> `;
}
renderContent() {
return html`${this._renderUserGroupList()}`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-input-user-group': UmbInputPickerUserGroupElement;
}
}

View File

@@ -0,0 +1,19 @@
import { expect, fixture, html } from '@open-wc/testing';
//TODO: Test has been commented out while we figure out how to setup import maps for the test environment
// import { UmbPickerUserGroupElement } from './picker-user-group.element';
// import { defaultA11yConfig } from '@umbraco-cms/test-utils';
// describe('UmbPickerLayoutUserGroupElement', () => {
// let element: UmbPickerUserGroupElement;
// beforeEach(async () => {
// element = await fixture(html`<umb-input-user-group></umb-input-user-group>`);
// });
// it('is defined with its own instance', () => {
// expect(element).to.be.instanceOf(UmbPickerUserGroupElement);
// });
// it('passes the a11y audit', async () => {
// await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
// });
// });

View File

@@ -0,0 +1,94 @@
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { css, html, nothing, PropertyValueMap } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UmbInputListBase } from '../input-list-base/input-list-base';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import type { UserEntity } from '@umbraco-cms/models';
import { UmbUserStore } from '@umbraco-cms/stores/user/user.store';
@customElement('umb-input-user')
export class UmbPickerUserElement extends UmbObserverMixin(UmbInputListBase) {
static styles = [
UUITextStyles,
css`
:host {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
#user-list {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
.user {
display: flex;
align-items: center;
gap: var(--uui-size-space-2);
}
.user uui-button {
margin-left: auto;
}
`,
];
@state()
private _users: Array<UserEntity> = [];
private _userStore?: UmbUserStore;
connectedCallback(): void {
super.connectedCallback();
this.pickerLayout = 'umb-picker-layout-user';
this.consumeContext('umbUserStore', (userStore: UmbUserStore) => {
this._userStore = userStore;
this._observeUser();
});
}
protected updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
super.updated(_changedProperties);
if (_changedProperties.has('value')) {
this._observeUser(); // TODO: This works, but it makes the value change twice.
}
}
private _observeUser() {
if (!this._userStore) return;
this.observe<Array<UserEntity>>(this._userStore.getByKeys(this.value), (users) => {
this._users = users;
});
}
selectionUpdated() {
this._observeUser();
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
}
private _renderUserList() {
if (this._users.length === 0) return nothing;
return html`<div id="user-list">
${this._users.map(
(user) => html`
<div class="user">
<uui-avatar .name=${user.name}></uui-avatar>
<div>${user.name}</div>
<uui-button @click=${() => this.removeFromSelection(user.key)} label="remove" color="danger"></uui-button>
</div>
`
)}
</div> `;
}
renderContent() {
return html`${this._renderUserList()}`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-input-user': UmbPickerUserElement;
}
}

View File

@@ -0,0 +1,19 @@
import { expect, fixture, html } from '@open-wc/testing';
//TODO: Test has been commented out while we figure out how to setup import maps for the test environment
// import { UmbPickerUserElement } from './picker-user.element';
// import { defaultA11yConfig } from '@umbraco-cms/test-utils';
// describe('UmbPickerUserElement', () => {
// let element: UmbPickerUserElement;
// beforeEach(async () => {
// element = await fixture(html`<umb-input-user></umb-input-user>`);
// });
// it('is defined with its own instance', () => {
// expect(element).to.be.instanceOf(UmbPickerUserElement);
// });
// it('passes the a11y audit', async () => {
// await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
// });
// });

View File

@@ -0,0 +1 @@
export * from './table.element';

View File

@@ -8,9 +8,6 @@ import { UmbDataTypeContext } from './data-type.context';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbContextProviderMixin, UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import '../shared/editor-entity-layout/editor-entity-layout.element';
/**
* @element umb-editor-data-type
* @description - Element for displaying a Data Type Editor

View File

@@ -8,10 +8,9 @@ import { DocumentTypeEntity } from '../../../core/mocks/data/document-type.data'
import { UmbDocumentTypeContext } from './document-type.context';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbContextConsumerMixin, UmbContextProviderMixin } from '@umbraco-cms/context-api';
import type { ManifestTypes, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestTypes } from '@umbraco-cms/models';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import '../shared/editor-entity-layout/editor-entity-layout.element';
import '../../property-editor-uis/icon-picker/property-editor-ui-icon-picker.element';
@customElement('umb-editor-document-type')
export class UmbEditorDocumentTypeElement extends UmbContextProviderMixin(
@@ -57,7 +56,7 @@ export class UmbEditorDocumentTypeElement extends UmbContextProviderMixin(
}
private _registerExtensions() {
const extensions: Array<ManifestWithLoader<ManifestTypes>> = [
const extensions: Array<ManifestTypes> = [
{
type: 'editorView',
alias: 'Umb.EditorView.DocumentType.Design',
@@ -122,7 +121,9 @@ export class UmbEditorDocumentTypeElement extends UmbContextProviderMixin(
render() {
return html`
<umb-editor-entity-layout alias="Umb.Editor.DocumentType">
<div slot="icon">Icon</div>
<div slot="icon">
<umb-property-editor-ui-icon-picker></umb-property-editor-ui-icon-picker>
</div>
<div slot="name">
<uui-input id="name" .value=${this._documentType?.name} @input="${this._handleInput}">

View File

@@ -2,7 +2,7 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import type { ManifestEditorView, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestEditorView } from '@umbraco-cms/models';
import '../shared/node/editor-node.element';
@@ -29,7 +29,7 @@ export class UmbEditorDocumentElement extends LitElement {
}
private _registerEditorViews() {
const dashboards: Array<ManifestWithLoader<ManifestEditorView>> = [
const dashboards: Array<ManifestEditorView> = [
{
type: 'editorView',
alias: 'Umb.EditorView.Document.Edit',

View File

@@ -6,8 +6,6 @@ import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import type { ManifestTypes } from '@umbraco-cms/models';
import '../shared/editor-entity-layout/editor-entity-layout.element';
@customElement('umb-editor-extensions')
export class UmbEditorExtensionsElement extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
@state()

View File

@@ -1,6 +1,6 @@
import type { ManifestEditor, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestEditor } from '@umbraco-cms/models';
export const manifests: Array<ManifestWithLoader<ManifestEditor>> = [
export const manifests: Array<ManifestEditor> = [
{
type: 'editor',
alias: 'Umb.Editor.Member',

View File

@@ -1,7 +1,7 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import type { ManifestEditorView, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestEditorView } from '@umbraco-cms/models';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import '../shared/node/editor-node.element';
@@ -29,7 +29,7 @@ export class UmbEditorMediaElement extends LitElement {
}
private _registerEditorViews() {
const dashboards: Array<ManifestWithLoader<ManifestEditorView>> = [
const dashboards: Array<ManifestEditorView> = [
{
type: 'editorView',
alias: 'Umb.EditorView.Media.Edit',

View File

@@ -2,8 +2,6 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import '../shared/editor-entity-layout/editor-entity-layout.element';
@customElement('umb-editor-member-group')
export class UmbEditorMemberGroupElement extends LitElement {
static styles = [

View File

@@ -2,8 +2,6 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import '../shared/editor-entity-layout/editor-entity-layout.element';
@customElement('umb-editor-member')
export class UmbEditorMemberElement extends LitElement {
static styles = [

View File

@@ -2,8 +2,6 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import '../shared/editor-entity-layout/editor-entity-layout.element';
@customElement('umb-editor-package-builder')
export class UmbEditorPackageBuilderElement extends LitElement {
static styles = [

View File

@@ -2,8 +2,6 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { css, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import '../shared/editor-entity-layout/editor-entity-layout.element';
@customElement('umb-editor-package')
export class UmbEditorPackageElement extends LitElement {
static styles = [

View File

@@ -18,6 +18,18 @@ export class UmbEditorPropertyLayoutElement extends LitElement {
grid-template-columns: 200px 600px;
gap: 32px;
}
:host {
border-bottom: 1px solid var(--uui-color-divider);
padding: var(--uui-size-space-6) 0;
}
p {
margin-bottom: 0;
}
#header {
position: sticky;
top: var(--uui-size-space-4);
height: min-content;
}
`,
];
@@ -41,7 +53,7 @@ export class UmbEditorPropertyLayoutElement extends LitElement {
render() {
return html`
<div>
<div id="header">
<uui-label>${this.label}</uui-label>
<slot name="property-action-menu"></slot>
<p>${this.description}</p>

View File

@@ -11,8 +11,6 @@ import { UmbNodeContext } from './node.context';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbContextConsumerMixin, UmbContextProviderMixin } from '@umbraco-cms/context-api';
import '../../shared/editor-entity-layout/editor-entity-layout.element';
// Lazy load
// TODO: Make this dynamic, use load-extensions method to loop over extensions for this node.
import './views/edit/editor-view-node-edit.element';

View File

@@ -0,0 +1,76 @@
import { css, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import type { UUIButtonState } from '@umbraco-ui/uui';
import type { UmbNotificationDefaultData } from '../../../../core/services/notification/layouts/default';
import type { UmbNotificationService } from '../../../../core/services/notification';
import { UmbUserGroupContext } from '../user-group.context';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import { UmbUserGroupStore } from 'src/core/stores/user/user-group.store';
import { UmbUserStore } from 'src/core/stores/user/user.store';
@customElement('umb-editor-action-user-group-save')
export class UmbEditorActionUserGroupSaveElement extends UmbContextConsumerMixin(LitElement) {
static styles = [UUITextStyles, css``];
@state()
private _saveButtonState?: UUIButtonState;
private _userGroupStore?: UmbUserGroupStore;
private _userStore?: UmbUserStore;
private _userGroupContext?: UmbUserGroupContext;
private _notificationService?: UmbNotificationService;
connectedCallback(): void {
super.connectedCallback();
this.consumeAllContexts(
['umbUserGroupStore', 'umbUserStore', 'umbUserGroupContext', 'umbNotificationService'],
(instances) => {
this._userGroupStore = instances['umbUserGroupStore'];
this._userStore = instances['umbUserStore'];
this._userGroupContext = instances['umbUserGroupContext'];
this._notificationService = instances['umbNotificationService'];
}
);
}
private async _handleSave() {
// TODO: What if store is not present, what if node is not loaded....
if (!this._userGroupStore || !this._userGroupContext) return;
try {
this._saveButtonState = 'waiting';
const userGroup = this._userGroupContext.getData();
await this._userGroupStore.save([userGroup]);
if (this._userStore && userGroup.users) {
await this._userStore.updateUserGroup(userGroup.users, userGroup.key);
}
const notificationData: UmbNotificationDefaultData = { message: 'User Group Saved' };
this._notificationService?.peek('positive', { data: notificationData });
this._saveButtonState = 'success';
} catch (error) {
const notificationData: UmbNotificationDefaultData = { message: 'User Group Save Failed' };
this._notificationService?.peek('danger', { data: notificationData });
this._saveButtonState = 'failed';
}
}
render() {
return html`<uui-button
@click=${this._handleSave}
look="primary"
color="positive"
label="save"
.state="${this._saveButtonState}"></uui-button>`;
}
}
export default UmbEditorActionUserGroupSaveElement;
declare global {
interface HTMLElementTagNameMap {
'umb-editor-action-user-group-save': UmbEditorActionUserGroupSaveElement;
}
}

View File

@@ -3,9 +3,20 @@ import { UUITextStyles } from '@umbraco-ui/uui-css';
import { css, html, LitElement, nothing } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { UmbUserGroupContext } from './user-group.context';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import '@umbraco-cms/components/input-user/input-user.element';
import '@umbraco-cms/components/input-section/input-section.element';
import { UmbContextConsumerMixin, UmbContextProviderMixin } from '@umbraco-cms/context-api';
import type { ManifestEditorAction, UserDetails, UserGroupDetails } from '@umbraco-cms/models';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import { UmbUserGroupStore } from '@umbraco-cms/stores/user/user-group.store';
import { UmbUserStore } from '@umbraco-cms/stores/user/user.store';
@customElement('umb-editor-user-group')
export class UmbEditorUserGroupElement extends LitElement {
export class UmbEditorUserGroupElement extends UmbContextProviderMixin(
UmbContextConsumerMixin(UmbObserverMixin(LitElement))
) {
static styles = [
UUITextStyles,
css`
@@ -63,12 +74,19 @@ export class UmbEditorUserGroupElement extends LitElement {
`,
];
@state()
private _userName = '';
@property({ type: String })
entityKey = '';
@state()
private _userGroup?: UserGroupDetails | null;
@state()
private _userKeys?: Array<string>;
private _userGroupStore?: UmbUserGroupStore;
private _userStore?: UmbUserStore;
private _userGroupContext?: UmbUserGroupContext;
defaultPermissions: Array<{
name: string;
permissions: Array<{ name: string; description: string; value: boolean }>;
@@ -180,35 +198,130 @@ export class UmbEditorUserGroupElement extends LitElement {
},
];
constructor() {
super();
this._registerEditorActions();
}
private _registerEditorActions() {
const manifests: Array<ManifestEditorAction> = [
{
type: 'editorAction',
alias: 'Umb.EditorAction.UserGroup.Save',
name: 'EditorActionUserGroupSave',
loader: () => import('./actions/editor-action-user-group-save.element'),
meta: {
editors: ['Umb.Editor.UserGroup'],
},
},
];
manifests.forEach((manifest) => {
if (umbExtensionsRegistry.isRegistered(manifest.alias)) return;
umbExtensionsRegistry.register(manifest);
});
}
connectedCallback(): void {
super.connectedCallback();
this.consumeAllContexts(['umbUserGroupStore', 'umbUserStore'], (instance) => {
this._userGroupStore = instance['umbUserGroupStore'];
this._userStore = instance['umbUserStore'];
this._observeUserGroup();
this._observeUsers();
});
}
private _observeUserGroup() {
if (!this._userGroupStore) return;
this.observe(this._userGroupStore.getByKey(this.entityKey), (userGroup) => {
this._userGroup = userGroup;
if (!this._userGroup) return;
if (!this._userGroupContext) {
this._userGroupContext = new UmbUserGroupContext(this._userGroup);
this.provideContext('umbUserGroupContext', this._userGroupContext);
} else {
this._userGroupContext.update(this._userGroup);
}
});
}
private _observeUsers() {
if (!this._userStore) return;
// TODO: Create method to only get users from this userGroup
// TODO: Find a better way to only call this once at the start
this.observe(this._userStore.getAll(), (users: Array<UserDetails>) => {
if (!this._userKeys && users.length > 0) {
this._userKeys = users.filter((user) => user.userGroups.includes(this.entityKey)).map((user) => user.key);
this._updateProperty('users', this._userKeys);
}
});
}
private _updateUserKeys(userKeys: Array<string>) {
this._userKeys = userKeys;
this._updateProperty('users', this._userKeys);
}
private _updateProperty(propertyName: string, value: unknown) {
this._userGroupContext?.update({ [propertyName]: value });
}
private _updatePermission(permission: { name: string; description: string; value: boolean }) {
if (!this._userGroupContext) return;
const checkValue = this._checkPermission(permission);
const selectedPermissions = this._userGroupContext.getData().permissions;
let newPermissions = [];
if (checkValue === false) {
newPermissions = [...selectedPermissions, permission.name];
} else {
newPermissions = selectedPermissions.filter((p) => p !== permission.name);
}
this._updateProperty('permissions', newPermissions);
}
private _checkPermission(permission: { name: string; description: string; value: boolean }) {
if (!this._userGroupContext) return false;
return this._userGroupContext.getData().permissions.includes(permission.name);
}
private renderLeftColumn() {
if (!this._userGroup) return nothing;
return html` <uui-box>
<div slot="headline">Assign access</div>
<div>
<b>Sections</b>
<div class="faded-text">Add sections to give users access</div>
</div>
<div>
<b>Content start nodes</b>
<div class="faded-text">Limit the content tree to specific start nodes</div>
<umb-property-editor-ui-content-picker></umb-property-editor-ui-content-picker>
</div>
<div>
<b>Media start nodes</b>
<div class="faded-text">Limit the media library to specific start nodes</div>
<umb-property-editor-ui-content-picker></umb-property-editor-ui-content-picker>
</div>
<b>Content</b>
<div class="access-content">
<uui-icon name="folder"></uui-icon>
<span>Content Root</span>
</div>
<b>Media</b>
<div class="access-content">
<uui-icon name="folder"></uui-icon>
<span>Media Root</span>
</div>
<umb-editor-property-layout label="Sections" description="Add sections to give users access">
<umb-input-section
slot="editor"
.value=${this._userGroup.sections}
@change=${(e: any) => this._updateProperty('sections', e.target.value)}></umb-input-section>
</umb-editor-property-layout>
<umb-editor-property-layout
label="Content start node"
description="Limit the content tree to a specific start node">
<uui-ref-node slot="editor" name="Content Root" border>
<uui-icon slot="icon" name="folder"></uui-icon>
<uui-button slot="actions" label="change"></uui-button>
<uui-button slot="actions" label="remove" color="danger"></uui-button>
</uui-ref-node>
</umb-editor-property-layout>
<umb-editor-property-layout
label="Media start node"
description="Limit the media library to a specific start node">
<uui-ref-node slot="editor" name="Media Root" border>
<uui-icon slot="icon" name="folder"></uui-icon>
<uui-button slot="actions" label="change"></uui-button>
<uui-button slot="actions" label="remove" color="danger"></uui-button>
</uui-ref-node>
</umb-editor-property-layout>
</uui-box>
<uui-box>
@@ -224,10 +337,8 @@ export class UmbEditorUserGroupElement extends LitElement {
(permission) => html`
<div class="default-permission">
<uui-toggle
.checked=${permission.value}
@change=${(e: Event) => {
permission.value = (e.target as HTMLInputElement).checked;
}}></uui-toggle>
.checked=${this._checkPermission(permission)}
@change=${() => this._updatePermission(permission)}></uui-toggle>
<div class="permission-info">
<b>${permission.name}</b>
<span class="faded-text">${permission.description}</span>
@@ -249,6 +360,9 @@ export class UmbEditorUserGroupElement extends LitElement {
private renderRightColumn() {
return html`<uui-box>
<div slot="headline">Users</div>
<umb-input-user
@change=${(e: Event) => this._updateUserKeys((e.target as any).value)}
.value=${this._userKeys || []}></umb-input-user>
</uui-box>`;
}
@@ -256,15 +370,15 @@ export class UmbEditorUserGroupElement extends LitElement {
private _handleInput(event: UUIInputEvent) {
if (event instanceof UUIInputEvent) {
const target = event.composedPath()[0] as UUIInputElement;
console.log('input', target.value);
}
}
render() {
if (!this._userGroup) return nothing;
return html`
<umb-editor-entity-layout alias="Umb.Editor.UserGroup">
<uui-input id="name" slot="name" .value=${this._userName} @input="${this._handleInput}"></uui-input>
<uui-input id="name" slot="name" .value=${this._userGroup.name} @input="${this._handleInput}"></uui-input>
<div id="main">
<div id="left-column">${this.renderLeftColumn()}</div>
<div id="right-column">${this.renderRightColumn()}</div>

View File

@@ -0,0 +1,20 @@
import { expect, fixture, html } from '@open-wc/testing';
//TODO: Test has been commented out while we figure out how to setup import maps for the test environment
// import UmbEditorUserGroupElement from './editor-user-group.element';
// import { defaultA11yConfig } from '@umbraco-cms/test-utils';
// describe('UmbEditorUserGroupElement', () => {
// let element: UmbEditorUserGroupElement;
// beforeEach(async () => {
// element = await fixture(html` <umb-editor-user-group></umb-editor-user-group> `);
// });
// it('is defined with its own instance', () => {
// expect(element).to.be.instanceOf(UmbEditorUserGroupElement);
// });
// it('passes the a11y audit', async () => {
// await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
// });
// });

View File

@@ -0,0 +1,33 @@
import { BehaviorSubject, Observable } from 'rxjs';
import type { UserGroupDetails } from '@umbraco-cms/models';
export class UmbUserGroupContext {
// TODO: figure out how fine grained we want to make our observables.
private _data = new BehaviorSubject<UserGroupDetails & { users?: Array<string> }>({
key: '',
name: '',
icon: '',
type: 'userGroup',
hasChildren: false,
parentKey: '',
isTrashed: false,
sections: [],
permissions: [],
users: [],
});
public readonly data: Observable<UserGroupDetails> = this._data.asObservable();
constructor(userGroup: UserGroupDetails & { users?: Array<string> }) {
if (!userGroup) return;
this._data.next(userGroup);
}
// TODO: figure out how we want to update data
public update(data: Partial<UserGroupDetails & { users?: Array<string> }>) {
this._data.next({ ...this._data.getValue(), ...data });
}
public getData() {
return this._data.getValue();
}
}

View File

@@ -2,21 +2,26 @@ import { UUIInputElement, UUIInputEvent } from '@umbraco-ui/uui';
import { css, html, LitElement, nothing } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property, state } from 'lit/decorators.js';
import { Subscription } from 'rxjs';
import { ifDefined } from 'lit-html/directives/if-defined.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { repeat } from 'lit/directives/repeat.js';
import { UmbUserStore } from '../../../core/stores/user/user.store';
import { getTagLookAndColor } from '../../sections/users/user-extensions';
import { UmbUserContext } from './user.context';
import { UmbUserStore } from '@umbraco-cms/stores/user/user.store';
import { UmbContextProviderMixin, UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import type { ManifestEditorAction, ManifestWithLoader, UserDetails } from '@umbraco-cms/models';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import type { ManifestEditorAction, UserDetails } from '@umbraco-cms/models';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import '../../property-editor-uis/content-picker/property-editor-ui-content-picker.element';
import '../shared/editor-entity-layout/editor-entity-layout.element';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import '@umbraco-cms/components/input-user-group/input-user-group.element';
@customElement('umb-editor-user')
export class UmbEditorUserElement extends UmbContextProviderMixin(UmbContextConsumerMixin(LitElement)) {
export class UmbEditorUserElement extends UmbContextProviderMixin(
UmbContextConsumerMixin(UmbObserverMixin(LitElement))
) {
static styles = [
UUITextStyles,
css`
@@ -72,18 +77,6 @@ export class UmbEditorUserElement extends UmbContextProviderMixin(UmbContextCons
#assign-access {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
}
.access-content {
margin-top: var(--uui-size-space-1);
margin-bottom: var(--uui-size-space-4);
display: flex;
align-items: center;
line-height: 1;
gap: var(--uui-size-space-3);
}
.access-content > span {
align-self: end;
}
`,
];
@@ -98,11 +91,7 @@ export class UmbEditorUserElement extends UmbContextProviderMixin(UmbContextCons
entityKey = '';
protected _userStore?: UmbUserStore;
protected _usersSubscription?: Subscription;
private _userContext?: UmbUserContext;
private _userNameSubscription?: Subscription;
private _languages = []; //TODO Add languages
constructor() {
@@ -112,7 +101,7 @@ export class UmbEditorUserElement extends UmbContextProviderMixin(UmbContextCons
}
private _registerEditorActions() {
const manifests: Array<ManifestWithLoader<ManifestEditorAction>> = [
const manifests: Array<ManifestEditorAction> = [
{
type: 'editorAction',
alias: 'Umb.EditorAction.User.Save',
@@ -140,9 +129,9 @@ export class UmbEditorUserElement extends UmbContextProviderMixin(UmbContextCons
}
private _observeUser() {
this._usersSubscription?.unsubscribe();
if (!this._userStore) return;
this._usersSubscription = this._userStore?.getByKey(this.entityKey).subscribe((user) => {
this.observe(this._userStore.getByKey(this.entityKey), (user) => {
this._user = user;
if (!this._user) return;
@@ -153,19 +142,18 @@ export class UmbEditorUserElement extends UmbContextProviderMixin(UmbContextCons
this._userContext.update(this._user);
}
this._userNameSubscription = this._userContext.data.subscribe((user) => {
if (user && user.name !== this._userName) {
this._userName = user.name;
}
});
this._observeUserName();
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
private _observeUserName() {
if (!this._userContext) return;
this._usersSubscription?.unsubscribe();
this._userNameSubscription?.unsubscribe();
this.observe(this._userContext.data, (user) => {
if (user.name !== this._userName) {
this._userName = user.name;
}
});
}
private _updateUserStatus() {
@@ -183,60 +171,97 @@ export class UmbEditorUserElement extends UmbContextProviderMixin(UmbContextCons
history.pushState(null, '', 'section/users/view/users/overview');
}
private renderLeftColumn() {
// TODO. find a way where we don't have to do this for all editors.
private _handleInput(event: UUIInputEvent) {
if (event instanceof UUIInputEvent) {
const target = event.composedPath()[0] as UUIInputElement;
if (typeof target?.value === 'string') {
this._updateProperty('name', target.value);
}
}
}
private _updateProperty(propertyName: string, value: unknown) {
this._userContext?.update({ [propertyName]: value });
}
private _renderContentStartNodes() {
if (!this._user) return;
if (this._user.contentStartNodes.length < 1)
return html`
<uui-ref-node name="Content Root">
<uui-icon slot="icon" name="folder"></uui-icon>
</uui-ref-node>
`;
//TODO Render the name of the content start node instead of it's key.
return repeat(
this._user.contentStartNodes,
(node) => node,
(node) => {
return html`
<uui-ref-node name=${node}>
<uui-icon slot="icon" name="folder"></uui-icon>
</uui-ref-node>
`;
}
);
}
private _renderLeftColumn() {
if (!this._user) return nothing;
return html` <uui-box>
<div slot="headline">Profile</div>
<uui-form-layout-item style="margin-top: 0">
<uui-label for="email">Email</uui-label>
<uui-input name="email" label="email" readonly value=${this._user.email}></uui-input>
</uui-form-layout-item>
<uui-form-layout-item style="margin-bottom: 0">
<uui-label for="language">Language</uui-label>
<uui-select name="language" label="language" .options=${this._languages}> </uui-select>
</uui-form-layout-item>
<umb-editor-property-layout label="Email">
<uui-input slot="editor" name="email" label="email" readonly value=${this._user.email}></uui-input>
</umb-editor-property-layout>
<umb-editor-property-layout label="Language">
<uui-select slot="editor" name="language" label="language" .options=${this._languages}> </uui-select>
</umb-editor-property-layout>
</uui-box>
<uui-box>
<div slot="headline">Assign access</div>
<div id="assign-access">
<div slot="headline">Assign access</div>
<div>
<b>Groups</b>
<div class="faded-text">Add groups to assign access and permissions</div>
</div>
<div>
<b>Content start nodes</b>
<div class="faded-text">Limit the content tree to specific start nodes</div>
<umb-property-editor-ui-content-picker></umb-property-editor-ui-content-picker>
</div>
<div>
<b>Media start nodes</b>
<div class="faded-text">Limit the media library to specific start nodes</div>
<umb-property-editor-ui-content-picker></umb-property-editor-ui-content-picker>
</div>
<umb-editor-property-layout label="Groups" description="Add groups to assign access and permissions">
<umb-input-user-group
slot="editor"
.value=${this._user.userGroups}
@change=${(e: any) => this._updateProperty('userGroups', e.target.value)}></umb-input-user-group>
</umb-editor-property-layout>
<umb-editor-property-layout
label="Content start node"
description="Limit the content tree to specific start nodes">
<umb-property-editor-ui-content-picker
.value=${this._user.contentStartNodes}
@property-editor-change=${(e: any) => this._updateProperty('contentStartNodes', e.target.value)}
slot="editor"></umb-property-editor-ui-content-picker>
</umb-editor-property-layout>
<umb-editor-property-layout
label="Media start nodes"
description="Limit the media library to specific start nodes">
<b slot="editor">NEED MEDIA PICKER</b>
</umb-editor-property-layout>
</div>
</uui-box>
<uui-box>
<div slot="headline">Access</div>
<uui-box headline="Access">
<div slot="header" class="faded-text">
Based on the assigned groups and start nodes, the user has access to the following nodes
</div>
<b>Content</b>
<div class="access-content">
<uui-icon name="folder"></uui-icon>
<span>Content Root</span>
</div>
${this._renderContentStartNodes()}
<hr />
<b>Media</b>
<div class="access-content">
<uui-icon name="folder"></uui-icon>
<span>Media Root</span>
</div>
<uui-ref-node name="Media Root">
<uui-icon slot="icon" name="folder"></uui-icon>
</uui-ref-node>
</uui-box>`;
}
private renderRightColumn() {
private _renderRightColumn() {
if (!this._user || !this._userStore) return nothing;
const statusLook = getTagLookAndColor(this._user.status);
@@ -300,17 +325,6 @@ export class UmbEditorUserElement extends UmbContextProviderMixin(UmbContextCons
</uui-box>`;
}
// TODO. find a way where we don't have to do this for all editors.
private _handleInput(event: UUIInputEvent) {
if (event instanceof UUIInputEvent) {
const target = event.composedPath()[0] as UUIInputElement;
if (typeof target?.value === 'string') {
this._userContext?.update({ name: target.value });
}
}
}
render() {
if (!this._user) return html`User not found`;
@@ -318,8 +332,8 @@ export class UmbEditorUserElement extends UmbContextProviderMixin(UmbContextCons
<umb-editor-entity-layout alias="Umb.Editor.User">
<uui-input id="name" slot="name" .value=${this._userName} @input="${this._handleInput}"></uui-input>
<div id="main">
<div id="left-column">${this.renderLeftColumn()}</div>
<div id="right-column">${this.renderRightColumn()}</div>
<div id="left-column">${this._renderLeftColumn()}</div>
<div id="right-column">${this._renderRightColumn()}</div>
</div>
</umb-editor-entity-layout>
`;
@@ -330,6 +344,6 @@ export default UmbEditorUserElement;
declare global {
interface HTMLElementTagNameMap {
'umb-editor-view-users-user-details': UmbEditorUserElement;
'umb-editor-user': UmbEditorUserElement;
}
}

View File

@@ -0,0 +1,20 @@
import { expect, fixture, html } from '@open-wc/testing';
//TODO: Test has been commented out while we figure out how to setup import maps for the test environment
// import { defaultA11yConfig } from '@umbraco-cms/test-utils';
// import UmbEditorUserElement from './editor-user.element';
// describe('UmbEditorUserElement', () => {
// let element: UmbEditorUserElement;
// beforeEach(async () => {
// element = await fixture(html`<umb-editor-user></umb-editor-user>`);
// });
// it('is defined with its own instance', () => {
// expect(element).to.be.instanceOf(UmbEditorUserElement);
// });
// it('passes the a11y audit', async () => {
// await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
// });
// });

View File

@@ -17,6 +17,9 @@ export class UmbUserContext {
updateDate: '8/27/2022',
createDate: '9/19/2022',
failedLoginAttempts: 0,
userGroups: [],
contentStartNodes: [],
mediaStartNodes: [],
});
public readonly data: Observable<UserDetails> = this._data.asObservable();

View File

@@ -1,6 +1,6 @@
import type { ManifestPropertyAction, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestPropertyAction } from '@umbraco-cms/models';
export const manifests: Array<ManifestWithLoader<ManifestPropertyAction>> = [
export const manifests: Array<ManifestPropertyAction> = [
{
type: 'propertyAction',
alias: 'Umb.PropertyAction.Copy',

View File

@@ -1,6 +1,6 @@
import type { ManifestPropertyEditorUI, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestPropertyEditorUI } from '@umbraco-cms/models';
export const manifests: Array<ManifestWithLoader<ManifestPropertyEditorUI>> = [
export const manifests: Array<ManifestPropertyEditorUI> = [
{
type: 'propertyEditorUI',
alias: 'Umb.PropertyEditorUI.BlockList',

View File

@@ -1,7 +1,7 @@
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import type { ManifestDashboard, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestDashboard } from '@umbraco-cms/models';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
@customElement('umb-content-section')
@@ -15,7 +15,7 @@ export class UmbContentSection extends LitElement {
}
private _registerDashboards() {
const dashboards: Array<ManifestWithLoader<ManifestDashboard>> = [
const dashboards: Array<ManifestDashboard> = [
{
type: 'dashboard',
alias: 'Umb.Dashboard.Welcome',

View File

@@ -1,6 +1,6 @@
import type { ManifestSection, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestSection } from '@umbraco-cms/models';
export const manifests: Array<ManifestWithLoader<ManifestSection>> = [
export const manifests: Array<ManifestSection> = [
{
type: 'section',
alias: 'Umb.Section.Content',

View File

@@ -2,7 +2,7 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import type { ManifestDashboard, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestDashboard } from '@umbraco-cms/models';
@customElement('umb-media-section')
export class UmbMediaSection extends LitElement {
@@ -14,7 +14,7 @@ export class UmbMediaSection extends LitElement {
}
private _registerDashboards() {
const dashboards: Array<ManifestWithLoader<ManifestDashboard>> = [
const dashboards: Array<ManifestDashboard> = [
{
type: 'dashboard',
alias: 'Umb.Dashboard.MediaManagement',

View File

@@ -2,7 +2,7 @@ import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import type { ManifestSectionView, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestSectionView } from '@umbraco-cms/models';
@customElement('umb-section-packages')
export class UmbSectionPackages extends UmbContextConsumerMixin(LitElement) {
@@ -13,7 +13,7 @@ export class UmbSectionPackages extends UmbContextConsumerMixin(LitElement) {
}
private _registerSectionViews() {
const manifests: Array<ManifestWithLoader<ManifestSectionView>> = [
const manifests: Array<ManifestSectionView> = [
{
type: 'sectionView',
alias: 'Umb.SectionView.Packages.Repo',

View File

@@ -1,7 +1,7 @@
import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import type { ManifestDashboard, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestDashboard } from '@umbraco-cms/models';
@customElement('umb-section-settings')
export class UmbSectionSettingsElement extends LitElement {
@@ -12,7 +12,7 @@ export class UmbSectionSettingsElement extends LitElement {
}
private _registerDashboards() {
const dashboards: Array<ManifestWithLoader<ManifestDashboard>> = [
const dashboards: Array<ManifestDashboard> = [
{
type: 'dashboard',
alias: 'Umb.Dashboard.SettingsWelcome',

View File

@@ -1,7 +1,7 @@
import { html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import type { ManifestSectionView, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestSectionView } from '@umbraco-cms/models';
@customElement('umb-section-users')
export class UmbSectionUsersElement extends LitElement {
@@ -12,7 +12,7 @@ export class UmbSectionUsersElement extends LitElement {
}
private _registerSectionViews() {
const manifests: Array<ManifestWithLoader<ManifestSectionView>> = [
const manifests: Array<ManifestSectionView> = [
{
type: 'sectionView',
alias: 'Umb.SectionView.Users.Users',

View File

@@ -0,0 +1,22 @@
import { expect } from '@open-wc/testing';
import { InterfaceColor, InterfaceLook } from '@umbraco-ui/uui-base/lib/types';
import type { UserStatus } from './user-extensions';
import { getTagLookAndColor } from './user-extensions';
describe('UmbUserExtensions', () => {
it('returns correct look and color from a status string', () => {
const testCases: { status: UserStatus; look: InterfaceLook; color: InterfaceColor }[] = [
{ status: 'enabled', look: 'primary', color: 'positive' },
{ status: 'inactive', look: 'primary', color: 'warning' },
{ status: 'invited', look: 'primary', color: 'warning' },
{ status: 'disabled', look: 'primary', color: 'danger' },
];
testCases.forEach((testCase) => {
const { look, color } = getTagLookAndColor(testCase.status);
expect(look).to.equal(testCase.look);
expect(color).to.equal(testCase.color);
});
});
});

View File

@@ -1,23 +1,25 @@
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { css, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { Subscription } from 'rxjs';
import { UmbUserGroupStore } from '../../../../../core/stores/user/user-group.store';
import UmbTableElement, {
import {
UmbTableColumn,
UmbTableConfig,
UmbTableDeselectedEvent,
UmbTableElement,
UmbTableItem,
UmbTableOrderedEvent,
UmbTableSelectedEvent,
} from '../../../../components/table/table.element';
} from '@umbraco-cms/components/table';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import type { UserGroupDetails } from '@umbraco-cms/models';
import './user-group-table-name-column-layout.element';
import './user-group-table-sections-column-layout.element';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbUserGroupStore } from '@umbraco-cms/stores/user/user-group.store';
@customElement('umb-editor-view-user-groups')
export class UmbEditorViewUserGroupsElement extends UmbContextConsumerMixin(LitElement) {
export class UmbEditorViewUserGroupsElement extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
static styles = [
UUITextStyles,
css`
@@ -47,6 +49,7 @@ export class UmbEditorViewUserGroupsElement extends UmbContextConsumerMixin(LitE
{
name: 'Sections',
alias: 'userGroupSections',
elementName: 'umb-user-group-table-sections-column-layout',
},
{
name: 'Content start node',
@@ -65,24 +68,21 @@ export class UmbEditorViewUserGroupsElement extends UmbContextConsumerMixin(LitE
private _selection: Array<string> = [];
private _userGroupStore?: UmbUserGroupStore;
private _userGroupsSubscription?: Subscription;
private _selectionSubscription?: Subscription;
connectedCallback(): void {
super.connectedCallback();
this.consumeContext('umbUserGroupStore', (userStore: UmbUserGroupStore) => {
this._userGroupStore = userStore;
this._observeUsers();
this._observeUserGroups();
});
}
private _observeUsers() {
this._userGroupsSubscription?.unsubscribe();
this._userGroupsSubscription = this._userGroupStore?.getAll().subscribe((userGroups) => {
this._userGroups = userGroups;
console.log('user groups', userGroups);
private _observeUserGroups() {
if (!this._userGroupStore) return;
this.observe<UserGroupDetails[]>(this._userGroupStore.getAll(), (userGroups) => {
this._userGroups = userGroups;
this._createTableItems(this._userGroups);
});
}
@@ -105,11 +105,11 @@ export class UmbEditorViewUserGroupsElement extends UmbContextConsumerMixin(LitE
},
{
columnAlias: 'userGroupContentStartNode',
value: userGroup.contentStartNode,
value: userGroup.contentStartNode || 'Content root',
},
{
columnAlias: 'userGroupMediaStartNode',
value: userGroup.mediaStartNode,
value: userGroup.mediaStartNode || 'Media root',
},
],
};
@@ -133,13 +133,6 @@ export class UmbEditorViewUserGroupsElement extends UmbContextConsumerMixin(LitE
console.log(`fetch users, order column: ${orderingColumn}, desc: ${orderingDesc}`);
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._userGroupsSubscription?.unsubscribe();
this._selectionSubscription?.unsubscribe();
}
render() {
return html`
<umb-table

View File

@@ -1,6 +1,6 @@
import { html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { UmbTableItem } from '../../../../components/table/table.element';
import { UmbTableItem } from '@umbraco-cms/components/table';
@customElement('umb-user-group-table-name-column-layout')
export class UmbUserGroupTableNameColumnLayoutElement extends LitElement {

View File

@@ -0,0 +1,42 @@
import { html, LitElement } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import type { ManifestSection } from '@umbraco-cms/models';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbTableItem } from '@umbraco-cms/components/table';
@customElement('umb-user-group-table-sections-column-layout')
export class UmbUserGroupTableSectionsColumnLayoutElement extends UmbObserverMixin(LitElement) {
@property({ type: Object, attribute: false })
item!: UmbTableItem;
@property({ attribute: false })
value!: any;
@state()
private _sectionsNames: Array<string> = [];
updated(changedProperties: Map<string, any>) {
if (changedProperties.has('value')) {
this.observeSectionNames();
}
}
private observeSectionNames() {
this.observe<Array<ManifestSection>>(umbExtensionsRegistry.extensionsOfType('section'), (sections) => {
this._sectionsNames = sections.filter((x) => this.value.includes(x.alias)).map((x) => x.meta.label || x.name);
});
}
render() {
return html`${this._sectionsNames.join(', ')}`;
}
}
export default UmbUserGroupTableSectionsColumnLayoutElement;
declare global {
interface HTMLElementTagNameMap {
'umb-user-group-table-sections-column-layout': UmbUserGroupTableSectionsColumnLayoutElement;
}
}

View File

@@ -0,0 +1,212 @@
import { css, html, nothing } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, query, state } from 'lit/decorators.js';
import { UUIInputPasswordElement } from '@umbraco-ui/uui';
import { UmbInputPickerUserGroupElement } from '@umbraco-cms/components/input-user-group/input-user-group.element';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import type { UserDetails } from '@umbraco-cms/models';
import { UmbModalLayoutElement, UmbNotificationDefaultData, UmbNotificationService } from '@umbraco-cms/services';
import { UmbUserStore } from '@umbraco-cms/stores/user/user.store';
export type UsersViewType = 'list' | 'grid';
@customElement('umb-editor-view-users-create')
export class UmbEditorViewUsersCreateElement extends UmbContextConsumerMixin(UmbModalLayoutElement) {
static styles = [
UUITextStyles,
css`
:host {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
uui-box {
max-width: 500px;
}
uui-form-layout-item {
display: flex;
flex-direction: column;
}
uui-input,
uui-input-password {
width: 100%;
}
form {
display: flex;
flex-direction: column;
box-sizing: border-box;
}
uui-form-layout-item {
margin-bottom: 0;
}
uui-textarea {
--uui-textarea-min-height: 100px;
}
/* TODO: Style below is to fix a11y contrast issue, find a proper solution */
[slot='description'] {
color: black;
}
`,
];
@query('#form')
private _form!: HTMLFormElement;
@state()
private _createdUser?: UserDetails;
protected _userStore?: UmbUserStore;
private _notificationService?: UmbNotificationService;
connectedCallback(): void {
super.connectedCallback();
this.consumeAllContexts(['umbUserStore', 'umbNotificationService'], (instances) => {
this._userStore = instances['umbUserStore'];
this._notificationService = instances['umbNotificationService'];
});
}
private _handleSubmit(e: Event) {
e.preventDefault();
const form = e.target as HTMLFormElement;
if (!form) return;
const isValid = form.checkValidity();
if (!isValid) return;
const formData = new FormData(form);
console.log('formData', formData);
const name = formData.get('name') as string;
const email = formData.get('email') as string;
//TODO: How should we handle pickers forms?
const userGroupPicker = form.querySelector('#userGroups') as UmbInputPickerUserGroupElement;
const userGroups = userGroupPicker?.value || [];
this._userStore?.invite(name, email, '', userGroups).then((user) => {
if (user) {
this._createdUser = user;
}
});
}
private _copyPassword() {
const passwordInput = this.shadowRoot?.querySelector('#password') as UUIInputPasswordElement;
if (!passwordInput || typeof passwordInput.value !== 'string') return;
navigator.clipboard.writeText(passwordInput.value);
const data: UmbNotificationDefaultData = { message: 'Password copied' };
this._notificationService?.peek('positive', { data });
}
private _submitForm() {
this._form?.requestSubmit();
}
private _closeModal() {
this.modalHandler?.close();
}
private _resetForm() {
this._createdUser = undefined;
}
private _goToProfile() {
if (!this._createdUser) return;
this._closeModal();
history.pushState(null, '', '/section/users/view/users/user/' + this._createdUser?.key); //TODO: URL Should be dynamic
}
private _renderForm() {
return html` <h1>Create user</h1>
<p style="margin-top: 0">
Create new users to give them access to Umbraco. When a user is created a password will be generated that you
can share with the user.
</p>
<uui-form>
<form id="form" name="form" @submit="${this._handleSubmit}">
<uui-form-layout-item>
<uui-label id="nameLabel" slot="label" for="name" required>Name</uui-label>
<uui-input id="name" label="name" type="text" name="name" required></uui-input>
</uui-form-layout-item>
<uui-form-layout-item>
<uui-label id="emailLabel" slot="label" for="email" required>Email</uui-label>
<uui-input id="email" label="email" type="email" name="email" required></uui-input>
</uui-form-layout-item>
<uui-form-layout-item>
<uui-label id="userGroupsLabel" slot="label" for="userGroups" required>User group</uui-label>
<span slot="description">Add groups to assign access and permissions</span>
<umb-input-user-group id="userGroups" name="userGroups"></umb-input-user-group>
</uui-form-layout-item>
</form>
</uui-form>`;
}
private _renderPostCreate() {
if (!this._createdUser) return nothing;
return html`<div>
<h1><b style="color: var(--uui-color-interactive-emphasis)">${this._createdUser.name}</b> has been created</h1>
<p>The new user has successfully been created. To log in to Umbraco use the password below</p>
<uui-label for="password">Password</uui-label>
<uui-input-password
id="password"
label="password"
name="password"
value="${'PUT GENERATED PASSWORD HERE'}"
readonly>
<!-- The button should be placed in the append part of the input, but that doesn't work with password inputs for now. -->
<uui-button slot="prepend" compact label="copy" @click=${this._copyPassword}></uui-button>
</uui-input-password>
</div>`;
}
render() {
return html`<uui-dialog-layout>
${this._createdUser ? this._renderPostCreate() : this._renderForm()}
${this._createdUser
? html`
<uui-button
@click=${this._closeModal}
style="margin-right: auto"
slot="actions"
label="Close"
look="secondary"></uui-button>
<uui-button
@click=${this._resetForm}
slot="actions"
label="Create another user"
look="secondary"></uui-button>
<uui-button @click=${this._goToProfile} slot="actions" label="Go to profile" look="primary"></uui-button>
`
: html`
<uui-button
@click=${this._closeModal}
style="margin-right: auto"
slot="actions"
label="Cancel"
look="secondary"></uui-button>
<uui-button
@click="${this._submitForm}"
slot="actions"
type="submit"
label="Create user"
look="primary"></uui-button>
`}
</uui-dialog-layout>`;
}
}
export default UmbEditorViewUsersCreateElement;
declare global {
interface HTMLElementTagNameMap {
'umb-editor-view-users-create': UmbEditorViewUsersCreateElement;
}
}

View File

@@ -0,0 +1,19 @@
import { expect, fixture, html } from '@open-wc/testing';
//TODO: Test has been commented out while we figure out how to setup import maps for the test environment
// import { defaultA11yConfig } from '@umbraco-cms/test-utils';
// import UmbEditorViewUsersCreateElement from './editor-view-users-create.element';
// describe('UmbEditorViewUsersCreateElement', () => {
// let element: UmbEditorViewUsersCreateElement;
// beforeEach(async () => {
// element = await fixture(html`<umb-editor-view-users-create></umb-editor-view-users-create>`);
// });
// it('is defined with its own instance', () => {
// expect(element).to.be.instanceOf(UmbEditorViewUsersCreateElement);
// });
// it('passes the a11y audit', async () => {
// await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
// });
// });

View File

@@ -1,11 +1,11 @@
import { css, html, nothing } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, query, state } from 'lit/decorators.js';
import { UmbModalLayoutElement } from '../../../../../core/services/modal/layouts/modal-layout.element';
import { UmbUserStore } from '../../../../../core/stores/user/user.store';
import { UmbNotificationService } from '../../../../../core/services/notification';
import { UmbInputPickerUserGroupElement } from '@umbraco-cms/components/input-user-group/input-user-group.element';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import type { UserDetails } from '@umbraco-cms/models';
import { UmbModalLayoutElement } from '@umbraco-cms/services';
import { UmbUserStore } from '@umbraco-cms/stores/user/user.store';
export type UsersViewType = 'list' | 'grid';
@customElement('umb-editor-view-users-invite')
@@ -41,11 +41,14 @@ export class UmbEditorViewUsersInviteElement extends UmbContextConsumerMixin(Umb
uui-textarea {
--uui-textarea-min-height: 100px;
}
uui
/* TODO: Style below is to fix a11y contrast issue, find a proper solution */
[slot='description'] {
color: black;
}
`,
];
@query('#invite-form')
@query('#form')
private _form!: HTMLFormElement;
@state()
@@ -74,10 +77,13 @@ export class UmbEditorViewUsersInviteElement extends UmbContextConsumerMixin(Umb
const name = formData.get('name') as string;
const email = formData.get('email') as string;
const userGroup = formData.get('userGroup') as string;
//TODO: How should we handle pickers forms?
const userGroupPicker = form.querySelector('#userGroups') as UmbInputPickerUserGroupElement;
const userGroups = userGroupPicker?.value || [];
const message = formData.get('message') as string;
this._userStore?.invite(name, email, message, [userGroup]).then((user) => {
this._userStore?.invite(name, email, message, userGroups).then((user) => {
if (user) {
this._invitedUser = user;
}
@@ -110,22 +116,22 @@ export class UmbEditorViewUsersInviteElement extends UmbContextConsumerMixin(Umb
how to log in to Umbraco. Invites last for 72 hours.
</p>
<uui-form>
<form id="invite-form" name="invite-form" @submit="${this._handleSubmit}">
<form id="form" name="form" @submit="${this._handleSubmit}">
<uui-form-layout-item>
<uui-label slot="label" for="name" required>Name</uui-label>
<uui-label id="nameLabel" slot="label" for="name" required>Name</uui-label>
<uui-input id="name" label="name" type="text" name="name" required></uui-input>
</uui-form-layout-item>
<uui-form-layout-item>
<uui-label slot="label" for="email" required>Email</uui-label>
<uui-label id="emailLabel" slot="label" for="email" required>Email</uui-label>
<uui-input id="email" label="email" type="email" name="email" required></uui-input>
</uui-form-layout-item>
<uui-form-layout-item>
<uui-label slot="label" for="userGroup" required>User group</uui-label>
<uui-label id="userGroupsLabel" slot="label" for="userGroups" required>User group</uui-label>
<span slot="description">Add groups to assign access and permissions</span>
<b>ADD USER GROUP PICKER HERE</b>
<umb-input-user-group id="userGroups" name="userGroups"></umb-input-user-group>
</uui-form-layout-item>
<uui-form-layout-item>
<uui-label slot="label" for="message" required>Message</uui-label>
<uui-label id="messageLabel" slot="label" for="message" required>Message</uui-label>
<uui-textarea id="message" label="message" name="message" required></uui-textarea>
</uui-form-layout-item>
</form>
@@ -135,7 +141,7 @@ export class UmbEditorViewUsersInviteElement extends UmbContextConsumerMixin(Umb
private _renderPostInvite() {
if (!this._invitedUser) return nothing;
return html`<div id="post-invite">
return html`<div>
<h1><b style="color: var(--uui-color-interactive-emphasis)">${this._invitedUser.name}</b> has been invited</h1>
<p>An invitation has been sent to the new user with details about how to log in to Umbraco.</p>
</div>`;

View File

@@ -0,0 +1,19 @@
import { expect, fixture, html } from '@open-wc/testing';
//TODO: Test has been commented out while we figure out how to setup import maps for the test environment
// import UmbEditorViewUsersInviteElement from './editor-view-users-invite.element';
// import { defaultA11yConfig } from '@umbraco-cms/test-utils';
// describe('UmbEditorViewUsersInviteElement', () => {
// let element: UmbEditorViewUsersInviteElement;
// beforeEach(async () => {
// element = await fixture(html`<umb-editor-view-users-invite></umb-editor-view-users-invite>`);
// });
// it('is defined with its own instance', () => {
// expect(element).to.be.instanceOf(UmbEditorViewUsersInviteElement);
// });
// it('passes the a11y audit', async () => {
// await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
// });
// });

View File

@@ -1,20 +1,23 @@
import { css, html, LitElement, nothing } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, state } from 'lit/decorators.js';
import { Subscription } from 'rxjs';
import { IRoute } from 'router-slot';
import { UUIPopoverElement } from '@umbraco-ui/uui';
import type { UmbSectionViewUsersElement } from './section-view-users.element';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import './list-view-layouts/table/editor-view-users-table.element';
import './list-view-layouts/grid/editor-view-users-grid.element';
import './editor-view-users-selection.element';
import './editor-view-users-invite.element';
import { IRoute } from 'router-slot';
import { UUIPopoverElement } from '@umbraco-ui/uui';
import { UmbModalService } from '../../../../../core/services/modal';
import UmbSectionViewUsersElement from './section-view-users.element';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import './editor-view-users-create.element';
import { UmbModalService } from '@umbraco-cms/services';
export type UsersViewType = 'list' | 'grid';
@customElement('umb-editor-view-users-overview')
export class UmbEditorViewUsersOverviewElement extends UmbContextConsumerMixin(LitElement) {
export class UmbEditorViewUsersOverviewElement extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
static styles = [
UUITextStyles,
css`
@@ -38,7 +41,7 @@ export class UmbEditorViewUsersOverviewElement extends UmbContextConsumerMixin(L
#user-list-top-bar {
padding: var(--uui-size-space-4) var(--uui-size-space-6);
background-color: var(--uui-color-surface-alt);
background-color: var(--uui-color-background);
display: flex;
justify-content: space-between;
white-space: nowrap;
@@ -80,6 +83,9 @@ export class UmbEditorViewUsersOverviewElement extends UmbContextConsumerMixin(L
@state()
private _selection: Array<string> = [];
@state()
private isCloud = false; //NOTE: Used to show either invite or create user buttons and views.
@state()
private _routes: IRoute[] = [
{
@@ -92,24 +98,21 @@ export class UmbEditorViewUsersOverviewElement extends UmbContextConsumerMixin(L
},
{
path: '**',
redirectTo: 'section/users/view/users/overview/grid', //TODO: this should be dynamic
redirectTo: 'grid',
},
];
private _usersContext?: UmbSectionViewUsersElement;
private _selectionSubscription?: Subscription;
private _modalService?: UmbModalService;
private _inputTimer?: NodeJS.Timeout;
private _inputTimerAmount = 500;
connectedCallback(): void {
super.connectedCallback();
this.consumeContext('umbUsersContext', (usersContext: UmbSectionViewUsersElement) => {
this._usersContext = usersContext;
this._selectionSubscription?.unsubscribe();
this._selectionSubscription = this._usersContext?.selection.subscribe((selection: Array<string>) => {
this._selection = selection;
});
this._observeSelection();
});
this.consumeContext('umbModalService', (modalService: UmbModalService) => {
@@ -117,10 +120,9 @@ export class UmbEditorViewUsersOverviewElement extends UmbContextConsumerMixin(L
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._selectionSubscription?.unsubscribe();
private _observeSelection() {
if (!this._usersContext) return;
this.observe<Array<string>>(this._usersContext.selection, (selection) => (this._selection = selection));
}
private _toggleViewType() {
@@ -146,19 +148,39 @@ export class UmbEditorViewUsersOverviewElement extends UmbContextConsumerMixin(L
}
}
private _showInvite() {
const invite = document.createElement('umb-editor-view-users-invite');
private _updateSearch(event: InputEvent) {
const target = event.target as HTMLInputElement;
const search = target.value || '';
clearTimeout(this._inputTimer);
this._inputTimer = setTimeout(() => this._refreshUsers(search), this._inputTimerAmount);
}
this._modalService?.open(invite, { type: 'dialog' });
private _refreshUsers(search: string) {
if (!this._usersContext) return;
this._usersContext.setSearch(search);
}
private _showInviteOrCreate() {
let modal = undefined;
if (this.isCloud) {
modal = document.createElement('umb-editor-view-users-invite');
} else {
modal = document.createElement('umb-editor-view-users-create');
}
this._modalService?.open(modal, { type: 'dialog' });
}
render() {
return html`
<div id="sticky-top">
<div id="user-list-top-bar">
<uui-button @click=${this._showInvite} label="Invite user" look="outline"></uui-button>
<uui-input label="search" id="input-search"></uui-input>
<uui-button
@click=${this._showInviteOrCreate}
label=${this.isCloud ? 'Invite' : 'Create' + ' user'}
look="outline"></uui-button>
<uui-input @input=${this._updateSearch} label="search" id="input-search"></uui-input>
<div>
<!-- TODO: consider making this a shared component, as we need similar for other locations, example media library, members. -->
<uui-popover margin="8">
<uui-button @click=${this._handleTogglePopover} slot="trigger" label="status">
Status: <b>All</b>
@@ -197,11 +219,11 @@ export class UmbEditorViewUsersOverviewElement extends UmbContextConsumerMixin(L
</uui-button>
</div>
</div>
${this._renderSelection()}
</div>
<router-slot .routes=${this._routes}></router-slot>
${this._renderSelection()}
`;
}
}

View File

@@ -0,0 +1,19 @@
import { expect, fixture, html } from '@open-wc/testing';
//TODO: Test has been commented out while we figure out how to setup import maps for the test environment
// import { defaultA11yConfig } from '@umbraco-cms/test-utils';
// import UmbEditorViewUsersOverviewElement from './editor-view-users-overview.element';
// describe('UmbEditorViewUsersOverviewElement', () => {
// let element: UmbEditorViewUsersOverviewElement;
// beforeEach(async () => {
// element = await fixture(html`<umb-editor-view-users-overview></umb-editor-view-users-overview>`);
// });
// it('is defined with its own instance', () => {
// expect(element).to.be.instanceOf(UmbEditorViewUsersOverviewElement);
// });
// it('passes the a11y audit', async () => {
// await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
// });
// });

View File

@@ -1,13 +1,13 @@
import { css, html, LitElement } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, state } from 'lit/decorators.js';
import { Subscription } from 'rxjs';
import type { UmbUserStore } from '../../../../../core/stores/user/user.store';
import { UmbSectionViewUsersElement } from './section-view-users.element';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbUserStore } from '@umbraco-cms/stores/user/user.store';
@customElement('umb-editor-view-users-selection')
export class UmbEditorViewUsersSelectionElement extends UmbContextConsumerMixin(LitElement) {
export class UmbEditorViewUsersSelectionElement extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
static styles = [
UUITextStyles,
css`
@@ -31,9 +31,7 @@ export class UmbEditorViewUsersSelectionElement extends UmbContextConsumerMixin(
private _totalUsers = 0;
private _usersContext?: UmbSectionViewUsersElement;
private _selectionSubscription?: Subscription;
private _userStore?: UmbUserStore;
private _totalUsersSubscription?: Subscription;
connectedCallback(): void {
super.connectedCallback();
@@ -49,23 +47,13 @@ export class UmbEditorViewUsersSelectionElement extends UmbContextConsumerMixin(
}
private _observeSelection() {
this._selectionSubscription?.unsubscribe();
this._selectionSubscription = this._usersContext?.selection.subscribe((selection: Array<string>) => {
this._selection = selection;
});
if (!this._usersContext) return;
this.observe<Array<string>>(this._usersContext.selection, (selection) => (this._selection = selection));
}
private _observeTotalUsers() {
this._totalUsersSubscription?.unsubscribe();
this._userStore?.totalUsers.subscribe((totalUsers: number) => {
this._totalUsers = totalUsers;
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._selectionSubscription?.unsubscribe();
this._totalUsersSubscription?.unsubscribe();
if (!this._userStore) return;
this.observe<number>(this._userStore.totalUsers, (totalUsers) => (this._totalUsers = totalUsers));
}
private _handleClearSelection() {

View File

@@ -0,0 +1,19 @@
import { expect, fixture, html } from '@open-wc/testing';
//TODO: Test has been commented out while we figure out how to setup import maps for the test environment
// import UmbEditorViewUsersSelectionElement from './editor-view-users-selection.element';
// import { defaultA11yConfig } from '@umbraco-cms/test-utils';
// describe('UmbEditorViewUsersSelectionElement', () => {
// let element: UmbEditorViewUsersSelectionElement;
// beforeEach(async () => {
// element = await fixture(html`<umb-editor-view-users-selection></umb-editor-view-users-selection>`);
// });
// it('is defined with its own instance', () => {
// expect(element).to.be.instanceOf(UmbEditorViewUsersSelectionElement);
// });
// it('passes the a11y audit', async () => {
// await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
// });
// });

View File

@@ -2,16 +2,16 @@ import { css, html, LitElement, nothing } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { Subscription } from 'rxjs';
import { ifDefined } from 'lit-html/directives/if-defined.js';
import UmbSectionViewUsersElement from '../../section-view-users.element';
import { UmbUserStore } from '../../../../../../../core/stores/user/user.store';
import type { UmbSectionViewUsersElement } from '../../section-view-users.element';
import { getTagLookAndColor } from '../../../../user-extensions';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import type { UserDetails, UserEntity } from '@umbraco-cms/models';
import type { UserDetails, UserEntity, UserGroupDetails, UserGroupEntity } from '@umbraco-cms/models';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbUserGroupStore } from 'src/core/stores/user/user-group.store';
@customElement('umb-editor-view-users-grid')
export class UmbEditorViewUsersGridElement extends UmbContextConsumerMixin(LitElement) {
export class UmbEditorViewUsersGridElement extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
static styles = [
UUITextStyles,
css`
@@ -25,8 +25,8 @@ export class UmbEditorViewUsersGridElement extends UmbContextConsumerMixin(LitEl
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: var(--uui-size-space-4);
padding: var(--uui-size-space-4);
padding-top: 0;
margin: var(--uui-size-layout-1);
margin-top: var(--uui-size-space-2);
}
uui-card-user {
@@ -46,44 +46,42 @@ export class UmbEditorViewUsersGridElement extends UmbContextConsumerMixin(LitEl
@state()
private _selection: Array<string> = [];
private _userStore?: UmbUserStore;
@state()
private _userGroups: Array<UserGroupEntity> = [];
private _userGroupStore?: UmbUserGroupStore;
private _usersContext?: UmbSectionViewUsersElement;
private _usersSubscription?: Subscription;
private _selectionSubscription?: Subscription;
connectedCallback(): void {
super.connectedCallback();
constructor() {
super();
this.consumeContext('umbUserStore', (userStore: UmbUserStore) => {
this._userStore = userStore;
this.consumeAllContexts(['umbUserGroupStore', 'umbUsersContext'], (instances) => {
this._userGroupStore = instances['umbUserGroupStore'];
this._usersContext = instances['umbUsersContext'];
this._observeUsers();
});
this.consumeContext('umbUsersContext', (usersContext: UmbSectionViewUsersElement) => {
this._usersContext = usersContext;
this._observeUserGroups();
this._observeSelection();
});
}
private _observeUsers() {
this._usersSubscription?.unsubscribe();
this._usersSubscription = this._userStore?.getAll().subscribe((users) => {
if (!this._usersContext) return;
this.observe<Array<UserDetails>>(this._usersContext.users, (users) => {
this._users = users;
});
}
private _observeSelection() {
this._selectionSubscription?.unsubscribe();
this._selectionSubscription = this._usersContext?.selection.subscribe((selection: Array<string>) => {
this._selection = selection;
});
private _observeUserGroups() {
if (!this._userGroupStore) return;
this.observe<Array<UserGroupDetails>>(
this._userGroupStore.getAll(),
(userGroups) => (this._userGroups = userGroups)
);
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._usersSubscription?.unsubscribe();
this._selectionSubscription?.unsubscribe();
private _observeSelection() {
if (!this._usersContext) return;
this.observe<Array<string>>(this._usersContext.selection, (selection) => (this._selection = selection));
}
private _isSelected(key: string) {
@@ -103,8 +101,16 @@ export class UmbEditorViewUsersGridElement extends UmbContextConsumerMixin(LitEl
this._usersContext?.deselect(user.key);
}
private _getUserGroupNames(keys: Array<string>) {
return keys
.map((key: string) => {
return this._userGroups.find((x) => x.key === key)?.name;
})
.join(', ');
}
private renderUserCard(user: UserDetails) {
if (!this._userStore) return;
if (!this._usersContext) return;
const statusLook = getTagLookAndColor(user.status);
@@ -126,7 +132,7 @@ export class UmbEditorViewUsersGridElement extends UmbContextConsumerMixin(LitEl
${user.status}
</uui-tag>`
: nothing}
<div>USER GROUPS NOT IMPLEMENTED</div>
<div>${this._getUserGroupNames(user.userGroups)}</div>
${user.lastLoginDate
? html`<div class="user-login-time">
<div>Last login</div>

View File

@@ -0,0 +1,18 @@
import { expect, fixture, html } from '@open-wc/testing';
import { UmbEditorViewUsersGridElement } from './editor-view-users-grid.element';
import { defaultA11yConfig } from '@umbraco-cms/test-utils';
describe('UmbEditorViewUsersCreateElement', () => {
let element: UmbEditorViewUsersGridElement;
beforeEach(async () => {
element = await fixture(html`<umb-editor-view-users-grid></umb-editor-view-users-grid>`);
});
it('is defined with its own instance', () => {
expect(element).to.be.instanceOf(UmbEditorViewUsersGridElement);
});
it('passes the a11y audit', async () => {
await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
});
});

View File

@@ -1,9 +1,7 @@
import { css, html, LitElement } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, state } from 'lit/decorators.js';
import { Subscription } from 'rxjs';
import type { UmbSectionViewUsersElement } from '../../section-view-users.element';
import { UmbUserStore } from '../../../../../../../core/stores/user/user.store';
import {
UmbTableElement,
UmbTableColumn,
@@ -14,13 +12,15 @@ import {
UmbTableOrderedEvent,
} from '../../../../../../components/table/table.element';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import type { UserDetails } from '@umbraco-cms/models';
import type { UserDetails, UserGroupDetails, UserGroupEntity } from '@umbraco-cms/models';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import './column-layouts/name/user-table-name-column-layout.element';
import './column-layouts/status/user-table-status-column-layout.element';
import { UmbUserGroupStore } from 'src/core/stores/user/user-group.store';
@customElement('umb-editor-view-users-table')
export class UmbEditorViewUsersTableElement extends UmbContextConsumerMixin(LitElement) {
export class UmbEditorViewUsersTableElement extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
static styles = [
UUITextStyles,
css`
@@ -68,40 +68,56 @@ export class UmbEditorViewUsersTableElement extends UmbContextConsumerMixin(LitE
@state()
private _selection: Array<string> = [];
private _userStore?: UmbUserStore;
@state()
private _userGroups: Array<UserGroupEntity> = [];
private _userGroupStore?: UmbUserGroupStore;
private _usersContext?: UmbSectionViewUsersElement;
private _usersSubscription?: Subscription;
private _selectionSubscription?: Subscription;
connectedCallback(): void {
super.connectedCallback();
constructor() {
super();
this.consumeContext('umbUserStore', (userStore: UmbUserStore) => {
this._userStore = userStore;
this.consumeAllContexts(['umbUserGroupStore', 'umbUsersContext'], (instances) => {
this._userGroupStore = instances['umbUserGroupStore'];
this._usersContext = instances['umbUsersContext'];
this._observeUsers();
});
this.consumeContext('umbUsersContext', (usersContext: UmbSectionViewUsersElement) => {
this._usersContext = usersContext;
this._observeUserGroups();
this._observeSelection();
});
}
private _observeUsers() {
this._usersSubscription?.unsubscribe();
this._usersSubscription = this._userStore?.getAll().subscribe((users) => {
if (!this._usersContext) return;
this.observe<Array<UserDetails>>(this._usersContext.users, (users) => {
this._users = users;
this._createTableItems(this._users);
});
}
private _observeSelection() {
this._selectionSubscription = this._usersContext?.selection.subscribe((selection: Array<string>) => {
if (!this._usersContext) return;
this.observe(this._usersContext.selection, (selection) => {
if (this._selection === selection) return;
this._selection = selection;
});
}
private _observeUserGroups() {
if (!this._userGroupStore) return;
this.observe<Array<UserGroupDetails>>(this._userGroupStore.getAll(), (userGroups) => {
this._userGroups = userGroups;
this._createTableItems(this._users);
});
}
private _getUserGroupNames(keys: Array<string>) {
return keys
.map((key: string) => {
return this._userGroups.find((x) => x.key === key)?.name;
})
.join(', ');
}
private _createTableItems(users: Array<UserDetails>) {
this._tableItems = users.map((user) => {
return {
@@ -116,7 +132,7 @@ export class UmbEditorViewUsersTableElement extends UmbContextConsumerMixin(LitE
},
{
columnAlias: 'userGroup',
value: user.userGroup,
value: this._getUserGroupNames(user.userGroups),
},
{
columnAlias: 'userLastLogin',
@@ -154,13 +170,6 @@ export class UmbEditorViewUsersTableElement extends UmbContextConsumerMixin(LitE
console.log(`fetch users, order column: ${orderingColumn}, desc: ${orderingDesc}`);
}
disconnectedCallback(): void {
super.disconnectedCallback();
this._usersSubscription?.unsubscribe();
this._selectionSubscription?.unsubscribe();
}
render() {
return html`
<umb-table

View File

@@ -3,16 +3,21 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, state } from 'lit/decorators.js';
import { BehaviorSubject, Observable } from 'rxjs';
import type { IRoute, IRoutingInfo } from 'router-slot';
import type { UmbEditorEntityElement } from '../../../../editors/shared/editor-entity/editor-entity.element';
import { UmbContextProviderMixin } from '@umbraco-cms/context-api';
import UmbEditorEntityElement from '../../../../editors/shared/editor-entity/editor-entity.element';
import { UmbContextConsumerMixin, UmbContextProviderMixin } from '@umbraco-cms/context-api';
import './list-view-layouts/table/editor-view-users-table.element';
import './list-view-layouts/grid/editor-view-users-grid.element';
import './editor-view-users-selection.element';
import './editor-view-users-invite.element';
import type { UserDetails } from '@umbraco-cms/models';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbUserStore } from 'src/core/stores/user/user.store';
@customElement('umb-section-view-users')
export class UmbSectionViewUsersElement extends UmbContextProviderMixin(LitElement) {
export class UmbSectionViewUsersElement extends UmbContextProviderMixin(
UmbContextConsumerMixin(UmbObserverMixin(LitElement))
) {
static styles = [
UUITextStyles,
css`
@@ -39,19 +44,51 @@ export class UmbSectionViewUsersElement extends UmbContextProviderMixin(LitEleme
},
{
path: '**',
redirectTo: 'section/users/view/users/overview', //TODO: this should be dynamic
redirectTo: 'overview',
},
];
private _userStore?: UmbUserStore;
private _selection: BehaviorSubject<Array<string>> = new BehaviorSubject(<Array<string>>[]);
public readonly selection: Observable<Array<string>> = this._selection.asObservable();
private _users: BehaviorSubject<Array<UserDetails>> = new BehaviorSubject(<Array<UserDetails>>[]);
public readonly users: Observable<Array<UserDetails>> = this._users.asObservable();
private _search: BehaviorSubject<string> = new BehaviorSubject('');
public readonly search: Observable<string> = this._search.asObservable();
constructor() {
super();
this.consumeAllContexts(['umbUserStore', 'umbUserGroupStore', 'umbUsersContext'], (instances) => {
this._userStore = instances['umbUserStore'];
this._observeUsers();
});
this.provideContext('umbUsersContext', this);
}
private _observeUsers() {
if (!this._userStore) return;
if (this._search.getValue()) {
this.observe<Array<UserDetails>>(this._userStore.getByName(this._search.getValue()), (users) =>
this._users.next(users)
);
} else {
this.observe<Array<UserDetails>>(this._userStore.getAll(), (users) => this._users.next(users));
}
}
public setSearch(value: string) {
if (!value) value = '';
this._search.next(value);
this._observeUsers();
this.requestUpdate('search');
}
public setSelection(value: Array<string>) {
if (!value) return;
this._selection.next(value);

View File

@@ -0,0 +1,19 @@
import { expect, fixture, html } from '@open-wc/testing';
//TODO: Test has been commented out while we figure out how to setup import maps for the test environment
// import { defaultA11yConfig } from '@umbraco-cms/test-utils';
// import UmbSectionViewUsersElement from './section-view-users.element';
// describe('UmbSectionViewUsersElement', () => {
// let element: UmbSectionViewUsersElement;
// beforeEach(async () => {
// element = await fixture(html`<umb-section-view-users></umb-section-view-users>`);
// });
// it('is defined with its own instance', () => {
// expect(element).to.be.instanceOf(UmbSectionViewUsersElement);
// });
// it('passes the a11y audit', async () => {
// await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
// });
// });

View File

@@ -5,7 +5,7 @@ import { UmbTreeBase } from '../shared/tree-base.element';
import { UmbTreeDataTypesDataContext } from './tree-data-types-data.context';
import { UmbContextConsumerMixin, UmbContextProviderMixin } from '@umbraco-cms/context-api';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import type { ManifestTreeItemAction, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestTreeItemAction } from '@umbraco-cms/models';
import '../shared/tree-navigator.element';
@@ -26,7 +26,7 @@ export class UmbTreeDataTypesElement extends UmbContextProviderMixin(UmbContextC
}
private _registerTreeItemActions() {
const dashboards: Array<ManifestWithLoader<ManifestTreeItemAction>> = [
const dashboards: Array<ManifestTreeItemAction> = [
{
type: 'treeItemAction',
alias: 'Umb.TreeItemAction.DataType.Create',

View File

@@ -4,11 +4,11 @@ import { UmbEntityStore } from '../../../core/stores/entity.store';
import { UmbTreeBase } from '../shared/tree-base.element';
import { UmbTreeDocumentDataContext } from './tree-documents-data.context';
import { UmbContextConsumerMixin, UmbContextProviderMixin } from '@umbraco-cms/context-api';
import type { ManifestTreeItemAction, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestTreeItemAction } from '@umbraco-cms/models';
import '../shared/tree-navigator.element';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
@customElement('umb-tree-document')
@customElement('umb-tree-documents')
export class UmbTreeDocumentElement extends UmbContextProviderMixin(UmbContextConsumerMixin(UmbTreeBase)) {
constructor() {
super();
@@ -25,7 +25,7 @@ export class UmbTreeDocumentElement extends UmbContextProviderMixin(UmbContextCo
}
private _registerTreeItemActions() {
const dashboards: Array<ManifestWithLoader<ManifestTreeItemAction>> = [
const dashboards: Array<ManifestTreeItemAction> = [
{
type: 'treeItemAction',
alias: 'Umb.TreeItemAction.Document.Create',

View File

@@ -1,6 +1,6 @@
import type { ManifestTree, ManifestWithLoader } from '@umbraco-cms/models';
import type { ManifestTree } from '@umbraco-cms/models';
export const manifests: Array<ManifestWithLoader<ManifestTree>> = [
export const manifests: Array<ManifestTree> = [
{
type: 'tree',
alias: 'Umb.Tree.Extensions',

View File

@@ -31,7 +31,10 @@ export class UmbTreeElement extends UmbContextProviderMixin(UmbContextConsumerMi
const oldVal = this._selectable;
this._selectable = newVal;
this.requestUpdate('selectable', oldVal);
this._treeContext?.setSelectable(newVal);
if (newVal && this._treeContext) {
this._treeContext?.setSelectable(newVal);
this._observeSelection();
}
}
private _selection: Array<string> = [];

View File

@@ -34,7 +34,7 @@ export class UmbTreeContextBase implements UmbTreeContext {
}
public select(key: string) {
const selection = this._selection.getValue();
this._selection.next([...selection, key]);
const selection = [...this._selection.getValue(), key];
this._selection.next(selection);
}
}

View File

@@ -77,6 +77,7 @@ export interface ManifestElement extends ManifestBase {
type: ManifestStandardTypes;
js?: string;
elementName?: string;
loader?: () => Promise<object | HTMLElement>;
meta?: any;
}
@@ -89,6 +90,3 @@ export interface ManifestEntrypoint extends ManifestBase {
type: 'entrypoint';
js: string;
}
// TODO: couldn't we make loader optional on all manifests? and not just the internal ones?
export type ManifestWithLoader<T> = T & { loader: () => Promise<object | HTMLElement> };

View File

@@ -15,6 +15,9 @@ export class UmbEntityData<T extends Entity> extends UmbData<T> {
getByKey(key: string) {
return this.data.find((item) => item.key === key);
}
getByKeys(keys: Array<string>) {
return this.data.filter((item) => keys.includes(item.key));
}
save(saveItems: Array<T>) {
saveItems.forEach((saveItem) => {

View File

@@ -0,0 +1,79 @@
import { UmbEntityData } from './entity.data';
import type { UserGroupDetails } from '@umbraco-cms/models';
// Temp mocked database
class UmbUserGroupsData extends UmbEntityData<UserGroupDetails> {
constructor(data: Array<UserGroupDetails>) {
super(data);
}
}
export const data: Array<UserGroupDetails> = [
{
key: 'c630d49e-4e7b-42ea-b2bc-edc0edacb6b1',
name: 'Administrators',
icon: 'umb:medal',
parentKey: '',
type: 'userGroup',
hasChildren: false,
isTrashed: false,
sections: [
'Umb.Section.Users',
'Umb.Section.Packages',
'Umb.Section.Settings',
'Umb.Section.Members',
'Umb.Section.Media',
'Umb.Section.Content',
],
permissions: [],
},
{
key: '9a9ad4e9-3b5b-4fe7-b0d9-e301b9675949',
name: 'Editors',
icon: 'umb:tools',
parentKey: '',
type: 'userGroup',
hasChildren: false,
isTrashed: false,
sections: ['Umb.Section.Members', 'Umb.Section.Media'],
permissions: [],
contentStartNode: '74e4008a-ea4f-4793-b924-15e02fd380d1',
},
{
key: 'b847398a-6875-4d7a-9f6d-231256b81471',
name: 'Sensitive Data',
icon: 'umb:lock',
parentKey: '',
type: 'userGroup',
hasChildren: false,
isTrashed: false,
sections: ['Umb.Section.Settings', 'Umb.Section.Members', 'Umb.Section.Media', 'Umb.Section.Content'],
permissions: [],
contentStartNode: 'cdd30288-2d1c-41b4-89a9-61647b4a10d5',
},
{
key: '2668f09b-320c-48a7-a78a-95047026ec0e',
name: 'Translators',
icon: 'umb:globe',
parentKey: '',
type: 'userGroup',
hasChildren: false,
isTrashed: false,
sections: ['Umb.Section.Packages', 'Umb.Section.Settings'],
permissions: [],
contentStartNode: 'cdd30288-2d1c-41b4-89a9-61647b4a10d5',
},
{
key: '397f3a8b-4ca3-4b01-9dd3-94e5c9eaa9b2',
name: 'Writers',
icon: 'umb:edit',
parentKey: '',
type: 'userGroup',
hasChildren: false,
isTrashed: false,
sections: ['Umb.Section.Content'],
permissions: [],
},
];
export const umbUserGroupsData = new UmbUserGroupsData(data);

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,10 @@
import { rest } from 'msw';
import { umbUserGroupsData } from '../data/user-groups.data';
import type { UserGroupDetails } from '@umbraco-cms/models';
export const handlers = [
rest.get('/umbraco/backoffice/user-groups/list/items', (req, res, ctx) => {
const items = fakeData;
const items = umbUserGroupsData.getItems('userGroup');
const response = {
total: items.length,
@@ -12,52 +13,29 @@ export const handlers = [
return res(ctx.status(200), ctx.json(response));
}),
];
const fakeData: Array<UserGroupDetails> = [
{
key: '10000000-0000-0000-0000-000000000000',
name: 'Administrators',
icon: 'umb:medal',
parentKey: '',
type: 'userGroup',
hasChildren: false,
isTrashed: false,
},
{
key: '20000000-0000-0000-0000-000000000000',
name: 'Editors',
icon: 'umb:tools',
parentKey: '',
type: 'userGroup',
hasChildren: false,
isTrashed: false,
},
{
key: '20000000-0000-0000-0000-000000000000',
name: 'Sensitive Data',
icon: 'umb:lock',
parentKey: '',
type: 'userGroup',
hasChildren: false,
isTrashed: false,
},
{
key: '20000000-0000-0000-0000-000000000000',
name: 'Translators',
icon: 'umb:globe',
parentKey: '',
type: 'userGroup',
hasChildren: false,
isTrashed: false,
},
{
key: '20000000-0000-0000-0000-000000000000',
name: 'Writers',
icon: 'umb:edit',
parentKey: '',
type: 'userGroup',
hasChildren: false,
isTrashed: false,
},
rest.get('/umbraco/backoffice/user-groups/details/:key', (req, res, ctx) => {
const key = req.params.key as string;
if (!key) return;
const userGroup = umbUserGroupsData.getByKey(key);
return res(ctx.status(200), ctx.json(userGroup));
}),
rest.get('/umbraco/backoffice/user-groups/getByKeys', (req, res, ctx) => {
const keys = req.url.searchParams.getAll('key');
if (keys.length === 0) return;
const userGroups = umbUserGroupsData.getByKeys(keys);
return res(ctx.status(200), ctx.json(userGroups));
}),
rest.post<Array<UserGroupDetails>>('/umbraco/backoffice/user-groups/save', async (req, res, ctx) => {
const data = await req.json();
if (!data) return;
const saved = umbUserGroupsData.save(data);
return res(ctx.status(200), ctx.json(saved));
}),
];

View File

@@ -16,7 +16,7 @@ export const handlers = [
return res(ctx.status(200), ctx.json(response));
}),
rest.get('/umbraco/backoffice/users/:key', (req, res, ctx) => {
rest.get('/umbraco/backoffice/users/details/:key', (req, res, ctx) => {
const key = req.params.key as string;
if (!key) return;
@@ -25,6 +25,14 @@ export const handlers = [
return res(ctx.status(200), ctx.json(user));
}),
rest.get('/umbraco/backoffice/users/getByKeys', (req, res, ctx) => {
const keys = req.url.searchParams.getAll('key');
if (keys.length === 0) return;
const users = umbUsersData.getByKeys(keys);
return res(ctx.status(200), ctx.json(users));
}),
rest.post<UserDetails[]>('/umbraco/backoffice/users/save', async (req, res, ctx) => {
const data = await req.json();
if (!data) return;
@@ -54,7 +62,9 @@ export const handlers = [
hasChildren: false,
type: 'user',
icon: 'umb:icon-user',
userGroup: data.userGroups[0],
userGroups: data.userGroups,
contentStartNodes: [],
mediaStartNodes: [],
};
const invited = umbUsersData.save([newUser]);
@@ -77,9 +87,18 @@ export const handlers = [
const data = await req.json();
if (!data) return;
const disabledKeys = umbUsersData.disable(data);
const enabledKeys = umbUsersData.disable(data);
return res(ctx.status(200), ctx.json(disabledKeys));
return res(ctx.status(200), ctx.json(enabledKeys));
}),
rest.post<Array<string>>('/umbraco/backoffice/users/updateUserGroup', async (req, res, ctx) => {
const data = await req.json();
if (!data) return;
const userKeys = umbUsersData.updateUserGroup(data.userKeys, data.userGroupKey);
return res(ctx.status(200), ctx.json(userKeys));
}),
rest.post<Array<string>>('/umbraco/backoffice/users/delete', async (req, res, ctx) => {

View File

@@ -23,7 +23,9 @@ export interface UserDetails extends UserEntity {
updateDate: string;
createDate: string;
failedLoginAttempts: number;
userGroup?: string; //TODO Implement this
userGroups: Array<string>;
contentStartNodes: Array<string>;
mediaStartNodes: Array<string>;
}
export interface UserGroupEntity extends Entity {
@@ -31,10 +33,8 @@ export interface UserGroupEntity extends Entity {
}
export interface UserGroupDetails extends UserGroupEntity {
key: string;
name: string;
icon: string;
sections?: Array<string>;
sections: Array<string>;
contentStartNode?: string;
mediaStartNode?: string;
permissions: Array<string>;
}

View File

@@ -11,6 +11,7 @@ export interface UmbModalContentPickerData {
import '../../../../../backoffice/trees/documents/tree-documents.element';
import { UmbTreeElement } from '../../../../../backoffice/trees/shared/tree.element';
// TODO: make use of UmbPickerLayoutBase
@customElement('umb-modal-layout-content-picker')
export class UmbModalLayoutContentPickerElement extends UmbModalLayoutElement<UmbModalContentPickerData> {
static styles = [

View File

@@ -3,13 +3,12 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property, state } from 'lit/decorators.js';
import { UmbModalLayoutElement } from '../modal-layout.element';
import '../../../../../backoffice/editors/shared/editor-entity-layout/editor-entity-layout.element';
export interface UmbModalIconPickerData {
multiple: boolean;
selection: string[];
}
// TODO: Make use of UmbPickerLayoutBase
@customElement('umb-modal-layout-icon-picker')
export class UmbModalLayoutIconPickerElement extends UmbModalLayoutElement<UmbModalIconPickerData> {
static styles = [

View File

@@ -0,0 +1,51 @@
import { state } from 'lit/decorators.js';
import { UmbModalLayoutElement } from '@umbraco-cms/services';
export interface UmbPickerData<selectType = string> {
multiple: boolean;
selection: Array<selectType>;
}
export class UmbModalLayoutPickerBase<selectType = string> extends UmbModalLayoutElement<UmbPickerData<selectType>> {
@state()
private _selection: Array<selectType> = [];
connectedCallback(): void {
super.connectedCallback();
this._selection = this.data?.selection || [];
}
protected _submit() {
this.modalHandler?.close({ selection: this._selection });
}
protected _close() {
this.modalHandler?.close();
}
protected _handleKeydown(e: KeyboardEvent, key: selectType) {
if (e.key === 'Enter') {
this._handleItemClick(key);
}
}
/* TODO: Write test for this select/deselect method. */
protected _handleItemClick(key: selectType) {
if (this.data?.multiple) {
if (this._isSelected(key)) {
this._selection = this._selection.filter((selectedKey) => selectedKey !== key);
} else {
this._selection.push(key);
}
} else {
this._selection = [key];
}
this.requestUpdate('_selection');
}
protected _isSelected(key: selectType): boolean {
return this._selection.includes(key);
}
}

View File

@@ -0,0 +1,100 @@
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { css, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UmbModalLayoutPickerBase } from '../modal-layout-picker-base';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-registry';
import type { ManifestSection } from '@umbraco-cms/models';
@customElement('umb-picker-layout-section')
export class UmbPickerLayoutSectionElement extends UmbContextConsumerMixin(UmbObserverMixin(UmbModalLayoutPickerBase)) {
static styles = [
UUITextStyles,
css`
uui-input {
width: 100%;
}
hr {
border: none;
border-bottom: 1px solid var(--uui-color-divider);
margin: 16px 0;
}
#item-list {
display: flex;
flex-direction: column;
gap: var(--uui-size-1);
}
.item {
color: var(--uui-color-interactive);
display: grid;
grid-template-columns: var(--uui-size-8) 1fr;
padding: var(--uui-size-4) var(--uui-size-2);
gap: var(--uui-size-space-5);
align-items: center;
border-radius: var(--uui-size-2);
cursor: pointer;
}
.item.selected {
background-color: var(--uui-color-selected);
color: var(--uui-color-selected-contrast);
}
.item:not(.selected):hover {
background-color: var(--uui-color-surface-emphasis);
color: var(--uui-color-interactive-emphasis);
}
.item.selected:hover {
background-color: var(--uui-color-selected-emphasis);
}
.item uui-icon {
width: 100%;
box-sizing: border-box;
display: flex;
height: fit-content;
}
`,
];
@state()
private _sections: Array<ManifestSection> = [];
connectedCallback(): void {
super.connectedCallback();
umbExtensionsRegistry.extensionsOfType('section').subscribe((sections: Array<ManifestSection>) => {
this._sections = sections;
});
}
render() {
return html`
<umb-editor-entity-layout headline="Select sections">
<uui-box>
<uui-input label="search"></uui-input>
<hr />
<div id="item-list">
${this._sections.map(
(item) => html`
<div
@click=${() => this._handleItemClick(item.alias)}
@keydown=${(e: KeyboardEvent) => this._handleKeydown(e, item.alias)}
class=${this._isSelected(item.alias) ? 'item selected' : 'item'}>
<span>${item.meta.label}</span>
</div>
`
)}
</div>
</uui-box>
<div slot="actions">
<uui-button label="Close" @click=${this._close}></uui-button>
<uui-button label="Submit" look="primary" color="positive" @click=${this._submit}></uui-button>
</div>
</umb-editor-entity-layout>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-picker-layout-section': UmbPickerLayoutSectionElement;
}
}

View File

@@ -0,0 +1,19 @@
import { expect, fixture, html } from '@open-wc/testing';
//TODO: Test has been commented out while we figure out how to setup import maps for the test environment
// import { UmbPickerLayoutSectionElement } from './picker-layout-section.element';
// import { defaultA11yConfig } from '@umbraco-cms/test-utils';
// describe('UmbPickerLayoutSectionElement', () => {
// let element: UmbPickerLayoutSectionElement;
// beforeEach(async () => {
// element = await fixture(html`<umb-picker-layout-section></umb-picker-layout-section>`);
// });
// it('is defined with its own instance', () => {
// expect(element).to.be.instanceOf(UmbPickerLayoutSectionElement);
// });
// it('passes the a11y audit', async () => {
// await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
// });
// });

View File

@@ -0,0 +1,112 @@
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { css, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UmbModalLayoutPickerBase } from '../modal-layout-picker-base';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import type { UserGroupDetails } from '@umbraco-cms/models';
import { UmbUserGroupStore } from '@umbraco-cms/stores/user/user-group.store';
@customElement('umb-picker-layout-user-group')
export class UmbPickerLayoutUserGroupElement extends UmbContextConsumerMixin(UmbObserverMixin(UmbModalLayoutPickerBase)) {
static styles = [
UUITextStyles,
css`
uui-input {
width: 100%;
}
hr {
border: none;
border-bottom: 1px solid var(--uui-color-divider);
margin: 16px 0;
}
#item-list {
display: flex;
flex-direction: column;
gap: var(--uui-size-1);
}
.item {
color: var(--uui-color-interactive);
display: grid;
grid-template-columns: var(--uui-size-8) 1fr;
padding: var(--uui-size-4) var(--uui-size-2);
gap: var(--uui-size-space-5);
align-items: center;
border-radius: var(--uui-size-2);
cursor: pointer;
}
.item.selected {
background-color: var(--uui-color-selected);
color: var(--uui-color-selected-contrast);
}
.item:not(.selected):hover {
background-color: var(--uui-color-surface-emphasis);
color: var(--uui-color-interactive-emphasis);
}
.item.selected:hover {
background-color: var(--uui-color-selected-emphasis);
}
.item uui-icon {
width: 100%;
box-sizing: border-box;
display: flex;
height: fit-content;
}
`,
];
@state()
private _userGroups: Array<UserGroupDetails> = [];
private _userGroupStore?: UmbUserGroupStore;
connectedCallback(): void {
super.connectedCallback();
this.consumeContext('umbUserGroupStore', (userGroupStore: UmbUserGroupStore) => {
this._userGroupStore = userGroupStore;
this._observeUserGroups();
});
}
private _observeUserGroups() {
if (!this._userGroupStore) return;
this.observe<Array<UserGroupDetails>>(
this._userGroupStore.getAll(),
(userGroups) => (this._userGroups = userGroups)
);
}
render() {
return html`
<umb-editor-entity-layout headline="Select user groups">
<uui-box>
<uui-input label="search"></uui-input>
<hr />
<div id="item-list">
${this._userGroups.map(
(item) => html`
<div
@click=${() => this._handleItemClick(item.key)}
@keydown=${(e: KeyboardEvent) => this._handleKeydown(e, item.key)}
class=${this._isSelected(item.key) ? 'item selected' : 'item'}>
<uui-icon .name=${item.icon}></uui-icon>
<span>${item.name}</span>
</div>
`
)}
</div>
</uui-box>
<div slot="actions">
<uui-button label="Close" @click=${this._close}></uui-button>
<uui-button label="Submit" look="primary" color="positive" @click=${this._submit}></uui-button>
</div>
</umb-editor-entity-layout>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-picker-layout-user-group': UmbPickerLayoutUserGroupElement;
}
}

View File

@@ -0,0 +1,19 @@
import { expect, fixture, html } from '@open-wc/testing';
//TODO: Test has been commented out while we figure out how to setup import maps for the test environment
// import { UmbPickerLayoutUserGroupElement } from './picker-layout-user-group.element';
// import { defaultA11yConfig } from '@umbraco-cms/test-utils';
// describe('UmbPickerLayoutUserGroupElement', () => {
// let element: UmbPickerLayoutUserGroupElement;
// beforeEach(async () => {
// element = await fixture(html`<umb-picker-layout-user-group></umb-picker-layout-user-group>`);
// });
// it('is defined with its own instance', () => {
// expect(element).to.be.instanceOf(UmbPickerLayoutUserGroupElement);
// });
// it('passes the a11y audit', async () => {
// await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
// });
// });

View File

@@ -0,0 +1,114 @@
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { css, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { UmbModalLayoutPickerBase } from '../modal-layout-picker-base';
import type { UserDetails } from '@umbraco-cms/models';
import { UmbContextConsumerMixin } from '@umbraco-cms/context-api';
import { UmbObserverMixin } from '@umbraco-cms/observable-api';
import { UmbUserStore } from '@umbraco-cms/stores/user/user.store';
@customElement('umb-picker-layout-user')
export class UmbPickerLayoutUserElement extends UmbContextConsumerMixin(UmbObserverMixin(UmbModalLayoutPickerBase)) {
static styles = [
UUITextStyles,
css`
uui-input {
width: 100%;
}
hr {
border: none;
border-bottom: 1px solid var(--uui-color-divider);
margin: 16px 0;
}
#item-list {
display: flex;
flex-direction: column;
gap: var(--uui-size-1);
font-size: 1rem;
}
.item {
color: var(--uui-color-interactive);
display: flex;
align-items: center;
padding: var(--uui-size-2);
gap: var(--uui-size-space-5);
cursor: pointer;
position: relative;
border-radius: var(--uui-size-2);
}
.item.selected {
background-color: var(--uui-color-selected);
color: var(--uui-color-selected-contrast);
}
.item:not(.selected):hover {
background-color: var(--uui-color-surface-emphasis);
color: var(--uui-color-interactive-emphasis);
}
.item.selected:hover {
background-color: var(--uui-color-selected-emphasis);
}
.item:hover uui-avatar {
border-color: var(--uui-color-surface-emphasis);
}
.item.selected uui-avatar {
border-color: var(--uui-color-selected-contrast);
}
uui-avatar {
border: 2px solid var(--uui-color-surface);
}
`,
];
@state()
private _users: Array<UserDetails> = [];
private _userStore?: UmbUserStore;
connectedCallback(): void {
super.connectedCallback();
this.consumeContext('umbUserStore', (userStore: UmbUserStore) => {
this._userStore = userStore;
this._observeUsers();
});
}
private _observeUsers() {
if (!this._userStore) return;
this.observe<Array<UserDetails>>(this._userStore.getAll(), (users) => (this._users = users));
}
render() {
return html`
<umb-editor-entity-layout headline="Select users">
<uui-box>
<uui-input label="search"></uui-input>
<hr />
<div id="item-list">
${this._users.map(
(item) => html`
<div
@click=${() => this._handleItemClick(item.key)}
@keydown=${(e: KeyboardEvent) => this._handleKeydown(e, item.key)}
class=${this._isSelected(item.key) ? 'item selected' : 'item'}>
<uui-avatar .name=${item.name}></uui-avatar>
<span>${item.name}</span>
</div>
`
)}
</div>
</uui-box>
<div slot="actions">
<uui-button label="Close" @click=${this._close}></uui-button>
<uui-button label="Submit" look="primary" color="positive" @click=${this._submit}></uui-button>
</div>
</umb-editor-entity-layout>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'umb-picker-layout-user': UmbPickerLayoutUserElement;
}
}

View File

@@ -0,0 +1,19 @@
import { expect, fixture, html } from '@open-wc/testing';
//TODO: Test has been commented out while we figure out how to setup import maps for the test environment
// import { UmbPickerLayoutUserElement } from './picker-layout-user.element';
// import { defaultA11yConfig } from '@umbraco-cms/test-utils';
// describe('UmbPickerUserElement', () => {
// let element: UmbPickerLayoutUserElement;
// beforeEach(async () => {
// element = await fixture(html`<umb-picker-layout-user></umb-picker-layout-user>`);
// });
// it('is defined with its own instance', () => {
// expect(element).to.be.instanceOf(UmbPickerLayoutUserElement);
// });
// it('passes the a11y audit', async () => {
// await expect(element).shadowDom.to.be.accessible(defaultA11yConfig);
// });
// });

View File

@@ -19,6 +19,7 @@ interface GroupedPropertyEditorUIs {
[key: string]: Array<ManifestPropertyEditorUI>;
}
// TODO: make use of UmbPickerLayoutBase
@customElement('umb-modal-layout-property-editor-ui-picker')
export class UmbModalLayoutPropertyEditorUIPickerElement extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
static styles = [

View File

@@ -22,6 +22,7 @@ export class UmbModalHandler {
this.size = options?.size || 'small';
this.element = this._createElement(element, options);
// TODO: Consider if its right to use Promises, or use another event based system? Would we need to be able to cancel an event, to then prevent the closing..?
this._closePromise = new Promise((resolve) => {
this._closeResolver = resolve;
});

View File

@@ -12,10 +12,10 @@ import type { UmbModalContentPickerData } from './layouts/content-picker/modal-l
import type { UmbModalPropertyEditorUIPickerData } from './layouts/property-editor-ui-picker/modal-layout-property-editor-ui-picker.element';
import { UmbModalHandler } from './';
export type UmbModelType = 'dialog' | 'sidebar';
export type UmbModalType = 'dialog' | 'sidebar';
export interface UmbModalOptions<UmbModalData> {
type?: UmbModelType;
type?: UmbModalType;
size?: UUIModalSidebarSize;
data?: UmbModalData;
}

View File

@@ -1,8 +1,7 @@
import { BehaviorSubject, map, Observable } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
import type { UserDetails, UserEntity, UserGroupDetails } from '../../models';
import { map, Observable } from 'rxjs';
import { UmbEntityStore } from '../entity.store';
import { UmbDataStoreBase } from '../store';
import type { UserGroupDetails, UserGroupEntity } from '@umbraco-cms/models';
/**
* @export
@@ -29,4 +28,52 @@ export class UmbUserGroupStore extends UmbDataStoreBase<UserGroupDetails> {
return this.items;
}
getByKey(key: string): Observable<UserGroupDetails | null> {
// TODO: use Fetcher API.
// TODO: only fetch if the data type is not in the store?
fetch(`/umbraco/backoffice/user-groups/details/${key}`)
.then((res) => res.json())
.then((data) => {
this.update([data]);
});
return this.items.pipe(
map(
(userGroups: Array<UserGroupDetails>) =>
userGroups.find((userGroup: UserGroupDetails) => userGroup.key === key) || null
)
);
}
getByKeys(keys: Array<string>): Observable<Array<UserGroupEntity>> {
const params = keys.map((key) => `key=${key}`).join('&');
fetch(`/umbraco/backoffice/user-groups/getByKeys?${params}`)
.then((res) => res.json())
.then((data) => {
this.update(data);
});
return this.items.pipe(
map((items: Array<UserGroupDetails>) => items.filter((node: UserGroupDetails) => keys.includes(node.key)))
);
}
async save(userGroups: Array<UserGroupDetails>): Promise<void> {
// TODO: use Fetcher API.
try {
const res = await fetch('/umbraco/backoffice/user-groups/save', {
method: 'POST',
body: JSON.stringify(userGroups),
headers: {
'Content-Type': 'application/json',
},
});
const json = await res.json();
this.update(json);
this._entityStore.update(json);
} catch (error) {
console.error('Save Data Type error', error);
}
}
}

View File

@@ -1,5 +1,4 @@
import { BehaviorSubject, map, Observable } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
import type { UserDetails, UserEntity } from '../../models';
import { UmbEntityStore } from '../entity.store';
import { UmbDataStoreBase } from '../store';
@@ -43,7 +42,7 @@ export class UmbUserStore extends UmbDataStoreBase<UserDetails> {
getByKey(key: string): Observable<UserDetails | null> {
// TODO: use Fetcher API.
// TODO: only fetch if the data type is not in the store?
fetch(`/umbraco/backoffice/users/${key}`)
fetch(`/umbraco/backoffice/users/details/${key}`)
.then((res) => res.json())
.then((data) => {
this.update([data]);
@@ -54,6 +53,37 @@ export class UmbUserStore extends UmbDataStoreBase<UserDetails> {
);
}
getByKeys(keys: Array<string>): Observable<Array<UserDetails>> {
const params = keys.map((key) => `key=${key}`).join('&');
fetch(`/umbraco/backoffice/users/getByKeys?${params}`)
.then((res) => res.json())
.then((data) => {
this.update(data);
});
return this.items.pipe(
map((items: Array<UserDetails>) => items.filter((node: UserDetails) => keys.includes(node.key)))
);
}
getByName(name: string): Observable<Array<UserDetails>> {
name = name.trim();
name = name.toLocaleLowerCase();
const params = `name=${name}`;
fetch(`/umbraco/backoffice/users/getByName?${params}`)
.then((res) => res.json())
.then((data) => {
this.update(data);
});
return this.items.pipe(
map((items: Array<UserDetails>) =>
items.filter((node: UserDetails) => node.name.toLocaleLowerCase().includes(name))
)
);
}
async enableUsers(userKeys: Array<string>): Promise<void> {
// TODO: use Fetcher API.
try {
@@ -78,6 +108,58 @@ export class UmbUserStore extends UmbDataStoreBase<UserDetails> {
}
}
async updateUserGroup(userKeys: Array<string>, userGroup: string): Promise<void> {
// TODO: use Fetcher API.
try {
const res = await fetch('/umbraco/backoffice/users/updateUserGroup', {
method: 'POST',
body: JSON.stringify({ userKeys, userGroup }),
headers: {
'Content-Type': 'application/json',
},
});
const enabledKeys = await res.json();
const storedUsers = this._items.getValue().filter((user) => enabledKeys.includes(user.key));
storedUsers.forEach((user) => {
if (userKeys.includes(user.key)) {
user.userGroups.push(userGroup);
} else {
user.userGroups = user.userGroups.filter((group) => group !== userGroup);
}
});
this.update(storedUsers);
this._entityStore.update(storedUsers);
} catch (error) {
console.error('Add user group failed', error);
}
}
async removeUserGroup(userKeys: Array<string>, userGroup: string): Promise<void> {
// TODO: use Fetcher API.
try {
const res = await fetch('/umbraco/backoffice/users/enable', {
method: 'POST',
body: JSON.stringify({ userKeys, userGroup }),
headers: {
'Content-Type': 'application/json',
},
});
const enabledKeys = await res.json();
const storedUsers = this._items.getValue().filter((user) => enabledKeys.includes(user.key));
storedUsers.forEach((user) => {
user.userGroups = user.userGroups.filter((group) => group !== userGroup);
});
this.update(storedUsers);
this._entityStore.update(storedUsers);
} catch (error) {
console.error('Remove user group failed', error);
}
}
async disableUsers(userKeys: Array<string>): Promise<void> {
// TODO: use Fetcher API.
try {

View File

@@ -28,7 +28,10 @@
"@umbraco-cms/observable-api": ["src/core/observable-api"],
"@umbraco-cms/utils": ["src/core/utils"],
"@umbraco-cms/test-utils": ["src/core/test-utils"],
"@umbraco-cms/services": ["src/core/services"]
"@umbraco-cms/services": ["src/core/services"],
"@umbraco-cms/components/*": ["src/backoffice/components/*"],
"@umbraco-cms/stores/*": ["src/core/stores/*"],
"@umbraco-cms/sections/*": ["src/backoffice/sections/*"]
}
},
"include": ["src/**/*.ts", "e2e/**/*.ts"],

View File

@@ -18,6 +18,7 @@ export default {
'@umbraco-cms/observable-api': './src/core/observable-api/index.ts',
'@umbraco-cms/utils': './src/core/utils/index.ts',
'@umbraco-cms/test-utils': './src/core/test-utils/index.ts',
'@umbraco-cms/extensions-registry': './src/core/extensions-registry/index.ts',
},
},
},