terriajs
Version:
Geospatial data visualization platform.
309 lines (257 loc) • 9.67 kB
text/typescript
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;