Compare commits
13 Commits
f8ee7b2da7
...
8.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 439de1500f | |||
| f2f10b819b | |||
| 44411ec165 | |||
| 66093db5fa | |||
| 04d3805fae | |||
| 44d91611a9 | |||
| 7ee5dfa119 | |||
| 74d07e77e2 | |||
| de03f56015 | |||
| 838ce9f390 | |||
| ab72022c08 | |||
| 8c3037bd86 | |||
| e4dcbd852e |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -13,4 +13,5 @@ Thumbs.db
|
||||
|
||||
# Environment
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.*.local
|
||||
/ayanova/logfile
|
||||
|
||||
@@ -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
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,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
|
||||
```
|
||||
|
||||
@@ -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