Merge remote-tracking branch 'origin/main' into feature/document-variants
# Conflicts: # src/backoffice/backoffice.element.ts # src/backoffice/documents/documents/workspace/document-workspace.context.ts # src/backoffice/settings/languages/language-picker/language-picker-modal-layout.element.ts # src/backoffice/settings/languages/workspace/language/language-workspace.context.ts # src/backoffice/settings/languages/workspace/language/views/edit/edit-language-workspace-view.element.ts # src/backoffice/shared/components/index.ts # src/backoffice/shared/components/input-culture-select/input-culture-select.element.ts # src/backoffice/shared/components/input-language-picker/input-language-picker.element.ts # src/backoffice/shared/components/section/section-sidebar/section-sidebar.element.ts # src/backoffice/shared/components/section/section.element.ts # src/backoffice/shared/components/workspace/workspace-context/workspace-entity-context.interface.ts # src/backoffice/shared/workspace-actions/save.action.ts # src/backoffice/translation/dashboards/dictionary/dashboard-translation-dictionary.element.ts # src/backoffice/translation/dictionary/entity-actions/create/create.action.ts # src/backoffice/users/users/workspace/user-workspace.context.ts # src/core/mocks/data/languages.data.ts # src/core/mocks/domains/language.handlers.ts # src/core/modal/layouts/modal-layout-picker-base.ts # src/core/modal/modal.service.ts
This commit is contained in:
@@ -11,21 +11,15 @@ import { html } from 'lit-html';
|
||||
import { initialize, mswDecorator } from 'msw-storybook-addon';
|
||||
import { setCustomElements } from '@storybook/web-components';
|
||||
|
||||
import {
|
||||
UMB_DATA_TYPE_STORE_CONTEXT_TOKEN,
|
||||
UmbDataTypeStore,
|
||||
} from '../src/backoffice/settings/data-types/repository/data-type.store.ts';
|
||||
import {
|
||||
UMB_DOCUMENT_TYPE_STORE_CONTEXT_TOKEN,
|
||||
UmbDocumentTypeStore,
|
||||
} from '../src/backoffice/documents/document-types/repository/document-type.store.ts';
|
||||
import { UmbDataTypeStore } from '../src/backoffice/settings/data-types/repository/data-type.store.ts';
|
||||
import { UmbDocumentTypeStore } from '../src/backoffice/documents/document-types/repository/document-type.store.ts';
|
||||
|
||||
import customElementManifests from '../custom-elements.json';
|
||||
import { UmbIconStore } from '../libs/store/icon/icon.store';
|
||||
import { onUnhandledRequest } from '../src/core/mocks/browser';
|
||||
import { handlers } from '../src/core/mocks/browser-handlers';
|
||||
import { LitElement } from 'lit';
|
||||
import { UmbModalService } from '../src/core/modal';
|
||||
import { UMB_MODAL_SERVICE_CONTEXT_TOKEN, UmbModalService } from '../src/core/modal';
|
||||
|
||||
// TODO: Fix storybook manifest registrations.
|
||||
|
||||
@@ -71,7 +65,10 @@ const documentTypeStoreProvider = (story) => html`
|
||||
`;
|
||||
|
||||
const modalServiceProvider = (story) => html`
|
||||
<umb-context-provider style="display: block; padding: 32px;" key="umbModalService" .value=${new UmbModalService()}>
|
||||
<umb-context-provider
|
||||
style="display: block; padding: 32px;"
|
||||
key="${UMB_MODAL_SERVICE_CONTEXT_TOKEN}"
|
||||
.value=${new UmbModalService()}>
|
||||
${story()}
|
||||
<umb-backoffice-modal-container></umb-backoffice-modal-container>
|
||||
</umb-context-provider>
|
||||
@@ -94,7 +91,7 @@ export const parameters = {
|
||||
storySort: {
|
||||
method: 'alphabetical',
|
||||
includeNames: true,
|
||||
order: ['Guides', ['Getting started'], '*']
|
||||
order: ['Guides', ['Getting started'], '*'],
|
||||
},
|
||||
},
|
||||
actions: { argTypesRegex: '^on.*' },
|
||||
|
||||
@@ -5,7 +5,7 @@ const headerApps: Array<ManifestHeaderApp> = [
|
||||
type: 'headerApp',
|
||||
alias: 'Umb.HeaderApp.Search',
|
||||
name: 'Header App Search',
|
||||
loader: () => import('src/backoffice/shared/components/header-app/header-app-button.element'),
|
||||
loader: () => import('./umb-search-header-app.element'),
|
||||
weight: 10,
|
||||
meta: {
|
||||
label: 'Search',
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { css, CSSResultGroup, html } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/modal';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
|
||||
@customElement('umb-search-header-app')
|
||||
export class UmbSearchHeaderApp extends UmbLitElement {
|
||||
static styles: CSSResultGroup = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
uui-button {
|
||||
font-size: 18px;
|
||||
--uui-button-background-color: transparent;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
private _modalService?: UmbModalService;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.consumeContext(UMB_MODAL_SERVICE_CONTEXT_TOKEN, (_instance) => {
|
||||
this._modalService = _instance;
|
||||
});
|
||||
}
|
||||
|
||||
#onSearchClick() {
|
||||
this._modalService?.search();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-button @click=${this.#onSearchClick} look="primary" label="search" compact>
|
||||
<uui-icon name="umb:search"></uui-icon>
|
||||
</uui-button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default UmbSearchHeaderApp;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-search-header-app': UmbSearchHeaderApp;
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import { customElement, state } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit-html/directives/repeat.js';
|
||||
import { UUIMenuItemElement, UUIMenuItemEvent } from '@umbraco-ui/uui';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
||||
import { UmbModalLayoutPickerBase } from '../../../../core/modal/layouts/modal-layout-picker-base';
|
||||
import { UmbLanguageRepository } from '../repository/language.repository';
|
||||
import { UmbModalLayoutPickerBase } from '../../../../core/modal/layouts/modal-layout-picker-base';
|
||||
import { LanguageModel } from '@umbraco-cms/backend-api';
|
||||
|
||||
export interface UmbLanguagePickerModalData {
|
||||
@@ -14,7 +14,7 @@ export interface UmbLanguagePickerModalData {
|
||||
}
|
||||
|
||||
@customElement('umb-language-picker-modal-layout')
|
||||
export class UmbLanguagePickerModalLayoutElement extends UmbModalLayoutPickerBase {
|
||||
export class UmbLanguagePickerModalLayoutElement extends UmbModalLayoutPickerBase<LanguageModel> {
|
||||
static styles = [UUITextStyles, css``];
|
||||
|
||||
@state()
|
||||
@@ -35,11 +35,19 @@ export class UmbLanguagePickerModalLayoutElement extends UmbModalLayoutPickerBas
|
||||
this.handleSelection(isoCode);
|
||||
}
|
||||
|
||||
get #filteredLanguages() {
|
||||
if (this.data?.filter) {
|
||||
return this._languages.filter(this.data.filter);
|
||||
} else {
|
||||
return this._languages;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<umb-body-layout headline="Select languages">
|
||||
<uui-box>
|
||||
${repeat(
|
||||
this._languages,
|
||||
this.#filteredLanguages,
|
||||
(item) => item.isoCode,
|
||||
(item) => html`
|
||||
<uui-menu-item
|
||||
|
||||
@@ -11,6 +11,10 @@ export class UmbLanguageWorkspaceContext extends UmbWorkspaceContext {
|
||||
#data = new ObjectState<LanguageModel | undefined>(undefined);
|
||||
data = this.#data.asObservable();
|
||||
|
||||
// TODO: this is a temp solution to bubble validation errors to the UI
|
||||
#validationErrors = new ObjectState<any | undefined>(undefined);
|
||||
validationErrors = this.#validationErrors.asObservable();
|
||||
|
||||
constructor(host: UmbControllerHostInterface) {
|
||||
super(host);
|
||||
this.#host = host;
|
||||
@@ -60,6 +64,12 @@ export class UmbLanguageWorkspaceContext extends UmbWorkspaceContext {
|
||||
this.#data.update({ fallbackIsoCode: isoCode });
|
||||
}
|
||||
|
||||
// TODO: this is a temp solution to bubble validation errors to the UI
|
||||
setValidationErrors(errorMap: any) {
|
||||
// TODO: I can't use the update method to set the value to undefined
|
||||
this.#validationErrors.next(errorMap);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.#data.complete();
|
||||
}
|
||||
|
||||
@@ -32,12 +32,15 @@ export class UmbEditLanguageWorkspaceViewElement extends UmbLitElement {
|
||||
#default-language-warning {
|
||||
background-color: var(--uui-color-warning);
|
||||
color: var(--uui-color-warning-contrast);
|
||||
border-color: var(--uui-color-warning-standalone);
|
||||
padding: var(--uui-size-space-4) var(--uui-size-space-5);
|
||||
border: 1px solid;
|
||||
border: 1px solid var(--uui-color-warning-standalone);
|
||||
margin-top: var(--uui-size-space-4);
|
||||
border-radius: var(--uui-border-radius);
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: var(--uui-color-danger);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -50,11 +53,16 @@ export class UmbEditLanguageWorkspaceViewElement extends UmbLitElement {
|
||||
@state()
|
||||
_isNew = false;
|
||||
|
||||
@state()
|
||||
_validationErrors?: { [key: string]: Array<any> };
|
||||
|
||||
#languageWorkspaceContext?: UmbLanguageWorkspaceContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
/* TODO: we will need some system to notify about an action has been executed.
|
||||
In the language workspace we want to clear a default language change warning and reset the initial state after a save action has been executed. */
|
||||
let initialStateSet = false;
|
||||
|
||||
this.consumeContext<UmbLanguageWorkspaceContext>('umbWorkspaceContext', (instance) => {
|
||||
@@ -75,6 +83,11 @@ export class UmbEditLanguageWorkspaceViewElement extends UmbLitElement {
|
||||
this.observe(this.#languageWorkspaceContext.isNew, (value) => {
|
||||
this._isNew = value;
|
||||
});
|
||||
|
||||
this.observe(this.#languageWorkspaceContext.validationErrors, (value) => {
|
||||
this._validationErrors = value;
|
||||
this.requestUpdate('_validationErrors');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -99,8 +112,8 @@ export class UmbEditLanguageWorkspaceViewElement extends UmbLitElement {
|
||||
|
||||
this.#languageWorkspaceContext?.setCulture(isoCode);
|
||||
|
||||
// If the language name is not set, we set it to the name of the selected language.
|
||||
if (!this._language?.name && cultureName) {
|
||||
// to improve UX, we set the name to the culture name if it's a new language
|
||||
if (this._isNew && cultureName) {
|
||||
this.#languageWorkspaceContext?.setName(cultureName);
|
||||
}
|
||||
}
|
||||
@@ -135,10 +148,16 @@ export class UmbEditLanguageWorkspaceViewElement extends UmbLitElement {
|
||||
<uui-box>
|
||||
<umb-workspace-property-layout label="Language">
|
||||
<div slot="editor">
|
||||
<!-- TODO: disable already created cultures in the select -->
|
||||
<umb-input-culture-select
|
||||
value=${ifDefined(this._language.isoCode)}
|
||||
@change=${this.#handleCultureChange}
|
||||
?readonly=${this._isNew === false}></umb-input-culture-select>
|
||||
|
||||
<!-- TEMP VALIDATION ERROR -->
|
||||
${this._validationErrors?.isoCode.map(
|
||||
(isoCodeError) => html`<div class="validation-message">${isoCodeError}</div>`
|
||||
)}
|
||||
</div>
|
||||
</umb-workspace-property-layout>
|
||||
|
||||
@@ -157,7 +176,6 @@ export class UmbEditLanguageWorkspaceViewElement extends UmbLitElement {
|
||||
<div>An Umbraco site can only have one default language set.</div>
|
||||
</div>
|
||||
</uui-toggle>
|
||||
|
||||
<!-- TODO: we need a UUI component for this -->
|
||||
${this._language.isDefault !== this._isDefaultLanguage
|
||||
? html`<div id="default-language-warning">
|
||||
@@ -182,7 +200,9 @@ export class UmbEditLanguageWorkspaceViewElement extends UmbLitElement {
|
||||
value=${ifDefined(this._language.fallbackIsoCode === null ? undefined : this._language.fallbackIsoCode)}
|
||||
slot="editor"
|
||||
max="1"
|
||||
@change=${this.#handleFallbackChange}></umb-input-language-picker>
|
||||
@change=${this.#handleFallbackChange}
|
||||
.filter=${(language: LanguageModel) =>
|
||||
language.isoCode !== this._language?.isoCode}></umb-input-language-picker>
|
||||
</umb-workspace-property-layout>
|
||||
</uui-box>
|
||||
`;
|
||||
|
||||
@@ -8,6 +8,7 @@ import './backoffice-frame/backoffice-main.element';
|
||||
import './backoffice-frame/backoffice-modal-container.element';
|
||||
import './backoffice-frame/backoffice-notification-container.element';
|
||||
import './code-block/code-block.element';
|
||||
import './debug/debug.element';
|
||||
import './dropdown/dropdown.element';
|
||||
import './empty-state/empty-state.element';
|
||||
import './extension-slot/extension-slot.element';
|
||||
@@ -31,5 +32,3 @@ import './workspace/workspace-action-menu/workspace-action-menu.element';
|
||||
import './workspace/workspace-action/workspace-action.element';
|
||||
import './workspace/workspace-content/workspace-content.element';
|
||||
import './workspace/workspace-layout/workspace-layout.element';
|
||||
|
||||
import './debug/debug.element';
|
||||
|
||||
@@ -5,8 +5,8 @@ import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { UUIComboboxElement, UUIComboboxEvent } from '@umbraco-ui/uui';
|
||||
import { UmbCultureRepository } from '../../../settings/cultures/repository/culture.repository';
|
||||
import { UmbChangeEvent } from '@umbraco-cms/events';
|
||||
import { UmbCultureRepository } from '../../../settings/cultures/repository/culture.repository';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
import { CultureModel } from '@umbraco-cms/backend-api';
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
||||
import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins';
|
||||
import { UmbChangeEvent } from '@umbraco-cms/events';
|
||||
import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../../../../core/modal';
|
||||
import { UmbLanguageRepository } from '../../../settings/languages/repository/language.repository';
|
||||
import { UmbChangeEvent } from '@umbraco-cms/events';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
import type { LanguageModel } from '@umbraco-cms/backend-api';
|
||||
import type { UmbObserverController } from '@umbraco-cms/observable-api';
|
||||
@@ -56,6 +56,9 @@ export class UmbInputLanguagePickerElement extends FormControlMixin(UmbLitElemen
|
||||
@property({ type: String, attribute: 'min-message' })
|
||||
maxMessage = 'This field exceeds the allowed amount of items';
|
||||
|
||||
@property({ type: Object, attribute: false })
|
||||
public filter: (language: LanguageModel) => boolean = () => true;
|
||||
|
||||
private _selectedIsoCodes: Array<string> = [];
|
||||
public get selectedIsoCodes(): Array<string> {
|
||||
return this._selectedIsoCodes;
|
||||
@@ -116,17 +119,15 @@ export class UmbInputLanguagePickerElement extends FormControlMixin(UmbLitElemen
|
||||
}
|
||||
|
||||
private _openPicker() {
|
||||
/*
|
||||
TODO: re implement when language picker PR is merged
|
||||
const modalHandler = this._modalService?.languagePicker({
|
||||
multiple: this.max === 1 ? false : true,
|
||||
selection: [...this._selectedIsoCodes],
|
||||
filter: this.filter,
|
||||
});
|
||||
|
||||
modalHandler?.onClose().then(({ selection }: any) => {
|
||||
this._setSelection(selection);
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
private _removeItem(item: LanguageModel) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { html } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar';
|
||||
import { UmbPickerData } from '../../../../core/modal/layouts/modal-layout-picker-base';
|
||||
import { UmbPickerModalData } from '../../../../core/modal/layouts/modal-layout-picker-base';
|
||||
import { UmbModalService, UmbModalType, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../../../../core/modal';
|
||||
|
||||
//TODO: These should probably be imported dynamically.
|
||||
@@ -45,7 +45,7 @@ export class UmbInputListBase extends UmbLitElement {
|
||||
selection: this.value,
|
||||
},
|
||||
});
|
||||
modalHandler?.onClose().then((data: UmbPickerData<string>) => {
|
||||
modalHandler?.onClose().then((data: UmbPickerModalData<string>) => {
|
||||
if (data) {
|
||||
this.value = data.selection;
|
||||
this.selectionUpdated();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
||||
import { css, html } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { UmbSectionSidebarContext, UMB_SECTION_SIDEBAR_CONTEXT_TOKEN } from './section-sidebar.context';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
|
||||
import '../../tree/context-menu/tree-context-menu.service';
|
||||
import '../section-sidebar-context-menu/section-sidebar-context-menu.element';
|
||||
import { UmbSectionSidebarContext, UMB_SECTION_SIDEBAR_CONTEXT_TOKEN } from './section-sidebar.context';
|
||||
import { UmbLitElement } from '@umbraco-cms/element';
|
||||
|
||||
@customElement('umb-section-sidebar')
|
||||
export class UmbSectionSidebarElement extends UmbLitElement {
|
||||
@@ -22,6 +22,11 @@ export class UmbSectionSidebarElement extends UmbLitElement {
|
||||
flex-direction: column;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#scroll-container {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -35,7 +40,7 @@ export class UmbSectionSidebarElement extends UmbLitElement {
|
||||
render() {
|
||||
return html`
|
||||
<umb-section-sidebar-context-menu>
|
||||
<uui-scroll-container>
|
||||
<uui-scroll-container id="scroll-container">
|
||||
<slot></slot>
|
||||
</uui-scroll-container>
|
||||
</umb-section-sidebar-context-menu>
|
||||
|
||||
@@ -45,6 +45,12 @@ export class UmbSectionElement extends UmbLitElement {
|
||||
@state()
|
||||
private _views?: Array<ManifestSectionView>;
|
||||
|
||||
@state()
|
||||
private _sectionLabel = '';
|
||||
|
||||
@state()
|
||||
private _sectionPathname = '';
|
||||
|
||||
private _workspaces?: Array<ManifestWorkspace>;
|
||||
private _sectionContext?: UmbSectionContext;
|
||||
private _sectionAlias?: string;
|
||||
@@ -194,6 +200,8 @@ export class UmbSectionElement extends UmbLitElement {
|
||||
${this._menus && this._menus.length > 0
|
||||
? html`
|
||||
<umb-section-sidebar>
|
||||
<!-- TODO: this should be an extension point and only shown in the content section sidebar -->
|
||||
<umb-app-language-select></umb-app-language-select>
|
||||
<umb-extension-slot
|
||||
type="sidebarMenu"
|
||||
.filter=${(items: ManifestSidebarMenu) => items.meta.sections.includes(this._sectionAlias || '')}
|
||||
|
||||
@@ -7,4 +7,6 @@ export interface UmbWorkspaceContextInterface<T = unknown> {
|
||||
getEntityType(): string;
|
||||
getData(): T;
|
||||
destroy(): void;
|
||||
// TODO: temp solution to bubble validation errors to the UI
|
||||
setValidationErrors?(errorMap: any): void;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ export class UmbSaveWorkspaceAction extends UmbWorkspaceAction<any, UmbWorkspace
|
||||
super(host, repositoryAlias);
|
||||
}
|
||||
|
||||
/* TODO: we need a solution for all actions to notify the system that is has been executed.
|
||||
There might be cases where we need to do something after the action has been executed.
|
||||
Ex. "reset" a workspace after a save action has been executed.
|
||||
*/
|
||||
async execute() {
|
||||
if (!this.workspaceContext) return;
|
||||
// TODO: it doesn't get the updated value
|
||||
@@ -15,11 +19,28 @@ export class UmbSaveWorkspaceAction extends UmbWorkspaceAction<any, UmbWorkspace
|
||||
// TODO: handle errors
|
||||
if (!data) return;
|
||||
|
||||
if (this.workspaceContext.getIsNew()) {
|
||||
await this.repository?.create(data);
|
||||
this.workspaceContext.setIsNew(false);
|
||||
this.workspaceContext.getIsNew() ? this.#create(data) : this.#update(data);
|
||||
}
|
||||
|
||||
async #create(data: any) {
|
||||
if (!this.workspaceContext) return;
|
||||
|
||||
// TODO: preferably the actions dont talk directly with repository, but instead with its context.
|
||||
const { error } = await this.repository.create(data);
|
||||
|
||||
// TODO: this is temp solution to bubble validation errors to the UI
|
||||
if (error) {
|
||||
if (error.type === 'validation') {
|
||||
this.workspaceContext.setValidationErrors?.(error.errors);
|
||||
}
|
||||
} else {
|
||||
await this.repository?.save(data);
|
||||
this.workspaceContext.setValidationErrors?.(undefined);
|
||||
// TODO: do not make it the buttons responsibility to set the workspace to not new.
|
||||
this.workspaceContext.setIsNew(false);
|
||||
}
|
||||
}
|
||||
|
||||
#update(data: any) {
|
||||
this.repository?.save(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ export class UmbWorkspaceUserContext
|
||||
// TODO: remove this magic connection, instead create the necessary methods to update parts.
|
||||
update = this.#manager.state.update;
|
||||
|
||||
setName(name: string) {
|
||||
this.#manager.state.update({ name: name });
|
||||
}
|
||||
getEntityType = this.#manager.getEntityType;
|
||||
getUnique = this.#manager.getEntityKey;
|
||||
getEntityKey = this.#manager.getEntityKey;
|
||||
@@ -29,9 +32,6 @@ export class UmbWorkspaceUserContext
|
||||
getName() {
|
||||
throw new Error('getName is not implemented for UmbWorkspaceUserContext');
|
||||
}
|
||||
setName(name: string) {
|
||||
this.#manager.state.update({ name: name });
|
||||
}
|
||||
|
||||
propertyValueByAlias(alias: string) {
|
||||
throw new Error('setPropertyValue is not implemented for UmbWorkspaceUserContext');
|
||||
|
||||
@@ -16,6 +16,16 @@ class UmbLanguagesData extends UmbData<LanguageModel> {
|
||||
return this.data.find((item) => item.isoCode === key);
|
||||
}
|
||||
|
||||
insert(language: LanguageModel) {
|
||||
const foundIndex = this.data.findIndex((item) => item.isoCode === language.isoCode);
|
||||
|
||||
if (foundIndex !== -1) {
|
||||
throw new Error('Language with same iso code already exists');
|
||||
}
|
||||
|
||||
this.data.push(language);
|
||||
}
|
||||
|
||||
save(saveItems: Array<LanguageModel>) {
|
||||
saveItems.forEach((saveItem) => {
|
||||
const foundIndex = this.data.findIndex((item) => item.isoCode === saveItem.isoCode);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { rest } from 'msw';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { umbLanguagesData } from '../data/languages.data';
|
||||
import { LanguageModel } from '@umbraco-cms/backend-api';
|
||||
import { LanguageModel, ProblemDetailsModel } from '@umbraco-cms/backend-api';
|
||||
import { umbracoPath } from '@umbraco-cms/utils';
|
||||
|
||||
// TODO: add schema
|
||||
@@ -36,12 +35,22 @@ export const handlers = [
|
||||
|
||||
if (!data) return;
|
||||
|
||||
data.id = umbLanguagesData.getAll().length + 1;
|
||||
data.key = uuidv4();
|
||||
|
||||
umbLanguagesData.save([data]);
|
||||
|
||||
return res(ctx.status(201));
|
||||
try {
|
||||
umbLanguagesData.insert(data);
|
||||
return res(ctx.status(201));
|
||||
} catch (error) {
|
||||
return res(
|
||||
ctx.status(400),
|
||||
ctx.json<ProblemDetailsModel>({
|
||||
status: 400,
|
||||
type: 'validation',
|
||||
detail: 'Something went wrong',
|
||||
errors: {
|
||||
isoCode: ['Language with same iso code already exists'],
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}),
|
||||
|
||||
rest.put<LanguageModel>(umbracoPath('/language/:key'), async (req, res, ctx) => {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { UmbModalLayoutElement } from '..';
|
||||
|
||||
export interface UmbPickerData<selectType = string> {
|
||||
export interface UmbPickerModalData<T> {
|
||||
multiple: boolean;
|
||||
selection: Array<selectType>;
|
||||
selection: Array<string>;
|
||||
filter?: (language: T) => boolean;
|
||||
}
|
||||
|
||||
// TODO: we should consider moving this into a class/context instead of an element.
|
||||
// So we don't have to extend an element to get basic picker/selection logic
|
||||
export class UmbModalLayoutPickerBase<selectType = string> extends UmbModalLayoutElement<UmbPickerData<selectType>> {
|
||||
export class UmbModalLayoutPickerBase<T> extends UmbModalLayoutElement<UmbPickerModalData<T>> {
|
||||
@property()
|
||||
selection: Array<selectType> = [];
|
||||
selection: Array<string> = [];
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
@@ -25,14 +26,14 @@ export class UmbModalLayoutPickerBase<selectType = string> extends UmbModalLayou
|
||||
this.modalHandler?.close();
|
||||
}
|
||||
|
||||
protected _handleKeydown(e: KeyboardEvent, key: selectType) {
|
||||
protected _handleKeydown(e: KeyboardEvent, key: string) {
|
||||
if (e.key === 'Enter') {
|
||||
this.handleSelection(key);
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO: Write test for this select/deselect method. */
|
||||
handleSelection(key: selectType) {
|
||||
handleSelection(key: string) {
|
||||
if (this.data?.multiple) {
|
||||
if (this.isSelected(key)) {
|
||||
this.selection = this.selection.filter((selectedKey) => selectedKey !== key);
|
||||
@@ -46,7 +47,7 @@ export class UmbModalLayoutPickerBase<selectType = string> extends UmbModalLayou
|
||||
this.requestUpdate('_selection');
|
||||
}
|
||||
|
||||
isSelected(key: selectType): boolean {
|
||||
isSelected(key: string): boolean {
|
||||
return this.selection.includes(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api';
|
||||
import type { ManifestSection } from '@umbraco-cms/models';
|
||||
|
||||
@customElement('umb-picker-layout-section')
|
||||
export class UmbPickerLayoutSectionElement extends UmbModalLayoutPickerBase {
|
||||
export class UmbPickerLayoutSectionElement extends UmbModalLayoutPickerBase<ManifestSection> {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { UmbUserGroupStore } from '../../../../backoffice/users/user-groups
|
||||
import type { UserGroupDetails } from '@umbraco-cms/models';
|
||||
|
||||
@customElement('umb-picker-layout-user-group')
|
||||
export class UmbPickerLayoutUserGroupElement extends UmbModalLayoutPickerBase {
|
||||
export class UmbPickerLayoutUserGroupElement extends UmbModalLayoutPickerBase<UserGroupDetails> {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
|
||||
@@ -6,7 +6,7 @@ import { UmbUserStore, UMB_USER_STORE_CONTEXT_TOKEN } from '../../../../backoffi
|
||||
import type { UserDetails } from '@umbraco-cms/models';
|
||||
|
||||
@customElement('umb-picker-layout-user')
|
||||
export class UmbPickerLayoutUserElement extends UmbModalLayoutPickerBase {
|
||||
export class UmbPickerLayoutUserElement extends UmbModalLayoutPickerBase<UserDetails> {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
import { UUITextStyles } from '@umbraco-ui/uui-css';
|
||||
import { css, html, LitElement, nothing } from 'lit';
|
||||
import { repeat } from 'lit-html/directives/repeat.js';
|
||||
import { customElement, query, state } from 'lit/decorators.js';
|
||||
|
||||
export type SearchItem = {
|
||||
name: string;
|
||||
icon?: string;
|
||||
href: string;
|
||||
parent: string;
|
||||
url?: string;
|
||||
};
|
||||
export type SearchGroupItem = {
|
||||
name: string;
|
||||
items: Array<SearchItem>;
|
||||
};
|
||||
@customElement('umb-modal-layout-search')
|
||||
export class UmbModalLayoutSearchElement extends LitElement {
|
||||
static styles = [
|
||||
UUITextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--uui-color-background);
|
||||
box-sizing: border-box;
|
||||
color: var(--uui-color-text);
|
||||
font-size: 1rem;
|
||||
}
|
||||
input {
|
||||
all: unset;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
#search-icon,
|
||||
#close-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
aspect-ratio: 1;
|
||||
height: 100%;
|
||||
}
|
||||
#close-icon {
|
||||
padding: 0 var(--uui-size-space-4);
|
||||
}
|
||||
#close-icon > button {
|
||||
background: var(--uui-color-surface-alt);
|
||||
border: 1px solid var(--uui-color-border);
|
||||
padding: 3px 6px 4px 6px;
|
||||
line-height: 1;
|
||||
border-radius: 3px;
|
||||
color: var(--uui-color-text-alt);
|
||||
font-weight: 800;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#close-icon > button:hover {
|
||||
border-color: var(--uui-color-focus);
|
||||
color: var(--uui-color-focus);
|
||||
}
|
||||
#top {
|
||||
background-color: var(--uui-color-surface);
|
||||
display: flex;
|
||||
height: 48px;
|
||||
}
|
||||
#main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px var(--uui-size-space-6) var(--uui-size-space-5) var(--uui-size-space-6);
|
||||
height: 100%;
|
||||
border-top: 1px solid var(--uui-color-border);
|
||||
}
|
||||
.group {
|
||||
margin-top: var(--uui-size-space-4);
|
||||
}
|
||||
.group-name {
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--uui-size-space-1);
|
||||
}
|
||||
.group-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--uui-size-space-3);
|
||||
}
|
||||
.item {
|
||||
background: var(--uui-color-surface);
|
||||
border: 1px solid var(--uui-color-border);
|
||||
padding: var(--uui-size-space-3) var(--uui-size-space-4);
|
||||
border-radius: var(--uui-border-radius);
|
||||
color: var(--uui-color-interactive);
|
||||
display: grid;
|
||||
grid-template-columns: var(--uui-size-space-6) 1fr var(--uui-size-space-5);
|
||||
height: min-content;
|
||||
align-items: center;
|
||||
}
|
||||
.item:hover {
|
||||
background-color: var(--uui-color-surface-emphasis);
|
||||
color: var(--uui-color-interactive-emphasis);
|
||||
}
|
||||
.item:hover .item-symbol {
|
||||
font-weight: unset;
|
||||
opacity: 1;
|
||||
}
|
||||
.item-icon {
|
||||
margin-bottom: auto;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.item-icon,
|
||||
.item-symbol {
|
||||
opacity: 0.4;
|
||||
}
|
||||
.item-url {
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.2;
|
||||
font-weight: 100;
|
||||
}
|
||||
.item-name {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.item-icon > * {
|
||||
height: 1rem;
|
||||
display: flex;
|
||||
width: min-content;
|
||||
}
|
||||
.item-symbol {
|
||||
font-weight: 100;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
#no-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin-top: var(--uui-size-space-5);
|
||||
color: var(--uui-color-text-alt);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@query('input')
|
||||
private _input!: HTMLInputElement;
|
||||
|
||||
@state()
|
||||
private _search = '';
|
||||
|
||||
@state()
|
||||
private _groups: Array<SearchGroupItem> = [];
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this._input.focus();
|
||||
});
|
||||
}
|
||||
|
||||
#onSearchChange(event: InputEvent) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
this._search = target.value;
|
||||
|
||||
this.#updateGroups();
|
||||
}
|
||||
|
||||
#onClearSearch() {
|
||||
this._search = '';
|
||||
this._input.value = '';
|
||||
this._input.focus();
|
||||
this.#updateGroups();
|
||||
}
|
||||
|
||||
#updateGroups() {
|
||||
const filtered = this.#mockData.filter((item) => {
|
||||
return item.name.toLowerCase().includes(this._search.toLowerCase());
|
||||
});
|
||||
|
||||
const grouped: Array<SearchGroupItem> = filtered.reduce((acc, item) => {
|
||||
const group = acc.find((group) => group.name === item.parent);
|
||||
if (group) {
|
||||
group.items.push(item);
|
||||
} else {
|
||||
acc.push({
|
||||
name: item.parent,
|
||||
items: [item],
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, [] as Array<SearchGroupItem>);
|
||||
|
||||
this._groups = grouped;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div id="top">
|
||||
<div id="search-icon">
|
||||
<uui-icon name="search"></uui-icon>
|
||||
</div>
|
||||
<input
|
||||
value=${this._search}
|
||||
@input=${this.#onSearchChange}
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
autocomplete="off" />
|
||||
<div id="close-icon">
|
||||
<button @click=${this.#onClearSearch}>clear</button>
|
||||
</div>
|
||||
</div>
|
||||
${this._search
|
||||
? html`<div id="main">
|
||||
${this._groups.length > 0
|
||||
? repeat(
|
||||
this._groups,
|
||||
(group) => group.name,
|
||||
(group) => this.#renderGroup(group.name, group.items)
|
||||
)
|
||||
: html`<div id="no-results">Only mock data for now <strong>Search for blog</strong></div>`}
|
||||
</div>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
#renderGroup(name: string, items: Array<SearchItem>) {
|
||||
return html`
|
||||
<div class="group">
|
||||
<div class="group-name">${name}</div>
|
||||
<div class="group-items">${repeat(items, (item) => item.name, this.#renderItem.bind(this))}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderItem(item: SearchItem) {
|
||||
return html`
|
||||
<a href="${item.href}" class="item">
|
||||
<span class="item-icon">
|
||||
${item.icon ? html`<uui-icon name="${item.icon}"></uui-icon>` : this.#renderHashTag()}
|
||||
</span>
|
||||
<span class="item-name">
|
||||
${item.name} ${item.url ? html`<span class="item-url">${item.url}</span>` : nothing}
|
||||
</span>
|
||||
<span class="item-symbol">></span>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderHashTag() {
|
||||
return html`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7.784 14l.42-4H4V8h4.415l.525-5h2.011l-.525 5h3.989l.525-5h2.011l-.525 5H20v2h-3.784l-.42 4H20v2h-4.415l-.525 5h-2.011l.525-5H9.585l-.525 5H7.049l.525-5H4v-2h3.784zm2.011 0h3.99l.42-4h-3.99l-.42 4z" />
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
#mockData: Array<SearchItem> = [
|
||||
{
|
||||
name: 'Blog',
|
||||
href: '#',
|
||||
icon: 'umb:thumbnail-list',
|
||||
parent: 'Content',
|
||||
url: '/blog/',
|
||||
},
|
||||
{
|
||||
name: 'Popular blogs',
|
||||
href: '#',
|
||||
icon: 'umb:article',
|
||||
parent: 'Content',
|
||||
url: '/blog/popular-blogs/',
|
||||
},
|
||||
{
|
||||
name: 'How to write a blog',
|
||||
href: '#',
|
||||
icon: 'umb:article',
|
||||
parent: 'Content',
|
||||
url: '/blog/how-to-write-a-blog/',
|
||||
},
|
||||
{
|
||||
name: 'Blog hero',
|
||||
href: '#',
|
||||
icon: 'umb:picture',
|
||||
parent: 'Media',
|
||||
},
|
||||
{
|
||||
name: 'Contact form for blog',
|
||||
href: '#',
|
||||
parent: 'Document Types',
|
||||
},
|
||||
{
|
||||
name: 'Blog',
|
||||
href: '#',
|
||||
parent: 'Document Types',
|
||||
},
|
||||
{
|
||||
name: 'Blog link item',
|
||||
href: '#',
|
||||
parent: 'Document Types',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default UmbModalLayoutSearchElement;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-modal-layout-search': UmbModalLayoutSearchElement;
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,12 @@ import './layouts/modal-layout-current-user.element';
|
||||
import './layouts/icon-picker/modal-layout-icon-picker.element';
|
||||
import '../../backoffice/settings/languages/language-picker/language-picker-modal-layout.element';
|
||||
import './layouts/link-picker/modal-layout-link-picker.element';
|
||||
import './layouts/basic/modal-layout-basic.element';
|
||||
import './layouts/search/modal-layout-search.element.ts';
|
||||
|
||||
import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import type { UUIModalDialogElement } from '@umbraco-ui/uui-modal-dialog';
|
||||
import { UmbModalChangePasswordData } from './layouts/modal-layout-change-password.element';
|
||||
import type { UmbModalIconPickerData } from './layouts/icon-picker/modal-layout-icon-picker.element';
|
||||
import type { UmbModalConfirmData } from './layouts/confirm/modal-layout-confirm.element';
|
||||
@@ -18,8 +21,10 @@ import type { UmbModalPropertyEditorUIPickerData } from './layouts/property-edit
|
||||
import type { UmbModalMediaPickerData } from './layouts/media-picker/modal-layout-media-picker.element';
|
||||
import type { UmbModalLinkPickerData } from './layouts/link-picker/modal-layout-link-picker.element';
|
||||
import { UmbModalHandler } from './modal-handler';
|
||||
import { UmbBasicModalData } from './layouts/basic/modal-layout-basic.element';
|
||||
import type { UmbBasicModalData } from './layouts/basic/modal-layout-basic.element';
|
||||
import { UmbPickerModalData } from './layouts/modal-layout-picker-base';
|
||||
import { UmbContextToken } from '@umbraco-cms/context-api';
|
||||
import { LanguageModel } from '@umbraco-cms/backend-api';
|
||||
|
||||
export type UmbModalType = 'dialog' | 'sidebar';
|
||||
|
||||
@@ -127,6 +132,16 @@ export class UmbModalService {
|
||||
return this.open('umb-modal-layout-change-password', { data, type: 'dialog' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a language picker sidebar modal
|
||||
* @public
|
||||
* @return {*} {UmbModalHandler}
|
||||
* @memberof UmbModalService
|
||||
*/
|
||||
public languagePicker(data: UmbPickerModalData<LanguageModel>): UmbModalHandler {
|
||||
return this.open('umb-language-picker-modal-layout', { data, type: 'sidebar' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a basic sidebar modal to display readonly information
|
||||
* @public
|
||||
@@ -141,6 +156,41 @@ export class UmbModalService {
|
||||
});
|
||||
}
|
||||
|
||||
public search(): UmbModalHandler {
|
||||
const modalHandler = new UmbModalHandler('umb-modal-layout-search');
|
||||
|
||||
//TODO START: This is a hack to get the search modal layout to look like i want it to.
|
||||
//TODO: Remove from here to END when the modal system is more flexible
|
||||
const topDistance = '50%';
|
||||
const margin = '16px';
|
||||
const maxHeight = '600px';
|
||||
const maxWidth = '500px';
|
||||
const dialog = document.createElement('dialog') as HTMLDialogElement;
|
||||
dialog.style.top = `max(${margin}, calc(${topDistance} - ${maxHeight} / 2))`;
|
||||
dialog.style.margin = '0 auto';
|
||||
dialog.style.transform = `translateY(${-maxHeight})`;
|
||||
dialog.style.maxHeight = `min(${maxHeight}, calc(100% - ${margin}px * 2))`;
|
||||
dialog.style.width = `min(${maxWidth}, calc(100vw - ${margin}))`;
|
||||
dialog.style.boxSizing = 'border-box';
|
||||
dialog.style.background = 'none';
|
||||
dialog.style.border = 'none';
|
||||
dialog.style.padding = '0';
|
||||
dialog.style.boxShadow = 'var(--uui-shadow-depth-5)';
|
||||
dialog.style.borderRadius = '9px';
|
||||
const search = document.createElement('umb-modal-layout-search');
|
||||
dialog.appendChild(search);
|
||||
requestAnimationFrame(() => {
|
||||
dialog.showModal();
|
||||
});
|
||||
modalHandler.element = dialog as unknown as UUIModalDialogElement;
|
||||
//TODO END
|
||||
|
||||
modalHandler.element.addEventListener('close-end', () => this._handleCloseEnd(modalHandler));
|
||||
|
||||
this.#modals.next([...this.#modals.getValue(), modalHandler]);
|
||||
return modalHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a modal or sidebar modal
|
||||
* @public
|
||||
|
||||
Reference in New Issue
Block a user