• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jumpinjackie / mapguide-react-layout / 29921515611

22 Jul 2026 12:53PM UTC coverage: 62.625% (+1.5%) from 61.081%
29921515611

push

github

web-flow
Update to React 18 + other package updates (#1683)

* Update to React 18 and other assorted package updates

* More package updates

* Update react-redux

* Fix TS error

3301 of 6236 branches covered (52.93%)

1 of 2 new or added lines in 2 files covered. (50.0%)

145 existing lines in 29 files now uncovered.

6910 of 11034 relevant lines covered (62.62%)

15.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.06
/src/api/registry/command.ts
1
import {
2
    IMapViewer,
3
    ICommand,
4
    Dictionary,
5
    IInvokeUrlCommand,
6
    ISearchCommand,
7
    ITargetedCommand,
8
    IApplicationState,
9
    ReduxDispatch,
10
    getSelectionSet,
11
    getRuntimeMap,
12
    NOOP,
13
    ALWAYS_FALSE,
14
    IInvokeUrlCommandParameter,
15
    ActiveMapTool} from "../../api/common";
16
import type { ComparisonMode, IComparisonPair } from "../../api/common";
17
import { getFusionRoot } from "../../api/runtime";
18
import { IItem, IInlineMenu, IFlyoutMenu, IComponentFlyoutItem } from "../../components/toolbar";
19
import { tr } from "../i18n";
20
import { getAssetRoot } from "../../utils/asset";
21
import {
22
    SPRITE_ICON_ERROR
23
} from "../../constants/assets";
24
import { assertNever } from "../../utils/never";
25
import { ensureParameters } from "../../utils/url";
26
import { ActionType } from '../../constants/actions';
27
import { showModalUrl } from '../../actions/modal';
28
import { error } from '../../utils/logger';
29

30
const FUSION_ICON_REGEX = /images\/icons\/[a-zA-Z\-]*.png/
21✔
31

32
function fixIconPath(path: string): string {
33
    if (FUSION_ICON_REGEX.test(path)) {
2!
34
        return `${getAssetRoot()}/${path}`.replace(/\/\//g, "/");
×
35
    }
36
    return path;
2✔
37
}
38

39
function fusionFixSpriteClass(tb: any, cmd?: ICommand): string | undefined {
40
    if (tb.spriteClass) {
3!
41
        return tb.spriteClass;
×
42
    }
43
    if (cmd && cmd.iconClass) {
3!
44
        return cmd.iconClass;
×
45
    }
46
    return undefined;
3✔
47
}
48

49
/**
50
 * @hidden
51
 */
52
export function mergeInvokeUrlParameters(currentParameters: IInvokeUrlCommandParameter[], extraParameters?: any) {
53
    const currentP = currentParameters.reduce<any>((prev, current, i, arr) => {
3✔
54
        prev[current.name] = current.value;
3✔
55
        return prev;
3✔
56
    }, {});
57
    if (extraParameters) {
3✔
58
        const keys = Object.keys(extraParameters);
2✔
59
        for (const k of keys) {
2✔
60
            currentP[k] = extraParameters[k];
4✔
61
        }
62
    }
63
    const merged: IInvokeUrlCommandParameter[] = [];
3✔
64
    const mkeys = Object.keys(currentP);
3✔
65
    for (const k of mkeys) {
3✔
66
        merged.push({ name: k, value: currentP[k] });
6✔
67
    }
68
    return merged;
3✔
69
}
70

71
function fixChildItems(childItems: any[], state: IToolbarAppState, commandInvoker: (cmd: ICommand, parameters?: any) => void): IItem[] {
72
    return childItems
1✔
73
        .map(tb => mapToolbarReference(tb, state, commandInvoker))
1✔
74
        .filter(tb => tb != null) as IItem[];
1✔
75
}
76

77
//TODO: This function should be its own react hook that layers on top of the
78
//useDispatch() and useSelector() hooks provided by react-redux
79
/**
80
 * @hidden
81
 */
82
export function mapToolbarReference(tb: any, state: IToolbarAppState, commandInvoker: (cmd: ICommand, parameters?: any) => void): IItem | IInlineMenu | IFlyoutMenu | IComponentFlyoutItem | null {
83
    if (tb.error) {
8✔
84
        const cmdItem: IItem = {
1✔
85
            iconClass: SPRITE_ICON_ERROR,
86
            tooltip: tb.error,
87
            label: tr("ERROR"),
88
            selected: ALWAYS_FALSE,
89
            enabled: ALWAYS_FALSE,
90
            invoke: NOOP
91
        };
92
        return cmdItem;
1✔
93
    } else if (tb.componentName) {
7✔
94
        return {
1✔
95
            icon: tb.icon,
96
            iconClass: fusionFixSpriteClass(tb),
97
            flyoutId: tb.flyoutId,
98
            tooltip: tb.tooltip,
99
            label: tb.label,
100
            componentName: tb.componentName,
101
            componentProps: tb.componentProps
102
        };
103
    } else if (tb.isSeparator === true) {
6✔
104
        return { isSeparator: true };
2✔
105
    } else if (tb.command) {
4✔
106
        const cmd = getCommand(tb.command);
1✔
107
        if (cmd != null) {
1!
108
            const cmdItem: IItem = {
×
109
                icon: fixIconPath(tb.icon || cmd.icon),
×
110
                iconClass: fusionFixSpriteClass(tb, cmd),
111
                tooltip: tb.tooltip,
112
                label: tb.label,
113
                selected: () => cmd.selected(state),
×
114
                enabled: () => cmd.enabled(state, tb.parameters),
×
115
                invoke: () => commandInvoker(cmd, tb.parameters)
×
116
            };
117
            return cmdItem;
×
118
        }
119
    } else if (tb.children) {
3✔
120
        const childItems: any[] = tb.children;
1✔
121
        return {
1✔
122
            icon: fixIconPath(tb.icon),
123
            iconClass: fusionFixSpriteClass(tb),
124
            label: tb.label,
125
            tooltip: tb.tooltip,
126
            childItems: fixChildItems(childItems, state, commandInvoker)
127
        };
128
    } else if (tb.label && tb.flyoutId) {
2✔
129
        return {
1✔
130
            icon: fixIconPath(tb.icon),
131
            iconClass: fusionFixSpriteClass(tb),
132
            label: tb.label,
133
            tooltip: tb.tooltip,
134
            flyoutId: tb.flyoutId
135
        }
136
    }
137
    return null;
2✔
138
}
139

140
/**
141
 * A subset of IApplicationState that's only relevant for toolbar items
142
 * @since 0.13
143
 */
144
export interface IToolbarAppState {
145
    /**
146
     * @since 0.14
147
     */
148
    stateless: boolean;
149
    visibleAndSelectableWmsLayerCount: number;
150
    busyWorkerCount: number;
151
    hasSelection: boolean;
152
    /**
153
     * @since 0.14
154
     */
155
    hasClientSelection: boolean;
156
    hasPreviousView: boolean;
157
    hasNextView: boolean;
158
    featureTooltipsEnabled: boolean;
159
    activeTool: ActiveMapTool;
160
    /**
161
     * @since 0.15
162
     */
163
    comparisonMode?: ComparisonMode;
164
    swipeActive: boolean;
165
    /**
166
     * @since 0.15
167
     */
168
    comparisonPairs?: IComparisonPair[];
169
    mapSwipePairs: IComparisonPair[];
170
    /**
171
     * @since 0.15
172
     */
173
    activeMapName: string | undefined;
174
}
175

176
/**
177
 * Helper function to reduce full application state to state relevant for toolbar items
178
 * 
179
 * @param state The full application state
180
 * @since 0.13
181
 */
182
export function reduceAppToToolbarState(state: Readonly<IApplicationState>): Readonly<IToolbarAppState> {
183
    let hasSelection = false;
23✔
184
    let hasClientSelection = false;
23✔
185
    let hasPreviousView = false;
23✔
186
    let hasNextView = false;
23✔
187
    let visibleWmsLayerCount = 0;
23✔
188
    const selection = getSelectionSet(state);
23✔
189
    hasSelection = (selection != null && selection.SelectedFeatures != null);
23✔
190
    if (state.config.activeMapName) {
23✔
191
        hasClientSelection = state.mapState[state.config.activeMapName].clientSelection != null;
10✔
192
        hasPreviousView = state.mapState[state.config.activeMapName].historyIndex > 0;
10✔
193
        hasNextView = state.mapState[state.config.activeMapName].historyIndex < state.mapState[state.config.activeMapName].history.length - 1;
10✔
194
        visibleWmsLayerCount = (state.mapState[state.config.activeMapName].layers ?? []).filter(l => l.visible && l.selectable && l.type == "WMS").length;
10!
195
    }
196
    return {
23✔
197
        stateless: state.config.viewer.isStateless,
198
        visibleAndSelectableWmsLayerCount: visibleWmsLayerCount,
199
        busyWorkerCount: state.viewer.busyCount,
200
        hasSelection,
201
        hasClientSelection,
202
        hasPreviousView,
203
        hasNextView,
204
        activeTool: state.viewer.tool,
205
        featureTooltipsEnabled: state.viewer.featureTooltipsEnabled,
206
        comparisonMode: state.config.comparisonMode ?? "none",
23!
207
        swipeActive: state.config.swipeActive === true,
208
        comparisonPairs: state.config.comparisonPairs ?? [],
46✔
209
        mapSwipePairs: state.config.comparisonPairs ?? [],
46✔
210
        activeMapName: state.config.activeMapName
211
    }
212
}
213

214
/**
215
 * Common command condition evaluators
216
 *
217
 *
218
 * @class CommandConditions
219
 */
220
export class CommandConditions {
221
    /**
222
     * The viewer is not busy
223
     *
224
     * @param {Readonly<IToolbarAppState>} state
225
     * @returns {boolean}
226
     */
227
    public static isNotBusy(state: Readonly<IToolbarAppState>): boolean {
228
        return state.busyWorkerCount == 0;
5✔
229
    }
230
    /**
231
     * The viewer has a MapGuide selection set
232
     *
233
     * @param {Readonly<IToolbarAppState>} state
234
     * @returns {boolean}
235
     */
236
    public static hasSelection(state: Readonly<IToolbarAppState>): boolean {
237
        return state.hasSelection;
5✔
238
    }
239
    /**
240
     * The viewer has a client-side selection set
241
     *
242
     * @param state 
243
     * @returns  
244
     * @since 0.14
245
     */
246
    public static hasClientSelection(state: Readonly<IToolbarAppState>): boolean {
247
        return state.hasClientSelection;
×
248
    }
249
    /**
250
     * The command is set to be disabled if selection is empty
251
     *
252
     * @param {*} [parameters]
253
     * @returns {boolean}
254
     */
255
    public static disabledIfEmptySelection(state: Readonly<IToolbarAppState>, parameters?: any): boolean {
256
        if (!state.hasSelection) {
10✔
257
            return (parameters != null && (parameters.DisableIfSelectionEmpty == "true" || parameters.DisableIfSelectionEmpty == true));
6✔
258
        } else
259
            return false;
4✔
260
    }
261
    /**
262
     * The viewer has a previous view in the view navigation stack
263
     *
264
     * @param {Readonly<IToolbarAppState>} state
265
     * @returns {boolean}
266
     */
267
    public static hasPreviousView(state: Readonly<IToolbarAppState>): boolean {
268
        return state.hasPreviousView;
3✔
269
    }
270
    /**
271
     * The viewer has a next view in the view navigation stack
272
     *
273
     * @param {Readonly<IToolbarAppState>} state
274
     * @returns {boolean}
275
     */
276
    public static hasNextView(state: Readonly<IToolbarAppState>): boolean {
277
        return state.hasNextView;
3✔
278
    }
279
}
280

281
/**
282
 * The set of default command names
283
 *
284
 *
285
 * @class DefaultCommands
286
 */
287
export enum DefaultCommands {
21✔
288
    Select = "Select",
21✔
289
    Pan = "Pan",
21✔
290
    Zoom = "Zoom",
21✔
291
    MapTip = "MapTip",
21✔
292
    ZoomIn = "ZoomIn",
21✔
293
    ZoomOut = "ZoomOut",
21✔
294
    RestoreView = "RestoreView",
21✔
295
    ZoomExtents = "ZoomExtents",
21✔
296
    SelectRadius = "SelectRadius",
21✔
297
    SelectPolygon = "SelectPolygon",
21✔
298
    ClearSelection = "ClearSelection",
21✔
299
    ZoomToSelection = "ZoomToSelection",
21✔
300
    PanLeft = "PanLeft",
21✔
301
    PanRight = "PanRight",
21✔
302
    PanUp = "PanUp",
21✔
303
    PanDown = "PanDown",
21✔
304
    RefreshMap = "RefreshMap",
21✔
305
    PreviousView = "PreviousView",
21✔
306
    NextView = "NextView",
21✔
307
    About = "About",
21✔
308
    Help = "Help",
21✔
309
    Measure = "Measure",
21✔
310
    ViewerOptions = "ViewerOptions",
21✔
311
    Buffer = "Buffer",
21✔
312
    SelectWithin = "SelectWithin",
21✔
313
    QuickPlot = "QuickPlot",
21✔
314
    Redline = "Redline",
21✔
315
    FeatureInfo = "FeatureInfo",
21✔
316
    Theme = "Theme",
21✔
317
    Query = "Query",
21✔
318
    Geolocation = "Geolocation",
21✔
319
    CoordinateTracker = "CoordinateTracker",
21✔
320
    /**
321
     * @since 0.11
322
     */
323
    AddManageLayers = "AddManageLayers",
21✔
324
    /**
325
     * @since 0.11
326
     */
327
    CenterSelection = "CenterSelection",
21✔
328
    /**
329
     * @since 0.14
330
     */
331
    Print = "Print",
21✔
332
    /**
333
     * @since 0.15
334
     */
335
    MapSwipe = "MapSwipe"
21✔
336
}
337

338
const commands: Dictionary<ICommand> = {};
21✔
339

340
function isInvokeUrlCommand(cmdDef: any): cmdDef is IInvokeUrlCommand {
341
    return typeof cmdDef.url !== 'undefined';
35✔
342
}
343

344
function isSearchCommand(cmdDef: any): cmdDef is ISearchCommand {
345
    return typeof cmdDef.layer !== 'undefined';
25✔
346
}
347

348
function openModalUrl(name: string, dispatch: ReduxDispatch, url: string, modalTitle?: string) {
349
    dispatch(showModalUrl({
×
350
        modal: {
351
            title: modalTitle || tr(name as any),
×
352
            backdrop: false,
353
            overflowYScroll: true
354
        },
355
        name: name,
356
        url: url
357
    }));
358
}
359

360
export function isSupportedCommandInStatelessMode(name: string | undefined) {
361
    switch (name) {
70✔
362
        case DefaultCommands.MapTip:
363
        case DefaultCommands.SelectRadius:
364
        case DefaultCommands.SelectPolygon:
365
        case DefaultCommands.Buffer:
366
        case DefaultCommands.SelectWithin:
367
        case DefaultCommands.Redline:
368
        case DefaultCommands.FeatureInfo:
369
        case DefaultCommands.Query:
370
        case DefaultCommands.Theme:
371
        case DefaultCommands.CenterSelection:
372
            return false;
18✔
373
    }
374
    return true;
52✔
375
}
376

377
/**
378
 * Opens the given URL in the specified target
379
 *
380
 * @hidden
381
 * @param name
382
 * @param cmdDef
383
 * @param dispatch
384
 * @param url
385
 */
386
export function openUrlInTarget(name: string, cmdDef: ITargetedCommand, hasTaskPane: boolean, dispatch: ReduxDispatch, url: string, modalTitle?: string): void {
387
    const target = cmdDef.target;
3✔
388
    if (target == "TaskPane") {
3!
389
        //If there's no actual task pane, fallback to modal dialog
390
        if (!hasTaskPane) {
3!
391
            openModalUrl(name, dispatch, url, modalTitle);
×
392
        } else {
393
            dispatch({
3✔
394
                type: ActionType.TASK_INVOKE_URL,
395
                payload: {
396
                    url: url
397
                }
398
            });
399
        }
UNCOV
400
    } else if (target == "NewWindow") {
×
401
        openModalUrl(name, dispatch, url, modalTitle);
×
402
    } else if (target == "SpecifiedFrame") {
×
403
        if (cmdDef.targetFrame) {
×
404
            const frames = (window as any).frames as any[];
×
405
            let bInvoked = false;
×
406
            for (let i = 0; i < frames.length; i++) {
×
407
                if (frames[i].name == cmdDef.targetFrame) {
×
408
                    frames[i].location = url;
×
409
                    bInvoked = true;
×
410
                    break;
×
411
                }
412
            }
413
            if (!bInvoked) {
×
414
                error(`Frame not found: ${cmdDef.targetFrame}`);
×
415
            }
416
        } else {
417
            error(`Command ${name} has a target of "SpecifiedFrame", but does not specify a target frame`);
×
418
        }
419
    } else {
420
        assertNever(target);
×
421
    }
422
}
423

424
/**
425
 * Registers a viewer command
426
 *
427
 *
428
 * @param {string} name
429
 * @param {(ICommand | IInvokeUrlCommand | ISearchCommand)} cmdDef
430
 */
431
export function registerCommand(name: string, cmdDef: ICommand | IInvokeUrlCommand | ISearchCommand) {
432
    let cmd: ICommand;
433
    if (isInvokeUrlCommand(cmdDef)) {
35✔
434
        cmd = {
10✔
435
            icon: cmdDef.icon,
436
            iconClass: cmdDef.iconClass,
437
            title: cmdDef.title,
438
            enabled: (state) => {
439
                if (cmdDef.disableIfSelectionEmpty === true) {
×
440
                    return CommandConditions.hasSelection(state);
×
441
                }
442
                return true;
×
443
            },
UNCOV
444
            selected: () => false,
×
445
            invoke: (dispatch: ReduxDispatch, getState: () => IApplicationState, viewer: IMapViewer, parameters?: any) => {
446
                const state = getState();
×
447
                const config = state.config;
×
448
                const map = getRuntimeMap(state);
×
449
                const params = mergeInvokeUrlParameters(cmdDef.parameters, parameters);
×
450
                const url = ensureParameters(cmdDef.url, map?.Name, map?.SessionId, config.locale, true, params);
×
451
                openUrlInTarget(name, cmdDef, config.capabilities.hasTaskPane, dispatch, url, cmd.title);
×
452
            }
453
        };
454
    } else if (isSearchCommand(cmdDef)) {
25!
455
        cmd = {
×
456
            icon: cmdDef.icon,
457
            iconClass: cmdDef.iconClass,
458
            title: cmdDef.title,
459
            enabled: state => !state.stateless,
×
460
            selected: () => false,
×
461
            invoke: (dispatch: ReduxDispatch, getState: () => IApplicationState, viewer: IMapViewer, parameters?: any) => {
462
                const state = getState();
×
463
                const config = state.config;
×
464
                const map = getRuntimeMap(state);
×
465
                if (map) {
×
466
                    const url = ensureParameters(`${getFusionRoot()}/widgets/Search/SearchPrompt.php`, map.Name, map.SessionId, config.locale, false)
×
467
                        + `&popup=0`
468
                        + `&target=TaskPane`
469
                        + `&title=${encodeURIComponent(cmdDef.title)}`
470
                        + `&prompt=${encodeURIComponent(cmdDef.prompt)}`
471
                        + `&layer=${encodeURIComponent(cmdDef.layer)}`
472
                        + `&pointZoomLevel=${parameters.PointZoomLevel}`
473
                        + (cmdDef.filter ? `&filter=${encodeURIComponent(cmdDef.filter)}` : '')
×
474
                        + `&limit=${cmdDef.matchLimit}`
475
                        + `&properties=${(cmdDef.resultColumns.Column || []).map(col => col.Property).join(",")}`
×
476
                        + `&propNames=${(cmdDef.resultColumns.Column || []).map(col => col.Name).join(",")}`;
×
477
                    openUrlInTarget(name, cmdDef, config.capabilities.hasTaskPane, dispatch, url, cmd.title);
×
478
                }
479
            }
480
        };
481
    } else {
482
        cmd = cmdDef;
25✔
483
    }
484
    commands[name] = cmd;
35✔
485
}
486

487
/**
488
 * Gets a registered viewer command by its name
489
 *
490
 *
491
 * @param {string} name
492
 * @returns {(ICommand | undefined)}
493
 */
494
export function getCommand(name: string): ICommand | undefined {
495
    return commands[name];
41✔
496
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc