diff --git a/.changeset/serious-lamps-design.md b/.changeset/serious-lamps-design.md new file mode 100644 index 000000000..a0b4f256e --- /dev/null +++ b/.changeset/serious-lamps-design.md @@ -0,0 +1,5 @@ +--- +'frontend-gelinkt-notuleren': minor +--- + +Add typescript support diff --git a/.ember-cli b/.ember-cli index 8c1812cff..978eea261 100644 --- a/.ember-cli +++ b/.ember-cli @@ -11,5 +11,5 @@ Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript rather than JavaScript by default, when a TypeScript version of a given blueprint is available. */ - "isTypeScriptProject": false + "isTypeScriptProject": true } diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index d474a40bd..000000000 --- a/.eslintignore +++ /dev/null @@ -1,25 +0,0 @@ -# unconventional js -/blueprints/*/files/ -/vendor/ - -# compiled output -/dist/ -/tmp/ - -# dependencies -/bower_components/ -/node_modules/ - -# misc -/coverage/ -!.* -.*/ -.eslintcache - -# ember-try -/.node_modules.ember-try/ -/bower.json.ember-try -/npm-shrinkwrap.json.ember-try -/package.json.ember-try -/package-lock.json.ember-try -/yarn.lock.ember-try diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 623d28815..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,71 +0,0 @@ -'use strict'; - -module.exports = { - root: true, - parser: '@babel/eslint-parser', - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - requireConfigFile: false, - babelOptions: { - plugins: [ - ['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }], - ], - }, - }, - plugins: ['ember'], - extends: [ - 'eslint:recommended', - 'plugin:ember/recommended', - 'plugin:prettier/recommended', - ], - env: { - browser: true, - }, - rules: { - 'ember/no-mixins': 'warn', - semi: 'error', // Remove this rule once Prettier is enabled - 'getter-return': ['error', { allowImplicit: true }], - }, - overrides: [ - { - files: ['**/*.gjs'], - parser: 'ember-eslint-parser', - plugins: ['ember'], - extends: [ - 'eslint:recommended', - 'plugin:ember/recommended', - 'plugin:ember/recommended-gts', - 'plugin:prettier/recommended', - ], - }, - // node files - { - files: [ - './.eslintrc.js', - './.prettierrc.js', - './.stylelintrc.js', - './.template-lintrc.js', - './ember-cli-build.js', - './testem.js', - './blueprints/*/index.js', - './config/**/*.js', - './lib/*/index.js', - './server/**/*.js', - ], - parserOptions: { - sourceType: 'script', - }, - env: { - browser: false, - node: true, - }, - extends: ['plugin:n/recommended'], - }, - { - // test files - files: ['tests/**/*-test.{js,ts}'], - extends: ['plugin:qunit/recommended'], - }, - ], -}; diff --git a/.woodpecker/.test-pr.yml b/.woodpecker/.test-pr.yml index 5b6426882..cefba3500 100644 --- a/.woodpecker/.test-pr.yml +++ b/.woodpecker/.test-pr.yml @@ -3,6 +3,11 @@ steps: image: danlynn/ember-cli:5.12.0-node_20.18 commands: - npm ci + lint-types: + image: node:20-slim + group: lint + commands: + - npm run lint:types lint-js: image: node:20-slim group: lint diff --git a/app/components/decision-copy-parts.gjs b/app/components/decision-copy-parts.gts similarity index 72% rename from app/components/decision-copy-parts.gjs rename to app/components/decision-copy-parts.gts index 43b38b853..127bead90 100644 --- a/app/components/decision-copy-parts.gjs +++ b/app/components/decision-copy-parts.gts @@ -9,9 +9,27 @@ import { trackedReset } from 'tracked-toolbox'; import AuButton from '@appuniversum/ember-appuniversum/components/au-button'; import { copyStringToClipboard } from '../utils/copy-string-to-clipboard'; import { stripHtmlForPublish } from '@lblod/ember-rdfa-editor/utils/strip-html-for-publish'; - -class DownloadButton extends Component { - @service intl; +import type IntlService from 'ember-intl/services/intl'; +type Section = { + label?: string; + selector?: string; + content?: string; + parts?: Section[]; + translatedLabel?: string; + contentSelector?: string; + callback?: (element: Element) => Element | null | undefined; + labelSelector?: string; + labelCallback?: (element: Element) => Element; +}; +interface Sig { + Element: HTMLButtonElement; + Args: { + translatedLabel?: string; + section: Section; + }; +} +class DownloadButton extends Component { + @service declare intl: IntlService; get isSuccess() { return this.copyToClipboard.last?.isSuccessful; @@ -20,11 +38,19 @@ class DownloadButton extends Component { return this.isSuccess ? 'circle-check' : undefined; } get label() { - return this.args.translatedLabel ?? this.intl.t(this.args.section.label); + if (this.args.translatedLabel) { + return this.args.translatedLabel; + } + if (this.args.section.label) { + return this.intl.t(this.args.section.label); + } + return ''; } copyToClipboard = task(async () => { - await copyStringToClipboard({ html: this.args.section.content.trim() }); + await copyStringToClipboard({ + html: this.args.section.content?.trim() ?? '', + }); }); } -const SECTIONS = [ +const SECTIONS: Section[] = [ { label: 'copy-options.section.title', selector: @@ -100,51 +126,63 @@ const SECTIONS = [ }, ]; -function htmlSafer(text) { - return htmlSafe(text); +function htmlSafer(text?: string) { + return htmlSafe(text ?? ''); } // This method of looking for query selectors is error-prone as it assumes that the document follows // the current DOM output specs. This is not necessarily true of historic or future documents. It // would be better to either use an RDFa parser that can also return the elements associated with // relations or a headless prosemirror instance. -function update(component) { +function update(component: DecisionCopyParts): Section[] | undefined { const parser = new DOMParser(); const parsed = parser.parseFromString( - stripHtmlForPublish(component.args.decision.content), + stripHtmlForPublish(component.args.decision.content ?? ''), 'text/html', ); const temporaryRenderingSpace = document.createElement('div'); - document.firstElementChild.appendChild(temporaryRenderingSpace); + const firstChild = document.firstElementChild; + if (!firstChild) { + return; + } + firstChild.appendChild(temporaryRenderingSpace); const mappedSections = SECTIONS.flatMap( - ({ label, selector, parts, callback = (a) => a }) => { - const elements = Array.from(parsed.querySelectorAll(selector)); + ({ label, selector, parts, callback = (a: Element) => a }) => { + const elements = selector + ? Array.from(parsed.querySelectorAll(selector)) + : []; return elements.map((element) => { const contentElement = callback(element); // Note, it's important to generate the content here as with the use of DOM apis in the // callbacks, it's easy to accidentally mutate `contentElement`. For example when appending // parts of the content to a 'container' element. - const contentHtml = contentElement.outerHTML; - let foundParts = []; + const contentHtml = contentElement?.outerHTML; + const foundParts: Section[] = []; if (parts) { - for (let partType of parts) { + for (const partType of parts) { const partCb = partType.callback || ((a) => a); - const partElements = - contentElement.querySelectorAll(partType.selector) ?? []; + const partElements = partType.selector + ? contentElement?.querySelectorAll(partType.selector) ?? [] + : []; partElements.forEach((part) => { const partElement = partCb(part); - const partLabel = partType.labelCallback - ? partType.labelCallback(part) - : partElement.querySelector(partType.labelSelector); + + let partLabel; + if (partType.labelCallback) { + partLabel = partType.labelCallback(part); + } else if (partType.labelSelector) { + partLabel = partElement?.querySelector(partType.labelSelector); + } const partContent = partType.contentSelector - ? partElement.querySelector(partType.contentSelector)?.outerHTML - : partElement.outerHTML; + ? partElement?.querySelector(partType.contentSelector) + ?.outerHTML + : partElement?.outerHTML; if (partLabel && partContent) { // Put the element into the DOM so that `innerText` can know which parts of the // content are human readable in `innerText` temporaryRenderingSpace.replaceChildren(partLabel); foundParts.push({ - translatedLabel: partLabel.innerText, + translatedLabel: (partLabel as HTMLElement).innerText, content: partContent, }); } @@ -164,12 +202,20 @@ function update(component) { return mappedSections; } -export default class DecisionCopyParts extends Component { +interface DecisionCopyPartsSig { + Args: { + decision: Section; + }; +} +export default class DecisionCopyParts extends Component { + @service declare intl: IntlService; @trackedReset({ memo: 'decision.content', update, }) sections = update(this); + sectionLabel = (section: Section) => + section.label ? this.intl.t(section.label) : '';