Files
raven-client/ayanova/src/views/ay-report-edit.vue
2020-09-04 15:19:30 +00:00

955 lines
28 KiB
Vue

<template>
<div v-resize="onResize">
<!-- {{ formState }} -->
<v-row>
<gz-error :errorBoxMessage="formState.errorBoxMessage"></gz-error>
<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="view == 'edit'">
<div id="editContainer"></div>
</v-col>
<v-col cols="12" v-show="view == '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:
Immediately render last report code needs proper implementation for both widget and widgets
currently it's nonsense
"Design" translation should be "Report editor"
diagnostic code that returns helpful information on render failure would be nice and cut way back on technical support hassles
(could actually render the errors to the page and still return it as a pdf?, though it's an api method that triggers it so can just return normal error style)
need to modify reportData data object to accept meta data outside of the main data collection
so can add all the bits that would appear on report potentially or be used on page like
datalist name, filter options, what have you
also ayReportData object, should it be named something else? Does it even matter?
need to ensure easy api access from report script, perhaps pass creds token into report along with report?
creds as meta data?
Ability to export / download and save entire report template as an independent object / file
and import again
.ayr8?
Make up sample reports for widget that show off the basics including logo etc
make them auto generate (import from files?) with sample data in seeder
Make a working template with api methods being called
export as example
Start thinking about currency formatting and date/time formatting etc.
Maybe client can pass a function to server, or server can use exact same libs but can be initiated with specific locale from client?
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,
"html"
);
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.view = "properties";
//---------------
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",
view: "edit", //"edit","properties","test"
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: `<html>
<body>
{{#each ayReportData}}
<h2>{{ Name }}</h2>
<div>Notes: <span class='example'>{{ Notes }}</span></div>
{{/each}}
</body>
</html>`,
style: `.example {
color: blue;
}`,
jsPrerender: `function reportPreRender(reportData){
//this function (if present) is called with the report data
//before the report is rendered
//modify data as required here and return it to change the data before the report renders
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 RENDER button if it can be rendered (has data)
if (this.reportData != null) {
if (!val.dirty && val.valid) {
window.$gz.eventBus.$emit("menu-enable-item", FORM_KEY + ":render");
} else {
window.$gz.eventBus.$emit(
"menu-disable-item",
FORM_KEY + ":render"
);
}
}
//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.view = "properties";
break;
case "template":
editor.setModel(vm.editData.template.model);
editor.restoreViewState(vm.editData.template.state);
editor.updateOptions({ readOnly: false });
vm.view = "edit";
editor.focus();
break;
case "style":
editor.setModel(vm.editData.style.model);
editor.restoreViewState(vm.editData.style.state);
editor.updateOptions({ readOnly: false });
vm.view = "edit";
editor.focus();
break;
case "jsPrerender":
editor.setModel(vm.editData.jsPrerender.model);
editor.restoreViewState(vm.editData.jsPrerender.state);
editor.updateOptions({ readOnly: false });
vm.view = "edit";
editor.focus();
break;
case "jsHelpers":
editor.setModel(vm.editData.jsHelpers.model);
editor.restoreViewState(vm.editData.jsHelpers.state);
editor.updateOptions({ readOnly: false });
vm.view = "edit";
editor.focus();
break;
case "rawData":
editor.setModel(vm.editData.rawData.model);
editor.restoreViewState(vm.editData.rawData.state);
editor.updateOptions({ readOnly: true });
vm.view = "edit";
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 render() {
let vm = this;
if (vm.$route.params.recordid == 0) {
return;
}
/*
public class RenderReportParameter
{
public long ReportId { 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) {
throw "Missing report data: to view report must come here from an object edit form or list so data can be provided for viewing the report";
}
reportDataOptions.ReportId = vm.obj.id;
window.$gz.form.setFormState({
vm: vm,
loading: true
});
let url = "report/render";
try {
window.$gz.form.deleteAllErrorBoxErrors(vm);
let res = await window.$gz.api.upsert(url, reportDataOptions);
if (res.error) {
vm.formState.serverError = res.error;
window.$gz.form.setErrorBoxErrors(vm);
} else {
let reportUrl = window.$gz.api.reportDownloadUrl(res.data);
//console.log("Report url:", reportUrl);
if (window.open(reportUrl, "Report") == null) {
vm.formState.serverError =
"Problem displaying report in new window. Browser must allow pop-ups to view reports; check your browser setting";
window.$gz.form.setErrorBoxErrors(vm);
}
}
} catch (ex) {
window.$gz.errorHandler.handleFormError(ex, vm);
} finally {
window.$gz.form.setFormState({
vm: vm,
loading: false
});
}
}
}
};
//end of vue object
/////////////////////////////
//
//
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;
case "render":
m.vm.render();
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.reportData != null) {
menuOptions.menuItems.push({
title: "Report",
icon: "fa-print",
key: FORM_KEY + ":render",
surface: true,
vm: vm
});
}
if (vm.rights.delete && vm.$route.params.recordid != 0) {
menuOptions.menuItems.push({
title: "Delete",
icon: "fa-trash-alt",
surface: false,
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/data", reportDataOptions);
//We never expect there to be no data here
if (!res.hasOwnProperty("data")) {
throw res;
} else {
vm.reportData = res.data;
}
}
</script>