diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7bc5e2db..41ecd814 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,38 +1,33 @@ { - "name": "PublicCodeEditor", - "dockerFile": "Dockerfile", - "runArgs": [ - "--name", - "PublicCodeEditor_devcontainer" - ], - "postCreateCommand": "sh .devcontainer/postCreateCommand.sh", - "customizations": { - "vscode": { - "settings": { - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": "always" - }, - "[typescript]": { - "editor.defaultFormatter": "dbaeumer.vscode-eslint" - }, - "[go]": { - "editor.insertSpaces": false, - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": "always" - } - } - }, - "extensions": [ - "dbaeumer.vscode-eslint", - "github.vscode-pull-request-github", - "github.vscode-github-actions", - "golang.go", - "firsttris.vscode-jest-runner", - "waderyan.gitblame" - ] - } - }, - "remoteUser": "root" -} \ No newline at end of file + "name": "PublicCodeEditor", + "dockerFile": "Dockerfile", + "runArgs": ["--name", "PublicCodeEditor_devcontainer"], + "postCreateCommand": "sh .devcontainer/postCreateCommand.sh", + "customizations": { + "vscode": { + "settings": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "always" + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[go]": { + "editor.insertSpaces": false, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "always" + } + } + }, + "extensions": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "github.vscode-pull-request-github", + "github.vscode-github-actions", + "golang.go", + "firsttris.vscode-jest-runner", + "waderyan.gitblame" + ] + } + } +} diff --git a/.devcontainer/githooks/pre-push b/.devcontainer/githooks/pre-push index da6eea44..d0e7fdc8 100644 --- a/.devcontainer/githooks/pre-push +++ b/.devcontainer/githooks/pre-push @@ -3,5 +3,7 @@ echo "Git pre-push hook" echo "Run Build" npm run build +echo "Run Lint" +npm run lint echo "Run Test" npm run test \ No newline at end of file diff --git a/.eslintrc.yml b/.eslintrc.yml index c3d02ea8..ac9e346b 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -6,6 +6,7 @@ extends: - plugin:@typescript-eslint/recommended - plugin:react/recommended - plugin:react/jsx-runtime + - prettier parser: "@typescript-eslint/parser" parserOptions: ecmaVersion: latest @@ -13,6 +14,7 @@ parserOptions: plugins: - "@typescript-eslint" - react + - prettier rules: react/prop-types: off settings: diff --git a/.github/workflows/create-release-pr.yml b/.github/workflows/create-release-pr.yml new file mode 100644 index 00000000..6c8a9e00 --- /dev/null +++ b/.github/workflows/create-release-pr.yml @@ -0,0 +1,74 @@ +name: Create Release PR +on: + workflow_dispatch: + inputs: + release_type: + description: 'Select type of release' + required: true + type: choice + default: patch + options: + - patch + - minor + - major + +permissions: + contents: write + pull-requests: write +jobs: + create_release_pr: + runs-on: ubuntu-latest + steps: + - name: Checkout develop branch + uses: actions/checkout@v4 + with: + ref: develop + fetch-depth: 0 + + - name: Set up Git user + run: | + git config --global user.name "${GITHUB_ACTOR}" + git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com" + + - name: Get Next Version + id: nv + run: + echo "new_version=$(npx semver $(jq -r .version package.json) -i ${{ github.event.inputs.release_type }})" >> $GITHUB_OUTPUT + + - name: Create release branch + run: | + RELEASE_BRANCH="release/${{ steps.nv.outputs.new_version }}" + git checkout -b "$RELEASE_BRANCH" develop + git push --set-upstream origin "$RELEASE_BRANCH" + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install deps + run: npm ci + + - name: Update release version + run: | + RELEASE_BRANCH="release/${{ steps.nv.outputs.new_version }}" + git status + if [ -f package.json ]; then + npm version ${{ steps.nv.outputs.new_version }} --no-git-tag-version + git add package.json package-lock.json + fi + if [ -f publiccode.yml ]; then + node scripts/ci/updatePubliccodeReleaseDate.js + node scripts/ci/updatePubliccodeSoftwareVersion.js ${{ steps.nv.outputs.new_version }} + git add publiccode.yml + fi + git commit -m "chore: bump version to ${{ steps.nv.outputs.new_version }}" || echo "No changes to commit" + git push origin "$RELEASE_BRANCH" + + - name: Create PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + RELEASE_BRANCH="release/${{ steps.nv.outputs.new_version }}" + git checkout "$RELEASE_BRANCH" + gh pr create -t "chore: release ${{ steps.nv.outputs.new_version }}" -b "Release ${{ steps.nv.outputs.new_version }}. Both Package version and publiccode.yml are already updated. Don't forget to edit CHANGELOG.md" -B main -l release \ No newline at end of file diff --git a/.github/workflows/dependabot-on-develop.yml b/.github/workflows/dependabot-on-develop.yml new file mode 100644 index 00000000..43918457 --- /dev/null +++ b/.github/workflows/dependabot-on-develop.yml @@ -0,0 +1,57 @@ +name: Recreate Dependabot PR on develop + +on: + pull_request: + types: [opened] + branches: + - main + +jobs: + rebase-dependabot-to-develop: + if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]' + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Fetch full history and all branches + run: git fetch --unshallow --all + + - name: Create a new branch from develop with dependabot changes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + ORIGINAL_BRANCH="${{ github.event.pull_request.head.ref }}" + NEW_BRANCH="dependabot-develop-${ORIGINAL_BRANCH}" + + # Create new branch based on develop + git checkout origin/develop -b $NEW_BRANCH + + # Get last commit message from original Dependabot branch + LAST_COMMIT_MSG=$(git log -1 --pretty=%B origin/$ORIGINAL_BRANCH) + + # Merge Dependabot changes into the new branch + git merge --no-commit origin/$ORIGINAL_BRANCH || true + git commit -m "$LAST_COMMIT_MSG" + git push origin $NEW_BRANCH + + # Create new pull request against develop + gh pr create \ + --base develop \ + --head $NEW_BRANCH \ + --title "${{ github.event.pull_request.title }} (rebased onto develop)" \ + --body "This is an automated copy of #${{ github.event.pull_request.number }}, targeting \`develop\` instead of \`main\`." + + # Close the original PR + gh pr close ${{ github.event.pull_request.number }} --comment "Automatically closed: a new PR has been created against \`develop\`." + + # Delete the original Dependabot branch + gh api \ + -X DELETE \ + repos/${{ github.repository }}/git/refs/heads/$ORIGINAL_BRANCH \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 320e1858..d0bddc76 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,14 +1,14 @@ on: push: branches: - - main + - cd-bund jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version: 20 - uses: actions/setup-go@v6 @@ -17,12 +17,9 @@ jobs: - run: npm ci - run: npm run build env: - DEFAULT_COUNTRY_SECTION: italy - ELASTIC_URL: "https://elasticsearch.developers.italia.it/indicepa_pec/_search" + DEFAULT_COUNTRY_SECTION: switzerland - run: | - git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/italia/publiccode-editor.git - echo publiccode-editor.developers.italia.it > dist/CNAME - - npm run deploy -m "Automated deployment: ${CIRCLE_SHA1} [ci skip]" + git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/swiss/publiccode-editor.git + npm run gdeploy -m "Automated deployment: ${CIRCLE_SHA1} [ci skip]" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/finish-release.yml b/.github/workflows/finish-release.yml new file mode 100644 index 00000000..e1a1ab3e --- /dev/null +++ b/.github/workflows/finish-release.yml @@ -0,0 +1,53 @@ +name: Finish Release PR +permissions: + contents: write +on: + pull_request: + branches: + - main + types: + - closed + +jobs: + finish_release: + if: startsWith(github.head_ref, 'release/') && github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - name: Checkout main branch + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Git user + run: | + git config --global user.name "${GITHUB_ACTOR}" + git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com" + + - name: Get Version + id: v + run: | + echo "version=$(jq -r .version package.json)" >> $GITHUB_OUTPUT + - name: Attach a tag + run: | + git fetch + git checkout main + git tag v${{ steps.v.outputs.version }} + git push origin main + git push --tags + - name: Merge to Develop + run: | + git fetch + git checkout develop + git merge --no-ff main -m "chore: release ${{ steps.v.outputs.version }}" + git push origin develop + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ steps.v.outputs.version }} + name: Release ${{ steps.v.outputs.version }} + body: "Automated release for tag v${{ steps.v.outputs.version }}" + draft: false + prerelease: false + make_latest: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 519fbe26..8140bbda 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version: 20 - uses: actions/setup-go@v6 @@ -13,4 +13,5 @@ jobs: go-version: '1.23.9' - run: npm ci - run: npm run build + - run: npm run lint - run: npm run test diff --git a/.gitignore b/.gitignore index d535886c..84a4515f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ dist-ssr # Editor directories and files .vscode/* +!.vscode/settings.json !.vscode/extensions.json .idea .DS_Store diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..05dd7e61 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "github.vscode-pull-request-github", + "github.vscode-github-actions", + "golang.go", + "firsttris.vscode-jest-runner", + "waderyan.gitblame" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..6ccc09a6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "always" + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[go]": { + "editor.insertSpaces": false, + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": "always" + } + } +} diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..ec1ef44f --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @olibrian \ No newline at end of file diff --git a/README.md b/README.md index 0245b9f0..0a904e17 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,19 @@ # publiccode yml Editor + +This project is a fork of [italia/publiccode-editor](https://github.com/italia/publiccode-editor), customized to align with the Design System of the Swiss Confederation 🇨🇭. + +For details about the Swiss Design System, see: + +- [Design System for the Swiss Confederation (GitHub)](https://github.com/swiss/designsystem) +- [Design System for the Swiss Confederation (Stoybook)](https://swiss.github.io/designsystem/?path=/docs/get-started--docs) +- [Design System Core Library (Figma)](https://www.figma.com/design/3UYgqxmcJbG0hpWuti3y8U/🇨🇭Design-System-Core-Library) + +The application is live at: https://swiss.github.io/publiccode-editor/ + +Below is the original README for the Italian version. + +--- + ![Build Status](https://img.shields.io/circleci/project/github/italia/publiccode-editor/master.svg) ![Issues](https://img.shields.io/github/issues/italia/publiccode-editor.svg) ![License](https://img.shields.io/github/license/italia/publiccode-editor.svg) [![Join the #publiccode channel](https://img.shields.io/badge/Slack%20channel-%23publiccode-blue.svg?logo=slack)](https://developersitalia.slack.com/messages/CAM3F785T) [![Get invited](https://slack.developers.italia.it/badge.svg)](https://slack.developers.italia.it/) diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..091882a0 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,37 @@ +# RELEASE + +The release process is automated using Github Actions. This automated process mirrors the manual one currently used. + +## Branch workflow + +### Start a release + +Everytime a new release starts, you have to run the action *create-release-pr*. +It will ask if you wanna relase a patch, a minor or a major. +After that, it will: + +- Generate a new version of the package +- Create a branch with this name release/*\* starting from *develop* +- It will update the *package.json* +- It will update the *publiccode.yml* +- It will open a PR on that branch + +The PR process has the aim of giving the freedom to do manual editing. +You MUST update *CHANGELOG.md* manually before closing the PR. + +### Finishing a release + +Another Github Action, *finish-release*, is triggered when the PR is closed on *main*. + +This action will: + +- Create a tag and push it +- Merge *main* on *develop* +- Create a *release package* on Github + +It's highly recommended to edit manually the description of the release. +It will be automated in the next releases. + +## Future enhancements + +This worfklow will be changed with the adoption of more solid and more robust solutions. \ No newline at end of file diff --git a/go.work.sum b/go.work.sum index b508229e..bcf421f9 100644 --- a/go.work.sum +++ b/go.work.sum @@ -11,6 +11,8 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU github.com/italia/publiccode-parser-go v1.2.4 h1:ASdOVjgCNtlRKW+/ZrPxmguUxjhKx74vv0TKwLH3U6M= github.com/italia/publiccode-parser-go v1.2.4/go.mod h1:zYlDR8AbitTI9RzX3IRV73tqsmR0SOmhWCJDb3FpMT0= github.com/italia/publiccode-parser-go/v3 v3.0.0/go.mod h1:MXFsgghRD+t6k+08WEeRLNrlTzvPo1AqIRL2tRB4tDE= +github.com/italia/publiccode-parser-go/v5 v5.2.1 h1:9aDiCrh84nHAJzDRhf/Gx+exusfd4iQ0GCwtEwofeqo= +github.com/italia/publiccode-parser-go/v5 v5.2.1/go.mod h1:xndoanQHcweEnJlubntvOHlT/cvde0eFDF59O5PwuCg= github.com/kyoh86/go-spdx v0.0.5-0.20220421143955-2f42f2d4c410/go.mod h1:0Ndah0G/f6NZOyvjm4hUmUGUjCKRzC1qirN4LKASBkM= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= diff --git a/package-lock.json b/package-lock.json index 440e4d25..24062242 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,12 +7,13 @@ "": { "name": "publiccode-editor", "version": "2.1.1", + "hasInstallScript": true, "license": "AGPL-3.0-or-later", "dependencies": { "@uiw/react-md-editor": "^4.0.5", "accessible-autocomplete": "^3.0.1", "bootstrap": "^5.3.6", - "bootstrap-italia": "^2.14.0", + "bootstrap-switzerland": "swiss/bootstrap-switzerland#gh-pages", "copy-to-clipboard": "^3.3.3", "countries-list": "^3.0.6", "date-fns": "^4.1.0", @@ -58,6 +59,8 @@ "@typescript-eslint/parser": "^8.11.0", "@vitejs/plugin-react": "^4.3.4", "eslint": "^9.13.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.13", @@ -65,13 +68,14 @@ "globals": "^15.9.0", "jest": "^26.6.3", "node-fetch": "^3.3.2", + "prettier": "^3.6.2", "release-it": "^19.0.4", "swc-loader": "^0.2.3", "ts-node": "^10.9.1", "tsx": "^4.19.3", "typescript": "^5.5.3", "typescript-eslint": "^8.11.0", - "vite": "^6.3.6" + "vite": "^6.4.1" } }, "node_modules/@adobe/css-tools": { @@ -3290,6 +3294,19 @@ "url": "https://github.com/phun-ky/typeof?sponsor=1" } }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -5567,6 +5584,24 @@ "resolved": "https://registry.npmjs.org/bootstrap-italia/-/bootstrap-italia-2.14.0.tgz", "integrity": "sha512-df8zE55x6FIViPxVXjfggaJVoR/9DMKR37dpvgs/Y6pwc8kKU8ShPEusnr2kyy4w5pQvBeuKmsBT+dltkXy9rA==", "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@popperjs/core": "^2.11.6", + "@splidejs/splide": "^4.1.4", + "@types/bootstrap": "^5.2.6", + "animejs": "^3.2.1", + "design-tokens-italia": "^1.1.1", + "just-validate": "^4.3.0", + "minimasonry": "^1.3.2", + "progressbar.js": "^1.1.0", + "uuid": "^8.3.2", + "video.js": "^8.21.0" + } + }, + "node_modules/bootstrap-switzerland": { + "version": "2.16.2", + "resolved": "git+ssh://git@github.com/swiss/bootstrap-switzerland.git#39349792d85f647c868d255d4f3e4b586f8946a7", + "license": "BSD-3-Clause", "dependencies": { "@popperjs/core": "^2.11.6", "@splidejs/splide": "^4.1.4", @@ -7772,6 +7807,53 @@ } } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-plugin-react": { "version": "7.37.2", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz", @@ -8267,6 +8349,13 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -15039,6 +15128,35 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-format": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", @@ -17826,6 +17944,22 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -18949,9 +19083,9 @@ } }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 37622b9d..ad60bee3 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,15 @@ "prebuild": "npm run build:wasm && npm run build:licenses", "build:providers-oembed": "tsx scripts/getProvidersOembed.ts src/generated/providers-oembed.json", "build:licenses": "mkdir -p src/generated && tsx scripts/genLicenseList.ts src/generated/licenses.json", + "build:organisations": "tsx src/app/data/generateOrganisations.ts", "build:wasm": "cp \"$(go env GOROOT)/misc/wasm/wasm_exec.js\" public && GOOS=js GOARCH=wasm go build -o public/main.wasm src/wasm/main.go", "serve": "rm -rf dist; npm run build && http-server dist", + "format": "prettier --write 'src/**/*.{ts,tsx,scss,css,json}' ", "test": "jest --passWithNoTests", + "gdeploy": "gh-pages -u 'Deploy Bot ' -d dist", "deploy": "gh-pages -u 'Deploy Bot ' -d dist", "release": "release-it", + "postinstall": "npm run build:organisations", "_postinstall": "patch-package" }, "keywords": [ @@ -27,7 +31,7 @@ ], "repository": { "type": "git", - "url": "git@github.com:italia/publiccode-editor.git" + "url": "git@github.com:swiss/publiccode-editor.git" }, "jest": { "moduleNameMapper": { @@ -42,7 +46,7 @@ "@uiw/react-md-editor": "^4.0.5", "accessible-autocomplete": "^3.0.1", "bootstrap": "^5.3.6", - "bootstrap-italia": "^2.14.0", + "bootstrap-switzerland": "swiss/bootstrap-switzerland#gh-pages", "copy-to-clipboard": "^3.3.3", "countries-list": "^3.0.6", "date-fns": "^4.1.0", @@ -88,6 +92,8 @@ "@typescript-eslint/parser": "^8.11.0", "@vitejs/plugin-react": "^4.3.4", "eslint": "^9.13.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.13", @@ -95,12 +101,13 @@ "globals": "^15.9.0", "jest": "^26.6.3", "node-fetch": "^3.3.2", + "prettier": "^3.6.2", "release-it": "^19.0.4", "swc-loader": "^0.2.3", "ts-node": "^10.9.1", "tsx": "^4.19.3", "typescript": "^5.5.3", "typescript-eslint": "^8.11.0", - "vite": "^6.3.6" + "vite": "^6.4.1" } -} \ No newline at end of file +} diff --git a/public/assets/img/favicon-32x32.png b/public/assets/img/favicon-32x32.png index 56b1cac7..11f10c46 100644 Binary files a/public/assets/img/favicon-32x32.png and b/public/assets/img/favicon-32x32.png differ diff --git a/publiccode.yml b/publiccode.yml index 8d0f2302..9810d2e1 100644 --- a/publiccode.yml +++ b/publiccode.yml @@ -1,69 +1,54 @@ -publiccodeYmlVersion: "0.3" +publiccodeYmlVersion: 0.5.0 +name: publiccode editor +url: https://github.com/swiss/publiccode-editor +softwareVersion: 2.1.0 +releaseDate: 2025-07-03 +platforms: + - web categories: - - it-development + - application-development +developmentStatus: development +softwareType: standalone/web +organisation: + uri: https://ld.admin.ch/FCh + name: Bundeskanzlei description: - it: - features: - - Generazione di publiccode.yml - - Modifica di publiccode.yml - - Validazione di publiccode.yml - - Importazione di publiccode.yml tramite URL - - Possibilità di gestire diverse lingue - genericName: Generatore di publiccode.yml - longDescription: > - publiccode editor è un'applicazione web per generare file - - [`publiccode.yml`](https://github.com/italia/publiccode.yml) validi. + de-CH: + localisedName: publiccode.yml Editor + shortDescription: Ein Web-Editor zur Erstellung und Validierung von publiccode.yml-Dateien. + longDescription: >- + Der PublicCode-Editor ist eine Webanwendung zur Erstellung gültiger + publiccode.yml-Dateien. - Compilando il form, genera automaticamente un file YAML compatible con - l'ultima versione dello standard publiccode. Questo file può essere - copiato + Basierend auf den Angaben im Formular wird eine YAML-Datei generiert, die + dem neuesten PublicCode-Standard entspricht. Die Datei kann direkt kopiert + oder heruntergeladen und in ein Repository übernommen werden. - o scaricato localmente per essere poi inserito nel repository di - destinazione. - - - Può inoltre essere usato come validatore. È possible incollare o importare - - un file `publiccode.yml` esistente all'interno dell'editor. L'editor - validerà - - il documento importato e aiuterà a correggere eventuali errori. - shortDescription: Un editor web per generare e validare file publiccode.yml. + Der Editor kann auch zur Validierung bestehender publiccode.yml-Dateien + verwendet werden. + features: + - Generierung von publiccode.yml + - Modifikation von publiccode.yml + - Validierung von publiccode.yml + - Import von publiccode.yml über URL + - Mehrere Sprachen können verwaltet werden screenshots: - screenshot.png -developmentStatus: stable -it: - countryExtensionVersion: "0.2" - piattaforme: - anpr: false - cie: false - pagopa: false - spid: false - riuso: - codiceIPA: pcm legal: license: AGPL-3.0-or-later - authorsFile: AUTHORS.md +maintenance: + type: community + contacts: + - name: Olivier Brian + email: brian@puzzle.ch + - name: Claudia Asti + email: asti@puzzle.ch localisation: + localisationReady: true availableLanguages: - de - en - fr - it - nl - localisationReady: true -maintenance: - contacts: - - name: Leonardo Favario - - name: Alessandro Sebastiani - - name: Fabio Bonelli - type: community -name: publiccode editor -platforms: - - web -releaseDate: 2025-07-23 -softwareType: standalone/web -softwareVersion: 2.1.1 -url: https://github.com/italia/publiccode-editor diff --git a/screenshot.png b/screenshot.png index cff9c1be..d945dce5 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/scripts/ci/constants.js b/scripts/ci/constants.js new file mode 100644 index 00000000..1a43049f --- /dev/null +++ b/scripts/ci/constants.js @@ -0,0 +1,9 @@ +const DEFAULT_VALUE_PUBLICCODE_YML = 'publiccode.yml' +const DEFAULT_VALUE_RELEASE_DATE = 'releaseDate' +const DEFAULT_VALUE_SOFTWARE_VERSION = 'softwareVersion' + +export { + DEFAULT_VALUE_PUBLICCODE_YML, + DEFAULT_VALUE_RELEASE_DATE, + DEFAULT_VALUE_SOFTWARE_VERSION +} diff --git a/scripts/ci/updatePubliccodeReleaseDate.js b/scripts/ci/updatePubliccodeReleaseDate.js new file mode 100644 index 00000000..bcd04990 --- /dev/null +++ b/scripts/ci/updatePubliccodeReleaseDate.js @@ -0,0 +1,56 @@ +import { DEFAULT_VALUE_PUBLICCODE_YML, DEFAULT_VALUE_RELEASE_DATE } from './constants.js'; +import YamlFileProxy from "./yaml-file-proxy.js"; + +function update(yamlFileProxy, keyPath) { + const data = yamlFileProxy.read() + + const currentDate = new Date().toISOString().split('T')[0]; + + if (keyPath === DEFAULT_VALUE_RELEASE_DATE) { + data[DEFAULT_VALUE_RELEASE_DATE] = currentDate + } else { + // Navigate to the correct keyPath + const keys = keyPath.split('.'); + const keysLength = keys.length + let current = data; + + for (let i = 0; i < keysLength - 1; i++) { + current = current[keys[i]]; + } + + if (current) { + current[keys[keys.length - 1]] = currentDate; + } else { + keys.reduce((acc, key, index) => { + const isLast = index === keys.length - 1; + if (isLast) { + acc[key] = currentDate; // set the current date + } else { + acc[key] = acc[key] || {}; // Create an object if it not exists + } + return acc[key]; + }, data); + } + } + + yamlFileProxy.write(data) +} + +// Extract command line arguments +const [, , filePathArg, keyPathArg] = process.argv; + +// Asking for help? +if (filePathArg === '--help') { + console.warn('\nUsage: node updateYaml.js \n'); + console.warn(`\t: path to publiccode.yml. default value: ${DEFAULT_VALUE_PUBLICCODE_YML}`); + console.warn(`\t: path to release date property. default value: ${DEFAULT_VALUE_RELEASE_DATE}\n`); + process.exit(0); +} + +if (!filePathArg) console.warn(`filePath is not defined. using the defaultvalue: ${DEFAULT_VALUE_PUBLICCODE_YML}`); +const filePath = filePathArg ?? DEFAULT_VALUE_PUBLICCODE_YML; +if (!keyPathArg) console.warn(`keyPath is not defined. using the defaultvalue: ${DEFAULT_VALUE_RELEASE_DATE}`); +const keyPath = keyPathArg ?? DEFAULT_VALUE_RELEASE_DATE; + +update(new YamlFileProxy(filePath), keyPath); +console.log(`${filePath} updated: ${keyPath} is set with the current date`); diff --git a/scripts/ci/updatePubliccodeSoftwareVersion.js b/scripts/ci/updatePubliccodeSoftwareVersion.js new file mode 100644 index 00000000..ddbcbacf --- /dev/null +++ b/scripts/ci/updatePubliccodeSoftwareVersion.js @@ -0,0 +1,31 @@ +import pj from "../../package.json" with { type: "json" }; +import { DEFAULT_VALUE_PUBLICCODE_YML, DEFAULT_VALUE_SOFTWARE_VERSION } from "./constants.js"; +import YamlFileProxy from "./yaml-file-proxy.js"; + +function update(softwareVersion, filePath) { + const publicCodeYml = new YamlFileProxy(filePath) + + const data = publicCodeYml.read(); + + data[DEFAULT_VALUE_SOFTWARE_VERSION] = softwareVersion + + publicCodeYml.write(data) +} + +// Extract command line arguments +const [, , softwareVersionArg, filePathArg] = process.argv; + +// Asking for help? +if (softwareVersionArg === '--help') { + console.warn('\nUsage: node updatePubliccodeVersion.js \n'); + console.warn(`\t: softwareVersion value in publiccode.yml. default value is taken from package.json}`); + console.warn(`\t: path to publiccode.yml. default value: ${DEFAULT_VALUE_PUBLICCODE_YML}`); + process.exit(0); +} + +const softwareVersion = softwareVersionArg ?? pj.version; + +const filePath = filePathArg ?? DEFAULT_VALUE_PUBLICCODE_YML; + +update(softwareVersion, filePath); +console.log(`${filePath} updated: softwareVersion is set to ${softwareVersion}`); diff --git a/scripts/ci/yaml-file-proxy.js b/scripts/ci/yaml-file-proxy.js new file mode 100644 index 00000000..40d81b28 --- /dev/null +++ b/scripts/ci/yaml-file-proxy.js @@ -0,0 +1,22 @@ +import fs from "node:fs"; +import yaml from "yaml"; + +class YamlFileProxy { + #filePath; + + constructor(filePath) { + this.#filePath = filePath; + } + + read() { + const fileContents = fs.readFileSync(this.#filePath, 'utf8'); + return yaml.parse(fileContents); + } + + write(data) { + const yamlString = yaml.stringify(data); + fs.writeFileSync(this.#filePath, yamlString, 'utf8'); + } +} + +export default YamlFileProxy; \ No newline at end of file diff --git a/src/app/App.tsx b/src/app/App.tsx index b8c7ea38..3a604c3c 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,4 +1,4 @@ -import "bootstrap-italia/dist/css/bootstrap-italia.min.css"; +import "bootstrap-switzerland/dist/css/bootstrap-switzerland.min.css"; import { NotificationManager } from "design-react-kit"; import { useState } from "react"; import { useTranslation } from "react-i18next"; diff --git a/src/app/components/CloseButton.tsx b/src/app/components/CloseButton.tsx index 48b932e1..1f2effe9 100644 --- a/src/app/components/CloseButton.tsx +++ b/src/app/components/CloseButton.tsx @@ -1,15 +1,13 @@ import img_close from "../../assets/img/close.svg"; interface Props { - onClick: () => void + onClick: () => void; } -const CloseButton = ({onClick}: Props) => -
- -
+const CloseButton = ({ onClick }: Props) => ( +
+ +
+); export default CloseButton; diff --git a/src/app/components/Editor.tsx b/src/app/components/Editor.tsx index 5cf4ba21..229bcbf6 100644 --- a/src/app/components/Editor.tsx +++ b/src/app/components/Editor.tsx @@ -12,7 +12,8 @@ import useFormPersist from "react-hook-form-persist"; import { useTranslation } from "react-i18next"; import { RequiredDeep } from "type-fest"; import licenses from "../../generated/licenses.json"; -import { allLangs, displayName } from "../../i18n"; +import organisationData from "../data/organisations.json"; +import { allLangs, displayName, getLocalizedText } from "../../i18n"; import categories from "../contents/categories"; import { DEFAULT_COUNTRY_SECTIONS } from "../contents/constants"; import * as countrySection from "../contents/countrySpecificSection"; @@ -22,6 +23,7 @@ import mimeTypes from "../contents/mime-types"; import platforms from "../contents/platforms"; import PublicCode, { defaultItaly, + IT_COUNTRY_EXTENSION_VERSION, LATEST_VERSION, PublicCodeWithDeprecatedFields, } from "../contents/publiccode"; @@ -33,11 +35,12 @@ import importStandard from "../importers/standard.importer"; import { CountrySection, useCountryStore, + useITCountrySpecific, useLanguagesStore, useWarningStore, useYamlStore, } from "../lib/store"; -import { getYaml, collectRemovedKeys } from "../lib/utils"; +import { collectRemovedKeys, getYaml } from "../lib/utils"; import linter from "../linter"; import publicCodeAdapter from "../publiccode-adapter"; import { toSemVerObject } from "../semver"; @@ -49,6 +52,7 @@ import EditorContractors from "./EditorContractors"; import EditorDate from "./EditorDate"; import EditorDescriptionInput from "./EditorDescriptionInput"; import EditorFeatures from "./EditorFeatures"; +import EditorFundedBy from "./EditorFundedBy"; import EditorInput from "./EditorInput"; import EditorMultiselect from "./EditorMultiselect"; import EditorRadio from "./EditorRadio"; @@ -59,7 +63,6 @@ import EditorUsedBy from "./EditorUsedBy"; import EditorVideos from "./EditorVideos"; import PubliccodeYmlLanguages from "./PubliccodeYmlLanguages"; import { yamlLoadEventBus } from "./UploadPanel"; -// import EditorMDInput from "./EditorMDInput"; const validatorFn = async (values: PublicCode) => { try { @@ -82,9 +85,7 @@ const checkWarnings = async (values: PublicCode) => { for (const { key, description } of res?.warnings || []) { //if no key is provided, create a unique one - const warningKey = !key - ? `Generic Warning ${++counter}` - : key + const warningKey = !key ? `Generic Warning ${++counter}` : key; warnings.set(warningKey, { type: "warning", @@ -115,9 +116,7 @@ const resolver: Resolver = async ( let counter = 0; for (const { key, description } of res?.errors || []) { - const errorKey = !key - ? `GenericError${++counter}` - : key + const errorKey = !key ? `GenericError${++counter}` : key; set(errors, errorKey, { type: "error", message: description, @@ -130,10 +129,11 @@ const resolver: Resolver = async ( const defaultValues = { publiccodeYmlVersion: LATEST_VERSION, legal: {}, + organisation: {}, localisation: { availableLanguages: [] }, maintenance: { contacts: undefined, contractors: undefined }, platforms: [], - categories: [], + categories: undefined, description: {}, it: defaultItaly, }; @@ -149,7 +149,7 @@ const isNotTheSameVersion = (version1: string, version2: string) => { export default function Editor() { //#region UI - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const { countrySections } = useCountryStore(); const { resetWarnings, setWarnings } = useWarningStore(); const { @@ -164,6 +164,16 @@ export default function Editor() { const { languages, setLanguages, resetLanguages } = useLanguagesStore(); const { setCountrySections } = useCountryStore(); + const organisations = organisationData.flatMap(data => + data.organisations.map(organisation => ({ + text: getLocalizedText(organisation.name, i18n.language), + value: organisation.id, + group: getLocalizedText(data.name, i18n.language) + " (" + getLocalizedText(data.abbreviation, i18n.language) + ")", + })) + ); + + const { showCountryExtensionVersion, setShowCountryExtensionVersion } = + useITCountrySpecific(); const getNestedValue = ( obj: PublicCodeWithDeprecatedFields, path: string @@ -199,6 +209,19 @@ export default function Editor() { } = getValues() as PublicCode; return type === "internal" || type === "community"; }; + const isConformeVisible = () => { + const values = getValues() as PublicCode; + if (!values?.it?.conforme) { + return false; + } + const conforme = values.it.conforme; + return ( + conforme.lineeGuidaDesign !== undefined || + conforme.modelloInteroperabilita !== undefined || + conforme.misureMinimeSicurezza !== undefined || + conforme.gdpr !== undefined + ); + }; //#endregion //#region form definition @@ -215,6 +238,27 @@ export default function Editor() { setPubliccodeYmlVersion(publiccodeYmlVersion); }, []); + const checkItCountryExtensionVersion = useCallback( + (publicCode: PublicCode) => { + const { it } = publicCode; + if (!it) { + return; + } + + const { countryExtensionVersion } = it; + const isCountryExtensionVersionDefined = Boolean(countryExtensionVersion); + const isDifferentFromSpecificDefinedValue = Boolean( + IT_COUNTRY_EXTENSION_VERSION !== countryExtensionVersion + ); + + const countryExtensionVersionVisible = + isCountryExtensionVersionDefined && isDifferentFromSpecificDefinedValue; + + setShowCountryExtensionVersion(countryExtensionVersionVisible); + }, + [] + ); + useFormPersist("form-values", { watch, setValue, @@ -222,6 +266,7 @@ export default function Editor() { (pc: PublicCode) => { setLanguages(Object.keys(pc?.description)); checkPubliccodeYmlVersion(pc); + checkItCountryExtensionVersion(pc); }, [setLanguages] ), @@ -249,14 +294,31 @@ export default function Editor() { [setValue] ); + const updateOrganisation = useCallback( + (value: Partial) => { + const uri = value.organisation?.uri; + + if (uri) { + const organisation = organisations.find(o => o.value === uri); + setValue("organisation.name", organisation?.text); + } else { + setValue("organisation", undefined) + } + }, + [organisations, setValue] + ) + useEffect(() => { const subscription = watch((value, { name }) => { if (name === "maintenance.type") { resetMaintenance(value as PublicCode); } + if (name === "organisation.uri") { + updateOrganisation(value as PublicCode) + } }); return () => subscription.unsubscribe(); - }, [watch, resetMaintenance]); + }, [watch, resetMaintenance, updateOrganisation]); //#endregion //#region form action handlers @@ -275,27 +337,28 @@ export default function Editor() { } }, (e: FieldErrors) => { - const genericErrors = Object.entries(e) - .filter(([key]) => key.startsWith("GenericError")); + const genericErrors = Object.entries(e).filter(([key]) => + key.startsWith("GenericError") + ); - const body = genericErrors.length - ? ( + const body = genericErrors.length ? ( + {genericErrors.map(([key, value]) => ( - {(value as { message: string }).message} + + {(value as { message: string }).message} + ))} - ) - : t("editor.form.validate.error.text") - - notify( - t("editor.form.validate.error.title"), - body, - { - dismissable: true, - state: "error", - } + + ) : ( + t("editor.form.validate.error.text") ); + + notify(t("editor.form.validate.error.title"), body, { + dismissable: true, + state: "error", + }); console.error("Errors:", e); } ); @@ -334,12 +397,16 @@ export default function Editor() { reset(publicCode); } + checkItCountryExtensionVersion(publicCode); checkPubliccodeYmlVersion(publicCode); setIsPublicCodeImported(true); const res = await checkWarnings(publicCode); setWarnings( - Array.from(res.warnings).map(([key, { message }]) => ({ key, message })) + Array.from(res.warnings).map(([key, { message }]) => ({ + key, + message, + })) ); } catch (error: unknown) { notify("Import error", (error as Error).message, { @@ -351,7 +418,11 @@ export default function Editor() { const processImported = async (raw: PublicCode) => { try { - try { getValues(); } catch {} + try { + getValues(); + } catch { + console.log("getValues() error"); + } const adapted = publicCodeAdapter({ publicCode: raw as PublicCode, defaultValues: defaultValues as unknown as Partial, @@ -368,14 +439,13 @@ export default function Editor() { ))} ); - notify( - t("editor.form.validate.info.title"), - body, - { state: "info", dismissable: true } - ); + notify(t("editor.form.validate.info.title"), body, { + state: "info", + dismissable: true, + }); } await setFormDataAfterImport(async () => adapted as PublicCode); - } catch (e) { + } catch { // fall back to standard flow on any error await setFormDataAfterImport(async () => raw as PublicCode); } @@ -444,7 +514,7 @@ export default function Editor() { fieldName="applicationSuite" /> -
+
{languages @@ -453,7 +523,7 @@ export default function Editor() { className="languages" key={`publiccodeyml.description.${lang}`} > -
+
{t(`publiccodeyml.description.title`)} (in{" "} {displayName(lang, undefined, "language")})
@@ -461,14 +531,14 @@ export default function Editor() { {isDeprecatedFieldVisible( `description.${lang}.genericName` as never ) && ( - - - fieldName="genericName" - lang={lang} - deprecated - /> - - )} + + + fieldName="genericName" + lang={lang} + deprecated + /> + + )}
fieldName="localisedName" @@ -512,7 +582,7 @@ export default function Editor() {
- + fieldName="longDescription" lang={lang} required @@ -537,6 +607,16 @@ export default function Editor() { fieldName="isBasedOn" /> +
+ + fieldName="organisation.uri" + data={organisations} + filter="contains" + /> +
+
+ +
fieldName="softwareVersion" /> @@ -603,7 +683,6 @@ export default function Editor() { fieldName="categories" data={categories.map((e) => ({ text: e, value: e }))} - required filter="contains" /> @@ -671,34 +750,55 @@ export default function Editor() {

{t("countrySpecificSection.italy")}

-
-
- - fieldName="it.countryExtensionVersion" - data={[{ text: "1.0", value: "1.0" }]} - required - /> -
-
-
-
{t("publiccodeyml.it.conforme.label")}
-
-
- fieldName="it.conforme.lineeGuidaDesign" /> -
-
- fieldName="it.conforme.modelloInteroperabilita" /> + {isPublicCodeImported && showCountryExtensionVersion && ( +
+
+ + fieldName="it.countryExtensionVersion" + data={[ + { + text: IT_COUNTRY_EXTENSION_VERSION, + value: IT_COUNTRY_EXTENSION_VERSION, + }, + ]} + required + />
-
-
- fieldName="it.conforme.misureMinimeSicurezza" /> + )} + {isConformeVisible() && ( +
+
{t("publiccodeyml.it.conforme.label")}
+
+
+ + fieldName="it.conforme.lineeGuidaDesign" + deprecated + /> +
+
+ + fieldName="it.conforme.modelloInteroperabilita" + deprecated + /> +
-
- fieldName="it.conforme.gdpr" /> +
+
+ + fieldName="it.conforme.misureMinimeSicurezza" + deprecated + /> +
+
+ + fieldName="it.conforme.gdpr" + deprecated + /> +
-
+ )}
{t("publiccodeyml.it.piattaforme.label")}
@@ -723,10 +823,13 @@ export default function Editor() {
-
+
{t("publiccodeyml.it.riuso.label")}
- fieldName="it.riuso.codiceIPA" /> + + fieldName="it.riuso.codiceIPA" + deprecated + />
diff --git a/src/app/components/EditorAwards.tsx b/src/app/components/EditorAwards.tsx index 61c7b744..0558b351 100644 --- a/src/app/components/EditorAwards.tsx +++ b/src/app/components/EditorAwards.tsx @@ -55,7 +55,7 @@ export default function EditorAwards({ lang }: Props): JSX.Element { useEffect(() => { const errorsRecord = flattenObject( - errors as Record + errors as Record, ); const formFieldKeys = Object.keys(errorsRecord); const isFirstError = @@ -70,7 +70,7 @@ export default function EditorAwards({ lang }: Props): JSX.Element { return (
-
+
+
+ ); +} diff --git a/src/app/components/EditorInput.tsx b/src/app/components/EditorInput.tsx index b9748629..5cdb1c76 100644 --- a/src/app/components/EditorInput.tsx +++ b/src/app/components/EditorInput.tsx @@ -28,7 +28,7 @@ type Props = { type PublicCodeData = PublicCode | PublicCodeWithDeprecatedFields; export default function EditorInput< - T extends FieldPathByValue, string> + T extends FieldPathByValue, string>, >({ fieldName, required, textarea, deprecated }: Props) { const { control } = useFormContext(); const { @@ -48,10 +48,11 @@ export default function EditorInput< return (
-
+
- + {description}
@@ -81,7 +81,7 @@ export default function EditorInput< */} {!isValid && validationText && ( -
+
{validationText}
)} diff --git a/src/app/components/EditorMultiselect.tsx b/src/app/components/EditorMultiselect.tsx index 8177e8d1..588f8b02 100644 --- a/src/app/components/EditorMultiselect.tsx +++ b/src/app/components/EditorMultiselect.tsx @@ -25,8 +25,17 @@ type Props = { type PublicCodeData = PublicCode | PublicCodeWithDeprecatedFields; +function filterValidValues( + value: string[] | undefined, + data: Array<{ value: string; text: string }>, +): string[] { + if (!value || !Array.isArray(value)) return []; + const validValues = data.map((item) => item.value); + return value.filter((val) => validValues.includes(val)); +} + export default function EditorMultiselect< - T extends FieldPathByValue, Array> + T extends FieldPathByValue, Array>, >({ fieldName, required, data, filter, deprecated }: Props): JSX.Element { const { control } = useFormContext(); const { @@ -38,6 +47,18 @@ export default function EditorMultiselect< }); const { t } = useTranslation(); + const filteredValue = filterValidValues(value, data); + + useEffect(() => { + if ( + value && + Array.isArray(value) && + filteredValue.length !== value.length + ) { + onChange(filteredValue); + } + }, [value, filteredValue, onChange]); + const label = t(`publiccodeyml.${fieldName}.label`); const description = t(`publiccodeyml.${fieldName}.description`); const errorMessage = get(errors, `${fieldName}.message`); @@ -47,7 +68,7 @@ export default function EditorMultiselect< useEffect(() => { const errorsRecord = flattenObject( - errors as Record + errors as Record, ); const formFieldKeys = Object.keys(errorsRecord); const isFirstError = @@ -82,7 +103,7 @@ export default function EditorMultiselect< id={fieldName} onBlur={onBlur} onChange={(arr) => onChange(arr.map((e) => e.value))} - value={value} + value={filteredValue} data={data} dataKey="value" textField="text" diff --git a/src/app/components/EditorRadio.tsx b/src/app/components/EditorRadio.tsx index c24ddd49..a3d252c7 100644 --- a/src/app/components/EditorRadio.tsx +++ b/src/app/components/EditorRadio.tsx @@ -17,7 +17,7 @@ type Props = { }; export default function EditorRadio< - T extends FieldPathByValue, string> + T extends FieldPathByValue, string>, >({ fieldName, required, data }: Props): JSX.Element { const { control } = useFormContext(); const { diff --git a/src/app/components/EditorScreenshots.tsx b/src/app/components/EditorScreenshots.tsx index 36bd1d9a..27be0240 100644 --- a/src/app/components/EditorScreenshots.tsx +++ b/src/app/components/EditorScreenshots.tsx @@ -51,7 +51,7 @@ export default function EditorScreenshots({ lang }: Props): JSX.Element { return (
-
+