8 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
23 changed files with 206 additions and 107 deletions

View File

@@ -11,14 +11,12 @@ The AyaNova v8 web client, in `ayanova/`. Vue 2.6, Vuetify 2.6, Vuex, Vue Router
JavaScript (no TypeScript), Vue CLI 4 / webpack 4. Stack details:
`../raven/devdocs/modernization/CODEBASE-REFERENCE.md`.
## During the .NET 10 port
## Release 8.3.0
The port is back-end only. **No changes in this repo** unless John asks, apart from a
rebuild if the port needs one. Whether the client gets rebuilt in this pass is an open
question in `../raven/devdocs/modernization/NET10-PORT.md`.
If a port item touches the client (for example removing a package from both repos), it
goes on a `net10-port` branch here too, with its own commit.
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

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

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

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