diff --git a/.github/workflows/ci_optimize_svgs.yml b/.github/workflows/ci_optimize_svgs.yml deleted file mode 100644 index 3c3bec5bc8..0000000000 --- a/.github/workflows/ci_optimize_svgs.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: SVG Optimization Workflow -on: - push: - # trigger on all branches except for main, release and dependabot-triggered push events - branches-ignore: [main, release, dependabot/**] - tags-ignore: [v*] - paths: - - '**.svg' - -# The minimum required permissions -permissions: - contents: write - -jobs: - svgs: - name: Optimize SVGs - runs-on: ubuntu-latest - if: github.repository_owner == 'webern-unibas-ch' - steps: - - name: Checkout repository - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # ratchet:actions/checkout@v4.1.3 - - name: Optimize SVGs - uses: ericcornelissen/svgo-action@b8b3198fffbb1210e81aa68cde2ca4e4568d5386 # ratchet:ericcornelissen/svgo-action@v4.0.8 - id: svgo - with: - repo-token: ${{secrets.GITHUB_TOKEN}} - svgo-version: 3 # defaults to 2 - - name: Commit optimizations - uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842 # ratchet:stefanzweifel/git-auto-commit-action@v5.0.1 - if: ${{steps.svgo.outputs.DID_OPTIMIZE}} - with: - commit_message: 'fix(assets): optimize ${{steps.svgo.outputs.OPTIMIZED_COUNT}} SVG(s) with SVGO' diff --git a/.github/workflows/ci_workflow.yml b/.github/workflows/ci_workflow.yml index 974ed9d522..22b7509e67 100644 --- a/.github/workflows/ci_workflow.yml +++ b/.github/workflows/ci_workflow.yml @@ -11,66 +11,196 @@ on: branches: [develop] types: [opened, synchronize, reopened] +permissions: + contents: read + # globals env: # general settings MAIN_REPO_OWNER: webern-unibas-ch # Main repo owner (default: webern-unibas-ch; should not be changed) + # dev settings + DEV_REPO: webern-unibas-ch/awg-app-dev + DEV_GH_PAGES_BRANCH: gh-pages + DEV_GH_PAGES_DIR: gh-pages-dir + DIST_DIR: dist + jobs: test: name: Run tests (Node v${{ matrix.node-version }}, ${{ matrix.os }}) runs-on: ${{ matrix.os }} + strategy: matrix: os: [ubuntu-latest] node-version: [18.19, 20.9] # TODO (when Angular allows it): 21.x + + outputs: + sha: ${{ steps.get-sha.outputs.SHA }} + steps: + - name: Harden Runner + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + with: + egress-policy: audit + - name: Checkout repository - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # ratchet:actions/checkout@v4.1.3 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # ratchet:actions/checkout@v4.1.5 with: fetch-depth: 0 # Get all history and branches + + - name: Get git sha + id: get-sha + run: echo "SHA=$(git describe)" >> $GITHUB_OUTPUT + + - name: Verify git sha + run: | + echo "SHA: ${{ steps.get-sha.outputs.SHA }}" + - name: Set up node ${{ matrix.node-version}} uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # ratchet:actions/setup-node@v4.0.2 with: node-version: ${{ matrix.node-version }} cache: 'yarn' + - name: yarn install dependencies run: | yarn install + - name: Run CI tests with coverage run: | yarn run test:ci + - name: Upload code coverage if: matrix.node-version == 20.9 # upload coverage report for current node version only - uses: codecov/codecov-action@84508663e988701840491b86de86b666e8a86bed # ratchet:codecov/codecov-action@v4.3.0 + uses: codecov/codecov-action@6d798873df2b1b8e5846dba6fb86631229fbcb17 # v4.4.0 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: flags: unittests env_vars: ${{ matrix.os }}, ${{ matrix.node-version }} + - name: Perform SonarCloud Analysis if: matrix.node-version == 20.9 && github.event_name != 'pull_request' && github.repository_owner == env.MAIN_REPO_OWNER # perform SonarCloud analysis only for current node version and not with pull requests or forks(token issue) uses: SonarSource/sonarcloud-github-action@49e6cd3b187936a73b8280d59ffd9da69df63ec9 # ratchet:SonarSource/sonarcloud-github-action@v2.1.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - - name: Test build for GH Pages + + - name: Test build from develop for GH Pages + if: github.ref == 'refs/heads/develop' + run: | + echo "Updating dev-version" + yarn run pre-release --release-as ${{ steps.get-sha.outputs.SHA }} --skip.changelog --skip.commit --skip.tag + echo "Building dev-version" + yarn run build:dev + + - name: Test build from main for GH Pages + if: github.ref == 'refs/heads/main' run: | yarn run build:gh + + - name: Upload build artifacts + if: matrix.node-version == 20.9 # upload build artifacts for current node version only + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # ratchet:actions/upload-artifact@v4.3.3 + with: + name: dist + path: ${{ github.workspace }}/${{ env.DIST_DIR }} + retention-days: 1 + + deploy_dev: + # run only on develop + if: github.ref == 'refs/heads/develop' + + name: Deploy app from develop (Node v${{ matrix.node-version }}, ${{ matrix.os }}) + runs-on: ${{ matrix.os }} + needs: test + + permissions: + contents: write + + env: + SHA: ${{ needs.test.outputs.sha }} + + strategy: + matrix: + os: [ubuntu-latest] + node-version: [20.9] + + steps: + - name: Harden Runner + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # ratchet:actions/checkout@v4.1.5 + with: + # ref (branch, tag or SHA) to check out + ref: ${{ env.DEV_GH_PAGES_BRANCH }} + # relative path under $GITHUB_WORKSPACE to place the repository + path: ${{ env.DEV_GH_PAGES_DIR }} + + - name: Download build artifacts + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # ratchet:actions/upload-artifact@v4.1.7 + with: + name: dist + path: ${{ github.workspace }}/${{ env.DIST_DIR }} + + - name: Copy artifacts to gh-pages + run: | + cp -r ${{ env.DIST_DIR }}/awg-app/. ${{ env.DEV_GH_PAGES_DIR }}/dev/ + + - name: Configure git + working-directory: ${{ env.DEV_GH_PAGES_DIR }} + run: | + echo "Configuring git" + git config user.name "github-actions" + git config user.email "github-actions@users.noreply.github.com" + + - name: Commit files + working-directory: ${{ env.DEV_GH_PAGES_DIR }} + run: | + echo "Running git commit" + git add . + git commit -m "Staging dev (${{ env.SHA }}) on gh-pages" + + - name: Push changes to gh-pages (dry-run mode) + working-directory: ${{ env.DEV_GH_PAGES_DIR }} + run: git push -v --dry-run origin HEAD:$DEV_GH_PAGES_BRANCH + + - name: Push changes to gh-pages + working-directory: ${{ env.DEV_GH_PAGES_DIR }} + run: git push -v origin HEAD:$DEV_GH_PAGES_BRANCH + + - name: Congratulations + if: ${{ success() }} + run: echo "🎉 New develop build deployed 🎊" + deploy: + # run only on main + if: github.ref == 'refs/heads/main' + name: Deploy app from main (Node v${{ matrix.node-version }}, ${{ matrix.os }}) runs-on: ${{ matrix.os }} needs: test - # run only on main - if: github.ref == 'refs/heads/main' + permissions: + contents: write + strategy: matrix: os: [ubuntu-latest] node-version: [20.9] + steps: + - name: Harden Runner + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + with: + egress-policy: audit + - name: Checkout repository - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # ratchet:actions/checkout@v4.1.3 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # ratchet:actions/checkout@v4.1.5 - name: Set up node ${{ matrix.node-version}} uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # ratchet:actions/setup-node@v4.0.2 with: @@ -87,25 +217,35 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | yarn run deploy:ci + release: + # run only on tags + if: startsWith(github.ref, 'refs/tags/') + name: Create Release from tag runs-on: ${{ matrix.os }} needs: test - # run only on tags - if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: write + strategy: matrix: os: [ubuntu-latest] node-version: [20.9] steps: + - name: Harden Runner + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + with: + egress-policy: audit + - name: Get tag version id: get_version run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/} - name: Create Release id: create_release if: ${{ success() && startsWith(github.ref, 'refs/tags/') }} - uses: softprops/action-gh-release@9d7c94cfd0a1f3ed45544c887983e9fa900f0564 # ratchet:softprops/action-gh-release@v2.0.4 + uses: softprops/action-gh-release@69320dbe05506a9a39fc8ae11030b214ec2d1f87 # ratchet:softprops/action-gh-release@v2.0.5 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG_VERSION: ${{ steps.get_version.outputs.VERSION }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql.yml similarity index 64% rename from .github/workflows/codeql-analysis.yml rename to .github/workflows/codeql.yml index d1e2bd0efd..068c567cec 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql.yml @@ -8,6 +8,9 @@ on: pull_request: branches: [develop] +permissions: + contents: read + jobs: analyze: name: Analyze @@ -22,19 +25,24 @@ jobs: language: ['javascript'] steps: + - name: Harden Runner + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + with: + egress-policy: audit + - name: Checkout repository - uses: actions/checkout@1d96c772d19495a3b5c517cd2bc0cb401ea0529f # ratchet:actions/checkout@v4.1.3 + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 with: fetch-depth: 2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@cdcdbb579706841c47f7063dda365e292e5cad7a # ratchet:github/codeql-action/init@v2.13.4 + uses: github/codeql-action/init@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5 with: languages: ${{ matrix.language }} # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@cdcdbb579706841c47f7063dda365e292e5cad7a # ratchet:github/codeql-action/autobuild@v2.13.4 + uses: github/codeql-action/autobuild@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cdcdbb579706841c47f7063dda365e292e5cad7a # ratchet:github/codeql-action/analyze@v2.13.4 + uses: github/codeql-action/analyze@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5 with: category: '/language:${{matrix.language}}' diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000000..eea3e827b9 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,27 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, +# surfacing known-vulnerable versions of the packages declared or updated in the PR. +# Once installed, if the workflow run is marked as required, +# PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + with: + egress-policy: audit + + - name: 'Checkout Repository' + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + - name: 'Dependency Review' + uses: actions/dependency-review-action@0c155c5e8556a497adf53f2c18edabf945ed8e70 # v4.3.2 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml new file mode 100644 index 0000000000..2eef15e29d --- /dev/null +++ b/.github/workflows/scorecards.yml @@ -0,0 +1,76 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '20 7 * * 2' + push: + branches: ["develop"] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + contents: read + actions: read + + steps: + - name: Harden Runner + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + with: + egress-policy: audit + + - name: "Checkout code" + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecards on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@b7cec7526559c32f1616476ff32d17ba4c59b2d6 # v3.25.5 + with: + sarif_file: results.sarif diff --git a/.github/workflows/visualize-repo.yml b/.github/workflows/visualize-repo.yml new file mode 100644 index 0000000000..8894752aee --- /dev/null +++ b/.github/workflows/visualize-repo.yml @@ -0,0 +1,30 @@ +name: Visualize repo with diagram + +on: + workflow_dispatch: {} + +permissions: + contents: read + +jobs: + visualize_repo: + permissions: + contents: write # for githubocto/repo-visualizer to commit and push diagrams + runs-on: ubuntu-latest + + steps: + - name: Harden Runner + uses: step-security/harden-runner@a4aa98b93cab29d9b1101a6143fb8bce00e2eac4 # v2.7.1 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # ratchet:actions/checkout@v4.1.5 + with: + ref: develop + token: ${{ secrets.REPO_TOKEN }} + + - name: Update diagram + uses: githubocto/repo-visualizer@a999615bdab757559bf94bda1fe6eef232765f85 # ratchet:githubocto/repo-visualizer@v0.9.1 + with: + excluded_paths: ".github,.husky,.yarn" diff --git a/.zenodo.json b/.zenodo.json index f6328cc49e..56efb64a5f 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -11,6 +11,16 @@ "name": "Thomas Ahrend", "type": "ProjectMember", "affiliation": "@webern-unibas-ch" + }, + { + "name": "Michael Matter", + "type": "ProjectMember", + "affiliation": "@webern-unibas-ch" + }, + { + "name": "Barbara Schingnitz", + "type": "ProjectMember", + "affiliation": "@webern-unibas-ch" } ], "license": "MIT", diff --git a/CHANGELOG.md b/CHANGELOG.md index 60749e5ca4..13c8fb9bef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,140 @@ # Changelog -All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. + +## [0.12.1](https://github.com/webern-unibas-ch/awg-app/compare/v0.12.0...v0.12.1) (2024-05-15) + +### Features + +- **app:** add DSP_API_URL to AppConfig ([e634f0c](https://github.com/webern-unibas-ch/awg-app/commit/e634f0c8b7bfa5a98483152353573a7d77d22874)) +- **assets:** update files for m22 ([33b6944](https://github.com/webern-unibas-ch/awg-app/commit/33b6944deacbe8482f8bdfd16ffb23b8d3e3c738); thanks to [@chael-mi](https://github.com/chael-mi)) +- **core:** add dev preview to footer ([4f24429](https://github.com/webern-unibas-ch/awg-app/commit/4f24429650532a5d5eea4f4994d59b58e1cfed59)) + +### Bug Fixes + +- **app:** use bracket notation to access logos ([f9139e4](https://github.com/webern-unibas-ch/awg-app/commit/f9139e4dc519ecc6f38e3ae61dbf390313cf75ad)) +- **assets:** fix files for m22 ([2e0b73e](https://github.com/webern-unibas-ch/awg-app/commit/2e0b73ed62eb759cb17fd87909aa806876723f0d); thanks to [@chael-mi](https://github.com/chael-mi)) +- **assets:** fix link boxes and textcritics for m22 ([4b12f0e](https://github.com/webern-unibas-ch/awg-app/commit/4b12f0e93dc0dee4a4c0cd176594c39b8604067e)) +- **assets:** fix lost changes in files for m22 ([7bd616a](https://github.com/webern-unibas-ch/awg-app/commit/7bd616a31c528e96794b8f5f968dab8ba46b629c); thanks to [@chael-mi](https://github.com/chael-mi)) +- **assets:** fix source descriptions op12 & 23 ([d559ff0](https://github.com/webern-unibas-ch/awg-app/commit/d559ff0d4b6f1189bb32ac9453e71cfd134676ec)) +- **assets:** fix source evaluation for TF1 of m22 ([#1478](https://github.com/webern-unibas-ch/awg-app/issues/1478)) ([440e4bd](https://github.com/webern-unibas-ch/awg-app/commit/440e4bdbf63429654c00fb63e5c031d704ac09cd); thanks to [@chael-mi](https://github.com/chael-mi)) +- **assets:** pathify svg files for m22 ([#1480](https://github.com/webern-unibas-ch/awg-app/issues/1480)) ([92230c0](https://github.com/webern-unibas-ch/awg-app/commit/92230c05e3ed576b4a2a0db4ca8a01bd47c54763); thanks to [@chael-mi](https://github.com/chael-mi)) +- **assets:** rename and format files for m22 ([fcd18a2](https://github.com/webern-unibas-ch/awg-app/commit/fcd18a25b199a0a98dae65d9ec9554b021c69ab3)) +- **assets:** update files for m22 ([#1491](https://github.com/webern-unibas-ch/awg-app/issues/1491)) ([8caf1df](https://github.com/webern-unibas-ch/awg-app/commit/8caf1dfd3141733d32b8e6e52205cb74ac56bd1b); thanks to [@chael-mi](https://github.com/chael-mi)) +- **assets:** update source description for op12 ([b90d23d](https://github.com/webern-unibas-ch/awg-app/commit/b90d23d37e2c5499ba7e9c6438f9155224dba104); thanks to [@masthom](https://github.com/masthom)) +- **assets:** update source descriptions for AWG I/5 ([d53de7b](https://github.com/webern-unibas-ch/awg-app/commit/d53de7bb7b2b442b56edb401220fcb50da46db5f); thanks to [@masthom](https://github.com/masthom)) +- **core:** remove obsolete app dev url ([9a5e0ca](https://github.com/webern-unibas-ch/awg-app/commit/9a5e0ca72f96348969d53631e0174ed05c35bafb)) +- **home:** fix link to DaSCH mission statement ([c2fce5b](https://github.com/webern-unibas-ch/awg-app/commit/c2fce5b67c55fcae43636d772ecce42908560cc4)) +- **search:** fix html hierarchy in ResourceDetailHeader ([5f0688b](https://github.com/webern-unibas-ch/awg-app/commit/5f0688bb03a9361bb81d6e8654d6cb1ca1211864)) +- **search:** fix tracking id for searchResponse subjects ([643d06a](https://github.com/webern-unibas-ch/awg-app/commit/643d06aca043a0a93228998d14eeaef5d9d7b1e9)) +- **search:** remove additional quotation marks in search results ([5559278](https://github.com/webern-unibas-ch/awg-app/commit/5559278eb3aa8c196423002848bc4036fad81dd2)) + +### Code Refactoring + +- **core:** improve preparation of fulltext search results ([#1495](https://github.com/webern-unibas-ch/awg-app/issues/1495)) ([6ee433d](https://github.com/webern-unibas-ch/awg-app/commit/6ee433d94e5df99afac1ec9d1c99c68f24fd58a1)) +- **core:** improve preparation of fulltext search result text ([936d6f7](https://github.com/webern-unibas-ch/awg-app/commit/936d6f7ba38d7a2961c0642560f5230a83ac553e)) +- **core:** make replaceParagraphTags static method ([a3bf9e9](https://github.com/webern-unibas-ch/awg-app/commit/a3bf9e919d326a083888d05515e716d56a621980)) +- **core:** simplify conversion of richtext values ([fc9ffba](https://github.com/webern-unibas-ch/awg-app/commit/fc9ffba363a430c813581f74b074f8614ebf749f)) + +### Tests + +- **core:** add tests for dev preview ([7685d49](https://github.com/webern-unibas-ch/awg-app/commit/7685d49fa910d3b680e4ce5d074e58ef63f8a27b)) + +### Continuous Integration + +- **gh-actions:** add permissions ([42b9017](https://github.com/webern-unibas-ch/awg-app/commit/42b9017843f6c605508e80f7969a736728cd4217)) +- **gh-actions:** adjust baseHref ([12a1b43](https://github.com/webern-unibas-ch/awg-app/commit/12a1b434cdb5499a208aae1615a0c96f5c11f66a)) +- **gh-actions:** adjust build workflow for dev ([47aa7e4](https://github.com/webern-unibas-ch/awg-app/commit/47aa7e426980c3fcdb1831431bafedfc2e26d5c6)) +- **gh-actions:** adjust gitHead variable ([baf3558](https://github.com/webern-unibas-ch/awg-app/commit/baf35580fe34a1eaee7fa4db5b9f28ee0682c9c3)) +- **gh-actions:** Apply security best practices [StepSecurity] ([a3e6ca0](https://github.com/webern-unibas-ch/awg-app/commit/a3e6ca005d8780ab191e93fffff99e644ba9c24e)) +- **gh-actions:** bring back node setup ([3722f6a](https://github.com/webern-unibas-ch/awg-app/commit/3722f6a157bc4667fdadee209f6218682dcac1b9)) +- **gh-actions:** build dev build once more ([e0741f4](https://github.com/webern-unibas-ch/awg-app/commit/e0741f461729608ffa0e16487e5b1b633a089d51)) +- **gh-actions:** change baseHref for dev build ([e7d8756](https://github.com/webern-unibas-ch/awg-app/commit/e7d87565c7b62494190c9638ecda0c1844b2fa7c)) +- **gh-actions:** copy only content of dist folder ([5454fc5](https://github.com/webern-unibas-ch/awg-app/commit/5454fc5fcea87979bea688cdaa3464dc887261cb)) +- **gh-actions:** create visualize-repo.yml ([3f4dab2](https://github.com/webern-unibas-ch/awg-app/commit/3f4dab20cf9f402013e1e67f85d6ce67195ed89f)) +- **gh-actions:** deploy to website dev ([6699039](https://github.com/webern-unibas-ch/awg-app/commit/669903953ea63002d9d12df45672265a23bbd2f2)) +- **gh-actions:** deploy to website dev (dry-run) ([886a072](https://github.com/webern-unibas-ch/awg-app/commit/886a072638ceb9401f70c76e3e9d53013c1690cf)) +- **gh-actions:** dry run deployment from develop ([2e8b295](https://github.com/webern-unibas-ch/awg-app/commit/2e8b295cb84583d868adca1480f34b3221564eb1)) +- **gh-actions:** fix env variable ([f487bf0](https://github.com/webern-unibas-ch/awg-app/commit/f487bf0a67a2fc399d54cad7ba14c1d61149d7ca)) +- **gh-actions:** fix file syntax ([2ef9f0b](https://github.com/webern-unibas-ch/awg-app/commit/2ef9f0ba97a158ff6d30c2c947b80e1149ba7c2b)) +- **gh-actions:** fix typo ([f5753cc](https://github.com/webern-unibas-ch/awg-app/commit/f5753cc8ad8ca68c2b2ea81f13790d9d62e7f61b)) +- **gh-actions:** fix typoin branch name ([7a39969](https://github.com/webern-unibas-ch/awg-app/commit/7a3996955bdededc6df062afdb8599db49e938da)) +- **gh-actions:** get git sha and fix typo ([a9ab2e5](https://github.com/webern-unibas-ch/awg-app/commit/a9ab2e56f1d439338ec0f4364122ffebfcb75291)) +- **gh-actions:** get sha from describe ([af2f3ab](https://github.com/webern-unibas-ch/awg-app/commit/af2f3ab543aa39f0c431ca7bf8c852568478f1b9)) +- **gh-actions:** move script args to package.json ([2bcafd0](https://github.com/webern-unibas-ch/awg-app/commit/2bcafd00d036b67ad394706cca59488854dfcc22)) +- **gh-actions:** reduce retention period for artifacts ([c288149](https://github.com/webern-unibas-ch/awg-app/commit/c288149d7a0a32399b6778f18569718c7fef9196)) +- **gh-actions:** refactor deploy_dev job ([7f3ddfb](https://github.com/webern-unibas-ch/awg-app/commit/7f3ddfbb4dc54b2870ec5f10266025729e0da95f)) +- **gh-actions:** remove deprecated svgo action ([4bf7a70](https://github.com/webern-unibas-ch/awg-app/commit/4bf7a70c70b49029293e150cf45e8d684caff190)) +- **gh-actions:** remove dry-run ([e622158](https://github.com/webern-unibas-ch/awg-app/commit/e622158cb4db867164f69a56c982a041c7c63244)) +- **gh-actions:** remove dry-run flag ([ef98c75](https://github.com/webern-unibas-ch/awg-app/commit/ef98c757d54ddaa58ae859f8030d4d5e295c4478)) +- **gh-actions:** remove obsolete node setup ([6129f6e](https://github.com/webern-unibas-ch/awg-app/commit/6129f6e4f8c36e637ca2dcdba95d330d2004a52a)) +- **gh-actions:** switch to deploy key ([b6e4957](https://github.com/webern-unibas-ch/awg-app/commit/b6e4957c7b052628e4182cab187b1716e5455777)) +- **gh-actions:** update ci_workflow.yml ([1706573](https://github.com/webern-unibas-ch/awg-app/commit/1706573158538ff5606c3361fccd79085c6094b0)) +- **gh-actions:** update codeql.yml ([8219c4e](https://github.com/webern-unibas-ch/awg-app/commit/8219c4e4b1e8e60b24acef88f7de1bf265b7696f)) +- **gh-actions:** update dev version on build ([ab43248](https://github.com/webern-unibas-ch/awg-app/commit/ab43248e77e78540ad3c0571a09cffb70b975787)) +- **gh-actions:** update scorecards.yml ([5204d88](https://github.com/webern-unibas-ch/awg-app/commit/5204d886db51a1492912f827d2ddca80f39c22cf)) +- **gh-actions:** update visualize-repo.yml ([ecccb8e](https://github.com/webern-unibas-ch/awg-app/commit/ecccb8ee43b90c0b8eb73994242a5b332689a26d)) +- **gh-actions:** upload build artifact only for latest node version ([a1726cf](https://github.com/webern-unibas-ch/awg-app/commit/a1726cfbb8f6840726603eec7774a5d555896a52)) +- **gh-actions:** upload build artifacts ([dbbaa58](https://github.com/webern-unibas-ch/awg-app/commit/dbbaa58dc5214428d1cfa10bfbb50dbc82c4f4b2)) +- **gh-actions:** use correct directory ([f1db97e](https://github.com/webern-unibas-ch/awg-app/commit/f1db97ed409000a6736bf8ac51863464384dd508)) +- **gh-actions:** use dev repo for develop build ([ca5a601](https://github.com/webern-unibas-ch/awg-app/commit/ca5a60172b2b433f9223354c59bdf9dc74201489)) +- **gh-actions:** use different build scripts for develop/main ([49f8b34](https://github.com/webern-unibas-ch/awg-app/commit/49f8b34ecfb59ca8e0da6655662f2af039722485)) +- **gh-actions:** use existing deploy command ([2ff00e9](https://github.com/webern-unibas-ch/awg-app/commit/2ff00e98b21fe9d4cad22e827173923f6e7a0bc6)) +- **gh-actions:** use prebuilt artifacts ([19f0a24](https://github.com/webern-unibas-ch/awg-app/commit/19f0a245f2f5cb91bd20d9dc8c3093aa3a14fe02)) +- **gh-actions:** use script instead of direct command ([acb2a52](https://github.com/webern-unibas-ch/awg-app/commit/acb2a5277964713fe8a3f1e7f989752e570e7522)) + +### Documentation + +- **app:** create SECURITY.md ([ee97005](https://github.com/webern-unibas-ch/awg-app/commit/ee9700566cd919a008a08c5b8f70b972a6bbc29a)) +- **CONTRIBUTING:** update contribution guidelines ([3ec4889](https://github.com/webern-unibas-ch/awg-app/commit/3ec48891f5bbbca61caefa79fa0d3dce483855e7)) +- **README:** add OSSF best pratices badge ([80ee019](https://github.com/webern-unibas-ch/awg-app/commit/80ee019bc1e01f7a52e81adc6f22129d5642ded2)) +- **README:** add sample image of the app ([ff3b009](https://github.com/webern-unibas-ch/awg-app/commit/ff3b0090abd7d534db17f758686e4b0d6ac35f9c)) +- **README:** fix typo in TOC ([39a2a44](https://github.com/webern-unibas-ch/awg-app/commit/39a2a444cd75b2ea361ec7baa3fd7c206bf4a5c6)) +- **README:** update codecov badge ([c652dcd](https://github.com/webern-unibas-ch/awg-app/commit/c652dcd82d3cd5e0957d9b2a142a20cb097bd399)) +- **README:** update README.md ([6f8c4db](https://github.com/webern-unibas-ch/awg-app/commit/6f8c4dbff580c1e4ffe192f7aada5dbd7b269e31)) +- **README:** update README.md ([496b8d4](https://github.com/webern-unibas-ch/awg-app/commit/496b8d4e7c19d9e9ac15e37c5a5cb083cb893825)) +- **README:** update README.md ([44a00af](https://github.com/webern-unibas-ch/awg-app/commit/44a00af977219949da1d4f25a92d31ff9573224b)) + +### Styles + +- **app:** rename class caps -> smallcaps ([#1485](https://github.com/webern-unibas-ch/awg-app/issues/1485)) ([c5a7427](https://github.com/webern-unibas-ch/awg-app/commit/c5a7427f93c55b6d61dea0775f4816f044462ddb)) +- **search:** fix grid and badge classes in search result list ([cf1c29c](https://github.com/webern-unibas-ch/awg-app/commit/cf1c29c98b5cabeaa17a5b337b16e35f158af6fa)) + +### Build System + +- **app:** move build attachments into separate script ([46a0f0b](https://github.com/webern-unibas-ch/awg-app/commit/46a0f0be6c2efb358a5589d31796cc891163b4ef)) +- **app:** update zenodo config file ([8b33b13](https://github.com/webern-unibas-ch/awg-app/commit/8b33b13cc72f66615da26342c3985d88070bb1ff)) +- **deps-dev:** bump @compodoc/compodoc from 1.1.23 to 1.1.24 ([18b5741](https://github.com/webern-unibas-ch/awg-app/commit/18b57411e4dba4ee6d18ca8ab4695ed6e05af084)) +- **deps-dev:** bump @types/node from 18.19.31 to 18.19.32 ([827fb66](https://github.com/webern-unibas-ch/awg-app/commit/827fb66b7dc6101b704d6507f82d90d44bb9dcac)) +- **deps-dev:** bump @types/node from 18.19.32 to 18.19.33 ([8f7bd32](https://github.com/webern-unibas-ch/awg-app/commit/8f7bd3230f95e72e88717c9b5aa3c33d8b1a26c8)) +- **deps-dev:** bump commit-and-tag-version from 12.4.0 to 12.4.1 ([2a75acf](https://github.com/webern-unibas-ch/awg-app/commit/2a75acf30ff113f8abd979f9f65a4a26283cf170)) +- **deps-dev:** bump conventional-recommended-bump from 9.0.0 to 10.0.0 ([f3951cc](https://github.com/webern-unibas-ch/awg-app/commit/f3951cc6366cf07bcf427fc9fe651f15f5865eaa)) +- **deps-dev:** bump eslint-plugin-jsdoc from 48.2.3 to 48.2.4 ([d269688](https://github.com/webern-unibas-ch/awg-app/commit/d269688af5be5d38347a59d8cb86795d2ed22045)) +- **deps-dev:** bump the angular-cli-devkit group with 2 updates ([156cdde](https://github.com/webern-unibas-ch/awg-app/commit/156cdde2e9fe5d710ccc7277cd5e8e5df1d40bb7)) +- **deps-dev:** bump the angular-cli-devkit group with 2 updates ([5f720d0](https://github.com/webern-unibas-ch/awg-app/commit/5f720d02e3ccbd9abecdc07dd02f3b6f635b989f)) +- **deps-dev:** bump the angular-eslint group with 5 updates ([30416a5](https://github.com/webern-unibas-ch/awg-app/commit/30416a5b52fbec24fccd37f6dcf200e8decc2fc4)) +- **deps-dev:** bump the angular-eslint group with 5 updates ([27dff2c](https://github.com/webern-unibas-ch/awg-app/commit/27dff2cd40dd93b3a95b181feab65598dfaebe97)) +- **deps-dev:** bump the typescript-eslint group with 2 updates ([f3eadf1](https://github.com/webern-unibas-ch/awg-app/commit/f3eadf1b79b43a1c6c2c1427551da7f4178d6de2)) +- **deps-dev:** bump the typescript-eslint group with 2 updates ([00bd33b](https://github.com/webern-unibas-ch/awg-app/commit/00bd33b5dde00ece41468b6f3f668bfc397970e8)) +- **deps-dev:** replace deprecated standard-version lib ([08276a5](https://github.com/webern-unibas-ch/awg-app/commit/08276a530f820fc8af0f7b978a1348462cd97011)) +- **deps:** bump actions/checkout from 3.6.0 to 4.1.4 ([ef2ed29](https://github.com/webern-unibas-ch/awg-app/commit/ef2ed29e87490cf67b6cc47ba92162cd0c852145)) +- **deps:** bump actions/checkout from 4.1.3 to 4.1.4 ([e77f141](https://github.com/webern-unibas-ch/awg-app/commit/e77f141e37dc0629fb75ead9122dd7de8fab747c)) +- **deps:** bump actions/checkout from 4.1.4 to 4.1.5 ([c07d798](https://github.com/webern-unibas-ch/awg-app/commit/c07d798ea6df79fd54e92f9d85ba46c87f77824c)) +- **deps:** bump actions/dependency-review-action from 2.5.1 to 4.3.1 ([e177336](https://github.com/webern-unibas-ch/awg-app/commit/e17733654d1ad113648f37cdd4504bb52c58ddbf)) +- **deps:** bump actions/dependency-review-action from 4.3.1 to 4.3.2 ([69f33b6](https://github.com/webern-unibas-ch/awg-app/commit/69f33b6fe33736191adbeef3c636666753b3955d)) +- **deps:** bump actions/upload-artifact from 3.1.3 to 4.3.3 ([9c3ddad](https://github.com/webern-unibas-ch/awg-app/commit/9c3ddadb365737c8f164a250e865bc54930d1d58)) +- **deps:** bump codecov/codecov-action from 4.3.0 to 4.3.1 ([6fb176a](https://github.com/webern-unibas-ch/awg-app/commit/6fb176a8c7a3e889b026e2a2144bd722abb46519)) +- **deps:** bump codecov/codecov-action from 4.3.1 to 4.4.0 ([98642d2](https://github.com/webern-unibas-ch/awg-app/commit/98642d2b36921d84b0b66aceff80005e62b40d79)) +- **deps:** bump ejs from 3.1.9 to 3.1.10 ([36c3416](https://github.com/webern-unibas-ch/awg-app/commit/36c34163d18854800dd7ee566406003bcb875e2a)) +- **deps:** bump github/codeql-action from 3.25.3 to 3.25.4 ([c369f84](https://github.com/webern-unibas-ch/awg-app/commit/c369f848aa4e4d0188546860f21f2466a903531f)) +- **deps:** bump github/codeql-action from 3.25.4 to 3.25.5 ([2c810d2](https://github.com/webern-unibas-ch/awg-app/commit/2c810d2f144455a2daf5d3613d345848b574ce03)) +- **deps:** bump ossf/scorecard-action from 2.0.6 to 2.3.1 ([9be10cf](https://github.com/webern-unibas-ch/awg-app/commit/9be10cfe31b0ac9a4422562c46aff726f170fad3)) +- **deps:** bump ossf/scorecard-action from 2.3.1 to 2.3.3 ([ea380f6](https://github.com/webern-unibas-ch/awg-app/commit/ea380f6e4407a0b7d703f7c8570547ce45e17b56)) +- **deps:** bump softprops/action-gh-release from 2.0.4 to 2.0.5 ([164013a](https://github.com/webern-unibas-ch/awg-app/commit/164013a87cabe4d537f232ae94cd0128a50b1f19)) +- **deps:** bump step-security/harden-runner from 2.7.0 to 2.7.1 ([7bbbc48](https://github.com/webern-unibas-ch/awg-app/commit/7bbbc4891d4d904ecdae193c3dff573c7cfe3da7)) +- **deps:** bump the angular group with 11 updates ([31dcc27](https://github.com/webern-unibas-ch/awg-app/commit/31dcc27e8da6cdc6fe2e941649b0c19032723b93)) +- **deps:** bump the angular group with 11 updates ([4436d0d](https://github.com/webern-unibas-ch/awg-app/commit/4436d0df081e92d87e1bd0a034980eacd816d517)) +- **deps:** bump the angular group with 11 updates ([5bcee59](https://github.com/webern-unibas-ch/awg-app/commit/5bcee59c0d545816fd01c085c33900648222f123)) ## [0.12.0](https://github.com/webern-unibas-ch/awg-app/compare/v0.11.7...v0.12.0) (2024-04-23) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e9ef2cfa5..650f48359a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,9 +8,13 @@ Please note that this project is released with a [Code of Conduct](CODE_OF_CONDU ## Table of Contents - [Contribution process](#contribution-process) + - [Code Formatting](#code-formatting) + - [Testing](#testing) - [Branching / Git flow](#branching--git-flow) - [Commit Message Schema](#commit-message-schema) + - [Pull Requests](#pull-requests) - [Release Versioning Convention](#release-versioning-convention) + - [Issue Reporting](#issue-reporting) - [Angular quick start guide](#quick-start-guide) - [Prerequisites](#prerequisites) - [Development server](#development-server) @@ -23,6 +27,32 @@ Please note that this project is released with a [Code of Conduct](CODE_OF_CONDU ## Contribution process +### Code Formatting + +This project is set up to use Prettier for code formatting. This should be automatically enabled upon installation. + +Prettier helps enforce a consistent style by parsing your code and re-printing it with its own rules that take the maximum line length into account, wrapping code when necessary. + +In addition to the automatic formatting, you can manually check and fix formatting issues using the following commands: + +- `yarn format-files:check`: This command checks the code for formatting issues. + +- `yarn format-files:fix`: This command fixes any formatting issues that it can. + +### Testing + +This project uses a dynamic testing approach with Jasmine and Karma for unit tests in Angular. Code coverage is measured with CodeCov. + +We encourage contributors to uphold these standards. As such, new contributions are expected to include tests whenever applicable. + +To assist with this, the following commands are provided: + +- `yarn test`: Launches the test runner. + +- `yarn test:cov`: Runs the tests and generates a coverage report. + +- `yarn test:cov:serve`: Runs the tests, generates a coverage report, and serves the coverage report at [http://localhost:9875](http://localhost:9875). + ### Branching / Git flow This project uses the [Gitflow Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) which defines a strict branching model designed around the project releases. Therefore the following branch structure is used @@ -35,15 +65,11 @@ This project uses the [Gitflow Workflow](https://www.atlassian.com/git/tutorials To initialize the GitFlow workflow execute `git flow init` inside your local copy of the repository. -To provide a new feature or changes to the code, create a new feature branch from develop and make a pull request when ready. Keep care of the [Commit Message Schema](#commit-message-schema) described below. - -For more information about pull requests go check out the GitHub Help [About pull requests](https://help.github.com/en/articles/about-pull-requests). - ### Commit Message Schema This project follows the [Conventional Commits Specification](https://conventionalcommits.org) using [commitlint](https://conventional-changelog.github.io/commitlint/#/) based on the [Angular configuration](https://github.com/conventional-changelog/commitlint/tree/master/@commitlint/config-angular) (further explanation can be found in the [Angular commit-message-guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines)). -Using these conventions leads to more readable messages that are easy to follow when looking through the project history. But also, we use the git commit messages to autogenerate the [CHANGELOG](https://github.com/webern-unibas-ch/awg-app/blob/main/LICENSE.md) and automate versions by means of [standard-version](https://github.com/conventional-changelog/standard-version) (see "Release Versioning Convention" section below). +Using these conventions leads to more readable messages that are easy to follow when looking through the project history. But also, we use the git commit messages to autogenerate the [CHANGELOG](https://github.com/webern-unibas-ch/awg-app/blob/main/LICENSE.md) and automate versions by means of [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) (see "Release Versioning Convention" section below). When writing commit messages, we stick to this schema: @@ -72,33 +98,33 @@ Types: Scopes (specific to this project, not part of the Angular convention): -- related to app structure - - `app` - - `assets` - - `contact` - - `core` - - `edition` - - `home` - - `page-not-found` - - `search` - - `shared` - - `side-info` - - `structure` - - `views` - - -- related to build process and tests - - `deps` - - `deps-dev` - - `gh-actions` - - `testing` - - -- related to documentation - - `CHANGELOG` - - `CONTRIBUTING` - - `LICENSE` - - `README` +- related to app structure + + - `app` + - `assets` + - `contact` + - `core` + - `edition` + - `home` + - `page-not-found` + - `search` + - `shared` + - `side-info` + - `structure` + - `views` + +- related to build process and tests + + - `deps` + - `deps-dev` + - `gh-actions` + - `testing` + +- related to documentation + - `CHANGELOG` + - `CONTRIBUTING` + - `LICENSE` + - `README` #### Examples: @@ -113,9 +139,21 @@ feat(edition): add route for resource creation docs(README): add new contributors for data ``` +### Pull Requests + +To provide a new feature or changes to the code, create a new feature branch from develop and make a pull request when ready. Keep care of the [Commit Message Schema](#commit-message-schema) described below. + +For more information about pull requests go check out the GitHub Help [About pull requests](https://help.github.com/en/articles/about-pull-requests). + ### Release Versioning Convention -We use the git commit messages to autogenerate the [CHANGELOG](https://github.com/webern-unibas-ch/awg-app/blob/main/CHANGELOG.md) and automate versions by means of [standard-version](https://github.com/conventional-changelog/standard-version). +We use the git commit messages to autogenerate the [CHANGELOG](https://github.com/webern-unibas-ch/awg-app/blob/main/CHANGELOG.md) and automate versions by means of [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version). See also [README#releases](README.md#releases). + +### Issue Reporting + +If you encounter a bug or any issue with the application, please report it by creating a new issue in the GitHub repository. When creating an issue, try to provide as much information as possible to help us understand and reproduce the problem. + +For security concerns, please do not create a public issue. Instead, send an email directly to , following our [Security Policy](SECURITY.md). We take all security issues seriously and will respond as quickly as possible to resolve the matter. ## Angular quick start guide diff --git a/README.md b/README.md index 8cf9e4705a..99909b6b3e 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,102 @@ ![Node.js version](https://img.shields.io/badge/node.js-%3E=v18.19.0-blue) ![GitHub commit activity](https://img.shields.io/github/commit-activity/m/webern-unibas-ch/awg-app) ![CI Workflow](https://github.com/webern-unibas-ch/awg-app/actions/workflows/ci_workflow.yml/badge.svg) -[![codecov](https://codecov.io/gh/webern-unibas-ch/awg-app/branch/main/graph/badge.svg)](https://codecov.io/gh/webern-unibas-ch/awg-app) +[![codecov](https://codecov.io/gh/webern-unibas-ch/awg-app/graph/badge.svg?token=IO5EgI81R6)](https://codecov.io/gh/webern-unibas-ch/awg-app) [![compodoc](https://edition.anton-webern.ch/compodoc/images/coverage-badge-documentation.svg)](https://edition.anton-webern.ch/compodoc/index.html) +[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/webern-unibas-ch/awg-app/badge)](https://scorecard.dev/viewer/?uri=github.com/webern-unibas-ch/awg-app) +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8862/badge)](https://www.bestpractices.dev/projects/8862) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.1%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md) [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.4717678.svg)](https://doi.org/10.5281/zenodo.4717678) -A prototype web application for the online edition of the [Anton Webern Gesamtausgabe](https://www.anton-webern.ch), located at the Department of Musicology of the University of Basel. It is written in [Angular](https://angular.io/) and runs on [edition.anton-webern.ch](https://edition.anton-webern.ch). +A prototype web application for the online edition of the [Anton Webern Gesamtausgabe](https://www.anton-webern.ch) (AWG), located at the Department of Musicology of the University of Basel. It is written in [Angular](https://angular.io/) and runs on [edition.anton-webern.ch](https://edition.anton-webern.ch). + +**Project Status**: This project is actively maintained. + +app + + +## Table of Contents + +- [Description](#description) +- [Prerequisites](#prerequisites) +- [Getting Started](#getting-started) +- [Usage](#usage) +- [Building](#building) +- [Releases](#releases) +- [Testing](#testing) +- [Contributing](#contributing) +- [Contributors ✨](#contributors-✨) +- [License](#license) +- [Contact and Issue Reporting](#contact-and-issue-reporting) + +## Description + +This repository houses the source code for the web application that powers the online edition of the Anton Webern Gesamtausgabe (AWG). Our goal is to provide a comprehensive, accessible, and interactive platform for exploring the works of Anton Webern. + +## Prerequisites + +To run the code base yourself, there are only a few prerequisites to take care of. We use [Yarn](https://classic.yarnpkg.com/) for dependency managing, so, before you begin, ensure you have met the following requirements: + +- You have installed the latest version of Node.js. You can check this by running `node -v` in your terminal. If Node.js is not installed, you can download it from [here](https://nodejs.org/). +- You have installed Yarn. You can check this by running `yarn -v` in your terminal. If Yarn is not installed, you can download it from [here](https://classic.yarnpkg.com/). + +## Getting Started + +To get started with this project, follow these steps: + +1. Make sure you meet the prerequisites. +2. Clone the repository: `git clone [repository_url]` +3. Navigate into the project directory: `cd [project_directory]` +4. Install the dependencies: `yarn install` + +## Usage + +In the project directory, you can run the following command to serve the app in development mode: + +- `yarn start`: Serves the app in the development mode. Open [http://localhost:4200](http://localhost:4200) to view it in the browser. + +During the development process, you'll also find the following commands useful for maintaining code quality and understanding the codebase: + +- `yarn lint`: Scans for linting errors using ESLint. + +- `yarn lint:fix`: Lints the project and automatically fixes any fixable issues. + +- `yarn doc:serve`: Generates documentation for the project using Compodoc and serves it at a local server. Open the URL provided in the terminal to view it in your web browser. + +## Building + +To build the app, use the following commands: + +- `yarn build:prod`: Builds the app for production to the `dist` folder. + +- `yarn build:gh`: Same as `yarn build:prod`, but additionally prepares the build for deployment on GitHub Pages (includes base-href setting). + +## Releases + +Releases for this project are automatically managed via Continuous Integration (CI). The following commands are involved in the release process: + +- `yarn pre-release`: Updates the app version and creates a changelog from the commit history. + +- `yarn deploy:ci`: Runs `angular-cli-ghpages` to deploy the app on GitHub Pages. To be used only from CI. + +## Testing + +This project uses a dynamic testing approach with Jasmine and Karma for unit tests in Angular. Code coverage is measured with CodeCov. + +We encourage contributors to uphold these standards. As such, new contributions are expected to include tests whenever applicable. + +To assist with this, the following commands are provided: + +- `yarn test`: Launches the test runner. + +- `yarn test:cov`: Runs the tests and generates a coverage report. + +- `yarn test:cov:serve`: Runs the tests, generates a coverage report, and serves the coverage report at [http://localhost:9875](http://localhost:9875). + +## Contributing + +We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md) for more details how you may contribute to this project. ## Contributors ✨ @@ -39,12 +128,16 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! -## Contributing - -Please read our [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) and [CONTRIBUTING.md](CONTRIBUTING.md) to see how you may contribute to this project. - ## License The software code of this project is released under [MIT](https://opensource.org/licenses/MIT) license, see [LICENSE.md](https://github.com/webern-unibas-ch/awg-app/blob/main/LICENSE.md). The contents of the webpage are released under [Creative Commons Attribution-ShareAlike 4.0 International License (CC BY-SA 4.0)](https://creativecommons.org/licenses/by-sa/4.0/), see [Disclaimer](http://edition.anton-webern.ch/contact#awg-disclaimer). + +## Contact and Issue Reporting + +If you encounter a bug or any issue with the application, please report it by creating a new issue in the GitHub repository. When creating an issue, try to provide as much information as possible to help us understand and reproduce the problem. + +For security concerns, please do not create a public issue. Instead, send an email directly to , following our [Security Policy](SECURITY.md). We take all security issues seriously and will respond as quickly as possible to resolve the matter. + +For any other queries or if you wish to reach out directly, please contact us at . diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..c2e4e4f6a2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,39 @@ +# Security Policy + +## Supported Versions + +We are committed to providing security updates for the latest version of our project. Please ensure you are using the most recent release to receive these updates. + +## Reporting a Vulnerability + +We take security issues very seriously. If you discover a security vulnerability in any webern-unibas-ch-owned repository, please report it to us through coordinated disclosure. + +**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** + +Instead, please send an email to info-awg[@]unibas.ch. + +Please include as much of the information listed below as you can to help us better understand and resolve the issue: + +* The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting) +* Full paths of source file(s) related to the manifestation of the issue +* The location of the affected source code (tag/branch/commit or direct URL) +* Any special configuration required to reproduce the issue +* Step-by-step instructions to reproduce the issue +* Proof-of-concept or exploit code (if possible) +* Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +## Acknowledgment + +We acknowledge every security vulnerability reported to us and strive to address them in a timely manner. We appreciate your patience as we work to resolve the issue. + +Contributors who report vulnerabilities will be acknowledged in the project's documentation. However, please note that we do not have a bounty program and cannot provide monetary rewards for vulnerability reports. + +Thank you for helping to keep our project secure. + +--- + +This policy is adapted from the policies of: + +* https://github.com/github/template/security/advisories diff --git a/diagram.svg b/diagram.svg new file mode 100644 index 0000000000..03e9cfab58 --- /dev/null +++ b/diagram.svg @@ -0,0 +1 @@ +srcsrctestingtestingassetsassetsappappimgimgdata/editiondata/editionviewsviewsside-infoside-infosharedsharedcorecoreeditioneditionseriesseriesedition-viewedition-viewdata-viewdata-viewapi-objectsapi-objectsservicesservicesseriesseries2/section/2a2/section/2a1/section1/sectionedition-outletsedition-outletsdata-outletsdata-outlets2/section/2a2/section/2a1/section1/section5522edition-complexedition-complex55edition-detailedition-detailedition-sheetsedition-sheetsedition-graphedition-graphedition-a...edition-a...edition-a...graph-visua...graph-visua...graph-visua....2.5.css.gitignore.html.js.json.md.properties.props.scss.sh.svg.ts.ymleach dot sized by file size \ No newline at end of file diff --git a/package.json b/package.json index ce9dd4d598..3ac93d4a12 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "awg-app", - "version": "0.12.0", + "version": "0.12.1", "license": "MIT", "author": { "name": "Stefan Münnich", @@ -69,7 +69,9 @@ "-- BUILD --": "", "build:prod": "ng build", "build:watch": "ng build --watch --configuration development", - "build:gh": "yarn build:prod --base-href $npm_package_homepage && yarn compress:dist && yarn doc:build", + "build:attach": "yarn compress:dist && yarn doc:build", + "build:gh": "yarn build:prod --base-href $npm_package_homepage && yarn build:attach", + "build:dev": "yarn build:prod --base-href ${npm_package_homepage}dev/ && yarn build:attach", "compress:dist": "gzipper compress ./dist/awg-app/ --gzip --brotli", "-- ANALYZE --": "", "pre_wpanalyzer": "yarn build:prod --stats-json", @@ -77,21 +79,21 @@ "pre_sourcemap": "yarn build:prod --source-map", "sourcemap": "yarn pre_sourcemap && source-map-explorer dist/awg-app/*.js", "-- DEPLOY --": "", - "pre-release": "standard-version -a", + "pre-release": "commit-and-tag-version -a", "update-appversion": "sh ./version.sh", "deploy:ci": "ng deploy --no-build --message=\"Release $npm_package_name (v$npm_package_version) on gh-pages\"" }, "dependencies": { - "@angular/animations": "^17.3.5", - "@angular/common": "^17.3.5", - "@angular/compiler": "^17.3.5", - "@angular/core": "^17.3.5", - "@angular/forms": "^17.3.5", - "@angular/localize": "^17.3.5", - "@angular/platform-browser": "^17.3.5", - "@angular/platform-browser-dynamic": "^17.3.5", - "@angular/platform-server": "^17.3.5", - "@angular/router": "^17.3.5", + "@angular/animations": "^17.3.8", + "@angular/common": "^17.3.8", + "@angular/compiler": "^17.3.8", + "@angular/core": "^17.3.8", + "@angular/forms": "^17.3.8", + "@angular/localize": "^17.3.8", + "@angular/platform-browser": "^17.3.8", + "@angular/platform-browser-dynamic": "^17.3.8", + "@angular/platform-server": "^17.3.8", + "@angular/router": "^17.3.8", "@codemirror/legacy-modes": "^6.4.0", "@fortawesome/angular-fontawesome": "^0.14.1", "@fortawesome/fontawesome-svg-core": "^6.5.2", @@ -117,30 +119,31 @@ "zone.js": "~0.14.2" }, "devDependencies": { - "@angular-devkit/build-angular": "^17.3.5", - "@angular-eslint/builder": "^17.3.0", - "@angular-eslint/eslint-plugin": "^17.3.0", - "@angular-eslint/eslint-plugin-template": "^17.3.0", - "@angular-eslint/schematics": "^17.3.0", - "@angular-eslint/template-parser": "^17.3.0", - "@angular/cli": "^17.3.5", - "@angular/compiler-cli": "^17.3.5", + "@angular-devkit/build-angular": "^17.3.7", + "@angular-eslint/builder": "^17.4.1", + "@angular-eslint/eslint-plugin": "^17.4.1", + "@angular-eslint/eslint-plugin-template": "^17.4.1", + "@angular-eslint/schematics": "^17.4.1", + "@angular-eslint/template-parser": "^17.4.1", + "@angular/cli": "^17.3.7", + "@angular/compiler-cli": "^17.3.8", "@commitlint/cli": "^19.3.0", "@commitlint/config-angular": "^19.3.0", - "@compodoc/compodoc": "^1.1.23", + "@compodoc/compodoc": "^1.1.24", "@types/d3": "^7.4.3", "@types/jasmine": "~5.1.4", - "@types/node": "^18.19.31", - "@typescript-eslint/eslint-plugin": "^7.7.1", - "@typescript-eslint/parser": "^7.7.1", + "@types/node": "^18.19.33", + "@typescript-eslint/eslint-plugin": "^7.9.0", + "@typescript-eslint/parser": "^7.9.0", "angular-cli-ghpages": "^1.0.7", - "conventional-recommended-bump": "^9.0.0", + "commit-and-tag-version": "^12.4.1", + "conventional-recommended-bump": "^10.0.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-angular": "^4.1.0", "eslint-plugin-deprecation": "^2.0.0", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jsdoc": "^48.2.3", + "eslint-plugin-jsdoc": "^48.2.4", "eslint-plugin-prettier": "^5.1.3", "gzipper": "^7.2.0", "husky": "^9.0.11", @@ -153,7 +156,6 @@ "lint-staged": "^15.2.2", "prettier": "^3.2.5", "source-map-explorer": "^2.5.3", - "standard-version": "^9.5.0", "typescript": "~5.2.2", "webpack-bundle-analyzer": "^4.10.2" }, diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 39616e55a0..df90d9fa49 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -124,6 +124,16 @@ export class AppConfig { return 'https://dhlab.philhist.unibas.ch/'; } + /** + * Getter for the URL of the DSP API endpoint + * ({@link http://api.dasch.swiss/v2/}). + * + * @returns {string} + */ + public static get DSP_API_URL(): string { + return 'https://api.dasch.swiss/v2/'; + } + /** * Getter for the URL of the INSERI Instance * ({@link https://apps.inseri.swiss}). @@ -149,7 +159,7 @@ export class AppConfig { * * @returns {string} */ - public static get OSM_LINK_URL() { + public static get OSM_LINK_URL(): string { const osmLinkRoot = 'https://www.openstreetmap.org/'; const osmLinkId = '?mlat=47.55897&mlon=7.58451#map=19/47.55897/7.58451'; return osmLinkRoot + osmLinkId; diff --git a/src/app/app.globals.ts b/src/app/app.globals.ts index 3316895c30..eb0b8a3e0c 100644 --- a/src/app/app.globals.ts +++ b/src/app/app.globals.ts @@ -1,15 +1,15 @@ // THIS IS AN AUTO-GENERATED FILE. DO NOT CHANGE IT MANUALLY! -// Generated last time on Tue, Apr 23, 2024 3:51:46 PM +// Generated last time on Wed, May 15, 2024 7:45:56 PM /** * The latest version of the AWG App */ -export const appVersion = '0.12.0'; +export const appVersion = '0.12.1'; /** * The release date of the latest version of the AWG App */ -export const appVersionReleaseDate = '23. April 2024'; +export const appVersionReleaseDate = '15. Mai 2024'; /** * The URL of the AWG App diff --git a/src/app/core/core-data/meta.data.ts b/src/app/core/core-data/meta.data.ts index 802a718167..06dac61509 100644 --- a/src/app/core/core-data/meta.data.ts +++ b/src/app/core/core-data/meta.data.ts @@ -10,6 +10,7 @@ const METAPAGE: MetaPage = { yearStart: 2015, yearCurrent: new Date().getFullYear(), awgAppUrl: AppConfig.AWG_APP_URL, + awgAppDevUrl: AppConfig.AWG_APP_URL + '/dev/', awgProjectUrl: AppConfig.AWG_PROJECT_URL, awgProjectName: AppConfig.AWG_PROJECT_NAME, compodocUrl: AppConfig.AWG_APP_COMPODOC_URL, diff --git a/src/app/core/core-models/meta.model.ts b/src/app/core/core-models/meta.model.ts index 16cdd07479..2cd4fc2a27 100644 --- a/src/app/core/core-models/meta.model.ts +++ b/src/app/core/core-models/meta.model.ts @@ -48,6 +48,11 @@ export class MetaPage { */ awgAppUrl: string; + /** + * The url to the dev version of the AWG edition homepage (awg-app-dev). + */ + awgAppDevUrl: string; + /** * The name of the AWG. */ diff --git a/src/app/core/footer/footer-copyright/footer-copyright.component.spec.ts b/src/app/core/footer/footer-copyright/footer-copyright.component.spec.ts index 774c9feb2e..03f419ea96 100644 --- a/src/app/core/footer/footer-copyright/footer-copyright.component.spec.ts +++ b/src/app/core/footer/footer-copyright/footer-copyright.component.spec.ts @@ -2,7 +2,7 @@ import { DebugElement } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { cleanStylesFromDOM } from '@testing/clean-up-helper'; -import { expectToBe, expectToContain, getAndExpectDebugElementByCss } from '@testing/expect-helper'; +import { expectToBe, expectToContain, expectToEqual, getAndExpectDebugElementByCss } from '@testing/expect-helper'; import { METADATA } from '@awg-core/core-data'; import { MetaPage, MetaSectionTypes } from '@awg-core/core-models'; @@ -74,6 +74,10 @@ describe('FooterCopyrightComponent (DONE)', () => { fixture.detectChanges(); }); + it('... should have pageMetaData', () => { + expectToEqual(component.pageMetaData, expectedPageMetaData); + }); + describe('VIEW', () => { it('... should render copyright period', () => { const expectedYearStart = expectedPageMetaData.yearStart; diff --git a/src/app/core/footer/footer-poweredby/footer-poweredby.component.html b/src/app/core/footer/footer-poweredby/footer-poweredby.component.html index 1167af038f..dd4a7f8608 100644 --- a/src/app/core/footer/footer-poweredby/footer-poweredby.component.html +++ b/src/app/core/footer/footer-poweredby/footer-poweredby.component.html @@ -1,6 +1,11 @@
- Built on + Built on
- with and - + with and + + + | +
diff --git a/src/app/core/footer/footer-poweredby/footer-poweredby.component.spec.ts b/src/app/core/footer/footer-poweredby/footer-poweredby.component.spec.ts index c1112e1f64..a1c9960ba4 100644 --- a/src/app/core/footer/footer-poweredby/footer-poweredby.component.spec.ts +++ b/src/app/core/footer/footer-poweredby/footer-poweredby.component.spec.ts @@ -1,6 +1,10 @@ import { Component, DebugElement, Input } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { FontAwesomeTestingModule } from '@fortawesome/angular-fontawesome/testing'; +import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; +import { faScrewdriverWrench } from '@fortawesome/free-solid-svg-icons'; + import { cleanStylesFromDOM } from '@testing/clean-up-helper'; import { expectToBe, @@ -9,8 +13,8 @@ import { getAndExpectDebugElementByDirective, } from '@testing/expect-helper'; -import { LOGOSDATA } from '@awg-core/core-data'; -import { Logo, Logos } from '@awg-core/core-models'; +import { LOGOSDATA, METADATA } from '@awg-core/core-data'; +import { Logo, Logos, MetaPage, MetaSectionTypes } from '@awg-core/core-models'; import { FooterPoweredbyComponent } from './footer-poweredby.component'; @@ -26,9 +30,12 @@ describe('FooterPoweredbyComponent (DONE)', () => { let compDe: DebugElement; let expectedLogos: Logos; + let expectedPageMetaData: MetaPage; + let expectedScrewdriverWrenchIcon: IconDefinition; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ + imports: [FontAwesomeTestingModule], declarations: [FooterPoweredbyComponent, FooterLogoStubComponent], }).compileComponents(); })); @@ -40,6 +47,8 @@ describe('FooterPoweredbyComponent (DONE)', () => { // Test data expectedLogos = LOGOSDATA; + expectedPageMetaData = METADATA[MetaSectionTypes.page]; + expectedScrewdriverWrenchIcon = faScrewdriverWrench; }); afterAll(() => { @@ -55,6 +64,14 @@ describe('FooterPoweredbyComponent (DONE)', () => { expect(component.logos).toBeUndefined(); }); + it('... should not have pageMetaData', () => { + expect(component.pageMetaData).toBeUndefined(); + }); + + it('... should have fontawesome icon', () => { + expectToEqual(component.faScrewdriverWrench, expectedScrewdriverWrenchIcon); + }); + describe('VIEW', () => { it('... should contain 1 div.awg-powered-by', () => { getAndExpectDebugElementByCss(compDe, 'div.awg-powered-by', 1, 1); @@ -63,6 +80,20 @@ describe('FooterPoweredbyComponent (DONE)', () => { it('... should contain 3 footer logo components (stubbed)', () => { getAndExpectDebugElementByDirective(compDe, FooterLogoStubComponent, 3, 3); }); + + it('... should contain 1 anchor #dev-preview-link with faIcon', () => { + getAndExpectDebugElementByCss(compDe, 'a#dev-preview-link', 1, 1); + + getAndExpectDebugElementByCss(compDe, 'a#dev-preview-link > fa-icon', 1, 1); + }); + + it('... should not render link to devPreview yet', () => { + const devDes = getAndExpectDebugElementByCss(compDe, 'a#dev-preview-link', 1, 1); + const devEl = devDes[0].nativeElement; + + expect(devEl).toBeDefined(); + expectToBe(devEl.href, ''); + }); }); }); @@ -70,6 +101,7 @@ describe('FooterPoweredbyComponent (DONE)', () => { beforeEach(() => { // Simulate the parent setting the input properties component.logos = expectedLogos; + component.pageMetaData = expectedPageMetaData; // Trigger initial data binding fixture.detectChanges(); @@ -79,6 +111,10 @@ describe('FooterPoweredbyComponent (DONE)', () => { expectToEqual(component.logos, expectedLogos); }); + it('... should have pageMetaData', () => { + expectToEqual(component.pageMetaData, expectedPageMetaData); + }); + describe('VIEW', () => { it('... should pass down logos to footer logo components', () => { const footerLogoDes = getAndExpectDebugElementByDirective(compDe, FooterLogoStubComponent, 3, 3); @@ -91,6 +127,21 @@ describe('FooterPoweredbyComponent (DONE)', () => { expectToEqual(footerLogoCmps[1].logo, expectedLogos['angular']); expectToEqual(footerLogoCmps[2].logo, expectedLogos['bootstrap']); }); + + it('... should display screwdriverWrench icon in devPreview link ', () => { + const faIconDe = getAndExpectDebugElementByCss(compDe, 'a#dev-preview-link > fa-icon', 1, 1); + const faIconIns = faIconDe[0].componentInstance.icon; + + expectToEqual(faIconIns, expectedScrewdriverWrenchIcon); + }); + + it('... should render link to devPreview', () => { + const devDes = getAndExpectDebugElementByCss(compDe, 'a#dev-preview-link', 1, 1); + const devEl = devDes[0].nativeElement; + + expect(devEl).toBeDefined(); + expectToBe(devEl.href, expectedPageMetaData.awgAppDevUrl); + }); }); }); }); diff --git a/src/app/core/footer/footer-poweredby/footer-poweredby.component.ts b/src/app/core/footer/footer-poweredby/footer-poweredby.component.ts index 1fb9f6add5..40a349d8c1 100644 --- a/src/app/core/footer/footer-poweredby/footer-poweredby.component.ts +++ b/src/app/core/footer/footer-poweredby/footer-poweredby.component.ts @@ -1,6 +1,8 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; -import { Logos } from '@awg-core/core-models'; +import { faScrewdriverWrench } from '@fortawesome/free-solid-svg-icons'; + +import { Logos, MetaPage } from '@awg-core/core-models'; /** * The FooterPoweredBy component. @@ -21,4 +23,19 @@ export class FooterPoweredbyComponent { */ @Input() logos: Logos; + + /** + * Input variable: pageMetaData. + * + * It keeps the page metadata for the component. + */ + @Input() + pageMetaData: MetaPage; + + /** + * Public variable: faScrewdriverWrench. + * + * It instantiates fontawesome's faScrewdriverWrench icon. + */ + faScrewdriverWrench = faScrewdriverWrench; } diff --git a/src/app/core/footer/footer.component.html b/src/app/core/footer/footer.component.html index 6db93e891e..a4c2268147 100644 --- a/src/app/core/footer/footer.component.html +++ b/src/app/core/footer/footer.component.html @@ -6,11 +6,11 @@
- +
- - + +
@@ -20,7 +20,7 @@
- +
diff --git a/src/app/core/footer/footer.component.spec.ts b/src/app/core/footer/footer.component.spec.ts index 71a8e22709..1d4ac51bf7 100644 --- a/src/app/core/footer/footer.component.spec.ts +++ b/src/app/core/footer/footer.component.spec.ts @@ -39,6 +39,8 @@ class FooterLogoStubComponent { class FooterPoweredbyStubComponent { @Input() logos: Logos; + @Input() + pageMetaData: MetaPage; } describe('FooterComponent (DONE)', () => { @@ -249,6 +251,20 @@ describe('FooterComponent (DONE)', () => { expectToEqual(footerCopyrightCmp.pageMetaData, expectedPageMetaData); }); + it('... should pass down pageMetaData to footer poweredby component', () => { + const footerPoweredbyDes = getAndExpectDebugElementByDirective( + compDe, + FooterPoweredbyStubComponent, + 1, + 1 + ); + const footerPoweredbyCmp = footerPoweredbyDes[0].injector.get( + FooterPoweredbyStubComponent + ) as FooterPoweredbyStubComponent; + + expectToEqual(footerPoweredbyCmp.pageMetaData, expectedPageMetaData); + }); + it('... should pass down logos to footer poweredby component', () => { const footerPoweredbyDes = getAndExpectDebugElementByDirective( compDe, diff --git a/src/app/core/services/conversion-service/conversion.service.spec.ts b/src/app/core/services/conversion-service/conversion.service.spec.ts index c7abdd4318..b9c58fe6af 100644 --- a/src/app/core/services/conversion-service/conversion.service.spec.ts +++ b/src/app/core/services/conversion-service/conversion.service.spec.ts @@ -1,25 +1,38 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; + +import Spy = jasmine.Spy; import { cleanStylesFromDOM } from '@testing/clean-up-helper'; import { mockSearchResponseJson } from '@testing/mock-data'; +import { expectSpyCall, expectToBe, expectToEqual } from '@testing/expect-helper'; import { AppModule } from '@awg-app/app.module'; +import { SearchResponseWithQuery } from '@awg-views/data-view/models'; import { ConversionService } from './conversion.service'; describe('ConversionService', () => { let conversionService: ConversionService; + let convertStandoffToHTMLSpy: Spy; + let replaceParagraphTagsSpy: Spy; + let replaceSalsahLinkSpy: Spy; + // TODO: add APP_BASE_HREF , see https://angular.io/api/common/APP_BASE_HREF beforeEach(() => { TestBed.configureTestingModule({ - imports: [AppModule, RouterTestingModule], + imports: [AppModule], providers: [ConversionService], }); conversionService = TestBed.inject(ConversionService); + + // Spies for service methods + replaceParagraphTagsSpy = spyOn(ConversionService, 'replaceParagraphTags').and.callThrough(); + + replaceSalsahLinkSpy = spyOn(conversionService, '_replaceSalsahLink').and.callThrough(); + convertStandoffToHTMLSpy = spyOn(conversionService, '_convertStandoffToHTML').and.callThrough(); }); afterAll(() => { @@ -37,4 +50,838 @@ describe('ConversionService', () => { expect(conversionService.filteredOut).toBeDefined(); }); + + describe('#replaceParagraphTags', () => { + it('... should have a static method `replaceParagraphTags`', () => { + expect(ConversionService.replaceParagraphTags).toBeDefined(); + }); + + it('... should replace paragraph tags correctly', () => { + const str = `

Hello World

`; + const expected = `Hello World`; + + const result = ConversionService.replaceParagraphTags(str); + + expectToBe(result, expected); + }); + + it('... should add line breaks correctly', () => { + const str = `

Hello

World

`; + const expected = `Hello
World`; + + const result = ConversionService.replaceParagraphTags(str); + + expectToBe(result, expected); + }); + + it('... should return undefined if input is falsy', () => { + const str = undefined; + + const result = ConversionService.replaceParagraphTags(str); + + expect(result).toBeUndefined(); + }); + }); + + describe('#prepareFullTextSearchResultText', () => { + it('... should have a method `prepareFullTextSearchResultText`', () => { + expect(conversionService.prepareFullTextSearchResultText).toBeDefined(); + }); + + describe('... should return an error message if', () => { + it('... subjects are undefined', () => { + const searchUrl = 'http://example.com'; + const emptySearchResponseWithQuery = new SearchResponseWithQuery( + JSON.parse(JSON.stringify(mockSearchResponseJson)), + 'Test' + ); + emptySearchResponseWithQuery.data.subjects = undefined; + const expected = `Die Abfrage ${searchUrl} ist leider fehlgeschlagen. Wiederholen Sie die Abfrage zu einem späteren Zeitpunkt oder überprüfen sie die Suchbegriffe.`; + + const result = conversionService.prepareFullTextSearchResultText( + emptySearchResponseWithQuery, + searchUrl + ); + + expectToBe(result, expected); + }); + + it('... subjects are null', () => { + const searchUrl = 'http://example.com'; + const emptySearchResponseWithQuery = new SearchResponseWithQuery( + JSON.parse(JSON.stringify(mockSearchResponseJson)), + 'Test' + ); + emptySearchResponseWithQuery.data.subjects = null; + const expected = `Die Abfrage ${searchUrl} ist leider fehlgeschlagen. Wiederholen Sie die Abfrage zu einem späteren Zeitpunkt oder überprüfen sie die Suchbegriffe.`; + + const result = conversionService.prepareFullTextSearchResultText( + emptySearchResponseWithQuery, + searchUrl + ); + + expectToBe(result, expected); + }); + }); + + it('... should return a string', () => { + const searchUrl = 'http://example.com'; + const searchResponseWithQuery = new SearchResponseWithQuery( + JSON.parse(JSON.stringify(mockSearchResponseJson)), + 'Test' + ); + + const result = conversionService.prepareFullTextSearchResultText(searchResponseWithQuery, searchUrl); + + expect(typeof result).toEqual('string'); + }); + + describe('... should return correct text', () => { + it('... for no results (default value for nhits = 0)', () => { + const searchUrl = 'http://example.com'; + const searchResponseWithQuery = new SearchResponseWithQuery( + JSON.parse(JSON.stringify(mockSearchResponseJson)), + 'Test' + ); + searchResponseWithQuery.data.subjects = []; + searchResponseWithQuery.data.nhits = undefined; + const subjectsLength = searchResponseWithQuery.data.subjects.length; + const expected = `0 / 0 Ergebnisse`; + + const result = conversionService.prepareFullTextSearchResultText(searchResponseWithQuery, searchUrl); + + expectToBe(result, expected); + }); + + it('... for a single result', () => { + const searchUrl = 'http://example.com'; + const searchResponseWithQuery = new SearchResponseWithQuery( + JSON.parse(JSON.stringify(mockSearchResponseJson)), + 'Test' + ); + searchResponseWithQuery.data.subjects = [mockSearchResponseJson.subjects[0]]; + searchResponseWithQuery.data.nhits = '1'; + const expected = `1 / 1 Ergebnis`; + + const result = conversionService.prepareFullTextSearchResultText(searchResponseWithQuery, searchUrl); + + expectToBe(result, expected); + }); + + it('... for multiple results', () => { + const searchUrl = 'http://example.com'; + const searchResponseWithQuery = new SearchResponseWithQuery( + JSON.parse(JSON.stringify(mockSearchResponseJson)), + 'Test' + ); + const subjectsLength = searchResponseWithQuery.data.subjects.length; + const expected = `${subjectsLength} / ${searchResponseWithQuery.data.nhits} Ergebnisse`; + + const result = conversionService.prepareFullTextSearchResultText(searchResponseWithQuery, searchUrl); + + expectToBe(result, expected); + }); + + it('... for multiple results with single duplicate filtered out', () => { + conversionService.filteredOut = 1; + + const searchUrl = 'http://example.com'; + const searchResponseWithQuery = new SearchResponseWithQuery( + JSON.parse(JSON.stringify(mockSearchResponseJson)), + 'Test' + ); + const subjectsLength = searchResponseWithQuery.data.subjects.length; + const expected = `${subjectsLength} / ${searchResponseWithQuery.data.nhits} Ergebnisse (1 Duplikat entfernt)`; + + const result = conversionService.prepareFullTextSearchResultText(searchResponseWithQuery, searchUrl); + + expectToBe(result, expected); + }); + + it('... for multiple results with multiple duplicates filtered out', () => { + conversionService.filteredOut = 2; + + const searchUrl = 'http://example.com'; + const searchResponseWithQuery = new SearchResponseWithQuery( + JSON.parse(JSON.stringify(mockSearchResponseJson)), + 'Test' + ); + const subjectsLength = searchResponseWithQuery.data.subjects.length; + const expected = `${subjectsLength} / ${searchResponseWithQuery.data.nhits} Ergebnisse (2 Duplikate entfernt)`; + + const result = conversionService.prepareFullTextSearchResultText(searchResponseWithQuery, searchUrl); + + expectToBe(result, expected); + }); + }); + }); + + describe('#_cleanSubjectValueLabels', () => { + it('... should have a method `_cleanSubjectValueLabels`', () => { + expect((conversionService as any)._cleanSubjectValueLabels).toBeDefined(); + }); + + it('... should clean valuelabel and obj_id', () => { + const subject = { + valuelabel: ['Test (Richtext)'], + obj_id: '123_-_local', + }; + + const result = (conversionService as any)._cleanSubjectValueLabels(subject); + + expectToBe(result.valuelabel[0], 'Test'); + expectToBe(result.obj_id, '123'); + }); + + it('... should not modify other properties', () => { + const subject = { + valuelabel: ['Test (Richtext)'], + obj_id: '123_-_local', + otherProp: 'otherValue', + }; + + const result = (conversionService as any)._cleanSubjectValueLabels(subject); + + expectToBe(result.valuelabel[0], 'Test'); + expectToBe(result.obj_id, '123'); + expectToBe(result.otherProp, 'otherValue'); + }); + }); + + describe('#_cleanSubjectValues', () => { + it('... should have a method `_cleanSubjectValues`', () => { + expect((conversionService as any)._cleanSubjectValues).toBeDefined(); + }); + + it('... should clean up richtext values for valuetype_id==14', () => { + const str = `A test string.`; + const jsonAttrs = { + italic: [{ start: 2, end: 6 }], + p: [{ start: 0, end: 14 }], + }; + const subject = { + valuetype_id: ['14'], + value: [{ utf8str: str, textattr: JSON.stringify(jsonAttrs) }], + }; + const expected = `A test string.`; + + const result = (conversionService as any)._cleanSubjectValues(subject); + + expectToBe(result.value[0], expected); + }); + + it('... should not clean up for other valuetype_ids', () => { + const str = `A test string.`; + const jsonAttrs = { + italic: [{ start: 2, end: 6 }], + p: [{ start: 0, end: 14 }], + }; + const subject = { + valuetype_id: ['15'], + value: [{ utf8str: str, textattr: JSON.stringify(jsonAttrs) }], + }; + + const result = (conversionService as any)._cleanSubjectValues(subject); + + expectToEqual(result, subject); + expectToEqual(result.value[0], subject.value[0]); + }); + + it('... should not clean up when no value is given', () => { + const subject = { + valuetype_id: ['14'], + value: [], + }; + + const result = (conversionService as any)._cleanSubjectValues(subject); + + expectToEqual(result, subject); + expect(result.value[0]).toBeUndefined(); + }); + }); + + describe('#_convertRichtextValue', () => { + it('... should have a method `_convertRichtextValue`', () => { + expect((conversionService as any)._convertRichtextValue).toBeDefined(); + }); + + it('... should trigger `_convertStandoffToHTML`', () => { + const str = `A test string.`; + const jsonAttrs = { + italic: [{ start: 2, end: 6 }], + p: [{ start: 0, end: 14 }], + }; + const expected = [str, JSON.stringify(jsonAttrs)]; + + (conversionService as any)._convertRichtextValue(str, JSON.stringify(jsonAttrs)); + + expectSpyCall(convertStandoffToHTMLSpy, 1, expected); + }); + + it('... should trigger `_replaceSalsahLink`', () => { + const str = `A test string.`; + const jsonAttrs = { + italic: [{ start: 2, end: 6 }], + p: [{ start: 0, end: 14 }], + }; + const expected = `

A test string.

`; + + (conversionService as any)._convertRichtextValue(str, JSON.stringify(jsonAttrs)); + + expectSpyCall(replaceSalsahLinkSpy, 1, expected); + }); + + it('... should trigger `replaceParagraphTags`', () => { + const str = `A test string.`; + const jsonAttrs = { + italic: [{ start: 2, end: 6 }], + p: [{ start: 0, end: 14 }], + }; + const expected = `

A test string.

`; + + (conversionService as any)._convertRichtextValue(str, JSON.stringify(jsonAttrs)); + + expectSpyCall(replaceParagraphTagsSpy, 1, expected); + }); + + describe('... should return undefined', () => { + it('... when the input string is empty', () => { + const str = ''; + const jsonAttrs = { italic: [{ start: 5, end: 9 }] }; + + const result = (conversionService as any)._convertRichtextValue(str, jsonAttrs); + + expect(result).toBeUndefined(); + }); + + it('... when the input string is null or undefined', () => { + const str = null; + const jsonAttrs = { italic: [{ start: 5, end: 9 }] }; + + const result = (conversionService as any)._convertRichtextValue(str, jsonAttrs); + + expect(result).toBeUndefined(); + }); + }); + + it('... should return the original string if no attributes are given', () => { + const str = `A test string without attributes.`; + const jsonAttrs = undefined; + const expected = 'A test string without attributes.'; + + const result = (conversionService as any)._convertRichtextValue(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + + it('... should return a string', () => { + const str = `A test string.`; + const jsonAttrs = { + italic: [{ start: 2, end: 6 }], + p: [{ start: 0, end: 14 }], + }; + + const result = (conversionService as any)._convertRichtextValue(str, JSON.stringify(jsonAttrs)); + + expect(typeof result).toEqual('string'); + }); + + it('... should correctly convert a rich text value with Standoff to HTML', () => { + const str = `A test string.`; + const jsonAttrs = { + italic: [{ start: 2, end: 6 }], + p: [{ start: 0, end: 14 }], + }; + const expected = `A test string.`; + + const result = (conversionService as any)._convertRichtextValue(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + + it('... should correctly replace salsah links', () => { + const str = `A test string with a link.`; + const jsonAttrs = { + _link: [{ start: 21, end: 25, href: 'http://www.salsah.org/api/resources/11690', resid: '11690' }], + italic: [{ start: 2, end: 6 }], + p: [{ start: 0, end: 26 }], + }; + const expected = `A test string with a link.`; + + const result = (conversionService as any)._convertRichtextValue(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + expect(result).not.toContain(`class="salsah"`); + }); + + it('... should correctly replace paragraph tags', () => { + const str = `

A test string with p-tags.

`; + const jsonAttrs = { italic: [{ start: 5, end: 9 }] }; + const expected = `A test string with p-tags.`; + + const result = (conversionService as any)._convertRichtextValue(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + }); + + describe('#_convertStandoffToHTML', () => { + it('... should have a method `_convertStandoffToHTML`', () => { + expect((conversionService as any)._convertStandoffToHTML).toBeDefined(); + }); + + describe('... should return undefined', () => { + it('... when the input string is empty', () => { + const str = ''; + const jsonAttrs = { italic: [{ start: 5, end: 9 }] }; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expect(result).toBeUndefined(); + }); + + it('... when the input string is null or undefined', () => { + const str = null; + const jsonAttrs = { italic: [{ start: 5, end: 9 }] }; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expect(result).toBeUndefined(); + }); + }); + + it('... should return the original string if no attributes are given', () => { + const str = `A test string without attributes.`; + const jsonAttrs = undefined; + const expected = 'A test string without attributes.'; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + + it('... should return a string', () => { + const str = `A test string.`; + const jsonAttrs = { + italic: [{ start: 2, end: 6 }], + p: [{ start: 0, end: 14 }], + }; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expect(typeof result).toEqual('string'); + }); + + it('... should correctly convert a rich text value with Standoff to HTML', () => { + const str = `A test string.`; + const jsonAttrs = { + italic: [{ start: 2, end: 6 }], + p: [{ start: 0, end: 14 }], + }; + const expected = `

A test string.

`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + + describe('... should correctly convert standoff to HTML tags', () => { + it('... bold --> strong', () => { + const str = `A test string.`; + const jsonAttrs = { + bold: [{ start: 2, end: 6 }], + }; + const expected = `A test string.`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + + it('... underline --> u', () => { + const str = `A test string.`; + const jsonAttrs = { + underline: [{ start: 2, end: 6 }], + }; + const expected = `A test string.`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + + it('... strikethrough --> s', () => { + const str = `A test string.`; + const jsonAttrs = { + strikethrough: [{ start: 2, end: 6 }], + }; + const expected = `A test string.`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + + it('... italic --> em', () => { + const str = `A test string.`; + const jsonAttrs = { + italic: [{ start: 2, end: 6 }], + }; + const expected = `A test string.`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + + it('... h1 --> h1', () => { + const str = `A test string.`; + const jsonAttrs = { + h1: [{ start: 2, end: 6 }], + }; + const expected = `A

test

string.`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + + it('... h2 --> h2', () => { + const str = `A test string.`; + const jsonAttrs = { + h2: [{ start: 2, end: 6 }], + }; + const expected = `A

test

string.`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + + it('... h3 --> h3', () => { + const str = `A test string.`; + const jsonAttrs = { + h3: [{ start: 2, end: 6 }], + }; + const expected = `A

test

string.`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + + it('... h4 --> h4', () => { + const str = `A test string.`; + const jsonAttrs = { + h4: [{ start: 2, end: 6 }], + }; + const expected = `A

test

string.`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + + it('... h5 --> h5', () => { + const str = `A test string.`; + const jsonAttrs = { + h5: [{ start: 2, end: 6 }], + }; + const expected = `A
test
string.`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + + it('... h6 --> h6', () => { + const str = `A test string.`; + const jsonAttrs = { + h6: [{ start: 2, end: 6 }], + }; + const expected = `A
test
string.`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + + it('... ol --> ol', () => { + const str = `A test string.`; + const jsonAttrs = { + ol: [{ start: 2, end: 6 }], + }; + const expected = `A
    test
string.`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + + it('... ul --> ul', () => { + const str = `A test string.`; + const jsonAttrs = { + ul: [{ start: 2, end: 6 }], + }; + const expected = `A
    test
string.`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + + it('... li --> li', () => { + const str = `A test string.`; + const jsonAttrs = { + li: [{ start: 2, end: 6 }], + }; + const expected = `A
  • test
  • string.`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + + it('... style --> span', () => { + const str = `A test string.`; + const jsonAttrs = { + style: [{ start: 2, end: 6 }], + }; + const expected = `A test string.`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToEqual(result, expected); + }); + + it('... p --> p', () => { + const str = `A test string.`; + const jsonAttrs = { + p: [{ start: 2, end: 6 }], + }; + const expected = `A

    test

    string.`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToEqual(result, expected); + }); + + it('... sup --> sup', () => { + const str = `A test string.`; + const jsonAttrs = { + sup: [{ start: 2, end: 6 }], + }; + const expected = `A test string.`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToEqual(result, expected); + }); + + it('... sub --> sub', () => { + const str = `A test string.`; + const jsonAttrs = { + sub: [{ start: 2, end: 6 }], + }; + const expected = `A test string.`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToEqual(result, expected); + }); + + it('... _link --> a', () => { + const str = `A test string.`; + const jsonAttrs = { + _link: [{ start: 2, end: 6, href: 'http://www.salsah.org/api/resources/54623', resid: '54623' }], + }; + const expected = `A test string.`; + + const result = (conversionService as any)._convertStandoffToHTML(str, JSON.stringify(jsonAttrs)); + + expectToBe(result, expected); + }); + }); + }); + + describe('#_distinctSubjects', () => { + it('... should have a method `_distinctSubjects`', () => { + expect((conversionService as any)._distinctSubjects).toBeDefined(); + }); + + it('... should return undefined when subjects are undefined', () => { + const subjects = undefined; + + const result = (conversionService as any)._distinctSubjects(subjects); + + expect(result).toBeUndefined(); + }); + + it('... should return unchanged array when input has no duplicates', () => { + const subjectsWithoutDuplicate = [ + { obj_id: '1', obj_label: 'Label 1' }, + { obj_id: '2', obj_label: 'Label 2' }, + ]; + + const result = (conversionService as any)._distinctSubjects(subjectsWithoutDuplicate); + + expectToEqual(result, subjectsWithoutDuplicate); + }); + + it('... should return distinct array when input has duplicates', () => { + const subjectsWithDuplicate = [ + { obj_id: '1', obj_label: 'Label 1' }, + { obj_id: '2', obj_label: 'Label 2' }, + { obj_id: '1', obj_label: 'Label 1' }, + ]; + + const expected = [ + { obj_id: '1', obj_label: 'Label 1' }, + { obj_id: '2', obj_label: 'Label 2' }, + ]; + + const result = (conversionService as any)._distinctSubjects(subjectsWithDuplicate); + + expectToEqual(result, expected); + }); + + it('... should set filteredOut to correct value when input has duplicates', () => { + const subjectsWithSingleDuplicate = [ + { obj_id: '1', obj_label: 'Label 1' }, + { obj_id: '2', obj_label: 'Label 2' }, + { obj_id: '1', obj_label: 'Label 1' }, + ]; + + (conversionService as any)._distinctSubjects(subjectsWithSingleDuplicate); + + expectToBe(conversionService.filteredOut, 1); + + const subjectsWithMultipleDuplicates = [ + { obj_id: '1', obj_label: 'Label 1' }, + { obj_id: '2', obj_label: 'Label 2' }, + { obj_id: '1', obj_label: 'Label 1' }, + { obj_id: '2', obj_label: 'Label 2' }, + ]; + + (conversionService as any)._distinctSubjects(subjectsWithMultipleDuplicates); + + expectToBe(conversionService.filteredOut, 2); + + const subjectsWithoutDuplicate = [ + { obj_id: '1', obj_label: 'Label 1' }, + { obj_id: '2', obj_label: 'Label 2' }, + ]; + + (conversionService as any)._distinctSubjects(subjectsWithoutDuplicate); + + expectToBe(conversionService.filteredOut, 0); + }); + }); + + describe('#_replaceSalsahLink', () => { + it('... should have a method `_replaceSalsahLink`', () => { + expect((conversionService as any)._replaceSalsahLink).toBeDefined(); + }); + + describe('... should return undefined', () => { + it('... when the input string is empty', () => { + const str = ''; + + const result = (conversionService as any)._replaceSalsahLink(str); + + expect(result).toBeUndefined(); + }); + + it('... when the input string is null or undefined', () => { + const str = null; + + const result = (conversionService as any)._replaceSalsahLink(str); + + expect(result).toBeUndefined(); + }); + }); + + describe('... should return the original string if', () => { + it('... there are no links', () => { + const str = `A test string without any links.`; + + const result = (conversionService as any)._replaceSalsahLink(str); + + expectToBe(result, str); + }); + + it('... there are no salsah links', () => { + const str = `A test string with an Example link.`; + + const result = (conversionService as any)._replaceSalsahLink(str); + + expectToBe(result, str); + }); + }); + + describe('... should correctly replace salsah links', () => { + it('... starting with https', () => { + const str = `A test string with a Salsah link.`; + const expected = `A test string with a Salsah link.`; + + const result = (conversionService as any)._replaceSalsahLink(str); + + expectToBe(result, expected); + }); + + it('... starting with `https://www.`', () => { + const str = `A test string with a Salsah link.`; + const expected = `A test string with a Salsah link.`; + + const result = (conversionService as any)._replaceSalsahLink(str); + + expectToBe(result, expected); + }); + + it('... starting with http', () => { + const str = `A test string with a Salsah link.`; + const expected = `A test string with a Salsah link.`; + + const result = (conversionService as any)._replaceSalsahLink(str); + + expectToBe(result, expected); + }); + + it('... starting with `http://www.`', () => { + const str = `A test string with a Salsah link.`; + const expected = `A test string with a Salsah link.`; + + const result = (conversionService as any)._replaceSalsahLink(str); + + expectToBe(result, expected); + }); + + it('... with multiple salsah links', () => { + const str = `A test string with a Salsah link and another Salsah link.`; + const expected = `A test string with a Salsah link and another Salsah link.`; + + const result = (conversionService as any)._replaceSalsahLink(str); + + expectToBe(result, expected); + }); + + it('... with small resource ids', () => { + const str = `A test string with a Salsah link.`; + const expected = `A test string with a Salsah link.`; + + const result = (conversionService as any)._replaceSalsahLink(str); + + expectToBe(result, expected); + }); + + it('... with large resource ids', () => { + const str = `A test string with a Salsah link.`; + const expected = `A test string with a Salsah link.`; + + const result = (conversionService as any)._replaceSalsahLink(str); + + expectToBe(result, expected); + }); + }); + }); }); diff --git a/src/app/core/services/conversion-service/conversion.service.ts b/src/app/core/services/conversion-service/conversion.service.ts index c5d95edbd1..93f01c3918 100644 --- a/src/app/core/services/conversion-service/conversion.service.ts +++ b/src/app/core/services/conversion-service/conversion.service.ts @@ -5,9 +5,8 @@ import { Observable } from 'rxjs'; import { NgxGalleryImage } from '@kolkov/ngx-gallery'; -import { ApiService } from '@awg-core/services/api-service'; - import { GeoNames } from '@awg-core/core-models'; +import { ApiService } from '@awg-core/services/api-service'; import { UtilityService } from '@awg-core/services/utility-service'; import { ContextJson, @@ -88,6 +87,24 @@ export class ConversionService extends ApiService { super(http); } + /** + * Public static method: replaceParagraphTags. + * + * It removes paragraph tags in richtext values + * and replaces line breaks instead for multiple lines. + * + * @param {string} str The given richtext value. + * + * @returns {string} The adjusted richtext value. + */ + public static replaceParagraphTags(str: string): string { + if (!str) { + return undefined; + } + const replacedStr = str.replace(/<\/p>

    /g, '
    ').replace(/

    |<\/p>/g, ''); + return replacedStr; + } + /** * Public method: convertFullTextSearchResults. * @@ -103,70 +120,19 @@ export class ConversionService extends ApiService { return searchResults; } - // TODO: refactor with reduce?? - searchResults.subjects.forEach(subject => { - // Clean value labels - subject.valuelabel[0] = subject.valuelabel[0].replace(' (Richtext)', ''); - subject.obj_id = subject.obj_id.replace('_-_local', ''); - - // =>Chronologie: salsah standoff needs to be converted before displaying - // Valuetype_id 14 = valuelabel 'Ereignis' - if (subject.valuetype_id[0] === '14' && subject.value[0]) { - let htmlstr = ''; - const utf8str: string = subject.value[0].utf8str; - const textattr: string = subject.value[0].textattr; - - // Check if there is standoff, otherwise leave res.value[0] alone - // Because when retrieved from cache the standoff is already converted - if (utf8str && textattr) { - htmlstr = this._convertStandoffToHTML(utf8str, textattr); + searchResults.subjects = searchResults.subjects.reduce((acc, subject) => { + subject = this._cleanSubjectValueLabels(subject); + subject = this._cleanSubjectValues(subject); - // Replace salsah links - htmlstr = this._replaceSalsahLink(htmlstr); + acc.push(subject); + return acc; + }, []); - // Strip & replace

    -tags for displaying - htmlstr = this._replaceParagraphTags(htmlstr); - - subject.value[0] = htmlstr; - } - } - }); // Remove duplicates from response searchResults.subjects = this._distinctSubjects(searchResults.subjects); return searchResults; } - /** - * Public method: prepareFullTextSearchResultText. - * - * It prepares the fulltext search result text - * to be displayed in the search info. - * - * @param {SearchResponseWithQuery} searchResponseWithQuery The given results and query of a search request. - * @param {string} searchUrl The given url of a search request. - * - * @returns {string} The text to be displayed. - */ - prepareFullTextSearchResultText(searchResponseWithQuery: SearchResponseWithQuery, searchUrl: string): string { - let resText: string; - - const searchResults = { ...searchResponseWithQuery.data }; - - if (searchResults.subjects) { - const currentLength = searchResults.subjects.length; - const totalLength = searchResults.nhits; - const resString: string = length === 1 ? 'Ergbnis' : 'Ergebnisse'; - resText = `${currentLength} / ${totalLength} ${resString}`; - if (this.filteredOut > 0) { - resText += ' (Duplikate enfternt)'; - } - } else { - resText = `Die Abfrage ${searchUrl} ist leider fehlgeschlagen. Wiederholen Sie die Abfrage zu einem späteren Zeitpunkt oder überprüfen sie die Suchbegriffe.`; - } - - return resText; - } - /** * Public method: convertObjectProperties. * @@ -219,11 +185,7 @@ export class ConversionService extends ApiService { let htmlstr = ''; // Convert linear salsah standoff to html (using plugin "htmlConverter") - htmlstr = this._convertStandoffToHTML(prop.values[i].utf8str, prop.values[i].textattr); - - // Replace salsah links &

    -tags - htmlstr = this._replaceSalsahLink(htmlstr); - htmlstr = htmlstr.replace('

    ', '').replace('

    ', ''); + htmlstr = this._convertRichtextValue(prop.values[i].utf8str, prop.values[i].textattr); // Trim string propValue[i] = htmlstr.trim(); @@ -263,7 +225,7 @@ export class ConversionService extends ApiService { }); // END forEach PROPS return convObj; - } // END convertObjectProperties (func) + } /** * Public method: convertResourceData. @@ -289,6 +251,34 @@ export class ConversionService extends ApiService { } } + /** + * Public method: prepareFullTextSearchResultText. + * + * It prepares the fulltext search result text + * to be displayed in the search info. + * + * @param {SearchResponseWithQuery} searchResponseWithQuery The given results and query of a search request. + * @param {string} searchUrl The given url of a search request. + * + * @returns {string} The text to be displayed. + */ + prepareFullTextSearchResultText(searchResponseWithQuery: SearchResponseWithQuery, searchUrl: string): string { + const { subjects, nhits: totalLength = 0 } = { ...searchResponseWithQuery?.data }; + + if (subjects) { + const currentLength = subjects.length; + const resString: string = currentLength === 1 ? 'Ergebnis' : 'Ergebnisse'; + let resText = `${currentLength} / ${totalLength} ${resString}`; + if (this.filteredOut > 0) { + const duplicateString = this.filteredOut === 1 ? 'Duplikat' : 'Duplikate'; + resText += ` (${this.filteredOut} ${duplicateString} entfernt)`; + } + return resText; + } else { + return `Die Abfrage ${searchUrl} ist leider fehlgeschlagen. Wiederholen Sie die Abfrage zu einem späteren Zeitpunkt oder überprüfen sie die Suchbegriffe.`; + } + } + /** * Private method: _prepareRestrictedResource. * @@ -531,6 +521,53 @@ export class ConversionService extends ApiService { return prop; } + /** + * Private method: _cleanSubjectValueLabels. + * + * It cleans the value labels of a subject (SubjectItemJson) + * to be displayed via HTML. + * + * @param {SubjectItemJson} subject The given subject. + * + * @returns {SubjectItemJson} The cleaned subject. + */ + _cleanSubjectValueLabels(subject: SubjectItemJson): SubjectItemJson { + let { valuelabel, obj_id } = subject; + + if (valuelabel?.[0]) { + valuelabel[0] = valuelabel[0].replace(' (Richtext)', ''); + } + if (obj_id) { + obj_id = obj_id.replace('_-_local', ''); + } + return { ...subject, valuelabel, obj_id }; + } + + /** + * Private method: _cleanSubjectValues. + * + * It cleans the values of a subject (SubjectItemJson) + * to be displayed via HTML. + * + * @param {SubjectItemJson} subject The given subject. + * + * @returns {SubjectItemJson} The cleaned subject. + */ + _cleanSubjectValues(subject: SubjectItemJson): SubjectItemJson { + let tmpSubject = { ...subject }; + const { valuetype_id, value } = tmpSubject; + const firstValue = value?.[0]; + + if (valuetype_id?.[0] === '14' && firstValue) { + const { utf8str, textattr } = firstValue; + if (utf8str && textattr) { + const htmlstr = this._convertRichtextValue(utf8str, textattr); + tmpSubject.value[0] = htmlstr; + } + } + return tmpSubject; + } + /** * Private method: _convertDateValue. * @@ -679,10 +716,13 @@ export class ConversionService extends ApiService { */ private _convertRichtextValue(str: string, attr: string): string { // Convert salsah standoff to html (using plugin "htmlConverter") - const rtValue: string = this._convertStandoffToHTML(str, attr); + const htmlValue: string = this._convertStandoffToHTML(str, attr); // Replace salsah links - return this._replaceSalsahLink(rtValue); + const replacedLinks = this._replaceSalsahLink(htmlValue); + + // Strip & replace

    -tags for displaying + return ConversionService.replaceParagraphTags(replacedLinks); } /** @@ -744,20 +784,45 @@ export class ConversionService extends ApiService { * to html using plugin 'htmlConverter'. * * @param {string} str The given utf8 string of a rich text property. - * @param {string} attr The given standoff attributes of a richtext property. + * @param {string} jsonAttrs The given standoff (JSON) attributes of a richtext property. * * @returns {string} The converted standoff. - * - * @todo check if it is possible to unify with hlist conversion? */ - private _convertStandoffToHTML(str: string, attr: string): string { + private _convertStandoffToHTML(str: string, jsonAttrs: string): string { if (!str) { return undefined; } - if (!attr) { + if (!jsonAttrs) { return str; } - return htmlConverter(JSON.parse(attr), str); + return htmlConverter(JSON.parse(jsonAttrs), str); + } + + /** + * Private method: _distinctSubjects. + * + * It removes duplicates from an array of subjects (SubjectItemJson[]). + * It uses the `reduce` method to create an object with unique `obj_id` keys, + * then converts this object back to an array. + * + * @param {SubjectItemJson[]} subjects The given subject with possible duplicates. + * + * @returns {SubjectItemJson[]} The distinct subjects. + */ + private _distinctSubjects(subjects: SubjectItemJson[]): SubjectItemJson[] { + if (!subjects) { + return undefined; + } + + const distinctObj = subjects.reduce((acc, subject) => { + acc[subject.obj_id] = subject; + return acc; + }, {}); + const distinctArr = Object.values(distinctObj) as SubjectItemJson[]; + + this.filteredOut = subjects.length - distinctArr.length; + + return distinctArr; } /** @@ -871,85 +936,26 @@ export class ConversionService extends ApiService { } // Regexp for Salsah links - // Including subgroup for object id: /[1-9]\d{0,9}/ (any up-to 10-digit integer greater 0) + // Including subgroup for object id: /[1-9]\d{0,12}/ (any up-to 13-digit integer greater 0) const regLink = - /]*?\s+)?href=(["'])((http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?salsah\.org\/api\/resources\/([1-9]\d{0,9}))\1 class=(["'])salsah-link\5>(.*?)<\/a>/i; + /]*?\s+)?href=(["'])((?:https?:\/\/(?:www\.)?)?salsah\.org\/api\/resources\/([1-9]\d{0,12}))\1 class=(["'])salsah-link\4>(.*?)<\/a>/i; let regArr: RegExpExecArray; // Check for salsah links in str - while (regLink.exec(str)) { - // I.e.: as long as regLink is detected in str do... - regArr = regLink.exec(str); - + while ((regArr = regLink.exec(str))) { // Resource id is in 4th array entry - const resId = regArr[4]; + const resId = regArr[3]; // Link text is stored in last array entry - const resTextContent = regArr[regArr.length - 1]; + const resTextContent = regArr.at(-1); // Replace href attribute with click-directive - const replaceValue = - '' + - resTextContent + - ''; - str = str.replace(regArr[0], replaceValue); - } // END while + const replaceValue = `${resTextContent}`; - return str; - } - - /** - * Private method: _replaceParagraphTags. - * - * It removes paragraph tags in richtext values - * and replaces line breaks instead for multiple lines. - * - * @param {string} str The given richtext value. - * - * @returns {string} The adjusted richtext value. - */ - private _replaceParagraphTags(str: string): string { - if (!str) { - return undefined; - } - str = str - .replace(/<\/p>

    /g, '
    ') - .replace(/

    |<\/p>/g, '') - .replace(str, '«$&»'); - return str; - } - - /** - * Private method: _distinctSubjects. - * - * It removes duplicates from an array (SubjectItemJson[]). - * It checks for every array position (reduce) if the obj_id - * of the entry at the current position (y) is already - * in the array (findIndex). If that is not the case it - * pushes y into x which is initialized as empty array []. - * - * See also {@link https://gist.github.com/telekosmos/3b62a31a5c43f40849bb#gistcomment-2137855}. - * - * @param {SubjectItemJson[]} subjects The given subject with possible duplicates. - * - * @returns {SubjectItemJson[]} The distinct subjects. - */ - private _distinctSubjects(subjects: SubjectItemJson[]): SubjectItemJson[] { - if (!subjects) { - return undefined; + str = str.replace(regArr[0], replaceValue); } - this.filteredOut = 0; - const distinctObj = {}; - let distinctArr = []; - - subjects.forEach((subject: SubjectItemJson) => (distinctObj[subject.obj_id] = subject)); - distinctArr = Object.values(distinctObj); - this.filteredOut = subjects.length - distinctArr.length; - - return distinctArr; + return str; } /** diff --git a/src/app/core/services/gnd-service/gnd.service.spec.ts b/src/app/core/services/gnd-service/gnd.service.spec.ts index 3d7cc7d238..013c0af28e 100644 --- a/src/app/core/services/gnd-service/gnd.service.spec.ts +++ b/src/app/core/services/gnd-service/gnd.service.spec.ts @@ -1,14 +1,14 @@ import { TestBed } from '@angular/core/testing'; +import Spy = jasmine.Spy; import { cleanStylesFromDOM } from '@testing/clean-up-helper'; import { expectSpyCall, expectToBe, expectToEqual } from '@testing/expect-helper'; import { mockConsole, mockLocalStorage, mockSessionStorage, mockWindow } from '@testing/mock-helper'; import { AppConfig } from '@awg-app/app.config'; - import { StorageType } from '@awg-core/services/storage-service'; + import { GndEvent, GndEventType, GndService } from './gnd.service'; -import Spy = jasmine.Spy; describe('GndService (DONE)', () => { let gndService: GndService; diff --git a/src/app/shared/address/address.component.html b/src/app/shared/address/address.component.html index 38d0c85e3d..e73de08fd1 100644 --- a/src/app/shared/address/address.component.html +++ b/src/app/shared/address/address.component.html @@ -1,5 +1,5 @@

    -

    +

    {{ pageMetaData?.awgProjectName }} diff --git a/src/app/views/contact-view/contact-view.component.html b/src/app/views/contact-view/contact-view.component.html index 628958e590..d9b7d39cc5 100644 --- a/src/app/views/contact-view/contact-view.component.html +++ b/src/app/views/contact-view/contact-view.component.html @@ -6,7 +6,7 @@

    -

    Empfohlene Zitierweisen:

    +

    Empfohlene Zitierweisen:

    Website:

    @@ -34,7 +34,7 @@

    - GitHub:
    + GitHub:
    Repository unter: {{ pageMetaData?.githubUrl }} @@ -42,7 +42,7 @@

    - Compodoc:
    + Compodoc:
    Dokumentation von Struktur und Funktionalitäten der Angular App:
    awg-app documentation

    @@ -55,7 +55,7 @@

    - Herausgeber:
    + Herausgeber:
    {{ pageMetaData.awgProjectName }}
    {{ contactMetaData.address.institution }}
    {{ contactMetaData.address.street }}
    @@ -64,21 +64,21 @@

    - Konzept:
    + Konzept:
    {{ pageMetaData.awgProjectName }}

    - Texte/Inhalte:
    + Texte/Inhalte:
    Die Verantwortung für die Inhalte der Website liegt bei der {{ pageMetaData.awgProjectName }}. Bei inhaltlichen Fragen wenden Sie sich bitte an die unter Kontakt angegebene Adresse.

    - Materialien, Notentexte und Bilder (vgl. Lizenzierung):
    + Materialien, Notentexte und Bilder (vgl. Lizenzierung):
    Digitales Archiv der {{ pageMetaData.awgProjectName }}
    Online-Edition der {{ pageMetaData.awgProjectName }}

    - Programmierung & Webdesign:
    + Programmierung & Webdesign:
    Stefan Münnich
    Wissenschaftlicher Mitarbeiter der {{ pageMetaData.awgProjectName }}, Basel

    -

    Disclaimer/Haftungserklärung:

    +

    Disclaimer/Haftungserklärung:

    Urheberrecht und Lizenzierung:

    Sämtliche im Rahmen der {{ pageMetaData.awgProjectName }} erarbeiteten und auf ihrer Website oder ihrer diff --git a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-header/resource-detail-header.component.html b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-header/resource-detail-header.component.html index af63f8ebe7..f91d48104f 100644 --- a/src/app/views/data-view/data-outlets/resource-detail/resource-detail-header/resource-detail-header.component.html +++ b/src/app/views/data-view/data-outlets/resource-detail/resource-detail-header/resource-detail-header.component.html @@ -2,14 +2,11 @@

    -

    -
    +
    +

    -

    -
    - -
    -

    + +