mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
Merge pull request #218 from cameronapak/cp/216-fix-users-link
Fix Sidebar Users Route Links
This commit is contained in:
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@@ -2,3 +2,4 @@ playwright-report
|
|||||||
test-results
|
test-results
|
||||||
bknd.config.*
|
bknd.config.*
|
||||||
__test__/helper.d.ts
|
__test__/helper.d.ts
|
||||||
|
.env.local
|
||||||
@@ -110,10 +110,11 @@ describe("MediaApi", () => {
|
|||||||
const api = new MediaApi({}, mockedBackend.request);
|
const api = new MediaApi({}, mockedBackend.request);
|
||||||
|
|
||||||
const name = "image.png";
|
const name = "image.png";
|
||||||
const res = await api.getFileStream(name);
|
const res = await api.getFile(name);
|
||||||
expect(isReadableStream(res)).toBe(true);
|
const stream = await api.getFileStream(name);
|
||||||
|
expect(isReadableStream(stream)).toBe(true);
|
||||||
|
|
||||||
const blob = (await new Response(res).blob()) as File;
|
const blob = (await res.res.blob()) as File;
|
||||||
expect(isFile(blob)).toBe(true);
|
expect(isFile(blob)).toBe(true);
|
||||||
expect(blob.size).toBeGreaterThan(0);
|
expect(blob.size).toBeGreaterThan(0);
|
||||||
expect(blob.type).toBe("image/png");
|
expect(blob.type).toBe("image/png");
|
||||||
|
|||||||
288
app/__test__/ui/client/utils/AppReduced.spec.ts
Normal file
288
app/__test__/ui/client/utils/AppReduced.spec.ts
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from "vitest";
|
||||||
|
import { AppReduced, type AppType } from "ui/client/utils/AppReduced";
|
||||||
|
import type { BkndAdminProps } from "ui/Admin";
|
||||||
|
|
||||||
|
// Import the normalizeAdminPath function for testing
|
||||||
|
// Note: This assumes the function is exported or we need to test it indirectly through public methods
|
||||||
|
|
||||||
|
describe("AppReduced", () => {
|
||||||
|
let mockAppJson: AppType;
|
||||||
|
let appReduced: AppReduced;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockAppJson = {
|
||||||
|
data: {
|
||||||
|
entities: {},
|
||||||
|
relations: {},
|
||||||
|
},
|
||||||
|
flows: {
|
||||||
|
flows: {},
|
||||||
|
},
|
||||||
|
auth: {},
|
||||||
|
} as AppType;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getSettingsPath", () => {
|
||||||
|
it("should return settings path with basepath", () => {
|
||||||
|
const options: BkndAdminProps["config"] = {
|
||||||
|
basepath: "/admin",
|
||||||
|
logo_return_path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, options);
|
||||||
|
const result = appReduced.getSettingsPath();
|
||||||
|
|
||||||
|
expect(result).toBe("~/admin/settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return settings path with empty basepath", () => {
|
||||||
|
const options: BkndAdminProps["config"] = {
|
||||||
|
basepath: "",
|
||||||
|
logo_return_path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, options);
|
||||||
|
const result = appReduced.getSettingsPath();
|
||||||
|
|
||||||
|
expect(result).toBe("~/settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should append additional path segments", () => {
|
||||||
|
const options: BkndAdminProps["config"] = {
|
||||||
|
basepath: "/admin",
|
||||||
|
logo_return_path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, options);
|
||||||
|
const result = appReduced.getSettingsPath(["user", "profile"]);
|
||||||
|
|
||||||
|
expect(result).toBe("~/admin/settings/user/profile");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should normalize multiple slashes", () => {
|
||||||
|
const options: BkndAdminProps["config"] = {
|
||||||
|
basepath: "//admin//",
|
||||||
|
logo_return_path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, options);
|
||||||
|
const result = appReduced.getSettingsPath(["//user//"]);
|
||||||
|
|
||||||
|
expect(result).toBe("~/admin/settings/user");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle basepath without leading slash", () => {
|
||||||
|
const options: BkndAdminProps["config"] = {
|
||||||
|
basepath: "admin",
|
||||||
|
logo_return_path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, options);
|
||||||
|
const result = appReduced.getSettingsPath();
|
||||||
|
|
||||||
|
expect(result).toBe("~/admin/settings");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getAbsolutePath", () => {
|
||||||
|
it("should return absolute path with basepath", () => {
|
||||||
|
const options: BkndAdminProps["config"] = {
|
||||||
|
basepath: "/admin",
|
||||||
|
logo_return_path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, options);
|
||||||
|
const result = appReduced.getAbsolutePath("dashboard");
|
||||||
|
|
||||||
|
expect(result).toBe("~/admin/dashboard");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return base path when no path provided", () => {
|
||||||
|
const options: BkndAdminProps["config"] = {
|
||||||
|
basepath: "/admin",
|
||||||
|
logo_return_path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, options);
|
||||||
|
const result = appReduced.getAbsolutePath();
|
||||||
|
|
||||||
|
expect(result).toBe("~/admin");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should normalize paths correctly", () => {
|
||||||
|
const options: BkndAdminProps["config"] = {
|
||||||
|
basepath: "//admin//",
|
||||||
|
logo_return_path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, options);
|
||||||
|
const result = appReduced.getAbsolutePath("//dashboard//");
|
||||||
|
|
||||||
|
expect(result).toBe("~/admin/dashboard");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("options getter", () => {
|
||||||
|
it("should return merged options with defaults", () => {
|
||||||
|
const customOptions: BkndAdminProps["config"] = {
|
||||||
|
basepath: "/custom-admin",
|
||||||
|
logo_return_path: "/custom-home",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, customOptions);
|
||||||
|
const options = appReduced.options;
|
||||||
|
|
||||||
|
expect(options).toEqual({
|
||||||
|
logo_return_path: "/custom-home",
|
||||||
|
basepath: "/custom-admin",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use default logo_return_path when not provided", () => {
|
||||||
|
const customOptions: BkndAdminProps["config"] = {
|
||||||
|
basepath: "/admin",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, customOptions);
|
||||||
|
const options = appReduced.options;
|
||||||
|
|
||||||
|
expect(options.logo_return_path).toBe("/");
|
||||||
|
expect(options.basepath).toBe("/admin");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("path normalization behavior", () => {
|
||||||
|
it("should normalize duplicate slashes in settings path", () => {
|
||||||
|
const options: BkndAdminProps["config"] = {
|
||||||
|
basepath: "/admin",
|
||||||
|
logo_return_path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, options);
|
||||||
|
const result = appReduced.getSettingsPath(["//nested//path//"]);
|
||||||
|
|
||||||
|
expect(result).toBe("~/admin/settings/nested/path");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle root path normalization", () => {
|
||||||
|
const options: BkndAdminProps["config"] = {
|
||||||
|
basepath: "/",
|
||||||
|
logo_return_path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, options);
|
||||||
|
const result = appReduced.getAbsolutePath();
|
||||||
|
|
||||||
|
// The normalizeAdminPath function removes trailing slashes except for root "/"
|
||||||
|
// When basepath is "/", the result is "~/" which becomes "~" after normalization
|
||||||
|
expect(result).toBe("~");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve entity paths ending with slash", () => {
|
||||||
|
const options: BkndAdminProps["config"] = {
|
||||||
|
basepath: "/admin",
|
||||||
|
logo_return_path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, options);
|
||||||
|
const result = appReduced.getAbsolutePath("entity/");
|
||||||
|
|
||||||
|
expect(result).toBe("~/admin/entity");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove trailing slashes from non-entity paths", () => {
|
||||||
|
const options: BkndAdminProps["config"] = {
|
||||||
|
basepath: "/admin",
|
||||||
|
logo_return_path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, options);
|
||||||
|
const result = appReduced.getAbsolutePath("dashboard/");
|
||||||
|
|
||||||
|
expect(result).toBe("~/admin/dashboard");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("withBasePath - double slash fix (admin_basepath with trailing slash)", () => {
|
||||||
|
it("should not produce double slashes when admin_basepath has trailing slash", () => {
|
||||||
|
const options: BkndAdminProps["config"] = {
|
||||||
|
basepath: "/",
|
||||||
|
admin_basepath: "/admin/",
|
||||||
|
logo_return_path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, options);
|
||||||
|
const result = appReduced.withBasePath("/data");
|
||||||
|
|
||||||
|
expect(result).toBe("/admin/data");
|
||||||
|
expect(result).not.toContain("//");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work correctly when admin_basepath has no trailing slash", () => {
|
||||||
|
const options: BkndAdminProps["config"] = {
|
||||||
|
basepath: "/",
|
||||||
|
admin_basepath: "/admin",
|
||||||
|
logo_return_path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, options);
|
||||||
|
const result = appReduced.withBasePath("/data");
|
||||||
|
|
||||||
|
expect(result).toBe("/admin/data");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle absolute paths with admin_basepath trailing slash", () => {
|
||||||
|
const options: BkndAdminProps["config"] = {
|
||||||
|
basepath: "/",
|
||||||
|
admin_basepath: "/admin/",
|
||||||
|
logo_return_path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, options);
|
||||||
|
const result = appReduced.getAbsolutePath("data");
|
||||||
|
|
||||||
|
expect(result).toBe("~/admin/data");
|
||||||
|
expect(result).not.toContain("//");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle settings path with admin_basepath trailing slash", () => {
|
||||||
|
const options: BkndAdminProps["config"] = {
|
||||||
|
basepath: "/",
|
||||||
|
admin_basepath: "/admin/",
|
||||||
|
logo_return_path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, options);
|
||||||
|
const result = appReduced.getSettingsPath(["general"]);
|
||||||
|
|
||||||
|
expect(result).toBe("~/admin/settings/general");
|
||||||
|
expect(result).not.toContain("//");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("edge cases", () => {
|
||||||
|
it("should handle undefined basepath", () => {
|
||||||
|
const options: BkndAdminProps["config"] = {
|
||||||
|
logo_return_path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, options);
|
||||||
|
const result = appReduced.getSettingsPath();
|
||||||
|
|
||||||
|
// When basepath is undefined, it defaults to empty string
|
||||||
|
expect(result).toBe("~/settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle null path segments", () => {
|
||||||
|
const options: BkndAdminProps["config"] = {
|
||||||
|
basepath: "/admin",
|
||||||
|
logo_return_path: "/",
|
||||||
|
};
|
||||||
|
|
||||||
|
appReduced = new AppReduced(mockAppJson, options);
|
||||||
|
const result = appReduced.getSettingsPath(["", "valid", ""]);
|
||||||
|
|
||||||
|
expect(result).toBe("~/admin/settings/valid");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,6 +11,7 @@ import { css, Style } from "hono/css";
|
|||||||
import { Controller } from "modules/Controller";
|
import { Controller } from "modules/Controller";
|
||||||
import * as SystemPermissions from "modules/permissions";
|
import * as SystemPermissions from "modules/permissions";
|
||||||
import type { TApiUser } from "Api";
|
import type { TApiUser } from "Api";
|
||||||
|
import type { AppTheme } from "ui/client/use-theme";
|
||||||
import type { Manifest } from "vite";
|
import type { Manifest } from "vite";
|
||||||
|
|
||||||
const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
|
const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
|
||||||
@@ -20,7 +21,7 @@ export type AdminBkndWindowContext = {
|
|||||||
logout_route: string;
|
logout_route: string;
|
||||||
admin_basepath: string;
|
admin_basepath: string;
|
||||||
logo_return_path?: string;
|
logo_return_path?: string;
|
||||||
theme?: "dark" | "light" | "system";
|
theme?: AppTheme;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @todo: add migration to remove admin path from config
|
// @todo: add migration to remove admin path from config
|
||||||
@@ -31,7 +32,7 @@ export type AdminControllerOptions = {
|
|||||||
html?: string;
|
html?: string;
|
||||||
forceDev?: boolean | { mainPath: string };
|
forceDev?: boolean | { mainPath: string };
|
||||||
debugRerenders?: boolean;
|
debugRerenders?: boolean;
|
||||||
theme?: "dark" | "light" | "system";
|
theme?: AppTheme;
|
||||||
logoReturnPath?: string;
|
logoReturnPath?: string;
|
||||||
manifest?: Manifest;
|
manifest?: Manifest;
|
||||||
};
|
};
|
||||||
@@ -122,7 +123,7 @@ export class AdminController extends Controller {
|
|||||||
const obj: AdminBkndWindowContext = {
|
const obj: AdminBkndWindowContext = {
|
||||||
user: c.get("auth")?.user,
|
user: c.get("auth")?.user,
|
||||||
logout_route: authRoutes.logout,
|
logout_route: authRoutes.logout,
|
||||||
admin_basepath: this.options.adminBasepath,
|
admin_basepath: this.options.adminBasepath.replace(/\/+$/, ""),
|
||||||
theme: this.options.theme,
|
theme: this.options.theme,
|
||||||
logo_return_path: this.options.logoReturnPath,
|
logo_return_path: this.options.logoReturnPath,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ export class AppReduced {
|
|||||||
protected appJson: AppType,
|
protected appJson: AppType,
|
||||||
protected _options: BkndAdminProps["config"] = {},
|
protected _options: BkndAdminProps["config"] = {},
|
||||||
) {
|
) {
|
||||||
//console.log("received appjson", appJson);
|
|
||||||
|
|
||||||
this._entities = Object.entries(this.appJson.data.entities ?? {}).map(([name, entity]) => {
|
this._entities = Object.entries(this.appJson.data.entities ?? {}).map(([name, entity]) => {
|
||||||
return constructEntity(name, entity);
|
return constructEntity(name, entity);
|
||||||
@@ -70,20 +69,27 @@ export class AppReduced {
|
|||||||
|
|
||||||
get options() {
|
get options() {
|
||||||
return {
|
return {
|
||||||
basepath: "",
|
basepath: "/",
|
||||||
logo_return_path: "/",
|
logo_return_path: "/",
|
||||||
...this._options,
|
...this._options,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withBasePath(path: string | string[], absolute = false): string {
|
||||||
|
const paths = Array.isArray(path) ? path : [path];
|
||||||
|
return [absolute ? "~" : null, this.options.basepath, this.options.admin_basepath, ...paths]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("/")
|
||||||
|
.replace(/\/+/g, "/")
|
||||||
|
.replace(/\/$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
getSettingsPath(path: string[] = []): string {
|
getSettingsPath(path: string[] = []): string {
|
||||||
const base = `~/${this.options.basepath}/settings`.replace(/\/+/g, "/");
|
return this.withBasePath(["settings", ...path], true);
|
||||||
return [base, ...path].join("/");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAbsolutePath(path?: string): string {
|
getAbsolutePath(path?: string): string {
|
||||||
const { basepath } = this.options;
|
return this.withBasePath(path ?? [], true);
|
||||||
return (path ? `~/${basepath}/${path}` : `~/${basepath}`).replace(/\/+/g, "/");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthConfig() {
|
getAuthConfig() {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function Routes({
|
|||||||
}) {
|
}) {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const ctx = useBkndWindowContext();
|
const ctx = useBkndWindowContext();
|
||||||
const actualBasePath = basePath || ctx.admin_basepath;
|
const actualBasePath = (basePath || ctx.admin_basepath).replace(/\/+$/, "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="bknd-admin" className={theme + " antialiased"}>
|
<div id="bknd-admin" className={theme + " antialiased"}>
|
||||||
|
|||||||
Reference in New Issue
Block a user