15 Commits

Author SHA1 Message Date
439de1500f case 4602 - logo and attachment forms: upload and delete only shown when usable
Business logos: file choosers only with change rights on global settings,
Upload only once a file is chosen (cleared after upload), Delete only for a
logo that exists (image load / error). Removed the unused, undeclared
readOnly. Attachments: Upload only once a file is selected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 10:11:35 -07:00
f2f10b819b case 4707 - remove the stray cypress.json
Left over from 2020: it points at tests/e2e, removed in 2022 when the
end-to-end tests moved to the raven-test repo. Cypress is not a dependency
here and nothing refers to the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 08:04:12 -07:00
44411ec165 case 4722 - DOMPurify 2.5.9 and Vuetify 2.7.2
DOMPurify 2.3.8 -> 2.5.9 and Vuetify 2.6.6 -> 2.7.2, the latest 2.x of
each; builds on Node 12.22.9. Only these two packages change in the
lockfile (kept in its v2 format).

Vuetify 2.7 calendar eventSummary() returns an element, not an HTML
string, so the six calendar event templates render it with the new
gz-vnodes component instead of v-html.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 18:06:28 -07:00
66093db5fa case 4721 - client escaping of record text
Record text shown as HTML is escaped: unit warranty summary (vendor name,
receipt number), work-order address dialog (now plain text), search excerpt
(escaped, search terms highlighted as plain text, not as a regular
expression). Confirmation dialog messages are sanitised with DOMPurify.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 17:19:52 -07:00
04d3805fae case 4690 - CLAUDE.md: port merged into main; release work continues on main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:31:15 -07:00
44d91611a9 Merge net10-port: .NET 10 port (case 4690) 2026-10-23 17:23:45 -07:00
7ee5dfa119 case 4690 - client build: Sass quietDeps to silence Vuetify deprecation warnings (CSS output unchanged)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:37:34 -07:00
74d07e77e2 case 4690 - version bump 2026-10-22 12:39:01 -07:00
de03f56015 point README at the build docs in raven 2026-10-22 11:55:14 -07:00
838ce9f390 Add CLAUDE.md; point README at the build docs in raven 2026-10-22 11:54:09 -07:00
ab72022c08 case 4672 2026-09-21 14:39:50 -07:00
8c3037bd86 4643 2026-02-15 14:59:52 -08:00
e4dcbd852e 4643 2026-02-15 12:57:31 -08:00
f8ee7b2da7 case 4642 2026-02-13 08:24:15 -08:00
fcd1da6ef8 case 4642 2026-02-13 07:23:56 -08:00
27 changed files with 286 additions and 125 deletions

3
.gitignore vendored
View File

@@ -13,4 +13,5 @@ Thumbs.db
# Environment
.env.local
.env.*.local
.env.*.local
/ayanova/logfile

59
CLAUDE.md Normal file
View 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).

View File

@@ -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
```

View File

@@ -1,9 +0,0 @@
{
"pluginsFile": "tests/e2e/plugins/index.js",
"baseUrl": "http://localhost:7575",
"defaultCommandTimeout":10000,
"env": {
"adminusername": "superuser",
"adminpassword": "l3tm3in"
}
}

View File

@@ -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": {

View File

@@ -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"
},

View File

@@ -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."
};

View File

@@ -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/";
}

View File

@@ -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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
},
///////////////////////////////////////////////
//
//
//

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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")) {

View File

@@ -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) {

View File

@@ -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) {

View 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>

View File

@@ -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>

View File

@@ -31,6 +31,7 @@
v-model="selectedContact"
:items="contacts"
:label="$ay.t('Contacts')"
data-cy="contactSelect"
></v-select>
</v-card-text>

View File

@@ -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) {

View File

@@ -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);
/////////////////////////////////////////////////////////////

View File

@@ -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;

View File

@@ -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"

View File

@@ -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>

View File

@@ -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"

View File

@@ -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

View File

@@ -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",