13 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
28 changed files with 283 additions and 404 deletions

3
.gitignore vendored
View File

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

View File

@@ -1,271 +0,0 @@
# AyaNova v8 Build Environment Documentation
> **Purpose**: This document captures the exact build environment for AyaNova v8 as of February 2026. Use this to recreate the build environment on a new machine or in a container.
## Overview
AyaNova v8 consists of three main build components:
1. **Frontend** - Vue.js 2 application (JavaScript, not TypeScript)
2. **Documentation** - MkDocs with Material theme
3. **Backend** - ASP.NET Core 8.0 server (C#)
The frontend and docs are built first, then copied into the backend project's `wwwroot` folder before the backend is built and packaged for distribution.
---
## Current Build Machine Specifications
| Component | Version | Notes |
|-----------|---------|-------|
| Operating System | Windows 11 | ThinkPad X13 Gen 3 |
| Node.js | v12.22.9 | **End-of-life** - do not upgrade without testing |
| npm | (check with `npm -v`) | Comes with Node |
| Python | 3.11.1 | For MkDocs |
| .NET SDK | 8.0.x | For backend build |
---
## Frontend Build Environment
### Directory Structure
```
c:\data\code\
├── raven-client\
│ └── ayanova\ # Vue.js frontend project
│ ├── src\ # Source code
│ ├── public\ # Static assets
│ ├── dist\ # Build output (generated)
│ ├── package.json
│ ├── vue.config.js
│ └── buildrelease.bat
├── raven\
│ ├── server\
│ │ └── AyaNova\
│ │ └── wwwroot\ # Frontend files copied here after build
│ └── docs\
│ └── 8.0\
│ └── ayanova\ # MkDocs source
│ └── mkdocs.yml
```
### Node.js Version: Critical Information
**Current version: Node.js 12.22.9**
This project uses an old Node version because:
1. The `fibers` package (used by sass-loader for faster Sass compilation) is a native C++ addon that stopped being maintained and doesn't compile on newer Node versions
2. Various other dependencies have peer dependency requirements that break on Node 14+
**Do not upgrade Node without first testing in an isolated environment.**
### Key Dependencies
From `package.json`:
| Package | Version | Purpose |
|---------|---------|---------|
| vue | 2.6.14 | Core framework (Vue 2, end-of-life Dec 2023) |
| vuetify | 2.6.6 | UI component library |
| @vue/cli-service | 4.5.15 | Build tooling |
| webpack | 4.46.0 | Module bundler |
| sass | 1.52.3 | Sass compiler |
| fibers | 4.0.3 | **Problematic** - native addon for Sass |
| monaco-editor | 0.30.1 | Code editor component |
### Build Process
The frontend build is executed via `buildrelease.bat`:
```batch
@echo **************************************************************
@echo ******************** BUILD CLIENT ****************************
@echo **************************************************************
rmdir c:\data\code\raven\server\AyaNova\wwwroot /s/q
mkdir c:\data\code\raven\server\AyaNova\wwwroot
cd c:\data\code\raven\docs\8.0\ayanova
mkdocs build
cd c:\data\code\raven-client\ayanova
call npm run build
xcopy c:\data\code\raven-client\ayanova\dist\* C:\data\code\raven\server\AyaNova\wwwroot\ /e
pause
```
This does:
1. Clears and recreates the backend's wwwroot folder
2. Builds the MkDocs documentation (output goes directly to wwwroot/docs via mkdocs.yml config)
3. Runs `npm run build` which calls `vue-cli-service build`
4. Copies the built frontend from `dist/` to the backend's `wwwroot/`
### Known Build Warnings (Safe to Ignore)
These warnings appear during every build and do not affect the output:
1. **Browserslist warning**: "caniuse-lite is outdated"
- Harmless - just means browser compatibility database is old
2. **Dart Sass division warnings**: "Using / for division is deprecated"
- Come from Vuetify's stylesheets, not your code
- Will continue working until Dart Sass 2.0
3. **Asset size warnings**: Various files exceed 244 KiB
- Expected with Monaco editor (ts.worker.js is 4.5MB)
- Application still works correctly
### NPM Scripts
```json
{
"serve": "vue-cli-service serve", // Development server
"build": "vue-cli-service build", // Production build
"lint": "vue-cli-service lint" // Code linting
}
```
---
## Documentation Build Environment
### MkDocs Configuration
From `mkdocs.yml`:
| Setting | Value |
|---------|-------|
| Theme | Material |
| Output directory | `../../../server/AyaNova/wwwroot/docs` |
| Site URL | https://ayanova.com/docs/ |
### Required Python Packages
```bash
pip install mkdocs mkdocs-material
```
The `pymdownx` extensions (highlight, superfences) come bundled with mkdocs-material.
### Build Command
```bash
cd c:\data\code\raven\docs\8.0\ayanova
mkdocs build
```
Output goes directly to the backend's wwwroot/docs folder.
---
## Backend Build Environment
### .NET Version
- **Target Framework**: .NET 8.0
- **Project Type**: ASP.NET Core
### Build Tools Required
- .NET 8.0 SDK
- Visual Studio 2022 or VS Code with C# extension
### Build Outputs
The backend build produces multiple deployment targets:
1. Linux hosted (DigitalOcean service)
2. Windows networked (IIS hosted)
3. Windows single-user (self-contained)
4. Linux server
5. Linux desktop
Windows installers are created with Inno Setup.
---
## Recreating the Build Environment
### Option 1: Native Installation (Windows)
1. Install Node.js 12.22.9 specifically:
- Download from https://nodejs.org/dist/v12.22.9/
- Or use nvm-windows: `nvm install 12.22.9 && nvm use 12.22.9`
2. Install Python 3.11:
- Download from python.org
- Ensure it's in PATH
3. Install MkDocs:
```bash
pip install mkdocs mkdocs-material
```
4. Install .NET 8.0 SDK:
- Download from https://dotnet.microsoft.com/download
5. Clone/checkout repository from SVN
6. Install frontend dependencies:
```bash
cd c:\data\code\raven-client\ayanova
npm ci
```
Note: Use `npm ci` (not `npm install`) to install exact versions from package-lock.json
### Option 2: Docker (Recommended for Reproducibility)
See `Dockerfile.frontend` and `Dockerfile.docs` in this repository for containerized builds that don't require any local installation except Docker.
---
## Troubleshooting
### "fibers" fails to install
This native module requires:
- Python 2.7 or 3.x
- Visual Studio Build Tools (Windows) or build-essential (Linux)
- Specific Node.js version compatibility
If it fails, you can try removing it (it's optional):
1. Remove `"fibers": "^4.0.3"` from package.json
2. Remove the `fiber: require("fibers")` line from webpack.config.js
3. Run `npm install` again
The build will be slightly slower but should work.
### Node version mismatch
If you see errors about incompatible Node versions:
1. Check your Node version: `node -v`
2. It must be 12.22.9 for guaranteed compatibility
3. Use nvm to switch versions if needed
### "Module not found" errors
Try:
```bash
rm -rf node_modules
rm package-lock.json
npm install
```
---
## Future Modernization Notes
When time permits, consider:
1. **Remove fibers dependency** - It's optional and causes most build issues
2. **Upgrade to Vue 3 + Vite** - Much faster builds, better tooling, active support
3. **Migrate to TypeScript** - Better IDE support and fewer runtime errors
4. **Update Node to LTS** - Current LTS is Node 20.x
These are significant changes that should be done incrementally with thorough testing.
---
## Version History
| Date | Change |
|------|--------|
| 2026-02-01 | Initial documentation created |

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,47 +1,35 @@
# AyaNova client ui
# AyaNova client UI
## Project setup
```
First confirm all dependencies are installed as per BUILT-ENVIRONMENT.md doc. They are very specific and in many cases outdated.
Then clone/checkout repository from GIT
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:
Finally:
Install frontend dependencies:
cd c:\data\code\raven-client\ayanova
```
cd C:\data\code\raven-client\ayanova
npm ci
Note: Use `npm ci` (not `npm install`) to install exact versions from package-lock.json
```
```
Use `npm ci`, not `npm install`: it installs the exact versions in `package-lock.json`.
## Development server with hot reload
### Compiles and hot-reloads for development
```
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",