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

jumpinjackie / mapguide-react-layout / 15160437878

21 May 2025 11:00AM UTC coverage: 21.631% (-42.6%) from 64.24%
15160437878

Pull #1552

github

web-flow
Merge 8b7153d9e into 236e2ea07
Pull Request #1552: Feature/package updates 2505

839 of 1165 branches covered (72.02%)

11 of 151 new or added lines in 25 files covered. (7.28%)

1332 existing lines in 50 files now uncovered.

4794 of 22163 relevant lines covered (21.63%)

6.89 hits per line

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

42.01
/src/api/registry/command.ts
1
import {
1✔
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 { getFusionRoot } from "../../api/runtime";
1✔
17
import { IItem, IInlineMenu, IFlyoutMenu, IComponentFlyoutItem } from "../../components/toolbar";
18
import { tr } from "../i18n";
1✔
19
import { getAssetRoot } from "../../utils/asset";
1✔
20
import {
1✔
21
    SPRITE_ICON_ERROR
22
} from "../../constants/assets";
23
import { assertNever } from "../../utils/never";
1✔
24
import { ensureParameters } from "../../utils/url";
1✔
25
import { ActionType } from '../../constants/actions';
1✔
26
import { showModalUrl } from '../../actions/modal';
1✔
27
import { error } from '../../utils/logger';
1✔
28

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

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

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

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

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

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

139
/**
140
 * A subset of IApplicationState that's only relevant for toolbar items
141
 * @since 0.13
142
 */
143
export interface IToolbarAppState {
144
    /**
145
     * @since 0.14
146
     */
147
    stateless: boolean;
148
    visibleAndSelectableWmsLayerCount: number;
149
    busyWorkerCount: number;
150
    hasSelection: boolean;
151
    /**
152
     * @since 0.14
153
     */
154
    hasClientSelection: boolean;
155
    hasPreviousView: boolean;
156
    hasNextView: boolean;
157
    featureTooltipsEnabled: boolean;
158
    activeTool: ActiveMapTool;
159
}
160

161
/**
162
 * Helper function to reduce full application state to state relevant for toolbar items
163
 * 
164
 * @param state The full application state
165
 * @since 0.13
166
 */
167
export function reduceAppToToolbarState(state: Readonly<IApplicationState>): Readonly<IToolbarAppState> {
1✔
168
    let hasSelection = false;
23✔
169
    let hasClientSelection = false;
23✔
170
    let hasPreviousView = false;
23✔
171
    let hasNextView = false;
23✔
172
    let visibleWmsLayerCount = 0;
23✔
173
    const selection = getSelectionSet(state);
23✔
174
    hasSelection = (selection != null && selection.SelectedFeatures != null);
23✔
175
    if (state.config.activeMapName) {
23✔
176
        hasClientSelection = state.mapState[state.config.activeMapName].clientSelection != null;
10✔
177
        hasPreviousView = state.mapState[state.config.activeMapName].historyIndex > 0;
10✔
178
        hasNextView = state.mapState[state.config.activeMapName].historyIndex < state.mapState[state.config.activeMapName].history.length - 1;
10✔
179
        visibleWmsLayerCount = (state.mapState[state.config.activeMapName].layers ?? []).filter(l => l.visible && l.selectable && l.type == "WMS").length;
10!
180
    }
10✔
181
    return {
23✔
182
        stateless: state.config.viewer.isStateless,
23✔
183
        visibleAndSelectableWmsLayerCount: visibleWmsLayerCount,
23✔
184
        busyWorkerCount: state.viewer.busyCount,
23✔
185
        hasSelection,
23✔
186
        hasClientSelection,
23✔
187
        hasPreviousView,
23✔
188
        hasNextView,
23✔
189
        activeTool: state.viewer.tool,
23✔
190
        featureTooltipsEnabled: state.viewer.featureTooltipsEnabled
23✔
191
    }
23✔
192
}
23✔
193

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

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

331
const commands: Dictionary<ICommand> = {};
1✔
332

UNCOV
333
function isInvokeUrlCommand(cmdDef: any): cmdDef is IInvokeUrlCommand {
×
334
    return typeof cmdDef.url !== 'undefined';
×
UNCOV
335
}
×
336

UNCOV
337
function isSearchCommand(cmdDef: any): cmdDef is ISearchCommand {
×
338
    return typeof cmdDef.layer !== 'undefined';
×
UNCOV
339
}
×
340

UNCOV
341
function openModalUrl(name: string, dispatch: ReduxDispatch, url: string, modalTitle?: string) {
×
342
    dispatch(showModalUrl({
×
UNCOV
343
        modal: {
×
UNCOV
344
            title: modalTitle || tr(name as any),
×
UNCOV
345
            backdrop: false,
×
UNCOV
346
            overflowYScroll: true
×
UNCOV
347
        },
×
UNCOV
348
        name: name,
×
UNCOV
349
        url: url
×
UNCOV
350
    }));
×
UNCOV
351
}
×
352

353
export function isSupportedCommandInStatelessMode(name: string | undefined) {
1✔
354
    switch (name) {
70✔
355
        case DefaultCommands.MapTip:
70✔
356
        case DefaultCommands.QuickPlot:
70✔
357
        case DefaultCommands.SelectRadius:
70✔
358
        case DefaultCommands.SelectPolygon:
70✔
359
        case DefaultCommands.Buffer:
70✔
360
        case DefaultCommands.SelectWithin:
70✔
361
        case DefaultCommands.Redline:
70✔
362
        case DefaultCommands.FeatureInfo:
70✔
363
        case DefaultCommands.Query:
70✔
364
        case DefaultCommands.Theme:
70✔
365
        case DefaultCommands.CenterSelection:
70✔
366
            return false;
19✔
367
    }
70✔
368
    return true;
51✔
369
}
51✔
370

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

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

481
/**
482
 * Gets a registered viewer command by its name
483
 *
484
 * @export
485
 * @param {string} name
486
 * @returns {(ICommand | undefined)}
487
 */
488
export function getCommand(name: string): ICommand | undefined {
1✔
489
    return commands[name];
×
UNCOV
490
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc