Compare commits
15 Commits
6c3ab29189
...
8.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 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.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ayanova",
|
||||
"version": "8.2.4",
|
||||
"version": "8.3.0",
|
||||
"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.0",
|
||||
"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,4 @@
|
||||
export default {
|
||||
version: "8.2.4",
|
||||
copyright: "© 1999-2025, Ground Zero Tech-Works Inc."
|
||||
version: "8.3.0",
|
||||
copyright: "© 1999-2026, 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, "'");
|
||||
},
|
||||
///////////////////////////////////////////////
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -27,6 +27,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