Skip to content

Commit 74bee0c

Browse files
authored
#2247 - Show pending indicator when new unsaved resource is exited (#2269)
1 parent 0795420 commit 74bee0c

File tree

3 files changed

+325
-15
lines changed

3 files changed

+325
-15
lines changed

geonode_mapstore_client/client/js/epics/gnresource.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,10 @@ const resourceTypes = {
323323
newResourceObservable: (options) =>
324324
Observable.defer(() => getNewGeoStoryConfig())
325325
.switchMap((gnGeoStory) => {
326+
const currentStory = options.data || {...gnGeoStory, sections: [{...gnGeoStory.sections[0], id: uuid(),
327+
contents: [{...gnGeoStory.sections[0].contents[0], id: uuid()}]}]};
326328
return Observable.of(
327-
setCurrentStory(options.data || {...gnGeoStory, sections: [{...gnGeoStory.sections[0], id: uuid(),
328-
contents: [{...gnGeoStory.sections[0].contents[0], id: uuid()}]}]}),
329+
setCurrentStory({...currentStory, defaultGeoStoryConfig: {...currentStory}}),
329330
setEditing(true),
330331
setGeoStoryResource({
331332
canEdit: true

geonode_mapstore_client/client/js/selectors/__tests__/resource-test.js

Lines changed: 269 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@ import expect from 'expect';
1010
import {
1111
getViewedResourceType,
1212
isNewResource,
13+
isNewResourcePk,
1314
getGeoNodeResourceDataFromGeoStory,
1415
getGeoNodeResourceFromDashboard,
1516
getResourceThumbnail,
1617
updatingThumbnailResource,
1718
isThumbnailChanged,
1819
canEditPermissions,
1920
canManageResourcePermissions,
20-
isNewMapViewerResource,
21+
isNewMapDirty,
22+
isNewDashboardDirty,
23+
isNewGeoStoryDirty,
2124
defaultViewerPluginsSelector
2225
} from '../resource';
2326
import { ResourceTypes } from '@js/utils/ResourceUtils';
@@ -62,6 +65,16 @@ describe('resource selector', () => {
6265
it('is new resource', () => {
6366
expect(isNewResource(testState)).toBeTruthy();
6467
});
68+
69+
it('is new resource by pk', () => {
70+
let state = {...testState, gnresource: {...testState.gnresource, params: {pk: "new"}}};
71+
expect(isNewResourcePk(state)).toBeTruthy();
72+
state.gnresource.params.pk = '1';
73+
expect(isNewResourcePk(state)).toBeFalsy();
74+
state.gnresource.params = undefined;
75+
expect(isNewResourcePk(state)).toBeFalsy();
76+
});
77+
6578
it('getGeoNodeResourceDataFromGeoStory', () => {
6679
expect(getGeoNodeResourceDataFromGeoStory(testState)).toEqual({ maps: [300], documents: [200, 100] });
6780
});
@@ -92,17 +105,266 @@ describe('resource selector', () => {
92105
expect(canManageResourcePermissions(state)).toBeFalsy();
93106
state.gnresource.data.perms = undefined;
94107
});
95-
it('test isNewMapViewerResource', () => {
96-
let state = {...testState, gnresource: {...testState.gnresource, type: ResourceTypes.VIEWER, params: {pk: "new"}}};
97-
expect(isNewMapViewerResource(state)).toBeTruthy();
98-
state.gnresource.params.pk = '1';
99-
expect(isNewMapViewerResource(state)).toBeFalsy();
100-
});
101108
it('test defaultViewerPluginsSelector', () => {
102109
let state = {...testState};
103110
state.gnresource = {...state.gnresource, defaultViewerPlugins: ["TOC"]};
104111
expect(defaultViewerPluginsSelector(state)).toEqual(["TOC"]);
105112
state.gnresource = {...state.gnresource, defaultViewerPlugins: undefined};
106113
expect(defaultViewerPluginsSelector(state)).toEqual([]);
107114
});
115+
116+
it('test isNewMapDirty returns false when no mapConfigRawData exists', () => {
117+
const state = {
118+
gnresource: {
119+
type: ResourceTypes.MAP
120+
},
121+
map: {
122+
present: {
123+
zoom: 5,
124+
center: { x: 0, y: 0, crs: 'EPSG:4326' }
125+
}
126+
}
127+
// No mapConfigRawData
128+
};
129+
expect(isNewMapDirty(state)).toBeFalsy();
130+
});
131+
132+
it('test isNewMapDirty returns false when map has not changed from initial config', () => {
133+
const initialConfig = {
134+
version: 2,
135+
map: {
136+
zoom: 5,
137+
center: { x: 0, y: 0, crs: 'EPSG:4326' },
138+
layers: [
139+
{ id: 'layer1', type: 'osm', group: 'background', visibility: true }
140+
]
141+
}
142+
};
143+
const state = {
144+
gnresource: {
145+
type: ResourceTypes.MAP
146+
},
147+
map: {
148+
present: {
149+
zoom: 5,
150+
center: { x: 0, y: 0, crs: 'EPSG:4326' },
151+
layers: [
152+
{ id: 'layer1', type: 'osm', group: 'background', visibility: true }
153+
]
154+
}
155+
},
156+
layers: {
157+
flat: [
158+
{ id: 'layer1', type: 'osm', group: 'background', visibility: true }
159+
]
160+
},
161+
mapConfigRawData: initialConfig
162+
};
163+
expect(isNewMapDirty(state)).toBeFalsy();
164+
});
165+
166+
it('test isNewMapDirty returns true when layers are added', () => {
167+
const initialConfig = {
168+
version: 2,
169+
map: {
170+
zoom: 5,
171+
center: { x: 0, y: 0, crs: 'EPSG:4326' },
172+
layers: [
173+
{ id: 'layer1', type: 'osm', group: 'background', visibility: true }
174+
]
175+
}
176+
};
177+
const state = {
178+
gnresource: {
179+
type: ResourceTypes.MAP
180+
},
181+
map: {
182+
present: {
183+
zoom: 5,
184+
center: { x: 0, y: 0, crs: 'EPSG:4326' },
185+
layers: [
186+
{ id: 'layer1', type: 'osm', group: 'background', visibility: true },
187+
{ id: 'layer2', type: 'wms', name: 'newLayer', visibility: true }
188+
]
189+
}
190+
},
191+
layers: {
192+
flat: [
193+
{ id: 'layer1', type: 'osm', group: 'background', visibility: true },
194+
{ id: 'layer2', type: 'wms', name: 'newLayer', visibility: true }
195+
]
196+
},
197+
mapConfigRawData: initialConfig
198+
};
199+
expect(isNewMapDirty(state)).toBeTruthy();
200+
});
201+
202+
it('test isNewMapDirty ignores ellipsoid terrain layer', () => {
203+
const initialConfig = {
204+
version: 2,
205+
map: {
206+
zoom: 5,
207+
center: { x: 0, y: 0, crs: 'EPSG:4326' },
208+
layers: [
209+
{ id: 'layer1', type: 'osm', group: 'background', visibility: true }
210+
]
211+
}
212+
};
213+
const state = {
214+
gnresource: {
215+
type: ResourceTypes.MAP
216+
},
217+
map: {
218+
present: {
219+
zoom: 5,
220+
center: { x: 0, y: 0, crs: 'EPSG:4326' },
221+
layers: [
222+
{ id: 'layer1', type: 'osm', group: 'background', visibility: true },
223+
{ id: 'ellipsoid', type: 'terrain', provider: 'ellipsoid', group: 'background' }
224+
]
225+
}
226+
},
227+
layers: {
228+
flat: [
229+
{ id: 'layer1', type: 'osm', group: 'background', visibility: true },
230+
{ id: 'ellipsoid', type: 'terrain', provider: 'ellipsoid', group: 'background' }
231+
]
232+
},
233+
mapConfigRawData: initialConfig
234+
};
235+
// Should be false because ellipsoid terrain is filtered out by compareMapChanges
236+
expect(isNewMapDirty(state)).toBeFalsy();
237+
});
238+
239+
it('test isNewDashboardDirty returns true when dashboard has widgets', () => {
240+
const state = {
241+
gnresource: {
242+
type: ResourceTypes.DASHBOARD
243+
},
244+
widgets: {
245+
containers: {
246+
floating: {
247+
widgets: [
248+
{ id: 'widget1', widgetType: 'text' }
249+
]
250+
}
251+
}
252+
}
253+
};
254+
expect(isNewDashboardDirty(state)).toBeTruthy();
255+
});
256+
257+
it('test isNewDashboardDirty returns false when dashboard has no widgets', () => {
258+
const state = {
259+
gnresource: {
260+
type: ResourceTypes.DASHBOARD
261+
},
262+
widgets: {
263+
containers: {
264+
floating: {
265+
widgets: []
266+
}
267+
}
268+
}
269+
};
270+
expect(isNewDashboardDirty(state)).toBeFalsy();
271+
});
272+
273+
it('test isNewGeoStoryDirty returns false for default geostory', () => {
274+
const defaultConfig = {
275+
sections: [{ title: 'Default Title', contents: [{ html: '' }] }],
276+
settings: {}
277+
};
278+
const state = {
279+
gnresource: {
280+
type: ResourceTypes.GEOSTORY
281+
},
282+
geostory: {
283+
currentStory: {
284+
...defaultConfig,
285+
defaultGeoStoryConfig: defaultConfig,
286+
resources: []
287+
}
288+
}
289+
};
290+
expect(isNewGeoStoryDirty(state)).toBeFalsy();
291+
});
292+
293+
it('test isNewGeoStoryDirty returns true when geostory has multiple sections', () => {
294+
const defaultConfig = {
295+
sections: [{ title: 'Default Title', contents: [{ html: '' }] }],
296+
settings: {}
297+
};
298+
const state = {
299+
gnresource: {
300+
type: ResourceTypes.GEOSTORY
301+
},
302+
geostory: {
303+
currentStory: {
304+
sections: [
305+
{ title: 'Section 1', contents: [{ html: '' }] },
306+
{ title: 'Section 2', contents: [{ html: '' }] }
307+
],
308+
defaultGeoStoryConfig: defaultConfig,
309+
resources: [],
310+
settings: {}
311+
}
312+
}
313+
};
314+
expect(isNewGeoStoryDirty(state)).toBeTruthy();
315+
});
316+
317+
it('test isNewGeoStoryDirty returns true when geostory has resources', () => {
318+
const defaultConfig = {
319+
sections: [{ title: 'Default Title', contents: [{ html: '' }] }],
320+
settings: {}
321+
};
322+
const state = {
323+
gnresource: {
324+
type: ResourceTypes.GEOSTORY
325+
},
326+
geostory: {
327+
currentStory: {
328+
sections: [{ title: 'Default Title', contents: [{ html: '' }] }],
329+
defaultGeoStoryConfig: defaultConfig,
330+
resources: [{ id: 1, type: 'map' }],
331+
settings: {}
332+
}
333+
}
334+
};
335+
expect(isNewGeoStoryDirty(state)).toBeTruthy();
336+
});
337+
338+
it('test isNewGeoStoryDirty returns true when title section has content', () => {
339+
const defaultConfig = {
340+
sections: [{ title: 'Default Title', contents: [{ html: '' }] }],
341+
settings: {}
342+
};
343+
const state = {
344+
gnresource: {
345+
type: ResourceTypes.GEOSTORY
346+
},
347+
geostory: {
348+
currentStory: {
349+
sections: [{ title: 'Default Title', contents: [{ html: 'Some content here' }] }],
350+
defaultGeoStoryConfig: defaultConfig,
351+
resources: [],
352+
settings: {}
353+
}
354+
}
355+
};
356+
expect(isNewGeoStoryDirty(state)).toBeTruthy();
357+
});
358+
359+
it('test isNewGeoStoryDirty returns false when currentData is null', () => {
360+
const state = {
361+
gnresource: {
362+
type: ResourceTypes.GEOSTORY
363+
},
364+
geostory: {
365+
currentStory: null
366+
}
367+
};
368+
expect(isNewGeoStoryDirty(state)).toBeFalsy();
369+
});
108370
});

geonode_mapstore_client/client/js/selectors/resource.js

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -337,16 +337,63 @@ function isResourceDataEqual(state, initialData = {}, currentData = {}) {
337337
}
338338
}
339339

340-
export const isNewMapViewerResource = (state) => {
341-
const isNew = state?.gnresource?.params?.pk === "new";
342-
const isMapViewer = state?.gnresource?.type === ResourceTypes.VIEWER;
343-
return isNew && isMapViewer;
340+
export const isNewResourcePk = (state) => {
341+
return state?.gnresource?.params?.pk === "new";
344342
};
345343

346-
export const getResourceDirtyState = (state) => {
347-
if (isNewMapViewerResource(state)) {
344+
export const isNewMapDirty = (state) => {
345+
const mapConfigRawData = state?.mapConfigRawData;
346+
if (!mapConfigRawData) {
347+
return false;
348+
}
349+
const currentMapData = mapSaveSelector(state);
350+
return !compareMapChanges(mapConfigRawData, currentMapData);
351+
};
352+
353+
export const isNewDashboardDirty = (state) => {
354+
const currentData = getDataPayload(state, ResourceTypes.DASHBOARD);
355+
return (currentData?.widgets || []).length > 0;
356+
};
357+
358+
export const isNewGeoStoryDirty = (state) => {
359+
const currentData = getDataPayload(state, ResourceTypes.GEOSTORY);
360+
if (!currentData) return false;
361+
362+
const defaultConfig = currentStorySelector(state)?.defaultGeoStoryConfig ?? {};
363+
return (
364+
currentData.sections?.length > 1 || // More than the default title section
365+
currentData.sections?.[0]?.contents?.[0]?.html?.trim() || // Title section has content
366+
currentData.sections?.[0]?.title !== defaultConfig.sections?.[0]?.title || // Title changed from default
367+
currentData.resources?.length > 0 || // Has resources
368+
!isEqual( // Settings changed from default
369+
omitBy(currentData.settings || {}, isNil),
370+
omitBy(defaultConfig.settings || {}, isNil)
371+
)
372+
);
373+
};
374+
375+
const isNewResourceDirty = (state) => {
376+
const resourceType = state?.gnresource?.type;
377+
378+
switch (resourceType) {
379+
case ResourceTypes.MAP:
380+
return isNewMapDirty(state);
381+
case ResourceTypes.VIEWER:
348382
return true;
383+
case ResourceTypes.DASHBOARD:
384+
return isNewDashboardDirty(state);
385+
case ResourceTypes.GEOSTORY:
386+
return isNewGeoStoryDirty(state);
387+
default:
388+
return false;
349389
}
390+
};
391+
392+
export const getResourceDirtyState = (state) => {
393+
if (isNewResourcePk(state)) {
394+
return isNewResourceDirty(state);
395+
}
396+
350397
const canEdit = canEditPermissions(state);
351398
const isDeleting = getCurrentResourceDeleteLoading(state);
352399
const isCopying = getCurrentResourceCopyLoading(state);

0 commit comments

Comments
 (0)