From abf999946144118cbad7643f9bc5c02a0d14c437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thea=20Sch=C3=B6bl?= Date: Wed, 3 Apr 2024 12:16:18 +0200 Subject: [PATCH] feat: type-safe sc-icons --- frontend/app/ICONS.md | 4 +- .../app/{icons.config.ts => icons.config.mjs} | 8 +-- frontend/app/package.json | 10 ++- ...te-licenses.ts => accumulate-licenses.mjs} | 23 ++++--- ...rectness.ts => check-icon-correctness.mjs} | 39 +++++------ ...er-used-icons.ts => gather-used-icons.mjs} | 25 ++++--- frontend/app/scripts/get-code-points.mjs | 23 +++++++ .../{icon-config.ts => icon-config.d.mts} | 1 - ...nify-icon-font.ts => minify-icon-font.mjs} | 48 ++++--------- .../about/about-page/about-page-content.html | 2 +- .../modules/config/config.provider.spec.ts | 2 +- .../data/add-event-action-chip.component.ts | 3 +- .../data/add-event-action-chip.config.ts | 8 +-- .../src/app/modules/data/data-icon.config.ts | 67 ++++++++++--------- .../src/app/modules/data/data-icon.pipe.ts | 2 +- .../data/elements/title-card.component.ts | 7 +- .../modules/menu/navigation/navigation.html | 4 +- .../menu/navigation/tabs.template.html | 2 +- .../profile/page/profile-page-section.html | 2 +- .../src/app/util/ion-icon/icon-match.d.mts | 5 ++ .../{icon-match.ts => icon-match.mjs} | 15 +++-- .../src/app/util/ion-icon/icon-match.spec.ts | 31 ++++++++- frontend/app/src/app/util/ion-icon/icon.ts | 11 ++- .../ion-icon/ion-back-button.directive.ts | 4 +- .../util/ion-icon/ion-breadcrumb.directive.ts | 4 +- .../app/util/ion-icon/ion-icon.directive.ts | 3 +- .../util/ion-icon/ion-reorder.directive.ts | 2 +- .../util/ion-icon/ion-searchbar.directive.ts | 4 +- .../app/src/config/profile-page-sections.ts | 24 +++---- 29 files changed, 211 insertions(+), 172 deletions(-) rename frontend/app/{icons.config.ts => icons.config.mjs} (88%) rename frontend/app/scripts/{accumulate-licenses.ts => accumulate-licenses.mjs} (66%) rename frontend/app/scripts/{check-icon-correctness.ts => check-icon-correctness.mjs} (63%) rename frontend/app/scripts/{gather-used-icons.ts => gather-used-icons.mjs} (64%) create mode 100644 frontend/app/scripts/get-code-points.mjs rename frontend/app/scripts/{icon-config.ts => icon-config.d.mts} (95%) rename frontend/app/scripts/{minify-icon-font.ts => minify-icon-font.mjs} (68%) create mode 100644 frontend/app/src/app/util/ion-icon/icon-match.d.mts rename frontend/app/src/app/util/ion-icon/{icon-match.ts => icon-match.mjs} (73%) diff --git a/frontend/app/ICONS.md b/frontend/app/ICONS.md index f55bda34..97b457c0 100644 --- a/frontend/app/ICONS.md +++ b/frontend/app/ICONS.md @@ -50,11 +50,11 @@ the config file. Icon font minification is done automatically, but requires you to follow a few simple rules: -1. Use the tagged template literal for referencing icon names in +1. Use the type-safe proxy for referencing icon names in TypeScript files and code ```ts -SCIcon`icon_name`; +SCIcon.icon_name; ``` 2. When using `ion-icon` in HTML, reference either icons that went through diff --git a/frontend/app/icons.config.ts b/frontend/app/icons.config.mjs similarity index 88% rename from frontend/app/icons.config.ts rename to frontend/app/icons.config.mjs index cf38cecd..d44b9fe8 100644 --- a/frontend/app/icons.config.ts +++ b/frontend/app/icons.config.mjs @@ -12,9 +12,9 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import type {IconConfig} from './scripts/icon-config'; -const config: IconConfig = { +/** @type {import('./scripts/icon-config').IconConfig} */ +const config = { inputPath: 'node_modules/material-symbols/material-symbols-rounded.woff2', outputPath: 'src/assets/icons.min.woff2', htmlGlob: 'src/**/*.html', @@ -38,10 +38,6 @@ const config: IconConfig = { 'work', ], }, - codePoints: { - ios_share: 'e6b8', - fact_check: 'f0c5', - }, }; export default config; diff --git a/frontend/app/package.json b/frontend/app/package.json index 77928523..f21738ac 100644 --- a/frontend/app/package.json +++ b/frontend/app/package.json @@ -21,7 +21,7 @@ "build:prod": "ng build --configuration=production", "build:stats": "ng build --configuration=production --stats-json", "changelog": "conventional-changelog -p angular -i src/assets/about/CHANGELOG.md -s -r 0", - "check-icons": "ts-node scripts/check-icon-correctness.ts", + "check-icons": "node scripts/check-icon-correctness.mjs", "chromium:no-cors": "chromium --disable-web-security --user-data-dir=\".browser-data/chromium\"", "chromium:virtual-host": "chromium --host-resolver-rules=\"MAP mobile.app.uni-frankfurt.de:* localhost:8100\" --ignore-certificate-errors", "cypress:open": "cypress open", @@ -35,10 +35,16 @@ "e2e": "ng e2e", "format": "prettier . -c", "format:fix": "prettier --write .", - "licenses": "license-checker --json > src/assets/about/licenses.json && ts-node ./scripts/accumulate-licenses.ts && git add src/assets/about/licenses.json", + "licenses": "license-checker --json > src/assets/about/licenses.json && node ./scripts/accumulate-licenses.mjs && git add src/assets/about/licenses.json", "lint": "ng lint && stylelint \"**/*.scss\"", "lint:fix": "eslint --fix -c .eslintrc.json --ignore-path .eslintignore --ext .ts,.html src/ && stylelint --fix \"**/*.scss\"", +<<<<<<< HEAD "minify-icons": "ts-node-esm scripts/minify-icon-font.ts", +||||||| parent of 7b431b8f (feat: type-safe sc-icons) + "minify-icons": "ts-node scripts/minify-icon-font.ts", +======= + "minify-icons": "node scripts/minify-icon-font.mjs", +>>>>>>> 7b431b8f (feat: type-safe sc-icons) "postinstall": "jetify && echo \"skipping jetify in production mode\"", "preview": "http-server www --p 8101 -o", "push": "git push && git push origin \"v$npm_package_version\"", diff --git a/frontend/app/scripts/accumulate-licenses.ts b/frontend/app/scripts/accumulate-licenses.mjs similarity index 66% rename from frontend/app/scripts/accumulate-licenses.ts rename to frontend/app/scripts/accumulate-licenses.mjs index 2f796328..4569d91b 100644 --- a/frontend/app/scripts/accumulate-licenses.ts +++ b/frontend/app/scripts/accumulate-licenses.mjs @@ -12,33 +12,34 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import fs from 'fs'; -import {omit} from '../src/app/_helpers/collections/omit'; -import {pickBy} from '../src/app/_helpers/collections/pick'; +import {readFileSync, writeFileSync} from 'fs'; +import {omit, pickBy} from '@openstapps/collection-utils'; /** * accumulate and transform licenses based on two license files + * @param {string} path + * @param {string} additionalLicensesPath */ -function accumulateFile(path: string, additionalLicensesPath: string) { - const packageJson = JSON.parse(fs.readFileSync('./package.json').toString()); +function accumulateFile(path, additionalLicensesPath) { + const packageJson = JSON.parse(readFileSync('./package.json').toString()); const dependencies = packageJson.dependencies; console.log(`Accumulating licenses from ${path}`); - fs.writeFileSync( + writeFileSync( path, JSON.stringify( // eslint-disable-next-line @typescript-eslint/no-explicit-any - Object.entries({ - ...pickBy(JSON.parse(fs.readFileSync(path).toString()), (_, key: string) => { - const parts = key.split('@'); + Object.entries({ + ...pickBy(JSON.parse(readFileSync(path).toString()), (_, key) => { + const parts = /** @type {string} */ (key).split('@'); return dependencies[parts.slice(0, -1).join('@')] === parts[parts.length - 1]; }), - ...JSON.parse(fs.readFileSync(additionalLicensesPath).toString()), + ...JSON.parse(readFileSync(additionalLicensesPath).toString()), }) .map(([key, value]) => ({ - licenseText: value.licenseFile && fs.readFileSync(value.licenseFile, 'utf8'), + licenseText: value.licenseFile && readFileSync(value.licenseFile, 'utf8'), name: key, ...omit(value, 'licenseFile', 'path'), })) diff --git a/frontend/app/scripts/check-icon-correctness.ts b/frontend/app/scripts/check-icon-correctness.mjs similarity index 63% rename from frontend/app/scripts/check-icon-correctness.ts rename to frontend/app/scripts/check-icon-correctness.mjs index 42c06c16..294bf9d8 100644 --- a/frontend/app/scripts/check-icon-correctness.ts +++ b/frontend/app/scripts/check-icon-correctness.mjs @@ -12,18 +12,20 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import fontkit, {Font} from 'fontkit'; -import config from '../icons.config'; +import {openSync} from 'fontkit'; +import config from '../icons.config.mjs'; import {existsSync} from 'fs'; -import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons'; +import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons.mjs'; +import {fetchCodePointMap} from './get-code-points.mjs'; const commandName = '"npm run minify-icons"'; -const originalFont = fontkit.openSync(config.inputPath); if (!existsSync(config.outputPath)) { console.error(`Minified font not found. Run ${commandName} first.`); process.exit(-1); } -const modifiedFont = fontkit.openSync(config.outputPath); + +/** @type {import('fontkit').Font} */ +const modifiedFont = openSync(config.outputPath); let success = true; @@ -48,25 +50,16 @@ async function checkAll() { } /** - * + * @param {Record} icons */ -function check(icons: Record) { - for (const [purpose, iconSet] of Object.entries(icons)) { - for (const icon of iconSet) { - if (!hasIcon(originalFont, icon)) { - success = false; - console.error(`${purpose}: ${icon} does not exist. Typo?`); - } else if (!hasIcon(modifiedFont, icon)) { - success = false; - console.error(`${purpose}: ${icon} not found in minified font. Run ${commandName} to regenerate it.`); - } +async function check(icons) { + const codePoints = await fetchCodePointMap(); + + for (const icon of Object.values(icons).flat()) { + const codePoint = codePoints.get(icon); + if (!codePoint) throw new Error(`"${icon}" is not a valid icon`); + if (!modifiedFont.getGlyph(Number.parseInt(codePoint, 16))) { + throw new Error(`"${icon}" (code point ${codePoint}) is missing`); } } } - -/** - * - */ -function hasIcon(font: Font, icon: string) { - return font.layout(icon).glyphs.some(it => it.isLigature); -} diff --git a/frontend/app/scripts/gather-used-icons.ts b/frontend/app/scripts/gather-used-icons.mjs similarity index 64% rename from frontend/app/scripts/gather-used-icons.ts rename to frontend/app/scripts/gather-used-icons.mjs index bfd2f6e0..585a39c4 100644 --- a/frontend/app/scripts/gather-used-icons.ts +++ b/frontend/app/scripts/gather-used-icons.mjs @@ -14,34 +14,39 @@ */ import {glob} from 'glob'; import {readFileSync} from 'fs'; -import {matchPropertyContent, matchTagProperties} from '../src/app/util/ion-icon/icon-match'; +import { + matchPropertyAccess, + matchPropertyContent, + matchTagProperties, +} from '../src/app/util/ion-icon/icon-match.mjs'; /** - * + * @returns {Promise>} */ -export async function getUsedIconsHtml(pattern = 'src/**/*.html'): Promise> { +export async function getUsedIconsHtml(pattern = 'src/**/*.html') { return Object.fromEntries( (await glob(pattern)) .map(file => [ file, - (readFileSync(file, 'utf8') + readFileSync(file, 'utf8') .match(matchTagProperties('ion-icon')) ?.flatMap(match => { return match.match(matchPropertyContent(['name', 'md', 'ios'])); }) - .filter(it => !!it) as string[]) || [], + .filter(it => !!it) || [], ]) - .filter(([, values]) => values.length > 0), + .filter(([, values]) => values && values.length > 0), ); } /** - * + * @returns {Promise>} */ -export async function getUsedIconsTS(pattern = 'src/**/*.ts'): Promise> { +export async function getUsedIconsTS(pattern = 'src/**/*.ts') { + const regex = matchPropertyAccess('SCIcon'); return Object.fromEntries( (await glob(pattern)) - .map(file => [file, readFileSync(file, 'utf8').match(/(?<=Icon`)[\w-]+(?=`)/g) || []]) - .filter(([, values]) => values.length > 0), + .map(file => [file, readFileSync(file, 'utf8').match(regex) || []]) + .filter(([, values]) => values && values.length > 0), ); } diff --git a/frontend/app/scripts/get-code-points.mjs b/frontend/app/scripts/get-code-points.mjs new file mode 100644 index 00000000..377fe01e --- /dev/null +++ b/frontend/app/scripts/get-code-points.mjs @@ -0,0 +1,23 @@ +const url = + 'https://raw.githubusercontent.com/google/material-design-icons/master/' + + 'variablefont/MaterialSymbolsRounded%5BFILL%2CGRAD%2Copsz%2Cwght%5D.codepoints'; + +export async function fetchCodePointMap() { + const icons = await fetch(url) + .then(it => it.text()) + .then(it => new Map(it.split('\n').map(it => /** @type {[string, string]} */ (it.split(' '))))); + if (icons.size < 100) throw new Error(`Code point map is very small, is the URL incorrect? ${url}`); + return icons; +} + +/** + * @param {string[]} icons + */ +export async function getCodePoints(icons) { + const codePoints = await fetchCodePointMap(); + return icons.map(icon => { + const code = codePoints.get(icon); + if (!code) throw new Error(`Code point for icon ${icon} not found`); + return code; + }); +} diff --git a/frontend/app/scripts/icon-config.ts b/frontend/app/scripts/icon-config.d.mts similarity index 95% rename from frontend/app/scripts/icon-config.ts rename to frontend/app/scripts/icon-config.d.mts index 54e65d78..c02642e7 100644 --- a/frontend/app/scripts/icon-config.ts +++ b/frontend/app/scripts/icon-config.d.mts @@ -19,5 +19,4 @@ export interface IconConfig { inputPath: string; outputPath: string; additionalIcons?: {[purpose: string]: string[]}; - codePoints?: {[name: string]: string}; } diff --git a/frontend/app/scripts/minify-icon-font.ts b/frontend/app/scripts/minify-icon-font.mjs similarity index 68% rename from frontend/app/scripts/minify-icon-font.ts rename to frontend/app/scripts/minify-icon-font.mjs index d2626681..d71cca9e 100644 --- a/frontend/app/scripts/minify-icon-font.ts +++ b/frontend/app/scripts/minify-icon-font.mjs @@ -14,16 +14,17 @@ */ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import fontkit from 'fontkit'; import {exec} from 'child_process'; -import config from '../icons.config'; +import config from '../icons.config.mjs'; import {statSync} from 'fs'; -import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons'; +import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons.mjs'; +import {getCodePoints} from './get-code-points.mjs'; /** - * + * @param {string[] | string} command + * @returns {Promise} */ -async function run(command: string[] | string): Promise { +async function run(command) { const fullCommand = Array.isArray(command) ? command.join(' ') : command; console.log(`>> ${fullCommand}`); @@ -44,7 +45,8 @@ async function run(command: string[] | string): Promise { * */ async function minifyIconFont() { - const icons = new Set(); + /** @type {Set} */ + const icons = new Set(); for (const iconSet of [ ...Object.values(config.additionalIcons || []), @@ -56,35 +58,7 @@ async function minifyIconFont() { } } - console.log('Icons used:', [...icons.values()].sort()); - const font = fontkit.openSync(config.inputPath); - - const glyphs: string[] = ['5f-7a', '30-39']; - for (const icon of icons) { - const iconGlyphs = font.layout(icon).glyphs; - if (iconGlyphs.length === 0) { - console.error(`${icon} not found in font. Typo?`); - process.exit(-1); - } - - const codePoints = iconGlyphs - .flatMap(it => font.stringsForGlyph(it.id)) - .flatMap(it => [...it]) - .map(it => it.codePointAt(0)!.toString(16)); - - if (codePoints.length === 0) { - if (config.codePoints?.[icon]) { - glyphs.push(config.codePoints[icon]); - } else { - console.log(); - console.error(`${icon} code point could not be determined. Add it to config.codePoints.`); - process.exit(-1); - } - } - - glyphs.push(...codePoints); - } - glyphs.sort(); + const glyphs = ['5f-7a', '30-39', ...(await getCodePoints([...icons]))].sort(); console.log( await run([ @@ -114,8 +88,10 @@ minifyIconFont(); /** * Bytes to respective units + * @param {number} value + * @returns {string} */ -function toByteUnit(value: number): string { +function toByteUnit(value) { if (value < 1024) { return `${value}B`; } else if (value < 1024 * 1024) { diff --git a/frontend/app/src/app/modules/about/about-page/about-page-content.html b/frontend/app/src/app/modules/about/about-page/about-page-content.html index 269c16d0..e07b328b 100644 --- a/frontend/app/src/app/modules/about/about-page/about-page-content.html +++ b/frontend/app/src/app/modules/about/about-page/about-page-content.html @@ -49,7 +49,7 @@ @if (content.type === 'router link') { @if (content.icon) { - + } {{ 'title' | translateSimple: content }} diff --git a/frontend/app/src/app/modules/config/config.provider.spec.ts b/frontend/app/src/app/modules/config/config.provider.spec.ts index f159cc51..93555478 100644 --- a/frontend/app/src/app/modules/config/config.provider.spec.ts +++ b/frontend/app/src/app/modules/config/config.provider.spec.ts @@ -106,7 +106,7 @@ describe('ConfigProvider', () => { storageProviderSpy.has.and.returnValue(Promise.resolve(true)); const wrongConfig = structuredClone(sampleIndexResponse); wrongConfig.backend.SCVersion = '0.1.0'; - storageProviderSpy.get.and.returnValue(wrongConfig); + storageProviderSpy.get.and.returnValue(Promise.resolve(wrongConfig)); spyOn(configProvider.client, 'handshake').and.returnValue(Promise.resolve(sampleIndexResponse)); await configProvider.init(); diff --git a/frontend/app/src/app/modules/data/chips/data/add-event-action-chip.component.ts b/frontend/app/src/app/modules/data/chips/data/add-event-action-chip.component.ts index aa962081..2460160a 100644 --- a/frontend/app/src/app/modules/data/chips/data/add-event-action-chip.component.ts +++ b/frontend/app/src/app/modules/data/chips/data/add-event-action-chip.component.ts @@ -26,6 +26,7 @@ import {AddEventStates, AddEventStatesMap} from './add-event-action-chip.config' import {EditEventSelectionComponent} from '../edit-event-selection.component'; import {AddEventReviewModalComponent} from '../../../calendar/add-event-review-modal.component'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; +import {MaterialSymbol} from 'material-symbols'; /** * Shows a horizontal list of action chips @@ -55,7 +56,7 @@ export class AddEventActionChipComponent { /** * Icon */ - icon: string; + icon: MaterialSymbol; /** * Current state of icon fill diff --git a/frontend/app/src/app/modules/data/chips/data/add-event-action-chip.config.ts b/frontend/app/src/app/modules/data/chips/data/add-event-action-chip.config.ts index 8d7b0330..ba2a2362 100644 --- a/frontend/app/src/app/modules/data/chips/data/add-event-action-chip.config.ts +++ b/frontend/app/src/app/modules/data/chips/data/add-event-action-chip.config.ts @@ -24,28 +24,28 @@ export enum AddEventStates { export const AddEventStatesMap = { [AddEventStates.ADDED_ALL]: { - icon: SCIcon`event_available`, + icon: SCIcon.event_available, fill: true, label: 'data.chips.add_events.ADDED_ALL', disabled: false, color: 'success', }, [AddEventStates.ADDED_SOME]: { - icon: SCIcon`event`, + icon: SCIcon.event, fill: true, label: 'data.chips.add_events.ADDED_SOME', disabled: false, color: 'success', }, [AddEventStates.REMOVED_ALL]: { - icon: SCIcon`calendar_today`, + icon: SCIcon.calendar_today, fill: false, label: 'data.chips.add_events.REMOVED_ALL', disabled: false, color: 'primary', }, [AddEventStates.UNAVAILABLE]: { - icon: SCIcon`event_busy`, + icon: SCIcon.event_busy, fill: false, label: 'data.chips.add_events.UNAVAILABLE', disabled: true, diff --git a/frontend/app/src/app/modules/data/data-icon.config.ts b/frontend/app/src/app/modules/data/data-icon.config.ts index 774c1db9..5cef5965 100644 --- a/frontend/app/src/app/modules/data/data-icon.config.ts +++ b/frontend/app/src/app/modules/data/data-icon.config.ts @@ -14,37 +14,38 @@ */ import {SCThingType} from '@openstapps/core'; import {SCIcon} from '../../util/ion-icon/icon'; +import {MaterialSymbol} from 'material-symbols'; -export const DataIcons: Record = { - 'academic event': SCIcon`school`, - 'assessment': SCIcon`fact_check`, - 'article': SCIcon`article`, - 'book': SCIcon`book`, - 'building': SCIcon`location_city`, - 'certification': SCIcon`contract`, - 'catalog': SCIcon`inventory_2`, - 'contact point': SCIcon`contact_page`, - 'course of study': SCIcon`school`, - 'date series': SCIcon`event`, - 'dish': SCIcon`lunch_dining`, - 'favorite': SCIcon`favorite`, - 'floor': SCIcon`foundation`, - 'id card': SCIcon`badge`, - 'message': SCIcon`newspaper`, - 'organization': SCIcon`business_center`, - 'periodical': SCIcon`feed`, - 'person': SCIcon`person`, - 'point of interest': SCIcon`pin_drop`, - 'publication event': SCIcon`campaign`, - 'room': SCIcon`meeting_room`, - 'semester': SCIcon`date_range`, - 'setting': SCIcon`settings`, - 'sport course': SCIcon`sports_soccer`, - 'study module': SCIcon`view_module`, - 'ticket': SCIcon`confirmation_number`, - 'todo': SCIcon`task`, - 'tour': SCIcon`tour`, - 'video': SCIcon`movie`, - 'diff': SCIcon`difference`, - 'job posting': SCIcon`work`, -}; +export const DataIcons = { + 'academic event': SCIcon.school, + 'assessment': SCIcon.fact_check, + 'article': SCIcon.article, + 'book': SCIcon.book, + 'building': SCIcon.location_city, + 'certification': SCIcon.contract, + 'catalog': SCIcon.inventory_2, + 'contact point': SCIcon.contact_page, + 'course of study': SCIcon.school, + 'date series': SCIcon.event, + 'dish': SCIcon.lunch_dining, + 'favorite': SCIcon.favorite, + 'floor': SCIcon.foundation, + 'id card': SCIcon.badge, + 'message': SCIcon.newspaper, + 'organization': SCIcon.business_center, + 'periodical': SCIcon.feed, + 'person': SCIcon.person, + 'point of interest': SCIcon.pin_drop, + 'publication event': SCIcon.campaign, + 'room': SCIcon.meeting_room, + 'semester': SCIcon.date_range, + 'setting': SCIcon.settings, + 'sport course': SCIcon.sports_soccer, + 'study module': SCIcon.view_module, + 'ticket': SCIcon.confirmation_number, + 'todo': SCIcon.task, + 'tour': SCIcon.tour, + 'video': SCIcon.movie, + 'diff': SCIcon.difference, + 'job posting': SCIcon.work, +} satisfies Record; diff --git a/frontend/app/src/app/modules/data/data-icon.pipe.ts b/frontend/app/src/app/modules/data/data-icon.pipe.ts index d33deb3c..084ac43a 100644 --- a/frontend/app/src/app/modules/data/data-icon.pipe.ts +++ b/frontend/app/src/app/modules/data/data-icon.pipe.ts @@ -31,7 +31,7 @@ export class DataIconPipe implements PipeTransform { /** * Provide the icon name from the data type */ - transform(type: SCThingType): string { + transform(type: SCThingType) { return this.typeIconMap[type]; } } diff --git a/frontend/app/src/app/modules/data/elements/title-card.component.ts b/frontend/app/src/app/modules/data/elements/title-card.component.ts index 9c18fc9e..df529895 100644 --- a/frontend/app/src/app/modules/data/elements/title-card.component.ts +++ b/frontend/app/src/app/modules/data/elements/title-card.component.ts @@ -18,8 +18,8 @@ import {SCThings} from '@openstapps/core'; import {SCIcon} from '../../../util/ion-icon/icon'; const AccordionButtonState = { - collapsed: SCIcon`expand_more`, - expanded: SCIcon`expand_less`, + collapsed: SCIcon.expand_more, + expanded: SCIcon.expand_less, }; @Component({ @@ -35,7 +35,8 @@ export class TitleCardComponent implements OnInit, OnChanges { @ViewChild('accordionTextArea') accordionTextArea: ElementRef; - buttonState = AccordionButtonState.collapsed; + buttonState: (typeof AccordionButtonState)[keyof typeof AccordionButtonState] = + AccordionButtonState.collapsed; buttonShown = true; diff --git a/frontend/app/src/app/modules/menu/navigation/navigation.html b/frontend/app/src/app/modules/menu/navigation/navigation.html index 707329c7..fb223962 100644 --- a/frontend/app/src/app/modules/menu/navigation/navigation.html +++ b/frontend/app/src/app/modules/menu/navigation/navigation.html @@ -35,13 +35,13 @@ lines="none" class="menu-category" > - + {{ category.translations[language]?.title | titlecase }} } @for (item of category.items; track item) { - + {{ item.translations[language]?.title | titlecase }} } diff --git a/frontend/app/src/app/modules/menu/navigation/tabs.template.html b/frontend/app/src/app/modules/menu/navigation/tabs.template.html index bd2c929a..56e47cc6 100644 --- a/frontend/app/src/app/modules/menu/navigation/tabs.template.html +++ b/frontend/app/src/app/modules/menu/navigation/tabs.template.html @@ -41,7 +41,7 @@ @for (category of menu; track category; let isFirst = $first) { - + {{ category.translations[language]?.title | titlecase }} } diff --git a/frontend/app/src/app/modules/profile/page/profile-page-section.html b/frontend/app/src/app/modules/profile/page/profile-page-section.html index 74c2dda6..308b92ad 100644 --- a/frontend/app/src/app/modules/profile/page/profile-page-section.html +++ b/frontend/app/src/app/modules/profile/page/profile-page-section.html @@ -35,7 +35,7 @@ >
@if (link.icon) { - + } {{ 'name' | translateSimple: link }}
diff --git a/frontend/app/src/app/util/ion-icon/icon-match.d.mts b/frontend/app/src/app/util/ion-icon/icon-match.d.mts new file mode 100644 index 00000000..60a8dc5b --- /dev/null +++ b/frontend/app/src/app/util/ion-icon/icon-match.d.mts @@ -0,0 +1,5 @@ +export function matchTagProperties(tag: string): RegExp; + +export function matchPropertyContent(properties: string[]): RegExp; + +export function matchPropertyAccess(objectName: string): RegExp; diff --git a/frontend/app/src/app/util/ion-icon/icon-match.ts b/frontend/app/src/app/util/ion-icon/icon-match.mjs similarity index 73% rename from frontend/app/src/app/util/ion-icon/icon-match.ts rename to frontend/app/src/app/util/ion-icon/icon-match.mjs index be9d985c..2697d35c 100644 --- a/frontend/app/src/app/util/ion-icon/icon-match.ts +++ b/frontend/app/src/app/util/ion-icon/icon-match.mjs @@ -14,17 +14,24 @@ */ /** - * + * @param {string} tag */ -export function matchTagProperties(tag: string) { +export function matchTagProperties(tag) { return new RegExp(`(?<=<${tag})[\\s\\S]*?(?=>\\s*<\\/${tag}\\s*>)`, 'g'); } /** - * + * @param {string[]} properties */ -export function matchPropertyContent(properties: string[]) { +export function matchPropertyContent(properties) { const names = properties.join('|'); return new RegExp(`((?<=(${names})=")[\\w-]+(?="))|((?<=\\[(${names})]="')[\\w-]+(?='"))`, 'g'); } + +/** + * @param {string} objectName + */ +export function matchPropertyAccess(objectName) { + return new RegExp(`(?<=${objectName}\\s*\\.\\s*)\\w+`, 'g'); +} diff --git a/frontend/app/src/app/util/ion-icon/icon-match.spec.ts b/frontend/app/src/app/util/ion-icon/icon-match.spec.ts index 088d0676..6b594c5a 100644 --- a/frontend/app/src/app/util/ion-icon/icon-match.spec.ts +++ b/frontend/app/src/app/util/ion-icon/icon-match.spec.ts @@ -12,9 +12,7 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -/* eslint-disable unicorn/no-null */ - -import {matchPropertyContent, matchTagProperties} from './icon-match'; +import {matchPropertyAccess, matchPropertyContent, matchTagProperties} from './icon-match.mjs'; describe('matchTagProperties', function () { const regex = matchTagProperties('test'); @@ -59,3 +57,30 @@ describe('matchPropertyContent', function () { expect(`no="content" [no]="'content'"`.match(regex)).toEqual(null); }); }); + +describe('matchPropertyAccess', function () { + const property = '0_20a_boninAo0_'; + const object = 'test'; + const regex = matchPropertyAccess(object); + + it('should match property access', function () { + expect(`${object}.${property}`.match(regex)).toEqual([property]); + }); + + it('should respect whitespace', function () { + expect(`${object}. ${property}`.match(regex)).toEqual([property]); + expect(`${object} .${property}`.match(regex)).toEqual([property]); + expect(`${object} . ${property}`.match(regex)).toEqual([property]); + expect(`${object} \n . \n ${property}`.match(regex)).toEqual([property]); + }); + + it('should not include invalid trailing stuff', function () { + expect(`${object}.${property}!`.match(regex)).toEqual([property]); + expect(`${object}.${property}.`.match(regex)).toEqual([property]); + expect(`${object}.${property}-`.match(regex)).toEqual([property]); + expect(`${object}.${property}]`.match(regex)).toEqual([property]); + expect(`${object}.${property}}`.match(regex)).toEqual([property]); + expect(`${object}.${property};`.match(regex)).toEqual([property]); + expect(`${object}.${property}:`.match(regex)).toEqual([property]); + }); +}); diff --git a/frontend/app/src/app/util/ion-icon/icon.ts b/frontend/app/src/app/util/ion-icon/icon.ts index d083989e..3da2fd38 100644 --- a/frontend/app/src/app/util/ion-icon/icon.ts +++ b/frontend/app/src/app/util/ion-icon/icon.ts @@ -13,9 +13,8 @@ * this program. If not, see . */ -/** - * A noop function to aid parsing icon names - */ -export function SCIcon(strings: TemplateStringsArray, ..._keys: string[]): string { - return strings.join(''); -} +import {MaterialSymbol} from 'material-symbols'; + +export const SCIcon = new Proxy({} as {[key in MaterialSymbol]: key}, { + get: (_target, prop: string) => prop as MaterialSymbol, +}); diff --git a/frontend/app/src/app/util/ion-icon/ion-back-button.directive.ts b/frontend/app/src/app/util/ion-icon/ion-back-button.directive.ts index 1b742ba9..edfadb79 100644 --- a/frontend/app/src/app/util/ion-icon/ion-back-button.directive.ts +++ b/frontend/app/src/app/util/ion-icon/ion-back-button.directive.ts @@ -48,8 +48,8 @@ export class IonBackButtonDirective extends IconReplacer implements OnInit { replace() { this.replaceIcon(this.host.querySelector('.button-inner'), { - md: SCIcon`arrow_back`, - ios: SCIcon`arrow_back_ios`, + md: SCIcon.arrow_back, + ios: SCIcon.arrow_back_ios, size: 24, }); } diff --git a/frontend/app/src/app/util/ion-icon/ion-breadcrumb.directive.ts b/frontend/app/src/app/util/ion-icon/ion-breadcrumb.directive.ts index 2e9de757..3b312a35 100644 --- a/frontend/app/src/app/util/ion-icon/ion-breadcrumb.directive.ts +++ b/frontend/app/src/app/util/ion-icon/ion-breadcrumb.directive.ts @@ -28,7 +28,7 @@ export class IonBreadcrumbDirective extends IconReplacer { this.replaceIcon( this.host.querySelector('span[part="separator"]'), { - name: SCIcon`arrow_forward_ios`, + name: SCIcon.arrow_forward_ios, size: 16, style: `color: var(--ion-color-tint);`, }, @@ -37,7 +37,7 @@ export class IonBreadcrumbDirective extends IconReplacer { this.replaceIcon( this.host.querySelector('button[part="collapsed-indicator"]'), { - name: SCIcon`more_horiz`, + name: SCIcon.more_horiz, size: 24, }, '-collapsed', diff --git a/frontend/app/src/app/util/ion-icon/ion-icon.directive.ts b/frontend/app/src/app/util/ion-icon/ion-icon.directive.ts index 80b96bc8..99ad2d2c 100644 --- a/frontend/app/src/app/util/ion-icon/ion-icon.directive.ts +++ b/frontend/app/src/app/util/ion-icon/ion-icon.directive.ts @@ -28,6 +28,7 @@ import { } from '@angular/core'; import {IconComponent} from './icon.component'; import {IonIcon} from '@ionic/angular'; +import {MaterialSymbol} from 'material-symbols'; // eslint-disable-next-line @typescript-eslint/no-empty-function const noop = () => {}; @@ -40,7 +41,7 @@ const noopProperty = { selector: 'ion-icon', }) export class IonIconDirective implements OnInit, OnDestroy, OnChanges { - @Input() name: string; + @Input() name: MaterialSymbol; @Input() md: string; diff --git a/frontend/app/src/app/util/ion-icon/ion-reorder.directive.ts b/frontend/app/src/app/util/ion-icon/ion-reorder.directive.ts index 2a8f74e9..d53a8463 100644 --- a/frontend/app/src/app/util/ion-icon/ion-reorder.directive.ts +++ b/frontend/app/src/app/util/ion-icon/ion-reorder.directive.ts @@ -26,7 +26,7 @@ export class IonReorderDirective extends IconReplacer { replace() { this.replaceIcon(this.host, { - name: SCIcon`reorder`, + name: SCIcon.reorder, size: 24, }); } diff --git a/frontend/app/src/app/util/ion-icon/ion-searchbar.directive.ts b/frontend/app/src/app/util/ion-icon/ion-searchbar.directive.ts index d9f6d324..eef39ce8 100644 --- a/frontend/app/src/app/util/ion-icon/ion-searchbar.directive.ts +++ b/frontend/app/src/app/util/ion-icon/ion-searchbar.directive.ts @@ -26,11 +26,11 @@ export class IonSearchbarDirective extends IconReplacer { replace() { this.replaceIcon(this.host.querySelector('.searchbar-input-container'), { - name: SCIcon`search`, + name: SCIcon.search, size: 24, }); this.replaceIcon(this.host.querySelector('.searchbar-clear-button'), { - name: SCIcon`close`, + name: SCIcon.close, size: 24, }); } diff --git a/frontend/app/src/config/profile-page-sections.ts b/frontend/app/src/config/profile-page-sections.ts index 37e80000..6b5f9845 100644 --- a/frontend/app/src/config/profile-page-sections.ts +++ b/frontend/app/src/config/profile-page-sections.ts @@ -62,7 +62,7 @@ export const profilePageSections: SCSection[] = [ links: [ { name: 'Favorites', - icon: SCIcon`grade`, + icon: SCIcon.grade, link: ['/favorites'], translations: { de: { @@ -73,7 +73,7 @@ export const profilePageSections: SCSection[] = [ }, { name: 'Schedule', - icon: SCIcon`calendar_today`, + icon: SCIcon.calendar_today, link: ['/schedule'], translations: { de: { @@ -84,7 +84,7 @@ export const profilePageSections: SCSection[] = [ }, { name: 'Course Catalog', - icon: SCIcon`inventory_2`, + icon: SCIcon.inventory_2, link: ['/catalog'], translations: { de: { @@ -95,7 +95,7 @@ export const profilePageSections: SCSection[] = [ }, { name: 'Settings', - icon: SCIcon`settings`, + icon: SCIcon.settings, link: ['/settings'], translations: { de: { @@ -106,7 +106,7 @@ export const profilePageSections: SCSection[] = [ }, { name: 'Feedback', - icon: SCIcon`rate_review`, + icon: SCIcon.rate_review, link: ['/feedback'], translations: { de: { @@ -117,7 +117,7 @@ export const profilePageSections: SCSection[] = [ }, { name: 'About', - icon: SCIcon`info`, + icon: SCIcon.info, link: ['/about'], translations: { de: { @@ -140,7 +140,7 @@ export const profilePageSections: SCSection[] = [ links: [ { name: 'Assessments', - icon: SCIcon`fact_check`, + icon: SCIcon.fact_check, link: ['/assessments'], needsAuth: true, translations: { @@ -164,7 +164,7 @@ export const profilePageSections: SCSection[] = [ links: [ { name: 'Library Catalog', - icon: SCIcon`local_library`, + icon: SCIcon.local_library, link: ['/hebis-search'], translations: { de: { @@ -175,7 +175,7 @@ export const profilePageSections: SCSection[] = [ }, { name: 'Library Account', - icon: SCIcon`badge`, + icon: SCIcon.badge, needsAuth: true, link: ['/library-account/profile'], translations: { @@ -187,7 +187,7 @@ export const profilePageSections: SCSection[] = [ }, { name: 'Orders & Reservations', - icon: SCIcon`collections_bookmark`, + icon: SCIcon.collections_bookmark, needsAuth: true, link: ['/library-account/holds'], translations: { @@ -199,7 +199,7 @@ export const profilePageSections: SCSection[] = [ }, { name: 'Checked out items', - icon: SCIcon`library_books`, + icon: SCIcon.library_books, needsAuth: true, link: ['/library-account/checked-out'], translations: { @@ -211,7 +211,7 @@ export const profilePageSections: SCSection[] = [ }, { name: 'Fines', - icon: SCIcon`request_page`, + icon: SCIcon.request_page, needsAuth: true, link: ['/library-account/fines'], translations: {