Skip to content

Instantly share code, notes, and snippets.

@pioh
Created April 8, 2024 18:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pioh/35a7eb2ecef31860510e5666ac213c06 to your computer and use it in GitHub Desktop.
Save pioh/35a7eb2ecef31860510e5666ac213c06 to your computer and use it in GitHub Desktop.
QueryStore.ts
import {action, computed, observable, reaction, toJS} from "mobx";
import base64url from "base64url";
import qs from "qs";
import {RealtyField} from "proto";
import {Provide} from "store/feature/DependencyManager";
import {routerStore} from "store/RouterStore";
export enum QueryParam {
OFFERS_LIST_VIEW_MODE = "ol",
OFFER_LIST_SORT_BY = "s",
OFFER_LIST_SORT_REVERSE = "r",
OFFER_LIST_PAGE = "p",
OFFER_LIST_COUNT_ON_PAGE = "c",
STATS_PANEL_FILTER_TYPE = "sp",
CURRENT_DRAFT = "cd",
FOCUS_DRAFT = "fd",
DEV_PPO = "dev_ppo",
STATS_PANEL_VIEW_TYPE = "spv",
}
export enum StatsPanelFilterType {
MAP = "M",
CLUSTER = "C",
ADDED = "A",
FAVORITE = "F",
}
export enum StatsPanelViewType {
SHORT = "S",
FULL = "F",
}
export enum OffersListViewMode {
CARD = "c",
DETAILED_LIST = "d",
}
export const defaultSearch = {
reportID: "",
from: "" as "" | "map" | "favorite",
hid: "",
secondary: false,
offerUrl: "",
bounds: "",
filter: "",
comparable: "",
tab: "",
cluster: 0,
calculationScroll: 0,
plclpsd: false,
prclpsd: false,
pbclpsd: true,
plsz: 300,
prsz: 470,
pbsz: 300,
requestId: "",
snapshotId: "",
showReports: false,
[QueryParam.OFFERS_LIST_VIEW_MODE]: OffersListViewMode.DETAILED_LIST,
[QueryParam.OFFER_LIST_SORT_BY]: RealtyField.pricePerMeter,
[QueryParam.OFFER_LIST_SORT_REVERSE]: true,
[QueryParam.OFFER_LIST_PAGE]: 1,
[QueryParam.OFFER_LIST_COUNT_ON_PAGE]: 0,
[QueryParam.STATS_PANEL_FILTER_TYPE]: StatsPanelFilterType.MAP,
[QueryParam.CURRENT_DRAFT]: "",
[QueryParam.FOCUS_DRAFT]: "",
[QueryParam.DEV_PPO]: 0, // as -1 | 0 | 1,
[QueryParam.STATS_PANEL_VIEW_TYPE]: StatsPanelViewType.SHORT,
};
export type ISearch = typeof defaultSearch;
type searchField = keyof ISearch;
interface IQueryStoreProps {
dispose: (disposer: () => void) => void;
}
export class QueryStore {
props: IQueryStoreProps;
constructor(props: IQueryStoreProps) {
this.props = props;
this.setUrlToSearch();
this.props.dispose(
reaction(
() => this.urlSearchString,
() => this.setUrlToSearch(),
{fireImmediately: false}
)
);
this.props.dispose(
reaction(
() => this.searchString,
() => this.setSearchToUrl()
)
);
Provide(this, this.props.dispose);
}
@observable search: ISearch = {...defaultSearch};
@computed private get searchString() {
return this.searchToString(toJS(this.search));
}
@computed private get urlSearchString() {
return (routerStore.location.search || "").replace(/^\??/, "?");
}
@action bind<T extends searchField>(
name: T,
get: () => ISearch[T],
set: (v: ISearch[T]) => void,
overwriteFromUrl = false
): () => void {
let defaultVal = defaultSearch[name];
let storeVal = get();
if (this.search[name] === defaultVal) {
if (storeVal !== defaultVal) {
if (overwriteFromUrl) {
set(this.search[name]);
} else {
this.search[name] = storeVal;
}
}
} else {
if (storeVal !== this.search[name]) {
try {
set(this.search[name]);
} catch (e) {
console.error(e.stack);
}
}
}
let fromSearch = reaction(
() => this.search[name],
action((searchVal: ISearch[T]) => {
let storeVal = get();
if (searchVal === storeVal) return;
try {
set(searchVal);
} catch (e) {
console.error(e.stack);
}
}),
{fireImmediately: false}
);
let fromStore = reaction(
() => get(),
action((storeVal: ISearch[T]) => {
let searchVal = this.search[name];
if (searchVal === storeVal) return;
this.search[name] = storeVal;
}),
{fireImmediately: false, delay: 1000}
);
return () => {
fromStore();
fromSearch();
};
}
@computed get ppoMode(): "force_enable" | "force_disable" | "default" {
let devPpo = this.search[QueryParam.DEV_PPO];
switch (devPpo) {
case 1:
return "force_enable";
case -1:
return "force_disable";
default:
return "default";
}
}
private searchToString(search: any) {
let omited: any = {};
for (let k in search) {
let v = search[k];
let defaultV = (defaultSearch as any)[k];
if (v === defaultV && typeof defaultV !== "boolean") {
continue;
}
if (v === null || v === void 0) {
continue;
}
// console.log(JSON.stringify({k, v, defaultV}));
if (v === true) {
omited[k] = null; // key without value
continue;
}
if (v === false && defaultV !== true) {
continue;
}
omited[k] = v;
}
if (omited.offerUrl) omited.offerUrl = btoa(omited.offerUrl);
let url = qs.stringify(omited, {strictNullHandling: true, addQueryPrefix: true});
// console.log("url", url);
return url;
}
searchStringWith(query: Partial<ISearch>): string {
return this.searchToString({
...this.search,
...query,
});
}
link(query: Partial<ISearch>, pathname = routerStore.location.pathname) {
return {
search: this.searchStringWith(query),
pathname,
};
}
@action private setSearchToUrl() {
if (routerStore.location.search !== this.searchString) {
routerStore.replace({
pathname: routerStore.location.pathname,
search: this.searchString,
});
}
}
@action private setUrlToSearch() {
let searchString = this.urlSearchString;
let search = {...defaultSearch} as any;
let omited = qs.parse(searchString, {ignoreQueryPrefix: true, strictNullHandling: true});
try {
if (omited.offerUrl) omited.offerUrl = base64url.decode(omited.offerUrl as string);
} catch (e) {
console.error(e.stack);
delete omited.offerUrl;
}
let wasSet = new Set<string>();
for (let k in omited) {
let v = omited[k];
wasSet.add(k);
let d = search[k];
let n = (this.search as any)[k];
if (typeof d === "string") {
if (v && v !== n) (this.search as any)[k] = v;
} else if (typeof d === "boolean") {
if (String(v) !== String(n)) {
(this.search as any)[k] =
String(v) === "true" || String(v) === "null" ? true : String(v) === "false" ? false : d;
}
// if (n !== true) (this.search as any)[k] = true;
} else if (typeof d === "number") {
if (Number.isFinite(Number(v))) {
let num = Number(v);
if (n !== num) {
(this.search as any)[k] = num;
}
}
} else if ((this.search as any)[k] !== v) {
(this.search as any)[k] = v;
}
}
for (let k in this.search as any) {
if (wasSet.has(k)) continue;
if ((this.search as any)[k] !== (defaultSearch as any)[k]) {
(this.search as any)[k] = (defaultSearch as any)[k];
}
}
}
@computed get requestIdFromReportId() {
const result = this.search.reportID.match(/9r-(\d{6,})-RESIDENTIAL/);
if (result) {
return result[1];
}
return "";
}
}
export const queryStore = new QueryStore({dispose: () => {}});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment