feat: add atomic undo redo (#2570)
Todo: - [ ] undo/sync a version restore - [x] add test for scenario where two peers create a layer and one click on save (both should be saved) - [x] undo/sync of conditional rules - [x] undo/sync of filter management from table editor
|
@ -14,11 +14,12 @@
|
|||
background-color: inherit;
|
||||
}
|
||||
.leaflet-container .edit-save,
|
||||
.leaflet-container .edit-cancel,
|
||||
.leaflet-container .edit-undo,
|
||||
.leaflet-container .edit-redo,
|
||||
.leaflet-container .edit-disable,
|
||||
.leaflet-container .connected-peers
|
||||
{
|
||||
display: block;
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
line-height: 30px;
|
||||
padding: 0 20px;
|
||||
|
@ -39,7 +40,8 @@
|
|||
color: var(--color-darkGray);
|
||||
}
|
||||
|
||||
.leaflet-container .edit-cancel:hover,
|
||||
.leaflet-container .edit-undo:hover,
|
||||
.leaflet-container .edit-redo:hover,
|
||||
.leaflet-container .edit-disable:hover {
|
||||
border: 0.5px solid rgba(153, 153, 153, 0.80);
|
||||
text-decoration: none;
|
||||
|
@ -76,19 +78,13 @@
|
|||
background: rgba(66, 236, 230, 0.10);
|
||||
}
|
||||
.leaflet-container .edit-save,
|
||||
.leaflet-container .edit-cancel,
|
||||
.leaflet-container .edit-disable,
|
||||
.umap-edit-enabled .edit-enable {
|
||||
display: none;
|
||||
}
|
||||
.umap-edit-enabled .edit-save,
|
||||
.umap-edit-enabled .edit-disable,
|
||||
.umap-edit-enabled.umap-is-dirty .edit-cancel {
|
||||
.umap-edit-enabled .edit-disable {
|
||||
display: inline-block;
|
||||
}
|
||||
.umap-is-dirty .edit-disable {
|
||||
display: none;
|
||||
}
|
||||
.umap-caption-bar {
|
||||
display: none;
|
||||
}
|
||||
|
@ -115,8 +111,6 @@
|
|||
.umap-right-edit-toolbox {
|
||||
display: flex;
|
||||
column-gap: 10px;
|
||||
}
|
||||
.umap-right-edit-toolbox {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
@ -135,17 +129,20 @@
|
|||
text-indent: -9999px;
|
||||
}
|
||||
.umap-main-edit-toolbox .map-name {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: bold;
|
||||
text-align: start;
|
||||
}
|
||||
.truncate {
|
||||
display: inline-flex;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.umap-main-edit-toolbox .username {
|
||||
max-width: 100px;
|
||||
}
|
||||
.umap-main-edit-toolbox .share-status {
|
||||
font-style: italic;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.map-name:after {
|
||||
content: '\00a0';
|
||||
|
@ -242,3 +239,14 @@
|
|||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
@media all and (max-width: 980px) {
|
||||
.umap-main-edit-toolbox button span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media all and (max-width: 770px) {
|
||||
.umap-main-edit-toolbox .umap-help-link,
|
||||
.umap-main-edit-toolbox .share-status {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,11 +167,15 @@ html[dir="rtl"] .icon {
|
|||
.icon-profile {
|
||||
background-position: 0 calc(var(--tile) * 4);
|
||||
}
|
||||
.icon-redo {
|
||||
background-position: calc(var(--tile) * 3) calc(var(--tile) * 7);
|
||||
}
|
||||
.icon-resize {
|
||||
background-position: calc(var(--tile) * 3) calc(var(--tile) * 6);
|
||||
}
|
||||
.icon-undo,
|
||||
.icon-restore {
|
||||
background-position: calc(var(--tile) * 5) calc(var(--tile) * 3);
|
||||
background-position: calc(var(--tile) * 2) calc(var(--tile) * 7);
|
||||
}
|
||||
.expanded .icon-resize {
|
||||
background-position: calc(var(--tile) * 2) calc(var(--tile) * 6);
|
||||
|
|
|
@ -18,6 +18,9 @@
|
|||
<clipPath id="clip0_3071_861">
|
||||
<rect id="rect3" width="18" height="20" fill="#fff"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip0_241_10857-3">
|
||||
<rect id="rect586-6" width="18.05" height="19.01" fill="#fff"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<metadata id="metadata7">
|
||||
<rdf:RDF>
|
||||
|
@ -67,9 +70,12 @@
|
|||
</g>
|
||||
</g>
|
||||
<path id="path4873" d="m108.15 816.36v3.8267h3.8544v-3.8267zm0.51755 4.3517-1.2459 2.3132 1.1669 0.61848 1.244-2.3151zm-1.8689 3.4717-0.27666 0.51571h-2.426v2.2441l-4.0916 4.064 1.3626 1.3528 3.862-3.8342h2.7214v-3.6959l0.015-0.028-0.015-8e-3v-0.0953h-0.17879l-0.97303-0.51571z" color="#000000" color-rendering="auto" fill="#f2f2f2" fill-rule="evenodd" image-rendering="auto" shape-rendering="auto" solid-color="#000000" stroke="#999" stroke-width=".25" style="isolation:auto;mix-blend-mode:normal;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none;white-space:normal"/>
|
||||
<g id="g4244" transform="matrix(.51357 -.54309 .54309 .51357 -518.02 506.23)">
|
||||
<g id="g4244" transform="matrix(.51357 -.54309 .54309 .51357 -590.02 601.73)">
|
||||
<path id="path4240" transform="matrix(.91922 .97205 -.97205 .91922 152.14 647.93)" d="m220.49 133.52c-0.33017 0.01-0.66239 0.0456-0.99414 0.10742-2.2249 0.41425-4.0267 1.9575-4.832 4.0098l-0.87696-1.8164a0.50005 0.50005 0 0 0-0.83398-0.11328 0.50005 0.50005 0 0 0-0.0664 0.54883l1.459 3.0195a0.50005 0.50005 0 0 0 0.60742 0.25586l2.8438-0.94532a0.50028 0.50028 0 1 0-0.31641-0.94922l-2.002 0.66797c0.61312-1.8902 2.2073-3.3241 4.2012-3.6953 2.2474-0.41845 4.5146 0.59912 5.6953 2.5566 1.1807 1.9575 1.022 4.4377-0.39648 6.2305-1.4185 1.7928-3.7961 2.5154-5.9727 1.8164a0.50005 0.50005 0 1 0-0.30469 0.95117c2.5704 0.82539 5.3874-0.0294 7.0625-2.1465 0.4188-0.52924 0.74532-1.1114 0.97657-1.7207 0.69373-1.828 0.53599-3.9147-0.50977-5.6484-1.22-2.0227-3.4291-3.1977-5.7402-3.1289z" color="#000000" color-rendering="auto" fill="#f2f2f2" image-rendering="auto" shape-rendering="auto" solid-color="#000000" stroke="#999" stroke-linecap="round" stroke-linejoin="round" stroke-width=".25" style="isolation:auto;mix-blend-mode:normal;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none;white-space:normal"/>
|
||||
</g>
|
||||
<g id="g4244-6" transform="matrix(-.51357 -.54309 -.54309 .51357 734.02 601.73)">
|
||||
<path id="path4240-7" transform="matrix(.91922 .97205 -.97205 .91922 152.14 647.93)" d="m220.49 133.52c-0.33017 0.01-0.66239 0.0456-0.99414 0.10742-2.2249 0.41425-4.0267 1.9575-4.832 4.0098l-0.87696-1.8164a0.50005 0.50005 0 0 0-0.83398-0.11328 0.50005 0.50005 0 0 0-0.0664 0.54883l1.459 3.0195a0.50005 0.50005 0 0 0 0.60742 0.25586l2.8438-0.94532a0.50028 0.50028 0 1 0-0.31641-0.94922l-2.002 0.66797c0.61312-1.8902 2.2073-3.3241 4.2012-3.6953 2.2474-0.41845 4.5146 0.59912 5.6953 2.5566 1.1807 1.9575 1.022 4.4377-0.39648 6.2305-1.4185 1.7928-3.7961 2.5154-5.9727 1.8164a0.50005 0.50005 0 1 0-0.30469 0.95117c2.5704 0.82539 5.3874-0.0294 7.0625-2.1465 0.4188-0.52924 0.74532-1.1114 0.97657-1.7207 0.69373-1.828 0.53599-3.9147-0.50977-5.6484-1.22-2.0227-3.4291-3.1977-5.7402-3.1289z" color="#000000" color-rendering="auto" fill="#f2f2f2" image-rendering="auto" shape-rendering="auto" solid-color="#000000" stroke="#999" stroke-linecap="round" stroke-linejoin="round" stroke-width=".25" style="isolation:auto;mix-blend-mode:normal;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none;white-space:normal"/>
|
||||
</g>
|
||||
<g id="delete-vertex" transform="translate(-90,-64)">
|
||||
<path id="path4349-2-2-9-3-8-3-5" transform="translate(0 852.36)" d="m227.55 53.105-6.0547 3.0273h-3.4414v3.5176l-2.9512 5.9023 1.7891 0.89453 3.1562-6.3145h2.0059v-2.043l6.3906-3.1953z" color="#000000" color-rendering="auto" fill="#f2f2f2" image-rendering="auto" shape-rendering="auto" solid-color="#000000" stroke="#999" stroke-width=".25" style="isolation:auto;mix-blend-mode:normal;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none;white-space:normal"/>
|
||||
<path id="path4353-1-6-1-3-5-1-9" d="m217 907.36 6 6" fill="none" stroke="#fff" stroke-width="2.482"/>
|
||||
|
@ -143,6 +149,10 @@
|
|||
<path id="path580" d="m1.07 4.41h9.42c0.9234-0.0066 1.8391 0.16957 2.6941 0.5184 0.8551 0.34883 1.6327 0.8634 2.288 1.5141s1.1754 1.4246 1.5303 2.2771c0.3549 0.85256 0.5376 1.767 0.5376 2.6904 0.0067 0.9277-0.1712 1.8474-0.5231 2.7058-0.3519 0.8583-0.871 1.6382-1.527 2.2941-0.656 0.656-1.4358 1.1751-2.2941 1.527-0.8584 0.352-1.7781 0.5298-2.7058 0.5231h-9.42"/>
|
||||
<path id="path582" d="m4.75 8.45-4.04-4.05 4.04-4.05"/>
|
||||
</g>
|
||||
<g id="undo-5" transform="matrix(-.71301 0 0 .66261 90.012 938.13)" clip-path="url(#clip0_241_10857-3)" fill="none" stroke="#f2f2f2" stroke-miterlimit="10" stroke-width="1.4549">
|
||||
<path id="path580-3" d="m1.07 4.41h9.42c0.9234-0.0066 1.8391 0.16957 2.6941 0.5184 0.8551 0.34883 1.6327 0.8634 2.288 1.5141s1.1754 1.4246 1.5303 2.2771c0.3549 0.85256 0.5376 1.767 0.5376 2.6904 0.0067 0.9277-0.1712 1.8474-0.5231 2.7058-0.3519 0.8583-0.871 1.6382-1.527 2.2941-0.656 0.656-1.4358 1.1751-2.2941 1.527-0.8584 0.352-1.7781 0.5298-2.7058 0.5231h-9.42"/>
|
||||
<path id="path582-5" d="m4.75 8.45-4.04-4.05 4.04-4.05"/>
|
||||
</g>
|
||||
<g id="g1" transform="translate(144 -24)" fill="none" stroke="#999">
|
||||
<path id="path438" d="m9 849.94v4.05c0 0.20708 0.1679 0.375 0.375 0.375h5.25c0.20708 0 0.375-0.16792 0.375-0.375v-4.05c0-0.20708-0.16792-0.375-0.375-0.375h-5.25c-0.2071 0-0.375 0.16792-0.375 0.375z"/>
|
||||
<path id="save" d="m15.213 842.36h-8.8376c-0.2071 0-0.375 0.1679-0.375 0.37499v11.25c0 0.20708 0.1679 0.375 0.375 0.375h11.25c0.20708 0 0.375-0.16792 0.375-0.375v-8.6766c0-0.0953-0.0363-0.18697-0.1014-0.25648l-2.4124-2.5733c-0.07095-0.0756-0.16995-0.11853-0.2736-0.11853z"/>
|
||||
|
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 28 KiB |
|
@ -21,8 +21,11 @@
|
|||
<clipPath id="clip0_3071_861">
|
||||
<rect width="18" height="20" fill="#ffffff" id="rect3" x="0" y="0" />
|
||||
</clipPath>
|
||||
<clipPath id="clip0_241_10857-3">
|
||||
<rect width="18.049999" height="19.01" fill="#ffffff" id="rect586-6" x="0" y="0" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="22.311152" inkscape:cx="196.69536" inkscape:cy="36.932203" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="true" inkscape:window-width="1920" inkscape:window-height="1011" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" showguides="true" inkscape:guide-bbox="true" inkscape:snap-grids="true" inkscape:snap-to-guides="true" inkscape:showpageshadow="2" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1">
|
||||
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="4.4320814" inkscape:cx="116.08541" inkscape:cy="109.65503" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="true" inkscape:window-width="1920" inkscape:window-height="1011" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" showguides="true" inkscape:guide-bbox="true" inkscape:snap-grids="true" inkscape:snap-to-guides="true" inkscape:showpageshadow="2" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1">
|
||||
<inkscape:grid type="xygrid" id="grid3004" empspacing="4" visible="true" enabled="true" snapvisiblegridlinesonly="true" originx="0" originy="0" spacingy="1" spacingx="1" units="px" />
|
||||
<inkscape:grid id="grid1" units="px" originx="0" originy="0" spacingx="24" spacingy="24" empcolor="#203fff" empopacity="0.85490196" color="#3f3fff" opacity="0.1254902" empspacing="1" enabled="true" visible="true" />
|
||||
</sodipodi:namedview>
|
||||
|
@ -78,9 +81,12 @@
|
|||
</g>
|
||||
</g>
|
||||
<path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#f2f2f2;fill-opacity:1;fill-rule:evenodd;stroke:#999999;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 108.14555,816.36218 v 3.8267 h 3.85445 v -3.8267 z m 0.51755,4.35174 -1.24591,2.31321 1.16687,0.61848 1.24404,-2.31507 z m -1.86888,3.47168 -0.27666,0.51571 h -2.42597 v 2.24408 l -4.09159,4.06399 1.36261,1.3528 3.86198,-3.83417 h 2.72145 v -3.6959 l 0.015,-0.028 -0.015,-0.008 v -0.0953 h -0.17879 l -0.97303,-0.51571 z" id="path4873" inkscape:connector-curvature="0" />
|
||||
<g id="g4244" transform="matrix(0.51357238,-0.54309229,0.54309229,0.51357238,-518.0199,506.22551)">
|
||||
<g id="g4244" transform="matrix(0.51357238,-0.54309229,0.54309229,0.51357238,-590.0195,601.72586)">
|
||||
<path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:#999999;stroke-width:0.25;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 220.49219,133.52344 c -0.33017,0.01 -0.66239,0.0456 -0.99414,0.10742 -2.22487,0.41425 -4.02666,1.95747 -4.83203,4.00976 l -0.87696,-1.8164 a 0.50004998,0.50004998 0 0 0 -0.83398,-0.11328 0.50004998,0.50004998 0 0 0 -0.0664,0.54883 l 1.45899,3.01953 a 0.50004998,0.50004998 0 0 0 0.60742,0.25586 l 2.84375,-0.94532 a 0.50028339,0.50028339 0 1 0 -0.31641,-0.94922 l -2.00195,0.66797 c 0.61312,-1.89015 2.20733,-3.32407 4.20117,-3.69531 2.24744,-0.41845 4.51458,0.59912 5.69531,2.55664 1.18073,1.95754 1.02202,4.43774 -0.39648,6.23047 -1.41851,1.79275 -3.79606,2.51535 -5.97266,1.81641 a 0.50004998,0.50004998 0 1 0 -0.30469,0.95117 c 2.57038,0.82539 5.38736,-0.0294 7.0625,-2.14649 0.4188,-0.52924 0.74532,-1.11137 0.97657,-1.7207 0.69373,-1.828 0.53599,-3.91467 -0.50977,-5.64844 -1.22005,-2.02271 -3.42908,-3.19767 -5.74023,-3.1289 z" transform="matrix(0.91921787,0.9720541,-0.9720541,0.91921787,152.1356,647.93271)" id="path4240" inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<g id="g4244-6" transform="matrix(-0.51357238,-0.54309229,-0.54309229,0.51357238,734.0195,601.72586)">
|
||||
<path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:#999999;stroke-width:0.25;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 220.49219,133.52344 c -0.33017,0.01 -0.66239,0.0456 -0.99414,0.10742 -2.22487,0.41425 -4.02666,1.95747 -4.83203,4.00976 l -0.87696,-1.8164 a 0.50004998,0.50004998 0 0 0 -0.83398,-0.11328 0.50004998,0.50004998 0 0 0 -0.0664,0.54883 l 1.45899,3.01953 a 0.50004998,0.50004998 0 0 0 0.60742,0.25586 l 2.84375,-0.94532 a 0.50028339,0.50028339 0 1 0 -0.31641,-0.94922 l -2.00195,0.66797 c 0.61312,-1.89015 2.20733,-3.32407 4.20117,-3.69531 2.24744,-0.41845 4.51458,0.59912 5.69531,2.55664 1.18073,1.95754 1.02202,4.43774 -0.39648,6.23047 -1.41851,1.79275 -3.79606,2.51535 -5.97266,1.81641 a 0.50004998,0.50004998 0 1 0 -0.30469,0.95117 c 2.57038,0.82539 5.38736,-0.0294 7.0625,-2.14649 0.4188,-0.52924 0.74532,-1.11137 0.97657,-1.7207 0.69373,-1.828 0.53599,-3.91467 -0.50977,-5.64844 -1.22005,-2.02271 -3.42908,-3.19767 -5.74023,-3.1289 z" transform="matrix(0.91921787,0.9720541,-0.9720541,0.91921787,152.1356,647.93271)" id="path4240-7" inkscape:connector-curvature="0" />
|
||||
</g>
|
||||
<g id="delete-vertex" transform="translate(-90,-64)">
|
||||
<path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:#999999;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 227.55273,53.105469 -6.05468,3.027343 h -3.44141 v 3.517579 l -2.95117,5.902343 1.78906,0.894532 3.15625,-6.314454 h 2.00586 v -2.042968 l 6.39063,-3.195313 z" transform="translate(0,852.36218)" id="path4349-2-2-9-3-8-3-5" inkscape:connector-curvature="0" />
|
||||
<path sodipodi:nodetypes="cc" style="fill:none;stroke:#ffffff;stroke-width:2.482;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m 217,907.36218 6,6" id="path4353-1-6-1-3-5-1-9" inkscape:connector-curvature="0" />
|
||||
|
@ -154,6 +160,10 @@
|
|||
<path d="m 1.07001,4.41003 h 9.41999 c 0.9234,-0.0066 1.8391,0.16957 2.6941,0.5184 0.8551,0.34883 1.6327,0.8634 2.288,1.51407 0.6553,0.65067 1.1754,1.42458 1.5303,2.27713 0.3549,0.85256 0.5376,1.76697 0.5376,2.69037 0.0067,0.9277 -0.1712,1.8474 -0.5231,2.7058 -0.3519,0.8583 -0.871,1.6382 -1.527,2.2941 -0.656,0.656 -1.4358,1.1751 -2.2941,1.527 -0.8584,0.352 -1.7781,0.5298 -2.7058,0.5231 h -9.41999" stroke="#f2f2f2" stroke-miterlimit="10" id="path580" style="fill:none;stroke:#f2f2f2;stroke-width:1.45486;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path d="m 4.75002,8.44998 -4.039998,-4.05 4.039998,-4.050004" stroke="#f2f2f2" stroke-miterlimit="10" id="path582" style="fill:none;stroke:#f2f2f2;stroke-width:1.45486;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
<g clip-path="url(#clip0_241_10857-3)" id="undo-5" transform="matrix(-0.71300568,0,0,0.66260978,90.012499,938.13028)" style="fill:none;stroke:#f2f2f2;stroke-width:1.45488;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1">
|
||||
<path d="m 1.07001,4.41003 h 9.41999 c 0.9234,-0.0066 1.8391,0.16957 2.6941,0.5184 0.8551,0.34883 1.6327,0.8634 2.288,1.51407 0.6553,0.65067 1.1754,1.42458 1.5303,2.27713 0.3549,0.85256 0.5376,1.76697 0.5376,2.69037 0.0067,0.9277 -0.1712,1.8474 -0.5231,2.7058 -0.3519,0.8583 -0.871,1.6382 -1.527,2.2941 -0.656,0.656 -1.4358,1.1751 -2.2941,1.527 -0.8584,0.352 -1.7781,0.5298 -2.7058,0.5231 h -9.41999" stroke="#f2f2f2" stroke-miterlimit="10" id="path580-3" style="fill:none;stroke:#f2f2f2;stroke-width:1.45486;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1" />
|
||||
<path d="m 4.75002,8.44998 -4.039998,-4.05 4.039998,-4.050004" stroke="#f2f2f2" stroke-miterlimit="10" id="path582-5" style="fill:none;stroke:#f2f2f2;stroke-width:1.45486;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</g>
|
||||
<g id="g1" transform="translate(144,-24.00004)">
|
||||
<path d="m 9,849.93721 v 4.04997 c 0,0.20708 0.167895,0.375 0.375,0.375 h 5.25 c 0.207075,0 0.375,-0.16792 0.375,-0.375 v -4.04997 c 0,-0.20708 -0.167925,-0.375 -0.375,-0.375 h -5.25 c -0.207105,0 -0.375,0.16792 -0.375,0.375 z" stroke="#f2f2f2" id="path438" style="fill:none;stroke:#999999;stroke-width:0.999997;stroke-opacity:1" />
|
||||
<path d="m 15.21255,842.36218 h -8.83755 c -0.207105,0 -0.375,0.1679 -0.375,0.37499 v 11.24993 c 0,0.20708 0.167895,0.375 0.375,0.375 h 11.25 c 0.207075,0 0.375,-0.16792 0.375,-0.375 v -8.67664 c 0,-0.0953 -0.0363,-0.18697 -0.1014,-0.25648 l -2.41245,-2.57327 c -0.07095,-0.0756 -0.16995,-0.11853 -0.2736,-0.11853 z" stroke="#f2f2f2" id="save" style="fill:none;stroke:#999999;stroke-width:0.999997;stroke-opacity:1" />
|
||||
|
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 50 KiB |
|
@ -91,6 +91,7 @@ class Feature {
|
|||
}
|
||||
|
||||
set geometry(value) {
|
||||
this._geometry_bk = Utils.CopyJSON(this._geometry)
|
||||
this._geometry = value
|
||||
this.pushGeometry()
|
||||
}
|
||||
|
@ -104,13 +105,15 @@ class Feature {
|
|||
}
|
||||
|
||||
pullGeometry(sync = true) {
|
||||
const oldGeometry = Utils.CopyJSON(this._geometry)
|
||||
this.fromLatLngs(this._getLatLngs())
|
||||
if (sync) {
|
||||
this.sync.update('geometry', this.geometry)
|
||||
this.sync.update('geometry', this.geometry, oldGeometry)
|
||||
}
|
||||
}
|
||||
|
||||
fromLatLngs(latlngs) {
|
||||
this._geometry_bk = Utils.CopyJSON(this._geometry)
|
||||
this._geometry = this.convertLatLngs(latlngs)
|
||||
}
|
||||
|
||||
|
@ -145,8 +148,15 @@ class Feature {
|
|||
onCommit() {
|
||||
// When the layer is a remote layer, we don't want to sync the creation of the
|
||||
// points via the websocket, as the other peers will get them themselves.
|
||||
const oldGeoJSON = this._just_married ? null : Utils.CopyJSON(this.toGeoJSON())
|
||||
this.pullGeometry(false)
|
||||
if (this.datalayer?.isRemoteLayer()) return
|
||||
this.sync.upsert(this.toGeoJSON())
|
||||
if (this._just_married) {
|
||||
this.sync.upsert(this.toGeoJSON(), null)
|
||||
this._just_married = false
|
||||
} else {
|
||||
this.sync.update('geometry', this.geometry, this._geometry_bk)
|
||||
}
|
||||
}
|
||||
|
||||
isReadOnly() {
|
||||
|
|
|
@ -16,7 +16,6 @@ import { Default as DefaultLayer } from '../rendering/layers/base.js'
|
|||
import { Categorized, Choropleth, Circles } from '../rendering/layers/classified.js'
|
||||
import { Cluster } from '../rendering/layers/cluster.js'
|
||||
import { Heat } from '../rendering/layers/heat.js'
|
||||
import { ServerStored } from '../saving.js'
|
||||
import * as Schema from '../schema.js'
|
||||
import TableEditor from '../tableeditor.js'
|
||||
import * as Utils from '../utils.js'
|
||||
|
@ -36,9 +35,8 @@ const LAYER_MAP = LAYER_TYPES.reduce((acc, klass) => {
|
|||
return acc
|
||||
}, {})
|
||||
|
||||
export class DataLayer extends ServerStored {
|
||||
export class DataLayer {
|
||||
constructor(umap, leafletMap, data = {}) {
|
||||
super()
|
||||
this._umap = umap
|
||||
this.sync = umap.syncEngine.proxy(this)
|
||||
this._index = Array()
|
||||
|
@ -49,7 +47,6 @@ export class DataLayer extends ServerStored {
|
|||
this._leafletMap = leafletMap
|
||||
this.parentPane = this._leafletMap.getPane('overlayPane')
|
||||
this.pane = this._leafletMap.createPane(`datalayer${stamp(this)}`, this.parentPane)
|
||||
this.pane.dataset.id = stamp(this)
|
||||
// FIXME: should be on layer
|
||||
this.renderer = L.svg({ pane: this.pane })
|
||||
this.defaultOptions = {
|
||||
|
@ -66,6 +63,7 @@ export class DataLayer extends ServerStored {
|
|||
data.id = data.id || crypto.randomUUID()
|
||||
|
||||
this.setOptions(data)
|
||||
this.pane.dataset.id = this.id
|
||||
|
||||
if (!Utils.isObject(this.options.remoteData)) {
|
||||
this.options.remoteData = {}
|
||||
|
@ -114,7 +112,6 @@ export class DataLayer extends ServerStored {
|
|||
|
||||
set isDeleted(status) {
|
||||
this._isDeleted = status
|
||||
if (status) this.isDirty = status
|
||||
}
|
||||
|
||||
get isDeleted() {
|
||||
|
@ -269,13 +266,11 @@ export class DataLayer extends ServerStored {
|
|||
}
|
||||
|
||||
clear() {
|
||||
this.layer.clearLayers()
|
||||
this._features = {}
|
||||
this._index = Array()
|
||||
if (this._geojson) {
|
||||
this.backupData()
|
||||
this._geojson = null
|
||||
this.sync.startBatch()
|
||||
for (const feature of Object.values(this._features)) {
|
||||
feature.del()
|
||||
}
|
||||
this.sync.commitBatch()
|
||||
this.dataChanged()
|
||||
}
|
||||
|
||||
|
@ -366,9 +361,8 @@ export class DataLayer extends ServerStored {
|
|||
}
|
||||
|
||||
connectToMap() {
|
||||
const id = stamp(this)
|
||||
if (!this._umap.datalayers[id]) {
|
||||
this._umap.datalayers[id] = this
|
||||
if (!this._umap.datalayers[this.id]) {
|
||||
this._umap.datalayers[this.id] = this
|
||||
}
|
||||
if (!this._umap.datalayersIndex.includes(this)) {
|
||||
this._umap.datalayersIndex.push(this)
|
||||
|
@ -417,7 +411,10 @@ export class DataLayer extends ServerStored {
|
|||
|
||||
removeFeature(feature, sync) {
|
||||
const id = stamp(feature)
|
||||
if (sync !== false) feature.sync.delete()
|
||||
if (sync !== false) {
|
||||
const oldValue = feature.toGeoJSON()
|
||||
feature.sync.delete(oldValue)
|
||||
}
|
||||
this.hideFeature(feature)
|
||||
delete this._umap.featuresIndex[feature.getSlug()]
|
||||
feature.disconnectFromDataLayer(this)
|
||||
|
@ -460,7 +457,10 @@ export class DataLayer extends ServerStored {
|
|||
try {
|
||||
// Do not fail if remote data is somehow invalid,
|
||||
// otherwise the layer becomes uneditable.
|
||||
return this.makeFeatures(geojson, sync)
|
||||
this.sync.startBatch()
|
||||
const features = this.makeFeatures(geojson, sync)
|
||||
this.sync.commitBatch()
|
||||
return features
|
||||
} catch (err) {
|
||||
console.debug('Error with DataLayer', this.id)
|
||||
console.error(err)
|
||||
|
@ -518,7 +518,7 @@ export class DataLayer extends ServerStored {
|
|||
}
|
||||
if (feature && !feature.isEmpty()) {
|
||||
this.addFeature(feature)
|
||||
if (sync) feature.onCommit()
|
||||
if (sync) feature.sync.upsert(feature.toGeoJSON(), null)
|
||||
return feature
|
||||
}
|
||||
}
|
||||
|
@ -527,10 +527,6 @@ export class DataLayer extends ServerStored {
|
|||
return this._umap.formatter
|
||||
.parse(raw, format)
|
||||
.then((geojson) => this.addData(geojson))
|
||||
.then((data) => {
|
||||
if (data?.length) this.isDirty = true
|
||||
return data
|
||||
})
|
||||
.catch((error) => {
|
||||
console.debug(error)
|
||||
Alert.error(translate('Import failed: invalid data'))
|
||||
|
@ -596,17 +592,17 @@ export class DataLayer extends ServerStored {
|
|||
}
|
||||
|
||||
del(sync = true) {
|
||||
const oldValue = Utils.CopyJSON(this.umapGeoJSON())
|
||||
this.erase()
|
||||
if (sync) {
|
||||
this.isDeleted = true
|
||||
this.sync.delete()
|
||||
this.sync.delete(oldValue)
|
||||
}
|
||||
}
|
||||
|
||||
empty() {
|
||||
if (this.isRemoteLayer()) return
|
||||
this.clear()
|
||||
this.isDirty = true
|
||||
}
|
||||
|
||||
clone() {
|
||||
|
@ -630,25 +626,6 @@ export class DataLayer extends ServerStored {
|
|||
this.clear()
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (!this.createdOnServer) {
|
||||
this.erase()
|
||||
return
|
||||
}
|
||||
|
||||
this.resetOptions()
|
||||
this.parentPane.appendChild(this.pane)
|
||||
if (this._leaflet_events_bk && !this._leaflet_events) {
|
||||
this._leaflet_events = this._leaflet_events_bk
|
||||
}
|
||||
this.clear()
|
||||
this.hide()
|
||||
if (this.isRemoteLayer()) this.fetchRemoteData()
|
||||
else if (this._geojson_bk) this.fromGeoJSON(this._geojson_bk)
|
||||
this.show()
|
||||
this.isDirty = false
|
||||
}
|
||||
|
||||
redraw() {
|
||||
if (!this.isVisible()) return
|
||||
this.eachFeature((feature) => feature.redraw())
|
||||
|
@ -940,11 +917,14 @@ export class DataLayer extends ServerStored {
|
|||
)
|
||||
if (!error) {
|
||||
if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat.
|
||||
if (geojson._umap_options) this.setOptions(geojson._umap_options)
|
||||
if (geojson._umap_options) {
|
||||
const oldOptions = Utils.CopyJSON(this.options)
|
||||
this.setOptions(geojson._umap_options)
|
||||
this.sync.update('options', this.options, oldOptions)
|
||||
}
|
||||
this.empty()
|
||||
if (this.isRemoteLayer()) this.fetchRemoteData()
|
||||
else this.addData(geojson)
|
||||
this.isDirty = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -1098,7 +1078,11 @@ export class DataLayer extends ServerStored {
|
|||
|
||||
setReferenceVersion({ response, sync }) {
|
||||
this._referenceVersion = response.headers.get('X-Datalayer-Version')
|
||||
if (sync) this.sync.update('_referenceVersion', this._referenceVersion)
|
||||
if (sync) {
|
||||
this.sync.update('_referenceVersion', this._referenceVersion, null, {
|
||||
undo: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async save() {
|
||||
|
@ -1127,6 +1111,10 @@ export class DataLayer extends ServerStored {
|
|||
}
|
||||
|
||||
async _trySave(url, headers, formData) {
|
||||
if (this._forceSave) {
|
||||
headers = {}
|
||||
this._forceSave = false
|
||||
}
|
||||
const [data, response, error] = await this._umap.server.post(url, headers, formData)
|
||||
if (error) {
|
||||
if (response && response.status === 412) {
|
||||
|
@ -1136,16 +1124,9 @@ export class DataLayer extends ServerStored {
|
|||
'This situation is tricky, you have to choose carefully which version is pertinent.'
|
||||
),
|
||||
async () => {
|
||||
// Save again this layer
|
||||
const status = await this._trySave(url, {}, formData)
|
||||
if (status) {
|
||||
this.isDirty = false
|
||||
|
||||
// Call the main save, in case something else needs to be saved
|
||||
// as the conflict stopped the saving flow
|
||||
this._forceSave = true
|
||||
await this._umap.saveAll()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
@ -1179,7 +1160,7 @@ export class DataLayer extends ServerStored {
|
|||
}
|
||||
|
||||
commitDelete() {
|
||||
delete this._umap.datalayers[stamp(this)]
|
||||
delete this._umap.datalayers[this.id]
|
||||
}
|
||||
|
||||
getName() {
|
||||
|
|
|
@ -135,7 +135,13 @@ export default class Facets {
|
|||
for (const [property, { label, type }] of parsed) {
|
||||
dumped.push([property, label, type].filter(Boolean).join('|'))
|
||||
}
|
||||
return dumped.join(',')
|
||||
const oldValue = this._umap.properties.facetKey
|
||||
this._umap.properties.facetKey = dumped.join(',')
|
||||
this._umap.sync.update(
|
||||
'properties.facetKey',
|
||||
this._umap.properties.facetKey,
|
||||
oldValue
|
||||
)
|
||||
}
|
||||
|
||||
has(property) {
|
||||
|
@ -146,15 +152,13 @@ export default class Facets {
|
|||
const defined = this.getDefined()
|
||||
if (!defined.has(property)) {
|
||||
defined.set(property, { label, type })
|
||||
this._umap.properties.facetKey = this.dumps(defined)
|
||||
this._umap.isDirty = true
|
||||
this.dumps(defined)
|
||||
}
|
||||
}
|
||||
|
||||
remove(property) {
|
||||
const defined = this.getDefined()
|
||||
defined.delete(property)
|
||||
this._umap.properties.facetKey = this.dumps(defined)
|
||||
this._umap.isDirty = true
|
||||
this.dumps(defined)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,21 +70,7 @@ export class Form extends Utils.WithEvents {
|
|||
}
|
||||
|
||||
setter(field, value) {
|
||||
const path = field.split('.')
|
||||
let obj = this.obj
|
||||
let what
|
||||
for (let i = 0, l = path.length; i < l; i++) {
|
||||
what = path[i]
|
||||
if (what === path[l - 1]) {
|
||||
if (typeof value === 'undefined') {
|
||||
delete obj[what]
|
||||
} else {
|
||||
obj[what] = value
|
||||
}
|
||||
} else {
|
||||
obj = obj[what]
|
||||
}
|
||||
}
|
||||
Utils.setObjectValue(this.obj, field, value)
|
||||
}
|
||||
|
||||
restoreField(field) {
|
||||
|
@ -190,13 +176,17 @@ export class MutatingForm extends Form {
|
|||
}
|
||||
|
||||
setter(field, value) {
|
||||
const oldValue = this.getter(field)
|
||||
if ('setter' in this.obj) {
|
||||
this.obj.setter(field, value)
|
||||
} else {
|
||||
super.setter(field, value)
|
||||
this.obj.isDirty = true
|
||||
}
|
||||
if ('render' in this.obj) {
|
||||
this.obj.render([field], this)
|
||||
}
|
||||
if ('sync' in this.obj) {
|
||||
this.obj.sync.update(field, value)
|
||||
this.obj.sync.update(field, value, oldValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -541,14 +541,14 @@ Fields.DataLayerSwitcher = class extends Fields.Select {
|
|||
!datalayer.isDataReadOnly() &&
|
||||
datalayer.isBrowsable()
|
||||
) {
|
||||
options.push([L.stamp(datalayer), datalayer.getName()])
|
||||
options.push([datalayer.id, datalayer.getName()])
|
||||
}
|
||||
})
|
||||
return options
|
||||
}
|
||||
|
||||
toHTML() {
|
||||
return L.stamp(this.obj.datalayer)
|
||||
return this.obj.datalayer.id
|
||||
}
|
||||
|
||||
toJS() {
|
||||
|
|
|
@ -249,7 +249,7 @@ export default class Importer extends Utils.WithTemplate {
|
|||
tagName: 'option',
|
||||
parent: layerSelect,
|
||||
textContent: datalayer.options.name,
|
||||
value: L.stamp(datalayer),
|
||||
value: datalayer.id,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -2,17 +2,15 @@ import { DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
|
|||
import { uMapAlert as Alert } from '../components/alerts/alert.js'
|
||||
import { MutatingForm } from './form/builder.js'
|
||||
import { translate } from './i18n.js'
|
||||
import { ServerStored } from './saving.js'
|
||||
import * as Utils from './utils.js'
|
||||
|
||||
// Dedicated object so we can deal with a separate dirty status, and thus
|
||||
// call the endpoint only when needed, saving one call at each save.
|
||||
export class MapPermissions extends ServerStored {
|
||||
export class MapPermissions {
|
||||
constructor(umap) {
|
||||
super()
|
||||
this.setProperties(umap.properties.permissions)
|
||||
this._umap = umap
|
||||
this._isDirty = false
|
||||
this.sync = umap.syncEngine.proxy(this)
|
||||
}
|
||||
|
||||
setProperties(properties) {
|
||||
|
@ -28,6 +26,13 @@ export class MapPermissions extends ServerStored {
|
|||
)
|
||||
}
|
||||
|
||||
getSyncMetadata() {
|
||||
return {
|
||||
subject: 'mappermissions',
|
||||
metadata: {},
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this._umap.render(['properties.permissions'])
|
||||
}
|
||||
|
@ -188,7 +193,6 @@ export class MapPermissions extends ServerStored {
|
|||
}
|
||||
|
||||
async save() {
|
||||
if (!this.isDirty) return
|
||||
const formData = new FormData()
|
||||
if (!this.isAnonymousMap() && this.properties.editors) {
|
||||
const editors = this.properties.editors.map((u) => u.id)
|
||||
|
@ -247,9 +251,8 @@ export class MapPermissions extends ServerStored {
|
|||
}
|
||||
}
|
||||
|
||||
export class DataLayerPermissions extends ServerStored {
|
||||
export class DataLayerPermissions {
|
||||
constructor(umap, datalayer) {
|
||||
super()
|
||||
this._umap = umap
|
||||
this.properties = Object.assign(
|
||||
{
|
||||
|
@ -259,6 +262,14 @@ export class DataLayerPermissions extends ServerStored {
|
|||
)
|
||||
|
||||
this.datalayer = datalayer
|
||||
this.sync = umap.syncEngine.proxy(this)
|
||||
}
|
||||
|
||||
getSyncMetadata() {
|
||||
return {
|
||||
subject: 'datalayerpermissions',
|
||||
metadata: { id: this.datalayer.id },
|
||||
}
|
||||
}
|
||||
|
||||
edit(container) {
|
||||
|
@ -289,7 +300,6 @@ export class DataLayerPermissions extends ServerStored {
|
|||
}
|
||||
|
||||
async save() {
|
||||
if (!this.isDirty) return
|
||||
const formData = new FormData()
|
||||
formData.append('edit_status', this.properties.edit_status)
|
||||
const [data, response, error] = await this._umap.server.post(
|
||||
|
|
|
@ -117,7 +117,7 @@ export const Choropleth = FeatureGroup.extend({
|
|||
},
|
||||
|
||||
_getValue: function (feature) {
|
||||
const key = this.datalayer.options.choropleth.property || 'value'
|
||||
const key = this.datalayer.options.choropleth?.property || 'value'
|
||||
const value = +feature.properties[key]
|
||||
if (!Number.isNaN(value)) return value
|
||||
},
|
||||
|
@ -130,12 +130,12 @@ export const Choropleth = FeatureGroup.extend({
|
|||
this.options.colors = []
|
||||
return
|
||||
}
|
||||
const mode = this.datalayer.options.choropleth.mode
|
||||
let classes = +this.datalayer.options.choropleth.classes || 5
|
||||
const mode = this.datalayer.options.choropleth?.mode
|
||||
let classes = +this.datalayer.options.choropleth?.classes || 5
|
||||
let breaks
|
||||
classes = Math.min(classes, values.length)
|
||||
if (mode === 'manual') {
|
||||
const manualBreaks = this.datalayer.options.choropleth.breaks
|
||||
const manualBreaks = this.datalayer.options.choropleth?.breaks
|
||||
if (manualBreaks) {
|
||||
breaks = manualBreaks
|
||||
.split(',')
|
||||
|
|
|
@ -97,7 +97,6 @@ const FeatureMixin = {
|
|||
},
|
||||
|
||||
onCommit: function () {
|
||||
this.feature.pullGeometry(false)
|
||||
this.feature.onCommit()
|
||||
},
|
||||
}
|
||||
|
@ -112,7 +111,7 @@ const PointMixin = {
|
|||
this.on('dragend', (event) => {
|
||||
this.isDirty = true
|
||||
this.feature.edit(event)
|
||||
this.feature.pullGeometry(false)
|
||||
// this.feature.pullGeometry(false)
|
||||
})
|
||||
if (!this.feature.isReadOnly()) this.on('mouseover', this._enableDragging)
|
||||
this.on('mouseout', this._onMouseOut)
|
||||
|
@ -303,13 +302,13 @@ const PathMixin = {
|
|||
this._container = null
|
||||
FeatureMixin.onAdd.call(this, map)
|
||||
this.setStyle()
|
||||
if (this.editing?.enabled()) this.editing.addHooks()
|
||||
if (this.editor?.enabled()) this.editor.addHooks()
|
||||
this.resetTooltip()
|
||||
this._path.dataset.feature = this.feature.id
|
||||
},
|
||||
|
||||
onRemove: function (map) {
|
||||
if (this.editing?.enabled()) this.editing.removeHooks()
|
||||
if (this.editor?.enabled()) this.editor.removeHooks()
|
||||
FeatureMixin.onRemove.call(this, map)
|
||||
},
|
||||
|
||||
|
@ -362,6 +361,13 @@ const PathMixin = {
|
|||
isOnScreen: function (bounds) {
|
||||
return bounds.overlaps(this.getBounds())
|
||||
},
|
||||
|
||||
_setLatLngs: function (latlngs) {
|
||||
this.parentClass.prototype._setLatLngs.call(this, latlngs)
|
||||
if (this.editor?.enabled()) {
|
||||
this.editor.reset()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const LeafletPolyline = Polyline.extend({
|
||||
|
|
|
@ -17,20 +17,10 @@ class Rule {
|
|||
this.parse()
|
||||
}
|
||||
|
||||
get isDirty() {
|
||||
return this._isDirty
|
||||
}
|
||||
|
||||
set isDirty(status) {
|
||||
this._isDirty = status
|
||||
if (status) this._umap.isDirty = status
|
||||
}
|
||||
|
||||
constructor(umap, condition = '', options = {}) {
|
||||
// TODO make this public properties when browser coverage is ok
|
||||
// cf https://caniuse.com/?search=public%20class%20field
|
||||
this._condition = null
|
||||
this._isDirty = false
|
||||
this.OPERATORS = [
|
||||
['>', this.gt],
|
||||
['<', this.lt],
|
||||
|
@ -190,17 +180,25 @@ class Rule {
|
|||
|
||||
_delete() {
|
||||
this._umap.rules.rules = this._umap.rules.rules.filter((rule) => rule !== this)
|
||||
this._umap.rules.commit()
|
||||
}
|
||||
|
||||
setter(key, value) {
|
||||
const oldRules = Utils.CopyJSON(this._umap.properties.rules || {})
|
||||
Utils.setObjectValue(this, key, value)
|
||||
this._umap.rules.commit()
|
||||
this._umap.sync.update('properties.rules', this._umap.properties.rules, oldRules)
|
||||
}
|
||||
}
|
||||
|
||||
export default class Rules {
|
||||
constructor(umap) {
|
||||
this._umap = umap
|
||||
this.rules = []
|
||||
this.load()
|
||||
}
|
||||
|
||||
load() {
|
||||
this.rules = []
|
||||
if (!this._umap.properties.rules?.length) return
|
||||
for (const { condition, options } of this._umap.properties.rules) {
|
||||
if (!condition) continue
|
||||
|
@ -222,8 +220,8 @@ export default class Rules {
|
|||
else if (finalIndex > initialIndex) newIdx = referenceIdx
|
||||
else newIdx = referenceIdx + 1
|
||||
this.rules.splice(newIdx, 0, moved)
|
||||
moved.isDirty = true
|
||||
this._umap.render(['rules'])
|
||||
this.commit()
|
||||
}
|
||||
|
||||
edit(container) {
|
||||
|
@ -242,7 +240,6 @@ export default class Rules {
|
|||
|
||||
addRule() {
|
||||
const rule = new Rule(this._umap)
|
||||
rule.isDirty = true
|
||||
this.rules.push(rule)
|
||||
rule.edit(map)
|
||||
}
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
const _queue = new Set()
|
||||
|
||||
export let isDirty = false
|
||||
|
||||
export async function save() {
|
||||
for (const obj of _queue) {
|
||||
const ok = await obj.save()
|
||||
if (!ok) break
|
||||
remove(obj)
|
||||
}
|
||||
}
|
||||
|
||||
export function clear() {
|
||||
_queue.clear()
|
||||
onUpdate()
|
||||
}
|
||||
|
||||
function add(obj) {
|
||||
_queue.add(obj)
|
||||
onUpdate()
|
||||
}
|
||||
|
||||
function remove(obj) {
|
||||
_queue.delete(obj)
|
||||
onUpdate()
|
||||
}
|
||||
|
||||
function has(obj) {
|
||||
return _queue.has(obj)
|
||||
}
|
||||
|
||||
function onUpdate() {
|
||||
isDirty = Boolean(_queue.size)
|
||||
document.body.classList.toggle('umap-is-dirty', isDirty)
|
||||
}
|
||||
|
||||
export class ServerStored {
|
||||
set isDirty(status) {
|
||||
if (status) {
|
||||
add(this)
|
||||
} else {
|
||||
remove(this)
|
||||
}
|
||||
this.onDirty(status)
|
||||
}
|
||||
|
||||
get isDirty() {
|
||||
return has(this)
|
||||
}
|
||||
|
||||
onDirty(status) {}
|
||||
}
|
|
@ -44,6 +44,10 @@ export const SCHEMA = {
|
|||
type: Object,
|
||||
impacts: ['data'],
|
||||
},
|
||||
center: {
|
||||
type: Object,
|
||||
impacts: [], // default center, doesn't need any update of the map
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
impacts: ['data'],
|
||||
|
@ -118,6 +122,9 @@ export const SCHEMA = {
|
|||
default: false,
|
||||
label: translate('Animated transitions'),
|
||||
},
|
||||
edit_status: {
|
||||
type: Number,
|
||||
},
|
||||
editinosmControl: {
|
||||
type: Boolean,
|
||||
impacts: ['ui'],
|
||||
|
@ -125,6 +132,9 @@ export const SCHEMA = {
|
|||
label: translate('Display the control to open OpenStreetMap editor'),
|
||||
default: null,
|
||||
},
|
||||
editors: {
|
||||
type: Array,
|
||||
},
|
||||
embedControl: {
|
||||
type: Boolean,
|
||||
impacts: ['ui'],
|
||||
|
@ -362,6 +372,9 @@ export const SCHEMA = {
|
|||
type: Object,
|
||||
impacts: ['background'],
|
||||
},
|
||||
owner: {
|
||||
type: Object,
|
||||
},
|
||||
permanentCredit: {
|
||||
type: 'Text',
|
||||
impacts: ['ui'],
|
||||
|
@ -436,6 +449,9 @@ export const SCHEMA = {
|
|||
label: translate('Display the search control'),
|
||||
default: true,
|
||||
},
|
||||
share_status: {
|
||||
type: Number,
|
||||
},
|
||||
shortCredit: {
|
||||
type: String,
|
||||
impacts: ['ui'],
|
||||
|
@ -500,6 +516,9 @@ export const SCHEMA = {
|
|||
helpEntries: ['sync'],
|
||||
default: false,
|
||||
},
|
||||
team: {
|
||||
type: Object,
|
||||
},
|
||||
tilelayer: {
|
||||
type: Object,
|
||||
impacts: ['background'],
|
||||
|
@ -566,7 +585,6 @@ export const SCHEMA = {
|
|||
type: Object,
|
||||
impacts: ['data'],
|
||||
},
|
||||
|
||||
_referenceVersion: {
|
||||
type: Number,
|
||||
impacts: ['data'],
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import * as SaveManager from '../saving.js'
|
||||
import * as Utils from '../utils.js'
|
||||
import { HybridLogicalClock } from './hlc.js'
|
||||
import { DataLayerUpdater, FeatureUpdater, MapUpdater } from './updaters.js'
|
||||
import { UndoManager } from './undo.js'
|
||||
import {
|
||||
DataLayerUpdater,
|
||||
FeatureUpdater,
|
||||
MapUpdater,
|
||||
MapPermissionsUpdater,
|
||||
DataLayerPermissionsUpdater,
|
||||
} from './updaters.js'
|
||||
import { WebSocketTransport } from './websocket.js'
|
||||
|
||||
// Start reconnecting after 2 seconds, then double the delay each time
|
||||
|
@ -55,6 +61,8 @@ export class SyncEngine {
|
|||
map: new MapUpdater(umap),
|
||||
feature: new FeatureUpdater(umap),
|
||||
datalayer: new DataLayerUpdater(umap),
|
||||
mappermissions: new MapPermissionsUpdater(umap),
|
||||
datalayerpermissions: new DataLayerPermissionsUpdater(umap),
|
||||
}
|
||||
this.transport = undefined
|
||||
this._operations = new Operations()
|
||||
|
@ -64,6 +72,7 @@ export class SyncEngine {
|
|||
this.websocketConnected = false
|
||||
this.closeRequested = false
|
||||
this.peerId = Utils.generateId()
|
||||
this._undoManager = new UndoManager(umap, this.updaters, this)
|
||||
}
|
||||
|
||||
get isOpen() {
|
||||
|
@ -122,16 +131,107 @@ export class SyncEngine {
|
|||
await this.authenticate()
|
||||
}, this._reconnectDelay)
|
||||
}
|
||||
upsert(subject, metadata, value) {
|
||||
this._send({ verb: 'upsert', subject, metadata, value })
|
||||
|
||||
startBatch() {
|
||||
this._batch = []
|
||||
}
|
||||
|
||||
update(subject, metadata, key, value) {
|
||||
this._send({ verb: 'update', subject, metadata, key, value })
|
||||
commitBatch(subject, metadata) {
|
||||
if (!this._batch.length) {
|
||||
this._batch = null
|
||||
return
|
||||
}
|
||||
const operations = this._batch.map((stage) => stage.operation)
|
||||
const operation = { verb: 'batch', operations, subject, metadata }
|
||||
this._undoManager.add({ operation, stages: this._batch })
|
||||
this._send(operation)
|
||||
this._batch = null
|
||||
}
|
||||
|
||||
delete(subject, metadata, key) {
|
||||
this._send({ verb: 'delete', subject, metadata, key })
|
||||
upsert(subject, metadata, value, oldValue) {
|
||||
const operation = {
|
||||
verb: 'upsert',
|
||||
subject,
|
||||
metadata,
|
||||
value,
|
||||
}
|
||||
const stage = {
|
||||
operation,
|
||||
newValue: value,
|
||||
oldValue: oldValue,
|
||||
}
|
||||
if (this._batch) {
|
||||
this._batch.push(stage)
|
||||
return
|
||||
}
|
||||
this._undoManager.add(stage)
|
||||
this._send(operation)
|
||||
}
|
||||
|
||||
update(subject, metadata, key, value, oldValue, { undo } = { undo: true }) {
|
||||
const operation = {
|
||||
verb: 'update',
|
||||
subject,
|
||||
metadata,
|
||||
key,
|
||||
value,
|
||||
}
|
||||
const stage = {
|
||||
operation,
|
||||
oldValue: oldValue,
|
||||
newValue: value,
|
||||
}
|
||||
if (this._batch) {
|
||||
this._batch.push(stage)
|
||||
return
|
||||
}
|
||||
if (undo) this._undoManager.add(stage)
|
||||
this._send(operation)
|
||||
}
|
||||
|
||||
delete(subject, metadata, oldValue) {
|
||||
const operation = {
|
||||
verb: 'delete',
|
||||
subject,
|
||||
metadata,
|
||||
}
|
||||
const stage = {
|
||||
operation,
|
||||
oldValue: oldValue,
|
||||
}
|
||||
if (this._batch) {
|
||||
this._batch.push(stage)
|
||||
return
|
||||
}
|
||||
this._undoManager.add(stage)
|
||||
this._send(operation)
|
||||
}
|
||||
|
||||
async save() {
|
||||
const needSave = new Map()
|
||||
if (!this._umap.id) {
|
||||
// There is no operation for first map save
|
||||
needSave.set(this._umap, [])
|
||||
}
|
||||
for (const operation of this._operations.sorted()) {
|
||||
if (operation.dirty) {
|
||||
const updater = this._getUpdater(operation.subject)
|
||||
const obj = updater.getStoredObject(operation.metadata)
|
||||
if (!needSave.has(obj)) {
|
||||
needSave.set(obj, [])
|
||||
}
|
||||
needSave.get(obj).push(operation)
|
||||
}
|
||||
}
|
||||
for (const [obj, operations] of needSave.entries()) {
|
||||
const ok = await obj.save()
|
||||
if (!ok) break
|
||||
for (const operation of operations) {
|
||||
operation.dirty = false
|
||||
}
|
||||
}
|
||||
this.saved()
|
||||
this._undoManager.toggleState()
|
||||
}
|
||||
|
||||
saved() {
|
||||
|
@ -144,8 +244,8 @@ export class SyncEngine {
|
|||
}
|
||||
}
|
||||
|
||||
_send(inputMessage) {
|
||||
const message = this._operations.addLocal(inputMessage)
|
||||
_send(operation) {
|
||||
const message = this._operations.addLocal(operation)
|
||||
|
||||
if (this.offline) return
|
||||
if (this.transport) {
|
||||
|
@ -153,7 +253,11 @@ export class SyncEngine {
|
|||
}
|
||||
}
|
||||
|
||||
_getUpdater(subject, metadata) {
|
||||
_getUpdater(subject, metadata, sync) {
|
||||
// For now, prevent permissions to be synced, for security reasons
|
||||
if (sync && (subject === 'mappermissions' || subject === 'datalayerpermissions')) {
|
||||
return
|
||||
}
|
||||
if (Object.keys(this.updaters).includes(subject)) {
|
||||
return this.updaters[subject]
|
||||
}
|
||||
|
@ -161,7 +265,15 @@ export class SyncEngine {
|
|||
}
|
||||
|
||||
_applyOperation(operation) {
|
||||
if (operation.verb === 'batch') {
|
||||
operation.operations.map((op) => this._applyOperation(op))
|
||||
return
|
||||
}
|
||||
const updater = this._getUpdater(operation.subject, operation.metadata)
|
||||
if (!updater) {
|
||||
debug('No updater for', operation)
|
||||
return
|
||||
}
|
||||
updater.applyMessage(operation)
|
||||
}
|
||||
|
||||
|
@ -304,9 +416,8 @@ export class SyncEngine {
|
|||
|
||||
onSavedMessage({ sender, lastKnownHLC }) {
|
||||
debug(`received saved message from peer ${sender}`, lastKnownHLC)
|
||||
if (lastKnownHLC === this._operations.getLastKnownHLC() && SaveManager.isDirty) {
|
||||
SaveManager.clear()
|
||||
}
|
||||
this._operations.saved(lastKnownHLC)
|
||||
this._undoManager.toggleState()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -356,7 +467,7 @@ export class SyncEngine {
|
|||
const handler = {
|
||||
get(target, prop) {
|
||||
// Only proxy these methods
|
||||
if (['upsert', 'update', 'delete'].includes(prop)) {
|
||||
if (['upsert', 'update', 'delete', 'commitBatch'].includes(prop)) {
|
||||
const { subject, metadata } = object.getSyncMetadata()
|
||||
// Reflect.get is calling the original method.
|
||||
// .bind is adding the parameters automatically
|
||||
|
@ -378,16 +489,22 @@ export class Operations {
|
|||
this._operations = new Array()
|
||||
}
|
||||
|
||||
saved(hlc) {
|
||||
for (const operation of this.getOperationsBefore(hlc)) {
|
||||
operation.dirty = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick the clock and store the passed message in the operations list.
|
||||
*
|
||||
* @param {*} inputMessage
|
||||
* @returns {*} clock-aware message
|
||||
*/
|
||||
addLocal(inputMessage) {
|
||||
const message = { ...inputMessage, hlc: this._hlc.tick() }
|
||||
this._operations.push(message)
|
||||
return message
|
||||
addLocal(operation) {
|
||||
operation.hlc = this._hlc.tick()
|
||||
this._operations.push(operation)
|
||||
return operation
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -445,6 +562,11 @@ export class Operations {
|
|||
return this._operations.filter((op) => op.hlc > hlc)
|
||||
}
|
||||
|
||||
getOperationsBefore(hlc) {
|
||||
if (!hlc) return this._operations
|
||||
return this._operations.filter((op) => op.hlc <= hlc)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last known HLC value.
|
||||
*/
|
||||
|
|
101
umap/static/umap/js/modules/sync/undo.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
import * as Utils from '../utils.js'
|
||||
import { DataLayerUpdater, FeatureUpdater, MapUpdater } from './updaters.js'
|
||||
|
||||
export class UndoManager {
|
||||
constructor(umap, updaters, syncEngine) {
|
||||
this._umap = umap
|
||||
this._syncEngine = syncEngine
|
||||
this.updaters = updaters
|
||||
this._undoStack = []
|
||||
this._redoStack = []
|
||||
}
|
||||
|
||||
toggleState() {
|
||||
// document is undefined during unittests
|
||||
if (typeof document === 'undefined') return
|
||||
const undoButton = document.querySelector('.edit-undo')
|
||||
const redoButton = document.querySelector('.edit-redo')
|
||||
if (undoButton) undoButton.disabled = !this._undoStack.length
|
||||
if (redoButton) redoButton.disabled = !this._redoStack.length
|
||||
const dirty = this.isDirty()
|
||||
document.body.classList.toggle('umap-is-dirty', dirty)
|
||||
for (const button of document.querySelectorAll('.disabled-on-dirty')) {
|
||||
button.disabled = dirty
|
||||
}
|
||||
for (const button of document.querySelectorAll('.enabled-on-dirty')) {
|
||||
button.disabled = !dirty
|
||||
}
|
||||
}
|
||||
|
||||
isDirty() {
|
||||
if (!this._umap.id) return true
|
||||
for (const stage of this._undoStack) {
|
||||
if (stage.operation.dirty) return true
|
||||
}
|
||||
for (const stage of this._redoStack) {
|
||||
if (stage.operation.dirty) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
add(stage) {
|
||||
stage.operation.dirty = true
|
||||
this._redoStack = []
|
||||
this._undoStack.push(stage)
|
||||
this.toggleState()
|
||||
}
|
||||
|
||||
copyOperation(stage, redo) {
|
||||
const operation = Utils.CopyJSON(stage.operation)
|
||||
const value = redo ? stage.newValue : stage.oldValue
|
||||
operation.value = value
|
||||
if (['delete', 'upsert'].includes(operation.verb)) {
|
||||
operation.verb = value === null || value === undefined ? 'delete' : 'upsert'
|
||||
}
|
||||
return operation
|
||||
}
|
||||
|
||||
undo(redo = false) {
|
||||
const fromStack = redo ? this._redoStack : this._undoStack
|
||||
const toStack = redo ? this._undoStack : this._redoStack
|
||||
const stage = fromStack.pop()
|
||||
if (!stage) return
|
||||
stage.operation.dirty = !stage.operation.dirty
|
||||
if (stage.operation.verb === 'batch') {
|
||||
for (const st of stage.stages) {
|
||||
this.applyOperation(this.copyOperation(st, redo))
|
||||
}
|
||||
} else {
|
||||
this.applyOperation(this.copyOperation(stage, redo))
|
||||
}
|
||||
toStack.push(stage)
|
||||
this.toggleState()
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.undo(true)
|
||||
}
|
||||
|
||||
applyOperation(operation) {
|
||||
const updater = this._getUpdater(operation.subject, operation.metadata)
|
||||
switch (operation.verb) {
|
||||
case 'update':
|
||||
updater.update(operation)
|
||||
break
|
||||
case 'delete':
|
||||
updater.delete(operation)
|
||||
break
|
||||
case 'upsert':
|
||||
updater.upsert(operation)
|
||||
break
|
||||
}
|
||||
this._syncEngine._send(operation)
|
||||
}
|
||||
|
||||
_getUpdater(subject, metadata) {
|
||||
if (Object.keys(this.updaters).includes(subject)) {
|
||||
return this.updaters[subject]
|
||||
}
|
||||
throw new Error(`Unknown updater ${subject}, ${metadata}`)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { fieldInSchema } from '../utils.js'
|
||||
import * as Utils from '../utils.js'
|
||||
|
||||
/**
|
||||
* Updaters are classes able to convert messages
|
||||
|
@ -10,27 +10,6 @@ class BaseUpdater {
|
|||
this._umap = umap
|
||||
}
|
||||
|
||||
updateObjectValue(obj, key, value) {
|
||||
const parts = key.split('.')
|
||||
const lastKey = parts.pop()
|
||||
|
||||
// Reduce the current list of attributes,
|
||||
// to find the object to set the property onto
|
||||
const objectToSet = parts.reduce((currentObj, part) => {
|
||||
if (currentObj !== undefined && part in currentObj) return currentObj[part]
|
||||
}, obj)
|
||||
|
||||
// In case the given path doesn't exist, stop here
|
||||
if (objectToSet === undefined) return
|
||||
|
||||
// Set the value (or delete it)
|
||||
if (typeof value === 'undefined') {
|
||||
delete objectToSet[lastKey]
|
||||
} else {
|
||||
objectToSet[lastKey] = value
|
||||
}
|
||||
}
|
||||
|
||||
getDataLayerFromID(layerId) {
|
||||
return this._umap.getDataLayerByUmapId(layerId)
|
||||
}
|
||||
|
@ -43,13 +22,17 @@ class BaseUpdater {
|
|||
|
||||
export class MapUpdater extends BaseUpdater {
|
||||
update({ key, value }) {
|
||||
if (fieldInSchema(key)) {
|
||||
this.updateObjectValue(this._umap, key, value)
|
||||
if (Utils.fieldInSchema(key)) {
|
||||
Utils.setObjectValue(this._umap, key, value)
|
||||
}
|
||||
|
||||
this._umap.onPropertiesUpdated([key])
|
||||
this._umap.render([key])
|
||||
}
|
||||
|
||||
getStoredObject() {
|
||||
return this._umap
|
||||
}
|
||||
}
|
||||
|
||||
export class DataLayerUpdater extends BaseUpdater {
|
||||
|
@ -58,14 +41,21 @@ export class DataLayerUpdater extends BaseUpdater {
|
|||
try {
|
||||
this.getDataLayerFromID(value.id)
|
||||
} catch {
|
||||
this._umap.createDataLayer(value, false)
|
||||
const datalayer = this._umap.createDataLayer(value._umap_options || value, false)
|
||||
if (value.features) {
|
||||
// FIXME: this will create new stages in the undoStack, thus this will empty
|
||||
// the redoStack
|
||||
datalayer.addData(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update({ key, metadata, value }) {
|
||||
const datalayer = this.getDataLayerFromID(metadata.id)
|
||||
if (fieldInSchema(key)) {
|
||||
this.updateObjectValue(datalayer, key, value)
|
||||
if (key === 'options') {
|
||||
datalayer.setOptions(value)
|
||||
} else if (Utils.fieldInSchema(key)) {
|
||||
Utils.setObjectValue(datalayer, key, value)
|
||||
} else {
|
||||
console.debug(
|
||||
'Not applying update for datalayer because key is not in the schema',
|
||||
|
@ -82,6 +72,10 @@ export class DataLayerUpdater extends BaseUpdater {
|
|||
datalayer.commitDelete()
|
||||
}
|
||||
}
|
||||
|
||||
getStoredObject(metadata) {
|
||||
return this.getDataLayerFromID(metadata.id)
|
||||
}
|
||||
}
|
||||
|
||||
export class FeatureUpdater extends BaseUpdater {
|
||||
|
@ -114,7 +108,7 @@ export class FeatureUpdater extends BaseUpdater {
|
|||
const feature = this.getFeatureFromMetadata(metadata)
|
||||
feature.geometry = value
|
||||
} else {
|
||||
this.updateObjectValue(feature, key, value)
|
||||
Utils.setObjectValue(feature, key, value)
|
||||
feature.datalayer.indexProperties(feature)
|
||||
}
|
||||
|
||||
|
@ -127,4 +121,32 @@ export class FeatureUpdater extends BaseUpdater {
|
|||
const feature = this.getFeatureFromMetadata(metadata)
|
||||
if (feature) feature.del(false)
|
||||
}
|
||||
|
||||
getStoredObject(metadata) {
|
||||
return this.getDataLayerFromID(metadata.layerId)
|
||||
}
|
||||
}
|
||||
|
||||
export class MapPermissionsUpdater extends BaseUpdater {
|
||||
update({ key, value }) {
|
||||
if (Utils.fieldInSchema(key)) {
|
||||
Utils.setObjectValue(this._umap.permissions, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
getStoredObject(metadata) {
|
||||
return this._umap.permissions
|
||||
}
|
||||
}
|
||||
|
||||
export class DataLayerPermissionsUpdater extends BaseUpdater {
|
||||
update({ key, value, metadata }) {
|
||||
if (Utils.fieldInSchema(key)) {
|
||||
Utils.setObjectValue(this.getDataLayerFromID(metadata.id), key, value)
|
||||
}
|
||||
}
|
||||
|
||||
getStoredObject(metadata) {
|
||||
return this.getDataLayerFromID(metadata.id).permissions
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,16 @@ const TOP_BAR_TEMPLATE = `
|
|||
<div class="umap-main-edit-toolbox with-transition dark">
|
||||
<div class="umap-left-edit-toolbox" data-ref="left">
|
||||
<div class="logo"><a class="" href="/" title="${translate('Go to the homepage')}">uMap</a></div>
|
||||
<button class="map-name flat" type="button" data-ref="name"></button>
|
||||
<button class="share-status flat" type="button" data-ref="share"></button>
|
||||
<button class="map-name flat truncate" type="button" data-ref="name"></button>
|
||||
<button class="share-status flat truncate" type="button" data-ref="share"></button>
|
||||
<button class="edit-undo round" type="button" data-ref="undo" disabled>
|
||||
<i class="icon icon-16 icon-undo"></i>
|
||||
<span>${translate('Undo')}</span>
|
||||
</button>
|
||||
<button class="edit-redo round" type="button" data-ref="redo" disabled>
|
||||
<i class="icon icon-16 icon-redo"></i>
|
||||
<span>${translate('Redo')}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="umap-right-edit-toolbox" data-ref="right">
|
||||
<button class="connected-peers round" type="button" data-ref="peers">
|
||||
|
@ -19,18 +27,14 @@ const TOP_BAR_TEMPLATE = `
|
|||
</button>
|
||||
<button class="umap-user flat" type="button" data-ref="user">
|
||||
<i class="icon icon-16 icon-profile"></i>
|
||||
<span class="username" data-ref="username"></span>
|
||||
<span class="username truncate" data-ref="username"></span>
|
||||
</button>
|
||||
<button class="umap-help-link flat" type="button" title="${translate('Help')}" data-ref="help">${translate('Help')}</button>
|
||||
<button class="edit-cancel round" type="button" data-ref="cancel">
|
||||
<i class="icon icon-16 icon-restore"></i>
|
||||
<span class="">${translate('Cancel edits')}</span>
|
||||
</button>
|
||||
<button class="edit-disable round" type="button" data-ref="view">
|
||||
<button class="edit-disable round disabled-on-dirty" type="button" data-ref="view">
|
||||
<i class="icon icon-16 icon-eye"></i>
|
||||
<span class="">${translate('View')}</span>
|
||||
<span>${translate('View')}</span>
|
||||
</button>
|
||||
<button class="edit-save button round" type="button" data-ref="save">
|
||||
<button class="edit-save button round enabled-on-dirty" type="button" data-ref="save">
|
||||
<i class="icon icon-16 icon-save"></i>
|
||||
<i class="icon icon-16 icon-save-disabled"></i>
|
||||
<span hidden data-ref="saveLabel">${translate('Save')}</span>
|
||||
|
@ -118,11 +122,12 @@ export class TopBar extends WithTemplate {
|
|||
})
|
||||
|
||||
this.elements.help.addEventListener('click', () => this._umap.help.showGetStarted())
|
||||
this.elements.cancel.addEventListener('click', () => this._umap.askForReset())
|
||||
this.elements.cancel.addEventListener('mouseover', () => {
|
||||
this.elements.redo.addEventListener('click', () => this._umap.redo())
|
||||
this.elements.undo.addEventListener('click', () => this._umap.undo())
|
||||
this.elements.undo.addEventListener('mouseover', () => {
|
||||
this._umap.tooltip.open({
|
||||
content: this._umap.help.displayLabel('CANCEL'),
|
||||
anchor: this.elements.cancel,
|
||||
anchor: this.elements.undo,
|
||||
position: 'bottom',
|
||||
delay: 500,
|
||||
duration: 5000,
|
||||
|
@ -154,9 +159,10 @@ export class TopBar extends WithTemplate {
|
|||
redraw() {
|
||||
const syncEnabled = this._umap.getProperty('syncEnabled')
|
||||
this.elements.peers.hidden = !syncEnabled
|
||||
this.elements.cancel.hidden = syncEnabled
|
||||
this.elements.view.disabled = this._umap.sync._undoManager.isDirty()
|
||||
this.elements.saveLabel.hidden = this._umap.permissions.isDraft()
|
||||
this.elements.saveDraftLabel.hidden = !this._umap.permissions.isDraft()
|
||||
this._umap.sync._undoManager.toggleState()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -270,10 +276,7 @@ export class EditBar extends WithTemplate {
|
|||
DomEvent.disableClickPropagation(this.element)
|
||||
this._onClick('marker', () => this._leafletMap.editTools.startMarker())
|
||||
this._onClick('polyline', () => this._leafletMap.editTools.startPolyline())
|
||||
this._onClick('multiline', () => {
|
||||
console.log('click click')
|
||||
this._umap.editedFeature.ui.editor.newShape()
|
||||
})
|
||||
this._onClick('multiline', () => this._umap.editedFeature.ui.editor.newShape())
|
||||
this._onClick('polygon', () => this._leafletMap.editTools.startPolygon())
|
||||
this._onClick('multipolygon', () => this._umap.editedFeature.ui.editor.newShape())
|
||||
this._onClick('caption', () => this._umap.editCaption())
|
||||
|
|
|
@ -22,8 +22,6 @@ import { MapPermissions } from './permissions.js'
|
|||
import { LeafletMap } from './rendering/map.js'
|
||||
import { Request, ServerRequest } from './request.js'
|
||||
import Rules from './rules.js'
|
||||
import { ServerStored } from './saving.js'
|
||||
import * as SAVEMANAGER from './saving.js'
|
||||
import { SCHEMA } from './schema.js'
|
||||
import Share from './share.js'
|
||||
import Slideshow from './slideshow.js'
|
||||
|
@ -36,9 +34,8 @@ import Tooltip from './ui/tooltip.js'
|
|||
import URLs from './urls.js'
|
||||
import * as Utils from './utils.js'
|
||||
|
||||
export default class Umap extends ServerStored {
|
||||
export default class Umap {
|
||||
constructor(element, geojson) {
|
||||
super()
|
||||
// We need to call async function in the init process,
|
||||
// the init itself does not need to be awaited, but some calls
|
||||
// in the process must be blocker
|
||||
|
@ -96,6 +93,10 @@ export default class Umap extends ServerStored {
|
|||
this._leafletMap.latLng(center)
|
||||
}
|
||||
|
||||
// Needed for permissions
|
||||
this.syncEngine = new SyncEngine(this)
|
||||
this.sync = this.syncEngine.proxy(this)
|
||||
|
||||
// Needed to render controls
|
||||
this.permissions = new MapPermissions(this)
|
||||
this.urls = new URLs(this.properties.urls)
|
||||
|
@ -130,9 +131,6 @@ export default class Umap extends ServerStored {
|
|||
this.share = new Share(this)
|
||||
this.rules = new Rules(this)
|
||||
|
||||
this.syncEngine = new SyncEngine(this)
|
||||
this.sync = this.syncEngine.proxy(this)
|
||||
|
||||
if (this.hasEditMode()) {
|
||||
this.editPanel = new EditPanel(this, this._leafletMap)
|
||||
this.fullPanel = new FullPanel(this, this._leafletMap)
|
||||
|
@ -196,7 +194,6 @@ export default class Umap extends ServerStored {
|
|||
// Creation mode
|
||||
if (!this.id) {
|
||||
if (!this.properties.preview) {
|
||||
this.isDirty = true
|
||||
this.enableEdit()
|
||||
}
|
||||
this._defaultExtent = true
|
||||
|
@ -212,10 +209,14 @@ export default class Umap extends ServerStored {
|
|||
this.propagate()
|
||||
}
|
||||
|
||||
window.onbeforeunload = () => (this.editEnabled && SAVEMANAGER.isDirty) || null
|
||||
window.onbeforeunload = () => (this.editEnabled && this.isDirty) || null
|
||||
this.backup()
|
||||
}
|
||||
|
||||
get isDirty() {
|
||||
return this.sync._undoManager.isDirty()
|
||||
}
|
||||
|
||||
get editedFeature() {
|
||||
return this._editedFeature
|
||||
}
|
||||
|
@ -349,7 +350,7 @@ export default class Umap extends ServerStored {
|
|||
const items = []
|
||||
if (this.hasEditMode()) {
|
||||
if (this.editEnabled) {
|
||||
if (!SAVEMANAGER.isDirty) {
|
||||
if (!this.isDirty) {
|
||||
items.push({
|
||||
label: this.help.displayLabel('STOP_EDIT'),
|
||||
action: () => this.disableEdit(),
|
||||
|
@ -543,19 +544,17 @@ export default class Umap extends ServerStored {
|
|||
let used = true
|
||||
switch (event.key) {
|
||||
case 'e':
|
||||
if (!SAVEMANAGER.isDirty) this.disableEdit()
|
||||
if (!this.isDirty) this.disableEdit()
|
||||
break
|
||||
case 's':
|
||||
if (SAVEMANAGER.isDirty) this.saveAll()
|
||||
if (this.isDirty) this.saveAll()
|
||||
break
|
||||
case 'z':
|
||||
if (Utils.isWritable(event.target)) {
|
||||
used = false
|
||||
break
|
||||
}
|
||||
if (SAVEMANAGER.isDirty) {
|
||||
this.askForReset()
|
||||
}
|
||||
this.sync._undoManager.undo()
|
||||
break
|
||||
case 'm':
|
||||
this._leafletMap.editTools.startMarker()
|
||||
|
@ -671,10 +670,10 @@ export default class Umap extends ServerStored {
|
|||
}
|
||||
|
||||
async saveAll() {
|
||||
if (!SAVEMANAGER.isDirty) return
|
||||
if (!this.isDirty) return
|
||||
if (this._defaultExtent) this._setCenterAndZoom()
|
||||
this.backup()
|
||||
await SAVEMANAGER.save()
|
||||
await this.sync.save()
|
||||
// Do a blind render for now, as we are not sure what could
|
||||
// have changed, we'll be more subtil when we'll remove the
|
||||
// save action
|
||||
|
@ -685,7 +684,6 @@ export default class Umap extends ServerStored {
|
|||
Alert.success(translate('Map has been saved!'))
|
||||
})
|
||||
}
|
||||
this.sync.saved()
|
||||
this.fire('saved')
|
||||
}
|
||||
|
||||
|
@ -1019,35 +1017,36 @@ export default class Umap extends ServerStored {
|
|||
'button',
|
||||
boundsButtons,
|
||||
translate('Use current bounds'),
|
||||
function () {
|
||||
() => {
|
||||
const bounds = this._leafletMap.getBounds()
|
||||
const oldLimitBounds = { ...this.properties.limitBounds }
|
||||
this.properties.limitBounds.south = LeafletUtil.formatNum(bounds.getSouth())
|
||||
this.properties.limitBounds.west = LeafletUtil.formatNum(bounds.getWest())
|
||||
this.properties.limitBounds.north = LeafletUtil.formatNum(bounds.getNorth())
|
||||
this.properties.limitBounds.east = LeafletUtil.formatNum(bounds.getEast())
|
||||
boundsBuilder.fetchAll()
|
||||
|
||||
this.sync.update(this, 'properties.limitBounds', this.properties.limitBounds)
|
||||
this.isDirty = true
|
||||
this._leafletMap.handleLimitBounds()
|
||||
},
|
||||
this
|
||||
this.sync.update(
|
||||
'properties.limitBounds',
|
||||
this.properties.limitBounds,
|
||||
oldLimitBounds
|
||||
)
|
||||
DomUtil.createButton(
|
||||
'button',
|
||||
boundsButtons,
|
||||
translate('Empty'),
|
||||
function () {
|
||||
this._leafletMap.handleLimitBounds()
|
||||
}
|
||||
)
|
||||
DomUtil.createButton('button', boundsButtons, translate('Empty'), () => {
|
||||
const oldLimitBounds = { ...this.properties.limitBounds }
|
||||
this.properties.limitBounds.south = null
|
||||
this.properties.limitBounds.west = null
|
||||
this.properties.limitBounds.north = null
|
||||
this.properties.limitBounds.east = null
|
||||
boundsBuilder.fetchAll()
|
||||
this.isDirty = true
|
||||
this._leafletMap.handleLimitBounds()
|
||||
},
|
||||
this
|
||||
this.sync.update(
|
||||
'properties.limitBounds',
|
||||
this.properties.limitBounds,
|
||||
oldLimitBounds
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
_editSlideshow(container) {
|
||||
|
@ -1160,23 +1159,7 @@ export default class Umap extends ServerStored {
|
|||
})
|
||||
}
|
||||
|
||||
reset() {
|
||||
if (this._leafletMap.editTools) this._leafletMap.editTools.stopDrawing()
|
||||
this.resetProperties()
|
||||
this.datalayersIndex = [].concat(this._datalayersIndex_bk)
|
||||
// Iter over all datalayers, including deleted if any.
|
||||
for (const datalayer of Object.values(this.datalayers)) {
|
||||
if (datalayer.isDeleted) datalayer.connectToMap()
|
||||
if (datalayer.isDirty) datalayer.reset()
|
||||
}
|
||||
this.ensurePanesOrder()
|
||||
this._leafletMap.initTileLayers()
|
||||
this.onDataLayersChanged()
|
||||
this.isDirty = !this.id
|
||||
}
|
||||
|
||||
async save() {
|
||||
this.rules.commit()
|
||||
const geojson = {
|
||||
type: 'Feature',
|
||||
geometry: this.geometry(),
|
||||
|
@ -1301,16 +1284,6 @@ export default class Umap extends ServerStored {
|
|||
this._leafletMap.fire(name)
|
||||
}
|
||||
|
||||
askForReset(e) {
|
||||
if (this.getProperty('syncEnabled')) return
|
||||
this.dialog
|
||||
.confirm(translate('Are you sure you want to cancel your changes?'))
|
||||
.then(() => {
|
||||
this.reset()
|
||||
this.disableEdit()
|
||||
})
|
||||
}
|
||||
|
||||
async initSyncEngine() {
|
||||
// this.properties.websocketEnabled is set by the server admin
|
||||
if (this.properties.websocketEnabled === false) return
|
||||
|
@ -1324,7 +1297,6 @@ export default class Umap extends ServerStored {
|
|||
|
||||
getSyncMetadata() {
|
||||
return {
|
||||
engine: this.sync,
|
||||
subject: 'map',
|
||||
}
|
||||
}
|
||||
|
@ -1348,6 +1320,9 @@ export default class Umap extends ServerStored {
|
|||
this.bottomBar.redraw()
|
||||
break
|
||||
case 'data':
|
||||
if (fields.includes('properties.rules')) {
|
||||
this.rules.load()
|
||||
}
|
||||
this.eachVisibleDataLayer((datalayer) => {
|
||||
datalayer.redraw()
|
||||
})
|
||||
|
@ -1522,7 +1497,7 @@ export default class Umap extends ServerStored {
|
|||
const form = builder.build()
|
||||
row.appendChild(form)
|
||||
row.classList.toggle('off', !datalayer.isVisible())
|
||||
row.dataset.id = stamp(datalayer)
|
||||
row.dataset.id = datalayer.id
|
||||
})
|
||||
const onReorder = (src, dst, initialIndex, finalIndex) => {
|
||||
const movedLayer = this.datalayers[src.dataset.id]
|
||||
|
@ -1553,7 +1528,7 @@ export default class Umap extends ServerStored {
|
|||
}
|
||||
|
||||
getDataLayerByUmapId(id) {
|
||||
const datalayer = this.findDataLayer((d) => d.id === id)
|
||||
const datalayer = this.datalayers[id]
|
||||
if (!datalayer) throw new Error(`Can't find datalayer with id ${id}`)
|
||||
return datalayer
|
||||
}
|
||||
|
@ -1669,7 +1644,6 @@ export default class Umap extends ServerStored {
|
|||
)
|
||||
this.render(fields)
|
||||
this._leafletMap._setDefaultCenter()
|
||||
this.isDirty = true
|
||||
}
|
||||
|
||||
importUmapFile(file) {
|
||||
|
@ -1768,13 +1742,26 @@ export default class Umap extends ServerStored {
|
|||
}
|
||||
|
||||
_setCenterAndZoom() {
|
||||
const oldCenter = { ...this.properties.center }
|
||||
const oldZoom = this.properties.zoom
|
||||
this.properties.center = this._leafletMap.getCenter()
|
||||
this.properties.zoom = this._leafletMap.getZoom()
|
||||
this.isDirty = true
|
||||
this._defaultExtent = false
|
||||
this.sync.startBatch()
|
||||
this.sync.update('properties.center', this.properties.center, oldCenter)
|
||||
this.sync.update('properties.zoom', this.properties.zoom, oldZoom)
|
||||
this.sync.commitBatch()
|
||||
}
|
||||
|
||||
getStaticPathFor(name) {
|
||||
return SCHEMA.iconUrl.default.replace('marker.svg', name)
|
||||
}
|
||||
|
||||
undo() {
|
||||
this.sync._undoManager.undo()
|
||||
}
|
||||
|
||||
redo() {
|
||||
this.sync._undoManager.redo()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -484,6 +484,27 @@ export const debounce = (callback, wait) => {
|
|||
}
|
||||
}
|
||||
|
||||
export function setObjectValue(obj, key, value) {
|
||||
const parts = key.split('.')
|
||||
const lastKey = parts.pop()
|
||||
|
||||
// Reduce the current list of attributes,
|
||||
// to find the object to set the property onto
|
||||
const objectToSet = parts.reduce((currentObj, part) => {
|
||||
if (currentObj !== undefined && part in currentObj) return currentObj[part]
|
||||
}, obj)
|
||||
|
||||
// In case the given path doesn't exist, stop here
|
||||
if (objectToSet === undefined) return
|
||||
|
||||
// Set the value (or delete it)
|
||||
if (typeof value === 'undefined') {
|
||||
delete objectToSet[lastKey]
|
||||
} else {
|
||||
objectToSet[lastKey] = value
|
||||
}
|
||||
}
|
||||
|
||||
export const COLORS = [
|
||||
'Black',
|
||||
'Navy',
|
||||
|
|
|
@ -297,6 +297,7 @@ U.TileLayerChooser = L.Control.extend({
|
|||
el,
|
||||
'click',
|
||||
() => {
|
||||
const oldTileLayer = this.map._umap.properties.tilelayer
|
||||
this.map.selectTileLayer(tilelayer)
|
||||
this.map._controls.tilelayers.setLayers()
|
||||
if (options?.edit) {
|
||||
|
@ -304,7 +305,8 @@ U.TileLayerChooser = L.Control.extend({
|
|||
this.map._umap.isDirty = true
|
||||
this.map._umap.sync.update(
|
||||
'properties.tilelayer',
|
||||
this.map._umap.properties.tilelayer
|
||||
this.map._umap.properties.tilelayer,
|
||||
oldTileLayer
|
||||
)
|
||||
}
|
||||
},
|
||||
|
@ -606,7 +608,7 @@ U.Editable = L.Editable.extend({
|
|||
this.on('editable:editing', (event) => {
|
||||
const feature = event.layer.feature
|
||||
feature.isDirty = true
|
||||
feature.pullGeometry(false)
|
||||
// feature.pullGeometry(false)
|
||||
})
|
||||
this.on('editable:vertex:ctrlclick', (event) => {
|
||||
const index = event.vertex.getIndex()
|
||||
|
@ -624,18 +626,20 @@ U.Editable = L.Editable.extend({
|
|||
|
||||
createPolyline: function (latlngs) {
|
||||
const datalayer = this._umap.defaultEditDataLayer()
|
||||
const point = new U.LineString(this._umap, datalayer, {
|
||||
const line = new U.LineString(this._umap, datalayer, {
|
||||
geometry: { type: 'LineString', coordinates: [] },
|
||||
})
|
||||
return point.ui
|
||||
line._just_married = true
|
||||
return line.ui
|
||||
},
|
||||
|
||||
createPolygon: function (latlngs) {
|
||||
const datalayer = this._umap.defaultEditDataLayer()
|
||||
const point = new U.Polygon(this._umap, datalayer, {
|
||||
const poly = new U.Polygon(this._umap, datalayer, {
|
||||
geometry: { type: 'Polygon', coordinates: [] },
|
||||
})
|
||||
return point.ui
|
||||
poly._just_married = true
|
||||
return poly.ui
|
||||
},
|
||||
|
||||
createMarker: function (latlng) {
|
||||
|
@ -643,6 +647,7 @@ U.Editable = L.Editable.extend({
|
|||
const point = new U.Point(this._umap, datalayer, {
|
||||
geometry: { type: 'Point', coordinates: [latlng.lng, latlng.lat] },
|
||||
})
|
||||
point._just_married = true
|
||||
return point.ui
|
||||
},
|
||||
|
||||
|
@ -734,6 +739,7 @@ U.Editable = L.Editable.extend({
|
|||
// Leaflet.Editable will delete the drawn shape if invalid
|
||||
// (eg. line has only one drawn point)
|
||||
// So let's check if the layer has no more shape
|
||||
event.layer.feature.pullGeometry(false)
|
||||
if (!event.layer.feature.hasGeom()) {
|
||||
event.layer.feature.del()
|
||||
} else {
|
||||
|
|
|
@ -986,7 +986,7 @@ a.umap-control-caption,
|
|||
.umap-main-edit-toolbox .umap-user span,
|
||||
.leaflet-container .leaflet-control-edit-save span,
|
||||
.leaflet-container .leaflet-control-edit-disable span,
|
||||
.leaflet-container .leaflet-control-edit-cancel span {
|
||||
.leaflet-container .edit-cancel span {
|
||||
display: none;
|
||||
}
|
||||
.umap-main-edit-toolbox .umap-help-button {
|
||||
|
|
|
@ -49,63 +49,6 @@ describe('#dispatch', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Updaters', () => {
|
||||
describe('BaseUpdater', () => {
|
||||
let updater
|
||||
let map
|
||||
let obj
|
||||
|
||||
beforeEach(() => {
|
||||
map = {}
|
||||
updater = new MapUpdater(map)
|
||||
obj = {}
|
||||
})
|
||||
it('should be able to set object properties', () => {
|
||||
let obj = {}
|
||||
updater.updateObjectValue(obj, 'foo', 'foo')
|
||||
expect(obj).deep.equal({ foo: 'foo' })
|
||||
})
|
||||
|
||||
it('should be able to set object properties recursively on existing objects', () => {
|
||||
let obj = { foo: {} }
|
||||
updater.updateObjectValue(obj, 'foo.bar', 'foo')
|
||||
expect(obj).deep.equal({ foo: { bar: 'foo' } })
|
||||
})
|
||||
|
||||
it('should be able to set object properties recursively on deep objects', () => {
|
||||
let obj = { foo: { bar: { baz: {} } } }
|
||||
updater.updateObjectValue(obj, 'foo.bar.baz.test', 'value')
|
||||
expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
|
||||
})
|
||||
|
||||
it('should be able to replace object properties recursively on deep objects', () => {
|
||||
let obj = { foo: { bar: { baz: { test: 'test' } } } }
|
||||
updater.updateObjectValue(obj, 'foo.bar.baz.test', 'value')
|
||||
expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
|
||||
})
|
||||
|
||||
it('should not set object properties recursively on non-existing objects', () => {
|
||||
let obj = { foo: {} }
|
||||
updater.updateObjectValue(obj, 'bar.bar', 'value')
|
||||
|
||||
expect(obj).deep.equal({ foo: {} })
|
||||
})
|
||||
|
||||
it('should delete keys for undefined values', () => {
|
||||
let obj = { foo: 'foo' }
|
||||
updater.updateObjectValue(obj, 'foo', undefined)
|
||||
|
||||
expect(obj).deep.equal({})
|
||||
})
|
||||
|
||||
it('should delete keys for undefined values, recursively', () => {
|
||||
let obj = { foo: { bar: 'bar' } }
|
||||
updater.updateObjectValue(obj, 'foo.bar', undefined)
|
||||
|
||||
expect(obj).deep.equal({ foo: {} })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Operations', () => {
|
||||
describe('haveSameContext', () => {
|
||||
|
|
|
@ -862,4 +862,51 @@ describe('Utils', () => {
|
|||
assert.equal(Utils.isObject(''), false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setObjectValue', () => {
|
||||
it('should be able to set object properties', () => {
|
||||
let obj = {}
|
||||
Utils.setObjectValue(obj, 'foo', 'foo')
|
||||
expect(obj).deep.equal({ foo: 'foo' })
|
||||
})
|
||||
|
||||
it('should be able to set object properties recursively on existing objects', () => {
|
||||
let obj = { foo: {} }
|
||||
Utils.setObjectValue(obj, 'foo.bar', 'foo')
|
||||
expect(obj).deep.equal({ foo: { bar: 'foo' } })
|
||||
})
|
||||
|
||||
it('should be able to set object properties recursively on deep objects', () => {
|
||||
let obj = { foo: { bar: { baz: {} } } }
|
||||
Utils.setObjectValue(obj, 'foo.bar.baz.test', 'value')
|
||||
expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
|
||||
})
|
||||
|
||||
it('should be able to replace object properties recursively on deep objects', () => {
|
||||
let obj = { foo: { bar: { baz: { test: 'test' } } } }
|
||||
Utils.setObjectValue(obj, 'foo.bar.baz.test', 'value')
|
||||
expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
|
||||
})
|
||||
|
||||
it('should not set object properties recursively on non-existing objects', () => {
|
||||
let obj = { foo: {} }
|
||||
Utils.setObjectValue(obj, 'bar.bar', 'value')
|
||||
|
||||
expect(obj).deep.equal({ foo: {} })
|
||||
})
|
||||
|
||||
it('should delete keys for undefined values', () => {
|
||||
let obj = { foo: 'foo' }
|
||||
Utils.setObjectValue(obj, 'foo', undefined)
|
||||
|
||||
expect(obj).deep.equal({})
|
||||
})
|
||||
|
||||
it('should delete keys for undefined values, recursively', () => {
|
||||
let obj = { foo: { bar: 'bar' } }
|
||||
Utils.setObjectValue(obj, 'foo.bar', undefined)
|
||||
|
||||
expect(obj).deep.equal({ foo: {} })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Literal, Optional, Union
|
||||
from typing import List, Literal, Optional, Union
|
||||
|
||||
from pydantic import BaseModel, Field, RootModel
|
||||
|
||||
|
@ -14,10 +14,11 @@ class OperationMessage(BaseModel):
|
|||
"""Message sent from one peer to all the others"""
|
||||
|
||||
kind: Literal["OperationMessage"] = "OperationMessage"
|
||||
verb: Literal["upsert", "update", "delete"]
|
||||
verb: Literal["upsert", "update", "delete", "batch"]
|
||||
subject: Literal["map", "datalayer", "feature"]
|
||||
metadata: Optional[dict] = None
|
||||
key: Optional[str] = None
|
||||
operations: Optional[List] = None
|
||||
|
||||
|
||||
class PeerMessage(BaseModel):
|
||||
|
|
|
@ -18,6 +18,7 @@ DATALAYER_DATA = {
|
|||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"id": "ExNTQ",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [14.68896484375, 48.55297816440071],
|
||||
|
@ -41,7 +42,7 @@ class LicenceFactory(factory.django.DjangoModelFactory):
|
|||
|
||||
|
||||
class TileLayerFactory(factory.django.DjangoModelFactory):
|
||||
name = "Test zoom layer"
|
||||
name = "Test tilelayer"
|
||||
url_template = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
attribution = "Test layer attribution"
|
||||
|
||||
|
|
|
@ -350,8 +350,7 @@ def test_should_redraw_list_on_feature_delete(live_server, openmap, page, bootst
|
|||
buttons.first.click()
|
||||
page.locator("dialog").get_by_role("button", name="OK").click()
|
||||
expect(buttons).to_have_count(2)
|
||||
page.get_by_role("button", name="Cancel edits").click()
|
||||
page.locator("dialog").get_by_role("button", name="OK").click()
|
||||
page.get_by_role("button", name="Undo").click()
|
||||
expect(buttons).to_have_count(3)
|
||||
|
||||
|
||||
|
|
|
@ -261,6 +261,9 @@ def test_can_create_new_rule(live_server, page, openmap):
|
|||
page.get_by_title("AliceBlue").first.click()
|
||||
colors = getColors(markers)
|
||||
assert colors.count("rgb(240, 248, 255)") == 3
|
||||
page.get_by_role("button", name="Undo").click()
|
||||
colors = getColors(markers)
|
||||
assert colors.count("rgb(240, 248, 255)") == 0
|
||||
|
||||
|
||||
def test_can_deactive_rule_from_list(live_server, page, openmap):
|
||||
|
|
|
@ -64,8 +64,7 @@ def test_cancel_deleting_datalayer_should_restore(
|
|||
page.get_by_role("button", name="OK").click()
|
||||
expect(markers).to_have_count(0)
|
||||
expect(page.get_by_text("test datalayer")).to_be_hidden()
|
||||
page.get_by_role("button", name="Cancel edits").click()
|
||||
page.locator("dialog").get_by_role("button", name="OK").click()
|
||||
page.get_by_role("button", name="Undo").click()
|
||||
expect(markers).to_have_count(1)
|
||||
expect(page.locator(".umap-browser").get_by_text("test datalayer")).to_be_visible()
|
||||
|
||||
|
@ -160,7 +159,6 @@ def test_can_create_new_datalayer(live_server, openmap, page, datalayer):
|
|||
page.locator('input[name="name"]').click()
|
||||
page.locator('input[name="name"]').fill("Layer A with a new name")
|
||||
expect(page.get_by_text("Layer A with a new name")).to_be_visible()
|
||||
page.get_by_role("button", name="Save").click()
|
||||
with page.expect_response(re.compile(".*/datalayer/update/.*")):
|
||||
page.get_by_role("button", name="Save").click()
|
||||
assert DataLayer.objects.count() == 2
|
||||
|
|
|
@ -117,8 +117,7 @@ def test_should_reset_style_on_cancel(live_server, openmap, page, bootstrap):
|
|||
expect(page.locator(".leaflet-overlay-pane path[fill='GoldenRod']")).to_have_count(
|
||||
1
|
||||
)
|
||||
page.get_by_role("button", name="Cancel edits").click()
|
||||
page.locator("dialog").get_by_role("button", name="OK").click()
|
||||
page.get_by_role("button", name="Undo").click()
|
||||
expect(page.locator(".leaflet-overlay-pane path[fill='DarkBlue']")).to_have_count(1)
|
||||
|
||||
|
||||
|
|
|
@ -292,9 +292,10 @@ def test_should_display_alert_on_conflict(context, live_server, datalayer, openm
|
|||
# Change name on page two and save
|
||||
page_two.locator(".leaflet-marker-icon").click(modifiers=["Shift"])
|
||||
page_two.locator('input[name="name"]').fill("name from page two")
|
||||
page_two.wait_for_timeout(300) # Time for the input debounce.
|
||||
|
||||
# Map should be in dirty status
|
||||
expect(page_two.get_by_text("Cancel edits")).to_be_visible()
|
||||
expect(page_two.get_by_text("Save", exact=True)).to_be_enabled()
|
||||
with page_two.expect_response(re.compile(r".*/datalayer/update/.*")):
|
||||
page_two.get_by_role("button", name="Save").click()
|
||||
|
||||
|
@ -306,7 +307,7 @@ def test_should_display_alert_on_conflict(context, live_server, datalayer, openm
|
|||
# We should have an alert with some actions
|
||||
expect(page_two.get_by_text("Whoops! Other contributor(s) changed")).to_be_visible()
|
||||
# Map should still be in dirty status
|
||||
expect(page_two.get_by_text("Cancel edits")).to_be_visible()
|
||||
expect(page_two.get_by_text("Save", exact=True)).to_be_enabled()
|
||||
|
||||
# Override data from page two
|
||||
with page_two.expect_response(re.compile(r".*/datalayer/update/.*")):
|
||||
|
@ -317,4 +318,4 @@ def test_should_display_alert_on_conflict(context, live_server, datalayer, openm
|
|||
data = json.loads(Path(saved.geojson.path).read_text())
|
||||
assert data["features"][0]["properties"]["name"] == "name from page two"
|
||||
# Map should not be in dirty status anymore
|
||||
expect(page_two.get_by_text("Cancel edits")).to_be_hidden()
|
||||
expect(page_two.get_by_text("Save", exact=True)).to_be_disabled()
|
||||
|
|
|
@ -16,17 +16,15 @@ def test_reseting_map_would_remove_from_save_queue(
|
|||
page.on("request", register_request)
|
||||
page.locator('input[name="name"]').click()
|
||||
page.locator('input[name="name"]').fill("new name")
|
||||
page.get_by_role("button", name="Cancel edits").click()
|
||||
page.get_by_role("button", name="OK").click()
|
||||
page.get_by_role("button", name="Undo").click()
|
||||
page.wait_for_timeout(500)
|
||||
page.get_by_role("button", name="Edit").click()
|
||||
page.get_by_role("button", name="Manage layers").click()
|
||||
page.get_by_role("button", name="Edit", exact=True).click()
|
||||
page.locator('input[name="name"]').click()
|
||||
page.locator('input[name="name"]').fill("new datalayer name")
|
||||
page.wait_for_timeout(300) # Time of the Input debounce
|
||||
with page.expect_response(re.compile(".*/datalayer/update/.*")):
|
||||
page.get_by_role("button", name="Save").click()
|
||||
page.get_by_role("button", name="Save", exact=True).click()
|
||||
assert len(requests) == 1
|
||||
assert requests == [
|
||||
(
|
||||
|
|
267
umap/tests/integration/test_undo_redo.py
Normal file
|
@ -0,0 +1,267 @@
|
|||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from umap.models import Map, TileLayer
|
||||
|
||||
from ..base import DataLayerFactory
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
DATALAYER_DATA = {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "name poly",
|
||||
},
|
||||
"id": "gyNzM",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[11.25, 53.585984],
|
||||
[10.151367, 52.975108],
|
||||
[12.689209, 52.167194],
|
||||
[14.084473, 53.199452],
|
||||
[12.634277, 53.618579],
|
||||
[11.25, 53.585984],
|
||||
[11.25, 53.585984],
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def map_with_polygon(map, live_server):
|
||||
map.settings["properties"]["zoom"] = 6
|
||||
map.settings["geometry"] = {
|
||||
"type": "Point",
|
||||
"coordinates": [8.429, 53.239],
|
||||
}
|
||||
map.edit_status = Map.ANONYMOUS
|
||||
map.save()
|
||||
DataLayerFactory(map=map, data=DATALAYER_DATA)
|
||||
return map
|
||||
|
||||
|
||||
def test_can_undo_redo_map_name_change(page, live_server, tilelayer):
|
||||
page.goto(f"{live_server.url}/en/map/new/")
|
||||
|
||||
expect(page.locator(".edit-undo")).to_be_disabled()
|
||||
expect(page.locator(".edit-redo")).to_be_disabled()
|
||||
page.get_by_title("Edit map name and caption").click()
|
||||
name_input = page.locator('.map-metadata input[name="name"]')
|
||||
expect(name_input).to_be_visible()
|
||||
name_input.click()
|
||||
name_input.press("Control+a")
|
||||
name_input.fill("New map name")
|
||||
expect(page.locator(".edit-undo")).to_be_enabled()
|
||||
expect(page.locator(".edit-redo")).to_be_disabled()
|
||||
map_name = page.locator(".umap-main-edit-toolbox .map-name")
|
||||
expect(map_name).to_have_text("New map name")
|
||||
name_input.fill("New name again")
|
||||
expect(map_name).to_have_text("New name again")
|
||||
|
||||
page.locator(".edit-undo").click()
|
||||
expect(map_name).to_have_text("New map name")
|
||||
expect(page.locator(".edit-undo")).to_be_enabled()
|
||||
expect(page.locator(".edit-redo")).to_be_enabled()
|
||||
|
||||
page.locator(".edit-redo").click()
|
||||
expect(map_name).to_have_text("New name again")
|
||||
expect(page.locator(".edit-undo")).to_be_enabled()
|
||||
expect(page.locator(".edit-redo")).to_be_disabled()
|
||||
|
||||
page.locator(".edit-undo").click()
|
||||
expect(map_name).to_have_text("New map name")
|
||||
expect(page.locator(".edit-undo")).to_be_enabled()
|
||||
expect(page.locator(".edit-redo")).to_be_enabled()
|
||||
|
||||
|
||||
def test_can_undo_redo_layer_color_change(
|
||||
page, map_with_polygon, live_server, tilelayer
|
||||
):
|
||||
page.goto(f"{live_server.url}{map_with_polygon.get_absolute_url()}?edit")
|
||||
|
||||
expect(page.locator(".edit-undo")).to_be_disabled()
|
||||
expect(page.locator(".edit-redo")).to_be_disabled()
|
||||
page.get_by_role("button", name="Manage layers").click()
|
||||
page.locator(".panel").get_by_title("Edit", exact=True).click()
|
||||
page.get_by_text("Shape properties").click()
|
||||
page.locator(".umap-field-color .define").click()
|
||||
expect(page.locator(".leaflet-overlay-pane path[fill='DarkBlue']")).to_have_count(1)
|
||||
page.get_by_title("DarkRed").first.click()
|
||||
expect(page.locator(".leaflet-overlay-pane path[fill='DarkRed']")).to_have_count(1)
|
||||
expect(page.locator(".edit-undo")).to_be_enabled()
|
||||
expect(page.locator(".edit-redo")).to_be_disabled()
|
||||
|
||||
page.locator(".edit-undo").click()
|
||||
expect(page.locator(".leaflet-overlay-pane path[fill='DarkBlue']")).to_have_count(1)
|
||||
expect(page.locator(".leaflet-overlay-pane path[fill='DarkRed']")).to_have_count(0)
|
||||
expect(page.locator(".edit-undo")).to_be_disabled()
|
||||
expect(page.locator(".edit-redo")).to_be_enabled()
|
||||
|
||||
page.locator(".edit-redo").click()
|
||||
expect(page.locator(".leaflet-overlay-pane path[fill='DarkRed']")).to_have_count(1)
|
||||
expect(page.locator(".leaflet-overlay-pane path[fill='DarkBlue']")).to_have_count(0)
|
||||
expect(page.locator(".edit-undo")).to_be_enabled()
|
||||
expect(page.locator(".edit-redo")).to_be_disabled()
|
||||
|
||||
|
||||
def test_can_undo_redo_tilelayer_change(live_server, page, openmap, tilelayer):
|
||||
TileLayer.objects.create(
|
||||
url_template="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",
|
||||
attribution="OSM/Carto",
|
||||
name="Black Tiles",
|
||||
)
|
||||
page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
|
||||
old_pattern = re.compile(
|
||||
r"https://[abc]{1}.tile.openstreetmap.fr/osmfr/\d+/\d+/\d+.png"
|
||||
)
|
||||
tiles = page.locator(".leaflet-tile-pane img")
|
||||
expect(tiles.first).to_have_attribute("src", old_pattern)
|
||||
|
||||
new_pattern = re.compile(
|
||||
r"https://[abcd]{1}.basemaps.cartocdn.com/dark_all/\d+/\d+/\d+.png"
|
||||
)
|
||||
page.get_by_role("button", name="Change tilelayers").click()
|
||||
page.locator("li").filter(has_text="Black Tiles").get_by_role("img").click()
|
||||
|
||||
tiles = page.locator(".leaflet-tile-pane img")
|
||||
expect(tiles.first).to_have_attribute("src", new_pattern)
|
||||
|
||||
page.locator(".edit-undo").click()
|
||||
tiles = page.locator(".leaflet-tile-pane img")
|
||||
expect(tiles.first).to_have_attribute("src", old_pattern)
|
||||
|
||||
page.locator(".edit-redo").click()
|
||||
tiles = page.locator(".leaflet-tile-pane img")
|
||||
expect(tiles.first).to_have_attribute("src", new_pattern)
|
||||
|
||||
|
||||
def test_can_undo_redo_marker_drag(live_server, page, tilelayer):
|
||||
page.goto(f"{live_server.url}/en/map/new")
|
||||
|
||||
marker = page.locator(".leaflet-marker-icon")
|
||||
map = page.locator("#map")
|
||||
|
||||
# Create a marker
|
||||
page.get_by_title("Draw a marker").click()
|
||||
map.click(position={"x": 225, "y": 225})
|
||||
expect(marker).to_have_count(1)
|
||||
|
||||
# Drag marker
|
||||
old_bbox = marker.bounding_box()
|
||||
marker.first.drag_to(map, target_position={"x": 250, "y": 250})
|
||||
assert marker.bounding_box() != old_bbox
|
||||
|
||||
# Undo
|
||||
page.locator(".edit-undo").click()
|
||||
assert marker.bounding_box() == old_bbox
|
||||
|
||||
# Redo
|
||||
page.locator(".edit-redo").click()
|
||||
assert marker.bounding_box() != old_bbox
|
||||
|
||||
|
||||
def test_can_undo_redo_polygon_geometry_change(live_server, page, tilelayer):
|
||||
page.goto(f"{live_server.url}/en/map/new")
|
||||
|
||||
# Click on the Draw a polygon button on a new map.
|
||||
page.get_by_title("Draw a polygon").click()
|
||||
|
||||
polygon = page.locator("path[fill='DarkBlue']")
|
||||
expect(polygon).to_have_count(0)
|
||||
|
||||
# Click on the map, it will create a polygon.
|
||||
map = page.locator("#map")
|
||||
map.click(position={"x": 200, "y": 200})
|
||||
map.click(position={"x": 100, "y": 200})
|
||||
map.click(position={"x": 100, "y": 100})
|
||||
map.click(position={"x": 100, "y": 100})
|
||||
|
||||
# It is created on peerA, and should be on peerB
|
||||
expect(polygon).to_have_count(1)
|
||||
old_bbox = polygon.bounding_box()
|
||||
|
||||
edited_vertex = page.locator(".leaflet-middle-icon:nth-child(3)").first
|
||||
edited_vertex.drag_to(map, target_position={"x": 250, "y": 250})
|
||||
page.keyboard.press("Escape")
|
||||
|
||||
assert polygon.bounding_box() != old_bbox
|
||||
|
||||
page.locator(".edit-undo").click()
|
||||
assert polygon.bounding_box() == old_bbox
|
||||
|
||||
page.locator(".edit-redo").click()
|
||||
assert polygon.bounding_box() != old_bbox
|
||||
|
||||
|
||||
def test_can_undo_redo_marker_create(live_server, page, tilelayer):
|
||||
page.goto(f"{live_server.url}/en/map/new")
|
||||
|
||||
page.get_by_title("Open Browser").click()
|
||||
marker = page.locator(".leaflet-marker-icon")
|
||||
map = page.locator("#map")
|
||||
|
||||
# Create a marker
|
||||
page.get_by_title("Draw a marker").click()
|
||||
map.click(position={"x": 600, "y": 100})
|
||||
expect(marker).to_have_count(1)
|
||||
expect(page.locator(".panel .datalayer")).to_have_count(1)
|
||||
|
||||
page.locator(".edit-undo").click()
|
||||
expect(marker).to_have_count(0)
|
||||
# Layer still exists
|
||||
expect(page.locator(".panel .datalayer")).to_have_count(1)
|
||||
|
||||
page.locator(".edit-undo").click()
|
||||
expect(page.locator(".panel .datalayer")).to_have_count(0)
|
||||
|
||||
page.locator(".edit-redo").click()
|
||||
expect(page.locator(".panel .datalayer")).to_have_count(1)
|
||||
|
||||
page.locator(".edit-redo").click()
|
||||
expect(marker).to_have_count(1)
|
||||
|
||||
|
||||
def test_undo_redo_import(live_server, page, tilelayer):
|
||||
page.goto(f"{live_server.url}/map/new/")
|
||||
page.get_by_title("Open Browser").click()
|
||||
|
||||
page.get_by_title("Import data").click()
|
||||
file_input = page.locator("input[type='file']")
|
||||
with page.expect_file_chooser() as fc_info:
|
||||
file_input.click()
|
||||
file_chooser = fc_info.value
|
||||
path = Path(__file__).parent.parent / "fixtures/test_upload_data.json"
|
||||
file_chooser.set_files(path)
|
||||
page.get_by_role("button", name="Import data", exact=True).click()
|
||||
# Close the import panel
|
||||
page.keyboard.press("Escape")
|
||||
|
||||
layers = page.locator(".umap-browser .datalayer")
|
||||
expect(layers).to_have_count(1)
|
||||
|
||||
features_count = page.locator(".umap-browser .datalayer-counter")
|
||||
expect(features_count).to_have_text("(5)")
|
||||
|
||||
page.locator(".edit-undo").click()
|
||||
expect(features_count).to_be_hidden()
|
||||
expect(layers).to_have_count(1)
|
||||
|
||||
page.locator(".edit-undo").click()
|
||||
expect(layers).to_have_count(0)
|
||||
|
||||
page.locator(".edit-redo").click()
|
||||
expect(layers).to_have_count(1)
|
||||
|
||||
page.locator(".edit-redo").click()
|
||||
expect(features_count).to_have_text("(5)")
|
|
@ -659,3 +659,73 @@ def test_should_sync_line_on_escape(new_page, asgi_live_server, tilelayer):
|
|||
|
||||
expect(peerA.locator("path")).to_have_count(1)
|
||||
expect(peerB.locator("path")).to_have_count(1)
|
||||
|
||||
|
||||
@pytest.mark.xdist_group(name="websockets")
|
||||
def test_should_sync_datalayer_clear(
|
||||
new_page, asgi_live_server, tilelayer, map, datalayer
|
||||
):
|
||||
map.settings["properties"]["syncEnabled"] = True
|
||||
map.edit_status = Map.ANONYMOUS
|
||||
map.save()
|
||||
|
||||
# Create two tabs
|
||||
peerA = new_page("Page A")
|
||||
peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
|
||||
peerB = new_page("Page B")
|
||||
peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
|
||||
expect(peerA.locator(".leaflet-marker-icon")).to_have_count(1)
|
||||
expect(peerB.locator(".leaflet-marker-icon")).to_have_count(1)
|
||||
|
||||
# Clear layer in peer A
|
||||
peerA.get_by_role("button", name="Manage layers").click()
|
||||
peerA.get_by_role("button", name="Edit", exact=True).click()
|
||||
peerA.locator("summary").filter(has_text="Advanced actions").click()
|
||||
peerA.get_by_role("button", name="Empty").click()
|
||||
expect(peerA.locator(".leaflet-marker-icon")).to_have_count(0)
|
||||
expect(peerB.locator(".leaflet-marker-icon")).to_have_count(0)
|
||||
|
||||
# Undo in peer A
|
||||
peerA.get_by_role("button", name="Undo").click()
|
||||
expect(peerA.locator(".leaflet-marker-icon")).to_have_count(1)
|
||||
expect(peerB.locator(".leaflet-marker-icon")).to_have_count(1)
|
||||
|
||||
|
||||
@pytest.mark.xdist_group(name="websockets")
|
||||
def test_should_save_remote_dirty_datalayers(new_page, asgi_live_server, tilelayer):
|
||||
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
|
||||
map.settings["properties"]["syncEnabled"] = True
|
||||
map.save()
|
||||
|
||||
assert not DataLayer.objects.count()
|
||||
|
||||
# Create two tabs
|
||||
peerA = new_page("Page A")
|
||||
peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
|
||||
peerB = new_page("Page B")
|
||||
peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
|
||||
|
||||
# Create a new layer from peerA
|
||||
peerA.get_by_role("button", name="Manage layers").click()
|
||||
peerA.get_by_role("button", name="Add a layer").click()
|
||||
|
||||
# Create a new layer from peerB
|
||||
peerB.get_by_role("button", name="Manage layers").click()
|
||||
peerB.get_by_role("button", name="Add a layer").click()
|
||||
|
||||
# Save from peerA to the server
|
||||
counter = 0
|
||||
|
||||
def on_response(response):
|
||||
nonlocal counter
|
||||
if "/datalayer/create/" in response.url:
|
||||
counter += 1
|
||||
# Wait for the two datalayer saves
|
||||
if counter == 2:
|
||||
return True
|
||||
return False
|
||||
|
||||
with peerA.expect_response(on_response):
|
||||
peerA.get_by_role("button", name="Save").click()
|
||||
|
||||
assert DataLayer.objects.count() == 2
|
||||
|
|
|
@ -103,6 +103,7 @@ def test_get_version(map, datalayer):
|
|||
],
|
||||
"type": "Point",
|
||||
},
|
||||
"id": "ExNTQ",
|
||||
"properties": {
|
||||
"_umap_options": {
|
||||
"color": "DarkCyan",
|
||||
|
|
|
@ -694,6 +694,7 @@ def test_download(client, map, datalayer):
|
|||
"coordinates": [14.68896484375, 48.55297816440071],
|
||||
"type": "Point",
|
||||
},
|
||||
"id": "ExNTQ",
|
||||
"properties": {
|
||||
"_umap_options": {"color": "DarkCyan", "iconClass": "Ball"},
|
||||
"description": "Da place anonymous again 755",
|
||||
|
|