Merge remote-tracking branch 'origin/main' into feature/media-section
This commit is contained in:
7281
src/Umbraco.Web.UI.Client/package-lock.json
generated
7281
src/Umbraco.Web.UI.Client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()}`;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
// });
|
||||
// });
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
// });
|
||||
// });
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
// });
|
||||
// });
|
||||
@@ -0,0 +1 @@
|
||||
export * from './table.element';
|
||||
@@ -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
|
||||
|
||||
@@ -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}">
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
// });
|
||||
// });
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
// });
|
||||
// });
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
// });
|
||||
// });
|
||||
@@ -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>`;
|
||||
|
||||
@@ -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);
|
||||
// });
|
||||
// });
|
||||
@@ -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()}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
// });
|
||||
// });
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
// });
|
||||
// });
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
// });
|
||||
// });
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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> = [];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> };
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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));
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
// });
|
||||
// });
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
// });
|
||||
// });
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
// });
|
||||
// });
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user