import * as ko from "knockout"; import Q from "q"; import DiscardIcon from "../../../images/discard.svg"; import SaveIcon from "../../../images/save-cosmos.svg"; import * as Constants from "../../Common/Constants"; import editable from "../../Common/EditableUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { loadMonaco, monaco } from "../LazyMonaco"; import TabsBase from "./TabsBase"; export default abstract class ScriptTabBase extends TabsBase implements ViewModels.WaitsForTemplate { public ariaLabel: ko.Observable; public editorState: ko.Observable; public id: ViewModels.Editable; public editorContent: ViewModels.Editable; public editorId: string; public editor: ko.Observable; public executeButton: ViewModels.Button; public saveButton: ViewModels.Button; public updateButton: ViewModels.Button; public discardButton: ViewModels.Button; public deleteButton: ViewModels.Button; public errors: ko.ObservableArray; public statusMessge: ko.Observable; public statusIcon: ko.Observable; public formFields: ko.ObservableArray>; public formIsValid: ko.Computed; public formIsDirty: ko.Computed; public isNew: ko.Observable; // TODO: Remove any. The SDK types for all the script.body are slightly incorrect which makes this REALLY hard to type correct. public resource: ko.Observable; public isTemplateReady: ko.Observable; protected _partitionKey: DataModels.PartitionKey; constructor(options: ViewModels.ScriptTabOption) { super(options); this._partitionKey = options.partitionKey; this.isNew = ko.observable(options.isNew); this.resource = ko.observable(options.resource); this.isTemplateReady = ko.observable(false); this.isTemplateReady.subscribe((isTemplateReady: boolean) => { if (isTemplateReady) { // setTimeout is needed as creating the edtior manipulates the dom directly and expects // Knockout to have completed all of the initial bindings for the component setTimeout(() => this._createBodyEditor(), Constants.ClientDefaults.waitForDOMElementMs); } }); this.editorId = `editor_${this.tabId}`; this.ariaLabel = ko.observable(); if (this.isNew()) { this.editorState = ko.observable(ViewModels.ScriptEditorState.newInvalid); } else { this.editorState = ko.observable(ViewModels.ScriptEditorState.exisitingNoEdits); } this.id = editable.observable(); this.id.validations([ScriptTabBase._isValidId]); this.editorContent = editable.observable(); this.editorContent.validations([ScriptTabBase._isNotEmpty]); this.formFields = ko.observableArray([this.id, this.editorContent]); this._setBaselines(); this.id.editableIsValid.subscribe((isValid) => { const currentState = this.editorState(); switch (currentState) { case ViewModels.ScriptEditorState.newValid: case ViewModels.ScriptEditorState.newInvalid: if (isValid) { this.editorState(ViewModels.ScriptEditorState.newValid); } else { this.editorState(ViewModels.ScriptEditorState.newInvalid); } break; case ViewModels.ScriptEditorState.exisitingDirtyInvalid: case ViewModels.ScriptEditorState.exisitingDirtyValid: if (isValid) { this.editorState(ViewModels.ScriptEditorState.exisitingDirtyValid); } else { this.editorState(ViewModels.ScriptEditorState.exisitingDirtyInvalid); } break; case ViewModels.ScriptEditorState.exisitingDirtyValid: default: break; } }); this.editor = ko.observable(); this.formIsValid = ko.computed(() => { const formIsValid: boolean = this.formFields().every((field) => { return field.editableIsValid(); }); return formIsValid; }); this.formIsDirty = ko.computed(() => { const formIsDirty: boolean = this.formFields().some((field) => { return field.editableIsDirty(); }); return formIsDirty; }); this.saveButton = { enabled: ko.computed(() => { if (!this.formIsValid()) { return false; } if (!this.formIsDirty()) { return false; } return true; }), visible: ko.computed(() => { return this.isNew(); }), }; this.updateButton = { enabled: ko.computed(() => { if (!this.formIsValid()) { return false; } if (!this.formIsDirty()) { return false; } return true; }), visible: ko.computed(() => { return !this.isNew(); }), }; this.discardButton = { enabled: ko.computed(() => { return this.formIsDirty(); }), visible: ko.computed(() => { return true; }), }; this.deleteButton = { enabled: ko.computed(() => { return !this.isNew(); }), visible: ko.computed(() => { return true; }), }; this.executeButton = { enabled: ko.computed(() => { return !this.isNew() && !this.formIsDirty() && this.formIsValid(); }), visible: ko.computed(() => { return true; }), }; } private _setBaselines() { const resource = this.resource(); this.id.setBaseline(resource.id); this.editorContent.setBaseline(resource.body); } public setBaselines() { this._setBaselines(); } public onTabClick(): void { super.onTabClick(); if (this.isNew()) { this.collection.selectedSubnodeKind(this.tabKind); } } public abstract onSaveClick: () => void; public abstract onUpdateClick: () => Promise; public onDiscard = (): Q.Promise => { this.setBaselines(); const original = this.editorContent.getEditableOriginalValue(); const editorModel = this.editor() && this.editor().getModel(); editorModel && editorModel.setValue(original); return Q(); }; protected getTabsButtons(): CommandButtonComponentProps[] { const buttons: CommandButtonComponentProps[] = []; const label = "Save"; if (this.saveButton.visible()) { buttons.push({ iconSrc: SaveIcon, iconAlt: label, onCommandClick: this.onSaveClick, commandButtonLabel: label, ariaLabel: label, hasPopup: false, disabled: !this.saveButton.enabled(), }); } if (this.updateButton.visible()) { const label = "Update"; buttons.push({ iconSrc: SaveIcon, iconAlt: label, onCommandClick: this.onUpdateClick, commandButtonLabel: label, ariaLabel: label, hasPopup: false, disabled: !this.updateButton.enabled(), }); } if (this.discardButton.visible()) { const label = "Discard"; buttons.push({ iconSrc: DiscardIcon, iconAlt: label, onCommandClick: this.onDiscard, commandButtonLabel: label, ariaLabel: label, hasPopup: false, disabled: !this.discardButton.enabled(), }); } return buttons; } protected buildCommandBarOptions(): void { ko.computed(() => ko.toJSON([ this.saveButton.visible, this.saveButton.enabled, this.updateButton.visible, this.updateButton.enabled, this.discardButton.visible, this.discardButton.enabled, ]) ).subscribe(() => this.updateNavbarWithTabsButtons()); this.updateNavbarWithTabsButtons(); } private static _isValidId(id: string): boolean { if (!id) { return false; } const invalidStartCharacters = /^[/?#\\]/; if (invalidStartCharacters.test(id)) { return false; } const invalidMiddleCharacters = /^.+[/?#\\]/; if (invalidMiddleCharacters.test(id)) { return false; } const invalidEndCharacters = /.*[/?#\\ ]$/; if (invalidEndCharacters.test(id)) { return false; } return true; } private static _isNotEmpty(value: string): boolean { return !!value; } protected async _createBodyEditor() { const id = this.editorId; const container = document.getElementById(id); const options = { value: this.editorContent(), language: "javascript", readOnly: false, ariaLabel: this.ariaLabel(), }; container.innerHTML = ""; const monaco = await loadMonaco(); const editor = monaco.editor.create(container, options); this.editor(editor); const editorModel = editor.getModel(); editorModel.onDidChangeContent(this._onBodyContentChange.bind(this)); } private _onBodyContentChange(e: monaco.editor.IModelContentChangedEvent) { const editorModel = this.editor().getModel(); this.editorContent(editorModel.getValue()); } }