Add support for bookmarks

pull/172/head
Candid Dauth 2021-05-06 03:40:59 +02:00
rodzic d22931710c
commit 827e6c7afa
19 zmienionych plików z 546 dodań i 43 usunięć

Wyświetl plik

@ -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);

Wyświetl plik

@ -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.

Wyświetl plik

@ -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

Wyświetl plik

@ -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 FacilMaps 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

Wyświetl plik

@ -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

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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 });
}
}

Wyświetl plik

@ -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>

Wyświetl plik

@ -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;
}
}
}

Wyświetl plik

@ -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;
}
}
}

Wyświetl plik

@ -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>

Wyświetl plik

@ -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", {

Wyświetl plik

@ -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"

Wyświetl plik

@ -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);

Wyświetl plik

@ -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>

Wyświetl plik

@ -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 });

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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;