Merge branch 'main' into feature/observer
This commit is contained in:
@@ -595,6 +595,50 @@ components:
|
||||
- meta
|
||||
- alias
|
||||
- name
|
||||
MetaSectionView:
|
||||
type: object
|
||||
properties:
|
||||
sections:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
label:
|
||||
type: string
|
||||
pathname:
|
||||
type: string
|
||||
weight:
|
||||
type: number
|
||||
format: float
|
||||
icon:
|
||||
type: string
|
||||
required:
|
||||
- sections
|
||||
- label
|
||||
- pathname
|
||||
- weight
|
||||
- icon
|
||||
IManifestSectionView:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- sectionView
|
||||
meta:
|
||||
$ref: '#/components/schemas/MetaSectionView'
|
||||
js:
|
||||
type: string
|
||||
elementName:
|
||||
type: string
|
||||
alias:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- type
|
||||
- meta
|
||||
- alias
|
||||
- name
|
||||
MetaTree:
|
||||
type: object
|
||||
properties:
|
||||
@@ -659,6 +703,81 @@ components:
|
||||
- meta
|
||||
- alias
|
||||
- name
|
||||
MetaEditorAction:
|
||||
type: object
|
||||
properties:
|
||||
editors:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- editors
|
||||
IManifestEditorAction:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- editorAction
|
||||
meta:
|
||||
$ref: '#/components/schemas/MetaEditorAction'
|
||||
js:
|
||||
type: string
|
||||
elementName:
|
||||
type: string
|
||||
alias:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- type
|
||||
- meta
|
||||
- alias
|
||||
- name
|
||||
MetaEditorView:
|
||||
type: object
|
||||
properties:
|
||||
editors:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pathname:
|
||||
type: string
|
||||
weight:
|
||||
type: number
|
||||
format: float
|
||||
label:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
required:
|
||||
- editors
|
||||
- pathname
|
||||
- weight
|
||||
- label
|
||||
- icon
|
||||
IManifestEditorView:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- editorView
|
||||
meta:
|
||||
$ref: '#/components/schemas/MetaEditorView'
|
||||
js:
|
||||
type: string
|
||||
elementName:
|
||||
type: string
|
||||
alias:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- type
|
||||
- meta
|
||||
- alias
|
||||
- name
|
||||
MetaTreeItemAction:
|
||||
type: object
|
||||
properties:
|
||||
@@ -837,50 +956,6 @@ components:
|
||||
- meta
|
||||
- alias
|
||||
- name
|
||||
MetaEditorView:
|
||||
type: object
|
||||
properties:
|
||||
editors:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
pathname:
|
||||
type: string
|
||||
weight:
|
||||
type: number
|
||||
format: float
|
||||
label:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
required:
|
||||
- editors
|
||||
- pathname
|
||||
- weight
|
||||
- label
|
||||
- icon
|
||||
IManifestEditorView:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- editorView
|
||||
meta:
|
||||
$ref: '#/components/schemas/MetaEditorView'
|
||||
js:
|
||||
type: string
|
||||
elementName:
|
||||
type: string
|
||||
alias:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- type
|
||||
- meta
|
||||
- alias
|
||||
- name
|
||||
MetaPropertyAction:
|
||||
type: object
|
||||
properties:
|
||||
@@ -979,12 +1054,14 @@ components:
|
||||
Manifest:
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/IManifestSection'
|
||||
- $ref: '#/components/schemas/IManifestSectionView'
|
||||
- $ref: '#/components/schemas/IManifestTree'
|
||||
- $ref: '#/components/schemas/IManifestEditor'
|
||||
- $ref: '#/components/schemas/IManifestEditorAction'
|
||||
- $ref: '#/components/schemas/IManifestEditorView'
|
||||
- $ref: '#/components/schemas/IManifestTreeItemAction'
|
||||
- $ref: '#/components/schemas/IManifestPropertyEditorUI'
|
||||
- $ref: '#/components/schemas/IManifestDashboard'
|
||||
- $ref: '#/components/schemas/IManifestEditorView'
|
||||
- $ref: '#/components/schemas/IManifestPropertyAction'
|
||||
- $ref: '#/components/schemas/IManifestPackageView'
|
||||
- $ref: '#/components/schemas/IManifestEntrypoint'
|
||||
@@ -993,12 +1070,14 @@ components:
|
||||
propertyName: type
|
||||
mapping:
|
||||
section: '#/components/schemas/IManifestSection'
|
||||
sectionView: '#/components/schemas/IManifestSectionView'
|
||||
tree: '#/components/schemas/IManifestTree'
|
||||
editor: '#/components/schemas/IManifestEditor'
|
||||
editorAction: '#/components/schemas/IManifestEditorAction'
|
||||
editorView: '#/components/schemas/IManifestEditorView'
|
||||
treeItemAction: '#/components/schemas/IManifestTreeItemAction'
|
||||
propertyEditorUI: '#/components/schemas/IManifestPropertyEditorUI'
|
||||
dashboard: '#/components/schemas/IManifestDashboard'
|
||||
editorView: '#/components/schemas/IManifestEditorView'
|
||||
propertyAction: '#/components/schemas/IManifestPropertyAction'
|
||||
packageView: '#/components/schemas/IManifestPackageView'
|
||||
entrypoint: '#/components/schemas/IManifestEntrypoint'
|
||||
|
||||
@@ -154,6 +154,23 @@ export interface components {
|
||||
alias: string;
|
||||
name: string;
|
||||
};
|
||||
MetaSectionView: {
|
||||
sections: string[];
|
||||
label: string;
|
||||
pathname: string;
|
||||
/** Format: float */
|
||||
weight: number;
|
||||
icon: string;
|
||||
};
|
||||
IManifestSectionView: {
|
||||
/** @enum {string} */
|
||||
type: "sectionView";
|
||||
meta: components["schemas"]["MetaSectionView"];
|
||||
js?: string;
|
||||
elementName?: string;
|
||||
alias: string;
|
||||
name: string;
|
||||
};
|
||||
MetaTree: {
|
||||
/** Format: float */
|
||||
weight: number;
|
||||
@@ -180,6 +197,35 @@ export interface components {
|
||||
alias: string;
|
||||
name: string;
|
||||
};
|
||||
MetaEditorAction: {
|
||||
editors: string[];
|
||||
};
|
||||
IManifestEditorAction: {
|
||||
/** @enum {string} */
|
||||
type: "editorAction";
|
||||
meta: components["schemas"]["MetaEditorAction"];
|
||||
js?: string;
|
||||
elementName?: string;
|
||||
alias: string;
|
||||
name: string;
|
||||
};
|
||||
MetaEditorView: {
|
||||
editors: string[];
|
||||
pathname: string;
|
||||
/** Format: float */
|
||||
weight: number;
|
||||
label: string;
|
||||
icon: string;
|
||||
};
|
||||
IManifestEditorView: {
|
||||
/** @enum {string} */
|
||||
type: "editorView";
|
||||
meta: components["schemas"]["MetaEditorView"];
|
||||
js?: string;
|
||||
elementName?: string;
|
||||
alias: string;
|
||||
name: string;
|
||||
};
|
||||
MetaTreeItemAction: {
|
||||
trees: string[];
|
||||
label: string;
|
||||
@@ -253,23 +299,6 @@ export interface components {
|
||||
alias: string;
|
||||
name: string;
|
||||
};
|
||||
MetaEditorView: {
|
||||
editors: string[];
|
||||
pathname: string;
|
||||
/** Format: float */
|
||||
weight: number;
|
||||
label: string;
|
||||
icon: string;
|
||||
};
|
||||
IManifestEditorView: {
|
||||
/** @enum {string} */
|
||||
type: "editorView";
|
||||
meta: components["schemas"]["MetaEditorView"];
|
||||
js?: string;
|
||||
elementName?: string;
|
||||
alias: string;
|
||||
name: string;
|
||||
};
|
||||
MetaPropertyAction: {
|
||||
propertyEditors: string[];
|
||||
};
|
||||
@@ -310,12 +339,14 @@ export interface components {
|
||||
};
|
||||
Manifest:
|
||||
| components["schemas"]["IManifestSection"]
|
||||
| components["schemas"]["IManifestSectionView"]
|
||||
| components["schemas"]["IManifestTree"]
|
||||
| components["schemas"]["IManifestEditor"]
|
||||
| components["schemas"]["IManifestEditorAction"]
|
||||
| components["schemas"]["IManifestEditorView"]
|
||||
| components["schemas"]["IManifestTreeItemAction"]
|
||||
| components["schemas"]["IManifestPropertyEditorUI"]
|
||||
| components["schemas"]["IManifestDashboard"]
|
||||
| components["schemas"]["IManifestEditorView"]
|
||||
| components["schemas"]["IManifestPropertyAction"]
|
||||
| components["schemas"]["IManifestPackageView"]
|
||||
| components["schemas"]["IManifestEntrypoint"]
|
||||
|
||||
@@ -5,9 +5,9 @@ import './components/backoffice-main.element';
|
||||
import './components/backoffice-modal-container.element';
|
||||
import './components/backoffice-notification-container.element';
|
||||
import './components/node-property/node-property.element';
|
||||
import './sections/shared/section-layout.element';
|
||||
import './sections/shared/section-main.element';
|
||||
import './sections/shared/section-sidebar.element';
|
||||
import './components/table/table.element';
|
||||
import './sections/shared/section-main/section-main.element';
|
||||
import './sections/shared/section-sidebar/section-sidebar.element';
|
||||
import './sections/shared/section.element';
|
||||
import './trees/shared/tree-base.element';
|
||||
import './trees/shared/tree.element';
|
||||
@@ -24,9 +24,11 @@ import { UmbDocumentTypeStore } from '../core/stores/document-type.store';
|
||||
import { UmbNodeStore } from '../core/stores/node.store';
|
||||
import { UmbSectionStore } from '../core/stores/section.store';
|
||||
import { UmbEntityStore } from '../core/stores/entity.store';
|
||||
import { UmbUserStore } from '../core/stores/user/user.store';
|
||||
import { UmbPropertyEditorStore } from '../core/stores/property-editor/property-editor.store';
|
||||
import { UmbIconStore } from '../core/stores/icon/icon.store';
|
||||
import { UmbPropertyEditorConfigStore } from '../core/stores/property-editor-config/property-editor-config.store';
|
||||
import { UmbUserGroupStore } from '../core/stores/user/user-group.store';
|
||||
|
||||
@defineElement('umb-backoffice')
|
||||
export class UmbBackofficeElement extends UmbContextConsumerMixin(UmbContextProviderMixin(LitElement)) {
|
||||
@@ -58,6 +60,8 @@ export class UmbBackofficeElement extends UmbContextConsumerMixin(UmbContextProv
|
||||
this.provideContext('umbNodeStore', new UmbNodeStore(this._umbEntityStore));
|
||||
this.provideContext('umbDataTypeStore', new UmbDataTypeStore(this._umbEntityStore));
|
||||
this.provideContext('umbDocumentTypeStore', new UmbDocumentTypeStore(this._umbEntityStore));
|
||||
this.provideContext('umbUserStore', new UmbUserStore(this._umbEntityStore));
|
||||
this.provideContext('umbUserGroupStore', new UmbUserGroupStore(this._umbEntityStore));
|
||||
this.provideContext('umbPropertyEditorStore', new UmbPropertyEditorStore());
|
||||
this.provideContext('umbPropertyEditorConfigStore', new UmbPropertyEditorConfigStore());
|
||||
this.provideContext('umbNotificationService', new UmbNotificationService());
|
||||
|
||||
@@ -23,6 +23,9 @@ export class UmbBackofficeMain extends UmbContextProviderMixin(UmbContextConsume
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
router-slot {
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
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';
|
||||
|
||||
export interface UmbTableItem {
|
||||
key: string;
|
||||
icon?: string;
|
||||
data: Array<UmbTableItemData>;
|
||||
}
|
||||
|
||||
export interface UmbTableItemData {
|
||||
columnAlias: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface UmbTableColumn {
|
||||
name: string;
|
||||
alias: string;
|
||||
elementName?: string;
|
||||
}
|
||||
|
||||
export interface UmbTableConfig {
|
||||
allowSelection: boolean;
|
||||
}
|
||||
|
||||
export class UmbTableSelectedEvent extends Event {
|
||||
public constructor() {
|
||||
super('selected', { bubbles: true, composed: true });
|
||||
}
|
||||
}
|
||||
|
||||
export class UmbTableDeselectedEvent extends Event {
|
||||
public constructor() {
|
||||
super('deselected', { bubbles: true, composed: true });
|
||||
}
|
||||
}
|
||||
|
||||
export class UmbTableOrderedEvent extends Event {
|
||||
public constructor() {
|
||||
super('ordered', { bubbles: true, composed: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @element umb-table
|
||||
* @description - Element for displaying a table
|
||||
* @fires {UmbTableSelectedEvent} selected - fires when a row is selected
|
||||
* @fires {UmbTableDeselectedEvent} deselected - fires when a row is deselected
|
||||
* @fires {UmbTableOrderedEvent} sort - fires when a column order is changed
|
||||
* @extends LitElement
|
||||
*/
|
||||
@customElement('umb-table')
|
||||
export class UmbTableElement extends LitElement {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: var(--uui-size-space-4);
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
uui-table {
|
||||
box-shadow: var(--uui-shadow-depth-1);
|
||||
}
|
||||
|
||||
uui-table-head {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: white;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
uui-table-row uui-checkbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
uui-table-row:focus uui-icon,
|
||||
uui-table-row:focus-within uui-icon,
|
||||
uui-table-row:hover uui-icon,
|
||||
uui-table-row[select-only] uui-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
uui-table-row:focus uui-checkbox,
|
||||
uui-table-row:focus-within uui-checkbox,
|
||||
uui-table-row:hover uui-checkbox,
|
||||
uui-table-row[select-only] uui-checkbox {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
uui-table-head-cell:focus,
|
||||
uui-table-head-cell:focus-within,
|
||||
uui-table-head-cell:hover {
|
||||
--uui-symbol-sort-hover: 1;
|
||||
}
|
||||
|
||||
uui-table-head-cell button {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: inherit;
|
||||
font-size: inherit;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
/**
|
||||
* Table Items
|
||||
* @type {Array<UmbTableItem>}
|
||||
* @memberof UmbTableElement
|
||||
*/
|
||||
@property({ type: Array, attribute: false })
|
||||
public items: Array<UmbTableItem> = [];
|
||||
|
||||
/**
|
||||
* @description Table Columns
|
||||
* @type {Array<UmbTableColumn>}
|
||||
* @memberof UmbTableElement
|
||||
*/
|
||||
@property({ type: Array, attribute: false })
|
||||
public columns: Array<UmbTableColumn> = [];
|
||||
|
||||
/**
|
||||
* @description Table Config
|
||||
* @type {UmbTableConfig}
|
||||
* @memberof UmbTableElement
|
||||
*/
|
||||
@property({ type: Object, attribute: false })
|
||||
public config: UmbTableConfig = {
|
||||
allowSelection: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Table Selection
|
||||
* @type {Array<string>}
|
||||
* @memberof UmbTableElement
|
||||
*/
|
||||
@property({ type: Array, attribute: false })
|
||||
public selection: Array<string> = [];
|
||||
|
||||
@property({ type: String, attribute: false })
|
||||
public orderingColumn = '';
|
||||
|
||||
@property({ type: String, attribute: false })
|
||||
public orderingDesc = false;
|
||||
|
||||
@state()
|
||||
private _selectionMode = false;
|
||||
|
||||
private _isSelected(key: string) {
|
||||
return this.selection.includes(key);
|
||||
}
|
||||
|
||||
private _handleRowCheckboxChange(event: Event, item: UmbTableItem) {
|
||||
const checkboxElement = event.target as HTMLInputElement;
|
||||
checkboxElement.checked ? this._selectRow(item.key) : this._deselectRow(item.key);
|
||||
}
|
||||
|
||||
private _handleAllRowsCheckboxChange(event: Event) {
|
||||
const checkboxElement = event.target as HTMLInputElement;
|
||||
checkboxElement.checked ? this._selectAllRows() : this._deselectAllRows();
|
||||
}
|
||||
|
||||
private _handleOrderingChange(column: UmbTableColumn) {
|
||||
this.orderingDesc = this.orderingColumn === column.alias ? !this.orderingDesc : false;
|
||||
this.orderingColumn = column.alias;
|
||||
this.dispatchEvent(new UmbTableOrderedEvent());
|
||||
}
|
||||
|
||||
private _selectRow(key: string) {
|
||||
this.selection = [...this.selection, key];
|
||||
this._selectionMode = this.selection.length > 0;
|
||||
this.dispatchEvent(new UmbTableSelectedEvent());
|
||||
}
|
||||
|
||||
private _deselectRow(key: string) {
|
||||
this.selection = this.selection.filter((selectionKey) => selectionKey !== key);
|
||||
this._selectionMode = this.selection.length > 0;
|
||||
this.dispatchEvent(new UmbTableDeselectedEvent());
|
||||
}
|
||||
|
||||
private _selectAllRows() {
|
||||
this.selection = this.items.map((item: UmbTableItem) => item.key);
|
||||
this._selectionMode = true;
|
||||
this.dispatchEvent(new UmbTableSelectedEvent());
|
||||
}
|
||||
|
||||
private _deselectAllRows() {
|
||||
this.selection = [];
|
||||
this._selectionMode = false;
|
||||
this.dispatchEvent(new UmbTableDeselectedEvent());
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <uui-table class="uui-text">
|
||||
<uui-table-column style="width: 60px;"></uui-table-column>
|
||||
<uui-table-head>
|
||||
<uui-table-head-cell style="--uui-table-cell-padding: 0">
|
||||
<uui-checkbox
|
||||
label="Select All"
|
||||
style="padding: var(--uui-size-4) var(--uui-size-5);"
|
||||
@change="${this._handleAllRowsCheckboxChange}"
|
||||
?checked="${this.selection.length === this.items.length}">
|
||||
</uui-checkbox>
|
||||
</uui-table-head-cell>
|
||||
${this.columns.map((column) => this._renderHeaderCell(column))}
|
||||
</uui-table-head>
|
||||
${repeat(this.items, (item) => item.key, this._renderRow)}
|
||||
</uui-table>`;
|
||||
}
|
||||
|
||||
private _renderHeaderCell(column: UmbTableColumn) {
|
||||
return html`
|
||||
<uui-table-head-cell style="--uui-table-cell-padding: 0">
|
||||
<button
|
||||
style="padding: var(--uui-size-4) var(--uui-size-5);"
|
||||
@click="${() => this._handleOrderingChange(column)}">
|
||||
${column.name}
|
||||
<uui-symbol-sort ?active=${this.orderingColumn === column.alias} ?descending=${this.orderingDesc}>
|
||||
</uui-symbol-sort></button
|
||||
></uui-table-head-cell>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderRow = (item: UmbTableItem) => {
|
||||
return html`<uui-table-row
|
||||
?selectable="${this.config.allowSelection && this._selectionMode}"
|
||||
?select-only=${this._selectionMode}
|
||||
?selected=${this._isSelected(item.key)}
|
||||
@selected=${() => this._selectRow(item.key)}
|
||||
@unselected=${() => this._deselectRow(item.key)}>
|
||||
<uui-table-cell>
|
||||
${item.icon ? html`<uui-icon name=${item.icon}></uui-icon>` : nothing}
|
||||
<uui-checkbox
|
||||
label="Select Row"
|
||||
@click=${(e: PointerEvent) => e.stopPropagation()}
|
||||
@change=${(event: Event) => this._handleRowCheckboxChange(event, item)}
|
||||
?checked="${this._isSelected(item.key)}">
|
||||
</uui-checkbox>
|
||||
</uui-table-cell>
|
||||
${this.columns.map((column) => this._renderRowCell(column, item))}
|
||||
</uui-table-row>`;
|
||||
};
|
||||
|
||||
private _renderRowCell(column: UmbTableColumn, item: UmbTableItem) {
|
||||
return html`<uui-table-cell style="--uui-table-cell-padding: 0"
|
||||
>${this._renderCellContent(column, item)}</uui-table-cell
|
||||
>
|
||||
</uui-table-cell>`;
|
||||
}
|
||||
|
||||
private _renderCellContent(column: UmbTableColumn, item: UmbTableItem) {
|
||||
const value = item.data.find((data) => data.columnAlias === column.alias)?.value;
|
||||
|
||||
if (column.elementName) {
|
||||
const element = document.createElement(column.elementName) as any; // TODO: add interface for UmbTableColumnLayoutElement
|
||||
element.column = column;
|
||||
element.item = item;
|
||||
element.value = value;
|
||||
return element;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbTableElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-table': UmbTableElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Meta, Story } from '@storybook/web-components';
|
||||
import { html } from 'lit-html';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import type { UmbTableElement, UmbTableColumn, UmbTableConfig, UmbTableItem } from './table.element';
|
||||
|
||||
import './table.element';
|
||||
|
||||
export default {
|
||||
title: 'Components/Table',
|
||||
component: 'umb-table',
|
||||
id: 'umb-table',
|
||||
} as Meta;
|
||||
|
||||
const columns: Array<UmbTableColumn> = [
|
||||
{
|
||||
name: 'Name',
|
||||
alias: 'name',
|
||||
},
|
||||
{
|
||||
name: 'Date',
|
||||
alias: 'date',
|
||||
},
|
||||
];
|
||||
|
||||
const today = new Intl.DateTimeFormat('en-US').format(new Date());
|
||||
|
||||
const items: Array<UmbTableItem> = [
|
||||
{
|
||||
key: uuidv4(),
|
||||
icon: 'umb:wand',
|
||||
data: [
|
||||
{
|
||||
columnAlias: 'name',
|
||||
value: 'Item 1',
|
||||
},
|
||||
{
|
||||
columnAlias: 'date',
|
||||
value: today,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: uuidv4(),
|
||||
icon: 'umb:document',
|
||||
data: [
|
||||
{
|
||||
columnAlias: 'name',
|
||||
value: 'Item 2',
|
||||
},
|
||||
{
|
||||
columnAlias: 'date',
|
||||
value: today,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: uuidv4(),
|
||||
icon: 'umb:user',
|
||||
data: [
|
||||
{
|
||||
columnAlias: 'name',
|
||||
value: 'Item 3',
|
||||
},
|
||||
{
|
||||
columnAlias: 'date',
|
||||
value: today,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const config: UmbTableConfig = {
|
||||
allowSelection: true,
|
||||
};
|
||||
|
||||
export const AAAOverview: Story<UmbTableElement> = () =>
|
||||
html`<umb-table .items=${items} .columns=${columns} .config=${config}></umb-table>`;
|
||||
AAAOverview.storyName = 'Overview';
|
||||
@@ -1,23 +0,0 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
@customElement('umb-dashboard-settings-about')
|
||||
export class UmbDashboardSettingsAboutElement extends LitElement {
|
||||
static styles = [UUITextStyles, css``];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-box>
|
||||
<h1>Settings</h1>
|
||||
</uui-box>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbDashboardSettingsAboutElement;
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-dashboard-settings-about': UmbDashboardSettingsAboutElement;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Meta, Story } from '@storybook/web-components';
|
||||
import { html } from 'lit-html';
|
||||
|
||||
import type { UmbDashboardSettingsAboutElement } from './dashboard-settings-about.element';
|
||||
import './dashboard-settings-about.element';
|
||||
|
||||
export default {
|
||||
title: 'Dashboards/Settings About',
|
||||
component: 'umb-dashboard-settings-about',
|
||||
id: 'umb-dashboard-settings-about',
|
||||
} as Meta;
|
||||
|
||||
export const AAAOverview: Story<UmbDashboardSettingsAboutElement> = () =>
|
||||
html` <umb-dashboard-settings-about></umb-dashboard-settings-about>`;
|
||||
AAAOverview.storyName = 'Overview';
|
||||
@@ -0,0 +1,32 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
@customElement('umb-dashboard-settings-welcome')
|
||||
export class UmbDashboardSettingsWelcomeElement extends LitElement {
|
||||
static styles = [UUITextStyles, css``];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-box>
|
||||
<h1>Start here</h1>
|
||||
<p>This section contains the building blocks for your Umbraco site. Follow the below links to find out more about working with the items in the Settings section.</p>
|
||||
<h2>Find out more:</h2>
|
||||
<ul>
|
||||
<li>Read more about working with the items in Settings <a href="https://our.umbraco.com/documentation/Getting-Started/Backoffice/Sections/" target="_blank" rel="noopener">in the Documentation section</a> of Our Umbraco</li>
|
||||
<li>Ask a question in the <a href="https://our.umbraco.com/forum" target="_blank" rel="noopener">Community Forum</a></li>
|
||||
<li>Watch our free <a href="https://umbra.co/ulb" target="_blank" rel="noopener">tutorial videos on the Umbraco Learning Base</a></li>
|
||||
<li>Find out about our <a href="https://umbraco.com/products/" target="_blank" rel="noopener">productivity boosting tools and commercial support</a></li>
|
||||
<li>Find out about real-life <a href="https://umbraco.com/training/" target="_blank" rel="noopener">training and certification</a> opportunities</li>
|
||||
</ul>
|
||||
</uui-box>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbDashboardSettingsWelcomeElement;
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-dashboard-settings-welcome': UmbDashboardSettingsWelcomeElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Meta, Story } from '@storybook/web-components';
|
||||
import { html } from 'lit-html';
|
||||
|
||||
import type { UmbDashboardSettingsWelcomeElement } from './dashboard-settings-welcome.element';
|
||||
import './dashboard-settings-welcome.element';
|
||||
|
||||
export default {
|
||||
title: 'Dashboards/Settings Welcome',
|
||||
component: 'umb-dashboard-settings-welcome',
|
||||
id: 'umb-dashboard-settings-welcome',
|
||||
} as Meta;
|
||||
|
||||
export const AAAOverview: Story<UmbDashboardSettingsWelcomeElement> = () =>
|
||||
html` <umb-dashboard-settings-welcome></umb-dashboard-settings-welcome>`;
|
||||
AAAOverview.storyName = 'Overview';
|
||||
@@ -0,0 +1,20 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import { defaultA11yConfig } from '../../../core/helpers/chai';
|
||||
import { UmbDashboardSettingsWelcomeElement } from './dashboard-settings-welcome.element';
|
||||
|
||||
describe('UmbDashboardSettingsWelcomeElement', () => {
|
||||
let element: UmbDashboardSettingsWelcomeElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
element = await fixture(html`<umb-dashboard-settings-welcome></umb-dashboard-settings-welcome>`);
|
||||
});
|
||||
|
||||
it('is defined with its own instance', () => {
|
||||
expect(element).to.be.instanceOf(UmbDashboardSettingsWelcomeElement);
|
||||
});
|
||||
|
||||
it('passes the a11y audit', async () => {
|
||||
await expect(element).to.be.accessible(defaultA11yConfig);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui';
|
||||
import { CSSResultGroup, html, LitElement } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { createExtensionElement } from '../../../../core/extension';
|
||||
import type { ManifestEditorAction } from '../../../../core/models';
|
||||
|
||||
@customElement('umb-editor-action-extension')
|
||||
export class UmbEditorActionExtensionElement extends LitElement {
|
||||
static styles: CSSResultGroup = [UUITextStyles];
|
||||
|
||||
private _editorAction?: ManifestEditorAction;
|
||||
@property({ type: Object })
|
||||
public get editorAction(): ManifestEditorAction | undefined {
|
||||
return this._editorAction;
|
||||
}
|
||||
public set editorAction(value: ManifestEditorAction | undefined) {
|
||||
this._editorAction = value;
|
||||
this._createElement();
|
||||
}
|
||||
|
||||
@state()
|
||||
private _element?: any;
|
||||
|
||||
private async _createElement() {
|
||||
if (!this.editorAction) return;
|
||||
|
||||
try {
|
||||
this._element = await createExtensionElement(this.editorAction);
|
||||
if (!this._element) return;
|
||||
|
||||
this._element.editorAction = this.editorAction;
|
||||
} catch (error) {
|
||||
// TODO: loading JS failed so we should do some nice UI. (This does only happen if extension has a js prop, otherwise we concluded that no source was needed resolved the load.)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`${this._element}`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-editor-action-extension': UmbEditorActionExtensionElement;
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,11 @@ import { map } from 'rxjs';
|
||||
|
||||
import { UmbContextConsumerMixin } from '../../../../core/context';
|
||||
import { createExtensionElement, UmbExtensionRegistry } from '../../../../core/extension';
|
||||
import type { ManifestEditorView } from '../../../../core/models';
|
||||
import { UmbObserverMixin } from '../../../../core/observer';
|
||||
import type { ManifestEditorAction, ManifestEditorView } from '../../../../core/models';
|
||||
|
||||
import '../editor-layout/editor-layout.element';
|
||||
import '../editor-action-extension/editor-action-extension.element';
|
||||
|
||||
/**
|
||||
* @element umb-editor-entity-layout
|
||||
@@ -87,6 +88,9 @@ export class UmbEditorEntityLayout extends UmbContextConsumerMixin(UmbObserverMi
|
||||
@state()
|
||||
private _editorViews: Array<ManifestEditorView> = [];
|
||||
|
||||
@state()
|
||||
private _editorActions: Array<ManifestEditorAction> = [];
|
||||
|
||||
@state()
|
||||
private _currentView = '';
|
||||
|
||||
@@ -94,6 +98,7 @@ export class UmbEditorEntityLayout extends UmbContextConsumerMixin(UmbObserverMi
|
||||
private _routes: Array<IRoute> = [];
|
||||
|
||||
private _extensionRegistry?: UmbExtensionRegistry;
|
||||
private _editorActionsSubscription?: Subscription;
|
||||
private _routerFolder = '';
|
||||
|
||||
constructor() {
|
||||
@@ -101,7 +106,8 @@ export class UmbEditorEntityLayout extends UmbContextConsumerMixin(UmbObserverMi
|
||||
|
||||
this.consumeContext('umbExtensionRegistry', (extensionRegistry: UmbExtensionRegistry) => {
|
||||
this._extensionRegistry = extensionRegistry;
|
||||
this._useEditorViews();
|
||||
this._observeEditorViews();
|
||||
this._observeEditorActions();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -111,7 +117,7 @@ export class UmbEditorEntityLayout extends UmbContextConsumerMixin(UmbObserverMi
|
||||
this._routerFolder = window.location.pathname.split('/view')[0];
|
||||
}
|
||||
|
||||
private _useEditorViews() {
|
||||
private _observeEditorViews() {
|
||||
if (!this._extensionRegistry) return;
|
||||
|
||||
this.observe<ManifestEditorView[]>(
|
||||
@@ -131,6 +137,19 @@ export class UmbEditorEntityLayout extends UmbContextConsumerMixin(UmbObserverMi
|
||||
);
|
||||
}
|
||||
|
||||
private _observeEditorActions() {
|
||||
if (!this._extensionRegistry) return;
|
||||
|
||||
this.observe(
|
||||
this._extensionRegistry
|
||||
?.extensionsOfType('editorAction')
|
||||
.pipe(map((extensions) => extensions.filter((extension) => extension.meta.editors.includes(this.alias)))),
|
||||
(editorActions) => {
|
||||
this._editorActions = editorActions;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _createRoutes() {
|
||||
if (this._editorViews.length > 0) {
|
||||
this._routes = [];
|
||||
@@ -204,7 +223,12 @@ export class UmbEditorEntityLayout extends UmbContextConsumerMixin(UmbObserverMi
|
||||
|
||||
<div id="footer" slot="footer">
|
||||
<slot name="footer"></slot>
|
||||
<slot id="actions" name="actions"></slot>
|
||||
<div id="actions">
|
||||
${this._editorActions.map(
|
||||
(action) => html`<umb-editor-action-extension .editorAction=${action}></umb-editor-action-extension>`
|
||||
)}
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</umb-editor-layout>
|
||||
`;
|
||||
|
||||
@@ -30,7 +30,7 @@ export class UmbEditorLayout extends LitElement {
|
||||
}
|
||||
|
||||
#main {
|
||||
padding: var(--uui-size-6);
|
||||
/* padding: 0 var(--uui-size-6); */
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
import { UUIInputElement, UUIInputEvent } from '@umbraco-ui/uui';
|
||||
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';
|
||||
|
||||
@customElement('umb-editor-user-group')
|
||||
export class UmbEditorUserGroupElement extends LitElement {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#main {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 350px;
|
||||
gap: var(--uui-size-space-6);
|
||||
padding: var(--uui-size-space-6);
|
||||
}
|
||||
#left-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--uui-size-space-4);
|
||||
}
|
||||
#right-column > uui-box > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--uui-size-space-2);
|
||||
}
|
||||
hr {
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--uui-color-divider);
|
||||
width: 100%;
|
||||
}
|
||||
uui-input {
|
||||
width: 100%;
|
||||
}
|
||||
.faded-text {
|
||||
color: var(--uui-color-text-alt);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
#default-permissions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--uui-size-space-4);
|
||||
}
|
||||
.default-permission {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--uui-size-space-4);
|
||||
padding: var(--uui-size-space-2);
|
||||
}
|
||||
.default-permission:not(:last-child) {
|
||||
border-bottom: 1px solid var(--uui-color-divider);
|
||||
}
|
||||
.permission-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
private _userName = '';
|
||||
|
||||
@property({ type: String })
|
||||
entityKey = '';
|
||||
|
||||
defaultPermissions: Array<{
|
||||
name: string;
|
||||
permissions: Array<{ name: string; description: string; value: boolean }>;
|
||||
}> = [
|
||||
{
|
||||
name: 'Administration',
|
||||
permissions: [
|
||||
{
|
||||
name: 'Culture and Hostnames',
|
||||
description: 'Allow access to assign culture and hostnames',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: 'Restrict Public Access',
|
||||
description: 'Allow access to set and change access restrictions for a node',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: 'Rollback',
|
||||
description: 'Allow access to roll back a node to a previous state',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Content',
|
||||
permissions: [
|
||||
{
|
||||
name: 'Browse Node',
|
||||
description: 'Allow access to view a node',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: 'Create Content Template',
|
||||
description: 'Allow access to create a Content Template',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
description: 'Allow access to delete nodes',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: 'Create',
|
||||
description: 'Allow access to create nodes',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: 'Publish',
|
||||
description: 'Allow access to publish nodes',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: 'Permissions',
|
||||
description: 'Allow access to change permissions for a node',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: 'Send To Publish',
|
||||
description: 'Allow access to send a node for approval before publishing',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: 'Unpublish',
|
||||
description: 'Allow access to unpublish a node',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
description: 'Allow access to save a node',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: 'Full restore',
|
||||
description: 'Allow the user to restore items',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: 'Partial restore',
|
||||
description: 'Allow the user to partial restore items',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: 'Queue for transfer',
|
||||
description: 'Allow the user to queue item(s)',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Structure',
|
||||
permissions: [
|
||||
{
|
||||
name: 'Copy',
|
||||
description: 'Allow access to copy a node',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: 'Move',
|
||||
description: 'Allow access to move a node',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: 'Sort',
|
||||
description: 'Allow access to change the sort order for nodes',
|
||||
value: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
private renderLeftColumn() {
|
||||
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>
|
||||
</uui-box>
|
||||
|
||||
<uui-box>
|
||||
<div slot="headline">Default Permissions</div>
|
||||
<div id="default-permissions">
|
||||
${repeat(
|
||||
this.defaultPermissions,
|
||||
(defaultPermission) => html`
|
||||
<div>
|
||||
<b>${defaultPermission.name}</b>
|
||||
${repeat(
|
||||
defaultPermission.permissions,
|
||||
(permission) => html`
|
||||
<div class="default-permission">
|
||||
<uui-toggle
|
||||
.checked=${permission.value}
|
||||
@change=${(e: Event) => {
|
||||
permission.value = (e.target as HTMLInputElement).checked;
|
||||
}}></uui-toggle>
|
||||
<div class="permission-info">
|
||||
<b>${permission.name}</b>
|
||||
<span class="faded-text">${permission.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</uui-box>
|
||||
|
||||
<uui-box>
|
||||
<div slot="headline">Granular permissions</div>
|
||||
</uui-box>`;
|
||||
}
|
||||
|
||||
private renderRightColumn() {
|
||||
return html`<uui-box>
|
||||
<div slot="headline">Users</div>
|
||||
</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;
|
||||
|
||||
console.log('input', target.value);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<umb-editor-entity-layout alias="Umb.Editor.UserGroup">
|
||||
<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>
|
||||
</umb-editor-entity-layout>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbEditorUserGroupElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-editor-user-group': UmbEditorUserGroupElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
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 { UmbContextConsumerMixin } from '../../../../core/context';
|
||||
import { UmbUserStore } from '../../../../core/stores/user/user.store';
|
||||
import type { UserEntity } from '../../../../core/models';
|
||||
import { UmbUserContext } from '../user.context';
|
||||
import { UUIButtonState } from '@umbraco-ui/uui';
|
||||
import { UmbNotificationDefaultData } from '../../../../core/services/notification/layouts/default';
|
||||
import { UmbNotificationService } from '../../../../core/services/notification';
|
||||
|
||||
@customElement('umb-editor-action-user-save')
|
||||
export class UmbEditorActionUserSaveElement extends UmbContextConsumerMixin(LitElement) {
|
||||
static styles = [UUITextStyles, css``];
|
||||
|
||||
@state()
|
||||
private _saveButtonState?: UUIButtonState;
|
||||
|
||||
private _userStore?: UmbUserStore;
|
||||
private _userContext?: UmbUserContext;
|
||||
private _notificationService?: UmbNotificationService;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.consumeContext('umbUserStore', (userStore: UmbUserStore) => {
|
||||
this._userStore = userStore;
|
||||
});
|
||||
|
||||
this.consumeContext('umbUserContext', (userContext: UmbUserContext) => {
|
||||
this._userContext = userContext;
|
||||
});
|
||||
|
||||
this.consumeContext('umbNotificationService', (service: UmbNotificationService) => {
|
||||
this._notificationService = service;
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleSave() {
|
||||
// TODO: What if store is not present, what if node is not loaded....
|
||||
if (!this._userStore || !this._userContext) return;
|
||||
|
||||
try {
|
||||
this._saveButtonState = 'waiting';
|
||||
const user = this._userContext.getData();
|
||||
await this._userStore.save([user]);
|
||||
const data: UmbNotificationDefaultData = { message: 'User Saved' };
|
||||
this._notificationService?.peek('positive', { data });
|
||||
this._saveButtonState = 'success';
|
||||
} catch (error) {
|
||||
this._saveButtonState = 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<uui-button
|
||||
@click=${this._handleSave}
|
||||
look="primary"
|
||||
color="positive"
|
||||
label="save"
|
||||
.state="${this._saveButtonState}"></uui-button>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbEditorActionUserSaveElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-editor-action-user-save': UmbEditorActionUserSaveElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
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 { UmbContextProviderMixin, UmbContextConsumerMixin } from '../../../core/context';
|
||||
import UmbSectionViewUsersElement from '../../sections/users/views/users/section-view-users.element';
|
||||
import { UmbUserStore } from '../../../core/stores/user/user.store';
|
||||
import type { UserDetails } from '../../../core/models';
|
||||
import { UmbUserContext } from './user.context';
|
||||
import '../../property-editor-uis/content-picker/property-editor-ui-content-picker.element';
|
||||
|
||||
import '../shared/editor-entity-layout/editor-entity-layout.element';
|
||||
import { getTagLookAndColor } from '../../sections/users/user-extensions';
|
||||
@customElement('umb-editor-user')
|
||||
export class UmbEditorUserElement extends UmbContextProviderMixin(UmbContextConsumerMixin(LitElement)) {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#main {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 350px;
|
||||
gap: var(--uui-size-space-6);
|
||||
padding: var(--uui-size-space-6);
|
||||
}
|
||||
|
||||
#left-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--uui-size-space-4);
|
||||
}
|
||||
#right-column > uui-box > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--uui-size-space-2);
|
||||
}
|
||||
uui-avatar {
|
||||
font-size: var(--uui-size-16);
|
||||
place-self: center;
|
||||
}
|
||||
hr {
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--uui-color-divider);
|
||||
width: 100%;
|
||||
}
|
||||
uui-input {
|
||||
width: 100%;
|
||||
}
|
||||
.faded-text {
|
||||
color: var(--uui-color-text-alt);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
uui-tag {
|
||||
width: fit-content;
|
||||
}
|
||||
#user-info {
|
||||
display: flex;
|
||||
gap: var(--uui-size-space-6);
|
||||
}
|
||||
#user-info > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
private _user?: UserDetails | null;
|
||||
|
||||
@state()
|
||||
private _userName = '';
|
||||
|
||||
@property({ type: String })
|
||||
entityKey = '';
|
||||
|
||||
protected _userStore?: UmbUserStore;
|
||||
protected _usersSubscription?: Subscription;
|
||||
private _userContext?: UmbUserContext;
|
||||
|
||||
private _userNameSubscription?: Subscription;
|
||||
|
||||
private _languages = []; //TODO Add languages
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.consumeContext('umbUserStore', (usersContext: UmbUserStore) => {
|
||||
this._userStore = usersContext;
|
||||
this._observeUser();
|
||||
});
|
||||
}
|
||||
|
||||
private _observeUser() {
|
||||
this._usersSubscription?.unsubscribe();
|
||||
|
||||
this._usersSubscription = this._userStore?.getByKey(this.entityKey).subscribe((user) => {
|
||||
this._user = user;
|
||||
if (!this._user) return;
|
||||
|
||||
if (!this._userContext) {
|
||||
this._userContext = new UmbUserContext(this._user);
|
||||
this.provideContext('umbUserContext', this._userContext);
|
||||
} else {
|
||||
this._userContext.update(this._user);
|
||||
}
|
||||
|
||||
this._userNameSubscription = this._userContext.data.subscribe((user) => {
|
||||
if (user && user.name !== this._userName) {
|
||||
this._userName = user.name;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
|
||||
this._usersSubscription?.unsubscribe();
|
||||
this._userNameSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
private _updateUserStatus() {
|
||||
if (!this._user || !this._userStore) return;
|
||||
|
||||
const isDisabled = this._user.status === 'disabled';
|
||||
isDisabled ? this._userStore.enableUsers([this._user.key]) : this._userStore.disableUsers([this._user.key]);
|
||||
}
|
||||
|
||||
private _deleteUser() {
|
||||
if (!this._user || !this._userStore) return;
|
||||
|
||||
this._userStore.deleteUsers([this._user.key]);
|
||||
|
||||
history.pushState(null, '', '/section/users/view/users/overview');
|
||||
}
|
||||
|
||||
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>
|
||||
</uui-box>
|
||||
<uui-box>
|
||||
<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>
|
||||
</div>
|
||||
</uui-box>
|
||||
<uui-box>
|
||||
<div slot="headline">Access</div>
|
||||
<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>
|
||||
|
||||
<b>Media</b>
|
||||
<div class="access-content">
|
||||
<uui-icon name="folder"></uui-icon>
|
||||
<span>Media Root</span>
|
||||
</div>
|
||||
</uui-box>`;
|
||||
}
|
||||
|
||||
private renderRightColumn() {
|
||||
if (!this._user || !this._userStore) return nothing;
|
||||
|
||||
const statusLook = getTagLookAndColor(this._user.status);
|
||||
|
||||
return html` <uui-box>
|
||||
<div id="user-info">
|
||||
<uui-avatar .name=${this._user?.name || ''}></uui-avatar>
|
||||
<uui-button label="Change photo"></uui-button>
|
||||
<hr />
|
||||
${this._user?.status !== 'invited'
|
||||
? html`
|
||||
<uui-button
|
||||
@click=${this._updateUserStatus}
|
||||
look="primary"
|
||||
color="${this._user.status === 'disabled' ? 'positive' : 'warning'}"
|
||||
label="${this._user.status === 'disabled' ? 'Enable' : 'Disable'}"></uui-button>
|
||||
`
|
||||
: nothing}
|
||||
<uui-button @click=${this._deleteUser} look="primary" color="danger" label="Delete User"></uui-button>
|
||||
<div>
|
||||
<b>Status:</b>
|
||||
<uui-tag look="${ifDefined(statusLook?.look)}" color="${ifDefined(statusLook?.color)}">
|
||||
${this._user.status}
|
||||
</uui-tag>
|
||||
</div>
|
||||
${this._user?.status === 'invited'
|
||||
? html`
|
||||
<uui-textarea placeholder="Enter a message..."> </uui-textarea>
|
||||
<uui-button look="primary" label="Resend invitation"></uui-button>
|
||||
`
|
||||
: nothing}
|
||||
<div>
|
||||
<b>Last login:</b>
|
||||
<span>${this._user.lastLoginDate || `${this._user.name} has not logged in yet`}</span>
|
||||
</div>
|
||||
<div>
|
||||
<b>Failed login attempts</b>
|
||||
<span>${this._user.failedLoginAttempts}</span>
|
||||
</div>
|
||||
<div>
|
||||
<b>Last lockout date:</b>
|
||||
<span>${this._user.lastLockoutDate || `${this._user.name} has not been locked out`}</span>
|
||||
</div>
|
||||
<div>
|
||||
<b>Password last changed:</b>
|
||||
<span>${this._user.lastLoginDate || `${this._user.name} has not changed password`}</span>
|
||||
</div>
|
||||
<div>
|
||||
<b>User created:</b>
|
||||
<span>${this._user.createDate}</span>
|
||||
</div>
|
||||
<div>
|
||||
<b>User last updated:</b>
|
||||
<span>${this._user.updateDate}</span>
|
||||
</div>
|
||||
<div>
|
||||
<b>Key:</b>
|
||||
<span>${this._user.key}</span>
|
||||
</div>
|
||||
</div>
|
||||
</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`;
|
||||
|
||||
return html`
|
||||
<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>
|
||||
</umb-editor-entity-layout>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbEditorUserElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-editor-view-users-user-details': UmbEditorUserElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import type { UserDetails } from '../../../core/models';
|
||||
|
||||
export class UmbUserContext {
|
||||
// TODO: figure out how fine grained we want to make our observables.
|
||||
private _data = new BehaviorSubject<UserDetails>({
|
||||
key: '',
|
||||
name: '',
|
||||
icon: '',
|
||||
type: 'user',
|
||||
hasChildren: false,
|
||||
parentKey: '',
|
||||
isTrashed: false,
|
||||
email: '',
|
||||
language: '',
|
||||
status: 'enabled',
|
||||
updateDate: '8/27/2022',
|
||||
createDate: '9/19/2022',
|
||||
failedLoginAttempts: 0,
|
||||
});
|
||||
public readonly data: Observable<UserDetails> = this._data.asObservable();
|
||||
|
||||
constructor(user: UserDetails) {
|
||||
if (!user) return;
|
||||
this._data.next(user);
|
||||
}
|
||||
|
||||
// TODO: figure out how we want to update data
|
||||
public update(data: Partial<UserDetails>) {
|
||||
this._data.next({ ...this._data.getValue(), ...data });
|
||||
}
|
||||
|
||||
public getData() {
|
||||
return this._data.getValue();
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,11 @@ export class UmbPackagesEditor extends LitElement {
|
||||
render() {
|
||||
return html`
|
||||
<uui-icon-registry-essential>
|
||||
<umb-section-layout>
|
||||
<umb-section-main>
|
||||
<umb-editor-entity alias="Umb.Editor.Packages">
|
||||
<h1 slot="name">Packages</h1>
|
||||
</umb-editor-entity>
|
||||
</umb-section-main>
|
||||
</umb-section-layout>
|
||||
<umb-section-main>
|
||||
<umb-editor-entity alias="Umb.Editor.Packages">
|
||||
<h1 slot="name">Packages</h1>
|
||||
</umb-editor-entity>
|
||||
</umb-section-main>
|
||||
</uui-icon-registry-essential>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BehaviorSubject, ReplaySubject } from 'rxjs';
|
||||
|
||||
import type { ManifestSection, ManifestTree } from '../../core/models';
|
||||
import type { ManifestSection, ManifestSectionView, ManifestTree } from '../../core/models';
|
||||
import { Entity } from '../../mocks/data/entities';
|
||||
|
||||
export class UmbSectionContext {
|
||||
@@ -27,6 +27,10 @@ export class UmbSectionContext {
|
||||
private _activeTreeItem = new ReplaySubject<Entity>(1);
|
||||
public readonly activeTreeItem = this._activeTreeItem.asObservable();
|
||||
|
||||
// TODO: what is the best context to put this in?
|
||||
private _activeView = new ReplaySubject<ManifestSectionView>(1);
|
||||
public readonly activeView = this._activeView.asObservable();
|
||||
|
||||
constructor(section: ManifestSection) {
|
||||
if (!section) return;
|
||||
this._data.next(section);
|
||||
@@ -48,4 +52,8 @@ export class UmbSectionContext {
|
||||
public setActiveTreeItem(treeItem: Entity) {
|
||||
this._activeTreeItem.next(treeItem);
|
||||
}
|
||||
|
||||
public setActiveView(view: ManifestSectionView) {
|
||||
this._activeView.next(view);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,21 @@ import { customElement, state } from 'lit/decorators.js';
|
||||
import { IRoutingInfo } from 'router-slot';
|
||||
import { first, map } from 'rxjs';
|
||||
|
||||
import { UmbContextConsumerMixin } from '../../../core/context';
|
||||
import { createExtensionElement, UmbExtensionRegistry } from '../../../core/extension';
|
||||
import { UmbSectionContext } from '../section.context';
|
||||
|
||||
import type { ManifestDashboard, ManifestSection } from '../../../core/models';
|
||||
import { UmbObserverMixin } from '../../../core/observer';
|
||||
import { UmbContextConsumerMixin } from '../../../../core/context';
|
||||
import { createExtensionElement, UmbExtensionRegistry } from '../../../../core/extension';
|
||||
import { UmbSectionContext } from '../../section.context';
|
||||
import type { ManifestDashboard, ManifestSection } from '../../../../core/models';
|
||||
import { UmbObserverMixin } from '../../../../core/observer';
|
||||
|
||||
@customElement('umb-section-dashboards')
|
||||
export class UmbSectionDashboards extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
|
||||
export class UmbSectionDashboardsElement extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -150,10 +151,10 @@ export class UmbSectionDashboards extends UmbContextConsumerMixin(UmbObserverMix
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbSectionDashboards;
|
||||
export default UmbSectionDashboardsElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-section-dashboards': UmbSectionDashboards;
|
||||
'umb-section-dashboards': UmbSectionDashboardsElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Meta, Story } from '@storybook/web-components';
|
||||
import { html } from 'lit-html';
|
||||
import { internalManifests } from '../../../../temp-internal-manifests';
|
||||
import type { ManifestSection } from '../../../../core/models';
|
||||
import { UmbSectionContext } from '../../section.context';
|
||||
import type { UmbSectionDashboardsElement } from './section-dashboards.element';
|
||||
import './section-dashboards.element';
|
||||
|
||||
const contentSectionManifest = internalManifests.find((m) => m.alias === 'Umb.Section.Content') as ManifestSection;
|
||||
|
||||
export default {
|
||||
title: 'Sections/Shared/Section Dashboards',
|
||||
component: 'umb-section-dashboards',
|
||||
id: 'umb-section-dashboards',
|
||||
decorators: [
|
||||
(story) =>
|
||||
html` <umb-context-provider key="umbSectionContext" .value=${new UmbSectionContext(contentSectionManifest)}>
|
||||
${story()}
|
||||
</umb-context-provider>`,
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
export const AAAOverview: Story<UmbSectionDashboardsElement> = () =>
|
||||
html` <umb-section-dashboards></umb-section-dashboards> `;
|
||||
AAAOverview.storyName = 'Overview';
|
||||
@@ -1,27 +0,0 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
@customElement('umb-section-layout')
|
||||
export class UmbSectionLayout extends LitElement {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
render() {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-section-layout': UmbSectionLayout;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { css, html, LitElement } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
@customElement('umb-section-main')
|
||||
export class UmbSectionMain extends LitElement {
|
||||
export class UmbSectionMainElement extends LitElement {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
@@ -11,6 +11,11 @@ export class UmbSectionMain extends LitElement {
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
}
|
||||
slot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -21,6 +26,6 @@ export class UmbSectionMain extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-section-main': UmbSectionMain;
|
||||
'umb-section-main': UmbSectionMainElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Meta, Story } from '@storybook/web-components';
|
||||
import { html } from 'lit-html';
|
||||
|
||||
import type { UmbSectionMainElement } from './section-main.element';
|
||||
import './section-main.element';
|
||||
|
||||
export default {
|
||||
title: 'Sections/Shared/Section Main',
|
||||
component: 'umb-section-main',
|
||||
id: 'umb-section-main',
|
||||
} as Meta;
|
||||
|
||||
export const AAAOverview: Story<UmbSectionMainElement> = () =>
|
||||
html` <umb-section-main>Section Main Area</umb-section-main> `;
|
||||
AAAOverview.storyName = 'Overview';
|
||||
@@ -1,14 +1,15 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { UmbContextConsumerMixin } from '../../../core/context';
|
||||
import { UmbSectionContext } from '../section.context';
|
||||
import '../../trees/shared/context-menu/tree-context-menu.service';
|
||||
import { UmbObserverMixin } from '../../../core/observer';
|
||||
import type { ManifestSection } from '../../../core/models';
|
||||
import { UmbContextConsumerMixin } from '../../../../core/context';
|
||||
import { UmbSectionContext } from '../../section.context';
|
||||
import { UmbObserverMixin } from '../../../../core/observer';
|
||||
import type { ManifestSection } from '../../../../core/models';
|
||||
|
||||
import '../../../trees/shared/context-menu/tree-context-menu.service';
|
||||
|
||||
@customElement('umb-section-sidebar')
|
||||
export class UmbSectionSidebar extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
|
||||
export class UmbSectionSidebarElement extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
@@ -71,6 +72,6 @@ export class UmbSectionSidebar extends UmbContextConsumerMixin(UmbObserverMixin(
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-section-sidebar': UmbSectionSidebar;
|
||||
'umb-section-sidebar': UmbSectionSidebarElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Meta, Story } from '@storybook/web-components';
|
||||
import { html } from 'lit-html';
|
||||
|
||||
import type { UmbSectionSidebarElement } from './section-sidebar.element';
|
||||
import './section-sidebar.element';
|
||||
|
||||
export default {
|
||||
title: 'Sections/Shared/Section Sidebar',
|
||||
component: 'umb-section-sidebar',
|
||||
id: 'umb-section-sidebar',
|
||||
} as Meta;
|
||||
|
||||
export const AAAOverview: Story<UmbSectionSidebarElement> = () =>
|
||||
html` <umb-section-sidebar>Section Sidebar Area</umb-section-sidebar> `;
|
||||
AAAOverview.storyName = 'Overview';
|
||||
@@ -3,15 +3,15 @@ import { html, LitElement } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { map, switchMap, EMPTY, of } from 'rxjs';
|
||||
|
||||
import { UmbContextConsumerMixin } from '../../../core/context';
|
||||
import { UmbExtensionRegistry } from '../../../core/extension';
|
||||
import { UmbSectionContext } from '../section.context';
|
||||
import { UmbContextConsumerMixin } from '../../../../core/context';
|
||||
import { UmbExtensionRegistry } from '../../../../core/extension';
|
||||
import { UmbSectionContext } from '../../section.context';
|
||||
import { UmbObserverMixin } from '../../../../core/observer';
|
||||
|
||||
import '../../trees/shared/tree-extension.element';
|
||||
import { UmbObserverMixin } from '../../../core/observer';
|
||||
import '../../../trees/shared/tree-extension.element';
|
||||
|
||||
@customElement('umb-section-trees')
|
||||
export class UmbSectionTrees extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
|
||||
export class UmbSectionTreesElement extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
|
||||
static styles = [UUITextStyles];
|
||||
|
||||
@state()
|
||||
@@ -66,10 +66,10 @@ export class UmbSectionTrees extends UmbContextConsumerMixin(UmbObserverMixin(Li
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbSectionTrees;
|
||||
export default UmbSectionTreesElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-section-trees': UmbSectionTrees;
|
||||
'umb-section-trees': UmbSectionTreesElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Meta, Story } from '@storybook/web-components';
|
||||
import { html } from 'lit-html';
|
||||
|
||||
import type { UmbSectionTreesElement } from './section-trees.element';
|
||||
import './section-trees.element';
|
||||
|
||||
export default {
|
||||
title: 'Sections/Shared/Section Sidebar',
|
||||
component: 'umb-section-sidebar',
|
||||
id: 'umb-section-sidebar',
|
||||
} as Meta;
|
||||
|
||||
export const AAAOverview: Story<UmbSectionTreesElement> = () => html` <umb-section-trees></umb-section-trees>`;
|
||||
AAAOverview.storyName = 'Overview';
|
||||
@@ -0,0 +1,142 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { EMPTY, map, of, Subscription, switchMap } from 'rxjs';
|
||||
import { UmbContextConsumerMixin } from '../../../../core/context';
|
||||
import { UmbExtensionRegistry } from '../../../../core/extension';
|
||||
import type { ManifestSectionView } from '../../../../core/models';
|
||||
import { UmbSectionContext } from '../../section.context';
|
||||
|
||||
@customElement('umb-section-views')
|
||||
export class UmbSectionViewsElement extends UmbContextConsumerMixin(LitElement) {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
#header {
|
||||
background-color: var(--uui-color-surface);
|
||||
border-bottom: 1px solid var(--uui-color-divider-standalone);
|
||||
}
|
||||
|
||||
uui-tab-group {
|
||||
justify-content: flex-end;
|
||||
--uui-tab-divider: var(--uui-color-divider-standalone);
|
||||
}
|
||||
|
||||
uui-tab-group uui-tab:first-child {
|
||||
border-left: 1px solid var(--uui-color-divider-standalone);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
private _views: Array<ManifestSectionView> = [];
|
||||
|
||||
@state()
|
||||
private _routerFolder = '';
|
||||
|
||||
@state()
|
||||
private _activeView?: ManifestSectionView;
|
||||
|
||||
private _extensionRegistry?: UmbExtensionRegistry;
|
||||
private _sectionContext?: UmbSectionContext;
|
||||
private _viewsSubscription?: Subscription;
|
||||
private _activeViewSubscription?: Subscription;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// TODO: wait for more contexts
|
||||
this.consumeContext('umbExtensionRegistry', (extensionsRegistry: UmbExtensionRegistry) => {
|
||||
this._extensionRegistry = extensionsRegistry;
|
||||
this._observeViews();
|
||||
});
|
||||
|
||||
this.consumeContext('umbSectionContext', (sectionContext: UmbSectionContext) => {
|
||||
this._sectionContext = sectionContext;
|
||||
this._observeViews();
|
||||
this._observeActiveView();
|
||||
});
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
/* TODO: find a way to construct absolute urls */
|
||||
this._routerFolder = window.location.pathname.split('/view')[0];
|
||||
}
|
||||
|
||||
private _observeViews() {
|
||||
if (!this._sectionContext || !this._extensionRegistry) return;
|
||||
|
||||
this._viewsSubscription?.unsubscribe();
|
||||
|
||||
this._viewsSubscription = this._sectionContext?.data
|
||||
.pipe(
|
||||
switchMap((section) => {
|
||||
if (!section) return EMPTY;
|
||||
|
||||
return (
|
||||
this._extensionRegistry
|
||||
?.extensionsOfType('sectionView')
|
||||
.pipe(
|
||||
map((views) =>
|
||||
views
|
||||
.filter((view) => view.meta.sections.includes(section.alias))
|
||||
.sort((a, b) => b.meta.weight - a.meta.weight)
|
||||
)
|
||||
) ?? of([])
|
||||
);
|
||||
})
|
||||
)
|
||||
.subscribe((views) => {
|
||||
this._views = views;
|
||||
});
|
||||
}
|
||||
|
||||
private _observeActiveView() {
|
||||
this._activeViewSubscription?.unsubscribe();
|
||||
|
||||
this._activeViewSubscription = this._sectionContext?.activeView.subscribe((view) => {
|
||||
this._activeView = view;
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._viewsSubscription?.unsubscribe();
|
||||
this._activeViewSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` ${this._views.length > 0 ? html` <div id="header">${this._renderViews()}</div> ` : nothing} `;
|
||||
}
|
||||
|
||||
private _renderViews() {
|
||||
return html`
|
||||
${this._views?.length > 0
|
||||
? html`
|
||||
<uui-tab-group>
|
||||
${this._views.map(
|
||||
(view: ManifestSectionView) => html`
|
||||
<uui-tab
|
||||
.label="${view.meta.label || view.name}"
|
||||
href="${this._routerFolder}/view/${view.meta.pathname}"
|
||||
?active="${this._activeView?.meta?.pathname.includes(view.meta.pathname)}">
|
||||
<uui-icon slot="icon" name=${view.meta.icon}></uui-icon>
|
||||
${view.meta.label || view.name}
|
||||
</uui-tab>
|
||||
`
|
||||
)}
|
||||
</uui-tab-group>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbSectionViewsElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-section-views': UmbSectionViewsElement;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { map, switchMap, EMPTY, of } from 'rxjs';
|
||||
import { UmbContextConsumerMixin } from '../../../core/context';
|
||||
import { UmbExtensionRegistry } from '../../../core/extension';
|
||||
import { createExtensionElement, UmbExtensionRegistry } from '../../../core/extension';
|
||||
import { UmbSectionContext } from '../section.context';
|
||||
import type { ManifestTree } from '../../../core/models';
|
||||
|
||||
import type { ManifestTree, ManifestSectionView } from '../../../core/models';
|
||||
import { UmbEditorEntityElement } from '../../editors/shared/editor-entity/editor-entity.element';
|
||||
import { UmbEntityStore } from '../../../core/stores/entity.store';
|
||||
import { UmbObserverMixin } from '../../../core/observer';
|
||||
|
||||
import '../shared/section-trees.element.ts';
|
||||
import './section-trees/section-trees.element.ts';
|
||||
import '../shared/section-views/section-views.element.ts';
|
||||
|
||||
@customElement('umb-section')
|
||||
export class UmbSectionElement extends UmbContextConsumerMixin(UmbObserverMixin(LitElement)) {
|
||||
@@ -21,6 +21,24 @@ export class UmbSectionElement extends UmbContextConsumerMixin(UmbObserverMixin(
|
||||
:host {
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#header {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
uui-tab-group {
|
||||
--uui-tab-divider: var(--uui-color-border);
|
||||
border-left: 1px solid var(--uui-color-border);
|
||||
border-right: 1px solid var(--uui-color-border);
|
||||
}
|
||||
#router-slot {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -30,7 +48,10 @@ export class UmbSectionElement extends UmbContextConsumerMixin(UmbObserverMixin(
|
||||
private _routes: Array<any> = [];
|
||||
|
||||
@state()
|
||||
private _trees?: Array<ManifestTree>;
|
||||
private _trees: Array<ManifestTree> = [];
|
||||
|
||||
@state()
|
||||
private _views: Array<ManifestSectionView> = [];
|
||||
|
||||
private _entityStore?: UmbEntityStore;
|
||||
private _sectionContext?: UmbSectionContext;
|
||||
@@ -43,16 +64,19 @@ export class UmbSectionElement extends UmbContextConsumerMixin(UmbObserverMixin(
|
||||
this.consumeContext('umbExtensionRegistry', (extensionsRegistry: UmbExtensionRegistry) => {
|
||||
this._extensionRegistry = extensionsRegistry;
|
||||
this._observeTrees();
|
||||
this._observeViews();
|
||||
});
|
||||
|
||||
this.consumeContext('umbSectionContext', (sectionContext: UmbSectionContext) => {
|
||||
this._sectionContext = sectionContext;
|
||||
this._observeTrees();
|
||||
this._observeViews();
|
||||
});
|
||||
|
||||
this.consumeContext('umbEntityStore', (entityStore: UmbEntityStore) => {
|
||||
this._entityStore = entityStore;
|
||||
this._observeTrees();
|
||||
this._observeViews();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,12 +103,13 @@ export class UmbSectionElement extends UmbContextConsumerMixin(UmbObserverMixin(
|
||||
),
|
||||
(trees) => {
|
||||
this._trees = trees;
|
||||
this._createRoutes();
|
||||
if (this._trees.length === 0) return;
|
||||
this._createTreeRoutes();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _createRoutes() {
|
||||
private _createTreeRoutes() {
|
||||
const treeRoutes =
|
||||
this._trees?.map(() => {
|
||||
return {
|
||||
@@ -100,7 +125,7 @@ export class UmbSectionElement extends UmbContextConsumerMixin(UmbObserverMixin(
|
||||
this._routes = [
|
||||
{
|
||||
path: 'dashboard',
|
||||
component: () => import('../shared/section-dashboards.element'),
|
||||
component: () => import('./section-dashboards/section-dashboards.element'),
|
||||
},
|
||||
...treeRoutes,
|
||||
{
|
||||
@@ -110,16 +135,66 @@ export class UmbSectionElement extends UmbContextConsumerMixin(UmbObserverMixin(
|
||||
];
|
||||
}
|
||||
|
||||
private _observeViews() {
|
||||
if (!this._sectionContext || !this._extensionRegistry || !this._entityStore) return;
|
||||
|
||||
this.observe<ManifestSectionView[]>(
|
||||
this._sectionContext.data.pipe(
|
||||
switchMap((section) => {
|
||||
if (!section) return EMPTY;
|
||||
|
||||
return (
|
||||
this._extensionRegistry
|
||||
?.extensionsOfType('sectionView')
|
||||
.pipe(
|
||||
map((views) =>
|
||||
views
|
||||
.filter((view) => view.meta.sections.includes(section.alias))
|
||||
.sort((a, b) => b.meta.weight - a.meta.weight)
|
||||
)
|
||||
) ?? of([])
|
||||
);
|
||||
})
|
||||
),
|
||||
(views) => {
|
||||
this._views = views;
|
||||
if (this._views.length === 0) return;
|
||||
this._createViewRoutes();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _createViewRoutes() {
|
||||
this._routes =
|
||||
this._views?.map((view) => {
|
||||
return {
|
||||
path: 'view/' + view.meta.pathname,
|
||||
component: () => createExtensionElement(view),
|
||||
setup: () => {
|
||||
this._sectionContext?.setActiveView(view);
|
||||
},
|
||||
};
|
||||
}) ?? [];
|
||||
|
||||
this._routes.push({
|
||||
path: '**',
|
||||
redirectTo: 'view/' + this._views?.[0]?.meta.pathname,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<umb-section-layout>
|
||||
<umb-section-sidebar>
|
||||
<umb-section-trees></umb-section-trees>
|
||||
</umb-section-sidebar>
|
||||
<umb-section-main>
|
||||
<router-slot id="router-slot" .routes="${this._routes}"></router-slot>
|
||||
</umb-section-main>
|
||||
</umb-section-layout>
|
||||
${this._trees.length > 0
|
||||
? html`
|
||||
<umb-section-sidebar>
|
||||
<umb-section-trees></umb-section-trees>
|
||||
</umb-section-sidebar>
|
||||
`
|
||||
: nothing}
|
||||
<umb-section-main>
|
||||
${this._views.length > 0 ? html`<umb-section-views></umb-section-views>` : nothing}
|
||||
<router-slot id="router-slot" .routes="${this._routes}"></router-slot>
|
||||
</umb-section-main>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { html, LitElement } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
@customElement('umb-section-users')
|
||||
export class UmbSectionUsersElement extends LitElement {
|
||||
render() {
|
||||
return html` <umb-section></umb-section> `;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbSectionUsersElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-section-users': UmbSectionUsersElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { InterfaceColor, InterfaceLook } from '@umbraco-ui/uui-base/lib/types';
|
||||
|
||||
export type UserStatus = 'enabled' | 'inactive' | 'invited' | 'disabled';
|
||||
|
||||
export const getTagLookAndColor = (status: UserStatus): { look: InterfaceLook; color: InterfaceColor } => {
|
||||
switch ((status || '').toLowerCase()) {
|
||||
case 'invited':
|
||||
case 'inactive':
|
||||
return { look: 'primary', color: 'warning' };
|
||||
case 'enabled':
|
||||
return { look: 'primary', color: 'positive' };
|
||||
case 'disabled':
|
||||
return { look: 'primary', color: 'danger' };
|
||||
default:
|
||||
return { look: 'secondary', color: 'default' };
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,163 @@
|
||||
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 { UmbContextConsumerMixin } from '../../../../../core/context';
|
||||
import type { UserGroupDetails } from '../../../../../core/models';
|
||||
import { UmbUserGroupStore } from '../../../../../core/stores/user/user-group.store';
|
||||
import UmbTableElement, {
|
||||
UmbTableColumn,
|
||||
UmbTableConfig,
|
||||
UmbTableDeselectedEvent,
|
||||
UmbTableItem,
|
||||
UmbTableOrderedEvent,
|
||||
UmbTableSelectedEvent,
|
||||
} from '../../../../components/table/table.element';
|
||||
|
||||
import './user-group-table-name-column-layout.element';
|
||||
|
||||
@customElement('umb-editor-view-user-groups')
|
||||
export class UmbEditorViewUserGroupsElement extends UmbContextConsumerMixin(LitElement) {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
private _userGroups: Array<UserGroupDetails> = [];
|
||||
|
||||
@state()
|
||||
private _tableConfig: UmbTableConfig = {
|
||||
allowSelection: true,
|
||||
};
|
||||
|
||||
@state()
|
||||
private _tableColumns: Array<UmbTableColumn> = [
|
||||
{
|
||||
name: 'Name',
|
||||
alias: 'userGroupName',
|
||||
elementName: 'umb-user-group-table-name-column-layout',
|
||||
},
|
||||
{
|
||||
name: 'Sections',
|
||||
alias: 'userGroupSections',
|
||||
},
|
||||
{
|
||||
name: 'Content start node',
|
||||
alias: 'userGroupContentStartNode',
|
||||
},
|
||||
{
|
||||
name: 'Media start node',
|
||||
alias: 'userGroupMediaStartNode',
|
||||
},
|
||||
];
|
||||
|
||||
@state()
|
||||
private _tableItems: Array<UmbTableItem> = [];
|
||||
|
||||
@state()
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
private _observeUsers() {
|
||||
this._userGroupsSubscription?.unsubscribe();
|
||||
this._userGroupsSubscription = this._userGroupStore?.getAll().subscribe((userGroups) => {
|
||||
this._userGroups = userGroups;
|
||||
console.log('user groups', userGroups);
|
||||
|
||||
this._createTableItems(this._userGroups);
|
||||
});
|
||||
}
|
||||
|
||||
private _createTableItems(userGroups: Array<UserGroupDetails>) {
|
||||
this._tableItems = userGroups.map((userGroup) => {
|
||||
return {
|
||||
key: userGroup.key,
|
||||
icon: userGroup.icon,
|
||||
data: [
|
||||
{
|
||||
columnAlias: 'userGroupName',
|
||||
value: {
|
||||
name: userGroup.name,
|
||||
},
|
||||
},
|
||||
{
|
||||
columnAlias: 'userGroupSections',
|
||||
value: userGroup.sections,
|
||||
},
|
||||
{
|
||||
columnAlias: 'userGroupContentStartNode',
|
||||
value: userGroup.contentStartNode,
|
||||
},
|
||||
{
|
||||
columnAlias: 'userGroupMediaStartNode',
|
||||
value: userGroup.mediaStartNode,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSelected(event: UmbTableSelectedEvent) {
|
||||
event.stopPropagation();
|
||||
console.log('HANDLE SELECT');
|
||||
}
|
||||
|
||||
private _handleDeselected(event: UmbTableDeselectedEvent) {
|
||||
event.stopPropagation();
|
||||
console.log('HANDLE DESELECT');
|
||||
}
|
||||
|
||||
private _handleOrdering(event: UmbTableOrderedEvent) {
|
||||
const table = event.target as UmbTableElement;
|
||||
const orderingColumn = table.orderingColumn;
|
||||
const orderingDesc = table.orderingDesc;
|
||||
console.log(`fetch users, order column: ${orderingColumn}, desc: ${orderingDesc}`);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
|
||||
this._userGroupsSubscription?.unsubscribe();
|
||||
this._selectionSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<umb-table
|
||||
.config=${this._tableConfig}
|
||||
.columns=${this._tableColumns}
|
||||
.items=${this._tableItems}
|
||||
.selection=${this._selection}
|
||||
@selected="${this._handleSelected}"
|
||||
@deselected="${this._handleDeselected}"
|
||||
@ordered="${this._handleOrdering}"></umb-table>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbEditorViewUserGroupsElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-editor-view-user-groups': UmbEditorViewUserGroupsElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { html, LitElement } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
import './editor-view-user-groups.element';
|
||||
|
||||
@customElement('umb-section-view-user-groups')
|
||||
export class UmbSectionViewUserGroupsElement extends LitElement {
|
||||
render() {
|
||||
return html`<umb-editor-view-user-groups></umb-editor-view-user-groups>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbSectionViewUserGroupsElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-section-view-user-groups': UmbSectionViewUserGroupsElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { html, LitElement } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { UmbTableItem } from '../../../../components/table/table.element';
|
||||
|
||||
@customElement('umb-user-group-table-name-column-layout')
|
||||
export class UmbUserGroupTableNameColumnLayoutElement extends LitElement {
|
||||
@property({ type: Object, attribute: false })
|
||||
item!: UmbTableItem;
|
||||
|
||||
@property({ attribute: false })
|
||||
value!: any;
|
||||
|
||||
render() {
|
||||
return html` <a style="font-weight: bold;" href="/section/users/view/users/userGroup/${this.item.key}">
|
||||
${this.value.name}
|
||||
</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbUserGroupTableNameColumnLayoutElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-user-group-table-name-column-layout': UmbUserGroupTableNameColumnLayoutElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { customElement, query, state } from 'lit/decorators.js';
|
||||
import { UmbContextConsumerMixin } from '../../../../../core/context';
|
||||
import { UmbModalLayoutElement } from '../../../../../core/services/modal/layouts/modal-layout.element';
|
||||
import { UmbUserStore } from '../../../../../core/stores/user/user.store';
|
||||
import type { UserDetails } from '../../../../../core/models';
|
||||
import { UmbNotificationService } from '../../../../../core/services/notification';
|
||||
|
||||
export type UsersViewType = 'list' | 'grid';
|
||||
@customElement('umb-editor-view-users-invite')
|
||||
export class UmbEditorViewUsersInviteElement 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 {
|
||||
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;
|
||||
}
|
||||
uui
|
||||
`,
|
||||
];
|
||||
|
||||
@query('#invite-form')
|
||||
private _form!: HTMLFormElement;
|
||||
|
||||
@state()
|
||||
private _invitedUser?: UserDetails;
|
||||
|
||||
protected _userStore?: UmbUserStore;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.consumeContext('umbUserStore', (usersContext: UmbUserStore) => {
|
||||
this._userStore = usersContext;
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const name = formData.get('name') as string;
|
||||
const email = formData.get('email') as string;
|
||||
const userGroup = formData.get('userGroup') as string;
|
||||
const message = formData.get('message') as string;
|
||||
|
||||
this._userStore?.invite(name, email, message, [userGroup]).then((user) => {
|
||||
if (user) {
|
||||
this._invitedUser = user;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _submitForm() {
|
||||
this._form?.requestSubmit();
|
||||
}
|
||||
|
||||
private _closeModal() {
|
||||
this.modalHandler?.close();
|
||||
}
|
||||
|
||||
private _resetForm() {
|
||||
this._invitedUser = undefined;
|
||||
}
|
||||
|
||||
private _goToProfile() {
|
||||
if (!this._invitedUser) return;
|
||||
|
||||
this._closeModal();
|
||||
history.pushState(null, '', '/section/users/view/users/user/' + this._invitedUser?.key); //TODO: URL Should be dynamic
|
||||
}
|
||||
|
||||
private _renderForm() {
|
||||
return html` <h1>Invite user</h1>
|
||||
<p style="margin-top: 0">
|
||||
Invite new users to give them access to Umbraco. An invite email will be sent to the user with information on
|
||||
how to log in to Umbraco. Invites last for 72 hours.
|
||||
</p>
|
||||
<uui-form>
|
||||
<form id="invite-form" name="invite-form" @submit="${this._handleSubmit}">
|
||||
<uui-form-layout-item>
|
||||
<uui-label 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-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>
|
||||
<span slot="description">Add groups to assign access and permissions</span>
|
||||
<b>ADD USER GROUP PICKER HERE</b>
|
||||
</uui-form-layout-item>
|
||||
<uui-form-layout-item>
|
||||
<uui-label slot="label" for="message" required>Message</uui-label>
|
||||
<uui-textarea id="message" label="message" name="message" required></uui-textarea>
|
||||
</uui-form-layout-item>
|
||||
</form>
|
||||
</uui-form>`;
|
||||
}
|
||||
|
||||
private _renderPostInvite() {
|
||||
if (!this._invitedUser) return nothing;
|
||||
|
||||
return html`<div id="post-invite">
|
||||
<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>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<uui-dialog-layout>
|
||||
${this._invitedUser ? this._renderPostInvite() : this._renderForm()}
|
||||
${this._invitedUser
|
||||
? 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="Invite 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="Send invite"
|
||||
look="primary"></uui-button>
|
||||
`}
|
||||
</uui-dialog-layout>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbEditorViewUsersInviteElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-editor-view-users-invite': UmbEditorViewUsersInviteElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
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 './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 { UmbContextConsumerMixin } from '../../../../../core/context';
|
||||
import UmbSectionViewUsersElement from './section-view-users.element';
|
||||
import { UmbModalService } from '../../../../../core/services/modal';
|
||||
|
||||
export type UsersViewType = 'list' | 'grid';
|
||||
@customElement('umb-editor-view-users-overview')
|
||||
export class UmbEditorViewUsersOverviewElement extends UmbContextConsumerMixin(LitElement) {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#sticky-top {
|
||||
position: sticky;
|
||||
top: -1px;
|
||||
z-index: 1;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0), 0 1px 2px rgba(0, 0, 0, 0);
|
||||
transition: 250ms box-shadow ease-in-out;
|
||||
}
|
||||
|
||||
#sticky-top.header-shadow {
|
||||
box-shadow: var(--uui-shadow-depth-2);
|
||||
}
|
||||
|
||||
#user-list-top-bar {
|
||||
padding: var(--uui-size-space-4) var(--uui-size-space-6);
|
||||
background-color: var(--uui-color-surface-alt);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
white-space: nowrap;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
#user-list {
|
||||
padding: var(--uui-size-space-6);
|
||||
padding-top: var(--uui-size-space-2);
|
||||
}
|
||||
#input-search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
uui-popover {
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.filter-dropdown {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: column;
|
||||
background-color: var(--uui-color-surface);
|
||||
padding: var(--uui-size-space-4);
|
||||
border-radius: var(--uui-size-border-radius);
|
||||
box-shadow: var(--uui-shadow-depth-2);
|
||||
width: fit-content;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
router-slot {
|
||||
overflow: hidden;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
private _selection: Array<string> = [];
|
||||
|
||||
@state()
|
||||
private _routes: IRoute[] = [
|
||||
{
|
||||
path: 'grid',
|
||||
component: () => import('./list-view-layouts/grid/editor-view-users-grid.element'),
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
component: () => import('./list-view-layouts/table/editor-view-users-table.element'),
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: '/section/users/view/users/overview/grid', //TODO: this should be dynamic
|
||||
},
|
||||
];
|
||||
|
||||
private _usersContext?: UmbSectionViewUsersElement;
|
||||
private _selectionSubscription?: Subscription;
|
||||
private _modalService?: UmbModalService;
|
||||
|
||||
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.consumeContext('umbModalService', (modalService: UmbModalService) => {
|
||||
this._modalService = modalService;
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
|
||||
this._selectionSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
private _toggleViewType() {
|
||||
const isList = window.location.pathname.split('/').pop() === 'list';
|
||||
|
||||
isList
|
||||
? history.pushState(null, '', '/section/users/view/users/overview/grid')
|
||||
: history.pushState(null, '', '/section/users/view/users/overview/list');
|
||||
}
|
||||
|
||||
private _renderSelection() {
|
||||
if (this._selection.length === 0) return nothing;
|
||||
|
||||
return html`<umb-editor-view-users-selection></umb-editor-view-users-selection>`;
|
||||
}
|
||||
|
||||
private _handleTogglePopover(event: PointerEvent) {
|
||||
const composedPath = event.composedPath();
|
||||
|
||||
const popover = composedPath.find((el) => el instanceof UUIPopoverElement) as UUIPopoverElement;
|
||||
if (popover) {
|
||||
popover.open = !popover.open;
|
||||
}
|
||||
}
|
||||
|
||||
private _showInvite() {
|
||||
const invite = document.createElement('umb-editor-view-users-invite');
|
||||
|
||||
this._modalService?.open(invite, { 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>
|
||||
<div>
|
||||
<uui-popover margin="8">
|
||||
<uui-button @click=${this._handleTogglePopover} slot="trigger" label="status">
|
||||
Status: <b>All</b>
|
||||
</uui-button>
|
||||
<div slot="popover" class="filter-dropdown">
|
||||
<uui-checkbox label="Active"></uui-checkbox>
|
||||
<uui-checkbox label="Inactive"></uui-checkbox>
|
||||
<uui-checkbox label="Invited"></uui-checkbox>
|
||||
<uui-checkbox label="Disabled"></uui-checkbox>
|
||||
</div>
|
||||
</uui-popover>
|
||||
<uui-popover margin="8">
|
||||
<uui-button @click=${this._handleTogglePopover} slot="trigger" label="groups">
|
||||
Groups: <b>All</b>
|
||||
</uui-button>
|
||||
<div slot="popover" class="filter-dropdown">
|
||||
<uui-checkbox label="Active"></uui-checkbox>
|
||||
<uui-checkbox label="Inactive"></uui-checkbox>
|
||||
<uui-checkbox label="Invited"></uui-checkbox>
|
||||
<uui-checkbox label="Disabled"></uui-checkbox>
|
||||
</div>
|
||||
</uui-popover>
|
||||
<uui-popover margin="8">
|
||||
<uui-button @click=${this._handleTogglePopover} slot="trigger" label="order by">
|
||||
Order by: <b>Name (A-Z)</b>
|
||||
</uui-button>
|
||||
<div slot="popover" class="filter-dropdown">
|
||||
<uui-checkbox label="Active"></uui-checkbox>
|
||||
<uui-checkbox label="Inactive"></uui-checkbox>
|
||||
<uui-checkbox label="Invited"></uui-checkbox>
|
||||
<uui-checkbox label="Disabled"></uui-checkbox>
|
||||
</div>
|
||||
</uui-popover>
|
||||
<uui-button label="view toggle" @click=${this._toggleViewType} compact look="outline">
|
||||
<uui-icon name="settings"></uui-icon>
|
||||
</uui-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this._renderSelection()}
|
||||
</div>
|
||||
|
||||
<router-slot .routes=${this._routes}></router-slot>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbEditorViewUsersOverviewElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-editor-view-users-overview': UmbEditorViewUsersOverviewElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
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 { UmbContextConsumerMixin } from '../../../../../core/context';
|
||||
import type { UmbUserStore } from '../../../../../core/stores/user/user.store';
|
||||
import { UmbSectionViewUsersElement } from './section-view-users.element';
|
||||
|
||||
@customElement('umb-editor-view-users-selection')
|
||||
export class UmbEditorViewUsersSelectionElement extends UmbContextConsumerMixin(LitElement) {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
gap: var(--uui-size-3);
|
||||
width: 100%;
|
||||
padding: var(--uui-size-space-4) var(--uui-size-space-6);
|
||||
background-color: var(--uui-color-selected);
|
||||
color: var(--uui-color-selected-contrast);
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
private _selection: Array<string> = [];
|
||||
|
||||
@state()
|
||||
private _totalUsers = 0;
|
||||
|
||||
private _usersContext?: UmbSectionViewUsersElement;
|
||||
private _selectionSubscription?: Subscription;
|
||||
private _userStore?: UmbUserStore;
|
||||
private _totalUsersSubscription?: Subscription;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.consumeContext('umbUsersContext', (usersContext: UmbSectionViewUsersElement) => {
|
||||
this._usersContext = usersContext;
|
||||
this._observeSelection();
|
||||
});
|
||||
|
||||
this.consumeContext('umbUserStore', (userStore: UmbUserStore) => {
|
||||
this._userStore = userStore;
|
||||
this._observeTotalUsers();
|
||||
});
|
||||
}
|
||||
|
||||
private _observeSelection() {
|
||||
this._selectionSubscription?.unsubscribe();
|
||||
this._selectionSubscription = this._usersContext?.selection.subscribe((selection: Array<string>) => {
|
||||
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();
|
||||
}
|
||||
|
||||
private _handleClearSelection() {
|
||||
this._usersContext?.setSelection([]);
|
||||
}
|
||||
|
||||
private _renderSelectionCount() {
|
||||
return html`<div>${this._selection.length} of ${this._totalUsers} selected</div>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<uui-button @click=${this._handleClearSelection} label="Clear selection" look="secondary"></uui-button>
|
||||
${this._renderSelectionCount()}
|
||||
<uui-button style="margin-left: auto" label="Set group" look="secondary"></uui-button>
|
||||
<uui-button label="Enable" look="secondary"></uui-button>
|
||||
<uui-button label="Unlock" disabled look="secondary"></uui-button>
|
||||
<uui-button label="Disable" look="secondary"></uui-button> `;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbEditorViewUsersSelectionElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-editor-view-users-selection': UmbEditorViewUsersSelectionElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
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 { UmbContextConsumerMixin } from '../../../../../../../core/context';
|
||||
import UmbSectionViewUsersElement from '../../section-view-users.element';
|
||||
import { UmbUserStore } from '../../../../../../../core/stores/user/user.store';
|
||||
import type { UserDetails, UserEntity } from '../../../../../../../core/models';
|
||||
import { getTagLookAndColor } from '../../../../user-extensions';
|
||||
|
||||
@customElement('umb-editor-view-users-grid')
|
||||
export class UmbEditorViewUsersGridElement extends UmbContextConsumerMixin(LitElement) {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#user-grid {
|
||||
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;
|
||||
}
|
||||
|
||||
uui-card-user {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.user-login-time {
|
||||
margin-top: auto;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
private _users: Array<UserDetails> = [];
|
||||
|
||||
@state()
|
||||
private _selection: Array<string> = [];
|
||||
|
||||
private _userStore?: UmbUserStore;
|
||||
private _usersContext?: UmbSectionViewUsersElement;
|
||||
private _usersSubscription?: Subscription;
|
||||
private _selectionSubscription?: Subscription;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.consumeContext('umbUserStore', (userStore: UmbUserStore) => {
|
||||
this._userStore = userStore;
|
||||
this._observeUsers();
|
||||
});
|
||||
|
||||
this.consumeContext('umbUsersContext', (usersContext: UmbSectionViewUsersElement) => {
|
||||
this._usersContext = usersContext;
|
||||
this._observeSelection();
|
||||
});
|
||||
}
|
||||
|
||||
private _observeUsers() {
|
||||
this._usersSubscription?.unsubscribe();
|
||||
this._usersSubscription = this._userStore?.getAll().subscribe((users) => {
|
||||
this._users = users;
|
||||
});
|
||||
}
|
||||
|
||||
private _observeSelection() {
|
||||
this._selectionSubscription?.unsubscribe();
|
||||
this._selectionSubscription = this._usersContext?.selection.subscribe((selection: Array<string>) => {
|
||||
this._selection = selection;
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
|
||||
this._usersSubscription?.unsubscribe();
|
||||
this._selectionSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
private _isSelected(key: string) {
|
||||
return this._selection.includes(key);
|
||||
}
|
||||
|
||||
//TODO How should we handle url stuff?
|
||||
private _handleOpenCard(key: string) {
|
||||
history.pushState(null, '', '/section/users/view/users/user/' + key); //TODO Change to a tag with href and make dynamic
|
||||
}
|
||||
|
||||
private _selectRowHandler(user: UserEntity) {
|
||||
this._usersContext?.select(user.key);
|
||||
}
|
||||
|
||||
private _deselectRowHandler(user: UserEntity) {
|
||||
this._usersContext?.deselect(user.key);
|
||||
}
|
||||
|
||||
private renderUserCard(user: UserDetails) {
|
||||
if (!this._userStore) return;
|
||||
|
||||
const statusLook = getTagLookAndColor(user.status);
|
||||
|
||||
return html`
|
||||
<uui-card-user
|
||||
.name=${user.name}
|
||||
selectable
|
||||
?select-only=${this._selection.length > 0}
|
||||
?selected=${this._isSelected(user.key)}
|
||||
@open=${() => this._handleOpenCard(user.key)}
|
||||
@selected=${() => this._selectRowHandler(user)}
|
||||
@unselected=${() => this._deselectRowHandler(user)}>
|
||||
${user.status && user.status !== 'enabled'
|
||||
? html`<uui-tag
|
||||
slot="tag"
|
||||
size="s"
|
||||
look="${ifDefined(statusLook?.look)}"
|
||||
color="${ifDefined(statusLook?.color)}">
|
||||
${user.status}
|
||||
</uui-tag>`
|
||||
: nothing}
|
||||
<div>USER GROUPS NOT IMPLEMENTED</div>
|
||||
${user.lastLoginDate
|
||||
? html`<div class="user-login-time">
|
||||
<div>Last login</div>
|
||||
${user.lastLoginDate}
|
||||
</div>`
|
||||
: html`<div class="user-login-time">${`${user.name} has not logged in yet`}</div>`}
|
||||
</uui-card-user>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-scroll-container>
|
||||
<div id="user-grid">
|
||||
${repeat(
|
||||
this._users,
|
||||
(user) => user.key,
|
||||
(user) => this.renderUserCard(user)
|
||||
)}
|
||||
</div>
|
||||
</uui-scroll-container>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbEditorViewUsersGridElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-editor-view-users-grid': UmbEditorViewUsersGridElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { html, LitElement } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import type { UmbTableColumn, UmbTableItem } from '../../../../../../../../components/table/table.element';
|
||||
|
||||
@customElement('umb-user-table-name-column-layout')
|
||||
export class UmbUserTableNameColumnLayoutElement extends LitElement {
|
||||
@property({ type: Object, attribute: false })
|
||||
column!: UmbTableColumn;
|
||||
|
||||
@property({ type: Object, attribute: false })
|
||||
item!: UmbTableItem;
|
||||
|
||||
@property({ attribute: false })
|
||||
value!: any;
|
||||
|
||||
render() {
|
||||
return html` <div style="display: flex; align-items: center;">
|
||||
<uui-avatar name="${this.value.name}" style="margin-right: 10px;"></uui-avatar>
|
||||
<a style="font-weight: bold;" href="/section/users/view/users/user/${this.item.key}">${this.value.name}</a>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbUserTableNameColumnLayoutElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-user-table-name-column-layout': UmbUserTableNameColumnLayoutElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { html, LitElement, nothing } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { getTagLookAndColor } from '../../../../../../user-extensions';
|
||||
|
||||
@customElement('umb-user-table-status-column-layout')
|
||||
export class UmbUserTableStatusColumnLayoutElement extends LitElement {
|
||||
@property({ attribute: false })
|
||||
value: any;
|
||||
|
||||
render() {
|
||||
return html`${this.value.status && this.value.status !== 'enabled'
|
||||
? html`<uui-tag
|
||||
size="s"
|
||||
look="${getTagLookAndColor(this.value.status).look}"
|
||||
color="${getTagLookAndColor(this.value.status).color}">
|
||||
${this.value.status}
|
||||
</uui-tag>`
|
||||
: nothing}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbUserTableStatusColumnLayoutElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-user-table-status-column-layout': UmbUserTableStatusColumnLayoutElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
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 { UmbContextConsumerMixin } from '../../../../../../../core/context';
|
||||
import type { UmbSectionViewUsersElement } from '../../section-view-users.element';
|
||||
import { UmbUserStore } from '../../../../../../../core/stores/user/user.store';
|
||||
import type { UserDetails } from '../../../../../../../core/models';
|
||||
import {
|
||||
UmbTableElement,
|
||||
UmbTableColumn,
|
||||
UmbTableDeselectedEvent,
|
||||
UmbTableItem,
|
||||
UmbTableSelectedEvent,
|
||||
UmbTableConfig,
|
||||
UmbTableOrderedEvent,
|
||||
} from '../../../../../../components/table/table.element';
|
||||
|
||||
import './column-layouts/name/user-table-name-column-layout.element';
|
||||
import './column-layouts/status/user-table-status-column-layout.element';
|
||||
|
||||
@customElement('umb-editor-view-users-table')
|
||||
export class UmbEditorViewUsersTableElement extends UmbContextConsumerMixin(LitElement) {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
private _users: Array<UserDetails> = [];
|
||||
|
||||
@state()
|
||||
private _tableConfig: UmbTableConfig = {
|
||||
allowSelection: true,
|
||||
};
|
||||
|
||||
@state()
|
||||
private _tableColumns: Array<UmbTableColumn> = [
|
||||
{
|
||||
name: 'Name',
|
||||
alias: 'userName',
|
||||
elementName: 'umb-user-table-name-column-layout',
|
||||
},
|
||||
{
|
||||
name: 'User group',
|
||||
alias: 'userGroup',
|
||||
},
|
||||
{
|
||||
name: 'Last login',
|
||||
alias: 'userLastLogin',
|
||||
},
|
||||
{
|
||||
name: 'Status',
|
||||
alias: 'userStatus',
|
||||
elementName: 'umb-user-table-status-column-layout',
|
||||
},
|
||||
];
|
||||
|
||||
@state()
|
||||
private _tableItems: Array<UmbTableItem> = [];
|
||||
|
||||
@state()
|
||||
private _selection: Array<string> = [];
|
||||
|
||||
private _userStore?: UmbUserStore;
|
||||
private _usersContext?: UmbSectionViewUsersElement;
|
||||
private _usersSubscription?: Subscription;
|
||||
private _selectionSubscription?: Subscription;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.consumeContext('umbUserStore', (userStore: UmbUserStore) => {
|
||||
this._userStore = userStore;
|
||||
this._observeUsers();
|
||||
});
|
||||
|
||||
this.consumeContext('umbUsersContext', (usersContext: UmbSectionViewUsersElement) => {
|
||||
this._usersContext = usersContext;
|
||||
this._observeSelection();
|
||||
});
|
||||
}
|
||||
|
||||
private _observeUsers() {
|
||||
this._usersSubscription?.unsubscribe();
|
||||
this._usersSubscription = this._userStore?.getAll().subscribe((users) => {
|
||||
this._users = users;
|
||||
this._createTableItems(this._users);
|
||||
});
|
||||
}
|
||||
|
||||
private _observeSelection() {
|
||||
this._selectionSubscription = this._usersContext?.selection.subscribe((selection: Array<string>) => {
|
||||
if (this._selection === selection) return;
|
||||
this._selection = selection;
|
||||
});
|
||||
}
|
||||
|
||||
private _createTableItems(users: Array<UserDetails>) {
|
||||
this._tableItems = users.map((user) => {
|
||||
return {
|
||||
key: user.key,
|
||||
icon: 'umb:user',
|
||||
data: [
|
||||
{
|
||||
columnAlias: 'userName',
|
||||
value: {
|
||||
name: user.name,
|
||||
},
|
||||
},
|
||||
{
|
||||
columnAlias: 'userGroup',
|
||||
value: user.userGroup,
|
||||
},
|
||||
{
|
||||
columnAlias: 'userLastLogin',
|
||||
value: user.lastLoginDate,
|
||||
},
|
||||
{
|
||||
columnAlias: 'userStatus',
|
||||
value: {
|
||||
status: user.status,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSelected(event: UmbTableSelectedEvent) {
|
||||
event.stopPropagation();
|
||||
const table = event.target as UmbTableElement;
|
||||
const selection = table.selection;
|
||||
this._usersContext?.setSelection(selection);
|
||||
}
|
||||
|
||||
private _handleDeselected(event: UmbTableDeselectedEvent) {
|
||||
event.stopPropagation();
|
||||
const table = event.target as UmbTableElement;
|
||||
const selection = table.selection;
|
||||
this._usersContext?.setSelection(selection);
|
||||
}
|
||||
|
||||
private _handleOrdering(event: UmbTableOrderedEvent) {
|
||||
const table = event.target as UmbTableElement;
|
||||
const orderingColumn = table.orderingColumn;
|
||||
const orderingDesc = table.orderingDesc;
|
||||
console.log(`fetch users, order column: ${orderingColumn}, desc: ${orderingDesc}`);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
|
||||
this._usersSubscription?.unsubscribe();
|
||||
this._selectionSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<umb-table
|
||||
.config=${this._tableConfig}
|
||||
.columns=${this._tableColumns}
|
||||
.items=${this._tableItems}
|
||||
.selection=${this._selection}
|
||||
@selected="${this._handleSelected}"
|
||||
@deselected="${this._handleDeselected}"
|
||||
@ordered="${this._handleOrdering}"></umb-table>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbEditorViewUsersTableElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-editor-view-users-table': UmbEditorViewUsersTableElement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { css, html, LitElement } from 'lit';
|
||||
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 { UmbContextProviderMixin } from '../../../../../core/context';
|
||||
import type { UmbEditorEntityElement } from '../../../../editors/shared/editor-entity/editor-entity.element';
|
||||
|
||||
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';
|
||||
|
||||
@customElement('umb-section-view-users')
|
||||
export class UmbSectionViewUsersElement extends UmbContextProviderMixin(LitElement) {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
height: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@state()
|
||||
private _routes: IRoute[] = [
|
||||
{
|
||||
path: 'overview',
|
||||
component: () => import('./editor-view-users-overview.element'),
|
||||
},
|
||||
{
|
||||
path: `:entityType/:key`,
|
||||
component: () => import('../../../../editors/shared/editor-entity/editor-entity.element'),
|
||||
setup: (component: HTMLElement, info: IRoutingInfo) => {
|
||||
const element = component as UmbEditorEntityElement;
|
||||
element.entityKey = info.match.params.key;
|
||||
element.entityType = info.match.params.entityType;
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: '/section/users/view/users/overview', //TODO: this should be dynamic
|
||||
},
|
||||
];
|
||||
|
||||
private _selection: BehaviorSubject<Array<string>> = new BehaviorSubject(<Array<string>>[]);
|
||||
public readonly selection: Observable<Array<string>> = this._selection.asObservable();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.provideContext('umbUsersContext', this);
|
||||
}
|
||||
|
||||
public setSelection(value: Array<string>) {
|
||||
if (!value) return;
|
||||
this._selection.next(value);
|
||||
this.requestUpdate('selection');
|
||||
}
|
||||
|
||||
public select(key: string) {
|
||||
const selection = this._selection.getValue();
|
||||
this._selection.next([...selection, key]);
|
||||
this.requestUpdate('selection');
|
||||
}
|
||||
|
||||
public deselect(key: string) {
|
||||
const selection = this._selection.getValue();
|
||||
this._selection.next(selection.filter((k) => k !== key));
|
||||
this.requestUpdate('selection');
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <router-slot .routes=${this._routes}></router-slot> `;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbSectionViewUsersElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-section-view-users': UmbSectionViewUsersElement;
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,11 @@ import type {
|
||||
ManifestPropertyAction,
|
||||
ManifestPropertyEditorUI,
|
||||
ManifestSection,
|
||||
ManifestSectionView,
|
||||
ManifestTree,
|
||||
ManifestTreeItemAction,
|
||||
ManifestEditor,
|
||||
ManifestEditorAction,
|
||||
ManifestCustom,
|
||||
ManifestPackageView,
|
||||
} from '../models';
|
||||
@@ -58,11 +60,13 @@ export class UmbExtensionRegistry {
|
||||
|
||||
// Typings concept, need to put all core types to get a good array return type for the provided type...
|
||||
extensionsOfType(type: 'section'): Observable<Array<ManifestSection>>;
|
||||
extensionsOfType(type: 'sectionView'): Observable<Array<ManifestSectionView>>;
|
||||
extensionsOfType(type: 'tree'): Observable<Array<ManifestTree>>;
|
||||
extensionsOfType(type: 'editor'): Observable<Array<ManifestEditor>>;
|
||||
extensionsOfType(type: 'treeItemAction'): Observable<Array<ManifestTreeItemAction>>;
|
||||
extensionsOfType(type: 'dashboard'): Observable<Array<ManifestDashboard>>;
|
||||
extensionsOfType(type: 'editorView'): Observable<Array<ManifestEditorView>>;
|
||||
extensionsOfType(type: 'editorAction'): Observable<Array<ManifestEditorAction>>;
|
||||
extensionsOfType(type: 'propertyEditorUI'): Observable<Array<ManifestPropertyEditorUI>>;
|
||||
extensionsOfType(type: 'propertyAction'): Observable<Array<ManifestPropertyAction>>;
|
||||
extensionsOfType(type: 'packageView'): Observable<Array<ManifestPackageView>>;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { components } from '../../../schemas/generated-schema';
|
||||
import type { UserStatus } from '../../backoffice/sections/users/user-extensions';
|
||||
import { Entity } from '../../mocks/data/entities';
|
||||
|
||||
export type PostInstallRequest = components['schemas']['InstallSetupRequest'];
|
||||
export type StatusResponse = components['schemas']['StatusResponse'];
|
||||
@@ -19,9 +21,11 @@ export type TelemetryModel = components['schemas']['TelemetryModel'];
|
||||
export type ServerStatus = components['schemas']['ServerStatus'];
|
||||
export type ManifestTypes = components['schemas']['Manifest'];
|
||||
export type ManifestSection = components['schemas']['IManifestSection'];
|
||||
export type ManifestSectionView = components['schemas']['IManifestSectionView'];
|
||||
export type ManifestTree = components['schemas']['IManifestTree'];
|
||||
export type ManifestTreeItemAction = components['schemas']['IManifestTreeItemAction'];
|
||||
export type ManifestEditor = components['schemas']['IManifestEditor'];
|
||||
export type ManifestEditorAction = components['schemas']['IManifestEditorAction'];
|
||||
export type ManifestPropertyEditorUI = components['schemas']['IManifestPropertyEditorUI'];
|
||||
export type ManifestDashboard = components['schemas']['IManifestDashboard'];
|
||||
export type ManifestEditorView = components['schemas']['IManifestEditorView'];
|
||||
@@ -43,6 +47,7 @@ export type PropertyEditorConfigDefaultData = components['schemas']['PropertyEdi
|
||||
|
||||
export type ManifestElementType =
|
||||
| ManifestSection
|
||||
| ManifestSectionView
|
||||
| ManifestTree
|
||||
| ManifestTreeItemAction
|
||||
| ManifestEditor
|
||||
@@ -50,7 +55,39 @@ export type ManifestElementType =
|
||||
| ManifestPropertyEditorUI
|
||||
| ManifestDashboard
|
||||
| ManifestEditorView
|
||||
| ManifestEditorAction
|
||||
| ManifestPackageView;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type HTMLElementConstructor<T = HTMLElement> = new (...args: any[]) => T;
|
||||
|
||||
// Users
|
||||
export interface UserEntity extends Entity {
|
||||
type: 'user';
|
||||
}
|
||||
|
||||
export interface UserDetails extends UserEntity {
|
||||
email: string;
|
||||
status: UserStatus;
|
||||
language: string;
|
||||
lastLoginDate?: string;
|
||||
lastLockoutDate?: string;
|
||||
lastPasswordChangeDate?: string;
|
||||
updateDate: string;
|
||||
createDate: string;
|
||||
failedLoginAttempts: number;
|
||||
userGroup?: string; //TODO Implement this
|
||||
}
|
||||
|
||||
export interface UserGroupEntity extends Entity {
|
||||
type: 'userGroup';
|
||||
}
|
||||
|
||||
export interface UserGroupDetails extends UserGroupEntity {
|
||||
key: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
sections?: Array<string>;
|
||||
contentStartNode?: string;
|
||||
mediaStartNode?: string;
|
||||
}
|
||||
|
||||
@@ -18,9 +18,16 @@ export interface UmbDataStore<T> {
|
||||
* @description - Base class for Data Stores
|
||||
*/
|
||||
export class UmbDataStoreBase<T extends UmbDataStoreIdentifiers> implements UmbDataStore<T> {
|
||||
private _items: BehaviorSubject<Array<T>> = new BehaviorSubject(<Array<T>>[]);
|
||||
protected _items: BehaviorSubject<Array<T>> = new BehaviorSubject(<Array<T>>[]);
|
||||
public readonly items: Observable<Array<T>> = this._items.asObservable();
|
||||
|
||||
public delete(deletedKeys: Array<string>): void {
|
||||
const remainingItems = this._items
|
||||
.getValue()
|
||||
.filter((item) => item.key && deletedKeys.includes(item.key) === false);
|
||||
this._items.next(remainingItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description - Update the store with new items. Existing items are updated, new items are added. Existing items are matched by the compareKey.
|
||||
* @param {Array<T>} updatedItems
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { BehaviorSubject, map, Observable } from 'rxjs';
|
||||
import type { UserDetails, UserEntity, UserGroupDetails } from '../../models';
|
||||
import { UmbEntityStore } from '../entity.store';
|
||||
import { UmbDataStoreBase } from '../store';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
/**
|
||||
* @export
|
||||
* @class UmbUserGroupStore
|
||||
* @extends {UmbDataStoreBase<UserGroupEntity>}
|
||||
* @description - Data Store for Users
|
||||
*/
|
||||
export class UmbUserGroupStore extends UmbDataStoreBase<UserGroupDetails> {
|
||||
private _entityStore: UmbEntityStore;
|
||||
|
||||
constructor(entityStore: UmbEntityStore) {
|
||||
super();
|
||||
this._entityStore = entityStore;
|
||||
}
|
||||
|
||||
getAll(): Observable<Array<UserGroupDetails>> {
|
||||
// TODO: use Fetcher API.
|
||||
// TODO: only fetch if the data type is not in the store?
|
||||
fetch(`/umbraco/backoffice/user-groups/list/items`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
this.update(data.items);
|
||||
});
|
||||
|
||||
return this.items;
|
||||
}
|
||||
}
|
||||
201
src/Umbraco.Web.UI.Client/src/core/stores/user/user.store.ts
Normal file
201
src/Umbraco.Web.UI.Client/src/core/stores/user/user.store.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { BehaviorSubject, map, Observable } from 'rxjs';
|
||||
import type { UserDetails, UserEntity } from '../../models';
|
||||
import { UmbEntityStore } from '../entity.store';
|
||||
import { UmbDataStoreBase } from '../store';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
/**
|
||||
* @export
|
||||
* @class UmbUserStore
|
||||
* @extends {UmbDataStoreBase<UserEntity>}
|
||||
* @description - Data Store for Users
|
||||
*/
|
||||
export class UmbUserStore extends UmbDataStoreBase<UserDetails> {
|
||||
private _entityStore: UmbEntityStore;
|
||||
|
||||
private _totalUsers: BehaviorSubject<number> = new BehaviorSubject(0);
|
||||
public readonly totalUsers: Observable<number> = this._totalUsers.asObservable();
|
||||
|
||||
constructor(entityStore: UmbEntityStore) {
|
||||
super();
|
||||
this._entityStore = entityStore;
|
||||
}
|
||||
|
||||
getAll(): Observable<Array<UserDetails>> {
|
||||
// TODO: use Fetcher API.
|
||||
// TODO: only fetch if the data type is not in the store?
|
||||
fetch(`/umbraco/backoffice/users/list/items`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
this._totalUsers.next(data.total);
|
||||
this.update(data.items);
|
||||
});
|
||||
|
||||
return this.items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description - Request a Data Type by key. The Data Type is added to the store and is returned as an Observable.
|
||||
* @param {string} key
|
||||
* @return {*} {(Observable<DataTypeDetails | null>)}
|
||||
* @memberof UmbDataTypeStore
|
||||
*/
|
||||
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}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
this.update([data]);
|
||||
});
|
||||
|
||||
return this.items.pipe(
|
||||
map((items: Array<UserDetails>) => items.find((node: UserDetails) => node.key === key) || null)
|
||||
);
|
||||
}
|
||||
|
||||
async enableUsers(userKeys: Array<string>): Promise<void> {
|
||||
// TODO: use Fetcher API.
|
||||
try {
|
||||
const res = await fetch('/umbraco/backoffice/users/enable', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(userKeys),
|
||||
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.status = 'enabled';
|
||||
});
|
||||
|
||||
this.update(storedUsers);
|
||||
this._entityStore.update(storedUsers);
|
||||
} catch (error) {
|
||||
console.error('Enable Users failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
async disableUsers(userKeys: Array<string>): Promise<void> {
|
||||
// TODO: use Fetcher API.
|
||||
try {
|
||||
const res = await fetch('/umbraco/backoffice/users/disable', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(userKeys),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const disabledKeys = await res.json();
|
||||
const storedUsers = this._items.getValue().filter((user) => disabledKeys.includes(user.key));
|
||||
|
||||
storedUsers.forEach((user) => {
|
||||
user.status = 'disabled';
|
||||
});
|
||||
|
||||
this.update(storedUsers);
|
||||
this._entityStore.update(storedUsers);
|
||||
} catch (error) {
|
||||
console.error('Disable Users failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteUsers(userKeys: Array<string>): Promise<void> {
|
||||
// TODO: use Fetcher API.
|
||||
try {
|
||||
const res = await fetch('/umbraco/backoffice/users/delete', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(userKeys),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const deletedKeys = await res.json();
|
||||
this.delete(deletedKeys);
|
||||
this._entityStore.delete(deletedKeys);
|
||||
} catch (error) {
|
||||
console.error('Delete Users failed', error);
|
||||
}
|
||||
}
|
||||
|
||||
async save(users: Array<UserDetails>): Promise<void> {
|
||||
// TODO: use Fetcher API.
|
||||
try {
|
||||
const res = await fetch('/umbraco/backoffice/users/save', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(users),
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
async invite(name: string, email: string, message: string, userGroups: Array<string>): Promise<UserDetails | null> {
|
||||
// TODO: use Fetcher API.
|
||||
try {
|
||||
const res = await fetch('/umbraco/backoffice/users/invite', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name, email, message, userGroups }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const json = (await res.json()) as UserDetails[];
|
||||
this.update(json);
|
||||
this._entityStore.update(json);
|
||||
return json[0];
|
||||
} catch (error) {
|
||||
console.error('Invite user error', error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// public updateUser(user: UserItem) {
|
||||
// const users = this._users.getValue();
|
||||
// const index = users.findIndex((u) => u.key === user.key);
|
||||
// if (index === -1) return;
|
||||
// users[index] = { ...users[index], ...user };
|
||||
// console.log('updateUser', user, users[index]);
|
||||
// this._users.next(users);
|
||||
// this.requestUpdate('users');
|
||||
// }
|
||||
|
||||
// public inviteUser(name: string, email: string, userGroup: string, message: string): UserItem {
|
||||
// const users = this._users.getValue();
|
||||
// const user = {
|
||||
// id: this._users.getValue().length + 1,
|
||||
// key: uuidv4(),
|
||||
// name: name,
|
||||
// email: email,
|
||||
// status: 'invited',
|
||||
// language: 'en',
|
||||
// updateDate: new Date().toISOString(),
|
||||
// createDate: new Date().toISOString(),
|
||||
// failedLoginAttempts: 0,
|
||||
// userGroup: userGroup,
|
||||
// };
|
||||
// this._users.next([...users, user]);
|
||||
// this.requestUpdate('users');
|
||||
|
||||
// //TODO: Send invite email with message
|
||||
// return user;
|
||||
// }
|
||||
|
||||
// public deleteUser(key: string) {
|
||||
// const users = this._users.getValue();
|
||||
// const index = users.findIndex((u) => u.key === key);
|
||||
// if (index === -1) return;
|
||||
// users.splice(index, 1);
|
||||
// this._users.next(users);
|
||||
// this.requestUpdate('users');
|
||||
// }
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import { handlers as upgradeHandlers } from './domains/upgrade.handlers';
|
||||
import { handlers as userHandlers } from './domains/user.handlers';
|
||||
import { handlers as telemetryHandlers } from './domains/telemetry.handlers';
|
||||
import { handlers as propertyEditorHandlers } from './domains/property-editor.handlers';
|
||||
import { handlers as usersHandlers } from './domains/users.handlers';
|
||||
import { handlers as userGroupsHandlers } from './domains/user-groups.handlers';
|
||||
|
||||
const handlers = [
|
||||
serverHandlers.serverVersionHandler,
|
||||
@@ -24,6 +26,8 @@ const handlers = [
|
||||
...manifestsHandlers.default,
|
||||
...telemetryHandlers,
|
||||
...publishedStatusHandlers,
|
||||
...usersHandlers,
|
||||
...userGroupsHandlers,
|
||||
];
|
||||
|
||||
switch (import.meta.env.VITE_UMBRACO_INSTALL_STATUS) {
|
||||
|
||||
@@ -7,9 +7,9 @@ export class UmbEntityData<T extends Entity> extends UmbData<T> {
|
||||
super(data);
|
||||
}
|
||||
|
||||
getItems(type = '', parentKey = '') {
|
||||
getItems(type: string, parentKey = '') {
|
||||
if (!type) return [];
|
||||
return entities.filter((item) => item.type === type && item.parentKey === parentKey);
|
||||
return this.data.filter((item) => item.type === type && item.parentKey === parentKey);
|
||||
}
|
||||
|
||||
getByKey(key: string) {
|
||||
@@ -47,6 +47,12 @@ export class UmbEntityData<T extends Entity> extends UmbData<T> {
|
||||
return trashedItems;
|
||||
}
|
||||
|
||||
delete(keys: Array<string>) {
|
||||
const deletedKeys = this.data.filter((item) => keys.includes(item.key)).map((item) => item.key);
|
||||
this.data = this.data.filter((item) => keys.indexOf(item.key) === -1);
|
||||
return deletedKeys;
|
||||
}
|
||||
|
||||
protected updateData(updateItem: T) {
|
||||
const itemIndex = this.data.findIndex((item) => item.key === updateItem.key);
|
||||
const item = this.data[itemIndex];
|
||||
|
||||
1796
src/Umbraco.Web.UI.Client/src/mocks/data/users.data.ts
Normal file
1796
src/Umbraco.Web.UI.Client/src/mocks/data/users.data.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
||||
import { rest } from 'msw';
|
||||
import type { UserGroupDetails } from '../../core/models';
|
||||
|
||||
export const handlers = [
|
||||
rest.get('/umbraco/backoffice/user-groups/list/items', (req, res, ctx) => {
|
||||
const items = fakeData;
|
||||
|
||||
const response = {
|
||||
total: items.length,
|
||||
items,
|
||||
};
|
||||
|
||||
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,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,93 @@
|
||||
import { rest } from 'msw';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { UserDetails } from '../../core/models';
|
||||
import { umbUsersData } from '../data/users.data';
|
||||
|
||||
// TODO: add schema
|
||||
export const handlers = [
|
||||
rest.get('/umbraco/backoffice/users/list/items', (req, res, ctx) => {
|
||||
const items = umbUsersData.getItems('user');
|
||||
|
||||
const response = {
|
||||
total: items.length,
|
||||
items,
|
||||
};
|
||||
|
||||
return res(ctx.status(200), ctx.json(response));
|
||||
}),
|
||||
|
||||
rest.get('/umbraco/backoffice/users/:key', (req, res, ctx) => {
|
||||
const key = req.params.key as string;
|
||||
if (!key) return;
|
||||
|
||||
const user = umbUsersData.getByKey(key);
|
||||
|
||||
return res(ctx.status(200), ctx.json(user));
|
||||
}),
|
||||
|
||||
rest.post<UserDetails[]>('/umbraco/backoffice/users/save', async (req, res, ctx) => {
|
||||
const data = await req.json();
|
||||
if (!data) return;
|
||||
|
||||
const saved = umbUsersData.save(data);
|
||||
|
||||
console.log('saved', saved);
|
||||
|
||||
return res(ctx.status(200), ctx.json(saved));
|
||||
}),
|
||||
|
||||
rest.post<UserDetails[]>('/umbraco/backoffice/users/invite', async (req, res, ctx) => {
|
||||
const data = await req.json();
|
||||
if (!data) return;
|
||||
|
||||
const newUser: UserDetails = {
|
||||
key: uuidv4(),
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
status: 'invited',
|
||||
language: 'en',
|
||||
updateDate: new Date().toISOString(),
|
||||
createDate: new Date().toISOString(),
|
||||
failedLoginAttempts: 0,
|
||||
parentKey: '',
|
||||
isTrashed: false,
|
||||
hasChildren: false,
|
||||
type: 'user',
|
||||
icon: 'umb:icon-user',
|
||||
userGroup: data.userGroups[0],
|
||||
};
|
||||
|
||||
const invited = umbUsersData.save([newUser]);
|
||||
|
||||
console.log('invited', invited);
|
||||
|
||||
return res(ctx.status(200), ctx.json(invited));
|
||||
}),
|
||||
|
||||
rest.post<Array<string>>('/umbraco/backoffice/users/enable', async (req, res, ctx) => {
|
||||
const data = await req.json();
|
||||
if (!data) return;
|
||||
|
||||
const enabledKeys = umbUsersData.enable(data);
|
||||
|
||||
return res(ctx.status(200), ctx.json(enabledKeys));
|
||||
}),
|
||||
|
||||
rest.post<Array<string>>('/umbraco/backoffice/users/disable', async (req, res, ctx) => {
|
||||
const data = await req.json();
|
||||
if (!data) return;
|
||||
|
||||
const disabledKeys = umbUsersData.disable(data);
|
||||
|
||||
return res(ctx.status(200), ctx.json(disabledKeys));
|
||||
}),
|
||||
|
||||
rest.post<Array<string>>('/umbraco/backoffice/users/delete', async (req, res, ctx) => {
|
||||
const data = await req.json();
|
||||
if (!data) return;
|
||||
|
||||
const deletedKeys = umbUsersData.delete(data);
|
||||
|
||||
return res(ctx.status(200), ctx.json(deletedKeys));
|
||||
}),
|
||||
];
|
||||
@@ -63,6 +63,17 @@ export const internalManifests: Array<ManifestTypes & { loader: () => Promise<ob
|
||||
weight: 20,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
alias: 'Umb.Section.Users',
|
||||
name: 'Users',
|
||||
loader: () => import('../backoffice/sections/users/section-users.element'),
|
||||
meta: {
|
||||
label: 'Users',
|
||||
pathname: 'users',
|
||||
weight: 20,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
alias: 'Umb.Dashboard.Welcome',
|
||||
@@ -91,14 +102,14 @@ export const internalManifests: Array<ManifestTypes & { loader: () => Promise<ob
|
||||
},
|
||||
{
|
||||
type: 'dashboard',
|
||||
alias: 'Umb.Dashboard.SettingsAbout',
|
||||
name: 'About Settings Dashboard',
|
||||
elementName: 'umb-dashboard-settings-about',
|
||||
loader: () => import('../backoffice/dashboards/settings-about/dashboard-settings-about.element'),
|
||||
alias: 'Umb.Dashboard.SettingsWelcome',
|
||||
name: 'Welcome Settings Dashboard',
|
||||
elementName: 'umb-dashboard-settings-welcome',
|
||||
loader: () => import('../backoffice/dashboards/settings-welcome/dashboard-settings-welcome.element'),
|
||||
meta: {
|
||||
label: 'About',
|
||||
label: 'Welcome',
|
||||
sections: ['Umb.Section.Settings'],
|
||||
pathname: 'about', // TODO: how to we want to support pretty urls?
|
||||
pathname: 'welcome', // TODO: how to we want to support pretty urls?
|
||||
weight: 10,
|
||||
},
|
||||
},
|
||||
@@ -273,6 +284,33 @@ export const internalManifests: Array<ManifestTypes & { loader: () => Promise<ob
|
||||
weight: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'editor',
|
||||
alias: 'Umb.Editor.User',
|
||||
name: 'User Editor',
|
||||
loader: () => import('../backoffice/editors/user/editor-user.element'),
|
||||
meta: {
|
||||
entityType: 'user',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'editor',
|
||||
alias: 'Umb.Editor.UserGroup',
|
||||
name: 'User Group Editor',
|
||||
loader: () => import('../backoffice/editors/user-group/editor-user-group.element'),
|
||||
meta: {
|
||||
entityType: 'userGroup',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'editorAction',
|
||||
alias: 'Umb.EditorAction.User.Save',
|
||||
name: 'EditorActionUserSave',
|
||||
loader: () => import('../backoffice/editors/user/actions/editor-action-user-save.element'),
|
||||
meta: {
|
||||
editors: ['Umb.Editor.User'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'propertyAction',
|
||||
alias: 'Umb.PropertyAction.Copy',
|
||||
@@ -498,4 +536,30 @@ export const internalManifests: Array<ManifestTypes & { loader: () => Promise<ob
|
||||
weight: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'sectionView',
|
||||
alias: 'Umb.SectionView.Users',
|
||||
name: 'Users Section View',
|
||||
loader: () => import('../backoffice/sections/users/views/users/section-view-users.element'),
|
||||
meta: {
|
||||
sections: ['Umb.Section.Users'],
|
||||
label: 'Users',
|
||||
pathname: 'users',
|
||||
weight: 200,
|
||||
icon: 'umb:user',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'sectionView',
|
||||
alias: 'Umb.SectionView.UserGroups',
|
||||
name: 'User Groups Section View',
|
||||
loader: () => import('../backoffice/sections/users/views/user-groups/section-view-user-groups.element'),
|
||||
meta: {
|
||||
sections: ['Umb.Section.Users'],
|
||||
label: 'User Groups',
|
||||
pathname: 'user-groups',
|
||||
weight: 100,
|
||||
icon: 'umb:users',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -32,12 +32,14 @@ export class ManifestsPackagesInstalled {
|
||||
|
||||
export type Manifest =
|
||||
| IManifestSection
|
||||
| IManifestSectionView
|
||||
| IManifestTree
|
||||
| IManifestEditor
|
||||
| IManifestEditorAction
|
||||
| IManifestEditorView
|
||||
| IManifestTreeItemAction
|
||||
| IManifestPropertyEditorUI
|
||||
| IManifestDashboard
|
||||
| IManifestEditorView
|
||||
| IManifestPropertyAction
|
||||
| IManifestPackageView
|
||||
| IManifestEntrypoint
|
||||
@@ -45,12 +47,14 @@ export type Manifest =
|
||||
|
||||
export type ManifestStandardTypes =
|
||||
| 'section'
|
||||
| 'sectionView'
|
||||
| 'tree'
|
||||
| 'editor'
|
||||
| 'editorView'
|
||||
| 'editorAction'
|
||||
| 'treeItemAction'
|
||||
| 'propertyEditorUI'
|
||||
| 'dashboard'
|
||||
| 'editorView'
|
||||
| 'propertyAction'
|
||||
| 'packageView'
|
||||
| 'entrypoint';
|
||||
@@ -85,6 +89,14 @@ export interface MetaSection {
|
||||
weight: number;
|
||||
}
|
||||
|
||||
export interface MetaSectionView {
|
||||
sections: Array<string>;
|
||||
label: string;
|
||||
pathname: string;
|
||||
weight: number;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface MetaTree {
|
||||
weight: number;
|
||||
sections: Array<string>;
|
||||
@@ -148,6 +160,20 @@ export interface IManifestSection extends IManifestElement {
|
||||
meta: MetaSection;
|
||||
}
|
||||
|
||||
export interface IManifestSectionView extends IManifestElement {
|
||||
type: 'sectionView';
|
||||
meta: MetaSectionView;
|
||||
}
|
||||
|
||||
export interface IManifestEditorAction extends IManifestElement {
|
||||
type: 'editorAction';
|
||||
meta: MetaEditorAction;
|
||||
}
|
||||
|
||||
export interface MetaEditorAction {
|
||||
editors: Array<string>;
|
||||
}
|
||||
|
||||
export interface IManifestTree extends IManifestElement {
|
||||
type: 'tree';
|
||||
meta: MetaTree;
|
||||
|
||||
Reference in New Issue
Block a user