From f384da79898a27364a4be44d1a95ad953f6d9fde Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Mon, 19 Feb 2024 15:55:31 +0100 Subject: [PATCH 001/246] context and edit view --- .../member-type/workspace/manifests.ts | 23 +- .../member-type-workspace.context.ts | 59 +- ...-workspace-view-edit-properties.element.ts | 254 ++++++++ ...pe-workspace-view-edit-property.element.ts | 495 +++++++++++++++ ...er-type-workspace-view-edit-tab.element.ts | 321 ++++++++++ ...member-type-workspace-view-edit.element.ts | 574 ++++++++++++++++++ 6 files changed, 1715 insertions(+), 11 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-properties.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-property.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-tab.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/manifests.ts index 7f4b217861..af8484fefa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/manifests.ts @@ -5,6 +5,8 @@ import type { } from '@umbraco-cms/backoffice/extension-registry'; import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; +export const UMB_MEMBER_TYPE_WORKSPACE_ALIAS = 'Umb.Workspace.MemberType'; + const workspace: ManifestWorkspace = { type: 'workspace', alias: 'Umb.Workspace.MemberType', @@ -15,7 +17,26 @@ const workspace: ManifestWorkspace = { }, }; -const workspaceViews: Array = []; +const workspaceViews: Array = [ + { + type: 'workspaceView', + alias: 'Umb.WorkspaceView.MemberType.Design', + name: 'Member Type Workspace Design View', + js: () => import('./views/design/member-type-workspace-view-edit.element.js'), + weight: 1000, + meta: { + label: 'Design', + pathname: 'design', + icon: 'icon-member-dashed-line', + }, + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: UMB_MEMBER_TYPE_WORKSPACE_ALIAS, + }, + ], + }, +]; const workspaceActions: Array = [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts index f3c3df8bca..0a3a8bed3b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts @@ -4,27 +4,66 @@ import { type UmbSaveableWorkspaceContextInterface, UmbEditableWorkspaceContextBase, } from '@umbraco-cms/backoffice/workspace'; -import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbBooleanState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import { UmbContentTypePropertyStructureManager } from '@umbraco-cms/backoffice/content-type'; +type EntityType = UmbMemberTypeDetailModel; export class UmbMemberTypeWorkspaceContext - extends UmbEditableWorkspaceContextBase + extends UmbEditableWorkspaceContextBase implements UmbSaveableWorkspaceContextInterface { - public readonly detailRepository = new UmbMemberTypeDetailRepository(this); + #isSorting = new UmbBooleanState(undefined); + isSorting = this.#isSorting.asObservable(); - #data = new UmbObjectState(undefined); - readonly data = this.#data.asObservable(); + public readonly repository = new UmbMemberTypeDetailRepository(this); - readonly name = this.#data.asObservablePart((data) => data?.name); + #data = new UmbObjectState(undefined); + + // General for content types: + readonly data; + readonly name; + readonly alias; + readonly description; + readonly icon; + + readonly allowedAsRoot; + readonly variesByCulture; + readonly variesBySegment; + readonly isElement; + readonly allowedContentTypes; + readonly compositions; + + readonly structure = new UmbContentTypePropertyStructureManager(this, this.repository); constructor(host: UmbControllerHostElement) { super(host, 'Umb.Workspace.MemberType'); + + // General for content types: + this.data = this.structure.ownerContentType; + this.name = this.structure.ownerContentTypeObservablePart((data) => data?.name); + this.alias = this.structure.ownerContentTypeObservablePart((data) => data?.alias); + this.description = this.structure.ownerContentTypeObservablePart((data) => data?.description); + this.icon = this.structure.ownerContentTypeObservablePart((data) => data?.icon); + this.allowedAsRoot = this.structure.ownerContentTypeObservablePart((data) => data?.allowedAsRoot); + this.variesByCulture = this.structure.ownerContentTypeObservablePart((data) => data?.variesByCulture); + this.variesBySegment = this.structure.ownerContentTypeObservablePart((data) => data?.variesBySegment); + this.isElement = this.structure.ownerContentTypeObservablePart((data) => data?.isElement); + this.allowedContentTypes = this.structure.ownerContentTypeObservablePart((data) => data?.allowedContentTypes); + this.compositions = this.structure.ownerContentTypeObservablePart((data) => data?.compositions); + } + + setIsSorting(isSorting: boolean) { + this.#isSorting.setValue(isSorting); + } + + setProperty(propertyName: PropertyName, value: EntityType[PropertyName]) { + this.structure.updateOwnerContentType({ [propertyName]: value }); } async load(unique: string) { - const { data } = await this.detailRepository.requestByUnique(unique); + const { data } = await this.repository.requestByUnique(unique); if (data) { this.setIsNew(false); @@ -33,7 +72,7 @@ export class UmbMemberTypeWorkspaceContext } async create(parentUnique: string | null) { - const { data } = await this.detailRepository.createScaffold(parentUnique); + const { data } = await this.repository.createScaffold(parentUnique); if (data) { this.setIsNew(true); @@ -48,9 +87,9 @@ export class UmbMemberTypeWorkspaceContext if (!data) throw new Error('No data to save'); if (this.getIsNew()) { - await this.detailRepository.create(data); + await this.repository.create(data); } else { - await this.detailRepository.save(data); + await this.repository.save(data); } this.saveComplete(data); diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-properties.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-properties.element.ts new file mode 100644 index 0000000000..bfc40781cc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-properties.element.ts @@ -0,0 +1,254 @@ +import type { UmbMemberTypeWorkspaceContext } from '../../member-type-workspace.context.js'; +import './member-type-workspace-view-edit-property.element.js'; +import type { UmbMemberTypeDetailModel } from '../../../types.js'; +import type { UmbMemberTypeWorkspacePropertyElement } from './member-type-workspace-view-edit-property.element.js'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { css, html, customElement, property, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { UmbPropertyContainerTypes, UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; +import { UmbContentTypePropertyStructureHelper } from '@umbraco-cms/backoffice/content-type'; +import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; +import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import { UMB_PROPERTY_SETTINGS_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; + +@customElement('umb-member-type-workspace-view-edit-properties') +export class UmbMemberTypeWorkspaceViewEditPropertiesElement extends UmbLitElement { + #model: Array = []; + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => { + return element.getAttribute('data-umb-property-id'); + }, + getUniqueOfModel: (modelEntry) => { + return modelEntry.id; + }, + identifier: 'member-type-property-sorter', + itemSelector: 'umb-member-type-workspace-view-edit-property', + //disabledItemSelector: '[inherited]', + //TODO: Set the property list (sorter wrapper) to inherited, if its inherited + // This is because we don't want to move local properties into an inherited group container. + // Or maybe we do, but we still need to check if the group exists locally, if not, then it needs to be created before we move a property into it. + // TODO: Fix bug where a local property turn into an inherited when moved to a new group container. + containerSelector: '#property-list', + onChange: ({ item, model }) => { + this.#model = model; + this._propertyStructure = model; + }, + onEnd: ({ item }) => { + /** Explanation: If the item is the first in list, we compare it to the item behind it to set a sortOrder. + * If it's not the first in list, we will compare to the item in before it, and check the following item to see if it caused overlapping sortOrder, then update + * the overlap if true, which may cause another overlap, so we loop through them till no more overlaps... + */ + const model = this.#model; + const newIndex = model.findIndex((entry) => entry.id === item.id); + + // Doesn't exist in model + if (newIndex === -1) return; + + // First in list + if (newIndex === 0 && model.length > 1) { + this._propertyStructureHelper.partialUpdateProperty(item.id, { + sortOrder: model[1].sortOrder - 1, + container: this._containerId ? { id: this._containerId } : null, + }); + return; + } + + // Not first in list + if (newIndex > 0 && model.length > 1) { + const prevItemSortOrder = model[newIndex - 1].sortOrder; + + let weight = 1; + this._propertyStructureHelper.partialUpdateProperty(item.id, { + sortOrder: prevItemSortOrder + weight, + container: this._containerId ? { id: this._containerId } : null, + }); + + // Check for overlaps + model.some((entry, index) => { + if (index <= newIndex) return; + if (entry.sortOrder === prevItemSortOrder + weight) { + weight++; + this._propertyStructureHelper.partialUpdateProperty(entry.id, { sortOrder: prevItemSortOrder + weight }); + } + // Break the loop + return true; + }); + } + }, + }); + + private _containerId: string | undefined; + + @property({ type: String, attribute: 'container-id', reflect: false }) + public get containerId(): string | undefined { + return this._containerId; + } + public set containerId(value: string | undefined) { + if (value === this._containerId) return; + const oldValue = this._containerId; + this._containerId = value; + this.requestUpdate('containerId', oldValue); + } + + @property({ type: String, attribute: 'container-name', reflect: false }) + public get containerName(): string | undefined { + return this._propertyStructureHelper.getContainerName(); + } + public set containerName(value: string | undefined) { + this._propertyStructureHelper.setContainerName(value); + } + + @property({ type: String, attribute: 'container-type', reflect: false }) + public get containerType(): UmbPropertyContainerTypes | undefined { + return this._propertyStructureHelper.getContainerType(); + } + public set containerType(value: UmbPropertyContainerTypes | undefined) { + this._propertyStructureHelper.setContainerType(value); + } + + _propertyStructureHelper = new UmbContentTypePropertyStructureHelper(this); + + @state() + _propertyStructure: Array = []; + + @state() + _ownerMemberTypes?: UmbMemberTypeDetailModel[]; + + @state() + protected _modalRouteNewProperty?: string; + + @state() + _sortModeActive?: boolean; + + constructor() { + super(); + + this.consumeContext(UMB_WORKSPACE_CONTEXT, async (workspaceContext) => { + this._propertyStructureHelper.setStructureManager((workspaceContext as UmbMemberTypeWorkspaceContext).structure); + this.observe( + (workspaceContext as UmbMemberTypeWorkspaceContext).isSorting, + (isSorting) => { + this._sortModeActive = isSorting; + if (isSorting) { + this.#sorter.setModel(this._propertyStructure); + } else { + this.#sorter.setModel([]); + } + }, + '_observeIsSorting', + ); + const docTypesObservable = await this._propertyStructureHelper.ownerMemberTypes(); + if (!docTypesObservable) return; + this.observe( + docTypesObservable, + (members) => { + this._ownerMemberTypes = members; + }, + 'observeOwnerMemberTypes', + ); + }); + this.observe(this._propertyStructureHelper.propertyStructure, (propertyStructure) => { + this._propertyStructure = propertyStructure; + if (this._sortModeActive) { + this.#sorter.setModel(this._propertyStructure); + } else { + this.#sorter.setModel([]); + } + }); + + // Note: Route for adding a new property + new UmbModalRouteRegistrationController(this, UMB_PROPERTY_SETTINGS_MODAL) + .addAdditionalPath('new-property') + .onSetup(async () => { + const memberTypeId = this._ownerMemberTypes?.find( + (types) => types.containers?.find((containers) => containers.id === this.containerId), + )?.unique; + if (memberTypeId === undefined) return false; + const propertyData = await this._propertyStructureHelper.createPropertyScaffold(this._containerId); + if (propertyData === undefined) return false; + return { data: { memberTypeId }, value: propertyData }; + }) + .onSubmit((value) => { + if (!value.dataType) { + throw new Error('No data type selected'); + } + this.#addProperty(value as UmbPropertyTypeModel); + }) + .observeRouteBuilder((routeBuilder) => { + this._modalRouteNewProperty = routeBuilder(null); + }); + } + + async #addProperty(propertyData: UmbPropertyTypeModel) { + const propertyPlaceholder = await this._propertyStructureHelper.addProperty(this._containerId); + if (!propertyPlaceholder) return; + + this._propertyStructureHelper.partialUpdateProperty(propertyPlaceholder.id, propertyData); + } + + render() { + return html` +
+ ${repeat( + this._propertyStructure, + (property) => '' + property.container?.id + property.id + '' + property.sortOrder, + (property) => { + // Note: This piece might be moved into the property component + const inheritedFromMember = this._ownerMemberTypes?.find( + (types) => types.containers?.find((containers) => containers.id === property.container?.id), + ); + + return html` + { + this._propertyStructureHelper.partialUpdateProperty(property.id, event.detail); + }} + @property-delete=${() => { + this._propertyStructureHelper.removeProperty(property.id!); + }}> + + `; + }, + )} +
+ + ${!this._sortModeActive + ? html` + Add property + ` + : ''} + `; + } + + static styles = [ + UmbTextStyles, + css` + #add { + width: 100%; + } + + #property-list[sort-mode-active]:not(:has(umb-member-type-workspace-view-edit-property)) { + /* Some height so that the sorter can target the area if the group is empty*/ + min-height: var(--uui-size-layout-1); + } + `, + ]; +} + +export default UmbMemberTypeWorkspaceViewEditPropertiesElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-type-workspace-view-edit-properties': UmbMemberTypeWorkspaceViewEditPropertiesElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-property.element.ts new file mode 100644 index 0000000000..1645cf94e8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-property.element.ts @@ -0,0 +1,495 @@ +import { UmbDataTypeDetailRepository } from '@umbraco-cms/backoffice/data-type'; +import type { UUIInputElement } from '@umbraco-cms/backoffice/external/uui'; +import { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import { css, html, customElement, property, state, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit'; +import type { UmbConfirmModalData } from '@umbraco-cms/backoffice/modal'; +import { + UMB_CONFIRM_MODAL, + UMB_MODAL_MANAGER_CONTEXT, + UMB_PROPERTY_SETTINGS_MODAL, + UMB_WORKSPACE_MODAL, + UmbModalRouteRegistrationController, +} from '@umbraco-cms/backoffice/modal'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { generateAlias } from '@umbraco-cms/backoffice/utils'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { UmbPropertyTypeModel, UmbPropertyTypeScaffoldModel } from '@umbraco-cms/backoffice/content-type'; + +/** + * @element umb-member-type-workspace-view-edit-property + * @description - Element for displaying a property in an workspace. + * @slot editor - Slot for rendering the Property Editor + */ +@customElement('umb-member-type-workspace-view-edit-property') +export class UmbMemberTypeWorkspacePropertyElement extends UmbLitElement { + private _property?: UmbPropertyTypeModel | UmbPropertyTypeScaffoldModel | undefined; + /** + * Property, the data object for the property. + * @type {UmbPropertyTypeModel | UmbPropertyTypeScaffoldModel | undefined} + * @attr + * @default undefined + */ + @property({ type: Object }) + public get property(): UmbPropertyTypeModel | UmbPropertyTypeScaffoldModel | undefined { + return this._property; + } + public set property(value: UmbPropertyTypeModel | UmbPropertyTypeScaffoldModel | undefined) { + const oldValue = this._property; + this._property = value; + this.#modalRegistration.setUniquePathValue('propertyId', value?.id?.toString()); + this.setDataType(this._property?.dataType?.unique); + this.requestUpdate('property', oldValue); + } + + /** + * Inherited, Determines if the property is part of the main member type thats being edited. + * If true, then the property is inherited from another member type, not a part of the main member type. + * @type {boolean} + * @attr + * @default undefined + */ + @property({ type: Boolean }) + public inherited?: boolean; + + @property({ type: Boolean, reflect: true, attribute: 'sort-mode-active' }) + public sortModeActive = false; + + #dataTypeDetailRepository = new UmbDataTypeDetailRepository(this); + + #modalRegistration; + private _modalManagerContext?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE; + + @state() + protected _modalRoute?: string; + + @state() + protected _editMemberTypePath?: string; + + @property() + public get modalRoute() { + return this._modalRoute; + } + + @property({ type: String, attribute: 'owner-member-type-id' }) + public ownerMemberTypeId?: string; + + @property({ type: String, attribute: 'owner-member-type-name' }) + public ownerMemberTypeName?: string; + + @state() + private _dataTypeName?: string; + + async setDataType(dataTypeId: string | undefined) { + if (!dataTypeId) return; + this.#dataTypeDetailRepository.requestByUnique(dataTypeId).then((x) => (this._dataTypeName = x?.data?.name)); + } + + constructor() { + super(); + this.#modalRegistration = new UmbModalRouteRegistrationController(this, UMB_PROPERTY_SETTINGS_MODAL) + .addUniquePaths(['propertyId']) + .onSetup(() => { + const memberTypeId = this.ownerMemberTypeId; + if (memberTypeId === undefined) return false; + const propertyData = this.property; + if (propertyData === undefined) return false; + return { data: { memberTypeId }, value: propertyData }; + }) + .onSubmit((result) => { + if (!result.dataType) { + throw new Error('No dataType found on property'); + } + this._partialUpdate(result as UmbPropertyTypeModel); + }) + .observeRouteBuilder((routeBuilder) => { + this._modalRoute = routeBuilder(null); + }); + + new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL) + .addAdditionalPath('member-type') + .onSetup(() => { + return { data: { entityType: 'member-type', preset: {} } }; + }) + .observeRouteBuilder((routeBuilder) => { + this._editMemberTypePath = routeBuilder({}); + }); + + this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (context) => { + this._modalManagerContext = context; + }); + } + + _partialUpdate(partialObject: UmbPropertyTypeModel) { + this.dispatchEvent(new CustomEvent('partial-property-update', { detail: partialObject })); + } + + _singleValueUpdate(propertyName: string, value: string | number | boolean | null | undefined) { + const partialObject = {} as any; + partialObject[propertyName] = value; + + this.dispatchEvent(new CustomEvent('partial-property-update', { detail: partialObject })); + } + + @state() + private _aliasLocked = true; + + #onToggleAliasLock() { + this._aliasLocked = !this._aliasLocked; + } + + #requestRemove(e: Event) { + e.preventDefault(); + e.stopImmediatePropagation(); + if (!this.property || !this.property.id) return; + + const modalData: UmbConfirmModalData = { + headline: `${this.localize.term('actions_delete')} property`, + content: html` + Are you sure you want to delete the property ${this.property.name || this.property.id} + + `, + confirmLabel: this.localize.term('actions_delete'), + color: 'danger', + }; + + const modalHandler = this._modalManagerContext?.open(UMB_CONFIRM_MODAL, { data: modalData }); + + modalHandler + ?.onSubmit() + .then(() => { + this.dispatchEvent(new CustomEvent('property-delete')); + }) + .catch(() => { + // We do not need to react to cancel, so we will leave an empty method to prevent Uncaught Promise Rejection error. + return; + }); + } + + #onNameChange(event: UUIInputEvent) { + if (event instanceof UUIInputEvent) { + const target = event.composedPath()[0] as UUIInputElement; + + if (typeof target?.value === 'string') { + const oldName = this.property?.name ?? ''; + const oldAlias = this.property?.alias ?? ''; + const newName = event.target.value.toString(); + if (this._aliasLocked) { + const expectedOldAlias = generateAlias(oldName ?? ''); + // Only update the alias if the alias matches a generated alias of the old name (otherwise the alias is considered one written by the user.) + if (expectedOldAlias === oldAlias) { + this._singleValueUpdate('alias', generateAlias(newName ?? '')); + } + } + this._singleValueUpdate('name', newName); + } + } + } + renderSortableProperty() { + if (!this.property) return; + return html` +
+ + ${this.property.name} (${this.property.alias}) +
+ + this._partialUpdate({ sortOrder: parseInt(e.target.value as string) || 0 } as UmbPropertyTypeModel)} + .value=${this.property.sortOrder ?? 0}> + `; + } + + renderEditableProperty() { + if (!this.property) return; + + if (this.sortModeActive) { + return this.renderSortableProperty(); + } else { + return html` + + + ${this.renderPropertyTags()} + + + + + + + `; + } + } + + renderInheritedProperty() { + if (!this.property) return; + + if (this.sortModeActive) { + return this.renderSortableProperty(); + } else { + return html` + +
+ ${this.renderPropertyTags()} + + + ${this.localize.term('contentTypeEditor_inheritedFrom')} + + ${this.ownerMemberTypeName ?? '??'} + + + +
+ `; + } + } + + renderPropertyAlias() { + return this.property + ? html` { + if (e.target) this._singleValueUpdate('alias', (e.target as HTMLInputElement).value); + }}> + + +
''} id="alias-lock" slot="prepend"> + +
+
` + : ''; + } + + renderPropertyTags() { + return this.property + ? html`
+ ${this.property.dataType?.unique ? html`${this._dataTypeName}` : nothing} + ${this.property.variesByCulture + ? html` + ${this.localize.term('contentTypeEditor_cultureVariantLabel')} + ` + : nothing} + ${this.property.appearance?.labelOnTop == true + ? html` + ${this.localize.term('contentTypeEditor_displaySettingsLabelOnTop')} + ` + : nothing} + ${this.property.validation.mandatory === true + ? html` + * ${this.localize.term('general_mandatory')} + ` + : nothing} +
` + : nothing; + } + + render() { + // TODO: Only show alias on label if user has access to MemberType within settings: + return this.inherited ? this.renderInheritedProperty() : this.renderEditableProperty(); + } + + static styles = [ + UmbTextStyles, + css` + :host(:not([sort-mode-active])) { + display: grid; + grid-template-columns: 200px auto; + column-gap: var(--uui-size-layout-2); + border-bottom: 1px solid var(--uui-color-divider); + padding: var(--uui-size-layout-1) 0; + container-type: inline-size; + } + + :host > div { + grid-column: span 2; + } + + @container (width > 600px) { + :host(:not([orientation='vertical'])) > div { + grid-column: span 1; + } + } + + :host(:first-of-type) { + padding-top: 0; + } + :host(:last-of-type) { + border-bottom: none; + } + + :host([sort-mode-active]) { + position: relative; + display: flex; + padding: 0; + margin-bottom: var(--uui-size-3); + } + + :host([sort-mode-active]:last-of-type) { + margin-bottom: 0; + } + + :host([sort-mode-active]:not([inherited])) { + cursor: grab; + } + + :host([sort-mode-active]) .sortable { + flex: 1; + display: flex; + background-color: var(--uui-color-divider); + align-items: center; + padding: 0 var(--uui-size-3); + gap: var(--uui-size-3); + } + + :host([sort-mode-active]) uui-input { + max-width: 75px; + } + + /* Placeholder style, used when property is being dragged.*/ + :host(.--umb-sorter-placeholder) > * { + visibility: hidden; + } + + :host(.--umb-sorter-placeholder)::after { + content: ''; + inset: 0; + position: absolute; + border: 1px dashed var(--uui-color-divider-emphasis); + border-radius: var(--uui-border-radius); + } + + p { + margin-bottom: 0; + } + + #header { + position: sticky; + top: var(--uui-size-space-4); + height: min-content; + z-index: 2; + } + + #editor { + position: relative; + background-color: var(--uui-color-background); + } + #alias-input, + #label-input, + #description-input { + width: 100%; + } + + #alias-input { + border-color: transparent; + background: var(--uui-color-surface); + } + + #label-input { + font-weight: bold; /* TODO: UUI Input does not support bold text yet */ + --uui-input-border-color: transparent; + } + #label-input input { + font-weight: bold; + --uui-input-border-color: transparent; + } + + #alias-lock { + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + } + #alias-lock uui-icon { + margin-bottom: 2px; + /* margin: 0; */ + } + #description-input { + --uui-textarea-border-color: transparent; + font-weight: 0.5rem; /* TODO: Cant change font size of UUI textarea yet */ + } + + .types > div uui-icon, + .inherited uui-icon { + vertical-align: sub; + } + + .inherited { + position: absolute; + top: var(--uui-size-space-2); + right: var(--uui-size-space-2); + } + + .types { + position: absolute; + top: var(--uui-size-space-2); + left: var(--uui-size-space-2); + display: flex; + gap: var(--uui-size-space-2); + } + + #editor uui-action-bar { + position: absolute; + top: var(--uui-size-space-2); + right: var(--uui-size-space-2); + display: none; + } + #editor:hover uui-action-bar, + #editor:focus uui-action-bar { + display: block; + } + + a { + color: inherit; + } + + :host([drag-placeholder]) { + opacity: 0.5; + } + :host([drag-placeholder]) uui-input { + visibility: hidden; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-type-workspace-view-edit-property': UmbMemberTypeWorkspacePropertyElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-tab.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-tab.element.ts new file mode 100644 index 0000000000..31280fc50a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-tab.element.ts @@ -0,0 +1,321 @@ +import type { UmbMemberTypeDetailModel } from '../../../types.js'; +import type { UmbMemberTypeWorkspaceContext } from '../../member-type-workspace.context.js'; +import type { UmbMemberTypeWorkspaceViewEditPropertiesElement } from './member-type-workspace-view-edit-properties.element.js'; +import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { css, html, customElement, property, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { + UmbContentTypeContainerStructureHelper, + type UmbPropertyTypeContainerModel, +} from '@umbraco-cms/backoffice/content-type'; +import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; + +import './member-type-workspace-view-edit-properties.element.js'; +import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; + +@customElement('umb-member-type-workspace-view-edit-tab') +export class UmbMemberTypeWorkspaceViewEditTabElement extends UmbLitElement { + #model: Array = []; + #sorter = new UmbSorterController( + this, + { + getUniqueOfElement: (element) => + element.querySelector('umb-member-type-workspace-view-edit-properties')!.getAttribute('container-id'), + getUniqueOfModel: (modelEntry) => modelEntry.id, + identifier: 'member-type-container-sorter', + itemSelector: '.container-handle', + containerSelector: '.container-list', + onChange: ({ model }) => { + this._groups = model; + this.#model = model; + }, + onEnd: ({ item }) => { + /** Explanation: If the item is the first in list, we compare it to the item behind it to set a sortOrder. + * If it's not the first in list, we will compare to the item in before it, and check the following item to see if it caused overlapping sortOrder, then update + * the overlap if true, which may cause another overlap, so we loop through them till no more overlaps... + */ + const model = this.#model; + const newIndex = model.findIndex((entry) => entry.id === item.id); + + // Doesn't exist in model + if (newIndex === -1) return; + + // First in list + if (newIndex === 0 && model.length > 1) { + this._groupStructureHelper.partialUpdateContainer(item.id, { sortOrder: model[1].sortOrder - 1 }); + return; + } + + // Not first in list + if (newIndex > 0 && model.length > 1) { + const prevItemSortOrder = model[newIndex - 1].sortOrder; + + let weight = 1; + this._groupStructureHelper.partialUpdateContainer(item.id, { sortOrder: prevItemSortOrder + weight }); + + // Check for overlaps + model.some((entry, index) => { + if (index <= newIndex) return; + if (entry.sortOrder === prevItemSortOrder + weight) { + weight++; + this._groupStructureHelper.partialUpdateContainer(entry.id, { sortOrder: prevItemSortOrder + weight }); + } + // Break the loop + return true; + }); + } + }, + }, + ); + + private _ownerTabId?: string | null; + + // TODO: get rid of this: + @property({ type: String }) + public get ownerTabId(): string | null | undefined { + return this._ownerTabId; + } + public set ownerTabId(value: string | null | undefined) { + if (value === this._ownerTabId) return; + const oldValue = this._ownerTabId; + this._ownerTabId = value; + this._groupStructureHelper.setOwnerId(value); + this.requestUpdate('ownerTabId', oldValue); + } + + private _tabName?: string | undefined; + + @property({ type: String }) + public get tabName(): string | undefined { + return this._groupStructureHelper.getName(); + } + public set tabName(value: string | undefined) { + if (value === this._tabName) return; + const oldValue = this._tabName; + this._tabName = value; + this._groupStructureHelper.setName(value); + this.requestUpdate('tabName', oldValue); + } + + @state() + private _noTabName?: boolean; + + @property({ type: Boolean }) + public get noTabName(): boolean { + return this._groupStructureHelper.getIsRoot(); + } + public set noTabName(value: boolean) { + this._noTabName = value; + this._groupStructureHelper.setIsRoot(value); + } + + _groupStructureHelper = new UmbContentTypeContainerStructureHelper(this); + + @state() + _groups: Array = []; + + @state() + _hasProperties = false; + + @state() + _sortModeActive?: boolean; + + constructor() { + super(); + + this.consumeContext(UMB_WORKSPACE_CONTEXT, (context) => { + this._groupStructureHelper.setStructureManager((context as UmbMemberTypeWorkspaceContext).structure); + this.observe( + (context as UmbMemberTypeWorkspaceContext).isSorting, + (isSorting) => { + this._sortModeActive = isSorting; + + if (isSorting) { + this.#sorter.setModel(this._groups); + } else { + this.#sorter.setModel([]); + } + }, + '_observeIsSorting', + ); + }); + this.observe(this._groupStructureHelper.containers, (groups) => { + this._groups = groups; + if (this._sortModeActive) { + this.#sorter.setModel(this._groups); + } else { + this.#sorter.setModel([]); + } + this.requestUpdate('_groups'); + }); + this.observe(this._groupStructureHelper.hasProperties, (hasProperties) => { + this._hasProperties = hasProperties; + this.requestUpdate('_hasProperties'); + }); + } + + #onAddGroup = () => { + // Idea, maybe we can gather the sortOrder from the last group rendered and add 1 to it? + this._groupStructureHelper.addContainer(this._ownerTabId); + }; + + render() { + return html` + ${ + this._sortModeActive + ? html`` + : '' + } + + ${ + !this._noTabName + ? html` + + + + ` + : '' + } +
+ ${repeat( + this._groups, + (group) => group.id + '' + group.name + group.sortOrder, + (group) => + html` + ${this.#renderHeader(group)} + + `, + )} +
+ ${this.#renderAddGroupButton()} + + `; + } + + #renderHeader(group: UmbPropertyTypeContainerModel) { + const inherited = !this._groupStructureHelper.isOwnerChildContainer(group.id!); + + if (this._sortModeActive) { + return html`
+
+ + ${this.#renderInputGroupName(group)} +
+ + this._groupStructureHelper.partialUpdateContainer(group.id!, { + sortOrder: parseInt(e.target.value as string) || 0, + })} + .value=${group.sortOrder || 0} + ?disabled=${inherited}> +
`; + } else { + return html`
+ ${inherited ? html`` : this.#renderInputGroupName(group)} +
`; + } + } + + #renderInputGroupName(group: UmbPropertyTypeContainerModel) { + return html` { + const newName = (e.target as HTMLInputElement).value; + this._groupStructureHelper.updateContainerName(group.id!, group.parent?.id ?? null, newName); + }}>`; + } + + #renderAddGroupButton() { + if (this._sortModeActive) return; + return html` + ${this.localize.term('contentTypeEditor_addGroup')} + `; + } + + static styles = [ + UmbTextStyles, + css` + [drag-placeholder] { + opacity: 0.5; + } + + [drag-placeholder] > * { + visibility: hidden; + } + + #add { + width: 100%; + } + + #add:first-child { + margin-top: var(--uui-size-layout-1); + } + + uui-box { + margin-bottom: var(--uui-size-layout-1); + } + + [data-umb-group-id] { + display: block; + position: relative; + } + + div[slot='header'] { + flex: 1; + display: flex; + align-items: center; + justify-content: space-between; + } + + div[slot='header'] > div { + display: flex; + align-items: center; + gap: var(--uui-size-3); + } + + uui-input[type='number'] { + max-width: 75px; + } + + [sort-mode-active] div[slot='header'] { + cursor: grab; + } + + .container-list { + display: grid; + gap: 10px; + } + + #convert-to-tab { + margin-bottom: var(--uui-size-layout-1); + display: flex; + } + `, + ]; +} + +export default UmbMemberTypeWorkspaceViewEditTabElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-type-workspace-view-edit-tab': UmbMemberTypeWorkspaceViewEditTabElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit.element.ts new file mode 100644 index 0000000000..8c14ecf68a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit.element.ts @@ -0,0 +1,574 @@ +// import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; +import type { UmbMemberTypeWorkspaceContext } from '../../member-type-workspace.context.js'; +import type { UmbMemberTypeDetailModel } from '../../../types.js'; +import type { UmbMemberTypeWorkspaceViewEditTabElement } from './member-type-workspace-view-edit-tab.element.js'; +import { css, html, customElement, state, repeat, nothing, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import type { UUIInputElement, UUIInputEvent, UUITabElement } from '@umbraco-cms/backoffice/external/uui'; +import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type'; +import { encodeFolderName } from '@umbraco-cms/backoffice/router'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { + CompositionTypeModel, + type PropertyTypeContainerModelBaseModel, +} from '@umbraco-cms/backoffice/external/backend-api'; +import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import type { UmbRoute, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router'; +import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; +import type { UmbConfirmModalData } from '@umbraco-cms/backoffice/modal'; +import { UMB_CONFIRM_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; + +@customElement('umb-member-type-workspace-view-edit') +export class UmbMemberTypeWorkspaceViewEditElement extends UmbLitElement implements UmbWorkspaceViewElement { + #model: Array = []; + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => element.getAttribute('data-umb-tabs-id'), + getUniqueOfModel: (modelEntry) => modelEntry.id, + identifier: 'member-type-tabs-sorter', + itemSelector: 'uui-tab', + containerSelector: 'uui-tab-group', + disabledItemSelector: '#root-tab', + resolveVerticalDirection: () => false, + onChange: ({ model }) => { + this.#model = model; + this._tabs = model; + }, + onEnd: ({ item }) => { + /** Explanation: If the item is the first in list, we compare it to the item behind it to set a sortOrder. + * If it's not the first in list, we will compare to the item in before it, and check the following item to see if it caused overlapping sortOrder, then update + * the overlap if true, which may cause another overlap, so we loop through them till no more overlaps... + */ + const model = this.#model; + const newIndex = model.findIndex((entry) => entry.id === item.id); + + // Doesn't exist in model + if (newIndex === -1) return; + + // First in list + if (newIndex === 0 && model.length > 1) { + this._tabsStructureHelper.partialUpdateContainer(item.id, { sortOrder: model[1].sortOrder - 1 }); + return; + } + + // Not first in list + if (newIndex > 0 && model.length > 1) { + const prevItemSortOrder = model[newIndex - 1].sortOrder; + + let weight = 1; + this._tabsStructureHelper.partialUpdateContainer(item.id, { sortOrder: prevItemSortOrder + weight }); + + // Check for overlaps + model.some((entry, index) => { + if (index <= newIndex) return; + if (entry.sortOrder === prevItemSortOrder + weight) { + weight++; + this._tabsStructureHelper.partialUpdateContainer(entry.id, { sortOrder: prevItemSortOrder + weight }); + } + // Break the loop + return true; + }); + } + }, + }); + + //private _hasRootProperties = false; + + @state() + private _hasRootGroups = false; + + @state() + private _routes: UmbRoute[] = []; + + @state() + _tabs?: Array; + + @state() + private _routerPath?: string; + + @state() + private _activePath = ''; + + @state() + private _sortModeActive?: boolean; + + @state() + private _buttonDisabled: boolean = false; + + private _workspaceContext?: UmbMemberTypeWorkspaceContext; + + private _tabsStructureHelper = new UmbContentTypeContainerStructureHelper(this); + + private _modalManagerContext?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE; + + // @state() + // private _compositionConfiguration?: UmbCompositionPickerModalData; + + constructor() { + super(); + + //TODO: We need to differentiate between local and composition tabs (and hybrids) + + this._tabsStructureHelper.setIsRoot(true); + this._tabsStructureHelper.setContainerChildType('Tab'); + this.observe(this._tabsStructureHelper.containers, (tabs) => { + this._tabs = tabs; + if (this._sortModeActive) { + this.#sorter.setModel(tabs); + } else { + this.#sorter.setModel([]); + } + + this._createRoutes(); + }); + + // _hasRootProperties can be gotten via _tabsStructureHelper.hasProperties. But we do not support root properties currently. + + this.consumeContext(UMB_WORKSPACE_CONTEXT, (workspaceContext) => { + this._workspaceContext = workspaceContext as UmbMemberTypeWorkspaceContext; + this._tabsStructureHelper.setStructureManager((workspaceContext as UmbMemberTypeWorkspaceContext).structure); + this.observe( + this._workspaceContext.isSorting, + (isSorting) => { + this._sortModeActive = isSorting; + if (isSorting) { + this.#sorter.setModel(this._tabs!); + } else { + this.#sorter.setModel([]); + } + }, + '_observeIsSorting', + ); + + const unique = this._workspaceContext.getEntityId(); + + // //TODO Figure out the correct data that needs to be sent to the compositions modal. Do we really have to send isElement, currentPropertyAliases - isn't unique enough? + // this.observe(this._workspaceContext.structure.contentTypes, (contentTypes) => { + // this._compositionConfiguration = { + // unique: unique ?? '', + // selection: contentTypes.map((contentType) => contentType.unique).filter((id) => id !== unique), + // isElement: contentTypes.find((contentType) => contentType.unique === unique)?.isElement ?? false, + // currentPropertyAliases: [], + // }; + // }); + + this._observeRootGroups(); + }); + + this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (context) => { + this._modalManagerContext = context; + }); + } + + private _observeRootGroups() { + if (!this._workspaceContext) return; + + this.observe( + this._workspaceContext.structure.hasRootContainers('Group'), + (hasRootGroups) => { + this._hasRootGroups = hasRootGroups; + this._createRoutes(); + }, + '_observeGroups', + ); + } + + #changeMode() { + this._workspaceContext?.setIsSorting(!this._sortModeActive); + } + + private _createRoutes() { + if (!this._workspaceContext || !this._tabs) return; + const routes: UmbRoute[] = []; + + if (this._tabs.length > 0) { + this._tabs?.forEach((tab) => { + const tabName = tab.name ?? ''; + routes.push({ + path: `tab/${encodeFolderName(tabName).toString()}`, + component: () => import('./member-type-workspace-view-edit-tab.element.js'), + setup: (component) => { + (component as UmbMemberTypeWorkspaceViewEditTabElement).tabName = tabName; + (component as UmbMemberTypeWorkspaceViewEditTabElement).ownerTabId = + this._tabsStructureHelper.isOwnerContainer(tab.id!) ? tab.id : undefined; + }, + }); + }); + } + + routes.push({ + path: 'root', + component: () => import('./member-type-workspace-view-edit-tab.element.js'), + setup: (component) => { + (component as UmbMemberTypeWorkspaceViewEditTabElement).noTabName = true; + (component as UmbMemberTypeWorkspaceViewEditTabElement).ownerTabId = null; + }, + }); + + if (this._hasRootGroups) { + routes.push({ + path: '', + redirectTo: 'root', + }); + } else if (routes.length !== 0) { + routes.push({ + path: '', + redirectTo: routes[0]?.path, + }); + } + + this._routes = routes; + } + + #requestRemoveTab(tab: PropertyTypeContainerModelBaseModel | undefined) { + const modalData: UmbConfirmModalData = { + headline: 'Delete tab', + content: html` + Are you sure you want to delete the tab ${tab?.name ?? tab?.id} + +
+ + This will delete all items that doesn't belong to a composition. + +
`, + confirmLabel: this.localize.term('actions_delete'), + color: 'danger', + }; + + // TODO: If this tab is composed of other tabs, then notify that it will only delete the local tab. + + const modalHandler = this._modalManagerContext?.open(UMB_CONFIRM_MODAL, { data: modalData }); + + modalHandler?.onSubmit().then(() => { + this.#remove(tab?.id); + }); + } + #remove(tabId?: string) { + if (!tabId) return; + this._workspaceContext?.structure.removeContainer(null, tabId); + this._tabsStructureHelper?.isOwnerContainer(tabId) + ? window.history.replaceState(null, '', this._routerPath + (this._routes[0]?.path ?? '/root')) + : ''; + } + async #addTab() { + if ( + (this.shadowRoot?.querySelector('uui-tab[active] uui-input') as UUIInputElement) && + (this.shadowRoot?.querySelector('uui-tab[active] uui-input') as UUIInputElement).value === '' + ) { + this.#focusInput(); + return; + } + + const tab = await this._workspaceContext?.structure.createContainer(null, null, 'Tab'); + if (tab) { + const path = this._routerPath + '/tab/' + encodeFolderName(tab.name || ''); + window.history.replaceState(null, '', path); + this.#focusInput(); + } + } + + async #focusInput() { + setTimeout(() => { + (this.shadowRoot?.querySelector('uui-tab[active] uui-input') as UUIInputElement | undefined)?.focus(); + }, 100); + } + + async #tabNameChanged(event: InputEvent, tab: PropertyTypeContainerModelBaseModel) { + if (this._buttonDisabled) this._buttonDisabled = !this._buttonDisabled; + let newName = (event.target as HTMLInputElement).value; + + if (newName === '') { + newName = 'Unnamed'; + (event.target as HTMLInputElement).value = 'Unnamed'; + } + + const changedName = this._workspaceContext?.structure.makeContainerNameUniqueForOwnerContentType( + newName, + 'Tab', + tab.id, + ); + + // Check if it collides with another tab name of this same member-type, if so adjust name: + if (changedName) { + newName = changedName; + (event.target as HTMLInputElement).value = newName; + } + + this._tabsStructureHelper.partialUpdateContainer(tab.id!, { + name: newName, + }); + + // Update the current URL, so we are still on this specific tab: + window.history.replaceState(null, '', this._routerPath + '/tab/' + encodeFolderName(newName)); + } + + async #openCompositionModal() { + throw new Error('Not implemented'); + // const modalContext = this._modalManagerContext?.open(UMB_COMPOSITION_PICKER_MODAL, { + // data: this._compositionConfiguration, + // }); + // await modalContext?.onSubmit(); + + // if (!modalContext?.value) return; + + // const compositionIds = modalContext.getValue().selection; + + // this._workspaceContext?.setCompositions( + // compositionIds.map((unique) => ({ contentType: { unique }, compositionType: CompositionTypeModel.COMPOSITION })), + // ); + } + + render() { + return html` + + + { + this._routerPath = event.target.absoluteRouterPath; + }} + @change=${(event: UmbRouterSlotChangeEvent) => { + this._activePath = event.target.absoluteActiveViewPath || ''; + }}> + + + `; + } + + renderAddButton() { + if (this._sortModeActive) return; + return html` + + Add tab + `; + } + + renderActions() { + const sortButtonText = this._sortModeActive + ? this.localize.term('general_reorderDone') + : this.localize.term('general_reorder'); + + return html`
+ + + ${this.localize.term('contentTypeEditor_compositions')} + + + + ${sortButtonText} + +
`; + } + + renderTabsNavigation() { + if (!this._tabs) return; + + return html`
+ + ${this.renderRootTab()} + ${repeat( + this._tabs, + (tab) => tab.id! + tab.name, + (tab) => this.renderTab(tab), + )} + +
`; + } + + renderRootTab() { + const rootTabPath = this._routerPath + '/root'; + const rootTabActive = rootTabPath === this._activePath; + return html` + ${this.localize.term('general_content')} + `; + } + + renderTab(tab: PropertyTypeContainerModelBaseModel) { + const path = this._routerPath + '/tab/' + encodeFolderName(tab.name || ''); + const tabActive = path === this._activePath; + const tabInherited = !this._tabsStructureHelper.isOwnerContainer(tab.id!); + + return html` + ${this.renderTabInner(tab, tabActive, tabInherited)} + `; + } + + renderTabInner(tab: PropertyTypeContainerModelBaseModel, tabActive: boolean, tabInherited: boolean) { + if (this._sortModeActive) { + return html`
+ ${tabInherited + ? html`${tab.name!}` + : html` ${tab.name!} + this.#changeOrderNumber(tab, e)}>`} +
`; + } + + if (tabActive && !tabInherited) { + return html`
+ this.#tabNameChanged(e, tab)} + @blur=${(e: InputEvent) => this.#tabNameChanged(e, tab)} + @input=${() => (this._buttonDisabled = true)} + @focus=${(e: UUIInputEvent) => (e.target.value ? nothing : (this._buttonDisabled = true))}> + ${this.renderDeleteFor(tab)} + +
`; + } + + if (tabInherited) { + return html`
${tab.name!}
`; + } else { + return html`
${tab.name!} ${this.renderDeleteFor(tab)}
`; + } + } + + #changeOrderNumber(tab: PropertyTypeContainerModelBaseModel, e: UUIInputEvent) { + if (!e.target.value || !tab.id) return; + const sortOrder = Number(e.target.value); + this._tabsStructureHelper.partialUpdateContainer(tab.id, { sortOrder }); + } + + renderDeleteFor(tab: PropertyTypeContainerModelBaseModel) { + return html` this.#requestRemoveTab(tab)} + compact> + + `; + } + + static styles = [ + UmbTextStyles, + css` + #buttons-wrapper { + flex: 1; + display: flex; + align-items: center; + justify-content: space-between; + align-items: stretch; + } + + :host { + position: relative; + display: flex; + flex-direction: column; + height: 100%; + --uui-tab-background: var(--uui-color-surface); + } + + [drag-placeholder] { + opacity: 0.5; + } + + [drag-placeholder] uui-input { + visibility: hidden; + } + + /* TODO: This should be replaced with a general workspace bar — naming is hard */ + + #header { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: nowrap; + } + + .flex { + display: flex; + } + + uui-tab-group { + flex-wrap: nowrap; + } + + .content-tab-is-empty { + align-self: center; + border-radius: 3px; + --uui-tab-text: var(--uui-color-text-alt); + border: dashed 1px var(--uui-color-border-emphasis); + } + + uui-tab { + position: relative; + border-left: 1px hidden transparent; + border-right: 1px solid var(--uui-color-border); + } + + .no-edit uui-input { + pointer-events: auto; + } + + .no-edit { + pointer-events: none; + display: inline-flex; + padding-left: var(--uui-size-space-3); + border: 1px solid transparent; + align-items: center; + gap: var(--uui-size-space-3); + } + + .trash { + opacity: 1; + transition: opacity 120ms; + } + + uui-tab:not(:hover, :focus) .trash { + opacity: 0; + transition: opacity 120ms; + } + + uui-input:not(:focus, :hover) { + border: 1px solid transparent; + } + + .inherited { + vertical-align: sub; + } + + [drag-placeholder] { + opacity: 0.2; + } + `, + ]; +} + +export default UmbMemberTypeWorkspaceViewEditElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-type-workspace-view-edit': UmbMemberTypeWorkspaceViewEditElement; + } +} From 1dbf6f2f4b780cf31b82f647f6a181a6ec564acd Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Mon, 19 Feb 2024 16:18:50 +0100 Subject: [PATCH 002/246] workspace editor --- .../member-type-workspace-editor.element.ts | 159 ++++++++++++++++-- 1 file changed, 145 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts index 79daf8f9b0..f4019691ae 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts @@ -1,52 +1,162 @@ import { UMB_MEMBER_TYPE_WORKSPACE_CONTEXT } from './member-type-workspace.context.js'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UUIInputElement } from '@umbraco-cms/backoffice/external/uui'; import { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal'; +import { UMB_ICON_PICKER_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import { generateAlias } from '@umbraco-cms/backoffice/utils'; @customElement('umb-member-type-workspace-editor') export class UmbMemberTypeWorkspaceEditorElement extends UmbLitElement { @state() - private _name = ''; + private _name?: string; + + @state() + private _alias?: string; + + @state() + private _aliasLocked = true; + + @state() + private _icon?: string; + + @state() + private _iconColorAlias?: string; + // TODO: Color should be using an alias, and look up in some dictionary/key/value) of project-colors. #workspaceContext?: typeof UMB_MEMBER_TYPE_WORKSPACE_CONTEXT.TYPE; + private _modalContext?: UmbModalManagerContext; + constructor() { super(); - this.consumeContext(UMB_MEMBER_TYPE_WORKSPACE_CONTEXT, (workspaceContext) => { - this.#workspaceContext = workspaceContext; - this.#observeName(); + this.consumeContext(UMB_MEMBER_TYPE_WORKSPACE_CONTEXT, (instance) => { + this.#workspaceContext = instance; + this.#observeMemberType(); + }); + + this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => { + this._modalContext = instance; }); } - #observeName() { + #observeMemberType() { if (!this.#workspaceContext) return; - this.observe(this.#workspaceContext.name, (name) => (this._name = name ?? '')); + this.observe(this.#workspaceContext.name, (name) => (this._name = name), '_observeName'); + this.observe(this.#workspaceContext.alias, (alias) => (this._alias = alias), '_observeAlias'); + this.observe(this.#workspaceContext.icon, (icon) => (this._icon = icon), '_observeIcon'); + + this.observe( + this.#workspaceContext.isNew, + (isNew) => { + if (isNew) { + // TODO: Would be good with a more general way to bring focus to the name input. + (this.shadowRoot?.querySelector('#name') as HTMLElement)?.focus(); + } + this.removeControllerByAlias('_observeIsNew'); + }, + '_observeIsNew', + ); } - // TODO. find a way where we don't have to do this for all Workspaces. - #handleInput(event: UUIInputEvent) { + // TODO. find a way where we don't have to do this for all workspaces. + #onNameChange(event: UUIInputEvent) { if (event instanceof UUIInputEvent) { const target = event.composedPath()[0] as UUIInputElement; if (typeof target?.value === 'string') { + const oldName = this._name; + const oldAlias = this._alias; + const newName = event.target.value.toString(); + if (this._aliasLocked) { + const expectedOldAlias = generateAlias(oldName ?? ''); + // Only update the alias if the alias matches a generated alias of the old name (otherwise the alias is considered one written by the user.) + if (expectedOldAlias === oldAlias) { + this.#workspaceContext?.set('alias', generateAlias(newName)); + } + } this.#workspaceContext?.setName(target.value); } } } + // TODO. find a way where we don't have to do this for all workspaces. + #onAliasChange(event: UUIInputEvent) { + if (event instanceof UUIInputEvent) { + const target = event.composedPath()[0] as UUIInputElement; + + if (typeof target?.value === 'string') { + this.#workspaceContext?.set('alias', target.value); + } + } + event.stopPropagation(); + } + + #onToggleAliasLock() { + this._aliasLocked = !this._aliasLocked; + } + + private async _handleIconClick() { + const modalContext = this._modalContext?.open(UMB_ICON_PICKER_MODAL, { + value: { + icon: this._icon, + color: this._iconColorAlias, + }, + }); + + modalContext?.onSubmit().then((saved) => { + if (saved.icon) this.#workspaceContext?.set('icon', saved.icon); + // TODO: save color ALIAS as well + }); + } + render() { return html` - + + +
+ + + Keyboard Shortcuts + + ALT + + + shift + + + k + + +
`; } static styles = [ - UmbTextStyles, css` :host { display: block; @@ -55,10 +165,31 @@ export class UmbMemberTypeWorkspaceEditorElement extends UmbLitElement { } #header { - /* TODO: can this be applied from layout slot CSS? */ - margin: 0 var(--uui-size-layout-1); + display: flex; flex: 1 1 auto; } + + #name { + width: 100%; + flex: 1 1 auto; + align-items: center; + } + + #alias-lock { + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + } + #alias-lock uui-icon { + margin-bottom: 2px; + } + + #icon { + font-size: calc(var(--uui-size-layout-3) / 2); + margin-right: var(--uui-size-space-2); + margin-left: calc(var(--uui-size-space-4) * -1); + } `, ]; } From 300c7d40a320b94d1239e0011624c9ee654df85e Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Mon, 19 Feb 2024 16:19:02 +0100 Subject: [PATCH 003/246] properties view fix --- .../member-type-workspace-view-edit-properties.element.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-properties.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-properties.element.ts index bfc40781cc..aefd7d051b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-properties.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-properties.element.ts @@ -137,7 +137,7 @@ export class UmbMemberTypeWorkspaceViewEditPropertiesElement extends UmbLitEleme }, '_observeIsSorting', ); - const docTypesObservable = await this._propertyStructureHelper.ownerMemberTypes(); + const docTypesObservable = await this._propertyStructureHelper.ownerDocumentTypes(); if (!docTypesObservable) return; this.observe( docTypesObservable, @@ -166,7 +166,7 @@ export class UmbMemberTypeWorkspaceViewEditPropertiesElement extends UmbLitEleme if (memberTypeId === undefined) return false; const propertyData = await this._propertyStructureHelper.createPropertyScaffold(this._containerId); if (propertyData === undefined) return false; - return { data: { memberTypeId }, value: propertyData }; + return { data: { documentTypeId: memberTypeId }, value: propertyData }; }) .onSubmit((value) => { if (!value.dataType) { From 3154b80cdd9fe21f35a70b15ece9f9aa62248c45 Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Mon, 19 Feb 2024 16:19:14 +0100 Subject: [PATCH 004/246] workspace context --- .../member-type-workspace.context.ts | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts index 0a3a8bed3b..2f35a13f09 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts @@ -58,43 +58,50 @@ export class UmbMemberTypeWorkspaceContext this.#isSorting.setValue(isSorting); } - setProperty(propertyName: PropertyName, value: EntityType[PropertyName]) { + set(propertyName: PropertyName, value: EntityType[PropertyName]) { this.structure.updateOwnerContentType({ [propertyName]: value }); } async load(unique: string) { - const { data } = await this.repository.requestByUnique(unique); + const { data } = await this.structure.loadType(unique); + if (!data) return undefined; - if (data) { - this.setIsNew(false); - this.#data.update(data); - } + this.setIsNew(false); + this.setIsSorting(false); + //this.#draft.next(data); + return { data } || undefined; } async create(parentUnique: string | null) { - const { data } = await this.repository.createScaffold(parentUnique); + const { data } = await this.structure.createScaffold(parentUnique); + if (!data) return undefined; - if (data) { - this.setIsNew(true); - this.#data.setValue(data); - } - - return { data }; + this.setIsNew(true); + this.setIsSorting(false); + //this.#draft.next(data); + return { data } || undefined; } async save() { const data = this.getData(); - if (!data) throw new Error('No data to save'); + if (data === undefined) throw new Error('Cannot save, no data'); if (this.getIsNew()) { - await this.repository.create(data); + if ((await this.structure.create()) === true) { + this.setIsNew(false); + } } else { - await this.repository.save(data); + await this.structure.save(); } this.saveComplete(data); } + public destroy(): void { + this.structure.destroy(); + super.destroy(); + } + getData() { return this.#data.getValue(); } From 1e9a8fa172004c1cfd7ee4f79df8bd2d92ce1b91 Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Tue, 20 Feb 2024 08:52:39 +0100 Subject: [PATCH 005/246] fix saving --- .../member-type/workspace/member-type-workspace.context.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts index 2f35a13f09..1927e37edd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts @@ -103,11 +103,11 @@ export class UmbMemberTypeWorkspaceContext } getData() { - return this.#data.getValue(); + return this.structure.getOwnerContentType(); } getEntityId() { - return this.getData()?.unique || ''; + return this.getData()?.unique; } getEntityType() { From a9394ca0fa9bb9aa307ce02f488d844c3f082d39 Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Tue, 20 Feb 2024 09:48:46 +0100 Subject: [PATCH 006/246] fix create new --- .../workspace/member-type-workspace.context.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts index 1927e37edd..fbab9126a2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts @@ -114,12 +114,14 @@ export class UmbMemberTypeWorkspaceContext return 'member-type'; } - getName() { - return this.#data.getValue()?.name; + setName(name: string) { + this.structure.updateOwnerContentType({ name }); } - - setName(name: string | undefined) { - this.#data.update({ name }); + setAlias(alias: string) { + this.structure.updateOwnerContentType({ alias }); + } + setDescription(description: string) { + this.structure.updateOwnerContentType({ description }); } } From 39f617dfad0d01efd30e8443bdac5cb15a22b9b8 Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Tue, 20 Feb 2024 09:49:38 +0100 Subject: [PATCH 007/246] scaffold icon --- .../repository/detail/member-type-detail.server.data-source.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/detail/member-type-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/detail/member-type-detail.server.data-source.ts index a701eb9463..b2391618d2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/detail/member-type-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/detail/member-type-detail.server.data-source.ts @@ -43,7 +43,7 @@ export class UmbMemberTypeServerDataSource implements UmbDetailDataSource Date: Tue, 20 Feb 2024 11:17:52 +0100 Subject: [PATCH 008/246] add member workspace views --- .../members/member/workspace/manifests.ts | 65 ++++++++++++++++++- .../member-workspace-editor.element.ts | 8 +-- .../member-workspace-view-content.element.ts | 22 +++++++ .../member-workspace-view-info.element.ts | 22 +++++++ .../member-workspace-view-member.element.ts | 22 +++++++ 5 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/info/member-workspace-view-info.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/manifests.ts index 644e1bce88..21c72b85c1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/manifests.ts @@ -1,6 +1,10 @@ import { UMB_MEMBER_ENTITY_TYPE } from '../entity.js'; import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; -import type { ManifestWorkspace, ManifestWorkspaceAction } from '@umbraco-cms/backoffice/extension-registry'; +import type { + ManifestWorkspace, + ManifestWorkspaceAction, + ManifestWorkspaceView, +} from '@umbraco-cms/backoffice/extension-registry'; export const UMB_MEMBER_WORKSPACE_ALIAS = 'Umb.Workspace.Member'; @@ -34,4 +38,61 @@ const workspaceActions: Array = [ }, ]; -export const manifests = [workspace, ...workspaceActions]; +export const workspaceViews: Array = [ + { + type: 'workspaceView', + alias: 'Umb.WorkspaceView.Member.Content', + name: 'Member Workspace Content View', + js: () => import('./views/content/member-workspace-view-content.element.js'), + weight: 300, + meta: { + label: 'Content', + pathname: 'content', + icon: 'icon-document', + }, + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: UMB_MEMBER_WORKSPACE_ALIAS, + }, + ], + }, + { + type: 'workspaceView', + alias: 'Umb.WorkspaceView.Member.Member', + name: 'Member Workspace Member View', + js: () => import('./views/member/member-workspace-view-member.element.js'), + weight: 200, + meta: { + label: 'Member', + pathname: 'member', + icon: 'icon-user', + }, + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: UMB_MEMBER_WORKSPACE_ALIAS, + }, + ], + }, + { + type: 'workspaceView', + alias: 'Umb.WorkspaceView.Member.Info', + name: 'Member Workspace Info View', + js: () => import('./views/info/member-workspace-view-info.element.js'), + weight: 100, + meta: { + label: 'Info', + pathname: 'info', + icon: 'icon-info', + }, + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: UMB_MEMBER_WORKSPACE_ALIAS, + }, + ], + }, +]; + +export const manifests = [workspace, ...workspaceActions, ...workspaceViews]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts index 681b611fe3..bd598f234f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts @@ -39,13 +39,7 @@ export class UmbMemberWorkspaceEditorElement extends UmbLitElement { }; render() { - return html` - -
Unique: ${this._data?.unique}
- - -
- `; + return html` `; } static styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content.element.ts new file mode 100644 index 0000000000..ca9295d631 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content.element.ts @@ -0,0 +1,22 @@ +// import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; +import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; + +@customElement('umb-member-workspace-view-content') +export class UmbMemberWorkspaceViewContentElement extends UmbLitElement implements UmbWorkspaceViewElement { + render() { + return html`content`; + } + + static styles = [UmbTextStyles, css``]; +} + +export default UmbMemberWorkspaceViewContentElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-workspace-view-content': UmbMemberWorkspaceViewContentElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/info/member-workspace-view-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/info/member-workspace-view-info.element.ts new file mode 100644 index 0000000000..04eb3da4b6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/info/member-workspace-view-info.element.ts @@ -0,0 +1,22 @@ +// import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; +import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; + +@customElement('umb-member-workspace-view-info') +export class UmbMemberWorkspaceViewInfoElement extends UmbLitElement implements UmbWorkspaceViewElement { + render() { + return html`info`; + } + + static styles = [UmbTextStyles, css``]; +} + +export default UmbMemberWorkspaceViewInfoElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-workspace-view-info': UmbMemberWorkspaceViewInfoElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts new file mode 100644 index 0000000000..8002199fb3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts @@ -0,0 +1,22 @@ +// import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; +import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; + +@customElement('umb-member-workspace-view-member') +export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implements UmbWorkspaceViewElement { + render() { + return html`member`; + } + + static styles = [UmbTextStyles, css``]; +} + +export default UmbMemberWorkspaceViewMemberElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-workspace-view-member': UmbMemberWorkspaceViewMemberElement; + } +} From 8e2fcf81b3b03f4bc938c7195416131de8064021 Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Tue, 20 Feb 2024 13:02:13 +0100 Subject: [PATCH 009/246] fix destroy --- .../members/member/workspace/member-workspace.context.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts index e9c78d4540..62fb627abb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts @@ -77,7 +77,8 @@ export class UmbMemberWorkspaceContext } public destroy(): void { - console.log('destroy'); + this.#data.destroy(); + super.destroy(); } } From 38dd4e965974e082f0f2fc389b22678d30da1983 Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Tue, 20 Feb 2024 13:02:22 +0100 Subject: [PATCH 010/246] init member view --- .../member-workspace-view-member.element.ts | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts index 8002199fb3..d2570f85b4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts @@ -7,10 +7,55 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; @customElement('umb-member-workspace-view-member') export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implements UmbWorkspaceViewElement { render() { - return html`member`; + return html` + + + + + + + + + + + + + + +
MEMBER GROUP PICKER
+
+ +
0
+ + + + + + + + + + + + + +
never
+ +
never
+ +
never
+
+
`; } - static styles = [UmbTextStyles, css``]; + static styles = [ + UmbTextStyles, + css` + uui-input { + width: 100%; + } + `, + ]; } export default UmbMemberWorkspaceViewMemberElement; From 99b8eb9305152f04c017bb890b7fd2c6556b7a3b Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Tue, 20 Feb 2024 13:46:07 +0100 Subject: [PATCH 011/246] work --- .../src/packages/members/member-type/index.ts | 2 + ...create-member-collection-action.element.ts | 130 ++++++++++++++++++ .../member/collection/action/manifests.ts | 3 +- .../workspace/member-workspace.context.ts | 7 + 4 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/index.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/index.ts index 48ebd24242..c5c0a92644 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/index.ts @@ -3,3 +3,5 @@ import './components/index.js'; export * from './components/index.js'; export * from './repository/index.js'; export * from './entity.js'; + +export type { UmbMemberTypeDetailModel } from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts new file mode 100644 index 0000000000..0e0d5db5de --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts @@ -0,0 +1,130 @@ +import { html, customElement, property, state, map } from '@umbraco-cms/backoffice/external/lit'; +import { UmbDocumentTypeStructureRepository } from '@umbraco-cms/backoffice/document-type'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/document'; +import type { ManifestCollectionAction } from '@umbraco-cms/backoffice/extension-registry'; +import type { UmbAllowedDocumentTypeModel } from '@umbraco-cms/backoffice/document-type'; + +@customElement('umb-create-member-collection-action') +export class UmbCreateDocumentCollectionActionElement extends UmbLitElement { + @state() + private _allowedDocumentTypes: Array = []; + + @state() + private _documentUnique?: string; + + @state() + private _documentTypeUnique?: string; + + @state() + private _popoverOpen = false; + + @property({ attribute: false }) + manifest?: ManifestCollectionAction; + + #documentTypeStructureRepository = new UmbDocumentTypeStructureRepository(this); + + constructor() { + super(); + + this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (workspaceContext) => { + this.observe(workspaceContext.unique, (unique) => { + this._documentUnique = unique; + }); + this.observe(workspaceContext.contentTypeUnique, (documentTypeUnique) => { + this._documentTypeUnique = documentTypeUnique; + }); + }); + } + + async firstUpdated() { + if (this._documentTypeUnique) { + this.#retrieveAllowedDocumentTypesOf(this._documentTypeUnique); + } + } + + async #retrieveAllowedDocumentTypesOf(unique: string | null) { + const { data } = await this.#documentTypeStructureRepository.requestAllowedChildrenOf(unique); + + if (data && data.items) { + this._allowedDocumentTypes = data.items; + } + } + + // TODO: This ignorer is just neede for JSON SCHEMA TO WORK, As its not updated with latest TS jet. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + #onPopoverToggle(event: ToggleEvent) { + this._popoverOpen = event.newState === 'open'; + } + + #onClick(item: UmbAllowedDocumentTypeModel, e: Event) { + e.preventDefault(); + // TODO: Do anything else here? [LK] + } + + #getCreateUrl(item: UmbAllowedDocumentTypeModel) { + // TODO: Review how the "Create" URL is generated. [LK] + return `section/content/workspace/document/create/${this._documentUnique ?? 'null'}/${item.unique}`; + } + + render() { + return html`HELLO`; + return this._allowedDocumentTypes.length !== 1 ? this.#renderDropdown() : this.#renderCreateButton(); + } + + #renderCreateButton() { + if (this._allowedDocumentTypes.length !== 1) return; + + const item = this._allowedDocumentTypes[0]; + const label = (this.manifest?.meta.label ?? this.localize.term('general_create')) + ' ' + item.name; + + return html` this.#onClick(item, e)} + color="default" + href=${this.#getCreateUrl(item)} + label=${label} + look="outline">`; + } + + #renderDropdown() { + if (!this._allowedDocumentTypes.length) return; + + const label = this.manifest?.meta.label ?? this.localize.term('general_create'); + + return html` + + ${label} + + + + + + ${map( + this._allowedDocumentTypes, + (item) => html` + this.#onClick(item, e)} + label=${item.name} + href=${this.#getCreateUrl(item)}> + + + `, + )} + + + + `; + } +} + +export default UmbCreateDocumentCollectionActionElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-create-member-collection-action': UmbCreateDocumentCollectionActionElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/manifests.ts index 516aa7f1b5..fe6221e4e8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/manifests.ts @@ -3,8 +3,8 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const createManifest: ManifestTypes = { type: 'collectionAction', - kind: 'button', name: 'Create Member Collection Action', + kind: 'button', alias: 'Umb.CollectionAction.Member.Create', weight: 200, meta: { @@ -17,6 +17,7 @@ export const createManifest: ManifestTypes = { match: 'Umb.Collection.Member', }, ], + // element: () => import('./create-member-collection-action.element.js'), }; export const manifests = [createManifest]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts index 62fb627abb..4060cdce97 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts @@ -1,6 +1,7 @@ import { UmbMemberDetailRepository } from '../repository/index.js'; import type { UmbMemberDetailModel } from '../types.js'; import { UMB_MEMBER_WORKSPACE_ALIAS } from './manifests.js'; +import { UmbMemberTypeDetailRepository, type UmbMemberTypeDetailModel } from '@umbraco-cms/backoffice/member-type'; import { type UmbSaveableWorkspaceContextInterface, UmbEditableWorkspaceContextBase, @@ -8,6 +9,7 @@ import { import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbContentTypePropertyStructureManager } from '@umbraco-cms/backoffice/content-type'; export class UmbMemberWorkspaceContext extends UmbEditableWorkspaceContextBase @@ -18,6 +20,11 @@ export class UmbMemberWorkspaceContext #data = new UmbObjectState(undefined); readonly data = this.#data.asObservable(); + readonly structure = new UmbContentTypePropertyStructureManager( + this, + new UmbMemberTypeDetailRepository(this), + ); + constructor(host: UmbControllerHostElement) { super(host, UMB_MEMBER_WORKSPACE_ALIAS); } From a60c88dfe9343251d5df073aa42bddef8cf19642 Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Tue, 20 Feb 2024 15:01:26 +0100 Subject: [PATCH 012/246] use structure --- .../data/member-type/member-type.data.ts | 134 +++++++++++++- .../src/mocks/data/member/member.data.ts | 59 +++++- .../workspace/member-workspace.context.ts | 4 +- ...rkspace-view-content-properties.element.ts | 72 ++++++++ ...mber-workspace-view-content-tab.element.ts | 111 ++++++++++++ .../member-workspace-view-content.element.ts | 170 ++++++++++++++++-- 6 files changed, 531 insertions(+), 19 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content-properties.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content-tab.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/member-type/member-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/member-type/member-type.data.ts index 3acd9907d2..7a6dfb8d52 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/member-type/member-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/member-type/member-type.data.ts @@ -17,8 +17,138 @@ export const data: Array = [ description: 'Member type 1 description', alias: 'memberType1', icon: 'icon-bug', - properties: [], - containers: [], + properties: [ + { + id: '5b4ca208-134e-4865-b423-06e5e97adf3c', + container: { id: 'c3cd2f12-b7c4-4206-8d8b-27c061589f75' }, + alias: 'blogPostText', + name: 'Blog Post Text', + description: null, + dataType: { id: '0cc0eba1-9960-42c9-bf9b-60e150b429ae' }, + variesByCulture: false, + variesBySegment: false, + sortOrder: 0, + validation: { + mandatory: true, + mandatoryMessage: null, + regEx: null, + regExMessage: null, + }, + appearance: { + labelOnTop: false, + }, + }, + { + id: 'ef7096b6-7c9e-49ba-8d49-395111e65ea2', + container: { id: '227d6ed2-e118-4494-b8f2-deb69854a56a' }, + alias: 'blogTextStringUnderMasterTab', + name: 'Blog text string under master tab', + description: null, + dataType: { id: '0cc0eba1-9960-42c9-bf9b-60e150b429ae' }, + variesByCulture: true, + variesBySegment: false, + sortOrder: 1, + validation: { + mandatory: false, + mandatoryMessage: null, + regEx: null, + regExMessage: null, + }, + appearance: { + labelOnTop: false, + }, + }, + { + id: 'e010c429-b298-499a-9bfe-79687af8972a', + container: { id: '22177c49-ecba-4f2e-b7fa-3f2c04d02cfb' }, + alias: 'blogTextStringUnderGroupUnderMasterTab', + name: 'Blog text string under group under master tab', + description: null, + dataType: { id: '0cc0eba1-9960-42c9-bf9b-60e150b429ae' }, + variesByCulture: true, + variesBySegment: false, + sortOrder: 2, + validation: { + mandatory: false, + mandatoryMessage: null, + regEx: null, + regExMessage: null, + }, + appearance: { + labelOnTop: false, + }, + }, + { + id: '1a22749a-c7d2-44bb-b36b-c977c2ced6ef', + container: { id: '2c943997-b685-432d-a6c5-601d8e7a298a' }, + alias: 'localBlogTabString', + name: 'Local Blog Tab String', + description: null, + dataType: { id: '0cc0eba1-9960-42c9-bf9b-60e150b429ae' }, + variesByCulture: false, + variesBySegment: false, + sortOrder: 3, + validation: { + mandatory: true, + mandatoryMessage: null, + regEx: '^[0-9]*$', + regExMessage: null, + }, + appearance: { + labelOnTop: true, + }, + }, + { + id: '22', + container: { id: '2c943997-b685-432d-a6c5-601d8e7a298a' }, + alias: 'blockGrid', + name: 'Block Grid', + description: '', + dataType: { id: 'dt-blockGrid' }, + variesByCulture: false, + variesBySegment: false, + sortOrder: 4, + validation: { + mandatory: true, + mandatoryMessage: null, + regEx: null, + regExMessage: null, + }, + appearance: { + labelOnTop: false, + }, + }, + ], + containers: [ + { + id: 'c3cd2f12-b7c4-4206-8d8b-27c061589f75', + parent: null, + name: 'Content-group', + type: 'Group', + sortOrder: 0, + }, + { + id: '227d6ed2-e118-4494-b8f2-deb69854a56a', + parent: null, + name: 'Master Tab', + type: 'Tab', + sortOrder: 0, + }, + { + id: '22177c49-ecba-4f2e-b7fa-3f2c04d02cfb', + parent: { id: '227d6ed2-e118-4494-b8f2-deb69854a56a' }, + name: 'Blog Group under master tab', + type: 'Group', + sortOrder: 0, + }, + { + id: '2c943997-b685-432d-a6c5-601d8e7a298a', + parent: null, + name: 'Local blog tab', + type: 'Tab', + sortOrder: 1, + }, + ], allowedAsRoot: false, variesByCulture: false, variesBySegment: false, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts index 36f8dd3244..1e30210e74 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts @@ -16,14 +16,63 @@ export const data: Array = [ lastPasswordChangeDate: null, memberType: { id: 'member-type-1-id', icon: '', hasListView: false }, username: 'member1', - values: [], - variants: [ + values: [ + { + culture: null, + segment: null, + alias: 'masterText', + value: 'i have a master text B', + }, + { + culture: null, + segment: null, + alias: 'pageTitle', + value: 'with a page title B', + }, + { + culture: null, + segment: null, + alias: 'blogPostText', + value: 'My first blog post B', + }, { - name: 'Member 1', culture: 'en-us', segment: null, - createDate: '2023-02-06T15:31:46.876902', - updateDate: '2023-02-06T15:31:51.354764', + alias: 'blogTextStringUnderMasterTab', + value: 'in the master tab B', + }, + { + culture: 'en-us', + segment: null, + alias: 'blogTextStringUnderGroupUnderMasterTab', + value: 'which is under another group in the tab B', + }, + { + culture: 'da-dk', + segment: null, + alias: 'blogTextStringUnderMasterTab', + value: 'på master dokument tab B', + }, + { + culture: 'da-dk', + segment: null, + alias: 'blogTextStringUnderGroupUnderMasterTab', + value: 'denne er under en anden gruppe i tab B', + }, + { + culture: null, + segment: null, + alias: 'localBlogTabString', + value: '1234567890', + }, + ], + variants: [ + { + culture: 'en-us', + segment: null, + name: 'Blog post B', + createDate: '2023-02-06T15:32:05.350038', + updateDate: '2023-02-06T15:32:24.957009', }, ], }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts index 4060cdce97..d783d612f2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts @@ -19,7 +19,7 @@ export class UmbMemberWorkspaceContext #data = new UmbObjectState(undefined); readonly data = this.#data.asObservable(); - + readonly contentTypeUnique = this.#data.asObservablePart((data) => data?.memberType.unique); readonly structure = new UmbContentTypePropertyStructureManager( this, new UmbMemberTypeDetailRepository(this), @@ -27,6 +27,8 @@ export class UmbMemberWorkspaceContext constructor(host: UmbControllerHostElement) { super(host, UMB_MEMBER_WORKSPACE_ALIAS); + + this.observe(this.contentTypeUnique, (unique) => this.structure.loadType(unique)); } async load(unique: string) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content-properties.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content-properties.element.ts new file mode 100644 index 0000000000..0c6fbb52dc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content-properties.element.ts @@ -0,0 +1,72 @@ +import { UMB_MEMBER_WORKSPACE_CONTEXT } from '../../member-workspace.context.js'; +import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { UmbPropertyContainerTypes, UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; +import { UmbContentTypePropertyStructureHelper } from '@umbraco-cms/backoffice/content-type'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('umb-member-workspace-view-content-properties') +export class UmbMemberWorkspaceViewContentPropertiesElement extends UmbLitElement { + @property({ type: String, attribute: 'container-name', reflect: false }) + public get containerName(): string | undefined { + return this._propertyStructureHelper.getContainerName(); + } + public set containerName(value: string | undefined) { + this._propertyStructureHelper.setContainerName(value); + } + + @property({ type: String, attribute: 'container-type', reflect: false }) + public get containerType(): UmbPropertyContainerTypes | undefined { + return this._propertyStructureHelper.getContainerType(); + } + public set containerType(value: UmbPropertyContainerTypes | undefined) { + this._propertyStructureHelper.setContainerType(value); + } + + _propertyStructureHelper = new UmbContentTypePropertyStructureHelper(this); + + @state() + _propertyStructure: Array = []; + + constructor() { + super(); + + this.consumeContext(UMB_MEMBER_WORKSPACE_CONTEXT, (workspaceContext) => { + this._propertyStructureHelper.setStructureManager(workspaceContext.structure); + }); + this.observe(this._propertyStructureHelper.propertyStructure, (propertyStructure) => { + this._propertyStructure = propertyStructure; + }); + } + + render() { + return repeat( + this._propertyStructure, + (property) => property.alias, + (property) => + html` `, + ); + } + + static styles = [ + UmbTextStyles, + css` + .property { + border-bottom: 1px solid var(--uui-color-divider); + } + .property:last-child { + border-bottom: 0; + } + `, + ]; +} + +export default UmbMemberWorkspaceViewContentPropertiesElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-workspace-view-content-properties': UmbMemberWorkspaceViewContentPropertiesElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content-tab.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content-tab.element.ts new file mode 100644 index 0000000000..0612fad852 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content-tab.element.ts @@ -0,0 +1,111 @@ +import { UMB_MEMBER_WORKSPACE_CONTEXT } from '../../member-workspace.context.js'; +import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { PropertyTypeContainerModelBaseModel } from '@umbraco-cms/backoffice/external/backend-api'; + +import './member-workspace-view-content-properties.element.js'; +@customElement('umb-member-workspace-view-content-tab') +export class UmbMemberWorkspaceViewContentTabElement extends UmbLitElement { + private _tabName?: string | undefined; + + @property({ type: String }) + public get tabName(): string | undefined { + return this._groupStructureHelper.getName(); + } + public set tabName(value: string | undefined) { + if (value === this._tabName) return; + const oldValue = this._tabName; + this._tabName = value; + this._groupStructureHelper.setName(value); + this.requestUpdate('tabName', oldValue); + } + + @property({ type: Boolean }) + public get noTabName(): boolean { + return this._groupStructureHelper.getIsRoot(); + } + public set noTabName(value: boolean) { + this._groupStructureHelper.setIsRoot(value); + } + + private _ownerTabId?: string | null; + @property({ type: String }) + public get ownerTabId(): string | null | undefined { + return this._ownerTabId; + } + public set ownerTabId(value: string | null | undefined) { + if (value === this._ownerTabId) return; + this._ownerTabId = value; + this._groupStructureHelper.setOwnerId(value); + } + + _groupStructureHelper = new UmbContentTypeContainerStructureHelper(this); + + @state() + _groups: Array = []; + + @state() + _hasProperties = false; + + constructor() { + super(); + + this.consumeContext(UMB_MEMBER_WORKSPACE_CONTEXT, (workspaceContext) => { + this._groupStructureHelper.setStructureManager(workspaceContext.structure); + }); + this.observe(this._groupStructureHelper.containers, (groups) => { + this._groups = groups; + }); + this.observe(this._groupStructureHelper.hasProperties, (hasProperties) => { + this._hasProperties = hasProperties; + }); + } + + render() { + return html` + ${this._hasProperties + ? html` + + + + ` + : ''} + ${repeat( + this._groups, + (group) => group.name, + (group) => + html` + + `, + )} + `; + } + + static styles = [ + UmbTextStyles, + css` + uui-box { + --uui-box-default-padding: 0 var(--uui-size-space-5); + } + uui-box:not(:first-child) { + margin-top: var(--uui-size-layout-1); + } + `, + ]; +} + +export default UmbMemberWorkspaceViewContentTabElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-workspace-view-content-tab': UmbMemberWorkspaceViewContentTabElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content.element.ts index ca9295d631..05bd3d4a68 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content.element.ts @@ -1,22 +1,170 @@ -// import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; -import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_MEMBER_WORKSPACE_CONTEXT } from '../../member-workspace.context.js'; +import type { UmbMemberWorkspaceViewContentTabElement } from './member-workspace-view-content-tab.element.js'; +import { css, html, customElement, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type'; +import type { UmbRoute, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router'; +import { encodeFolderName } from '@umbraco-cms/backoffice/router'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { PropertyTypeContainerModelBaseModel } from '@umbraco-cms/backoffice/external/backend-api'; +import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; -@customElement('umb-member-workspace-view-content') -export class UmbMemberWorkspaceViewContentElement extends UmbLitElement implements UmbWorkspaceViewElement { - render() { - return html`content`; +@customElement('umb-member-workspace-view-edit') +export class UmbMemberWorkspaceViewEditElement extends UmbLitElement implements UmbWorkspaceViewElement { + //@state() + //private _hasRootProperties = false; + + @state() + private _hasRootGroups = false; + + @state() + private _routes: UmbRoute[] = []; + + @state() + private _tabs?: Array; + + @state() + private _routerPath?: string; + + @state() + private _activePath = ''; + + private _workspaceContext?: typeof UMB_MEMBER_WORKSPACE_CONTEXT.TYPE; + + private _tabsStructureHelper = new UmbContentTypeContainerStructureHelper(this); + + constructor() { + super(); + + this._tabsStructureHelper.setIsRoot(true); + this._tabsStructureHelper.setContainerChildType('Tab'); + this.observe(this._tabsStructureHelper.containers, (tabs) => { + this._tabs = tabs; + this._createRoutes(); + }); + + // _hasRootProperties can be gotten via _tabsStructureHelper.hasProperties. But we do not support root properties currently. + + this.consumeContext(UMB_MEMBER_WORKSPACE_CONTEXT, (workspaceContext) => { + this._workspaceContext = workspaceContext; + this._tabsStructureHelper.setStructureManager(workspaceContext.structure); + this._observeRootGroups(); + }); } - static styles = [UmbTextStyles, css``]; + private _observeRootGroups() { + if (!this._workspaceContext) return; + + this.observe( + this._workspaceContext.structure.hasRootContainers('Group'), + (hasRootGroups) => { + this._hasRootGroups = hasRootGroups; + this._createRoutes(); + }, + '_observeGroups', + ); + } + + private _createRoutes() { + if (!this._tabs || !this._workspaceContext) return; + const routes: UmbRoute[] = []; + + if (this._tabs.length > 0) { + this._tabs?.forEach((tab) => { + const tabName = tab.name ?? ''; + routes.push({ + path: `tab/${encodeFolderName(tabName).toString()}`, + component: () => import('./member-workspace-view-content-tab.element.js'), + setup: (component) => { + (component as UmbMemberWorkspaceViewContentTabElement).tabName = tabName; + // TODO: Consider if we can link these more simple, and not parse this on. + (component as UmbMemberWorkspaceViewContentTabElement).ownerTabId = + this._tabsStructureHelper.isOwnerContainer(tab.id!) ? tab.id : undefined; + }, + }); + }); + } + + if (this._hasRootGroups) { + routes.push({ + path: '', + component: () => import('./member-workspace-view-content-tab.element.js'), + setup: (component) => { + (component as UmbMemberWorkspaceViewContentTabElement).noTabName = true; + (component as UmbMemberWorkspaceViewContentTabElement).ownerTabId = null; + }, + }); + } + + if (routes.length !== 0) { + routes.push({ + path: '', + redirectTo: routes[0]?.path, + }); + } + + this._routes = routes; + } + + render() { + if (!this._routes || !this._tabs) return nothing; + + return html` + + ${this._routerPath && (this._tabs.length > 1 || (this._tabs.length === 1 && this._hasRootGroups)) + ? html` + ${this._hasRootGroups && this._tabs.length > 0 + ? html` + Content + ` + : ''} + ${repeat( + this._tabs, + (tab) => tab.name, + (tab) => { + const path = this._routerPath + '/tab/' + encodeFolderName(tab.name || ''); + return html`${tab.name}`; + }, + )} + ` + : ''} + + { + this._routerPath = event.target.absoluteRouterPath; + }} + @change=${(event: UmbRouterSlotChangeEvent) => { + this._activePath = event.target.absoluteActiveViewPath || ''; + }}> + + + `; + } + + static styles = [ + UmbTextStyles, + css` + :host { + display: block; + height: 100%; + --uui-tab-background: var(--uui-color-surface); + } + `, + ]; } -export default UmbMemberWorkspaceViewContentElement; +export default UmbMemberWorkspaceViewEditElement; declare global { interface HTMLElementTagNameMap { - 'umb-member-workspace-view-content': UmbMemberWorkspaceViewContentElement; + 'umb-member-workspace-view-edit': UmbMemberWorkspaceViewEditElement; } } From ec8049f9dc888eac0ee14f7f518f2cc21602ba67 Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Tue, 20 Feb 2024 15:15:51 +0100 Subject: [PATCH 013/246] simple document mock --- .../data/document-type/document-type.data.ts | 55 +++++++++++++++++++ .../src/mocks/data/document/document.data.ts | 39 +++++++++++++ 2 files changed, 94 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts index b6ee696086..f62f2d0dbc 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts @@ -12,6 +12,61 @@ type UmbMockDocumentTypeModelHack = DocumentTypeResponseModel & export interface UmbMockDocumentTypeModel extends Omit {} export const data: Array = [ + { + allowedTemplates: [], + defaultTemplate: { id: 'the-simplest-document-type-id' }, + id: 'the-simplest-document-type-id', + alias: 'theSimplestDocumentType', + name: 'The simple document type', + description: null, + icon: 'icon-document', + allowedAsRoot: true, + variesByCulture: false, + variesBySegment: false, + isElement: false, + hasChildren: false, + parent: null, + isFolder: false, + properties: [ + { + id: '1680d4d2-cda8-4ac2-affd-a69fc10382b1', + container: { id: 'the-simplest-document-type-id-container' }, + alias: 'prop1', + name: 'Prop 1', + description: null, + dataType: { id: '0cc0eba1-9960-42c9-bf9b-60e150b429ae' }, + variesByCulture: false, + variesBySegment: false, + sortOrder: 0, + validation: { + mandatory: false, + mandatoryMessage: null, + regEx: null, + regExMessage: null, + }, + appearance: { + labelOnTop: false, + }, + }, + ], + containers: [ + { + id: 'the-simplest-document-type-id-container', + parent: null, + name: 'Content', + type: 'Group', + sortOrder: 0, + }, + ], + allowedDocumentTypes: [], + compositions: [], + cleanup: { + preventCleanup: false, + keepAllVersionsNewerThanDays: null, + keepLatestVersionPerDayForDays: null, + }, + }, + { allowedTemplates: [], defaultTemplate: null, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts index 2bd492a9b8..8564d14324 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts @@ -10,6 +10,45 @@ type UmbMockDocumentTypeModelHack = DocumentResponseModel & DocumentTreeItemResp export interface UmbMockDocumentModel extends Omit {} export const data: Array = [ + { + urls: [ + { + culture: 'en-US', + url: '/', + }, + ], + template: null, + id: 'the-simplest-document-id', + parent: null, + documentType: { + id: 'the-simplest-document-type-id', + icon: 'icon-document', + hasListView: true, + }, + hasChildren: false, + noAccess: false, + isProtected: false, + isTrashed: false, + variants: [ + { + state: DocumentVariantStateModel.DRAFT, + publishDate: '2023-02-06T15:32:24.957009', + culture: 'en-us', + segment: null, + name: 'The Simplest Document', + createDate: '2023-02-06T15:32:05.350038', + updateDate: '2023-02-06T15:32:24.957009', + }, + ], + values: [ + { + alias: 'prop1', + culture: null, + segment: null, + value: 'default value here', + }, + ], + }, { urls: [ { From 6e942c17dbd3d34669252d67decd58a383c53e25 Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Tue, 20 Feb 2024 15:16:38 +0100 Subject: [PATCH 014/246] member mock --- .../data/member-type/member-type.data.ts | 113 +------------ .../src/mocks/data/member/member.data.ts | 148 +++++++----------- 2 files changed, 59 insertions(+), 202 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/member-type/member-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/member-type/member-type.data.ts index 7a6dfb8d52..88b7fa5735 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/member-type/member-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/member-type/member-type.data.ts @@ -19,35 +19,15 @@ export const data: Array = [ icon: 'icon-bug', properties: [ { - id: '5b4ca208-134e-4865-b423-06e5e97adf3c', - container: { id: 'c3cd2f12-b7c4-4206-8d8b-27c061589f75' }, - alias: 'blogPostText', - name: 'Blog Post Text', + id: '1680d4d2-cda8-4ac2-affd-a69fc10382b1', + container: { id: 'the-simplest-document-type-id-container' }, + alias: 'prop1', + name: 'Prop 1', description: null, dataType: { id: '0cc0eba1-9960-42c9-bf9b-60e150b429ae' }, variesByCulture: false, variesBySegment: false, sortOrder: 0, - validation: { - mandatory: true, - mandatoryMessage: null, - regEx: null, - regExMessage: null, - }, - appearance: { - labelOnTop: false, - }, - }, - { - id: 'ef7096b6-7c9e-49ba-8d49-395111e65ea2', - container: { id: '227d6ed2-e118-4494-b8f2-deb69854a56a' }, - alias: 'blogTextStringUnderMasterTab', - name: 'Blog text string under master tab', - description: null, - dataType: { id: '0cc0eba1-9960-42c9-bf9b-60e150b429ae' }, - variesByCulture: true, - variesBySegment: false, - sortOrder: 1, validation: { mandatory: false, mandatoryMessage: null, @@ -58,96 +38,15 @@ export const data: Array = [ labelOnTop: false, }, }, - { - id: 'e010c429-b298-499a-9bfe-79687af8972a', - container: { id: '22177c49-ecba-4f2e-b7fa-3f2c04d02cfb' }, - alias: 'blogTextStringUnderGroupUnderMasterTab', - name: 'Blog text string under group under master tab', - description: null, - dataType: { id: '0cc0eba1-9960-42c9-bf9b-60e150b429ae' }, - variesByCulture: true, - variesBySegment: false, - sortOrder: 2, - validation: { - mandatory: false, - mandatoryMessage: null, - regEx: null, - regExMessage: null, - }, - appearance: { - labelOnTop: false, - }, - }, - { - id: '1a22749a-c7d2-44bb-b36b-c977c2ced6ef', - container: { id: '2c943997-b685-432d-a6c5-601d8e7a298a' }, - alias: 'localBlogTabString', - name: 'Local Blog Tab String', - description: null, - dataType: { id: '0cc0eba1-9960-42c9-bf9b-60e150b429ae' }, - variesByCulture: false, - variesBySegment: false, - sortOrder: 3, - validation: { - mandatory: true, - mandatoryMessage: null, - regEx: '^[0-9]*$', - regExMessage: null, - }, - appearance: { - labelOnTop: true, - }, - }, - { - id: '22', - container: { id: '2c943997-b685-432d-a6c5-601d8e7a298a' }, - alias: 'blockGrid', - name: 'Block Grid', - description: '', - dataType: { id: 'dt-blockGrid' }, - variesByCulture: false, - variesBySegment: false, - sortOrder: 4, - validation: { - mandatory: true, - mandatoryMessage: null, - regEx: null, - regExMessage: null, - }, - appearance: { - labelOnTop: false, - }, - }, ], containers: [ { - id: 'c3cd2f12-b7c4-4206-8d8b-27c061589f75', + id: 'the-simplest-document-type-id-container', parent: null, - name: 'Content-group', + name: 'Content', type: 'Group', sortOrder: 0, }, - { - id: '227d6ed2-e118-4494-b8f2-deb69854a56a', - parent: null, - name: 'Master Tab', - type: 'Tab', - sortOrder: 0, - }, - { - id: '22177c49-ecba-4f2e-b7fa-3f2c04d02cfb', - parent: { id: '227d6ed2-e118-4494-b8f2-deb69854a56a' }, - name: 'Blog Group under master tab', - type: 'Group', - sortOrder: 0, - }, - { - id: '2c943997-b685-432d-a6c5-601d8e7a298a', - parent: null, - name: 'Local blog tab', - type: 'Tab', - sortOrder: 1, - }, ], allowedAsRoot: false, variesByCulture: false, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts index 1e30210e74..b176825a0d 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts @@ -16,112 +16,70 @@ export const data: Array = [ lastPasswordChangeDate: null, memberType: { id: 'member-type-1-id', icon: '', hasListView: false }, username: 'member1', - values: [ - { - culture: null, - segment: null, - alias: 'masterText', - value: 'i have a master text B', - }, - { - culture: null, - segment: null, - alias: 'pageTitle', - value: 'with a page title B', - }, - { - culture: null, - segment: null, - alias: 'blogPostText', - value: 'My first blog post B', - }, - { - culture: 'en-us', - segment: null, - alias: 'blogTextStringUnderMasterTab', - value: 'in the master tab B', - }, - { - culture: 'en-us', - segment: null, - alias: 'blogTextStringUnderGroupUnderMasterTab', - value: 'which is under another group in the tab B', - }, - { - culture: 'da-dk', - segment: null, - alias: 'blogTextStringUnderMasterTab', - value: 'på master dokument tab B', - }, - { - culture: 'da-dk', - segment: null, - alias: 'blogTextStringUnderGroupUnderMasterTab', - value: 'denne er under en anden gruppe i tab B', - }, - { - culture: null, - segment: null, - alias: 'localBlogTabString', - value: '1234567890', - }, - ], variants: [ { culture: 'en-us', segment: null, - name: 'Blog post B', + name: 'The Simplest Member', createDate: '2023-02-06T15:32:05.350038', updateDate: '2023-02-06T15:32:24.957009', }, ], - }, - { - email: 'member2@member.com', - failedPasswordAttempts: 0, - groups: [], - id: '6ff6f75a-c14e-4172-a80b-d3ffcbc37979', - isApproved: true, - isLockedOut: false, - isTwoFactorEnabled: false, - lastLockoutDate: null, - lastLoginDate: null, - lastPasswordChangeDate: null, - memberType: { id: 'member-type-1-id', icon: '', hasListView: false }, - username: 'member2', - values: [], - variants: [ + values: [ { - name: 'Member 2', - culture: 'en-us', + alias: 'prop1', + culture: null, segment: null, - createDate: '2023-02-06T15:31:46.876902', - updateDate: '2023-02-06T15:31:51.354764', - }, - ], - }, - { - email: 'member3@member.com', - failedPasswordAttempts: 0, - groups: [], - id: '6ff6f75a-c14e-4172-a80b-d3ffcbc37979', - isApproved: false, - isLockedOut: false, - isTwoFactorEnabled: false, - lastLockoutDate: null, - lastLoginDate: null, - lastPasswordChangeDate: null, - memberType: { id: 'member-type-1-id', icon: '', hasListView: false }, - username: 'member3', - values: [], - variants: [ - { - name: 'Member 3', - culture: 'en-us', - segment: null, - createDate: '2023-02-06T15:31:46.876902', - updateDate: '2023-02-06T15:31:51.354764', + value: 'default value here', }, ], }, + // { + // email: 'member2@member.com', + // failedPasswordAttempts: 0, + // groups: [], + // id: '6ff6f75a-c14e-4172-a80b-d3ffcbc37979', + // isApproved: true, + // isLockedOut: false, + // isTwoFactorEnabled: false, + // lastLockoutDate: null, + // lastLoginDate: null, + // lastPasswordChangeDate: null, + // memberType: { id: 'member-type-1-id', icon: '', hasListView: false }, + // username: 'member2', + // values: [], + // variants: [ + // { + // name: 'Member 2', + // culture: 'en-us', + // segment: null, + // createDate: '2023-02-06T15:31:46.876902', + // updateDate: '2023-02-06T15:31:51.354764', + // }, + // ], + // }, + // { + // email: 'member3@member.com', + // failedPasswordAttempts: 0, + // groups: [], + // id: '6ff6f75a-c14e-4172-a80b-d3ffcbc37979', + // isApproved: false, + // isLockedOut: false, + // isTwoFactorEnabled: false, + // lastLockoutDate: null, + // lastLoginDate: null, + // lastPasswordChangeDate: null, + // memberType: { id: 'member-type-1-id', icon: '', hasListView: false }, + // username: 'member3', + // values: [], + // variants: [ + // { + // name: 'Member 3', + // culture: 'en-us', + // segment: null, + // createDate: '2023-02-06T15:31:46.876902', + // updateDate: '2023-02-06T15:31:51.354764', + // }, + // ], + // }, ]; From 0035f13a9c51193f8831526920c234de807d147d Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Wed, 21 Feb 2024 11:07:29 +0100 Subject: [PATCH 015/246] generate api models --- .../src/external/backend-api/src/index.ts | 1 + .../src/models/PagedMemberResponseModel.ts | 12 ++++++ .../src/services/MemberResource.ts | 40 +++++++++++++++++++ .../backend-api/src/services/UserResource.ts | 6 ++- 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/external/backend-api/src/models/PagedMemberResponseModel.ts diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/index.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/index.ts index 17285517c5..d66d735ce5 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/index.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/index.ts @@ -264,6 +264,7 @@ export type { PagedMediaCollectionResponseModel } from './models/PagedMediaColle export type { PagedMediaRecycleBinItemResponseModel } from './models/PagedMediaRecycleBinItemResponseModel'; export type { PagedMediaTreeItemResponseModel } from './models/PagedMediaTreeItemResponseModel'; export type { PagedMediaTypeTreeItemResponseModel } from './models/PagedMediaTypeTreeItemResponseModel'; +export type { PagedMemberResponseModel } from './models/PagedMemberResponseModel'; export type { PagedNamedEntityTreeItemResponseModel } from './models/PagedNamedEntityTreeItemResponseModel'; export type { PagedObjectTypeResponseModel } from './models/PagedObjectTypeResponseModel'; export type { PagedPackageDefinitionResponseModel } from './models/PagedPackageDefinitionResponseModel'; diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models/PagedMemberResponseModel.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models/PagedMemberResponseModel.ts new file mode 100644 index 0000000000..06cea577ab --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models/PagedMemberResponseModel.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { MemberResponseModel } from './MemberResponseModel'; + +export type PagedMemberResponseModel = { + total: number; + items: Array; +}; + diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/MemberResource.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/MemberResource.ts index d432800b20..b739bae4c0 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/MemberResource.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/MemberResource.ts @@ -3,8 +3,10 @@ /* tslint:disable */ /* eslint-disable */ import type { CreateMemberRequestModel } from '../models/CreateMemberRequestModel'; +import type { DirectionModel } from '../models/DirectionModel'; import type { MemberItemResponseModel } from '../models/MemberItemResponseModel'; import type { MemberResponseModel } from '../models/MemberResponseModel'; +import type { PagedMemberResponseModel } from '../models/PagedMemberResponseModel'; import type { UpdateMemberRequestModel } from '../models/UpdateMemberRequestModel'; import type { CancelablePromise } from '../core/CancelablePromise'; @@ -156,6 +158,44 @@ export class MemberResource { }); } + /** + * @returns PagedMemberResponseModel Success + * @throws ApiError + */ + public static getMemberFilter({ + memberTypeId, + orderBy = 'username', + orderDirection, + filter, + skip, + take = 100, + }: { + memberTypeId?: string, + orderBy?: string, + orderDirection?: DirectionModel, + filter?: string, + skip?: number, + take?: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/member/filter', + query: { + 'memberTypeId': memberTypeId, + 'orderBy': orderBy, + 'orderDirection': orderDirection, + 'filter': filter, + 'skip': skip, + 'take': take, + }, + errors: { + 400: `Bad Request`, + 401: `The resource is protected and requires an authentication token`, + 404: `Not Found`, + }, + }); + } + /** * @returns any Success * @throws ApiError diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/UserResource.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/UserResource.ts index d8dd9e1ebe..9a714f6675 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/UserResource.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/UserResource.ts @@ -617,7 +617,7 @@ export class UserResource { } /** - * @returns any Success + * @returns PagedUserResponseModel Success * @throws ApiError */ public static getUserFilter({ @@ -636,7 +636,7 @@ export class UserResource { userGroupIds?: Array, userStates?: Array, filter?: string, - }): CancelablePromise { + }): CancelablePromise { return __request(OpenAPI, { method: 'GET', url: '/umbraco/management/api/v1/user/filter', @@ -650,7 +650,9 @@ export class UserResource { 'filter': filter, }, errors: { + 400: `Bad Request`, 401: `The resource is protected and requires an authentication token`, + 404: `Not Found`, }, }); } From caab72f118e145193c0baf33b5e013ecbeb08443 Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Wed, 21 Feb 2024 11:07:46 +0100 Subject: [PATCH 016/246] member view --- .../member-type-workspace.context.ts | 1 - .../workspace/member-workspace.context.ts | 39 +++++++++++++++- .../member-workspace-view-member.element.ts | 46 +++++++++++++++++-- 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts index fbab9126a2..aff329fc88 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts @@ -68,7 +68,6 @@ export class UmbMemberTypeWorkspaceContext this.setIsNew(false); this.setIsSorting(false); - //this.#draft.next(data); return { data } || undefined; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts index d783d612f2..ff1e4f8ac4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts @@ -31,9 +31,18 @@ export class UmbMemberWorkspaceContext this.observe(this.contentTypeUnique, (unique) => this.structure.loadType(unique)); } + set( + propertyName: PropertyName, + value: UmbMemberDetailModel[PropertyName], + ) { + this.structure.updateOwnerContentType({ [propertyName]: value }); + } + async load(unique: string) { const { data } = await this.repository.requestByUnique(unique); + console.log('data', data); + if (data) { this.setIsNew(false); this.#data.setValue(data); @@ -78,13 +87,41 @@ export class UmbMemberWorkspaceContext } getEntityId() { - return this.getData()?.unique || ''; + return this.#get('unique'); } getEntityType() { return 'member'; } + getEmail() { + return this.#get('email') || ''; + } + + getUsername() { + return this.#get('username') || ''; + } + + getLockedOut() { + return this.#get('isLockedOut') || false; + } + + getIsTwoFactorEnabled() { + return this.#get('isTwoFactorEnabled') || false; + } + + getIsApproved() { + return this.#get('isApproved') || false; + } + + getOldPassword() { + return this.#get('oldPassword') || ''; + } + + #get(propertyName: PropertyName) { + return this.#data.getValue()?.[propertyName]; + } + public destroy(): void { this.#data.destroy(); super.destroy(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts index d2570f85b4..71971dcc4c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts @@ -1,12 +1,36 @@ // import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; +import { UMB_MEMBER_WORKSPACE_CONTEXT } from '../../member-workspace.context.js'; import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbMemberDetailModel } from '../../../types.js'; +import { UUIBooleanInputEvent } from '@umbraco-ui/uui'; @customElement('umb-member-workspace-view-member') export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implements UmbWorkspaceViewElement { + private _workspaceContext?: typeof UMB_MEMBER_WORKSPACE_CONTEXT.TYPE; + + constructor() { + super(); + + this.consumeContext(UMB_MEMBER_WORKSPACE_CONTEXT, (workspaceContext) => { + this._workspaceContext = workspaceContext; + console.log('workspaceContext', !!this._workspaceContext.get('isApproved')); + }); + } + + #onChange(propertyName: keyof UmbMemberDetailModel, value: UmbMemberDetailModel[keyof UmbMemberDetailModel]) { + if (!this._workspaceContext) return; + + this._workspaceContext.set(propertyName, value); + } + render() { + if (!this._workspaceContext) { + return html`
Not found
`; + } + return html` @@ -14,7 +38,11 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement - + @@ -25,14 +53,24 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement
MEMBER GROUP PICKER
-
0
+ +
${this._workspaceContext.get('failedPasswordAttempts')}
+
- + this.#onChange('isApproved', e.target.checked)}> + - + this.#onChange('isLockedOut', e.target.checked)}> + From 50dfaed0bb3039d7791ade22ff597c18ab2af1a2 Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Thu, 22 Feb 2024 12:49:36 +0100 Subject: [PATCH 017/246] change password --- .../workspace/member-workspace.context.ts | 28 ++- .../member-workspace-view-member.element.ts | 194 ++++++++++++++---- 2 files changed, 170 insertions(+), 52 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts index ff1e4f8ac4..0662b94f62 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts @@ -36,6 +36,7 @@ export class UmbMemberWorkspaceContext value: UmbMemberDetailModel[PropertyName], ) { this.structure.updateOwnerContentType({ [propertyName]: value }); + console.log('set', propertyName, value); } async load(unique: string) { @@ -94,28 +95,41 @@ export class UmbMemberWorkspaceContext return 'member'; } - getEmail() { + get email() { return this.#get('email') || ''; } - getUsername() { + get username() { return this.#get('username') || ''; } - getLockedOut() { + get isLockedOut() { return this.#get('isLockedOut') || false; } - getIsTwoFactorEnabled() { + get isTwoFactorEnabled() { return this.#get('isTwoFactorEnabled') || false; } - getIsApproved() { + get isApproved() { return this.#get('isApproved') || false; } - getOldPassword() { - return this.#get('oldPassword') || ''; + get failedPasswordAttempts() { + return this.#get('failedPasswordAttempts') || 0; + } + + //TODO Use localization for "never" + get lastLockOutDate() { + return this.#get('lastLockoutDate') || 'never'; + } + + get lastLoginDate() { + return this.#get('lastLoginDate') || 'never'; + } + + get lastPasswordChangeDate() { + return this.#get('lastPasswordChangeDate') || 'never'; } #get(propertyName: PropertyName) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts index 71971dcc4c..aeb84f66a0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts @@ -1,11 +1,12 @@ // import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; import { UMB_MEMBER_WORKSPACE_CONTEXT } from '../../member-workspace.context.js'; -import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbMemberDetailModel } from '../../../types.js'; import { UUIBooleanInputEvent } from '@umbraco-ui/uui'; +import { UMB_CHANGE_PASSWORD_MODAL, UMB_MODAL_CONTEXT, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; @customElement('umb-member-workspace-view-member') export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implements UmbWorkspaceViewElement { @@ -14,75 +15,147 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement constructor() { super(); - this.consumeContext(UMB_MEMBER_WORKSPACE_CONTEXT, (workspaceContext) => { - this._workspaceContext = workspaceContext; - console.log('workspaceContext', !!this._workspaceContext.get('isApproved')); + this.consumeContext(UMB_MEMBER_WORKSPACE_CONTEXT, (context) => { + this._workspaceContext = context; }); } + @state() + private _showChangePasswordForm = false; + + @state() + private _newPasswordError = ''; + #onChange(propertyName: keyof UmbMemberDetailModel, value: UmbMemberDetailModel[keyof UmbMemberDetailModel]) { if (!this._workspaceContext) return; this._workspaceContext.set(propertyName, value); } + #onPasswordUpdate = () => { + const newPassword = this.shadowRoot?.querySelector('uui-input[name="newPassword"]')?.value; + const confirmPassword = this.shadowRoot?.querySelector('uui-input[name="confirmPassword"]') + ?.value; + + if (newPassword !== confirmPassword) { + this._newPasswordError = 'Passwords do not match'; + return; + } + + this._newPasswordError = ''; + + this._workspaceContext?.set('newPassword', newPassword); + }; + + #onNewPasswordCancel = () => { + this._workspaceContext?.set('newPassword', ''); + this._showChangePasswordForm = false; + this._newPasswordError = ''; + }; + render() { if (!this._workspaceContext) { return html`
Not found
`; } return html` - - - - +
+ + + this.#onChange('username', (e.target as HTMLInputElement).value)}> + - - - + + this.#onChange('email', (e.target as HTMLInputElement).value)} + value=${this._workspaceContext.email}> + - - - + + ${when( + this._showChangePasswordForm, + () => html` +
+ + this.#onPasswordUpdate()}> + + + this.#onPasswordUpdate()}> + + ${when(this._newPasswordError, () => html`

${this._newPasswordError}

`)} + +
+ `, + () => + html` (this._showChangePasswordForm = true)}>`, + )} +
- -
MEMBER GROUP PICKER
-
+ +
MEMBER GROUP PICKER
+
- -
${this._workspaceContext.get('failedPasswordAttempts')}
-
+ + this.#onChange('isApproved', e.target.checked)}> + + - - this.#onChange('isApproved', e.target.checked)}> - - + + this.#onChange('isLockedOut', e.target.checked)}> + + - - this.#onChange('isLockedOut', e.target.checked)}> - - + + + +
- - - + + +
${this._workspaceContext.failedPasswordAttempts}
+
-
never
+ +
${this._workspaceContext.lastLockOutDate}
+
-
never
+ +
${this._workspaceContext.lastLoginDate}
+
-
never
-
+ +
${this._workspaceContext.lastPasswordChangeDate}
+
+ +
`; } @@ -92,6 +165,37 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement uui-input { width: 100%; } + #main { + display: flex; + flex-wrap: wrap; + gap: var(--uui-size-space-4); + } + #left-column { + /* Is there a way to make the wrapped right column grow only when wrapped? */ + flex-grow: 9999999; + flex-shrink: 0; + flex-basis: 700px; + } + #right-column { + flex-basis: 300px; + flex-grow: 1; + } + uui-box { + height: fit-content; + } + umb-property-layout { + padding-block: var(--uui-size-space-4); + } + umb-property-layout:first-child { + padding-top: 0; + } + umb-property-layout:last-child { + padding-bottom: 0; + } + .validation-error { + margin-top: 0; + color: var(--uui-color-danger); + } `, ]; } From c6aedec2a35bb6940ebf1c1d472aa18106060bd7 Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Thu, 22 Feb 2024 13:15:06 +0100 Subject: [PATCH 018/246] layout fixes --- .../member-group-table-collection-view.element.ts | 4 ++++ .../table/member-table-collection-view.element.ts | 3 +++ .../member/member-workspace-view-member.element.ts | 12 ++++-------- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/collection/views/table/member-group-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/collection/views/table/member-group-table-collection-view.element.ts index 2065ad1bdb..a6d8cd3fc2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/collection/views/table/member-group-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/collection/views/table/member-group-table-collection-view.element.ts @@ -70,6 +70,10 @@ export class UmbMemberGroupTableCollectionViewElement extends UmbLitElement { display: flex; flex-direction: column; } + + umb-table { + padding: 0; + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/views/table/member-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/views/table/member-table-collection-view.element.ts index 3b30d688c3..db29fcdb1b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/views/table/member-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/views/table/member-table-collection-view.element.ts @@ -71,6 +71,9 @@ export class UmbMemberTableCollectionViewElement extends UmbLitElement { display: flex; flex-direction: column; } + umb-table { + padding: 0; + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts index aeb84f66a0..efb964a459 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts @@ -1,12 +1,11 @@ // import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; import { UMB_MEMBER_WORKSPACE_CONTEXT } from '../../member-workspace.context.js'; +import type { UmbMemberDetailModel } from '../../../types.js'; import { css, html, customElement, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { UmbMemberDetailModel } from '../../../types.js'; -import { UUIBooleanInputEvent } from '@umbraco-ui/uui'; -import { UMB_CHANGE_PASSWORD_MODAL, UMB_MODAL_CONTEXT, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import type { UUIBooleanInputEvent } from '@umbraco-cms/backoffice/external/uui'; @customElement('umb-member-workspace-view-member') export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implements UmbWorkspaceViewElement { @@ -172,13 +171,10 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement } #left-column { /* Is there a way to make the wrapped right column grow only when wrapped? */ - flex-grow: 9999999; - flex-shrink: 0; - flex-basis: 700px; + flex: 9999 1 500px; } #right-column { - flex-basis: 300px; - flex-grow: 1; + flex: 1 1 300px; } uui-box { height: fit-content; From 0367242bcbbbc54033d155e2da9849fa90fea90b Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Mon, 26 Feb 2024 14:29:42 +0100 Subject: [PATCH 019/246] fix mock --- src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts index e01e21d2b5..d56ef51c37 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts @@ -16,6 +16,7 @@ export const data: Array = [ lastPasswordChangeDate: null, memberType: { id: 'member-type-1-id', icon: '' }, username: 'member1', + values: [], variants: [ { culture: 'en-us', From 4be4bed0b07d45176cfd6eb0181ac8ae785891a4 Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Mon, 26 Feb 2024 15:31:52 +0100 Subject: [PATCH 020/246] create member --- .../src/packages/members/member-type/index.ts | 2 + .../members/member-type/workspace/index.ts | 1 + .../member-type-workspace.context-token.ts | 12 ++ ...create-member-collection-action.element.ts | 162 +++++++----------- .../member/collection/action/manifests.ts | 1 + 5 files changed, 78 insertions(+), 100 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context-token.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/index.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/index.ts index c5c0a92644..61022096c2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/index.ts @@ -1,5 +1,7 @@ import './components/index.js'; +export * from './workspace/index.js'; + export * from './components/index.js'; export * from './repository/index.js'; export * from './entity.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/index.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/index.ts new file mode 100644 index 0000000000..20a2aba255 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/index.ts @@ -0,0 +1 @@ +export * from './member-type-workspace.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context-token.ts new file mode 100644 index 0000000000..3307c71b19 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context-token.ts @@ -0,0 +1,12 @@ +import type { UmbMemberTypeWorkspaceContext } from './member-type-workspace.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; + +export const UMB_MEMBER_TYPE_WORKSPACE_CONTEXT = new UmbContextToken< + UmbSaveableWorkspaceContextInterface, + UmbMemberTypeWorkspaceContext +>( + 'UmbWorkspaceContext', + undefined, + (context): context is UmbMemberTypeWorkspaceContext => context.getEntityType?.() === 'member-type', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts index 0e0d5db5de..e7c7833598 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts @@ -1,124 +1,86 @@ -import { html, customElement, property, state, map } from '@umbraco-cms/backoffice/external/lit'; -import { UmbDocumentTypeStructureRepository } from '@umbraco-cms/backoffice/document-type'; +import { html, customElement, state, repeat, css } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/document'; -import type { ManifestCollectionAction } from '@umbraco-cms/backoffice/extension-registry'; -import type { UmbAllowedDocumentTypeModel } from '@umbraco-cms/backoffice/document-type'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import UmbMemberTypeTreeRepository from 'src/packages/members/member-type/tree/member-type-tree.repository'; @customElement('umb-create-member-collection-action') export class UmbCreateDocumentCollectionActionElement extends UmbLitElement { @state() - private _allowedDocumentTypes: Array = []; + private _options: Array<{ label: string; unique: string; icon: string }> = []; - @state() - private _documentUnique?: string; - - @state() - private _documentTypeUnique?: string; - - @state() - private _popoverOpen = false; - - @property({ attribute: false }) - manifest?: ManifestCollectionAction; - - #documentTypeStructureRepository = new UmbDocumentTypeStructureRepository(this); + #memberTypeCollectionRepository = new UmbMemberTypeTreeRepository(this); constructor() { super(); - this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (workspaceContext) => { - this.observe(workspaceContext.unique, (unique) => { - this._documentUnique = unique; - }); - this.observe(workspaceContext.contentTypeUnique, (documentTypeUnique) => { - this._documentTypeUnique = documentTypeUnique; - }); + this.#test(); + } + + async #test() { + //TODO: Should we use the tree repository or make a collection repository? + //TODO: And how would we get all the member types? + //TODO: This only works because member types can't have folders. + const { data } = await this.#memberTypeCollectionRepository.requestRootTreeItems(); + if (!data) return; + + this._options = data.items.map((item) => { + return { + label: item.name, + unique: item.unique, + icon: item.icon || '', + }; }); + console.log(this._options); + this.requestUpdate(); } - async firstUpdated() { - if (this._documentTypeUnique) { - this.#retrieveAllowedDocumentTypesOf(this._documentTypeUnique); - } - } + #onButtonClick = () => { + console.log('Create'); + }; - async #retrieveAllowedDocumentTypesOf(unique: string | null) { - const { data } = await this.#documentTypeStructureRepository.requestAllowedChildrenOf(unique); - - if (data && data.items) { - this._allowedDocumentTypes = data.items; - } - } - - // TODO: This ignorer is just neede for JSON SCHEMA TO WORK, As its not updated with latest TS jet. - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - #onPopoverToggle(event: ToggleEvent) { - this._popoverOpen = event.newState === 'open'; - } - - #onClick(item: UmbAllowedDocumentTypeModel, e: Event) { - e.preventDefault(); - // TODO: Do anything else here? [LK] - } - - #getCreateUrl(item: UmbAllowedDocumentTypeModel) { - // TODO: Review how the "Create" URL is generated. [LK] - return `section/content/workspace/document/create/${this._documentUnique ?? 'null'}/${item.unique}`; + #renderOptions() { + return html` + ${repeat( + this._options, + (option) => option.unique, + (option) => + html` + ${option.label} + `, + )} + `; } render() { - return html`HELLO`; - return this._allowedDocumentTypes.length !== 1 ? this.#renderDropdown() : this.#renderCreateButton(); - } - - #renderCreateButton() { - if (this._allowedDocumentTypes.length !== 1) return; - - const item = this._allowedDocumentTypes[0]; - const label = (this.manifest?.meta.label ?? this.localize.term('general_create')) + ' ' + item.name; - - return html` this.#onClick(item, e)} - color="default" - href=${this.#getCreateUrl(item)} - label=${label} - look="outline">`; - } - - #renderDropdown() { - if (!this._allowedDocumentTypes.length) return; - - const label = this.manifest?.meta.label ?? this.localize.term('general_create'); - return html` - - ${label} - - - - - - ${map( - this._allowedDocumentTypes, - (item) => html` - this.#onClick(item, e)} - label=${item.name} - href=${this.#getCreateUrl(item)}> - - - `, - )} - - + + +
${this.#renderOptions()}
`; } + + static styles = [ + UmbTextStyles, + css` + #popover-content { + background-color: var(--uui-color-surface); + box-shadow: var(--uui-shadow-depth-3); + border-radius: var(--uui-border-radius); + display: flex; + flex-direction: column; + --uui-button-content-align: left; + } + `, + ]; } export default UmbCreateDocumentCollectionActionElement; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/manifests.ts index fe6221e4e8..f84bbe5029 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/manifests.ts @@ -11,6 +11,7 @@ export const createManifest: ManifestTypes = { label: 'Create', href: 'section/member-management/workspace/member/create/member-type-1-id', // TODO: remove hardcoded member type id }, + js: () => import('./create-member-collection-action.element.js'), conditions: [ { alias: UMB_COLLECTION_ALIAS_CONDITION, From 5e357690ec8d34404bd206169f2f253f0f50c71c Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Mon, 26 Feb 2024 15:42:53 +0100 Subject: [PATCH 021/246] fix saving --- .../member/workspace/member-workspace-editor.element.ts | 6 ------ .../members/member/workspace/member-workspace.context.ts | 5 +---- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts index bd598f234f..c614489782 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts @@ -32,12 +32,6 @@ export class UmbMemberWorkspaceEditorElement extends UmbLitElement { }); } - // Only for CRUD demonstration purposes - #onChange = (e: Event) => { - const input = e.target as HTMLInputElement; - this.#workspaceContext!.updateData({ email: input.value }); - }; - render() { return html` `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts index 0662b94f62..2e98238693 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts @@ -35,15 +35,12 @@ export class UmbMemberWorkspaceContext propertyName: PropertyName, value: UmbMemberDetailModel[PropertyName], ) { - this.structure.updateOwnerContentType({ [propertyName]: value }); - console.log('set', propertyName, value); + this.#data.update({ [propertyName]: value }); } async load(unique: string) { const { data } = await this.repository.requestByUnique(unique); - console.log('data', data); - if (data) { this.setIsNew(false); this.#data.setValue(data); From 5783e64dcd8be87ffaf70623d019b42ef096e305 Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Mon, 26 Feb 2024 16:44:49 +0100 Subject: [PATCH 022/246] wip --- .../member-detail.server.data-source.ts | 10 +++++- .../member-workspace-editor.element.ts | 35 ++++++++++++------- .../workspace/member-workspace.context.ts | 25 ++++++++++++- .../member-workspace-view-member.element.ts | 2 -- 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/detail/member-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/detail/member-detail.server.data-source.ts index 7ec960fded..1c326ed78f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/detail/member-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/detail/member-detail.server.data-source.ts @@ -49,7 +49,15 @@ export class UmbMemberServerDataSource implements UmbDetailDataSource { this.#workspaceContext = workspaceContext; - this.#observeData(); + if (this.#workspaceContext) { + this._name = this.#workspaceContext.getName() || ''; + } }); } - // Only for CRUD demonstration purposes - #observeData() { - this.observe(this.#workspaceContext!.data, (data) => { - this._data = data; - console.log(data); - }); + #onInput(event: UUIInputEvent) { + if (event instanceof UUIInputEvent) { + const target = event.composedPath()[0] as UUIInputElement; + + if (typeof target?.value === 'string') { + this.#workspaceContext?.setName(target.value); + } + } } render() { - return html` `; + return html` + + + + `; } static styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts index 2e98238693..afe7341206 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts @@ -8,8 +8,9 @@ import { } from '@umbraco-cms/backoffice/workspace'; import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbObjectState, partialUpdateFrozenArray } from '@umbraco-cms/backoffice/observable-api'; import { UmbContentTypePropertyStructureManager } from '@umbraco-cms/backoffice/content-type'; +import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; export class UmbMemberWorkspaceContext extends UmbEditableWorkspaceContextBase @@ -36,6 +37,7 @@ export class UmbMemberWorkspaceContext value: UmbMemberDetailModel[PropertyName], ) { this.#data.update({ [propertyName]: value }); + console.log('set', propertyName, value, this.#data.getValue()); } async load(unique: string) { @@ -92,6 +94,27 @@ export class UmbMemberWorkspaceContext return 'member'; } + getName(variantId?: UmbVariantId) { + const variants = this.#data.getValue()?.variants; + if (!variants) return; + if (variantId) { + return variants.find((x) => variantId.compare(x))?.name; + } else { + return variants[0]?.name; + } + } + + setName(name: string, variantId?: UmbVariantId) { + const oldVariants = this.#data.getValue()?.variants || []; + const variants = partialUpdateFrozenArray( + oldVariants, + { name }, + variantId ? (x) => variantId.compare(x) : () => true, + ); + console.log('setName', oldVariants, variants); + this.#data.update({ variants }); + } + get email() { return this.#get('email') || ''; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts index efb964a459..7a94bdf6c3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts @@ -41,8 +41,6 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement return; } - this._newPasswordError = ''; - this._workspaceContext?.set('newPassword', newPassword); }; From 3352e0d58dac7645b61519370835fa50f94fdc42 Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Tue, 27 Feb 2024 09:16:15 +0100 Subject: [PATCH 023/246] work --- .../members/member/collection/manifests.ts | 3 +- .../collection/member-collection.context.ts | 16 +++++ .../collection/member-collection.element.ts | 19 ++++++ .../member-collection.repository.ts | 18 ++++-- .../member-collection.server.data-source.ts | 60 ++++++++++++------- .../member-detail.server.data-source.ts | 2 +- .../repository/member-repository-base.ts | 33 ++++++++++ .../member-workspace-editor.element.ts | 6 +- .../workspace/member-workspace.context.ts | 15 ++--- .../member-workspace-view-member.element.ts | 4 +- 10 files changed, 130 insertions(+), 46 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.context.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member/repository/member-repository-base.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/manifests.ts index 91e14b657e..46fa9d358a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/manifests.ts @@ -8,9 +8,10 @@ export const UMB_MEMBER_COLLECTION_ALIAS = 'Umb.Collection.Member'; const collectionManifest: ManifestTypes = { type: 'collection', - kind: 'default', alias: UMB_MEMBER_COLLECTION_ALIAS, name: 'Member Collection', + api: () => import('./member-collection.context.js'), + element: () => import('./member-collection.element.js'), meta: { repositoryAlias: UMB_MEMBER_COLLECTION_REPOSITORY_ALIAS, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.context.ts new file mode 100644 index 0000000000..ab01ec035b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.context.ts @@ -0,0 +1,16 @@ +import type { UmbMemberDetailModel } from '../types.js'; +import type { UmbMemberCollectionFilterModel } from './types.js'; +import { UMB_MEMBER_TABLE_COLLECTION_VIEW_ALIAS } from './views/manifests.js'; +import { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; +import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbMemberCollectionContext extends UmbDefaultCollectionContext< + UmbMemberDetailModel, + UmbMemberCollectionFilterModel +> { + constructor(host: UmbControllerHostElement) { + super(host, UMB_MEMBER_TABLE_COLLECTION_VIEW_ALIAS); + } +} + +export default UmbMemberCollectionContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.element.ts new file mode 100644 index 0000000000..922295c29c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.element.ts @@ -0,0 +1,19 @@ +import { html, customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection'; + +// import './member-collection-header.element.js'; + +@customElement('umb-member-collection') +export class UmbMemberCollectionElement extends UmbCollectionDefaultElement { + // protected renderToolbar() { + // return html` `; + // } +} + +export default UmbMemberCollectionElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-collection': UmbMemberCollectionElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/repository/member-collection.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/repository/member-collection.repository.ts index 3cf5f4de08..62ec20d00d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/repository/member-collection.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/repository/member-collection.repository.ts @@ -1,21 +1,29 @@ +import { UmbMemberRepositoryBase } from '../../repository/member-repository-base.js'; import type { UmbMemberCollectionFilterModel } from '../types.js'; import { UmbMemberCollectionServerDataSource } from './member-collection.server.data-source.js'; import type { UmbMemberCollectionDataSource } from './types.js'; import type { UmbCollectionRepository } from '@umbraco-cms/backoffice/collection'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbMemberCollectionRepository implements UmbCollectionRepository { +export class UmbMemberCollectionRepository extends UmbMemberRepositoryBase implements UmbCollectionRepository { #collectionSource: UmbMemberCollectionDataSource; constructor(host: UmbControllerHost) { + super(host); this.#collectionSource = new UmbMemberCollectionServerDataSource(host); } - async requestCollection(filter: UmbMemberCollectionFilterModel) { - return this.#collectionSource.getCollection(filter); - } + async requestCollection(filter: UmbMemberCollectionFilterModel = { skip: 0, take: 100 }) { + await this.init; - destroy(): void {} + const { data, error } = await this.#collectionSource.getCollection(filter); + + if (data) { + this.detailStore!.appendItems(data.items); + } + + return { data, error, asObservable: () => this.detailStore!.all() }; + } } export default UmbMemberCollectionRepository; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/repository/member-collection.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/repository/member-collection.server.data-source.ts index 5c07640b3f..c1bd0e153a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/repository/member-collection.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/repository/member-collection.server.data-source.ts @@ -1,10 +1,12 @@ -import type { UmbMemberCollectionFilterModel, UmbMemberCollectionModel } from '../types.js'; -import type { UmbMemberDetailModel } from '../../types.js'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import type { UmbMemberDetailModel, UmbMemberValueModel } from '../../types.js'; import { UMB_MEMBER_ENTITY_TYPE } from '../../entity.js'; +import type { UmbMemberCollectionFilterModel } from '../types.js'; import type { UmbCollectionDataSource } from '@umbraco-cms/backoffice/collection'; +import type { MemberResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { MemberResource } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import type { UmbVariantModel } from '@umbraco-cms/backoffice/variant'; /** * A data source that fetches the member collection data from the server. @@ -31,28 +33,40 @@ export class UmbMemberCollectionServerDataSource implements UmbCollectionDataSou * @memberof UmbMemberCollectionServerDataSource */ async getCollection(filter: UmbMemberCollectionFilterModel) { - //const { data, error } = await tryExecuteAndNotify(this.#host, MemberResource.getCollectionMember(filter)); + const { data, error } = await tryExecuteAndNotify(this.#host, MemberResource.getFilterMember(filter)); - const { data, error } = await tryExecuteAndNotify( - this.#host, - fetch('/umbraco/management/api/v1/collection/member'), - ); - - if (data) { - const json = await data.json(); - const items = json.items.map((item: any) => { - const model: UmbMemberCollectionModel = { - unique: item.id, - entityType: UMB_MEMBER_ENTITY_TYPE, - variants: item.variants, - }; - - return model; - }); - - return { data: { items, total: json.total } }; + if (error) { + return { error }; } - return { error }; + if (!data) { + return { data: { items: [], total: 0 } }; + } + + const { items, total } = data; + + const mappedItems: Array = items.map((item: MemberResponseModel) => { + const memberDetail: UmbMemberDetailModel = { + entityType: UMB_MEMBER_ENTITY_TYPE, + email: item.email, + variants: item.variants as UmbVariantModel[], + unique: item.id, + lastLoginDate: item.lastLoginDate || null, + lastLockoutDate: item.lastLockoutDate || null, + lastPasswordChangeDate: item.lastPasswordChangeDate || null, + failedPasswordAttempts: item.failedPasswordAttempts, + isApproved: item.isApproved, + isLockedOut: item.isLockedOut, + groups: item.groups, + isTwoFactorEnabled: item.isTwoFactorEnabled, + memberType: { unique: item.memberType.id }, + username: item.username, + values: item.values as UmbMemberValueModel[], + }; + + return memberDetail; + }); + + return { data: { items: mappedItems, total } }; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/detail/member-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/detail/member-detail.server.data-source.ts index 1c326ed78f..e8eac77570 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/detail/member-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/detail/member-detail.server.data-source.ts @@ -132,7 +132,7 @@ export class UmbMemberServerDataSource implements UmbDetailDataSource { + this.detailStore = instance; + }).asPromise(), + + this.consumeContext(UMB_MEMBER_ITEM_STORE_CONTEXT, (instance) => { + this.itemStore = instance; + }).asPromise(), + + this.consumeContext(UMB_NOTIFICATION_CONTEXT, (instance) => { + this.notificationContext = instance; + }).asPromise(), + ]); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts index e7f3cf9496..b0a81e8bd6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts @@ -21,9 +21,9 @@ export class UmbMemberWorkspaceEditorElement extends UmbLitElement { this.consumeContext(UMB_MEMBER_WORKSPACE_CONTEXT, (workspaceContext) => { this.#workspaceContext = workspaceContext; - if (this.#workspaceContext) { - this._name = this.#workspaceContext.getName() || ''; - } + this.observe(this.#workspaceContext.name, (name) => { + this._name = name || ''; + }); }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts index afe7341206..ca5e31f5f4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts @@ -20,6 +20,7 @@ export class UmbMemberWorkspaceContext #data = new UmbObjectState(undefined); readonly data = this.#data.asObservable(); + readonly name = this.#data.asObservablePart((data) => data?.variants[0].name); readonly contentTypeUnique = this.#data.asObservablePart((data) => data?.memberType.unique); readonly structure = new UmbContentTypePropertyStructureManager( this, @@ -94,16 +95,6 @@ export class UmbMemberWorkspaceContext return 'member'; } - getName(variantId?: UmbVariantId) { - const variants = this.#data.getValue()?.variants; - if (!variants) return; - if (variantId) { - return variants.find((x) => variantId.compare(x))?.name; - } else { - return variants[0]?.name; - } - } - setName(name: string, variantId?: UmbVariantId) { const oldVariants = this.#data.getValue()?.variants || []; const variants = partialUpdateFrozenArray( @@ -149,7 +140,9 @@ export class UmbMemberWorkspaceContext } get lastPasswordChangeDate() { - return this.#get('lastPasswordChangeDate') || 'never'; + const date = this.#get('lastPasswordChangeDate'); + if (!date) return 'never'; + return new Date(date).toLocaleString(); } #get(propertyName: PropertyName) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts index 7a94bdf6c3..f5f2c7dc40 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts @@ -1,10 +1,10 @@ // import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UMB_MEMBER_WORKSPACE_CONTEXT } from '../../member-workspace.context.js'; import type { UmbMemberDetailModel } from '../../../types.js'; import { css, html, customElement, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UUIBooleanInputEvent } from '@umbraco-cms/backoffice/external/uui'; @customElement('umb-member-workspace-view-member') @@ -172,7 +172,7 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement flex: 9999 1 500px; } #right-column { - flex: 1 1 300px; + flex: 1 1 350px; } uui-box { height: fit-content; From f048dc2f55f11a56acaabe54eef1eab4db0e427b Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Tue, 27 Feb 2024 10:24:24 +0100 Subject: [PATCH 024/246] fix create member action --- .../src/packages/members/member-type/index.ts | 1 + ...create-member-collection-action.element.ts | 30 +++++++++---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/index.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/index.ts index 61022096c2..b67effb41a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/index.ts @@ -5,5 +5,6 @@ export * from './workspace/index.js'; export * from './components/index.js'; export * from './repository/index.js'; export * from './entity.js'; +export * from './tree/index.js'; export type { UmbMemberTypeDetailModel } from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts index e7c7833598..c09114ee47 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts @@ -1,7 +1,7 @@ -import { html, customElement, state, repeat, css } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import UmbMemberTypeTreeRepository from 'src/packages/members/member-type/tree/member-type-tree.repository'; +import { html, customElement, state, repeat, css, until } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbMemberTypeTreeRepository } from '@umbraco-cms/backoffice/member-type'; @customElement('umb-create-member-collection-action') export class UmbCreateDocumentCollectionActionElement extends UmbLitElement { @@ -9,14 +9,9 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement { private _options: Array<{ label: string; unique: string; icon: string }> = []; #memberTypeCollectionRepository = new UmbMemberTypeTreeRepository(this); + #optionRequestPromise?: Promise; - constructor() { - super(); - - this.#test(); - } - - async #test() { + async #getOptions() { //TODO: Should we use the tree repository or make a collection repository? //TODO: And how would we get all the member types? //TODO: This only works because member types can't have folders. @@ -30,15 +25,18 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement { icon: item.icon || '', }; }); - console.log(this._options); this.requestUpdate(); } #onButtonClick = () => { - console.log('Create'); + if (this._options.length > 0) return; + + this.#getOptions(); }; - #renderOptions() { + async #renderOptions() { + await this.#optionRequestPromise; + return html` ${repeat( this._options, @@ -48,8 +46,8 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement { compact label=${option.label} href="section/member-management/workspace/member/create/${option.unique}"> - ${option.label} + + ${option.label} `, )} `; @@ -63,7 +61,7 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement { look="outline" popovertarget="create-popover"> -
${this.#renderOptions()}
+
${until(this.#renderOptions(), html``)}
`; } From b90ca48119b55630e785105f53fc40dc16b1923a Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Tue, 27 Feb 2024 11:20:25 +0100 Subject: [PATCH 025/246] move info to member view --- .../members/member/workspace/manifests.ts | 18 -- .../workspace/member-workspace.context.ts | 2 + .../member-workspace-view-info.element.ts | 22 --- ...mber-workspace-view-member-info.element.ts | 136 +++++++++++++ .../member-workspace-view-member.element.ts | 178 ++++++++++-------- 5 files changed, 238 insertions(+), 118 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/info/member-workspace-view-info.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/manifests.ts index 21c72b85c1..2c76a1bf17 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/manifests.ts @@ -75,24 +75,6 @@ export const workspaceViews: Array = [ }, ], }, - { - type: 'workspaceView', - alias: 'Umb.WorkspaceView.Member.Info', - name: 'Member Workspace Info View', - js: () => import('./views/info/member-workspace-view-info.element.js'), - weight: 100, - meta: { - label: 'Info', - pathname: 'info', - icon: 'icon-info', - }, - conditions: [ - { - alias: 'Umb.Condition.WorkspaceAlias', - match: UMB_MEMBER_WORKSPACE_ALIAS, - }, - ], - }, ]; export const manifests = [workspace, ...workspaceActions, ...workspaceViews]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts index ca5e31f5f4..1e15b20b5e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts @@ -21,6 +21,8 @@ export class UmbMemberWorkspaceContext #data = new UmbObjectState(undefined); readonly data = this.#data.asObservable(); readonly name = this.#data.asObservablePart((data) => data?.variants[0].name); + readonly createDate = this.#data.asObservablePart((data) => data?.variants[0].createDate); + readonly updateDate = this.#data.asObservablePart((data) => data?.variants[0].updateDate); readonly contentTypeUnique = this.#data.asObservablePart((data) => data?.memberType.unique); readonly structure = new UmbContentTypePropertyStructureManager( this, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/info/member-workspace-view-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/info/member-workspace-view-info.element.ts deleted file mode 100644 index 04eb3da4b6..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/info/member-workspace-view-info.element.ts +++ /dev/null @@ -1,22 +0,0 @@ -// import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; -import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; - -@customElement('umb-member-workspace-view-info') -export class UmbMemberWorkspaceViewInfoElement extends UmbLitElement implements UmbWorkspaceViewElement { - render() { - return html`info`; - } - - static styles = [UmbTextStyles, css``]; -} - -export default UmbMemberWorkspaceViewInfoElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-member-workspace-view-info': UmbMemberWorkspaceViewInfoElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts new file mode 100644 index 0000000000..8a6e00ea1c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts @@ -0,0 +1,136 @@ +// import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { UmbMemberWorkspaceContext } from '../../member-workspace.context.js'; +import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; +import { UmbMemberTypeItemRepository } from '@umbraco-cms/backoffice/member-type'; + +@customElement('umb-member-workspace-view-member-info') +export class UmbMemberWorkspaceViewMemberInfoElement extends UmbLitElement implements UmbWorkspaceViewElement { + @state() + private _memberTypeUnique = ''; + @state() + private _memberTypeName = ''; + @state() + private _memberTypeIcon = ''; + + private _workspaceContext?: UmbMemberWorkspaceContext; + private _memberTypeItemRepository: UmbMemberTypeItemRepository = new UmbMemberTypeItemRepository(this); + + @state() + private _editMemberTypePath = ''; + + @state() + private _createDate = 'Unknown'; + @state() + private _updateDate = 'Unknown'; + + constructor() { + super(); + + new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL) + .addAdditionalPath('member-type') + .onSetup(() => { + return { data: { entityType: 'member-type', preset: {} } }; + }) + .observeRouteBuilder((routeBuilder) => { + this._editMemberTypePath = routeBuilder({}); + }); + + this.consumeContext(UMB_WORKSPACE_CONTEXT, async (nodeContext) => { + this._workspaceContext = nodeContext as UmbMemberWorkspaceContext; + this.observe(this._workspaceContext.contentTypeUnique, (unique) => (this._memberTypeUnique = unique || '')); + this.observe(this._workspaceContext.createDate, (date) => (this._createDate = date || 'Unknown')); + this.observe(this._workspaceContext.updateDate, (date) => (this._updateDate = date || 'Unknown')); + + const memberType = (await this._memberTypeItemRepository.requestItems([this._memberTypeUnique])).data?.[0]; + if (!memberType) return; + this._memberTypeName = memberType.name; + this._memberTypeIcon = memberType.icon; + }); + } + + render() { + return this.#renderGeneralSection(); + } + + #renderGeneralSection() { + return html` +
+ + + + +
+
+ + + + +
+
+ Member Type +
+ + ${this._memberTypeName} + +
+
+
+ + ${this._memberTypeUnique} +
+ `; + } + + static styles = [ + UmbTextStyles, + css` + .headline { + font-weight: bold; + } + + .member-type-edit { + display: flex; + align-items: center; + } + .member-type-edit uui-icon { + margin-right: var(--uui-size-space-1); + } + .member-type-edit uui-button { + margin-left: auto; + } + + //General section + + #general-section { + display: flex; + flex-direction: column; + } + + .general-item { + display: flex; + flex-direction: column; + gap: var(--uui-size-space-1); + } + + .general-item:not(:last-child) { + margin-bottom: var(--uui-size-space-6); + } + `, + ]; +} + +export default UmbMemberWorkspaceViewMemberInfoElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-workspace-view-member-info': UmbMemberWorkspaceViewMemberInfoElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts index f5f2c7dc40..0956891c0d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts @@ -7,6 +7,8 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; import type { UUIBooleanInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import './member-workspace-view-member-info.element.js'; + @customElement('umb-member-workspace-view-member') export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implements UmbWorkspaceViewElement { private _workspaceContext?: typeof UMB_MEMBER_WORKSPACE_CONTEXT.TYPE; @@ -50,92 +52,95 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement this._newPasswordError = ''; }; - render() { - if (!this._workspaceContext) { - return html`
Not found
`; - } + #renderLeftColumn() { + if (!this._workspaceContext) return; + return html`
+ + + this.#onChange('username', (e.target as HTMLInputElement).value)}> + - return html` -
- - - this.#onChange('username', (e.target as HTMLInputElement).value)}> - + + this.#onChange('email', (e.target as HTMLInputElement).value)} + value=${this._workspaceContext.email}> + - - this.#onChange('email', (e.target as HTMLInputElement).value)} - value=${this._workspaceContext.email}> - + + ${when( + this._showChangePasswordForm, + () => html` +
+ + this.#onPasswordUpdate()}> + + + this.#onPasswordUpdate()}> + + ${when(this._newPasswordError, () => html`

${this._newPasswordError}

`)} + +
+ `, + () => + html` (this._showChangePasswordForm = true)}>`, + )} +
- - ${when( - this._showChangePasswordForm, - () => html` -
- - this.#onPasswordUpdate()}> - - - this.#onPasswordUpdate()}> - - ${when(this._newPasswordError, () => html`

${this._newPasswordError}

`)} - -
- `, - () => - html` (this._showChangePasswordForm = true)}>`, - )} -
+ +
MEMBER GROUP PICKER
+
- -
MEMBER GROUP PICKER
-
+ + this.#onChange('isApproved', e.target.checked)}> + + - - this.#onChange('isApproved', e.target.checked)}> - - + + this.#onChange('isLockedOut', e.target.checked)}> + + - - this.#onChange('isLockedOut', e.target.checked)}> - - + + + +
+
`; + } - - - -
+ #renderRightColumn() { + if (!this._workspaceContext) return; - + return html` +
+
${this._workspaceContext.failedPasswordAttempts}
@@ -152,7 +157,21 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement
${this._workspaceContext.lastPasswordChangeDate}
+ + + +
+ `; + } + + render() { + if (!this._workspaceContext) { + return html`
Not found
`; + } + + return html` +
${this.#renderLeftColumn()} ${this.#renderRightColumn()}
`; } @@ -173,6 +192,9 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement } #right-column { flex: 1 1 350px; + display: flex; + flex-direction: column; + gap: var(--uui-size-space-4); } uui-box { height: fit-content; From 7905586bde4fd37d62b88b6b6409d1a1f684312b Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Tue, 27 Feb 2024 11:28:03 +0100 Subject: [PATCH 026/246] fix info --- ...mber-workspace-view-member-info.element.ts | 15 ++----- .../member-workspace-view-member.element.ts | 43 ++++++++++++------- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts index 8a6e00ea1c..1c26c2fd29 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts @@ -92,10 +92,6 @@ export class UmbMemberWorkspaceViewMemberInfoElement extends UmbLitElement imple static styles = [ UmbTextStyles, css` - .headline { - font-weight: bold; - } - .member-type-edit { display: flex; align-items: center; @@ -107,22 +103,17 @@ export class UmbMemberWorkspaceViewMemberInfoElement extends UmbLitElement imple margin-left: auto; } - //General section - - #general-section { - display: flex; - flex-direction: column; - } - .general-item { display: flex; flex-direction: column; gap: var(--uui-size-space-1); } - .general-item:not(:last-child) { margin-bottom: var(--uui-size-space-6); } + .general-item .headline { + font-weight: bold; + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts index 0956891c0d..e7e95e44b1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts @@ -141,21 +141,22 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement return html`
- -
${this._workspaceContext.failedPasswordAttempts}
-
- - -
${this._workspaceContext.lastLockOutDate}
-
- - -
${this._workspaceContext.lastLoginDate}
-
- - -
${this._workspaceContext.lastPasswordChangeDate}
-
+
+ + ${this._workspaceContext.failedPasswordAttempts} +
+
+ + ${this._workspaceContext.lastLockOutDate} +
+
+ + ${this._workspaceContext.lastLoginDate} +
+
+ + ${this._workspaceContext.lastPasswordChangeDate} +
@@ -212,6 +213,18 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement margin-top: 0; color: var(--uui-color-danger); } + + .general-item { + display: flex; + flex-direction: column; + gap: var(--uui-size-space-1); + } + .general-item:not(:last-child) { + margin-bottom: var(--uui-size-space-6); + } + .general-item .headline { + font-weight: bold; + } `, ]; } From 55239e196fde33e2c81f72b2f24bb939f2497801 Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Tue, 27 Feb 2024 12:19:12 +0100 Subject: [PATCH 027/246] login form --- .../member-workspace-editor.element.ts | 3 + .../workspace/member-workspace.context.ts | 1 - .../member-workspace-view-member.element.ts | 104 ++++++++++++------ 3 files changed, 74 insertions(+), 34 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts index b0a81e8bd6..9ab088ed9d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts @@ -53,6 +53,9 @@ export class UmbMemberWorkspaceEditorElement extends UmbLitElement { width: 100%; height: 100%; } + uui-input { + width: 100%; + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts index 1e15b20b5e..9d1cb458ef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts @@ -45,7 +45,6 @@ export class UmbMemberWorkspaceContext async load(unique: string) { const { data } = await this.repository.requestByUnique(unique); - if (data) { this.setIsNew(false); this.#data.setValue(data); diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts index e7e95e44b1..38096a5407 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts @@ -18,6 +18,10 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement this.consumeContext(UMB_MEMBER_WORKSPACE_CONTEXT, (context) => { this._workspaceContext = context; + + this.observe(this._workspaceContext.isNew, (isNew) => { + this._isNew = !!isNew; + }); }); } @@ -27,6 +31,9 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement @state() private _newPasswordError = ''; + @state() + private _isNew = true; + #onChange(propertyName: keyof UmbMemberDetailModel, value: UmbMemberDetailModel[keyof UmbMemberDetailModel]) { if (!this._workspaceContext) return; @@ -43,6 +50,8 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement return; } + this._newPasswordError = ''; + this._workspaceContext?.set('newPassword', newPassword); }; @@ -52,6 +61,67 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement this._newPasswordError = ''; }; + #renderPasswordInput() { + if (this._isNew) { + return html` + + this.#onPasswordUpdate()}> + + + + this.#onPasswordUpdate()}> + + ${when(this._newPasswordError, () => html`

${this._newPasswordError}

`)} + `; + } + + return html` + + ${when( + this._showChangePasswordForm, + () => html` +
+ + this.#onPasswordUpdate()}> + + + this.#onPasswordUpdate()}> + + ${when(this._newPasswordError, () => html`

${this._newPasswordError}

`)} + +
+ `, + () => + html` (this._showChangePasswordForm = true)}>`, + )} +
+ `; + } + #renderLeftColumn() { if (!this._workspaceContext) return; return html`
@@ -74,39 +144,7 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement value=${this._workspaceContext.email}> - - ${when( - this._showChangePasswordForm, - () => html` -
- - this.#onPasswordUpdate()}> - - - this.#onPasswordUpdate()}> - - ${when(this._newPasswordError, () => html`

${this._newPasswordError}

`)} - -
- `, - () => - html` (this._showChangePasswordForm = true)}>`, - )} -
+ ${this.#renderPasswordInput()}
MEMBER GROUP PICKER
From fdfdf13808cb4c45d9af49807c2f99d0353fc079 Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Tue, 27 Feb 2024 12:21:19 +0100 Subject: [PATCH 028/246] add username --- .../views/member/member-workspace-view-member.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts index 38096a5407..019c1b46f9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts @@ -131,7 +131,7 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement slot="editor" name="login" label="${this.localize.term('general_login')}" - value="" + value=${this._workspaceContext.username} @input=${(e: Event) => this.#onChange('username', (e.target as HTMLInputElement).value)}>
From e75a266d55df71b2b7c5001f9632b4702dcdf52c Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Tue, 27 Feb 2024 12:22:46 +0100 Subject: [PATCH 029/246] change translation --- .../views/member/member-workspace-view-member.element.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts index 019c1b46f9..a13cc303e7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts @@ -126,11 +126,11 @@ export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implement if (!this._workspaceContext) return; return html`
- + this.#onChange('username', (e.target as HTMLInputElement).value)}> From e81b072ee8d17b881220e3a05009ce042359e27b Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Tue, 27 Feb 2024 13:22:49 +0100 Subject: [PATCH 030/246] add filter --- .../member-collection-header.element.ts | 120 ++++++++++++++++++ .../collection/member-collection.context.ts | 9 ++ .../collection/member-collection.element.ts | 8 +- .../members/member/collection/types.ts | 2 + 4 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection-header.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection-header.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection-header.element.ts new file mode 100644 index 0000000000..96899be5b5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection-header.element.ts @@ -0,0 +1,120 @@ +import { UmbMemberTypeTreeRepository } from '../../member-type/tree/member-type-tree.repository.js'; +import type { UmbMemberTypeItemModel } from '../../member-type/repository/item/types.js'; +import type { UmbMemberCollectionContext } from './member-collection.context.js'; +import { css, customElement, html, ifDefined, repeat, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UMB_DEFAULT_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; +import type { UUIBooleanInputEvent, UUICheckboxElement } from '@umbraco-cms/backoffice/external/uui'; + +// import './action/create-member-collection-action.element.js'; + +@customElement('umb-member-collection-header') +export class UmbMemberCollectionHeaderElement extends UmbLitElement { + @state() + private _contentTypes: Array = []; + + #inputTimer?: NodeJS.Timeout; + #inputTimerAmount = 300; + + #collectionContext?: UmbMemberCollectionContext; + // TODO: Should we make a collection repository for member types? + #contentTypeRepository = new UmbMemberTypeTreeRepository(this); + + @state() + private _selectedContentTypeUnique?: string; + + constructor() { + super(); + + this.consumeContext(UMB_DEFAULT_COLLECTION_CONTEXT, (instance) => { + this.#collectionContext = instance as UmbMemberCollectionContext; + }); + + this.#requestContentTypes(); + } + + async #requestContentTypes() { + const { data } = await this.#contentTypeRepository.requestRootTreeItems(); + + if (data) { + this._contentTypes = data.items.map((item) => ({ + unique: item.unique, + name: item.name, + icon: item.icon || '', + entityType: item.entityType, + })); + } + } + + get #getContentTypeFilterLabel() { + if (!this._selectedContentTypeUnique) return this.localize.term('general_all'); + + return ( + this._contentTypes.find((type) => type.unique === this._selectedContentTypeUnique)?.name || + this.localize.term('general_all') + ); + } + + #onSearch(event: InputEvent) { + const target = event.target as HTMLInputElement; + const filter = target.value || ''; + clearTimeout(this.#inputTimer); + this.#inputTimer = setTimeout(() => this.#collectionContext?.setFilter({ filter }), this.#inputTimerAmount); + } + + #onContentTypeFilterChange(contentTypeUnique: string) { + this._selectedContentTypeUnique = contentTypeUnique; + this.#collectionContext?.setMemberTypeFilter(contentTypeUnique); + } + + render() { + return html` + + ${this.#renderContentTypeFilter()} `; + } + + #renderContentTypeFilter() { + return html` + + ${this.#getContentTypeFilterLabel} + + + `; + } + static styles = [ + css` + #dropdown-layout { + display: flex; + flex-direction: column; + --uui-button-content-align: left; + } + `, + ]; +} + +export default UmbMemberCollectionHeaderElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-collection-header': UmbMemberCollectionHeaderElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.context.ts index ab01ec035b..20d8614c73 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.context.ts @@ -11,6 +11,15 @@ export class UmbMemberCollectionContext extends UmbDefaultCollectionContext< constructor(host: UmbControllerHostElement) { super(host, UMB_MEMBER_TABLE_COLLECTION_VIEW_ALIAS); } + + /** + * Sets the member type filter for the collection and refreshes the collection. + * @param {Array} selection + * @memberof UmbMemberCollectionContext + */ + setMemberTypeFilter(selection: string) { + this.setFilter({ memberTypeId: selection }); + } } export default UmbMemberCollectionContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.element.ts index 922295c29c..7abebe8305 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.element.ts @@ -1,13 +1,13 @@ import { html, customElement } from '@umbraco-cms/backoffice/external/lit'; import { UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection'; -// import './member-collection-header.element.js'; +import './member-collection-header.element.js'; @customElement('umb-member-collection') export class UmbMemberCollectionElement extends UmbCollectionDefaultElement { - // protected renderToolbar() { - // return html` `; - // } + protected renderToolbar() { + return html` `; + } } export default UmbMemberCollectionElement; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/types.ts index ff3ffe3bbc..bb3b83cc3b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/types.ts @@ -3,6 +3,8 @@ import type { UmbMemberEntityType } from '../entity.js'; export interface UmbMemberCollectionFilterModel { skip?: number; take?: number; + memberTypeId?: string; + filter?: string; } export interface UmbMemberCollectionModel { From d156f65cd977016de7f13fa6d31b66311a4ec231 Mon Sep 17 00:00:00 2001 From: JesmoDev Date: Wed, 28 Feb 2024 17:37:03 +0100 Subject: [PATCH 031/246] styling --- ...create-member-collection-action.element.ts | 4 +-- .../member-collection-header.element.ts | 25 ++++++++++++++----- .../member-table-collection-view.element.ts | 2 +- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts index c09114ee47..dc13c0cfcf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts @@ -8,14 +8,14 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement { @state() private _options: Array<{ label: string; unique: string; icon: string }> = []; - #memberTypeCollectionRepository = new UmbMemberTypeTreeRepository(this); + #memberTypeTreeRepository = new UmbMemberTypeTreeRepository(this); #optionRequestPromise?: Promise; async #getOptions() { //TODO: Should we use the tree repository or make a collection repository? //TODO: And how would we get all the member types? //TODO: This only works because member types can't have folders. - const { data } = await this.#memberTypeCollectionRepository.requestRootTreeItems(); + const { data } = await this.#memberTypeTreeRepository.requestRootTreeItems(); if (!data) return; this._options = data.items.map((item) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection-header.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection-header.element.ts index 96899be5b5..5fe826742e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection-header.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection-header.element.ts @@ -4,9 +4,6 @@ import type { UmbMemberCollectionContext } from './member-collection.context.js' import { css, customElement, html, ifDefined, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UMB_DEFAULT_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; -import type { UUIBooleanInputEvent, UUICheckboxElement } from '@umbraco-cms/backoffice/external/uui'; - -// import './action/create-member-collection-action.element.js'; @customElement('umb-member-collection-header') export class UmbMemberCollectionHeaderElement extends UmbLitElement { @@ -47,7 +44,7 @@ export class UmbMemberCollectionHeaderElement extends UmbLitElement { } get #getContentTypeFilterLabel() { - if (!this._selectedContentTypeUnique) return this.localize.term('general_all'); + if (!this._selectedContentTypeUnique) return this.localize.term('general_all') + ' Member types'; return ( this._contentTypes.find((type) => type.unique === this._selectedContentTypeUnique)?.name || @@ -71,8 +68,8 @@ export class UmbMemberCollectionHeaderElement extends UmbLitElement { return html` ${this.#renderContentTypeFilter()} `; } @@ -84,6 +81,7 @@ export class UmbMemberCollectionHeaderElement extends UmbLitElement {