kopia lustrzana https://github.com/FacilMap/facilmap
Add support for bookmarks
rodzic
d22931710c
commit
827e6c7afa
|
@ -1,9 +1,9 @@
|
|||
import { io, Socket as SocketIO } from "socket.io-client";
|
||||
import {
|
||||
Bbox,
|
||||
BboxWithZoom, EventHandler, EventName, FindOnMapQuery, FindQuery, HistoryEntry, ID, Line, LineCreate,
|
||||
BboxWithZoom, EventHandler, EventName, FindOnMapQuery, FindPadsQuery, FindPadsResult, FindQuery, GetPadQuery, HistoryEntry, ID, Line, LineCreate,
|
||||
LineExportRequest, LineTemplateRequest, LineToRouteCreate, LineUpdate, MapEvents, Marker, MarkerCreate, MarkerUpdate, MultipleEvents, ObjectWithId,
|
||||
PadData, PadDataCreate, PadDataUpdate, PadId, RequestData, RequestName, ResponseData, Route, RouteClear, RouteCreate, RouteExportRequest,
|
||||
PadData, PadDataCreate, PadDataUpdate, PadId, PagedResults, RequestData, RequestName, ResponseData, Route, RouteClear, RouteCreate, RouteExportRequest,
|
||||
RouteInfo,
|
||||
RouteRequest,
|
||||
SearchResult,
|
||||
|
@ -110,7 +110,7 @@ export default class Client<DataType = Record<string, string>> {
|
|||
_fixResponseObject<T>(requestName: RequestName, obj: T): T {
|
||||
if (typeof obj != "object" || !(obj as any)?.data || !["getMarker", "addMarker", "editMarker", "deleteMarker", "getLineTemplate", "addLine", "editLine", "deleteLine"].includes(requestName))
|
||||
return obj;
|
||||
|
||||
|
||||
return {
|
||||
...obj,
|
||||
data: this._decodeData((obj as any).data)
|
||||
|
@ -120,7 +120,7 @@ export default class Client<DataType = Record<string, string>> {
|
|||
_fixEventObject<T extends any[]>(eventName: EventName<ClientEvents>, obj: T): T {
|
||||
if (typeof obj?.[0] != "object" || !obj?.[0]?.data || !["marker", "line"].includes(eventName))
|
||||
return obj;
|
||||
|
||||
|
||||
return [
|
||||
{
|
||||
...obj[0],
|
||||
|
@ -344,6 +344,14 @@ export default class Client<DataType = Record<string, string>> {
|
|||
});
|
||||
}
|
||||
|
||||
getPad(data: GetPadQuery): Promise<FindPadsResult | undefined> {
|
||||
return this._emit("getPad", data);
|
||||
}
|
||||
|
||||
findPads(data: FindPadsQuery): Promise<PagedResults<FindPadsResult>> {
|
||||
return this._emit("findPads", data);
|
||||
}
|
||||
|
||||
createPad(data: PadDataCreate): Promise<void> {
|
||||
return this._emit("createPad", data).then((obj) => {
|
||||
this._set(this, 'readonly', false);
|
||||
|
|
|
@ -49,6 +49,29 @@ Updates the bbox. This will cause all markers, line points and route points with
|
|||
* **Events:** Causes events to be fired with the markers, line points and route points within the bbox.
|
||||
* **Availability:** Always.
|
||||
|
||||
## `getPad(data)`
|
||||
|
||||
Finds a collaborative map by ID. This can be used to check if a map with a certain ID exists.
|
||||
|
||||
* `data`: An object with the following properties:
|
||||
* `padId`: The read-only, writable or admin ID of the map.
|
||||
* **Returns:** A promise that is resolved with undefined (if no map with that ID exists) or with an object with an `id` (read-only ID), `name` and `description` property.
|
||||
* **Events:** None.
|
||||
* **Availability:** Always.
|
||||
|
||||
## `findPads(data)`
|
||||
|
||||
Finds collaborative maps by a search term. Only finds maps that have been made public by setting [`searchEngines`](./types.md#paddata) to `true`.
|
||||
|
||||
* `data`: An object with the following properties:
|
||||
* `query` (string): A search term. `*` can be used as a wildcard and `?` as a single-character wildcard.
|
||||
* `start`, `limit` (number): If specified, can be used for paging.
|
||||
* **Returns:**: A promise that is resolved to an object with the following properties:
|
||||
* `results`: An array of objects with an `id`, `name` and `description` property.
|
||||
* `totalLength`: The total number of results. If paging is used, this number may be higher than the number of `results` returned.
|
||||
* **Events:** None.
|
||||
* **Availability:** Always.
|
||||
|
||||
## `createPad(data)`
|
||||
|
||||
Creates a new collaborative map and opens it.
|
||||
|
|
|
@ -6,9 +6,9 @@ While using a collaborative map, users can still use all of the non-collaborativ
|
|||
|
||||
When changing something on the map, there is no need to “Save” the map. Changes are applied immediately.
|
||||
|
||||
## Start a collaborative map
|
||||
## Create a map
|
||||
|
||||
To start a collaborative map, click the “Start collaborative map” button in the [toolbox](../ui/#toolbox).
|
||||
To create a collaborative map, click “Collaborative maps” and then “Create a new map” in the [toolbox](../ui/#toolbox).
|
||||
|
||||
![](./save.png)
|
||||
|
||||
|
@ -33,9 +33,23 @@ When creating a collaborative map, some random characters are proposed for the d
|
|||
| Change [map settings](../map-settings/) | ✘ | ✘ | ✔ |
|
||||
| [Delete map](../map-settings/#delete-the-map) | ✘ | ✘ | ✔ |
|
||||
|
||||
## Exit a collaborative map
|
||||
## Open an existing map
|
||||
|
||||
If you want to go back to the basic FacilMap, click on “Tools” in the [toolbox](../ui/#toolbox) and then “Exit collaborative map”.
|
||||
If someone has shared a link to a collaborative map with you, you can simply open that link in your browser to open the map. Alternatively, click “Collaborative maps” and then “Open an existing map” (or, if you are already viewing another map, “Open another map”) in the [toolbox](../ui/#toolbox). A dialog will open where you can paste the link into the text field on top and click “Open” to open the map.
|
||||
|
||||
In the bottom section of the dialog you can search for existing collaborative maps that people have [made public](../map-settings/#search-engines). Simply type in a search term and click the search icon to search for maps. You can use `*` as a wildcard and `?` as a single-character wildcard.
|
||||
|
||||
## Close a map
|
||||
|
||||
If you want to close a collaborative map and go back to the basic FacilMap, click “Collaborative maps” and then “Close …” (where “…” is the name of the open map) in the [toolbox](../ui/#toolbox).
|
||||
|
||||
## Bookmark a map
|
||||
|
||||
While you can simply add a bookmark in your browser to remember the link to a specific map, FacilMap also brings its own bookmark function.
|
||||
|
||||
To bookmark a particular map, open that map and then click “Collaborative maps” and then “Bookmark …” (where “…” is the name of the open map) in the [toolbox](../ui/#toolbox). This will add the map as a new item to the “Collaborative maps” menu. Bookmarks are stored in your browser, so they are not visible to anyone else, but they may get removed if you clean up your browser history.
|
||||
|
||||
To reorder, rename or remove bookmarks, click “Manage bookmarks” (only visible if you have any bookmarks).
|
||||
|
||||
## Delete a collaborative map
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@ The map name is shown as the title of your browser tab and window. It will also
|
|||
|
||||
If you check the “Accessible for search engines” checkbox, search engines like Google or Duckduckgo will be allowed to list the read-only version of the map. Note that FacilMap itself does not report these maps to the search engines, but when they find them through a link on a public website, it is this setting that allows them to add the map to their results.
|
||||
|
||||
Enabling this will also make the map available in FacilMap’s list of public maps in the [open map dialog](../collaborative/#open-an-existing-map).
|
||||
|
||||
When this is enabled, an additional field “Short description” is shown. Search engines will list the map with the [map name](#map-name) and this description.
|
||||
|
||||
## Cluster markers
|
||||
|
|
|
@ -17,6 +17,7 @@ The following data is sent to the FacilMap server and *persisted* there:
|
|||
|
||||
The following data is persisted in your browser:
|
||||
* If you change the [zoom settings](../search/#zoom-settings), these are persisted in the local storage of your browser.
|
||||
* If you add [bookmarks](../collaborative/#bookmark-a-map), these are persisted in the local storage of your browser.
|
||||
|
||||
## Layers
|
||||
|
||||
|
|
|
@ -3,11 +3,12 @@ import Vue from "vue";
|
|||
import FmClient from "facilmap-client";
|
||||
import "./client.scss";
|
||||
import WithRender from "./client.vue";
|
||||
import { PadId } from "facilmap-types";
|
||||
import { PadData, PadId } from "facilmap-types";
|
||||
import context from "../context";
|
||||
import PadSettings from "../pad-settings/pad-settings";
|
||||
import { Client, CLIENT_INJECT_KEY } from "../../utils/decorators";
|
||||
import StringMap from "../../utils/string-map";
|
||||
import storage from "../../utils/storage";
|
||||
|
||||
@WithRender
|
||||
@Component({
|
||||
|
@ -50,8 +51,15 @@ export class ClientProvider extends Vue {
|
|||
}
|
||||
|
||||
@Watch("client.padId")
|
||||
handlePadIdChange(padId: PadId): void {
|
||||
handlePadIdChange(padId: PadId, oldPadId: PadId | undefined): void {
|
||||
this.$emit("padId", padId);
|
||||
|
||||
if (oldPadId) {
|
||||
for (const bookmark of storage.bookmarks) {
|
||||
if (bookmark.id == oldPadId)
|
||||
bookmark.id = padId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Watch("client.padData.name")
|
||||
|
@ -59,6 +67,17 @@ export class ClientProvider extends Vue {
|
|||
this.$emit("padName", padName);
|
||||
}
|
||||
|
||||
@Watch("client.padData")
|
||||
handlePadDataChange(newPadData: PadData, oldPadData: PadData | undefined): void {
|
||||
for (const bookmark of storage.bookmarks) {
|
||||
if (oldPadData && oldPadData.id == bookmark.padId)
|
||||
bookmark.padId = newPadData.id;
|
||||
|
||||
if (bookmark.padId == newPadData.id)
|
||||
bookmark.name = newPadData.name;
|
||||
}
|
||||
}
|
||||
|
||||
@Watch("client.serverError")
|
||||
handleServerErrorChange(serverError: Error | undefined): void {
|
||||
if (serverError?.message?.includes("does not exist") && context.interactive) {
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import WithRender from "./manage-bookmarks.vue";
|
||||
import Vue from "vue";
|
||||
import { Component, Prop } from "vue-property-decorator";
|
||||
import { Client, InjectClient, InjectMapComponents } from "../../utils/decorators";
|
||||
import { MapComponents } from "../leaflet-map/leaflet-map";
|
||||
import storage, { Bookmark } from "../../utils/storage";
|
||||
import Icon from "../ui/icon/icon";
|
||||
import draggable from "vuedraggable";
|
||||
|
||||
@WithRender
|
||||
@Component({
|
||||
components: { draggable, Icon }
|
||||
})
|
||||
export default class ManageBookmarks extends Vue {
|
||||
|
||||
@InjectClient() client!: Client;
|
||||
@InjectMapComponents() mapComponents!: MapComponents;
|
||||
|
||||
@Prop({ type: String, required: true }) id!: string;
|
||||
|
||||
get bookmarks(): Bookmark[] {
|
||||
return storage.bookmarks;
|
||||
}
|
||||
|
||||
set bookmarks(bookmarks: Bookmark[]) {
|
||||
// Needed for draggable
|
||||
storage.bookmarks = bookmarks;
|
||||
}
|
||||
|
||||
get isBookmarked(): boolean {
|
||||
return !!this.client.padId && storage.bookmarks.some((bookmark) => bookmark.id == this.client.padId);
|
||||
}
|
||||
|
||||
deleteBookmark(bookmark: Bookmark): void {
|
||||
const index = storage.bookmarks.indexOf(bookmark);
|
||||
if (index != -1)
|
||||
storage.bookmarks.splice(index, 1);
|
||||
}
|
||||
|
||||
addBookmark(): void {
|
||||
storage.bookmarks.push({ id: this.client.padId!, padId: this.client.padData!.id, name: this.client.padData!.name });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<b-modal :id="id" title="Manage Bookmarks" ok-only ok-title="Close" size="lg" dialog-class="fm-manage-bookmarks">
|
||||
<p>Bookmarks are a quick way to remember and access collaborative maps. They are saved in your browser, other users will not be able to see them.</p>
|
||||
<b-table-simple striped hover>
|
||||
<b-thead>
|
||||
<b-tr>
|
||||
<b-th>Map ID</b-th>
|
||||
<b-th>Name</b-th>
|
||||
<b-th></b-th>
|
||||
</b-tr>
|
||||
</b-thead>
|
||||
<draggable v-model="bookmarks" tag="tbody" handle=".fm-drag-handle">
|
||||
<b-tr v-for="bookmark in bookmarks">
|
||||
<b-td :class="{ 'font-weight-bold': bookmark.id == client.padId }">
|
||||
{{bookmark.id}}
|
||||
</b-td>
|
||||
<b-td>
|
||||
<b-input v-model="bookmark.customName" :placeholder="bookmark.name"></b-input>
|
||||
</b-td>
|
||||
<b-td class="td-buttons text-right">
|
||||
<b-button @click="deleteBookmark(bookmark)">Delete</b-button>
|
||||
<b-button class="fm-drag-handle"><Icon icon="resize-vertical" alt="Reorder"></Icon></b-button>
|
||||
</b-td>
|
||||
</b-tr>
|
||||
</draggable>
|
||||
<b-tfoot v-if="client.padData && !isBookmarked">
|
||||
<b-tr>
|
||||
<b-td colspan="3">
|
||||
<b-button @click="addBookmark()">Bookmark current map</b-button>
|
||||
</b-td>
|
||||
</b-tr>
|
||||
</b-tfoot>
|
||||
</b-table-simple>
|
||||
</b-modal>
|
|
@ -0,0 +1,26 @@
|
|||
.fm-open-map {
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.results {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.alert, .table-wrapper {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow: auto;
|
||||
min-height: 7rem;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
import WithRender from "./open-map.vue";
|
||||
import Vue from "vue";
|
||||
import { Component, Prop } from "vue-property-decorator";
|
||||
import { Client, InjectClient } from "../../utils/decorators";
|
||||
import { extend, ValidationObserver, ValidationProvider } from "vee-validate";
|
||||
import context from "../context";
|
||||
import { getValidationState } from "../../utils/validation";
|
||||
import { showErrorToast } from "../../utils/toasts";
|
||||
import Icon from "../ui/icon/icon";
|
||||
import { FindPadsResult } from "facilmap-types";
|
||||
import "./open-map.scss";
|
||||
import decodeURIComponent from "decode-uri-component";
|
||||
|
||||
const ITEMS_PER_PAGE = 20;
|
||||
|
||||
function parsePadId(val: string): { padId: string; hash: string } {
|
||||
if (val.startsWith(context.urlPrefix))
|
||||
val = decodeURIComponent(val.substr(context.urlPrefix.length));
|
||||
|
||||
const hashIdx = val.indexOf("#");
|
||||
if (hashIdx == -1)
|
||||
return { padId: val, hash: "" };
|
||||
else
|
||||
return { padId: val.substr(0, hashIdx), hash: val.substr(hashIdx) };
|
||||
}
|
||||
|
||||
extend("openPadId", {
|
||||
validate: async (val: string, data: any) => {
|
||||
const client = data.getClient() as Client;
|
||||
const parsed = parsePadId(val);
|
||||
|
||||
if (parsed.padId.includes("/"))
|
||||
return "Please enter a valid map ID or URL.";
|
||||
|
||||
const padInfo = await client.getPad({ padId: parsed.padId });
|
||||
if (!padInfo)
|
||||
return "No map with this ID could be found.";
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
params: ["getClient"]
|
||||
});
|
||||
|
||||
@WithRender
|
||||
@Component({
|
||||
components: { Icon, ValidationObserver, ValidationProvider }
|
||||
})
|
||||
export default class OpenMap extends Vue {
|
||||
|
||||
@InjectClient() client!: Client;
|
||||
|
||||
@Prop({ type: String, required: true }) id!: string;
|
||||
|
||||
padId = "";
|
||||
|
||||
searchQuery = "";
|
||||
submittedSearchQuery: string | null = null;
|
||||
isSearching = false;
|
||||
results: FindPadsResult[] = [];
|
||||
pages = 0;
|
||||
activePage = 1;
|
||||
|
||||
get url(): string {
|
||||
const parsed = parsePadId(this.padId);
|
||||
return context.urlPrefix + encodeURIComponent(parsed.padId) + parsed.hash;
|
||||
}
|
||||
|
||||
get urlPrefix(): string {
|
||||
return context.urlPrefix;
|
||||
}
|
||||
|
||||
getValidationState = getValidationState;
|
||||
|
||||
getClient(): Client {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
handleSubmit(): void {
|
||||
location.href = this.url;
|
||||
}
|
||||
|
||||
async search(query: string, page: number): Promise<void> {
|
||||
if (!query) {
|
||||
this.submittedSearchQuery = null;
|
||||
this.results = [];
|
||||
this.pages = 0;
|
||||
this.activePage = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSearching = true;
|
||||
this.$bvModal.hide("fm-open-map-search-error");
|
||||
|
||||
try {
|
||||
const results = await this.client.findPads({
|
||||
query,
|
||||
start: (page - 1) * ITEMS_PER_PAGE,
|
||||
limit: ITEMS_PER_PAGE
|
||||
});
|
||||
this.submittedSearchQuery = query;
|
||||
this.activePage = page;
|
||||
this.results = results.results;
|
||||
this.pages = Math.ceil(results.totalLength / ITEMS_PER_PAGE);
|
||||
} catch (err) {
|
||||
showErrorToast(this, "fm-open-map-search-error", "Error searching for public maps", err);
|
||||
} finally {
|
||||
this.isSearching = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<b-modal :id="id" title="Open collaborative map" ok-only ok-title="Close" ok-variant="secondary" size="lg" dialog-class="fm-open-map" scrollable>
|
||||
<ValidationObserver v-slot="observer">
|
||||
<b-form method="get" :action="url" @submit.prevent="observer.handleSubmit(handleSubmit)">
|
||||
<p>Enter the link or ID of an existing collaborative map here to open that map.</p>
|
||||
<ValidationProvider name="Map ID/link" v-slot="v" :rules="{ openPadId: { getClient } }" :debounce="300">
|
||||
<b-form-group :state="v | validationState">
|
||||
<b-input-group>
|
||||
<b-form-input v-model="padId" :state="v | validationState"></b-form-input>
|
||||
<b-input-group-append>
|
||||
<b-button type="submit" variant="primary" :disabled="!padId">
|
||||
<b-spinner small v-if="observer.pending"></b-spinner>
|
||||
Open
|
||||
</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
<template #invalid-feedback><span v-html="v.errors[0]"></span></template>
|
||||
</b-form-group>
|
||||
</ValidationProvider>
|
||||
</b-form>
|
||||
</ValidationObserver>
|
||||
|
||||
<hr/>
|
||||
|
||||
<h4>Search public maps</h4>
|
||||
<b-form @submit.prevent="search(searchQuery, 1)" class="results">
|
||||
<b-input-group>
|
||||
<b-form-input type="search" v-model="searchQuery" placeholder="Search term"></b-form-input>
|
||||
<b-input-group-append>
|
||||
<b-button type="submit" variant="secondary" :disabled="isSearching">
|
||||
<b-spinner small v-if="isSearching"></b-spinner>
|
||||
<Icon v-else icon="search" alt="Search"></Icon>
|
||||
</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
|
||||
<b-alert v-if="submittedSearchQuery && results.length == 0" variant="danger" show>
|
||||
No maps could be found.
|
||||
</b-alert>
|
||||
|
||||
<template v-if="submittedSearchQuery && results.length > 0">
|
||||
<div class="table-wrapper">
|
||||
<b-table-simple hover striped>
|
||||
<b-thead>
|
||||
<b-tr>
|
||||
<b-th>Name</b-th>
|
||||
<b-th>Description</b-th>
|
||||
<b-th></b-th>
|
||||
</b-tr>
|
||||
</b-thead>
|
||||
<b-tbody>
|
||||
<b-tr v-for="result in results">
|
||||
<b-td>{{result.name}}</b-td>
|
||||
<b-td>{{result.description}}</b-td>
|
||||
<b-td class="td-buttons"><b-button :href="urlPrefix + encodeURIComponent(result.id)">Open</b-button></b-td>
|
||||
</b-tr>
|
||||
</b-tbody>
|
||||
</b-table-simple>
|
||||
</div>
|
||||
|
||||
<b-pagination
|
||||
v-if="pages > 1"
|
||||
:total-rows="pages"
|
||||
:per-page="1"
|
||||
:value="activePage"
|
||||
align="center"
|
||||
@input="search(submittedSearchQuery, $event)"
|
||||
></b-pagination>
|
||||
</template>
|
||||
</b-form>
|
||||
</b-modal>
|
|
@ -10,7 +10,7 @@ import { mergeObject } from "../../utils/utils";
|
|||
import { isEqual } from "lodash";
|
||||
import copyToClipboard from "copy-to-clipboard";
|
||||
import FormModal from "../ui/form-modal/form-modal";
|
||||
import { showErrorToast, showToast } from "../../utils/toasts";
|
||||
import { showErrorToast } from "../../utils/toasts";
|
||||
import "./pad-settings.scss";
|
||||
|
||||
extend("padId", {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<FormModal
|
||||
:id="id"
|
||||
:title="isCreate ? 'Start collaborative map' : 'Map settings'"
|
||||
:title="isCreate ? 'Create collaborative map' : 'Map settings'"
|
||||
dialog-class="fm-pad-settings"
|
||||
:no-cancel="noCancel"
|
||||
:is-saving="isSaving"
|
||||
|
|
|
@ -18,10 +18,13 @@ import ManageTypes from "../manage-types/manage-types";
|
|||
import EditFilter from "../edit-filter/edit-filter";
|
||||
import History from "../history/history";
|
||||
import { MapComponents, MapContext } from "../leaflet-map/leaflet-map";
|
||||
import storage, { Bookmark } from "../../utils/storage";
|
||||
import ManageBookmarks from "../manage-bookmarks/manage-bookmarks";
|
||||
import OpenMap from "../open-map/open-map";
|
||||
|
||||
@WithRender
|
||||
@Component({
|
||||
components: { About, EditFilter, History, Icon, ManageViews, ManageTypes, PadSettings, SaveView, Sidebar }
|
||||
components: { About, EditFilter, History, Icon, ManageBookmarks, ManageViews, ManageTypes, OpenMap, PadSettings, SaveView, Sidebar }
|
||||
})
|
||||
export default class Toolbox extends Vue {
|
||||
|
||||
|
@ -34,13 +37,22 @@ export default class Toolbox extends Vue {
|
|||
return context.isNarrow;
|
||||
}
|
||||
|
||||
get urlPrefix(): string {
|
||||
return context.urlPrefix;
|
||||
}
|
||||
|
||||
get hash(): string {
|
||||
const v = this.mapContext;
|
||||
return v.hash && v.hash != "#" ? v.hash : `${v.zoom}/${v.center.lat}/${v.center.lng}`;
|
||||
}
|
||||
|
||||
get links(): Record<'osm' | 'google' | 'bing' | 'facilmap', string> {
|
||||
const v = this.mapContext;
|
||||
return {
|
||||
osm: `https://www.openstreetmap.org/#map=${v.zoom}/${v.center.lat}/${v.center.lng}`,
|
||||
google: `https://www.google.com/maps/@${v.center.lat},${v.center.lng},${v.zoom}z`,
|
||||
bing: `https://www.bing.com/maps?cp=${v.center.lat}~${v.center.lng}&lvl=${v.zoom}`,
|
||||
facilmap: `${context.urlPrefix}#${v.hash && v.hash != "#" ? v.hash : `${v.zoom}/${v.center.lat}/${v.center.lng}`}`
|
||||
facilmap: `${context.urlPrefix}#${this.hash}`
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -74,6 +86,18 @@ export default class Toolbox extends Vue {
|
|||
}));
|
||||
}
|
||||
|
||||
get bookmarks(): Bookmark[] {
|
||||
return storage.bookmarks;
|
||||
}
|
||||
|
||||
get isBookmarked(): boolean {
|
||||
return !!this.client.padId && storage.bookmarks.some((bookmark) => bookmark.id == this.client.padId);
|
||||
}
|
||||
|
||||
addBookmark(): void {
|
||||
storage.bookmarks.push({ id: this.client.padId!, padId: this.client.padData!.id, name: this.client.padData!.name });
|
||||
}
|
||||
|
||||
addObject(type: Type): void {
|
||||
if(type.type == "marker")
|
||||
this.addMarker(type);
|
||||
|
|
|
@ -2,7 +2,48 @@
|
|||
<a v-if="isNarrow" href="javascript:" class="fm-toolbox-toggle" v-b-toggle.fm-toolbox-sidebar><Icon icon="menu-hamburger"></Icon></a>
|
||||
|
||||
<Sidebar id="fm-toolbox-sidebar">
|
||||
<b-nav-item v-if="!client.padId && interactive" href="javascript:" v-b-modal.fm-toolbox-create-pad v-b-toggle.fm-toolbox-sidebar>Start collaborative map</b-nav-item>
|
||||
<b-nav-item-dropdown
|
||||
text="Collaborative maps"
|
||||
v-if="interactive"
|
||||
:disabled="!!mapContext.interaction"
|
||||
right
|
||||
>
|
||||
<b-dropdown-item
|
||||
v-for="bookmark in bookmarks"
|
||||
:href="`${urlPrefix}${encodeURIComponent(bookmark.id)}#${hash}`"
|
||||
v-b-toggle.fm-toolbox-sidebar
|
||||
:active="bookmark.id == client.padId"
|
||||
>{{bookmark.customName || bookmark.name}}</b-dropdown-item>
|
||||
<b-dropdown-divider v-if="bookmarks.length > 0"></b-dropdown-divider>
|
||||
<b-dropdown-item
|
||||
v-if="client.padData && !this.isBookmarked"
|
||||
href="javascript:"
|
||||
@click="addBookmark()"
|
||||
>Bookmark {{client.padData.name}}</b-dropdown-item>
|
||||
<b-dropdown-item
|
||||
v-if="bookmarks.length > 0"
|
||||
href="javascript:"
|
||||
v-b-modal.fm-toolbox-manage-bookmarks
|
||||
v-b-toggle.fm-toolbox-sidebar
|
||||
>Manage bookmarks</b-dropdown-item>
|
||||
<b-dropdown-divider v-if="(client.padData && !this.isBookmarked) || bookmarks.length > 0"></b-dropdown-divider>
|
||||
<b-dropdown-item
|
||||
v-if="!client.padId"
|
||||
href="javascript:"
|
||||
v-b-modal.fm-toolbox-create-pad
|
||||
v-b-toggle.fm-toolbox-sidebar
|
||||
>Create a new map</b-dropdown-item>
|
||||
<b-dropdown-item
|
||||
href="javascript:"
|
||||
v-b-modal.fm-toolbox-open-map
|
||||
v-b-toggle.fm-toolbox-sidebar
|
||||
>Open {{client.padId ? "another" : "an existing"}} map</b-dropdown-item>
|
||||
<b-dropdown-item
|
||||
v-if="client.padData"
|
||||
:href="links.facilmap"
|
||||
>Close {{client.padData.name}}</b-dropdown-item>
|
||||
</b-nav-item-dropdown>
|
||||
|
||||
<b-nav-item-dropdown v-if="!client.readonly && client.padData" text="Add" :disabled="!!mapContext.interaction" right>
|
||||
<b-dropdown-item v-for="type in client.types" :disabled="!!mapContext.interaction" href="javascript:" @click="addObject(type)" v-b-toggle.fm-toolbox-sidebar>{{type.name}}</b-dropdown-item>
|
||||
<b-dropdown-divider v-if="client.writable == 2"></b-dropdown-divider>
|
||||
|
@ -33,8 +74,6 @@
|
|||
<b-dropdown-item v-if="client.padData" href="javascript:" v-b-modal.fm-toolbox-edit-filter v-b-toggle.fm-toolbox-sidebar>Filter</b-dropdown-item>
|
||||
<b-dropdown-item v-if="client.writable == 2 && client.padData" href="javascript:" v-b-modal.fm-toolbox-edit-pad v-b-toggle.fm-toolbox-sidebar>Settings</b-dropdown-item>
|
||||
<b-dropdown-item v-if="!client.readonly && client.padData" href="javascript:" v-b-modal.fm-toolbox-history v-b-toggle.fm-toolbox-sidebar>Show edit history</b-dropdown-item>
|
||||
<b-dropdown-divider v-if="client.padData"></b-dropdown-divider>
|
||||
<b-dropdown-item v-if="client.padData && interactive" :href="links.facilmap">Exit collaborative map</b-dropdown-item>
|
||||
</b-nav-item-dropdown>
|
||||
<b-nav-item-dropdown text="Help" right>
|
||||
<b-dropdown-item href="https://docs.facilmap.org/users/" target="_blank">Documentation</b-dropdown-item>
|
||||
|
@ -44,6 +83,8 @@
|
|||
</b-nav-item-dropdown>
|
||||
</Sidebar>
|
||||
|
||||
<OpenMap id="fm-toolbox-open-map"></OpenMap>
|
||||
<ManageBookmarks id="fm-toolbox-manage-bookmarks"></ManageBookmarks>
|
||||
<About id="fm-toolbox-about"></About>
|
||||
<PadSettings v-if="!client.padData" id="fm-toolbox-create-pad" :isCreate="true"></PadSettings>
|
||||
<PadSettings v-if="client.padData" id="fm-toolbox-edit-pad"></PadSettings>
|
||||
|
|
|
@ -1,33 +1,58 @@
|
|||
import { PadId } from "facilmap-types";
|
||||
import { isEqual } from "lodash";
|
||||
import Vue from "vue";
|
||||
|
||||
export interface Bookmark {
|
||||
/** ID used to open the map */
|
||||
id: PadId;
|
||||
/** Read-only ID of the map */
|
||||
padId: PadId;
|
||||
/** Last known name of the map */
|
||||
name: string;
|
||||
/** If this is defined, it is shown instead of the map name. */
|
||||
customName?: string;
|
||||
}
|
||||
|
||||
export interface Storage {
|
||||
zoomToAll: boolean;
|
||||
autoZoom: boolean;
|
||||
bookmarks: Bookmark[];
|
||||
}
|
||||
|
||||
const storage = Vue.observable({
|
||||
const storage: Storage = Vue.observable({
|
||||
zoomToAll: false,
|
||||
autoZoom: true
|
||||
autoZoom: true,
|
||||
bookmarks: []
|
||||
});
|
||||
|
||||
export default storage;
|
||||
|
||||
try {
|
||||
const val = localStorage.getItem("facilmap");
|
||||
if (val) {
|
||||
const parsed = JSON.parse(val);
|
||||
storage.zoomToAll = !!parsed.zoomToAll;
|
||||
storage.autoZoom = !!parsed.autoZoom;
|
||||
function load(): void {
|
||||
try {
|
||||
const val = localStorage.getItem("facilmap");
|
||||
if (val) {
|
||||
const parsed = JSON.parse(val);
|
||||
storage.zoomToAll = !!parsed.zoomToAll;
|
||||
storage.autoZoom = !!parsed.autoZoom;
|
||||
storage.bookmarks = parsed.bookmarks || [];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error reading local storage", err);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error reading local storage", err);
|
||||
}
|
||||
|
||||
const watcher = new Vue({ data: { storage } });
|
||||
watcher.$watch("storage", () => {
|
||||
function save() {
|
||||
try {
|
||||
localStorage.setItem("facilmap", JSON.stringify(storage));
|
||||
const currentItem = localStorage.getItem("facilmap");
|
||||
if (!currentItem || !isEqual(JSON.parse(currentItem), storage))
|
||||
localStorage.setItem("facilmap", JSON.stringify(storage));
|
||||
} catch (err) {
|
||||
console.error("Error saving to local storage", err);
|
||||
}
|
||||
}, { deep: true });
|
||||
}
|
||||
|
||||
load();
|
||||
window.addEventListener("storage", load);
|
||||
|
||||
const watcher = new Vue({ data: { storage } });
|
||||
watcher.$watch("storage", save, { deep: true });
|
|
@ -1,5 +1,5 @@
|
|||
import { DataTypes, Model, Op } from "sequelize";
|
||||
import { PadData, PadDataCreate, PadDataUpdate, PadId } from "facilmap-types";
|
||||
import { DataTypes, Model, Op, Sequelize } from "sequelize";
|
||||
import { FindPadsQuery, FindPadsResult, PadData, PadDataCreate, PadDataUpdate, PadId, PagedResults } from "facilmap-types";
|
||||
import Database from "./database";
|
||||
import { streamEachPromise } from "../utils/streams";
|
||||
|
||||
|
@ -185,6 +185,24 @@ export default class DatabasePads {
|
|||
this._db.emit("deletePad", padId);
|
||||
}
|
||||
|
||||
async findPads(query: FindPadsQuery): Promise<PagedResults<FindPadsResult>> {
|
||||
const like = query.query.toLowerCase().replace(/[%_\\]/g, "\\$&").replace(/[*]/g, "%").replace(/[?]/g, "_");
|
||||
const { count, rows } = await this.PadModel.findAndCountAll({
|
||||
where: Sequelize.and(
|
||||
{ searchEngines: true },
|
||||
Sequelize.where(Sequelize.fn("lower", Sequelize.col(`Pad.name`)), {[Op.like]: `%${like}%`})
|
||||
),
|
||||
offset: query.start ?? 0,
|
||||
...(query.limit != null ? { limit: query.limit } : {}),
|
||||
attributes: ["id", "name", "description"]
|
||||
});
|
||||
|
||||
return {
|
||||
results: rows.map((row) => row.toJSON()),
|
||||
totalLength: count
|
||||
};
|
||||
}
|
||||
|
||||
/*function copyPad(fromPadId, toPadId, callback) {
|
||||
function _handleStream(stream, next, cb) {
|
||||
stream.on("data", function(data) {
|
||||
|
|
|
@ -76,7 +76,7 @@ class SocketConnection {
|
|||
this.socket.on(i, async (data, callback) => {
|
||||
try {
|
||||
const res = await this.socketHandlers[i](data);
|
||||
|
||||
|
||||
if(!callback && res)
|
||||
console.trace("No callback available to send result of socket handler " + i);
|
||||
|
||||
|
@ -170,7 +170,7 @@ class SocketConnection {
|
|||
this.padId = undefined;
|
||||
throw new Error("This pad does not exist");
|
||||
}
|
||||
|
||||
|
||||
this.padId = pad.id;
|
||||
this.writable = pad.writable;
|
||||
|
||||
|
@ -233,6 +233,29 @@ class SocketConnection {
|
|||
}
|
||||
},
|
||||
|
||||
getPad: async (data) => {
|
||||
this.validateConditions(Writable.READ, data, {
|
||||
padId: "string"
|
||||
});
|
||||
|
||||
const padData = await this.database.pads.getPadDataByAnyId(data.padId);
|
||||
return padData && {
|
||||
id: padData.id,
|
||||
name: padData.name,
|
||||
description: padData.description
|
||||
};
|
||||
},
|
||||
|
||||
findPads: async (data) => {
|
||||
this.validateConditions(Writable.READ, data, {
|
||||
query: "string",
|
||||
start: "number",
|
||||
limit: "number"
|
||||
});
|
||||
|
||||
return this.database.pads.findPads(data);
|
||||
},
|
||||
|
||||
createPad: async (data) => {
|
||||
this.validateConditions(Writable.READ, data, {
|
||||
name: "string",
|
||||
|
@ -273,7 +296,7 @@ class SocketConnection {
|
|||
legend1: "string",
|
||||
legend2: "string"
|
||||
});
|
||||
|
||||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error("No map opened.");
|
||||
|
||||
|
@ -385,7 +408,7 @@ class SocketConnection {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return await this.database.lines.createLine(this.padId, data, fromRoute);
|
||||
},
|
||||
|
||||
|
@ -473,7 +496,7 @@ class SocketConnection {
|
|||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error("No map opened.");
|
||||
|
||||
|
||||
return await this.database.views.createView(this.padId, data);
|
||||
},
|
||||
|
||||
|
@ -583,10 +606,10 @@ class SocketConnection {
|
|||
// We first update the type (without updating the styles). If that succeeds, we rename the data fields.
|
||||
// Only then we update the object styles (as they often depend on the field values).
|
||||
const newType = await this.database.types.updateType(this.padId, data.id, data, false)
|
||||
|
||||
|
||||
if(Object.keys(rename).length > 0)
|
||||
await this.database.helpers.renameObjectDataField(this.padId, data.id, rename, newType.type == "line");
|
||||
|
||||
|
||||
await this.database.types.recalculateObjectStylesForType(newType.padId, newType.id, newType.type == "line")
|
||||
|
||||
return newType;
|
||||
|
@ -771,7 +794,7 @@ class SocketConnection {
|
|||
|
||||
if (!isPadId(this.padId))
|
||||
throw new Error("No map opened.");
|
||||
|
||||
|
||||
if(this.historyListener)
|
||||
throw new Error("Already listening to history.");
|
||||
|
||||
|
@ -804,12 +827,12 @@ class SocketConnection {
|
|||
throw new Error("No map opened.");
|
||||
|
||||
const historyEntry = await this.database.history.getHistoryEntry(this.padId, data.id);
|
||||
|
||||
|
||||
if(!["Marker", "Line"].includes(historyEntry.type) && this.writable != Writable.ADMIN)
|
||||
throw new Error("This kind of change can only be reverted in admin mode.");
|
||||
|
||||
this.pauseHistoryListener++;
|
||||
|
||||
|
||||
try {
|
||||
await this.database.history.revertHistoryEntry(this.padId, data.id);
|
||||
} finally {
|
||||
|
|
|
@ -8,6 +8,23 @@ import { View, ViewCreate, ViewUpdate } from "./view";
|
|||
import { MapEvents, MultipleEvents } from "./events";
|
||||
import { SearchResult } from "./searchResult";
|
||||
|
||||
export interface GetPadQuery {
|
||||
padId: string;
|
||||
}
|
||||
|
||||
export interface FindPadsQuery {
|
||||
query: string;
|
||||
start?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export type FindPadsResult = Pick<PadData, "id" | "name" | "description">;
|
||||
|
||||
export interface PagedResults<T> {
|
||||
results: T[];
|
||||
totalLength: number;
|
||||
}
|
||||
|
||||
export interface LineTemplateRequest {
|
||||
typeId: ID
|
||||
}
|
||||
|
@ -38,6 +55,8 @@ export type FindOnMapResult = FindOnMapMarker | FindOnMapLine;
|
|||
|
||||
export interface RequestDataMap<DataType = Record<string, string>> {
|
||||
updateBbox: BboxWithZoom;
|
||||
getPad: GetPadQuery;
|
||||
findPads: FindPadsQuery;
|
||||
createPad: PadDataCreate;
|
||||
editPad: PadDataUpdate;
|
||||
deletePad: void;
|
||||
|
@ -72,6 +91,8 @@ export interface RequestDataMap<DataType = Record<string, string>> {
|
|||
|
||||
export interface ResponseDataMap<DataType = Record<string, string>> {
|
||||
updateBbox: MultipleEvents<MapEvents<DataType>>;
|
||||
getPad: FindPadsResult | undefined;
|
||||
findPads: PagedResults<FindPadsResult>;
|
||||
createPad: MultipleEvents<MapEvents<DataType>>;
|
||||
editPad: PadData;
|
||||
deletePad: void;
|
||||
|
|
Ładowanie…
Reference in New Issue