diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index c676ffcb0..6df7f285b 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -23,9 +23,9 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up JDK 25 - uses: actions/setup-java@v4 + uses: actions/setup-java@v6 with: java-version: 25 architecture: x64 diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index 7d22c66f5..f14d3ec14 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -16,15 +16,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: run_install: false - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: '24' registry-url: 'https://registry.npmjs.org' diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml index 2bff8bd6f..8a564c360 100644 --- a/.github/workflows/publish-snapshot.yml +++ b/.github/workflows/publish-snapshot.yml @@ -28,17 +28,17 @@ jobs: timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up JDK 25 - uses: actions/setup-java@v4 + uses: actions/setup-java@v6 with: java-version: 25 architecture: x64 distribution: temurin cache: maven server-id: github - server-username: MAVEN_USERNAME - server-password: MAVEN_PASSWORD + server-username-env-var: MAVEN_USERNAME + server-password-env-var: MAVEN_PASSWORD - name: Verify and deploy SNAPSHOT # Se invoca deploy:deploy directo porque central-publishing-maven-plugin (extensions=true) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cacb3f5ed..6737a1fc4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,17 +37,17 @@ jobs: timeout-minutes: 45 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up JDK 25 - uses: actions/setup-java@v4 + uses: actions/setup-java@v6 with: java-version: 25 architecture: x64 distribution: temurin cache: maven server-id: github - server-username: MAVEN_USERNAME - server-password: MAVEN_PASSWORD + server-username-env-var: MAVEN_USERNAME + server-password-env-var: MAVEN_PASSWORD - name: Check the version # A wrong version would publish an immutable artifact (and spend Central quota), so fail before building. @@ -86,17 +86,17 @@ jobs: timeout-minutes: 45 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up JDK 25 - uses: actions/setup-java@v4 + uses: actions/setup-java@v6 with: java-version: 25 architecture: x64 distribution: temurin cache: maven server-id: central - server-username: MAVEN_USERNAME # env variable for username in deploy - server-password: MAVEN_PASSWORD # env variable for token in deploy + server-username-env-var: MAVEN_USERNAME # env variable for username in deploy + server-password-env-var: MAVEN_PASSWORD # env variable for token in deploy gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} - name: Sign and deploy to Maven Central diff --git a/docs/backend/ARCHITECTURE.md b/docs/backend/ARCHITECTURE.md index baffb0e97..a863094f3 100644 --- a/docs/backend/ARCHITECTURE.md +++ b/docs/backend/ARCHITECTURE.md @@ -403,6 +403,24 @@ Rules: browser on its own. - Unauthenticated requests get a JSON 401 (not a redirect to `/login`), so `fetch` callers can handle them. +### Caching the metadata endpoints + +View descriptors, entity metadata and navigation (`GET /apps/api/metadata/views/{id}`, `/entities`, `/entities/{id}`, +`/entities/{id}/views[/{view}]`, `/entities/by-path`, `/navigation`) are practically static, but not always: texts depend +on the request locale (`Messages.getDefaultLocale()`, which an application resolves per request through its +`LocaleProvider`), and an application may build descriptors dynamically or vary them per user. The framework therefore +adds nothing by default and leaves the decision to the application through `MetadataHttpCachePolicy`: + +- **Default:** no cache headers, exactly as before. +- **`dynamia.app.metadata.http-cache=etag`:** the included `ContentEtagMetadataHttpCachePolicy`. A strong ETag from the + content and the locale, `Cache-Control: private, no-cache` and `Vary: Accept-Language`. The client revalidates on every + use and gets a `304` with no body when nothing changed, and never a stale descriptor after a deployment or a hot-reload. +- **Your own policy:** register a `MetadataHttpCachePolicy` bean (for example an ETag from the application version, or a + `Vary` that includes the user). It replaces the included one. + +Only the read-only endpoints above take part; actions, entity data (`/entities/ref/**`) and executions are never cached, +and neither are `/apps/api/dashboard` or `/apps/api/reports`. + --- ## Dependency Injection & Spring Integration diff --git a/examples/demo-zk-books/pom.xml b/examples/demo-zk-books/pom.xml index 3e4eafdae..bfb849738 100644 --- a/examples/demo-zk-books/pom.xml +++ b/examples/demo-zk-books/pom.xml @@ -47,7 +47,7 @@ ${maven.build.timestamp} yyyyMMdd - 26.10.1 + 26.10.2 diff --git a/examples/dynamia-movies/README.md b/examples/dynamia-movies/README.md index c13769197..f5f3a5c35 100644 --- a/examples/dynamia-movies/README.md +++ b/examples/dynamia-movies/README.md @@ -2,7 +2,7 @@ A movie catalog in the spirit of IMDb, built to try **everything new in DynamiaTools** in one project that was generated with the CLI (`dynamia new`, backend Java + frontend Vue) and runs against the **SNAPSHOT** of this -repository (`26.10.1`). +repository (`26.10.2`). | Folder | What it is | |---|---| diff --git a/examples/dynamia-movies/backend/README.md b/examples/dynamia-movies/backend/README.md index 7c560a6c7..ae3da65b2 100644 --- a/examples/dynamia-movies/backend/README.md +++ b/examples/dynamia-movies/backend/README.md @@ -1,6 +1,6 @@ # Dynamia Movies — backend -Spring Boot 4 + DynamiaTools `26.10.1`. See the [project README](../README.md) for the whole picture. +Spring Boot 4 + DynamiaTools `26.10.2`. See the [project README](../README.md) for the whole picture. ```bash ./mvnw spring-boot:run # http://localhost:8484 (admin / adminadmin) diff --git a/examples/dynamia-movies/backend/pom.xml b/examples/dynamia-movies/backend/pom.xml index 35cff80d6..373da50e9 100644 --- a/examples/dynamia-movies/backend/pom.xml +++ b/examples/dynamia-movies/backend/pom.xml @@ -10,12 +10,12 @@ tools.dynamia.demos movies - 26.10.1 + 26.10.2 dynamia-movies Movie catalog demo: Dynamia Tools backoffice + public Vue site 25 - 26.10.1 + 26.10.2 diff --git a/examples/dynamia-movies/frontend/package.json b/examples/dynamia-movies/frontend/package.json index 5e00a4d82..fa4af7980 100644 --- a/examples/dynamia-movies/frontend/package.json +++ b/examples/dynamia-movies/frontend/package.json @@ -1,7 +1,7 @@ { "name": "dynamia-movies", "private": true, - "version": "26.10.1", + "version": "26.10.2", "description": "Public movie site (Vue 3) for the Dynamia Movies demo. It reads the automatic REST API of DynamiaTools.", "type": "module", "scripts": { diff --git a/extensions/dashboard/packages/dashboard-sdk/package.json b/extensions/dashboard/packages/dashboard-sdk/package.json index 4c58519de..d739c9fbf 100644 --- a/extensions/dashboard/packages/dashboard-sdk/package.json +++ b/extensions/dashboard/packages/dashboard-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@dynamia-tools/dashboard-sdk", - "version": "26.10.1", + "version": "26.10.2", "website": "https://dynamia.tools", "description": "TypeScript/JavaScript client SDK for the Dynamia Dashboard extension REST API", "keywords": [ diff --git a/extensions/dashboard/packages/dashboard-vue/package.json b/extensions/dashboard/packages/dashboard-vue/package.json index c7319619b..c9c7617fc 100644 --- a/extensions/dashboard/packages/dashboard-vue/package.json +++ b/extensions/dashboard/packages/dashboard-vue/package.json @@ -1,6 +1,6 @@ { "name": "@dynamia-tools/dashboard-vue", - "version": "26.10.1", + "version": "26.10.2", "website": "https://dynamia.tools", "description": "Vue 3 components and composables to render Dynamia dashboards without ZK", "keywords": [ diff --git a/extensions/dashboard/packages/dashboard-vue/src/components/ViewerWidget.vue b/extensions/dashboard/packages/dashboard-vue/src/components/ViewerWidget.vue index 8287757c2..7427904d5 100644 --- a/extensions/dashboard/packages/dashboard-vue/src/components/ViewerWidget.vue +++ b/extensions/dashboard/packages/dashboard-vue/src/components/ViewerWidget.vue @@ -1,16 +1,16 @@ diff --git a/extensions/dashboard/packages/dashboard-vue/test/components.test.ts b/extensions/dashboard/packages/dashboard-vue/test/components.test.ts index e2c8f9637..4151d2e38 100644 --- a/extensions/dashboard/packages/dashboard-vue/test/components.test.ts +++ b/extensions/dashboard/packages/dashboard-vue/test/components.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { defineComponent } from 'vue'; +import { defineComponent, h } from 'vue'; import { flushPromises, mount } from '@vue/test-utils'; -import { DynamiaDashboard, KpiWidget, WidgetRendererRegistry, registerBuiltinWidgets } from '../src/index.js'; +import { DynamiaDashboard, KpiWidget, ViewerWidget, WidgetRendererRegistry, registerBuiltinWidgets } from '../src/index.js'; import { dashboardDescriptor, makeClient, widgetResponse } from './helpers.js'; const chartCtor = vi.fn(); @@ -139,3 +139,23 @@ describe('built-in widgets', () => { expect(chartDestroy).toHaveBeenCalled(); }); }); + +describe('ViewerWidget', () => { + const Viewer = defineComponent({ props: ['descriptorId', 'viewType', 'source', 'value', 'readOnly'], setup: () => () => h('div', { 'data-testid': 'viewer' }) }); + const mountWidget = (data: unknown) => mount(ViewerWidget, { props: { data: data as never }, global: { stubs: { DynamiaViewer: Viewer } } }); + + it('gives a list value to the viewer as its data source', () => { + const rows = [{ a: 1 }, { a: 2 }]; + const props = mountWidget({ descriptorId: 'clientesWidget', value: rows }).findComponent(Viewer).props(); + + expect(props).toMatchObject({ descriptorId: 'clientesWidget', source: rows, readOnly: true }); + expect(props.value).toBeUndefined(); + }); + + it('gives any other value to the viewer as its value', () => { + const props = mountWidget({ viewType: 'form', value: { name: 'Ana' } }).findComponent(Viewer).props(); + + expect(props).toMatchObject({ viewType: 'form', value: { name: 'Ana' } }); + expect(props.source).toBeUndefined(); + }); +}); diff --git a/extensions/dashboard/sources/core/pom.xml b/extensions/dashboard/sources/core/pom.xml index 2e2add073..488c1cbe4 100644 --- a/extensions/dashboard/sources/core/pom.xml +++ b/extensions/dashboard/sources/core/pom.xml @@ -23,7 +23,7 @@ tools.dynamia.modules tools.dynamia.modules.dashboard.parent - 26.10.1 + 26.10.2 ../pom.xml @@ -36,12 +36,12 @@ tools.dynamia tools.dynamia.viewers - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.web - 26.10.1 + 26.10.2 diff --git a/extensions/dashboard/sources/pom.xml b/extensions/dashboard/sources/pom.xml index e2e124abd..430ffb623 100644 --- a/extensions/dashboard/sources/pom.xml +++ b/extensions/dashboard/sources/pom.xml @@ -23,7 +23,7 @@ tools.dynamia.modules tools.dynamia.modules.parent - 26.10.1 + 26.10.2 ../../pom.xml diff --git a/extensions/dashboard/sources/zk/pom.xml b/extensions/dashboard/sources/zk/pom.xml index e99fac6e3..f92101580 100644 --- a/extensions/dashboard/sources/zk/pom.xml +++ b/extensions/dashboard/sources/zk/pom.xml @@ -23,7 +23,7 @@ tools.dynamia.modules tools.dynamia.modules.dashboard.parent - 26.10.1 + 26.10.2 ../pom.xml @@ -38,17 +38,17 @@ tools.dynamia.modules tools.dynamia.modules.dashboard.core - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.zk - 26.10.1 + 26.10.2 tools.dynamia.modules tools.dynamia.modules.saas.api - 26.10.1 + 26.10.2 diff --git a/extensions/email-sms/sources/core/pom.xml b/extensions/email-sms/sources/core/pom.xml index cb0fc074a..847ebedd5 100644 --- a/extensions/email-sms/sources/core/pom.xml +++ b/extensions/email-sms/sources/core/pom.xml @@ -23,7 +23,7 @@ tools.dynamia.modules.email.parent tools.dynamia.modules - 26.10.1 + 26.10.2 tools.dynamia.modules.email @@ -50,17 +50,17 @@ tools.dynamia tools.dynamia.domain.jpa - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.crud - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.templates - 26.10.1 + 26.10.2 org.springframework diff --git a/extensions/email-sms/sources/pom.xml b/extensions/email-sms/sources/pom.xml index 623e6b108..d0bfaca5f 100644 --- a/extensions/email-sms/sources/pom.xml +++ b/extensions/email-sms/sources/pom.xml @@ -23,7 +23,7 @@ tools.dynamia.modules tools.dynamia.modules.parent - 26.10.1 + 26.10.2 ../../pom.xml @@ -85,7 +85,7 @@ tools.dynamia.modules tools.dynamia.modules.saas.jpa - 26.10.1 + 26.10.2 diff --git a/extensions/email-sms/sources/ui/pom.xml b/extensions/email-sms/sources/ui/pom.xml index f6b7786bf..5737b321d 100644 --- a/extensions/email-sms/sources/ui/pom.xml +++ b/extensions/email-sms/sources/ui/pom.xml @@ -22,7 +22,7 @@ tools.dynamia.modules.email.parent tools.dynamia.modules - 26.10.1 + 26.10.2 DynamiaModules - Email UI @@ -34,12 +34,12 @@ tools.dynamia tools.dynamia.zk - 26.10.1 + 26.10.2 tools.dynamia.modules tools.dynamia.modules.email - 26.10.1 + 26.10.2 tools.dynamia.zk.addons diff --git a/extensions/entity-files/packages/files-sdk/package.json b/extensions/entity-files/packages/files-sdk/package.json index 8a3763f06..732c44fcc 100644 --- a/extensions/entity-files/packages/files-sdk/package.json +++ b/extensions/entity-files/packages/files-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@dynamia-tools/files-sdk", - "version": "26.10.1", + "version": "26.10.2", "website": "https://dynamia.tools", "description": "TypeScript/JavaScript client SDK for the Dynamia Entity Files extension REST API", "keywords": [ diff --git a/extensions/entity-files/sources/core/pom.xml b/extensions/entity-files/sources/core/pom.xml index ffc86c95f..ff3a111d8 100644 --- a/extensions/entity-files/sources/core/pom.xml +++ b/extensions/entity-files/sources/core/pom.xml @@ -22,7 +22,7 @@ tools.dynamia.modules.entityfiles.parent tools.dynamia.modules - 26.10.1 + 26.10.2 DynamiaModules - EntityFiles - Core tools.dynamia.modules.entityfiles @@ -53,7 +53,7 @@ tools.dynamia tools.dynamia.ui.testing - 26.10.1 + 26.10.2 test @@ -65,20 +65,20 @@ tools.dynamia tools.dynamia.domain.jpa - 26.10.1 + 26.10.2 jar tools.dynamia tools.dynamia.io - 26.10.1 + 26.10.2 jar tools.dynamia tools.dynamia.web - 26.10.1 + 26.10.2 jar diff --git a/extensions/entity-files/sources/pom.xml b/extensions/entity-files/sources/pom.xml index 4e165233e..37bf1041d 100644 --- a/extensions/entity-files/sources/pom.xml +++ b/extensions/entity-files/sources/pom.xml @@ -23,7 +23,7 @@ tools.dynamia.modules tools.dynamia.modules.parent - 26.10.1 + 26.10.2 ../../pom.xml diff --git a/extensions/entity-files/sources/s3/pom.xml b/extensions/entity-files/sources/s3/pom.xml index 6eb90dc44..afdf5812f 100644 --- a/extensions/entity-files/sources/s3/pom.xml +++ b/extensions/entity-files/sources/s3/pom.xml @@ -23,7 +23,7 @@ tools.dynamia.modules tools.dynamia.modules.entityfiles.parent - 26.10.1 + 26.10.2 DynamiaModules - EntityFiles - S3 @@ -49,7 +49,7 @@ tools.dynamia.modules tools.dynamia.modules.entityfiles - 26.10.1 + 26.10.2 software.amazon.awssdk diff --git a/extensions/entity-files/sources/ui/pom.xml b/extensions/entity-files/sources/ui/pom.xml index b0d7c30db..bd0cbd098 100644 --- a/extensions/entity-files/sources/ui/pom.xml +++ b/extensions/entity-files/sources/ui/pom.xml @@ -22,7 +22,7 @@ tools.dynamia.modules.entityfiles.parent tools.dynamia.modules - 26.10.1 + 26.10.2 DynamiaModules - EntityFiles UI tools.dynamia.modules.entityfiles.ui @@ -48,18 +48,18 @@ tools.dynamia.modules tools.dynamia.modules.entityfiles - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.zk - 26.10.1 + 26.10.2 jar tools.dynamia tools.dynamia.ui.testing - 26.10.1 + 26.10.2 test diff --git a/extensions/file-importer/sources/core/pom.xml b/extensions/file-importer/sources/core/pom.xml index 51310dce3..56a2feeba 100644 --- a/extensions/file-importer/sources/core/pom.xml +++ b/extensions/file-importer/sources/core/pom.xml @@ -23,7 +23,7 @@ tools.dynamia.modules.importer.parent tools.dynamia.modules - 26.10.1 + 26.10.2 Dynamia Modules - Importer Core tools.dynamia.modules.importer @@ -56,7 +56,7 @@ tools.dynamia tools.dynamia.reports - 26.10.1 + 26.10.2 diff --git a/extensions/file-importer/sources/pom.xml b/extensions/file-importer/sources/pom.xml index f7be914eb..7062894f8 100644 --- a/extensions/file-importer/sources/pom.xml +++ b/extensions/file-importer/sources/pom.xml @@ -26,7 +26,7 @@ tools.dynamia.modules tools.dynamia.modules.parent - 26.10.1 + 26.10.2 ../../pom.xml diff --git a/extensions/file-importer/sources/ui/pom.xml b/extensions/file-importer/sources/ui/pom.xml index c6661dca5..d95084fc3 100644 --- a/extensions/file-importer/sources/ui/pom.xml +++ b/extensions/file-importer/sources/ui/pom.xml @@ -23,7 +23,7 @@ tools.dynamia.modules.importer.parent tools.dynamia.modules - 26.10.1 + 26.10.2 Dynamia Modules - Importer UI tools.dynamia.modules.importer.ui @@ -55,13 +55,13 @@ tools.dynamia tools.dynamia.zk - 26.10.1 + 26.10.2 tools.dynamia.modules tools.dynamia.modules.importer - 26.10.1 + 26.10.2 diff --git a/extensions/finances/sources/api/pom.xml b/extensions/finances/sources/api/pom.xml index 433ee5540..30547be34 100644 --- a/extensions/finances/sources/api/pom.xml +++ b/extensions/finances/sources/api/pom.xml @@ -26,7 +26,7 @@ tools.dynamia.modules tools.dynamia.modules.finances.parent - 26.10.1 + 26.10.2 Dynamia Modules - Finances API diff --git a/extensions/finances/sources/pom.xml b/extensions/finances/sources/pom.xml index 3a45d04ba..ba8ddd479 100644 --- a/extensions/finances/sources/pom.xml +++ b/extensions/finances/sources/pom.xml @@ -26,7 +26,7 @@ tools.dynamia.modules tools.dynamia.modules.parent - 26.10.1 + 26.10.2 ../../pom.xml diff --git a/extensions/http-functions/packages/http-functions-sdk/package.json b/extensions/http-functions/packages/http-functions-sdk/package.json index abc7f24cd..eacb43bda 100644 --- a/extensions/http-functions/packages/http-functions-sdk/package.json +++ b/extensions/http-functions/packages/http-functions-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@dynamia-tools/http-functions-sdk", - "version": "26.10.1", + "version": "26.10.2", "website": "https://dynamia.tools", "description": "TypeScript/JavaScript client SDK for the Dynamia HTTP Functions extension REST API", "keywords": [ diff --git a/extensions/http-functions/sources/core/pom.xml b/extensions/http-functions/sources/core/pom.xml index 5914e2b89..5fbf549ad 100644 --- a/extensions/http-functions/sources/core/pom.xml +++ b/extensions/http-functions/sources/core/pom.xml @@ -4,7 +4,7 @@ tools.dynamia.modules tools.dynamia.modules.parent - 26.10.1 + 26.10.2 ../../../pom.xml DynamiaModules - Http Functions Core @@ -20,12 +20,12 @@ tools.dynamia.modules tools.dynamia.modules.saas.jpa - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.web - 26.10.1 + 26.10.2 diff --git a/extensions/http-functions/sources/pom.xml b/extensions/http-functions/sources/pom.xml index 90800f87f..79c4632be 100644 --- a/extensions/http-functions/sources/pom.xml +++ b/extensions/http-functions/sources/pom.xml @@ -5,7 +5,7 @@ tools.dynamia.modules tools.dynamia.modules.parent - 26.10.1 + 26.10.2 ../../pom.xml @@ -66,7 +66,7 @@ tools.dynamia.modules tools.dynamia.modules.saas.jpa - 26.10.1 + 26.10.2 diff --git a/extensions/http-functions/sources/ui/pom.xml b/extensions/http-functions/sources/ui/pom.xml index 483607195..08ed4ab39 100644 --- a/extensions/http-functions/sources/ui/pom.xml +++ b/extensions/http-functions/sources/ui/pom.xml @@ -5,7 +5,7 @@ tools.dynamia.modules tools.dynamia.modules.functions.parent - 26.10.1 + 26.10.2 DynamiaModules - Http Functions UI @@ -18,12 +18,12 @@ tools.dynamia tools.dynamia.zk - 26.10.1 + 26.10.2 tools.dynamia.modules tools.dynamia.modules.functions.core - 26.10.1 + 26.10.2 diff --git a/extensions/pom.xml b/extensions/pom.xml index 1b3427ed7..fa36af765 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -6,7 +6,7 @@ tools.dynamia tools.dynamia.parent - 26.10.1 + 26.10.2 ../pom.xml diff --git a/extensions/reports/packages/reports-sdk/package.json b/extensions/reports/packages/reports-sdk/package.json index 2de8c1f36..bdbec4e48 100644 --- a/extensions/reports/packages/reports-sdk/package.json +++ b/extensions/reports/packages/reports-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@dynamia-tools/reports-sdk", - "version": "26.10.1", + "version": "26.10.2", "website": "https://dynamia.tools", "description": "TypeScript/JavaScript client SDK for the Dynamia Reports extension REST API", "keywords": [ diff --git a/extensions/reports/packages/reports-sdk/src/types.ts b/extensions/reports/packages/reports-sdk/src/types.ts index 03f14c80d..77bba6595 100644 --- a/extensions/reports/packages/reports-sdk/src/types.ts +++ b/extensions/reports/packages/reports-sdk/src/types.ts @@ -122,6 +122,10 @@ export interface ReportColumn { format?: string | null; width?: string | null; upperCase: boolean; + /** CSS class name(s) the designer chose for every cell of the column, e.g. `green lighten-5`; absent when not set */ + cellStyle?: string; + /** CSS class name(s) the designer chose for the header of the column; absent when not set */ + columnStyle?: string; } /** A filter a report accepts. */ diff --git a/extensions/reports/packages/reports-vue/package.json b/extensions/reports/packages/reports-vue/package.json index 82208b8ac..94ba85327 100644 --- a/extensions/reports/packages/reports-vue/package.json +++ b/extensions/reports/packages/reports-vue/package.json @@ -1,6 +1,6 @@ { "name": "@dynamia-tools/reports-vue", - "version": "26.10.1", + "version": "26.10.2", "website": "https://dynamia.tools", "description": "Vue 3 components and composables to list, run, export and design Dynamia reports without ZK", "keywords": [ diff --git a/extensions/reports/sources/api/pom.xml b/extensions/reports/sources/api/pom.xml index e1cb0664f..e921f40b0 100644 --- a/extensions/reports/sources/api/pom.xml +++ b/extensions/reports/sources/api/pom.xml @@ -23,7 +23,7 @@ tools.dynamia.modules tools.dynamia.modules.reports.parent - 26.10.1 + 26.10.2 DynamiaModules - Reports API diff --git a/extensions/reports/sources/api/src/main/java/tools/dynamia/modules/reports/api/v2/ReportColumn.java b/extensions/reports/sources/api/src/main/java/tools/dynamia/modules/reports/api/v2/ReportColumn.java index bb2d0ba39..eabe048fb 100644 --- a/extensions/reports/sources/api/src/main/java/tools/dynamia/modules/reports/api/v2/ReportColumn.java +++ b/extensions/reports/sources/api/src/main/java/tools/dynamia/modules/reports/api/v2/ReportColumn.java @@ -1,5 +1,7 @@ package tools.dynamia.modules.reports.api.v2; +import com.fasterxml.jackson.annotation.JsonInclude; + /** * A column of a report result. * @@ -10,7 +12,19 @@ * @param format optional display format pattern * @param width optional width, for example {@code 120px} * @param upperCase true if the text should be shown in upper case + * @param cellStyle optional CSS class name(s) the designer chose for every cell of the column (for example {@code green lighten-5}); + * omitted from the JSON when not set + * @param columnStyle optional CSS class name(s) the designer chose for the header of the column; omitted from the JSON when not set */ public record ReportColumn(String name, String label, String dataType, String align, String format, String width, - boolean upperCase) { + boolean upperCase, + @JsonInclude(JsonInclude.Include.NON_NULL) String cellStyle, + @JsonInclude(JsonInclude.Include.NON_NULL) String columnStyle) { + + /** + * A column without designer styles. + */ + public ReportColumn(String name, String label, String dataType, String align, String format, String width, boolean upperCase) { + this(name, label, dataType, align, format, width, upperCase, null, null); + } } diff --git a/extensions/reports/sources/core/pom.xml b/extensions/reports/sources/core/pom.xml index 78f97f8f6..ce5382aac 100644 --- a/extensions/reports/sources/core/pom.xml +++ b/extensions/reports/sources/core/pom.xml @@ -23,7 +23,7 @@ tools.dynamia.modules tools.dynamia.modules.reports.parent - 26.10.1 + 26.10.2 DynamiaModules - Reports Core @@ -50,17 +50,17 @@ tools.dynamia.modules tools.dynamia.modules.reports.api - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.domain.jpa - 26.10.1 + 26.10.2 tools.dynamia.modules tools.dynamia.modules.saas.jpa - 26.10.1 + 26.10.2 org.springframework @@ -69,23 +69,23 @@ tools.dynamia tools.dynamia.reports - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.templates - 26.10.1 + 26.10.2 compile tools.dynamia tools.dynamia.web - 26.10.1 + 26.10.2 tools.dynamia.modules tools.dynamia.modules.dashboard.core - 26.10.1 + 26.10.2 com.github.librepdf diff --git a/extensions/reports/sources/core/src/main/java/tools/dynamia/modules/reports/core/api/ReportColumns.java b/extensions/reports/sources/core/src/main/java/tools/dynamia/modules/reports/core/api/ReportColumns.java index f10bfef02..3944e7c79 100644 --- a/extensions/reports/sources/core/src/main/java/tools/dynamia/modules/reports/core/api/ReportColumns.java +++ b/extensions/reports/sources/core/src/main/java/tools/dynamia/modules/reports/core/api/ReportColumns.java @@ -80,7 +80,11 @@ private static ReportColumn of(ReportField field) { DataType type = field.getDataType() != null ? field.getDataType() : DataType.TEXT; TextAlign align = field.getAlign() != null ? field.getAlign() : TextAlign.LEFT; return new ReportColumn(field.getName(), field.getLabel(), type.name(), align.name(), field.getFormat(), - field.getWidth(), field.isUpperCase()); + field.getWidth(), field.isUpperCase(), blankToNull(field.getCellStyle()), blankToNull(field.getColumnStyle())); + } + + private static String blankToNull(String style) { + return style == null || style.isBlank() ? null : style.trim(); } private static DataType infer(String name, ReportData data) { diff --git a/extensions/reports/sources/core/src/test/java/tools/dynamia/modules/reports/core/api/ReportColumnStylesTest.java b/extensions/reports/sources/core/src/test/java/tools/dynamia/modules/reports/core/api/ReportColumnStylesTest.java new file mode 100644 index 000000000..b150d8946 --- /dev/null +++ b/extensions/reports/sources/core/src/test/java/tools/dynamia/modules/reports/core/api/ReportColumnStylesTest.java @@ -0,0 +1,85 @@ +package tools.dynamia.modules.reports.core.api; + +import org.junit.jupiter.api.Test; +import tools.dynamia.modules.reports.core.domain.Report; +import tools.dynamia.modules.reports.core.domain.ReportField; +import tools.jackson.databind.json.JsonMapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The styles a designer chooses for a field reach the API columns, so a front end other than ZK can honor them. + */ +class ReportColumnStylesTest { + + private static ReportField field(Report report, String name, int order, String cellStyle, String columnStyle) { + var field = new ReportField(); + field.setReport(report); + field.setName(name); + field.setLabel(name); + field.setOrder(order); + field.setCellStyle(cellStyle); + field.setColumnStyle(columnStyle); + report.getFields().add(field); + return field; + } + + private static Report report() { + var report = new Report(); + report.setName("Styles"); + report.setAutofields(false); + return report; + } + + @Test + void declaredColumnsCarryTheDesignersStyles() { + var report = report(); + field(report, "total", 1, "green lighten-5", "bold"); + + var column = ReportColumns.declared(report).get(0); + + assertEquals("green lighten-5", column.cellStyle()); + assertEquals("bold", column.columnStyle()); + } + + @Test + void blankStylesAreNotSent() { + var report = report(); + field(report, "a", 1, " ", ""); + field(report, "b", 2, null, null); + + var columns = ReportColumns.declared(report); + + assertNull(columns.get(0).cellStyle()); + assertNull(columns.get(0).columnStyle()); + assertNull(columns.get(1).cellStyle()); + } + + @Test + void theJsonOmitsTheStyleKeysWhenThereAreNoneAndKeepsTheRest() { + var report = report(); + field(report, "a", 1, null, null); + field(report, "b", 2, "red lighten-4", null); + var mapper = JsonMapper.builder().build(); + + var plain = mapper.writeValueAsString(ReportColumns.declared(report).get(0)); + var styled = mapper.writeValueAsString(ReportColumns.declared(report).get(1)); + + assertFalse(plain.contains("cellStyle")); + assertFalse(plain.contains("columnStyle")); + assertTrue(plain.contains("\"format\":null"), "the existing keys keep being sent as before: " + plain); + assertTrue(styled.contains("\"cellStyle\":\"red lighten-4\"")); + assertFalse(styled.contains("columnStyle")); + } + + @Test + void theOldConstructorStillWorks() { + var column = new tools.dynamia.modules.reports.api.v2.ReportColumn("a", "A", "TEXT", "LEFT", null, null, false); + + assertNull(column.cellStyle()); + assertNull(column.columnStyle()); + } +} diff --git a/extensions/reports/sources/pom.xml b/extensions/reports/sources/pom.xml index 18f8072d8..fd76f5fda 100644 --- a/extensions/reports/sources/pom.xml +++ b/extensions/reports/sources/pom.xml @@ -23,7 +23,7 @@ tools.dynamia.modules tools.dynamia.modules.parent - 26.10.1 + 26.10.2 ../../pom.xml diff --git a/extensions/reports/sources/ui/pom.xml b/extensions/reports/sources/ui/pom.xml index b674f4189..444f3a7d9 100644 --- a/extensions/reports/sources/ui/pom.xml +++ b/extensions/reports/sources/ui/pom.xml @@ -23,7 +23,7 @@ tools.dynamia.modules tools.dynamia.modules.reports.parent - 26.10.1 + 26.10.2 DynamiaModules - Reports UI @@ -49,12 +49,12 @@ tools.dynamia.modules tools.dynamia.modules.reports.core - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.zk - 26.10.1 + 26.10.2 io.swagger.core.v3 @@ -65,7 +65,7 @@ tools.dynamia tools.dynamia.ui.testing - 26.10.1 + 26.10.2 test diff --git a/extensions/saas/packages/saas-sdk/package.json b/extensions/saas/packages/saas-sdk/package.json index f0e96dc93..d6bc4f65a 100644 --- a/extensions/saas/packages/saas-sdk/package.json +++ b/extensions/saas/packages/saas-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@dynamia-tools/saas-sdk", - "version": "26.10.1", + "version": "26.10.2", "website": "https://dynamia.tools", "description": "TypeScript/JavaScript client SDK for the Dynamia SaaS extension REST API", "keywords": [ diff --git a/extensions/saas/sources/api/pom.xml b/extensions/saas/sources/api/pom.xml index 6af4e60c1..fa92b380c 100644 --- a/extensions/saas/sources/api/pom.xml +++ b/extensions/saas/sources/api/pom.xml @@ -26,7 +26,7 @@ tools.dynamia.modules tools.dynamia.modules.saas.parent - 26.10.1 + 26.10.2 @@ -55,7 +55,7 @@ tools.dynamia tools.dynamia.actions - 26.10.1 + 26.10.2 org.springframework.boot diff --git a/extensions/saas/sources/core/pom.xml b/extensions/saas/sources/core/pom.xml index 474aba04c..d1f5ff3d2 100644 --- a/extensions/saas/sources/core/pom.xml +++ b/extensions/saas/sources/core/pom.xml @@ -22,7 +22,7 @@ tools.dynamia.modules tools.dynamia.modules.saas.parent - 26.10.1 + 26.10.2 DynamiaModules - SaaS Core @@ -49,18 +49,18 @@ tools.dynamia.modules tools.dynamia.modules.saas.api - 26.10.1 + 26.10.2 tools.dynamia.modules tools.dynamia.modules.saas.jpa - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.integration - 26.10.1 + 26.10.2 @@ -96,12 +96,12 @@ tools.dynamia tools.dynamia.web - 26.10.1 + 26.10.2 tools.dynamia.modules tools.dynamia.modules.entityfiles - 26.10.1 + 26.10.2 org.hibernate.orm diff --git a/extensions/saas/sources/jpa/pom.xml b/extensions/saas/sources/jpa/pom.xml index 1d2282dfe..cc01f5698 100644 --- a/extensions/saas/sources/jpa/pom.xml +++ b/extensions/saas/sources/jpa/pom.xml @@ -24,7 +24,7 @@ tools.dynamia.modules.saas.parent tools.dynamia.modules - 26.10.1 + 26.10.2 DynamiaModules - SaaS JPA @@ -35,12 +35,12 @@ tools.dynamia.modules tools.dynamia.modules.saas.api - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.domain.jpa - 26.10.1 + 26.10.2 diff --git a/extensions/saas/sources/migration/pom.xml b/extensions/saas/sources/migration/pom.xml index 84d735b81..504b656da 100644 --- a/extensions/saas/sources/migration/pom.xml +++ b/extensions/saas/sources/migration/pom.xml @@ -24,7 +24,7 @@ tools.dynamia.modules tools.dynamia.modules.saas.parent - 26.10.1 + 26.10.2 DynamiaModules - SaaS Migration @@ -57,42 +57,42 @@ tools.dynamia.modules tools.dynamia.modules.saas - 26.10.1 + 26.10.2 tools.dynamia.modules tools.dynamia.modules.saas.api - 26.10.1 + 26.10.2 tools.dynamia.modules tools.dynamia.modules.saas.jpa - 26.10.1 + 26.10.2 tools.dynamia.modules tools.dynamia.modules.entityfiles - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.integration - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.domain.jpa - 26.10.1 + 26.10.2 diff --git a/extensions/saas/sources/pom.xml b/extensions/saas/sources/pom.xml index e933d4cc0..bc5a1270a 100644 --- a/extensions/saas/sources/pom.xml +++ b/extensions/saas/sources/pom.xml @@ -23,7 +23,7 @@ tools.dynamia.modules tools.dynamia.modules.parent - 26.10.1 + 26.10.2 ../../pom.xml diff --git a/extensions/saas/sources/remote/pom.xml b/extensions/saas/sources/remote/pom.xml index 612acdbb6..fec8c5da0 100644 --- a/extensions/saas/sources/remote/pom.xml +++ b/extensions/saas/sources/remote/pom.xml @@ -25,7 +25,7 @@ tools.dynamia.modules.saas.parent tools.dynamia.modules - 26.10.1 + 26.10.2 @@ -38,7 +38,7 @@ tools.dynamia.modules tools.dynamia.modules.saas.jpa - 26.10.1 + 26.10.2 diff --git a/extensions/saas/sources/ui/pom.xml b/extensions/saas/sources/ui/pom.xml index c82088ac4..024950cca 100644 --- a/extensions/saas/sources/ui/pom.xml +++ b/extensions/saas/sources/ui/pom.xml @@ -22,7 +22,7 @@ tools.dynamia.modules tools.dynamia.modules.saas.parent - 26.10.1 + 26.10.2 DynamiaModules - SaaS UI tools.dynamia.modules.saas.ui @@ -54,12 +54,12 @@ tools.dynamia tools.dynamia.zk - 26.10.1 + 26.10.2 tools.dynamia.modules tools.dynamia.modules.saas - 26.10.1 + 26.10.2 @@ -70,12 +70,12 @@ tools.dynamia.modules tools.dynamia.modules.entityfiles.ui - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.ui.testing - 26.10.1 + 26.10.2 test diff --git a/extensions/security/sources/core/pom.xml b/extensions/security/sources/core/pom.xml index c2522e97d..4381ae4c6 100644 --- a/extensions/security/sources/core/pom.xml +++ b/extensions/security/sources/core/pom.xml @@ -17,7 +17,7 @@ tools.dynamia.modules tools.dynamia.modules.security.parent - 26.10.1 + 26.10.2 4.0.0 @@ -30,41 +30,41 @@ tools.dynamia tools.dynamia.ui - 26.10.1 + 26.10.2 tools.dynamia.modules tools.dynamia.modules.saas.api - 26.10.1 + 26.10.2 tools.dynamia.modules tools.dynamia.modules.entityfiles - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.domain.jpa - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.domain - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.integration - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.web - 26.10.1 + 26.10.2 diff --git a/extensions/security/sources/pom.xml b/extensions/security/sources/pom.xml index e9b10a112..eb800cb10 100644 --- a/extensions/security/sources/pom.xml +++ b/extensions/security/sources/pom.xml @@ -19,7 +19,7 @@ tools.dynamia.modules tools.dynamia.modules.parent - 26.10.1 + 26.10.2 ../../pom.xml diff --git a/extensions/security/sources/ui/pom.xml b/extensions/security/sources/ui/pom.xml index 0e446c9bd..259fbe02b 100644 --- a/extensions/security/sources/ui/pom.xml +++ b/extensions/security/sources/ui/pom.xml @@ -17,7 +17,7 @@ tools.dynamia.modules tools.dynamia.modules.security.parent - 26.10.1 + 26.10.2 DynamiaModules - Security UI @@ -44,18 +44,18 @@ tools.dynamia.modules tools.dynamia.modules.security - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.zk - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.app - 26.10.1 + 26.10.2 diff --git a/platform/app/pom.xml b/platform/app/pom.xml index 5d0770b90..da70f9e29 100644 --- a/platform/app/pom.xml +++ b/platform/app/pom.xml @@ -23,7 +23,7 @@ tools.dynamia tools.dynamia.parent - 26.10.1 + 26.10.2 ../../pom.xml @@ -82,58 +82,58 @@ tools.dynamia tools.dynamia.actions - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.commons - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.crud - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.domain - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.integration - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.io - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.navigation - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.reports - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.templates - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.viewers - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.web - 26.10.1 + 26.10.2 @@ -213,7 +213,7 @@ tools.dynamia tools.dynamia.domain.jpa - 26.10.1 + 26.10.2 test diff --git a/platform/app/src/main/java/tools/dynamia/app/controllers/cache/ContentEtagMetadataHttpCachePolicy.java b/platform/app/src/main/java/tools/dynamia/app/controllers/cache/ContentEtagMetadataHttpCachePolicy.java new file mode 100644 index 000000000..309802ece --- /dev/null +++ b/platform/app/src/main/java/tools/dynamia/app/controllers/cache/ContentEtagMetadataHttpCachePolicy.java @@ -0,0 +1,36 @@ +package tools.dynamia.app.controllers.cache; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.Locale; + +/** + * Included policy, enabled with {@code dynamia.app.metadata.http-cache=etag}: a strong ETag derived from the content + * and the locale, {@code Cache-Control: private, no-cache} and {@code Vary: Accept-Language}. + *

+ * {@code no-cache} makes the client revalidate on every use, which costs a {@code 304} with no body instead of the + * whole descriptor, and never serves a stale one after a deployment or a descriptor hot-reload: the ETag is the hash of + * what would be sent, so it changes with it. + */ +public class ContentEtagMetadataHttpCachePolicy implements MetadataHttpCachePolicy { + + @Override + public MetadataCacheDirectives directives(String path, Locale locale, byte[] content) { + MessageDigest digest = sha256(); + digest.update(locale.toLanguageTag().getBytes(StandardCharsets.UTF_8)); + digest.update((byte) 0); + digest.update(content); + String eTag = "\"" + HexFormat.of().formatHex(digest.digest(), 0, 16) + "\""; + return new MetadataCacheDirectives(eTag, "private, no-cache", "Accept-Language"); + } + + private static MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/platform/app/src/main/java/tools/dynamia/app/controllers/cache/MetadataCacheDirectives.java b/platform/app/src/main/java/tools/dynamia/app/controllers/cache/MetadataCacheDirectives.java new file mode 100644 index 000000000..072aea6b1 --- /dev/null +++ b/platform/app/src/main/java/tools/dynamia/app/controllers/cache/MetadataCacheDirectives.java @@ -0,0 +1,12 @@ +package tools.dynamia.app.controllers.cache; + +/** + * HTTP cache headers for one metadata response. + * + * @param eTag the validator, quoted ({@code "abc"}); when present a matching {@code If-None-Match} is answered with + * {@code 304 Not Modified} and no body. May be {@code null}. + * @param cacheControl the {@code Cache-Control} value, or {@code null} to not set it + * @param vary the {@code Vary} header names, comma separated, or {@code null} to not set it + */ +public record MetadataCacheDirectives(String eTag, String cacheControl, String vary) { +} diff --git a/platform/app/src/main/java/tools/dynamia/app/controllers/cache/MetadataHttpCacheAdvice.java b/platform/app/src/main/java/tools/dynamia/app/controllers/cache/MetadataHttpCacheAdvice.java new file mode 100644 index 000000000..39e6a6731 --- /dev/null +++ b/platform/app/src/main/java/tools/dynamia/app/controllers/cache/MetadataHttpCacheAdvice.java @@ -0,0 +1,102 @@ +package tools.dynamia.app.controllers.cache; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.core.MethodParameter; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; +import tools.dynamia.app.controllers.ApplicationMetadataController; +import tools.dynamia.commons.Messages; +import tools.dynamia.commons.StringPojoParser; +import tools.jackson.databind.json.JsonMapper; + +import java.util.List; + +/** + * Applies the {@link MetadataHttpCachePolicy} to the read-only metadata endpoints of + * {@link ApplicationMetadataController}: view descriptors, entity metadata and navigation. Nothing else is touched + * (actions, entity data and executions are never cached). + *

+ * The policy is the application's bean if there is one; otherwise {@link ContentEtagMetadataHttpCachePolicy} when + * {@code dynamia.app.metadata.http-cache=etag}; otherwise none, and responses are exactly as before. + */ +@RestControllerAdvice(assignableTypes = ApplicationMetadataController.class) +public class MetadataHttpCacheAdvice implements ResponseBodyAdvice { + + /** Property that enables the included ETag policy. */ + public static final String PROPERTY = "dynamia.app.metadata.http-cache"; + + private static final List CACHEABLE = List.of( + "/views/*", "/navigation", "/entities", "/entities/by-path", "/entities/*", "/entities/*/views", "/entities/*/views/*"); + + private final AntPathMatcher matcher = new AntPathMatcher(); + private final MetadataHttpCachePolicy policy; + private final JsonMapper mapper; + + public MetadataHttpCacheAdvice(ObjectProvider custom, ObjectProvider mapper, Environment environment) { + MetadataHttpCachePolicy configured = custom.getIfAvailable(); + if (configured == null && "etag".equalsIgnoreCase(environment.getProperty(PROPERTY))) { + configured = new ContentEtagMetadataHttpCachePolicy(); + } + this.policy = configured; + this.mapper = mapper.getIfAvailable(StringPojoParser::createJsonMapper); + } + + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + return policy != null; + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, + Class> selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + if (policy == null || body == null || request.getMethod() != HttpMethod.GET + || !(request instanceof ServletServerHttpRequest servletRequest) + || !(response instanceof ServletServerHttpResponse servletResponse)) { + return body; + } + String path = relativePath(servletRequest); + if (!isCacheable(path)) { + return body; + } + + MetadataCacheDirectives directives = policy.directives(path, Messages.getDefaultLocale(), mapper.writeValueAsBytes(body)); + if (directives == null) { + return body; + } + if (directives.cacheControl() != null) { + response.getHeaders().set("Cache-Control", directives.cacheControl()); + } + if (directives.vary() != null) { + response.getHeaders().set("Vary", directives.vary()); + } + if (directives.eTag() != null) { + var webRequest = new ServletWebRequest(servletRequest.getServletRequest(), servletResponse.getServletResponse()); + if (webRequest.checkNotModified(directives.eTag())) { + return null; // 304: the validator matched, no body + } + } + return body; + } + + private static String relativePath(ServletServerHttpRequest request) { + String uri = request.getServletRequest().getRequestURI(); + String context = request.getServletRequest().getContextPath(); + String path = context != null && !context.isEmpty() && uri.startsWith(context) ? uri.substring(context.length()) : uri; + return path.startsWith(ApplicationMetadataController.PATH) ? path.substring(ApplicationMetadataController.PATH.length()) : path; + } + + private boolean isCacheable(String path) { + return CACHEABLE.stream().anyMatch(pattern -> matcher.match(pattern, path)); + } +} diff --git a/platform/app/src/main/java/tools/dynamia/app/controllers/cache/MetadataHttpCachePolicy.java b/platform/app/src/main/java/tools/dynamia/app/controllers/cache/MetadataHttpCachePolicy.java new file mode 100644 index 000000000..65dfe130e --- /dev/null +++ b/platform/app/src/main/java/tools/dynamia/app/controllers/cache/MetadataHttpCachePolicy.java @@ -0,0 +1,32 @@ +package tools.dynamia.app.controllers.cache; + +import java.util.Locale; + +/** + * Decides how the clients may cache what {@code /apps/api/metadata/**} serves (view descriptors, entity metadata and + * navigation). Those responses are practically static, but not always: texts depend on the request locale and an + * application may build descriptors dynamically or vary them per user. The framework therefore does not impose a + * policy: the application registers a bean of this type, or enables the included one with + * {@code dynamia.app.metadata.http-cache=etag}. Without either, nothing is added to the responses. + * + *
{@code
+ * @Component
+ * class VersionedMetadataCache implements MetadataHttpCachePolicy {
+ *     public MetadataCacheDirectives directives(String path, Locale locale, byte[] content) {
+ *         return new MetadataCacheDirectives("\"" + version + "-" + locale + "\"", "private, no-cache", "Accept-Language");
+ *     }
+ * }
+ * }
+ */ +public interface MetadataHttpCachePolicy { + + /** + * Cache directives for one response. + * + * @param path the request path relative to {@code /apps/api/metadata}, e.g. {@code /views/clientesWidget} + * @param locale the locale the response was produced with ({@code Messages.getDefaultLocale()}) + * @param content the serialized JSON body + * @return the directives, or {@code null} to leave the response untouched + */ + MetadataCacheDirectives directives(String path, Locale locale, byte[] content); +} diff --git a/platform/app/src/test/java/tools/dynamia/app/controllers/cache/MetadataHttpCacheAdviceTest.java b/platform/app/src/test/java/tools/dynamia/app/controllers/cache/MetadataHttpCacheAdviceTest.java new file mode 100644 index 000000000..f97c5e370 --- /dev/null +++ b/platform/app/src/test/java/tools/dynamia/app/controllers/cache/MetadataHttpCacheAdviceTest.java @@ -0,0 +1,156 @@ +package tools.dynamia.app.controllers.cache; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import tools.jackson.databind.json.JsonMapper; + +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies the HTTP cache of the metadata endpoints: nothing changes by default, the included ETag policy validates by + * content, and only the read-only metadata endpoints take part. + */ +class MetadataHttpCacheAdviceTest { + + private static final String PATH = "/apps/api/metadata"; + private final Map descriptor = Map.of("id", "clientesWidget", "view", "table"); + + private MetadataHttpCacheAdvice advice(MetadataHttpCachePolicy custom, String property) { + var factory = new DefaultListableBeanFactory(); + if (custom != null) { + factory.registerSingleton("policy", custom); + } + var environment = new MockEnvironment(); + if (property != null) { + environment.setProperty(MetadataHttpCacheAdvice.PROPERTY, property); + } + return new MetadataHttpCacheAdvice(factory.getBeanProvider(MetadataHttpCachePolicy.class), factory.getBeanProvider(JsonMapper.class), environment); + } + + private record Exchange(MockHttpServletRequest request, MockHttpServletResponse response, Object body) { + } + + private Exchange get(MetadataHttpCacheAdvice advice, String method, String path, String ifNoneMatch, Object body) { + var request = new MockHttpServletRequest(method, PATH + path); + if (ifNoneMatch != null) { + request.addHeader("If-None-Match", ifNoneMatch); + } + var response = new MockHttpServletResponse(); + Object result = advice.beforeBodyWrite(body, null, MediaType.APPLICATION_JSON, null, + new ServletServerHttpRequest(request), new ServletServerHttpResponse(response)); + return new Exchange(request, response, result); + } + + @Test + void doesNothingByDefault() { + var advice = advice(null, null); + + assertFalse(advice.supports(null, null)); + var exchange = get(advice, "GET", "/views/clientesWidget", null, descriptor); + assertSame(descriptor, exchange.body()); + assertNull(exchange.response().getHeader("ETag")); + assertNull(exchange.response().getHeader("Cache-Control")); + } + + @Test + void etagPropertyEnablesContentValidation() { + var advice = advice(null, "etag"); + + assertTrue(advice.supports(null, null)); + var exchange = get(advice, "GET", "/views/clientesWidget", null, descriptor); + + assertSame(descriptor, exchange.body()); + assertNotNull(exchange.response().getHeader("ETag")); + assertEquals("private, no-cache", exchange.response().getHeader("Cache-Control")); + assertEquals("Accept-Language", exchange.response().getHeader("Vary")); + assertEquals(200, exchange.response().getStatus()); + } + + @Test + void aMatchingIfNoneMatchAnswers304WithNoBody() { + var advice = advice(null, "etag"); + String eTag = get(advice, "GET", "/views/clientesWidget", null, descriptor).response().getHeader("ETag"); + + var exchange = get(advice, "GET", "/views/clientesWidget", eTag, descriptor); + + assertNull(exchange.body()); + assertEquals(HttpStatus.NOT_MODIFIED.value(), exchange.response().getStatus()); + } + + @Test + void aChangedDescriptorGetsAnotherEtagAndAStaleValidatorGetsTheBody() { + var advice = advice(null, "etag"); + String old = get(advice, "GET", "/views/clientesWidget", null, descriptor).response().getHeader("ETag"); + var changed = Map.of("id", "clientesWidget", "view", "table", "fields", List.of("alias")); + + var exchange = get(advice, "GET", "/views/clientesWidget", old, changed); + + assertSame(changed, exchange.body()); + assertEquals(200, exchange.response().getStatus()); + assertNotEquals(old, exchange.response().getHeader("ETag")); + } + + @Test + void theLocaleIsPartOfTheValidator() { + var policy = new ContentEtagMetadataHttpCachePolicy(); + byte[] content = "{}".getBytes(); + + var es = policy.directives("/views/x", Locale.forLanguageTag("es-CO"), content).eTag(); + var en = policy.directives("/views/x", Locale.ENGLISH, content).eTag(); + + assertNotEquals(es, en); + assertEquals(es, policy.directives("/views/x", Locale.forLanguageTag("es-CO"), content).eTag()); + } + + @Test + void onlyTheReadOnlyMetadataEndpointsTakePart() { + var advice = advice(null, "etag"); + + for (String path : List.of("/views/a", "/navigation", "/entities", "/entities/Book", "/entities/by-path", "/entities/Book/views", "/entities/Book/views/table")) { + assertNotNull(get(advice, "GET", path, null, descriptor).response().getHeader("ETag"), path); + } + for (String path : List.of("", "/actions", "/entities/ref/Book/1", "/entities/ref/Book/search", "/entities/Book/action/save")) { + assertNull(get(advice, "GET", path, null, descriptor).response().getHeader("ETag"), path); + } + assertNull(get(advice, "POST", "/views/a", null, descriptor).response().getHeader("ETag")); + } + + @Test + void anApplicationPolicyReplacesTheIncludedOne() { + MetadataHttpCachePolicy custom = (path, locale, content) -> new MetadataCacheDirectives("\"v1\"", "max-age=60", null); + var advice = advice(custom, "etag"); + + var exchange = get(advice, "GET", "/views/a", null, descriptor); + + assertEquals("\"v1\"", exchange.response().getHeader("ETag")); + assertEquals("max-age=60", exchange.response().getHeader("Cache-Control")); + assertNull(exchange.response().getHeader("Vary")); + } + + @Test + void aPolicyThatReturnsNothingLeavesTheResponseUntouched() { + var advice = advice((path, locale, content) -> null, null); + + var exchange = get(advice, "GET", "/views/a", null, descriptor); + + assertSame(descriptor, exchange.body()); + assertNull(exchange.response().getHeader("ETag")); + } +} diff --git a/platform/contract/ui-contract-generator/pom.xml b/platform/contract/ui-contract-generator/pom.xml index 4fb634d56..4ffa79915 100644 --- a/platform/contract/ui-contract-generator/pom.xml +++ b/platform/contract/ui-contract-generator/pom.xml @@ -23,7 +23,7 @@ tools.dynamia tools.dynamia.parent - 26.10.1 + 26.10.2 ../../../pom.xml @@ -64,12 +64,12 @@ tools.dynamia tools.dynamia.actions - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.ui - 26.10.1 + 26.10.2 com.fasterxml.jackson.core diff --git a/platform/core/actions/pom.xml b/platform/core/actions/pom.xml index 913fa446c..5e5fa99c1 100644 --- a/platform/core/actions/pom.xml +++ b/platform/core/actions/pom.xml @@ -23,7 +23,7 @@ tools.dynamia tools.dynamia.parent - 26.10.1 + 26.10.2 ../../../pom.xml @@ -65,19 +65,19 @@ tools.dynamia tools.dynamia.integration - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.commons - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.ui - 26.10.1 + 26.10.2 diff --git a/platform/core/commons/pom.xml b/platform/core/commons/pom.xml index d81d8871f..1b7855365 100644 --- a/platform/core/commons/pom.xml +++ b/platform/core/commons/pom.xml @@ -25,7 +25,7 @@ tools.dynamia tools.dynamia.parent - 26.10.1 + 26.10.2 ../../../pom.xml DynamiaTools - Commons diff --git a/platform/core/crud/pom.xml b/platform/core/crud/pom.xml index a05a46802..f40a55691 100644 --- a/platform/core/crud/pom.xml +++ b/platform/core/crud/pom.xml @@ -23,7 +23,7 @@ tools.dynamia tools.dynamia.parent - 26.10.1 + 26.10.2 ../../../pom.xml @@ -62,23 +62,23 @@ tools.dynamia tools.dynamia.actions - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.viewers - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.navigation - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.domain.jpa - 26.10.1 + 26.10.2 test - + + + - + + + + @@ -155,10 +162,15 @@ const { viewer, loading, error } = useViewer({ |------|------|-------------| | `viewType` | `string` | View type name: `'form'`, `'table'`, `'crud'`, `'tree'`, `'kanban'`, … | | `beanClass` | `string` | Entity id (class simple name) | -| `descriptor` | `ViewDescriptor` | Pre-loaded descriptor (skips backend fetch) | +| `descriptor` | `ViewDescriptor \| ViewDescriptorInput` | Pre-loaded descriptor (skips backend fetch). A hand-written object is completed with defaults: only `view` is required, `fields` may be names, partial fields or a record keyed by field name (the YAML shape) — see `normalizeViewDescriptor` in `@dynamia-tools/ui-core` | | `descriptorId` | `string` | Descriptor ID to fetch from backend | +| `client` | `DynamiaClient` | Client used to fetch descriptors; defaults to the one provided to `DynamiaVue` | +| `source` | `unknown` | Data source of the view (the rows of a table); changes are applied to the live view | +| `value` | `unknown` | Primary value of the view (the object of a form); changes are applied to the live view | | `readOnly` | `boolean` | Propagates to the inner view | +The viewer is rebuilt when `descriptor` (by content), `descriptorId`, `viewType`, `beanClass` or `client` change. + **Events:** | Event | Payload | Description | diff --git a/platform/packages/vue/package.json b/platform/packages/vue/package.json index c5f92c58d..f923f6238 100644 --- a/platform/packages/vue/package.json +++ b/platform/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@dynamia-tools/vue", - "version": "26.10.1", + "version": "26.10.2", "description": "Vue 3 adapter for Dynamia Platform UI", "keywords": [ "dynamia", @@ -60,6 +60,8 @@ "@dynamia-tools/ui-core": "workspace:*", "@types/node": "^26.4.1", "@vitejs/plugin-vue": "^6.0.8", + "@vue/test-utils": "^2.5.1", + "happy-dom": "^20.14.5", "typescript": "^6.0.3", "vite": "^8.2.2", "vite-plugin-dts": "^5.1.0", diff --git a/platform/packages/vue/src/components/Table.vue b/platform/packages/vue/src/components/Table.vue index dd3fda5b2..1c21ab6ee 100644 --- a/platform/packages/vue/src/components/Table.vue +++ b/platform/packages/vue/src/components/Table.vue @@ -41,7 +41,7 @@ :model-value="(row as Record)[col.name]" read-only /> - + @@ -65,9 +65,9 @@ diff --git a/platform/packages/vue/src/components/Viewer.test.ts b/platform/packages/vue/src/components/Viewer.test.ts new file mode 100644 index 000000000..6be3f21b0 --- /dev/null +++ b/platform/packages/vue/src/components/Viewer.test.ts @@ -0,0 +1,130 @@ +// @vitest-environment happy-dom +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { defineComponent, h, ref } from 'vue'; +import { flushPromises, mount } from '@vue/test-utils'; +import type { DynamiaClient, ViewDescriptor } from '@dynamia-tools/sdk'; +import { View, ViewRendererRegistry } from '@dynamia-tools/ui-core'; +import type { ViewType } from '@dynamia-tools/ui-core'; +import Viewer from './Viewer.vue'; +import { DYNAMIA_CLIENT_KEY } from '../composables/useDynamiaClient.js'; +import { VueTableRenderer } from '../renderers/VueTableRenderer.js'; +import { VueTableView } from '../views/VueTableView.js'; + +const TABLE: ViewType = { name: 'table' }; + +class ProbeView extends View { + // reactive, like the rows of VueTableView + rows = ref([]); + constructor(descriptor: ViewDescriptor) { super(TABLE, descriptor, null); } + async initialize(): Promise {} + validate(): boolean { return true; } + override setSource(source: unknown): void { this.rows.value = (source as unknown[]) ?? []; } + override getSource(): unknown { return this.rows.value; } +} + +// The probe component shows the descriptor id and the rows of the view it is given. +const Probe = defineComponent({ + props: { view: { type: Object, required: true }, readOnly: Boolean }, + setup: (props) => () => h('div', { 'data-testid': 'probe' }, `${(props.view as ProbeView).descriptor.id}:${((props.view as ProbeView).getSource() as unknown[]).length}`), +}); + +const widget: ViewDescriptor = { id: 'clientesWidget', beanClass: '', view: 'table', params: {}, fields: [] }; +const client = (getView = vi.fn().mockResolvedValue(widget)) => ({ metadata: { getView } }) as unknown as DynamiaClient; + +beforeEach(() => { + ViewRendererRegistry.registerViewFactory(TABLE, (descriptor) => new ProbeView(descriptor)); + ViewRendererRegistry.register(TABLE, { render: () => Probe } as never); +}); +afterEach(() => ViewRendererRegistry.clear()); + +describe('DynamiaViewer', () => { + it('renders a descriptor written as a plain object, with no client', async () => { + const wrapper = mount(Viewer, { props: { descriptor: { view: 'table', fields: ['a'] }, source: [1, 2, 3] } }); + await flushPromises(); + + expect(wrapper.find('[data-testid=probe]').text()).toBe('inline-table:3'); + }); + + it('resolves a descriptor id with the client prop', async () => { + const getView = vi.fn().mockResolvedValue(widget); + const wrapper = mount(Viewer, { props: { descriptorId: 'clientesWidget', client: client(getView), source: [1] } }); + await flushPromises(); + + expect(getView).toHaveBeenCalledWith('clientesWidget'); + expect(wrapper.find('[data-testid=probe]').text()).toBe('clientesWidget:1'); + }); + + it('resolves a descriptor id with the client provided to the app', async () => { + const getView = vi.fn().mockResolvedValue(widget); + const wrapper = mount(Viewer, { props: { descriptorId: 'clientesWidget' }, global: { provide: { [DYNAMIA_CLIENT_KEY as symbol]: client(getView) } } }); + await flushPromises(); + + expect(wrapper.find('[data-testid=probe]').text()).toBe('clientesWidget:0'); + }); + + it('applies a new source to the live view', async () => { + const wrapper = mount(Viewer, { props: { descriptor: { view: 'table' }, source: [1] } }); + await flushPromises(); + + await wrapper.setProps({ source: [1, 2, 3, 4] }); + + expect(wrapper.find('[data-testid=probe]').text()).toBe('inline-table:4'); + }); + + it('loads the other descriptor when descriptorId changes, and ignores a stale load', async () => { + const other = { ...widget, id: 'otro' }; + const getView = vi.fn(async (id: string) => (id === 'otro' ? other : widget)); + const wrapper = mount(Viewer, { props: { descriptorId: 'clientesWidget', client: client(getView) } }); + await flushPromises(); + + await wrapper.setProps({ descriptorId: 'otro' }); + await flushPromises(); + + expect(wrapper.find('[data-testid=probe]').text()).toBe('otro:0'); + }); + + it('does not reload when an inline descriptor is re-created with the same content', async () => { + const getView = vi.fn(); + const ready = vi.fn(); + const wrapper = mount(Viewer, { props: { descriptor: { view: 'table' }, client: client(getView), onReady: ready } }); + await flushPromises(); + + await wrapper.setProps({ descriptor: { view: 'table' } }); + await flushPromises(); + + expect(ready).toHaveBeenCalledTimes(1); + }); + + it('reports an unknown descriptor id through the error event', async () => { + const onError = vi.fn(); + const failing = client(vi.fn().mockRejectedValue(new Error('404'))); + (failing.metadata as unknown as Record)['getEntities'] = vi.fn().mockResolvedValue({ entities: [] }); + const wrapper = mount(Viewer, { props: { descriptorId: 'nope', client: failing, onError } }); + await flushPromises(); + + expect(onError).toHaveBeenCalledTimes(1); + expect(wrapper.find('.dynamia-viewer-error').exists()).toBe(true); + }); +}); + +describe('DynamiaViewer with the real table renderer', () => { + it('draws the table of a plain descriptor, with no app.use(DynamiaVue)', async () => { + ViewRendererRegistry.register(TABLE, new VueTableRenderer()); + ViewRendererRegistry.registerViewFactory(TABLE, (descriptor, metadata) => new VueTableView(descriptor, metadata)); + + const wrapper = mount(Viewer, { + props: { + descriptor: { view: 'table', fields: { name: { label: 'Name' }, total: { label: 'Total', component: 'label', params: { converter: 'converters.Currency' } } } }, + source: [{ name: 'Ana', total: 1500 }, { name: 'Luis', total: 20 }], + readOnly: true, + }, + }); + await vi.waitFor(() => expect(wrapper.find('.dynamia-table-grid').exists()).toBe(true)); + + const text = wrapper.find('.dynamia-table-grid').text(); + expect(text).toContain('Name'); + expect(text).toContain('Ana'); + expect(text).toContain('1,500.00'); + expect(wrapper.findAll('.dynamia-table-row')).toHaveLength(2); + }); +}); diff --git a/platform/packages/vue/src/components/Viewer.vue b/platform/packages/vue/src/components/Viewer.vue index b671ee4d0..fdf20688a 100644 --- a/platform/packages/vue/src/components/Viewer.vue +++ b/platform/packages/vue/src/components/Viewer.vue @@ -27,11 +27,13 @@ diff --git a/platform/packages/vue/src/components/cellValue.test.ts b/platform/packages/vue/src/components/cellValue.test.ts new file mode 100644 index 000000000..070b05da7 --- /dev/null +++ b/platform/packages/vue/src/components/cellValue.test.ts @@ -0,0 +1,35 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import type { ResolvedField } from '@dynamia-tools/ui-core'; +import { cellValue } from './cellValue.js'; + +const field = (name: string, params?: Record) => ({ name, params }) as unknown as ResolvedField; + +const ORIGINAL_TZ = process.env['TZ']; +afterEach(() => { + if (ORIGINAL_TZ === undefined) delete process.env['TZ']; + else process.env['TZ'] = ORIGINAL_TZ; +}); + +describe('cellValue', () => { + it('returns the raw value when the column declares no converter', () => { + expect(cellValue({ name: 'Ana' }, field('name'))).toBe('Ana'); + }); + + it('applies params.converter (backend-style name) to the cell', () => { + expect(cellValue({ total: 1234.5 }, field('total', { converter: 'converters.Currency' }))).toBe('1,234.50'); + }); + + it('keeps the calendar day of a date-only value behind UTC', () => { + process.env['TZ'] = 'America/Bogota'; + expect(cellValue({ d: '2026-10-07' }, field('d', { converter: 'converters.Date' }))).toBe(new Date(2026, 9, 7).toLocaleDateString()); + }); + + it('shows the name of a referenced entity and ignores unknown converters', () => { + expect(cellValue({ c: { id: 2, name: 'Fantasy' } }, field('c'))).toBe('Fantasy'); + expect(cellValue({ x: 'raw' }, field('x', { converter: 'converters.Nope' }))).toBe('raw'); + }); + + it('returns empty for a missing row', () => { + expect(cellValue(null, field('x'))).toBe(''); + }); +}); diff --git a/platform/packages/vue/src/components/cellValue.ts b/platform/packages/vue/src/components/cellValue.ts new file mode 100644 index 000000000..a14b39891 --- /dev/null +++ b/platform/packages/vue/src/components/cellValue.ts @@ -0,0 +1,14 @@ +import { entityDisplayLabel, resolveConverter } from '@dynamia-tools/ui-core'; +import type { ResolvedField } from '@dynamia-tools/ui-core'; + +/** + * Text shown in a table cell that is not drawn with a field component. A referenced entity displays its name, and a + * column that declares `params.converter` (e.g. `converters.Date`) formats the value with it, as `Label` does. + */ +export function cellValue(row: unknown, field: ResolvedField): unknown { + if (!row || typeof row !== 'object') return ''; + const value = (row as Record)[field.name]; + if (value && typeof value === 'object') return entityDisplayLabel(value); + const converter = resolveConverter(field.params?.['converter'] as string | undefined); + return converter ? converter(value, field.params) : value; +} diff --git a/platform/packages/vue/src/renderers/VueCrudRenderer.ts b/platform/packages/vue/src/renderers/VueCrudRenderer.ts index 6ad94d6b6..187bf9fc9 100644 --- a/platform/packages/vue/src/renderers/VueCrudRenderer.ts +++ b/platform/packages/vue/src/renderers/VueCrudRenderer.ts @@ -1,6 +1,7 @@ // VueCrudRenderer: implements CrudRenderer for Vue import type { Component } from 'vue'; +import CrudComponent from '../components/Crud.vue'; import { ViewTypes } from '@dynamia-tools/ui-core'; import type { CrudRenderer } from '@dynamia-tools/ui-core'; import type { CrudView } from '@dynamia-tools/ui-core'; @@ -13,6 +14,6 @@ export class VueCrudRenderer implements CrudRenderer { readonly supportedViewType = ViewTypes.Crud; render(_view: CrudView): Component { - return { name: 'DynamiaCrud' } as Component; + return CrudComponent as Component; } } diff --git a/platform/packages/vue/src/renderers/VueFormRenderer.ts b/platform/packages/vue/src/renderers/VueFormRenderer.ts index 9068dd23d..26c76e765 100644 --- a/platform/packages/vue/src/renderers/VueFormRenderer.ts +++ b/platform/packages/vue/src/renderers/VueFormRenderer.ts @@ -1,6 +1,7 @@ // VueFormRenderer: implements FormRenderer for Vue import type { Component } from 'vue'; +import FormComponent from '../components/Form.vue'; import { ViewTypes } from '@dynamia-tools/ui-core'; import type { FormRenderer } from '@dynamia-tools/ui-core'; import type { FormView } from '@dynamia-tools/ui-core'; @@ -14,6 +15,6 @@ export class VueFormRenderer implements FormRenderer { render(_view: FormView): Component { // Lazy import to avoid circular dependency at module initialization time - return { name: 'DynamiaForm' } as Component; + return FormComponent as Component; } } diff --git a/platform/packages/vue/src/renderers/VueTableRenderer.ts b/platform/packages/vue/src/renderers/VueTableRenderer.ts index 2901e3f65..aed4c6b0c 100644 --- a/platform/packages/vue/src/renderers/VueTableRenderer.ts +++ b/platform/packages/vue/src/renderers/VueTableRenderer.ts @@ -1,6 +1,7 @@ // VueTableRenderer: implements TableRenderer for Vue import type { Component } from 'vue'; +import TableComponent from '../components/Table.vue'; import { ViewTypes } from '@dynamia-tools/ui-core'; import type { TableRenderer } from '@dynamia-tools/ui-core'; import type { TableView } from '@dynamia-tools/ui-core'; @@ -13,6 +14,6 @@ export class VueTableRenderer implements TableRenderer { readonly supportedViewType = ViewTypes.Table; render(_view: TableView): Component { - return { name: 'DynamiaTable' } as Component; + return TableComponent as Component; } } diff --git a/platform/starters/zk-starter/pom.xml b/platform/starters/zk-starter/pom.xml index b4f2d90b4..ccfd52183 100644 --- a/platform/starters/zk-starter/pom.xml +++ b/platform/starters/zk-starter/pom.xml @@ -4,7 +4,7 @@ tools.dynamia tools.dynamia.parent - 26.10.1 + 26.10.2 ../../../pom.xml @@ -28,22 +28,22 @@ tools.dynamia tools.dynamia.app - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.commons - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.zk - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.domain.jpa - 26.10.1 + 26.10.2 org.hibernate.validator diff --git a/platform/testing/arch-rules/pom.xml b/platform/testing/arch-rules/pom.xml index 85f0a8f6a..557f7bdd7 100644 --- a/platform/testing/arch-rules/pom.xml +++ b/platform/testing/arch-rules/pom.xml @@ -23,7 +23,7 @@ tools.dynamia tools.dynamia.parent - 26.10.1 + 26.10.2 ../../../pom.xml diff --git a/platform/testing/ui-contract/pom.xml b/platform/testing/ui-contract/pom.xml index 022afa993..4d3512b3e 100644 --- a/platform/testing/ui-contract/pom.xml +++ b/platform/testing/ui-contract/pom.xml @@ -23,7 +23,7 @@ tools.dynamia tools.dynamia.parent - 26.10.1 + 26.10.2 ../../../pom.xml @@ -64,12 +64,12 @@ tools.dynamia tools.dynamia.ui - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.domain - 26.10.1 + 26.10.2 diff --git a/platform/testing/ui-testing/pom.xml b/platform/testing/ui-testing/pom.xml index 64b31d848..63c9fc3d9 100644 --- a/platform/testing/ui-testing/pom.xml +++ b/platform/testing/ui-testing/pom.xml @@ -23,7 +23,7 @@ tools.dynamia tools.dynamia.parent - 26.10.1 + 26.10.2 ../../../pom.xml @@ -64,22 +64,22 @@ tools.dynamia tools.dynamia.ui - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.actions - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.crud - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.ui.contract - 26.10.1 + 26.10.2 test diff --git a/platform/ui/ui-shared/pom.xml b/platform/ui/ui-shared/pom.xml index 31169c45f..1fd39894f 100644 --- a/platform/ui/ui-shared/pom.xml +++ b/platform/ui/ui-shared/pom.xml @@ -23,7 +23,7 @@ tools.dynamia tools.dynamia.parent - 26.10.1 + 26.10.2 ../../../pom.xml @@ -64,17 +64,17 @@ tools.dynamia tools.dynamia.integration - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.commons - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.io - 26.10.1 + 26.10.2 diff --git a/platform/ui/zk/pom.xml b/platform/ui/zk/pom.xml index 4e83985be..2c1769735 100644 --- a/platform/ui/zk/pom.xml +++ b/platform/ui/zk/pom.xml @@ -21,7 +21,7 @@ tools.dynamia.parent tools.dynamia - 26.10.1 + 26.10.2 ../../../pom.xml 4.0.0 @@ -99,31 +99,31 @@ tools.dynamia tools.dynamia.web - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.navigation - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.ui - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.domain - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.viewers - 26.10.1 + 26.10.2 org.yaml @@ -134,19 +134,19 @@ tools.dynamia tools.dynamia.crud - 26.10.1 + 26.10.2 tools.dynamia tools.dynamia.reports - 26.10.1 + 26.10.2 compile tools.dynamia tools.dynamia.templates - 26.10.1 + 26.10.2 compile diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66a345336..045dd58b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -355,6 +355,12 @@ importers: '@vitejs/plugin-vue': specifier: ^6.0.8 version: 6.0.8(vite@8.2.2(@types/node@26.4.1)(jiti@2.7.0))(vue@3.5.43(typescript@6.0.3)) + '@vue/test-utils': + specifier: ^2.5.1 + version: 2.5.1(@vue/compiler-dom@3.5.43)(@vue/server-renderer@3.5.43)(vue@3.5.43(typescript@6.0.3)) + happy-dom: + specifier: ^20.14.5 + version: 20.14.5 typescript: specifier: ^6.0.3 version: 6.0.3 diff --git a/pom.xml b/pom.xml index 2ded61ae5..d1095db2f 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ 4.0.0 tools.dynamia tools.dynamia.parent - 26.10.1 + 26.10.2 pom Dynamia Soluciones IT SAS diff --git a/themes/pom.xml b/themes/pom.xml index 78f784624..b32f3b158 100644 --- a/themes/pom.xml +++ b/themes/pom.xml @@ -6,7 +6,7 @@ tools.dynamia tools.dynamia.parent - 26.10.1 + 26.10.2 ../pom.xml diff --git a/themes/theme-dynamical-vue/sources/pom.xml b/themes/theme-dynamical-vue/sources/pom.xml index 8eb7d8602..5989cf810 100644 --- a/themes/theme-dynamical-vue/sources/pom.xml +++ b/themes/theme-dynamical-vue/sources/pom.xml @@ -24,7 +24,7 @@ tools.dynamia.themes tools.dynamia.themes.parent - 26.10.1 + 26.10.2 ../../pom.xml @@ -166,7 +166,7 @@ tools.dynamia tools.dynamia.templates - 26.10.1 + 26.10.2 provided diff --git a/themes/theme-dynamical-vue/sources/src/main/frontend/package.json b/themes/theme-dynamical-vue/sources/src/main/frontend/package.json index fbb49afca..c3f2161bd 100644 --- a/themes/theme-dynamical-vue/sources/src/main/frontend/package.json +++ b/themes/theme-dynamical-vue/sources/src/main/frontend/package.json @@ -1,6 +1,6 @@ { "name": "theme-dynamical-vue-frontend", - "version": "26.10.1", + "version": "26.10.2", "description": "Vue 3 + Tailwind CSS frontend for the DynamicalVue application template", "private": true, "type": "module", diff --git a/themes/theme-dynamical/sources/pom.xml b/themes/theme-dynamical/sources/pom.xml index e5799b80c..96e2f4782 100644 --- a/themes/theme-dynamical/sources/pom.xml +++ b/themes/theme-dynamical/sources/pom.xml @@ -24,7 +24,7 @@ tools.dynamia.themes tools.dynamia.themes.parent - 26.10.1 + 26.10.2 ../../pom.xml @@ -102,7 +102,7 @@ tools.dynamia tools.dynamia.zk - 26.10.1 + 26.10.2 provided