django-bananas
Version:
React admin frontend for Django.
441 lines (424 loc) • 14.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _history = require("history");
var _jsLogger = _interopRequireDefault(require("js-logger"));
var _pages = require("./pages");
var _utils = require("./utils");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : String(i); }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
var logger = _jsLogger.default.get("bananas");
class Router {
constructor() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
_defineProperty(this, "multipleParamsRegexp", new RegExp(/\{[^}]+\}/g));
_defineProperty(this, "getAdjustedPathLength", route => route.path.replace(this.multipleParamsRegexp, "").split("/"));
var basename = options.prefix || "";
if (basename.endsWith("/")) {
basename = basename.substring(0, basename.length - 1);
}
this.history = options.history ? typeof options.history === "function" ? options.history({
basename
}) : options.history : (0, _history.createBrowserHistory)({
basename
});
// Disable muting of route notification events
this.isMuted = false;
// Listen on leaving admin or full page reload event
window.addEventListener("beforeunload", this.routeWillUpdate.bind(this));
}
initialize(swagger) {
this.swagger = swagger;
// Add API prospect routes
this.routes = Object.entries(swagger.spec.paths).reduce((specs, _ref) => {
var [endpoint, methods] = _ref;
return [...specs, ...Object.entries(methods).map(_ref2 => {
var [method, spec] = _ref2;
return [endpoint, method, spec];
})];
}, []).filter(_ref3 => {
var [, method, spec] = _ref3;
return spec.operationId && ["get", "post"].includes(method);
}).map(_ref4 => {
var [endpoint, method, spec] = _ref4;
var {
__originalOperationId,
operationId,
parameters,
summary,
tags
} = spec;
var id = __originalOperationId;
var action = this.getAction(id);
var path = this.getPath(endpoint, method, action);
var template = this.getTemplate(id, path, action);
var app = this.getAppLabel(tags);
var apiView = this.getAPIView(id);
var title = this.getTitle(summary, id);
var params = parameters.filter(param => param.required && param.in === "path");
return {
id,
operationId,
path,
action,
template,
app,
apiView,
title,
navigation: tags.includes("navigation"),
parameters: {
names: params.map(param => param.name) || null,
types: params.reduce((types, p) => _objectSpread(_objectSpread({}, types), {}, {
[p.name]: p.type
}), {})
},
pattern: new RegExp(params.reduce((pattern, param) => pattern.replace("{".concat(param.name, "}"), param.type === "integer" ? "(\\d+)" // Named: `(?<${param.name}>\\d+)`
: "([\\.\\%\\w\\d_-]+)" // Named: `(?<${param.name}>[\\.\\%\\w\\d_-]+)`
), "^".concat(path, "$")))
};
});
// Add non-api routes
if (swagger.isAuthenticated) {
this.add({
id: "home",
app: "",
path: "/",
title: "Dashboard",
template: "index.js",
navigation: true
});
}
// Build reverse routes
this.reverseRoutes = this.routes.reduce((operations, route) => {
operations[route.id] = route;
operations[route.operationId] = route;
return operations;
}, {});
// Build navigation routes
this.navigationRoutes = this.routes.filter(route => route.navigation).sort((route1, route2) => {
if (route1.app < route2.app) {
return -1;
} else if (route1.app > route2.app) {
return 1;
} else if (route1.title < route2.title) {
return -1;
} else if (route1.title > route2.title) {
return 1;
}
return 0;
}).map(route => ({
id: route.id,
path: route.path,
app: route.app || "",
title: route.title,
name: route.action === "list" ? route.apiView : route.action,
parent: route.action === "list" ? null : route.apiView
}));
logger.debug("Initialized Router:", this.routes);
}
add(_ref5) {
var {
id,
operationId,
path,
title,
template,
pattern,
app,
navigation
} = _ref5;
var action = this.getAction(operationId);
this.routes.push({
id,
operationId: operationId || id,
parameters: {
// TODO: Parse from path, i.e. /foo/bar/{id}/ -> ["id"]
names: null,
types: null
},
path,
action,
template: template || this.getTemplate(path, action),
app: app !== undefined ? app : id.split(":")[0].split(".")[0],
title: this.getTitle(title, id),
navigation,
pattern: new RegExp(pattern || "^".concat(path, "$"))
});
this.sort();
}
sort() {
this.routes = this.routes.sort((route1, route2) => {
var path1 = this.getAdjustedPathLength(route1);
var path2 = this.getAdjustedPathLength(route2);
// If both paths has the same segment length, grade based on ... :
return path1.length === path2.length // ... alphabetical order
? path1 < path2 ? 1 : -1 : path1.length < path2.length // ... segment length
? 1 : -1;
});
}
on(eventName, handler) {
var _this = this;
if (typeof handler !== "function") {
logger.error("Invalid handler when subcribing on event:", handler);
return null;
}
if (eventName === "routeWillUpdate") {
this.onRouteWillUpdate = handler;
return () => {
// Unlisten
this.onRouteWillUpdate = undefined;
};
} else if (eventName === "routeDidUpdate") {
return this.history.listen(function () {
if (!_this.isMuted) {
handler(...arguments);
}
});
}
logger.error("Unknown router event name:", eventName);
return null;
}
routeWillUpdate() {
/*
* Notify listeners of routeWillUpdate event
*/
if (this.onRouteWillUpdate && !this.isMuted) {
this.onRouteWillUpdate(...arguments);
}
}
updateState(state) {
/*
* Updates current history location's state without notifiying listeners
*/
logger.debug("Router.updateState()", state);
this.isMuted = true;
var {
location
} = this.history;
this.history.replace(_objectSpread(_objectSpread({}, location), {}, {
state: _objectSpread(_objectSpread({}, location.state), state)
}));
this.isMuted = false;
}
getBasePath(path) {
// Extract base path: /foo/bar/baz/ -> /foo/bar/
return path.substring(0, (0, _utils.nthIndexOf)(path, "/", 2, 1) + 1 || undefined);
}
getPath(endpoint, method, action) {
return method === "get" ? endpoint : "".concat(endpoint).concat(action, "/");
}
getAction(originalOperationId) {
// Extract action part: bananas.change_password:create -> create
return originalOperationId ? originalOperationId.split(":")[1] : null;
}
getTemplate(id, path, action) {
var internalPages = {
"bananas.me:list": _pages.MePage
};
var template = internalPages[id];
// Build route template relative url path
if (!template) {
var basePath = this.getBasePath(path, true);
var relativeBasePath = basePath.substring(1);
template = "".concat(relativeBasePath).concat(action, ".js");
}
return template;
}
getAppLabel(tags) {
var app = tags.filter(tag => tag.startsWith("app:"))[0];
return app.split(":")[1];
}
getAPIView(id) {
return id.substring(id.indexOf(".") + 1, id.indexOf(":"));
}
getTitle(summary, apiView) {
return summary || apiView && (0, _utils.capitalize)(apiView.replace("_", " ")) || null;
}
getRoute(id) {
return this.reverseRoutes[id];
}
getOperationTemplate(id) {
var route = this.getRoute(id);
return route ? route.template : undefined;
}
ResolvedRoute(route, overrides) {
// Resolved route structure
return _objectSpread({
id: route.id,
operationId: route.operationId,
path: route.path,
app: route.app,
action: route.action,
title: route.title,
template: typeof route.template === "string" ? route.template : "Component"
}, overrides);
}
resolve(path) {
var _this2 = this;
// Resolve path to matching route
var {
location
} = this.history;
var pathname = (0, _utils.absolutePath)(path, location.pathname);
var _loop = function _loop(route) {
var match = route.pattern.exec(pathname);
if (!match) {
return 0; // continue
}
/*
// Future RegExp groups
const params =
route.parameters.names && route.parameters.names.length
? route.pattern.exec(pathname).groups
: null;
*/
// Extract URL params
var params = route.parameters.names && route.parameters.names.length ? match.slice(1).map((value, index) => [route.parameters.names[index], value]).reduce((groups, _ref6) => {
var [name, value] = _ref6;
return _objectSpread(_objectSpread({}, groups), {}, {
[name]: route.parameters.types[name] === "integer" ? parseInt(value, 10) : value
});
}, {}) : null;
var resolved = _this2.ResolvedRoute(route, {
path: pathname,
params
});
logger.debug("Router.resolve():", resolved);
return {
v: resolved
};
},
_ret;
for (var route of this.routes) {
_ret = _loop(route);
if (_ret === 0) continue;
if (_ret) return _ret.v;
}
return null;
}
reverse(id, params) {
// Reverse resolve route by operationId
var route = this.getRoute(id);
if (route) {
// Render parameterized path
var path = params ? Object.entries(params).reduce((_path, _ref7) => {
var [param, value] = _ref7;
return _path.replace("{".concat(param, "}"), value);
}, route.path) : route.path;
return this.ResolvedRoute(route, {
path,
params: params || null
});
}
return null;
}
reroute(to) {
// Rewrite route to current window location
logger.debug("Router.reroute()");
var {
location
} = this.history;
var {
pathname,
search,
hash
} = location;
return this.route(to || {
path: pathname,
query: search,
hash
}, {
rewrite: true
});
}
route(to) {
var {
rewrite = false,
patch = false
} = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
/*
* `to` can either be string...
*
* "/foo/bar/"
*
* ...or object...
*
* {
* id: "foo_bar_read",
* params: {id: 1},
* path: /foo/bar/1/
* query: "?x=y" | {x: "y"}
* hash: "tab1"
* }
*
* Note: `id` + optional `params` OR `path`
*/
var current = this.history.location;
var next = typeof to === "string" ? {
pathname: to,
search: "",
hash: ""
} : {
pathname: to.path || current.pathname,
search: typeof to.query === "object" ? (0, _utils.toQuery)(to.query) : to.query || "",
hash: (0, _utils.ensureLeadingHash)(to.hash) || ""
};
// Resolve
var route = undefined;
if (to.id) {
route = this.reverse(to.id, to.params);
} else {
route = this.resolve(next.pathname);
}
if (route) {
next.pathname = route.path;
}
var referer = (this.history.location.state ? this.history.location.state.referer : null) || {};
var pageChange = next.pathname !== current.pathname;
var rewind = pageChange && next.pathname === referer.pathname;
// Patch next location and keep parts from current or referer location
if (patch && (!pageChange || rewind)) {
var origin = next.pathname === referer.pathname ? referer : current;
next.search = next.search ? next.search : origin.search;
next.hash = next.hash ? next.hash : origin.hash;
}
// Extend resolved route with query/hash
if (route) {
route.query = next.search ? (0, _utils.fromQuery)(next.search) : null;
route.hash = next.hash ? next.hash.substring(1) : null; // Strip leading #
}
var locationChange = pageChange || next.search !== current.search || next.hash !== current.hash;
var replace = rewrite || !locationChange;
var action = replace ? "REPLACE" : "PUSH";
var navigate = replace ? this.history.replace : this.history.push;
// Notify that route about to update
this.routeWillUpdate(next, action);
// Refresh current, event listeners may have modified state
current = this.history.location;
// Set next location's state
next.state = _objectSpread(_objectSpread({
scroll: rewind ? referer.state.scroll : 0
}, replace ? current.state : undefined), {}, {
route,
referer: pageChange && !rewrite ? _objectSpread(_objectSpread({}, current), {}, {
state: _objectSpread(_objectSpread({}, current.state), {}, {
referer: undefined // Drop referer's referer to not endless nest
})
}) : referer
});
// Change history
logger.debug("Router.route():", next);
navigate(next);
return {
location: next,
action
};
}
}
exports.default = Router;