mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-04 05:28:59 +01:00 
			
		
		
		
	chore(react/collections): start porting geomap
This commit is contained in:
		
							parent
							
								
									5ea15cc7eb
								
							
						
					
					
						commit
						1969ce562a
					
				@ -4,6 +4,7 @@ import FNote from "../../entities/fnote";
 | 
				
			|||||||
import "./NoteList.css";
 | 
					import "./NoteList.css";
 | 
				
			||||||
import { ListView, GridView } from "./legacy/ListOrGridView";
 | 
					import { ListView, GridView } from "./legacy/ListOrGridView";
 | 
				
			||||||
import { useEffect, useRef, useState } from "preact/hooks";
 | 
					import { useEffect, useRef, useState } from "preact/hooks";
 | 
				
			||||||
 | 
					import GeoView from "./geomap";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface NoteListProps {
 | 
					interface NoteListProps {
 | 
				
			||||||
    note?: FNote | null;
 | 
					    note?: FNote | null;
 | 
				
			||||||
@ -67,6 +68,8 @@ function getComponentByViewType(note: FNote, noteIds: string[], viewType: ViewTy
 | 
				
			|||||||
            return <ListView {...props} />;
 | 
					            return <ListView {...props} />;
 | 
				
			||||||
        case "grid":
 | 
					        case "grid":
 | 
				
			||||||
            return <GridView {...props} />;
 | 
					            return <GridView {...props} />;
 | 
				
			||||||
 | 
					        case "geoMap":
 | 
				
			||||||
 | 
					            return <GeoView {...props} />;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										74
									
								
								apps/client/src/widgets/collections/geomap/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								apps/client/src/widgets/collections/geomap/index.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,74 @@
 | 
				
			|||||||
 | 
					.geo-view {
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.geo-map-container {
 | 
				
			||||||
 | 
					  height: 100%;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.leaflet-pane {
 | 
				
			||||||
 | 
					  z-index: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.leaflet-top,
 | 
				
			||||||
 | 
					.leaflet-bottom {
 | 
				
			||||||
 | 
					  z-index: 997;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.geo-map-container.placing-note {
 | 
				
			||||||
 | 
					  cursor: crosshair;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.geo-map-container .marker-pin {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.geo-map-container .leaflet-div-icon {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  background: transparent;
 | 
				
			||||||
 | 
					  border: 0;
 | 
				
			||||||
 | 
					  overflow: visible;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.geo-map-container .leaflet-div-icon .icon-shadow {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  z-index: -1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.geo-map-container .leaflet-div-icon .bx {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 3px;
 | 
				
			||||||
 | 
					  left: 2px;
 | 
				
			||||||
 | 
					  background-color: white;
 | 
				
			||||||
 | 
					  color: black;
 | 
				
			||||||
 | 
					  padding: 2px;
 | 
				
			||||||
 | 
					  border-radius: 50%;
 | 
				
			||||||
 | 
					  font-size: 17px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.geo-map-container .leaflet-div-icon .title-label {
 | 
				
			||||||
 | 
					  display: block;
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 100%;
 | 
				
			||||||
 | 
					  left: 50%;
 | 
				
			||||||
 | 
					  transform: translateX(-50%);
 | 
				
			||||||
 | 
					  font-size: 0.75rem;
 | 
				
			||||||
 | 
					  height: 1rem;
 | 
				
			||||||
 | 
					  color: black;
 | 
				
			||||||
 | 
					  width: 100px;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  text-overflow: ellipsis;
 | 
				
			||||||
 | 
					  text-shadow: -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 1px 0 white;
 | 
				
			||||||
 | 
					  white-space: no-wrap;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.geo-map-container.dark .leaflet-div-icon .title-label {
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					  text-shadow: -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black, 1px 1px 0 black;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										16
									
								
								apps/client/src/widgets/collections/geomap/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								apps/client/src/widgets/collections/geomap/index.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					import Map from "./map";
 | 
				
			||||||
 | 
					import "./index.css";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659];
 | 
				
			||||||
 | 
					const DEFAULT_ZOOM = 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function GeoView() {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <div className="geo-view">
 | 
				
			||||||
 | 
					            <Map
 | 
				
			||||||
 | 
					                coordinates={DEFAULT_COORDINATES}
 | 
				
			||||||
 | 
					                zoom={DEFAULT_ZOOM}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										70
									
								
								apps/client/src/widgets/collections/geomap/map.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								apps/client/src/widgets/collections/geomap/map.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,70 @@
 | 
				
			|||||||
 | 
					import { useEffect, useRef, useState } from "preact/hooks";
 | 
				
			||||||
 | 
					import L, { LatLng, Layer } from "leaflet";
 | 
				
			||||||
 | 
					import "leaflet/dist/leaflet.css";
 | 
				
			||||||
 | 
					import { useNoteContext, useNoteLabel } from "../../react/hooks";
 | 
				
			||||||
 | 
					import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS } from "./map_layer";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface MapProps {
 | 
				
			||||||
 | 
					    coordinates: LatLng | [number, number];
 | 
				
			||||||
 | 
					    zoom: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Map({ coordinates, zoom }: MapProps) {
 | 
				
			||||||
 | 
					    const mapRef = useRef<L.Map>(null);
 | 
				
			||||||
 | 
					    const containerRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					    const { note } = useNoteContext();
 | 
				
			||||||
 | 
					    const [ layerName ] = useNoteLabel(note, "map:style");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        if (!containerRef.current) return;
 | 
				
			||||||
 | 
					        mapRef.current = L.map(containerRef.current, {
 | 
				
			||||||
 | 
					            worldCopyJump: true
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Load the layer asynchronously.
 | 
				
			||||||
 | 
					    const [ layer, setLayer ] = useState<Layer>();
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        async function load() {
 | 
				
			||||||
 | 
					            const layerData = MAP_LAYERS[layerName ?? DEFAULT_MAP_LAYER_NAME];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (layerData.type === "vector") {
 | 
				
			||||||
 | 
					                const style = (typeof layerData.style === "string" ? layerData.style : await layerData.style());
 | 
				
			||||||
 | 
					                await import("@maplibre/maplibre-gl-leaflet");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                setLayer(L.maplibreGL({
 | 
				
			||||||
 | 
					                    style: style as any
 | 
				
			||||||
 | 
					                }));
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                setLayer(L.tileLayer(layerData.url, {
 | 
				
			||||||
 | 
					                    attribution: layerData.attribution,
 | 
				
			||||||
 | 
					                    detectRetina: true
 | 
				
			||||||
 | 
					                }));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        load();
 | 
				
			||||||
 | 
					    }, [ layerName ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Attach layer to the map.
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        const map = mapRef.current;
 | 
				
			||||||
 | 
					        const layerToAdd = layer;
 | 
				
			||||||
 | 
					        console.log("Add layer ", map, layerToAdd);
 | 
				
			||||||
 | 
					        if (!map || !layerToAdd) return;
 | 
				
			||||||
 | 
					        layerToAdd.addTo(map);
 | 
				
			||||||
 | 
					        return () => layerToAdd.removeFrom(map);
 | 
				
			||||||
 | 
					    }, [ mapRef, layer ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // React to coordinate changes.
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        if (!mapRef.current) return;
 | 
				
			||||||
 | 
					        mapRef.current.setView(coordinates, zoom);
 | 
				
			||||||
 | 
					    }, [ mapRef, coordinates, zoom ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <div ref={containerRef} className="geo-map-container">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -2,7 +2,7 @@ import { t } from "i18next";
 | 
				
			|||||||
import FNote from "../../entities/fnote";
 | 
					import FNote from "../../entities/fnote";
 | 
				
			||||||
import attributes from "../../services/attributes";
 | 
					import attributes from "../../services/attributes";
 | 
				
			||||||
import NoteContextAwareWidget from "../note_context_aware_widget";
 | 
					import NoteContextAwareWidget from "../note_context_aware_widget";
 | 
				
			||||||
import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS, type MapLayer } from "../view_widgets/geo_view/map_layer";
 | 
					import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS, type MapLayer } from "../collections/geomap/map_layer";
 | 
				
			||||||
import { ViewTypeOptions } from "../collections/interface";
 | 
					import { ViewTypeOptions } from "../collections/interface";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface BookConfig {
 | 
					interface BookConfig {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
import ViewMode, { ViewModeArgs } from "../view_mode.js";
 | 
					import ViewMode, { ViewModeArgs } from "../view_mode.js";
 | 
				
			||||||
import L from "leaflet";
 | 
					import L from "leaflet";
 | 
				
			||||||
import type { GPX, LatLng, Layer, LeafletMouseEvent, Map, Marker } from "leaflet";
 | 
					import type { GPX, LatLng, Layer, LeafletMouseEvent, Map, Marker } from "leaflet";
 | 
				
			||||||
import "leaflet/dist/leaflet.css";
 | 
					 | 
				
			||||||
import SpacedUpdate from "../../../services/spaced_update.js";
 | 
					import SpacedUpdate from "../../../services/spaced_update.js";
 | 
				
			||||||
import { t } from "../../../services/i18n.js";
 | 
					import { t } from "../../../services/i18n.js";
 | 
				
			||||||
import processNoteWithMarker, { processNoteWithGpxTrack } from "./markers.js";
 | 
					import processNoteWithMarker, { processNoteWithGpxTrack } from "./markers.js";
 | 
				
			||||||
@ -13,88 +12,6 @@ import { openMapContextMenu } from "./context_menu.js";
 | 
				
			|||||||
import attributes from "../../../services/attributes.js";
 | 
					import attributes from "../../../services/attributes.js";
 | 
				
			||||||
import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS } from "./map_layer.js";
 | 
					import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS } from "./map_layer.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TPL = /*html*/`
 | 
					 | 
				
			||||||
<div class="geo-view">
 | 
					 | 
				
			||||||
    <style>
 | 
					 | 
				
			||||||
        .geo-view {
 | 
					 | 
				
			||||||
            overflow: hidden;
 | 
					 | 
				
			||||||
            position: relative;
 | 
					 | 
				
			||||||
            height: 100%;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .geo-map-container {
 | 
					 | 
				
			||||||
            height: 100%;
 | 
					 | 
				
			||||||
            overflow: hidden;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .leaflet-pane {
 | 
					 | 
				
			||||||
            z-index: 1;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .leaflet-top,
 | 
					 | 
				
			||||||
        .leaflet-bottom {
 | 
					 | 
				
			||||||
            z-index: 997;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .geo-map-container.placing-note {
 | 
					 | 
				
			||||||
            cursor: crosshair;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .geo-map-container .marker-pin {
 | 
					 | 
				
			||||||
            position: relative;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .geo-map-container .leaflet-div-icon {
 | 
					 | 
				
			||||||
            position: relative;
 | 
					 | 
				
			||||||
            background: transparent;
 | 
					 | 
				
			||||||
            border: 0;
 | 
					 | 
				
			||||||
            overflow: visible;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .geo-map-container .leaflet-div-icon .icon-shadow {
 | 
					 | 
				
			||||||
            position: absolute;
 | 
					 | 
				
			||||||
            top: 0;
 | 
					 | 
				
			||||||
            left: 0;
 | 
					 | 
				
			||||||
            z-index: -1;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .geo-map-container .leaflet-div-icon .bx {
 | 
					 | 
				
			||||||
            position: absolute;
 | 
					 | 
				
			||||||
            top: 3px;
 | 
					 | 
				
			||||||
            left: 2px;
 | 
					 | 
				
			||||||
            background-color: white;
 | 
					 | 
				
			||||||
            color: black;
 | 
					 | 
				
			||||||
            padding: 2px;
 | 
					 | 
				
			||||||
            border-radius: 50%;
 | 
					 | 
				
			||||||
            font-size: 17px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .geo-map-container .leaflet-div-icon .title-label {
 | 
					 | 
				
			||||||
            display: block;
 | 
					 | 
				
			||||||
            position: absolute;
 | 
					 | 
				
			||||||
            top: 100%;
 | 
					 | 
				
			||||||
            left: 50%;
 | 
					 | 
				
			||||||
            transform: translateX(-50%);
 | 
					 | 
				
			||||||
            font-size: 0.75rem;
 | 
					 | 
				
			||||||
            height: 1rem;
 | 
					 | 
				
			||||||
            color: black;
 | 
					 | 
				
			||||||
            width: 100px;
 | 
					 | 
				
			||||||
            text-align: center;
 | 
					 | 
				
			||||||
            text-overflow: ellipsis;
 | 
					 | 
				
			||||||
            text-shadow: -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 1px 0 white;
 | 
					 | 
				
			||||||
            white-space: no-wrap;
 | 
					 | 
				
			||||||
            overflow: hidden;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .geo-map-container.dark .leaflet-div-icon .title-label {
 | 
					 | 
				
			||||||
            color: white;
 | 
					 | 
				
			||||||
            text-shadow: -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black, 1px 1px 0 black;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    </style>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="geo-map-container"></div>
 | 
					 | 
				
			||||||
</div>`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface MapData {
 | 
					interface MapData {
 | 
				
			||||||
    view?: {
 | 
					    view?: {
 | 
				
			||||||
        center?: LatLng | [number, number];
 | 
					        center?: LatLng | [number, number];
 | 
				
			||||||
@ -102,8 +19,6 @@ interface MapData {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659];
 | 
					 | 
				
			||||||
const DEFAULT_ZOOM = 2;
 | 
					 | 
				
			||||||
export const LOCATION_ATTRIBUTE = "geolocation";
 | 
					export const LOCATION_ATTRIBUTE = "geolocation";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum State {
 | 
					enum State {
 | 
				
			||||||
@ -142,27 +57,8 @@ export default class GeoView extends ViewMode<MapData> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async renderMap() {
 | 
					    async renderMap() {
 | 
				
			||||||
        const map = L.map(this.$container[0], {
 | 
					        const layerName = this.parentNote.getLabelValue("map:style") ?? ;
 | 
				
			||||||
            worldCopyJump: true
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const layerName = this.parentNote.getLabelValue("map:style") ?? DEFAULT_MAP_LAYER_NAME;
 | 
					 | 
				
			||||||
        let layer: Layer;
 | 
					 | 
				
			||||||
        const layerData = MAP_LAYERS[layerName];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (layerData.type === "vector") {
 | 
					 | 
				
			||||||
            const style = (typeof layerData.style === "string" ? layerData.style : await layerData.style());
 | 
					 | 
				
			||||||
            await import("@maplibre/maplibre-gl-leaflet");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            layer = L.maplibreGL({
 | 
					 | 
				
			||||||
                style: style as any
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            layer = L.tileLayer(layerData.url, {
 | 
					 | 
				
			||||||
                attribution: layerData.attribution,
 | 
					 | 
				
			||||||
                detectRetina: true
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (this.parentNote.hasLabel("map:scale")) {
 | 
					        if (this.parentNote.hasLabel("map:scale")) {
 | 
				
			||||||
            L.control.scale().addTo(map);
 | 
					            L.control.scale().addTo(map);
 | 
				
			||||||
@ -220,7 +116,6 @@ export default class GeoView extends ViewMode<MapData> {
 | 
				
			|||||||
        // Restore viewport position & zoom
 | 
					        // Restore viewport position & zoom
 | 
				
			||||||
        const center = parsedContent?.view?.center ?? DEFAULT_COORDINATES;
 | 
					        const center = parsedContent?.view?.center ?? DEFAULT_COORDINATES;
 | 
				
			||||||
        const zoom = parsedContent?.view?.zoom ?? DEFAULT_ZOOM;
 | 
					        const zoom = parsedContent?.view?.zoom ?? DEFAULT_ZOOM;
 | 
				
			||||||
        map.setView(center, zoom);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private onSave() {
 | 
					    private onSave() {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user