mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-02 17:53:30 +00:00
Merge branch 'develop' into acs-10246-make-dates-semantically-conveyed
This commit is contained in:
@@ -35,7 +35,7 @@ runs:
|
||||
- name: get latest tag sha
|
||||
if: ${{ inputs.full-setup == 'true' }}
|
||||
id: tag-sha
|
||||
uses: Alfresco/alfresco-build-tools/.github/actions/git-latest-tag@5177eca5d3d71342d7f7e0a2a4d74cc16b1eeb1b # v18.21.3
|
||||
uses: Alfresco/alfresco-build-tools/.github/actions/git-latest-tag@98bcfbe06aafffdc0e9a790f352602316f82303b # v18.23.0
|
||||
- name: load "NPM TAG"
|
||||
if: ${{ inputs.full-setup == 'true' }}
|
||||
id: set-npm-tag
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v3.29.5
|
||||
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v3.29.5
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
with:
|
||||
languages: javascript
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v3.29.5
|
||||
uses: github/codeql-action/autobuild@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v3.29.5
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -53,4 +53,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v3.29.5
|
||||
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v3.29.5
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
steps:
|
||||
- name: Check if label was added after PR creation (with time threshold)
|
||||
id: check_label_timing
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const prCreatedAt = new Date('${{ github.event.pull_request.created_at }}');
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
|
||||
- name: Send Teams notification
|
||||
if: steps.check_label_timing.outputs.should_notify == 'true'
|
||||
uses: Alfresco/alfresco-build-tools/.github/actions/send-teams-notification@5177eca5d3d71342d7f7e0a2a4d74cc16b1eeb1b # v18.21.3
|
||||
uses: Alfresco/alfresco-build-tools/.github/actions/send-teams-notification@98bcfbe06aafffdc0e9a790f352602316f82303b # v18.23.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.TEAMS_NOTIFICATION_ADF_BDU_WEBHOOK }}
|
||||
skip_checkout: true
|
||||
|
||||
@@ -254,6 +254,8 @@ jobs:
|
||||
name: "Unit Tests"
|
||||
needs: [setup]
|
||||
uses: ./.github/workflows/unit-test-workflow.yml
|
||||
secrets:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
with:
|
||||
base_ref: ${{ github.base_ref || 'develop' }}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name: "SonarCloud Full Scan (develop)"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * *'
|
||||
workflow_dispatch: {}
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
full-unit-tests-and-sonar-scan:
|
||||
name: "Full Unit Tests + SonarCloud Scan"
|
||||
uses: ./.github/workflows/unit-test-workflow.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
full: true
|
||||
@@ -11,4 +11,4 @@ permissions:
|
||||
|
||||
jobs:
|
||||
stale-pr-cleanup:
|
||||
uses: Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml@5177eca5d3d71342d7f7e0a2a4d74cc16b1eeb1b # v18.21.3
|
||||
uses: Alfresco/alfresco-build-tools/.github/workflows/stale-pr-cleanup.yml@98bcfbe06aafffdc0e9a790f352602316f82303b # v18.23.0
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment@5177eca5d3d71342d7f7e0a2a4d74cc16b1eeb1b # v18.21.3
|
||||
- uses: Alfresco/alfresco-build-tools/.github/actions/github-upsert-comment@98bcfbe06aafffdc0e9a790f352602316f82303b # v18.23.0
|
||||
with:
|
||||
comment-identifier: supply-chain-review-instructions
|
||||
comment-body: |
|
||||
|
||||
+7
-7
@@ -41,7 +41,7 @@
|
||||
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9)
|
||||
# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
# - github/gh-aw-actions/setup@8914f47b6c1bb8a802c7549f5ac1a81434b66403 # v0.86.1
|
||||
# - github/gh-aw-actions/setup@6aab9e5b5c91c615506061f09bedd81a23babe3c # v0.86.2
|
||||
#
|
||||
# Container images used:
|
||||
# - ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@8914f47b6c1bb8a802c7549f5ac1a81434b66403 # v0.86.1
|
||||
uses: github/gh-aw-actions/setup@6aab9e5b5c91c615506061f09bedd81a23babe3c # v0.86.2
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -494,7 +494,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@8914f47b6c1bb8a802c7549f5ac1a81434b66403 # v0.86.1
|
||||
uses: github/gh-aw-actions/setup@6aab9e5b5c91c615506061f09bedd81a23babe3c # v0.86.2
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -1128,7 +1128,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@8914f47b6c1bb8a802c7549f5ac1a81434b66403 # v0.86.1
|
||||
uses: github/gh-aw-actions/setup@6aab9e5b5c91c615506061f09bedd81a23babe3c # v0.86.2
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -1404,7 +1404,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@8914f47b6c1bb8a802c7549f5ac1a81434b66403 # v0.86.1
|
||||
uses: github/gh-aw-actions/setup@6aab9e5b5c91c615506061f09bedd81a23babe3c # v0.86.2
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -1653,7 +1653,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@8914f47b6c1bb8a802c7549f5ac1a81434b66403 # v0.86.1
|
||||
uses: github/gh-aw-actions/setup@6aab9e5b5c91c615506061f09bedd81a23babe3c # v0.86.2
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
@@ -1732,7 +1732,7 @@ jobs:
|
||||
steps:
|
||||
- name: Setup Scripts
|
||||
id: setup
|
||||
uses: github/gh-aw-actions/setup@8914f47b6c1bb8a802c7549f5ac1a81434b66403 # v0.86.1
|
||||
uses: github/gh-aw-actions/setup@6aab9e5b5c91c615506061f09bedd81a23babe3c # v0.86.2
|
||||
with:
|
||||
destination: ${{ runner.temp }}/gh-aw/actions
|
||||
job-name: ${{ github.job }}
|
||||
|
||||
@@ -2,12 +2,21 @@ name: "Unit Tests Workflow"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
SONAR_TOKEN:
|
||||
description: 'Token for SonarCloud analysis'
|
||||
required: false
|
||||
inputs:
|
||||
base_ref:
|
||||
description: 'Base branch for affected calculation'
|
||||
required: false
|
||||
type: string
|
||||
default: 'develop'
|
||||
full:
|
||||
description: 'Run the full (non-affected) test suite for every project instead of only affected ones'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
generate-affected-matrix:
|
||||
@@ -30,9 +39,15 @@ jobs:
|
||||
id: set-matrix
|
||||
env:
|
||||
BASE_REF: ${{ inputs.base_ref }}
|
||||
FULL_RUN: ${{ inputs.full }}
|
||||
run: |
|
||||
if [ "$FULL_RUN" == "true" ]; then
|
||||
echo "Running full (non-affected) test suite"
|
||||
AFFECTED_UNIT=$(pnpm nx show projects --target=test --select=projects --plain --exclude=cli,stories,eslint-angular)
|
||||
else
|
||||
echo "Base ref is $BASE_REF"
|
||||
AFFECTED_UNIT=$(pnpm nx show projects --affected --target=test --base=origin/$BASE_REF --head=HEAD --select=projects --plain --exclude=cli,stories,eslint-angular)
|
||||
fi
|
||||
echo "Affected projects for UNIT: $AFFECTED_UNIT"
|
||||
|
||||
if [ -z "$AFFECTED_UNIT" ]; then
|
||||
@@ -74,8 +89,56 @@ jobs:
|
||||
NODE_OPTIONS: "--max-old-space-size=5120"
|
||||
run: |
|
||||
xvfb-run --auto-servernum pnpm nx run ${{ matrix.project }}:test
|
||||
- name: Upload coverage report
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: coverage-${{ matrix.project }}
|
||||
path: coverage/${{ matrix.project }}/lcov.info
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
- name: Save nx cache
|
||||
if: ${{ success() }}
|
||||
uses: ./.github/actions/save-nx-cache
|
||||
with:
|
||||
cache-suffix: test-${{ matrix.project }}
|
||||
|
||||
sonarcloud:
|
||||
name: "SonarCloud Scan"
|
||||
runs-on: ubuntu-latest
|
||||
needs: [generate-affected-matrix, unit-tests]
|
||||
if: ${{ needs.generate-affected-matrix.outputs.hasProjects == 'true' && always() && needs.unit-tests.result != 'cancelled' }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
pattern: coverage-*
|
||||
path: coverage-reports
|
||||
- name: Merge coverage reports
|
||||
run: |
|
||||
mkdir -p coverage
|
||||
echo "Artifact structure:"
|
||||
find coverage-reports -type f -name 'lcov.info' 2>/dev/null || true
|
||||
for dir in coverage-reports/coverage-*/; do
|
||||
project_name=$(basename "$dir" | sed 's/^coverage-//')
|
||||
lcov_file=$(find "$dir" -name 'lcov.info' -type f | head -1)
|
||||
if [ -n "$lcov_file" ]; then
|
||||
mkdir -p "coverage/${project_name}"
|
||||
cp "$lcov_file" "coverage/${project_name}/lcov.info"
|
||||
echo "Copied coverage for ${project_name}"
|
||||
fi
|
||||
done
|
||||
echo "Coverage files found:"
|
||||
find coverage -name 'lcov.info' -type f
|
||||
- name: SonarCloud Scan
|
||||
uses: SonarSource/sonarqube-scan-action@aa494459d7c39c106cc77b166de8b4250a32bb97 # v5.1.0
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: https://sonarcloud.io
|
||||
|
||||
@@ -52,7 +52,8 @@ Contains the value and metadata for a field of a [`Form`](../../../lib/process-s
|
||||
| columns | [`ContainerColumnModel`](../../../lib/core/src/lib/form/components/widgets/core/container-column.model.ts)\[] | \[] | Column definitions for a container field |
|
||||
| rows | [`ContainerRowModel`](../../../lib/core/src/lib/form/components/widgets/core/container-row.model.ts)\[] | \[] | Row definitions for a repeatable section field |
|
||||
| emptyOption | [`FormFieldOption`](../../../lib/core/src/lib/form/components/widgets/core/form-field-option.ts) | | Dropdown menu item to use when no option is chosen |
|
||||
| validationSummary | string | | Error/information message added during field validation (see [`FormFieldValidator`](../../../lib/core/src/lib/form/components/widgets/core/form-field-validator.ts) interface) |
|
||||
| validationSummary | [`ErrorMessageModel`](../../../lib/core/src/lib/form/components/widgets/core/error-message.model.ts) | | Error/information message added during field validation (see [`FormFieldValidator`](../../../lib/core/src/lib/form/components/widgets/core/form-field-validator.ts) interface) |
|
||||
| validationSummaryChanges$ | Observable<[`ErrorMessageModel`](../../../lib/core/src/lib/form/components/widgets/core/error-message.model.ts)> | | Replays the current validation summary to subscribers and emits the completed summary after each validation |
|
||||
|
||||
## Details
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ module.exports = function (config) {
|
||||
coverageReporter: {
|
||||
dir: join(__dirname, '../../coverage/content-services'),
|
||||
subdir: '.',
|
||||
reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
check: {
|
||||
global: {
|
||||
statements: 75,
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/cdk": ">=20.2.14",
|
||||
"@angular/common": ">=20.3.25",
|
||||
"@angular/compiler": ">=20.3.25",
|
||||
"@angular/core": ">=20.3.25",
|
||||
"@angular/forms": ">=20.3.25",
|
||||
"@angular/common": ">=20.3.27",
|
||||
"@angular/compiler": ">=20.3.27",
|
||||
"@angular/core": ">=20.3.27",
|
||||
"@angular/forms": ">=20.3.27",
|
||||
"@angular/material": ">=20.2.14",
|
||||
"@angular/platform-browser": ">=20.3.25",
|
||||
"@angular/platform-browser-dynamic": ">=20.3.25",
|
||||
"@angular/router": ">=20.3.25",
|
||||
"@angular/platform-browser": ">=20.3.27",
|
||||
"@angular/platform-browser-dynamic": ">=20.3.27",
|
||||
"@angular/router": ">=20.3.27",
|
||||
"@alfresco/js-api": ">=10.0.0",
|
||||
"@ngx-translate/core": ">=17.0.0",
|
||||
"@alfresco/adf-core": ">=9.0.0"
|
||||
|
||||
+2
@@ -1,6 +1,8 @@
|
||||
<div class="adf-search-filter-menu-card">
|
||||
<div class="adf-search-filter-title">
|
||||
<h2 class="adf-search-filter-title-heading">
|
||||
<ng-content select="filter-title" />
|
||||
</h2>
|
||||
<button mat-icon-button
|
||||
class="adf-search-filter-title-action"
|
||||
aria-hidden="false"
|
||||
|
||||
+5
@@ -7,6 +7,11 @@
|
||||
height: 32px;
|
||||
font: var(--mat-sys-body-medium);
|
||||
|
||||
&-heading {
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
&-action {
|
||||
float: right;
|
||||
}
|
||||
|
||||
+7
@@ -38,4 +38,11 @@ describe('SearchFilterMenuComponent', () => {
|
||||
closeButton.click();
|
||||
expect(spyCloseEvent).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should expose the title as a heading', () => {
|
||||
const heading = fixture.debugElement.nativeElement.querySelector('.adf-search-filter-title-heading');
|
||||
|
||||
expect(heading).not.toBeNull();
|
||||
expect(heading.tagName).toBe('H2');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ module.exports = function (config) {
|
||||
coverageReporter: {
|
||||
dir: join(__dirname, '../../coverage/core'),
|
||||
subdir: '.',
|
||||
reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
check: {
|
||||
global: {
|
||||
statements: 75,
|
||||
|
||||
@@ -23,13 +23,13 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/cdk": ">=20.2.14",
|
||||
"@angular/common": ">=20.3.25",
|
||||
"@angular/core": ">=20.3.25",
|
||||
"@angular/forms": ">=20.3.25",
|
||||
"@angular/common": ">=20.3.27",
|
||||
"@angular/core": ">=20.3.27",
|
||||
"@angular/forms": ">=20.3.27",
|
||||
"@angular/material": ">=20.2.14",
|
||||
"@angular/material-date-fns-adapter": ">=20.2.14",
|
||||
"@angular/platform-browser": ">=20.3.25",
|
||||
"@angular/router": ">=20.3.25",
|
||||
"@angular/platform-browser": ">=20.3.27",
|
||||
"@angular/router": ">=20.3.27",
|
||||
"@mat-datetimepicker/core": ">=12.0.1",
|
||||
"@ngx-translate/core": ">=17.0.0",
|
||||
"@alfresco/js-api": ">=10.0.0",
|
||||
|
||||
@@ -131,7 +131,10 @@ export class ResizableDirective implements OnInit, OnDestroy {
|
||||
.pipe(filter(() => !!this.currentRect));
|
||||
|
||||
mouseDrag
|
||||
.pipe(map(({ clientX }) => this.getNewBoundingRectangle(this.startingRect, clientX + this.coverPadding)))
|
||||
.pipe(
|
||||
map(({ clientX }) => this.getNewBoundingRectangle(this.startingRect, clientX + this.coverPadding)),
|
||||
takeUntilDestroyed(this.destroyRef)
|
||||
)
|
||||
.subscribe((rectangle: BoundingRectangle) => {
|
||||
if (this.resizing.observers.length > 0) {
|
||||
this.zone.run(() => {
|
||||
|
||||
@@ -131,6 +131,28 @@ describe('ResizeHandleDirective', () => {
|
||||
expect(renderer.listen).toHaveBeenCalledWith(element.nativeElement, 'mousemove', jasmine.any(Function));
|
||||
expect(renderer.listen).toHaveBeenCalledWith('document', 'mouseup', jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('should unregister previous mouseup listener before registering a new one on repeated mousedown', () => {
|
||||
const firstUnlistenMouseUp = jasmine.createSpy('firstUnlistenMouseUp');
|
||||
const secondUnlistenMouseUp = jasmine.createSpy('secondUnlistenMouseUp');
|
||||
let mouseUpCallCount = 0;
|
||||
|
||||
renderer.listen.and.callFake((_target: any, eventName: string, _callback: (event: MouseEvent) => void) => {
|
||||
if (eventName === 'mouseup') {
|
||||
mouseUpCallCount++;
|
||||
return mouseUpCallCount === 1 ? firstUnlistenMouseUp : secondUnlistenMouseUp;
|
||||
}
|
||||
return () => {};
|
||||
});
|
||||
|
||||
const mouseEvent = new MouseEvent('mousedown', { cancelable: true });
|
||||
|
||||
mousedownCallback(mouseEvent);
|
||||
expect(firstUnlistenMouseUp).not.toHaveBeenCalled();
|
||||
|
||||
mousedownCallback(mouseEvent);
|
||||
expect(firstUnlistenMouseUp).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyboard resizing', () => {
|
||||
|
||||
@@ -86,6 +86,7 @@ export class ResizeHandleDirective implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
this.unlistenMouseUp?.();
|
||||
this.unlistenMouseUp = this.renderer.listen('document', 'mouseup', (mouseUpEvent: MouseEvent) => {
|
||||
this.onMouseup(mouseUpEvent);
|
||||
});
|
||||
@@ -96,7 +97,8 @@ export class ResizeHandleDirective implements OnInit, OnDestroy {
|
||||
private onMouseup(event: MouseEvent): void {
|
||||
this.unlistenMouseMove?.();
|
||||
this.unlistenMouseMove = undefined;
|
||||
this.unlistenMouseUp();
|
||||
this.unlistenMouseUp?.();
|
||||
this.unlistenMouseUp = undefined;
|
||||
this.resizableContainer.mouseup.next(event);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
.mat-mdc-form-field-infix {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.adf-form-field-input:not(.adf-inplace-input-mat-form-field, .adf-people-cloud, .adf-cloud-group) {
|
||||
#{ms.$mat-form-field-subscript-wrapper} {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alfresco-tabs-widget {
|
||||
@@ -40,7 +46,7 @@
|
||||
|
||||
.adf-container-widget {
|
||||
.adf-form-field-input:not(.adf-inplace-input-mat-form-field, .adf-people-cloud, .adf-cloud-group) {
|
||||
margin-bottom: 35px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.adf-grid-list {
|
||||
@@ -265,7 +271,7 @@
|
||||
}
|
||||
|
||||
&-error-messages-container {
|
||||
min-height: 35px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
&-error-messages-container-visible {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
&-single-column {
|
||||
display: flex;
|
||||
flex-wrap: inherit;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 1%;
|
||||
|
||||
@include flex.layout-bp(lt-md) {
|
||||
|
||||
@@ -23,13 +23,14 @@ import { FormModel } from '../core/form.model';
|
||||
import { HarnessLoader } from '@angular/cdk/testing';
|
||||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
import { UnitTestingUtils } from '../../../../testing/unit-testing-utils';
|
||||
import { of } from 'rxjs';
|
||||
import { firstValueFrom, of } from 'rxjs';
|
||||
import { FormService } from '../../../services/form.service';
|
||||
import { FormFieldEvent } from '../../../events/form-field.event';
|
||||
import { TranslationService } from '../../../../translation/translation.service';
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
import localeDeExtra from '@angular/common/locales/extra/de';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
registerLocaleData(localeDe, 'de-DE', localeDeExtra);
|
||||
|
||||
@@ -394,6 +395,76 @@ describe('AmountWidgetComponent - rendering', () => {
|
||||
expect(errors[0].trim()).toContain('FORM.FIELD.VALIDATOR.INVALID_NUMBER');
|
||||
});
|
||||
|
||||
describe('when validation runs without amount interaction', () => {
|
||||
const validatorTranslations = {
|
||||
FORM: {
|
||||
FIELD: {
|
||||
VALIDATOR: {
|
||||
NOT_LESS_THAN: "Can't be less than {{ minValue }}",
|
||||
NOT_GREATER_THAN: "Can't be greater than {{ maxValue }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let amountField: FormFieldModel;
|
||||
let form: FormModel;
|
||||
|
||||
beforeEach(async () => {
|
||||
const translateService = TestBed.inject(TranslateService);
|
||||
translateService.setTranslation('en', validatorTranslations);
|
||||
await firstValueFrom(translateService.use('en'));
|
||||
|
||||
form = new FormModel({ taskId: '<id>' }, undefined, false, formService);
|
||||
amountField = new FormFieldModel(form, {
|
||||
id: 'amount-id',
|
||||
type: FormFieldTypes.AMOUNT,
|
||||
value: 1,
|
||||
minValue: '10'
|
||||
});
|
||||
form.fieldsCache = [amountField];
|
||||
amountField.validate();
|
||||
fixture.componentRef.setInput('field', amountField);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render updated parameters after direct revalidation', async () => {
|
||||
const formField = await testingUtils.formField.get();
|
||||
let errors = await formField.getTextErrors();
|
||||
expect(errors[0]).toContain("Can't be less than 10");
|
||||
|
||||
amountField.value = 10;
|
||||
amountField.minValue = '1';
|
||||
amountField.maxValue = '5';
|
||||
amountField.validate();
|
||||
fixture.detectChanges();
|
||||
|
||||
errors = await formField.getTextErrors();
|
||||
expect(errors[0]).toContain("Can't be greater than 5");
|
||||
expect(errors[0]).not.toContain('{{');
|
||||
});
|
||||
|
||||
it('should render updated parameters after sibling field revalidation', async () => {
|
||||
const siblingField = new FormFieldModel(form, {
|
||||
id: 'sibling-id',
|
||||
type: FormFieldTypes.TEXT,
|
||||
value: 'before'
|
||||
});
|
||||
form.fieldsCache = [amountField, siblingField];
|
||||
amountField.value = 10;
|
||||
amountField.minValue = '1';
|
||||
amountField.maxValue = '5';
|
||||
|
||||
siblingField.value = 'after';
|
||||
form.onFormFieldChanged(siblingField);
|
||||
fixture.detectChanges();
|
||||
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
expect(errors[0]).toContain("Can't be greater than 5");
|
||||
expect(errors[0]).not.toContain('{{');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when form model has left labels', () => {
|
||||
it('should have left labels classes on leftLabels true', async () => {
|
||||
widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', leftLabels: true }), {
|
||||
|
||||
@@ -25,6 +25,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { getValidationSummaryTranslationParameters } from '../core/error-message.model';
|
||||
import { WidgetComponent } from '../widget.component';
|
||||
import { filter, isObservable, Observable } from 'rxjs';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
@@ -121,7 +122,9 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
|
||||
this.subscribeToFieldChanges();
|
||||
this.setInitialValues();
|
||||
this.initErrorStateMatcher();
|
||||
this.updateTranslateParameters();
|
||||
this.field.validationSummaryChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((validationSummary) => {
|
||||
this.translateParameters = getValidationSummaryTranslationParameters(validationSummary);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +146,6 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
this.markAsTouched();
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
amountWidgetOnFocus(): void {
|
||||
@@ -163,7 +165,6 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
|
||||
this.field.value = this.amountWidgetValue;
|
||||
super.onFieldChanged(this.field);
|
||||
this.markAsTouched();
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
setInitialValues(): void {
|
||||
@@ -188,7 +189,6 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
|
||||
} else if (!this.isInputInFocus) {
|
||||
this.amountWidgetValue = ev.field.value;
|
||||
}
|
||||
this.updateTranslateParameters();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -208,12 +208,4 @@ export class AmountWidgetComponent extends WidgetComponent implements OnInit {
|
||||
!!this.field.validationSummary?.message || (this.isInvalidFieldRequired() && this.isTouched())
|
||||
};
|
||||
}
|
||||
|
||||
private updateTranslateParameters(): void {
|
||||
if (this.field?.validationSummary?.isActive()) {
|
||||
this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
|
||||
} else {
|
||||
this.translateParameters = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,9 @@
|
||||
<span class="adf-asterisk" [style.visibility]="isRequired() ? 'visible' : 'hidden'">*</span>
|
||||
</mat-checkbox>
|
||||
<div class="adf-error-messages-container">
|
||||
<error-widget [error]="field.validationSummary" />
|
||||
<error-widget
|
||||
*ngIf="isInvalidFieldRequired() && isTouched()"
|
||||
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"
|
||||
[error]="field.validationSummary"
|
||||
[required]="isInvalidFieldRequired() && isTouched() ? ('FORM.FIELD.REQUIRED' | translate) : ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
|
||||
import { NgClass, NgIf } from '@angular/common';
|
||||
import { NgClass } from '@angular/common';
|
||||
import { Component, ViewEncapsulation } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
@@ -47,7 +47,7 @@ import { WidgetComponent } from '../widget.component';
|
||||
'(invalid)': 'event($event)',
|
||||
'(select)': 'event($event)'
|
||||
},
|
||||
imports: [NgClass, MatCheckboxModule, FormsModule, TranslatePipe, ErrorWidgetComponent, NgIf],
|
||||
imports: [NgClass, MatCheckboxModule, FormsModule, TranslatePipe, ErrorWidgetComponent],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class CheckboxWidgetComponent extends WidgetComponent {}
|
||||
|
||||
@@ -17,24 +17,36 @@
|
||||
|
||||
export class ErrorMessageModel {
|
||||
message: string = '';
|
||||
attributes: Map<string, string> = null;
|
||||
attributes: Map<string, string> = new Map();
|
||||
|
||||
constructor(obj?: any) {
|
||||
this.message = obj?.message || '';
|
||||
this.attributes = obj?.attributes || new Map();
|
||||
|
||||
if (obj?.attributes) {
|
||||
this.attributes = obj.attributes;
|
||||
}
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return !!this.message;
|
||||
}
|
||||
|
||||
getAttributesAsJsonObj() {
|
||||
const result = {};
|
||||
getAttributesAsJsonObj(): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
if (this.attributes.size > 0) {
|
||||
this.attributes.forEach((value, key) => {
|
||||
result[key] = typeof value === 'string' ? value : JSON.stringify(value);
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export const getValidationSummaryTranslationParameters = (validationSummary?: ErrorMessageModel): Record<string, string> => {
|
||||
if (validationSummary?.isActive()) {
|
||||
return validationSummary.getAttributesAsJsonObj();
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
|
||||
import { DateFnsUtils } from '../../../../common';
|
||||
import { FormRulesEvent } from '../../../events/form-rules.event';
|
||||
import { firstValueFrom, map, Subject, take, timeout } from 'rxjs';
|
||||
import { firstValueFrom, map, skip, Subject, take, timeout } from 'rxjs';
|
||||
import { ErrorMessageModel, getValidationSummaryTranslationParameters } from './error-message.model';
|
||||
import { FormFieldTypes } from './form-field-types';
|
||||
import { RequiredFieldValidator } from './form-field-validator';
|
||||
import { MinValueFieldValidator, RequiredFieldValidator } from './form-field-validator';
|
||||
import { FormFieldModel } from './form-field.model';
|
||||
import { FormModel } from './form.model';
|
||||
|
||||
@@ -36,6 +37,58 @@ describe('FormFieldModel', () => {
|
||||
expect(model.json).toBe(json);
|
||||
});
|
||||
|
||||
it('should return an isolated authored value snapshot', () => {
|
||||
const authoredValue = { blocks: [{ data: { text: '${field.name}' } }] };
|
||||
const model = new FormFieldModel(new FormModel(), { id: 'richText', type: FormFieldTypes.DISPLAY_RICH_TEXT, value: authoredValue });
|
||||
|
||||
const snapshot = model.authoredValue as typeof authoredValue;
|
||||
snapshot.blocks[0].data.text = 'changed';
|
||||
|
||||
expect((model.authoredValue as typeof authoredValue).blocks[0].data.text).toBe('${field.name}');
|
||||
expect(authoredValue.blocks[0].data.text).toBe('${field.name}');
|
||||
});
|
||||
|
||||
it('should not capture authored values for other field types', () => {
|
||||
const model = new FormFieldModel(new FormModel(), { id: 'json', type: FormFieldTypes.JSON, value: { content: 'value' } });
|
||||
|
||||
expect(model.authoredValue).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined for authored values that cannot be cloned', () => {
|
||||
const circularValue: { self?: unknown } = {};
|
||||
circularValue.self = circularValue;
|
||||
|
||||
const circularValueModel = new FormFieldModel(new FormModel(), {
|
||||
id: 'circular',
|
||||
type: FormFieldTypes.DISPLAY_RICH_TEXT,
|
||||
value: circularValue
|
||||
});
|
||||
const bigintValueModel = new FormFieldModel(new FormModel(), {
|
||||
id: 'bigint',
|
||||
type: FormFieldTypes.DISPLAY_RICH_TEXT,
|
||||
value: BigInt(1)
|
||||
});
|
||||
|
||||
expect(circularValueModel.authoredValue).toBeUndefined();
|
||||
expect(bigintValueModel.authoredValue).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should preserve authored value when form data overrides the field value', () => {
|
||||
const authoredValue = { blocks: [{ data: { text: '${field.name}' } }] };
|
||||
const savedValue = { blocks: [{ data: { text: 'John' } }] };
|
||||
const form = new FormModel(
|
||||
{
|
||||
fields: [{ id: 'richText', name: 'richText', type: FormFieldTypes.DISPLAY_RICH_TEXT, value: authoredValue }]
|
||||
},
|
||||
{ richText: savedValue }
|
||||
);
|
||||
const model = form.getFieldById('richText');
|
||||
|
||||
expect(model.value).toEqual(savedValue);
|
||||
expect(model.authoredValue).toEqual(authoredValue);
|
||||
expect(model.authoredValue).not.toBe(authoredValue);
|
||||
});
|
||||
|
||||
it('should setup with json config', () => {
|
||||
const json = {
|
||||
fieldType: '<fieldType>',
|
||||
@@ -1222,6 +1275,57 @@ describe('FormFieldModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('validation summary changes', () => {
|
||||
const createField = (): FormFieldModel => {
|
||||
const form = new FormModel();
|
||||
form.fieldValidators = [new MinValueFieldValidator()];
|
||||
|
||||
return new FormFieldModel(form, {
|
||||
id: 'number-field',
|
||||
type: FormFieldTypes.NUMBER,
|
||||
value: 1,
|
||||
minValue: '10'
|
||||
});
|
||||
};
|
||||
|
||||
it('should replay an inactive validation summary before the first validation', async () => {
|
||||
const field = new FormFieldModel(new FormModel());
|
||||
|
||||
const validationSummary = await firstValueFrom(field.validationSummaryChanges$);
|
||||
|
||||
expect(validationSummary).toEqual(jasmine.any(ErrorMessageModel));
|
||||
expect(validationSummary.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('should replay the completed validation summary when subscribing after validation', async () => {
|
||||
const field = createField();
|
||||
field.validate();
|
||||
|
||||
const validationSummary = await firstValueFrom(field.validationSummaryChanges$);
|
||||
|
||||
expect(validationSummary.message).toBe('FORM.FIELD.VALIDATOR.NOT_LESS_THAN');
|
||||
expect(validationSummary.attributes.get('minValue')).toBe('10');
|
||||
});
|
||||
|
||||
it('should emit completed summaries when validation state changes', async () => {
|
||||
const field = createField();
|
||||
const invalidSummaryPromise = firstValueFrom(field.validationSummaryChanges$.pipe(skip(1)));
|
||||
|
||||
field.validate();
|
||||
const invalidSummary = await invalidSummaryPromise;
|
||||
|
||||
field.value = 10;
|
||||
const validSummaryPromise = firstValueFrom(field.validationSummaryChanges$.pipe(skip(1)));
|
||||
field.validate();
|
||||
const validSummary = await validSummaryPromise;
|
||||
|
||||
expect(invalidSummary.message).toBe('FORM.FIELD.VALIDATOR.NOT_LESS_THAN');
|
||||
expect(invalidSummary.attributes.get('minValue')).toBe('10');
|
||||
expect(validSummary.isActive()).toBe(false);
|
||||
expect(validSummary.attributes.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail validation for readOnly required display-external-property field with null value', () => {
|
||||
const form = new FormModel();
|
||||
const field = new FormFieldModel(form, {
|
||||
@@ -2056,3 +2160,35 @@ describe('FormFieldTypes', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ErrorMessageModel', () => {
|
||||
it('should initialize empty attributes when attributes are omitted', () => {
|
||||
const errorMessage = new ErrorMessageModel();
|
||||
|
||||
expect(errorMessage.attributes).toEqual(new Map());
|
||||
});
|
||||
|
||||
it('should retain provided attributes', () => {
|
||||
const attributes = new Map([['minValue', '10']]);
|
||||
|
||||
const errorMessage = new ErrorMessageModel({ attributes });
|
||||
|
||||
expect(errorMessage.attributes).toBe(attributes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValidationSummaryTranslationParameters', () => {
|
||||
it('should return validation attributes when the summary is active', () => {
|
||||
const validationSummary = new ErrorMessageModel({
|
||||
message: 'FORM.FIELD.VALIDATOR.NOT_LESS_THAN',
|
||||
attributes: new Map([['minValue', '10']])
|
||||
});
|
||||
|
||||
expect(getValidationSummaryTranslationParameters(validationSummary)).toEqual({ minValue: '10' });
|
||||
});
|
||||
|
||||
it('should return empty parameters when the summary is inactive or omitted', () => {
|
||||
expect(getValidationSummaryTranslationParameters(new ErrorMessageModel())).toEqual({});
|
||||
expect(getValidationSummaryTranslationParameters()).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ import { VariableConfig } from './form-field-variable-options';
|
||||
import { DataColumn } from '../../../../datatable/data/data-column.model';
|
||||
import { DateFnsUtils } from '../../../../common';
|
||||
import { isValid as isValidDate } from 'date-fns';
|
||||
import { Observable, ReplaySubject } from 'rxjs';
|
||||
import { ContainerRowModel } from './container-row.model';
|
||||
import { RepeatableSectionModel, ROW_ID_PREFIX, TEMPLATE_ROW_ID } from './repeatable-section.model';
|
||||
import { formFieldRuleHandler } from './handlers/form-field-rule.handler';
|
||||
@@ -38,12 +39,35 @@ export type FieldOptionType = 'rest' | 'manual' | 'variable';
|
||||
export type FieldSelectionType = 'single' | 'multiple';
|
||||
export type FieldAlignmentType = 'vertical' | 'horizontal';
|
||||
|
||||
interface ValidationSummaryChangesState {
|
||||
subject: ReplaySubject<ErrorMessageModel>;
|
||||
observable: Observable<ErrorMessageModel>;
|
||||
}
|
||||
|
||||
const validationSummaryChangesByField = new WeakMap<FormFieldModel, ValidationSummaryChangesState>();
|
||||
|
||||
const isJsonPrimitive = (value: unknown): value is null | string | number | boolean =>
|
||||
value === null || ['string', 'number', 'boolean'].includes(typeof value);
|
||||
|
||||
const cloneJsonCompatibleValue = (value: unknown): unknown => {
|
||||
if (value === undefined || isJsonPrimitive(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// Maps to FormFieldRepresentation
|
||||
export class FormFieldModel extends FormWidgetModel {
|
||||
private _value: string;
|
||||
private _readOnly: boolean = false;
|
||||
private _isValid: boolean = true;
|
||||
private _required: boolean = false;
|
||||
private readonly _authoredValue: unknown;
|
||||
|
||||
readonly defaultDateFormat: string = 'D-M-YYYY';
|
||||
readonly defaultDateTimeFormat: string = 'D-M-YYYY hh:mm A';
|
||||
@@ -110,7 +134,21 @@ export class FormFieldModel extends FormWidgetModel {
|
||||
|
||||
// util members
|
||||
emptyOption: FormFieldOption;
|
||||
validationSummary: ErrorMessageModel;
|
||||
validationSummary: ErrorMessageModel = new ErrorMessageModel();
|
||||
|
||||
get validationSummaryChanges$(): Observable<ErrorMessageModel> {
|
||||
const existingState = validationSummaryChangesByField.get(this);
|
||||
if (existingState) {
|
||||
return existingState.observable;
|
||||
}
|
||||
|
||||
const subject = new ReplaySubject<ErrorMessageModel>(1);
|
||||
const observable = subject.asObservable();
|
||||
validationSummaryChangesByField.set(this, { subject, observable });
|
||||
subject.next(this.validationSummary);
|
||||
|
||||
return observable;
|
||||
}
|
||||
|
||||
get value(): any {
|
||||
return this._value;
|
||||
@@ -123,6 +161,10 @@ export class FormFieldModel extends FormWidgetModel {
|
||||
}
|
||||
}
|
||||
|
||||
get authoredValue(): unknown {
|
||||
return cloneJsonCompatibleValue(this._authoredValue);
|
||||
}
|
||||
|
||||
get readOnly(): boolean {
|
||||
if (this.form?.readOnly) {
|
||||
return true;
|
||||
@@ -173,16 +215,19 @@ export class FormFieldModel extends FormWidgetModel {
|
||||
for (const validator of validators) {
|
||||
if (!validator.validate(this)) {
|
||||
this._isValid = false;
|
||||
validationSummaryChangesByField.get(this)?.subject.next(this.validationSummary);
|
||||
return this._isValid;
|
||||
}
|
||||
}
|
||||
|
||||
this._isValid = true;
|
||||
validationSummaryChangesByField.get(this)?.subject.next(this.validationSummary);
|
||||
return this._isValid;
|
||||
}
|
||||
|
||||
constructor(form: any, json?: any, parent?: RepeatableSectionModel) {
|
||||
super(form, json);
|
||||
this._authoredValue = json?.type === FormFieldTypes.DISPLAY_RICH_TEXT ? cloneJsonCompatibleValue(json.value) : undefined;
|
||||
if (json) {
|
||||
this.fieldType = json.fieldType;
|
||||
this.id = this.getId(json.id, parent);
|
||||
@@ -221,7 +266,6 @@ export class FormFieldModel extends FormWidgetModel {
|
||||
this.enableFractions = json.enableFractions;
|
||||
this.currency = json.currency;
|
||||
this.dateDisplayFormat = json.dateDisplayFormat || this.getDefaultDateFormat(json);
|
||||
this.validationSummary = new ErrorMessageModel();
|
||||
this.tooltip = json.tooltip || '';
|
||||
this.selectionType = json.selectionType;
|
||||
this.alignmentType = json.alignmentType;
|
||||
|
||||
@@ -22,8 +22,20 @@ import { UnitTestingUtils } from '../../../../testing';
|
||||
import { FormService } from '../../../services/form.service';
|
||||
import { FormFieldModel, FormFieldTypes, FormModel } from '../core';
|
||||
import { DecimalWidgetComponent } from './decimal.component';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
describe('DecimalComponent', () => {
|
||||
const validatorTranslations = {
|
||||
FORM: {
|
||||
FIELD: {
|
||||
VALIDATOR: {
|
||||
NOT_LESS_THAN: "Can't be less than {{ minValue }}",
|
||||
NOT_GREATER_THAN: "Can't be greater than {{ maxValue }}",
|
||||
INVALID_DECIMAL_PRECISION: 'Precision {{ precision }}'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let loader: HarnessLoader;
|
||||
let widget: DecimalWidgetComponent;
|
||||
let fixture: ComponentFixture<DecimalWidgetComponent>;
|
||||
@@ -107,6 +119,62 @@ describe('DecimalComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when validation runs without widget interaction', () => {
|
||||
let field: FormFieldModel;
|
||||
|
||||
beforeEach(() => {
|
||||
const translateService = TestBed.inject(TranslateService);
|
||||
translateService.use('en').subscribe();
|
||||
translateService.setTranslation('en', validatorTranslations);
|
||||
field = new FormFieldModel(new FormModel({ taskId: '<id>' }), {
|
||||
id: 'decimal-id',
|
||||
type: FormFieldTypes.DECIMAL,
|
||||
value: 1,
|
||||
minValue: 10
|
||||
});
|
||||
field.validate();
|
||||
field.form.showAllValidationErrors = true;
|
||||
fixture.componentRef.setInput('field', field);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render the minimum value in the message when initial validation fails', async () => {
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain("Can't be less than 10");
|
||||
});
|
||||
|
||||
it('should render the updated maximum value when programmatic revalidation fails', async () => {
|
||||
field.value = 10;
|
||||
field.minValue = '1';
|
||||
field.maxValue = '5';
|
||||
field.validate();
|
||||
fixture.detectChanges();
|
||||
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain("Can't be greater than 5");
|
||||
});
|
||||
|
||||
it('should render decimal precision when programmatic revalidation fails', async () => {
|
||||
field.value = 1.234;
|
||||
field.minValue = '1';
|
||||
field.precision = 2;
|
||||
field.validate();
|
||||
fixture.detectChanges();
|
||||
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain('Precision 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when form model has left labels', () => {
|
||||
it('should have left labels classes on leftLabels true', async () => {
|
||||
widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', leftLabels: true }), {
|
||||
|
||||
@@ -16,13 +16,15 @@
|
||||
*/
|
||||
|
||||
import { NgIf } from '@angular/common';
|
||||
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { FormsModule, FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms';
|
||||
import { ErrorStateMatcher } from '@angular/material/core';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { getValidationSummaryTranslationParameters } from '../core/error-message.model';
|
||||
import { WidgetComponent } from '../widget.component';
|
||||
|
||||
@Component({
|
||||
@@ -46,19 +48,21 @@ import { WidgetComponent } from '../widget.component';
|
||||
export class DecimalWidgetComponent extends WidgetComponent implements OnInit {
|
||||
errorStateMatcher: ErrorStateMatcher;
|
||||
translateParameters: Record<string, string> = {};
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initErrorStateMatcher();
|
||||
this.field.validationSummaryChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((validationSummary) => {
|
||||
this.translateParameters = getValidationSummaryTranslationParameters(validationSummary);
|
||||
});
|
||||
}
|
||||
|
||||
onBlur(): void {
|
||||
this.markAsTouched();
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
onDecimalFieldChanged(): void {
|
||||
this.onFieldChanged(this.field);
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
private initErrorStateMatcher(): void {
|
||||
@@ -67,12 +71,4 @@ export class DecimalWidgetComponent extends WidgetComponent implements OnInit {
|
||||
!this.field.isValid && this.isTouched()
|
||||
};
|
||||
}
|
||||
|
||||
private updateTranslateParameters(): void {
|
||||
if (this.field.validationSummary?.isActive()) {
|
||||
this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
|
||||
} else {
|
||||
this.translateParameters = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
error-widget {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.adf-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&-widget-container {
|
||||
height: auto;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
&-animate {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.adf-hyperlink-widget {
|
||||
padding: 0.4375em 0;
|
||||
border-top: 0.8438em solid transparent;
|
||||
margin-bottom: 20px;
|
||||
|
||||
a {
|
||||
color: var(--mat-sys-primary);
|
||||
|
||||
@@ -43,6 +43,6 @@
|
||||
@include mixins.adf-error-icon;
|
||||
}
|
||||
|
||||
.adf-container-widget .adf-multiline-text-widget .adf-form-field-input.adf-has-counter {
|
||||
margin-bottom: 44px;
|
||||
.adf-container-widget .adf-multiline-text-widget mat-form-field.adf-form-field-input.adf-has-counter {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@@ -25,8 +25,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { UnitTestingUtils } from '../../../../testing/unit-testing-utils';
|
||||
import { ADF_CUSTOM_MESSAGE } from '../core/custom-validation-message.token';
|
||||
import { of, Subject } from 'rxjs';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
describe('MultilineTextWidgetComponentComponent', () => {
|
||||
const validatorTranslations = {
|
||||
FORM: {
|
||||
FIELD: {
|
||||
VALIDATOR: {
|
||||
AT_LEAST_LONG: 'Minimum {{ minLength }}',
|
||||
NO_LONGER_THAN: 'Maximum {{ maxLength }}'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let loader: HarnessLoader;
|
||||
let widget: MultilineTextWidgetComponentComponent;
|
||||
let fixture: ComponentFixture<MultilineTextWidgetComponentComponent>;
|
||||
@@ -109,6 +120,48 @@ describe('MultilineTextWidgetComponentComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when validation runs without widget interaction', () => {
|
||||
let field: FormFieldModel;
|
||||
|
||||
beforeEach(() => {
|
||||
const translateService = TestBed.inject(TranslateService);
|
||||
translateService.use('en').subscribe();
|
||||
translateService.setTranslation('en', validatorTranslations);
|
||||
field = new FormFieldModel(new FormModel({ taskId: '<id>' }), {
|
||||
id: 'multiline-text-id',
|
||||
type: FormFieldTypes.MULTILINE_TEXT,
|
||||
value: 'text',
|
||||
minLength: 10
|
||||
});
|
||||
field.validate();
|
||||
field.form.showAllValidationErrors = true;
|
||||
fixture.componentRef.setInput('field', field);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render the minimum length in the message when initial validation fails', async () => {
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain('Minimum 10');
|
||||
});
|
||||
|
||||
it('should render the updated maximum length when programmatic revalidation fails', async () => {
|
||||
field.value = 'too long';
|
||||
field.minLength = 1;
|
||||
field.maxLength = 5;
|
||||
field.validate();
|
||||
fixture.detectChanges();
|
||||
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain('Maximum 5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when is required', () => {
|
||||
beforeEach(() => {
|
||||
widget.field = new FormFieldModel(new FormModel({ taskId: '<id>' }), {
|
||||
|
||||
@@ -27,6 +27,7 @@ import { MatInputModule } from '@angular/material/input';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { isObservable } from 'rxjs';
|
||||
import { ADF_CUSTOM_MESSAGE } from '../core/custom-validation-message.token';
|
||||
import { getValidationSummaryTranslationParameters } from '../core/error-message.model';
|
||||
import { WidgetComponent } from '../widget.component';
|
||||
|
||||
@Component({
|
||||
@@ -56,6 +57,9 @@ export class MultilineTextWidgetComponentComponent extends WidgetComponent imple
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initErrorStateMatcher();
|
||||
this.field.validationSummaryChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((validationSummary) => {
|
||||
this.translateParameters = getValidationSummaryTranslationParameters(validationSummary);
|
||||
});
|
||||
if (this.enableCustomMessage != null) {
|
||||
if (isObservable(this.enableCustomMessage)) {
|
||||
this.enableCustomMessage.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((enabled: boolean) => {
|
||||
@@ -73,12 +77,10 @@ export class MultilineTextWidgetComponentComponent extends WidgetComponent imple
|
||||
|
||||
onBlur(): void {
|
||||
this.markAsTouched();
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
onMultilineTextFieldChanged(): void {
|
||||
this.onFieldChanged(this.field);
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
private initErrorStateMatcher(): void {
|
||||
@@ -87,12 +89,4 @@ export class MultilineTextWidgetComponentComponent extends WidgetComponent imple
|
||||
!this.field.isValid && this.isTouched()
|
||||
};
|
||||
}
|
||||
|
||||
private updateTranslateParameters(): void {
|
||||
if (this.field.validationSummary?.isActive()) {
|
||||
this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
|
||||
} else {
|
||||
this.translateParameters = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,20 @@ import { UnitTestingUtils } from '../../../../testing';
|
||||
import { FormFieldModel, FormFieldTypes, FormModel } from '../core';
|
||||
import { NumberWidgetComponent } from './number.widget';
|
||||
import { DecimalNumberPipe } from '../../../../pipes';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
describe('NumberWidgetComponent', () => {
|
||||
const validatorTranslations = {
|
||||
FORM: {
|
||||
FIELD: {
|
||||
VALIDATOR: {
|
||||
NOT_LESS_THAN: "Can't be less than {{ minValue }}",
|
||||
NOT_GREATER_THAN: "Can't be greater than {{ maxValue }}",
|
||||
NO_LONGER_THAN: 'Maximum length {{ maxLength }}'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let loader: HarnessLoader;
|
||||
let widget: NumberWidgetComponent;
|
||||
let fixture: ComponentFixture<NumberWidgetComponent>;
|
||||
@@ -175,6 +187,60 @@ describe('NumberWidgetComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when validation runs without widget interaction', () => {
|
||||
let field: FormFieldModel;
|
||||
|
||||
beforeEach(() => {
|
||||
const translateService = TestBed.inject(TranslateService);
|
||||
translateService.use('en').subscribe();
|
||||
translateService.setTranslation('en', validatorTranslations);
|
||||
field = new FormFieldModel(new FormModel({ taskId: '<id>' }), {
|
||||
id: 'number-id',
|
||||
type: FormFieldTypes.NUMBER,
|
||||
value: 1,
|
||||
minValue: 10
|
||||
});
|
||||
field.validate();
|
||||
fixture.componentRef.setInput('field', field);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render the minimum value in the message when initial validation fails', async () => {
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain("Can't be less than 10");
|
||||
});
|
||||
|
||||
it('should render the updated maximum value when programmatic revalidation fails', async () => {
|
||||
field.value = 10;
|
||||
field.minValue = '1';
|
||||
field.maxValue = '5';
|
||||
field.validate();
|
||||
fixture.detectChanges();
|
||||
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain("Can't be greater than 5");
|
||||
});
|
||||
|
||||
it('should render the maximum length when programmatic revalidation fails', async () => {
|
||||
field.value = 12345678901;
|
||||
field.minValue = '1';
|
||||
field.validate();
|
||||
fixture.detectChanges();
|
||||
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain('Maximum length 10');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when form model has left labels', () => {
|
||||
it('should have left labels classes on leftLabels true', async () => {
|
||||
widget.field = new FormFieldModel(new FormModel({ taskId: 'fake-task-id', leftLabels: true }), {
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
|
||||
import { NgIf } from '@angular/common';
|
||||
import { Component, inject, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { Component, DestroyRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { FormsModule, FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms';
|
||||
import { ErrorStateMatcher } from '@angular/material/core';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -26,6 +27,7 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { DecimalNumberPipe } from '../../../../pipes';
|
||||
import { getValidationSummaryTranslationParameters } from '../core/error-message.model';
|
||||
import { WidgetComponent } from '../widget.component';
|
||||
|
||||
@Component({
|
||||
@@ -53,6 +55,7 @@ export class NumberWidgetComponent extends WidgetComponent implements OnInit {
|
||||
translateParameters: Record<string, string> = {};
|
||||
|
||||
private readonly decimalNumberPipe = inject(DecimalNumberPipe);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
ngOnInit() {
|
||||
if (this.field.readOnly) {
|
||||
@@ -61,11 +64,13 @@ export class NumberWidgetComponent extends WidgetComponent implements OnInit {
|
||||
this.displayValue = this.field.value;
|
||||
}
|
||||
this.initErrorStateMatcher();
|
||||
this.field.validationSummaryChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((validationSummary) => {
|
||||
this.translateParameters = getValidationSummaryTranslationParameters(validationSummary);
|
||||
});
|
||||
}
|
||||
|
||||
onBlur(): void {
|
||||
this.markAsTouched();
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
protected onNumberChange(value: string) {
|
||||
@@ -74,7 +79,6 @@ export class NumberWidgetComponent extends WidgetComponent implements OnInit {
|
||||
}
|
||||
|
||||
this.onFieldChanged(this.field);
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
private initErrorStateMatcher(): void {
|
||||
@@ -83,12 +87,4 @@ export class NumberWidgetComponent extends WidgetComponent implements OnInit {
|
||||
!!this.field.validationSummary?.message || (this.isInvalidFieldRequired() && this.isTouched())
|
||||
};
|
||||
}
|
||||
|
||||
private updateTranslateParameters(): void {
|
||||
if (this.field.validationSummary?.isActive()) {
|
||||
this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
|
||||
} else {
|
||||
this.translateParameters = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
}
|
||||
|
||||
&-row-action {
|
||||
margin-left: 10px;
|
||||
margin-inline-start: 10px;
|
||||
margin-block-end: 35px;
|
||||
}
|
||||
|
||||
&-row-limit {
|
||||
|
||||
@@ -27,9 +27,20 @@ import { UnitTestingUtils } from '../../../../testing/unit-testing-utils';
|
||||
import { ADF_CUSTOM_MESSAGE } from '../core/custom-validation-message.token';
|
||||
import { ADF_TYPED_VALUE_FORMATTING_ENABLED } from '../../../services/form-field-value-formatter.token';
|
||||
import { of, Subject } from 'rxjs';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
describe('TextWidgetComponent', () => {
|
||||
const form = new FormModel({ taskId: 'fake-task-id' });
|
||||
const validatorTranslations = {
|
||||
FORM: {
|
||||
FIELD: {
|
||||
VALIDATOR: {
|
||||
AT_LEAST_LONG: 'Minimum {{ minLength }}',
|
||||
NO_LONGER_THAN: 'Maximum {{ maxLength }}'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let loader: HarnessLoader;
|
||||
let widget: TextWidgetComponent;
|
||||
@@ -64,6 +75,47 @@ describe('TextWidgetComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when validation runs without widget interaction', () => {
|
||||
let field: FormFieldModel;
|
||||
|
||||
beforeEach(() => {
|
||||
const translateService = TestBed.inject(TranslateService);
|
||||
translateService.use('en').subscribe();
|
||||
translateService.setTranslation('en', validatorTranslations);
|
||||
field = new FormFieldModel(form, {
|
||||
id: 'text-id',
|
||||
type: FormFieldTypes.TEXT,
|
||||
value: 'text',
|
||||
minLength: 10
|
||||
});
|
||||
field.validate();
|
||||
fixture.componentRef.setInput('field', field);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render the minimum length in the message when initial validation fails', async () => {
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain('Minimum 10');
|
||||
});
|
||||
|
||||
it('should render the updated maximum length when programmatic revalidation fails', async () => {
|
||||
field.value = 'too long';
|
||||
field.minLength = 1;
|
||||
field.maxLength = 5;
|
||||
field.validate();
|
||||
fixture.detectChanges();
|
||||
|
||||
const formField = await testingUtils.formField.get();
|
||||
const errors = await formField.getTextErrors();
|
||||
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0]).toContain('Maximum 5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when template is ready', () => {
|
||||
describe('and no mask is configured on text element', () => {
|
||||
it('should raise ngModelChange event', async () => {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import { NgIf, NgTemplateOutlet } from '@angular/common';
|
||||
import { Component, Directive, inject, InjectionToken, Input, TemplateRef, ViewEncapsulation } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { FormsModule, FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms';
|
||||
import { ErrorStateMatcher } from '@angular/material/core';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -26,7 +27,7 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { WidgetComponent } from '../widget.component';
|
||||
import { ErrorMessageModel } from '../core/error-message.model';
|
||||
import { ErrorMessageModel, getValidationSummaryTranslationParameters } from '../core/error-message.model';
|
||||
import { FormattableTextWidgetComponent } from '../core/formattable-text.widget';
|
||||
import { DEFAULT_TEXT_MAX_LENGTH } from '../core/form-field-validator';
|
||||
import { InputMaskDirective } from './text-mask.component';
|
||||
@@ -97,6 +98,9 @@ export class TextWidgetComponent extends FormattableTextWidgetComponent {
|
||||
this.isMaskReversed = this.field.params['inputMaskReversed'] ? this.field.params['inputMaskReversed'] : false;
|
||||
}
|
||||
this.initErrorStateMatcher();
|
||||
this.field.validationSummaryChanges$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((validationSummary) => {
|
||||
this.translateParameters = getValidationSummaryTranslationParameters(validationSummary);
|
||||
});
|
||||
}
|
||||
|
||||
onPaste(event: ClipboardEvent): void {
|
||||
@@ -125,12 +129,10 @@ export class TextWidgetComponent extends FormattableTextWidgetComponent {
|
||||
|
||||
onBlur(): void {
|
||||
this.markAsTouched();
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
onTextFieldChanged(): void {
|
||||
this.onFieldChanged(this.field);
|
||||
this.updateTranslateParameters();
|
||||
}
|
||||
|
||||
private initErrorStateMatcher(): void {
|
||||
@@ -143,14 +145,6 @@ export class TextWidgetComponent extends FormattableTextWidgetComponent {
|
||||
};
|
||||
}
|
||||
|
||||
private updateTranslateParameters(): void {
|
||||
if (this.field.validationSummary?.isActive()) {
|
||||
this.translateParameters = this.field.validationSummary.getAttributesAsJsonObj();
|
||||
} else {
|
||||
this.translateParameters = {};
|
||||
}
|
||||
}
|
||||
|
||||
private getLengthAfterPaste(input: HTMLInputElement, pastedValue: string): number {
|
||||
const value = input.value ?? '';
|
||||
const selectionStart = input.selectionStart ?? value.length;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
import { FormEvent } from '../events/form.event';
|
||||
import { FormFieldEvent } from '../events/form-field.event';
|
||||
import { FormRulesEvent } from '../events/form-rules.event';
|
||||
import { ValidateFormFieldEvent } from '../events/validate-form-field.event';
|
||||
@@ -26,4 +27,5 @@ export interface FormValidationService {
|
||||
validateForm: Subject<ValidateFormEvent>;
|
||||
validateFormField: Subject<ValidateFormFieldEvent>;
|
||||
formRulesEvent?: Subject<FormRulesEvent>;
|
||||
formVisibilityRefreshed?: Subject<FormEvent>;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,12 @@ export class FormService implements FormValidationService {
|
||||
|
||||
formRulesEvent = new Subject<FormRulesEvent>();
|
||||
|
||||
/**
|
||||
* Emitted after form field/outcome visibility has been re-evaluated via WidgetVisibilityService.refreshVisibility.
|
||||
* Internal ADF form-rendering event — not part of the FormValidationService contract.
|
||||
*/
|
||||
formVisibilityRefreshed = new Subject<FormEvent>();
|
||||
|
||||
constructor() {
|
||||
const injectedFieldValidators = inject(FORM_SERVICE_FIELD_VALIDATORS_TOKEN, { optional: true });
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { TestBed } from '@angular/core/testing';
|
||||
import { ContainerModel, FormFieldModel, FormFieldTypes, FormModel, TabModel } from '../components/widgets/core';
|
||||
import { WidgetVisibilityModel } from '../models/widget-visibility.model';
|
||||
import { WidgetVisibilityService } from './widget-visibility.service';
|
||||
import { FormService } from './form.service';
|
||||
import {
|
||||
fakeFormJson,
|
||||
formTest,
|
||||
@@ -50,6 +51,30 @@ describe('WidgetVisibilityService', () => {
|
||||
service = TestBed.inject(WidgetVisibilityService);
|
||||
});
|
||||
|
||||
it('should emit formVisibilityRefreshed when visibility is refreshed', () => {
|
||||
const formService = TestBed.inject(FormService);
|
||||
let emittedForm: FormModel | undefined;
|
||||
|
||||
formService.formVisibilityRefreshed.subscribe((event) => {
|
||||
emittedForm = event.form;
|
||||
});
|
||||
|
||||
service.refreshVisibility(stubFormWithFields);
|
||||
|
||||
expect(emittedForm).toBe(stubFormWithFields);
|
||||
});
|
||||
|
||||
it('should not emit formVisibilityRefreshed when form is null', () => {
|
||||
const formService = TestBed.inject(FormService);
|
||||
let emitCount = 0;
|
||||
|
||||
formService.formVisibilityRefreshed.subscribe(() => emitCount++);
|
||||
|
||||
service.refreshVisibility(null);
|
||||
|
||||
expect(emitCount).toBe(0);
|
||||
});
|
||||
|
||||
describe('should be able to evaluate next condition operations', () => {
|
||||
it('using == and return true', () => {
|
||||
const resultsArray = evaluateConditions(
|
||||
|
||||
@@ -15,16 +15,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { FormFieldModel, FormModel, TabModel, ContainerModel, FormOutcomeModel } from '../components/widgets/core';
|
||||
import { FormEvent } from '../events/form.event';
|
||||
import { TaskProcessVariableModel } from '../models/task-process-variable.model';
|
||||
import { WidgetVisibilityModel, WidgetTypeEnum } from '../models/widget-visibility.model';
|
||||
import { format, isValid, parse } from 'date-fns';
|
||||
import { FormService } from './form.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class WidgetVisibilityService {
|
||||
private readonly formService = inject(FormService);
|
||||
|
||||
private processVarList: TaskProcessVariableModel[];
|
||||
private form: FormModel;
|
||||
|
||||
@@ -45,6 +49,8 @@ export class WidgetVisibilityService {
|
||||
}
|
||||
|
||||
form.getFormFields().map((field) => this.refreshEntityVisibility(field));
|
||||
|
||||
this.formService.formVisibilityRefreshed.next(new FormEvent(form));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ $mat-button: '.mat-mdc-button';
|
||||
$mat-button-label: '.mdc-button__label';
|
||||
$mat-form-field: '.mat-mdc-form-field';
|
||||
$mat-form-field-wrapper: '.mat-mdc-text-field-wrapper';
|
||||
$mat-form-field-subscript-wrapper: '.mat-mdc-form-field-subscript-wrapper';
|
||||
$mat-line-ripple: '.mdc-line-ripple';
|
||||
$mat-form-field-prefix: '.mat-mdc-form-field-text-prefix';
|
||||
$mat-form-field-suffix: '.mat-mdc-form-field-text-suffix';
|
||||
|
||||
@@ -23,7 +23,7 @@ module.exports = function (config) {
|
||||
coverageReporter: {
|
||||
dir: join(__dirname, '../../coverage/extensions'),
|
||||
subdir: '.',
|
||||
reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
check: {
|
||||
global: {
|
||||
statements: 75,
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
"url": "https://github.com/Alfresco/alfresco-ng2-components/issues"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=20.3.25",
|
||||
"@angular/core": ">=20.3.25",
|
||||
"@angular/common": ">=20.3.27",
|
||||
"@angular/core": ">=20.3.27",
|
||||
"@alfresco/js-api": ">=10.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@@ -44,7 +44,7 @@ module.exports = function (config) {
|
||||
coverageReporter: {
|
||||
dir: join(__dirname, '../../coverage/insights'),
|
||||
subdir: '.',
|
||||
reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
check: {
|
||||
global: {
|
||||
statements: 75,
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
"raphael": ">=2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=20.3.25",
|
||||
"@angular/compiler": ">=20.3.25",
|
||||
"@angular/core": ">=20.3.25",
|
||||
"@angular/forms": ">=20.3.25",
|
||||
"@angular/common": ">=20.3.27",
|
||||
"@angular/compiler": ">=20.3.27",
|
||||
"@angular/core": ">=20.3.27",
|
||||
"@angular/forms": ">=20.3.27",
|
||||
"@angular/material": ">=20.2.14",
|
||||
"@alfresco/adf-core": ">=9.0.0",
|
||||
"@alfresco/adf-content-services": ">=9.0.0",
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Chart } from './chart.model';
|
||||
|
||||
describe('Chart Model', () => {
|
||||
describe('constructor', () => {
|
||||
it('should create with default values when no argument is provided', () => {
|
||||
const chart = new Chart();
|
||||
expect(chart.labels).toEqual([]);
|
||||
expect(chart.data).toEqual([]);
|
||||
expect(chart.datasets).toEqual([]);
|
||||
expect(chart.showDetails).toBe(false);
|
||||
});
|
||||
|
||||
it('should populate properties from input object', () => {
|
||||
const chart = new Chart({
|
||||
id: '1',
|
||||
title: 'Test Chart',
|
||||
titleKey: 'KEY',
|
||||
labels: ['a', 'b'],
|
||||
data: [1, 2],
|
||||
datasets: [{ data: [1] }],
|
||||
showDetails: true,
|
||||
detailsTable: { key: 'value' },
|
||||
options: { responsive: true }
|
||||
});
|
||||
|
||||
expect(chart.id).toBe('1');
|
||||
expect(chart.title).toBe('Test Chart');
|
||||
expect(chart.titleKey).toBe('KEY');
|
||||
expect(chart.labels).toEqual(['a', 'b']);
|
||||
expect(chart.data).toEqual([1, 2]);
|
||||
expect(chart.datasets).toEqual([{ data: [1] }]);
|
||||
expect(chart.showDetails).toBe(true);
|
||||
expect(chart.detailsTable).toEqual({ key: 'value' });
|
||||
expect(chart.options).toEqual({ responsive: true });
|
||||
});
|
||||
|
||||
it('should convert type and set icon for pieChart', () => {
|
||||
const chart = new Chart({ type: 'pieChart' });
|
||||
expect(chart.type).toBe('pie');
|
||||
expect(chart.icon).toBe('pie_chart');
|
||||
});
|
||||
|
||||
it('should convert type and set icon for barChart', () => {
|
||||
const chart = new Chart({ type: 'barChart' });
|
||||
expect(chart.type).toBe('bar');
|
||||
expect(chart.icon).toBe('equalizer');
|
||||
});
|
||||
|
||||
it('should convert type and set icon for line', () => {
|
||||
const chart = new Chart({ type: 'line' });
|
||||
expect(chart.type).toBe('line');
|
||||
expect(chart.icon).toBe('show_chart');
|
||||
});
|
||||
|
||||
it('should convert type and set icon for table', () => {
|
||||
const chart = new Chart({ type: 'table' });
|
||||
expect(chart.type).toBe('table');
|
||||
expect(chart.icon).toBe('web');
|
||||
});
|
||||
|
||||
it('should convert type and set icon for multiBarChart', () => {
|
||||
const chart = new Chart({ type: 'multiBarChart' });
|
||||
expect(chart.type).toBe('multiBar');
|
||||
expect(chart.icon).toBe('poll');
|
||||
});
|
||||
|
||||
it('should convert type and set icon for processDefinitionHeatMap', () => {
|
||||
const chart = new Chart({ type: 'processDefinitionHeatMap' });
|
||||
expect(chart.type).toBe('HeatMap');
|
||||
expect(chart.icon).toBe('share');
|
||||
});
|
||||
|
||||
it('should convert type and set icon for masterDetailTable', () => {
|
||||
const chart = new Chart({ type: 'masterDetailTable' });
|
||||
expect(chart.type).toBe('masterDetailTable');
|
||||
expect(chart.icon).toBe('subtitles');
|
||||
});
|
||||
|
||||
it('should default to table type for unknown types', () => {
|
||||
const chart = new Chart({ type: 'unknown' });
|
||||
expect(chart.type).toBe('table');
|
||||
expect(chart.icon).toBe('web');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasData', () => {
|
||||
it('should return true when data is not empty', () => {
|
||||
const chart = new Chart({ data: [1, 2, 3] });
|
||||
expect(chart.hasData()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when data is empty', () => {
|
||||
const chart = new Chart({ data: [] });
|
||||
expect(chart.hasData()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when no data is provided', () => {
|
||||
const chart = new Chart();
|
||||
expect(chart.hasData()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasDatasets', () => {
|
||||
it('should return true when datasets is not empty', () => {
|
||||
const chart = new Chart({ datasets: [{ data: [1] }] });
|
||||
expect(chart.hasDatasets()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when datasets is empty', () => {
|
||||
const chart = new Chart({ datasets: [] });
|
||||
expect(chart.hasDatasets()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasZeroValues', () => {
|
||||
it('should return true when all data values are zero', () => {
|
||||
const chart = new Chart({ data: [0, 0, 0] });
|
||||
expect(chart.hasZeroValues()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when at least one value is non-zero', () => {
|
||||
const chart = new Chart({ data: [0, 1, 0] });
|
||||
expect(chart.hasZeroValues()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when data is empty', () => {
|
||||
const chart = new Chart({ data: [] });
|
||||
expect(chart.hasZeroValues()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -48,7 +48,7 @@ module.exports = function (config) {
|
||||
coverageReporter: {
|
||||
dir: join(__dirname, '../../coverage/process-services-cloud'),
|
||||
subdir: '.',
|
||||
reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
check: {
|
||||
global: {
|
||||
statements: 75,
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/cdk": ">=20.2.14",
|
||||
"@angular/common": ">=20.3.25",
|
||||
"@angular/compiler": ">=20.3.25",
|
||||
"@angular/core": ">=20.3.25",
|
||||
"@angular/forms": ">=20.3.25",
|
||||
"@angular/common": ">=20.3.27",
|
||||
"@angular/compiler": ">=20.3.27",
|
||||
"@angular/core": ">=20.3.27",
|
||||
"@angular/forms": ">=20.3.27",
|
||||
"@angular/material": ">=20.2.14",
|
||||
"@angular/platform-browser": ">=20.3.25",
|
||||
"@angular/platform-browser-dynamic": ">=20.3.25",
|
||||
"@angular/router": ">=20.3.25",
|
||||
"@angular/platform-browser": ">=20.3.27",
|
||||
"@angular/platform-browser-dynamic": ">=20.3.27",
|
||||
"@angular/router": ">=20.3.27",
|
||||
"@alfresco/js-api": ">=10.0.0",
|
||||
"@alfresco/adf-core": ">=9.0.0",
|
||||
"@alfresco/adf-content-services": ">=9.0.0",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import { VersionCompatibilityService, AlfrescoApiService } from '@alfresco/adf-content-services';
|
||||
import {
|
||||
ADF_DISPLAY_TEXT_SETTINGS,
|
||||
ContentLinkModel,
|
||||
CoreModule,
|
||||
FormFieldModel,
|
||||
@@ -31,6 +32,8 @@ import {
|
||||
provideTranslations,
|
||||
AuthModule,
|
||||
FormFieldEvent,
|
||||
FormEvent,
|
||||
FormRulesEvent,
|
||||
NoopTranslateModule,
|
||||
NoopAuthModule,
|
||||
FORM_FIELD_VALIDATORS
|
||||
@@ -117,7 +120,8 @@ describe('FormCloudComponent', () => {
|
||||
useValue: {}
|
||||
},
|
||||
{ provide: FormRenderingService, useClass: CloudFormRenderingService },
|
||||
{ provide: FORM_CLOUD_FIELD_VALIDATORS_TOKEN, useValue: [fakeValidator] }
|
||||
{ provide: FORM_CLOUD_FIELD_VALIDATORS_TOKEN, useValue: [fakeValidator] },
|
||||
{ provide: ADF_DISPLAY_TEXT_SETTINGS, useValue: { enableExpressionEvaluation: true } }
|
||||
]
|
||||
});
|
||||
const apiService = TestBed.inject(AlfrescoApiService);
|
||||
@@ -874,6 +878,39 @@ describe('FormCloudComponent', () => {
|
||||
expect(savedForm).toEqual(formModel);
|
||||
});
|
||||
|
||||
it('should materialize unrendered rich text expressions when saving a task form', () => {
|
||||
spyOn(formCloudService, 'saveTaskForm').and.returnValue(of(undefined));
|
||||
const formModel = new FormModel({
|
||||
id: '23',
|
||||
taskId: '123-223',
|
||||
fields: [
|
||||
{
|
||||
id: 'richText',
|
||||
type: FormFieldTypes.DISPLAY_RICH_TEXT,
|
||||
value: { blocks: [{ type: 'paragraph', data: { text: 'Hello ${field.name}' } }] }
|
||||
},
|
||||
{ id: 'name', type: FormFieldTypes.TEXT, value: 'John' }
|
||||
]
|
||||
});
|
||||
const originalValues = JSON.parse(JSON.stringify(formModel.values));
|
||||
formComponent.form = formModel;
|
||||
formComponent.taskId = formModel.taskId;
|
||||
formComponent.appName = 'test-app';
|
||||
|
||||
formComponent.saveTaskForm();
|
||||
|
||||
expect(formCloudService.saveTaskForm).toHaveBeenCalledWith(
|
||||
'test-app',
|
||||
formModel.taskId,
|
||||
undefined,
|
||||
formModel.id,
|
||||
jasmine.objectContaining({
|
||||
richText: { blocks: [{ type: 'paragraph', data: { text: 'Hello John' } }] }
|
||||
})
|
||||
);
|
||||
expect(formModel.values).toEqual(originalValues);
|
||||
});
|
||||
|
||||
it('should handle error during form save', () => {
|
||||
const error = 'Error';
|
||||
spyOn(formCloudService, 'saveTaskForm').and.callFake(() => throwError(error));
|
||||
@@ -981,6 +1018,39 @@ describe('FormCloudComponent', () => {
|
||||
expect(completedForm).toBe(formComponent.form);
|
||||
});
|
||||
|
||||
it('should materialize unrendered rich text expressions when completing a task form', () => {
|
||||
spyOn(formCloudService, 'completeTaskForm').and.returnValue(of(undefined));
|
||||
const formModel = new FormModel({
|
||||
id: '23',
|
||||
taskId: '123-223',
|
||||
fields: [
|
||||
{
|
||||
id: 'richText',
|
||||
type: FormFieldTypes.DISPLAY_RICH_TEXT,
|
||||
value: { blocks: [{ type: 'paragraph', data: { text: '${field.name}' } }] }
|
||||
},
|
||||
{ id: 'name', type: FormFieldTypes.TEXT, value: 'John' }
|
||||
]
|
||||
});
|
||||
formComponent.form = formModel;
|
||||
formComponent.taskId = formModel.taskId;
|
||||
formComponent.appName = 'test-app';
|
||||
|
||||
formComponent.completeTaskForm('complete');
|
||||
|
||||
expect(formCloudService.completeTaskForm).toHaveBeenCalledWith(
|
||||
'test-app',
|
||||
formModel.taskId,
|
||||
undefined,
|
||||
formModel.id,
|
||||
jasmine.objectContaining({
|
||||
richText: { blocks: [{ type: 'paragraph', data: { text: 'John' } }] }
|
||||
}),
|
||||
'complete',
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it('should open confirmation dialog on complete task', async () => {
|
||||
formComponent.form = new FormModel({
|
||||
confirmMessage: {
|
||||
@@ -1231,6 +1301,38 @@ describe('FormCloudComponent', () => {
|
||||
expect(formComponent.visibleOutcomes).toEqual([]);
|
||||
});
|
||||
|
||||
it('should recompute visibleOutcomes when form visibility is refreshed', () => {
|
||||
formComponent.showCompleteButton = true;
|
||||
const formModel = new FormModel(cloudFormMock);
|
||||
formComponent.form = formModel;
|
||||
|
||||
expect(formComponent.visibleOutcomes.length).toBeGreaterThan(0);
|
||||
|
||||
formModel.outcomes.forEach((outcome) => {
|
||||
outcome.isVisible = false;
|
||||
});
|
||||
|
||||
TestBed.inject(FormService).formVisibilityRefreshed.next(new FormEvent(formModel));
|
||||
|
||||
expect(formComponent.visibleOutcomes).toEqual([]);
|
||||
});
|
||||
|
||||
it('should recompute visibleOutcomes when fieldValueChanged rule event fires', () => {
|
||||
formComponent.showCompleteButton = true;
|
||||
const formModel = new FormModel(cloudFormMock);
|
||||
formComponent.form = formModel;
|
||||
|
||||
expect(formComponent.visibleOutcomes.length).toBeGreaterThan(0);
|
||||
|
||||
formModel.outcomes.forEach((outcome) => {
|
||||
outcome.isVisible = false;
|
||||
});
|
||||
|
||||
TestBed.inject(FormService).formRulesEvent.next(new FormRulesEvent('fieldValueChanged', new FormEvent(formModel)));
|
||||
|
||||
expect(formComponent.visibleOutcomes).toEqual([]);
|
||||
});
|
||||
|
||||
it('should raise [executeOutcome] event for formService', async () => {
|
||||
spyOn(formComponent.executeOutcome, 'emit');
|
||||
|
||||
|
||||
@@ -30,14 +30,17 @@ import {
|
||||
SimpleChanges,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { forkJoin, isObservable, Observable, of, Subscription } from 'rxjs';
|
||||
import { forkJoin, isObservable, merge, Observable, of, Subscription } from 'rxjs';
|
||||
import { filter, map, switchMap } from 'rxjs/operators';
|
||||
import {
|
||||
ConfirmDialogComponent,
|
||||
ContentLinkModel,
|
||||
ADF_DISPLAY_TEXT_SETTINGS,
|
||||
DisplayTextWidgetSettings,
|
||||
FormatSpacePipe,
|
||||
FormBaseComponent,
|
||||
FormEvent,
|
||||
FormExpressionService,
|
||||
FormFieldModel,
|
||||
FormRulesEvent,
|
||||
FormFieldValidator,
|
||||
@@ -67,6 +70,7 @@ import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { A11yModule } from '@angular/cdk/a11y';
|
||||
import { getExpressionEvaluationEnabled$, materializeSubmissionValues } from '../services/form-cloud-submission-values';
|
||||
|
||||
interface FormFieldRuntimeState {
|
||||
value: any;
|
||||
@@ -228,6 +232,8 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
|
||||
protected changeDetector = inject(ChangeDetectorRef);
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly expressions = inject(FormExpressionService);
|
||||
private enableExpressionEvaluation = false;
|
||||
|
||||
private get currentForm(): FormModel | undefined {
|
||||
return super.form;
|
||||
@@ -252,6 +258,9 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
|
||||
constructor() {
|
||||
const injectedFieldValidators = inject(FORM_CLOUD_FIELD_VALIDATORS_TOKEN, { optional: true });
|
||||
const tabNavEnabledToken = inject(ADF_FORM_TAB_NAV_ENABLED, { optional: true });
|
||||
const displayTextSettings = inject<Observable<DisplayTextWidgetSettings> | DisplayTextWidgetSettings>(ADF_DISPLAY_TEXT_SETTINGS, {
|
||||
optional: true
|
||||
});
|
||||
|
||||
super();
|
||||
this.loadInjectedFieldValidators(injectedFieldValidators);
|
||||
@@ -270,6 +279,12 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
|
||||
}
|
||||
}
|
||||
|
||||
getExpressionEvaluationEnabled$(displayTextSettings)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((enabled) => {
|
||||
this.enableExpressionEvaluation = enabled;
|
||||
});
|
||||
|
||||
this.formService.formContentClicked.pipe(takeUntilDestroyed()).subscribe((content) => {
|
||||
if (content instanceof UploadWidgetContentLinkModel) {
|
||||
this.form.setNodeIdValueForViewersLinkedToUploadWidget(content);
|
||||
@@ -291,11 +306,11 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
|
||||
}
|
||||
});
|
||||
|
||||
this.formService.formRulesEvent
|
||||
.pipe(
|
||||
filter((event) => event?.type === 'fieldValueChanged' && event.form?.id === this.form?.id),
|
||||
takeUntilDestroyed()
|
||||
merge(
|
||||
this.formService.formVisibilityRefreshed.pipe(filter((event) => event.form?.id === this.form?.id)),
|
||||
this.formService.formRulesEvent.pipe(filter((event) => event?.type === 'fieldValueChanged' && event.form?.id === this.form?.id))
|
||||
)
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe(() => this.recomputeVisibleOutcomes());
|
||||
}
|
||||
|
||||
@@ -482,7 +497,7 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
|
||||
saveTaskForm() {
|
||||
if (this.form && this.appName && this.taskId) {
|
||||
this.formCloudService
|
||||
.saveTaskForm(this.appName, this.taskId, this.processInstanceId, `${this.form.id}`, this.form.values)
|
||||
.saveTaskForm(this.appName, this.taskId, this.processInstanceId, `${this.form.id}`, this.getSubmissionValues())
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
@@ -523,7 +538,15 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
|
||||
private completeForm(outcome?: string, outcomeId?: string) {
|
||||
if (this.form && this.appName && this.taskId) {
|
||||
this.formCloudService
|
||||
.completeTaskForm(this.appName, this.taskId, this.processInstanceId, `${this.form.id}`, this.form.values, outcome, this.appVersion)
|
||||
.completeTaskForm(
|
||||
this.appName,
|
||||
this.taskId,
|
||||
this.processInstanceId,
|
||||
`${this.form.id}`,
|
||||
this.getSubmissionValues(),
|
||||
outcome,
|
||||
this.appVersion
|
||||
)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
@@ -536,6 +559,10 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
|
||||
}
|
||||
}
|
||||
|
||||
private getSubmissionValues(): FormValues {
|
||||
return materializeSubmissionValues(this.form, { enableExpressionEvaluation: this.enableExpressionEvaluation }, this.expressions);
|
||||
}
|
||||
|
||||
parseForm(formCloudRepresentationJSON?: any): FormModel | null {
|
||||
if (formCloudRepresentationJSON) {
|
||||
const formValues: FormValues = {};
|
||||
@@ -568,7 +595,6 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
|
||||
checkVisibility(field: FormFieldModel) {
|
||||
if (field?.form) {
|
||||
this.visibilityService.refreshVisibility(field.form);
|
||||
this.recomputeVisibleOutcomes();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,7 +612,6 @@ export class FormCloudComponent extends FormBaseComponent implements OnChanges,
|
||||
this.setCheckParentVisibilityForValidationOnFields();
|
||||
this.visibilityService.refreshVisibility(this.form);
|
||||
this.form.validateForm();
|
||||
this.recomputeVisibleOutcomes();
|
||||
this.onFormLoaded(this.form);
|
||||
this.formService.formRulesEvent.next(new FormRulesEvent('dataRefreshed', new FormEvent(this.form)));
|
||||
this.onFormDataRefreshed(this.form);
|
||||
|
||||
+18
-10
@@ -1,21 +1,26 @@
|
||||
<div class="adf-attach-file-widget-container">
|
||||
<div class="adf-attach-widget {{field.className}}"
|
||||
[class.adf-readonly]="field.readOnly">
|
||||
<label class="adf-label" [attr.for]="field.id + '-label'">{{field.name}}
|
||||
<span class="adf-asterisk" *ngIf="isRequired()">*</span>
|
||||
<div class="adf-attach-widget {{ field.className }}" [class.adf-readonly]="field.readOnly">
|
||||
<label class="adf-label" [attr.for]="field.id + '-label'"
|
||||
>{{ field.name }}
|
||||
@if (isRequired()) {
|
||||
<span class="adf-asterisk">*</span>
|
||||
}
|
||||
</label>
|
||||
<div class="adf-attach-widget-container" (focusout)="markAsTouched()">
|
||||
<div class="adf-attach-widget__menu-upload" *ngIf="isUploadButtonVisible()">
|
||||
@if (isUploadButtonVisible()) {
|
||||
<div class="adf-attach-widget__menu-upload">
|
||||
<button
|
||||
(click)="openSelectDialog()"
|
||||
mat-raised-button
|
||||
class="adf-attach-widget__menu-upload__button"
|
||||
[id]="field.id"
|
||||
[title]="field.tooltip">
|
||||
[title]="field.tooltip"
|
||||
>
|
||||
{{ 'FORM.FIELD.ATTACH' | translate }}
|
||||
<mat-icon class="adf-attach-widget__menu-upload__button__icon" [adf-icon]="getWidgetIcon()" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,12 +39,15 @@
|
||||
(contentModelFileHandler)="contentModelFormFileHandler($event)"
|
||||
(removeAttachFile)="onRemoveAttachFile($event)"
|
||||
/>
|
||||
<div *ngIf="!hasFile && field.readOnly" id="{{'adf-attach-empty-list-'+field.id}}">
|
||||
@if (!hasFile && field.readOnly) {
|
||||
<div id="{{ 'adf-attach-empty-list-' + field.id }}">
|
||||
{{ 'FORM.FIELD.NO_FILE_ATTACHED' | translate }}
|
||||
</div>
|
||||
|
||||
}
|
||||
</div>
|
||||
|
||||
<error-widget [error]="field.validationSummary" />
|
||||
<error-widget *ngIf="!field.isValid && isTouched() && !isSelected()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}" />
|
||||
<error-widget
|
||||
[error]="field.validationSummary"
|
||||
[required]="!field.isValid && isTouched() && !isSelected() ? ('FORM.FIELD.REQUIRED' | translate) : ''"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+8
-16
@@ -1,31 +1,23 @@
|
||||
<div class="adf-data-table-widget-container">
|
||||
<div class="adf-data-table-widget-label">
|
||||
<label
|
||||
class="adf-label"
|
||||
[class.adf-left-label]="field.leftLabels"
|
||||
[attr.for]="field.id">
|
||||
{{field.name | translate }}
|
||||
</label>
|
||||
<label class="adf-label" [class.adf-left-label]="field.leftLabels" [attr.for]="field.id"> {{field.name | translate }} </label>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="!previewState; else previewTemplate">
|
||||
@if (!previewState) {
|
||||
<adf-datatable data-automation-id="adf-data-table-widget" [data]="dataSource">
|
||||
<adf-no-content-template>
|
||||
<ng-template>
|
||||
<adf-empty-content
|
||||
icon="border_all"
|
||||
[title]="'FORM.FIELD.DATA_TABLE_EMPTY_CONTENT' | translate" />
|
||||
<adf-empty-content icon="border_all" [title]="'FORM.FIELD.DATA_TABLE_EMPTY_CONTENT' | translate" />
|
||||
</ng-template>
|
||||
</adf-no-content-template>
|
||||
</adf-datatable>
|
||||
|
||||
<error-widget *ngIf="dataTableLoadFailed"
|
||||
<error-widget
|
||||
class="adf-data-table-widget-failed-message"
|
||||
[required]="'FORM.FIELD.DATA_TABLE_LOAD_FAILED' | translate" />
|
||||
</ng-container>
|
||||
|
||||
<ng-template #previewTemplate>
|
||||
[required]="dataTableLoadFailed ? ('FORM.FIELD.DATA_TABLE_LOAD_FAILED' | translate) : ''"
|
||||
/>
|
||||
} @else {
|
||||
<adf-datatable data-automation-id="adf-data-table-widget-preview" />
|
||||
<div class="adf-preview-placeholder"></div>
|
||||
</ng-template>
|
||||
}
|
||||
</div>
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
.adf-data-table-widget-failed-message {
|
||||
display: block;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -283,7 +283,8 @@ describe('DataTableWidgetComponent', () => {
|
||||
const failedErrorMsgElement = fixture.debugElement.query(By.css('.adf-data-table-widget-failed-message'));
|
||||
|
||||
assertData(mockCountryColumns, []);
|
||||
expect(failedErrorMsgElement).toBeNull();
|
||||
expect(failedErrorMsgElement).toBeTruthy();
|
||||
expect(failedErrorMsgElement.nativeElement.textContent.trim()).toBe('');
|
||||
});
|
||||
|
||||
it('path points to single object with appropriate schema definition', () => {
|
||||
@@ -294,7 +295,8 @@ describe('DataTableWidgetComponent', () => {
|
||||
const failedErrorMsgElement = fixture.debugElement.query(By.css('.adf-data-table-widget-failed-message'));
|
||||
|
||||
assertData(mockCountryColumns, [mockEuropeCountriesRows[1]]);
|
||||
expect(failedErrorMsgElement).toBeNull();
|
||||
expect(failedErrorMsgElement).toBeTruthy();
|
||||
expect(failedErrorMsgElement.nativeElement.textContent.trim()).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+1
-2
@@ -27,7 +27,6 @@ import {
|
||||
NoContentTemplateDirective,
|
||||
EmptyContentComponent
|
||||
} from '@alfresco/adf-core';
|
||||
import { NgIf } from '@angular/common';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { FormCloudService } from '../../../services/form-cloud.service';
|
||||
import { TaskVariableCloud } from '../../../models/task-variable-cloud.model';
|
||||
@@ -36,7 +35,7 @@ import { DataTablePathParserHelper } from './helpers/data-table-path-parser.help
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [NgIf, TranslatePipe, FormBaseModule, DataTableComponent, NoContentTemplateDirective, EmptyContentComponent],
|
||||
imports: [TranslatePipe, FormBaseModule, DataTableComponent, NoContentTemplateDirective, EmptyContentComponent],
|
||||
selector: 'data-table',
|
||||
templateUrl: './data-table.widget.html',
|
||||
styleUrls: ['./data-table.widget.scss'],
|
||||
|
||||
+45
@@ -200,6 +200,51 @@ describe('DisplayRichTextWidgetComponent', () => {
|
||||
expect(widget.field.value.blocks[0].data.text).toBe('Hello John');
|
||||
});
|
||||
|
||||
it('should resolve from authored value after saved data rehydrates the field', () => {
|
||||
const form = new FormModel(
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
id: 'richText1',
|
||||
name: 'richText1',
|
||||
type: 'display-rich-text',
|
||||
value: {
|
||||
blocks: [{ type: 'paragraph', data: { text: 'Hello ${field.name}' } }]
|
||||
}
|
||||
},
|
||||
{ id: 'name', name: 'name', type: 'text', value: 'John' }
|
||||
]
|
||||
},
|
||||
{
|
||||
richText1: {
|
||||
blocks: [{ type: 'paragraph', data: { text: 'Hello John' } }]
|
||||
},
|
||||
name: 'Jane'
|
||||
}
|
||||
);
|
||||
|
||||
widget.field = form.getFieldById('richText1');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value.blocks[0].data.text).toBe('Hello Jane');
|
||||
});
|
||||
|
||||
it('should preserve the current value when the authored value is unavailable', () => {
|
||||
const form = new FormModel({
|
||||
fields: [{ id: 'richText1', type: 'display-rich-text' }]
|
||||
});
|
||||
const currentValue = {
|
||||
blocks: [{ type: 'paragraph', data: { text: 'Current value' } }]
|
||||
};
|
||||
const field = form.getFieldById('richText1');
|
||||
field.value = currentValue;
|
||||
widget.field = field;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(widget.field.value).toBe(currentValue);
|
||||
});
|
||||
|
||||
it('should resolve expressions in multiple blocks', () => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
|
||||
+10
-13
@@ -22,6 +22,7 @@ import { BaseDisplayTextWidgetComponent } from '@alfresco/adf-core';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { RichTextParserService } from '../../../services/rich-text-parser.service';
|
||||
import { resolveRichTextExpressions } from './rich-text-expression-resolver';
|
||||
|
||||
export const RICH_TEXT_PARSER_TOKEN = new InjectionToken<RichTextParserService>('RichTextParserService', {
|
||||
factory: () => new RichTextParserService()
|
||||
@@ -66,7 +67,10 @@ export class DisplayRichTextWidgetComponent extends BaseDisplayTextWidgetCompone
|
||||
|
||||
protected storeOriginalValue(): void {
|
||||
if (this.field) {
|
||||
this.originalFieldValue = JSON.stringify(this.field.value);
|
||||
const authoredValue = this.field.authoredValue;
|
||||
if (authoredValue !== undefined) {
|
||||
this.originalFieldValue = JSON.stringify(authoredValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +79,10 @@ export class DisplayRichTextWidgetComponent extends BaseDisplayTextWidgetCompone
|
||||
return;
|
||||
}
|
||||
|
||||
const value = JSON.parse(JSON.stringify(this.field.value));
|
||||
this.applyExpressionsToBlocks(value);
|
||||
const authoredValue = this.field.authoredValue;
|
||||
if (authoredValue !== undefined) {
|
||||
this.applyExpressionsToBlocks(authoredValue);
|
||||
}
|
||||
}
|
||||
|
||||
protected reevaluateExpressions(): void {
|
||||
@@ -89,16 +95,7 @@ export class DisplayRichTextWidgetComponent extends BaseDisplayTextWidgetCompone
|
||||
}
|
||||
|
||||
private applyExpressionsToBlocks(value: any): void {
|
||||
for (const block of value.blocks) {
|
||||
if (block.type === 'list') {
|
||||
for (const item of block.data.items) {
|
||||
item.content = this.resolveExpressions(item.content, true);
|
||||
}
|
||||
} else {
|
||||
block.data.text = this.resolveExpressions(block.data.text, true);
|
||||
}
|
||||
}
|
||||
this.field.value = value;
|
||||
this.field.value = resolveRichTextExpressions(value, (content) => this.resolveExpressions(content, true));
|
||||
}
|
||||
|
||||
private parseAndSanitize(): void {
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { resolveRichTextExpressions } from './rich-text-expression-resolver';
|
||||
|
||||
describe('resolveRichTextExpressions', () => {
|
||||
const resolve = (value: string) => value.replaceAll('${field.name}', 'John').replaceAll('${variable.status}', 'Active');
|
||||
|
||||
it('should resolve supported rich text content without mutating the input', () => {
|
||||
const value = {
|
||||
time: 1,
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello ${field.name}',
|
||||
caption: 'Status: ${variable.status}',
|
||||
content: [
|
||||
['Cell ${field.name}'],
|
||||
{
|
||||
label: '${variable.status}'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
data: {
|
||||
items: [
|
||||
{
|
||||
content: '${field.name}',
|
||||
items: [{ content: '${variable.status}' }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
version: '2.30.0'
|
||||
};
|
||||
const originalValue = JSON.parse(JSON.stringify(value));
|
||||
|
||||
const result = resolveRichTextExpressions(value, resolve);
|
||||
|
||||
expect(result).toEqual({
|
||||
time: 1,
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello John',
|
||||
caption: 'Status: Active',
|
||||
content: [['Cell John'], { label: 'Active' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
data: {
|
||||
items: [{ content: 'John', items: [{ content: 'Active' }] }]
|
||||
}
|
||||
}
|
||||
],
|
||||
version: '2.30.0'
|
||||
});
|
||||
expect(value).toEqual(originalValue);
|
||||
expect(result).not.toBe(value);
|
||||
});
|
||||
|
||||
it('should resolve a caller-owned clone without cloning it again', () => {
|
||||
const value = {
|
||||
blocks: [{ type: 'paragraph', data: { text: 'Hello ${field.name}' } }]
|
||||
};
|
||||
|
||||
const result = resolveRichTextExpressions(value, resolve, { cloneValue: false }) as typeof value;
|
||||
|
||||
expect(result).toBe(value);
|
||||
expect(result.blocks[0].data.text).toBe('Hello John');
|
||||
});
|
||||
|
||||
it('should preserve unknown blocks and properties', () => {
|
||||
const value = {
|
||||
blocks: [
|
||||
{
|
||||
type: 'custom',
|
||||
data: {
|
||||
label: '${field.name}'
|
||||
},
|
||||
metadata: '${variable.status}'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
expect(resolveRichTextExpressions(value, resolve)).toEqual(value);
|
||||
});
|
||||
|
||||
it('should not introduce missing content properties', () => {
|
||||
const result = resolveRichTextExpressions({ blocks: [{ type: 'paragraph', data: {} }] }, resolve) as {
|
||||
blocks: Array<{ data: Record<string, unknown> }>;
|
||||
};
|
||||
|
||||
expect(result.blocks[0].data).toEqual({});
|
||||
});
|
||||
|
||||
it('should return malformed values unchanged', () => {
|
||||
const malformedValues = [null, undefined, 'text', [], {}, { blocks: null }];
|
||||
|
||||
malformedValues.forEach((value) => {
|
||||
expect(resolveRichTextExpressions(value, resolve)).toBe(value);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return non-cloneable values without mutating them', () => {
|
||||
const value: {
|
||||
blocks: Array<{ data: { text: string } }>;
|
||||
self?: unknown;
|
||||
} = {
|
||||
blocks: [{ data: { text: '${field.name}' } }]
|
||||
};
|
||||
value.self = value;
|
||||
|
||||
expect(resolveRichTextExpressions(value, resolve)).toBe(value);
|
||||
expect(value.blocks[0].data.text).toBe('${field.name}');
|
||||
});
|
||||
});
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
type JsonObject = Record<string, unknown>;
|
||||
|
||||
export type RichTextExpressionResolver = (value: string) => string;
|
||||
|
||||
export interface RichTextExpressionResolverOptions {
|
||||
cloneValue?: boolean;
|
||||
}
|
||||
|
||||
const isJsonObject = (value: unknown): value is JsonObject => typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
|
||||
const cloneJsonValue = (value: unknown): unknown => {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const resolveNestedContent = (value: unknown, resolve: RichTextExpressionResolver): unknown => {
|
||||
if (typeof value === 'string') {
|
||||
return resolve(value);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => resolveNestedContent(entry, resolve));
|
||||
}
|
||||
|
||||
if (isJsonObject(value)) {
|
||||
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, resolveNestedContent(entry, resolve)]));
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const resolveListItems = (items: unknown, resolve: RichTextExpressionResolver): unknown => {
|
||||
if (!Array.isArray(items)) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return items.map((item) => {
|
||||
if (!isJsonObject(item)) {
|
||||
return item;
|
||||
}
|
||||
|
||||
if (Object.hasOwn(item, 'content')) {
|
||||
item.content = resolveNestedContent(item.content, resolve);
|
||||
}
|
||||
|
||||
if (Object.hasOwn(item, 'items')) {
|
||||
item.items = resolveListItems(item.items, resolve);
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
};
|
||||
|
||||
export const resolveRichTextExpressions = (
|
||||
value: unknown,
|
||||
resolve: RichTextExpressionResolver,
|
||||
options: RichTextExpressionResolverOptions = {}
|
||||
): unknown => {
|
||||
if (!isJsonObject(value) || !Array.isArray(value.blocks)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const resolvedValue = options.cloneValue === false ? value : cloneJsonValue(value);
|
||||
if (!isJsonObject(resolvedValue) || !Array.isArray(resolvedValue.blocks)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
resolvedValue.blocks.forEach((block) => {
|
||||
if (!isJsonObject(block) || !isJsonObject(block.data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof block.data.text === 'string') {
|
||||
block.data.text = resolve(block.data.text);
|
||||
}
|
||||
|
||||
if (typeof block.data.caption === 'string') {
|
||||
block.data.caption = resolve(block.data.caption);
|
||||
}
|
||||
|
||||
if (Object.hasOwn(block.data, 'content')) {
|
||||
block.data.content = resolveNestedContent(block.data.content, resolve);
|
||||
}
|
||||
|
||||
if (block.type === 'list' && Object.hasOwn(block.data, 'items')) {
|
||||
block.data.items = resolveListItems(block.data.items, resolve);
|
||||
}
|
||||
});
|
||||
|
||||
return resolvedValue;
|
||||
};
|
||||
+3
-7
@@ -4,11 +4,13 @@
|
||||
[class.adf-readonly]="field.readOnly"
|
||||
[class.adf-left-label-input-container]="field.leftLabels"
|
||||
>
|
||||
<div *ngIf="field.leftLabels">
|
||||
@if (field.leftLabels) {
|
||||
<div>
|
||||
<label class="adf-label adf-left-label" [attr.for]="field.id"
|
||||
>{{field.name | translate }}<span class="adf-asterisk" [style.visibility]="isRequired() ? 'visible' : 'hidden'">*</span></label
|
||||
>
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
<adf-cloud-group
|
||||
[mode]="mode"
|
||||
@@ -24,11 +26,5 @@
|
||||
[attr.title]="field.tooltip"
|
||||
[label]="field.name | translate"
|
||||
/>
|
||||
<error-widget [error]="field.validationSummary" />
|
||||
<error-widget
|
||||
class="adf-dropdown-required-message"
|
||||
*ngIf="isInvalidFieldRequired() && isTouched()"
|
||||
required="{{ 'FORM.FIELD.REQUIRED' | translate }}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+3
-2
@@ -141,8 +141,9 @@ describe('GroupCloudWidgetComponent', () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(element.querySelector('.adf-error-text')).toBeTruthy();
|
||||
expect(element.querySelector('.adf-error-text').textContent).toContain('ADF_CLOUD_GROUPS.ERROR.NOT_FOUND');
|
||||
const errorMessages = element.querySelectorAll('.adf-error-text');
|
||||
expect(errorMessages.length).toBe(1);
|
||||
expect(errorMessages[0].textContent).toContain('ADF_CLOUD_GROUPS.ERROR.NOT_FOUND');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import { Component, DestroyRef, inject, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { ErrorWidgetComponent, WidgetComponent } from '@alfresco/adf-core';
|
||||
import { WidgetComponent } from '@alfresco/adf-core';
|
||||
import { UntypedFormControl } from '@angular/forms';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { ComponentSelectionMode } from '../../../../types';
|
||||
@@ -31,7 +31,7 @@ import { GroupCloudComponent } from '../../../../group/components/group-cloud.co
|
||||
|
||||
@Component({
|
||||
selector: 'group-cloud-widget',
|
||||
imports: [CommonModule, TranslatePipe, ErrorWidgetComponent, GroupCloudComponent],
|
||||
imports: [CommonModule, TranslatePipe, GroupCloudComponent],
|
||||
templateUrl: './group-cloud.widget.html',
|
||||
host: {
|
||||
'(click)': 'event($event)',
|
||||
|
||||
+10
-10
@@ -1,10 +1,16 @@
|
||||
<div class="adf-dropdown-widget {{field.className}}"
|
||||
<div
|
||||
class="adf-dropdown-widget {{field.className}}"
|
||||
[class.adf-invalid]="!field.isValid && isTouched()"
|
||||
[class.adf-readonly]="field.readOnly"
|
||||
[class.adf-left-label-input-container]="field.leftLabels">
|
||||
<div *ngIf="field.leftLabels">
|
||||
<label class="adf-label adf-left-label" [attr.for]="field.id">{{field.name | translate }}<span class="adf-asterisk" [style.visibility]="isRequired() ? 'visible' : 'hidden'">*</span></label>
|
||||
[class.adf-left-label-input-container]="field.leftLabels"
|
||||
>
|
||||
@if (field.leftLabels) {
|
||||
<div>
|
||||
<label class="adf-label adf-left-label" [attr.for]="field.id"
|
||||
>{{field.name | translate }}<span class="adf-asterisk" [style.visibility]="isRequired() ? 'visible' : 'hidden'">*</span></label
|
||||
>
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
<adf-cloud-people
|
||||
[preSelectUsers]="preSelectUsers"
|
||||
@@ -21,11 +27,5 @@
|
||||
[attr.title]="field.tooltip"
|
||||
[label]="field.name | translate"
|
||||
/>
|
||||
<error-widget [error]="field.validationSummary" />
|
||||
<error-widget
|
||||
class="adf-dropdown-required-message"
|
||||
*ngIf="isInvalidFieldRequired() && isTouched()"
|
||||
required="{{ 'FORM.FIELD.REQUIRED' | translate }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+3
-2
@@ -171,8 +171,9 @@ describe('PeopleCloudWidgetComponent', () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(element.querySelector('.adf-error-text')).toBeTruthy();
|
||||
expect(element.querySelector('.adf-error-text').textContent).toContain('ADF_CLOUD_USERS.ERROR.NOT_FOUND');
|
||||
const errorMessages = element.querySelectorAll('.adf-error-text');
|
||||
expect(errorMessages.length).toBe(1);
|
||||
expect(errorMessages[0].textContent).toContain('ADF_CLOUD_USERS.ERROR.NOT_FOUND');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+2
-3
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import { Component, DestroyRef, inject, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
|
||||
import { ErrorWidgetComponent, WidgetComponent } from '@alfresco/adf-core';
|
||||
import { WidgetComponent } from '@alfresco/adf-core';
|
||||
import { UntypedFormControl } from '@angular/forms';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { ComponentSelectionMode } from '../../../../types';
|
||||
@@ -27,13 +27,12 @@ import { ReactivePreselectionService } from '../reactive-preselection.service';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { PeopleCloudComponent } from '../../../../people/components/people-cloud.component';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
|
||||
/* eslint-disable @angular-eslint/component-selector */
|
||||
|
||||
@Component({
|
||||
selector: 'people-cloud-widget',
|
||||
imports: [CommonModule, TranslatePipe, ErrorWidgetComponent, PeopleCloudComponent, MatFormFieldModule],
|
||||
imports: [CommonModule, TranslatePipe, PeopleCloudComponent],
|
||||
templateUrl: './people-cloud.widget.html',
|
||||
host: {
|
||||
'(click)': 'event($event)',
|
||||
|
||||
-1
@@ -11,7 +11,6 @@
|
||||
}
|
||||
|
||||
&-radio-button-container-horizontal {
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
+12
-13
@@ -4,7 +4,8 @@
|
||||
>
|
||||
<div class="adf-cloud-upload-widget-container">
|
||||
<div>
|
||||
<mat-list *ngIf="hasFile">
|
||||
@if (hasFile) {
|
||||
<mat-list>
|
||||
<mat-list-item class="adf-upload-files-row" *ngFor="let file of uploadedFiles">
|
||||
<img
|
||||
matListItemLine
|
||||
@@ -27,28 +28,26 @@
|
||||
tabindex="0"
|
||||
>{{file.name}}</span
|
||||
>
|
||||
<button
|
||||
*ngIf="!field.readOnly"
|
||||
mat-icon-button
|
||||
[id]="'file-'+file.id+'-remove'"
|
||||
(click)="removeFile(file);"
|
||||
(keyup.enter)="removeFile(file);"
|
||||
>
|
||||
@if (!field.readOnly) {
|
||||
<button mat-icon-button [id]="'file-'+file.id+'-remove'" (click)="removeFile(file);" (keyup.enter)="removeFile(file);">
|
||||
<mat-icon class="mat-24" adf-icon="highlight_off" />
|
||||
</button>
|
||||
}
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div *ngIf="(!hasFile || multipleOption) && !field.readOnly">
|
||||
@if ((!hasFile || multipleOption) && !field.readOnly) {
|
||||
<div>
|
||||
<button mat-raised-button (click)="uploadFiles.click()" [title]="field.tooltip">
|
||||
{{ 'FORM.FIELD.UPLOAD' | translate }}<mat-icon adf-icon="file_upload" />
|
||||
<input #uploadFiles [multiple]="multipleOption" type="file" [id]="field.form.nodeId" (change)="onFileChanged($event)" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!hasFile && field.readOnly">{{ 'FORM.FIELD.NO_FILE_ATTACHED' | translate }}</div>
|
||||
} @if (!hasFile && field.readOnly) {
|
||||
<div>{{ 'FORM.FIELD.NO_FILE_ATTACHED' | translate }}</div>
|
||||
}
|
||||
</div>
|
||||
<error-widget [error]="field.validationSummary" />
|
||||
<error-widget *ngIf="isInvalidFieldRequired()" required="{{ 'FORM.FIELD.REQUIRED' | translate }}" />
|
||||
<error-widget [error]="field.validationSummary" [required]="isInvalidFieldRequired() ? ('FORM.FIELD.REQUIRED' | translate) : ''" />
|
||||
</div>
|
||||
|
||||
+8
@@ -55,4 +55,12 @@ describe('UploadCloudWidgetComponent', () => {
|
||||
expect(eventSpy).toHaveBeenCalledWith(clickEvent);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render one reserved form field status area', () => {
|
||||
widget.field = new FormFieldModel(new FormModel(), {});
|
||||
fixture.detectChanges();
|
||||
|
||||
const statusAreas = fixture.nativeElement.querySelectorAll('error-widget');
|
||||
expect(statusAreas.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,5 +43,6 @@ export * from './services/form-cloud.service';
|
||||
export * from './services/content-cloud-node-selector.service';
|
||||
export * from './services/process-cloud-content.service';
|
||||
export * from './services/display-mode.service';
|
||||
export * from './services/form-cloud-submission-values';
|
||||
|
||||
export * from './form-cloud.module';
|
||||
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { FormExpressionService, FormFieldModel, FormFieldTypes, FormModel } from '@alfresco/adf-core';
|
||||
import { firstValueFrom, of } from 'rxjs';
|
||||
import { getExpressionEvaluationEnabled$, materializeSubmissionValues } from './form-cloud-submission-values';
|
||||
|
||||
describe('getExpressionEvaluationEnabled$', () => {
|
||||
it('should return the configured static value', async () => {
|
||||
const enabled = await firstValueFrom(getExpressionEvaluationEnabled$({ enableExpressionEvaluation: true }));
|
||||
|
||||
expect(enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should return values emitted by observable settings', async () => {
|
||||
const enabled = await firstValueFrom(getExpressionEvaluationEnabled$(of({ enableExpressionEvaluation: true })));
|
||||
|
||||
expect(enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when settings are unavailable', async () => {
|
||||
const enabled = await firstValueFrom(getExpressionEvaluationEnabled$(undefined));
|
||||
|
||||
expect(enabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('materializeSubmissionValues', () => {
|
||||
let expressions: FormExpressionService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [FormExpressionService]
|
||||
});
|
||||
expressions = TestBed.inject(FormExpressionService);
|
||||
});
|
||||
|
||||
it('should resolve root rich text values from the authored template without mutating the form', () => {
|
||||
const authoredValue = {
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello ${field.name} - ${variable.status} - ${field.missing} - ${field.unsafe}'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{ id: 'richText', name: 'richText', type: FormFieldTypes.DISPLAY_RICH_TEXT, value: authoredValue },
|
||||
{ id: 'name', name: 'name', type: FormFieldTypes.TEXT, value: 'John' },
|
||||
{ id: 'unsafe', name: 'unsafe', type: FormFieldTypes.TEXT, value: '<b>John</b>' }
|
||||
],
|
||||
variables: [{ id: 'status', name: 'status', type: 'string', value: 'Active' }]
|
||||
});
|
||||
const richTextField = form.getFieldById('richText');
|
||||
richTextField.value = { blocks: [{ type: 'paragraph', data: { text: 'stale rendered value' } }] };
|
||||
const originalValues = JSON.parse(JSON.stringify(form.values));
|
||||
const originalDefinition = JSON.parse(JSON.stringify(form.json));
|
||||
|
||||
const values = materializeSubmissionValues(form, { enableExpressionEvaluation: true }, expressions);
|
||||
|
||||
expect(values.richText).toEqual({
|
||||
blocks: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: 'Hello John - Active - - <b>John</b>'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
expect(form.values).toEqual(originalValues);
|
||||
expect(form.json).toEqual(originalDefinition);
|
||||
expect(richTextField.value).toEqual({ blocks: [{ type: 'paragraph', data: { text: 'stale rendered value' } }] });
|
||||
});
|
||||
|
||||
it('should produce stable values across repeated materialization', () => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText',
|
||||
type: FormFieldTypes.DISPLAY_RICH_TEXT,
|
||||
value: { blocks: [{ type: 'paragraph', data: { text: '${field.name}' } }] }
|
||||
},
|
||||
{ id: 'name', type: FormFieldTypes.TEXT, value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
const firstValues = materializeSubmissionValues(form, { enableExpressionEvaluation: true }, expressions);
|
||||
const secondValues = materializeSubmissionValues(form, { enableExpressionEvaluation: true }, expressions);
|
||||
|
||||
expect(secondValues).toEqual(firstValues);
|
||||
});
|
||||
|
||||
it('should return a shallow clone without resolving expressions when evaluation is disabled', () => {
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText',
|
||||
type: FormFieldTypes.DISPLAY_RICH_TEXT,
|
||||
value: { blocks: [{ type: 'paragraph', data: { text: '${field.name}' } }] }
|
||||
},
|
||||
{ id: 'name', type: FormFieldTypes.TEXT, value: 'John' }
|
||||
]
|
||||
});
|
||||
|
||||
const values = materializeSubmissionValues(form, { enableExpressionEvaluation: false }, expressions);
|
||||
|
||||
expect(values).toEqual(form.values);
|
||||
expect(values).not.toBe(form.values);
|
||||
expect(values.richText).toBe(form.values.richText);
|
||||
});
|
||||
|
||||
it('should isolate materialized repeatable section rows', () => {
|
||||
const form = new FormModel();
|
||||
form.values = {
|
||||
section: [
|
||||
{ richText: 'saved row one', untouched: 'one' },
|
||||
{ richText: 'saved row two', untouched: 'two' }
|
||||
],
|
||||
name: 'John'
|
||||
};
|
||||
const nameField = new FormFieldModel(form, { id: 'name', type: FormFieldTypes.TEXT, value: 'John' });
|
||||
const firstField = new FormFieldModel(
|
||||
form,
|
||||
{
|
||||
id: 'richText',
|
||||
type: FormFieldTypes.DISPLAY_RICH_TEXT,
|
||||
value: { blocks: [{ type: 'paragraph', data: { text: 'First ${field.name}' } }] }
|
||||
},
|
||||
{ id: 'section', uid: 'richText-Row1', fields: {}, rowIndex: 0 }
|
||||
);
|
||||
const secondField = new FormFieldModel(
|
||||
form,
|
||||
{
|
||||
id: 'richText',
|
||||
type: FormFieldTypes.DISPLAY_RICH_TEXT,
|
||||
value: { blocks: [{ type: 'paragraph', data: { text: 'Second ${field.name}' } }] }
|
||||
},
|
||||
{ id: 'section', uid: 'richText-Row2', fields: {}, rowIndex: 1 }
|
||||
);
|
||||
form.fieldsCache = [nameField, firstField, secondField];
|
||||
form.values.section = [
|
||||
{ richText: 'saved row one', untouched: 'one' },
|
||||
{ richText: 'saved row two', untouched: 'two' }
|
||||
];
|
||||
const originalSection = form.values.section;
|
||||
const originalFirstRow = form.values.section[0];
|
||||
const originalSecondRow = form.values.section[1];
|
||||
|
||||
const values = materializeSubmissionValues(form, { enableExpressionEvaluation: true }, expressions);
|
||||
|
||||
expect(values.section).toEqual([
|
||||
{
|
||||
richText: { blocks: [{ type: 'paragraph', data: { text: 'First John' } }] },
|
||||
untouched: 'one'
|
||||
},
|
||||
{
|
||||
richText: { blocks: [{ type: 'paragraph', data: { text: 'Second John' } }] },
|
||||
untouched: 'two'
|
||||
}
|
||||
]);
|
||||
expect(values.section).not.toBe(originalSection);
|
||||
expect(values.section[0]).not.toBe(originalFirstRow);
|
||||
expect(values.section[1]).not.toBe(originalSecondRow);
|
||||
expect(form.values.section).toBe(originalSection);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DisplayTextWidgetSettings, FormExpressionService, FormFieldTypes, FormModel, FormValues, ROW_ID_PREFIX } from '@alfresco/adf-core';
|
||||
import { isObservable, Observable, of } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { resolveRichTextExpressions } from '../components/widgets/display-rich-text/rich-text-expression-resolver';
|
||||
|
||||
type SubmissionRow = Record<string, unknown>;
|
||||
|
||||
const isSubmissionRow = (value: unknown): value is SubmissionRow => typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
|
||||
export interface FormCloudSubmissionValuesOptions {
|
||||
enableExpressionEvaluation: boolean;
|
||||
}
|
||||
|
||||
export const getExpressionEvaluationEnabled$ = (
|
||||
settings: Observable<DisplayTextWidgetSettings> | DisplayTextWidgetSettings | null | undefined
|
||||
): Observable<boolean> =>
|
||||
isObservable(settings)
|
||||
? settings.pipe(map((value) => value?.enableExpressionEvaluation ?? false))
|
||||
: of(settings?.enableExpressionEvaluation ?? false);
|
||||
|
||||
export const materializeSubmissionValues = (
|
||||
form: FormModel,
|
||||
options: FormCloudSubmissionValuesOptions,
|
||||
expressions: FormExpressionService
|
||||
): FormValues => {
|
||||
const values = { ...form.values };
|
||||
|
||||
if (!options.enableExpressionEvaluation) {
|
||||
return values;
|
||||
}
|
||||
|
||||
for (const field of form.getFormFields([FormFieldTypes.DISPLAY_RICH_TEXT])) {
|
||||
const { authoredValue, parent } = field;
|
||||
if (authoredValue === undefined || parent?.isTemplate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const materializedValue = resolveRichTextExpressions(authoredValue, (content) => expressions.resolveExpressions(form, content, true), {
|
||||
cloneValue: false
|
||||
});
|
||||
|
||||
if (!parent) {
|
||||
values[field.id] = materializedValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
const sectionValues = values[parent.id];
|
||||
const sectionRow = Array.isArray(sectionValues) ? sectionValues[parent.rowIndex] : undefined;
|
||||
if (!isSubmissionRow(sectionRow)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const materializedRows = [...sectionValues];
|
||||
const fieldId = field.id.split(ROW_ID_PREFIX)[0];
|
||||
materializedRows[parent.rowIndex] = {
|
||||
...sectionRow,
|
||||
[fieldId]: materializedValue
|
||||
};
|
||||
values[parent.id] = materializedRows;
|
||||
}
|
||||
|
||||
return values;
|
||||
};
|
||||
@@ -1,18 +1,26 @@
|
||||
<form>
|
||||
<mat-form-field class="adf-cloud-group adf-form-field-input" [class.adf-invalid]="hasError() && isDirty()">
|
||||
@if (label || required) { <mat-label><span>{{label}}</span></mat-label> }
|
||||
<mat-form-field subscriptSizing="dynamic" class="adf-cloud-group adf-form-field-input" [class.adf-invalid]="hasError() && isDirty()">
|
||||
@if (label || required) {
|
||||
<mat-label
|
||||
><span>{{ label }}</span></mat-label
|
||||
>
|
||||
}
|
||||
<mat-chip-grid [required]="required" [disabled]="isReadonly()" #groupChipList data-automation-id="adf-cloud-group-chip-list">
|
||||
<mat-chip-row
|
||||
*ngFor="let group of selectedGroups"
|
||||
[removable]="!(group.readonly)"
|
||||
[removable]="!group.readonly"
|
||||
[attr.data-automation-id]="'adf-cloud-group-chip-' + group.name"
|
||||
(removed)="onRemove(group)"
|
||||
[disabled]="readOnly || isValidationLoading()"
|
||||
title="{{ (group.readonly ? 'ADF_CLOUD_GROUPS.MANDATORY' : '') | translate }}">
|
||||
title="{{ (group.readonly ? 'ADF_CLOUD_GROUPS.MANDATORY' : '') | translate }}"
|
||||
>
|
||||
{{ group.name }}
|
||||
<mat-icon *ngIf="!(group.readonly || readOnly)" matChipRemove [attr.data-automation-id]="'adf-cloud-group-chip-remove-icon-' + group.name" adf-icon="cancel" />
|
||||
@if (!(group.readonly || readOnly)) {
|
||||
<mat-icon matChipRemove [attr.data-automation-id]="'adf-cloud-group-chip-remove-icon-' + group.name" adf-icon="cancel" />
|
||||
}
|
||||
</mat-chip-row>
|
||||
<input matInput
|
||||
<input
|
||||
matInput
|
||||
[formControl]="searchGroupsControl"
|
||||
[matAutocomplete]="auto"
|
||||
[matChipInputFor]="groupChipList"
|
||||
@@ -21,7 +29,9 @@
|
||||
(focus)="setFocus(true)"
|
||||
(blur)="setFocus(false); markAsTouched()"
|
||||
class="adf-group-input"
|
||||
data-automation-id="adf-cloud-group-search-input" #groupInput>
|
||||
data-automation-id="adf-cloud-group-search-input"
|
||||
#groupInput
|
||||
/>
|
||||
</mat-chip-grid>
|
||||
|
||||
<mat-autocomplete
|
||||
@@ -30,57 +40,77 @@
|
||||
class="adf-cloud-group-list"
|
||||
(optionSelected)="onSelect($event.option.value)"
|
||||
[displayWith]="getDisplayName"
|
||||
data-automation-id="adf-cloud-group-autocomplete">
|
||||
<ng-container *ngIf="(searchGroups$ | async)?.length else noResults">
|
||||
<mat-option *ngFor="let group of searchGroups$ | async; let i = index" [value]="group"
|
||||
data-automation-id="adf-cloud-group-autocomplete"
|
||||
>
|
||||
@if ((searchGroups$ | async)?.length) {
|
||||
<mat-option
|
||||
*ngFor="let group of searchGroups$ | async; let i = index"
|
||||
[value]="group"
|
||||
[attr.data-automation-id]="'adf-cloud-group-chip-' + group.name"
|
||||
class="adf-cloud-group-option-active">
|
||||
<div
|
||||
class="adf-cloud-group-row"
|
||||
id="adf-group-{{i}}"
|
||||
data-automation-id="adf-cloud-group-row">
|
||||
class="adf-cloud-group-option-active"
|
||||
>
|
||||
<div class="adf-cloud-group-row" id="adf-group-{{ i }}" data-automation-id="adf-cloud-group-row">
|
||||
<button class="adf-group-short-name" mat-fab>{{ getGroupNameInitials(group) }}</button>
|
||||
<span>{{ group.name }}</span>
|
||||
</div>
|
||||
</mat-option>
|
||||
</ng-container>
|
||||
} @else {
|
||||
<ng-container [ngTemplateOutlet]="noResults" />
|
||||
}
|
||||
<ng-template #noResults>
|
||||
<mat-option *ngIf="searchGroupsControl.hasError('searchTypingError') && !searchLoading" disabled
|
||||
class="adf-cloud-group-option-not-active"
|
||||
data-automation-id="adf-cloud-group-no-results">
|
||||
@if (searchGroupsControl.hasError('searchTypingError') && !searchLoading) {
|
||||
<mat-option disabled class="adf-cloud-group-option-not-active" data-automation-id="adf-cloud-group-no-results">
|
||||
<span> {{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate }}</span>
|
||||
</mat-option>
|
||||
}
|
||||
</ng-template>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
<mat-progress-bar *ngIf="validationLoading" mode="indeterminate" />
|
||||
|
||||
<div class="adf-error-container adf-error-messages-container">
|
||||
<mat-error *ngIf="hasPreselectError() && !isValidationLoading()" class="adf-error">
|
||||
@if (validationLoading) {
|
||||
<mat-progress-bar mode="indeterminate" />
|
||||
}
|
||||
@if (hasPreselectError() && !isValidationLoading()) {
|
||||
<mat-error class="adf-error">
|
||||
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate }}</div>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="searchGroupsControl.hasError('pattern')" class="adf-error">
|
||||
}
|
||||
@if (searchGroupsControl.hasError('pattern')) {
|
||||
<mat-error class="adf-error">
|
||||
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate: { pattern: getValidationPattern() } }}</div>
|
||||
<div class="adf-error-text">
|
||||
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate: { pattern: getValidationPattern() } }}
|
||||
</div>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="searchGroupsControl.hasError('maxlength')" class="adf-error">
|
||||
}
|
||||
@if (searchGroupsControl.hasError('maxlength')) {
|
||||
<mat-error class="adf-error">
|
||||
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate: { requiredLength: getValidationMaxLength() } }}</div>
|
||||
<div class="adf-error-text">
|
||||
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate: { requiredLength: getValidationMaxLength() } }}
|
||||
</div>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="searchGroupsControl.hasError('minlength')" class="adf-error">
|
||||
}
|
||||
@if (searchGroupsControl.hasError('minlength')) {
|
||||
<mat-error class="adf-error">
|
||||
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate: { requiredLength: getValidationMinLength() } }}</div>
|
||||
<div class="adf-error-text">
|
||||
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate: { requiredLength: getValidationMinLength() } }}
|
||||
</div>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="(searchGroupsControl.hasError('required') || groupChipsCtrl.hasError('required')) && isDirty()"
|
||||
class="adf-error">
|
||||
}
|
||||
@if ((searchGroupsControl.hasError('required') || groupChipsCtrl.hasError('required')) && isDirty()) {
|
||||
<mat-error class="adf-error">
|
||||
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.REQUIRED' | translate }}</div>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="searchGroupsControl.hasError('searchTypingError') && !this.isFocused"
|
||||
data-automation-id="invalid-groups-typing-error" class="adf-error">
|
||||
}
|
||||
@if (searchGroupsControl.hasError('searchTypingError') && !this.isFocused) {
|
||||
<mat-error data-automation-id="invalid-groups-typing-error" class="adf-error">
|
||||
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_GROUPS.ERROR.NOT_FOUND' | translate }}</div>
|
||||
</mat-error>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -57,10 +57,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.adf-error-messages-container .adf-error-icon {
|
||||
.adf-error-messages-container {
|
||||
.adf-error-icon {
|
||||
@include mixins.adf-error-icon;
|
||||
}
|
||||
|
||||
.adf-error-messages-container .adf-error {
|
||||
.adf-error {
|
||||
animation: slide-down-fade-in 300ms cubic-bezier(0.55, 0, 0.55, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
import { MatChipHarness } from '@angular/material/chips/testing';
|
||||
import { MatIconHarness } from '@angular/material/icon/testing';
|
||||
import { MatInputHarness } from '@angular/material/input/testing';
|
||||
import { MatFormField } from '@angular/material/form-field';
|
||||
import { MatProgressBar } from '@angular/material/progress-bar';
|
||||
|
||||
describe('GroupCloudComponent', () => {
|
||||
let loader: HarnessLoader;
|
||||
@@ -98,6 +100,22 @@ describe('GroupCloudComponent', () => {
|
||||
expect(await inputElement.getPlaceholder()).toEqual('');
|
||||
});
|
||||
|
||||
it('should use dynamic form field subscript sizing', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
const formField = fixture.debugElement.query(By.directive(MatFormField)).componentInstance as MatFormField;
|
||||
expect(formField.subscriptSizing).toBe('dynamic');
|
||||
});
|
||||
|
||||
it('should render validation progress inside the reserved status area', () => {
|
||||
component.validationLoading = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
const progressBar = fixture.debugElement.query(By.directive(MatProgressBar));
|
||||
|
||||
expect(progressBar.parent.classes['adf-error-messages-container']).toBeTrue();
|
||||
});
|
||||
|
||||
describe('Search group', () => {
|
||||
beforeEach(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -4,10 +4,14 @@
|
||||
class="adf-people-cloud adf-form-field-input"
|
||||
[class.adf-invalid]="hasError() && isDirty()"
|
||||
>
|
||||
<mat-label *ngIf="!title">
|
||||
@if (!title) {
|
||||
<mat-label>
|
||||
<span>{{ label }}</span>
|
||||
</mat-label>
|
||||
<mat-label *ngIf="title">{{ title | translate }}</mat-label>
|
||||
}
|
||||
@if (title) {
|
||||
<mat-label>{{ title | translate }}</mat-label>
|
||||
}
|
||||
|
||||
<mat-chip-grid [required]="required" [disabled]="isReadonly()" #userMultipleChipList data-automation-id="adf-cloud-people-chip-list">
|
||||
<mat-chip-row
|
||||
@@ -20,7 +24,9 @@
|
||||
[matTooltip]="showFullNameOnHover ? (user | fullName: true) : user.email"
|
||||
>
|
||||
{{ user | fullName }}
|
||||
<mat-icon matChipRemove *ngIf="!(user.readonly || readOnly)" [attr.data-automation-id]="'adf-people-cloud-chip-remove-icon-' + user.username" adf-icon="cancel" />
|
||||
@if (!(user.readonly || readOnly)) {
|
||||
<mat-icon matChipRemove [attr.data-automation-id]="'adf-people-cloud-chip-remove-icon-' + user.username" adf-icon="cancel" />
|
||||
}
|
||||
</mat-chip-row>
|
||||
<input
|
||||
matInput
|
||||
@@ -44,64 +50,73 @@
|
||||
(optionSelected)="onSelect($event.option.value)"
|
||||
[displayWith]="getDisplayName"
|
||||
>
|
||||
<ng-container *ngIf="(searchUsers$ | async)?.length; else noResults">
|
||||
@if ((searchUsers$ | async)?.length) {
|
||||
<mat-option *ngFor="let user of searchUsers$ | async; let i = index" [value]="user" class="adf-people-cloud-option-active">
|
||||
<div class="adf-people-cloud-row" id="adf-people-cloud-user-{{ user.username }}" data-automation-id="adf-people-cloud-row">
|
||||
<div [outerHTML]="user | usernameInitials: 'adf-people-cloud-pic'"></div>
|
||||
<span class="adf-people-label-name"> {{ user | fullName: true }}</span>
|
||||
</div>
|
||||
</mat-option>
|
||||
</ng-container>
|
||||
} @else {
|
||||
<ng-container [ngTemplateOutlet]="noResults" />
|
||||
}
|
||||
<ng-template #noResults>
|
||||
<mat-option
|
||||
*ngIf="searchUserCtrl.hasError('searchTypingError') && !searchLoading"
|
||||
disabled
|
||||
class="adf-people-cloud-option-not-active"
|
||||
data-automation-id="adf-people-cloud-no-results"
|
||||
>
|
||||
@if (searchUserCtrl.hasError('searchTypingError') && !searchLoading) {
|
||||
<mat-option disabled class="adf-people-cloud-option-not-active" data-automation-id="adf-people-cloud-no-results">
|
||||
<span> {{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate: { userName: searchedValue } }}</span>
|
||||
</mat-option>
|
||||
}
|
||||
</ng-template>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-progress-bar *ngIf="validationLoading" mode="indeterminate" />
|
||||
|
||||
<div class="adf-error-container adf-error-messages-container" *ngIf="showErrors">
|
||||
<mat-error *ngIf="hasPreselectError() && !isValidationLoading()" class="adf-error adf-error-animate">
|
||||
<div class="adf-error-container adf-error-messages-container">
|
||||
@if (validationLoading) {
|
||||
<mat-progress-bar mode="indeterminate" />
|
||||
}
|
||||
@if (showErrors) {
|
||||
@if (hasPreselectError() && !isValidationLoading()) {
|
||||
<mat-error class="adf-error adf-error-animate">
|
||||
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate: { userName: validateUsersMessage } }}</div>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="searchUserCtrl.hasError('pattern')" class="adf-error adf-error-animate">
|
||||
}
|
||||
@if (searchUserCtrl.hasError('pattern')) {
|
||||
<mat-error class="adf-error adf-error-animate">
|
||||
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate : { pattern: getValidationPattern() } }}</div>
|
||||
<div class="adf-error-text">
|
||||
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_PATTERN' | translate: { pattern: getValidationPattern() } }}
|
||||
</div>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="searchUserCtrl.hasError('maxlength')" class="adf-error adf-error-animate">
|
||||
}
|
||||
@if (searchUserCtrl.hasError('maxlength')) {
|
||||
<mat-error class="adf-error adf-error-animate">
|
||||
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
|
||||
<div class="adf-error-text">
|
||||
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MAX_LENGTH' | translate: { requiredLength: getValidationMaxLength() } }}
|
||||
</div>
|
||||
</mat-error>
|
||||
<mat-error *ngIf="searchUserCtrl.hasError('minlength')" class="adf-error adf-error-animate">
|
||||
}
|
||||
@if (searchUserCtrl.hasError('minlength')) {
|
||||
<mat-error class="adf-error adf-error-animate">
|
||||
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
|
||||
<div class="adf-error-text">
|
||||
{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.INVALID_MIN_LENGTH' | translate: { requiredLength: getValidationMinLength() } }}
|
||||
</div>
|
||||
</mat-error>
|
||||
<mat-error
|
||||
*ngIf="(searchUserCtrl.hasError('required') || userChipsCtrl.hasError('required')) && isDirty()"
|
||||
class="adf-error adf-error-animate"
|
||||
>
|
||||
}
|
||||
@if ((searchUserCtrl.hasError('required') || userChipsCtrl.hasError('required')) && isDirty()) {
|
||||
<mat-error class="adf-error adf-error-animate">
|
||||
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_PEOPLE_GROUPS.ERROR.REQUIRED' | translate }}</div>
|
||||
</mat-error>
|
||||
<mat-error
|
||||
*ngIf="searchUserCtrl.hasError('searchTypingError') && !this.isFocused"
|
||||
data-automation-id="invalid-users-typing-error"
|
||||
class="adf-error adf-error-animate"
|
||||
>
|
||||
}
|
||||
@if (searchUserCtrl.hasError('searchTypingError') && !this.isFocused) {
|
||||
<mat-error data-automation-id="invalid-users-typing-error" class="adf-error adf-error-animate">
|
||||
<mat-icon class="adf-error-icon" adf-icon="error_outline" />
|
||||
<div class="adf-error-text">{{ 'ADF_CLOUD_USERS.ERROR.NOT_FOUND' | translate: { userName: searchedValue } }}</div>
|
||||
</mat-error>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -27,6 +27,7 @@ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
import { MatChipHarness } from '@angular/material/chips/testing';
|
||||
import { MatInputHarness } from '@angular/material/input/testing';
|
||||
import { MatFormFieldHarness } from '@angular/material/form-field/testing';
|
||||
import { MatProgressBar } from '@angular/material/progress-bar';
|
||||
import { IdentityUserService } from '../services/identity-user.service';
|
||||
|
||||
describe('PeopleCloudComponent', () => {
|
||||
@@ -99,6 +100,19 @@ describe('PeopleCloudComponent', () => {
|
||||
expect(await inputField.getLabel()).toEqual('TITLE_KEY');
|
||||
});
|
||||
|
||||
it('should use dynamic form field subscript sizing by default', () => {
|
||||
expect(component.formFieldSubscriptSizing).toBe('dynamic');
|
||||
});
|
||||
|
||||
it('should render validation progress inside the reserved status area', () => {
|
||||
component.validationLoading = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
const progressBar = fixture.debugElement.query(By.directive(MatProgressBar));
|
||||
|
||||
expect(progressBar.parent.classes['adf-error-messages-container']).toBeTrue();
|
||||
});
|
||||
|
||||
describe('Search user', () => {
|
||||
beforeEach(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
@@ -169,7 +169,7 @@ export class PeopleCloudComponent implements OnInit, OnChanges, AfterViewInit {
|
||||
* Material form field subscript sizing (fixed / dynamic)
|
||||
*/
|
||||
@Input()
|
||||
formFieldSubscriptSizing: SubscriptSizing = 'fixed';
|
||||
formFieldSubscriptSizing: SubscriptSizing = 'dynamic';
|
||||
|
||||
/**
|
||||
* Show errors under the form field
|
||||
|
||||
+55
-4
@@ -17,8 +17,8 @@
|
||||
|
||||
import { DebugElement, SimpleChange } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
|
||||
import { FormModel, FormOutcomeEvent, FormOutcomeModel } from '@alfresco/adf-core';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { ADF_DISPLAY_TEXT_SETTINGS, FormFieldTypes, FormModel, FormOutcomeEvent, FormOutcomeModel } from '@alfresco/adf-core';
|
||||
import { Subject, of, throwError } from 'rxjs';
|
||||
import { StartProcessCloudService } from '../services/start-process-cloud.service';
|
||||
import { FormCloudService } from '../../../form/services/form-cloud.service';
|
||||
import { FormCloudComponent } from '../../../form/components/form-cloud.component';
|
||||
@@ -47,7 +47,7 @@ import { HarnessLoader } from '@angular/cdk/testing';
|
||||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
import { MatAutocompleteHarness } from '@angular/material/autocomplete/testing';
|
||||
import { MatButtonHarness } from '@angular/material/button/testing';
|
||||
import { FormCloudDisplayMode } from '../../../services/form-fields.interfaces';
|
||||
import { FormCloudDisplayMode, FormContent } from '../../../services/form-fields.interfaces';
|
||||
import { MatDialogHarness } from '@angular/material/dialog/testing';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
@@ -92,7 +92,10 @@ describe('StartProcessCloudComponent', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [StartProcessCloudComponent, ReactiveFormsModule, StartProcessScreenCloudComponent],
|
||||
providers: [provideScreen(screenId, MockedTaskScreenCloudComponent)]
|
||||
providers: [
|
||||
provideScreen(screenId, MockedTaskScreenCloudComponent),
|
||||
{ provide: ADF_DISPLAY_TEXT_SETTINGS, useValue: { enableExpressionEvaluation: true } }
|
||||
]
|
||||
});
|
||||
processService = TestBed.inject(StartProcessCloudService);
|
||||
formCloudService = TestBed.inject(FormCloudService);
|
||||
@@ -417,6 +420,54 @@ describe('StartProcessCloudComponent', () => {
|
||||
expect(startBtn.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should keep the start action unavailable while the form definition is loading', async () => {
|
||||
const formDefinition = new Subject<FormContent>();
|
||||
formDefinitionSpy.and.returnValue(formDefinition);
|
||||
typeValueInto('[data-automation-id="adf-inplace-input"]', 'My new process with form');
|
||||
await selectOptionByName('processwithform');
|
||||
|
||||
const startButton = fixture.nativeElement.querySelector('#button-start');
|
||||
expect(startButton).toBeNull();
|
||||
expect(startProcessWithFormSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should materialize unrendered rich text expressions when starting a process', async () => {
|
||||
formDefinitionSpy.and.returnValue(of(fakeStartForm));
|
||||
component.processDefinitionCurrent = fakeProcessDefinitions[2];
|
||||
component.processPayloadCloud.processDefinitionKey = fakeProcessDefinitions[2].key;
|
||||
component.processInstanceName.setValue('My process');
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const form = new FormModel({
|
||||
fields: [
|
||||
{
|
||||
id: 'richText',
|
||||
type: FormFieldTypes.DISPLAY_RICH_TEXT,
|
||||
value: { blocks: [{ type: 'paragraph', data: { text: 'Hello ${field.name}' } }] }
|
||||
},
|
||||
{ id: 'name', type: FormFieldTypes.TEXT, value: 'John' }
|
||||
]
|
||||
});
|
||||
const formElement = fixture.debugElement.query(By.css('adf-cloud-form'));
|
||||
const startButton = fixture.debugElement.query(By.css('#button-start'));
|
||||
|
||||
formElement.triggerEventHandler('formLoaded', form);
|
||||
fixture.detectChanges();
|
||||
startButton.triggerEventHandler('click', null);
|
||||
|
||||
expect(startProcessWithFormSpy).toHaveBeenCalledWith(
|
||||
component.appName,
|
||||
fakeProcessDefinitions[2].formKey,
|
||||
fakeProcessDefinitions[2].version,
|
||||
jasmine.objectContaining({
|
||||
values: jasmine.objectContaining({
|
||||
richText: { blocks: [{ type: 'paragraph', data: { text: 'Hello John' } }] }
|
||||
})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to start a process with form full display mode', async () => {
|
||||
component.displayModeConfigurations = [
|
||||
{
|
||||
|
||||
+35
-3
@@ -30,10 +30,14 @@ import {
|
||||
ViewEncapsulation
|
||||
} from '@angular/core';
|
||||
import {
|
||||
ADF_DISPLAY_TEXT_SETTINGS,
|
||||
ConfirmDialogComponent,
|
||||
ContentLinkModel,
|
||||
DisplayTextWidgetSettings,
|
||||
FormExpressionService,
|
||||
FormModel,
|
||||
FormOutcomeEvent,
|
||||
FormValues,
|
||||
IconModule,
|
||||
InplaceFormInputComponent,
|
||||
LocalizedDatePipe,
|
||||
@@ -65,6 +69,7 @@ import { FormCustomOutcomesComponent } from '../../../form/components/form-cloud
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { StartProcessScreenCloudComponent } from '../../../screen/components/screen-cloud/start-process-event-screen/start-process-screen-cloud.component';
|
||||
import { TaskTypeResolverService } from '../../../services/task-type-resolver/task-type-resolver.service';
|
||||
import { getExpressionEvaluationEnabled$, materializeSubmissionValues } from '../../../form/services/form-cloud-submission-values';
|
||||
|
||||
const MAX_NAME_LENGTH: number = 255;
|
||||
const PROCESS_DEFINITION_DEBOUNCE: number = 300;
|
||||
@@ -211,6 +216,11 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
|
||||
private readonly hasVisibleOutcomesSubject = new BehaviorSubject<boolean>(false);
|
||||
private readonly dialog = inject(MatDialog);
|
||||
private readonly taskTypeResolverService = inject(TaskTypeResolverService);
|
||||
private readonly expressions = inject(FormExpressionService);
|
||||
private readonly displayTextSettings = inject<Observable<DisplayTextWidgetSettings> | DisplayTextWidgetSettings>(ADF_DISPLAY_TEXT_SETTINGS, {
|
||||
optional: true
|
||||
});
|
||||
private enableExpressionEvaluation = false;
|
||||
|
||||
private screenSubmitPayload: unknown;
|
||||
|
||||
@@ -218,8 +228,12 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
|
||||
showCompleteButton = false;
|
||||
|
||||
get isProcessFormValid(): boolean {
|
||||
if (this.hasForm && this.isFormCloudLoaded) {
|
||||
return (this.formCloud ? !Object.keys(this.formCloud.values).length : false) || this.formCloud?.isValid || this.isProcessStarting;
|
||||
if (this.hasForm) {
|
||||
if (!this.isFormCloudLoaded || !this.formCloud) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !Object.keys(this.formCloud.values).length || this.formCloud.isValid || this.isProcessStarting;
|
||||
} else if (this.hasScreen) {
|
||||
return true;
|
||||
} else {
|
||||
@@ -268,6 +282,12 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
|
||||
constructor() {
|
||||
this.startProcessButtonLabel = this.defaultStartProcessButtonLabel;
|
||||
this.cancelButtonLabel = this.defaultCancelProcessButtonLabel;
|
||||
|
||||
getExpressionEvaluationEnabled$(this.displayTextSettings)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((enabled) => {
|
||||
this.enableExpressionEvaluation = enabled;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -482,6 +502,14 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
|
||||
}
|
||||
|
||||
startProcessWithoutConfirmation() {
|
||||
let submissionValues = this.screenSubmitPayload;
|
||||
if (this.hasForm) {
|
||||
if (!this.formCloud) {
|
||||
return;
|
||||
}
|
||||
submissionValues = this.getFormSubmissionValues(this.formCloud);
|
||||
}
|
||||
|
||||
this.isProcessStarting = true;
|
||||
|
||||
let action: Observable<ProcessInstanceCloud>;
|
||||
@@ -495,7 +523,7 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
|
||||
processName: this.processInstanceName.value,
|
||||
processDefinitionKey: this.processPayloadCloud.processDefinitionKey,
|
||||
variables: this.variables ?? {},
|
||||
values: this.hasForm ? this.formCloud.values : this.screenSubmitPayload,
|
||||
values: submissionValues,
|
||||
outcome: this.customOutcomeName
|
||||
})
|
||||
);
|
||||
@@ -524,6 +552,10 @@ export class StartProcessCloudComponent implements OnChanges, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
private getFormSubmissionValues(form: FormModel): FormValues {
|
||||
return materializeSubmissionValues(form, { enableExpressionEvaluation: this.enableExpressionEvaluation }, this.expressions);
|
||||
}
|
||||
|
||||
startProcess() {
|
||||
if (!this.formCloud?.confirmMessage?.show) {
|
||||
this.startProcessWithoutConfirmation();
|
||||
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
/*!
|
||||
* @license
|
||||
* Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, ComponentRef, OnDestroy } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { BaseScreenCloudComponent } from './base-screen-cloud.component';
|
||||
import { provideScreen } from '../../../services/provide-screen';
|
||||
import { ScreenRenderingService } from '../../../services/screen-rendering.service';
|
||||
|
||||
@Component({
|
||||
selector: 'adf-cloud-test-dynamic-screen',
|
||||
template: `<div class="adf-cloud-test-dynamic-screen">dynamic screen</div>`
|
||||
})
|
||||
class TestDynamicScreenComponent implements OnDestroy {
|
||||
destroyed = false;
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'adf-cloud-test-host-screen',
|
||||
template: `<ng-container #container />`
|
||||
})
|
||||
class TestHostScreenComponent extends BaseScreenCloudComponent<TestDynamicScreenComponent> {
|
||||
setInputsCalls: ComponentRef<TestDynamicScreenComponent>[] = [];
|
||||
subscribeToOutputsCalls: ComponentRef<TestDynamicScreenComponent>[] = [];
|
||||
|
||||
get dynamicComponentRef(): ComponentRef<TestDynamicScreenComponent> | undefined {
|
||||
return this.componentRef;
|
||||
}
|
||||
|
||||
get dynamicComponentRefSignalValue(): ComponentRef<TestDynamicScreenComponent> | undefined {
|
||||
return this.componentRefChanged();
|
||||
}
|
||||
|
||||
protected override setInputsForDynamicComponent(componentRef: ComponentRef<TestDynamicScreenComponent>): void {
|
||||
this.setInputsCalls.push(componentRef);
|
||||
}
|
||||
|
||||
protected subscribeToOutputs(componentRef: ComponentRef<TestDynamicScreenComponent>): void {
|
||||
this.subscribeToOutputsCalls.push(componentRef);
|
||||
}
|
||||
}
|
||||
|
||||
/** Same host component, but without the `#container` anchor in its template. */
|
||||
@Component({
|
||||
selector: 'adf-cloud-test-host-screen-without-container',
|
||||
template: `<div class="adf-cloud-no-container"></div>`
|
||||
})
|
||||
class TestHostScreenWithoutContainerComponent extends TestHostScreenComponent {}
|
||||
|
||||
describe('BaseScreenCloudComponent', () => {
|
||||
const screenId = 'test-screen';
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TestHostScreenComponent, TestHostScreenWithoutContainerComponent, TestDynamicScreenComponent],
|
||||
providers: [provideScreen(screenId, TestDynamicScreenComponent)]
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a screenId is provided', () => {
|
||||
let fixture: ComponentFixture<TestHostScreenComponent>;
|
||||
let component: TestHostScreenComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TestHostScreenComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.componentRef.setInput('screenId', screenId);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create the dynamic component and expose it through the signal', () => {
|
||||
expect(component.dynamicComponentRef).toBeDefined();
|
||||
expect(component.dynamicComponentRefSignalValue).toBe(component.dynamicComponentRef);
|
||||
expect(fixture.debugElement.query(By.css('.adf-cloud-test-dynamic-screen'))).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should wire inputs and outputs once, passing the created component reference', () => {
|
||||
expect(component.setInputsCalls).toEqual([component.dynamicComponentRef!]);
|
||||
expect(component.subscribeToOutputsCalls).toEqual([component.dynamicComponentRef!]);
|
||||
});
|
||||
|
||||
it('should destroy the dynamic component reference on destroy', () => {
|
||||
const destroySpy = spyOn(component.dynamicComponentRef!, 'destroy').and.callThrough();
|
||||
|
||||
fixture.destroy();
|
||||
|
||||
expect(destroySpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should run the ngOnDestroy hook of the dynamic component on destroy', () => {
|
||||
const dynamicComponentInstance = component.dynamicComponentRef?.instance;
|
||||
expect(dynamicComponentInstance?.destroyed).toBeFalse();
|
||||
|
||||
fixture.destroy();
|
||||
|
||||
expect(dynamicComponentInstance?.destroyed).toBeTrue();
|
||||
});
|
||||
|
||||
it('should clear the dynamic component reference and the signal on destroy', () => {
|
||||
fixture.destroy();
|
||||
|
||||
expect(component.dynamicComponentRef).toBeUndefined();
|
||||
expect(component.dynamicComponentRefSignalValue).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should destroy the dynamic component reference only once when ngOnDestroy runs again', () => {
|
||||
const destroySpy = spyOn(component.dynamicComponentRef!, 'destroy');
|
||||
|
||||
component.ngOnDestroy();
|
||||
component.ngOnDestroy();
|
||||
|
||||
expect(destroySpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when no screenId is provided', () => {
|
||||
let fixture: ComponentFixture<TestHostScreenComponent>;
|
||||
let component: TestHostScreenComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TestHostScreenComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not create any dynamic component nor wire inputs and outputs', () => {
|
||||
expect(component.dynamicComponentRef).toBeUndefined();
|
||||
expect(component.dynamicComponentRefSignalValue).toBeUndefined();
|
||||
expect(component.setInputsCalls).toEqual([]);
|
||||
expect(component.subscribeToOutputsCalls).toEqual([]);
|
||||
expect(fixture.debugElement.query(By.css('.adf-cloud-test-dynamic-screen'))).toBeNull();
|
||||
});
|
||||
|
||||
it('should not throw on destroy', () => {
|
||||
expect(() => fixture.destroy()).not.toThrow();
|
||||
expect(component.dynamicComponentRef).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the container anchor is missing', () => {
|
||||
let fixture: ComponentFixture<TestHostScreenWithoutContainerComponent>;
|
||||
let component: TestHostScreenWithoutContainerComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TestHostScreenWithoutContainerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.componentRef.setInput('screenId', screenId);
|
||||
});
|
||||
|
||||
it('should not throw and should not create any dynamic component', () => {
|
||||
expect(() => fixture.detectChanges()).not.toThrow();
|
||||
|
||||
expect(component.container).toBeUndefined();
|
||||
expect(component.dynamicComponentRef).toBeUndefined();
|
||||
expect(component.dynamicComponentRefSignalValue).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not wire inputs and outputs when no dynamic component was created', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.setInputsCalls).toEqual([]);
|
||||
expect(component.subscribeToOutputsCalls).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not resolve any component type', () => {
|
||||
const resolveComponentTypeSpy = spyOn(TestBed.inject(ScreenRenderingService), 'resolveComponentType').and.callThrough();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(resolveComponentTypeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not throw on destroy', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(() => fixture.destroy()).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
+22
-12
@@ -15,20 +15,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, ComponentRef, inject, Input, OnInit, signal, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import { Component, ComponentRef, inject, Input, OnDestroy, OnInit, signal, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import { ScreenRenderingService } from '../../../services/screen-rendering.service';
|
||||
|
||||
@Component({
|
||||
template: ''
|
||||
})
|
||||
export abstract class BaseScreenCloudComponent<TScreenComponent = unknown> implements OnInit {
|
||||
export abstract class BaseScreenCloudComponent<TScreenComponent = unknown> implements OnInit, OnDestroy {
|
||||
@Input()
|
||||
screenId: string = '';
|
||||
|
||||
@ViewChild('container', { read: ViewContainerRef, static: true })
|
||||
container: ViewContainerRef;
|
||||
container: ViewContainerRef | undefined;
|
||||
|
||||
protected componentRef: ComponentRef<TScreenComponent>;
|
||||
protected componentRef: ComponentRef<TScreenComponent> | undefined;
|
||||
private readonly _componentRefChanged = signal<ComponentRef<TScreenComponent> | undefined>(undefined);
|
||||
protected readonly componentRefChanged = this._componentRefChanged.asReadonly();
|
||||
protected readonly screenRenderingService = inject(ScreenRenderingService);
|
||||
@@ -37,17 +37,27 @@ export abstract class BaseScreenCloudComponent<TScreenComponent = unknown> imple
|
||||
this.createDynamicComponent();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.componentRef?.destroy();
|
||||
this.componentRef = undefined;
|
||||
this._componentRefChanged.set(undefined);
|
||||
}
|
||||
|
||||
private createDynamicComponent(): void {
|
||||
if (this.screenId) {
|
||||
if (!this.screenId || !this.container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const componentType = this.screenRenderingService.resolveComponentType({ type: this.screenId });
|
||||
this.componentRef = this.container.createComponent(componentType);
|
||||
this._componentRefChanged.set(this.componentRef);
|
||||
this.setInputsForDynamicComponent();
|
||||
this.subscribeToOutputs();
|
||||
}
|
||||
const componentRef: ComponentRef<TScreenComponent> = this.container.createComponent(componentType);
|
||||
|
||||
this.componentRef = componentRef;
|
||||
this._componentRefChanged.set(componentRef);
|
||||
this.setInputsForDynamicComponent(componentRef);
|
||||
this.subscribeToOutputs(componentRef);
|
||||
}
|
||||
|
||||
protected setInputsForDynamicComponent(): void {}
|
||||
protected setInputsForDynamicComponent(_componentRef: ComponentRef<TScreenComponent>): void {}
|
||||
|
||||
protected abstract subscribeToOutputs(): void;
|
||||
protected abstract subscribeToOutputs(componentRef: ComponentRef<TScreenComponent>): void;
|
||||
}
|
||||
|
||||
+120
-4
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Component, Input, OnDestroy, input, output } from '@angular/core';
|
||||
import { StartProcessScreenCloudComponent } from './start-process-screen-cloud.component';
|
||||
import { MockedTaskScreenCloudComponent } from '../../../../testing/start-process-screen-mock.component';
|
||||
import { provideScreen } from '../../../services/provide-screen';
|
||||
@@ -66,11 +67,11 @@ describe('StartProcessScreenCloudComponent', () => {
|
||||
|
||||
it('should set appName', () => {
|
||||
const screenInstance: StartProcessScreenCloud = fixture.debugElement.query(By.directive(MockedTaskScreenCloudComponent)).componentInstance;
|
||||
expect(screenInstance.appName()).toEqual('');
|
||||
expect(screenInstance.appName?.()).toEqual('');
|
||||
const newValue = 'new-app-name';
|
||||
fixture.componentRef.setInput('appName', newValue);
|
||||
fixture.detectChanges();
|
||||
expect(screenInstance.appName()).toEqual(newValue);
|
||||
expect(screenInstance.appName?.()).toEqual(newValue);
|
||||
});
|
||||
|
||||
it('should set process definition id', () => {
|
||||
@@ -84,10 +85,125 @@ describe('StartProcessScreenCloudComponent', () => {
|
||||
|
||||
it('should set resolvedValues', () => {
|
||||
const screenInstance: StartProcessScreenCloud = fixture.debugElement.query(By.directive(MockedTaskScreenCloudComponent)).componentInstance;
|
||||
expect(screenInstance.resolvedValues()).toBeUndefined();
|
||||
expect(screenInstance.resolvedValues?.()).toBeUndefined();
|
||||
const newValues = [new TaskVariableCloud({ id: 'new-id', name: 'new-name' })];
|
||||
fixture.componentRef.setInput('resolvedValues', newValues);
|
||||
fixture.detectChanges();
|
||||
expect(screenInstance.resolvedValues()).toEqual(newValues);
|
||||
expect(screenInstance.resolvedValues?.()).toEqual(newValues);
|
||||
});
|
||||
});
|
||||
|
||||
@Component({
|
||||
selector: 'adf-cloud-destroy-tracking-screen',
|
||||
template: `<div class="adf-cloud-destroy-tracking-screen">screen</div>`
|
||||
})
|
||||
class DestroyTrackingScreenComponent implements StartProcessScreenCloud, OnDestroy {
|
||||
readonly appName = input('');
|
||||
processDefinitionId = input('');
|
||||
readonly resolvedValues = input<TaskVariableCloud[] | undefined>();
|
||||
defaultStartProcessButtonsConfigurationChange = output<StartProcessScreenDefaultButtons>();
|
||||
startProcessPayloadChanged = output<unknown>();
|
||||
|
||||
destroyed = false;
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'adf-cloud-test-start-process-wrapper',
|
||||
template: `
|
||||
@if (showScreen) {
|
||||
<adf-cloud-start-process-screen-cloud [screenId]="screenId" [processDefinitionId]="'definition-id'" />
|
||||
}
|
||||
`,
|
||||
imports: [StartProcessScreenCloudComponent]
|
||||
})
|
||||
class TestStartProcessWrapperComponent {
|
||||
@Input() screenId = '';
|
||||
showScreen = true;
|
||||
}
|
||||
|
||||
describe('StartProcessScreenCloudComponent - destroy', () => {
|
||||
let fixture: ComponentFixture<TestStartProcessWrapperComponent>;
|
||||
let component: TestStartProcessWrapperComponent;
|
||||
const screenId = 'screen-1234-5678-121212-123456';
|
||||
|
||||
const getScreenInstance = (): DestroyTrackingScreenComponent =>
|
||||
fixture.debugElement.query(By.directive(DestroyTrackingScreenComponent)).componentInstance;
|
||||
|
||||
const destroyScreen = () => {
|
||||
component.showScreen = false;
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TestStartProcessWrapperComponent],
|
||||
providers: [provideScreen(screenId, DestroyTrackingScreenComponent)]
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(TestStartProcessWrapperComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.componentRef.setInput('screenId', screenId);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should destroy the screen component when the host is destroyed', () => {
|
||||
const screenInstance = getScreenInstance();
|
||||
expect(screenInstance.destroyed).toBeFalse();
|
||||
|
||||
destroyScreen();
|
||||
|
||||
expect(screenInstance.destroyed).toBeTrue();
|
||||
});
|
||||
|
||||
it('should remove the screen component from the DOM when the host is destroyed', () => {
|
||||
expect(fixture.debugElement.query(By.css('.adf-cloud-destroy-tracking-screen'))).toBeTruthy();
|
||||
|
||||
destroyScreen();
|
||||
|
||||
expect(fixture.debugElement.query(By.css('.adf-cloud-destroy-tracking-screen'))).toBeNull();
|
||||
});
|
||||
|
||||
it('should create a new screen component instance when the host is re-created', () => {
|
||||
const firstInstance = getScreenInstance();
|
||||
|
||||
destroyScreen();
|
||||
component.showScreen = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
const secondInstance = getScreenInstance();
|
||||
expect(secondInstance).not.toBe(firstInstance);
|
||||
expect(secondInstance.destroyed).toBeFalse();
|
||||
expect(secondInstance.processDefinitionId()).toBe('definition-id');
|
||||
});
|
||||
});
|
||||
|
||||
describe('StartProcessScreenCloudComponent - without screenId', () => {
|
||||
let fixture: ComponentFixture<StartProcessScreenCloudComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [StartProcessScreenCloudComponent]
|
||||
});
|
||||
fixture = TestBed.createComponent(StartProcessScreenCloudComponent);
|
||||
});
|
||||
|
||||
it('should not create any screen component and should not throw', () => {
|
||||
expect(() => fixture.detectChanges()).not.toThrow();
|
||||
expect(fixture.debugElement.query(By.directive(DestroyTrackingScreenComponent))).toBeNull();
|
||||
});
|
||||
|
||||
it('should not throw when inputs change or on destroy', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(() => {
|
||||
fixture.componentRef.setInput('appName', 'new-app-name');
|
||||
fixture.componentRef.setInput('resolvedValues', [new TaskVariableCloud({ id: 'id', name: 'name' })]);
|
||||
fixture.detectChanges();
|
||||
}).not.toThrow();
|
||||
expect(() => fixture.destroy()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
+5
-5
@@ -15,7 +15,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ChangeDetectionStrategy, Component, effect, input, output, signal } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, ComponentRef, effect, input, output, signal } from '@angular/core';
|
||||
import { BaseScreenCloudComponent } from '../base-screen/base-screen-cloud.component';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { CommonModule } from '@angular/common';
|
||||
@@ -43,7 +43,7 @@ export class StartProcessScreenCloudComponent extends BaseScreenCloudComponent<S
|
||||
super();
|
||||
effect(() => {
|
||||
const componentRef = this.componentRefChanged();
|
||||
if (componentRef.instance && 'appName' in componentRef.instance) {
|
||||
if (componentRef?.instance && 'appName' in componentRef.instance) {
|
||||
componentRef.setInput('appName', this.appName());
|
||||
}
|
||||
});
|
||||
@@ -56,9 +56,9 @@ export class StartProcessScreenCloudComponent extends BaseScreenCloudComponent<S
|
||||
});
|
||||
}
|
||||
|
||||
protected subscribeToOutputs(): void {
|
||||
this.componentRef.instance.startProcessPayloadChanged.subscribe((payload) => this.screenStartProcessPayloadChange.emit(payload));
|
||||
this.componentRef.instance.defaultStartProcessButtonsConfigurationChange.subscribe((config) => {
|
||||
protected subscribeToOutputs(componentRef: ComponentRef<StartProcessScreenCloud>): void {
|
||||
componentRef.instance.startProcessPayloadChanged.subscribe((payload) => this.screenStartProcessPayloadChange.emit(payload));
|
||||
componentRef.instance.defaultStartProcessButtonsConfigurationChange.subscribe((config) => {
|
||||
this.showStartProcessButtons.set(config.show);
|
||||
this.disableStartProcessButton.emit(config.disable);
|
||||
});
|
||||
|
||||
+121
-5
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnDestroy, Output, ViewChild } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ScreenRenderingService } from '../../../services/screen-rendering.service';
|
||||
import { TaskScreenCloudComponent } from './screen-cloud.component';
|
||||
@@ -32,18 +32,22 @@ import { TaskScreenCloudComponent } from './screen-cloud.component';
|
||||
</div>
|
||||
`
|
||||
})
|
||||
class TestComponent {
|
||||
class TestComponent implements OnDestroy {
|
||||
@Input() taskId = '';
|
||||
@Input() screenId = '';
|
||||
@Input() rootProcessInstanceId = '';
|
||||
@Output() taskCompleted = new EventEmitter();
|
||||
displayMode: string;
|
||||
displayMode: string | undefined;
|
||||
destroyed = false;
|
||||
onComplete() {
|
||||
this.taskCompleted.emit();
|
||||
}
|
||||
switchToDisplayMode(newDisplayMode?: string) {
|
||||
this.displayMode = newDisplayMode;
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -61,7 +65,7 @@ class TestComponent {
|
||||
})
|
||||
class TestWrapperComponent {
|
||||
@Input() screenId = '';
|
||||
@ViewChild('adfCloudTaskScreen') adfCloudTaskScreen: TaskScreenCloudComponent;
|
||||
@ViewChild('adfCloudTaskScreen') adfCloudTaskScreen: TaskScreenCloudComponent | undefined;
|
||||
onTaskCompleted() {}
|
||||
switchToDisplayMode(newDisplayMode?: string): void {
|
||||
if (this.adfCloudTaskScreen) {
|
||||
@@ -118,6 +122,118 @@ describe('TaskScreenCloudComponent', () => {
|
||||
component.switchToDisplayMode();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.adfCloudTaskScreen.switchToDisplayMode).toHaveBeenCalled();
|
||||
expect(component.adfCloudTaskScreen?.switchToDisplayMode).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@Component({
|
||||
selector: 'adf-cloud-test-conditional-component',
|
||||
template: `
|
||||
@if (showTaskScreen) {
|
||||
<adf-cloud-task-screen [taskId]="'1'" [appName]="'app-name-test'" [screenId]="'test'" (taskCompleted)="onTaskCompleted()" />
|
||||
}
|
||||
`,
|
||||
imports: [TaskScreenCloudComponent]
|
||||
})
|
||||
class TestConditionalWrapperComponent {
|
||||
showTaskScreen = true;
|
||||
onTaskCompleted() {}
|
||||
}
|
||||
|
||||
describe('TaskScreenCloudComponent - destroy', () => {
|
||||
let fixture: ComponentFixture<TestConditionalWrapperComponent>;
|
||||
let component: TestConditionalWrapperComponent;
|
||||
|
||||
const getDynamicComponentInstance = (): TestComponent => fixture.debugElement.query(By.directive(TestComponent)).componentInstance;
|
||||
|
||||
const getTaskScreen = (): TaskScreenCloudComponent => fixture.debugElement.query(By.directive(TaskScreenCloudComponent)).componentInstance;
|
||||
|
||||
const destroyTaskScreen = () => {
|
||||
component.showTaskScreen = false;
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TaskScreenCloudComponent, TestComponent, TestConditionalWrapperComponent]
|
||||
});
|
||||
TestBed.inject(ScreenRenderingService).register({ ['test']: () => TestComponent });
|
||||
|
||||
fixture = TestBed.createComponent(TestConditionalWrapperComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should destroy the dynamic component when the task screen is destroyed', () => {
|
||||
const dynamicComponentInstance = getDynamicComponentInstance();
|
||||
expect(dynamicComponentInstance.destroyed).toBeFalse();
|
||||
|
||||
destroyTaskScreen();
|
||||
|
||||
expect(dynamicComponentInstance.destroyed).toBeTrue();
|
||||
});
|
||||
|
||||
it('should remove the dynamic component from the DOM when the task screen is destroyed', () => {
|
||||
expect(fixture.debugElement.query(By.css('.adf-cloud-test-container'))).toBeTruthy();
|
||||
|
||||
destroyTaskScreen();
|
||||
|
||||
expect(fixture.debugElement.query(By.css('.adf-cloud-test-container'))).toBeNull();
|
||||
});
|
||||
|
||||
it('should not emit outputs of the dynamic component after the task screen is destroyed', () => {
|
||||
const onTaskCompletedSpy = spyOn(component, 'onTaskCompleted');
|
||||
const dynamicComponentInstance = getDynamicComponentInstance();
|
||||
|
||||
destroyTaskScreen();
|
||||
dynamicComponentInstance.taskCompleted.emit();
|
||||
|
||||
expect(onTaskCompletedSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call the dynamic component when switching display mode after destroy', () => {
|
||||
const taskScreen = getTaskScreen();
|
||||
const switchToDisplayModeSpy = spyOn(getDynamicComponentInstance(), 'switchToDisplayMode');
|
||||
|
||||
destroyTaskScreen();
|
||||
|
||||
expect(() => taskScreen.switchToDisplayMode('mode')).not.toThrow();
|
||||
expect(switchToDisplayModeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create a new dynamic component instance when the task screen is re-created', () => {
|
||||
const firstInstance = getDynamicComponentInstance();
|
||||
|
||||
destroyTaskScreen();
|
||||
component.showTaskScreen = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
const secondInstance = getDynamicComponentInstance();
|
||||
expect(secondInstance).not.toBe(firstInstance);
|
||||
expect(secondInstance.destroyed).toBeFalse();
|
||||
expect(secondInstance.taskId).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TaskScreenCloudComponent - without screenId', () => {
|
||||
let fixture: ComponentFixture<TaskScreenCloudComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TaskScreenCloudComponent]
|
||||
});
|
||||
fixture = TestBed.createComponent(TaskScreenCloudComponent);
|
||||
});
|
||||
|
||||
it('should not create any dynamic component and should not throw', () => {
|
||||
expect(() => fixture.detectChanges()).not.toThrow();
|
||||
expect(fixture.debugElement.query(By.directive(TestComponent))).toBeNull();
|
||||
});
|
||||
|
||||
it('should not throw when switching display mode or destroying', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(() => fixture.componentInstance.switchToDisplayMode('mode')).not.toThrow();
|
||||
expect(() => fixture.destroy()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
+39
-39
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, DestroyRef, EventEmitter, inject, Input, Output } from '@angular/core';
|
||||
import { Component, ComponentRef, DestroyRef, EventEmitter, inject, Input, Output } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { UserTaskCustomUi } from './screen-cloud.model';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
@@ -102,65 +102,65 @@ export class TaskScreenCloudComponent extends BaseScreenCloudComponent<UserTaskC
|
||||
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
|
||||
protected override setInputsForDynamicComponent(): void {
|
||||
if (this.taskId && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'taskId')) {
|
||||
this.componentRef.setInput('taskId', this.taskId);
|
||||
protected override setInputsForDynamicComponent(componentRef: ComponentRef<UserTaskCustomUi>): void {
|
||||
if (this.taskId && Object.prototype.hasOwnProperty.call(componentRef.instance, 'taskId')) {
|
||||
componentRef.setInput('taskId', this.taskId);
|
||||
}
|
||||
if (this.appName && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'appName')) {
|
||||
this.componentRef.setInput('appName', this.appName);
|
||||
if (this.appName && Object.prototype.hasOwnProperty.call(componentRef.instance, 'appName')) {
|
||||
componentRef.setInput('appName', this.appName);
|
||||
}
|
||||
if (this.screenId && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'screenId')) {
|
||||
this.componentRef.setInput('screenId', this.screenId);
|
||||
if (this.screenId && Object.prototype.hasOwnProperty.call(componentRef.instance, 'screenId')) {
|
||||
componentRef.setInput('screenId', this.screenId);
|
||||
}
|
||||
if (this.processInstanceId && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'processInstanceId')) {
|
||||
this.componentRef.setInput('processInstanceId', this.processInstanceId);
|
||||
if (this.processInstanceId && Object.prototype.hasOwnProperty.call(componentRef.instance, 'processInstanceId')) {
|
||||
componentRef.setInput('processInstanceId', this.processInstanceId);
|
||||
}
|
||||
if (this.taskName && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'taskName')) {
|
||||
this.componentRef.setInput('taskName', this.taskName);
|
||||
if (this.taskName && Object.prototype.hasOwnProperty.call(componentRef.instance, 'taskName')) {
|
||||
componentRef.setInput('taskName', this.taskName);
|
||||
}
|
||||
if (this.canClaimTask && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'canClaimTask')) {
|
||||
this.componentRef.setInput('canClaimTask', this.canClaimTask);
|
||||
if (this.canClaimTask && Object.prototype.hasOwnProperty.call(componentRef.instance, 'canClaimTask')) {
|
||||
componentRef.setInput('canClaimTask', this.canClaimTask);
|
||||
}
|
||||
if (this.canUnclaimTask && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'canUnclaimTask')) {
|
||||
this.componentRef.setInput('canUnclaimTask', this.canUnclaimTask);
|
||||
if (this.canUnclaimTask && Object.prototype.hasOwnProperty.call(componentRef.instance, 'canUnclaimTask')) {
|
||||
componentRef.setInput('canUnclaimTask', this.canUnclaimTask);
|
||||
}
|
||||
if (this.showCancelButton && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'showCancelButton')) {
|
||||
this.componentRef.setInput('showCancelButton', this.showCancelButton);
|
||||
if (this.showCancelButton && Object.prototype.hasOwnProperty.call(componentRef.instance, 'showCancelButton')) {
|
||||
componentRef.setInput('showCancelButton', this.showCancelButton);
|
||||
}
|
||||
if (this.rootProcessInstanceId && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'rootProcessInstanceId')) {
|
||||
this.componentRef.setInput('rootProcessInstanceId', this.rootProcessInstanceId);
|
||||
if (this.rootProcessInstanceId && Object.prototype.hasOwnProperty.call(componentRef.instance, 'rootProcessInstanceId')) {
|
||||
componentRef.setInput('rootProcessInstanceId', this.rootProcessInstanceId);
|
||||
}
|
||||
if (this.showNextTaskCheckbox && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'showNextTaskCheckbox')) {
|
||||
this.componentRef.setInput('showNextTaskCheckbox', this.showNextTaskCheckbox);
|
||||
if (this.showNextTaskCheckbox && Object.prototype.hasOwnProperty.call(componentRef.instance, 'showNextTaskCheckbox')) {
|
||||
componentRef.setInput('showNextTaskCheckbox', this.showNextTaskCheckbox);
|
||||
}
|
||||
if (this.isNextTaskCheckboxChecked && Object.prototype.hasOwnProperty.call(this.componentRef.instance, 'isNextTaskCheckboxChecked')) {
|
||||
this.componentRef.setInput('isNextTaskCheckboxChecked', this.isNextTaskCheckboxChecked);
|
||||
if (this.isNextTaskCheckboxChecked && Object.prototype.hasOwnProperty.call(componentRef.instance, 'isNextTaskCheckboxChecked')) {
|
||||
componentRef.setInput('isNextTaskCheckboxChecked', this.isNextTaskCheckboxChecked);
|
||||
}
|
||||
}
|
||||
|
||||
protected override subscribeToOutputs(): void {
|
||||
if (this.componentRef.instance?.taskSaved) {
|
||||
this.componentRef.instance.taskSaved.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.taskSaved.emit());
|
||||
protected override subscribeToOutputs(componentRef: ComponentRef<UserTaskCustomUi>): void {
|
||||
if (componentRef.instance?.taskSaved) {
|
||||
componentRef.instance.taskSaved.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => this.taskSaved.emit());
|
||||
}
|
||||
if (this.componentRef.instance?.taskCompleted) {
|
||||
this.componentRef.instance.taskCompleted
|
||||
if (componentRef.instance?.taskCompleted) {
|
||||
componentRef.instance.taskCompleted
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((openNextTask) => this.taskCompleted.emit(openNextTask));
|
||||
}
|
||||
if (this.componentRef.instance?.error) {
|
||||
this.componentRef.instance.error.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.error.emit(data));
|
||||
if (componentRef.instance?.error) {
|
||||
componentRef.instance.error.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.error.emit(data));
|
||||
}
|
||||
if (this.componentRef.instance?.claimTask) {
|
||||
this.componentRef.instance.claimTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.claimTask.emit(data));
|
||||
if (componentRef.instance?.claimTask) {
|
||||
componentRef.instance.claimTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.claimTask.emit(data));
|
||||
}
|
||||
if (this.componentRef.instance?.unclaimTask) {
|
||||
this.componentRef.instance.unclaimTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.unclaimTask.emit(data));
|
||||
if (componentRef.instance?.unclaimTask) {
|
||||
componentRef.instance.unclaimTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.unclaimTask.emit(data));
|
||||
}
|
||||
if (this.componentRef.instance?.cancelTask) {
|
||||
this.componentRef.instance.cancelTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.cancelTask.emit(data));
|
||||
if (componentRef.instance?.cancelTask) {
|
||||
componentRef.instance.cancelTask.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => this.cancelTask.emit(data));
|
||||
}
|
||||
if (this.componentRef.instance?.nextTaskCheckboxCheckedChanged) {
|
||||
this.componentRef.instance.nextTaskCheckboxCheckedChanged
|
||||
if (componentRef.instance?.nextTaskCheckboxCheckedChanged) {
|
||||
componentRef.instance.nextTaskCheckboxCheckedChanged
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((data) => this.nextTaskCheckboxCheckedChanged.emit(data));
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ module.exports = function (config) {
|
||||
coverageReporter: {
|
||||
dir: join(__dirname, '../../coverage/process-services'),
|
||||
subdir: '.',
|
||||
reporters: [{ type: 'html' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
reporters: [{ type: 'html' }, { type: 'lcov' }, { type: 'text-summary' }, { type: 'text-summary', subdir: '.', file: 'summary.txt' }],
|
||||
check: {
|
||||
global: {
|
||||
statements: 75,
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/cdk": ">=20.2.14",
|
||||
"@angular/common": ">=20.3.25",
|
||||
"@angular/compiler": ">=20.3.25",
|
||||
"@angular/core": ">=20.3.25",
|
||||
"@angular/forms": ">=20.3.25",
|
||||
"@angular/common": ">=20.3.27",
|
||||
"@angular/compiler": ">=20.3.27",
|
||||
"@angular/core": ">=20.3.27",
|
||||
"@angular/forms": ">=20.3.27",
|
||||
"@angular/material": ">=20.2.14",
|
||||
"@angular/platform-browser": ">=20.3.25",
|
||||
"@angular/platform-browser-dynamic": ">=20.3.25",
|
||||
"@angular/router": ">=20.3.25",
|
||||
"@angular/platform-browser": ">=20.3.27",
|
||||
"@angular/platform-browser-dynamic": ">=20.3.27",
|
||||
"@angular/router": ">=20.3.27",
|
||||
"@alfresco/js-api": ">=10.0.0",
|
||||
"@alfresco/adf-core": ">=9.0.0",
|
||||
"@alfresco/adf-content-services": ">=9.0.0",
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"cache": true
|
||||
},
|
||||
"test": {
|
||||
"cache": true
|
||||
"cache": true,
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectName}"]
|
||||
},
|
||||
"stylelint": {
|
||||
"cache": true
|
||||
|
||||
+12
-12
@@ -40,17 +40,17 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@angular-eslint/utils": "20.7.0",
|
||||
"@angular/animations": "20.3.26",
|
||||
"@angular/animations": "20.3.27",
|
||||
"@angular/cdk": "20.2.14",
|
||||
"@angular/common": "20.3.27",
|
||||
"@angular/compiler": "20.3.27",
|
||||
"@angular/core": "20.3.27",
|
||||
"@angular/forms": "20.3.26",
|
||||
"@angular/forms": "20.3.27",
|
||||
"@angular/material": "20.2.14",
|
||||
"@angular/material-date-fns-adapter": "20.2.14",
|
||||
"@angular/platform-browser": "20.3.26",
|
||||
"@angular/platform-browser-dynamic": "20.3.26",
|
||||
"@angular/router": "20.3.26",
|
||||
"@angular/platform-browser": "20.3.27",
|
||||
"@angular/platform-browser-dynamic": "20.3.27",
|
||||
"@angular/router": "20.3.27",
|
||||
"@apollo/client": "3.13.1",
|
||||
"@cspell/eslint-plugin": "10.0.0",
|
||||
"@mat-datetimepicker/core": "16.0.1",
|
||||
@@ -83,7 +83,7 @@
|
||||
"@angular-eslint/eslint-plugin-template": "20.7.0",
|
||||
"@angular-eslint/template-parser": "22.1.0",
|
||||
"@angular/build": "20.3.32",
|
||||
"@angular/compiler-cli": "20.3.26",
|
||||
"@angular/compiler-cli": "20.3.27",
|
||||
"@chromatic-com/storybook": "4.1.3",
|
||||
"@eslint/compat": "^2.1.0",
|
||||
"@nx/angular": "23.1.1",
|
||||
@@ -98,10 +98,10 @@
|
||||
"@types/jasminewd2": "2.0.13",
|
||||
"@types/node": "26.1.1",
|
||||
"@types/sinon": "22.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.66.0",
|
||||
"@typescript-eslint/parser": "8.66.0",
|
||||
"@typescript-eslint/typescript-estree": "8.66.0",
|
||||
"@typescript-eslint/utils": "8.66.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.67.0",
|
||||
"@typescript-eslint/parser": "8.67.0",
|
||||
"@typescript-eslint/typescript-estree": "8.67.0",
|
||||
"@typescript-eslint/utils": "8.67.0",
|
||||
"ajv": "8.20.0",
|
||||
"dotenv": "16.4.7",
|
||||
"eslint": "10.8.0",
|
||||
@@ -115,7 +115,7 @@
|
||||
"eslint-plugin-unicorn": "61.0.2",
|
||||
"graphql": "16.14.0",
|
||||
"husky": "9.1.7",
|
||||
"jasmine-core": "5.13.0",
|
||||
"jasmine-core": "6.3.0",
|
||||
"jasmine-reporters": "2.5.2",
|
||||
"jsonc-eslint-parser": "2.4.2",
|
||||
"karma": "6.4.4",
|
||||
@@ -140,7 +140,7 @@
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"undici": "8.9.0",
|
||||
"webpack": "5.109.0"
|
||||
"webpack": "5.109.2"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
|
||||
Generated
+471
-347
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -8,9 +8,10 @@ minimumReleaseAgeStrict: true
|
||||
overrides:
|
||||
# Security fixes
|
||||
fast-uri: 3.1.5
|
||||
"js-yaml@4.2.0": "4.3.0"
|
||||
"js-yaml@4.2.0": "4.3.1"
|
||||
"brace-expansion@5.0.8": "5.0.9"
|
||||
"brace-expansion@<1.1.18": "1.1.18"
|
||||
"socket.io-parser@4.2.6": 4.2.7
|
||||
svgo: 4.0.2
|
||||
shell-quote: 1.9.0
|
||||
adm-zip: 0.6.0
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
sonar.organization=alfresco
|
||||
sonar.projectKey=Alfresco_alfresco-ng2-components
|
||||
|
||||
sonar.sources=lib
|
||||
sonar.tests=lib
|
||||
sonar.test.inclusions=**/*.spec.ts
|
||||
sonar.exclusions=**/node_modules/**,**/dist/**,**/*.spec.ts,**/*.mock.ts,**/mock/**,**/mocks/**,**/testing/**,**/stories/**
|
||||
|
||||
sonar.javascript.lcov.reportPaths=coverage/core/lcov.info,coverage/content-services/lcov.info,coverage/extensions/lcov.info,coverage/insights/lcov.info,coverage/process-services/lcov.info,coverage/process-services-cloud/lcov.info
|
||||
|
||||
sonar.sourceEncoding=UTF-8
|
||||
Reference in New Issue
Block a user