diff --git a/CHANGELOG.md b/CHANGELOG.md index 1393d8e7c1..21b7044e4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,11 @@ * Added workaround for problematic use of SASS-syntax-in-CSS shipped by `react-dates`. This began throwing "This function isn't allowed in plain CSS" with latest version of sass/sass-loader. +### ⚙️ Typescript API Adjustments + +* Improved accuracy of `IconProps` interface, with use of the `IconName` and `IconPrefix` types + provided by FontAwesome. + ## v70.0.0 - 2024-11-15 ### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW - changes to advanced persistence APIs) diff --git a/admin/tabs/general/alertBanner/AlertBannerModel.ts b/admin/tabs/general/alertBanner/AlertBannerModel.ts index 4fe8aebd41..50c295d205 100644 --- a/admin/tabs/general/alertBanner/AlertBannerModel.ts +++ b/admin/tabs/general/alertBanner/AlertBannerModel.ts @@ -11,7 +11,7 @@ import {fragment, p} from '@xh/hoist/cmp/layout'; import {HoistModel, Intent, LoadSpec, managed, PlainObject, XH} from '@xh/hoist/core'; import {dateIs, required} from '@xh/hoist/data'; import {action, bindable, computed, makeObservable, observable} from '@xh/hoist/mobx'; -import {AlertBannerSpec} from '@xh/hoist/svc'; +import {AlertBannerIconName, AlertBannerSpec} from '@xh/hoist/svc'; import {isEqual, isMatch, sortBy, without} from 'lodash'; export class AlertBannerModel extends HoistModel { @@ -61,7 +61,7 @@ export class AlertBannerModel extends HoistModel { return ['primary', 'success', 'warning', 'danger']; } - get iconOptions() { + get iconOptions(): AlertBannerIconName[] { return [ 'bullhorn', 'check-circle', diff --git a/desktop/cmp/dash/container/DashContainerModel.ts b/desktop/cmp/dash/container/DashContainerModel.ts index 936964a455..3c7e7046ee 100644 --- a/desktop/cmp/dash/container/DashContainerModel.ts +++ b/desktop/cmp/dash/container/DashContainerModel.ts @@ -16,7 +16,7 @@ import { TaskObserver, XH } from '@xh/hoist/core'; -import {convertIconToHtml, deserializeIcon} from '@xh/hoist/icon'; +import {convertIconToHtml, deserializeIcon, ResolvedIconProps} from '@xh/hoist/icon'; import {showContextMenu} from '@xh/hoist/kit/blueprint'; import {GoldenLayout} from '@xh/hoist/kit/golden-layout'; import {action, bindable, makeObservable, observable, runInAction} from '@xh/hoist/mobx'; @@ -540,7 +540,7 @@ export class DashContainerModel if (icon) { const $currentIcon = $el.find(iconSelector).first(), currentIconType = $currentIcon ? $currentIcon?.data('icon') : null, - newIconType = icon.props.iconName; + newIconType = (icon.props as ResolvedIconProps).iconName; if (currentIconType !== newIconType) { const iconSvg = convertIconToHtml(icon); diff --git a/icon/Icon.ts b/icon/Icon.ts index 98293dada2..cd2ba77c81 100644 --- a/icon/Icon.ts +++ b/icon/Icon.ts @@ -4,29 +4,23 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ +import {IconName} from '@fortawesome/fontawesome-svg-core'; import {FontAwesomeIconProps} from '@fortawesome/react-fontawesome'; import {div} from '@xh/hoist/cmp/layout'; +import {HoistProps, Intent, Thunkable} from '@xh/hoist/core'; import {throwIf} from '@xh/hoist/utils/js'; import classNames from 'classnames'; import {last, pickBy, split, toLower} from 'lodash'; +import {ReactElement} from 'react'; import {iconCmp} from './impl/IconCmp'; import {enhanceFaClasses, iconHtml} from './impl/IconHtml'; -import {ReactElement} from 'react'; -import {HoistProps, Intent, Thunkable} from '@xh/hoist/core'; export interface IconProps extends HoistProps, Partial> { /** Name of the icon in FontAwesome. */ - iconName?: string; + iconName?: IconName; - /** - * Prefix / weight of the icon (or "fab" if your app has imported the free-brand-icons pkg). - * - far - Regular - * - fas - Solid - * - fal - Light - * - fat - Thin (yes, unfortunate) - * - fab - Brand (requires optional import, see Toolbox) - */ - prefix?: 'far' | 'fas' | 'fal' | 'fat' | 'fab'; + /** Weight / family style of the icon. */ + prefix?: HoistIconPrefix; intent?: Intent; @@ -59,27 +53,50 @@ export interface IconProps extends HoistProps, Partial; } +/** + * Icon props after defaults applied by Hoist factory methods. + * @internal + */ +export interface ResolvedIconProps extends IconProps { + iconName: IconName; + prefix: HoistIconPrefix; +} + +/** Supported FA prefixes, used to request a family-specific variant of an icon. */ +export type HoistIconPrefix = + | 'far' // regular + | 'fas' // solid + | 'fal' // light + | 'fat' // thin + | 'fab'; // brands (requires optional import, see Toolbox) + /** * Singleton class to provide factories for creating standard FontAwesome-based icons. * - * Currently we are importing the licensed "pro" library with additional icons - note this requires - * fetching the FA npm package via a registry URL w/license token. + * Hoist imports the licensed "pro" library with additional icons - note this requires fetching the + * FA npm package via a registry URL w/license token. * * See https://fontawesome.com/pro#license. */ export const Icon = { /** - * Return a standard Hoist FontAwesome-based icon. + * Return a Hoist element wrapper around a FontAwesome-based icon. * - * Note that in order to use an icon with this factory, its definition must have been already - * imported and registered with FontAwesome via a call to library.add(). + * Note that for an app to use an icon with this factory, its definition must have been already + * imported and registered with FontAwesome. Apps will find many/most of the icons they need + * pre-registered and enumerated by the Hoist factories below. Favor those ready-made factories + * wherever possible for consistency across and within apps. * - * Applications will often not need to use this factory directly when creating specific - * icons enumerated by Hoist. In that case use the supplied factories on the Icon class - * directly (e.g. Icon.add(), Icon.book(), etc.) These factories will delegate to this method, - * with the name of a pre-imported icon preset. + * If the FA icon of your dreams is not available, however, you can do a one-time import within + * your app bootstrap code, eg: + * ``` + * import {library} from '@fortawesome/fontawesome-svg-core'; + * import {faDreamIcon} from '@fortawesome/pro-regular-svg-icons'; + * library.add(faDreamIcon); + * ``` + * and then pass its string name to this factory: `icon({iconName: 'dream-icon'})` */ - icon(opts?: IconProps): any { + icon(opts: IconProps & {iconName: IconName}): any { let { iconName, prefix = 'far', @@ -908,30 +925,30 @@ export const Icon = { }; /** - * Translate an icon into an html tag. + * Translate an icon into an HTML `` tag. * - * Not typically used by applications. Applications that need html for an icon, e.g. - * for a grid column renderer should use the 'asHtml' flag on the Icon factory functions - * instead. + * Not typically used by applications. Applications that need HTML for an icon should use the + * {@link IconProps.asHtml} flag on the Icon factory functions instead. * - * @param iconElem - react element representing a Hoist Icon component. - * This must be element created by Hoist's built-in Icon factories. - * @returns html of the tag representing the icon. + * @param iconElem - React element representing a Hoist Icon component. + * Must be created by Hoist's built-in Icon factories. + * @returns HTML string for the icon's `` tag. + * @internal */ export function convertIconToHtml(iconElem: ReactElement): string { throwIf( !(iconElem?.type as any)?.isHoistComponent, 'Icon not provided, or not created by a Hoist Icon factory - cannot convert to HTML/SVG.' ); - return iconHtml(iconElem.props); + return iconHtml(iconElem.props as ResolvedIconProps); } /** - * Serialize an icon into a form that can be persisted. + * Serialize an icon into a JSON format with relevant props for persistence. * - * @param iconElem - react element representing a icon component. - * This must be an element created by Hoist's built-in Icon factories. - * @returns json representation of icon. + * @param iconElem - React element representing a Hoist Icon component. + * Must be created by Hoist's built-in Icon factories. + * @returns JSON representation of icon. */ export function serializeIcon(iconElem: ReactElement): any { throwIf( @@ -939,16 +956,14 @@ export function serializeIcon(iconElem: ReactElement): any { 'Icon not provided, or not created by a Hoist Icon factory - cannot serialize.' ); - return pickBy(iconElem.props); + return pickBy(iconElem.props as ResolvedIconProps); } /** - * Deserialize an icon. - * - * This is the inverse operation of serializeIcon(). + * Deserialize an icon - the inverse operation of {@link serializeIcon}. * - * @param iconDef - json representation of icon, produced by serializeIcon. - * @returns react element representing a FontAwesome icon component. + * @param iconDef - JSON representation of icon, produced by serializeIcon. + * @returns React element representing a Hoist Icon component. * This is the form of element created by Hoist's built-in Icon class factories. */ export function deserializeIcon(iconDef: any): ReactElement | string { diff --git a/icon/impl/IconHtml.ts b/icon/impl/IconHtml.ts index 183eedc8f0..8f7d5959e8 100644 --- a/icon/impl/IconHtml.ts +++ b/icon/impl/IconHtml.ts @@ -4,16 +4,27 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ -import {findIconDefinition, icon} from '@fortawesome/fontawesome-svg-core'; +import {findIconDefinition, icon, IconName, IconPrefix} from '@fortawesome/fontawesome-svg-core'; import classNames from 'classnames'; import {isString} from 'lodash'; /** - * Get the raw text of an SVG tag for an icon - * Applications should use the factory methods on Icon instead. - * @internal + * Get the raw HTML string for an icon's SVG tag. + * @internal - apps should use the Hoist Icon factories instead with {@link IconProps.asHtml}. */ -export function iconHtml({iconName, prefix, title, className, size}) { +export function iconHtml({ + iconName, + prefix = 'far', + title, + className, + size +}: { + iconName: IconName; + prefix: IconPrefix; + title?: string; + className?: string; + size?: string; +}) { const iconDef = findIconDefinition({prefix, iconName}), classes = enhanceFaClasses(className, size); @@ -21,9 +32,5 @@ export function iconHtml({iconName, prefix, title, className, size}) { } export function enhanceFaClasses(className: string, size: string) { - let ret = classNames(className, 'fa-fw', 'xh-icon'); - if (isString(size)) { - ret = classNames(ret, `fa-${size}`); - } - return ret; + return classNames(className, 'fa-fw', 'xh-icon', isString(size) ? `fa-${size}` : null); } diff --git a/svc/AlertBannerService.ts b/svc/AlertBannerService.ts index 3a2808bf30..6d9a53717c 100644 --- a/svc/AlertBannerService.ts +++ b/svc/AlertBannerService.ts @@ -4,6 +4,7 @@ * * Copyright © 2024 Extremely Heavy Industries Inc. */ +import {IconName} from '@fortawesome/fontawesome-svg-core'; import {BannerModel} from '@xh/hoist/appcontainer/BannerModel'; import {markdown} from '@xh/hoist/cmp/markdown'; import {BannerSpec, HoistService, Intent, XH} from '@xh/hoist/core'; @@ -47,7 +48,7 @@ export class AlertBannerService extends HoistService { genBannerSpec( message: string, intent: Intent, - iconName: string, + iconName: IconName, enableClose: boolean ): BannerSpec { const icon = iconName ? Icon.icon({iconName, size: 'lg'}) : null, @@ -90,19 +91,26 @@ export class AlertBannerService extends HoistService { } } -/** - * @internal - */ +/** @internal */ export interface AlertBannerSpec { active: boolean; expires: number; publishDate: number; message: string; intent: Intent; - iconName: string; + iconName: AlertBannerIconName; enableClose: boolean; clientApps: string[]; created: number; updated: number; updatedBy: string; } + +/** @internal */ +export type AlertBannerIconName = + | 'bullhorn' + | 'check-circle' + | 'exclamation-triangle' + | 'times-circle' + | 'info-circle' + | 'question-circle';