Files
raven-client/ayanova/src/views/ay-report-edit.vue
2020-09-02 15:10:20 +00:00

864 lines
26 KiB
Vue

<template>
<div v-resize="onResize">
<!-- {{ formState }} -->
<v-row>
<v-col cols="12" sm="6" class="py-2">
<v-btn-toggle
v-model="activeTab"
tile
color="deep-purple accent-3"
group
@change="onViewChange"
mandatory
>
<v-btn value="properties">
{{ $ay.t("ReportEditorProperties") }}
</v-btn>
<v-btn value="template">
{{ $ay.t("ReportTemplate") }}
</v-btn>
<v-btn value="style">
CSS
</v-btn>
<v-btn value="jsPrerender">
Pre-render
</v-btn>
<v-btn value="jsHelpers">
Helpers
</v-btn>
<v-btn value="rawData" v-if="reportData != null">
{{ $ay.t("ReportEditorData") }}
</v-btn>
</v-btn-toggle>
</v-col>
<v-col cols="12" v-show="showEditor">
<div id="editContainer"></div>
</v-col>
<v-col cols="12" v-show="activeTab == 'properties'">
<v-form ref="form">
<v-row>
<gz-error :errorBoxMessage="formState.errorBoxMessage"></gz-error>
<v-col cols="12" sm="6" lg="4" xl="3">
<v-text-field
v-model="obj.name"
:readonly="formState.readOnly"
:disabled="formState.readOnly"
:clearable="!formState.readOnly"
@click:clear="fieldValueChanged('name')"
:counter="255"
:label="$ay.t('ReportName')"
:rules="[
form().max255(this, 'name'),
form().required(this, 'name')
]"
:error-messages="form().serverErrors(this, 'name')"
ref="name"
:data-cy="!!$ay.dev ? 'name' : false"
@input="fieldValueChanged('name')"
></v-text-field>
</v-col>
<v-col cols="12" sm="6" lg="4" xl="3">
<v-checkbox
v-model="obj.active"
:readonly="formState.readOnly"
:disabled="formState.readOnly"
:label="$ay.t('Active')"
ref="active"
:data-cy="!!$ay.dev ? 'active' : false"
:error-messages="form().serverErrors(this, 'active')"
@change="fieldValueChanged('active')"
></v-checkbox>
</v-col>
<v-col cols="12" sm="6" lg="4" xl="3">
<gz-role-picker
:label="$ay.t('AuthorizationRoles')"
v-model="obj.roles"
:readonly="formState.readOnly"
:disabled="formState.readOnly"
ref="roles"
testId="roles"
:error-messages="form().serverErrors(this, 'roles')"
@input="fieldValueChanged('roles')"
limitSelectionTo="inside"
></gz-role-picker>
</v-col>
<v-col cols="12">
<v-textarea
v-model="obj.notes"
:readonly="formState.readOnly"
:disabled="formState.readOnly"
:label="$ay.t('ReportNotes')"
:error-messages="form().serverErrors(this, 'notes')"
ref="notes"
:data-cy="!!$ay.dev ? 'notes' : false"
@input="fieldValueChanged('notes')"
auto-grow
:clearable="!formState.readOnly"
></v-textarea>
</v-col>
</v-row>
</v-form>
</v-col>
</v-row>
</div>
</template>
<script>
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
/* Xeslint-disable */
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////
import * as monaco from "monaco-editor";
//jsreport demo
//https://playground.jsreport.net/w/admin/hBfqC7af
//jsreport text editor source
//https://github.com/jsreport/jsreport-studio/blob/master/src/components/Editor/TextEditor.js
//Monaco editor info page with links
//https://microsoft.github.io/monaco-editor/
//vue-monaco component source
//https://github.com/egoist/vue-monaco/blob/master/src/MonacoEditor.js
/*TODO:
report selector
direct edit or render report from list
Modify report editor to handle lack of data
template roundtrip save and open template from server
decision: render here or at server in designer for preview of report?
going to happen a lot during design build cycle, faster to do locally?
decision: how to handle opening report templates in the UI - should every reportdata object have built in fake data stored with report template?
It will be a bit shittier if can't open the report template directly and see *something* in it like from history and event log
Maybe if a type but no records are provided it can infer and just find a record to populate with like picks the first widget that exists or something?
Or, maybe it just saves the initial data used to last edit the report - NO this would expose real data in the editor which could be a rights issue situationally
in v7 you can't just open a report template directly can you?
Or, maybe it just handles that situation in the editor, no data, no problem, just can't preview the report, can only edit / view the template, and have to go through an object to view it with data
This seems like a good interim way to handle it and also easy peasy to code, just accepts if it doesn't have the idlist or whatever and disables preview mode
working widget template
Immediately render last report code needs proper implementation for both widget and widgets
currently it's nonsense
print / preview coded to work with saved report and renders
build release for joyce to play with and give feedback
*/
const FORM_KEY = "ay-report-edit";
let JUST_DELETED = false;
let editor = null;
export default {
async created() {
let vm = this;
try {
await initForm(vm);
vm.rights = window.$gz.role.getRights(window.$gz.type.Report);
vm.formState.readOnly = !vm.rights.change;
window.$gz.eventBus.$on("menu-click", clickHandler);
//route params MUST have source data
// if (!vm.$route.params.reportDataOptions) {
// throw "ay-report-edit::created - missing reportDataOptions route parameter";
// }
//id 0 means create a new record don't load one
if (vm.$route.params.recordid != 0) {
//is there already an obj from a prior operation?
if (vm.$route.params.obj) {
//yes, no need to fetch it
vm.obj = vm.$route.params.obj;
window.$gz.form.setFormState({
vm: vm,
loading: false
});
} else {
await vm.getDataFromApi(vm.$route.params.recordid); //let getdata handle loading
}
} else {
//New record so there *MUST* be an ayType on the route params
window.$gz.form.setFormState({
vm: vm,
loading: false
});
}
//---------------
//setup the editor and models
//Created editor models for each type of report element that can be edited
vm.editData.template.model = monaco.editor.createModel(
vm.obj.template,
"javascript"
);
vm.editData.style.model = monaco.editor.createModel(vm.obj.style, "css");
vm.editData.jsPrerender.model = monaco.editor.createModel(
vm.obj.jsPrerender,
"javascript"
);
if (vm.reportData != null) {
vm.editData.rawData.model = monaco.editor.createModel(
JSON.stringify(vm.reportData, null, "\t"),
"json"
);
}
vm.editData.jsHelpers.model = monaco.editor.createModel(
vm.obj.jsHelpers,
"javascript"
);
//Create the editor itself
editor = monaco.editor.create(document.getElementById("editContainer"), {
model: vm.editData.template.model
});
//save the initial state because we're going to move away from it immediately
vm.editData.template.state = editor.saveViewState();
//change subscription
editor.onDidChangeModelContent(event => {
const editorValue = editor.getValue();
// console.log("editorchange active tab=", vm.activeTab);
// console.log("editorchange, value of editor is", editorValue);
switch (vm.activeTab) {
case "template":
vm.obj.template = editorValue;
break;
case "style":
vm.obj.style = editorValue;
break;
case "jsPrerender":
vm.obj.jsPrerender = editorValue;
break;
case "jsHelpers":
vm.obj.jsHelpers = editorValue;
break;
}
vm.formState.dirty = true;
});
vm.showEditor = false;
//---------------
window.$gz.form.setFormState({
vm: vm,
dirty: false,
valid: true
});
generateMenu(vm);
} catch (error) {
window.$gz.errorHandler.handleFormError(error, vm);
} finally {
vm.formState.ready = true;
}
},
async beforeRouteLeave(to, from, next) {
if (!this.formState.dirty || JUST_DELETED) {
next();
return;
}
if ((await window.$gz.dialog.confirmLeaveUnsaved()) === true) {
next();
} else {
next(false);
}
},
beforeDestroy() {
window.$gz.eventBus.$off("menu-click", clickHandler);
//TODO: dispose all models and editor
editor && editor.dispose();
},
data() {
return {
reportData: null,
editAreaHeight: 300,
activeTab: "properties",
lastTab: "properties",
showEditor: true,
editData: {
template: {
model: null,
state: null
},
style: {
model: null,
state: null
},
jsPrerender: {
model: null,
state: null
},
jsHelpers: {
model: null,
state: null
},
rawData: {
model: null,
state: null
}
},
obj: {
id: 0,
concurrency: 0,
name: "report",
active: true,
notes: "",
roles: 124927, //all except customers
objectType: 0,
template: `console.log('hello world');`,
style: `.example {
color: blue;
}`,
jsPrerender: `function preRender(reportdata){
//this is called before the report is rendered
//modify data as required here
return reportData;
}`,
jsHelpers: `//Register custom Handlebars helpers here to use in your report script
//https://handlebarsjs.com/guide/#custom-helpers
Handlebars.registerHelper('loud', function (aString) {
return aString.toUpperCase()
})`,
renderType: 0
},
formState: {
ready: false,
dirty: false,
valid: true,
readOnly: false,
loading: true,
errorBoxMessage: null,
appError: null,
serverError: {}
},
rights: window.$gz.role.defaultRightsObject(),
ayaType: window.$gz.type.Report
};
},
//WATCHERS
watch: {
formState: {
handler: function(val) {
//,oldval is available here too if necessary
if (this.formState.loading) {
return;
}
//enable / disable save button
if (val.dirty && val.valid && !val.readOnly) {
window.$gz.eventBus.$emit("menu-enable-item", FORM_KEY + ":save");
} else {
window.$gz.eventBus.$emit("menu-disable-item", FORM_KEY + ":save");
}
//enable / disable duplicate / new button
if (!val.dirty && val.valid && !val.readOnly) {
window.$gz.eventBus.$emit(
"menu-enable-item",
FORM_KEY + ":duplicate"
);
window.$gz.eventBus.$emit("menu-enable-item", FORM_KEY + ":new");
} else {
window.$gz.eventBus.$emit(
"menu-disable-item",
FORM_KEY + ":duplicate"
);
window.$gz.eventBus.$emit("menu-disable-item", FORM_KEY + ":new");
}
},
deep: true
}
},
methods: {
//alternate method, one editor with tabs example
//probably should do this way
//https://github.com/Microsoft/monaco-editor/issues/604
//https://github.com/Microsoft/monaco-editor/blob/bad3c34056624dca34ac8be5028ae3454172125c/website/playground/playground.js#L108
//https://github.com/microsoft/monaco-editor-samples/tree/master/browser-esm-webpack-monaco-plugin
//
onViewChange() {
let vm = this;
let currentState = editor.saveViewState();
let currentModel = editor.getModel();
// debugger;
//save edit state before switching
switch (vm.lastTab) {
case "properties":
//no state to save here
break;
case "template":
vm.editData.template.state = currentState;
break;
case "style":
vm.editData.style.state = currentState;
break;
case "jsPrerender":
vm.editData.jsPrerender.state = currentState;
break;
case "jsHelpers":
vm.editData.jsHelpers.state = currentState;
break;
case "rawData":
vm.editData.rawData.state = currentState;
break;
}
//set new view stuff
switch (vm.activeTab) {
case "properties":
vm.showEditor = false;
break;
case "template":
editor.setModel(vm.editData.template.model);
editor.restoreViewState(vm.editData.template.state);
editor.updateOptions({ readOnly: false });
vm.showEditor = true;
editor.focus();
break;
case "style":
editor.setModel(vm.editData.style.model);
editor.restoreViewState(vm.editData.style.state);
editor.updateOptions({ readOnly: false });
vm.showEditor = true;
editor.focus();
break;
case "jsPrerender":
editor.setModel(vm.editData.jsPrerender.model);
editor.restoreViewState(vm.editData.jsPrerender.state);
editor.updateOptions({ readOnly: false });
vm.showEditor = true;
editor.focus();
break;
case "jsHelpers":
editor.setModel(vm.editData.jsHelpers.model);
editor.restoreViewState(vm.editData.jsHelpers.state);
editor.updateOptions({ readOnly: false });
vm.showEditor = true;
editor.focus();
break;
case "rawData":
editor.setModel(vm.editData.rawData.model);
editor.restoreViewState(vm.editData.rawData.state);
editor.updateOptions({ readOnly: true });
vm.showEditor = true;
editor.focus();
break;
}
vm.lastTab = vm.activeTab;
},
onResize() {
//resize related links:
//https://github.com/Microsoft/monaco-editor/issues/28
let el = document.getElementById("editContainer");
el.style = `width:100%;height:${window.innerHeight * 0.77}px`;
if (editor != null) {
editor.layout();
}
},
canSave: function() {
return this.formState.valid && this.formState.dirty;
},
canDuplicate: function() {
return this.formState.valid && !this.formState.dirty;
},
ayaTypes: function() {
return window.$gz.type;
},
form() {
return window.$gz.form;
},
fieldValueChanged(ref) {
if (
this.formState.ready &&
!this.formState.loading &&
!this.formState.readOnly
) {
window.$gz.form.fieldValueChanged(this, ref);
}
},
async getDataFromApi(recordId) {
let vm = this;
window.$gz.form.setFormState({
vm: vm,
loading: true
});
if (!recordId) {
throw FORM_KEY + "::getDataFromApi -> Missing recordID!";
}
let url = "report/" + recordId;
try {
window.$gz.form.deleteAllErrorBoxErrors(vm);
let res = await window.$gz.api.get(url);
if (res.error) {
//Not found?
if (res.error.code == "2010") {
//notify not found error then navigate backwards
window.$gz.eventBus.$emit("notify-error", vm.$ay.t("ErrorAPI2010"));
// navigate backwards
window.$gz._.delay(function() {
vm.$router.go(-1);
}, 2000);
}
vm.formState.serverError = res.error;
window.$gz.form.setErrorBoxErrors(vm);
} else {
vm.obj = res.data;
//modify the menu as necessary
generateMenu(vm);
//Update the form status
window.$gz.form.setFormState({
vm: vm,
dirty: false,
valid: true,
loading: false
});
}
} catch (error) {
window.$gz.errorHandler.handleFormError(error, vm);
} finally {
window.$gz.form.setFormState({
vm: vm,
loading: false
});
}
},
async submit() {
//######################
//need to set vm.obj first with model get value of text of each type being edited
//as only properties will be set already on obj, the rest is still as it was upon initial fetch
//unless I code it to always save to obj on each change of view or something but not necessary really
//USE MODEL.getValue() to get the text value of the model being edited
//Models sb already set in vm if they were set or edited previously
let vm = this;
if (vm.canSave == false) {
return;
}
try {
window.$gz.form.setFormState({
vm: vm,
loading: true
});
let url = "report/"; // + vm.$route.params.recordid;
//clear any errors vm might be around from previous submit
window.$gz.form.deleteAllErrorBoxErrors(vm);
let res = await window.$gz.api.upsert(url, vm.obj);
if (res.error) {
vm.formState.serverError = res.error;
window.$gz.form.setErrorBoxErrors(vm);
} else {
//Logic for detecting if a post or put: if id then it was a post, if no id then it was a put
if (res.data.id) {
//POST - whole new object returned
vm.obj = res.data;
//Change URL to new record
//NOTE: will not cause a page re-render, almost nothing does unless forced with a KEY property or using router.GO()
this.$router.push({
name: "ay-report-edit",
params: {
recordid: res.data.id,
obj: res.data, // Pass data object to new form
reportDataOptions: vm.$route.params.reportDataOptions
}
});
} else {
//PUT - only concurrency token is returned (**warning, if server changes object other fields then this needs to act more like POST above but is more efficient this way**)
//Handle "put" of an existing record (UPDATE)
vm.obj.concurrency = res.data.concurrency;
}
//Update the form status
window.$gz.form.setFormState({
vm: vm,
dirty: false,
valid: true
});
}
} catch (ex) {
window.$gz.errorHandler.handleFormError(ex, vm);
} finally {
window.$gz.form.setFormState({
vm: vm,
loading: false
});
}
},
async remove() {
let vm = this;
try {
let dialogResult = await window.$gz.dialog.confirmDelete();
if (dialogResult != true) {
return;
}
//do the delete
window.$gz.form.setFormState({
vm: vm,
loading: true
});
//No need to delete a new record, just abandon it...
if (vm.$route.params.recordid == 0) {
//this should not get offered for delete but to be safe and clear just in case:
JUST_DELETED = true;
// navigate backwards
vm.$router.go(-1);
} else {
let url = "report/" + vm.$route.params.recordid;
window.$gz.form.deleteAllErrorBoxErrors(vm);
let res = await window.$gz.api.remove(url);
if (res.error) {
vm.formState.serverError = res.error;
window.$gz.form.setErrorBoxErrors(vm);
} else {
//workaround to prevent warning about leaving dirty record
//For some reason I couldn't just reset isdirty in formstate
JUST_DELETED = true;
// navigate backwards
vm.$router.go(-1);
}
}
} catch (error) {
//Update the form status
window.$gz.form.setFormState({
vm: vm,
loading: false
});
window.$gz.errorHandler.handleFormError(error, vm);
}
},
async duplicate() {
let vm = this;
if (!vm.canDuplicate || vm.$route.params.recordid == 0) {
return;
}
window.$gz.form.setFormState({
vm: vm,
loading: true
});
let url = "report/duplicate/" + vm.$route.params.recordid;
try {
window.$gz.form.deleteAllErrorBoxErrors(vm);
let res = await window.$gz.api.upsert(url);
if (res.error) {
vm.formState.serverError = res.error;
window.$gz.form.setErrorBoxErrors(vm);
} else {
//Navigate to new record
this.$router.push({
name: "ay-report-edit",
params: {
recordid: res.data.id,
obj: res.data // Pass data object to new form
}
});
}
} catch (ex) {
window.$gz.errorHandler.handleFormError(ex, vm);
} finally {
window.$gz.form.setFormState({
vm: vm,
loading: false
});
}
}
}
};
/////////////////////////////
//
//
async function clickHandler(menuItem) {
if (!menuItem) {
return;
}
let m = window.$gz.menu.parseMenuItem(menuItem);
if (m.owner == FORM_KEY && !m.disabled) {
switch (m.key) {
case "save":
m.vm.submit();
break;
case "delete":
m.vm.remove();
break;
case "new":
m.vm.$router.push({
name: "ay-report-edit",
params: { recordid: 0, new: true }
});
break;
case "duplicate":
m.vm.duplicate();
break;
default:
window.$gz.eventBus.$emit(
"notify-warning",
FORM_KEY + "::context click: [" + m.key + "]"
);
}
}
}
//////////////////////
//
//
function generateMenu(vm) {
let menuOptions = {
isMain: false,
icon: "fa-drafting-compass",
title: "ReportDesignReport",
helpUrl: "form-ay-report-edit",
formData: {
ayaType: window.$gz.type.Report,
recordId: vm.$route.params.recordid
},
menuItems: []
};
if (vm.rights.change) {
menuOptions.menuItems.push({
title: "Save",
icon: "fa-save",
surface: true,
key: FORM_KEY + ":save",
vm: vm
});
}
if (vm.rights.delete && vm.$route.params.recordid != 0) {
menuOptions.menuItems.push({
title: "Delete",
icon: "fa-trash-alt",
surface: true,
key: FORM_KEY + ":delete",
vm: vm
});
}
// //STUB REPORTS
// //Report not Print, print is a further option
// menuOptions.menuItems.push({
// title: "Report",
// icon: "fa-file-alt",
// key: FORM_KEY + ":report",
// vm: vm
// });
// //get last report selected
// let lastReport = window.$gz.form.getLastReport(FORM_KEY);
// if (lastReport != null) {
// menuOptions.menuItems.push({
// title: lastReport.name,
// icon: "fa-file-alt",
// key: FORM_KEY + ":report:" + lastReport.id,
// vm: vm
// });
// }
if (vm.rights.change) {
menuOptions.menuItems.push({
title: "New",
icon: "fa-plus",
key: FORM_KEY + ":new",
vm: vm
});
}
if (vm.rights.change) {
menuOptions.menuItems.push({
title: "Duplicate",
icon: "fa-clone",
key: FORM_KEY + ":duplicate",
vm: vm
});
}
window.$gz.eventBus.$emit("menu-change", menuOptions);
}
/////////////////////////////////
//
//
async function initForm(vm) {
await fetchTranslatedText(vm);
await fetchReportData(vm);
}
//////////////////////////////////////////////////////////
//
// Ensures UI translated text is available
//
async function fetchTranslatedText(vm) {
await window.$gz.translation.cacheTranslations([
"ReportDesignReport",
"ReportName",
"ReportEditorProperties",
"ReportEditorData",
"ReportNotes",
"ReportTemplate",
"AuthorizationRoles"
]);
}
////////////////////
//
async function fetchReportData(vm) {
/* public AyaType ObjectType { get; set; }
public long[] SelectedRowIds { get; set; }
public string DataListKey { get; set; }
public string ListView { get; set; }//optional, if null or empty will use default list view built into DataList
*/
let reportDataOptions = vm.$route.params.reportDataOptions;
if (!reportDataOptions) {
vm.reportData = null;
return;
// throw "ay-report-edit:fetchReportData - route parameter reportDataOptions is missing or empty, unable to init report designer";
}
if (reportDataOptions.ObjectType == null) {
throw "ay-report-edit:fetchReportData - route parameter ObjectType is missing or empty, unable to init report designer";
}
vm.obj.objectType = reportDataOptions.ObjectType;
let res = await window.$gz.api.upsert(
"report/object-report-data",
reportDataOptions
);
//We never expect there to be no data here
if (!res.hasOwnProperty("data")) {
throw res;
} else {
vm.reportData = res.data;
}
}
</script>