This commit is contained in:
2022-12-27 18:55:47 +00:00
parent 4ef5a0ffe1
commit 7d0a00cb19
168 changed files with 70186 additions and 0 deletions

View File

@@ -0,0 +1,184 @@
import bizrolerights from "./biz-role-rights";
export default {
ROLE_RIGHTS: bizrolerights,
AUTHORIZATION_ROLES: {
///<summary>No role set</summary>
NoRole: 0,
///<summary>BizAdminRestricted</summary>
BizAdminRestricted: 1,
///<summary>BizAdmin</summary>
BizAdmin: 2,
///<summary>ServiceRestricted</summary>
ServiceRestricted: 4,
///<summary>Service</summary>
Service: 8,
///<summary>InventoryRestricted</summary>
InventoryRestricted: 16,
///<summary>Inventory</summary>
Inventory: 32,
///<summary>Accounting</summary>
Accounting: 64, //No restricted role, not sure if there is a need
///<summary>TechRestricted</summary>
TechRestricted: 128,
///<summary>Tech</summary>
Tech: 256,
///<summary>SubContractorRestricted</summary>
SubContractorRestricted: 512,
///<summary>SubContractor</summary>
SubContractor: 1024,
///<summary>CustomerRestricted</summary>
CustomerRestricted: 2048,
///<summary>Customer</summary>
Customer: 4096,
///<summary>OpsAdminRestricted</summary>
OpsAdminRestricted: 8192,
///<summary>OpsAdmin</summary>
OpsAdmin: 16384,
///<summary>Sales</summary>
Sales: 32768,
///<summary>SalesRestricted</summary>
SalesRestricted: 65536
},
//////////////////////////////////////////////////////////
// Does current logged in user have role?
// (Can be an array of roles or a single role, if array returns true if any of the array roles are present for this user)
//
hasRole(desiredRole) {
if (!window.$gz.store.state.roles || window.$gz.store.state.roles === 0) {
return false;
}
//array form?
if (Array.isArray(desiredRole)) {
//it's an array of roles, iterate and if any are present then return true
for (let i = 0; i < desiredRole.length; i++) {
if ((window.$gz.store.state.roles & desiredRole[i]) != 0) {
return true;
}
}
return false;
} else {
return (window.$gz.store.state.roles & desiredRole) != 0;
}
},
//////////////////////////////////////////////////////////
// Does current logged in user have *ANY* role?
//
//
hasAnyRole() {
if (!window.$gz.store.state.roles || window.$gz.store.state.roles === 0) {
return false;
}
return true;
},
///////////////////////////////////////////////////////////////////////
// Get a default empty rights object so that it can be present when a
// form first loads
//
defaultRightsObject() {
return {
change: false,
read: false,
delete: false
};
},
///////////////////////////////////////////////////////////////////////
// Get a default FULL rights object for forms that don't really need
// to check rights but fits into system for forms in place (e.g. change password)
//
fullRightsObject() {
return {
change: true,
read: true,
delete: true
};
},
///////////////////////////////////////////////////////////////////////
// Get a read only rights object (customer workorder for example)
//
readOnlyRightsObject() {
return {
change: false,
read: true,
delete: false
};
},
/////////////////////////////////
// aType is the name of the object type as defined in socktype.js
//
getRights(aType) {
//from bizroles.cs:
//HOW THIS WORKS / WHATS EXPECTED
//Change = CREATE, RETRIEVE, UPDATE, DELETE - Full rights
//
//ReadFullRecord = You can read *all* the fields of the record, but can't modify it. Change is automatically checked for so only add different roles from change
//PICKLIST NOTE: this does not control getting a list of names for selection which is role independent because it's required for so much indirectly
//DELETE = SAME AS CHANGE FOR NOW (There is no specific delete right for now though it's checked for by routes in Authorized.cs in case we want to add it in future as a separate right from create.)
//NOTE: biz rules can supersede this, this is just for general rights purposes, if an object has restrictive business rules they will take precedence every time.
const ret = this.defaultRightsObject();
//Get the type name from the type enum value
let typeName = undefined;
for (const [key, value] of Object.entries(window.$gz.type)) {
if (value == aType) {
typeName = key;
break;
}
}
//Get the Sockeye stock REQUIRED role rights for that object
const objectRoleRights = this.ROLE_RIGHTS[typeName];
if (!objectRoleRights) {
throw new Error(
`authorizationroles::getRights type ${aType} not found in roles collection`
);
}
//get the logged in user's role
const userRole = window.$gz.store.state.roles;
//calculate the effective rights
//a non zero result of the bitwise calculation means true and zero means false so using !! to force it into a boolean value
//(contrary to some style guides that say !! is obscure but I say it saves a lot of typing)
const canChange = !!(userRole & objectRoleRights.Change);
//sometimes rights to read are false if change is true since change trumps read anyway so accordingly:
let canReadFullRecord = canChange;
if (!canReadFullRecord) {
//can't change but might have special rights to full record:
canReadFullRecord = !!(userRole & objectRoleRights.ReadFullRecord);
}
ret.change = canChange;
ret.delete = ret.change; //FOR NOW
ret.read = canReadFullRecord;
// console.log("authorizationroles::canOpen", {
// typeName: typeName,
// userRole: userRole,
// objectRoleRights: objectRoleRights,
// retResultIs: ret
// });
return ret;
},
/////////////////////////////////
// convenience method for forms that deal with multiple object types
// (i.e. grids, history etc, initialization of main menu etc)
//
canOpen(aType) {
const r = this.getRights(aType);
//convention is change might be defined but not read so canOpen is true eitehr way
return r.change == true || r.read == true;
},
/////////////////////////////////
// convenience method for forms that deal with multiple object types
// (i.e. grids, history etc, initialization of main menu etc)
//
canChange(aType) {
const r = this.getRights(aType);
return r.change == true;
}
};

124
src/api/authutil.js Normal file
View File

@@ -0,0 +1,124 @@
import jwt_decode from "jwt-decode";
import initialize from "./initialize";
import notifypoll from "./notifypoll";
export function processLogin(authResponse, loggedInWithKnownPassword) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async function(resolve, reject) {
try {
//check there is a response of some kind
if (!authResponse) {
window.$gz.store.commit("logItem", "auth::processLogin -> no response");
return reject();
}
//is token present?
if (!authResponse || !authResponse.token) {
window.$gz.store.commit(
"logItem",
"auth::processLogin -> response contains no data"
);
return reject();
}
const token = jwt_decode(authResponse.token);
if (!token || !token.iss) {
window.$gz.store.commit(
"logItem",
"auth::processLogin -> response token empty"
);
return reject();
}
if (token.iss != "rockfish.ayanova.com") {
window.$gz.store.commit(
"logItem",
"auth::processLogin -> token invalid (iss): " + token.iss
);
return reject();
}
//ensure the store is clean first in case we didn't come here from a clean logout
window.$gz.store.commit("logout");
sessionStorage.clear(); //clear all temporary session storage data
//encourage password changing if a purchased license
if (loggedInWithKnownPassword)
window.$gz.store.commit("setKnownPassword", true);
//Put app relevant items into vuex store so app can use them
window.$gz.store.commit("login", {
apiToken: authResponse.token,
authenticated: true,
userId: Number(token.id),
translationId: authResponse.tid,
userName: authResponse.name,
roles: authResponse.roles,
userType: authResponse.usertype,
dlt: authResponse.dlt,
l: authResponse.l,
tfaEnabled: authResponse.tfa,
customerRights: authResponse.customerRights
});
//decided to remove this as it is not an out of the ordinary scenario to log
// however left this block here in case in future becomes necessary for some common issue
// //log the login
// window.$gz.store.commit(
// "logItem",
// "auth::processLogin -> User " + token.id + " logged in"
// );
//Get global settings
const gsets = await window.$gz.api.get("global-biz-setting/client");
if (gsets.error) {
//In a form this would trigger a bunch of validation or error display code but for here and now:
//convert error to human readable string for display and popup a notification to user
const msg = window.$gz.api.apiErrorToHumanString(gsets.error);
window.$gz.eventBus.$emit("notify-error", msg);
} else {
//Check if overrides and use them here
//or else use browser defaults
window.$gz.store.commit("setGlobalSettings", gsets.data);
}
await initialize();
} catch (err) {
reject(err);
}
//start notification polling
notifypoll.startPolling();
resolve();
//-------------------------------------------------
});
}
export function processLogout() {
notifypoll.stopPolling();
window.$gz.store.commit("logout");
sessionStorage.clear(); //clear all temporary session storage data
}
export function isLoggedIn() {
return (
!!window.$gz.store.state.apiToken &&
!isTokenExpired(window.$gz.store.state.apiToken)
);
}
function getTokenExpirationDate(encodedToken) {
const token = jwt_decode(encodedToken);
if (!token.exp) {
return null;
}
const date = new Date(0);
date.setUTCSeconds(token.exp);
return date;
}
function isTokenExpired(token) {
const expirationDate = getTokenExpirationDate(token);
return expirationDate < new Date();
}

View File

@@ -0,0 +1,47 @@
/**
*
* Auto generated by BizRoles.cs in server project, update here whenever that changes
*
*
*/
export default {
Customer: { Change: 32842, ReadFullRecord: 65797, Select: 131071 },
CustomerNote: { Change: 32842, ReadFullRecord: 65797, Select: 131071 },
CustomerNotifySubscription: {
Change: 10,
ReadFullRecord: 65797,
Select: 131071
},
HeadOffice: { Change: 32842, ReadFullRecord: 65797, Select: 131071 },
Global: { Change: 2, ReadFullRecord: 1, Select: 0 },
GlobalOps: { Change: 16384, ReadFullRecord: 8192, Select: 0 },
User: { Change: 2, ReadFullRecord: 1, Select: 131071 },
UserOptions: { Change: 2, ReadFullRecord: 1, Select: 0 },
Vendor: { Change: 106, ReadFullRecord: 98565, Select: 131071 },
ServerState: { Change: 16384, ReadFullRecord: 131071, Select: 0 },
LogFile: { Change: 0, ReadFullRecord: 24576, Select: 0 },
Backup: { Change: 16384, ReadFullRecord: 8195, Select: 0 },
FileAttachment: { Change: 2, ReadFullRecord: 3, Select: 0 },
ServerJob: { Change: 16384, ReadFullRecord: 8195, Select: 0 },
OpsNotificationSettings: { Change: 16384, ReadFullRecord: 8195, Select: 0 },
ServerMetrics: { Change: 16384, ReadFullRecord: 24576, Select: 0 },
Translation: { Change: 2, ReadFullRecord: 1, Select: 131071 },
DataListSavedFilter: { Change: 2, ReadFullRecord: 131071, Select: 0 },
FormUserOptions: { Change: 131071, ReadFullRecord: 131071, Select: 0 },
FormCustom: { Change: 2, ReadFullRecord: 131071, Select: 0 },
PickListTemplate: { Change: 2, ReadFullRecord: 131071, Select: 0 },
BizMetrics: { Change: 2, ReadFullRecord: 98369, Select: 0 },
Notification: { Change: 131071, ReadFullRecord: 131071, Select: 0 },
NotifySubscription: { Change: 131071, ReadFullRecord: 131071, Select: 0 },
Report: { Change: 3, ReadFullRecord: 131071, Select: 131071 },
Memo: { Change: 124927, ReadFullRecord: 124927, Select: 124927 },
Reminder: { Change: 124927, ReadFullRecord: 124927, Select: 124927 },
Review: { Change: 124927, ReadFullRecord: 124927, Select: 124927 },
Integration: { Change: 49514, ReadFullRecord: 49514, Select: 49514 },
License: { Change: 32842, ReadFullRecord: 65797, Select: 131071 },
TrialLicenseRequest: { Change: 32842, ReadFullRecord: 65797, Select: 131071 },
SubscriptionServer: { Change: 32842, ReadFullRecord: 65797, Select: 131071 },
Purchase: { Change: 32842, ReadFullRecord: 65797, Select: 131071 },
Product: { Change: 32842, ReadFullRecord: 65797, Select: 131071 },
GZCase: { Change: 32842, ReadFullRecord: 65797, Select: 131071 }
};

400
src/api/dash-registry.js Normal file
View File

@@ -0,0 +1,400 @@
import authorizationroles from "./authorizationroles";
const role = authorizationroles.AUTHORIZATION_ROLES;
/*
*/
export default {
registry: [
{
roles: [
role.BizAdmin,
role.BizAdminRestricted,
role.ServiceRestricted,
role.Service,
role.Accounting,
role.Tech,
role.TechRestricted
],
title: "DashboardWorkOrderByStatusList",
icon: "$sockiListAlt",
type: "GzDashWorkorderByStatusList",
singleOnly: false,
settings: {
customTitle: null,
timeSpan: "*thisyear*",
interval: "month",
wostatus: null,
wotags: [],
wotagsany: true,
woitemtags: [],
woitemtagsany: true
}
},
{
roles: [
role.BizAdmin,
role.BizAdminRestricted,
role.ServiceRestricted,
role.Service,
role.Accounting
],
title: "DashboardWorkOrderStatusCount",
icon: "$sockiChartBar",
type: "GzDashWorkOrderStatusCount",
scheduleableUserOnly: false,
singleOnly: false,
settings: {
customTitle: null,
timeSpan: "*thisyear*",
interval: "month",
wotags: [],
wotagsany: true
}
},
{
roles: [
role.BizAdmin,
role.BizAdminRestricted,
role.ServiceRestricted,
role.Service,
role.Accounting
],
title: "DashboardWorkOrderStatusPct",
icon: "$sockiChartBar",
type: "GzDashWorkOrderStatusPct",
scheduleableUserOnly: false,
singleOnly: false,
settings: {
customTitle: null,
timeSpan: "*thisyear*",
interval: "month",
wotags: [],
wotagsany: true
}
},
{
roles: [
role.BizAdmin,
role.BizAdminRestricted,
role.ServiceRestricted,
role.Service,
role.Accounting
],
title: "DashboardPctWorkOrderCompletedOnTime",
icon: "$sockiChartBar",
type: "GzDashPctWorkOrderCompletedOnTimeBar",
scheduleableUserOnly: false,
singleOnly: false,
settings: {
customTitle: null,
timeSpan: "*thisyear*",
interval: "month",
wotags: [],
wotagsany: true,
color: "#00205BFF"
}
},
{
roles: [
role.BizAdmin,
role.BizAdminRestricted,
role.ServiceRestricted,
role.Service,
role.Accounting
],
title: "DashboardCountWorkOrdersCreated",
icon: "$sockiChartLine",
type: "GzDashWorkOrderCreatedCountLine",
scheduleableUserOnly: false,
singleOnly: false,
settings: {
customTitle: null,
timeSpan: "*thisyear*",
interval: "day",
wotags: [],
wotagsany: true,
woitemtags: [],
woitemtagsany: true,
color: "#00205BFF"
}
},
{
roles: [
role.BizAdmin,
role.BizAdminRestricted,
role.ServiceRestricted,
role.Service,
role.Accounting
],
title: "DashboardCountWorkOrdersCreated",
icon: "$sockiChartBar",
type: "GzDashWorkOrderCreatedCountBar",
scheduleableUserOnly: false,
singleOnly: false,
settings: {
customTitle: null,
timeSpan: "*thisyear*",
wotags: [],
wotagsany: true,
woitemtags: [],
woitemtagsany: true,
interval: "month",
color: "#00205BFF"
}
},
{
roles: [
role.BizAdmin,
role.BizAdminRestricted,
role.ServiceRestricted,
role.Service,
role.Accounting
],
title: "DashboardOverdueAll",
icon: "$sockiListAlt",
type: "GzDashWorkorderOverdueAllList",
scheduleableUserOnly: false,
singleOnly: false,
settings: {
customTitle: null,
wotags: [],
wotagsany: true,
woitemtags: [],
woitemtagsany: true
}
},
{
roles: [role.Tech, role.TechRestricted],
title: "DashboardOverdue",
icon: "$sockiListAlt",
type: "GzDashWorkorderOverduePersonalList",
scheduleableUserOnly: true,
singleOnly: true,
settings: {
customTitle: null,
wotags: [],
wotagsany: true,
woitemtags: [],
woitemtagsany: true
}
},
{
roles: [
role.BizAdmin,
role.BizAdminRestricted,
role.ServiceRestricted,
role.Service,
role.Tech,
role.TechRestricted
],
title: "DashboardOpenCSR",
icon: "$sockiListAlt",
type: "GzDashCSROpenList",
singleOnly: false,
settings: {
customTitle: null,
custtags: [],
custtagsany: true
}
},
{
roles: [
role.BizAdmin,
role.BizAdminRestricted,
role.ServiceRestricted,
role.Service,
role.Accounting,
role.Tech,
role.TechRestricted
],
title: "DashboardNotScheduled",
icon: "$sockiListAlt",
type: "GzDashWorkorderUnscheduledOpenList",
singleOnly: false,
settings: {
customTitle: null,
wostatus: null,
wotags: [],
wotagsany: true,
woitemtags: [],
woitemtagsany: true
}
},
{
roles: [
role.BizAdmin,
role.BizAdminRestricted,
role.ServiceRestricted,
role.Service,
role.InventoryRestricted,
role.Inventory,
role.Accounting,
role.Tech,
role.TechRestricted,
role.OpsAdmin,
role.OpsAdminRestricted,
role.Sales,
role.SalesRestricted
],
title: "ReminderList",
icon: "$sockiCalendarDay",
type: "GzDashTodayReminders",
singleOnly: true,
settings: {}
},
{
roles: [
role.BizAdmin,
role.BizAdminRestricted,
role.ServiceRestricted,
role.Service,
role.InventoryRestricted,
role.Inventory,
role.Accounting,
role.Tech,
role.TechRestricted,
role.OpsAdmin,
role.OpsAdminRestricted,
role.Sales,
role.SalesRestricted
],
title: "ReviewList",
icon: "$sockiCalendarDay",
type: "GzDashTodayReviews",
singleOnly: true,
settings: {}
},
{
roles: [role.Tech, role.TechRestricted],
title: "DashboardScheduled",
icon: "$sockiCalendarDay",
type: "GzDashTodayScheduledWo",
scheduleableUserOnly: true,
singleOnly: true,
settings: {}
},
{
roles: [role.Tech, role.TechRestricted],
title: "WorkOrderItemLaborServiceRateQuantity",
icon: "$sockiChartLine",
type: "GzDashLaborHoursPersonalLine",
scheduleableUserOnly: true,
singleOnly: false,
settings: {
customTitle: null,
timeSpan: "*thisyear*",
interval: "day",
wotags: [],
wotagsany: true,
woitemtags: [],
woitemtagsany: true,
color: "#00205BFF"
}
},
{
roles: [role.Tech, role.TechRestricted],
title: "WorkOrderItemLaborServiceRateQuantity",
icon: "$sockiChartBar",
type: "GzDashLaborHoursPersonalBar",
scheduleableUserOnly: true,
singleOnly: false,
settings: {
customTitle: null,
timeSpan: "*thisyear*",
wotags: [],
wotagsany: true,
woitemtags: [],
woitemtagsany: true,
interval: "month",
color: "#00205BFF"
}
},
{
roles: [
role.BizAdmin,
role.BizAdminRestricted,
role.ServiceRestricted,
role.Service,
role.Accounting
],
title: "DashboardServiceRateQuantityAllUsers",
icon: "$sockiChartLine",
type: "GzDashLaborHoursEveryoneLine",
scheduleableUserOnly: false,
singleOnly: false,
settings: {
customTitle: null,
timeSpan: "*thisyear*",
interval: "month",
wotags: [],
wotagsany: true,
woitemtags: [],
woitemtagsany: true,
techtags: [],
techtagsany: true,
userid: null,
color: "#00205BFF"
}
},
{
roles: [
role.BizAdmin,
role.BizAdminRestricted,
role.ServiceRestricted,
role.Service,
role.Accounting
],
title: "DashboardServiceRateQuantityAllUsers",
icon: "$sockiChartBar",
type: "GzDashLaborHoursEveryoneBar",
scheduleableUserOnly: false,
singleOnly: false,
settings: {
customTitle: null,
timeSpan: "*thisyear*",
wotags: [],
wotagsany: true,
woitemtags: [],
woitemtagsany: true,
techtags: [],
techtagsany: true,
userid: null,
interval: "month",
color: "#00205BFF"
}
}
],
availableItems() {
const ret = [];
for (let i = 0; i < this.registry.length; i++) {
const item = this.registry[i];
if (authorizationroles.hasRole(item.roles)) {
//if it's only for sched users and not then skip
if (
item.scheduleableUserOnly &&
!window.$gz.store.getters.isScheduleableUser
) {
continue;
}
ret.push({
id: i,
title: item.title,
icon: item.icon,
type: item.type,
singleOnly: item.singleOnly,
settings: item.settings
});
}
}
return ret;
},
async cacheTranslationsForAvailableItems() {
const items = this.availableItems();
//await window.$gz.translation.cacheTranslations(items.map(z => z.title));
await window.$gz.translation.cacheTranslations([
...new Set(items.map(z => z.title))
]);
}
};

101
src/api/enums.js Normal file
View File

@@ -0,0 +1,101 @@
export default {
get(enumKey, enumValue) {
enumKey = enumKey.toLowerCase();
if (enumKey != "authorizationroles") {
if (window.$gz.store.state.enums[enumKey] == undefined) {
throw new Error(
"ERROR enums::get -> enumKey " + enumKey + " is missing from store"
);
}
const ret = window.$gz.store.state.enums[enumKey][enumValue];
if (ret == undefined) {
return "";
} else {
return ret;
}
} else {
const ret = [];
if (enumValue == null || enumValue == 0) {
return "";
}
const availableRoles = this.getSelectionList("AuthorizationRoles");
for (let i = 0; i < availableRoles.length; i++) {
const role = availableRoles[i];
if (enumValue & role.id) {
ret.push(role.name);
}
}
return ret.join(", ");
}
},
//////////////////////////////////
//
// Used by forms to fetch selection list data
// Sorts alphabetically by default but can be turned off with do not sort
//
getSelectionList(enumKey, noSort) {
enumKey = enumKey.toLowerCase();
const e = window.$gz.store.state.enums[enumKey];
if (!e) {
throw new Error(
"ERROR enums::getSelectionList -> enumKey " +
enumKey +
" is missing from store"
);
}
const ret = [];
//turn it into an array suitable for selection lists
for (const [key, value] of Object.entries(e)) {
ret.push({ id: Number(key), name: value });
}
//sort by name
if (!noSort) {
ret.sort(window.$gz.util.sortByKey("name"));
}
return ret;
},
///////////////////////////////////
//
// Fetches enum list from server
// and puts in store. if necessary
// ACCEPTS an ARRAY or a single STRING KEY
//
async fetchEnumList(enumKey) {
if (!Array.isArray(enumKey)) {
enumKey = [enumKey];
}
for (let i = 0; i < enumKey.length; i++) {
//check if list
//if not then fetch it and store it
const k = enumKey[i].toLowerCase();
//de-lodash
// if (!window.$gz. _.has(window.$gz.store.state.enums, k)) {
//enums is an object this is checking if that object has a key with the name in k
if (!window.$gz.util.has(window.$gz.store.state.enums, k)) {
const that = this;
const dat = await that.fetchEnumKey(k);
//massage the data as necessary
const e = { enumKey: k, items: {} };
for (let i = 0; i < dat.length; i++) {
const o = dat[i];
e.items[o.id] = o.name;
}
//stuff the data into the store
window.$gz.store.commit("setEnum", e);
}
}
},
async fetchEnumKey(enumKey) {
const res = await window.$gz.api.get("enum-list/list/" + enumKey);
//We never expect there to be no data here
//if (!Object.prototype.hasOwnProperty.call(res, "data")) {
if (!Object.prototype.hasOwnProperty.call(res, "data")) {
return Promise.reject(res);
}
return res.data;
}
};

230
src/api/errorhandler.js Normal file
View File

@@ -0,0 +1,230 @@
let lastMessageHash = 0;
let lastMessageTimeStamp = new Date();
////////////////////////////////////////////////////////
//
// translate, Log and optionally display errors
// return translated message in case caller needs it
async function dealWithError(msg, vm) {
//Check if this is the same message again as last time within a short time span to avoid endless looping errors of same message
//but still allow for user to repeat operation that causes error so they can view it
const newHash = window.$gz.util.quickHash(msg);
if (newHash == lastMessageHash) {
const tsnow = new Date();
//don't show the same exact message if it was just shown less than 1 second ago
if (tsnow - lastMessageTimeStamp < 1000) return;
}
lastMessageHash = newHash;
lastMessageTimeStamp = new Date();
//translate as necessary
msg = await window.$gz.translation.translateStringWithMultipleKeysAsync(msg);
//In some cases the error may not be translatable, if this is not a debug run then it should show without the ?? that translating puts in keys not found
//so it's not as weird looking to the user
//vm may be null here so check window gz for dev
if (!window.$gz.dev && msg.includes("??")) {
msg = msg.replace("??", "");
}
window.$gz.store.commit("logItem", msg);
if (window.$gz.dev) {
const errMsg = "DEV MODE errorHandler.js:: Unexpected error: \r\n" + msg;
// eslint-disable-next-line no-console
console.error(errMsg);
// eslint-disable-next-line no-debugger
debugger;
}
//If a form instance was provided (vue instance)
//and it can display and error then put the error into it
if (!vm || vm.formState == undefined) {
//Special work around to not redundantly display errors when Sockeye job fails
// and Vue decides to throw it's own error into the mix when we've already displayed appropriate message
if (msg.includes("Vue error") && msg.includes("Job failed")) {
return;
}
//popup if no place to display it elsewise
window.$gz.eventBus.$emit("notify-error", msg);
return;
}
//should be able to display in form...
if (vm.$sock.dev) {
//make sure formState.appError is defined on data
if (!window.$gz.util.has(vm, "formState.appError")) {
throw new Error(
"DEV ERROR errorHandler::dealWithError -> formState.appError seems to be missing from form's vue data object"
);
}
}
vm.formState.appError = msg;
//TODO: What is this doing exactly?
//it's related to server errors but I'm setting appError above
//why two error properties?
window.$gz.form.setErrorBoxErrors(vm);
}
///////////////////////////////////////////////////////////////////////////////////
// DECODE ERROR TO TEXT
// accept an unknown type of error variable
// and return human readable text
//
function decodeError(e, vm) {
// console.log("decodeError full e object as is: ");
// console.log(e);
// console.log("decodeError full e object stringified: ", JSON.stringify(e));
// console.log("decodeError is typeof:", typeof e);
// console.log("decodeError e is instanceof Error ", e instanceof Error);
// console.log(
// "decodeError e is a string already: ",
// window.$gz.util.isString(e)
// );
//already a string?
if (window.$gz.util.isString(e)) {
return e; //nothing to do here, already a string
}
if (e instanceof Error) {
//an Error object?
return `Error - Name:${e.name}, Message:${e.message}`;
}
if (
e == null ||
e == "" ||
(typeof e === "object" && Object.keys(e).length === 0)
) {
return `errorHandler::decodeError - Error is unknown / empty (e:${e})`;
}
//API error object or error RESPONSE object?
if (e.error || e.code) {
let err = null;
//could be the error RESPONSE or it could be the error object *inside* the error response so sort out here
if (e.error) {
//it's the entire resopnse object
err = e.error;
} else {
//it's the inner error object only
err = e;
}
let msg = "";
if (err.code) {
msg += err.code;
msg += " - ";
if (vm) {
msg += vm.$sock.t("ErrorAPI" + err.code);
}
msg += "\n";
}
if (err.target) {
msg += err.target;
msg += "\n";
}
if (err.message && !err.message.startsWith("ErrorAPI")) {
//errapi already dealt with above no need to repeat it here
msg += err.message;
msg += "\n";
}
if (err.details) {
err.details.forEach(z => {
let zerror = null;
if (z.error) {
zerror = z.error + " - ";
}
msg += `${zerror}${z.message}\n`;
});
}
//console.log("errorhandler:decodeError returning message:", msg);
return msg;
}
//Javascript Fetch API Response object?
if (e instanceof Response) {
return `http error: ${e.statusText} - ${e.status} Url: ${e.url}`;
}
//last resort
return JSON.stringify(e);
}
export default {
handleGeneralError(message, source, lineno, colno, error) {
let msg = "General error: \n" + message;
if (source) {
msg += "\nsource: " + source;
}
if (lineno) {
msg += "\nlineno: " + lineno;
}
if (colno) {
msg += "\ncolno: " + colno;
}
if (error) {
if (typeof error === "object") {
error = JSON.stringify(error);
}
msg += "\nerror: " + error;
}
dealWithError(msg);
},
handleVueError(err, vm, info) {
let msg = "Vue error: \n" + decodeError(err, vm);
if (err.fileName) {
msg += "\nfilename: " + err.fileName;
}
if (err.lineNumber) {
msg += "\nlineNumber: " + err.lineNumber;
}
if (info) {
msg += "\ninfo: " + info;
}
if (err.stack) {
msg += "\nSTACK:\n " + err.stack;
}
dealWithError(msg, vm);
},
handleVueWarning(wmsg, vm, trace) {
let msg = "Vue warning: \n" + decodeError(wmsg, vm);
if (trace) {
msg += "\ntrace: " + trace;
}
dealWithError(msg, vm);
},
/////////////////////////////////////////////////
// translate, log and return error
//
handleFormError(err, vm) {
if (window.$gz.dev) {
console.trace(err);
}
//called inside forms when things go unexpectedly wrong
dealWithError(decodeError(err, vm), vm);
},
/////////////////////////////////////////////////
// decode error into string suitable to display
//
errorToString(err, vm) {
//called inside forms when things go unexpectedly wrong
return decodeError(err, vm);
}
};
/*
ERROR CODES USED:
Client error codes are all in the range of E16 to E999
Server error codes are all in the range of E1000 to E1999
API specific (logic) error codes are all in the range of 2000 to 3000
CLIENT ERROR CODES:
E16 - ErrorUserNotAuthenticated
E17 - ErrorServerUnresponsive
E18 - Misc error without a translation key, unexpected throws etc or api error during server call, details in the message / Any error without a translation key defined basically
*/

2
src/api/eventbus.js Normal file
View File

@@ -0,0 +1,2 @@
import Vue from "vue";
export default new Vue();

View File

@@ -0,0 +1,83 @@
///Add data key names which make the custom fields control work more easily
///Since the names can be inferred from the data that comes from the server it saves bandwidth to do it here at the client
function addDataKeyNames(obj) {
//iterate the array of objects
//if it has a "type" property then it's a custom field so add its data key name
for (let i = 0; i < obj.length; i++) {
if (obj[i].type) {
obj[i]["dataKey"] = "c" + parseInt(obj[i].fld.replace(/^\D+/g, ""));
}
}
//return the whole thing again now translated
return obj;
}
export default {
////////////////////////////////
// Cache the form customization data if it's not already present
// NOTE: FORM KEY **MUST** BE THE AYATYPE NAME WHERE POSSIBLE, IF NO TYPE THEN AN EXCEPTION NEEDS TO BE CODED IN
//SERVER FormFieldReference.cs -> public static List<string> FormFieldKeys
//
async get(formKey, vm, forceRefresh) {
if (
forceRefresh ||
!window.$gz.util.has(window.$gz.store.state.formCustomTemplate, formKey)
) {
//fetch and populate the store
const res = await window.$gz.api.get("form-custom/" + formKey);
if (res.error) {
throw new Error(window.$gz.errorHandler.errorToString(res, vm));
}
window.$gz.store.commit("setFormCustomTemplateItem", {
formKey: formKey,
concurrency: res.data.concurrency,
value: addDataKeyNames(JSON.parse(res.data.template))
});
}
},
set(formKey, token, template) {
window.$gz.store.commit("setFormCustomTemplateItem", {
formKey: formKey,
concurrency: token,
value: addDataKeyNames(JSON.parse(template))
});
},
getFieldTemplateValue(formKey, fieldKey) {
if (fieldKey === undefined) {
throw new Error(
"ERROR form-custom-template::getFieldTemplateValue -> fieldKey not specified for template for form [" +
formKey +
"]"
);
}
const template = window.$gz.store.state.formCustomTemplate[formKey];
if (template === undefined) {
throw new Error(
"ERROR form-custom-template::getFieldTemplateValue -> Store is missing form template for [" +
formKey +
"]"
);
}
//Note that not every field being requested will exist so it's valid to return undefined
//template is an array of objects that contain a key called "fld"
return template.find(z => z.fld == fieldKey);
},
getTemplateConcurrencyToken(formKey) {
const tok =
window.$gz.store.state.formCustomTemplate[formKey + "_concurrencyToken"];
if (tok === undefined) {
throw new Error(
"ERROR form-custom-template::getTemplateConcurrencyToken -> Store is missing concurrency token for [" +
formKey +
"]"
);
}
return tok;
}
};

672
src/api/gzapi.js Normal file
View File

@@ -0,0 +1,672 @@
import router from "../router";
function stringifyPrimitive(v) {
switch (typeof v) {
case "string":
return v;
case "boolean":
return v ? "true" : "false";
case "number":
return isFinite(v) ? v : "";
default:
return "";
}
}
////////////////////////////////////////////
// Try to handle an api error
// return true if handled or false if not
//
function handleError(action, error, route) {
const errorMessage =
"API error: " + action + " route =" + route + ", message =" + error.message;
window.$gz.store.commit("logItem", errorMessage);
//Handle 403 not authorized
//popup not authorized, log, then go to HOME
//was going to go back one page, but realized most of the time a not authorized is in
//reaction to directly entered or opened link, not application logic driving it, so home is safest choice
//
if (error.message && error.message.includes("NotAuthorized")) {
window.$gz.eventBus.$emit(
"notify-warning",
window.$gz.translation.get("ErrorUserNotAuthorized")
);
router.push(window.$gz.store.state.homePage);
throw new Error("LT:ErrorUserNotAuthorized");
}
//Handle 401 not authenticated
if (error.message && error.message.includes("NotAuthenticated")) {
window.$gz.eventBus.$emit(
"notify-error",
window.$gz.translation.get("ErrorUserNotAuthenticated")
);
router.push("/login");
throw new Error("LT:ErrorUserNotAuthenticated");
}
//is it a network error?
//https://medium.com/@vinhlh/how-to-handle-networkerror-when-using-fetch-ff2663220435
if (error instanceof TypeError) {
if (
error.message.includes("Failed to fetch") ||
error.message.includes("NetworkError") ||
error.message.includes("Network request failed")
) {
let msg = "";
if (window.$gz.store.state.authenticated) {
msg = window.$gz.translation.get("ErrorServerUnresponsive");
} else {
msg = "Could not connect to Sockeye server ";
}
msg += window.$gz.api.APIUrl("") + "\r\nError: " + error.message;
window.$gz.eventBus.$emit("notify-error", msg);
//note: using translation key in square brackets
throw new Error(msg);
}
}
//Ideally this should never get called because any issue should be addressed above
window.$gz.errorHandler.handleFormError(error);
}
export default {
status(response) {
//Handle expected api errors
if (response.status == 401) {
throw new Error("LT:ErrorUserNotAuthenticated");
}
if (response.status == 403) {
throw new Error("LT:ErrorUserNotAuthorized");
}
//404 not found is an expected status not worth logging allow to bubble up
//for client code to deal with
if (response.status == 404) {
return Promise.resolve(response);
}
if (response.status == 405) {
//Probably a development error
throw new Error("Method Not Allowed (route issue?) " + response.url);
}
if (response.status >= 200 && response.status < 300) {
return Promise.resolve(response);
} else {
//log unhandled api error
window.$gz.store.commit(
"logItem",
"API error: status=" +
response.status +
", statusText=" +
response.statusText +
", url=" +
response.url
);
//let it float up for dealing with by caller(s)
return Promise.resolve(response);
}
},
statusEx(response) {
//Handle expected api errors
if (response.status == 401) {
throw new Error("LT:ErrorUserNotAuthenticated");
}
if (response.status == 403) {
throw new Error("LT:ErrorUserNotAuthorized");
}
//404 not found is an expected status not worth logging allow to bubble up
//for client code to deal with
if (response.status == 404) {
return;
}
if (response.status == 405) {
//Probably a development error
throw new Error("Method Not Allowed (route issue?) " + response.url);
}
if (response.status >= 200 && response.status < 300) {
return;
} else {
//log unhandled api error
window.$gz.store.commit(
"logItem",
"API error: status=" +
response.status +
", statusText=" +
response.statusText +
", url=" +
response.url
);
}
},
async extractBodyEx(response) {
if (response.status == 204) {
//no content, nothing to process
return response;
}
const contentType = response.headers.get("content-type");
if (!contentType) {
return response;
}
if (contentType.includes("json")) {
return await response.json();
}
if (contentType.includes("text/plain")) {
return await response.text();
}
if (contentType.includes("application/pdf")) {
return await response.blob();
}
return response;
},
extractBody(response) {
if (response.status == 204) {
//no content, nothing to process
return response;
}
const contentType = response.headers.get("content-type");
if (!contentType) {
return response;
}
if (contentType.includes("json")) {
return response.json();
}
if (contentType.includes("text/plain")) {
return response.text();
}
return response;
},
apiErrorToHumanString(apiError) {
//empty error object?
if (!apiError) {
return "(E18) - apiErrorToHumanString():: Empty API eror, unknown";
}
//convert to readable string
return "(E18) - " + JSON.stringify(apiError);
},
patchAuthorizedHeaders() {
return {
Accept: "application/json",
"Content-Type": "application/json-patch+json",
Authorization: "Bearer " + window.$gz.store.state.apiToken
};
},
postAuthorizedHeaders() {
return {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: "Bearer " + window.$gz.store.state.apiToken
//this maybe useful in future like batch ops etc so keeping as a reminder
//,"X-AY-Import-Mode": true
};
},
postUnAuthorizedHeaders() {
return {
Accept: "application/json",
"Content-Type": "application/json"
};
},
fetchPostNoAuthOptions(data) {
return {
method: "post",
mode: "cors",
headers: this.postUnAuthorizedHeaders(),
body: JSON.stringify(data)
};
},
fetchPostOptions(data) {
return {
method: "post",
mode: "cors",
headers: this.postAuthorizedHeaders(),
body: JSON.stringify(data)
};
},
fetchPutOptions(data) {
return {
method: "put",
mode: "cors",
headers: this.postAuthorizedHeaders(),
body: JSON.stringify(data)
};
},
fetchGetOptions() {
/* GET WITH AUTH */
return {
method: "get",
mode: "cors",
headers: this.postAuthorizedHeaders()
};
},
fetchRemoveOptions() {
/* REMOVE WITH AUTH */
return {
method: "delete",
mode: "cors",
headers: this.postAuthorizedHeaders()
};
},
APIUrl(apiPath) {
if (
window.$gz.dev &&
window.location.hostname == "localhost" &&
window.location.port == "8080"
) {
return "http://localhost:7676/api/v8.0/" + apiPath;
}
return (
window.location.protocol +
"//" +
window.location.host +
"/api/v8.0/" +
apiPath
);
},
helpUrl() {
if (
window.$gz.dev &&
window.location.hostname == "localhost" &&
window.location.port == "8080"
) {
return "http://localhost:7676/docs/";
}
return window.location.protocol + "//" + window.location.host + "/docs/";
},
helpUrlCustomer() {
if (
window.$gz.dev &&
window.location.hostname == "localhost" &&
window.location.port == "8080"
) {
return "http://localhost:7676/cust/";
}
return window.location.protocol + "//" + window.location.host + "/cust/";
},
/////////////////////////////
// Just the server itself
// used by profiler etc
//
ServerBaseUrl() {
return this.helpUrl().replace("/docs/", "/");
},
/////////////////////////////
// generic routed download URL
//
genericDownloadUrl(route) {
//http://localhost:7676/api/v8/backup/download/100?t=sssss
return this.APIUrl(route + "?t=" + window.$gz.store.state.downloadToken);
},
/////////////////////////////
// report file download URL
//
reportDownloadUrl(fileName) {
//http://localhost:7676/api/v8/report/download/filename.pdf?t=sssss
return this.APIUrl(
"report/download/" +
fileName +
"?t=" +
window.$gz.store.state.downloadToken
);
},
/////////////////////////////
// backup file download URL
//
backupDownloadUrl(fileName) {
//http://localhost:7676/api/v8/backup/download/100?t=sssss
return this.APIUrl(
"backup/download/" +
fileName +
"?t=" +
window.$gz.store.state.downloadToken
);
},
/////////////////////////////
// attachment download URL
//
attachmentDownloadUrl(fileId, ctype) {
//http://localhost:7676/api/v8/attachment/download/100?t=sssss
//Ctype is optional and is the MIME content type, used to detect image urls at client for drag and drop ops
//in wiki but ignored by server
let url =
"attachment/download/" +
fileId +
"?t=" +
window.$gz.store.state.downloadToken;
if (ctype && ctype.includes("image")) {
url += "&i=1";
}
return this.APIUrl(url);
},
/////////////////////////////
// logo download URL
// (size= 'small', 'medium', 'large')
logoUrl(size) {
//http://localhost:7676/api/v8/logo/small
return this.APIUrl("logo/" + size);
},
/////////////////////////////
// REPLACE END OF URL
// (used to change ID in url)
replaceAfterLastSlash(theUrl, theReplacement) {
return theUrl.substr(0, theUrl.lastIndexOf("\\") + 1) + theReplacement;
},
/////////////////////////////
// ENCODE QUERY STRING
//
buildQuery(obj, sep, eq, name) {
sep = sep || "&";
eq = eq || "=";
if (obj === null) {
obj = undefined;
}
if (typeof obj === "object") {
return Object.keys(obj)
.map(function(k) {
const ks = encodeURIComponent(stringifyPrimitive(k)) + eq;
if (Array.isArray(obj[k])) {
return obj[k]
.map(function(v) {
return ks + encodeURIComponent(stringifyPrimitive(v));
})
.join(sep);
} else {
return ks + encodeURIComponent(stringifyPrimitive(obj[k]));
}
})
.filter(Boolean)
.join(sep);
}
if (!name) return "";
return (
encodeURIComponent(stringifyPrimitive(name)) +
eq +
encodeURIComponent(stringifyPrimitive(obj))
);
},
///////////////////////////////////
// GET DATA FROM API SERVER
//
async get(route) {
try {
const that = this;
let r = await fetch(that.APIUrl(route), that.fetchGetOptions());
that.statusEx(r);
r = await that.extractBodyEx(r);
return r;
} catch (error) {
//fundamental error, can't proceed with this call
handleError("GET", error, route);
}
},
//////////////////////////////////////
// Test delay for troubleshooting
//
doDelayAsync: () => {
// eslint-disable-next-line
return new Promise(resolve => {
setTimeout(() => resolve("I did something"), 10000);
});
},
///////////////////////////////////
// POST / PUT DATA TO API SERVER
//
async upsert(route, data, isLogin = false) {
try {
const that = this;
//determine if this is a new or existing record
let fetchOptions = undefined;
//put?
if (data && data.concurrency) {
fetchOptions = that.fetchPutOptions(data);
} else {
//post
//ensure the route doesn't end in /0 which will happen if it's a new record
//since the edit forms just send the url here with the ID regardless
if (route.endsWith("/0")) {
route = route.slice(0, -2);
}
if (isLogin == false) {
fetchOptions = that.fetchPostOptions(data);
} else {
fetchOptions = that.fetchPostNoAuthOptions(data);
}
}
let r = await fetch(that.APIUrl(route), fetchOptions);
that.statusEx(r);
r = await that.extractBodyEx(r);
return r;
} catch (error) {
if (isLogin == false) {
handleError("UPSERT", error, route);
} else {
//specifically this is for the login page
console.log("upser error is: ", error);
throw new Error(window.$gz.errorHandler.errorToString(error));
}
}
},
///////////////////////////////////
// DELETE DATA FROM API SERVER
//
async remove(route) {
const that = this;
try {
let r = await fetch(that.APIUrl(route), that.fetchRemoveOptions());
that.statusEx(r);
//delete will return a body if there is an error of some kind with the request
r = await that.extractBodyEx(r);
return r;
} catch (error) {
//fundamental error, can't proceed with this call
handleError("DELETE", error, route);
}
},
///////////////////////////////////
// PUT DATA TO API SERVER
// (used for puts that can't have a concurrency token like above)
async put(route, data) {
try {
const that = this;
let r = await fetch(that.APIUrl(route), that.fetchPutOptions(data));
that.statusEx(r);
r = await that.extractBodyEx(r);
return r;
} catch (error) {
handleError("PUT", error, route);
}
},
///////////////////////////////////
// POST DATA TO API SERVER
// (used for post only routes not needing upserts)
async post(route, data) {
try {
const that = this;
let r = await fetch(that.APIUrl(route), that.fetchPostOptions(data));
that.statusEx(r);
r = await that.extractBodyEx(r);
return r;
} catch (error) {
handleError("POST", error, route);
}
},
///////////////////////////////////
// POST FILE ATTACHMENTS
// @param {sockId:objectid, sockType:aType, files:[array of files]}
//
async uploadAttachment(at) {
const that = this;
try {
var files = at.files;
var data = new FormData();
for (var i = 0; i < files.length; i++) {
data.append(files[i].name, files[i]);
}
data.append("AttachToAType", at.sockType);
data.append("AttachToObjectId", at.sockId);
data.append("Notes", at.notes);
data.append("FileData", at.fileData);
//-----------------
const fetchOptions = {
method: "post",
mode: "cors",
headers: {
Authorization: "Bearer " + window.$gz.store.state.apiToken
},
body: data
};
let r = await fetch(that.APIUrl("attachment"), fetchOptions);
that.statusEx(r);
r = await that.extractBodyEx(r);
return r;
} catch (error) {
handleError("POSTATTACHMENT", error, "uploadAttachmentRoute");
}
},
//////////////////////////////////////////////
// POST (UPLOAD) FILE TO ARBITRARY ROUTE
// for various things that require an upload
// e.g. translation import etc
//
//
async upload(route, at) {
const that = this;
try {
var files = at.files;
var data = new FormData();
for (var i = 0; i < files.length; i++) {
data.append(files[i].name, files[i]);
}
if (at.sockType) {
data.append("SockType", at.sockType);
}
if (at.sockId) {
data.append("ObjectId", at.sockId);
}
if (at.notes) {
data.append("Notes", at.notes);
}
data.append("FileData", at.fileData);
//-----------------
const fetchOptions = {
method: "post",
mode: "cors",
headers: {
Authorization: "Bearer " + window.$gz.store.state.apiToken
},
body: data
};
let r = await fetch(that.APIUrl(route), fetchOptions);
that.statusEx(r);
r = await that.extractBodyEx(r);
return r;
} catch (error) {
handleError("POSTATTACHMENT", error, route);
}
},
///////////////////////////////////
// POST LOGO
//
//
async uploadLogo(fileData, size) {
const that = this;
try {
const data = new FormData();
data.append(fileData.name, fileData);
//-----------------
const fetchOptions = {
method: "post",
mode: "cors",
headers: {
Authorization: "Bearer " + window.$gz.store.state.apiToken
},
body: data
};
let r = await fetch(that.APIUrl("logo/" + size), fetchOptions);
that.statusEx(r);
r = await that.extractBodyEx(r);
return r;
} catch (error) {
handleError("uploadLogo", error, "postLogoRoute");
}
},
///////////////////////////////////
// REPORT CLIENT META DATA
//
//
reportClientMetaData() {
const nowUtc = window.$gz.locale.nowUTC8601String();
return {
UserName: window.$gz.store.state.userName,
UserId: window.$gz.store.state.userId,
Authorization: "Bearer " + window.$gz.store.state.apiToken, //api token for using api methods as current user viewing report
DownloadToken: window.$gz.store.state.downloadToken,
TimeZoneName: window.$gz.locale.getResolvedTimeZoneName(),
LanguageName: window.$gz.locale.getResolvedLanguage(),
Hour12: window.$gz.locale.getHour12(),
CurrencyName: window.$gz.locale.getCurrencyName(),
DefaultLocale: window.$gz.locale.getResolvedLanguage().split("-", 1)[0], //kind of suspect, maybe it can be removed
PDFDate: window.$gz.locale.utcDateToShortDateLocalized(nowUtc),
PDFTime: window.$gz.locale.utcDateToShortTimeLocalized(nowUtc)
};
},
///////////////////////////////////
// FETCH BIZ OBJECT NAME
//
//
async fetchBizObjectName(sockType, objectId) {
const res = await this.get(`name/${sockType}/${objectId}`);
//We never expect there to be no data here
if (!Object.prototype.hasOwnProperty.call(res, "data")) {
return Promise.reject(res);
} else {
return res.data;
}
}
//---------------
//new functions above here
};

188
src/api/gzdialog.js Normal file
View File

@@ -0,0 +1,188 @@
let VM_LOCAL = null;
//Calculate a reasonable time to show the alert based on the size of the message and some sane bounds
//https://ux.stackexchange.com/a/85898
function CalculateDelay(msg) {
//Min 2 seconds max 8 seconds
return Math.min(Math.max(msg.length * 50, 3000), 8000);
}
/////////////////////////////////
// Dialog, toast, notification
// utils and handlers
//
export default {
///////////////////////////////////
// WIRE UP DIALOG EVENTS
//
// called once from app.vue only
//
wireUpEventHandlers(vm) {
//###########################################
//Notifications: pops up and slowly disappears
//ACTUAL UI IN gznotify.vue
//###########################################
///////////
//ERROR
window.$gz.eventBus.$on("notify-error", function handleNotifyWarn(
msg,
helpUrl
) {
//log full message
window.$gz.store.commit("logItem", "notify-error: " + msg);
//trim really long message as it's likely useless beyond the first few lines (stack trace etc)
msg = msg.substring(0, 600);
vm.$root.$gznotify({
message: msg,
type: "error",
timeout: CalculateDelay(msg),
helpUrl: helpUrl
});
});
///////////
//WARNING
window.$gz.eventBus.$on("notify-warning", function handleNotifyWarn(
msg,
helpUrl
) {
window.$gz.store.commit("logItem", "notify-warning: " + msg);
msg = msg.substring(0, 600);
vm.$root.$gznotify({
message: msg,
type: "warning",
timeout: CalculateDelay(msg),
helpUrl: helpUrl
});
});
///////////
//INFO
window.$gz.eventBus.$on("notify-info", function handleNotifyInfo(
msg,
helpUrl
) {
window.$gz.store.commit("logItem", "notify-info: " + msg);
msg = msg.substring(0, 600);
vm.$root.$gznotify({
message: msg,
type: "info",
timeout: CalculateDelay(msg),
helpUrl: helpUrl
});
});
///////////
//SUCCESS
window.$gz.eventBus.$on("notify-success", function handleNotifySuccess(
msg,
helpUrl
) {
vm.$root.$gznotify({
message: msg,
type: "success",
timeout: CalculateDelay(msg),
helpUrl: helpUrl
});
});
VM_LOCAL = vm;
},
//###########################################
//CONFIRMATION DIALOGS
//ACTUAL UI IN gzconfirm.vue
//###########################################
/////////////////////////////////////
// Are you sure you want to delete?
//
confirmDelete() {
return VM_LOCAL.$root.$gzconfirm({
message: window.$gz.translation.get("DeletePrompt"),
yesButtonText: window.$gz.translation.get("Delete"),
noButtonText: window.$gz.translation.get("Cancel"),
type: "warning"
});
},
/////////////////////////////////////
// Are you sure you want to leave unsaved?
//
confirmLeaveUnsaved() {
return VM_LOCAL.$root.$gzconfirm({
message: window.$gz.translation.get("AreYouSureUnsavedChanges"),
yesButtonText: window.$gz.translation.get("Leave"),
noButtonText: window.$gz.translation.get("Cancel"),
type: "warning"
});
},
/////////////////////////////////////
// Display LT message with wait for ok
//
displayLTErrorMessage(tKeyText, tKeyTitle = undefined) {
return VM_LOCAL.$root.$gzconfirm({
message: tKeyText ? window.$gz.translation.get(tKeyText) : "",
title: tKeyTitle ? window.$gz.translation.get(tKeyTitle) : "",
yesButtonText: window.$gz.translation.get("OK"),
type: "error"
});
},
/////////////////////////////////////
// Display LT message with wait for ok
//
displayLTModalNotificationMessage(
tKeyText,
tKeyTitle = undefined,
ttype = "info",
tHelpUrl = undefined
) {
return VM_LOCAL.$root.$gzconfirm({
message: tKeyText ? window.$gz.translation.get(tKeyText) : "",
title: tKeyTitle ? window.$gz.translation.get(tKeyTitle) : "",
yesButtonText: window.$gz.translation.get("OK"),
type: ttype,
helpUrl: tHelpUrl
});
},
/////////////////////////////////////
// Custom confirmation
//
confirmGeneric(tKey, ttype = "info") {
return VM_LOCAL.$root.$gzconfirm({
message: window.$gz.translation.get(tKey),
yesButtonText: window.$gz.translation.get("OK"),
noButtonText: window.$gz.translation.get("Cancel"),
type: ttype
});
},
/////////////////////////////////////
// Custom confirmation pre-translated
//
confirmGenericPreTranslated(msg, ttype = "info") {
return VM_LOCAL.$root.$gzconfirm({
message: msg,
yesButtonText: window.$gz.translation.get("OK"),
noButtonText: window.$gz.translation.get("Cancel"),
type: ttype
});
},
/////////////////////////////////////
// Custom confirmation no translation
// with all options available
//
displayNoTranslationModalNotificationMessage(
tKeyText,
tKeyTitle = undefined,
ttype = "info",
tHelpUrl = undefined
) {
return VM_LOCAL.$root.$gzconfirm({
message: tKeyText,
title: tKeyTitle,
yesButtonText: window.$gz.translation.get("OK"),
type: ttype,
helpUrl: tHelpUrl
});
}
//new functions above here
};

1002
src/api/gzform.js Normal file

File diff suppressed because it is too large Load Diff

438
src/api/gzmenu.js Normal file
View File

@@ -0,0 +1,438 @@
/////////////////////////////////
// Menu utils and handlers
//
export default {
///////////////////////////////////////////
// TECH SUPPORT / CONTACT FORUM URL
//
contactSupportUrl() {
const dbId = encodeURIComponent(
window.$gz.store.state.globalSettings.serverDbId
);
const company = encodeURIComponent(
window.$gz.store.state.globalSettings.company
);
return `https://contact.ayanova.com/contact?dbid=${dbId}&company=${company}`;
},
///////////////////////////////
// CHANGE HANDLER
//
// Deal with a menu change request
// called from App.vue
handleMenuChange(vm, ctx) {
const UTILITY_TYPES = [
window.$gz.type.NoType,
window.$gz.type.Global,
window.$gz.type.NoType,
window.$gz.type.ServerState,
window.$gz.type.License,
window.$gz.type.LogFile,
window.$gz.type.ServerJob,
window.$gz.type.TrialSeeder,
window.$gz.type.ServerMetrics,
window.$gz.type.UserOptions,
window.$gz.type.FormCustom,
window.$gz.type.DataListSavedFilter,
window.$gz.type.GlobalOps,
window.$gz.type.BizMetrics,
window.$gz.type.Backup,
window.$gz.type.Notification,
window.$gz.type.NotifySubscription
];
vm.appBar.isMain = ctx.isMain;
vm.appBar.icon = ctx.icon;
vm.appBar.title = ""; //this prevents fou[translated]c
vm.appBar.readOnly = ctx.readOnly;
if (ctx.readOnly === true) {
vm.appBar.color = "readonlybanner";
} else {
vm.appBar.color = ctx.isMain ? "primary" : "secondary";
}
//ctx.title if set is a Translation key
//ctx.formData.recordName is the object name or serial number or whatever identifies it uniquely
let recordName = "";
if (
ctx &&
ctx.formData &&
ctx.formData.recordName &&
ctx.formData.recordName != "null" //some forms (part) present "null" as the record name due to attempts to format a name so if that's the case just turn it into null here to bypass
) {
recordName = ctx.formData.recordName;
}
const hasRecordName = !window.$gz.util.stringIsNullOrEmpty(recordName);
if (ctx.title) {
//it has a title translation key
const translatedTitle = vm.$sock.t(ctx.title);
if (hasRecordName) {
//recordname takes all precedence in AppBar in order to conserve space (narrow view etc)
//also it just looks cleaner, the icon is already there to indicate where the user is at
vm.appBar.title = recordName;
document.title = `${recordName} - ${translatedTitle} Sockeye `;
} else {
vm.appBar.title = translatedTitle;
document.title = `${translatedTitle} ${recordName}`;
}
} else {
if (hasRecordName) {
//not title but has record name
vm.appBar.title = recordName;
document.title = `${recordName} Sockeye`;
} else {
document.title = "Sockeye";
}
}
//Parse the formdata if present
//FORMDATA is OPTIONAL and only required for forms that need to allow
//viewing object history, attachments, custom fields, etc that kind of thing
//usually CORE objects with an id, NOT utility type forms
let formSockType = 0;
let formRecordId = 0;
if (ctx.formData) {
if (ctx.formData.sockType != null) {
formSockType = ctx.formData.sockType;
}
if (ctx.formData.recordId != null) {
formRecordId = ctx.formData.recordId;
}
}
//flag for if it's wikiable, reviewable, attachable, searchable, historical
const isCoreBizObject = formSockType != 0 && formRecordId != 0;
//set the help url if presented or default to the User section intro
vm.appBar.helpUrl = ctx.helpUrl ? ctx.helpUrl : "user-intro";
vm.appBar.menuItems = [];
//CONTEXT TOP PORTION
//populate the context portion of the menu so handle accordingly
if (ctx.menuItems) {
vm.appBar.menuItems = ctx.menuItems;
}
//STANDARD BIZ OBJECT OPTIONS
//NOTE: This applies equally to all core business object types that are basically real world and have an id and a type (all are wikiable, attachable and reviewable)
//Not utility type objects like datalist etc
//there will be few exceptions so they will be coded in later if needed but assume anything with an id and a type
if (isCoreBizObject && !ctx.hideCoreBizStandardOptions) {
//"Review" was follow up type of schedule marker
//basically it's now a "Reminder" type of object but it's own thing with separate collection
vm.appBar.menuItems.push({
title: "Review",
icon: "$sockiCalendarCheck",
key: "app:review",
data: {
sockType: formSockType,
recordId: formRecordId,
recordName: recordName
}
});
//AFAIK right now any item with an id and a type can have a history
//anything not would be the exception rather than the rule
vm.appBar.menuItems.push({
title: "History",
icon: "$sockiHistory",
key: "app:history",
data: { sockType: formSockType, recordId: formRecordId }
});
}
//CUSTOMIZE
//set custom fields and link to translation text editor
if (
isCoreBizObject &&
ctx.formData &&
ctx.formData.formCustomTemplateKey != undefined &&
window.$gz.role.hasRole([
window.$gz.role.AUTHORIZATION_ROLES.BizAdmin,
window.$gz.role.AUTHORIZATION_ROLES.BizAdminRestricted
])
) {
//NOTE: BizAdmin can edit, BizAdminRestricted can read only
//add customize menu item
//customize
vm.appBar.menuItems.push({
title: "Customize",
icon: "$sockiCustomize",
data: ctx.formData.formCustomTemplateKey,
key: "app:customize"
});
}
//GLOBAL BOTTOM PORTION
//SEARCH
//all forms except the search form
if (!ctx.hideSearch && !UTILITY_TYPES.includes(formSockType)) {
//For all forms but not on the search form itself; if this is necessary for others then make a nosearch or something flag controlled by incoming ctx but if not then this should suffice
vm.appBar.menuItems.push({
title: "Search",
icon: "$sockiSearch",
key: "app:search",
data: formSockType
});
}
//HELP
vm.appBar.menuItems.push({
title: "MenuHelp",
icon: "$sockiQuestionCircle",
key: "app:help",
data: vm.appBar.helpUrl
});
//ABOUT
if (!isCoreBizObject && ctx.helpUrl != "sock-about") {
vm.appBar.menuItems.push({
title: "HelpAboutSockeye",
icon: "$sockiInfoCircle",
key: "app:nav:abt",
data: "sock-about"
});
}
},
//Unused to date of beta 0.9
// ///////////////////////////////
// // CHANGE HANDLER
// //
// // Deal with a menu item update request
// // called from App.vue
// handleReplaceMenuItem(vm, newItem) {
// if (!vm.appBar.menuItems || !newItem) {
// return;
// }
// //Find the key that is in the collection and replace it
// for (let i = 0; i < vm.appBar.menuItems.length; i++) {
// if (vm.appBar.menuItems[i].key == newItem.key) {
// //NOTE: since we are adding a new object, it has no reactivity in it so we need to use the Vue.Set to set it which
// //automatically adds the setters and getters that trigger reactivity
// //If it was set directly on the array it wouldn't update the UI
// vm.$set(vm.appBar.menuItems, i, newItem);
// return;
// }
// }
// },
//////////////////////////////////////////////
// LAST REPORT CHANGE HANDLER
// update / add last report menu item
//
handleUpsertLastReport(vm, newItem) {
if (!vm.appBar.menuItems || !newItem) {
return;
}
/*
window.$gz.eventBus.$emit("menu-upsert-last-report", {
title: reportSelected.name,
notrans: true,
icon: "$sockiFileAlt",
key: formKey + ":report:" + reportSelected.id,
vm: vm
});
*/
let key = null;
//Find the last report key and update it if present
for (let i = 0; i < vm.appBar.menuItems.length; i++) {
key = vm.appBar.menuItems[i].key;
if (key && key.includes(":report:")) {
vm.appBar.menuItems[i].key = newItem.key;
vm.appBar.menuItems[i].title = newItem.title;
return;
}
}
//No prior last report so slot it in under the report one
for (let i = 0; i < vm.appBar.menuItems.length; i++) {
key = vm.appBar.menuItems[i].key;
if (key && key.endsWith(":report")) {
vm.appBar.menuItems.splice(i + 1, 0, newItem);
}
}
},
///////////////////////////////
// ENABLE / DISABLE HANDLER
//
// Deal with a menu item enable / disable
// called from App.vue
handleDisableMenuItem(vm, key, disabled) {
if (!vm.appBar.menuItems || !key) {
return;
}
//Find the menu item and set it to disabled and recolor it to disabled color and return
for (let i = 0; i < vm.appBar.menuItems.length; i++) {
const menuItem = vm.appBar.menuItems[i];
if (menuItem.key == key) {
vm.$set(vm.appBar.menuItems[i], "disabled", disabled);
//menuItem.disabled = disabled;
vm.$set(vm.appBar.menuItems[i], "color", disabled ? "disabled" : "");
return;
}
}
},
///////////////////////////////
// CHANGE ICON HANDLER
// Change icon dymanically
// (note, can pass null for new icon to clear it)
//
handleChangeMenuItemIcon(vm, key, newIcon) {
if (!vm.appBar.menuItems || !key) {
return;
}
//Find the menu item and change it's icon
for (let i = 0; i < vm.appBar.menuItems.length; i++) {
const menuItem = vm.appBar.menuItems[i];
if (menuItem.key == key) {
vm.$set(vm.appBar.menuItems[i], "icon", newIcon);
return;
}
}
},
///////////////////////////////
// APP (GLOBAL) CLICK HANDLER
//
// Deal with a menu change request
// called from App.vue
handleAppClick(vm, menuItem) {
//Key will start with the string "app:" if it's a global application command that should be handled here,
//otherwise it's a local command for a local form only
//If there is any extended information required for the command it will be in the data property of the menu item
//split a key into component parts, part one is the responsible party, part two is the command, part three only exists to make it unique if necessary
//each part is separated by a colon
//Handle different items
const item = this.parseMenuItem(menuItem);
if (!item.disabled && item.owner == "app") {
switch (item.key) {
case "help":
if (item.data.includes("~customer~")) {
window.open(
window.$gz.api.helpUrlCustomer() +
item.data.replace("~customer~", ""),
"_blank"
);
} else {
window.open(window.$gz.api.helpUrl() + item.data, "_blank");
}
break;
case "search":
vm.$router.push({
name: "home-search",
params: { socktype: item.data }
});
break;
case "review":
//go to list
// path: "/home-reviews/:aType?/:objectId?",
vm.$router.push({
name: "home-reviews",
params: {
aType: window.$gz.util.stringToIntOrNull(item.data.sockType),
objectId: window.$gz.util.stringToIntOrNull(item.data.recordId),
name: item.data.recordName
}
});
break;
case "history":
vm.$router.push({
name: "sock-history",
params: {
socktype: item.data.sockType,
recordid: item.data.recordId
}
});
break;
case "customize":
vm.$router.push({
name: "sock-customize",
params: { formCustomTemplateKey: item.data }
});
break;
case "nav":
vm.$router.push({ name: item.data });
break;
default:
window.$gz.eventBus.$emit(
"notify-warning",
"gzmenu:handleAppClick - unrecognized command [" +
menuItem.key +
"]"
);
}
}
},
///////////////////////////////
// PARSE MENU ITEM CLICK
//
// parse out the parts of a
// menu item from a click event
//
parseMenuItem(menuItem) {
//format is "AREA:KEY:UNIQUEID"
//and data is in data portion
const keyparts = menuItem.key.split(":");
const ret = {
owner: keyparts[0],
key: keyparts[1],
data: menuItem.data,
disabled: menuItem.disabled,
vm: menuItem.vm ? menuItem.vm : null
};
if (keyparts.length > 2) {
ret.id = keyparts[2];
}
return ret;
},
///////////////////////////////////
// WIRE UP MENU EVENTS
//
// called once from app.vue only
//
wireUpEventHandlers(vm) {
const that = this;
window.$gz.eventBus.$on("menu-change", function handleMenuChange(ctx) {
that.handleMenuChange(vm, ctx);
});
window.$gz.eventBus.$on(
"menu-upsert-last-report",
function handleUpsertLastReport(newItem) {
that.handleUpsertLastReport(vm, newItem);
}
);
window.$gz.eventBus.$on("menu-disable-item", function handleDisableMenuItem(
key
) {
that.handleDisableMenuItem(vm, key, true);
});
window.$gz.eventBus.$on("menu-enable-item", function handleDisableMenuItem(
key
) {
that.handleDisableMenuItem(vm, key, false);
});
window.$gz.eventBus.$on(
"menu-change-item-icon",
function handleChangeMenuItemIcon(key, newIcon) {
that.handleChangeMenuItemIcon(vm, key, newIcon);
}
);
window.$gz.eventBus.$on("menu-click", function handleMenuClick(menuitem) {
that.handleAppClick(vm, menuitem);
});
}
//new functions above here
};

931
src/api/gzutil.js Normal file
View File

@@ -0,0 +1,931 @@
/////////////////////////////////
// General utility library
//
const icons = {
image: "$sockiFileImage",
pdf: "$sockiFilePdf",
word: "$sockiFileWord",
powerpoint: "$sockiFilePowerpoint",
excel: "$sockiFileExcel",
csv: "$sockiFileCsv",
audio: "$sockiFileAudio",
video: "$sockiFileVidio",
archive: "$sockiFileArchive",
code: "$sockiFileCode",
text: "$sockiFileAlt",
file: "$sockiFile"
};
const mimeTypes = {
"image/gif": icons.image,
"image/jpeg": icons.image,
"image/png": icons.image,
"image/webp": icons.image,
"application/pdf": icons.pdf,
"application/msword": icons.word,
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
icons.word,
"application/mspowerpoint": icons.powerpoint,
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
icons.powerpoint,
"application/msexcel": icons.excel,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
icons.excel,
"text/csv": icons.csv,
"audio/aac": icons.audio,
"audio/wav": icons.audio,
"audio/mpeg": icons.audio,
"audio/mp4": icons.audio,
"audio/ogg": icons.audio,
"video/x-msvideo": icons.video,
"video/mpeg": icons.video,
"video/mp4": icons.video,
"video/ogg": icons.video,
"video/quicktime": icons.video,
"video/webm": icons.video,
"application/gzip": icons.archive,
"application/zip": icons.archive,
"application/x-tar": icons.archive,
"text/css": icons.code,
"text/html": icons.code,
"text/javascript": icons.code,
"application/javascript": icons.code,
"text/plain": icons.text,
"text/richtext": icons.text,
"text/rtf": icons.text,
"application/rtf": icons.text,
"application/json": icons.text
};
const extensions = {
gif: icons.image,
jpeg: icons.image,
jpg: icons.image,
png: icons.image,
webp: icons.image,
pdf: icons.pdf,
doc: icons.word,
docx: icons.word,
ppt: icons.powerpoint,
pptx: icons.powerpoint,
xls: icons.excel,
xlsx: icons.excel,
csv: icons.csv,
aac: icons.audio,
mp3: icons.audio,
ogg: icons.audio,
avi: icons.video,
flv: icons.video,
mkv: icons.video,
mp4: icons.video,
gz: icons.archive,
zip: icons.archive,
tar: icons.archive,
"7z": icons.archive,
css: icons.code,
html: icons.code,
js: icons.code,
txt: icons.text,
json: icons.text,
rtf: icons.text
};
export default {
///////////////////////////////
// CLEAN OBJECT
// Clear all properties from object without resorting to assigning a new object (o={})
// which can be problematic in some cases (IE bugs, watched data items in forms etc)
removeAllPropertiesFromObject: function(o) {
for (let variableKey in o) {
if (Object.prototype.hasOwnProperty.call(o, variableKey)) {
delete o[variableKey];
}
}
},
///////////////////////////////
// DEEP COPY FOR API UPDATE
// Deep copy an object skipping all *Viz and named properties from object
//
deepCopySkip: function(source, skipNames) {
if (skipNames == null) {
skipNames = [];
}
let o = {};
for (let key in source) {
if (
!key.endsWith("Viz") &&
!skipNames.some(x => x == key) &&
Object.prototype.hasOwnProperty.call(source, key)
) {
o[key] = source[key];
}
}
return o;
},
/**
* Copy a string to clipboard
* @param {String} string The string to be copied to clipboard
* @return {Boolean} returns a boolean correspondent to the success of the copy operation.
* Modified from an example here: https://stackoverflow.com/a/53951634/8939
* Basically a fallback if navigator.clipboard is not available
*/
copyToClipboard: function(string) {
let textarea;
let result;
if (navigator && navigator.clipboard) {
navigator.clipboard.writeText(string);
} else {
try {
textarea = document.createElement("textarea");
textarea.setAttribute("readonly", true);
textarea.setAttribute("contenteditable", true);
textarea.style.position = "fixed"; // prevent scroll from jumping to the bottom when focus is set.
textarea.value = string;
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
const range = document.createRange();
range.selectNodeContents(textarea);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
textarea.setSelectionRange(0, textarea.value.length);
result = document.execCommand("copy");
} catch (err) {
result = null;
} finally {
document.body.removeChild(textarea);
}
// manual copy fallback using prompt
if (!result) {
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
const copyHotkey = isMac ? "⌘C" : "CTRL+C";
result = prompt(`Press ${copyHotkey}`, string);
if (!result) {
return false;
}
}
}
return true;
},
///////////////////////////////
// ROUNDING
// //https://medium.com/swlh/how-to-round-to-a-certain-number-of-decimal-places-in-javascript-ed74c471c1b8
roundAccurately: function(number, decimalPlaces) {
if (!number || number == 0 || Number.isNaN(number)) {
return number;
}
const wasNegative = number < 0;
if (wasNegative) {
number = Math.abs(number); //make sure it's positive because rounding negative numbers is weird in JS
}
number = Number(
Math.round(number + "e" + decimalPlaces) + "e-" + decimalPlaces
);
if (wasNegative) {
number = 0 - number;
}
return number;
},
///////////////////////////////
// CLEAN TAG NAME
// Clean up a tag with same rules as server
//
normalizeTag: function(tagName) {
if (!tagName || tagName == "") {
return null;
}
tagName = tagName.toLowerCase();
//spaces to dashes
tagName = tagName.replace(/ /gi, "-");
//multiple dashes to single dashes
tagName = tagName.replace(/-+/g, "-");
//ensure doesn't start or end with a dash
tagName = this.trimSpecific(tagName, "-");
//No longer than 255 characters
tagName = tagName.length > 255 ? tagName.substr(0, 255 - 1) : tagName;
return tagName;
},
///////////////////////////////
// Quick hash for trivial purposes
// not cryptographic
// https://stackoverflow.com/a/7616484/8939
//
quickHash: function(theString) {
let hash = 0;
let i;
let chr;
if (theString.length === 0) return hash;
for (i = 0; i < theString.length; i++) {
chr = theString.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
},
////////////////////////////////////////
// Random password / login generator
// https://stackoverflow.com/a/51540480/8939
// using 32 character (128 bit) as default
//
getRandomPassword: function() {
const wishlist = "0123456789abcdefghijkmnopqrstuvwxyz";
return Array.from(crypto.getRandomValues(new Uint32Array(32)))
.map(x => wishlist[x % wishlist.length])
.join("");
},
///////////////////////////////
// CONVERT STRING TO BOOLEAN
// https://stackoverflow.com/a/1414175/8939
//
stringToBoolean: function(string) {
switch (string.toLowerCase().trim()) {
case "true":
case "yes":
case "1":
return true;
case "false":
case "no":
case "0":
case null:
return false;
default:
return Boolean(string);
}
}, ///////////////////////////////
// CONVERT STRING TO FLOAT
// https://stackoverflow.com/a/9409894/8939
//
stringToFloat: function(string) {
//null or empty then zero
if (!string) {
return 0;
}
//A number already then parse and return
if (this.isNumeric(string)) {
if (Number.isNaN(string)) {
return 0;
}
return parseFloat(string);
}
//Not a string at all?
if (!this.isString(string)) {
return 0;
}
const ret = parseFloat(string.replace(/[^\d.-]/g, ""));
if (Number.isNaN(ret)) {
return 0;
}
return ret;
},
///////////////////////////////
// Is negative number
//
//
isNegative: function(v) {
//null or empty then zero
if (!v || v == 0 || Number.isNaN(v)) {
return false;
}
return parseFloat(v) < 0;
},
///////////////////////////////
// Splice a string
//changes the content of a string by removing a range of
// characters and/or adding new characters.
//
// @param {String} source string
// @param {number} start Index at which to start changing the string.
// @param {number} delCount An integer indicating the number of old chars to remove.
// @param {string} newSubStr The String that is spliced in.
// @return {string} A new string with the spliced substring.
stringSplice: function(source, start, delCount, newSubStr) {
if (source == null || source == "") {
if (newSubStr) {
return newSubStr;
}
return "";
}
return (
source.slice(0, start) +
newSubStr +
source.slice(start + Math.abs(delCount))
);
},
///////////////////////////////
// Truncate a string
//truncates and adds ellipses
//
// @param {String} source string
// @param {number} length desired
// @return {string} A new string truncated with ellipses at end
truncateString: function(s, len) {
if (this.stringIsNullOrEmpty(s)) {
return s;
}
if (s.length > len) {
return s.substring(0, len) + "...";
} else {
return s;
}
},
///////////////////////////////
// Format tags for display
//
//
// @param {String} tags raw from server
// @return {string} A new string with the tags formatted or an empty string if no tags
formatTags: function(tags) {
if (tags && tags.length > 0) {
return tags.join(", ");
}
return "";
},
///////////////////////////////
// ICON FOR *ALL* OBJECT TYPES
//(used for search results and event log / history)
//NOTE: Any object type could appear in event log, they all need to be supported where possible
//CoreBizObject add here
iconForType: function(sockType) {
switch (sockType) {
case window.$gz.type.NoType:
case null:
return "$sockiGenderless";
case window.$gz.type.Global:
return "$sockiGlobe";
case window.$gz.type.User:
return "$sockiUser";
case window.$gz.type.ServerState:
return "$sockiDoorOpen";
case window.$gz.type.LogFile:
return "$sockiGlasses";
case window.$gz.type.PickListTemplate:
return "$sockiPencilRuler";
case window.$gz.type.Customer:
return "$sockiAddressCard";
case window.$gz.type.Vendor:
return "$ayiStore";
case window.$gz.type.ServerJob:
return "$sockiRobot";
case window.$gz.type.Metrics:
return "$sockiFileMedicalAlt";
case window.$gz.type.Translation:
return "$sockiLanguage";
case window.$gz.type.UserOptions:
return "$sockiUserCog";
case window.$gz.type.HeadOffice:
return "$sockiSitemap";
case window.$gz.type.FileAttachment:
return "$sockiPaperclip";
case window.$gz.type.DataListSavedFilter:
return "$sockiFilter";
case window.$gz.type.FormCustom:
return "$sockiCustomize";
case window.$gz.type.Backup:
return "$sockiFileArchive";
case window.$gz.type.Notification:
return "$sockiBell";
case window.$gz.type.NotifySubscription:
return "$sockiBullhorn";
case window.$gz.type.Reminder:
return "$sockiStickyNote";
case window.$gz.type.OpsNotificationSettings:
return "$sockiBullhorn";
case window.$gz.type.Report:
return "$sockiThList";
case window.$gz.type.DashboardView:
return "$sockiTachometer";
case window.$gz.type.CustomerNote:
return "$sockiClipboard";
case window.$gz.type.Memo:
return "$sockiInbox";
case window.$gz.type.Review:
return "$sockiCalendarCheck";
case window.$gz.type.License:
return "$sockiGem";
case window.$gz.type.TrialLicenseRequest:
return "$sockiHandHoldingWater";
case window.$gz.type.SubscriptionServer:
return "$sockiCloud";
case window.$gz.type.Purchase:
return "$sockiShoppingCart";
case window.$gz.type.Product:
return "$sockiBarCode";
case window.$gz.type.GZCase:
return "$sockiCoffee";
//scroll icon is good one for something
default:
return null;
}
},
//https://gist.github.com/colemanw/9c9a12aae16a4bfe2678de86b661d922
iconForFile: function(fileName, mimeType) {
// List of official MIME Types: http://www.iana.org/assignments/media-types/media-types.xhtml
let extension = null;
if (fileName && fileName.includes(".")) {
extension = fileName.split(".").pop();
extension = extension.toLowerCase();
}
if (!extension && !mimeType) {
console.log(
"gzutil:iconForFile -> No mime or extension for " +
fileName +
" " +
mimeType
);
return "$sockiFile";
}
if (!mimeType) {
mimeType = "";
}
mimeType = mimeType.toLowerCase();
const iconFromExtension = extensions[extension];
const iconFromMIME = mimeTypes[mimeType];
if (iconFromMIME) {
return iconFromMIME;
}
if (iconFromExtension) {
return iconFromExtension;
}
return "$sockiFile";
},
///////////////////////////////////////////////
// attempt to detect image extension name
//
isImageAttachment: function(fileName, mimeType) {
return this.iconForFile(fileName, mimeType) == "$sockiFileImage";
},
///////////////////////////////////////////////
// Sleep async
//
sleepAsync: function(milliseconds) {
// eslint-disable-next-line
return new Promise(resolve => setTimeout(resolve, milliseconds));
},
///////////////////////////////////////////////
// sortByKey lodash "sortBy" replacement
// https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_sortby-and-_orderby
//usage:
// The native sort modifies the array in place. `_.orderBy` and `_.sortBy` do not, so we use `.concat()` to
// copy the array, then sort.
// fruits.concat().sort(sortBy("name"));
// => [{name:"apple", amount: 4}, {name:"banana", amount: 2}, {name:"mango", amount: 1}, {name:"pineapple", amount: 2}]
sortByKey: key => {
return (a, b) => {
const aaa = a[key].toUpperCase();
const bbb = b[key].toUpperCase();
return aaa > bbb ? 1 : bbb > aaa ? -1 : 0;
//this was the original but it was sorting weird as it was taking case into account with uppercase higher than lowercase
//so PMItem came before Part in the object lists
//return a[key] > b[key] ? 1 : b[key] > a[key] ? -1 : 0;
};
},
///////////////////////////////////////////////
// "has" lodash replacement
// https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_has
//
has: function(obj, key) {
var keyParts = key.split(".");
return (
!!obj &&
(keyParts.length > 1
? this.has(obj[key.split(".")[0]], keyParts.slice(1).join("."))
: hasOwnProperty.call(obj, key))
);
},
///////////////////////////////////////////////
// Check if object is empty
//
objectIsEmpty: function(obj) {
//https://stackoverflow.com/a/4994265/8939
return !obj || Object.keys(obj).length === 0;
},
///////////////////////////////////////////////
// Trim specific character from start and end
// https://stackoverflow.com/a/55292366/8939
//
trimSpecific: function trim(str, ch) {
var start = 0;
var end = str.length;
while (start < end && str[start] === ch) ++start;
while (end > start && str[end - 1] === ch) --end;
return start > 0 || end < str.length ? str.substring(start, end) : str;
},
///////////////////////////////////////////////
// is numeric replacement for lodash
// https://stackoverflow.com/a/52986361/8939
//
isNumeric: function(n) {
//lodash isNumber returned false if it's a string and that's what the rest of the code expects even though it's parseable to a number
return !this.isString(n) && !isNaN(parseFloat(n)) && isFinite(n);
},
///////////////////////////////////////////////
// is string replacement for lodash
// https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_isString
//
isString: function(str) {
return str != null && typeof str.valueOf() === "string";
},
///////////////////////////////////////////////
//
//
//
stringIsNullOrEmpty: function(str) {
if (str === null || str === undefined) {
return true;
}
if (this.isString(str)) {
if (str.trim() == "") {
return true;
}
}
return false;
},
///////////////////////////////////////////////
// is Boolean replacement for lodash
// https://stackoverflow.com/a/43718478/8939
//
isBoolean: function(obj) {
return obj === true || obj === false || typeof variable === "boolean";
},
///////////////////////////////////////////////
// parse to number or null if not a number
// used because route params can turn into strings
// on their own
//
stringToIntOrNull: function(n) {
const ret = Number.parseInt(n, 10);
if (Number.isNaN(ret)) {
return null;
}
return ret;
},
///////////////////////////////////////////////
// Simple array equality comparison
// (will NOT work on arrays of objects)
// Array order is relevant here as they are not sorted
// change of order will equal change of array
// as this is required for datatable sortby
//
isEqualArraysOfPrimitives: function(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;
// If you don't care about the order of the elements inside
// the array, you should sort both arrays here.
// Please note that calling sort on an array will modify that array.
// you might want to clone your array first.
for (var i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false;
}
return true;
},
///////////////////////////////////////////////
// Use geolocation api to attempt to get current location
// try high accuracy first and downgrade if unavailable
//https://www.openstreetmap.org/?mlat=48.3911&mlon=-124.7353#map=12/48.3910/-124.7353
//https://www.openstreetmap.org/#map=18/49.68155/-125.00435
//https://www.openstreetmap.org/?mlat=49.71236&mlon=-124.96961#map=17/49.71236/-124.96961
//https://www.google.com/maps/search/?api=1&query=47.5951518,-122.3316393
getGeoLocation: async function() {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
function successHigh(pos) {
resolve({
latitude: pos.coords.latitude,
longitude: pos.coords.longitude
});
},
function error(err) {
//if here due to timeout getting high accuracy then try again with low accuracy
if (error.code == error.TIMEOUT) {
navigator.geolocation.getCurrentPosition(
function successLow(pos) {
resolve({
latitude: pos.coords.latitude,
longitude: pos.coords.longitude
});
},
function error(err) {
reject(
new Error(
`ERROR getting location(low_accuracy: ${err.code}): ${err.message}`
)
);
},
{
maximumAge: 600000,
timeout: 10000,
enableHighAccuracy: false
}
);
return;
}
reject(
new Error(
`ERROR GETTING LOCATION(high_accuracy:${err.code}): ${err.message}`
)
);
},
{ maximumAge: 600000, timeout: 5000, enableHighAccuracy: true }
);
});
},
///////////////////////////////////////////////
// Open map url
//
//
viewGeoLocation: function(obj) {
const hasGeo =
obj.latitude != null &&
obj.latitude != 0 &&
obj.longitude != null &&
obj.longitude != 0;
const hasAddress =
!this.stringIsNullOrEmpty(obj.address) &&
!this.stringIsNullOrEmpty(obj.city) &&
!this.stringIsNullOrEmpty(obj.region) &&
!this.stringIsNullOrEmpty(obj.country) &&
!this.stringIsNullOrEmpty(obj.postCode);
if (!hasGeo && !hasAddress) {
return;
}
let mapUrl = window.$gz.store.state.userOptions.mapUrlTemplate;
//No pre-set?
if (!mapUrl || mapUrl == "") {
mapUrl =
"https://www.google.com/maps/search/?api=1&query={ayaddress}<|>https://www.google.com/maps/search/?api=1&query={aylatitude},{aylongitude}";
}
let geoMapUrl = null;
let addressMapUrl = null;
//Parse the map url
let mapUrls = [mapUrl];
if (mapUrl.includes("<|>")) {
mapUrls = mapUrl.split("<|>");
}
mapUrls.forEach(z => {
if (!geoMapUrl && z.includes("{aylatitude}")) {
geoMapUrl = z;
}
if (!addressMapUrl && z.includes("{ayaddress}")) {
addressMapUrl = z;
}
});
//decide which map to use here, favor geocode
if (hasGeo && geoMapUrl) {
//geo view
mapUrl = geoMapUrl;
mapUrl = mapUrl.split("{aylatitude}").join(obj.latitude);
mapUrl = mapUrl.split("{aylongitude}").join(obj.longitude);
} else if (hasAddress && addressMapUrl) {
mapUrl = addressMapUrl;
//compile address fields together
//order street to country seems to be standard
//note, if google need plus symbol delimiter, if bing, need comma delimiter
//but both might accept one big string space delimited and url encoded so test that on all first
const delimiter = " ";
let q = "";
if (obj.address) {
q += obj.address + delimiter;
}
if (obj.city) {
q += obj.city + delimiter;
}
if (obj.region) {
q += obj.region + delimiter;
}
if (obj.country) {
q += obj.country + delimiter;
}
if (obj.postCode) {
q += obj.postCode + delimiter;
}
if (obj.addressPostal) {
q += obj.addressPostal + delimiter;
}
if (q.length > 1) {
q = q.substring(0, q.length - 1);
}
//url encode the query
q = encodeURIComponent(q);
mapUrl = mapUrl.split("{ayaddress}").join(q);
} else {
throw new Error(
"View map: error - no matching mapurl / address / geo coordinates set for display, nothing to view"
);
}
window.open(mapUrl, "map");
//This is not valid to do as some platforms don't open a new web browser window
//but rather a map application in which case this is null and throws up the exception even though it's working
// if (window.open(mapUrl, "map") == null) {
// throw new Error(
// "Problem displaying map in new window. Browser must allow pop-ups to view maps; check your browser setting"
// );
// }
},
///////////////////////////////////////////////
// Online mapping service url formats
//
//
mapProviderUrls: function() {
return [
{
name: "Apple",
value:
"http://maps.apple.com/?q={ayaddress}<|>http://maps.apple.com/?ll={aylatitude},{aylongitude}"
},
{
name: "Bing",
value:
"https://bing.com/maps/default.aspx?where1={ayaddress}<|>https://bing.com/maps/default.aspx?cp={aylatitude}~{aylongitude}&lvl=17&style=r&sp=point.{aylatitude}_{aylongitude}"
},
{
name: "Google",
value:
"https://www.google.com/maps/search/?api=1&query={ayaddress}<|>https://www.google.com/maps/search/?api=1&query={aylatitude},{aylongitude}"
},
{
name: "MapQuest",
value:
"https://mapquest.com/?center={ayaddress}&zoom=17<|>https://mapquest.com/?center={aylatitude},{aylongitude}&zoom=17"
},
{
name: "Open Street Map",
value:
"https://www.openstreetmap.org/search?query={ayaddress}<|>https://www.openstreetmap.org/?mlat={aylatitude}&mlon={aylongitude}#map=17/{aylatitude}/{aylongitude}"
},
{
name: "geo URI",
value: "geo:{aylatitude},{aylongitude}"
},
{
name: "Waze",
value:
"https://waze.com/ul?q={ayaddress}<|>https://www.waze.com/ul?ll={aylatitude},{aylongitude}&navigate=yes&zoom=17"
},
{
name: "Yandex",
value:
"https://yandex.ru/maps/?mode=search&text={ayaddress}&z=17<|>https://yandex.ru/maps/?ll={aylatitude},{aylongitude}&z=12&l=map"
}
];
},
///////////////////////////////////////////////
// v-calendar view to Sockeye scheduleview enum
//
//
calendarViewToSockeyeEnum: function(view) {
switch (view) {
case "day":
return 1;
case "week":
return 2;
case "month":
return 3;
case "4day":
return 4;
case "category":
return 5;
default:
throw new Error(
`gzutil->calendarViewtoSockeyeEnum - Unknown view type '${view}'`
);
}
},
///////////////////////////////////////////////
// GZDaysOfWeek to VCalendar weekdays
//
//
DaysOfWeekToWeekdays: function(dow) {
/*
AyaDaysOfWeek
Monday = 1,
Tuesday = 2,
Wednesday = 4,
Thursday = 8,
Friday = 16,
Saturday = 32,
Sunday = 64
vCalendar [
0,//sunday
1,
2,
3,
4,
5,
6//saturday
]
*/
if (dow == null || dow == 0) {
return [0, 1, 2, 3, 4, 5, 6]; //all the days
}
const ret = [];
//turn EXCLUDE selected gzDaysOfWeek into INCLUDE selected days for vCalendar
if (!(dow & 64)) {
ret.push(0);
}
if (!(dow & 1)) {
ret.push(1);
}
if (!(dow & 2)) {
ret.push(2);
}
if (!(dow & 4)) {
ret.push(3);
}
if (!(dow & 8)) {
ret.push(4);
}
if (!(dow & 16)) {
ret.push(5);
}
if (!(dow & 32)) {
ret.push(6);
}
return ret;
},
///////////////////////////////////////////////
// Random integer from 0 to max
//
//
getRandomInt: function(max) {
return Math.floor(Math.random() * max);
}
/**
*
*
*/
//new functions above here
};

614
src/api/initialize.js Normal file
View File

@@ -0,0 +1,614 @@
function addNavItem(title, icon, route, navItems, key, testid, color = null) {
if (!testid) {
testid = route;
}
const o = {
title,
icon,
route,
navItems,
key: key,
testid: testid
};
if (color != null) {
o["color"] = color;
}
o.navItems.forEach(z => {
if (z.testid == null) {
z.testid = z.route;
}
});
window.$gz.store.commit("addNavItem", o);
}
function initNavPanel() {
let key = 0;
let sub = [];
/*Service = 1,
NotService = 2,
Customer = 3,
HeadOffice = 4,
ServiceContractor = 5 */
//########## OUTSIDE USERS GROUP (CUSTOMER / HEADOFFICE) ###
if (window.$gz.store.getters.isCustomerUser == true) {
//clear sublevel array
sub = [];
//Set homePage in store to customer csr for this user type
let CustomerHomePageSet = false;
//USER SETTINGS
//if (window.$gz.store.state.customerRights.userSettings == true) {
sub.push({
title: "UserSettings",
icon: "$sockiUserCog",
route: "/home-user-settings",
key: key++
});
window.$gz.store.commit("setHomePage", "/home-user-settings");
CustomerHomePageSet = true;
// }
if (window.$gz.store.getters.canSubscribeToNotifications) {
sub.push({
title: "NotifySubscriptionList",
icon: "$sockiBullhorn",
route: "/home-notify-subscriptions",
key: key++
});
window.$gz.store.commit("setHomePage", "/home-notify-subscriptions");
CustomerHomePageSet = true;
}
//** CUSTOMER LOGIN HOME (TOP)
addNavItem("Home", "$sockiHome", undefined, sub, key++, "homecustomer");
//last resort home page if nothing else kicked in
if (!CustomerHomePageSet) {
window.$gz.store.commit("setHomePage", "/no-features-available");
}
return;
}
//###### ALL INSIDE USERS FROM HERE DOWN ###############
//####### HOME GROUP
//DASHBOARD
sub.push({
title: "Dashboard",
icon: "$sockiTachometer",
route: "/home-dashboard",
key: key++
});
//SEARCH
sub.push({
title: "Search",
icon: "$sockiSearch",
route: "/home-search",
key: key++
});
//SCHEDULE (personal)
sub.push({
title: "Schedule",
icon: "$sockiCalendarDay",
route: "/home-schedule",
key: key++
});
//MEMOS
sub.push({
title: "MemoList",
icon: "$sockiInbox",
route: "/home-memos",
key: key++
});
//REMINDERS
sub.push({
title: "ReminderList",
icon: "$sockiStickyNote",
route: "/home-reminders",
key: key++
});
//REVIEWS
sub.push({
title: "ReviewList",
icon: "$sockiCalendarCheck",
route: "/home-reviews",
key: key++
});
//USER SETTINGS
sub.push({
title: "UserSettings",
icon: "$sockiUserCog",
route: "/home-user-settings",
key: key++
});
//USER NOTIFICATION SUBSCRIPTIONS
sub.push({
title: "NotifySubscriptionList",
icon: "$sockiBullhorn",
route: "/home-notify-subscriptions",
key: key++
});
//HISTORY / MRU / ACTIVITY (personal)
sub.push({
title: "History",
icon: "$sockiHistory",
route: `/history/3/${window.$gz.store.state.userId}/true`,
key: key++,
testid: "/home-history"
});
//HOME
if (sub.length > 0) {
//Set homePage in store to dashboard
window.$gz.store.commit("setHomePage", "/home-dashboard");
addNavItem("Home", "$sockiHome", undefined, sub, key++, "home");
}
////////////////////////////////////////////////////////
// BUSINESS GROUP
//
sub = [];
sub.push({
title: "GZCaseList",
icon: "$sockiCoffee",
route: "/biz-gzcase-list",
key: key++
});
sub.push({
title: "LicenseList",
icon: "$sockiGem",
route: "/biz-license-list",
key: key++
});
sub.push({
title: "TrialLicenseRequestList",
icon: "$sockiHandHoldingWater",
route: "/biz-trial-request-list",
key: key++
});
sub.push({
title: "SubscriptionServerList",
icon: "$sockiCloud",
route: "/biz-subscription-server-list",
key: key++
});
sub.push({
title: "PurchaseList",
icon: "$sockiShoppingCart",
route: "/biz-purchase-list",
key: key++
});
sub.push({
title: "ProductList",
icon: "$sockiBarCode",
route: "/biz-product-list",
key: key++
});
sub.push({
title: "VendorList",
icon: "$sockiStore",
route: "/biz-vendor-list",
key: key++
});
if (window.$gz.role.canOpen(window.$gz.type.Customer)) {
addNavItem(
"BusinessSettings",
"$sockiBriefcase",
undefined,
sub,
key++,
"biz"
);
}
////////////////////////////////////////////////////
//######### CUSTOMER GROUP
if (window.$gz.role.canOpen(window.$gz.type.Customer)) {
//these all require Customer rights so all in the same block
//clear sublevel array
sub = [];
//CUSTOMERS subitem
sub.push({
title: "CustomerList",
icon: "$sockiAddressCard",
route: "/cust-customers",
key: key++
});
//HEAD OFFICES subitem
sub.push({
title: "HeadOfficeList",
icon: "$sockiSitemap",
route: "/cust-head-offices",
key: key++
});
//Customer / Headoffice Users subitem
sub.push({
title: "Contacts",
icon: "$sockiUsers",
route: "/cust-users",
key: key++
});
sub.push({
title: "CustomerNotifySubscriptionList",
icon: "$sockiBullhorn",
route: "/cust-notify-subscriptions",
key: key++
});
// ** CUSTOMER (TOP)
addNavItem(
"CustomerList",
"$sockiAddressBook",
undefined,
sub,
key++,
"customer"
);
}
// //####### SERVICE GROUP
// sub = [];
// //SCHEDULE (shared)
// sub.push({
// title: "Schedule",
// icon: "$sockiCalendarAlt",
// route: "/svc-schedule",
// key: key++
// });
// //**** SHARED (TOP GROUP)
// if (
// sub.length > 0 &&
// !window.$gz.role.hasRole([
// window.$gz.role.AUTHORIZATION_ROLES.TechRestricted
// ])
// ) {
// addNavItem("Service", "$sockiToolbox", undefined, sub, key++, "service");
// }
//****************** ACCOUNTING
//SOCKEYE Keeping this for very likely future accounting functionality
sub = [];
// ** ACCOUNTING (TOP)
if (sub.length > 0) {
addNavItem(
"Accounting",
"$sockiCoins",
undefined,
sub,
key++,
"accounting"
);
}
//############# ADMINISTRATION
//clear sublevel array
sub = [];
// GLOBAL SETTINGS
if (window.$gz.role.canOpen(window.$gz.type.Global)) {
sub.push({
title: "AdministrationGlobalSettings",
icon: "$sockiCogs",
route: "/adm-global-settings",
key: key++
});
}
// USERS
if (window.$gz.role.canOpen(window.$gz.type.User)) {
sub.push({
title: "UserList",
icon: "$sockiUsers",
route: "/adm-users",
key: key++
});
}
//TRANSLATION
if (window.$gz.role.canOpen(window.$gz.type.Translation)) {
sub.push({
title: "TranslationList",
icon: "$sockiLanguage",
route: "/adm-translations",
key: key++
});
}
//REPORT TEMPLATES
if (window.$gz.role.canChange(window.$gz.type.Report)) {
sub.push({
title: "ReportList",
icon: "$sockiThList",
route: "/adm-report-templates",
key: key++
});
}
//FILES IN DATABASE
if (window.$gz.role.canOpen(window.$gz.type.FileAttachment)) {
sub.push({
title: "Attachments",
icon: "$sockiPaperclip",
route: "/adm-attachments",
key: key++
});
}
//EVENT LOG / HISTORY
if (window.$gz.role.canOpen(window.$gz.type.Global)) {
//not really an appropriate object here just guessing
sub.push({
title: "History",
icon: "$sockiHistory",
route: "/adm-history",
key: key++
});
}
//IMPORT
if (window.$gz.role.canOpen(window.$gz.type.Global)) {
//again, not really an appropriate object type
sub.push({
title: "Import",
icon: "$sockiFileImport",
route: "/adm-import",
key: key++
});
}
//INTEGRATION
//decision here is that only teh biz admin can *control* or remove an integration
//even though all full role inside users can create or edit integrations (just not through the Sockeye user interface)
//this is required to support integrations made for various roles like inventory accounting etc
if (window.$gz.role.canOpen(window.$gz.type.Global)) {
sub.push({
title: "IntegrationList",
icon: "$sockiCampground",
route: "/adm-integrations",
key: key++
});
}
// ** ADMINISTRATION (TOP)
if (sub.length > 0) {
addNavItem(
"Administration",
"$sockiUserTie",
undefined,
sub,
key++,
"administration"
);
}
//############ OPERATIONS
//clear sublevel array
sub = [];
// BACKUP
if (window.$gz.role.canOpen(window.$gz.type.Backup)) {
sub.push({
title: "Backup",
icon: "$sockiFileArchive",
route: "/ops-backup",
key: key++
});
}
// SERVER STATE
if (window.$gz.role.canChange(window.$gz.type.ServerState)) {
sub.push({
title: "ServerState",
icon: "$sockiDoorOpen",
route: "/ops-server-state",
key: key++
});
}
// JOBS
if (window.$gz.role.canOpen(window.$gz.type.ServerJob)) {
sub.push({
title: "ServerJobs",
icon: "$sockiRobot",
route: "/ops-jobs",
key: key++
});
}
// LOGS
if (window.$gz.role.canOpen(window.$gz.type.LogFile)) {
sub.push({
title: "ServerLog",
icon: "$sockiHistory",
route: "/ops-log",
key: key++
});
}
//METRICS
if (window.$gz.role.canOpen(window.$gz.type.ServerMetrics)) {
sub.push({
title: "ServerMetrics",
icon: "$sockiFileMedicalAlt",
route: "/ops-metrics",
key: key++
});
// //PROFILE
// //metrics rights
// sub.push({
// title: "ServerProfiler",
// icon: "$sockiBinoculars",
// route: "/ops-profile",
// key: key++
// });
}
//NOTIFICATION CONFIG AND HISTORY
if (window.$gz.role.canOpen(window.$gz.type.OpsNotificationSettings)) {
sub.push({
title: "OpsNotificationSettings",
icon: "$sockiBullhorn",
route: "/ops-notification-settings",
key: key++
});
}
if (window.$gz.role.canOpen(window.$gz.type.OpsNotificationSettings)) {
sub.push({
title: "NotificationDeliveryLog",
icon: "$sockiHistory",
route: "/ops-notify-log",
key: key++
});
}
if (window.$gz.role.canOpen(window.$gz.type.OpsNotificationSettings)) {
sub.push({
title: "NotificationCustomerDeliveryLog",
icon: "$sockiHistory",
route: "/ops-customer-notify-log",
key: key++
});
}
// OPS VIEW SERVER CONFIGURATION
if (window.$gz.role.canOpen(window.$gz.type.GlobalOps)) {
sub.push({
title: "ViewServerConfiguration",
icon: "$sockiInfoCircle",
route: "/ops-view-configuration",
key: key++
});
}
// ** OPERATIONS (TOP)
if (sub.length > 0) {
addNavItem(
"Operations",
"$sockiServer",
undefined,
sub,
key++,
"operations"
);
}
}
async function getUserOptions() {
try {
const res = await window.$gz.api.get(
"user-option/" + window.$gz.store.state.userId
);
if (res.error) {
//In a form this would trigger a bunch of validation or error display code but for here and now:
//convert error to human readable string for display and popup a notification to user
const msg = window.$gz.api.apiErrorToHumanString(res.error);
window.$gz.store.commit(
"logItem",
"Initialize::() fetch useroptions -> error" + msg
);
window.$gz.eventBus.$emit("notify-error", msg);
} else {
//Check if overrides and use them here
//or else use browser defaults
const l = {
languageOverride: null,
timeZoneOverride: null,
currencyName: null,
hour12: true,
//uiColor: "#000000ff",
emailAddress: null,
mapUrlTemplate: null
};
l.languageOverride = res.data.languageOverride;
l.timeZoneOverride = res.data.timeZoneOverride;
//No browser setting for this so meh
l.currencyName = res.data.currencyName;
if (res.data.hour12 != null) {
l.hour12 = res.data.hour12;
}
// l.uiColor = res.data.uiColor || "#000000ff";
l.emailAddress = res.data.emailAddress || null;
l.mapUrlTemplate = res.data.mapUrlTemplate || null;
window.$gz.store.commit("setUserOptions", l);
}
} catch (error) {
window.$gz.store.commit(
"logItem",
"Initialize::() fetch useroptions -> error" + error
);
throw new Error(window.$gz.errorHandler.errorToString(error));
}
}
/////////////////////////////////////
// Initialize the app
// on change of authentication status
export default function initialize() {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async function(resolve, reject) {
if (!window.$gz.store.state.authenticated) {
throw new Error("initialize: Error, called but user not authenticated!");
}
try {
await window.$gz.translation.cacheTranslations(
window.$gz.translation.coreKeys
);
initNavPanel();
await getUserOptions();
resolve();
} catch (err) {
reject(err);
}
});
}

604
src/api/locale.js Normal file
View File

@@ -0,0 +1,604 @@
//Browser Locale conversion utilities
//dates,numbers currency etc
export default {
////////////////////////////////////////////////////////
// attempt to determine user's preferred language settings
// As of Jan 2020 all major browsers support
// navigator.languages
// but some use navigator.language (singular) to denote UI language preference
// not browsing language preference
// so the ideal way to do this is to use navigator.languages[0] for the preferred language
// and ignore the singular property since we don't care about the actual browser UI language
// only how the user expects to see the page itself
//
// also for sake of future proofing and edge cases need to have it be manually settable as well
//
//https://appmakers.dev/bcp-47-language-codes-list/
///////////////////////////////////////////
// Get users default language code
// first check if overriden in useroptions
// if not then use browsers own setting
//if not that then final default of en-US
getResolvedLanguage() {
let l = window.$gz.store.state.userOptions.languageOverride;
if (!window.$gz.util.stringIsNullOrEmpty(l)) {
return l;
} else {
l = window.navigator.languages[0];
if (!window.$gz.util.stringIsNullOrEmpty(l)) {
return l;
} else {
return "en-US";
}
}
},
///////////////////////////////////////////
// Get users default time zone
// first check if overriden in useroptions
// if not then use browsers own setting
// if that is empty then final default of "America/New_York"
//https://www.iana.org/time-zones
//https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
getResolvedTimeZoneName() {
let tz = window.$gz.store.state.userOptions.timeZoneOverride;
if (!window.$gz.util.stringIsNullOrEmpty(tz)) {
return tz;
} else {
tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (!window.$gz.util.stringIsNullOrEmpty(tz)) {
return tz;
} else {
return "America/New_York";
}
}
},
//////////////////////////////////////////////////
// Get the user's chosen currency name
//https://en.wikipedia.org/wiki/ISO_4217
//default to USD if nothing specified
getCurrencyName() {
const cur = window.$gz.store.state.userOptions.currencyName;
if (!window.$gz.util.stringIsNullOrEmpty(cur)) {
return cur;
} else {
return "USD";
}
},
//////////////////////////////////////////////////
// Get the user's chosen 12hr clock
//
getHour12() {
return window.$gz.store.state.userOptions.hour12;
},
/////////////////////////////////////////////////////////////////////
// Turn a utc ISO date from server into a vuetify calendar
// schedule control compatible (epoch) format
// localized.
// For ease of use in schedule the epoch (milliseconds) is the best format
// "It must be a Date, number of seconds since Epoch, or a string in the format of YYYY-MM-DD or YYYY-MM-DD hh:mm. Zero-padding is optional and seconds are ignored.""
//
//
utcDateToScheduleCompatibleFormatLocalized(value, timeZoneName) {
//This function takes a UTC iso format date string, parses it into a date then converts that date to the User's configured time zone
//outputs that in a format close to ISO, fixes the space in the middle of the output to match ISO 8601 format then returns as an
//epoch
//this is to support controls that are not time zone settable so they are always in local browser time zone of os, however user may be operating
//sockeye in another desired time zone so this is all to support that scenario
if (!value) {
if (window.$gz.dev) {
throw new Error(
`locale::utcDateToScheduleCompatibleFormatLocalized - Value is empty`
);
}
return null;
}
return new Date(
new Date(value) //convert to locale timezone and output in the closest thing to iso-8601 format
.toLocaleString("sv-SE", {
timeZone: timeZoneName
})
.replace(" ", "T") //Safari can't parse the date from here because sv-SE puts a space between date and time and Safari will only parse if it has a T between
).getTime();
},
///////////////////////////////////////////////
// Convert a local schedule epoch timestamp
// to specified time zone equivalent then
// to UTC and output as ISO 8601
//
//
localScheduleFormatToUTC8601String(value, timeZoneName) {
if (!timeZoneName) {
timeZoneName = this.getResolvedTimeZoneName();
}
//input: epoch in local browser time zone
//output: transform to date and time string
//convert to desired time zone but at same time and date
//(i.e. if it browser is vancouver and 1pm is selected but desired is new york's 1pm
// so convert the string as if it was new york then back to iso so that the time is adjusted forward
// as if the user was in new york in their browser default)
//parse in the time in to the specified timezone
let ret = window.$gz.DateTime.fromISO(
//output the sched epoch as local time string without zone
new Date(value).toLocaleString("sv-SE").replace(" ", "T"),
{
zone: timeZoneName
}
);
ret = ret.setZone("utc"); //convert to UTC
ret = ret.toISO(); //output as ISO 8601
return ret;
},
///////////////////////////////////////////
// Turn a utc date into a displayable
// short date and time
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString
//
utcDateToShortDateAndTimeLocalized(
value,
timeZoneName,
languageName,
hour12
) {
if (!value) {
return "";
}
if (!timeZoneName) {
timeZoneName = this.getResolvedTimeZoneName();
}
if (!languageName) {
languageName = this.getResolvedLanguage();
}
if (!hour12) {
hour12 = this.getHour12();
}
//parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z")
const parsedDate = new Date(value);
//is it a valid date?
if (!(parsedDate instanceof Date && !isNaN(parsedDate))) {
return "not valid";
}
return parsedDate.toLocaleString(languageName, {
timeZone: timeZoneName,
dateStyle: "short",
timeStyle: "short",
hour12: hour12
});
},
///////////////////////////////////////////
// Turn a utc date into a displayable
// date and time with specific formats
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString
//
utcDateToSpecifiedDateAndTimeLocalized(
value,
timeZoneName,
languageName,
hour12,
dateStyle,
timeStyle
) {
if (!value) {
return "";
}
if (!timeZoneName) {
timeZoneName = this.getResolvedTimeZoneName();
}
if (!languageName) {
languageName = this.getResolvedLanguage();
}
if (!hour12) {
hour12 = this.getHour12();
}
//parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z")
const parsedDate = new Date(value);
//is it a valid date?
if (!(parsedDate instanceof Date && !isNaN(parsedDate))) {
return "not valid";
}
return parsedDate.toLocaleString(languageName, {
timeZone: timeZoneName,
dateStyle: dateStyle,
timeStyle: timeStyle,
hour12: hour12
});
},
///////////////////////////////////////////
// Turn a utc date into a displayable
// short date
//
utcDateToShortDateLocalized(value, timeZoneName, languageName) {
if (!value) {
return "";
}
if (!timeZoneName) {
timeZoneName = this.getResolvedTimeZoneName();
}
if (!languageName) {
languageName = this.getResolvedLanguage();
}
//parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z")
const parsedDate = new Date(value);
//is it a valid date?
if (!(parsedDate instanceof Date && !isNaN(parsedDate))) {
return "not valid";
}
return parsedDate.toLocaleDateString(languageName, {
timeZone: timeZoneName,
dateStyle: "short"
});
}, ///////////////////////////////////////////
// Turn a utc date into a displayable
// short time
//
utcDateToShortTimeLocalized(value, timeZoneName, languageName, hour12) {
if (!value) {
return "";
}
if (!timeZoneName) {
timeZoneName = this.getResolvedTimeZoneName();
}
if (!languageName) {
languageName = this.getResolvedLanguage();
}
if (!hour12) {
hour12 = this.getHour12();
}
//parse the date which is identified as utc ("2020-02-06T18:18:49.148011Z")
const parsedDate = new Date(value);
//is it a valid date?
if (!(parsedDate instanceof Date && !isNaN(parsedDate))) {
return "not valid";
}
return parsedDate.toLocaleTimeString(languageName, {
timeZone: timeZoneName,
timeStyle: "short",
hour12: hour12
});
},
///////////////////////////////////////////
// Turn a duration value into a display
//
durationLocalized(value, hideSeconds) {
if (value == null || value == "00:00:00") {
return "";
}
let theDays = 0;
let theHours = 0;
let theMinutes = 0;
let theSeconds = 0;
let ret = "";
const work = value.split(":");
//has days?
if (work[0].includes(".")) {
let dh = work[0].split(".");
theDays = Number(dh[0]);
theHours = Number(dh[1]);
} else {
theHours = Number(work[0]);
}
theMinutes = Number(work[1]);
//has milliseconds? (ignore them)
if (work[2].includes(".")) {
let dh = work[2].split(".");
theSeconds = Number(dh[0]);
} else {
theSeconds = Number(work[2]);
}
if (theDays != 0) {
ret += theDays + " " + window.$gz.translation.get("TimeSpanDays") + " ";
}
if (theHours != 0) {
ret += theHours + " " + window.$gz.translation.get("TimeSpanHours") + " ";
}
if (theMinutes != 0) {
ret +=
theMinutes + " " + window.$gz.translation.get("TimeSpanMinutes") + " ";
}
if (!hideSeconds && theSeconds != 0) {
ret +=
theSeconds + " " + window.$gz.translation.get("TimeSpanSeconds") + " ";
}
return ret;
},
///////////////////////////////////////////////
// Convert a utc date to local time zone
// and return time portion only in iso 8601
// format (used by time and date picker components)
//
utcDateStringToLocal8601TimeOnlyString(value, timeZoneName) {
if (!value) {
//if no value, return the current time as expected by the time picker
} else {
//ok, the reason for sv-SE is that it's a locale that returns the time already in ISO format and 24hr by default
//that can change over time so if this breaks that's why
//also fr-CA does as well as possibly en-CA
//https://stackoverflow.com/a/58633686/8939
if (!timeZoneName) {
timeZoneName = this.getResolvedTimeZoneName();
}
return new Date(value).toLocaleTimeString("sv-SE", {
timeZone: timeZoneName
});
}
},
///////////////////////////////////////////////
// Convert a local time only string with date string
// to UTC and output as ISO 8601
// also converts to time zone specified if diff from browser
// (used by time and date picker components)
//
localTimeDateStringToUTC8601String(value, timeZoneName) {
//https://moment.github.io/luxon/docs/manual/zones.html#creating-datetimes-in-a-zone
if (!timeZoneName) {
timeZoneName = this.getResolvedTimeZoneName();
}
//parse in the time in the currently used timezone
let ret = window.$gz.DateTime.fromISO(value, {
zone: timeZoneName
});
ret = ret.setZone("utc"); //convert to UTC
ret = ret.toISO(); //output as ISO 8601
return ret;
},
///////////////////////////////////////////////
// UTC Now in api format
// to UTC and output as ISO 8601
// (used to set defaults)
//
nowUTC8601String(timeZoneName) {
if (!timeZoneName) {
timeZoneName = this.getResolvedTimeZoneName();
}
const ret = window.$gz.DateTime.local()
.setZone(timeZoneName)
.toUTC()
.toString();
return ret;
},
///////////////////////////////////////////////
// UTC ISO 8601 string add minutes
// and return as UTC ISO 8601 string
// (used to set automatic / default adjusted times)
//
addMinutesToUTC8601String(val, minutes) {
if (!val || val == "" || minutes == null || minutes == 0) {
return val;
}
//instantiate a luxon date object from val which is assumed to be an iso string
let dt = window.$gz.DateTime.fromISO(val);
if (!dt.isValid) {
console.error("locale::addMinutes, input not valid:", {
val: val,
dt: dt
});
return val;
}
//add minutes
dt = dt.plus({ minutes: minutes });
return dt.toUTC().toString();
},
///////////////////////////////////////////////
// UTC ISO 8601 string add arbitrary value based
// on luxon duration format
// and return as UTC ISO 8601 string
//https://moment.github.io/luxon/api-docs/index.html#datetimeplus
//
addDurationToUTC8601String(val, duration) {
if (
!val ||
val == "" ||
duration == null ||
!typeof duration === "object"
) {
return val;
}
//instantiate a luxon date object from val which is assumed to be an iso string
let dt = window.$gz.DateTime.fromISO(val);
if (!dt.isValid) {
console.error("locale::addDurationToUTC8601String, input not valid:", {
val: val,
dt: dt
});
return val;
}
//add minutes
dt = dt.plus(duration);
return dt.toUTC().toString();
},
///////////////////////////////////////////////
// parse UTC ISO 8601 strings, diff, return hours
//
diffHoursFromUTC8601String(start, stop) {
if (!start || start == "" || !stop == null || stop == "") {
return 0;
}
//instantiate a luxon date object from val which is assumed to be an iso string
const startDate = window.$gz.DateTime.fromISO(start);
if (!startDate.isValid) {
console.error("locale::diffHours, start not valid:", {
start: start,
startDate: startDate
});
return 0;
}
const stopDate = window.$gz.DateTime.fromISO(stop);
if (!stopDate.isValid) {
console.error("locale::diffHours, start not valid:", {
stop: stop,
stopDate: stopDate
});
return 0;
}
// console.log(
// "locale:diffhours...",
// stopDate.diff(startDate, "hours").toObject().hours
// );
// console.log(
// "locale:diffhours.. ROUNDED.",
// window.$gz.util.roundAccurately(
// stopDate.diff(startDate, "hours").toObject().hours,
// 2
// )
// );
return window.$gz.util.roundAccurately(
stopDate.diff(startDate, "hours").toObject().hours,
2
);
},
///////////////////////////////////////////////
// Local now timestamp converted to timeZoneName
// and output as ISO 8601
// (used to inform server of local client time)
//
clientLocalZoneTimeStamp(timeZoneName) {
if (!timeZoneName) {
timeZoneName = this.getResolvedTimeZoneName();
}
const ret = window.$gz.DateTime.local()
.setZone(timeZoneName)
.toString();
return ret;
},
///////////////////////////////////////////////
// Get default start date time in api format
// (this is used to centralize and for future)
defaultStartDateTime() {
return {
start: window.$gz.DateTime.local()
.toUTC()
.toString(),
end: window.$gz.DateTime.local()
.plus({ hours: 1 })
.toUTC()
.toString()
};
},
///////////////////////////////////////////////
// Convert a utc date to local time zone
// and return date only portion only in iso 8601
// format (used by time and date picker components)
//
utcDateStringToLocal8601DateOnlyString(value, timeZoneName) {
if (!value) {
//if no value, return the current time as expected by the time picker
} else {
//ok, the reason for sv-SE is that it's a locale that returns the time already in ISO format and 24hr by default
//that can change over time so if this breaks that's why
//also fr-CA does as well as possibly en-CA
//https://stackoverflow.com/a/58633686/8939
if (!timeZoneName) {
timeZoneName = this.getResolvedTimeZoneName();
}
return new Date(value).toLocaleDateString("sv-SE", {
timeZone: timeZoneName
});
}
},
///////////////////////////////////////////////
// Date/time past or future evaluation
//
dateIsPast(value) {
if (!value) {
return false;
}
return new Date(value) < new Date();
},
///////////////////////////////////////////
// Turn a decimal number into a local
// currency display
//
currencyLocalized(value, languageName, currencyName) {
if (value == null) return "";
if (!languageName) {
languageName = this.getResolvedLanguage();
}
if (!currencyName) {
currencyName = this.getCurrencyName();
}
return new Intl.NumberFormat(languageName, {
style: "currency",
currency: currencyName
}).format(value);
},
///////////////////////////////////////////
// Turn a decimal number into a local
// decimal format display
//
decimalLocalized(value, languageName) {
if (value == null) return "";
if (!languageName) {
languageName = this.getResolvedLanguage();
}
//This forces 2 digits after the decimal
// return new Intl.NumberFormat(languageName, {
// minimumFractionDigits: 2
// }).format(value);
//this goes with whatever is the local format which for dev testing turned out to be perfect: 1.00 displays as 1 and 1.75 displays as 1.75
//alignment goes out the window but it follows v7 format
return new Intl.NumberFormat(languageName).format(value);
},
///////////////////////////////////////////
// Turn a file / memory size number into a local
// decimal format display and in reasonable human readable range
//
humanFileSize(bytes, languageName, si = false, dp = 1) {
const thresh = si ? 1000 : 1024;
if (Math.abs(bytes) < thresh) {
return bytes + " B";
}
const units = si
? ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
: ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
let u = -1;
const r = 10 ** dp;
do {
bytes /= thresh;
++u;
} while (
Math.round(Math.abs(bytes) * r) / r >= thresh &&
u < units.length - 1
);
return (
this.decimalLocalized(bytes.toFixed(dp), languageName) + " " + units[u]
);
}
};

46
src/api/notifypoll.js Normal file
View File

@@ -0,0 +1,46 @@
let keepChecking = false;
const DEFAULT_POLLING_INTERVAL = 60000;
const MAX_POLLING_INTERVAL = 30 * 60 * 1000; //30 minutes maximum wait time
export default {
async startPolling() {
if (keepChecking == true) {
return;
}
keepChecking = true;
//initial delay so it fetches "immediately"
let pollingInterval = 3000;
let status = null;
while (keepChecking == true) {
try {
await window.$gz.util.sleepAsync(pollingInterval);
if (keepChecking && window.$gz.store.state.authenticated) {
if (window.$gz.erasingDatabase == false) {
status = await window.$gz.api.get("notify/new-count");
if (status.error) {
throw new Error(window.$gz.errorHandler.errorToString(status));
// throw new Error(status.error);
} else {
window.$gz.store.commit("setNewNotificationCount", status.data);
//success so go to default in case it was changed by an error
pollingInterval = DEFAULT_POLLING_INTERVAL;
}
}
} else {
keepChecking = false;
}
} catch (error) {
//fixup if fails on very first iteration with initial short polling interval
if (pollingInterval < DEFAULT_POLLING_INTERVAL) {
pollingInterval = DEFAULT_POLLING_INTERVAL;
}
pollingInterval *= 1.5;
if (pollingInterval > MAX_POLLING_INTERVAL) {
pollingInterval = MAX_POLLING_INTERVAL;
}
}
}
},
stopPolling() {
keepChecking = false;
}
};

View File

@@ -0,0 +1,277 @@
import socktype from "./socktype";
export default {
///////////////////////////////
// APP (GLOBAL) openobject CLICK HANDLER
//
// Deal with a request to open an object (from main datatables mainly)
// if it's an open object url that triggered here the url would be in the format of {host/open/[socktype integer]/[id integer]}, i.e.
// http://localhost:8080/open/2/105
// called from App.vue
handleOpenObjectClick(vm, tid) {
//expects extra data (tid) to be one of { type: [AYATYPE], id: [RECORDID] }
//NOTE: for new objects all edit pages assume record ID 0 (or null) means create rather than open
//for sake of ease of coding I'm going to assume null id also means make a new record intent
//so I don't have to parse and decide constantly on forms for every control that has a open record link in it
if (tid.id == null) {
tid.id = 0;
}
if (tid.type && tid.id != null) {
const isCustomerTypeUser =
window.$gz.store.state.userType == 3 ||
window.$gz.store.state.userType == 4;
//if these come from route parameters they may well be strings
tid.type = Number.parseInt(tid.type, 10);
tid.id = Number.parseInt(tid.id, 10);
if (isCustomerTypeUser) {
switch (tid.type) {
case socktype.NotifySubscription:
vm.$router.push({
name: "home-notify-subscription",
params: { recordid: tid.id }
});
break;
default:
window.$gz.eventBus.$emit(
"notify-warning",
`Customer user: open-object-handler unable to open link - [type:${tid.type}, id:${tid.id}]`
);
}
} else {
switch (tid.type) {
case socktype.Memo:
vm.$router.push({
name: "memo-edit",
params: { recordid: tid.id }
});
break;
case socktype.Customer:
vm.$router.push({
name: "customer-edit",
params: { recordid: tid.id }
});
break;
case socktype.CustomerNote:
vm.$router.push({
name: "customer-note-edit",
params: { recordid: tid.id }
});
break;
case socktype.HeadOffice:
vm.$router.push({
name: "head-office-edit",
params: { recordid: tid.id }
});
break;
case socktype.User:
//Is it an "Inside" user (staff or subcontractor)
//or an "outside" user (customer or headoffice)
//if key doesn't provide this then need to directly find out first before determining which form to redirect to
if (tid.id != 0) {
//lookup which one to open from server
(async () => {
try {
//shortcut for superuser, always id 1
if (tid.inside == undefined && tid.id == 1) {
tid.inside = true;
}
if (tid.inside == undefined) {
const res = await window.$gz.api.get(
"user/inside-type/" + tid.id
);
if (res.error) {
throw new Error(
window.$gz.errorHandler.errorToString(res, vm)
);
}
if (res.data) {
tid.inside = res.data;
}
}
if (tid.inside == true) {
vm.$router.push({
name: "adm-user",
params: { recordid: tid.id }
});
} else {
vm.$router.push({
name: "cust-user",
params: { recordid: tid.id }
});
}
} catch (e) {
throw new Error(window.$gz.errorHandler.errorToString(e, vm));
//throw new Error(e);
}
})();
}
break;
case socktype.NotifySubscription:
vm.$router.push({
name: "home-notify-subscription",
params: { recordid: tid.id }
});
break;
case socktype.FileAttachment:
//lookup the actual type
//then call this method again to do the actual open
(async () => {
try {
const res = await window.$gz.api.get(
"attachment/parent/" + tid.id
);
if (res.error) {
throw new Error(
window.$gz.errorHandler.errorToString(res, vm)
);
// throw new Error(res.error);
}
if (res.data.id && res.data.id != 0) {
this.handleOpenObjectClick(vm, res.data);
return;
}
} catch (e) {
//throw new Error(e);
throw new Error(window.$gz.errorHandler.errorToString(e, vm));
}
})();
break;
case socktype.Translation:
vm.$router.push({
name: "adm-translation",
params: { recordid: tid.id }
});
break;
case socktype.Report:
vm.$router.push({
name: "sock-report-edit",
params: { recordid: tid.id }
});
break;
case socktype.Backup:
vm.$router.push({
name: "ops-backup"
});
break;
case socktype.FormCustom:
//all we have is the id, but need the formkey to open it
(async () => {
try {
const res = await window.$gz.api.get(
"form-custom/form-key/" + tid.id
);
if (res.error) {
throw new Error(
window.$gz.errorHandler.errorToString(res, vm)
);
}
if (res && res.data) {
vm.$router.push({
name: "sock-customize",
params: {
formCustomTemplateKey: res.data
}
});
return;
}
} catch (e) {
//throw new Error(e);
throw new Error(window.$gz.errorHandler.errorToString(e, vm));
}
})();
break;
case socktype.Reminder:
vm.$router.push({
name: "reminder-edit",
params: { recordid: tid.id }
});
break;
case socktype.Review:
vm.$router.push({
name: "review-edit",
params: { recordid: tid.id }
});
break;
case socktype.CustomerNotifySubscription:
vm.$router.push({
name: "cust-notify-subscription",
params: { recordid: tid.id }
});
break;
case socktype.OpsNotificationSettings:
vm.$router.push({
name: "ops-notification-settings"
});
break;
case socktype.Integration:
vm.$router.push({
name: "adm-integration",
params: { recordid: tid.id }
});
break;
case socktype.Vendor:
vm.$router.push({
name: "vendor-edit",
params: { recordid: tid.id }
});
break;
case socktype.Product:
vm.$router.push({
name: "product-edit",
params: { recordid: tid.id }
});
break;
case socktype.SubscriptionServer:
vm.$router.push({
name: "subscription-server-edit",
params: { recordid: tid.id }
});
break;
case socktype.GZCase:
vm.$router.push({
name: "gzcase-edit",
params: { recordid: tid.id }
});
break;
default:
window.$gz.eventBus.$emit(
"notify-warning",
`open-object-handler: unknown [type:${tid.type}, id:${tid.id}]`
);
}
}
}
},
///////////////////////////////////
// WIRE UP MENU EVENTS
//
// called once from app.vue only
//
wireUpEventHandlers(vm) {
const that = this;
//expects extra data (tid) to be { type: [AYATYPE], id: [RECORDID] }
window.$gz.eventBus.$on("openobject", function handleOpenObjectClickHandler(
tid
) {
that.handleOpenObjectClick(vm, tid);
});
}
//new functions above here
};

60
src/api/palette.js Normal file
View File

@@ -0,0 +1,60 @@
//https://colorpalettes.net
export default {
color: {
blue: "#1f77b4",
red: "#d62728",
orange: "#fe7f0e",
green: "#2ca02c",
purple: "#9c27b0",
black: "#000000",
cyan: "#00BCD4",
teal: "#009688",
primary: "#00205B", //APP Canucks dark blue
secondary: "#00843D", //APP canucks green
accent: "#db7022", //APP lighter orangey red, more friendly looking though not as much clarity it seems
soft_sand: "#f1d3a1",
soft_sand_taupe: "#e3dbd9",
soft_pale_blue: "#e6eff6",
soft_deep_blue: "#89b4c4",
soft_green: "#ccdb86",
soft_brown: "#c8bcb1",
soft_brown_darker: "#8d7053",
soft_gray: "#d2d7db"
},
getBoldPaletteArray(size) {
const palette = [
this.color.blue,
this.color.red,
this.color.green,
this.color.orange,
this.color.purple,
this.color.cyan,
this.color.teal,
this.color.black
];
const paletteLength = palette.length;
const ret = [];
for (let i = 0; i < size; i++) {
ret.push(palette[i % paletteLength]);
}
return ret;
},
getSoftPaletteArray(size) {
const palette = [
this.color.soft_sand,
this.color.soft_pale_blue,
this.color.soft_gray,
this.color.soft_green,
this.color.soft_brown,
this.color.soft_deep_blue,
this.color.soft_sand_taupe,
this.color.soft_brown_darker
];
const paletteLength = palette.length;
const ret = [];
for (let i = 0; i < size; i++) {
ret.push(palette[i % paletteLength]);
}
return ret;
}
};

View File

@@ -0,0 +1,620 @@
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////
// Convert a date token to local
// date range to UTC for server
// dataListView consumption
//
export default {
///////////////////////////////
// token to date range
//
tokenToDates: function(token) {
if (token == null || token.length == 0) {
throw new Error(
"relative-date-filter-calculator: date token is null or empty"
);
}
//return object contains the two dates that encompass the time period
//the token represents to the local browser time zone but in UTC
//and iso8601 format
//NOTE: it's valid for one of the two ret values might be undefined as it's valid to have a single date for
//Past or Future
const ret = { after: undefined, before: undefined };
const dtNow = window.$gz.DateTime.local();
const dtToday = window.$gz.DateTime.local(
dtNow.year,
dtNow.month,
dtNow.day
);
let dtAfter = null;
let dtBefore = null;
switch (token) {
case "*yesterday*":
//Between Day before yesterday at midnight and yesterday at midnight
ret.after = dtToday
.plus({ days: -1, seconds: -1 })
.toUTC()
.toString();
ret.before = dtToday.toUTC().toString();
break;
case "*today*":
//Between yesterday at midnight and tommorow at midnight
ret.after = dtToday
.plus({ seconds: -1 })
.toUTC()
.toString();
ret.before = dtToday
.plus({ days: 1 })
.toUTC()
.toString();
break;
case "*tomorrow*":
//Between Tonight at midnight and day after tommorow at midnight
ret.after = dtToday
.plus({ days: 1, seconds: -1 })
.toUTC()
.toString();
ret.before = dtToday
.plus({ days: 2 })
.toUTC()
.toString();
break;
case "*lastweek*":
//Between two Sundays ago at midnight and last sunday at midnight
//go back a week
dtAfter = dtToday.plus({ days: -7 });
//go backwards to Sunday (In Luxon Monday is 1, Sunday is 7)
while (dtAfter.weekday != 7) {
dtAfter = dtAfter.plus({ days: -1 });
}
//go to very start of eighth dayahead
dtBefore = dtAfter.plus({ days: 8 });
//remove a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*thisweek*":
//Between Sunday at midnight and Next sunday at midnight
//Start with today
dtAfter = dtToday;
//SET dtAfter to Monday start of this week
//go backwards to monday (In Luxon Monday is 1, Sunday is 7)
while (dtAfter.weekday != 1) {
dtAfter = dtAfter.plus({ days: -1 });
}
//Now go back to sunday last second
dtAfter = dtAfter.plus({ seconds: -1 });
//Start with today
dtBefore = dtToday;
//SET dtBefore to next monday
//is it monday now?
if (dtBefore.weekday == 1) {
//Monday today? then go to next monday
dtBefore = dtBefore.plus({ days: 7 });
} else {
//Find next monday...
while (dtBefore.weekday != 1) {
dtBefore = dtBefore.plus({ days: 1 });
}
}
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*nextweek*":
//Between Next Sunday at midnight and Next Next sunday at midnight
//Start with today
dtAfter = dtToday;
//If today is monday skip over it first, we're looking for *next* monday, not this one
if (dtAfter.weekday == 1) {
dtAfter = dtAfter.plus({ days: 1 });
}
//go forwards to next monday 12:00am (In Luxon Monday is 1, Sunday is 7)
while (dtAfter.weekday != 1) {
dtAfter = dtAfter.plus({ days: 1 });
}
//Now go back to sunday last second
dtAfter = dtAfter.plus({ seconds: -1 });
//set dtBefore 7 days ahead of dtAfter
//(sb BEFORE two mondays from now at zero hour so need to add a second due to prior removal of a second to make sunday)
dtBefore = dtAfter.plus({ days: 7, seconds: 1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*lastmonth*":
//start with the first day of this month
dtAfter = window.$gz.DateTime.local(dtNow.year, dtNow.month, 1);
//subtract a Month
dtAfter = dtAfter.plus({ months: -1 });
//Add one month to dtAfter to get end date
dtBefore = dtAfter.plus({ months: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*thismonth*":
//start with the first day of this month
dtAfter = window.$gz.DateTime.local(dtNow.year, dtNow.month, 1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.plus({ months: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*nextmonth*":
//start with the first day of this month
dtAfter = window.$gz.DateTime.local(dtNow.year, dtNow.month, 1);
//add a month
dtAfter = dtAfter.plus({ months: 1 });
//Add one month to dtAfter to get end date
dtBefore = dtAfter.plus({ months: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*14daywindow*":
//Start with today
dtAfter = dtToday;
//subtract 7 days
dtAfter = dtAfter.plus({ days: -7 });
//Add 15 days to dtAfter to get end date
dtBefore = dtAfter.plus({ days: 15 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*past*":
//Any time before Now
//set return values from calculated values
ret.after = undefined;
ret.before = dtNow.toUTC().toString();
break;
case "*future*":
//Any time after Now
//set return values from calculated values
ret.after = dtNow.toUTC().toString();
ret.before = undefined;
break;
case "*lastyear*":
//"last year" means prior calendar year from start of january to end of december
//start with the first day of this year
dtAfter = window.$gz.DateTime.local(dtNow.year);
//subtract a year
dtAfter = dtAfter.plus({ years: -1 });
//Before zero hour january 1st this year
dtBefore = window.$gz.DateTime.local(dtNow.year);
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*thisyear*":
//From zero hour january 1 this year (minus a second) to zero hour jan 1 next year
//start with the first day of this year
dtAfter = window.$gz.DateTime.local(dtNow.year);
//Before zero hour january 1st next year
dtBefore = window.$gz.DateTime.local(dtNow.year);
dtBefore = dtBefore.plus({ years: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*last3months*":
//From Now minus 3 months
dtAfter = dtToday.plus({ months: -3 });
//Before now
dtBefore = dtNow;
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*last6months*":
//From Now minus 6 months
dtAfter = dtToday.plus({ months: -6 });
//Before now
dtBefore = dtNow;
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*pastyear*": //within the prior 365 days before today
//From Now minus 365 days
dtAfter = dtToday.plus({ days: -365 });
//Before now
dtBefore = dtNow;
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*past90days*":
//From Now minus 90 days
dtAfter = dtNow.plus({ days: -90 });
//Before now
dtBefore = dtNow;
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*past30days*":
//From Now minus 30 days
dtAfter = dtNow.plus({ days: -30 });
//Before now
dtBefore = dtNow;
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*past7days*":
//From Now minus 7 days
dtAfter = dtNow.plus({ days: -7 });
//Before now
dtBefore = dtNow;
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*past24hours*":
//From Now minus 24 hours
dtAfter = dtNow.plus({ hours: -24 });
//Before now
dtBefore = dtNow;
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*past6hours*":
//From Now minus 6 hours
dtAfter = dtNow.plus({ hours: -6 });
//Before now
dtBefore = dtNow;
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
///////////////////////////////////////////////////////////////////////////
case "*january*":
//This year specific month (month is 1 based)
dtAfter = window.$gz.DateTime.local(dtNow.year, 1, 1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.plus({ months: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*february*":
//This year specific month (month is 1 based)
dtAfter = window.$gz.DateTime.local(dtNow.year, 2, 1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.plus({ months: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*march*":
//This year specific month (month is 1 based)
dtAfter = window.$gz.DateTime.local(dtNow.year, 3, 1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.plus({ months: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*april*":
//This year specific month (month is 1 based)
dtAfter = window.$gz.DateTime.local(dtNow.year, 4, 1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.plus({ months: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*may*":
//This year specific month (month is 1 based)
dtAfter = window.$gz.DateTime.local(dtNow.year, 5, 1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.plus({ months: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*june*":
//This year specific month (month is 1 based)
dtAfter = window.$gz.DateTime.local(dtNow.year, 6, 1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.plus({ months: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*july*":
//This year specific month (month is 1 based)
dtAfter = window.$gz.DateTime.local(dtNow.year, 7, 1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.plus({ months: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*august*":
//This year specific month (month is 1 based)
dtAfter = window.$gz.DateTime.local(dtNow.year, 8, 1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.plus({ months: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*september*":
//This year specific month (month is 1 based)
dtAfter = window.$gz.DateTime.local(dtNow.year, 9, 1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.plus({ months: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*october*":
//This year specific month (month is 1 based)
dtAfter = window.$gz.DateTime.local(dtNow.year, 10, 1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.plus({ months: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*november*":
//This year specific month (month is 1 based)
dtAfter = window.$gz.DateTime.local(dtNow.year, 11, 1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.plus({ months: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*december*":
//This year specific month (month is 1 based)
dtAfter = window.$gz.DateTime.local(dtNow.year, 12, 1);
//Add one month to dtAfter to get end date
dtBefore = dtAfter.plus({ months: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*lastyearlastmonth*":
//start with the first day of this month
dtAfter = window.$gz.DateTime.local(dtNow.year, dtNow.month, 1);
//subtract a Year and a Month
dtAfter = dtAfter.plus({ years: -1, months: -1 });
//Add one month to dtAfter to get end date
dtBefore = dtAfter.plus({ months: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*lastyearthismonth*":
//start with the first day of this month
dtAfter = window.$gz.DateTime.local(dtNow.year, dtNow.month, 1);
//subtract a Year
dtAfter = dtAfter.plus({ years: -1 });
//Add one month to dtAfter to get end date
dtBefore = dtAfter.plus({ months: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
case "*lastyearnextmonth*":
//start with the first day of this month
dtAfter = window.$gz.DateTime.local(dtNow.year, dtNow.month, 1);
//subtract a year, add a month
dtAfter = dtAfter.plus({ years: -1, months: 1 });
//Add one month to dtAfter to get end date
dtBefore = dtAfter.plus({ months: 1 });
//move after back a second for boundary
dtAfter = dtAfter.plus({ seconds: -1 });
//set return values from calculated values
ret.after = dtAfter.toUTC().toString();
ret.before = dtBefore.toUTC().toString();
break;
default:
throw new Error(
"relative-date-time-filter-calculater: Date token [" +
token +
"] was not recognized"
);
//--------------------------
}
return ret;
}
};

View File

@@ -0,0 +1,4 @@
export default {
version: "8.0.28",
copyright: "© 1999-2022, Ground Zero Tech-Works Inc."
};

48
src/api/socktype.js Normal file
View File

@@ -0,0 +1,48 @@
export default {
NoType: 0,
Global: 1,
FormUserOptions: 2,
User: 3,
ServerState: 4,
LogFile: 6,
PickListTemplate: 7,
Customer: 8,
ServerJob: 9,
ServerMetrics: 12,
Translation: 13,
UserOptions: 14,
HeadOffice: 15,
FileAttachment: 17,
DataListSavedFilter: 18,
FormCustom: 19,
Vendor: 33,
GlobalOps: 47, //really only used for rights, not an object type of any kind
BizMetrics: 48, //deprecate? Not used for anything as of nov 2020
Backup: 49,
Notification: 50,
NotifySubscription: 51,
Reminder: 52,
OpsNotificationSettings: 56,
Report: 57,
DashboardView: 58,
CustomerNote: 59,
Memo: 60,
Review: 61,
DataListColumnView: 68,
CustomerNotifySubscription: 84, //proxy subs for customers
Integration: 92, //3rd party or add-on integration data store,
License: 93,
TrialLicenseRequest: 94,
SubscriptionServer: 95,
Purchase: 96,
Product: 97,
GZCase: 98
};
/**
*
* This is a mirror of SockType.cs in server project
* To update just copy the contents of SockType.cs and replace " :" with ":" (without quotes obvsly)
*
*
*/

327
src/api/translation.js Normal file
View File

@@ -0,0 +1,327 @@
export default {
////////////////////////////////
// Update the local cache
//
//
async updateCache(editedTranslation) {
//This function is only called if there is a requirement to refresh the local cache
//either they just changed translations and saved it in user settings
//or they just edited a translation and saved it in translation editor and it's also their own local translation
if (editedTranslation) {
//iterate the keys that are cached and set them from whatever is in editedTranslation for that key
for (const [key] of Object.entries(
window.$gz.store.state.translationText
)) {
const display = editedTranslation.translationItems.find(
z => z.key == key
).display;
window.$gz.store.commit("setTranslationText", {
key: key,
value: display
});
}
} else {
//gather up the keys that are cached and fetch the latest and then replace them
const needIt = [];
Object.keys(window.$gz.store.state.translationText).forEach(z => {
needIt.push(z);
});
//fetch these keys
const transData = await window.$gz.api.upsert(
"translation/subset",
needIt
);
transData.data.forEach(function commitFetchedTranslationItemToStore(
item
) {
window.$gz.store.commit("setTranslationText", item);
});
}
},
get(key) {
if (!key) {
console.trace("translation.js::get, no translation key was presented");
return "";
}
//no translation for Wiki
if (key == "Wiki") {
return "Wiki";
}
if (!window.$gz.util.has(window.$gz.store.state.translationText, key)) {
return "??" + key;
}
return window.$gz.store.state.translationText[key];
},
async cacheTranslations(keys, forceTranslationId) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async function fetchTranslationKeysFromServer(resolve) {
//
//step 1: build an array of keys that we don't have already
//Note: this will ensure only unique keys go into the store so it's safe to call this with dupes as can happen
//for example datatables have dynamic column names so they need to fetch on demand
const needIt = [];
for (let i = 0; i < keys.length; i++) {
if (
!window.$gz.util.has(window.$gz.store.state.translationText, keys[i])
) {
needIt.push(keys[i]);
}
}
if (needIt.length == 0) {
return resolve();
}
//step 2: get it
let transData = null;
if (forceTranslationId) {
transData = await window.$gz.api.upsert(
`translation/subset/${forceTranslationId}`,
needIt
);
} else {
transData = await window.$gz.api.upsert("translation/subset", needIt);
}
transData.data.forEach(function commitFetchedTranslationItemToStore(
item
) {
window.$gz.store.commit("setTranslationText", item);
});
return resolve();
});
},
//Keys that will always be required for any Sockeye work for any user
coreKeys: [
//main nav options
"Home",
"Dashboard",
"Schedule",
"MemoList",
"ReviewList",
"UserSettings",
"SetLoginPassword",
"NotifySubscriptionList",
"UserPreferences",
"Service",
"CustomerList",
"HeadOfficeList",
"VendorList",
"CustomerNotifySubscriptionList",
"Contacts",
"AdministrationGlobalSettings",
"HelpLicense",
"UserList",
"Translation",
"TranslationList",
"ReportList",
"ReminderList",
"Accounting",
"Administration",
"Operations",
"Attachments",
"Review",
"Extensions",
"History",
"Statistics",
"Backup",
"ServerState",
"ServerJobs",
"ServerLog",
"ServerMetrics",
"ServerProfiler",
"OpsNotificationSettings",
"ViewServerConfiguration",
"NotificationCustomerDeliveryLog",
"NotificationDeliveryLog",
"HelpAboutSockeye",
"MenuHelp",
"More",
"Logout",
"Active",
"Copy",
"New",
"Cancel",
"Close",
"Save",
"SaveACopy",
"Delete",
"SoftDelete",
"SoftDeleteAll",
"Undelete",
"Add",
"Replace",
"Remove",
"OK",
"Open",
"Print",
"Report",
"Refresh",
"Sort",
"Duplicate",
"RecordHistory",
"Search",
"TypeToSearchOrAdd",
"SelectedItems",
"AllItemsInList",
"NoData",
"Errors",
"ErrorFieldLengthExceeded",
"ErrorStartDateAfterEndDate",
"ErrorRequiredFieldEmpty",
"ErrorFieldValueNotInteger",
"ErrorFieldValueNotDecimal",
"ErrorAPI2000",
"ErrorAPI2001",
"ErrorAPI2002",
"ErrorAPI2003",
"ErrorAPI2004",
"ErrorAPI2005",
"ErrorAPI2006",
"ErrorAPI2010",
"ErrorAPI2020",
"ErrorAPI2030",
"ErrorAPI2040",
"ErrorAPI2200",
"ErrorAPI2201",
"ErrorAPI2202",
"ErrorAPI2203",
"ErrorAPI2204",
"ErrorAPI2205",
"ErrorAPI2206",
"ErrorAPI2207",
"ErrorAPI2208",
"ErrorAPI2209",
"ErrorAPI2210",
"ErrorAPI2212",
"ErrorServerUnresponsive",
"ErrorUserNotAuthenticated",
"ErrorUserNotAuthorized",
"ErrorNoMatch",
"ErrorPickListQueryInvalid",
"ErrorSecurityUserCapacity",
"ErrorDBForeignKeyViolation",
"DeletePrompt",
"AreYouSureUnsavedChanges",
"Leave",
"Tags",
"Tag",
"Customize",
"ObjectCustomFieldCustomGrid",
"RowsPerPage",
"PageOfPageText",
"Loading",
"Filter",
"Heading",
"Table",
"InsertLink",
"LinkUrl",
"LinkText",
"InsertImage",
"ImageUrl",
"ImageDescription",
"AttachFile",
"AttachmentNotes",
"Upload",
"AttachmentFileName",
"FileAttachment",
"MaintenanceExpired",
"MaintenanceExpiredNote",
"Import",
"Export",
"TimeSpanYears",
"TimeSpanMonths",
"TimeSpanDays",
"TimeSpanHours",
"TimeSpanMinutes",
"TimeSpanSeconds",
"DirectNotification",
"UpdateAvailable",
"DropFilesHere",
"First",
"Backward",
"Forward",
"Last",
"GeoCapture",
"GeoView",
"CopyToClipboard",
"SockType",
"Now",
"DateRangeToday",
"ReportRenderTimeOut",
"RenderingReport",
"Settings",
"IntegrationList",
"BusinessSettings",
"LicenseList",
"License",
"TrialLicenseRequestList",
"SubscriptionServerList",
"ProductList",
"PurchaseList",
"GZCaseList"
],
////////////////////////////////////////////////////////
// Take in a string that contains one or more
//translation keys that start with LT:
//translate each and replace and return the string translated
// (fetch and cache any missing strings)
async translateStringWithMultipleKeysAsync(s) {
if (s == null) {
return s;
}
let ret = s;
const found = s.match(/LT:[\w]*/gm);
if (found == null) {
return ret;
}
//clean up the keys for fetching
const keysToCache = found.map(z => z.replace("LT:", ""));
//cache / fetch any that are not already present
await this.cacheTranslations(keysToCache);
//replace
found.forEach(z => {
const translated = this.get(z.replace("LT:", ""));
//replace all
ret = ret.split(z).join(translated);
});
return ret;
},
////////////////////////////////////////////////////////
// Take in a string that contains one or more
//translation keys that start with LT:
//translate each and replace and return the string translated
// (DOES NOT fetch and cache any missing strings, they must exist)
//this is the sync version to be used in non async capable code
translateStringWithMultipleKeys(s) {
let ret = s;
const found = s.match(/LT:[\w]*/gm);
if (found == null) {
return ret;
}
//replace
found.forEach(z => {
const translated = this.get(z.replace("LT:", ""));
//replace all
ret = ret.split(z).join(translated);
});
return ret;
},
////////////////////////////////////////////////////////
// dynamically set the vuetify language elements from
// users translated text
// Keeping vuetify using en locale and just adjusting on top of that
//
setVuetifyDefaultLanguageElements(vm) {
vm.$vuetify.lang.locales.en.close = this.get("OK");
}
};