mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-04-15 18:59:24 +00:00
feat: type-safe sc-icons
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
5
frontend/app/src/app/util/ion-icon/icon-match.d.mts
Normal file
5
frontend/app/src/app/util/ion-icon/icon-match.d.mts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function matchTagProperties(tag: string): RegExp;
|
||||
|
||||
export function matchPropertyContent(properties: string[]): RegExp;
|
||||
|
||||
export function matchPropertyAccess(objectName: string): RegExp;
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export class IonReorderDirective extends IconReplacer {
|
||||
|
||||
replace() {
|
||||
this.replaceIcon(this.host, {
|
||||
name: SCIcon`reorder`,
|
||||
name: SCIcon.reorder,
|
||||
size: 24,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user