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: 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 d58b3faeb..0f2bf7b55 Binary files a/src/main/resources/assets/admin/common/icons/fonts/icomoon.woff and b/src/main/resources/assets/admin/common/icons/fonts/icomoon.woff differ diff --git a/src/main/resources/assets/admin/common/icons/fonts/icomoon.woff2 b/src/main/resources/assets/admin/common/icons/fonts/icomoon.woff2 index dc22206f4..caf47e15d 100644 Binary files a/src/main/resources/assets/admin/common/icons/fonts/icomoon.woff2 and b/src/main/resources/assets/admin/common/icons/fonts/icomoon.woff2 differ 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/AiHelper.ts b/src/main/resources/assets/admin/common/js/ai/AiHelper.ts new file mode 100644 index 000000000..60096182f --- /dev/null +++ b/src/main/resources/assets/admin/common/js/ai/AiHelper.ts @@ -0,0 +1,120 @@ +import {PropertyPath} from '../data/PropertyPath'; +import {Element} from '../dom/Element'; +import {Store} from '../store/Store'; +import {i18n} from '../util/Messages'; +import {AiHelperState} from './AiHelperState'; +import {AiActionButton} from './ui/AiActionButton'; + +export interface AiHelperConfig { + dataPathElement: Element; + getPathFunc: () => PropertyPath; + icon?: { + container: Element; + }; + label?: string; + setValueFunc?: (value: string) => void; +} + +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 aiIcon?: AiActionButton; + + private state: AiHelperState = AiHelperState.DEFAULT; + + constructor(config: AiHelperConfig) { + this.config = config; + + const updatePathCall = setInterval(() => { + this.updateInputElDataPath(); + }, 1000); + + this.config.dataPathElement.onRemoved(() => { + clearInterval(updatePathCall); + const helper: AiHelper[] = Store.instance().get(AI_HELPERS_KEY) ?? []; + Store.instance().set(AI_HELPERS_KEY, helper.filter(h => h !== this)); + }); + + Store.instance().get(AI_HELPERS_KEY).push(this); + + if (this.config.icon) { + 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.aiIcon?.setDataPath(dataPath); + } + + setState(state: AiHelperState): this { + if (state === this.state) { + return this; + } + + this.state = state; + this.aiIcon?.setState(state); + + if (state === AiHelperState.COMPLETED || state === AiHelperState.FAILED) { + setTimeout(() => { + 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 === AiHelperState.PROCESSING) { + this.config.dataPathElement.getEl().setDisabled(true); + this.config.dataPathElement.addClass('ai-helper-mask'); + this.updateTitle(); + } + + return this; + } + + setValue(value: string): this { + this.config.setValueFunc?.(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 Store.instance().get(AI_HELPERS_KEY).find(helper => helper.getDataPath() === 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.field.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')); + } + } +} 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..762ef703d --- /dev/null +++ b/src/main/resources/assets/admin/common/js/ai/AiHelperState.ts @@ -0,0 +1,6 @@ +export enum AiHelperState { + DEFAULT = 'default', + PROCESSING = 'processing', + COMPLETED = 'completed', + FAILED = 'failed', +} diff --git a/src/main/resources/assets/admin/common/js/ai/event/EnonicAiContentOperatorOpenDialogEvent.ts b/src/main/resources/assets/admin/common/js/ai/event/EnonicAiContentOperatorOpenDialogEvent.ts new file mode 100644 index 000000000..7e5a0c0c9 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/ai/event/EnonicAiContentOperatorOpenDialogEvent.ts @@ -0,0 +1,27 @@ +import {ClassHelper} from '../../ClassHelper'; +import {Event} from '../../event/Event'; + +export class EnonicAiContentOperatorOpenDialogEvent + extends Event { + + private readonly sourceDataPath?: string; + + constructor(dataPath?: string) { + super(); + + this.sourceDataPath = dataPath; + } + + getSourceDataPath(): string | undefined { + return this.sourceDataPath; + } + + static on(handler: (event: EnonicAiContentOperatorOpenDialogEvent) => void) { + Event.bind(ClassHelper.getFullName(this), handler); + } + + 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 new file mode 100644 index 000000000..3be0bd2e3 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/ai/ui/AiActionButton.ts @@ -0,0 +1,63 @@ +import * as Q from 'q'; +import {DivEl} from '../../dom/DivEl'; +import {Button} from '../../ui/button/Button'; +import {i18n} from '../../util/Messages'; +import {AiHelperState} from '../AiHelperState'; +import {EnonicAiContentOperatorOpenDialogEvent} from '../event/EnonicAiContentOperatorOpenDialogEvent'; + +export class AiActionButton + extends DivEl { + + private static readonly BASE_CLASS = 'ai-button-container'; + + private dataPath?: string; + + private button: Button; + + private loader: DivEl; + + constructor() { + super(); + + this.initElements(); + this.initListeners(); + } + + protected initElements(): void { + 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); + } + + setState(state: AiHelperState): this { + this.setClass(`${AiActionButton.BASE_CLASS} ${state}`); + + return this; + } + + setDataPath(dataPath: string): AiActionButton { + this.dataPath = dataPath; + return this; + } + + getDataPath(): string { + return this.dataPath; + } + + protected initListeners(): void { + this.button.onClicked(() => { + if (this.dataPath) { + new EnonicAiContentOperatorOpenDialogEvent(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/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/FormItemOccurrenceView.ts b/src/main/resources/assets/admin/common/js/form/FormItemOccurrenceView.ts index 19fb82bfc..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,22 +1,44 @@ import * as Q from 'q'; -import {DivEl} from '../dom/DivEl'; import {PropertyPath} from '../data/PropertyPath'; -import {InputValidationRecording} from './inputtype/InputValidationRecording'; +import {DivEl} from '../dom/DivEl'; import {FormItemOccurrence} from './FormItemOccurrence'; import {HelpTextContainer} from './HelpTextContainer'; import {RemoveButtonClickedEvent} from './RemoveButtonClickedEvent'; +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 { + // + } + + protected postInitElements() { + // } isExpandable(): boolean { @@ -110,4 +132,5 @@ export abstract class FormItemOccurrenceView setEnabled(enable: boolean) { // } + } 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..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,9 +83,14 @@ export class InputView } } - if (this.input.getHelpText()) { - this.helpText = new HelpTextContainer(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()); } @@ -92,14 +98,12 @@ export class InputView 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()); } @@ -281,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 4ccd6984a..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)[] = []; 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..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 @@ -1,24 +1,32 @@ 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} 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'; +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; + private inputWrapper: DivEl; private removeButtonEl: ButtonEl; private dragControl: DivEl; private propertyValueChangedHandler: (event: PropertyValueChangedEvent) => void; @@ -26,20 +34,24 @@ 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.inputWrapper = new DivEl('input-wrapper'); + + 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(); @@ -48,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.prependChild(this.inputElement); dataBlock.appendChild(this.removeButtonEl); this.appendChild(this.validationErrorBlock); @@ -86,13 +97,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 +111,7 @@ export class InputOccurrenceView } getIndex(): number { - return this.inputOccurrence.getIndex(); + return this.formItemOccurrence.getIndex(); } getInputElement(): Element { @@ -131,7 +142,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) => { @@ -180,6 +193,21 @@ export class InputOccurrenceView }); this.property.onPropertyValueChanged(this.propertyValueChangedHandler); + + if (this.inputTypeView.isAiEditable()) { + new AiHelper({ + dataPathElement: this.inputElement, + getPathFunc: () => this.getDataPath(), + icon: { + 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); + } + }); + } } private registerProperty(property: Property) { 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..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 { @@ -161,4 +161,8 @@ export abstract class TextInputType return rendered; }); } + + 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 13ac1829b..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 @@ -26,8 +26,8 @@ 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(); + this.prependChild(this.helpTextContainer.getToggler()); if (helpTextDiv) { this.appendChild(helpTextDiv); } 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..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,46 +1,47 @@ 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 {FormItemOccurrenceView} from '../FormItemOccurrenceView'; +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 {FormSetOccurrence} from './FormSetOccurrence'; -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'; export interface FormSetOccurrenceViewConfig { context: FormContext; layer: FormItemLayer; - formSetOccurrence: FormSetOccurrence; + formItemOccurrence: FormItemOccurrence; formSet: FormSet; @@ -49,10 +50,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 +77,8 @@ export abstract class FormSetOccurrenceView protected formSet: FormSet; + protected config: FormSetOccurrenceViewConfigExtended; + private dirtyFormItemViewsMap: object = {}; private deleteConfirmationMask: ConfirmationMask; @@ -87,17 +94,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 +143,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 +166,9 @@ export abstract class FormSetOccurrenceView .build(); } - protected postInitElements() { + protected postInitElements(): void { + super.postInitElements(); + this.label.setExpandable(this.isExpandable()); if (!this.isExpandable()) { @@ -166,6 +177,8 @@ export abstract class FormSetOccurrenceView } protected initListeners() { + super.initListeners(); + this.label.onClicked(() => this.setContainerVisible(!this.isContainerVisible())); this.confirmDeleteAction.onExecuted(() => { @@ -222,6 +235,11 @@ export abstract class FormSetOccurrenceView this.releasePropertySet(this.propertySet); } }); + + new AiHelper({ + dataPathElement: this, + getPathFunc: () => this.getDataPath(), + }); } private initOccurrencesContainer(): void { @@ -698,4 +716,5 @@ export abstract class FormSetOccurrenceView return new MoreButton([addAboveAction, addBelowAction, removeAction]); } + } 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/optionset/FormOptionSetOptionView.ts b/src/main/resources/assets/admin/common/js/form/set/optionset/FormOptionSetOptionView.ts index 1d492ffd7..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,24 +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 {FormOptionSet} from './FormOptionSet'; +import {FormOptionSetOccurrenceView} from './FormOptionSetOccurrenceView'; +import {FormOptionSetOption} from './FormOptionSetOption'; export interface FormOptionSetOptionViewConfig extends CreatedFormItemLayerConfig { @@ -41,7 +43,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,17 +56,21 @@ export class FormOptionSetOptionView parent: config.parent } as FormItemViewConfig); - this.formOptionSetOption = config.formOptionSetOption; + this.initElements(config); + } + 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')); + + new AiHelper({ + dataPathElement: this, + getPathFunc: () => PropertyPath.fromParent(this.getParent().getDataPath(), new PropertyPathElement(this.getName(), 0)), + }); } toggleHelpText(show?: boolean) { @@ -95,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(); 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..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 @@ -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,114 @@ color: @input-red-text; } + .ai-button-container { + width: 20px; + height: 20px; + border: none; + position: absolute; + z-index: 1; + top: 0; + right: 0; + + &-icon { + .button(@admin-font-gray3, transparent, @admin-font-gray2); + padding: 2px; + width: 100%; + height: 100%; + font-size: 16px; + display: flex; + justify-content: center; + align-items: center; + } + + &-loader { + box-sizing: border-box; + position: absolute; + border: 2px dotted @admin-dark-gray-border; + border-radius: 50%; + width: 20px; + height: 20px; + animation: spin 3s linear infinite; + } + + @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 { + animation-play-state: paused; + } + } + + &.default { + .ai-button-container-loader { + display: none; + } + + .ai-icon::before { + background: url("../../../../../images/juke-eye-centered.svg") center / contain no-repeat; + } + } + + &.processing { + .ai-button-container-loader { + border-color: #550072; + } + + .ai-icon::before { + background: url("../../../../../images/juke-eye-centered-animated.svg") center / contain no-repeat; + } + } + + &.completed { + .ai-button-container-loader { + border-color: @admin-green; + } + + .ai-icon { + .icon-checkmark(); + + &::before { + color: @admin-green; + transform: scale(0.8); + } + } + } + + &.failed { + .ai-button-container-loader { + border-color: @admin-red; + } + + .ai-icon { + .icon-close(); + + &::before { + color: @admin-red; + transform: scale(0.9); + } + } + } + } + &.single-occurrence { .data-block { > .drag-control, @@ -53,6 +162,10 @@ display: none; } } + + .ai-button-container { + top: -21px; + } } &.multiple-occurrence { @@ -72,5 +185,25 @@ .error-block { padding: 0 0 10px 17px; } + + .ai-button-container { + top: 4px; + right: 4px; + + &:not(.processing) { + background-color: @admin-white; + + &:hover { + background-color: @admin-white; + } + } + + } } } + +.ai-helper-mask { + background-color: @admin-bg-light-gray; + opacity: 0.75; + border-color: @admin-medium-gray-border !important; +} 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..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,10 +15,22 @@ .input-glow(); } - > .input-label { - display: inline-block; - max-width: calc(100% - 30px); + &: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; white-space: nowrap; @@ -35,13 +47,19 @@ color: @input-red-text; } } + + &:focus-within, &:hover { + + .input-type-view { + > .input-occurrence-view:first-child { + .ai-button-container { + display: block; // showing AI icon for the first occurrence of the input on hover over label + } + } + } + } } > label { - display: inline-block; - max-width: calc(100% - 30px); - .ellipsis(); - &.required::after { content: '\00a0*'; color: @input-red-text; diff --git a/src/main/resources/i18n/common.properties b/src/main/resources/i18n/common.properties index 32ce802f0..f92cf5b54 100644 --- a/src/main/resources/i18n/common.properties +++ b/src/main/resources/i18n/common.properties @@ -215,6 +215,12 @@ tooltip.header.collapse=Click to collapse # warning.optionsview.truncated = The list is truncated +# +# AI +# +ai.action.contentOperator.use=Use in Content Operator +ai.field.processing={0} is being processed... + # # Accessibility #