feat: type-safe sc-icons

This commit is contained in:
2024-04-03 12:16:18 +02:00
committed by Rainer Killinger
parent 53c3d0ba0c
commit abf9999461
29 changed files with 211 additions and 172 deletions

View File

@@ -49,7 +49,7 @@
@if (content.type === 'router link') {
<ion-item [routerLink]="content.link">
@if (content.icon) {
<ion-icon [name]="content.icon" slot="start"></ion-icon>
<ion-icon [name]="$any(content.icon)" slot="start"></ion-icon>
}
<ion-label>{{ 'title' | translateSimple: content }}</ion-label>
</ion-item>

View File

@@ -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();

View File

@@ -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

View File

@@ -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,

View File

@@ -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<SCThingType, string> = {
'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<SCThingType, MaterialSymbol>;

View File

@@ -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];
}
}

View File

@@ -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;

View File

@@ -35,13 +35,13 @@
lines="none"
class="menu-category"
>
<ion-icon slot="end" [name]="category.icon"></ion-icon>
<ion-icon slot="end" [name]="$any(category.icon)"></ion-icon>
<ion-label> {{ category.translations[language]?.title | titlecase }} </ion-label>
</ion-item>
}
@for (item of category.items; track item) {
<ion-item [rootLink]="item.route" [redirectedFrom]="item.route">
<ion-icon slot="end" [name]="item.icon"></ion-icon>
<ion-icon slot="end" [name]="$any(item.icon)"></ion-icon>
<ion-label> {{ item.translations[language]?.title | titlecase }} </ion-label>
</ion-item>
}

View File

@@ -41,7 +41,7 @@
</ion-menu-toggle>
@for (category of menu; track category; let isFirst = $first) {
<ion-tab-button [rootLink]="category.route" [redirectedFrom]="category.route" [tab]="category.title">
<ion-icon [name]="category.icon"></ion-icon>
<ion-icon [name]="$any(category.icon)"></ion-icon>
<ion-label>{{ category.translations[language]?.title | titlecase }}</ion-label>
</ion-tab-button>
}

View File

@@ -35,7 +35,7 @@
>
<div>
@if (link.icon) {
<ion-icon [name]="link.icon" [size]="36" color="dark"></ion-icon>
<ion-icon [name]="$any(link.icon)" [size]="36" color="dark"></ion-icon>
}
<ion-label>{{ 'name' | translateSimple: link }}</ion-label>
</div>

View File

@@ -0,0 +1,5 @@
export function matchTagProperties(tag: string): RegExp;
export function matchPropertyContent(properties: string[]): RegExp;
export function matchPropertyAccess(objectName: string): RegExp;

View File

@@ -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');
}

View File

@@ -12,9 +12,7 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* 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]);
});
});

View File

@@ -13,9 +13,8 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* 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,
});

View File

@@ -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,
});
}

View File

@@ -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',

View File

@@ -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;

View File

@@ -26,7 +26,7 @@ export class IonReorderDirective extends IconReplacer {
replace() {
this.replaceIcon(this.host, {
name: SCIcon`reorder`,
name: SCIcon.reorder,
size: 24,
});
}

View File

@@ -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,
});
}

View File

@@ -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: {