diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d5364a62..57edab5e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -28,10 +28,15 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 24 + node-version: 24.13.0 cache: "npm" cache-dependency-path: web/package-lock.json + - name: Pin npm version + run: | + npm install --global npm@11.6.2 + npm --version + - name: Install dependencies run: npm ci working-directory: web diff --git a/.github/workflows/lint_and_formatting.yaml b/.github/workflows/lint_and_formatting.yaml index 79074f61..2826843e 100644 --- a/.github/workflows/lint_and_formatting.yaml +++ b/.github/workflows/lint_and_formatting.yaml @@ -20,10 +20,15 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v6 with: - node-version: "24" + node-version: "24.13.0" cache: "npm" cache-dependency-path: web/package-lock.json + - name: Pin npm version + run: | + npm install --global npm@11.6.2 + npm --version + - name: Install dependencies run: npm ci working-directory: web diff --git a/web/package-lock.json b/web/package-lock.json index 694eb3dc..68462d64 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -14,7 +14,7 @@ "react-markdown": "^10.1.0" }, "devDependencies": { - "@eslint/js": "^10.0.1", + "@eslint/js": "^9.39.4", "@tailwindcss/postcss": "^4.3.0", "@types/react": "^19.2.16", "@types/react-dom": "^19.2.3", @@ -78,6 +78,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -287,33 +288,10 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -442,24 +420,16 @@ } }, "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } } }, "node_modules/@eslint/object-schema": { @@ -878,6 +848,40 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", @@ -1142,27 +1146,6 @@ "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.10.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.10.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "dev": true, @@ -1327,6 +1310,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.16.tgz", "integrity": "sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1385,6 +1369,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1570,6 +1555,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -1927,6 +1913,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -2041,19 +2028,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -4014,6 +3988,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -4101,6 +4076,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -4110,6 +4086,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -4515,6 +4492,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4771,6 +4749,7 @@ "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -4908,6 +4887,7 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/web/package.json b/web/package.json index f4e698c4..61b76ad5 100644 --- a/web/package.json +++ b/web/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "vite build", + "test": "node --test src/utils/suggestToolJson.test.js", "lint": "eslint .", "format-json:check": "prettier --check ../quality-tools", "format-json:fix": "prettier --write ../quality-tools", @@ -18,7 +19,7 @@ "react-markdown": "^10.1.0" }, "devDependencies": { - "@eslint/js": "^10.0.1", + "@eslint/js": "^9.39.4", "@tailwindcss/postcss": "^4.3.0", "@types/react": "^19.2.16", "@types/react-dom": "^19.2.3", @@ -37,4 +38,4 @@ "tailwindcss": "^4.2.4", "vite": "^8.0.14" } -} +} \ No newline at end of file diff --git a/web/src/components/SuggestToolForm.jsx b/web/src/components/SuggestToolForm.jsx index d4e26164..e054393f 100644 --- a/web/src/components/SuggestToolForm.jsx +++ b/web/src/components/SuggestToolForm.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { X, AlertCircle, CheckCircle, Copy, ExternalLink, ChevronDown } from 'lucide-react'; import { useIndicatorOptions } from '../hooks/useIndicators'; import InfoTooltip from './InfoTooltip'; +import { slugify, buildSuggestedToolJson, stringifySuggestedToolJson } from '../utils/suggestToolJson'; const APPLICATION_CATEGORIES = [ { id: 'rs:AnalysisCode', label: 'Analysis Code' }, @@ -35,14 +36,6 @@ const INITIAL_FORM = { maintainer: '', }; -function slugify(str) { - return str - .toLowerCase() - .trim() - .replace(/[^a-z0-9]+/g, '-') - .replace(/(^-|-$)/g, ''); -} - function formatDimensionLabel(dim) { return dim .split('_') @@ -50,82 +43,6 @@ function formatDimensionLabel(dim) { .join(' '); } -function buildJson(form) { - const slug = slugify(form.name); - const obj = { - '@context': 'https://w3id.org/everse/rs#', - '@id': `https://w3id.org/everse/tools/${slug}`, - '@type': 'SoftwareApplication', - }; - - // applicationCategory - if (form.applicationCategory.length === 1) { - obj.applicationCategory = { '@id': form.applicationCategory[0], '@type': '@id' }; - } else if (form.applicationCategory.length > 1) { - obj.applicationCategory = form.applicationCategory.map(id => ({ '@id': id, '@type': '@id' })); - } - - // Programming languages - const langs = form.appliesToProgrammingLanguage - .split(',') - .map(l => l.trim()) - .filter(Boolean); - if (langs.length > 0) { - obj.appliesToProgrammingLanguage = langs; - } - - // author - if (form.author.trim()) { - obj.author = form.author.trim(); - } - - obj.description = form.description; - - // hasQualityDimension - if (form.hasQualityDimension.length === 1) { - obj.hasQualityDimension = { '@id': `dim:${form.hasQualityDimension[0]}`, '@type': '@id' }; - } else if (form.hasQualityDimension.length > 1) { - obj.hasQualityDimension = form.hasQualityDimension.map(d => ({ '@id': `dim:${d}`, '@type': '@id' })); - } - - // measuresQualityIndicator - if (form.measuresQualityIndicator.length === 1) { - obj.measuresQualityIndicator = { '@id': form.measuresQualityIndicator[0], '@type': '@id' }; - } else if (form.measuresQualityIndicator.length > 1) { - obj.measuresQualityIndicator = form.measuresQualityIndicator.map(i => ({ '@id': i, '@type': '@id' })); - } - - // improvesQualityIndicator - if (form.improvesQualityIndicator.length === 1) { - obj.improvesQualityIndicator = { '@id': form.improvesQualityIndicator[0], '@type': '@id' }; - } else if (form.improvesQualityIndicator.length > 1) { - obj.improvesQualityIndicator = form.improvesQualityIndicator.map(i => ({ '@id': i, '@type': '@id' })); - } - - // howToUse - if (form.howToUse.length > 0) { - obj.howToUse = form.howToUse; - } - - obj.isAccessibleForFree = form.isAccessibleForFree; - obj.license = form.license; - - // maintainer - if (form.maintainer.trim()) { - obj.maintainer = form.maintainer.trim(); - } - - obj.name = form.name; - obj.url = form.url; - - // usedBy - if (form.usedBy.length > 0) { - obj.usedBy = form.usedBy; - } - - return obj; -} - const MultiSelectDropdown = ({ options, value, onChange, placeholder, loading, error }) => { const [isOpen, setIsOpen] = useState(false); const [search, setSearch] = useState(''); @@ -301,8 +218,8 @@ const SuggestToolForm = ({ isOpen, onClose }) => { return errs; }; - const handleCopyToClipboard = () => { - const json = JSON.stringify(buildJson(form), null, 2); + const handleCopyToClipboard = async () => { + const json = await stringifySuggestedToolJson(form); navigator.clipboard.writeText(json).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); @@ -326,7 +243,7 @@ const SuggestToolForm = ({ isOpen, onClose }) => { if (e.target === backdropRef.current) onClose(); }; - const jsonPreview = JSON.stringify(buildJson(form), null, 2); + const jsonPreview = JSON.stringify(buildSuggestedToolJson(form), null, 2); return (