From 4d76f48ee420a4b27ce630927fd5f82ed33749c0 Mon Sep 17 00:00:00 2001 From: ashklianko Date: Tue, 21 May 2024 14:21:24 +0200 Subject: [PATCH 01/17] Set data-path attribute on inputs #3488 --- .../js/form/inputtype/support/InputOccurrenceView.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts index 2ad004259..885c87c8a 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts @@ -162,6 +162,10 @@ export class InputOccurrenceView } }; + const updatePathCall = setInterval(() => { + this.updateInputElDataPath(); + }, 1000); + this.onRemoved(() => { if (this.property) { this.property.unPropertyValueChanged(this.propertyValueChangedHandler); @@ -170,6 +174,8 @@ export class InputOccurrenceView if (this.inputTypeView) { this.inputTypeView.unOccurrenceValueChanged(this.occurrenceValueChangedHandler); } + + clearInterval(updatePathCall); }); this.removeButtonEl.onClicked((event: MouseEvent) => { @@ -203,4 +209,9 @@ export class InputOccurrenceView this.validationErrorBlock.setHtml(errorMessage); this.toggleClass('invalid', !!errorMessage); } + + private updateInputElDataPath(): void { + this.inputElement.getEl().setAttribute('data-path', this.getDataPath()?.toString().replace(/\./g, '/')); + } + } From 283a8adc6c53d4f98123d3fe94a85379236392a3 Mon Sep 17 00:00:00 2001 From: ashklianko Date: Tue, 18 Jun 2024 15:25:27 +0200 Subject: [PATCH 02/17] Item set: add AI Assistant icon #3596 --- .../set/itemset/FormItemSetOccurrenceView.ts | 34 +++++++++++++++++ .../js/saga/event/EnonicAiSetScopeEvent.ts | 27 +++++++++++++ .../styles/api/form/set/form-set-view.less | 38 ++++++++++++++++++- 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 src/main/resources/assets/admin/common/js/saga/event/EnonicAiSetScopeEvent.ts diff --git a/src/main/resources/assets/admin/common/js/form/set/itemset/FormItemSetOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/set/itemset/FormItemSetOccurrenceView.ts index 140310c2d..334773dd1 100644 --- a/src/main/resources/assets/admin/common/js/form/set/itemset/FormItemSetOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/set/itemset/FormItemSetOccurrenceView.ts @@ -2,6 +2,9 @@ import {FormItemSet} from './FormItemSet'; import {FormSetOccurrenceView, FormSetOccurrenceViewConfig} from '../FormSetOccurrenceView'; import {FormItem} from '../../FormItem'; import {PropertyArray} from '../../../data/PropertyArray'; +import {Button} from '../../../ui/button/Button'; +import {i18n} from '../../../util/Messages'; +import {EnonicAiSetScopeEvent} from '../../../saga/event/EnonicAiSetScopeEvent'; export class FormItemSetOccurrenceView extends FormSetOccurrenceView { @@ -10,6 +13,18 @@ export class FormItemSetOccurrenceView super('form-item-set-', config); } + protected initListeners(): void { + super.initListeners(); + + const updatePathCall = setInterval(() => { + this.updateInputElDataPath(); + }, 1000); + + this.onRemoved(() => { + clearInterval(updatePathCall); + }); + } + protected getLabelText(): string { return this.getFirstPropertyValue() || this.getFormSet().getLabel(); } @@ -46,4 +61,23 @@ export class FormItemSetOccurrenceView protected getFormItems(): FormItem[] { return this.getFormSet().getFormItems(); } + + protected layoutElements(): void { + super.layoutElements(); + this.addSagaIcon(); + } + + private addSagaIcon(): void { + const sagaIcon = new Button(); + sagaIcon.setTitle(i18n('action.saga')).addClass('icon-saga icon-sparkling').insertBeforeEl(this.moreButton); + + sagaIcon.onClicked(() => { + const dataPath = this.getEl().getAttribute('data-path'); + new EnonicAiSetScopeEvent(dataPath).fire(); + }); + } + + private updateInputElDataPath(): void { + this.getEl().setAttribute('data-path', this.getDataPath()?.toString().replace(/\./g, '/')); + } } diff --git a/src/main/resources/assets/admin/common/js/saga/event/EnonicAiSetScopeEvent.ts b/src/main/resources/assets/admin/common/js/saga/event/EnonicAiSetScopeEvent.ts new file mode 100644 index 000000000..d09765013 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/saga/event/EnonicAiSetScopeEvent.ts @@ -0,0 +1,27 @@ +import {ClassHelper} from '../../ClassHelper'; +import {Event} from '../../event/Event'; + +export class EnonicAiSetScopeEvent + extends Event { + + private readonly sourceDataPath?: string; + + constructor(dataPath?: string) { + super(); + + this.sourceDataPath = dataPath; + } + + getSourceDataPath(): string | undefined { + return this.sourceDataPath; + } + + static on(handler: (event: EnonicAiSetScopeEvent) => void) { + Event.bind(ClassHelper.getFullName(this), handler); + } + + static un(handler?: (event: EnonicAiSetScopeEvent) => void) { + Event.unbind(ClassHelper.getFullName(this), handler); + } + +} diff --git a/src/main/resources/assets/admin/common/styles/api/form/set/form-set-view.less b/src/main/resources/assets/admin/common/styles/api/form/set/form-set-view.less index c587a36ec..5ec9d3ee1 100644 --- a/src/main/resources/assets/admin/common/styles/api/form/set/form-set-view.less +++ b/src/main/resources/assets/admin/common/styles/api/form/set/form-set-view.less @@ -70,8 +70,11 @@ > .form-item-set-occurrence-view, > .form-option-set-occurrence-view { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: 1fr 26px 26px; + grid-template-areas: + 'drag saga more' + 'container container container'; margin: 0; box-shadow: 0 1px 2px 1px rgba(0, 0, 0, 0.2); padding: 10px 10px 10px 13px; @@ -87,6 +90,22 @@ &:focus:not(:disabled) { border: none; } + + grid-area: more; + } + + .icon-saga { + width: 26px; + height: 26px; + padding: 0; + grid-area: saga; + background-color: @admin-white; + text-align: center; + font-size: 16px; + + &:before { + color: @admin-dark-gray; + } } .form-occurrence-draggable-label { @@ -96,6 +115,8 @@ width: calc(100% - 55px); .ellipsis(); + grid-area: drag; + &.expandable { cursor: zoom-out; } @@ -135,6 +156,7 @@ > .form-option-set-occurrences-container { flex-basis: 100%; min-width: 0; + grid-area: container; } > .form-item-set-occurrences-container, @@ -159,6 +181,18 @@ } } } + + .selection-message { + grid-area: container; + } + } + + > .form-option-set-occurrence-view.single-selection { + grid-template-columns: 1fr; + + .single-selection-header { + width: auto; + } } } From 526cd6a62668927c4f363f1dbbd7c17a96f2bfc5 Mon Sep 17 00:00:00 2001 From: ashklianko Date: Mon, 24 Jun 2024 13:45:50 +0200 Subject: [PATCH 03/17] Add Saga support for option-set #3597 - Adding data-path attribute for occurrences in: a) text-based inputs b) Form Item Sets c) Form option sets d) Form option set options Refactored classes a bit to store data-path related logic in one place --- .../common/js/form/FormItemOccurrenceView.ts | 50 +++++++++++++++-- .../support/BaseInputTypeNotManagingAdd.ts | 4 ++ .../inputtype/support/InputOccurrenceView.ts | 55 +++++++++++-------- .../js/form/inputtype/text/TextInputType.ts | 4 ++ .../js/form/set/FormSetOccurrenceView.ts | 54 ++++++++++++------ .../common/js/form/set/FormSetOccurrences.ts | 2 +- .../set/itemset/FormItemSetOccurrenceView.ts | 34 ------------ .../set/optionset/FormOptionSetOptionView.ts | 27 +++++++-- .../assets/admin/common/js/saga/SagaHelper.ts | 4 ++ .../js/saga/event/EnonicAiSetScopeEvent.ts | 27 --------- .../styles/api/form/set/form-set-view.less | 38 +------------ 11 files changed, 151 insertions(+), 148 deletions(-) create mode 100644 src/main/resources/assets/admin/common/js/saga/SagaHelper.ts delete mode 100644 src/main/resources/assets/admin/common/js/saga/event/EnonicAiSetScopeEvent.ts diff --git a/src/main/resources/assets/admin/common/js/form/FormItemOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/FormItemOccurrenceView.ts index 19fb82bfc..2b6506f46 100644 --- a/src/main/resources/assets/admin/common/js/form/FormItemOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/FormItemOccurrenceView.ts @@ -1,22 +1,54 @@ import * as Q from 'q'; import {DivEl} from '../dom/DivEl'; import {PropertyPath} from '../data/PropertyPath'; -import {InputValidationRecording} from './inputtype/InputValidationRecording'; import {FormItemOccurrence} from './FormItemOccurrence'; import {HelpTextContainer} from './HelpTextContainer'; import {RemoveButtonClickedEvent} from './RemoveButtonClickedEvent'; +import {SagaHelper} from '../saga/SagaHelper'; +import {Element} from '../dom/Element'; + +export interface FormItemOccurrenceViewConfig { + className: string; + formItemOccurrence: FormItemOccurrence +} export abstract class FormItemOccurrenceView extends DivEl { protected formItemOccurrence: FormItemOccurrence; protected helpText: HelpTextContainer; + protected readonly config: FormItemOccurrenceViewConfig; private removeButtonClickedListeners: ((event: RemoveButtonClickedEvent) => void)[] = []; private occurrenceChangedListeners: ((view: FormItemOccurrenceView) => void)[] = []; - constructor(className: string, formItemOccurrence: FormItemOccurrence) { - super(className); - this.formItemOccurrence = formItemOccurrence; + protected constructor(config: FormItemOccurrenceViewConfig) { + super(config.className); + + this.config = config; + + this.initElements(); + this.postInitElements(); + this.initListeners(); + } + + protected initElements(): void { + this.formItemOccurrence = this.config.formItemOccurrence; + } + + protected initListeners(): void { + if (this.isSagaEditableType()) { + const updatePathCall = setInterval(() => { + this.updateInputElDataPath(); + }, 1000); + + this.onRemoved(() => { + clearInterval(updatePathCall); + }); + } + } + + protected postInitElements() { + // } isExpandable(): boolean { @@ -110,4 +142,14 @@ export abstract class FormItemOccurrenceView setEnabled(enable: boolean) { // } + + protected updateInputElDataPath(): void { + this.getDataPathElement().getEl().setAttribute(SagaHelper.DATA_ATTR, this.getDataPath()?.toString().replace(/\./g, '/')); + } + + protected isSagaEditableType(): boolean { + return false; + } + + protected abstract getDataPathElement(): Element; } diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputTypeNotManagingAdd.ts b/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputTypeNotManagingAdd.ts index 4ccd6984a..cde84d11f 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputTypeNotManagingAdd.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputTypeNotManagingAdd.ts @@ -397,4 +397,8 @@ export abstract class BaseInputTypeNotManagingAdd } }); } + + isSagaEditable(): boolean { + return false; + } } diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts index 885c87c8a..5fbf78ec2 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts @@ -1,7 +1,7 @@ import * as Q from 'q'; import {Property} from '../../../data/Property'; import {PropertyValueChangedEvent} from '../../../data/PropertyValueChangedEvent'; -import {FormItemOccurrenceView} from '../../FormItemOccurrenceView'; +import {FormItemOccurrenceView, FormItemOccurrenceViewConfig} from '../../FormItemOccurrenceView'; import {Element} from '../../../dom/Element'; import {DivEl} from '../../../dom/DivEl'; import {Value} from '../../../data/Value'; @@ -11,11 +11,16 @@ import {BaseInputTypeNotManagingAdd} from './BaseInputTypeNotManagingAdd'; import {ButtonEl} from '../../../dom/ButtonEl'; import {OccurrenceValidationRecord} from './OccurrenceValidationRecord'; +export interface InputOccurrenceViewConfig extends FormItemOccurrenceViewConfig { + inputTypeView: BaseInputTypeNotManagingAdd; + property: Property; +} + export class InputOccurrenceView extends FormItemOccurrenceView { public static debug: boolean = false; - private inputOccurrence: InputOccurrence; + protected config: InputOccurrenceViewConfig; private property: Property; private inputTypeView: BaseInputTypeNotManagingAdd; private inputElement: Element; @@ -26,20 +31,23 @@ export class InputOccurrenceView private validationErrorBlock: DivEl; constructor(inputOccurrence: InputOccurrence, baseInputTypeView: BaseInputTypeNotManagingAdd, property: Property) { - super('input-occurrence-view', inputOccurrence); - - this.inputTypeView = baseInputTypeView; - this.inputOccurrence = inputOccurrence; - this.property = property; - - this.initElements(); - this.initListeners(); + super({ + className: 'input-occurrence-view', + formItemOccurrence: inputOccurrence, + inputTypeView: baseInputTypeView, + property: property, + } as InputOccurrenceViewConfig); this.refresh(); } - private initElements() { - this.inputElement = this.inputTypeView.createInputOccurrenceElement(this.inputOccurrence.getIndex(), this.property); + protected initElements(): void { + super.initElements(); + + this.inputTypeView = this.config.inputTypeView; + this.property = this.config.property; + + this.inputElement = this.inputTypeView.createInputOccurrenceElement(this.formItemOccurrence.getIndex(), this.property); this.dragControl = new DivEl('drag-control'); this.validationErrorBlock = new DivEl('error-block'); this.removeButtonEl = new ButtonEl(); @@ -86,13 +94,13 @@ export class InputOccurrenceView } refresh() { - if (this.inputOccurrence.oneAndOnly()) { + if (this.formItemOccurrence.oneAndOnly()) { this.addClass('single-occurrence').removeClass('multiple-occurrence'); } else { this.addClass('multiple-occurrence').removeClass('single-occurrence'); } - this.removeButtonEl.setVisible(this.inputOccurrence.isRemoveButtonRequiredStrict()); + this.removeButtonEl.setVisible(this.formItemOccurrence.isRemoveButtonRequiredStrict()); } getDataPath(): PropertyPath { @@ -100,7 +108,7 @@ export class InputOccurrenceView } getIndex(): number { - return this.inputOccurrence.getIndex(); + return this.formItemOccurrence.getIndex(); } getInputElement(): Element { @@ -131,7 +139,9 @@ export class InputOccurrenceView this.inputElement.unBlur(listener); } - private initListeners() { + protected initListeners(): void { + super.initListeners(); + let ignorePropertyChange: boolean = false; this.occurrenceValueChangedHandler = (occurrence: Element, value: Value) => { @@ -162,10 +172,6 @@ export class InputOccurrenceView } }; - const updatePathCall = setInterval(() => { - this.updateInputElDataPath(); - }, 1000); - this.onRemoved(() => { if (this.property) { this.property.unPropertyValueChanged(this.propertyValueChangedHandler); @@ -174,8 +180,6 @@ export class InputOccurrenceView if (this.inputTypeView) { this.inputTypeView.unOccurrenceValueChanged(this.occurrenceValueChangedHandler); } - - clearInterval(updatePathCall); }); this.removeButtonEl.onClicked((event: MouseEvent) => { @@ -210,8 +214,11 @@ export class InputOccurrenceView this.toggleClass('invalid', !!errorMessage); } - private updateInputElDataPath(): void { - this.inputElement.getEl().setAttribute('data-path', this.getDataPath()?.toString().replace(/\./g, '/')); + protected isSagaEditableType(): boolean { + return this.inputTypeView.isSagaEditable(); } + protected getDataPathElement(): Element { + return this.inputElement; + } } diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/text/TextInputType.ts b/src/main/resources/assets/admin/common/js/form/inputtype/text/TextInputType.ts index 5b9d4f62d..83112ebd3 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/text/TextInputType.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/text/TextInputType.ts @@ -161,4 +161,8 @@ export abstract class TextInputType return rendered; }); } + + isSagaEditable(): boolean { + return true; + } } diff --git a/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrenceView.ts index b08bda447..1ebec1106 100644 --- a/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrenceView.ts @@ -6,7 +6,7 @@ import {i18n} from '../../util/Messages'; import {Value} from '../../data/Value'; import {DivEl} from '../../dom/DivEl'; import {PropertyPath} from '../../data/PropertyPath'; -import {FormItemOccurrenceView} from '../FormItemOccurrenceView'; +import {FormItemOccurrenceView, FormItemOccurrenceViewConfig} from '../FormItemOccurrenceView'; import {FormItemView} from '../FormItemView'; import {RecordingValidityChangedEvent} from '../RecordingValidityChangedEvent'; import {FormOccurrenceDraggableLabel} from '../FormOccurrenceDraggableLabel'; @@ -34,13 +34,15 @@ import {FormItemSet} from './itemset/FormItemSet'; import {Input} from '../Input'; import {RadioButton} from '../inputtype/radiobutton/RadioButton'; import {ObjectHelper} from '../../ObjectHelper'; +import {SagaHelper} from '../../saga/SagaHelper'; +import {FormItemOccurrence} from '../FormItemOccurrence'; export interface FormSetOccurrenceViewConfig { context: FormContext; layer: FormItemLayer; - formSetOccurrence: FormSetOccurrence; + formItemOccurrence: FormItemOccurrence; formSet: FormSet; @@ -49,10 +51,14 @@ export interface FormSetOccurrenceViewConfig { dataSet: PropertySet; } +export interface FormSetOccurrenceViewConfigExtended extends FormSetOccurrenceViewConfig, FormItemOccurrenceViewConfig { + classPrefix: string; +} + export abstract class FormSetOccurrenceView extends FormItemOccurrenceView { - protected formItemViews: FormItemView[] = []; + protected formItemViews: FormItemView[]; protected validityChangedListeners: ((event: RecordingValidityChangedEvent) => void)[] = []; @@ -72,6 +78,8 @@ export abstract class FormSetOccurrenceView protected formSet: FormSet; + protected config: FormSetOccurrenceViewConfigExtended; + private dirtyFormItemViewsMap: object = {}; private deleteConfirmationMask: ConfirmationMask; @@ -87,17 +95,12 @@ export abstract class FormSetOccurrenceView private expandRequestedListeners: ((view: FormSetOccurrenceView) => void)[] = []; protected constructor(classPrefix: string, config: FormSetOccurrenceViewConfig) { - super(`${classPrefix}occurrence-view`, config.formSetOccurrence); - - this.occurrenceContainerClassName = `${classPrefix}occurrences-container`; - this.formItemOccurrence = config.formSetOccurrence; - this.formSet = config.formSet; - this.propertySet = config.dataSet; - this.formItemLayer = config.layer; + super({ + ...config, + className: `${classPrefix}occurrence-view`, + classPrefix: classPrefix, + } as FormSetOccurrenceViewConfigExtended); - this.initElements(); - this.postInitElements(); - this.initListeners(); this.layoutElements(); } @@ -141,10 +144,17 @@ export abstract class FormSetOccurrenceView this.refresh(); } - protected initElements() { + protected initElements(): void { + super.initElements(); + + this.formItemViews = []; + this.occurrenceContainerClassName = `${this.config.classPrefix}occurrences-container`; + this.formSetOccurrencesContainer = new DivEl(this.occurrenceContainerClassName); + this.formSet = this.config.formSet; + this.propertySet = this.config.dataSet; + this.formItemLayer = this.config.layer; this.moreButton = this.createMoreButton(); this.label = new FormOccurrenceDraggableLabel(); - this.formSetOccurrencesContainer = new DivEl(this.occurrenceContainerClassName); this.formSetOccurrencesContainer.setVisible(false); this.confirmDeleteAction = new Action(i18n('action.delete')).setClass('red large delete-button'); this.noAction = new Action(i18n('action.cancel')).setClass('black large'); @@ -157,7 +167,9 @@ export abstract class FormSetOccurrenceView .build(); } - protected postInitElements() { + protected postInitElements(): void { + super.postInitElements(); + this.label.setExpandable(this.isExpandable()); if (!this.isExpandable()) { @@ -166,6 +178,8 @@ export abstract class FormSetOccurrenceView } protected initListeners() { + super.initListeners(); + this.label.onClicked(() => this.setContainerVisible(!this.isContainerVisible())); this.confirmDeleteAction.onExecuted(() => { @@ -698,4 +712,12 @@ export abstract class FormSetOccurrenceView return new MoreButton([addAboveAction, addBelowAction, removeAction]); } + + protected getDataPathElement(): Element { + return this; + } + + protected isSagaEditableType(): boolean { + return true; + } } diff --git a/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrences.ts b/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrences.ts index 1505133c9..a37974b63 100644 --- a/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrences.ts +++ b/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrences.ts @@ -69,7 +69,7 @@ export class FormSetOccurrences return { context: this.context, layer: layer, - formSetOccurrence: occurrence, + formItemOccurrence: occurrence, formSet: this.formSet, parent: this.parent, dataSet: dataSet diff --git a/src/main/resources/assets/admin/common/js/form/set/itemset/FormItemSetOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/set/itemset/FormItemSetOccurrenceView.ts index 334773dd1..140310c2d 100644 --- a/src/main/resources/assets/admin/common/js/form/set/itemset/FormItemSetOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/set/itemset/FormItemSetOccurrenceView.ts @@ -2,9 +2,6 @@ import {FormItemSet} from './FormItemSet'; import {FormSetOccurrenceView, FormSetOccurrenceViewConfig} from '../FormSetOccurrenceView'; import {FormItem} from '../../FormItem'; import {PropertyArray} from '../../../data/PropertyArray'; -import {Button} from '../../../ui/button/Button'; -import {i18n} from '../../../util/Messages'; -import {EnonicAiSetScopeEvent} from '../../../saga/event/EnonicAiSetScopeEvent'; export class FormItemSetOccurrenceView extends FormSetOccurrenceView { @@ -13,18 +10,6 @@ export class FormItemSetOccurrenceView super('form-item-set-', config); } - protected initListeners(): void { - super.initListeners(); - - const updatePathCall = setInterval(() => { - this.updateInputElDataPath(); - }, 1000); - - this.onRemoved(() => { - clearInterval(updatePathCall); - }); - } - protected getLabelText(): string { return this.getFirstPropertyValue() || this.getFormSet().getLabel(); } @@ -61,23 +46,4 @@ export class FormItemSetOccurrenceView protected getFormItems(): FormItem[] { return this.getFormSet().getFormItems(); } - - protected layoutElements(): void { - super.layoutElements(); - this.addSagaIcon(); - } - - private addSagaIcon(): void { - const sagaIcon = new Button(); - sagaIcon.setTitle(i18n('action.saga')).addClass('icon-saga icon-sparkling').insertBeforeEl(this.moreButton); - - sagaIcon.onClicked(() => { - const dataPath = this.getEl().getAttribute('data-path'); - new EnonicAiSetScopeEvent(dataPath).fire(); - }); - } - - private updateInputElDataPath(): void { - this.getEl().setAttribute('data-path', this.getDataPath()?.toString().replace(/\./g, '/')); - } } diff --git a/src/main/resources/assets/admin/common/js/form/set/optionset/FormOptionSetOptionView.ts b/src/main/resources/assets/admin/common/js/form/set/optionset/FormOptionSetOptionView.ts index 1d492ffd7..62aa4abf7 100644 --- a/src/main/resources/assets/admin/common/js/form/set/optionset/FormOptionSetOptionView.ts +++ b/src/main/resources/assets/admin/common/js/form/set/optionset/FormOptionSetOptionView.ts @@ -19,6 +19,7 @@ import {RecordingValidityChangedEvent} from '../../RecordingValidityChangedEvent import {ValidationRecording} from '../../ValidationRecording'; import {CreatedFormItemLayerConfig, FormItemLayerFactory} from '../../FormItemLayerFactory'; import {FormItemState} from '../../FormItemState'; +import {SagaHelper} from '../../../saga/SagaHelper'; export interface FormOptionSetOptionViewConfig extends CreatedFormItemLayerConfig { @@ -41,7 +42,7 @@ export class FormOptionSetOptionView private formItemLayer: FormItemLayer; private selectionChangedListeners: ((isSelected: boolean) => void)[] = []; private checkbox: Checkbox; - private readonly isOptionSetExpandedByDefault: boolean; + private isOptionSetExpandedByDefault: boolean; private formItemState: FormItemState; private notificationDialog: NotificationDialog; private isSelectedInitially: boolean; @@ -54,19 +55,29 @@ export class FormOptionSetOptionView parent: config.parent } as FormItemViewConfig); - this.formOptionSetOption = config.formOptionSetOption; + this.initElements(config); + this.initListeners(); + } + private initElements(config: FormOptionSetOptionViewConfig): void { + this.formOptionSetOption = config.formOptionSetOption; this.isOptionSetExpandedByDefault = (config.formOptionSetOption.getParent() as FormOptionSet).isExpanded(); - this.formItemState = config.formItemState; - this.addClass(this.formOptionSetOption.getPath().getElements().length % 2 ? 'even' : 'odd'); - this.formItemLayer = config.layerFactory.createLayer(config); - this.notificationDialog = new NotificationDialog(i18n('notify.optionset.notempty')); } + private initListeners(): void { + const updatePathCall = setInterval(() => { + this.updateInputElDataPath(); + }, 1000); + + this.onRemoved(() => { + clearInterval(updatePathCall); + }); + } + toggleHelpText(show?: boolean) { this.formItemLayer.toggleHelpText(show); this.helpText?.toggleHelpText(show); @@ -489,4 +500,8 @@ export class FormOptionSetOptionView this.checkbox.setEnabled(false); } } + + protected updateInputElDataPath(): void { + this.getEl().setAttribute(SagaHelper.DATA_ATTR, `${this.getParent().getDataPath()?.toString().replace(/\./g, '/')}/${this.getName()}`); + } } diff --git a/src/main/resources/assets/admin/common/js/saga/SagaHelper.ts b/src/main/resources/assets/admin/common/js/saga/SagaHelper.ts new file mode 100644 index 000000000..9ead151c7 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/saga/SagaHelper.ts @@ -0,0 +1,4 @@ +export class SagaHelper { + + public static DATA_ATTR = 'data-path'; +} diff --git a/src/main/resources/assets/admin/common/js/saga/event/EnonicAiSetScopeEvent.ts b/src/main/resources/assets/admin/common/js/saga/event/EnonicAiSetScopeEvent.ts deleted file mode 100644 index d09765013..000000000 --- a/src/main/resources/assets/admin/common/js/saga/event/EnonicAiSetScopeEvent.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {ClassHelper} from '../../ClassHelper'; -import {Event} from '../../event/Event'; - -export class EnonicAiSetScopeEvent - extends Event { - - private readonly sourceDataPath?: string; - - constructor(dataPath?: string) { - super(); - - this.sourceDataPath = dataPath; - } - - getSourceDataPath(): string | undefined { - return this.sourceDataPath; - } - - static on(handler: (event: EnonicAiSetScopeEvent) => void) { - Event.bind(ClassHelper.getFullName(this), handler); - } - - static un(handler?: (event: EnonicAiSetScopeEvent) => void) { - Event.unbind(ClassHelper.getFullName(this), handler); - } - -} diff --git a/src/main/resources/assets/admin/common/styles/api/form/set/form-set-view.less b/src/main/resources/assets/admin/common/styles/api/form/set/form-set-view.less index 5ec9d3ee1..c587a36ec 100644 --- a/src/main/resources/assets/admin/common/styles/api/form/set/form-set-view.less +++ b/src/main/resources/assets/admin/common/styles/api/form/set/form-set-view.less @@ -70,11 +70,8 @@ > .form-item-set-occurrence-view, > .form-option-set-occurrence-view { - display: grid; - grid-template-columns: 1fr 26px 26px; - grid-template-areas: - 'drag saga more' - 'container container container'; + display: flex; + flex-wrap: wrap; margin: 0; box-shadow: 0 1px 2px 1px rgba(0, 0, 0, 0.2); padding: 10px 10px 10px 13px; @@ -90,22 +87,6 @@ &:focus:not(:disabled) { border: none; } - - grid-area: more; - } - - .icon-saga { - width: 26px; - height: 26px; - padding: 0; - grid-area: saga; - background-color: @admin-white; - text-align: center; - font-size: 16px; - - &:before { - color: @admin-dark-gray; - } } .form-occurrence-draggable-label { @@ -115,8 +96,6 @@ width: calc(100% - 55px); .ellipsis(); - grid-area: drag; - &.expandable { cursor: zoom-out; } @@ -156,7 +135,6 @@ > .form-option-set-occurrences-container { flex-basis: 100%; min-width: 0; - grid-area: container; } > .form-item-set-occurrences-container, @@ -181,18 +159,6 @@ } } } - - .selection-message { - grid-area: container; - } - } - - > .form-option-set-occurrence-view.single-selection { - grid-template-columns: 1fr; - - .single-selection-header { - width: auto; - } } } From 7276470f229c4c17d901464ba8c1f12b7754e2d2 Mon Sep 17 00:00:00 2001 From: ashklianko Date: Mon, 24 Jun 2024 15:03:51 +0200 Subject: [PATCH 04/17] Remove dedicated help icons over inputs #3601 --- src/main/resources/assets/admin/common/js/form/InputView.ts | 2 -- .../resources/assets/admin/common/js/form/set/FormSetHeader.ts | 1 - 2 files changed, 3 deletions(-) diff --git a/src/main/resources/assets/admin/common/js/form/InputView.ts b/src/main/resources/assets/admin/common/js/form/InputView.ts index e338e159c..c452b3647 100644 --- a/src/main/resources/assets/admin/common/js/form/InputView.ts +++ b/src/main/resources/assets/admin/common/js/form/InputView.ts @@ -84,8 +84,6 @@ export class InputView if (this.input.getHelpText()) { this.helpText = new HelpTextContainer(this.input.getHelpText()); - - this.appendChild(this.helpText.getToggler()); } if (this.input.isMaximizeUIInputWidth() === false) { diff --git a/src/main/resources/assets/admin/common/js/form/set/FormSetHeader.ts b/src/main/resources/assets/admin/common/js/form/set/FormSetHeader.ts index 13ac1829b..3e33b1ecc 100644 --- a/src/main/resources/assets/admin/common/js/form/set/FormSetHeader.ts +++ b/src/main/resources/assets/admin/common/js/form/set/FormSetHeader.ts @@ -26,7 +26,6 @@ export class FormSetHeader return super.doRender().then(rendered => { this.appendChild(this.title); if (this.helpTextContainer) { - this.prependChild(this.helpTextContainer.getToggler()); const helpTextDiv = this.helpTextContainer.getHelpText(); if (helpTextDiv) { this.appendChild(helpTextDiv); From 8dc829f742d02ff9fc7c8c69dc6bd00b6ea70f40 Mon Sep 17 00:00:00 2001 From: ashklianko Date: Tue, 25 Jun 2024 15:34:48 +0200 Subject: [PATCH 05/17] Add level navigation for item sets #332 --- .../common/js/form/FormItemOccurrenceView.ts | 19 +------- .../inputtype/support/InputOccurrenceView.ts | 29 +++++++------ .../js/form/set/FormSetOccurrenceView.ts | 7 --- .../set/optionset/FormOptionSetOptionView.ts | 23 +++------- .../assets/admin/common/js/saga/SagaHelper.ts | 43 +++++++++++++++++++ .../js/saga/event/EnonicAiOpenDialogEvent.ts | 27 ++++++++++++ .../common/js/saga/ui/SagaActionButton.ts | 29 +++++++++++++ .../support/input-occurrence-view.less | 19 ++++++++ 8 files changed, 143 insertions(+), 53 deletions(-) create mode 100644 src/main/resources/assets/admin/common/js/saga/event/EnonicAiOpenDialogEvent.ts create mode 100644 src/main/resources/assets/admin/common/js/saga/ui/SagaActionButton.ts diff --git a/src/main/resources/assets/admin/common/js/form/FormItemOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/FormItemOccurrenceView.ts index 2b6506f46..831b679b6 100644 --- a/src/main/resources/assets/admin/common/js/form/FormItemOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/FormItemOccurrenceView.ts @@ -36,15 +36,7 @@ export abstract class FormItemOccurrenceView } protected initListeners(): void { - if (this.isSagaEditableType()) { - const updatePathCall = setInterval(() => { - this.updateInputElDataPath(); - }, 1000); - - this.onRemoved(() => { - clearInterval(updatePathCall); - }); - } + // } protected postInitElements() { @@ -143,13 +135,4 @@ export abstract class FormItemOccurrenceView // } - protected updateInputElDataPath(): void { - this.getDataPathElement().getEl().setAttribute(SagaHelper.DATA_ATTR, this.getDataPath()?.toString().replace(/\./g, '/')); - } - - protected isSagaEditableType(): boolean { - return false; - } - - protected abstract getDataPathElement(): Element; } diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts index 5fbf78ec2..9e9a31608 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts @@ -10,8 +10,10 @@ import {InputOccurrence} from './InputOccurrence'; import {BaseInputTypeNotManagingAdd} from './BaseInputTypeNotManagingAdd'; import {ButtonEl} from '../../../dom/ButtonEl'; import {OccurrenceValidationRecord} from './OccurrenceValidationRecord'; +import {SagaHelper} from '../../../saga/SagaHelper'; -export interface InputOccurrenceViewConfig extends FormItemOccurrenceViewConfig { +export interface InputOccurrenceViewConfig + extends FormItemOccurrenceViewConfig { inputTypeView: BaseInputTypeNotManagingAdd; property: Property; } @@ -24,6 +26,7 @@ export class InputOccurrenceView private property: Property; private inputTypeView: BaseInputTypeNotManagingAdd; private inputElement: Element; + private inputWrapper: DivEl; private removeButtonEl: ButtonEl; private dragControl: DivEl; private propertyValueChangedHandler: (event: PropertyValueChangedEvent) => void; @@ -46,6 +49,7 @@ export class InputOccurrenceView this.inputTypeView = this.config.inputTypeView; this.property = this.config.property; + this.inputWrapper = new DivEl('input-wrapper'); this.inputElement = this.inputTypeView.createInputOccurrenceElement(this.formItemOccurrence.getIndex(), this.property); this.dragControl = new DivEl('drag-control'); @@ -56,12 +60,11 @@ export class InputOccurrenceView layout(_validate: boolean = true): Q.Promise { return super.layout(_validate).then(() => { const dataBlock: DivEl = new DivEl('data-block'); - const inputWrapper: DivEl = new DivEl('input-wrapper'); this.appendChild(dataBlock); dataBlock.appendChild(this.dragControl); - dataBlock.appendChild(inputWrapper); - inputWrapper.appendChild(this.inputElement); + dataBlock.appendChild(this.inputWrapper); + this.inputWrapper.appendChild(this.inputElement); dataBlock.appendChild(this.removeButtonEl); this.appendChild(this.validationErrorBlock); @@ -190,6 +193,16 @@ export class InputOccurrenceView }); this.property.onPropertyValueChanged(this.propertyValueChangedHandler); + + if (this.inputTypeView.isSagaEditable()) { + new SagaHelper({ + dataPathElement: this.inputElement, + getPathFunc: () => this.getDataPath(), + icon: { + parent: this.inputWrapper, + } + }); + } } private registerProperty(property: Property) { @@ -213,12 +226,4 @@ export class InputOccurrenceView this.validationErrorBlock.setHtml(errorMessage); this.toggleClass('invalid', !!errorMessage); } - - protected isSagaEditableType(): boolean { - return this.inputTypeView.isSagaEditable(); - } - - protected getDataPathElement(): Element { - return this.inputElement; - } } diff --git a/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrenceView.ts index 1ebec1106..d6b0483d3 100644 --- a/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrenceView.ts @@ -713,11 +713,4 @@ export abstract class FormSetOccurrenceView return new MoreButton([addAboveAction, addBelowAction, removeAction]); } - protected getDataPathElement(): Element { - return this; - } - - protected isSagaEditableType(): boolean { - return true; - } } diff --git a/src/main/resources/assets/admin/common/js/form/set/optionset/FormOptionSetOptionView.ts b/src/main/resources/assets/admin/common/js/form/set/optionset/FormOptionSetOptionView.ts index 62aa4abf7..fd4bfdef4 100644 --- a/src/main/resources/assets/admin/common/js/form/set/optionset/FormOptionSetOptionView.ts +++ b/src/main/resources/assets/admin/common/js/form/set/optionset/FormOptionSetOptionView.ts @@ -20,6 +20,7 @@ import {ValidationRecording} from '../../ValidationRecording'; import {CreatedFormItemLayerConfig, FormItemLayerFactory} from '../../FormItemLayerFactory'; import {FormItemState} from '../../FormItemState'; import {SagaHelper} from '../../../saga/SagaHelper'; +import {PropertyPath, PropertyPathElement} from '../../../data/PropertyPath'; export interface FormOptionSetOptionViewConfig extends CreatedFormItemLayerConfig { @@ -56,7 +57,6 @@ export class FormOptionSetOptionView } as FormItemViewConfig); this.initElements(config); - this.initListeners(); } private initElements(config: FormOptionSetOptionViewConfig): void { @@ -66,15 +66,10 @@ export class FormOptionSetOptionView this.addClass(this.formOptionSetOption.getPath().getElements().length % 2 ? 'even' : 'odd'); this.formItemLayer = config.layerFactory.createLayer(config); this.notificationDialog = new NotificationDialog(i18n('notify.optionset.notempty')); - } - - private initListeners(): void { - const updatePathCall = setInterval(() => { - this.updateInputElDataPath(); - }, 1000); - this.onRemoved(() => { - clearInterval(updatePathCall); + new SagaHelper({ + dataPathElement: this, + getPathFunc: () => PropertyPath.fromParent(this.getParent().getDataPath(), new PropertyPathElement(this.getName(), 0)), }); } @@ -106,9 +101,9 @@ export class FormOptionSetOptionView const optionItemsPropertySet: PropertySet = this.parent.getOrPopulateOptionItemsPropertySet(this.getName()); - if (isDefaultAndNew) { - this.parent.handleOptionSelected(this); - } + if (isDefaultAndNew) { + this.parent.handleOptionSelected(this); + } this.optionItemsContainer = new DivEl('option-items-container'); const isContainerVisibleByDefault = this.isOptionSetExpandedByDefault || isSingleSelection || this.isSelected(); @@ -500,8 +495,4 @@ export class FormOptionSetOptionView this.checkbox.setEnabled(false); } } - - protected updateInputElDataPath(): void { - this.getEl().setAttribute(SagaHelper.DATA_ATTR, `${this.getParent().getDataPath()?.toString().replace(/\./g, '/')}/${this.getName()}`); - } } diff --git a/src/main/resources/assets/admin/common/js/saga/SagaHelper.ts b/src/main/resources/assets/admin/common/js/saga/SagaHelper.ts index 9ead151c7..bf6b0b077 100644 --- a/src/main/resources/assets/admin/common/js/saga/SagaHelper.ts +++ b/src/main/resources/assets/admin/common/js/saga/SagaHelper.ts @@ -1,4 +1,47 @@ +import {PropertyPath} from '../data/PropertyPath'; +import {Element} from '../dom/Element'; +import {SagaActionButton} from './ui/SagaActionButton'; + +export interface SagaHelperConfig { + dataPathElement: Element; + getPathFunc: () => PropertyPath; + icon?: { + parent: Element; + }; +} + export class SagaHelper { public static DATA_ATTR = 'data-path'; + + private readonly config: SagaHelperConfig; + + private readonly sagaIcon?: SagaActionButton; + + constructor(config: SagaHelperConfig) { + this.config = config; + + const updatePathCall = setInterval(() => { + this.updateInputElDataPath(); + }, 1000); + + this.config.dataPathElement.onRemoved(() => { + clearInterval(updatePathCall); + }); + + if (config.icon?.parent) { + this.sagaIcon = new SagaActionButton(); + this.config.icon.parent.appendChild(this.sagaIcon); + } + } + + private updateInputElDataPath(): void { + const dataPath = SagaHelper.convertToPath(this.config.getPathFunc()); + this.config.dataPathElement.getEl().setAttribute(SagaHelper.DATA_ATTR, dataPath); + this.sagaIcon?.setDataPath(dataPath); + } + + public static convertToPath(path: PropertyPath): string { + return path?.toString().replace(/\./g, '/') || ''; + } } diff --git a/src/main/resources/assets/admin/common/js/saga/event/EnonicAiOpenDialogEvent.ts b/src/main/resources/assets/admin/common/js/saga/event/EnonicAiOpenDialogEvent.ts new file mode 100644 index 000000000..8ffb97bbb --- /dev/null +++ b/src/main/resources/assets/admin/common/js/saga/event/EnonicAiOpenDialogEvent.ts @@ -0,0 +1,27 @@ +import {ClassHelper} from '../../ClassHelper'; +import {Event} from '../../event/Event'; + +export class EnonicAiOpenDialogEvent + extends Event { + + private readonly sourceDataPath?: string; + + constructor(dataPath?: string) { + super(); + + this.sourceDataPath = dataPath; + } + + getSourceDataPath(): string | undefined { + return this.sourceDataPath; + } + + static on(handler: (event: EnonicAiOpenDialogEvent) => void) { + Event.bind(ClassHelper.getFullName(this), handler); + } + + static un(handler?: (event: EnonicAiOpenDialogEvent) => void) { + Event.unbind(ClassHelper.getFullName(this), handler); + } + +} diff --git a/src/main/resources/assets/admin/common/js/saga/ui/SagaActionButton.ts b/src/main/resources/assets/admin/common/js/saga/ui/SagaActionButton.ts new file mode 100644 index 000000000..4565721ce --- /dev/null +++ b/src/main/resources/assets/admin/common/js/saga/ui/SagaActionButton.ts @@ -0,0 +1,29 @@ +import {Button} from '../../ui/button/Button'; +import {i18n} from '../../util/Messages'; +import {EnonicAiOpenDialogEvent} from '../event/EnonicAiOpenDialogEvent'; + +export class SagaActionButton extends Button { + + private dataPath?: string; + + constructor() { + super(); + + this.setTitle(i18n('action.saga')).addClass('icon-saga icon-sparkling'); + + this.initListeners(); + } + + setDataPath(dataPath: string): SagaActionButton { + this.dataPath = dataPath; + return this; + } + + protected initListeners(): void { + this.onClicked(() => { + if (this.dataPath) { + new EnonicAiOpenDialogEvent(this.dataPath).fire(); + } + }); + } +} diff --git a/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less b/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less index 4ddd5b3ad..c10a94725 100644 --- a/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less +++ b/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less @@ -1,6 +1,7 @@ .input-occurrence-view { display: flex; flex-direction: column; + position: relative; // to absolutely position elements within .data-block { .clearfix(); @@ -46,6 +47,15 @@ color: @input-red-text; } + .icon-saga { + .button(@admin-font-gray3, @admin-white); + padding: 4px; + position: absolute; + top: 0; + right: 0; + background-color: transparent; + } + &.single-occurrence { .data-block { > .drag-control, @@ -53,6 +63,10 @@ display: none; } } + + .icon-saga { + top: -21px; + } } &.multiple-occurrence { @@ -72,5 +86,10 @@ .error-block { padding: 0 0 10px 17px; } + + .icon-saga { + top: 1px; + right: 1px; + } } } From 62c0da262421b0f8422fabd8e15bb032f6594800 Mon Sep 17 00:00:00 2001 From: ashklianko Date: Tue, 25 Jun 2024 11:08:56 +0200 Subject: [PATCH 06/17] Add level navigation for item sets #332 --- .../assets/admin/common/js/ai/AIHelper.ts | 60 +++++++++++++++++++ .../event/EnonicAiOpenDialogEvent.ts | 0 .../ui/AIActionButton.ts} | 7 ++- .../common/js/form/FormItemOccurrenceView.ts | 2 +- .../support/BaseInputTypeNotManagingAdd.ts | 2 +- .../inputtype/support/InputOccurrenceView.ts | 8 +-- .../js/form/inputtype/text/TextInputType.ts | 2 +- .../js/form/set/FormSetOccurrenceView.ts | 8 ++- .../set/optionset/FormOptionSetOptionView.ts | 4 +- .../assets/admin/common/js/saga/SagaHelper.ts | 47 --------------- .../support/input-occurrence-view.less | 10 ++-- 11 files changed, 84 insertions(+), 66 deletions(-) create mode 100644 src/main/resources/assets/admin/common/js/ai/AIHelper.ts rename src/main/resources/assets/admin/common/js/{saga => ai}/event/EnonicAiOpenDialogEvent.ts (100%) rename src/main/resources/assets/admin/common/js/{saga/ui/SagaActionButton.ts => ai/ui/AIActionButton.ts} (74%) delete mode 100644 src/main/resources/assets/admin/common/js/saga/SagaHelper.ts diff --git a/src/main/resources/assets/admin/common/js/ai/AIHelper.ts b/src/main/resources/assets/admin/common/js/ai/AIHelper.ts new file mode 100644 index 000000000..b443f34c7 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/ai/AIHelper.ts @@ -0,0 +1,60 @@ +import {PropertyPath} from '../data/PropertyPath'; +import {Element} from '../dom/Element'; +import {AIActionButton} from './ui/AIActionButton'; + +export interface AIHelperConfig { + dataPathElement: Element; + getPathFunc: () => PropertyPath; + icon?: { + parent: Element; + focusContainer?: Element; + }; +} + +export class AIHelper { + + public static DATA_ATTR = 'data-path'; + + private readonly config: AIHelperConfig; + + private readonly sagaIcon?: AIActionButton; + + constructor(config: AIHelperConfig) { + this.config = config; + + const updatePathCall = setInterval(() => { + this.updateInputElDataPath(); + }, 1000); + + this.config.dataPathElement.onRemoved(() => { + clearInterval(updatePathCall); + }); + + if (config.icon?.parent) { + this.sagaIcon = new AIActionButton(); + this.sagaIcon.hide(); + this.config.icon.parent.appendChild(this.sagaIcon); + + const focusContainer = config.icon.focusContainer || config.icon.parent; + focusContainer.onFocusIn((event: Event) => { + this.sagaIcon.show(); + }); + + focusContainer.onFocusOut((event: MouseEvent) => { + if (event.relatedTarget !== this.sagaIcon.getHTMLElement()) { + this.sagaIcon.hide(); + } + }); + } + } + + private updateInputElDataPath(): void { + const dataPath = AIHelper.convertToPath(this.config.getPathFunc()); + this.config.dataPathElement.getEl().setAttribute(AIHelper.DATA_ATTR, dataPath); + this.sagaIcon?.setDataPath(dataPath); + } + + public static convertToPath(path: PropertyPath): string { + return path?.toString().replace(/\./g, '/') || ''; + } +} diff --git a/src/main/resources/assets/admin/common/js/saga/event/EnonicAiOpenDialogEvent.ts b/src/main/resources/assets/admin/common/js/ai/event/EnonicAiOpenDialogEvent.ts similarity index 100% rename from src/main/resources/assets/admin/common/js/saga/event/EnonicAiOpenDialogEvent.ts rename to src/main/resources/assets/admin/common/js/ai/event/EnonicAiOpenDialogEvent.ts diff --git a/src/main/resources/assets/admin/common/js/saga/ui/SagaActionButton.ts b/src/main/resources/assets/admin/common/js/ai/ui/AIActionButton.ts similarity index 74% rename from src/main/resources/assets/admin/common/js/saga/ui/SagaActionButton.ts rename to src/main/resources/assets/admin/common/js/ai/ui/AIActionButton.ts index 4565721ce..28cb7b689 100644 --- a/src/main/resources/assets/admin/common/js/saga/ui/SagaActionButton.ts +++ b/src/main/resources/assets/admin/common/js/ai/ui/AIActionButton.ts @@ -2,19 +2,20 @@ import {Button} from '../../ui/button/Button'; import {i18n} from '../../util/Messages'; import {EnonicAiOpenDialogEvent} from '../event/EnonicAiOpenDialogEvent'; -export class SagaActionButton extends Button { +export class AIActionButton + extends Button { private dataPath?: string; constructor() { super(); - this.setTitle(i18n('action.saga')).addClass('icon-saga icon-sparkling'); + this.setTitle(i18n('action.saga')).addClass('icon-ai icon-sparkling'); this.initListeners(); } - setDataPath(dataPath: string): SagaActionButton { + setDataPath(dataPath: string): AIActionButton { this.dataPath = dataPath; return this; } diff --git a/src/main/resources/assets/admin/common/js/form/FormItemOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/FormItemOccurrenceView.ts index 831b679b6..5c6930a70 100644 --- a/src/main/resources/assets/admin/common/js/form/FormItemOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/FormItemOccurrenceView.ts @@ -4,7 +4,7 @@ import {PropertyPath} from '../data/PropertyPath'; import {FormItemOccurrence} from './FormItemOccurrence'; import {HelpTextContainer} from './HelpTextContainer'; import {RemoveButtonClickedEvent} from './RemoveButtonClickedEvent'; -import {SagaHelper} from '../saga/SagaHelper'; +import {AIHelper} from '../ai/AIHelper'; import {Element} from '../dom/Element'; export interface FormItemOccurrenceViewConfig { diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputTypeNotManagingAdd.ts b/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputTypeNotManagingAdd.ts index cde84d11f..10d148c95 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputTypeNotManagingAdd.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputTypeNotManagingAdd.ts @@ -398,7 +398,7 @@ export abstract class BaseInputTypeNotManagingAdd }); } - isSagaEditable(): boolean { + isEditableByAI(): boolean { return false; } } diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts index 9e9a31608..c9124f210 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts @@ -10,7 +10,7 @@ import {InputOccurrence} from './InputOccurrence'; import {BaseInputTypeNotManagingAdd} from './BaseInputTypeNotManagingAdd'; import {ButtonEl} from '../../../dom/ButtonEl'; import {OccurrenceValidationRecord} from './OccurrenceValidationRecord'; -import {SagaHelper} from '../../../saga/SagaHelper'; +import {AIHelper} from '../../../ai/AIHelper'; export interface InputOccurrenceViewConfig extends FormItemOccurrenceViewConfig { @@ -64,7 +64,7 @@ export class InputOccurrenceView this.appendChild(dataBlock); dataBlock.appendChild(this.dragControl); dataBlock.appendChild(this.inputWrapper); - this.inputWrapper.appendChild(this.inputElement); + this.inputWrapper.prependChild(this.inputElement); dataBlock.appendChild(this.removeButtonEl); this.appendChild(this.validationErrorBlock); @@ -194,8 +194,8 @@ export class InputOccurrenceView this.property.onPropertyValueChanged(this.propertyValueChangedHandler); - if (this.inputTypeView.isSagaEditable()) { - new SagaHelper({ + if (this.inputTypeView.isEditableByAI()) { + new AIHelper({ dataPathElement: this.inputElement, getPathFunc: () => this.getDataPath(), icon: { diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/text/TextInputType.ts b/src/main/resources/assets/admin/common/js/form/inputtype/text/TextInputType.ts index 83112ebd3..9ead1a40e 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/text/TextInputType.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/text/TextInputType.ts @@ -162,7 +162,7 @@ export abstract class TextInputType }); } - isSagaEditable(): boolean { + isEditableByAI(): boolean { return true; } } diff --git a/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrenceView.ts index d6b0483d3..8022f5686 100644 --- a/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrenceView.ts @@ -16,7 +16,6 @@ import {ValidationRecordingPath} from '../ValidationRecordingPath'; import {FormSet} from './FormSet'; import {FormItem} from '../FormItem'; import {FormContext} from '../FormContext'; -import {FormSetOccurrence} from './FormSetOccurrence'; import {Action} from '../../ui/Action'; import {MoreButton} from '../../ui/button/MoreButton'; import {ConfirmationMask} from '../../ui/mask/ConfirmationMask'; @@ -34,8 +33,8 @@ import {FormItemSet} from './itemset/FormItemSet'; import {Input} from '../Input'; import {RadioButton} from '../inputtype/radiobutton/RadioButton'; import {ObjectHelper} from '../../ObjectHelper'; -import {SagaHelper} from '../../saga/SagaHelper'; import {FormItemOccurrence} from '../FormItemOccurrence'; +import {AIHelper} from '../../ai/AIHelper'; export interface FormSetOccurrenceViewConfig { context: FormContext; @@ -236,6 +235,11 @@ export abstract class FormSetOccurrenceView this.releasePropertySet(this.propertySet); } }); + + new AIHelper({ + dataPathElement: this, + getPathFunc: () => this.getDataPath(), + }); } private initOccurrencesContainer(): void { diff --git a/src/main/resources/assets/admin/common/js/form/set/optionset/FormOptionSetOptionView.ts b/src/main/resources/assets/admin/common/js/form/set/optionset/FormOptionSetOptionView.ts index fd4bfdef4..32e4aa44b 100644 --- a/src/main/resources/assets/admin/common/js/form/set/optionset/FormOptionSetOptionView.ts +++ b/src/main/resources/assets/admin/common/js/form/set/optionset/FormOptionSetOptionView.ts @@ -19,7 +19,7 @@ import {RecordingValidityChangedEvent} from '../../RecordingValidityChangedEvent import {ValidationRecording} from '../../ValidationRecording'; import {CreatedFormItemLayerConfig, FormItemLayerFactory} from '../../FormItemLayerFactory'; import {FormItemState} from '../../FormItemState'; -import {SagaHelper} from '../../../saga/SagaHelper'; +import {AIHelper} from '../../../ai/AIHelper'; import {PropertyPath, PropertyPathElement} from '../../../data/PropertyPath'; export interface FormOptionSetOptionViewConfig @@ -67,7 +67,7 @@ export class FormOptionSetOptionView this.formItemLayer = config.layerFactory.createLayer(config); this.notificationDialog = new NotificationDialog(i18n('notify.optionset.notempty')); - new SagaHelper({ + new AIHelper({ dataPathElement: this, getPathFunc: () => PropertyPath.fromParent(this.getParent().getDataPath(), new PropertyPathElement(this.getName(), 0)), }); diff --git a/src/main/resources/assets/admin/common/js/saga/SagaHelper.ts b/src/main/resources/assets/admin/common/js/saga/SagaHelper.ts deleted file mode 100644 index bf6b0b077..000000000 --- a/src/main/resources/assets/admin/common/js/saga/SagaHelper.ts +++ /dev/null @@ -1,47 +0,0 @@ -import {PropertyPath} from '../data/PropertyPath'; -import {Element} from '../dom/Element'; -import {SagaActionButton} from './ui/SagaActionButton'; - -export interface SagaHelperConfig { - dataPathElement: Element; - getPathFunc: () => PropertyPath; - icon?: { - parent: Element; - }; -} - -export class SagaHelper { - - public static DATA_ATTR = 'data-path'; - - private readonly config: SagaHelperConfig; - - private readonly sagaIcon?: SagaActionButton; - - constructor(config: SagaHelperConfig) { - this.config = config; - - const updatePathCall = setInterval(() => { - this.updateInputElDataPath(); - }, 1000); - - this.config.dataPathElement.onRemoved(() => { - clearInterval(updatePathCall); - }); - - if (config.icon?.parent) { - this.sagaIcon = new SagaActionButton(); - this.config.icon.parent.appendChild(this.sagaIcon); - } - } - - private updateInputElDataPath(): void { - const dataPath = SagaHelper.convertToPath(this.config.getPathFunc()); - this.config.dataPathElement.getEl().setAttribute(SagaHelper.DATA_ATTR, dataPath); - this.sagaIcon?.setDataPath(dataPath); - } - - public static convertToPath(path: PropertyPath): string { - return path?.toString().replace(/\./g, '/') || ''; - } -} diff --git a/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less b/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less index c10a94725..33347ebbd 100644 --- a/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less +++ b/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less @@ -47,13 +47,13 @@ color: @input-red-text; } - .icon-saga { - .button(@admin-font-gray3, @admin-white); + .icon-ai { + .button(@admin-font-gray3, transparent, @admin-font-gray2); padding: 4px; position: absolute; + z-index: 1; top: 0; right: 0; - background-color: transparent; } &.single-occurrence { @@ -64,7 +64,7 @@ } } - .icon-saga { + .icon-ai { top: -21px; } } @@ -87,7 +87,7 @@ padding: 0 0 10px 17px; } - .icon-saga { + .icon-ai { top: 1px; right: 1px; } From a877e3fcd2ebb9661a9be4b892a99fde40dd40dd Mon Sep 17 00:00:00 2001 From: ashklianko Date: Tue, 25 Jun 2024 11:08:56 +0200 Subject: [PATCH 07/17] Add level navigation for item sets #332 --- .../assets/admin/common/js/ai/AIHelper.ts | 19 +++---------------- .../admin/common/js/ai/ui/AIActionButton.ts | 4 +++- .../inputtype/support/InputOccurrenceView.ts | 2 +- .../support/input-occurrence-view.less | 7 +++++++ .../admin/common/styles/api/input-common.less | 13 ++++++++++--- 5 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/main/resources/assets/admin/common/js/ai/AIHelper.ts b/src/main/resources/assets/admin/common/js/ai/AIHelper.ts index b443f34c7..622326901 100644 --- a/src/main/resources/assets/admin/common/js/ai/AIHelper.ts +++ b/src/main/resources/assets/admin/common/js/ai/AIHelper.ts @@ -6,8 +6,7 @@ export interface AIHelperConfig { dataPathElement: Element; getPathFunc: () => PropertyPath; icon?: { - parent: Element; - focusContainer?: Element; + container: Element; }; } @@ -30,21 +29,9 @@ export class AIHelper { clearInterval(updatePathCall); }); - if (config.icon?.parent) { + if (config.icon?.container) { this.sagaIcon = new AIActionButton(); - this.sagaIcon.hide(); - this.config.icon.parent.appendChild(this.sagaIcon); - - const focusContainer = config.icon.focusContainer || config.icon.parent; - focusContainer.onFocusIn((event: Event) => { - this.sagaIcon.show(); - }); - - focusContainer.onFocusOut((event: MouseEvent) => { - if (event.relatedTarget !== this.sagaIcon.getHTMLElement()) { - this.sagaIcon.hide(); - } - }); + this.config.icon.container.appendChild(this.sagaIcon); } } diff --git a/src/main/resources/assets/admin/common/js/ai/ui/AIActionButton.ts b/src/main/resources/assets/admin/common/js/ai/ui/AIActionButton.ts index 28cb7b689..de5bec995 100644 --- a/src/main/resources/assets/admin/common/js/ai/ui/AIActionButton.ts +++ b/src/main/resources/assets/admin/common/js/ai/ui/AIActionButton.ts @@ -5,12 +5,14 @@ import {EnonicAiOpenDialogEvent} from '../event/EnonicAiOpenDialogEvent'; export class AIActionButton extends Button { + public static iconClass = 'icon-ai'; + private dataPath?: string; constructor() { super(); - this.setTitle(i18n('action.saga')).addClass('icon-ai icon-sparkling'); + this.setTitle(i18n('action.saga')).addClass('icon-sparkling').addClass(AIActionButton.iconClass); this.initListeners(); } diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts index c9124f210..d450c00c5 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts @@ -199,7 +199,7 @@ export class InputOccurrenceView dataPathElement: this.inputElement, getPathFunc: () => this.getDataPath(), icon: { - parent: this.inputWrapper, + container: this.inputWrapper, } }); } diff --git a/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less b/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less index 33347ebbd..20160ad7f 100644 --- a/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less +++ b/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less @@ -49,6 +49,7 @@ .icon-ai { .button(@admin-font-gray3, transparent, @admin-font-gray2); + display: none; padding: 4px; position: absolute; z-index: 1; @@ -56,6 +57,12 @@ right: 0; } + &:focus-within, &:hover { + .icon-ai { + display: block; + } + } + &.single-occurrence { .data-block { > .drag-control, diff --git a/src/main/resources/assets/admin/common/styles/api/input-common.less b/src/main/resources/assets/admin/common/styles/api/input-common.less index 117993dfa..e876d0184 100644 --- a/src/main/resources/assets/admin/common/styles/api/input-common.less +++ b/src/main/resources/assets/admin/common/styles/api/input-common.less @@ -16,9 +16,6 @@ } > .input-label { - display: inline-block; - max-width: calc(100% - 30px); - .@{_COMMON_PREFIX}wrapper { display: flex; white-space: nowrap; @@ -35,6 +32,16 @@ color: @input-red-text; } } + + &:focus-within, &:hover { + + .input-type-view { + > .input-occurrence-view:first-child { + .icon-ai { + display: block; // showing AI icon for the first occurrence of the input on hover over label + } + } + } + } } > label { From b9af52b3714da3a0d155d2aa7dbdac6e91af6da9 Mon Sep 17 00:00:00 2001 From: ashklianko Date: Thu, 27 Jun 2024 14:11:32 +0200 Subject: [PATCH 08/17] Use same AI icon for text inputs and AI dialog button #3622 --- .../admin/common/icons/fonts/icomoon.svg | 3 ++- .../admin/common/icons/fonts/icomoon.woff | Bin 10684 -> 11664 bytes .../admin/common/icons/fonts/icomoon.woff2 | Bin 4884 -> 5520 bytes .../assets/admin/common/icons/icons.less | 8 ++++++++ .../admin/common/js/ai/ui/AIActionButton.ts | 2 +- .../support/input-occurrence-view.less | 11 ++++++++--- 6 files changed, 19 insertions(+), 5 deletions(-) mode change 100755 => 100644 src/main/resources/assets/admin/common/icons/fonts/icomoon.woff diff --git a/src/main/resources/assets/admin/common/icons/fonts/icomoon.svg b/src/main/resources/assets/admin/common/icons/fonts/icomoon.svg index 781ce49c5..16c1f4a1d 100644 --- a/src/main/resources/assets/admin/common/icons/fonts/icomoon.svg +++ b/src/main/resources/assets/admin/common/icons/fonts/icomoon.svg @@ -38,6 +38,7 @@ + @@ -65,4 +66,4 @@ - + \ No newline at end of file diff --git a/src/main/resources/assets/admin/common/icons/fonts/icomoon.woff b/src/main/resources/assets/admin/common/icons/fonts/icomoon.woff old mode 100755 new mode 100644 index d58b3faebdcfaa095729420168a5430c1ae47c55..0f2bf7b554e3752166aab49f77de7da9883da084 GIT binary patch delta 2145 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 Gv_1e|)oH!} diff --git a/src/main/resources/assets/admin/common/icons/icons.less b/src/main/resources/assets/admin/common/icons/icons.less index 35c1b439f..7e8cd4f16 100644 --- a/src/main/resources/assets/admin/common/icons/icons.less +++ b/src/main/resources/assets/admin/common/icons/icons.less @@ -602,3 +602,11 @@ content: "\e9c9"; } } + +.icon-ai { + .icon(); + + &::before { + content: "\e922"; + } +} diff --git a/src/main/resources/assets/admin/common/js/ai/ui/AIActionButton.ts b/src/main/resources/assets/admin/common/js/ai/ui/AIActionButton.ts index de5bec995..1e3e4fa96 100644 --- a/src/main/resources/assets/admin/common/js/ai/ui/AIActionButton.ts +++ b/src/main/resources/assets/admin/common/js/ai/ui/AIActionButton.ts @@ -12,7 +12,7 @@ export class AIActionButton constructor() { super(); - this.setTitle(i18n('action.saga')).addClass('icon-sparkling').addClass(AIActionButton.iconClass); + this.setTitle(i18n('action.saga')).addClass('icon-ai icon-small').addClass(AIActionButton.iconClass); this.initListeners(); } diff --git a/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less b/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less index 20160ad7f..a61b9b1f8 100644 --- a/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less +++ b/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less @@ -59,7 +59,7 @@ &:focus-within, &:hover { .icon-ai { - display: block; + display: flex; } } @@ -95,8 +95,13 @@ } .icon-ai { - top: 1px; - right: 1px; + top: 4px; + right: 4px; + background-color: @admin-white; + + &:hover { + background-color: @admin-white; + } } } } From 4ec792a764dedcd1db60cb6cb338bd6326d36768 Mon Sep 17 00:00:00 2001 From: ashklianko Date: Mon, 1 Jul 2024 10:42:21 +0200 Subject: [PATCH 09/17] Making epic-ai branch published and available for CS --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 6f20a473e..63fe33d91 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest outputs: - publish: ${{ steps.publish_vars.outputs.release != 'true' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/4.')) }} + publish: ${{ steps.publish_vars.outputs.release != 'true' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/4.') || startsWith(github.ref, 'refs/heads/epic-ai')) }} repo: ${{ steps.publish_vars.outputs.repo }} steps: From 8e127cff458e3465ba7912962ceabb23c092c033 Mon Sep 17 00:00:00 2001 From: ashklianko Date: Mon, 1 Jul 2024 10:18:16 +0200 Subject: [PATCH 10/17] Let form decide if it's inputs to be AI interactable #3623 --- .../admin/common/js/form/FormContext.ts | 23 +++++++++++++++---- .../inputtype/support/InputOccurrenceView.ts | 2 +- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/main/resources/assets/admin/common/js/form/FormContext.ts b/src/main/resources/assets/admin/common/js/form/FormContext.ts index 974103844..c3b4e79e4 100644 --- a/src/main/resources/assets/admin/common/js/form/FormContext.ts +++ b/src/main/resources/assets/admin/common/js/form/FormContext.ts @@ -14,11 +14,14 @@ export class FormContext { private validationErrors: ValidationError[]; + private readonly aiEditable: boolean; + constructor(builder: FormContextBuilder) { this.showEmptyFormItemSetOccurrences = builder.showEmptyFormItemSetOccurrences; this.formState = builder.formState; this.language = builder.language; this.validationErrors = builder.validationErrors || []; + this.aiEditable = builder.aiEditable ?? false; } static create(): FormContextBuilder { @@ -71,6 +74,11 @@ export class FormContext { setLanguage(lang: string) { this.language = lang; } + + isAiEditable(): boolean { + return this.aiEditable; + } + } export class FormContextBuilder { @@ -83,26 +91,33 @@ export class FormContextBuilder { validationErrors: ValidationError[]; - public setShowEmptyFormItemSetOccurrences(value: boolean): FormContextBuilder { + aiEditable: boolean; + + public setShowEmptyFormItemSetOccurrences(value: boolean): this { this.showEmptyFormItemSetOccurrences = value; return this; } - public setFormState(value: FormState): FormContextBuilder { + public setFormState(value: FormState): this { this.formState = value; return this; } - public setLanguage(lang: string): FormContextBuilder { + public setLanguage(lang: string): this { this.language = lang; return this; } - public setValidationErrors(value: ValidationError[]): FormContextBuilder { + public setValidationErrors(value: ValidationError[]): this { this.validationErrors = value; return this; } + public setAiEditable(value: boolean): this { + this.aiEditable = value; + return this; + } + public build(): FormContext { return new FormContext(this); } diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts index d450c00c5..d8d001f17 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts @@ -194,7 +194,7 @@ export class InputOccurrenceView this.property.onPropertyValueChanged(this.propertyValueChangedHandler); - if (this.inputTypeView.isEditableByAI()) { + if (this.inputTypeView.getContext()?.formContext?.isAiEditable() && this.inputTypeView.isEditableByAI()) { new AIHelper({ dataPathElement: this.inputElement, getPathFunc: () => this.getDataPath(), From 4275237cb385bdd71d5af659e8c102f78467b04d Mon Sep 17 00:00:00 2001 From: ashklianko Date: Wed, 3 Jul 2024 11:08:18 +0200 Subject: [PATCH 11/17] Animate text input that is being translated #3633 --- .../assets/admin/common/js/ai/AIHelper.ts | 77 +++++++++++++ .../admin/common/js/ai/AIHelperState.ts | 6 + .../admin/common/js/ai/ui/AIActionButton.ts | 41 ++++++- .../inputtype/support/InputOccurrenceView.ts | 3 +- .../support/input-occurrence-view.less | 107 ++++++++++++++++-- .../admin/common/styles/api/input-common.less | 2 +- src/main/resources/i18n/common.properties | 5 + 7 files changed, 227 insertions(+), 14 deletions(-) create mode 100644 src/main/resources/assets/admin/common/js/ai/AIHelperState.ts diff --git a/src/main/resources/assets/admin/common/js/ai/AIHelper.ts b/src/main/resources/assets/admin/common/js/ai/AIHelper.ts index 622326901..c11ae103d 100644 --- a/src/main/resources/assets/admin/common/js/ai/AIHelper.ts +++ b/src/main/resources/assets/admin/common/js/ai/AIHelper.ts @@ -1,6 +1,9 @@ import {PropertyPath} from '../data/PropertyPath'; import {Element} from '../dom/Element'; import {AIActionButton} from './ui/AIActionButton'; +import {AI_HELPER_STATE} from './AIHelperState'; +import {i18n} from '../util/Messages'; +import {Mask} from '../ui/mask/Mask'; export interface AIHelperConfig { dataPathElement: Element; @@ -8,6 +11,7 @@ export interface AIHelperConfig { icon?: { container: Element; }; + label?: string; } export class AIHelper { @@ -18,6 +22,10 @@ export class AIHelper { private readonly sagaIcon?: AIActionButton; + private state: AI_HELPER_STATE = AI_HELPER_STATE.DEFAULT; + + private readonly mask: Mask; + constructor(config: AIHelperConfig) { this.config = config; @@ -27,12 +35,18 @@ export class AIHelper { this.config.dataPathElement.onRemoved(() => { clearInterval(updatePathCall); + AI_HELPER_REGISTRY.get().remove(this); }); + AI_HELPER_REGISTRY.get().add(this); + if (config.icon?.container) { this.sagaIcon = new AIActionButton(); this.config.icon.container.appendChild(this.sagaIcon); } + + const maskTitle = this.config.label ? i18n('ai.assistant.processing', this.config.label) : ''; + this.mask = new Mask(this.config.dataPathElement).setTitle(maskTitle).addClass('ai-helper-mask') as Mask; } private updateInputElDataPath(): void { @@ -41,7 +55,70 @@ export class AIHelper { this.sagaIcon?.setDataPath(dataPath); } + setState(state: AI_HELPER_STATE): this { + if (state === this.state) { + return this; + } + + this.state = state; + this.sagaIcon?.setState(state); + + if (state === AI_HELPER_STATE.COMPLETED || state === AI_HELPER_STATE.FAILED) { + setTimeout(() => { + if (this.state === AI_HELPER_STATE.COMPLETED || this.state === AI_HELPER_STATE.FAILED) { + this.setState(AI_HELPER_STATE.DEFAULT); + } + }, 1000); + + this.mask.hide(); + this.config.dataPathElement.getEl().setDisabled(false); + } else if (state === AI_HELPER_STATE.PROCESSING) { + this.config.dataPathElement.getEl().setDisabled(true); + this.mask.show(); + } + + return this; + } + + setValue(value: string): this { + // set value + return this; + } + + getDataPath(): string { + return this.config.dataPathElement.getEl().getAttribute(AIHelper.DATA_ATTR); + } + public static convertToPath(path: PropertyPath): string { return path?.toString().replace(/\./g, '/') || ''; } + + public static getAIHelperByPath(dataPath: string): AIHelper | undefined { + return AI_HELPER_REGISTRY.get().find(dataPath); + } +} + +class AI_HELPER_REGISTRY { + + private registry: AIHelper[]; + + private constructor() { + this.registry = []; + } + + static get(): AI_HELPER_REGISTRY { + return window['AI_HELPER_REGISTRY'] ?? (window['AI_HELPER_REGISTRY'] = new AI_HELPER_REGISTRY()); + } + + add(helper: AIHelper): void { + this.registry.push(helper); + } + + remove(helper: AIHelper): void { + this.registry = this.registry.filter(h => h !== helper); + } + + find(dataPath: string): AIHelper | undefined { + return this.registry.find(helper => helper.getDataPath() === dataPath); + } } diff --git a/src/main/resources/assets/admin/common/js/ai/AIHelperState.ts b/src/main/resources/assets/admin/common/js/ai/AIHelperState.ts new file mode 100644 index 000000000..b9ff44df8 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/ai/AIHelperState.ts @@ -0,0 +1,6 @@ +export enum AI_HELPER_STATE { + DEFAULT = 'default', + PROCESSING = 'processing', + COMPLETED = 'completed', + FAILED = 'failed', +} diff --git a/src/main/resources/assets/admin/common/js/ai/ui/AIActionButton.ts b/src/main/resources/assets/admin/common/js/ai/ui/AIActionButton.ts index 1e3e4fa96..fc84a1274 100644 --- a/src/main/resources/assets/admin/common/js/ai/ui/AIActionButton.ts +++ b/src/main/resources/assets/admin/common/js/ai/ui/AIActionButton.ts @@ -1,32 +1,63 @@ import {Button} from '../../ui/button/Button'; import {i18n} from '../../util/Messages'; import {EnonicAiOpenDialogEvent} from '../event/EnonicAiOpenDialogEvent'; +import {DivEl} from '../../dom/DivEl'; +import * as Q from 'q'; +import {AI_HELPER_STATE} from '../AIHelperState'; export class AIActionButton - extends Button { + extends DivEl { - public static iconClass = 'icon-ai'; + private static baseClass = 'ai-button-container'; private dataPath?: string; + private button: Button; + + private loader: DivEl; + constructor() { super(); - this.setTitle(i18n('action.saga')).addClass('icon-ai icon-small').addClass(AIActionButton.iconClass); - + this.initElements(); this.initListeners(); } + protected initElements(): void { + this.button = new Button().addClass(`${AIActionButton.baseClass}-icon icon-ai`) as Button; + this.loader = new DivEl(`${AIActionButton.baseClass}-loader`); + this.setTitle(i18n('action.saga')); + this.setState(AI_HELPER_STATE.DEFAULT); + } + + setState(state: AI_HELPER_STATE): this { + this.setClass(`${AIActionButton.baseClass} ${state}`); + + return this; + } + setDataPath(dataPath: string): AIActionButton { this.dataPath = dataPath; return this; } + getDataPath(): string { + return this.dataPath; + } + protected initListeners(): void { - this.onClicked(() => { + this.button.onClicked(() => { if (this.dataPath) { new EnonicAiOpenDialogEvent(this.dataPath).fire(); } }); } + + doRender(): Q.Promise { + return super.doRender().then((rendered: boolean) => { + this.appendChildren(this.loader, this.button); + + return rendered; + }); + } } diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts index d8d001f17..4908290b6 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts @@ -200,7 +200,8 @@ export class InputOccurrenceView getPathFunc: () => this.getDataPath(), icon: { container: this.inputWrapper, - } + }, + label: this.inputTypeView.getInput().getLabel(), }); } } diff --git a/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less b/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less index a61b9b1f8..c719ae6ab 100644 --- a/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less +++ b/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less @@ -47,19 +47,107 @@ color: @input-red-text; } - .icon-ai { - .button(@admin-font-gray3, transparent, @admin-font-gray2); + .ai-button-container { + width: 24px; + height: 24px; display: none; - padding: 4px; + border: none; position: absolute; z-index: 1; top: 0; right: 0; + + &-icon { + .button(@admin-font-gray3, transparent, @admin-font-gray2); + padding: 4px; + width: 100%; + height: 100%; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + } + + &-loader { + box-sizing: border-box; + display: none; + position: absolute; + border: 2px solid @admin-white; + border-radius: 50%; + width: 24px; + height: 24px; + -webkit-animation: spin 2s linear infinite; + animation: spin 2s linear infinite; + } + + &.processing, &.completed, &.failed { + display: block; + + .ai-button-container-loader { + display: block; + } + } + + &.processing { + .ai-button-container-loader { + border-top: 2px solid #2c76e9; + border-bottom: 2px solid #2c76e9; + } + } + + &.fade { + display: block; + animation: fadingOpacity 2s ease-in-out infinite; + + .ai-button-container-icon { + &:before { + color: @admin-blue; + } + } + } + + &.completed { + .ai-button-container-loader { + border-top: 2px solid @admin-green; + border-bottom: 2px solid @admin-green; + } + + .ai-button-container-icon { + &:before { + color: @admin-green; + } + } + } + + &.failed { + .ai-button-container-loader { + border-top: 2px solid @admin-red; + border-bottom: 2px solid @admin-red; + } + + .ai-button-container-icon { + &:before { + color: @admin-red; + } + } + } + + @keyframes spin { + 100% { + transform: rotateZ(360deg); + } + } + + @keyframes fadingOpacity { + 0% {opacity: 0.2;} + 50% {opacity: 1;} + 100% {opacity: 0.2;} + } } &:focus-within, &:hover { - .icon-ai { - display: flex; + .ai-button-container { + display: block; } } @@ -71,7 +159,7 @@ } } - .icon-ai { + .ai-button-container { top: -21px; } } @@ -94,7 +182,7 @@ padding: 0 0 10px 17px; } - .icon-ai { + .ai-button-container { top: 4px; right: 4px; background-color: @admin-white; @@ -105,3 +193,8 @@ } } } + +.ai-helper-mask { + background-color: @admin-blue; + opacity: 0.2; +} diff --git a/src/main/resources/assets/admin/common/styles/api/input-common.less b/src/main/resources/assets/admin/common/styles/api/input-common.less index e876d0184..8af746ef1 100644 --- a/src/main/resources/assets/admin/common/styles/api/input-common.less +++ b/src/main/resources/assets/admin/common/styles/api/input-common.less @@ -36,7 +36,7 @@ &:focus-within, &:hover { + .input-type-view { > .input-occurrence-view:first-child { - .icon-ai { + .ai-button-container { display: block; // showing AI icon for the first occurrence of the input on hover over label } } diff --git a/src/main/resources/i18n/common.properties b/src/main/resources/i18n/common.properties index 32ce802f0..8a544a91e 100644 --- a/src/main/resources/i18n/common.properties +++ b/src/main/resources/i18n/common.properties @@ -215,6 +215,11 @@ tooltip.header.collapse=Click to collapse # warning.optionsview.truncated = The list is truncated +# +# AI +# +ai.assistant.processing={0} is being processed... + # # Accessibility # From f9e5e3078b2489e7b1733f00e1820d4c8163cc01 Mon Sep 17 00:00:00 2001 From: ashklianko Date: Wed, 3 Jul 2024 12:20:03 +0200 Subject: [PATCH 12/17] Populate text inputs with received translations #3635 --- src/main/resources/assets/admin/common/js/ai/AIHelper.ts | 3 ++- .../common/js/form/inputtype/support/InputOccurrenceView.ts | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/resources/assets/admin/common/js/ai/AIHelper.ts b/src/main/resources/assets/admin/common/js/ai/AIHelper.ts index c11ae103d..f0e2aa4e5 100644 --- a/src/main/resources/assets/admin/common/js/ai/AIHelper.ts +++ b/src/main/resources/assets/admin/common/js/ai/AIHelper.ts @@ -12,6 +12,7 @@ export interface AIHelperConfig { container: Element; }; label?: string; + setValueFunc?: (value: string) => void; } export class AIHelper { @@ -81,7 +82,7 @@ export class AIHelper { } setValue(value: string): this { - // set value + this.config.setValueFunc?.(value); return this; } diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts index 4908290b6..5b1e7a927 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts @@ -202,6 +202,10 @@ export class InputOccurrenceView container: this.inputWrapper, }, label: this.inputTypeView.getInput().getLabel(), + setValueFunc: (val: string) => { + this.property.setValue(this.inputTypeView.getValueType().newValue(val)); + this.inputTypeView.updateInputOccurrenceElement(this.inputElement, this.property); + } }); } } From 32985efac760a6f4b7e370eb2d6ad877c4d6a459 Mon Sep 17 00:00:00 2001 From: ashklianko Date: Wed, 3 Jul 2024 16:15:03 +0200 Subject: [PATCH 13/17] Populate text inputs with received translations #3635 --- .../support/input-occurrence-view.less | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less b/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less index c719ae6ab..f3a4e8a1f 100644 --- a/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less +++ b/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less @@ -48,8 +48,8 @@ } .ai-button-container { - width: 24px; - height: 24px; + width: 20px; + height: 20px; display: none; border: none; position: absolute; @@ -59,7 +59,7 @@ &-icon { .button(@admin-font-gray3, transparent, @admin-font-gray2); - padding: 4px; + padding: 2px; width: 100%; height: 100%; font-size: 16px; @@ -72,12 +72,11 @@ box-sizing: border-box; display: none; position: absolute; - border: 2px solid @admin-white; + border: 2px dotted @admin-button-blue2; border-radius: 50%; - width: 24px; - height: 24px; - -webkit-animation: spin 2s linear infinite; - animation: spin 2s linear infinite; + width: 20px; + height: 20px; + animation: spin 3s linear infinite; } &.processing, &.completed, &.failed { @@ -90,16 +89,15 @@ &.processing { .ai-button-container-loader { - border-top: 2px solid #2c76e9; - border-bottom: 2px solid #2c76e9; + border-color: @admin-button-blue2; } } &.fade { display: block; - animation: fadingOpacity 2s ease-in-out infinite; .ai-button-container-icon { + animation: fadingOpacity 2s ease-in-out infinite; &:before { color: @admin-blue; } @@ -108,8 +106,7 @@ &.completed { .ai-button-container-loader { - border-top: 2px solid @admin-green; - border-bottom: 2px solid @admin-green; + border-color: @admin-green; } .ai-button-container-icon { @@ -121,8 +118,7 @@ &.failed { .ai-button-container-loader { - border-top: 2px solid @admin-red; - border-bottom: 2px solid @admin-red; + border-color: @admin-red; } .ai-button-container-icon { From 767de02d7efa0276d53a55095f9114f849980f91 Mon Sep 17 00:00:00 2001 From: ashklianko Date: Fri, 5 Jul 2024 14:30:39 +0200 Subject: [PATCH 14/17] AI translation: locked state look #3637 --- .../assets/admin/common/js/ai/AIHelper.ts | 31 ++++++++++++++----- .../support/input-occurrence-view.less | 28 ++++++++++++----- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/main/resources/assets/admin/common/js/ai/AIHelper.ts b/src/main/resources/assets/admin/common/js/ai/AIHelper.ts index f0e2aa4e5..f08f1889a 100644 --- a/src/main/resources/assets/admin/common/js/ai/AIHelper.ts +++ b/src/main/resources/assets/admin/common/js/ai/AIHelper.ts @@ -3,7 +3,6 @@ import {Element} from '../dom/Element'; import {AIActionButton} from './ui/AIActionButton'; import {AI_HELPER_STATE} from './AIHelperState'; import {i18n} from '../util/Messages'; -import {Mask} from '../ui/mask/Mask'; export interface AIHelperConfig { dataPathElement: Element; @@ -25,8 +24,6 @@ export class AIHelper { private state: AI_HELPER_STATE = AI_HELPER_STATE.DEFAULT; - private readonly mask: Mask; - constructor(config: AIHelperConfig) { this.config = config; @@ -45,9 +42,6 @@ export class AIHelper { this.sagaIcon = new AIActionButton(); this.config.icon.container.appendChild(this.sagaIcon); } - - const maskTitle = this.config.label ? i18n('ai.assistant.processing', this.config.label) : ''; - this.mask = new Mask(this.config.dataPathElement).setTitle(maskTitle).addClass('ai-helper-mask') as Mask; } private updateInputElDataPath(): void { @@ -71,11 +65,13 @@ export class AIHelper { } }, 1000); - this.mask.hide(); this.config.dataPathElement.getEl().setDisabled(false); + this.config.dataPathElement.removeClass('ai-helper-mask'); + this.resetTitle(); } else if (state === AI_HELPER_STATE.PROCESSING) { this.config.dataPathElement.getEl().setDisabled(true); - this.mask.show(); + this.config.dataPathElement.addClass('ai-helper-mask'); + this.updateTitle(); } return this; @@ -97,6 +93,25 @@ export class AIHelper { public static getAIHelperByPath(dataPath: string): AIHelper | undefined { return AI_HELPER_REGISTRY.get().find(dataPath); } + + private updateTitle(): void { + const parent = this.config.dataPathElement.getEl().getParent(); + + if (parent.hasAttribute('title') && !parent.hasAttribute('data-title')) { + parent.setAttribute('data-title', parent.getTitle()); + } + + parent.setTitle(i18n('ai.assistant.processing', this.config.label)); + } + + private resetTitle(): void { + const parent = this.config.dataPathElement.getEl().getParent(); + parent.removeAttribute('title'); + + if (parent.hasAttribute('data-title')) { + parent.setTitle(parent.getAttribute('data-title')); + } + } } class AI_HELPER_REGISTRY { diff --git a/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less b/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less index f3a4e8a1f..be3d4eabd 100644 --- a/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less +++ b/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less @@ -94,10 +94,11 @@ } &.fade { - display: block; + display: block; .ai-button-container-icon { animation: fadingOpacity 2s ease-in-out infinite; + &:before { color: @admin-blue; } @@ -135,9 +136,15 @@ } @keyframes fadingOpacity { - 0% {opacity: 0.2;} - 50% {opacity: 1;} - 100% {opacity: 0.2;} + 0% { + opacity: 0.2; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.2; + } } } @@ -181,16 +188,21 @@ .ai-button-container { top: 4px; right: 4px; - background-color: @admin-white; - &:hover { + &:not(.processing) { background-color: @admin-white; + + &:hover { + background-color: @admin-white; + } } + } } } .ai-helper-mask { - background-color: @admin-blue; - opacity: 0.2; + background-color: @admin-bg-light-gray; + opacity: 0.75; + border-color: @admin-medium-gray-border !important; } From 6fb103ab153bf8ee334d4e376664d7d6b3c61801 Mon Sep 17 00:00:00 2001 From: Mikita Taukachou Date: Wed, 18 Sep 2024 14:51:44 +0300 Subject: [PATCH 15/17] Update AI Helper syntax #3710 (#3711) * Update AI Helper syntax #3710 --- .../common/js/ai/{AIHelper.ts => AiHelper.ts} | 80 +++++++------------ .../ai/{AIHelperState.ts => AiHelperState.ts} | 2 +- ...EnonicAiContentOperatorOpenDialogEvent.ts} | 6 +- .../{AIActionButton.ts => AiActionButton.ts} | 28 +++---- .../common/js/form/FormItemOccurrenceView.ts | 4 +- .../inputtype/support/InputOccurrenceView.ts | 16 ++-- .../js/form/set/FormSetOccurrenceView.ts | 50 ++++++------ .../set/optionset/FormOptionSetOptionView.ts | 28 +++---- src/main/resources/i18n/common.properties | 3 +- 9 files changed, 98 insertions(+), 119 deletions(-) rename src/main/resources/assets/admin/common/js/ai/{AIHelper.ts => AiHelper.ts} (53%) rename src/main/resources/assets/admin/common/js/ai/{AIHelperState.ts => AiHelperState.ts} (78%) rename src/main/resources/assets/admin/common/js/ai/event/{EnonicAiOpenDialogEvent.ts => EnonicAiContentOperatorOpenDialogEvent.ts} (69%) rename src/main/resources/assets/admin/common/js/ai/ui/{AIActionButton.ts => AiActionButton.ts} (54%) diff --git a/src/main/resources/assets/admin/common/js/ai/AIHelper.ts b/src/main/resources/assets/admin/common/js/ai/AiHelper.ts similarity index 53% rename from src/main/resources/assets/admin/common/js/ai/AIHelper.ts rename to src/main/resources/assets/admin/common/js/ai/AiHelper.ts index f08f1889a..c81cc7d7e 100644 --- a/src/main/resources/assets/admin/common/js/ai/AIHelper.ts +++ b/src/main/resources/assets/admin/common/js/ai/AiHelper.ts @@ -1,10 +1,11 @@ import {PropertyPath} from '../data/PropertyPath'; import {Element} from '../dom/Element'; -import {AIActionButton} from './ui/AIActionButton'; -import {AI_HELPER_STATE} from './AIHelperState'; +import {Store} from '../store/Store'; import {i18n} from '../util/Messages'; +import {AiHelperState} from './AiHelperState'; +import {AiActionButton} from './ui/AiActionButton'; -export interface AIHelperConfig { +export interface AiHelperConfig { dataPathElement: Element; getPathFunc: () => PropertyPath; icon?: { @@ -14,17 +15,20 @@ export interface AIHelperConfig { setValueFunc?: (value: string) => void; } -export class AIHelper { +const AI_HELPERS_KEY = 'AiHelpers'; +Store.instance().set(AI_HELPERS_KEY, []); + +export class AiHelper { public static DATA_ATTR = 'data-path'; - private readonly config: AIHelperConfig; + private readonly config: AiHelperConfig; - private readonly sagaIcon?: AIActionButton; + private readonly aiIcon?: AiActionButton; - private state: AI_HELPER_STATE = AI_HELPER_STATE.DEFAULT; + private state: AiHelperState = AiHelperState.DEFAULT; - constructor(config: AIHelperConfig) { + constructor(config: AiHelperConfig) { this.config = config; const updatePathCall = setInterval(() => { @@ -33,42 +37,43 @@ export class AIHelper { this.config.dataPathElement.onRemoved(() => { clearInterval(updatePathCall); - AI_HELPER_REGISTRY.get().remove(this); + const helper: AiHelper[] = Store.instance().get(AI_HELPERS_KEY) ?? []; + Store.instance().set(AI_HELPERS_KEY, helper.filter(h => h !== this)); }); - AI_HELPER_REGISTRY.get().add(this); + Store.instance().get(AI_HELPERS_KEY).push(this); if (config.icon?.container) { - this.sagaIcon = new AIActionButton(); - this.config.icon.container.appendChild(this.sagaIcon); + this.aiIcon = new AiActionButton(); + this.config.icon.container.appendChild(this.aiIcon); } } private updateInputElDataPath(): void { - const dataPath = AIHelper.convertToPath(this.config.getPathFunc()); - this.config.dataPathElement.getEl().setAttribute(AIHelper.DATA_ATTR, dataPath); - this.sagaIcon?.setDataPath(dataPath); + const dataPath = AiHelper.convertToPath(this.config.getPathFunc()); + this.config.dataPathElement.getEl().setAttribute(AiHelper.DATA_ATTR, dataPath); + this.aiIcon?.setDataPath(dataPath); } - setState(state: AI_HELPER_STATE): this { + setState(state: AiHelperState): this { if (state === this.state) { return this; } this.state = state; - this.sagaIcon?.setState(state); + this.aiIcon?.setState(state); - if (state === AI_HELPER_STATE.COMPLETED || state === AI_HELPER_STATE.FAILED) { + if (state === AiHelperState.COMPLETED || state === AiHelperState.FAILED) { setTimeout(() => { - if (this.state === AI_HELPER_STATE.COMPLETED || this.state === AI_HELPER_STATE.FAILED) { - this.setState(AI_HELPER_STATE.DEFAULT); + if (this.state === AiHelperState.COMPLETED || this.state === AiHelperState.FAILED) { + this.setState(AiHelperState.DEFAULT); } }, 1000); this.config.dataPathElement.getEl().setDisabled(false); this.config.dataPathElement.removeClass('ai-helper-mask'); this.resetTitle(); - } else if (state === AI_HELPER_STATE.PROCESSING) { + } else if (state === AiHelperState.PROCESSING) { this.config.dataPathElement.getEl().setDisabled(true); this.config.dataPathElement.addClass('ai-helper-mask'); this.updateTitle(); @@ -83,15 +88,15 @@ export class AIHelper { } getDataPath(): string { - return this.config.dataPathElement.getEl().getAttribute(AIHelper.DATA_ATTR); + return this.config.dataPathElement.getEl().getAttribute(AiHelper.DATA_ATTR); } public static convertToPath(path: PropertyPath): string { return path?.toString().replace(/\./g, '/') || ''; } - public static getAIHelperByPath(dataPath: string): AIHelper | undefined { - return AI_HELPER_REGISTRY.get().find(dataPath); + public static getAiHelperByPath(dataPath: string): AiHelper | undefined { + return Store.instance().get(AI_HELPERS_KEY).find(helper => helper.getDataPath() === dataPath); } private updateTitle(): void { @@ -101,7 +106,7 @@ export class AIHelper { parent.setAttribute('data-title', parent.getTitle()); } - parent.setTitle(i18n('ai.assistant.processing', this.config.label)); + parent.setTitle(i18n('ai.field.processing', this.config.label)); } private resetTitle(): void { @@ -113,28 +118,3 @@ export class AIHelper { } } } - -class AI_HELPER_REGISTRY { - - private registry: AIHelper[]; - - private constructor() { - this.registry = []; - } - - static get(): AI_HELPER_REGISTRY { - return window['AI_HELPER_REGISTRY'] ?? (window['AI_HELPER_REGISTRY'] = new AI_HELPER_REGISTRY()); - } - - add(helper: AIHelper): void { - this.registry.push(helper); - } - - remove(helper: AIHelper): void { - this.registry = this.registry.filter(h => h !== helper); - } - - find(dataPath: string): AIHelper | undefined { - return this.registry.find(helper => helper.getDataPath() === dataPath); - } -} diff --git a/src/main/resources/assets/admin/common/js/ai/AIHelperState.ts b/src/main/resources/assets/admin/common/js/ai/AiHelperState.ts similarity index 78% rename from src/main/resources/assets/admin/common/js/ai/AIHelperState.ts rename to src/main/resources/assets/admin/common/js/ai/AiHelperState.ts index b9ff44df8..762ef703d 100644 --- a/src/main/resources/assets/admin/common/js/ai/AIHelperState.ts +++ b/src/main/resources/assets/admin/common/js/ai/AiHelperState.ts @@ -1,4 +1,4 @@ -export enum AI_HELPER_STATE { +export enum AiHelperState { DEFAULT = 'default', PROCESSING = 'processing', COMPLETED = 'completed', diff --git a/src/main/resources/assets/admin/common/js/ai/event/EnonicAiOpenDialogEvent.ts b/src/main/resources/assets/admin/common/js/ai/event/EnonicAiContentOperatorOpenDialogEvent.ts similarity index 69% rename from src/main/resources/assets/admin/common/js/ai/event/EnonicAiOpenDialogEvent.ts rename to src/main/resources/assets/admin/common/js/ai/event/EnonicAiContentOperatorOpenDialogEvent.ts index 8ffb97bbb..7e5a0c0c9 100644 --- a/src/main/resources/assets/admin/common/js/ai/event/EnonicAiOpenDialogEvent.ts +++ b/src/main/resources/assets/admin/common/js/ai/event/EnonicAiContentOperatorOpenDialogEvent.ts @@ -1,7 +1,7 @@ import {ClassHelper} from '../../ClassHelper'; import {Event} from '../../event/Event'; -export class EnonicAiOpenDialogEvent +export class EnonicAiContentOperatorOpenDialogEvent extends Event { private readonly sourceDataPath?: string; @@ -16,11 +16,11 @@ export class EnonicAiOpenDialogEvent return this.sourceDataPath; } - static on(handler: (event: EnonicAiOpenDialogEvent) => void) { + static on(handler: (event: EnonicAiContentOperatorOpenDialogEvent) => void) { Event.bind(ClassHelper.getFullName(this), handler); } - static un(handler?: (event: EnonicAiOpenDialogEvent) => void) { + static un(handler?: (event: EnonicAiContentOperatorOpenDialogEvent) => void) { Event.unbind(ClassHelper.getFullName(this), handler); } diff --git a/src/main/resources/assets/admin/common/js/ai/ui/AIActionButton.ts b/src/main/resources/assets/admin/common/js/ai/ui/AiActionButton.ts similarity index 54% rename from src/main/resources/assets/admin/common/js/ai/ui/AIActionButton.ts rename to src/main/resources/assets/admin/common/js/ai/ui/AiActionButton.ts index fc84a1274..eea7951d2 100644 --- a/src/main/resources/assets/admin/common/js/ai/ui/AIActionButton.ts +++ b/src/main/resources/assets/admin/common/js/ai/ui/AiActionButton.ts @@ -1,14 +1,14 @@ +import * as Q from 'q'; +import {DivEl} from '../../dom/DivEl'; import {Button} from '../../ui/button/Button'; import {i18n} from '../../util/Messages'; -import {EnonicAiOpenDialogEvent} from '../event/EnonicAiOpenDialogEvent'; -import {DivEl} from '../../dom/DivEl'; -import * as Q from 'q'; -import {AI_HELPER_STATE} from '../AIHelperState'; +import {AiHelperState} from '../AiHelperState'; +import {EnonicAiContentOperatorOpenDialogEvent} from '../event/EnonicAiContentOperatorOpenDialogEvent'; -export class AIActionButton +export class AiActionButton extends DivEl { - private static baseClass = 'ai-button-container'; + private static readonly BASE_CLASS = 'ai-button-container'; private dataPath?: string; @@ -24,19 +24,19 @@ export class AIActionButton } protected initElements(): void { - this.button = new Button().addClass(`${AIActionButton.baseClass}-icon icon-ai`) as Button; - this.loader = new DivEl(`${AIActionButton.baseClass}-loader`); - this.setTitle(i18n('action.saga')); - this.setState(AI_HELPER_STATE.DEFAULT); + this.button = new Button().addClass(`${AiActionButton.BASE_CLASS}-icon icon-ai`) as Button; + this.loader = new DivEl(`${AiActionButton.BASE_CLASS}-loader`); + this.setTitle(i18n('ai.action.contentOperator.use')); + this.setState(AiHelperState.DEFAULT); } - setState(state: AI_HELPER_STATE): this { - this.setClass(`${AIActionButton.baseClass} ${state}`); + setState(state: AiHelperState): this { + this.setClass(`${AiActionButton.BASE_CLASS} ${state}`); return this; } - setDataPath(dataPath: string): AIActionButton { + setDataPath(dataPath: string): AiActionButton { this.dataPath = dataPath; return this; } @@ -48,7 +48,7 @@ export class AIActionButton protected initListeners(): void { this.button.onClicked(() => { if (this.dataPath) { - new EnonicAiOpenDialogEvent(this.dataPath).fire(); + new EnonicAiContentOperatorOpenDialogEvent(this.dataPath).fire(); } }); } diff --git a/src/main/resources/assets/admin/common/js/form/FormItemOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/FormItemOccurrenceView.ts index 5c6930a70..c8a705498 100644 --- a/src/main/resources/assets/admin/common/js/form/FormItemOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/FormItemOccurrenceView.ts @@ -1,11 +1,9 @@ import * as Q from 'q'; -import {DivEl} from '../dom/DivEl'; import {PropertyPath} from '../data/PropertyPath'; +import {DivEl} from '../dom/DivEl'; import {FormItemOccurrence} from './FormItemOccurrence'; import {HelpTextContainer} from './HelpTextContainer'; import {RemoveButtonClickedEvent} from './RemoveButtonClickedEvent'; -import {AIHelper} from '../ai/AIHelper'; -import {Element} from '../dom/Element'; export interface FormItemOccurrenceViewConfig { className: string; diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts index 5b1e7a927..9aec840b2 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts @@ -1,16 +1,16 @@ import * as Q from 'q'; +import {AiHelper} from '../../../ai/AiHelper'; import {Property} from '../../../data/Property'; +import {PropertyPath} from '../../../data/PropertyPath'; import {PropertyValueChangedEvent} from '../../../data/PropertyValueChangedEvent'; -import {FormItemOccurrenceView, FormItemOccurrenceViewConfig} from '../../FormItemOccurrenceView'; -import {Element} from '../../../dom/Element'; -import {DivEl} from '../../../dom/DivEl'; import {Value} from '../../../data/Value'; -import {PropertyPath} from '../../../data/PropertyPath'; -import {InputOccurrence} from './InputOccurrence'; -import {BaseInputTypeNotManagingAdd} from './BaseInputTypeNotManagingAdd'; import {ButtonEl} from '../../../dom/ButtonEl'; +import {DivEl} from '../../../dom/DivEl'; +import {Element} from '../../../dom/Element'; +import {FormItemOccurrenceView, FormItemOccurrenceViewConfig} from '../../FormItemOccurrenceView'; +import {BaseInputTypeNotManagingAdd} from './BaseInputTypeNotManagingAdd'; +import {InputOccurrence} from './InputOccurrence'; import {OccurrenceValidationRecord} from './OccurrenceValidationRecord'; -import {AIHelper} from '../../../ai/AIHelper'; export interface InputOccurrenceViewConfig extends FormItemOccurrenceViewConfig { @@ -195,7 +195,7 @@ export class InputOccurrenceView this.property.onPropertyValueChanged(this.propertyValueChangedHandler); if (this.inputTypeView.getContext()?.formContext?.isAiEditable() && this.inputTypeView.isEditableByAI()) { - new AIHelper({ + new AiHelper({ dataPathElement: this.inputElement, getPathFunc: () => this.getDataPath(), icon: { diff --git a/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrenceView.ts index 8022f5686..ef10763d6 100644 --- a/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/set/FormSetOccurrenceView.ts @@ -1,40 +1,40 @@ import * as Q from 'q'; -import {PropertySet} from '../../data/PropertySet'; +import {AiHelper} from '../../ai/AiHelper'; +import {Property} from '../../data/Property'; +import {PropertyAddedEvent} from '../../data/PropertyAddedEvent'; import {PropertyArray} from '../../data/PropertyArray'; +import {PropertyPath} from '../../data/PropertyPath'; +import {PropertyRemovedEvent} from '../../data/PropertyRemovedEvent'; +import {PropertySet} from '../../data/PropertySet'; import {PropertyValueChangedEvent} from '../../data/PropertyValueChangedEvent'; -import {i18n} from '../../util/Messages'; import {Value} from '../../data/Value'; +import {ValueType} from '../../data/ValueType'; +import {ValueTypes} from '../../data/ValueTypes'; import {DivEl} from '../../dom/DivEl'; -import {PropertyPath} from '../../data/PropertyPath'; +import {Element} from '../../dom/Element'; +import {ObjectHelper} from '../../ObjectHelper'; +import {Action} from '../../ui/Action'; +import {MoreButton} from '../../ui/button/MoreButton'; +import {KeyBinding} from '../../ui/KeyBinding'; +import {KeyBindings} from '../../ui/KeyBindings'; +import {ConfirmationMask} from '../../ui/mask/ConfirmationMask'; +import {i18n} from '../../util/Messages'; +import {FormContext} from '../FormContext'; +import {FormItem} from '../FormItem'; +import {FormItemLayer} from '../FormItemLayer'; +import {FormItemOccurrence} from '../FormItemOccurrence'; import {FormItemOccurrenceView, FormItemOccurrenceViewConfig} from '../FormItemOccurrenceView'; import {FormItemView} from '../FormItemView'; -import {RecordingValidityChangedEvent} from '../RecordingValidityChangedEvent'; import {FormOccurrenceDraggableLabel} from '../FormOccurrenceDraggableLabel'; +import {Input} from '../Input'; +import {RadioButton} from '../inputtype/radiobutton/RadioButton'; +import {RecordingValidityChangedEvent} from '../RecordingValidityChangedEvent'; import {ValidationRecording} from '../ValidationRecording'; -import {FormItemLayer} from '../FormItemLayer'; import {ValidationRecordingPath} from '../ValidationRecordingPath'; import {FormSet} from './FormSet'; -import {FormItem} from '../FormItem'; -import {FormContext} from '../FormContext'; -import {Action} from '../../ui/Action'; -import {MoreButton} from '../../ui/button/MoreButton'; -import {ConfirmationMask} from '../../ui/mask/ConfirmationMask'; -import {Element} from '../../dom/Element'; -import {KeyBindings} from '../../ui/KeyBindings'; -import {KeyBinding} from '../../ui/KeyBinding'; -import {Property} from '../../data/Property'; -import {ValueType} from '../../data/ValueType'; -import {ValueTypes} from '../../data/ValueTypes'; -import {PropertyAddedEvent} from '../../data/PropertyAddedEvent'; -import {PropertyRemovedEvent} from '../../data/PropertyRemovedEvent'; +import {FormItemSet} from './itemset/FormItemSet'; import {FormOptionSet} from './optionset/FormOptionSet'; import {FormOptionSetOption} from './optionset/FormOptionSetOption'; -import {FormItemSet} from './itemset/FormItemSet'; -import {Input} from '../Input'; -import {RadioButton} from '../inputtype/radiobutton/RadioButton'; -import {ObjectHelper} from '../../ObjectHelper'; -import {FormItemOccurrence} from '../FormItemOccurrence'; -import {AIHelper} from '../../ai/AIHelper'; export interface FormSetOccurrenceViewConfig { context: FormContext; @@ -236,7 +236,7 @@ export abstract class FormSetOccurrenceView } }); - new AIHelper({ + new AiHelper({ dataPathElement: this, getPathFunc: () => this.getDataPath(), }); diff --git a/src/main/resources/assets/admin/common/js/form/set/optionset/FormOptionSetOptionView.ts b/src/main/resources/assets/admin/common/js/form/set/optionset/FormOptionSetOptionView.ts index 32e4aa44b..49eba7c3f 100644 --- a/src/main/resources/assets/admin/common/js/form/set/optionset/FormOptionSetOptionView.ts +++ b/src/main/resources/assets/admin/common/js/form/set/optionset/FormOptionSetOptionView.ts @@ -1,26 +1,26 @@ import * as $ from 'jquery'; import * as Q from 'q'; +import {AiHelper} from '../../../ai/AiHelper'; +import {PropertyPath, PropertyPathElement} from '../../../data/PropertyPath'; import {PropertySet} from '../../../data/PropertySet'; -import {Occurrences} from '../../Occurrences'; -import {Checkbox} from '../../../ui/Checkbox'; -import {NotificationDialog} from '../../../ui/dialog/NotificationDialog'; -import {i18n} from '../../../util/Messages'; -import {DivEl} from '../../../dom/DivEl'; import {DefaultErrorHandler} from '../../../DefaultErrorHandler'; +import {DivEl} from '../../../dom/DivEl'; import {Element} from '../../../dom/Element'; import {FormEl} from '../../../dom/FormEl'; -import {FormOptionSetOption} from './FormOptionSetOption'; -import {FormOptionSetOccurrenceView} from './FormOptionSetOccurrenceView'; +import {Checkbox} from '../../../ui/Checkbox'; +import {NotificationDialog} from '../../../ui/dialog/NotificationDialog'; +import {i18n} from '../../../util/Messages'; +import {FormItemLayer} from '../../FormItemLayer'; +import {CreatedFormItemLayerConfig, FormItemLayerFactory} from '../../FormItemLayerFactory'; +import {FormItemState} from '../../FormItemState'; import {FormItemView, FormItemViewConfig} from '../../FormItemView'; import {HelpTextContainer} from '../../HelpTextContainer'; -import {FormItemLayer} from '../../FormItemLayer'; -import {FormOptionSet} from './FormOptionSet'; +import {Occurrences} from '../../Occurrences'; import {RecordingValidityChangedEvent} from '../../RecordingValidityChangedEvent'; import {ValidationRecording} from '../../ValidationRecording'; -import {CreatedFormItemLayerConfig, FormItemLayerFactory} from '../../FormItemLayerFactory'; -import {FormItemState} from '../../FormItemState'; -import {AIHelper} from '../../../ai/AIHelper'; -import {PropertyPath, PropertyPathElement} from '../../../data/PropertyPath'; +import {FormOptionSet} from './FormOptionSet'; +import {FormOptionSetOccurrenceView} from './FormOptionSetOccurrenceView'; +import {FormOptionSetOption} from './FormOptionSetOption'; export interface FormOptionSetOptionViewConfig extends CreatedFormItemLayerConfig { @@ -67,7 +67,7 @@ export class FormOptionSetOptionView this.formItemLayer = config.layerFactory.createLayer(config); this.notificationDialog = new NotificationDialog(i18n('notify.optionset.notempty')); - new AIHelper({ + new AiHelper({ dataPathElement: this, getPathFunc: () => PropertyPath.fromParent(this.getParent().getDataPath(), new PropertyPathElement(this.getName(), 0)), }); diff --git a/src/main/resources/i18n/common.properties b/src/main/resources/i18n/common.properties index 8a544a91e..f92cf5b54 100644 --- a/src/main/resources/i18n/common.properties +++ b/src/main/resources/i18n/common.properties @@ -218,7 +218,8 @@ warning.optionsview.truncated = The list is truncated # # AI # -ai.assistant.processing={0} is being processed... +ai.action.contentOperator.use=Use in Content Operator +ai.field.processing={0} is being processed... # # Accessibility From 0a69d2a3100cd0e53b0d2d132e4264d610a42c4c Mon Sep 17 00:00:00 2001 From: Mikita Taukachou Date: Mon, 23 Sep 2024 18:22:00 +0300 Subject: [PATCH 16/17] Restore help text icon for non AI inputs #3716 --- .../assets/admin/common/js/ai/AiHelper.ts | 2 +- .../assets/admin/common/js/form/InputView.ts | 54 ++++++++++--------- .../form/inputtype/support/BaseInputType.ts | 19 ++++--- .../support/BaseInputTypeNotManagingAdd.ts | 31 +++++------ .../inputtype/support/InputOccurrenceView.ts | 2 +- .../js/form/inputtype/text/TextInputType.ts | 32 +++++------ .../admin/common/js/form/set/FormSetHeader.ts | 5 +- .../admin/common/styles/api/input-common.less | 19 +++++-- 8 files changed, 89 insertions(+), 75 deletions(-) diff --git a/src/main/resources/assets/admin/common/js/ai/AiHelper.ts b/src/main/resources/assets/admin/common/js/ai/AiHelper.ts index c81cc7d7e..60096182f 100644 --- a/src/main/resources/assets/admin/common/js/ai/AiHelper.ts +++ b/src/main/resources/assets/admin/common/js/ai/AiHelper.ts @@ -43,7 +43,7 @@ export class AiHelper { Store.instance().get(AI_HELPERS_KEY).push(this); - if (config.icon?.container) { + if (this.config.icon) { this.aiIcon = new AiActionButton(); this.config.icon.container.appendChild(this.aiIcon); } diff --git a/src/main/resources/assets/admin/common/js/form/InputView.ts b/src/main/resources/assets/admin/common/js/form/InputView.ts index c452b3647..cca6b36e8 100644 --- a/src/main/resources/assets/admin/common/js/form/InputView.ts +++ b/src/main/resources/assets/admin/common/js/form/InputView.ts @@ -1,31 +1,32 @@ import * as Q from 'q'; +import {Property} from '../data/Property'; import {PropertyArray} from '../data/PropertyArray'; import {PropertySet} from '../data/PropertySet'; -import {Property} from '../data/Property'; -import {BaseInputTypeNotManagingAdd} from './inputtype/support/BaseInputTypeNotManagingAdd'; -import {i18n} from '../util/Messages'; -import {StringHelper} from '../util/StringHelper'; -import {FormItemView, FormItemViewConfig} from './FormItemView'; -import {InputTypeView} from './inputtype/InputTypeView'; import {DivEl} from '../dom/DivEl'; +import {ObjectHelper} from '../ObjectHelper'; import {Button} from '../ui/button/Button'; -import {OccurrenceRemovedEvent} from './OccurrenceRemovedEvent'; -import {InputValidityChangedEvent} from './inputtype/InputValidityChangedEvent'; -import {InputTypeName} from './InputTypeName'; -import {InputValidationRecording} from './inputtype/InputValidationRecording'; +import {TogglerButton} from '../ui/button/TogglerButton'; +import {assertNotNull} from '../util/Assert'; +import {i18n} from '../util/Messages'; +import {StringHelper} from '../util/StringHelper'; import {FormContext} from './FormContext'; -import {Input} from './Input'; import {FormItemOccurrenceView} from './FormItemOccurrenceView'; -import {ValidationRecording} from './ValidationRecording'; -import {RecordingValidityChangedEvent} from './RecordingValidityChangedEvent'; +import {FormItemView, FormItemViewConfig} from './FormItemView'; import {HelpTextContainer} from './HelpTextContainer'; -import {assertNotNull} from '../util/Assert'; +import {Input} from './Input'; import {InputLabel} from './InputLabel'; import {InputTypeManager} from './inputtype/InputTypeManager'; -import {ValidationRecordingPath} from './ValidationRecordingPath'; -import {InputViewValidationViewer} from './InputViewValidationViewer'; -import {TogglerButton} from '../ui/button/TogglerButton'; +import {InputTypeView} from './inputtype/InputTypeView'; +import {InputValidationRecording} from './inputtype/InputValidationRecording'; +import {InputValidityChangedEvent} from './inputtype/InputValidityChangedEvent'; import {BaseInputType} from './inputtype/support/BaseInputType'; +import {BaseInputTypeNotManagingAdd} from './inputtype/support/BaseInputTypeNotManagingAdd'; +import {InputTypeName} from './InputTypeName'; +import {InputViewValidationViewer} from './InputViewValidationViewer'; +import {OccurrenceRemovedEvent} from './OccurrenceRemovedEvent'; +import {RecordingValidityChangedEvent} from './RecordingValidityChangedEvent'; +import {ValidationRecording} from './ValidationRecording'; +import {ValidationRecordingPath} from './ValidationRecordingPath'; export interface InputViewConfig { @@ -44,7 +45,7 @@ export class InputView public static debug: boolean = false; private static ERROR_DETAILS_HIDDEN_CLS: string = 'error-details-hidden'; - private input: Input; + private readonly input: Input; private parentPropertySet: PropertySet; private propertyArray: PropertyArray; private inputTypeView: InputTypeView; @@ -82,22 +83,27 @@ export class InputView } } - if (this.input.getHelpText()) { + this.inputTypeView = this.createInputTypeView(); + const isAiEditable = ObjectHelper.iFrameSafeInstanceOf(this.inputTypeView, BaseInputType) && + (this.inputTypeView as BaseInputType).isAiEditable(); + + this.toggleClass('ai-editable', isAiEditable); + + if (this.input.getHelpText() && !isAiEditable) { this.helpText = new HelpTextContainer(this.input.getHelpText()); + this.appendChild(this.helpText.getToggler()); } if (this.input.isMaximizeUIInputWidth() === false) { this.addClass('label-inline'); } - this.inputTypeView = this.createInputTypeView(); - this.propertyArray = this.getPropertyArray(this.parentPropertySet); return this.inputTypeView.layout(this.input, this.propertyArray).then(() => { this.appendChild(this.inputTypeView.getElement()); - if (!!this.helpText) { + if (this.helpText) { this.appendChild(this.helpText.getHelpText()); } @@ -279,9 +285,7 @@ export class InputView } toggleHelpText(show?: boolean) { - if (!!this.helpText) { - this.helpText.toggleHelpText(show); - } + this.helpText?.toggleHelpText(show); } hasHelpText(): boolean { diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputType.ts b/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputType.ts index 0cd023b71..3a5b1df44 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputType.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputType.ts @@ -1,16 +1,16 @@ -import {DivEl} from '../../../dom/DivEl'; -import {InputTypeView} from '../InputTypeView'; -import {Input} from '../../Input'; -import {InputValidityChangedEvent} from '../InputValidityChangedEvent'; -import {Element} from '../../../dom/Element'; -import {PropertyArray} from '../../../data/PropertyArray'; import * as Q from 'q'; -import {Value} from '../../../data/Value'; import {ClassHelper} from '../../../ClassHelper'; +import {PropertyArray} from '../../../data/PropertyArray'; +import {Value} from '../../../data/Value'; import {ValueType} from '../../../data/ValueType'; +import {DivEl} from '../../../dom/DivEl'; +import {Element} from '../../../dom/Element'; +import {Input} from '../../Input'; +import {InputTypeView} from '../InputTypeView'; +import {InputTypeViewContext} from '../InputTypeViewContext'; import {InputValidationRecording} from '../InputValidationRecording'; +import {InputValidityChangedEvent} from '../InputValidityChangedEvent'; import {ValueChangedEvent} from '../ValueChangedEvent'; -import {InputTypeViewContext} from '../InputTypeViewContext'; export abstract class BaseInputType extends DivEl implements InputTypeView { @@ -124,4 +124,7 @@ export abstract class BaseInputType extends DivEl // } + isAiEditable(): boolean { + return false; + } } diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputTypeNotManagingAdd.ts b/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputTypeNotManagingAdd.ts index 10d148c95..5390f7c6c 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputTypeNotManagingAdd.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputTypeNotManagingAdd.ts @@ -1,35 +1,34 @@ import * as $ from 'jquery'; import * as Q from 'q'; +import {ClassHelper} from '../../../ClassHelper'; import {Property} from '../../../data/Property'; import {PropertyArray} from '../../../data/PropertyArray'; import {Value} from '../../../data/Value'; +import {Element} from '../../../dom/Element'; import {FormInputEl} from '../../../dom/FormInputEl'; -import {InputTypeViewContext} from '../InputTypeViewContext'; +import {ObjectHelper} from '../../../ObjectHelper'; +import {assertNotNull} from '../../../util/Assert'; +import {i18n} from '../../../util/Messages'; +import {ValidationError} from '../../../ValidationError'; +import {AdditionalValidationRecord} from '../../AdditionalValidationRecord'; import {Input} from '../../Input'; -import {InputValidityChangedEvent} from '../InputValidityChangedEvent'; -import {Element} from '../../../dom/Element'; -import {InputValidationRecording} from '../InputValidationRecording'; import {OccurrenceAddedEvent} from '../../OccurrenceAddedEvent'; -import {OccurrenceRenderedEvent} from '../../OccurrenceRenderedEvent'; import {OccurrenceRemovedEvent} from '../../OccurrenceRemovedEvent'; +import {OccurrenceRenderedEvent} from '../../OccurrenceRenderedEvent'; +import {InputTypeViewContext} from '../InputTypeViewContext'; +import {InputValidationRecording} from '../InputValidationRecording'; +import {InputValidityChangedEvent} from '../InputValidityChangedEvent'; import {ValueChangedEvent} from '../ValueChangedEvent'; -import {ClassHelper} from '../../../ClassHelper'; -import {ObjectHelper} from '../../../ObjectHelper'; -import {InputOccurrenceView} from './InputOccurrenceView'; +import {BaseInputType} from './BaseInputType'; import {InputOccurrences} from './InputOccurrences'; -import {assertNotNull} from '../../../util/Assert'; +import {InputOccurrenceView} from './InputOccurrenceView'; import {OccurrenceValidationRecord} from './OccurrenceValidationRecord'; -import {BaseInputType} from './BaseInputType'; -import {ValidationError} from '../../../ValidationError'; -import {AdditionalValidationRecord} from '../../AdditionalValidationRecord'; -import {i18n} from '../../../util/Messages'; export abstract class BaseInputTypeNotManagingAdd extends BaseInputType { public static debug: boolean = false; protected propertyArray: PropertyArray; - protected ignorePropertyChange: boolean; protected occurrenceValidationState: Map = new Map(); private inputOccurrences: InputOccurrences; private occurrenceValueChangedListeners: ((occurrence: Element, value: Value) => void)[] = []; @@ -397,8 +396,4 @@ export abstract class BaseInputTypeNotManagingAdd } }); } - - isEditableByAI(): boolean { - return false; - } } diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts index 9aec840b2..15a501973 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/support/InputOccurrenceView.ts @@ -194,7 +194,7 @@ export class InputOccurrenceView this.property.onPropertyValueChanged(this.propertyValueChangedHandler); - if (this.inputTypeView.getContext()?.formContext?.isAiEditable() && this.inputTypeView.isEditableByAI()) { + if (this.inputTypeView.isAiEditable()) { new AiHelper({ dataPathElement: this.inputElement, getPathFunc: () => this.getDataPath(), diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/text/TextInputType.ts b/src/main/resources/assets/admin/common/js/form/inputtype/text/TextInputType.ts index 9ead1a40e..bd1b28959 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/text/TextInputType.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/text/TextInputType.ts @@ -1,21 +1,21 @@ -import {NumberHelper} from '../../../util/NumberHelper'; -import {FormInputEl} from '../../../dom/FormInputEl'; -import {ValueTypes} from '../../../data/ValueTypes'; -import {i18n} from '../../../util/Messages'; -import {BaseInputTypeNotManagingAdd} from '../support/BaseInputTypeNotManagingAdd'; -import {InputTypeViewContext} from '../InputTypeViewContext'; -import {Property} from '../../../data/Property'; -import {AdditionalValidationRecord} from '../../AdditionalValidationRecord'; -import {InputValueLengthCounterEl} from './InputValueLengthCounterEl'; import * as Q from 'q'; +import {Property} from '../../../data/Property'; import {Value} from '../../../data/Value'; -import {StringHelper} from '../../../util/StringHelper'; -import {Element, LangDirection} from '../../../dom/Element'; -import {ValueTypeConverter} from '../../../data/ValueTypeConverter'; -import {ValueChangedEvent} from '../../../ValueChangedEvent'; import {ValueType} from '../../../data/ValueType'; -import {TextInput} from '../../../ui/text/TextInput'; +import {ValueTypeConverter} from '../../../data/ValueTypeConverter'; +import {ValueTypes} from '../../../data/ValueTypes'; +import {Element, LangDirection} from '../../../dom/Element'; +import {FormInputEl} from '../../../dom/FormInputEl'; import {Locale} from '../../../locale/Locale'; +import {TextInput} from '../../../ui/text/TextInput'; +import {i18n} from '../../../util/Messages'; +import {NumberHelper} from '../../../util/NumberHelper'; +import {StringHelper} from '../../../util/StringHelper'; +import {ValueChangedEvent} from '../../../ValueChangedEvent'; +import {AdditionalValidationRecord} from '../../AdditionalValidationRecord'; +import {InputTypeViewContext} from '../InputTypeViewContext'; +import {BaseInputTypeNotManagingAdd} from '../support/BaseInputTypeNotManagingAdd'; +import {InputValueLengthCounterEl} from './InputValueLengthCounterEl'; export abstract class TextInputType extends BaseInputTypeNotManagingAdd { @@ -162,7 +162,7 @@ export abstract class TextInputType }); } - isEditableByAI(): boolean { - return true; + isAiEditable(): boolean { + return this.getContext().formContext?.isAiEditable() === true; } } diff --git a/src/main/resources/assets/admin/common/js/form/set/FormSetHeader.ts b/src/main/resources/assets/admin/common/js/form/set/FormSetHeader.ts index 3e33b1ecc..70cb7e5c3 100644 --- a/src/main/resources/assets/admin/common/js/form/set/FormSetHeader.ts +++ b/src/main/resources/assets/admin/common/js/form/set/FormSetHeader.ts @@ -1,7 +1,7 @@ -import {H5El} from '../../dom/H5El'; -import {HelpTextContainer} from '../HelpTextContainer'; import {DivEl} from '../../dom/DivEl'; +import {H5El} from '../../dom/H5El'; import {SpanEl} from '../../dom/SpanEl'; +import {HelpTextContainer} from '../HelpTextContainer'; import {FormSet} from './FormSet'; export class FormSetHeader @@ -27,6 +27,7 @@ export class FormSetHeader this.appendChild(this.title); if (this.helpTextContainer) { const helpTextDiv = this.helpTextContainer.getHelpText(); + this.prependChild(this.helpTextContainer.getToggler()); if (helpTextDiv) { this.appendChild(helpTextDiv); } diff --git a/src/main/resources/assets/admin/common/styles/api/input-common.less b/src/main/resources/assets/admin/common/styles/api/input-common.less index 8af746ef1..a9bb99254 100644 --- a/src/main/resources/assets/admin/common/styles/api/input-common.less +++ b/src/main/resources/assets/admin/common/styles/api/input-common.less @@ -15,6 +15,21 @@ .input-glow(); } + &:not(.ai-editable) { + > .input-label, + > label { + display: inline-block; + max-width: calc(100% - 18px); + .ellipsis(); + } + } + + &.ai-editable { + .help-text-toggler { + display: none; + } + } + > .input-label { .@{_COMMON_PREFIX}wrapper { display: flex; @@ -45,10 +60,6 @@ } > label { - display: inline-block; - max-width: calc(100% - 30px); - .ellipsis(); - &.required::after { content: '\00a0*'; color: @input-red-text; From 654fa3002272bf630429bd1782a8a96a5b261640 Mon Sep 17 00:00:00 2001 From: Mikita Taukachou Date: Wed, 2 Oct 2024 21:39:08 +0300 Subject: [PATCH 17/17] Replace Saga with Juke icon #3732 Added regular and optimized Juke icons for inputs (both slightly different from the one we use in apps). Optimized AI related styles. Replaced Saga with Juke, check, and cross icons. Stopped animations, when status is completed or failed. --- .../assets/admin/common/icons/icons.less | 8 -- .../images/juke-eye-centered-animated.svg | 20 +++++ .../admin/common/images/juke-eye-centered.svg | 16 ++++ .../admin/common/js/ai/ui/AiActionButton.ts | 2 +- .../support/input-occurrence-view.less | 89 ++++++++++--------- 5 files changed, 82 insertions(+), 53 deletions(-) create mode 100644 src/main/resources/assets/admin/common/images/juke-eye-centered-animated.svg create mode 100644 src/main/resources/assets/admin/common/images/juke-eye-centered.svg diff --git a/src/main/resources/assets/admin/common/icons/icons.less b/src/main/resources/assets/admin/common/icons/icons.less index 7e8cd4f16..35c1b439f 100644 --- a/src/main/resources/assets/admin/common/icons/icons.less +++ b/src/main/resources/assets/admin/common/icons/icons.less @@ -602,11 +602,3 @@ content: "\e9c9"; } } - -.icon-ai { - .icon(); - - &::before { - content: "\e922"; - } -} diff --git a/src/main/resources/assets/admin/common/images/juke-eye-centered-animated.svg b/src/main/resources/assets/admin/common/images/juke-eye-centered-animated.svg new file mode 100644 index 000000000..a9d87af86 --- /dev/null +++ b/src/main/resources/assets/admin/common/images/juke-eye-centered-animated.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/src/main/resources/assets/admin/common/images/juke-eye-centered.svg b/src/main/resources/assets/admin/common/images/juke-eye-centered.svg new file mode 100644 index 000000000..d10b0af49 --- /dev/null +++ b/src/main/resources/assets/admin/common/images/juke-eye-centered.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/src/main/resources/assets/admin/common/js/ai/ui/AiActionButton.ts b/src/main/resources/assets/admin/common/js/ai/ui/AiActionButton.ts index eea7951d2..3be0bd2e3 100644 --- a/src/main/resources/assets/admin/common/js/ai/ui/AiActionButton.ts +++ b/src/main/resources/assets/admin/common/js/ai/ui/AiActionButton.ts @@ -24,7 +24,7 @@ export class AiActionButton } protected initElements(): void { - this.button = new Button().addClass(`${AiActionButton.BASE_CLASS}-icon icon-ai`) as Button; + this.button = new Button().addClass(`${AiActionButton.BASE_CLASS}-icon ai-icon`) as Button; this.loader = new DivEl(`${AiActionButton.BASE_CLASS}-loader`); this.setTitle(i18n('ai.action.contentOperator.use')); this.setState(AiHelperState.DEFAULT); diff --git a/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less b/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less index be3d4eabd..3b1f31ccc 100644 --- a/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less +++ b/src/main/resources/assets/admin/common/styles/api/form/inputtype/support/input-occurrence-view.less @@ -50,7 +50,6 @@ .ai-button-container { width: 20px; height: 20px; - display: none; border: none; position: absolute; z-index: 1; @@ -70,38 +69,58 @@ &-loader { box-sizing: border-box; - display: none; position: absolute; - border: 2px dotted @admin-button-blue2; + border: 2px dotted @admin-dark-gray-border; border-radius: 50%; width: 20px; height: 20px; animation: spin 3s linear infinite; } - &.processing, &.completed, &.failed { - display: block; - - .ai-button-container-loader { - display: block; + @keyframes spin { + 100% { + transform: rotateZ(360deg); } } + &:not(:hover):not(:focus):not(:focus-within).default { + display: none; + } + + &.default, &.processing { + .ai-icon::before { + content: ''; + display: inline-block; + width: 1em; + height: 1em; + } + } + + &.completed, + &.failed { .ai-button-container-loader { - border-color: @admin-button-blue2; + animation-play-state: paused; } } - &.fade { - display: block; + &.default { + .ai-button-container-loader { + display: none; + } - .ai-button-container-icon { - animation: fadingOpacity 2s ease-in-out infinite; + .ai-icon::before { + background: url("../../../../../images/juke-eye-centered.svg") center / contain no-repeat; + } + } - &:before { - color: @admin-blue; - } + &.processing { + .ai-button-container-loader { + border-color: #550072; + } + + .ai-icon::before { + background: url("../../../../../images/juke-eye-centered-animated.svg") center / contain no-repeat; } } @@ -110,9 +129,12 @@ border-color: @admin-green; } - .ai-button-container-icon { - &:before { + .ai-icon { + .icon-checkmark(); + + &::before { color: @admin-green; + transform: scale(0.8); } } } @@ -122,36 +144,15 @@ border-color: @admin-red; } - .ai-button-container-icon { - &:before { + .ai-icon { + .icon-close(); + + &::before { color: @admin-red; + transform: scale(0.9); } } } - - @keyframes spin { - 100% { - transform: rotateZ(360deg); - } - } - - @keyframes fadingOpacity { - 0% { - opacity: 0.2; - } - 50% { - opacity: 1; - } - 100% { - opacity: 0.2; - } - } - } - - &:focus-within, &:hover { - .ai-button-container { - display: block; - } } &.single-occurrence {