UNPKG

terriajs

Version:

Geospatial data visualization platform.

309 lines (257 loc) 9.67 kB
import { ReactNode } from "react"; import isDefined from "../../Core/isDefined"; import { IconGlyph } from "../../Styled/Icon"; /** `Dimension` (and child interfaces - eg `EnumDimension`, `NumericalDimension`, ...) are Trait/JSON friendly interfaces. They are used as base to the `SelectableDimension` interfaces. * * This is useful because it means we can directly use Traits to create SelectableDimensions - for example see `EnumDimensionTraits` in `lib/Traits/TraitsClasses/DimensionTraits.ts` */ interface Dimension { /** Machine readable ID */ readonly id?: string; /** Human readable name */ readonly name?: string; } export interface EnumDimensionOption<T = string> { readonly id?: T; readonly name?: string; } export interface EnumDimension<T = string> extends Dimension { readonly options?: readonly EnumDimensionOption<T>[]; readonly selectedId?: T; readonly allowUndefined?: boolean; /** If true, then the user can set the value to arbitrary text */ readonly allowCustomInput?: boolean; readonly undefinedLabel?: string; } /** Similar to EnumDimension, but supports multiple selected values */ export interface MultiEnumDimension<T = string> extends Dimension { readonly options?: readonly EnumDimensionOption<T>[]; readonly selectedIds?: T[]; readonly allowUndefined?: boolean; } export interface NumericalDimension extends Dimension { readonly value?: number; readonly min?: number; readonly max?: number; readonly allowUndefined?: boolean; } export interface TextDimension extends Dimension { readonly value?: string; readonly allowUndefined?: boolean; } export interface ColorDimension extends Dimension { readonly value?: string; readonly allowUndefined?: boolean; } export interface ButtonDimension extends Dimension { readonly value?: string; readonly icon?: | IconGlyph // Any Icon glyph | "spinner"; // Animated spinner icon } export type SelectableDimensionType = | undefined | "select" | "select-multi" | "numeric" | "text" | "checkbox" | "checkbox-group" | "group" | "button" | "color"; export type Placement = "default" | "belowLegend"; export const DEFAULT_PLACEMENT: Placement = "default"; /** Base SelectableDimension interface. Each following SelectableDimension will extend this and the Dimension interface above */ export interface SelectableDimensionBase<T = string> { setDimensionValue(stratumId: string, value: T | undefined): void; disable?: boolean; /** Placement of dimension in Workbench: * - default (above legend and short-report sections) * - belowLegend * This is only relevant to top level SelectableDimensions (not nested in groups) */ placement?: Placement; type?: SelectableDimensionType; } export type OptionRenderer = (option: { value: string | undefined; }) => ReactNode; export interface SelectableDimensionEnum extends SelectableDimensionBase<string>, EnumDimension { type?: undefined | "select"; /** Render ReactNodes for each option - instead of plain label */ optionRenderer?: OptionRenderer; } /** Similar to SelectableDimensionEnum, but supports multiple selected values */ export interface SelectableDimensionMultiEnum extends SelectableDimensionBase<string[]>, MultiEnumDimension { type?: undefined | "select-multi"; /** Render ReactNodes for each option - instead of plain label */ optionRenderer?: OptionRenderer; } export interface SelectableDimensionCheckbox extends SelectableDimensionBase<"true" | "false">, EnumDimension<"true" | "false"> { type: "checkbox"; } export interface SelectableDimensionCheckboxGroup extends SelectableDimensionBase<"true" | "false">, Omit<SelectableDimensionGroup, "type">, EnumDimension<"true" | "false"> { type: "checkbox-group"; /** * Text to show if the group is empty */ emptyText?: string; // We don't allow nested groups for now to keep the UI simple readonly selectableDimensions: Exclude< SelectableDimension, SelectableDimensionGroup | SelectableDimensionCheckboxGroup >[]; } export interface SelectableDimensionButton extends SelectableDimensionBase<true>, ButtonDimension { type: "button"; } export interface SelectableDimensionNumeric extends SelectableDimensionBase<number>, NumericalDimension { type: "numeric"; } export interface SelectableDimensionText extends SelectableDimensionBase<string>, TextDimension { type: "text"; } export interface SelectableDimensionColor extends SelectableDimensionBase<string>, ColorDimension { type: "color"; } export interface SelectableDimensionGroup extends Omit<SelectableDimensionBase, "setDimensionValue">, Dimension { type: "group"; /** Group is **closed** by default */ isOpen?: boolean; /** Function is called whenever SelectableDimensionGroup is toggled (closed or opened). * Return value is `true` if the listener has consumed the event, `false` otherwise. * This means you can manage group open state separately if desired */ onToggle?: (isOpen: boolean) => boolean | undefined; // We don't allow nested groups for now to keep the UI simple readonly selectableDimensions: Exclude< SelectableDimension, SelectableDimensionGroup >[]; } export type FlatSelectableDimension = Exclude< SelectableDimension, SelectableDimensionGroup >; export type SelectableDimension = | SelectableDimensionEnum | SelectableDimensionMultiEnum | SelectableDimensionCheckbox | SelectableDimensionCheckboxGroup | SelectableDimensionGroup | SelectableDimensionNumeric | SelectableDimensionText | SelectableDimensionButton | SelectableDimensionColor; export const isEnum = ( dim: SelectableDimension ): dim is SelectableDimensionEnum => dim.type === "select" || dim.type === undefined; export const isMultiEnum = ( dim: SelectableDimension ): dim is SelectableDimensionMultiEnum => dim.type === "select-multi"; /** Return only SelectableDimensionSelect from array of SelectableDimension */ export const filterEnums = ( dims: SelectableDimension[] ): SelectableDimensionEnum[] => dims.filter(isEnum); export const isGroup = ( dim: SelectableDimension ): dim is SelectableDimensionGroup => dim.type === "group"; export const isNumeric = ( dim: SelectableDimension ): dim is SelectableDimensionNumeric => dim.type === "numeric"; export const isText = ( dim: SelectableDimension ): dim is SelectableDimensionText => dim.type === "text"; export const isButton = ( dim: SelectableDimension ): dim is SelectableDimensionButton => dim.type === "button"; export const isCheckbox = ( dim: SelectableDimension ): dim is SelectableDimensionCheckbox => dim.type === "checkbox"; export const isCheckboxGroup = ( dim: SelectableDimension ): dim is SelectableDimensionCheckboxGroup => dim.type === "checkbox-group"; export const isColor = ( dim: SelectableDimension ): dim is SelectableDimensionColor => dim.type === "color"; const isCorrectPlacement = (placement?: Placement) => (dim: SelectableDimension) => dim.placement ? dim.placement === placement : placement === DEFAULT_PLACEMENT; const isEnabled = (dim: SelectableDimension) => !dim.disable; /** Filter out dimensions with only 1 option (unless they have 1 option and allow undefined - which is 2 total options) */ const enumHasValidOptions = (dim: EnumDimension) => { const minLength = dim.allowUndefined ? 1 : 2; return isDefined(dim.options) && dim.options.length >= minLength; }; /** Multi enums just need one option (they don't have `allowUndefined`) */ const multiEnumHasValidOptions = (dim: MultiEnumDimension) => { return isDefined(dim.options) && dim.options.length > 0; }; /** Filter with SelectableDimension should be shown for a given placement. * This will take into account whether SelectableDimension is valid, not disabled, etc... */ export const filterSelectableDimensions = (placement?: Placement) => (selectableDimensions: SelectableDimension[] = []) => selectableDimensions.filter( (dim) => // Filter by placement if defined, otherwise use default placement (!isDefined(placement) || isCorrectPlacement(placement)(dim)) && isEnabled(dim) && // Check enum (select and checkbox) dimensions for valid options ((!isEnum(dim) && !isCheckbox(dim)) || enumHasValidOptions(dim)) && // Check multi-enum (!isMultiEnum(dim) || multiEnumHasValidOptions(dim)) && // Only show groups if they have at least one SelectableDimension (!isGroup(dim) || dim.selectableDimensions.length > 0) ); /** Find human readable name for the current value for a SelectableDimension */ export const findSelectedValueName = ( dim: Exclude<SelectableDimension, SelectableDimensionGroup> ): string | undefined => { if (isCheckbox(dim)) { return dim.selectedId === "true" ? "Enabled" : "Disabled"; } if (isEnum(dim)) { return dim.options?.find((opt) => opt.id === dim.selectedId)?.name; } if (isMultiEnum(dim)) { // return names as CSV return dim.options ?.filter((opt) => dim.selectedIds?.some((id) => opt.id === id)) ?.map((option) => option.name) ?.join(", "); } if (isNumeric(dim)) { return dim.value?.toString(); } if (isText(dim)) return dim.value; }; /** Interface to be implemented by BaseModels (eg CatalogMembers) to add selectableDimensions */ interface SelectableDimensions { selectableDimensions: SelectableDimension[]; } namespace SelectableDimensions { export function is(model: any): model is SelectableDimensions { return "selectableDimensions" in model; } } export default SelectableDimensions;