Compare commits
20 Commits
6c3ab29189
...
8.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a5e551484 | |||
| a524f01dd6 | |||
| f6917dd089 | |||
| 7ae5ec486e | |||
| be3c45cd78 | |||
| 439de1500f | |||
| f2f10b819b | |||
| 44411ec165 | |||
| 66093db5fa | |||
| 04d3805fae | |||
| 44d91611a9 | |||
| 7ee5dfa119 | |||
| 74d07e77e2 | |||
| de03f56015 | |||
| 838ce9f390 | |||
| ab72022c08 | |||
| 8c3037bd86 | |||
| e4dcbd852e | |||
| f8ee7b2da7 | |||
| fcd1da6ef8 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -13,4 +13,5 @@ Thumbs.db
|
||||
|
||||
# Environment
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.*.local
|
||||
/ayanova/logfile
|
||||
|
||||
59
CLAUDE.md
Normal file
59
CLAUDE.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# AyaNova v8 front end — instructions for Claude Code
|
||||
|
||||
Shared AyaNova context and working rules, from the server repo (the master):
|
||||
@../raven/CLAUDE.md
|
||||
|
||||
Everything below is specific to this repo.
|
||||
|
||||
## What this is
|
||||
|
||||
The AyaNova v8 web client, in `ayanova/`. Vue 2.6, Vuetify 2.6, Vuex, Vue Router, plain
|
||||
JavaScript (no TypeScript), Vue CLI 4 / webpack 4. Stack details:
|
||||
`../raven/devdocs/modernization/CODEBASE-REFERENCE.md`.
|
||||
|
||||
## Release 8.3.0
|
||||
|
||||
The .NET 10 port is merged into `main` here too (2026-10-23); its only client changes were
|
||||
the version bump and the Sass `quietDeps` build setting. Release work continues on `main`.
|
||||
**No changes in this repo** unless John asks. A "next release" case that touches the
|
||||
client gets its own commits on `main`, under its own case number.
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Node.js 12.22.9 exactly.** `fibers` breaks on anything newer. Check with `node -v` before building.
|
||||
- **No dependency changes**: no `npm install <package>`, no `npm update`, no version edits in `package.json`. Front-end upgrades are roadmap step F, not current work.
|
||||
- Install with `npm ci` (exact versions from `package-lock.json`), never `npm install`.
|
||||
- Vue 2 / Vuetify 2 are end of life. Do not upgrade them, and do not write Vue 3 style code.
|
||||
- Add `data-testid` attributes to elements that Cypress tests select.
|
||||
|
||||
## Commands
|
||||
|
||||
```powershell
|
||||
cd ayanova
|
||||
npm ci # first time, or after package-lock.json changes
|
||||
npm run serve # dev server with hot reload
|
||||
npm run build # production build into dist\
|
||||
npm run lint
|
||||
```
|
||||
|
||||
## How the build reaches the server
|
||||
|
||||
`npm run build` writes to `ayanova\dist\`. `raven\build-release.bat` (or `buildrelease.bat`
|
||||
here) copies it into `raven\server\AyaNova\wwwroot\`. The .NET build does not produce
|
||||
`wwwroot`: whatever is there when the server is published is what ships.
|
||||
|
||||
Webpack content-hash file names (`app.<hash>.js` …) change on every build even when the
|
||||
contents do not. Compare file contents, not names.
|
||||
|
||||
Warnings on every build that are harmless: Browserslist "caniuse-lite is outdated"; Dart
|
||||
Sass division deprecation (from Vuetify); asset size (Monaco's workers).
|
||||
|
||||
## Code layout
|
||||
|
||||
`src/api/` API utilities, auth and error handling; `src/components/` about 80 components;
|
||||
`src/views/` about 100 pages; `src/plugins/` Vuetify setup; `router.js`, `store.js`,
|
||||
`main.js`, `App.vue`.
|
||||
|
||||
View prefixes: `acc-` accounting, `adm-` administration, `cust-` customers, `home-`
|
||||
dashboard, `inv-` inventory, `ops-` server operations, `svc-` service (work orders, quotes,
|
||||
PMs).
|
||||
@@ -1,36 +1,35 @@
|
||||
# ayanova
|
||||
# AyaNova client UI
|
||||
|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
1. Install the build tools exactly as listed in `C:\data\code\raven\devdocs\build\BUILD.md`.
|
||||
They are very specific, and in several cases outdated on purpose (Node.js 12.22.9).
|
||||
2. Clone this repository from Gitea into `C:\data\code\raven-client`.
|
||||
3. Install dependencies:
|
||||
|
||||
```
|
||||
cd C:\data\code\raven-client\ayanova
|
||||
npm ci
|
||||
```
|
||||
|
||||
Use `npm ci`, not `npm install`: it installs the exact versions in `package-lock.json`.
|
||||
|
||||
## Development server with hot reload
|
||||
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
## Production build
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run your tests
|
||||
```
|
||||
npm run test
|
||||
```
|
||||
Output goes to `dist\`. The release build copies it into the server's `wwwroot`.
|
||||
|
||||
## Lint
|
||||
|
||||
### Lints and fixes files
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Run your end-to-end tests
|
||||
```
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### Run your unit tests
|
||||
```
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"pluginsFile": "tests/e2e/plugins/index.js",
|
||||
"baseUrl": "http://localhost:7575",
|
||||
"defaultCommandTimeout":10000,
|
||||
"env": {
|
||||
"adminusername": "superuser",
|
||||
"adminpassword": "l3tm3in"
|
||||
}
|
||||
}
|
||||
32
ayanova/package-lock.json
generated
32
ayanova/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ayanova",
|
||||
"version": "8.2.4",
|
||||
"version": "8.3.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ayanova",
|
||||
"version": "8.2.4",
|
||||
"version": "8.3.1",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.15.4",
|
||||
@@ -15,7 +15,7 @@
|
||||
"chart.js": "^2.9.4",
|
||||
"chartjs-adapter-luxon": "^0.2.2",
|
||||
"core-js": "^3.23.1",
|
||||
"dompurify": "^2.3.8",
|
||||
"dompurify": "^2.5.9",
|
||||
"fontsource-roboto": "^3.1.5",
|
||||
"github-markdown-css": "^4.0.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
@@ -31,7 +31,7 @@
|
||||
"vue-currency-input": "1.22.3",
|
||||
"vue-router": "^3.5.4",
|
||||
"vue-signature": "^2.5.5",
|
||||
"vuetify": "^2.6.6",
|
||||
"vuetify": "^2.7.2",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-persistedstate": "^2.7.1"
|
||||
},
|
||||
@@ -6190,9 +6190,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.8.tgz",
|
||||
"integrity": "sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw=="
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.9.tgz",
|
||||
"integrity": "sha512-i6mvVmWN4xo9LrhCOZrDgSs9noW6nOahbrmzjRbPF36YPyj5Ue5lgok0MHDWkG7xzpWFO2OYttXdzM7rJxHvNA=="
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "2.8.0",
|
||||
@@ -15398,9 +15398,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vuetify": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.6.6.tgz",
|
||||
"integrity": "sha512-H4KtxDFmDN8QiTRiGfBySyjMhVaHAJTKB0llGGKZT5jKxtnx9gvEtMWXKtVuRP0NJJP0H6xBPJHNOH7nT18qiQ==",
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.7.2.tgz",
|
||||
"integrity": "sha512-qr04ww7uzAPQbpk751x4fSdjsJ+zREzjQ/rBlcQGuWS6MIMFMXcXcwvp4+/tnGsULZxPMWfQ0kmZmg5Yc/XzgQ==",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/johnleider"
|
||||
@@ -21772,9 +21772,9 @@
|
||||
}
|
||||
},
|
||||
"dompurify": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.8.tgz",
|
||||
"integrity": "sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw=="
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.9.tgz",
|
||||
"integrity": "sha512-i6mvVmWN4xo9LrhCOZrDgSs9noW6nOahbrmzjRbPF36YPyj5Ue5lgok0MHDWkG7xzpWFO2OYttXdzM7rJxHvNA=="
|
||||
},
|
||||
"domutils": {
|
||||
"version": "2.8.0",
|
||||
@@ -29104,9 +29104,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"vuetify": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.6.6.tgz",
|
||||
"integrity": "sha512-H4KtxDFmDN8QiTRiGfBySyjMhVaHAJTKB0llGGKZT5jKxtnx9gvEtMWXKtVuRP0NJJP0H6xBPJHNOH7nT18qiQ==",
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.7.2.tgz",
|
||||
"integrity": "sha512-qr04ww7uzAPQbpk751x4fSdjsJ+zREzjQ/rBlcQGuWS6MIMFMXcXcwvp4+/tnGsULZxPMWfQ0kmZmg5Yc/XzgQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"vuetify-loader": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ayanova",
|
||||
"version": "8.2.4",
|
||||
"version": "8.3.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
@@ -16,7 +16,7 @@
|
||||
"chart.js": "^2.9.4",
|
||||
"chartjs-adapter-luxon": "^0.2.2",
|
||||
"core-js": "^3.23.1",
|
||||
"dompurify": "^2.3.8",
|
||||
"dompurify": "^2.5.9",
|
||||
"fontsource-roboto": "^3.1.5",
|
||||
"github-markdown-css": "^4.0.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
@@ -32,7 +32,7 @@
|
||||
"vue-currency-input": "1.22.3",
|
||||
"vue-router": "^3.5.4",
|
||||
"vue-signature": "^2.5.5",
|
||||
"vuetify": "^2.6.6",
|
||||
"vuetify": "^2.7.2",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-persistedstate": "^2.7.1"
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export default {
|
||||
version: "8.2.4",
|
||||
copyright: "© 1999-2025, Ground Zero Tech-Works Inc."
|
||||
version: "8.3.1",
|
||||
//case 4630: the year is set when the client is built (vue.config.js)
|
||||
copyright: `© 1999-${process.env.VUE_APP_BUILD_YEAR}, Ground Zero Tech-Works Inc.`
|
||||
};
|
||||
|
||||
@@ -290,7 +290,7 @@ export default {
|
||||
if (
|
||||
window.$gz.dev &&
|
||||
window.location.hostname == "localhost" &&
|
||||
window.location.port == "8080"
|
||||
(window.location.port == "8080" || window.location.port == "8081")
|
||||
) {
|
||||
return "http://localhost:7575/api/v8.0/" + apiPath;
|
||||
}
|
||||
@@ -307,7 +307,7 @@ export default {
|
||||
if (
|
||||
window.$gz.dev &&
|
||||
window.location.hostname == "localhost" &&
|
||||
window.location.port == "8080"
|
||||
(window.location.port == "8080" || window.location.port == "8081")
|
||||
) {
|
||||
return "http://localhost:7575/docs/";
|
||||
}
|
||||
@@ -317,7 +317,7 @@ export default {
|
||||
if (
|
||||
window.$gz.dev &&
|
||||
window.location.hostname == "localhost" &&
|
||||
window.location.port == "8080"
|
||||
(window.location.port == "8080" || window.location.port == "8081")
|
||||
) {
|
||||
return "http://localhost:7575/cust/";
|
||||
}
|
||||
|
||||
@@ -646,6 +646,20 @@ export default {
|
||||
return str != null && typeof str.valueOf() === "string";
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
// case 4721: escape text that goes into HTML (v-html) so it shows as text
|
||||
//
|
||||
escapeHtml: function(str) {
|
||||
if (str === null || str === undefined) {
|
||||
return "";
|
||||
}
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
@@ -37,7 +37,7 @@ export default {
|
||||
// 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"
|
||||
// if the browser can't name its time zone then its current offset from UTC
|
||||
//https://www.iana.org/time-zones
|
||||
//https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
getResolvedTimeZoneName() {
|
||||
@@ -49,7 +49,14 @@ export default {
|
||||
if (!window.$gz.util.stringIsNullOrEmpty(tz)) {
|
||||
return tz;
|
||||
} else {
|
||||
return "America/New_York";
|
||||
//case 4739: e.g. a new Windows time zone the browser doesn't know yet; this used to be "America/New_York"
|
||||
//a fixed offset zone in whole hours, the only kind every browser and luxon accept by name
|
||||
//(Etc/GMT signs are reversed: UTC-7 is Etc/GMT+7, which is what getTimezoneOffset returns, 420 minutes)
|
||||
const hours = Math.round(new Date().getTimezoneOffset() / 60);
|
||||
if (hours == 0) {
|
||||
return "Etc/GMT";
|
||||
}
|
||||
return `Etc/GMT${hours > 0 ? "+" : "-"}${Math.abs(hours)}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -115,9 +115,13 @@
|
||||
v-model="notes"
|
||||
:label="$ay.t('AttachmentNotes')"
|
||||
></v-text-field>
|
||||
<v-btn color="primary" text @click="upload">{{
|
||||
$ay.t("Upload")
|
||||
}}</v-btn>
|
||||
<v-btn
|
||||
v-if="uploadFiles && uploadFiles.length > 0"
|
||||
color="primary"
|
||||
text
|
||||
@click="upload"
|
||||
>{{ $ay.t("Upload") }}</v-btn
|
||||
>
|
||||
</div>
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
|
||||
@@ -26,11 +26,9 @@
|
||||
>
|
||||
<template v-slot:event="{ event, eventSummary }">
|
||||
<div>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
:class="event.textColor + '--text'"
|
||||
v-html="eventSummary()"
|
||||
/><v-icon v-if="!event.editable" x-small :color="event.textColor">
|
||||
<span :class="event.textColor + '--text'"
|
||||
><gz-vnodes :vnodes="eventSummary()"/></span
|
||||
><v-icon v-if="!event.editable" x-small :color="event.textColor">
|
||||
$ayiLock</v-icon
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -25,11 +25,9 @@
|
||||
>
|
||||
<template v-slot:event="{ event, eventSummary }">
|
||||
<div>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
:class="event.textColor + '--text'"
|
||||
v-html="eventSummary()"
|
||||
/><v-icon v-if="!event.editable" x-small :color="event.textColor">
|
||||
<span :class="event.textColor + '--text'"
|
||||
><gz-vnodes :vnodes="eventSummary()"/></span
|
||||
><v-icon v-if="!event.editable" x-small :color="event.textColor">
|
||||
$ayiLock</v-icon
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -26,11 +26,9 @@
|
||||
>
|
||||
<template v-slot:event="{ event, eventSummary }">
|
||||
<div>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
:class="event.textColor + '--text'"
|
||||
v-html="eventSummary()"
|
||||
/><v-icon v-if="!event.editable" x-small :color="event.textColor">
|
||||
<span :class="event.textColor + '--text'"
|
||||
><gz-vnodes :vnodes="eventSummary()"/></span
|
||||
><v-icon v-if="!event.editable" x-small :color="event.textColor">
|
||||
$ayiLock</v-icon
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -31,10 +31,7 @@
|
||||
<span v-if="options.title" class="ml-5"> {{ options.title }} </span>
|
||||
</v-card-title>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<v-card-text
|
||||
class="text-body-1 text-sm-h6 my-5"
|
||||
v-html="options.message"
|
||||
>
|
||||
<v-card-text class="text-body-1 text-sm-h6 my-5" v-html="safeMessage">
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
@@ -82,6 +79,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import DOMPurify from "dompurify";
|
||||
export default {
|
||||
data: () => ({
|
||||
width: 290,
|
||||
@@ -97,6 +95,12 @@ export default {
|
||||
type: "info" //one of success, info, question, warning, and error, see v-alert docs for more info
|
||||
}
|
||||
}),
|
||||
computed: {
|
||||
//case 4721: messages can hold formatting (line breaks), but never script: sanitised before it is shown
|
||||
safeMessage() {
|
||||
return DOMPurify.sanitize(this.options.message || "");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(options) {
|
||||
if (options.message.includes("\n")) {
|
||||
|
||||
@@ -250,7 +250,7 @@ Ground Zero will comply with all applicable laws pertaining to personal data inc
|
||||
|
||||
A: Data obtained
|
||||
|
||||
Information about you (such as your email address, name, company information) is provided to us when you email us (such as when you email us directly, email us via our online form, email us via our online temporary Key request form) or when you register on our AyaNova Support Forum.
|
||||
Information about you (such as your email address, name, company information) is provided to us when you email us (such as when you email us directly, email us via our online form, email us via our online temporary Key request form).
|
||||
|
||||
Information you provided in your order (such as your company name, company contact information, contact name, along with the AyaNova products/services/subscriptions purchased) is sent to us from our payment processing partner with whom you placed your order.
|
||||
|
||||
@@ -260,7 +260,7 @@ Ground Zero will not share your information for marketing purposes with companie
|
||||
|
||||
C: Use and Sharing of Data
|
||||
|
||||
Personal data you provided when you contact us (such as when you email us directly, email us via our online form, email us via our online temporary Key request form or register on our AyaNova Support Forum) will be used to provide technical support, to respond to user inquiries, to provide temporary Key(s), to maintain case and bug issues reported by you, and to send you relevant AyaNova information.
|
||||
Personal data you provided when you contact us (such as when you email us directly, email us via our online form, or email us via our online temporary Key request form) will be used to provide technical support, to respond to user inquiries, to provide temporary Key(s), to maintain case and bug issues reported by you, and to send you relevant AyaNova information.
|
||||
|
||||
Ground Zero will also receive your company information you provided to our payment processing partner, so that we may process your product/service/subscription order.
|
||||
|
||||
@@ -282,8 +282,7 @@ E: Deleting of Data
|
||||
(i) Temporary key requests without subsequent purchase will be deleted within one year.
|
||||
(ii) Email correspondence without subsequent purchase will be deleted within two years except where required for maintaining case and support history.
|
||||
(iii) Email correspondence including temporary key requests with subsequent subscription purchase will be retained for the life of your subscription. Once a subscription has been allowed to lapse (not renewed), correspondence will be deleted within two years except where required for maintaining case and support history.
|
||||
(iv) Customer information obtained through AyaNova Support Forum registration will be deleted within three years of forum login inactivity.
|
||||
(v) Purchase information along with company information as provided by our payment processing partner will be retained for a minimum of six years after the end of the taxation year to which they relate as per the Canada Revenue Agency and no longer than 10 years after which will be deleted within one year.
|
||||
(iv) Purchase information along with company information as provided by our payment processing partner will be retained for a minimum of six years after the end of the taxation year to which they relate as per the Canada Revenue Agency and no longer than 10 years after which will be deleted within one year.
|
||||
|
||||
|
||||
I HAVE READ AND UNDERSTOOD ALL OF THE ABOVE TERMS AND CONDITIONS AND ACKNOWLEDGE THAT THESE TERMS AND CONDITIONS FORM PART OF THIS AGREEMENT. I AGREE THAT A PRINT-OUT OF THIS AGREEMENT WILL CONSTITUTE A "WRITING" UNDER ANY APPLICABLE LAWS.
|
||||
|
||||
@@ -592,7 +592,9 @@ export default {
|
||||
|
||||
let PurchasedFrom = "-";
|
||||
if (r.purchaseFromVendorId) {
|
||||
PurchasedFrom = `<a href="/vendors/${r.purchaseFromVendorId}"> ${r.purchasedFromVendor}</a>`;
|
||||
PurchasedFrom = `<a href="/vendors/${
|
||||
r.purchaseFromVendorId
|
||||
}"> ${window.$gz.util.escapeHtml(r.purchasedFromVendor)}</a>`; //case 4721: escaped
|
||||
}
|
||||
let PurchasedDate = "-";
|
||||
if (r.purchaseDate) {
|
||||
@@ -609,7 +611,7 @@ export default {
|
||||
"UnitPurchasedDate"
|
||||
)}: ${PurchasedDate}<br/>${this.$ay.t(
|
||||
"UnitReceipt"
|
||||
)}: ${r.purchaseReceiptNumber ?? "-"}`;
|
||||
)}: ${window.$gz.util.escapeHtml(r.purchaseReceiptNumber ?? "-")}`; //case 4721: escaped
|
||||
|
||||
let RecentWorkOrderList = "";
|
||||
if (r.recentWorkOrders.length > 0) {
|
||||
|
||||
@@ -584,7 +584,9 @@ export default {
|
||||
|
||||
let PurchasedFrom = "-";
|
||||
if (r.purchaseFromVendorId) {
|
||||
PurchasedFrom = `<a href="/vendors/${r.purchaseFromVendorId}"> ${r.purchasedFromVendor}</a>`;
|
||||
PurchasedFrom = `<a href="/vendors/${
|
||||
r.purchaseFromVendorId
|
||||
}"> ${window.$gz.util.escapeHtml(r.purchasedFromVendor)}</a>`; //case 4721: escaped
|
||||
}
|
||||
let PurchasedDate = "-";
|
||||
if (r.purchaseDate) {
|
||||
@@ -601,7 +603,7 @@ export default {
|
||||
"UnitPurchasedDate"
|
||||
)}: ${PurchasedDate}<br/>${this.$ay.t(
|
||||
"UnitReceipt"
|
||||
)}: ${r.purchaseReceiptNumber ?? "-"}`;
|
||||
)}: ${window.$gz.util.escapeHtml(r.purchaseReceiptNumber ?? "-")}`; //case 4721: escaped
|
||||
|
||||
let RecentWorkOrderList = "";
|
||||
if (r.recentWorkOrders.length > 0) {
|
||||
|
||||
11
ayanova/src/components/vnodes-control.vue
Normal file
11
ayanova/src/components/vnodes-control.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<script>
|
||||
//case 4722: renders the element(s) passed in vnodes
|
||||
//(Vuetify 2.7 calendar eventSummary() returns an element, not an HTML string)
|
||||
export default {
|
||||
functional: true,
|
||||
props: {
|
||||
vnodes: { type: [Object, Array], default: null }
|
||||
},
|
||||
render: (h, ctx) => ctx.props.vnodes
|
||||
};
|
||||
</script>
|
||||
@@ -368,9 +368,9 @@
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<!-- case 4721: plain text, the address is never HTML -->
|
||||
<v-list-item-subtitle
|
||||
v-html="item.viz"
|
||||
v-text="item.viz"
|
||||
></v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
v-model="selectedContact"
|
||||
:items="contacts"
|
||||
:label="$ay.t('Contacts')"
|
||||
data-cy="contactSelect"
|
||||
></v-select>
|
||||
</v-card-text>
|
||||
|
||||
|
||||
@@ -624,7 +624,9 @@ export default {
|
||||
|
||||
let PurchasedFrom = "-";
|
||||
if (r.purchaseFromVendorId) {
|
||||
PurchasedFrom = `<a href="/vendors/${r.purchaseFromVendorId}"> ${r.purchasedFromVendor}</a>`;
|
||||
PurchasedFrom = `<a href="/vendors/${
|
||||
r.purchaseFromVendorId
|
||||
}"> ${window.$gz.util.escapeHtml(r.purchasedFromVendor)}</a>`; //case 4721: escaped
|
||||
}
|
||||
let PurchasedDate = "-";
|
||||
if (r.purchaseDate) {
|
||||
@@ -641,7 +643,7 @@ export default {
|
||||
"UnitPurchasedDate"
|
||||
)}: ${PurchasedDate}<br/>${this.$ay.t(
|
||||
"UnitReceipt"
|
||||
)}: ${r.purchaseReceiptNumber ?? "-"}`;
|
||||
)}: ${window.$gz.util.escapeHtml(r.purchaseReceiptNumber ?? "-")}`; //case 4721: escaped
|
||||
|
||||
let RecentWorkOrderList = "";
|
||||
if (r.recentWorkOrders.length > 0) {
|
||||
|
||||
@@ -59,6 +59,7 @@ import attachmentControl from "./components/attachment-control.vue";
|
||||
import chartLineControl from "./components/chart-line-control.vue";
|
||||
import chartBarControl from "./components/chart-bar-control.vue";
|
||||
import chartBarHorizontalControl from "./components/chart-bar-horizontal-control.vue";
|
||||
import vnodesControl from "./components/vnodes-control.vue";
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// LIBS AND GLOBAL ITEMS
|
||||
@@ -180,6 +181,7 @@ Vue.component("gz-attachments", attachmentControl);
|
||||
Vue.component("gz-chart-line", chartLineControl);
|
||||
Vue.component("gz-chart-bar", chartBarControl);
|
||||
Vue.component("gz-chart-bar-horizontal", chartBarHorizontalControl);
|
||||
Vue.component("gz-vnodes", vnodesControl);
|
||||
//3rd party components
|
||||
Vue.use(VueCurrencyInput);
|
||||
/////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -16,8 +16,11 @@
|
||||
class="grey lighten-2"
|
||||
:src="smallUrl"
|
||||
:alt="$ay.t('SmallLogo')"
|
||||
@load="hasSmall = true"
|
||||
@error="hasSmall = false"
|
||||
/>
|
||||
<v-file-input
|
||||
v-if="rights.change"
|
||||
v-model="uploadSmall"
|
||||
dense
|
||||
accept="image/*"
|
||||
@@ -26,20 +29,31 @@
|
||||
:rules="rules"
|
||||
data-cy="uploadSmall"
|
||||
></v-file-input>
|
||||
<v-btn color="primary" text @click="remove('small')">{{
|
||||
$ay.t("Delete")
|
||||
}}</v-btn>
|
||||
<v-btn color="primary" text @click="upload('small')">{{
|
||||
$ay.t("Upload")
|
||||
}}</v-btn>
|
||||
<v-btn
|
||||
v-if="rights.change && hasSmall"
|
||||
color="primary"
|
||||
text
|
||||
@click="remove('small')"
|
||||
>{{ $ay.t("Delete") }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
v-if="rights.change && uploadSmall"
|
||||
color="primary"
|
||||
text
|
||||
@click="upload('small')"
|
||||
>{{ $ay.t("Upload") }}</v-btn
|
||||
>
|
||||
</v-col>
|
||||
<v-col cols="12" class="mt-10">
|
||||
<img
|
||||
class="mt-10 grey lighten-2"
|
||||
:src="mediumUrl"
|
||||
:alt="$ay.t('MediumLogo')"
|
||||
@load="hasMedium = true"
|
||||
@error="hasMedium = false"
|
||||
/>
|
||||
<v-file-input
|
||||
v-if="rights.change"
|
||||
v-model="uploadMedium"
|
||||
dense
|
||||
accept="image/*"
|
||||
@@ -47,20 +61,31 @@
|
||||
:label="$ay.t('MediumLogo')"
|
||||
:rules="rules"
|
||||
></v-file-input>
|
||||
<v-btn color="primary" text @click="remove('medium')">{{
|
||||
$ay.t("Delete")
|
||||
}}</v-btn>
|
||||
<v-btn color="primary" text @click="upload('medium')">{{
|
||||
$ay.t("Upload")
|
||||
}}</v-btn>
|
||||
<v-btn
|
||||
v-if="rights.change && hasMedium"
|
||||
color="primary"
|
||||
text
|
||||
@click="remove('medium')"
|
||||
>{{ $ay.t("Delete") }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
v-if="rights.change && uploadMedium"
|
||||
color="primary"
|
||||
text
|
||||
@click="upload('medium')"
|
||||
>{{ $ay.t("Upload") }}</v-btn
|
||||
>
|
||||
</v-col>
|
||||
<v-col cols="12" class="mt-10">
|
||||
<img
|
||||
class="mt-10 grey lighten-2"
|
||||
:src="largeUrl"
|
||||
:alt="$ay.t('LargeLogo')"
|
||||
@load="hasLarge = true"
|
||||
@error="hasLarge = false"
|
||||
/>
|
||||
<v-file-input
|
||||
v-if="rights.change"
|
||||
v-model="uploadLarge"
|
||||
dense
|
||||
accept="image/*"
|
||||
@@ -68,12 +93,20 @@
|
||||
:label="$ay.t('LargeLogo')"
|
||||
:rules="rules"
|
||||
></v-file-input>
|
||||
<v-btn color="primary" text @click="remove('large')">{{
|
||||
$ay.t("Delete")
|
||||
}}</v-btn>
|
||||
<v-btn color="primary" text @click="upload('large')">{{
|
||||
$ay.t("Upload")
|
||||
}}</v-btn>
|
||||
<v-btn
|
||||
v-if="rights.change && hasLarge"
|
||||
color="primary"
|
||||
text
|
||||
@click="remove('large')"
|
||||
>{{ $ay.t("Delete") }}</v-btn
|
||||
>
|
||||
<v-btn
|
||||
v-if="rights.change && uploadLarge"
|
||||
color="primary"
|
||||
text
|
||||
@click="upload('large')"
|
||||
>{{ $ay.t("Upload") }}</v-btn
|
||||
>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
@@ -94,6 +127,10 @@ export default {
|
||||
mediumUrl: null,
|
||||
largeUrl: null,
|
||||
smallUrl: null,
|
||||
//case 4602: Delete is shown only for a logo that exists (set by the image's load / error)
|
||||
hasSmall: false,
|
||||
hasMedium: false,
|
||||
hasLarge: false,
|
||||
fileSizeExceededWarning: "",
|
||||
formState: {
|
||||
ready: false,
|
||||
@@ -119,7 +156,6 @@ export default {
|
||||
try {
|
||||
await initForm(vm);
|
||||
vm.formState.ready = true;
|
||||
vm.readOnly = !vm.rights.change;
|
||||
window.$gz.eventBus.$on("menu-click", clickHandler);
|
||||
//NOTE: this would normally be in getDataFromAPI but this form doesn't really need that function so doing it here
|
||||
generateMenu(vm);
|
||||
@@ -171,12 +207,15 @@ export default {
|
||||
switch (size) {
|
||||
case "small":
|
||||
vm.smallUrl = `${window.$gz.api.logoUrl(size)}?x=${Date.now()}`;
|
||||
vm.uploadSmall = null;
|
||||
break;
|
||||
case "medium":
|
||||
vm.mediumUrl = `${window.$gz.api.logoUrl(size)}?x=${Date.now()}`;
|
||||
vm.uploadMedium = null;
|
||||
break;
|
||||
case "large":
|
||||
vm.largeUrl = `${window.$gz.api.logoUrl(size)}?x=${Date.now()}`;
|
||||
vm.uploadLarge = null;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
@@ -207,12 +246,15 @@ export default {
|
||||
switch (size) {
|
||||
case "small":
|
||||
vm.smallUrl = null;
|
||||
vm.hasSmall = false;
|
||||
break;
|
||||
case "medium":
|
||||
vm.mediumUrl = null;
|
||||
vm.hasMedium = false;
|
||||
break;
|
||||
case "large":
|
||||
vm.largeUrl = null;
|
||||
vm.hasLarge = false;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
|
||||
@@ -103,11 +103,9 @@
|
||||
<v-icon small :color="event.textColor" class="mt-n3 mr-1">{{
|
||||
iconForEvent(event.type)
|
||||
}}</v-icon>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
:class="event.textColor + '--text'"
|
||||
v-html="eventSummary()"
|
||||
/><v-icon
|
||||
<span :class="event.textColor + '--text'"
|
||||
><gz-vnodes :vnodes="eventSummary()"/></span
|
||||
><v-icon
|
||||
v-if="!event.editable"
|
||||
x-small
|
||||
:color="event.textColor"
|
||||
|
||||
@@ -206,16 +206,9 @@ export default {
|
||||
vm.formState.serverError = res.error;
|
||||
window.$gz.form.setErrorBoxErrors(vm);
|
||||
} else {
|
||||
let showInfo = res.data;
|
||||
//case 4721: the excerpt is record text, so it is escaped; the search terms are highlighted around it
|
||||
const searchTerms = vm.searchPhrase.replace(/[*]/gi, "").split(" ");
|
||||
for (let i = 0; i < searchTerms.length; i++) {
|
||||
const regEx = new RegExp(searchTerms[i], "ig");
|
||||
const replaceMask =
|
||||
"<span class='v-list-item__mask'>" + searchTerms[i] + "</span>";
|
||||
showInfo = showInfo.replace(regEx, replaceMask);
|
||||
}
|
||||
|
||||
item.info = showInfo;
|
||||
item.info = highlightTerms(res.data || "", searchTerms);
|
||||
}
|
||||
} catch (error) {
|
||||
window.$gz.errorHandler.handleFormError(error, vm);
|
||||
@@ -357,4 +350,39 @@ async function populateSelectionLists(vm) {
|
||||
window.$gz.form.addNoSelectionItem(vm.selectLists.objectTypes);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////
|
||||
// case 4721: excerpt as escaped text with the search terms highlighted (case insensitive);
|
||||
// the terms are matched as plain text, not as a regular expression
|
||||
function highlightTerms(text, terms) {
|
||||
const esc = window.$gz.util.escapeHtml;
|
||||
const lowerText = text.toLowerCase();
|
||||
const lowerTerms = terms.map(z => z.toLowerCase()).filter(z => z.length > 0);
|
||||
let html = "";
|
||||
let pos = 0;
|
||||
while (pos < text.length) {
|
||||
let best = -1;
|
||||
let bestLength = 0;
|
||||
lowerTerms.forEach(t => {
|
||||
const i = lowerText.indexOf(t, pos);
|
||||
if (
|
||||
i !== -1 &&
|
||||
(best === -1 || i < best || (i === best && t.length > bestLength))
|
||||
) {
|
||||
best = i;
|
||||
bestLength = t.length;
|
||||
}
|
||||
});
|
||||
if (best === -1) {
|
||||
break;
|
||||
}
|
||||
html +=
|
||||
esc(text.substring(pos, best)) +
|
||||
"<span class='v-list-item__mask'>" +
|
||||
esc(text.substring(best, best + bestLength)) +
|
||||
"</span>";
|
||||
pos = best + bestLength;
|
||||
}
|
||||
return html + esc(text.substring(pos));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -39,7 +39,10 @@
|
||||
dense
|
||||
:readonly="formState.readOnly"
|
||||
:label="$ay.t('BackupSetsToKeep')"
|
||||
:rules="[form().integerValid(this, 'backupSetsToKeep')]"
|
||||
:rules="[
|
||||
form().integerValid(this, 'backupSetsToKeep'),
|
||||
form().minValueValid(this, 'backupSetsToKeep', 1)
|
||||
]"
|
||||
:error-messages="form().serverErrors(this, 'backupSetsToKeep')"
|
||||
type="number"
|
||||
@input="fieldValueChanged('backupSetsToKeep')"
|
||||
|
||||
@@ -96,11 +96,9 @@
|
||||
<v-icon small :color="event.textColor" class="mr-1">{{
|
||||
iconForEvent(event.type)
|
||||
}}</v-icon>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
:class="event.textColor + '--text'"
|
||||
v-html="eventSummary()"
|
||||
/><v-icon
|
||||
<span :class="event.textColor + '--text'"
|
||||
><gz-vnodes :vnodes="eventSummary()"/></span
|
||||
><v-icon
|
||||
v-if="!event.editable"
|
||||
x-small
|
||||
:color="event.textColor"
|
||||
|
||||
@@ -132,8 +132,9 @@
|
||||
|
||||
<template v-slot:event="{ event, eventSummary }">
|
||||
<div class="v-event-draggable">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span :class="event.textColor + '--text'" v-html="eventSummary()" />
|
||||
<span :class="event.textColor + '--text'"
|
||||
><gz-vnodes :vnodes="eventSummary()"
|
||||
/></span>
|
||||
<v-icon
|
||||
v-if="!event.editable"
|
||||
x-small
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
|
||||
|
||||
//case 4630: the year the client is built, for the copyright line (src/api/ayanova-version.js); VUE_APP_ variables
|
||||
//are compiled into the client
|
||||
process.env.VUE_APP_BUILD_YEAR = new Date().getFullYear().toString();
|
||||
|
||||
module.exports = {
|
||||
//https://medium.com/@mrodal/how-to-make-lazy-loading-actually-work-in-vue-cli-3-7f3f88cfb102
|
||||
chainWebpack: config => {
|
||||
@@ -27,6 +31,14 @@ module.exports = {
|
||||
|
||||
transpileDependencies: ["vuetify"],
|
||||
|
||||
//Silence Dart Sass deprecation warnings raised inside node_modules (Vuetify 2's own styles); our own styles still warn
|
||||
css: {
|
||||
loaderOptions: {
|
||||
sass: { sassOptions: { quietDeps: true } },
|
||||
scss: { sassOptions: { quietDeps: true } }
|
||||
}
|
||||
},
|
||||
|
||||
pwa: {
|
||||
//https://cli.vuejs.org/core-plugins/pwa.html#configuration
|
||||
// manifestPath: "manifest.json",
|
||||
|
||||
Reference in New Issue
Block a user