mirror of
https://github.com/stefanpejcic/openpanel
synced 2025-06-26 18:28:26 +00:00
packages
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
import { waitFor } from "@testing-library/react";
|
||||
|
||||
import { asyncDebounce } from ".";
|
||||
|
||||
describe("asyncDebounce", () => {
|
||||
it("should debounce the function", async () => {
|
||||
const fn = jest.fn((num: number) => Promise.resolve(num));
|
||||
const debounced = asyncDebounce(fn, 1000);
|
||||
|
||||
const result1 = debounced(1);
|
||||
const result2 = debounced(2);
|
||||
const result3 = debounced(3);
|
||||
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
|
||||
await Promise.allSettled([result1, result2, result3]);
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
expect(fn).toHaveBeenCalledWith(3);
|
||||
});
|
||||
|
||||
it("should flush the debounced function", async () => {
|
||||
jest.useRealTimers();
|
||||
const fn = jest.fn((num: number) => Promise.resolve(num));
|
||||
const catcher = jest.fn();
|
||||
const debounced = asyncDebounce(fn, 1000);
|
||||
|
||||
debounced(0).catch(catcher);
|
||||
const result1 = debounced(1);
|
||||
debounced.flush();
|
||||
const result2 = debounced(2);
|
||||
|
||||
await Promise.allSettled([result1, result2]);
|
||||
|
||||
expect(catcher).toHaveBeenCalledTimes(1);
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(fn).toHaveBeenCalledWith(1);
|
||||
expect(fn).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it("should cancel the debounced function", async () => {
|
||||
const fn = jest.fn((num: number) => Promise.resolve(num));
|
||||
const catcher = jest.fn();
|
||||
const debounced = asyncDebounce(fn, 1000);
|
||||
|
||||
debounced(1).catch(catcher);
|
||||
debounced.cancel();
|
||||
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
await waitFor(() => expect(catcher).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it("should respect the wait time", async () => {
|
||||
jest.useFakeTimers();
|
||||
|
||||
const fn = jest.fn((num: number) => Promise.resolve(num));
|
||||
const catcher = jest.fn();
|
||||
|
||||
const debounced = asyncDebounce(fn, 2000);
|
||||
|
||||
debounced(1).catch(catcher);
|
||||
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
debounced(2).catch(catcher);
|
||||
|
||||
jest.advanceTimersByTime(2000);
|
||||
|
||||
await waitFor(() => expect(fn).toHaveBeenCalledTimes(1));
|
||||
|
||||
await waitFor(() => expect(fn).toHaveBeenCalledWith(2));
|
||||
|
||||
await waitFor(() => expect(catcher).toHaveBeenCalledTimes(1));
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it("should debounce non-promises", async () => {
|
||||
const fn = jest.fn((num: number) => num);
|
||||
const catcher = jest.fn();
|
||||
const debounced = asyncDebounce(fn, 1000);
|
||||
|
||||
const result1 = debounced(1).catch(catcher);
|
||||
const result2 = debounced(2).catch(catcher);
|
||||
const result3 = debounced(3).catch(catcher);
|
||||
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
|
||||
await Promise.allSettled([result1, result2, result3]);
|
||||
|
||||
await waitFor(() => expect(fn).toHaveBeenCalledTimes(1));
|
||||
|
||||
await waitFor(() => expect(fn).toHaveBeenCalledWith(3));
|
||||
|
||||
await waitFor(() => expect(catcher).toHaveBeenCalledTimes(2));
|
||||
});
|
||||
|
||||
it("should reject by cancel reason", async () => {
|
||||
const fn = jest.fn((num: number) => Promise.resolve(num));
|
||||
const catcher = jest.fn();
|
||||
const debounced = asyncDebounce(fn, 1000, "canceled");
|
||||
|
||||
debounced(1).catch(catcher);
|
||||
debounced(2).catch(catcher);
|
||||
|
||||
await waitFor(() => expect(catcher).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(catcher).toHaveBeenCalledWith("canceled"));
|
||||
|
||||
debounced.cancel();
|
||||
|
||||
await waitFor(() => expect(catcher).toHaveBeenCalledTimes(2));
|
||||
await waitFor(() => expect(catcher).toHaveBeenCalledWith("canceled"));
|
||||
});
|
||||
|
||||
it("should call the correct callback in long awaits", async () => {
|
||||
const resolvedMock = jest.fn();
|
||||
const fn = jest.fn(
|
||||
(num: number) =>
|
||||
new Promise((res) => {
|
||||
setTimeout(() => {
|
||||
resolvedMock(num);
|
||||
res(num);
|
||||
}, 2000);
|
||||
}),
|
||||
);
|
||||
const catcher = jest.fn();
|
||||
const resolver = jest.fn();
|
||||
const nextResolver = jest.fn();
|
||||
const debounced = asyncDebounce(fn, 1000, "canceled");
|
||||
|
||||
const options = { timeout: 3000 };
|
||||
|
||||
debounced(1).catch(catcher);
|
||||
debounced(2).catch(catcher);
|
||||
debounced(3).then(resolver).catch(catcher);
|
||||
|
||||
await waitFor(() => expect(catcher).toHaveBeenCalledTimes(2));
|
||||
|
||||
// wait for function to be triggered
|
||||
await waitFor(() => expect(fn).toHaveBeenCalledTimes(1));
|
||||
// but not resolved yet
|
||||
await waitFor(() => expect(resolvedMock).not.toBeCalled());
|
||||
|
||||
// call the debounced again
|
||||
debounced(4).then(nextResolver).catch(catcher);
|
||||
|
||||
// next call should not interrupt the previous call and successfully resolve with `resolver`
|
||||
await waitFor(() => expect(resolver).toBeCalledTimes(1), options);
|
||||
await waitFor(() => expect(resolvedMock).toBeCalledWith(3));
|
||||
// next call should not be resolved
|
||||
await waitFor(() => expect(nextResolver).not.toBeCalled());
|
||||
|
||||
// wait for second call to be made
|
||||
await waitFor(() => expect(fn).toHaveBeenCalledTimes(2), options);
|
||||
// wait for it to be resolved
|
||||
await waitFor(() => expect(nextResolver).toBeCalledTimes(1), options);
|
||||
await waitFor(() => expect(resolvedMock).toBeCalledWith(4));
|
||||
// fourth call should not reject the third because it's already resolved
|
||||
await waitFor(() => expect(catcher).toHaveBeenCalledTimes(2));
|
||||
// third call should not be resolved again
|
||||
await waitFor(() => expect(resolver).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it("should cancel previous and reject last if errored", async () => {
|
||||
const fn = jest.fn((num: number) => {
|
||||
if (num === 3) {
|
||||
return Promise.reject("error");
|
||||
}
|
||||
return Promise.resolve(num);
|
||||
});
|
||||
const catcher = jest.fn();
|
||||
const debounced = asyncDebounce(fn, 1000, "canceled");
|
||||
|
||||
debounced(1).catch(catcher);
|
||||
debounced(2).catch(catcher);
|
||||
|
||||
await waitFor(() => expect(catcher).toHaveBeenCalledTimes(1));
|
||||
await waitFor(() => expect(catcher).toHaveBeenCalledWith("canceled"));
|
||||
|
||||
debounced(3).catch(catcher);
|
||||
|
||||
await waitFor(() => expect(catcher).toHaveBeenCalledTimes(2));
|
||||
await waitFor(() => expect(catcher).toHaveBeenCalledWith("canceled"));
|
||||
|
||||
await waitFor(() => expect(catcher).toHaveBeenCalledTimes(3));
|
||||
await waitFor(() => expect(catcher).toHaveBeenCalledWith("error"));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import debounce from "lodash/debounce";
|
||||
|
||||
type Callbacks<T extends (...args: any) => any> = {
|
||||
resolve?: (value: Awaited<ReturnType<T>>) => void;
|
||||
reject?: (reason?: any) => void;
|
||||
};
|
||||
|
||||
type DebouncedFunction<T extends (...args: any) => any> = {
|
||||
(...args: Parameters<T>): Promise<Awaited<ReturnType<T>>>;
|
||||
flush: () => void;
|
||||
cancel: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Debounces sync and async functions with given wait time. The debounced function returns a promise which can be awaited or catched.
|
||||
* Only the last call of the debounced function will resolve or reject.
|
||||
* Previous calls will be rejected with the given cancelReason.
|
||||
*
|
||||
* The original debounce function doesn't work well with async functions,
|
||||
* It won't return a promise to resolve/reject and therefore it's not possible to await the result.
|
||||
* This will always return a promise to handle and await the result.
|
||||
* Previous calls will be rejected immediately after a new call made.
|
||||
*/
|
||||
export const asyncDebounce = <T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait = 1000,
|
||||
cancelReason?: string,
|
||||
): DebouncedFunction<T> => {
|
||||
let callbacks: Array<Callbacks<T>> = [];
|
||||
|
||||
const cancelPrevious = () => {
|
||||
callbacks.forEach((cb) => cb.reject?.(cancelReason));
|
||||
callbacks = [];
|
||||
};
|
||||
|
||||
const debouncedFunc = debounce((...args: Parameters<T>) => {
|
||||
const { resolve, reject } = callbacks.pop() || {};
|
||||
Promise.resolve(func(...args))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}, wait);
|
||||
|
||||
const runner = (...args: Parameters<T>) => {
|
||||
return new Promise<Awaited<ReturnType<T>>>((resolve, reject) => {
|
||||
cancelPrevious();
|
||||
|
||||
callbacks.push({
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
|
||||
debouncedFunc(...args);
|
||||
});
|
||||
};
|
||||
|
||||
runner.flush = () => debouncedFunc.flush();
|
||||
runner.cancel = () => {
|
||||
debouncedFunc.cancel();
|
||||
cancelPrevious();
|
||||
};
|
||||
|
||||
return runner;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { mockLegacyRouterProvider, mockRouterProvider } from "@test/index";
|
||||
|
||||
import { checkRouterPropMisuse } from ".";
|
||||
|
||||
describe("checkRouterPropMisuse", () => {
|
||||
it("should return false when pass routerBindings", () => {
|
||||
expect(checkRouterPropMisuse(mockRouterProvider())).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return true when pass legacyRouterProvider", () => {
|
||||
expect(checkRouterPropMisuse(mockLegacyRouterProvider())).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { LegacyRouterProvider } from "../../../contexts/router/legacy/types";
|
||||
import type { RouterProvider } from "../../../contexts/router/types";
|
||||
|
||||
export const checkRouterPropMisuse = (
|
||||
value: LegacyRouterProvider | RouterProvider,
|
||||
) => {
|
||||
// check if `routerProvider` prop is passed with legacy properties.
|
||||
// If yes, console.warn the user to use `legacyRuterProvider` prop instead.
|
||||
const bindings = ["go", "parse", "back", "Link"];
|
||||
|
||||
// check if `value` contains properties other than `bindings`
|
||||
const otherProps = Object.keys(value).filter(
|
||||
(key) => !bindings.includes(key),
|
||||
);
|
||||
|
||||
const hasOtherProps = otherProps.length > 0;
|
||||
|
||||
if (hasOtherProps) {
|
||||
console.warn(
|
||||
`Unsupported properties are found in \`routerProvider\` prop. You provided \`${otherProps.join(
|
||||
", ",
|
||||
)}\`. Supported properties are \`${bindings.join(
|
||||
", ",
|
||||
)}\`. You may wanted to use \`legacyRouterProvider\` prop instead.`,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { waitFor } from "@testing-library/react";
|
||||
import { deferExecution } from ".";
|
||||
|
||||
describe("deferExecution", () => {
|
||||
beforeEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
it("should defer the call after caller returns", async () => {
|
||||
const array: number[] = [];
|
||||
|
||||
const fn = () => {
|
||||
array.push(1);
|
||||
|
||||
deferExecution(() => {
|
||||
array.push(3);
|
||||
});
|
||||
|
||||
array.push(2);
|
||||
};
|
||||
|
||||
fn();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(array).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Delays the execution of a callback function asynchronously.
|
||||
* This utility function is used to defer the execution of the provided
|
||||
* callback, allowing the current call stack to clear before the callback
|
||||
* is invoked. It is particularly useful for ensuring non-blocking behavior
|
||||
* and providing a clear intent when a 0 ms timeout is used.
|
||||
*/
|
||||
export const deferExecution = (fn: Function) => {
|
||||
setTimeout(fn, 0);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
export const downloadInBrowser = (
|
||||
filename: string,
|
||||
content: string,
|
||||
type?: string,
|
||||
) => {
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([content], { type });
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.setAttribute("visibility", "hidden");
|
||||
link.download = filename;
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
link.href = blobUrl;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
// As per documentation, call URL.revokeObjectURL to remove the blob from memory.
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import { flattenObjectKeys } from ".";
|
||||
|
||||
describe("flattenObjectKeys", () => {
|
||||
it("should flatten an object with nested objects and arrays", () => {
|
||||
const obj = {
|
||||
a: 1,
|
||||
b: {
|
||||
c: 2,
|
||||
d: [3, 4],
|
||||
},
|
||||
e: {
|
||||
f: {
|
||||
g: 5,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const flattenedObj = flattenObjectKeys(obj);
|
||||
|
||||
expect(flattenedObj).toEqual({
|
||||
a: 1,
|
||||
b: {
|
||||
c: 2,
|
||||
d: [3, 4],
|
||||
},
|
||||
"b.c": 2,
|
||||
"b.d": [3, 4],
|
||||
"b.d.0": 3,
|
||||
"b.d.1": 4,
|
||||
e: {
|
||||
f: {
|
||||
g: 5,
|
||||
},
|
||||
},
|
||||
"e.f": {
|
||||
g: 5,
|
||||
},
|
||||
"e.f.g": 5,
|
||||
});
|
||||
});
|
||||
|
||||
it("should flatten an object with empty nested objects and arrays", () => {
|
||||
const obj = {
|
||||
a: 1,
|
||||
b: {},
|
||||
c: [],
|
||||
};
|
||||
|
||||
const flattenedObj = flattenObjectKeys(obj);
|
||||
|
||||
expect(flattenedObj).toEqual({
|
||||
a: 1,
|
||||
b: {},
|
||||
c: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("should flatten an object with nested objects and arrays with custom prefix", () => {
|
||||
const obj = {
|
||||
a: 1,
|
||||
b: {
|
||||
c: 2,
|
||||
d: [3, 4],
|
||||
},
|
||||
e: {
|
||||
f: {
|
||||
g: 5,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const flattenedObj = flattenObjectKeys(obj, "prefix");
|
||||
|
||||
expect(flattenedObj).toEqual({
|
||||
"prefix.a": 1,
|
||||
"prefix.b": {
|
||||
c: 2,
|
||||
d: [3, 4],
|
||||
},
|
||||
"prefix.b.c": 2,
|
||||
"prefix.b.d": [3, 4],
|
||||
"prefix.b.d.0": 3,
|
||||
"prefix.b.d.1": 4,
|
||||
"prefix.e": {
|
||||
f: {
|
||||
g: 5,
|
||||
},
|
||||
},
|
||||
"prefix.e.f": {
|
||||
g: 5,
|
||||
},
|
||||
"prefix.e.f.g": 5,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
const isNested = (obj: any) => typeof obj === "object" && obj !== null;
|
||||
const isArray = (obj: any) => Array.isArray(obj);
|
||||
|
||||
export const flattenObjectKeys = (obj: any, prefix = "") => {
|
||||
if (!isNested(obj)) {
|
||||
return {
|
||||
[prefix]: obj,
|
||||
};
|
||||
}
|
||||
|
||||
return Object.keys(obj).reduce(
|
||||
(acc, key) => {
|
||||
const currentPrefix = prefix.length ? `${prefix}.` : "";
|
||||
|
||||
if (isNested(obj[key]) && Object.keys(obj[key]).length) {
|
||||
if (isArray(obj[key]) && obj[key].length) {
|
||||
obj[key].forEach((item: unknown[], index: number) => {
|
||||
Object.assign(
|
||||
acc,
|
||||
flattenObjectKeys(item, `${currentPrefix + key}.${index}`),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
Object.assign(acc, flattenObjectKeys(obj[key], currentPrefix + key));
|
||||
}
|
||||
// Even if it's a nested object, it should be treated as a key as well
|
||||
acc[currentPrefix + key] = obj[key];
|
||||
} else {
|
||||
acc[currentPrefix + key] = obj[key];
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, unknown>,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
import { generateDefaultDocumentTitle } from ".";
|
||||
import * as UseRefineContext from "../../../hooks/refine/useRefineContext";
|
||||
import { defaultRefineOptions } from "@contexts/refine";
|
||||
|
||||
const translateMock = jest.fn(
|
||||
(key: string, options?: any, defaultMessage?: string | undefined) => {
|
||||
return defaultMessage ?? options;
|
||||
},
|
||||
);
|
||||
|
||||
describe("generateDocumentTitle", () => {
|
||||
jest.spyOn(UseRefineContext, "useRefineContext").mockReturnValue({
|
||||
options: defaultRefineOptions,
|
||||
} as any);
|
||||
|
||||
beforeEach(() => {
|
||||
translateMock.mockClear();
|
||||
});
|
||||
|
||||
it("should return the default title when resource is undefined", () => {
|
||||
expect(generateDefaultDocumentTitle(translateMock)).toBe("Refine");
|
||||
});
|
||||
|
||||
it("should return `resource name` when action is `list`", () => {
|
||||
expect(
|
||||
generateDefaultDocumentTitle(translateMock, { name: "posts" }, "list"),
|
||||
).toBe("Posts | Refine");
|
||||
});
|
||||
|
||||
it("should return the label of the resource when it is provided", () => {
|
||||
expect(
|
||||
generateDefaultDocumentTitle(
|
||||
translateMock,
|
||||
{ name: "posts", label: "Posts Label" },
|
||||
"list",
|
||||
),
|
||||
).toBe("Posts Label | Refine");
|
||||
});
|
||||
|
||||
it("should return the meta.label of the resource when it is provided", () => {
|
||||
expect(
|
||||
generateDefaultDocumentTitle(
|
||||
translateMock,
|
||||
{
|
||||
name: "posts",
|
||||
label: undefined,
|
||||
meta: { label: "Meta Label" },
|
||||
},
|
||||
"list",
|
||||
),
|
||||
).toBe("Meta Label | Refine");
|
||||
});
|
||||
|
||||
it("should return `Create new resource name` when action is `create`", () => {
|
||||
expect(
|
||||
generateDefaultDocumentTitle(translateMock, { name: "posts" }, "create"),
|
||||
).toBe("Create new Post | Refine");
|
||||
});
|
||||
|
||||
it("should return `#id Clone resource name` when action is `clone`", () => {
|
||||
expect(
|
||||
generateDefaultDocumentTitle(
|
||||
translateMock,
|
||||
{ name: "posts" },
|
||||
"clone",
|
||||
"1",
|
||||
),
|
||||
).toBe("#1 Clone Post | Refine");
|
||||
});
|
||||
|
||||
it("should return `#id Edit resource name` when action is `edit`", () => {
|
||||
expect(
|
||||
generateDefaultDocumentTitle(
|
||||
translateMock,
|
||||
{ name: "posts" },
|
||||
"edit",
|
||||
"1",
|
||||
),
|
||||
).toBe("#1 Edit Post | Refine");
|
||||
});
|
||||
|
||||
it("should return `#id Show resource name` when action is `show`", () => {
|
||||
expect(
|
||||
generateDefaultDocumentTitle(
|
||||
translateMock,
|
||||
{ name: "posts" },
|
||||
"show",
|
||||
"1",
|
||||
),
|
||||
).toBe("#1 Show Post | Refine");
|
||||
});
|
||||
|
||||
it("should pass `id` to `translate` function", () => {
|
||||
generateDefaultDocumentTitle(translateMock, { name: "posts" }, "show", "1");
|
||||
|
||||
expect(translateMock).toHaveBeenCalledWith(
|
||||
"documentTitle.posts.show",
|
||||
{ id: "1" },
|
||||
"#1 Show Post | Refine",
|
||||
);
|
||||
});
|
||||
|
||||
it("should use the fallback value when `translate` returns the key", () => {
|
||||
translateMock.mockReturnValueOnce("documentTitle.posts.show");
|
||||
expect(
|
||||
generateDefaultDocumentTitle(
|
||||
translateMock,
|
||||
{ name: "posts" },
|
||||
"show",
|
||||
"1",
|
||||
),
|
||||
).toBe("#1 Show Post | Refine");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { useTranslate } from "@hooks/i18n";
|
||||
|
||||
import type { IResourceItem } from "../../../contexts/resource/types";
|
||||
import { safeTranslate } from "../safe-translate";
|
||||
import { userFriendlyResourceName } from "../userFriendlyResourceName";
|
||||
|
||||
/**
|
||||
* Generates document title for the given resource and action.
|
||||
*/
|
||||
export function generateDefaultDocumentTitle(
|
||||
translate: ReturnType<typeof useTranslate>,
|
||||
resource?: IResourceItem,
|
||||
action?: string,
|
||||
id?: string,
|
||||
resourceName?: string,
|
||||
) {
|
||||
const actionPrefixMatcher = {
|
||||
create: "Create new ",
|
||||
clone: `#${id ?? ""} Clone `,
|
||||
edit: `#${id ?? ""} Edit `,
|
||||
show: `#${id ?? ""} Show `,
|
||||
list: "",
|
||||
};
|
||||
|
||||
const identifier = resource?.identifier ?? resource?.name;
|
||||
|
||||
const resourceNameFallback =
|
||||
resource?.label ??
|
||||
resource?.meta?.label ??
|
||||
userFriendlyResourceName(
|
||||
identifier,
|
||||
action === "list" ? "plural" : "singular",
|
||||
);
|
||||
|
||||
const resourceNameWithFallback = resourceName ?? resourceNameFallback;
|
||||
|
||||
const defaultTitle = safeTranslate(
|
||||
translate,
|
||||
"documentTitle.default",
|
||||
"Refine",
|
||||
);
|
||||
const suffix = safeTranslate(translate, "documentTitle.suffix", " | Refine");
|
||||
let autoGeneratedTitle = defaultTitle;
|
||||
|
||||
if (action && identifier) {
|
||||
autoGeneratedTitle = safeTranslate(
|
||||
translate,
|
||||
`documentTitle.${identifier}.${action}`,
|
||||
`${
|
||||
actionPrefixMatcher[action as keyof typeof actionPrefixMatcher] ?? ""
|
||||
}${resourceNameWithFallback}${suffix}`,
|
||||
{ id },
|
||||
);
|
||||
}
|
||||
|
||||
return autoGeneratedTitle;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { handleMultiple } from ".";
|
||||
|
||||
describe("handleMultiple", () => {
|
||||
it("should be resolve multiple promise", () => {
|
||||
expect(
|
||||
handleMultiple([
|
||||
Promise.resolve({ data: 1 }),
|
||||
Promise.resolve({ data: 2 }),
|
||||
]),
|
||||
).toEqual(Promise.resolve({ data: [1, 2] }));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
export const handleMultiple = async <TData = unknown>(
|
||||
promises: Promise<{ data: TData }>[],
|
||||
): Promise<{ data: TData[] }> => {
|
||||
return {
|
||||
data: (await Promise.all(promises)).map((res) => res.data),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { handlePaginationParams } from ".";
|
||||
|
||||
describe("handlePaginationParams", () => {
|
||||
it("should return default pagination", () => {
|
||||
expect(handlePaginationParams()).toEqual({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
mode: "server",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return pagination from `pagination` prop", () => {
|
||||
expect(
|
||||
handlePaginationParams({
|
||||
pagination: { current: 2, pageSize: 20, mode: "client" },
|
||||
}),
|
||||
).toEqual({
|
||||
current: 2,
|
||||
pageSize: 20,
|
||||
mode: "client",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return pagination from `config` prop", () => {
|
||||
expect(
|
||||
handlePaginationParams({
|
||||
configPagination: { current: 3, pageSize: 30 },
|
||||
}),
|
||||
).toEqual({
|
||||
current: 3,
|
||||
pageSize: 30,
|
||||
mode: "server",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return pagination from `pagination` prop if config is defined", () => {
|
||||
expect(
|
||||
handlePaginationParams({
|
||||
pagination: { current: 2, pageSize: 20, mode: "client" },
|
||||
configPagination: { current: 3, pageSize: 30 },
|
||||
}),
|
||||
).toEqual({
|
||||
current: 2,
|
||||
pageSize: 20,
|
||||
mode: "client",
|
||||
});
|
||||
});
|
||||
|
||||
it("if `mode` is not defined in `pagination` prop, should return according to `hasPagination` prop", () => {
|
||||
expect(
|
||||
handlePaginationParams({
|
||||
hasPagination: false,
|
||||
}),
|
||||
).toEqual({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
mode: "off",
|
||||
});
|
||||
});
|
||||
|
||||
it("if both `hasPagination` and `pagination.mode` are defined, should return according to `pagination` prop", () => {
|
||||
expect(
|
||||
handlePaginationParams({
|
||||
hasPagination: true,
|
||||
pagination: { mode: "client" },
|
||||
}),
|
||||
).toEqual({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
mode: "client",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Pagination } from "../../../contexts/data/types";
|
||||
import { pickNotDeprecated } from "../pickNotDeprecated";
|
||||
|
||||
type HandlePaginationParamsProps = {
|
||||
hasPagination?: boolean;
|
||||
pagination?: Pagination;
|
||||
configPagination?: Pagination;
|
||||
};
|
||||
|
||||
export const handlePaginationParams = ({
|
||||
hasPagination,
|
||||
pagination,
|
||||
configPagination,
|
||||
}: HandlePaginationParamsProps = {}): Required<Pagination> => {
|
||||
const hasPaginationString = hasPagination === false ? "off" : "server";
|
||||
const mode = pagination?.mode ?? hasPaginationString;
|
||||
|
||||
const current =
|
||||
pickNotDeprecated(pagination?.current, configPagination?.current) ?? 1;
|
||||
|
||||
const pageSize =
|
||||
pickNotDeprecated(pagination?.pageSize, configPagination?.pageSize) ?? 10;
|
||||
|
||||
return {
|
||||
current,
|
||||
pageSize,
|
||||
mode,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,292 @@
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { defaultRefineOptions } from "@contexts/refine";
|
||||
|
||||
import { handleRefineOptions } from ".";
|
||||
import type { IRefineOptions } from "../../../contexts/refine/types";
|
||||
|
||||
describe("handleRefineOptions", () => {
|
||||
it("should return the default options if no options are provided", () => {
|
||||
const { optionsWithDefaults } = handleRefineOptions();
|
||||
|
||||
expect(optionsWithDefaults).toEqual(defaultRefineOptions);
|
||||
});
|
||||
|
||||
it("should return the options if they are provided", () => {
|
||||
const options: IRefineOptions = {
|
||||
mutationMode: "optimistic",
|
||||
disableTelemetry: true,
|
||||
liveMode: "auto",
|
||||
reactQuery: {
|
||||
clientConfig: {
|
||||
defaultOptions: { queries: { enabled: false } },
|
||||
},
|
||||
devtoolConfig: false,
|
||||
},
|
||||
undoableTimeout: 1000,
|
||||
syncWithLocation: true,
|
||||
warnWhenUnsavedChanges: true,
|
||||
redirect: {
|
||||
afterClone: "show",
|
||||
afterCreate: "edit",
|
||||
afterEdit: "show",
|
||||
},
|
||||
breadcrumb: false,
|
||||
};
|
||||
|
||||
const {
|
||||
optionsWithDefaults,
|
||||
disableTelemetryWithDefault,
|
||||
reactQueryWithDefaults,
|
||||
} = handleRefineOptions({ options });
|
||||
|
||||
expect(optionsWithDefaults).toEqual({
|
||||
liveMode: "auto",
|
||||
mutationMode: "optimistic",
|
||||
syncWithLocation: true,
|
||||
undoableTimeout: 1000,
|
||||
warnWhenUnsavedChanges: true,
|
||||
redirect: {
|
||||
afterClone: "show",
|
||||
afterCreate: "edit",
|
||||
afterEdit: "show",
|
||||
},
|
||||
breadcrumb: false,
|
||||
overtime: {
|
||||
interval: 1000,
|
||||
},
|
||||
textTransformers: {
|
||||
humanize: expect.any(Function),
|
||||
plural: expect.any(Function),
|
||||
singular: expect.any(Function),
|
||||
},
|
||||
disableServerSideValidation: false,
|
||||
title: expect.objectContaining({
|
||||
icon: expect.any(Object),
|
||||
text: "Refine Project",
|
||||
}),
|
||||
});
|
||||
expect(disableTelemetryWithDefault).toBe(true);
|
||||
expect(reactQueryWithDefaults).toEqual({
|
||||
clientConfig: {
|
||||
defaultOptions: { queries: { enabled: false } },
|
||||
},
|
||||
devtoolConfig: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return the options if they are provided both in options and passed directly <Refine>", () => {
|
||||
const options: IRefineOptions = {
|
||||
mutationMode: "optimistic",
|
||||
disableTelemetry: true,
|
||||
liveMode: "auto",
|
||||
reactQuery: {
|
||||
clientConfig: {
|
||||
defaultOptions: { queries: { enabled: false } },
|
||||
},
|
||||
devtoolConfig: false,
|
||||
},
|
||||
undoableTimeout: 1000,
|
||||
syncWithLocation: true,
|
||||
warnWhenUnsavedChanges: true,
|
||||
};
|
||||
|
||||
const {
|
||||
optionsWithDefaults,
|
||||
disableTelemetryWithDefault,
|
||||
reactQueryWithDefaults,
|
||||
} = handleRefineOptions({
|
||||
options,
|
||||
mutationMode: "pessimistic",
|
||||
disableTelemetry: false,
|
||||
liveMode: "manual",
|
||||
reactQueryClientConfig: {
|
||||
defaultOptions: { queries: { enabled: true } },
|
||||
},
|
||||
reactQueryDevtoolConfig: {
|
||||
position: "bottom-left",
|
||||
},
|
||||
syncWithLocation: false,
|
||||
undoableTimeout: 2000,
|
||||
warnWhenUnsavedChanges: false,
|
||||
});
|
||||
|
||||
expect(optionsWithDefaults).toEqual({
|
||||
liveMode: "auto",
|
||||
mutationMode: "optimistic",
|
||||
syncWithLocation: true,
|
||||
undoableTimeout: 1000,
|
||||
warnWhenUnsavedChanges: true,
|
||||
redirect: {
|
||||
afterClone: "list",
|
||||
afterCreate: "list",
|
||||
afterEdit: "list",
|
||||
},
|
||||
overtime: {
|
||||
interval: 1000,
|
||||
},
|
||||
textTransformers: {
|
||||
humanize: expect.any(Function),
|
||||
plural: expect.any(Function),
|
||||
singular: expect.any(Function),
|
||||
},
|
||||
disableServerSideValidation: false,
|
||||
title: expect.objectContaining({
|
||||
icon: expect.any(Object),
|
||||
text: "Refine Project",
|
||||
}),
|
||||
});
|
||||
expect(disableTelemetryWithDefault).toBe(true);
|
||||
expect(reactQueryWithDefaults).toEqual({
|
||||
clientConfig: {
|
||||
defaultOptions: { queries: { enabled: false } },
|
||||
},
|
||||
devtoolConfig: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return directly passed <Refine> options if options are not provided", () => {
|
||||
const {
|
||||
optionsWithDefaults,
|
||||
disableTelemetryWithDefault,
|
||||
reactQueryWithDefaults,
|
||||
} = handleRefineOptions({
|
||||
mutationMode: "pessimistic",
|
||||
disableTelemetry: true,
|
||||
liveMode: "manual",
|
||||
reactQueryClientConfig: {
|
||||
defaultOptions: { queries: { enabled: false } },
|
||||
},
|
||||
reactQueryDevtoolConfig: {
|
||||
position: "bottom-right",
|
||||
},
|
||||
syncWithLocation: false,
|
||||
undoableTimeout: 2000,
|
||||
warnWhenUnsavedChanges: false,
|
||||
});
|
||||
|
||||
expect(optionsWithDefaults).toEqual({
|
||||
liveMode: "manual",
|
||||
mutationMode: "pessimistic",
|
||||
syncWithLocation: false,
|
||||
undoableTimeout: 2000,
|
||||
warnWhenUnsavedChanges: false,
|
||||
redirect: {
|
||||
afterClone: "list",
|
||||
afterCreate: "list",
|
||||
afterEdit: "list",
|
||||
},
|
||||
overtime: {
|
||||
interval: 1000,
|
||||
},
|
||||
textTransformers: {
|
||||
humanize: expect.any(Function),
|
||||
plural: expect.any(Function),
|
||||
singular: expect.any(Function),
|
||||
},
|
||||
disableServerSideValidation: false,
|
||||
title: expect.objectContaining({
|
||||
icon: expect.any(Object),
|
||||
text: "Refine Project",
|
||||
}),
|
||||
});
|
||||
expect(disableTelemetryWithDefault).toBe(true);
|
||||
expect(reactQueryWithDefaults).toEqual({
|
||||
clientConfig: {
|
||||
defaultOptions: { queries: { enabled: false } },
|
||||
},
|
||||
devtoolConfig: {
|
||||
position: "bottom-right",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("if some of the redirect options are not provided, should return the default ones for those options", () => {
|
||||
const { optionsWithDefaults } = handleRefineOptions({
|
||||
options: {
|
||||
redirect: {
|
||||
afterClone: "show",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(optionsWithDefaults.redirect).toEqual({
|
||||
afterClone: "show",
|
||||
afterCreate: "list",
|
||||
afterEdit: "list",
|
||||
});
|
||||
});
|
||||
|
||||
it("it should return provided query client", () => {
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const options: IRefineOptions = {
|
||||
reactQuery: {
|
||||
clientConfig: queryClient,
|
||||
devtoolConfig: false,
|
||||
},
|
||||
};
|
||||
|
||||
const { reactQueryWithDefaults } = handleRefineOptions({ options });
|
||||
|
||||
expect(reactQueryWithDefaults).toEqual({
|
||||
clientConfig: queryClient,
|
||||
devtoolConfig: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("it should return projectId", () => {
|
||||
const options: IRefineOptions = {
|
||||
projectId: "test",
|
||||
};
|
||||
|
||||
const { optionsWithDefaults } = handleRefineOptions({ options });
|
||||
|
||||
expect(optionsWithDefaults.projectId).toEqual("test");
|
||||
});
|
||||
|
||||
it("it should return title", () => {
|
||||
const options: IRefineOptions = {
|
||||
title: {
|
||||
icon: "My Icon",
|
||||
text: "My Project",
|
||||
},
|
||||
};
|
||||
|
||||
const { optionsWithDefaults } = handleRefineOptions({ options });
|
||||
|
||||
expect(optionsWithDefaults.title).toEqual(
|
||||
expect.objectContaining({ icon: "My Icon", text: "My Project" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("it should return modified title partially", () => {
|
||||
const options: IRefineOptions = {
|
||||
title: {
|
||||
icon: undefined,
|
||||
text: "My Project",
|
||||
},
|
||||
};
|
||||
|
||||
const { optionsWithDefaults } = handleRefineOptions({ options });
|
||||
|
||||
expect(optionsWithDefaults.title).toEqual(
|
||||
expect.objectContaining({ icon: expect.any(Object), text: "My Project" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("it should accept null values for title", () => {
|
||||
const options: IRefineOptions = {
|
||||
title: {
|
||||
icon: null,
|
||||
text: "My Project",
|
||||
},
|
||||
};
|
||||
|
||||
const { optionsWithDefaults } = handleRefineOptions({ options });
|
||||
|
||||
expect(optionsWithDefaults.title).toEqual(
|
||||
expect.objectContaining({ icon: null, text: "My Project" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { QueryClient, QueryClientConfig } from "@tanstack/react-query";
|
||||
|
||||
import { defaultRefineOptions } from "@contexts/refine";
|
||||
|
||||
import type { MutationMode } from "../../../contexts/data/types";
|
||||
import type { LiveModeProps } from "../../../contexts/live/types";
|
||||
import type {
|
||||
IRefineContextOptions,
|
||||
IRefineOptions,
|
||||
} from "../../../contexts/refine/types";
|
||||
|
||||
type HandleRefineOptionsProps = {
|
||||
options?: IRefineOptions;
|
||||
mutationMode?: MutationMode;
|
||||
syncWithLocation?: boolean;
|
||||
warnWhenUnsavedChanges?: boolean;
|
||||
undoableTimeout?: number;
|
||||
liveMode?: LiveModeProps["liveMode"];
|
||||
disableTelemetry?: boolean;
|
||||
reactQueryClientConfig?: QueryClientConfig;
|
||||
reactQueryDevtoolConfig?: any | false;
|
||||
};
|
||||
|
||||
type HandleRefineOptionsReturnValues = {
|
||||
optionsWithDefaults: IRefineContextOptions;
|
||||
disableTelemetryWithDefault: boolean;
|
||||
reactQueryWithDefaults: {
|
||||
clientConfig: QueryClientConfig | InstanceType<typeof QueryClient>;
|
||||
devtoolConfig: false | any;
|
||||
};
|
||||
};
|
||||
|
||||
export const handleRefineOptions = ({
|
||||
options,
|
||||
disableTelemetry,
|
||||
liveMode,
|
||||
mutationMode,
|
||||
reactQueryClientConfig,
|
||||
reactQueryDevtoolConfig,
|
||||
syncWithLocation,
|
||||
undoableTimeout,
|
||||
warnWhenUnsavedChanges,
|
||||
}: HandleRefineOptionsProps = {}): HandleRefineOptionsReturnValues => {
|
||||
const optionsWithDefaults: IRefineContextOptions = {
|
||||
breadcrumb: options?.breadcrumb,
|
||||
mutationMode:
|
||||
options?.mutationMode ??
|
||||
mutationMode ??
|
||||
defaultRefineOptions.mutationMode,
|
||||
undoableTimeout:
|
||||
options?.undoableTimeout ??
|
||||
undoableTimeout ??
|
||||
defaultRefineOptions.undoableTimeout,
|
||||
syncWithLocation:
|
||||
options?.syncWithLocation ??
|
||||
syncWithLocation ??
|
||||
defaultRefineOptions.syncWithLocation,
|
||||
warnWhenUnsavedChanges:
|
||||
options?.warnWhenUnsavedChanges ??
|
||||
warnWhenUnsavedChanges ??
|
||||
defaultRefineOptions.warnWhenUnsavedChanges,
|
||||
liveMode: options?.liveMode ?? liveMode ?? defaultRefineOptions.liveMode,
|
||||
redirect: {
|
||||
afterCreate:
|
||||
options?.redirect?.afterCreate ??
|
||||
defaultRefineOptions.redirect.afterCreate,
|
||||
afterClone:
|
||||
options?.redirect?.afterClone ??
|
||||
defaultRefineOptions.redirect.afterClone,
|
||||
afterEdit:
|
||||
options?.redirect?.afterEdit ?? defaultRefineOptions.redirect.afterEdit,
|
||||
},
|
||||
overtime: options?.overtime ?? defaultRefineOptions.overtime,
|
||||
textTransformers: {
|
||||
humanize:
|
||||
options?.textTransformers?.humanize ??
|
||||
defaultRefineOptions.textTransformers.humanize,
|
||||
plural:
|
||||
options?.textTransformers?.plural ??
|
||||
defaultRefineOptions.textTransformers.plural,
|
||||
singular:
|
||||
options?.textTransformers?.singular ??
|
||||
defaultRefineOptions.textTransformers.singular,
|
||||
},
|
||||
disableServerSideValidation:
|
||||
options?.disableServerSideValidation ??
|
||||
defaultRefineOptions.disableServerSideValidation,
|
||||
projectId: options?.projectId,
|
||||
useNewQueryKeys: options?.useNewQueryKeys,
|
||||
title: {
|
||||
icon:
|
||||
typeof options?.title?.icon === "undefined"
|
||||
? defaultRefineOptions.title.icon
|
||||
: options?.title?.icon,
|
||||
text:
|
||||
typeof options?.title?.text === "undefined"
|
||||
? defaultRefineOptions.title.text
|
||||
: options?.title?.text,
|
||||
},
|
||||
};
|
||||
|
||||
const disableTelemetryWithDefault =
|
||||
options?.disableTelemetry ?? disableTelemetry ?? false;
|
||||
|
||||
const reactQueryWithDefaults = {
|
||||
clientConfig:
|
||||
options?.reactQuery?.clientConfig ?? reactQueryClientConfig ?? {},
|
||||
devtoolConfig:
|
||||
options?.reactQuery?.devtoolConfig ?? reactQueryDevtoolConfig ?? {},
|
||||
};
|
||||
|
||||
return {
|
||||
optionsWithDefaults,
|
||||
disableTelemetryWithDefault,
|
||||
reactQueryWithDefaults,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import { handleUseParams } from ".";
|
||||
|
||||
const paramsWithoutId = {
|
||||
resource: "posts",
|
||||
};
|
||||
|
||||
const paramsWithId = {
|
||||
resource: "posts",
|
||||
id: "11",
|
||||
};
|
||||
|
||||
const paramsWithOutId = {
|
||||
resource: "posts",
|
||||
id: null,
|
||||
};
|
||||
|
||||
const paramsWithUniqueId = {
|
||||
resource: "posts",
|
||||
id: "hede/hede",
|
||||
};
|
||||
|
||||
const paramsWithBase64EncodedId = {
|
||||
resource: "posts",
|
||||
id: "cmVmaW5lIHJ1bHo=",
|
||||
};
|
||||
|
||||
describe("handleUseParams", () => {
|
||||
it("should return params true even id does not passed", () => {
|
||||
expect(handleUseParams(paramsWithoutId)).toEqual(paramsWithoutId);
|
||||
});
|
||||
|
||||
it("should return params true with Id", () => {
|
||||
expect(
|
||||
handleUseParams({
|
||||
...paramsWithId,
|
||||
id: encodeURIComponent(paramsWithId.id),
|
||||
}),
|
||||
).toEqual(paramsWithId);
|
||||
});
|
||||
|
||||
it("should return params true with unique Id", () => {
|
||||
expect(
|
||||
handleUseParams({
|
||||
...paramsWithUniqueId,
|
||||
id: encodeURIComponent(paramsWithUniqueId.id),
|
||||
}),
|
||||
).toEqual(paramsWithUniqueId);
|
||||
});
|
||||
|
||||
it("should return params true with nique Id", () => {
|
||||
expect(
|
||||
handleUseParams({
|
||||
...paramsWithBase64EncodedId,
|
||||
id: encodeURIComponent(paramsWithBase64EncodedId.id),
|
||||
}),
|
||||
).toEqual(paramsWithBase64EncodedId);
|
||||
});
|
||||
|
||||
it("should return params without id", () => {
|
||||
expect(handleUseParams(paramsWithOutId)).toEqual(paramsWithOutId);
|
||||
});
|
||||
|
||||
it("should return params empty object with does not passed", () => {
|
||||
expect(handleUseParams()).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
export const handleUseParams = (params: any = {}): any => {
|
||||
if (params?.id) {
|
||||
return {
|
||||
...params,
|
||||
id: decodeURIComponent(params.id),
|
||||
};
|
||||
}
|
||||
return params;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { hasPermission } from ".";
|
||||
|
||||
describe("hasPermission", () => {
|
||||
it("should return true if permissions includes the action", () => {
|
||||
expect(hasPermission(["create", "edit"], "create")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return false if permissions not includes the action", () => {
|
||||
expect(hasPermission(["edit", "show"], "create")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return false if permissions equal to undefined", () => {
|
||||
expect(hasPermission(undefined, "create")).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return false if action equal to undefined", () => {
|
||||
expect(hasPermission(["create", "edit"], undefined)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return false both of params equal to undefined", () => {
|
||||
expect(hasPermission(undefined, undefined)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
export const hasPermission = (
|
||||
permissions: string[] | undefined,
|
||||
action: string | undefined,
|
||||
): boolean => {
|
||||
if (!permissions || !action) {
|
||||
return false;
|
||||
}
|
||||
return !!permissions.find((i) => i === action);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { humanizeString } from ".";
|
||||
|
||||
describe("humanizeString", () => {
|
||||
it("should return strings for humans!", () => {
|
||||
expect(humanizeString("fooBar")).toBe("Foo bar");
|
||||
expect(humanizeString("foo-bar")).toBe("Foo bar");
|
||||
expect(humanizeString("foo_bar")).toBe("Foo bar");
|
||||
expect(humanizeString("myFOOBar")).toBe("My foo bar");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
export const humanizeString = (text: string): string => {
|
||||
text = text.replace(/([a-z]{1})([A-Z]{1})/g, "$1-$2");
|
||||
text = text.replace(/([A-Z]{1})([A-Z]{1})([a-z]{1})/g, "$1-$2$3");
|
||||
|
||||
text = text
|
||||
.toLowerCase()
|
||||
.replace(/[_-]+/g, " ")
|
||||
.replace(/\s{2,}/g, " ")
|
||||
.trim();
|
||||
text = text.charAt(0).toUpperCase() + text.slice(1);
|
||||
|
||||
return text;
|
||||
};
|
||||
// https://www.npmjs.com/package/humanize-string
|
||||
@@ -0,0 +1,48 @@
|
||||
import { importCSVMapper } from ".";
|
||||
|
||||
const rawData = [
|
||||
["id", "title", "content"],
|
||||
["600f5fce", "Fantastic Granite", "Ea expedita doloremque et."],
|
||||
["946a03b1", "Sky blue Nepal", "Consequatur ad aut odit."],
|
||||
];
|
||||
|
||||
const transformedData = [
|
||||
{
|
||||
id: "600f5fce",
|
||||
title: "Fantastic Granite",
|
||||
content: "Ea expedita doloremque et.",
|
||||
},
|
||||
{
|
||||
id: "946a03b1",
|
||||
title: "Sky blue Nepal",
|
||||
content: "Consequatur ad aut odit.",
|
||||
},
|
||||
];
|
||||
|
||||
describe("importCSVMapper", () => {
|
||||
it("should transform the given data", () => {
|
||||
expect(importCSVMapper(rawData)).toEqual(transformedData);
|
||||
});
|
||||
|
||||
it("should run the given mapper callback with correct parameters", () => {
|
||||
const mapperFn = jest.fn((item) => item);
|
||||
|
||||
importCSVMapper(rawData, mapperFn);
|
||||
|
||||
expect(mapperFn).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(mapperFn).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
transformedData[0],
|
||||
0,
|
||||
transformedData,
|
||||
);
|
||||
|
||||
expect(mapperFn).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
transformedData[1],
|
||||
1,
|
||||
transformedData,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import fromPairs from "lodash/fromPairs";
|
||||
import zip from "lodash/zip";
|
||||
|
||||
import type { MapDataFn } from "../../../hooks/export/types";
|
||||
|
||||
export const importCSVMapper = <TItem = any, TVariables = any>(
|
||||
data: any[][],
|
||||
mapData: MapDataFn<TItem, TVariables> = (item) => item as any,
|
||||
): TVariables[] => {
|
||||
const [headers, ...body] = data;
|
||||
return body
|
||||
.map((entry) => fromPairs(zip(headers, entry)))
|
||||
.map((item: any, index, array: any) =>
|
||||
mapData.call(undefined, item, index, array),
|
||||
);
|
||||
};
|
||||
36
packages/core/src/definitions/helpers/index.ts
Normal file
36
packages/core/src/definitions/helpers/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
export { userFriendlySecond } from "./userFriendlySeconds";
|
||||
export { importCSVMapper } from "./importCSVMapper";
|
||||
export { userFriendlyResourceName } from "./userFriendlyResourceName";
|
||||
export { handleUseParams } from "./handleUseParams";
|
||||
export { queryKeys, queryKeysReplacement } from "./queryKeys";
|
||||
export { hasPermission } from "./hasPermission";
|
||||
export { routeGenerator } from "./routeGenerator";
|
||||
export { createTreeView } from "./treeView/createTreeView";
|
||||
export { humanizeString } from "./humanizeString";
|
||||
export { handleRefineOptions } from "./handleRefineOptions";
|
||||
export { redirectPage } from "./redirectPage";
|
||||
export { sequentialPromises } from "./sequentialPromises";
|
||||
export { pickDataProvider } from "./pickDataProvider";
|
||||
export { handleMultiple } from "./handleMultiple";
|
||||
export {
|
||||
getNextPageParam,
|
||||
getPreviousPageParam,
|
||||
} from "./useInfinitePagination";
|
||||
export { pickNotDeprecated } from "./pickNotDeprecated";
|
||||
export { legacyResourceTransform } from "./legacy-resource-transform";
|
||||
export { matchResourceFromRoute } from "./router/match-resource-from-route";
|
||||
export { getActionRoutesFromResource } from "./router";
|
||||
export { composeRoute } from "./router/compose-route";
|
||||
export { useActiveAuthProvider } from "./useActiveAuthProvider";
|
||||
export { handlePaginationParams } from "./handlePaginationParams";
|
||||
export { useMediaQuery } from "./useMediaQuery";
|
||||
export { generateDefaultDocumentTitle } from "./generateDocumentTitle";
|
||||
export { useUserFriendlyName } from "./useUserFriendlyName";
|
||||
export { keys, stripUndefined } from "./keys";
|
||||
export { KeyBuilder } from "./keys";
|
||||
export { flattenObjectKeys } from "./flatten-object-keys";
|
||||
export { propertyPathToArray } from "./property-path-to-array";
|
||||
export { downloadInBrowser } from "./downloadInBrowser";
|
||||
export { deferExecution } from "./defer-execution";
|
||||
export { asyncDebounce } from "./async-debounce";
|
||||
export { prepareQueryContext } from "./prepare-query-context";
|
||||
438
packages/core/src/definitions/helpers/keys/index.spec.ts
Normal file
438
packages/core/src/definitions/helpers/keys/index.spec.ts
Normal file
@@ -0,0 +1,438 @@
|
||||
import { arrayReplace, arrayFindIndex, stripUndefined, keys } from ".";
|
||||
import { queryKeys } from "../queryKeys";
|
||||
|
||||
describe("keys", () => {
|
||||
describe("keys()", () => {
|
||||
it("keys().get() === []", () => {
|
||||
const keyBuilder = keys();
|
||||
expect(keyBuilder.get()).toEqual([]);
|
||||
});
|
||||
|
||||
it("keys().get(true) === []", () => {
|
||||
const keyBuilder = keys();
|
||||
expect(keyBuilder.legacy()).toEqual([]);
|
||||
});
|
||||
|
||||
it("keys().get() === []", () => {
|
||||
const keyBuilder = keys();
|
||||
expect(keyBuilder.get()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("keys().auth()", () => {
|
||||
it("keys().auth().get() === [auth]", () => {
|
||||
const keyBuilder = keys().auth();
|
||||
expect(keyBuilder.get()).toEqual(["auth"]);
|
||||
});
|
||||
|
||||
it("keys().auth().get(true) === [auth]", () => {
|
||||
const keyBuilder = keys().auth();
|
||||
expect(keyBuilder.legacy()).toEqual(["auth"]);
|
||||
});
|
||||
|
||||
it("keys().auth().get() === [auth]", () => {
|
||||
const keyBuilder = keys().auth();
|
||||
expect(keyBuilder.get()).toEqual(["auth"]);
|
||||
});
|
||||
|
||||
it("keys().auth().action(login).get() === [auth, login]", () => {
|
||||
const keyBuilder = keys().auth().action("login");
|
||||
expect(keyBuilder.get()).toEqual(["auth", "login"]);
|
||||
});
|
||||
|
||||
it("keys().auth().action(login).params({ username: 'test' }) === [auth, login, { username: test }]", () => {
|
||||
const keyBuilder = keys().auth().action("login").params({
|
||||
username: "test",
|
||||
});
|
||||
|
||||
expect(keyBuilder.get()).toEqual(["auth", "login", { username: "test" }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("keys().data()", () => {
|
||||
it("keys().data().get() === [data, default]", () => {
|
||||
const keyBuilder = keys().data();
|
||||
expect(keyBuilder.get()).toEqual(["data", "default"]);
|
||||
});
|
||||
|
||||
it("keys().data('my-data-provider').get() === [data, my-data-provider]", () => {
|
||||
const keyBuilder = keys().data("my-data-provider");
|
||||
expect(keyBuilder.get()).toEqual(["data", "my-data-provider"]);
|
||||
});
|
||||
|
||||
it("keys().data().resource(users).get() === [data, default, users]", () => {
|
||||
const keyBuilder = keys().data().resource("users");
|
||||
expect(keyBuilder.get()).toEqual(["data", "default", "users"]);
|
||||
});
|
||||
|
||||
it("keys().data().resource(posts).action(list).get() === [data, default, posts, list]", () => {
|
||||
const keyBuilder = keys().data().resource("posts").action("list");
|
||||
expect(keyBuilder.get()).toEqual(["data", "default", "posts", "list"]);
|
||||
});
|
||||
|
||||
it("keys().data().resource(posts).action(list).params({ page: 1 }).get() === [data, default, posts, list, { page: 1 }]", () => {
|
||||
const keyBuilder = keys()
|
||||
.data()
|
||||
.resource("posts")
|
||||
.action("list")
|
||||
.params({ page: 1 });
|
||||
expect(keyBuilder.get()).toEqual([
|
||||
"data",
|
||||
"default",
|
||||
"posts",
|
||||
"list",
|
||||
{ page: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keys().data().resource(posts).action(one).id(1).params({ foo: bar }).get() === [data, default, posts, one, 1, { foo: bar }]", () => {
|
||||
const keyBuilder = keys()
|
||||
.data()
|
||||
.resource("posts")
|
||||
.action("one")
|
||||
.id(1)
|
||||
.params({ foo: "bar" });
|
||||
expect(keyBuilder.get()).toEqual([
|
||||
"data",
|
||||
"default",
|
||||
"posts",
|
||||
"one",
|
||||
"1",
|
||||
{ foo: "bar" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keys().data().mutation(update).params({ foo: bar }).get() === [data, update, { foo: bar }]", () => {
|
||||
const keyBuilder = keys()
|
||||
.data()
|
||||
.mutation("update")
|
||||
.params({ foo: "bar" });
|
||||
expect(keyBuilder.get()).toEqual(["data", "update", { foo: "bar" }]);
|
||||
});
|
||||
|
||||
it("keys().data().mutation(custom).params({ foo: bar }).get() === [data, default, custom, { foo: bar }]", () => {
|
||||
const keyBuilder = keys()
|
||||
.data()
|
||||
.mutation("custom")
|
||||
.params({ foo: "bar" });
|
||||
expect(keyBuilder.get()).toEqual([
|
||||
"data",
|
||||
"default",
|
||||
"custom",
|
||||
{ foo: "bar" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("keys().access()", () => {
|
||||
it("keys().access().get() === [access]", () => {
|
||||
const keyBuilder = keys().access();
|
||||
expect(keyBuilder.get()).toEqual(["access"]);
|
||||
});
|
||||
|
||||
it("keys().access().get(true) === [access]", () => {
|
||||
const keyBuilder = keys().access();
|
||||
expect(keyBuilder.get(true)).toEqual(["access"]);
|
||||
});
|
||||
|
||||
it("keys().access().get() === [access]", () => {
|
||||
const keyBuilder = keys().access();
|
||||
expect(keyBuilder.get()).toEqual(["access"]);
|
||||
});
|
||||
|
||||
it("keys().access().resource(users).get() === [access, users]", () => {
|
||||
const keyBuilder = keys().access().resource("users");
|
||||
expect(keyBuilder.get()).toEqual(["access", "users"]);
|
||||
});
|
||||
|
||||
it("keys().access().resource(users).action(create).get() === [access, users, create]", () => {
|
||||
const keyBuilder = keys().access().resource("users").action("create");
|
||||
expect(keyBuilder.get()).toEqual(["access", "users", "create"]);
|
||||
});
|
||||
|
||||
it("keys().access().resource(users).action(create).params({ foo: bar }).get() === [access, users, create, { foo: bar }]", () => {
|
||||
const keyBuilder = keys()
|
||||
.access()
|
||||
.resource("users")
|
||||
.action("create")
|
||||
.params({ foo: "bar" });
|
||||
expect(keyBuilder.get()).toEqual([
|
||||
"access",
|
||||
"users",
|
||||
"create",
|
||||
{ foo: "bar" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("keys().audit()", () => {
|
||||
it("keys().audit().get() === [audit]", () => {
|
||||
const keyBuilder = keys().audit();
|
||||
expect(keyBuilder.get()).toEqual(["audit"]);
|
||||
});
|
||||
|
||||
it("keys().audit().get(true) === [audit]", () => {
|
||||
const keyBuilder = keys().audit();
|
||||
expect(keyBuilder.get(true)).toEqual(["audit"]);
|
||||
});
|
||||
|
||||
it("keys().audit().get() === [audit]", () => {
|
||||
const keyBuilder = keys().audit();
|
||||
expect(keyBuilder.get()).toEqual(["audit"]);
|
||||
});
|
||||
|
||||
it("keys().audit().action(rename).get() === [audit, rename]", () => {
|
||||
const keyBuilder = keys().audit().action("rename");
|
||||
expect(keyBuilder.get()).toEqual(["audit", "rename"]);
|
||||
});
|
||||
|
||||
it("keys().audit().resource(posts).action(list).params({ foo: bar }).get() === [audit, posts, list, { foo: bar }]", () => {
|
||||
const keyBuilder = keys()
|
||||
.audit()
|
||||
.resource("posts")
|
||||
.action("list")
|
||||
.params({ foo: "bar" });
|
||||
expect(keyBuilder.get()).toEqual([
|
||||
"audit",
|
||||
"posts",
|
||||
"list",
|
||||
{ foo: "bar" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("array-replace", () => {
|
||||
it("arrayReplace([1, 2, 3, 4, 5], [2, 3], [6, 7]) === [1, 6, 7, 4, 5]", () => {
|
||||
expect(arrayReplace([1, 2, 3, 4, 5], [2, 3], [6, 7])).toEqual([
|
||||
1, 6, 7, 4, 5,
|
||||
]);
|
||||
});
|
||||
|
||||
it("arrayReplace([1, 2, 3, 4, 5], [2, 3], [6, 7, 8]) === [1, 6, 7, 8, 4, 5]", () => {
|
||||
expect(arrayReplace([1, 2, 3, 4, 5], [2, 3], [6, 7, 8])).toEqual([
|
||||
1, 6, 7, 8, 4, 5,
|
||||
]);
|
||||
});
|
||||
|
||||
it("arrayReplace([1, 2, 3, 4, 5], [2, 3], [6]) === [1, 6, 4, 5]", () => {
|
||||
expect(arrayReplace([1, 2, 3, 4, 5], [2, 3], [6])).toEqual([1, 6, 4, 5]);
|
||||
});
|
||||
|
||||
it("arrayReplace([1, 2, 3, 4, 5], [2, 3], [6, 7, 8, 9]) === [1, 6, 7, 8, 9, 4, 5]", () => {
|
||||
expect(arrayReplace([1, 2, 3, 4, 5], [2, 3], [6, 7, 8, 9])).toEqual([
|
||||
1, 6, 7, 8, 9, 4, 5,
|
||||
]);
|
||||
});
|
||||
|
||||
it("arrayReplace([1, 2, 3, 4, 5], [2, 3], [6, 7, 8, 9, 10]) === [1, 6, 7, 8, 9, 10, 4, 5]", () => {
|
||||
expect(arrayReplace([1, 2, 3, 4, 5], [2, 3], [6, 7, 8, 9, 10])).toEqual([
|
||||
1, 6, 7, 8, 9, 10, 4, 5,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("array-find-index", () => {
|
||||
it("arrayFindIndex([1, 2, 3, 4, 5], [2, 3]) === 1", () => {
|
||||
expect(arrayFindIndex([1, 2, 3, 4, 5], [2, 3])).toEqual(1);
|
||||
});
|
||||
|
||||
it("arrayFindIndex([1, 2, 3, 4, 5], [2, 3, 4]) === 1", () => {
|
||||
expect(arrayFindIndex([1, 2, 3, 4, 5], [2, 3, 4])).toEqual(1);
|
||||
});
|
||||
|
||||
it("arrayFindIndex([1, 2, 3, 4, 5], [ 3, 4, 5]) === 2", () => {
|
||||
expect(arrayFindIndex([1, 2, 3, 4, 5], [3, 4, 5])).toEqual(2);
|
||||
});
|
||||
|
||||
it("arrayFindIndex([1, 2, 3, 4, 5], [2, 3, 4, 5, 6]) === -1", () => {
|
||||
expect(arrayFindIndex([1, 2, 3, 4, 5], [2, 3, 4, 5, 6])).toEqual(-1);
|
||||
});
|
||||
|
||||
it("arrayFindIndex([1, 2, 3, 4, 5], [4, 3]) === -1", () => {
|
||||
expect(arrayFindIndex([1, 2, 3, 4, 5], [4, 3])).toEqual(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("strip-undefined", () => {
|
||||
it("stripUndefined([1, undefined, 2, undefined, 3]) === [1, 2, 3]", () => {
|
||||
expect(stripUndefined([1, undefined, 2, undefined, 3])).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Legacy keys are identical with `queryKeys`", () => {
|
||||
describe("queryKeys(posts, undefined, { foo: bar }) matches with all properties", () => {
|
||||
const legacyQueryKeys = queryKeys("posts", undefined, { foo: "bar" });
|
||||
|
||||
it("all === keys().data()", () => {
|
||||
expect(keys().data().get(true)).toEqual(legacyQueryKeys.all);
|
||||
});
|
||||
|
||||
it("resourceAll === keys().data().resource(posts)", () => {
|
||||
expect(keys().data().resource("posts").get(true)).toEqual(
|
||||
legacyQueryKeys.resourceAll,
|
||||
);
|
||||
});
|
||||
|
||||
it("logList === keys().audit().resource(posts).action(list).params({ foo: bar })", () => {
|
||||
expect(
|
||||
keys()
|
||||
.audit()
|
||||
.resource("posts")
|
||||
.action("list")
|
||||
.params({ foo: "bar" })
|
||||
.get(true),
|
||||
).toEqual(legacyQueryKeys.logList({ foo: "bar" }));
|
||||
});
|
||||
|
||||
it("detail(1) === keys().data().resource(posts).action(one).id(1).params({ foo: bar })", () => {
|
||||
expect(
|
||||
keys()
|
||||
.data()
|
||||
.resource("posts")
|
||||
.action("one")
|
||||
.id(1)
|
||||
.params({ foo: "bar" })
|
||||
.get(true),
|
||||
).toEqual(legacyQueryKeys.detail(1));
|
||||
});
|
||||
|
||||
it("many([1, 2, 3]) === keys().data().resource(posts).action(many).ids(1,2,3).params({ foo: bar })", () => {
|
||||
expect(
|
||||
keys()
|
||||
.data()
|
||||
.resource("posts")
|
||||
.action("many")
|
||||
.ids(1, 2, 3)
|
||||
.params({ foo: "bar" })
|
||||
.get(true),
|
||||
).toEqual(legacyQueryKeys.many([1, 2, 3]));
|
||||
});
|
||||
|
||||
it("list({ pagination: { current: 1, pageSize: 5 }) === keys().data().resource(posts).action(list).params({ pagination: { current: 1, pageSize: 5 }, foo: bar })", () => {
|
||||
expect(
|
||||
keys()
|
||||
.data()
|
||||
.resource("posts")
|
||||
.action("list")
|
||||
.params({
|
||||
pagination: { current: 1, pageSize: 5 },
|
||||
foo: "bar",
|
||||
})
|
||||
.get(true),
|
||||
).toEqual(
|
||||
legacyQueryKeys.list({
|
||||
pagination: { current: 1, pageSize: 5 },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("logList matches the legacy", () => {
|
||||
const metaData = {};
|
||||
const meta = { foo: "bar" };
|
||||
const resource = "posts";
|
||||
|
||||
const legacy = queryKeys(resource, undefined, metaData).logList(meta);
|
||||
|
||||
const newKey = keys()
|
||||
.audit()
|
||||
.resource(resource)
|
||||
.action("list")
|
||||
.params(meta);
|
||||
|
||||
expect(newKey.get(true)).toEqual(legacy);
|
||||
});
|
||||
|
||||
it("useCustom matches the legacy key", () => {
|
||||
const dataProviderName = "default";
|
||||
const method = "GET";
|
||||
const url = "/posts";
|
||||
const preferredMeta = { foo: "bar" };
|
||||
const config = { enabled: true };
|
||||
|
||||
const legacyKey = [
|
||||
dataProviderName,
|
||||
"custom",
|
||||
method,
|
||||
url,
|
||||
{ ...config, ...(preferredMeta || {}) },
|
||||
];
|
||||
|
||||
const newKey = keys()
|
||||
.data(dataProviderName)
|
||||
.mutation("custom")
|
||||
.params({
|
||||
method,
|
||||
url,
|
||||
...config,
|
||||
...(preferredMeta || {}),
|
||||
});
|
||||
|
||||
expect(newKey.get(true)).toEqual(legacyKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Legacy keys are matching auth hooks", () => {
|
||||
it("keys().auth().action(login).params({ username: 'test' }).get(true) === [useLogin]", () => {
|
||||
expect(
|
||||
keys().auth().action("login").params({ username: "test" }).get(true),
|
||||
).toEqual(["useLogin"]);
|
||||
});
|
||||
it("keys().auth().action(check).params({ foo: bar }) === [useAuthenticated, { foo: bar }]", () => {
|
||||
expect(
|
||||
keys().auth().action("check").params({ foo: "bar" }).get(true),
|
||||
).toEqual(["useAuthenticated", { foo: "bar" }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Legacy keys are matching access hooks", () => {
|
||||
it("keys().access().resource().action(list).params({ params: { foo: bar }, enabled: true }) === [useCan, { resource: undefined, action: list, params: { foo: bar }, enabled: true } }]", () => {
|
||||
expect(
|
||||
keys()
|
||||
.access()
|
||||
.resource()
|
||||
.action("list")
|
||||
.params({ params: { foo: "bar" }, enabled: true })
|
||||
.get(true),
|
||||
).toEqual([
|
||||
"useCan",
|
||||
{
|
||||
resource: undefined,
|
||||
action: "list",
|
||||
params: { foo: "bar" },
|
||||
enabled: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("generates the same key for legacy", () => {
|
||||
const action = "list";
|
||||
const resource = "posts";
|
||||
const paramsRest = { foo: "bar" };
|
||||
const queryOptions = { enabled: true };
|
||||
const sanitizedResource = { name: "posts" };
|
||||
|
||||
const legacyKey = [
|
||||
"useCan",
|
||||
{
|
||||
action,
|
||||
resource,
|
||||
params: { ...paramsRest, resource: sanitizedResource },
|
||||
enabled: queryOptions?.enabled,
|
||||
},
|
||||
];
|
||||
|
||||
const newKey = keys()
|
||||
.access()
|
||||
.resource(resource)
|
||||
.action(action)
|
||||
.params({
|
||||
params: { ...paramsRest, resource: sanitizedResource },
|
||||
enabled: queryOptions?.enabled,
|
||||
})
|
||||
.legacy();
|
||||
|
||||
expect(newKey).toEqual(legacyKey);
|
||||
});
|
||||
});
|
||||
283
packages/core/src/definitions/helpers/keys/index.ts
Normal file
283
packages/core/src/definitions/helpers/keys/index.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
import type { BaseKey } from "../../../contexts/data/types";
|
||||
|
||||
type ParametrizedDataActions = "list" | "infinite";
|
||||
type IdRequiredDataActions = "one";
|
||||
type IdsRequiredDataActions = "many";
|
||||
type DataMutationActions =
|
||||
| "custom"
|
||||
| "customMutation"
|
||||
| "create"
|
||||
| "createMany"
|
||||
| "update"
|
||||
| "updateMany"
|
||||
| "delete"
|
||||
| "deleteMany";
|
||||
|
||||
type AuthActionType =
|
||||
| "login"
|
||||
| "logout"
|
||||
| "identity"
|
||||
| "register"
|
||||
| "forgotPassword"
|
||||
| "check"
|
||||
| "onError"
|
||||
| "permissions"
|
||||
| "updatePassword";
|
||||
|
||||
type AuditActionType = "list" | "log" | "rename";
|
||||
|
||||
type IdType = BaseKey;
|
||||
type IdsType = IdType[];
|
||||
|
||||
type ParamsType = any;
|
||||
|
||||
type KeySegment = string | IdType | IdsType | ParamsType;
|
||||
|
||||
export function arrayFindIndex<T>(array: T[], slice: T[]): number {
|
||||
return array.findIndex(
|
||||
(item, index) =>
|
||||
index <= array.length - slice.length &&
|
||||
slice.every(
|
||||
(sliceItem, sliceIndex) => array[index + sliceIndex] === sliceItem,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function arrayReplace<T>(
|
||||
array: T[],
|
||||
partToBeReplaced: T[],
|
||||
newPart: T[],
|
||||
): T[] {
|
||||
const newArray: T[] = [...array];
|
||||
const startIndex = arrayFindIndex(array, partToBeReplaced);
|
||||
|
||||
if (startIndex !== -1) {
|
||||
newArray.splice(startIndex, partToBeReplaced.length, ...newPart);
|
||||
}
|
||||
|
||||
return newArray;
|
||||
}
|
||||
|
||||
export function stripUndefined(segments: KeySegment[]) {
|
||||
return segments.filter((segment) => segment !== undefined);
|
||||
}
|
||||
|
||||
function convertToLegacy(segments: KeySegment[]) {
|
||||
// for `list`, `many` and `one`
|
||||
if (segments[0] === "data") {
|
||||
// [data, dpName, resource, action, ...];
|
||||
const newSegments = segments.slice(1);
|
||||
|
||||
if (newSegments[2] === "many") {
|
||||
newSegments[2] = "getMany";
|
||||
} else if (newSegments[2] === "infinite") {
|
||||
newSegments[2] = "list";
|
||||
} else if (newSegments[2] === "one") {
|
||||
newSegments[2] = "detail";
|
||||
} else if (newSegments[1] === "custom") {
|
||||
const newParams = {
|
||||
...newSegments[2],
|
||||
};
|
||||
delete newParams.method;
|
||||
delete newParams.url;
|
||||
|
||||
return [
|
||||
newSegments[0],
|
||||
newSegments[1],
|
||||
newSegments[2].method,
|
||||
newSegments[2].url,
|
||||
newParams,
|
||||
];
|
||||
}
|
||||
|
||||
return newSegments;
|
||||
}
|
||||
// for `audit` -> `logList`
|
||||
if (segments[0] === "audit") {
|
||||
// [audit, resource, action, params] (for log and list)
|
||||
// or
|
||||
// [audit, action, params] (for rename)
|
||||
if (segments[2] === "list") {
|
||||
return ["logList", segments[1], segments[3]];
|
||||
}
|
||||
}
|
||||
// for `access` -> `useCan`
|
||||
if (segments[0] === "access") {
|
||||
// [access, resource, action, params]
|
||||
if (segments.length === 4) {
|
||||
return [
|
||||
"useCan",
|
||||
{
|
||||
resource: segments[1],
|
||||
action: segments[2],
|
||||
...segments[3], // params: { params, enabled }
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
// for `auth`
|
||||
if (segments[0] === "auth") {
|
||||
if (arrayFindIndex(segments, ["auth", "login"]) !== -1) {
|
||||
return ["useLogin"];
|
||||
}
|
||||
if (arrayFindIndex(segments, ["auth", "logout"]) !== -1) {
|
||||
return ["useLogout"];
|
||||
}
|
||||
if (arrayFindIndex(segments, ["auth", "identity"]) !== -1) {
|
||||
return ["getUserIdentity"];
|
||||
}
|
||||
if (arrayFindIndex(segments, ["auth", "register"]) !== -1) {
|
||||
return ["useRegister"];
|
||||
}
|
||||
if (arrayFindIndex(segments, ["auth", "forgotPassword"]) !== -1) {
|
||||
return ["useForgotPassword"];
|
||||
}
|
||||
if (arrayFindIndex(segments, ["auth", "check"]) !== -1) {
|
||||
return ["useAuthenticated", segments[2]]; // [auth, check, params]
|
||||
}
|
||||
if (arrayFindIndex(segments, ["auth", "onError"]) !== -1) {
|
||||
return ["useCheckError"];
|
||||
}
|
||||
if (arrayFindIndex(segments, ["auth", "permissions"]) !== -1) {
|
||||
return ["usePermissions"];
|
||||
}
|
||||
if (arrayFindIndex(segments, ["auth", "updatePassword"]) !== -1) {
|
||||
return ["useUpdatePassword"];
|
||||
}
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
class BaseKeyBuilder {
|
||||
segments: KeySegment[] = [];
|
||||
|
||||
constructor(segments: KeySegment[] = []) {
|
||||
this.segments = segments;
|
||||
}
|
||||
|
||||
key() {
|
||||
return this.segments;
|
||||
}
|
||||
|
||||
legacy() {
|
||||
return convertToLegacy(this.segments);
|
||||
}
|
||||
|
||||
get(legacy?: boolean) {
|
||||
return legacy ? this.legacy() : this.segments;
|
||||
}
|
||||
}
|
||||
|
||||
class ParamsKeyBuilder extends BaseKeyBuilder {
|
||||
params(paramsValue?: ParamsType) {
|
||||
return new BaseKeyBuilder([...this.segments, paramsValue]);
|
||||
}
|
||||
}
|
||||
|
||||
class DataIdRequiringKeyBuilder extends BaseKeyBuilder {
|
||||
id(idValue?: IdType) {
|
||||
return new ParamsKeyBuilder([
|
||||
...this.segments,
|
||||
idValue ? String(idValue) : undefined,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class DataIdsRequiringKeyBuilder extends BaseKeyBuilder {
|
||||
ids(...idsValue: IdsType) {
|
||||
return new ParamsKeyBuilder([
|
||||
...this.segments,
|
||||
...(idsValue.length ? [idsValue.map((el) => String(el))] : []),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class DataResourceKeyBuilder extends BaseKeyBuilder {
|
||||
action(actionType: ParametrizedDataActions): ParamsKeyBuilder;
|
||||
action(actionType: IdRequiredDataActions): DataIdRequiringKeyBuilder;
|
||||
action(actionType: IdsRequiredDataActions): DataIdsRequiringKeyBuilder;
|
||||
action(
|
||||
actionType:
|
||||
| ParametrizedDataActions
|
||||
| IdRequiredDataActions
|
||||
| IdsRequiredDataActions,
|
||||
): ParamsKeyBuilder | DataIdRequiringKeyBuilder | DataIdsRequiringKeyBuilder {
|
||||
if (actionType === "one") {
|
||||
return new DataIdRequiringKeyBuilder([...this.segments, actionType]);
|
||||
}
|
||||
if (actionType === "many") {
|
||||
return new DataIdsRequiringKeyBuilder([...this.segments, actionType]);
|
||||
}
|
||||
if (["list", "infinite"].includes(actionType)) {
|
||||
return new ParamsKeyBuilder([...this.segments, actionType]);
|
||||
}
|
||||
throw new Error("Invalid action type");
|
||||
}
|
||||
}
|
||||
|
||||
class DataKeyBuilder extends BaseKeyBuilder {
|
||||
resource(resourceName?: string) {
|
||||
return new DataResourceKeyBuilder([...this.segments, resourceName]);
|
||||
}
|
||||
|
||||
mutation(mutationName: DataMutationActions) {
|
||||
return new ParamsKeyBuilder([
|
||||
...(mutationName === "custom" ? this.segments : [this.segments[0]]),
|
||||
mutationName,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthKeyBuilder extends BaseKeyBuilder {
|
||||
action(actionType: AuthActionType) {
|
||||
return new ParamsKeyBuilder([...this.segments, actionType]);
|
||||
}
|
||||
}
|
||||
|
||||
class AccessResourceKeyBuilder extends BaseKeyBuilder {
|
||||
action(resourceName: string) {
|
||||
return new ParamsKeyBuilder([...this.segments, resourceName]);
|
||||
}
|
||||
}
|
||||
|
||||
class AccessKeyBuilder extends BaseKeyBuilder {
|
||||
resource(resourceName?: string) {
|
||||
return new AccessResourceKeyBuilder([...this.segments, resourceName]);
|
||||
}
|
||||
}
|
||||
|
||||
class AuditActionKeyBuilder extends BaseKeyBuilder {
|
||||
action(actionType: Extract<AuditActionType, "list">) {
|
||||
return new ParamsKeyBuilder([...this.segments, actionType]);
|
||||
}
|
||||
}
|
||||
|
||||
class AuditKeyBuilder extends BaseKeyBuilder {
|
||||
resource(resourceName?: string) {
|
||||
return new AuditActionKeyBuilder([...this.segments, resourceName]);
|
||||
}
|
||||
|
||||
action(actionType: Extract<AuditActionType, "rename" | "log">) {
|
||||
return new ParamsKeyBuilder([...this.segments, actionType]);
|
||||
}
|
||||
}
|
||||
|
||||
export class KeyBuilder extends BaseKeyBuilder {
|
||||
data(name?: string) {
|
||||
return new DataKeyBuilder(["data", name || "default"]);
|
||||
}
|
||||
|
||||
auth() {
|
||||
return new AuthKeyBuilder(["auth"]);
|
||||
}
|
||||
|
||||
access() {
|
||||
return new AccessKeyBuilder(["access"]);
|
||||
}
|
||||
|
||||
audit() {
|
||||
return new AuditKeyBuilder(["audit"]);
|
||||
}
|
||||
}
|
||||
|
||||
export const keys = () => new KeyBuilder([]);
|
||||
@@ -0,0 +1,43 @@
|
||||
import { legacyResourceTransform } from ".";
|
||||
|
||||
describe("legacyResourceTransform", () => {
|
||||
it("should return the legacy resource", () => {
|
||||
expect(
|
||||
legacyResourceTransform([
|
||||
{
|
||||
name: "posts",
|
||||
meta: {
|
||||
label: "Custom Post Label",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "categories",
|
||||
options: {
|
||||
label: "Custom Category Label",
|
||||
},
|
||||
},
|
||||
]),
|
||||
).toEqual([
|
||||
{
|
||||
name: "posts",
|
||||
meta: { label: "Custom Post Label" },
|
||||
label: "Custom Post Label",
|
||||
route: "/posts",
|
||||
canCreate: false,
|
||||
canEdit: false,
|
||||
canShow: false,
|
||||
canDelete: undefined,
|
||||
},
|
||||
{
|
||||
name: "categories",
|
||||
options: { label: "Custom Category Label" },
|
||||
label: "Custom Category Label",
|
||||
route: "/categories",
|
||||
canCreate: false,
|
||||
canEdit: false,
|
||||
canShow: false,
|
||||
canDelete: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import type {
|
||||
IResourceItem,
|
||||
ResourceProps,
|
||||
} from "../../../contexts/resource/types";
|
||||
import { routeGenerator } from "../routeGenerator";
|
||||
|
||||
/**
|
||||
* For the legacy definition of resources, we did a basic transformation for provided resources
|
||||
* - This is meant to provide an easier way to access properties.
|
||||
* - In the new definition, we don't need to do transformations and properties can be accessed via helpers or manually.
|
||||
* This is kept for backward compability
|
||||
*/
|
||||
export const legacyResourceTransform = (resources: ResourceProps[]) => {
|
||||
const _resources: IResourceItem[] = [];
|
||||
|
||||
resources.forEach((resource) => {
|
||||
_resources.push({
|
||||
...resource,
|
||||
label: resource.meta?.label ?? resource.options?.label,
|
||||
route: routeGenerator(resource, resources),
|
||||
canCreate: !!resource.create,
|
||||
canEdit: !!resource.edit,
|
||||
canShow: !!resource.show,
|
||||
canDelete: resource.canDelete,
|
||||
});
|
||||
});
|
||||
|
||||
return _resources;
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { createResourceKey } from "../create-resource-key";
|
||||
|
||||
describe("createResourceKey", () => {
|
||||
it("should return only name when no parents exist", () => {
|
||||
expect(createResourceKey({ name: "posts" }, [{ name: "posts" }])).toBe(
|
||||
"/posts",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the key with parent name", () => {
|
||||
expect(
|
||||
createResourceKey({ name: "posts", meta: { parent: "cms" } }, [
|
||||
{ name: "posts" },
|
||||
]),
|
||||
).toBe("/cms/posts");
|
||||
});
|
||||
|
||||
it("should return the key with multiple parents", () => {
|
||||
expect(
|
||||
createResourceKey({ name: "posts", meta: { parent: "orgs" } }, [
|
||||
{ name: "orgs", meta: { parent: "cms" } },
|
||||
{ name: "cms" },
|
||||
]),
|
||||
).toBe("/cms/orgs/posts");
|
||||
});
|
||||
|
||||
it("should return the key with identifier", () => {
|
||||
expect(
|
||||
createResourceKey({ name: "posts", identifier: "foo" }, [
|
||||
{ name: "posts", identifier: "foo" },
|
||||
]),
|
||||
).toBe("/foo");
|
||||
});
|
||||
|
||||
it("should return custom route with legacy", () => {
|
||||
expect(
|
||||
createResourceKey(
|
||||
{
|
||||
name: "posts",
|
||||
meta: { parent: "orgs" },
|
||||
route: "custom-route",
|
||||
},
|
||||
[{ name: "orgs", meta: { parent: "cms" } }, { name: "cms" }],
|
||||
true,
|
||||
),
|
||||
).toBe("/cms/orgs/custom-route");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
import { createTree } from "../create-tree";
|
||||
|
||||
describe("createTree", () => {
|
||||
it("should return an empty array if no resources are provided", () => {
|
||||
expect(createTree([])).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return a tree with a single resource", () => {
|
||||
const resources = [
|
||||
{
|
||||
name: "posts",
|
||||
},
|
||||
];
|
||||
|
||||
expect(createTree(resources)).toEqual([
|
||||
{
|
||||
name: "posts",
|
||||
key: "/posts",
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return two items", () => {
|
||||
const resources = [
|
||||
{
|
||||
name: "posts",
|
||||
},
|
||||
{
|
||||
name: "comments",
|
||||
},
|
||||
];
|
||||
|
||||
expect(createTree(resources)).toEqual([
|
||||
{
|
||||
name: "posts",
|
||||
key: "/posts",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
name: "comments",
|
||||
key: "/comments",
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return nested items", () => {
|
||||
const resources = [
|
||||
{
|
||||
name: "posts",
|
||||
},
|
||||
{
|
||||
name: "categories",
|
||||
},
|
||||
{
|
||||
name: "comments",
|
||||
meta: { parent: "posts" },
|
||||
},
|
||||
];
|
||||
|
||||
expect(createTree(resources)).toEqual([
|
||||
{
|
||||
name: "posts",
|
||||
key: "/posts",
|
||||
children: [
|
||||
{
|
||||
name: "comments",
|
||||
key: "/posts/comments",
|
||||
meta: { parent: "posts" },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "categories",
|
||||
key: "/categories",
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return double nested items", () => {
|
||||
const resources = [
|
||||
{
|
||||
name: "posts",
|
||||
meta: { parent: "cms" },
|
||||
},
|
||||
{
|
||||
name: "categories",
|
||||
},
|
||||
{
|
||||
name: "comments",
|
||||
meta: { parent: "posts" },
|
||||
},
|
||||
];
|
||||
|
||||
expect(createTree(resources)).toEqual([
|
||||
{
|
||||
name: "cms",
|
||||
key: "/cms",
|
||||
children: [
|
||||
{
|
||||
name: "posts",
|
||||
key: "/cms/posts",
|
||||
meta: { parent: "cms" },
|
||||
children: [
|
||||
{
|
||||
name: "comments",
|
||||
key: "/cms/posts/comments",
|
||||
meta: { parent: "posts" },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "categories",
|
||||
key: "/categories",
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should define a key based on the identifier", () => {
|
||||
const resources = [
|
||||
{
|
||||
name: "posts",
|
||||
},
|
||||
{
|
||||
name: "posts",
|
||||
identifier: "recent-posts",
|
||||
meta: { parent: "posts" },
|
||||
},
|
||||
{
|
||||
name: "posts",
|
||||
identifier: "featured-posts",
|
||||
meta: { parent: "posts" },
|
||||
},
|
||||
];
|
||||
|
||||
expect(createTree(resources)).toEqual([
|
||||
{
|
||||
name: "posts",
|
||||
key: "/posts",
|
||||
children: [
|
||||
{
|
||||
name: "posts",
|
||||
key: "/posts/recent-posts",
|
||||
identifier: "recent-posts",
|
||||
meta: { parent: "posts" },
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
name: "posts",
|
||||
key: "/posts/featured-posts",
|
||||
identifier: "featured-posts",
|
||||
meta: { parent: "posts" },
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { IResourceItem } from "../../../contexts/resource/types";
|
||||
import {
|
||||
getParentResource,
|
||||
removeLeadingTrailingSlashes,
|
||||
} from "../../helpers/router";
|
||||
|
||||
export const createResourceKey = (
|
||||
resource: IResourceItem,
|
||||
resources: IResourceItem[],
|
||||
legacy = false,
|
||||
) => {
|
||||
const parents: IResourceItem[] = [];
|
||||
|
||||
let currentParentResource = getParentResource(resource, resources);
|
||||
while (currentParentResource) {
|
||||
parents.push(currentParentResource);
|
||||
currentParentResource = getParentResource(currentParentResource, resources);
|
||||
}
|
||||
parents.reverse();
|
||||
|
||||
const key = [...parents, resource]
|
||||
.map((r) =>
|
||||
removeLeadingTrailingSlashes(
|
||||
(legacy ? r.route : undefined) ?? r.identifier ?? r.name,
|
||||
),
|
||||
)
|
||||
.join("/");
|
||||
|
||||
return `/${key.replace(/^\//, "")}`;
|
||||
};
|
||||
85
packages/core/src/definitions/helpers/menu/create-tree.ts
Normal file
85
packages/core/src/definitions/helpers/menu/create-tree.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { IResourceItem } from "../../../contexts/resource/types";
|
||||
import { getParentResource } from "../router";
|
||||
import { createResourceKey } from "./create-resource-key";
|
||||
|
||||
export type Tree = {
|
||||
item: IResourceItem;
|
||||
children: { [key: string]: Tree };
|
||||
};
|
||||
|
||||
export type FlatTreeItem = IResourceItem & {
|
||||
key: string;
|
||||
children: FlatTreeItem[];
|
||||
};
|
||||
|
||||
export const createTree = (
|
||||
resources: IResourceItem[],
|
||||
legacy = false,
|
||||
): FlatTreeItem[] => {
|
||||
const root: Tree = {
|
||||
item: {
|
||||
name: "__root__",
|
||||
},
|
||||
children: {},
|
||||
};
|
||||
|
||||
resources.forEach((resource) => {
|
||||
const parents: IResourceItem[] = [];
|
||||
|
||||
let currentParent = getParentResource(resource, resources);
|
||||
while (currentParent) {
|
||||
parents.push(currentParent);
|
||||
currentParent = getParentResource(currentParent, resources);
|
||||
}
|
||||
parents.reverse();
|
||||
|
||||
let currentTree = root;
|
||||
|
||||
parents.forEach((parent) => {
|
||||
const key =
|
||||
(legacy ? parent.route : undefined) ?? parent.identifier ?? parent.name;
|
||||
|
||||
if (!currentTree.children[key]) {
|
||||
currentTree.children[key] = {
|
||||
item: parent,
|
||||
children: {},
|
||||
};
|
||||
}
|
||||
currentTree = currentTree.children[key];
|
||||
});
|
||||
|
||||
const key =
|
||||
(legacy ? resource.route : undefined) ??
|
||||
resource.identifier ??
|
||||
resource.name;
|
||||
|
||||
if (!currentTree.children[key]) {
|
||||
currentTree.children[key] = {
|
||||
item: resource,
|
||||
children: {},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const flatten = (tree: Tree): FlatTreeItem[] => {
|
||||
const items: FlatTreeItem[] = [];
|
||||
|
||||
Object.keys(tree.children).forEach((key) => {
|
||||
const itemKey = createResourceKey(
|
||||
tree.children[key].item,
|
||||
resources,
|
||||
legacy,
|
||||
);
|
||||
const item: FlatTreeItem = {
|
||||
...tree.children[key].item,
|
||||
key: itemKey,
|
||||
children: flatten(tree.children[key]),
|
||||
};
|
||||
items.push(item);
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
return flatten(root);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import { pickResource } from ".";
|
||||
import { legacyResourceTransform } from "../legacy-resource-transform";
|
||||
|
||||
describe("pickResource", () => {
|
||||
it("should pick by name", () => {
|
||||
const resource = pickResource("name", [
|
||||
{ name: "name", options: { route: "route" } },
|
||||
]);
|
||||
|
||||
expect(resource?.name).toBe("name");
|
||||
});
|
||||
|
||||
it("should match by identifier", () => {
|
||||
const resource = pickResource("identifier", [
|
||||
{
|
||||
name: "name",
|
||||
identifier: "identifier",
|
||||
options: { route: "route" },
|
||||
},
|
||||
]);
|
||||
|
||||
expect(resource?.identifier).toBe("identifier");
|
||||
});
|
||||
|
||||
it("should not match by identifier if legacy", () => {
|
||||
const resource = pickResource(
|
||||
"identifier",
|
||||
[
|
||||
{
|
||||
name: "name",
|
||||
identifier: "identifier",
|
||||
options: { route: "route" },
|
||||
},
|
||||
],
|
||||
true,
|
||||
);
|
||||
|
||||
expect(resource).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return undefined if name and identifier does not match", () => {
|
||||
const resource = pickResource("users", [
|
||||
{ name: "name", identifier: "identifier" },
|
||||
]);
|
||||
|
||||
expect(resource).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should match by route first if legacy", () => {
|
||||
const resource = pickResource(
|
||||
"route",
|
||||
legacyResourceTransform([
|
||||
{ name: "name", options: { route: "route" } },
|
||||
{
|
||||
name: "route",
|
||||
},
|
||||
]),
|
||||
true,
|
||||
);
|
||||
|
||||
expect(resource?.name).toBe("name");
|
||||
});
|
||||
|
||||
it("should return undefined if `identifier` is not defined", () => {
|
||||
const resource = pickResource(undefined, [{ name: "name" }], true);
|
||||
|
||||
expect(resource).toBeUndefined();
|
||||
});
|
||||
});
|
||||
41
packages/core/src/definitions/helpers/pick-resource/index.ts
Normal file
41
packages/core/src/definitions/helpers/pick-resource/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { IResourceItem } from "../../../contexts/resource/types";
|
||||
import { removeLeadingTrailingSlashes } from "../router/remove-leading-trailing-slashes";
|
||||
|
||||
/**
|
||||
* Picks the resource based on the provided identifier.
|
||||
* Identifier fallbacks to `name` if `identifier` is not explicitly provided to the resource.
|
||||
* If legacy is true, then resource is matched by `route` first and then by `name`.
|
||||
*/
|
||||
export const pickResource = (
|
||||
identifier?: string,
|
||||
resources: IResourceItem[] = [],
|
||||
/**
|
||||
* If true, the identifier will be checked for `route` and `name` properties
|
||||
*/
|
||||
legacy = false,
|
||||
): IResourceItem | undefined => {
|
||||
if (!identifier) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (legacy) {
|
||||
const resourceByRoute = resources.find(
|
||||
(r) =>
|
||||
removeLeadingTrailingSlashes(r.route ?? "") ===
|
||||
removeLeadingTrailingSlashes(identifier),
|
||||
);
|
||||
|
||||
const resource = resourceByRoute
|
||||
? resourceByRoute
|
||||
: resources.find((r) => r.name === identifier);
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
let resource = resources.find((r) => r.identifier === identifier);
|
||||
if (!resource) {
|
||||
resource = resources.find((r) => r.name === identifier);
|
||||
}
|
||||
|
||||
return resource;
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { pickDataProvider } from ".";
|
||||
import type { IResourceItem } from "../../../contexts/resource/types";
|
||||
|
||||
describe("pickDataProvider", () => {
|
||||
it("should return the dataProvider from the params", () => {
|
||||
expect(pickDataProvider("resourceName", "custom-provider", [])).toBe(
|
||||
"custom-provider",
|
||||
);
|
||||
});
|
||||
it("should return default if resource is not found in resources and no dataprovidername is provided", () => {
|
||||
expect(pickDataProvider("resourceName", undefined, [])).toBe("default");
|
||||
});
|
||||
it("should return resource's dataprovidername", () => {
|
||||
const resources: IResourceItem[] = [
|
||||
{
|
||||
name: "resourceName",
|
||||
meta: {
|
||||
dataProviderName: "custom-provider",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(pickDataProvider("resourceName", undefined, resources)).toBe(
|
||||
"custom-provider",
|
||||
);
|
||||
});
|
||||
it("should return dataprovidername from params even if resource matches", () => {
|
||||
const resources: IResourceItem[] = [
|
||||
{
|
||||
name: "resourceName",
|
||||
meta: {
|
||||
dataProviderName: "custom-provider",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
pickDataProvider("resourceName", "other-custom-provider", resources),
|
||||
).toBe("other-custom-provider");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { IResourceItem } from "../../../contexts/resource/types";
|
||||
import { pickResource } from "../pick-resource";
|
||||
import { pickNotDeprecated } from "../pickNotDeprecated";
|
||||
|
||||
/**
|
||||
* Picks the data provider name based on the provided name or fallbacks to resource definition, or `default`.
|
||||
*/
|
||||
export const pickDataProvider = (
|
||||
resourceName?: string,
|
||||
dataProviderName?: string,
|
||||
resources?: IResourceItem[],
|
||||
) => {
|
||||
if (dataProviderName) {
|
||||
return dataProviderName;
|
||||
}
|
||||
|
||||
/**
|
||||
* In this helper, we don't do `route` based matching therefore there's no need to check for `legacy` behaviors.
|
||||
*/
|
||||
const resource = pickResource(resourceName, resources);
|
||||
|
||||
const meta = pickNotDeprecated(resource?.meta, resource?.options);
|
||||
|
||||
if (meta?.dataProviderName) {
|
||||
return meta.dataProviderName;
|
||||
}
|
||||
|
||||
return "default";
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { pickNotDeprecated } from ".";
|
||||
|
||||
describe("pickNotDeprecated", () => {
|
||||
test("should returns a first value that is not undefined", () => {
|
||||
const testCases = [
|
||||
{
|
||||
args: [undefined, 1, undefined],
|
||||
expected: 1,
|
||||
},
|
||||
{
|
||||
args: [undefined, 0, true],
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
args: [false, {}],
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
args: [{ id: 1 }, undefined],
|
||||
expected: { id: 1 },
|
||||
},
|
||||
];
|
||||
|
||||
testCases.forEach(({ args, expected }) => {
|
||||
expect(pickNotDeprecated(...args)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Returns first value that is not undefined.
|
||||
* @internal This is an internal helper function. Please do not use externally.
|
||||
*/
|
||||
export const pickNotDeprecated = <T extends unknown[]>(
|
||||
...args: T
|
||||
): T[never] => {
|
||||
return args.find((arg) => typeof arg !== "undefined");
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { QueryFunctionContext, QueryKey } from "@tanstack/react-query";
|
||||
|
||||
export const prepareQueryContext = (
|
||||
context: QueryFunctionContext<QueryKey, any>,
|
||||
): Omit<QueryFunctionContext<QueryKey, any>, "meta"> => {
|
||||
const queryContext = {
|
||||
queryKey: context.queryKey,
|
||||
pageParam: context.pageParam,
|
||||
};
|
||||
|
||||
Object.defineProperty(queryContext, "signal", {
|
||||
enumerable: true,
|
||||
get: () => {
|
||||
return context.signal;
|
||||
},
|
||||
});
|
||||
|
||||
return queryContext;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { propertyPathToArray } from ".";
|
||||
|
||||
describe("propertyPathToArray", () => {
|
||||
it("should return an array of strings when given a string of dot-separated property names", () => {
|
||||
const propertyPath = "foo.bar.baz";
|
||||
const result = propertyPathToArray(propertyPath);
|
||||
expect(result).toEqual(["foo", "bar", "baz"]);
|
||||
});
|
||||
|
||||
it("should return an array of numbers and strings when given a string of dot-separated property names that include numbers", () => {
|
||||
const propertyPath = "foo.1.bar.2.baz";
|
||||
const result = propertyPathToArray(propertyPath);
|
||||
expect(result).toEqual(["foo", 1, "bar", 2, "baz"]);
|
||||
});
|
||||
|
||||
it("should return an array with a single element when given a string with no dots", () => {
|
||||
const propertyPath = "foo";
|
||||
const result = propertyPathToArray(propertyPath);
|
||||
expect(result).toEqual(["foo"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export const propertyPathToArray = (propertyPath: string) => {
|
||||
return propertyPath
|
||||
.split(".")
|
||||
.map((item) => (!Number.isNaN(Number(item)) ? Number(item) : item));
|
||||
};
|
||||
584
packages/core/src/definitions/helpers/queryKeys/index.spec.ts
Normal file
584
packages/core/src/definitions/helpers/queryKeys/index.spec.ts
Normal file
@@ -0,0 +1,584 @@
|
||||
import { queryKeys, queryKeysReplacement } from ".";
|
||||
|
||||
describe("queryKeys", () => {
|
||||
describe("all", () => {
|
||||
it("should return default data-provider", () => {
|
||||
expect(queryKeys().all).toEqual(["default"]);
|
||||
});
|
||||
it("should return custom data-provider", () => {
|
||||
expect(queryKeys(undefined, "custom-data-provider").all).toEqual([
|
||||
"custom-data-provider",
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe("resourceAll", () => {
|
||||
it("should return without data provider", () => {
|
||||
expect(queryKeys("post").resourceAll).toEqual(["default", "post"]);
|
||||
});
|
||||
it("should return with data provider", () => {
|
||||
expect(queryKeys("post", "custom-data-provider").resourceAll).toEqual([
|
||||
"custom-data-provider",
|
||||
"post",
|
||||
]);
|
||||
});
|
||||
it("should return without resource", () => {
|
||||
expect(queryKeys(undefined, "custom-data-provider").resourceAll).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe("list", () => {
|
||||
it("should return without data provider", () => {
|
||||
expect(queryKeys("post").list()).toEqual(["default", "post", "list", {}]);
|
||||
});
|
||||
it("should return with data provider", () => {
|
||||
expect(queryKeys("post", "custom-data-provider").list()).toEqual([
|
||||
"custom-data-provider",
|
||||
"post",
|
||||
"list",
|
||||
{},
|
||||
]);
|
||||
});
|
||||
it("should return without resource", () => {
|
||||
expect(queryKeys(undefined, "custom-data-provider").list()).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"list",
|
||||
{},
|
||||
]);
|
||||
});
|
||||
it("should return with config", () => {
|
||||
expect(
|
||||
queryKeys(undefined, "custom-data-provider").list({
|
||||
hasPagination: false,
|
||||
}),
|
||||
).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"list",
|
||||
{
|
||||
hasPagination: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("should return with config and meta", () => {
|
||||
expect(
|
||||
queryKeys(undefined, "custom-data-provider", {
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
}).list({
|
||||
hasPagination: false,
|
||||
}),
|
||||
).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"list",
|
||||
{
|
||||
hasPagination: false,
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("should return with config and metaData", () => {
|
||||
expect(
|
||||
queryKeys(undefined, "custom-data-provider", undefined, {
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
}).list({
|
||||
hasPagination: false,
|
||||
}),
|
||||
).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"list",
|
||||
{
|
||||
hasPagination: false,
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe("many", () => {
|
||||
it("should return without data provider", () => {
|
||||
expect(queryKeys("post").many()).toEqual([
|
||||
"default",
|
||||
"post",
|
||||
"getMany",
|
||||
{},
|
||||
]);
|
||||
});
|
||||
it("should return with data provider", () => {
|
||||
expect(queryKeys("post", "custom-data-provider").many()).toEqual([
|
||||
"custom-data-provider",
|
||||
"post",
|
||||
"getMany",
|
||||
{},
|
||||
]);
|
||||
});
|
||||
it("should return without resource", () => {
|
||||
expect(queryKeys(undefined, "custom-data-provider").many()).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"getMany",
|
||||
{},
|
||||
]);
|
||||
});
|
||||
it("should return with ids", () => {
|
||||
expect(
|
||||
queryKeys(undefined, "custom-data-provider").many([1, 2, 3]),
|
||||
).toEqual(["custom-data-provider", "", "getMany", ["1", "2", "3"], {}]);
|
||||
});
|
||||
it("should return with ids and meta", () => {
|
||||
expect(
|
||||
queryKeys(undefined, "custom-data-provider", {
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
}).many([1, 2, 3]),
|
||||
).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"getMany",
|
||||
["1", "2", "3"],
|
||||
{
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("should return with ids and metaData", () => {
|
||||
expect(
|
||||
queryKeys(undefined, "custom-data-provider", undefined, {
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
}).many([1, 2, 3]),
|
||||
).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"getMany",
|
||||
["1", "2", "3"],
|
||||
{
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe("detail", () => {
|
||||
it("should return without data provider", () => {
|
||||
expect(queryKeys("post").detail(1)).toEqual([
|
||||
"default",
|
||||
"post",
|
||||
"detail",
|
||||
"1",
|
||||
{},
|
||||
]);
|
||||
});
|
||||
it("should return with data provider", () => {
|
||||
expect(queryKeys("post", "custom-data-provider").detail(1)).toEqual([
|
||||
"custom-data-provider",
|
||||
"post",
|
||||
"detail",
|
||||
"1",
|
||||
{},
|
||||
]);
|
||||
});
|
||||
it("should return without resource", () => {
|
||||
expect(queryKeys(undefined, "custom-data-provider").detail(1)).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"detail",
|
||||
"1",
|
||||
{},
|
||||
]);
|
||||
});
|
||||
it("should return without id", () => {
|
||||
expect(queryKeys(undefined, "custom-data-provider").detail()).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"detail",
|
||||
undefined,
|
||||
{},
|
||||
]);
|
||||
});
|
||||
it("should return with ids", () => {
|
||||
expect(queryKeys(undefined, "custom-data-provider").detail(1)).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"detail",
|
||||
"1",
|
||||
{},
|
||||
]);
|
||||
});
|
||||
it("should return with ids and meta", () => {
|
||||
expect(
|
||||
queryKeys(undefined, "custom-data-provider", {
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
}).detail(1),
|
||||
).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"detail",
|
||||
"1",
|
||||
{
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("should return with ids and metaData", () => {
|
||||
expect(
|
||||
queryKeys(undefined, "custom-data-provider", undefined, {
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
}).detail(1),
|
||||
).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"detail",
|
||||
"1",
|
||||
{
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe("logList", () => {
|
||||
it("should return with resource", () => {
|
||||
expect(queryKeys("post").logList()).toEqual(["logList", "post"]);
|
||||
});
|
||||
it("should return with meta", () => {
|
||||
expect(
|
||||
queryKeys("post").logList({
|
||||
foo: "bar",
|
||||
}),
|
||||
).toEqual([
|
||||
"logList",
|
||||
"post",
|
||||
{
|
||||
foo: "bar",
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("should return with metaData", () => {
|
||||
expect(
|
||||
queryKeys("post", undefined, undefined, {
|
||||
foo: "bar",
|
||||
}).logList(),
|
||||
).toEqual([
|
||||
"logList",
|
||||
"post",
|
||||
{
|
||||
foo: "bar",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("queryKeysReplacement should match queryKeys", () => {
|
||||
describe("all", () => {
|
||||
it("should return default data-provider", () => {
|
||||
expect(queryKeysReplacement(true)().all).toEqual(["default"]);
|
||||
});
|
||||
it("should return custom data-provider", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)(undefined, "custom-data-provider").all,
|
||||
).toEqual(["custom-data-provider"]);
|
||||
});
|
||||
});
|
||||
describe("resourceAll", () => {
|
||||
it("should return without data provider", () => {
|
||||
expect(queryKeysReplacement(true)("post").resourceAll).toEqual([
|
||||
"default",
|
||||
"post",
|
||||
]);
|
||||
});
|
||||
it("should return with data provider", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)("post", "custom-data-provider").resourceAll,
|
||||
).toEqual(["custom-data-provider", "post"]);
|
||||
});
|
||||
it("should return without resource", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)(undefined, "custom-data-provider")
|
||||
.resourceAll,
|
||||
).toEqual(["custom-data-provider", ""]);
|
||||
});
|
||||
});
|
||||
describe("list", () => {
|
||||
it("should return without data provider", () => {
|
||||
expect(queryKeysReplacement(true)("post").list()).toEqual([
|
||||
"default",
|
||||
"post",
|
||||
"list",
|
||||
{},
|
||||
]);
|
||||
});
|
||||
it("should return with data provider", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)("post", "custom-data-provider").list(),
|
||||
).toEqual(["custom-data-provider", "post", "list", {}]);
|
||||
});
|
||||
it("should return without resource", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)(undefined, "custom-data-provider").list(),
|
||||
).toEqual(["custom-data-provider", "", "list", {}]);
|
||||
});
|
||||
it("should return with config", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)(undefined, "custom-data-provider").list({
|
||||
hasPagination: false,
|
||||
}),
|
||||
).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"list",
|
||||
{
|
||||
hasPagination: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("should return with config and meta", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)(undefined, "custom-data-provider", {
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
}).list({
|
||||
hasPagination: false,
|
||||
}),
|
||||
).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"list",
|
||||
{
|
||||
hasPagination: false,
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("should return with config and metaData", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)(
|
||||
undefined,
|
||||
"custom-data-provider",
|
||||
undefined,
|
||||
{
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
).list({
|
||||
hasPagination: false,
|
||||
}),
|
||||
).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"list",
|
||||
{
|
||||
hasPagination: false,
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe("many", () => {
|
||||
it("should return without data provider", () => {
|
||||
expect(queryKeysReplacement(true)("post").many()).toEqual([
|
||||
"default",
|
||||
"post",
|
||||
"getMany",
|
||||
{},
|
||||
]);
|
||||
});
|
||||
it("should return with data provider", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)("post", "custom-data-provider").many(),
|
||||
).toEqual(["custom-data-provider", "post", "getMany", {}]);
|
||||
});
|
||||
it("should return without resource", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)(undefined, "custom-data-provider").many(),
|
||||
).toEqual(["custom-data-provider", "", "getMany", {}]);
|
||||
});
|
||||
it("should return with ids", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)(undefined, "custom-data-provider").many([
|
||||
1, 2, 3,
|
||||
]),
|
||||
).toEqual(["custom-data-provider", "", "getMany", ["1", "2", "3"], {}]);
|
||||
});
|
||||
it("should return with ids and meta", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)(undefined, "custom-data-provider", {
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
}).many([1, 2, 3]),
|
||||
).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"getMany",
|
||||
["1", "2", "3"],
|
||||
{
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("should return with ids and metaData", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)(
|
||||
undefined,
|
||||
"custom-data-provider",
|
||||
undefined,
|
||||
{
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
).many([1, 2, 3]),
|
||||
).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"getMany",
|
||||
["1", "2", "3"],
|
||||
{
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe("detail", () => {
|
||||
it("should return without data provider", () => {
|
||||
expect(queryKeysReplacement(true)("post").detail(1)).toEqual([
|
||||
"default",
|
||||
"post",
|
||||
"detail",
|
||||
"1",
|
||||
{},
|
||||
]);
|
||||
});
|
||||
it("should return with data provider", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)("post", "custom-data-provider").detail(1),
|
||||
).toEqual(["custom-data-provider", "post", "detail", "1", {}]);
|
||||
});
|
||||
it("should return without resource", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)(undefined, "custom-data-provider").detail(1),
|
||||
).toEqual(["custom-data-provider", "", "detail", "1", {}]);
|
||||
});
|
||||
it("should return without id", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)(undefined, "custom-data-provider").detail(),
|
||||
).toEqual(["custom-data-provider", "", "detail", undefined, {}]);
|
||||
});
|
||||
it("should return with ids", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)(undefined, "custom-data-provider").detail(1),
|
||||
).toEqual(["custom-data-provider", "", "detail", "1", {}]);
|
||||
});
|
||||
it("should return with ids and meta", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)(undefined, "custom-data-provider", {
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
}).detail(1),
|
||||
).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"detail",
|
||||
"1",
|
||||
{
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("should return with ids and metaData", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)(
|
||||
undefined,
|
||||
"custom-data-provider",
|
||||
undefined,
|
||||
{
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
).detail(1),
|
||||
).toEqual([
|
||||
"custom-data-provider",
|
||||
"",
|
||||
"detail",
|
||||
"1",
|
||||
{
|
||||
meta: {
|
||||
foo: "bar",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe("logList", () => {
|
||||
it("should return with resource", () => {
|
||||
expect(queryKeysReplacement(true)("post").logList()).toEqual([
|
||||
"logList",
|
||||
"post",
|
||||
]);
|
||||
});
|
||||
it("should return with meta", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)("post").logList({
|
||||
foo: "bar",
|
||||
}),
|
||||
).toEqual([
|
||||
"logList",
|
||||
"post",
|
||||
{
|
||||
foo: "bar",
|
||||
},
|
||||
]);
|
||||
});
|
||||
it("should return with metaData", () => {
|
||||
expect(
|
||||
queryKeysReplacement(true)("post", undefined, undefined, {
|
||||
foo: "bar",
|
||||
}).logList(),
|
||||
).toEqual([
|
||||
"logList",
|
||||
"post",
|
||||
{
|
||||
foo: "bar",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
114
packages/core/src/definitions/helpers/queryKeys/index.ts
Normal file
114
packages/core/src/definitions/helpers/queryKeys/index.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import type { QueryKey } from "@tanstack/react-query";
|
||||
|
||||
import type { IQueryKeys, MetaQuery } from "../../../contexts/data/types";
|
||||
import { keys as newKeys } from "../keys";
|
||||
import { pickNotDeprecated } from "../pickNotDeprecated";
|
||||
|
||||
/**
|
||||
* @deprecated `queryKeys` is deprecated. Please use `keys` instead.
|
||||
*/
|
||||
export const queryKeys = (
|
||||
resource?: string,
|
||||
dataProviderName?: string,
|
||||
meta?: MetaQuery,
|
||||
/**
|
||||
* @deprecated `metaData` is deprecated with refine@4, refine will pass `meta` instead, however, we still support `metaData` for backward compatibility.
|
||||
*/
|
||||
metaData?: MetaQuery | undefined,
|
||||
): IQueryKeys => {
|
||||
const providerName = dataProviderName || "default";
|
||||
const keys: IQueryKeys = {
|
||||
all: [providerName],
|
||||
resourceAll: [providerName, resource || ""],
|
||||
list: (config) => [
|
||||
...keys.resourceAll,
|
||||
"list",
|
||||
{
|
||||
...config,
|
||||
...(pickNotDeprecated(meta, metaData) || {}),
|
||||
} as QueryKey,
|
||||
],
|
||||
many: (ids) =>
|
||||
[
|
||||
...keys.resourceAll,
|
||||
"getMany",
|
||||
ids?.map(String) as QueryKey,
|
||||
{ ...(pickNotDeprecated(meta, metaData) || {}) } as QueryKey,
|
||||
].filter((item) => item !== undefined),
|
||||
detail: (id) => [
|
||||
...keys.resourceAll,
|
||||
"detail",
|
||||
id?.toString(),
|
||||
{ ...(pickNotDeprecated(meta, metaData) || {}) } as QueryKey,
|
||||
],
|
||||
logList: (meta) =>
|
||||
["logList", resource, meta as any, metaData as QueryKey].filter(
|
||||
(item) => item !== undefined,
|
||||
),
|
||||
};
|
||||
return keys;
|
||||
};
|
||||
|
||||
export const queryKeysReplacement = (preferLegacyKeys?: boolean) => {
|
||||
return (
|
||||
resource?: string,
|
||||
dataProviderName?: string,
|
||||
meta?: MetaQuery,
|
||||
/**
|
||||
* @deprecated `metaData` is deprecated with refine@4, refine will pass `meta` instead, however, we still support `metaData` for backward compatibility.
|
||||
*/
|
||||
metaData?: MetaQuery | undefined,
|
||||
): IQueryKeys => {
|
||||
const providerName = dataProviderName || "default";
|
||||
|
||||
const keys: IQueryKeys = {
|
||||
all: newKeys().data(providerName).get(preferLegacyKeys),
|
||||
resourceAll: newKeys()
|
||||
.data(dataProviderName)
|
||||
.resource(resource ?? "")
|
||||
.get(preferLegacyKeys),
|
||||
list: (config) =>
|
||||
newKeys()
|
||||
.data(dataProviderName)
|
||||
.resource(resource ?? "")
|
||||
.action("list")
|
||||
.params({
|
||||
...config,
|
||||
...(pickNotDeprecated(meta, metaData) || {}),
|
||||
})
|
||||
.get(preferLegacyKeys),
|
||||
many: (ids) =>
|
||||
newKeys()
|
||||
.data(dataProviderName)
|
||||
.resource(resource ?? "")
|
||||
.action("many")
|
||||
.ids(...(ids ?? []))
|
||||
.params({
|
||||
...(pickNotDeprecated(meta, metaData) || {}),
|
||||
})
|
||||
.get(preferLegacyKeys),
|
||||
detail: (id) =>
|
||||
newKeys()
|
||||
.data(dataProviderName)
|
||||
.resource(resource ?? "")
|
||||
.action("one")
|
||||
.id(id ?? "")
|
||||
.params({
|
||||
...(pickNotDeprecated(meta, metaData) || {}),
|
||||
})
|
||||
.get(preferLegacyKeys),
|
||||
logList: (meta) =>
|
||||
[
|
||||
...newKeys()
|
||||
.audit()
|
||||
.resource(resource)
|
||||
.action("list")
|
||||
.params(meta)
|
||||
.get(preferLegacyKeys),
|
||||
metaData as QueryKey,
|
||||
].filter((item) => item !== undefined),
|
||||
};
|
||||
|
||||
return keys;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { redirectPage } from ".";
|
||||
import type { IRefineContextOptions } from "../../../contexts/refine/types";
|
||||
import type { RedirectAction } from "../../../hooks/form/types";
|
||||
|
||||
describe("redirectPath", () => {
|
||||
it.each(["edit", "list", "show", false] as RedirectAction[])(
|
||||
"should return redirectFromProps if it is provided %s",
|
||||
(redirectFromProps) => {
|
||||
const action = "create";
|
||||
const redirectOptions: IRefineContextOptions["redirect"] = {
|
||||
afterClone: "edit",
|
||||
afterCreate: "list",
|
||||
afterEdit: "show",
|
||||
};
|
||||
|
||||
const result = redirectPage({
|
||||
redirectFromProps,
|
||||
action,
|
||||
redirectOptions,
|
||||
});
|
||||
expect(result).toEqual(redirectFromProps);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["edit", "create", "clone", "list"] as const)(
|
||||
"should return redirect option according to action %s",
|
||||
(action) => {
|
||||
const redirectOptions: IRefineContextOptions["redirect"] = {
|
||||
afterClone: "edit",
|
||||
afterCreate: "list",
|
||||
afterEdit: "show",
|
||||
};
|
||||
|
||||
const result = redirectPage({
|
||||
action,
|
||||
redirectOptions,
|
||||
});
|
||||
|
||||
switch (action) {
|
||||
case "clone":
|
||||
expect(result).toEqual(redirectOptions.afterClone);
|
||||
break;
|
||||
case "create":
|
||||
expect(result).toEqual(redirectOptions.afterCreate);
|
||||
break;
|
||||
case "edit":
|
||||
expect(result).toEqual(redirectOptions.afterEdit);
|
||||
break;
|
||||
case "list":
|
||||
expect(result).toBeFalsy();
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
30
packages/core/src/definitions/helpers/redirectPage/index.ts
Normal file
30
packages/core/src/definitions/helpers/redirectPage/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { IRefineContextOptions } from "../../../contexts/refine/types";
|
||||
import type { Action } from "../../../contexts/router/types";
|
||||
import type { RedirectAction } from "../../../hooks/form/types";
|
||||
|
||||
type RedirectPageProps = {
|
||||
redirectFromProps?: RedirectAction;
|
||||
action: Action;
|
||||
redirectOptions: IRefineContextOptions["redirect"];
|
||||
};
|
||||
|
||||
export const redirectPage = ({
|
||||
redirectFromProps,
|
||||
action,
|
||||
redirectOptions,
|
||||
}: RedirectPageProps): RedirectAction => {
|
||||
if (redirectFromProps || redirectFromProps === false) {
|
||||
return redirectFromProps;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "clone":
|
||||
return redirectOptions.afterClone;
|
||||
case "create":
|
||||
return redirectOptions.afterCreate;
|
||||
case "edit":
|
||||
return redirectOptions.afterEdit;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { routeGenerator } from ".";
|
||||
import type { ResourceProps } from "../../../contexts/resource/types";
|
||||
|
||||
const mockResources: ResourceProps[] = [
|
||||
{ name: "posts" },
|
||||
{
|
||||
name: "category",
|
||||
},
|
||||
{
|
||||
name: "active",
|
||||
parentName: "posts",
|
||||
},
|
||||
{
|
||||
name: "first",
|
||||
parentName: "active",
|
||||
},
|
||||
];
|
||||
|
||||
const mockItemResourcePropsWithoutParent: ResourceProps = {
|
||||
name: "category",
|
||||
};
|
||||
|
||||
const mockItemResourcePropsWithParent: ResourceProps = {
|
||||
name: "active",
|
||||
parentName: "posts",
|
||||
};
|
||||
|
||||
const mockItemResourcePropsWithTwoParent: ResourceProps = {
|
||||
name: "first",
|
||||
parentName: "active",
|
||||
};
|
||||
|
||||
describe("routeGenerator", () => {
|
||||
it("should return category route", async () => {
|
||||
const route = routeGenerator(
|
||||
mockItemResourcePropsWithoutParent,
|
||||
mockResources,
|
||||
);
|
||||
expect(route).toEqual("/category");
|
||||
});
|
||||
|
||||
it("should equal return with parent route", async () => {
|
||||
const route = routeGenerator(
|
||||
mockItemResourcePropsWithParent,
|
||||
mockResources,
|
||||
);
|
||||
expect(route).toEqual("/posts/active");
|
||||
});
|
||||
|
||||
it("should return exect route", async () => {
|
||||
const route = routeGenerator(
|
||||
mockItemResourcePropsWithTwoParent,
|
||||
mockResources,
|
||||
);
|
||||
expect(route).toEqual("/posts/active/first");
|
||||
});
|
||||
|
||||
it("should return exect route with meta.route", async () => {
|
||||
const route = routeGenerator(
|
||||
{
|
||||
...mockItemResourcePropsWithTwoParent,
|
||||
meta: {
|
||||
route: "foo",
|
||||
},
|
||||
},
|
||||
mockResources,
|
||||
);
|
||||
expect(route).toEqual("/posts/active/foo");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { ResourceProps } from "../../../contexts/resource/types";
|
||||
import { pickNotDeprecated } from "../pickNotDeprecated";
|
||||
import { getParentPrefixForResource } from "../router";
|
||||
|
||||
/**
|
||||
* generates route for the resource based on parents and custom routes
|
||||
* @deprecated this is a **legacy** function and works only with the old resource definition
|
||||
*/
|
||||
export const routeGenerator = (
|
||||
item: ResourceProps,
|
||||
resourcesFromProps: ResourceProps[],
|
||||
): string | undefined => {
|
||||
let route;
|
||||
|
||||
const parentPrefix = getParentPrefixForResource(
|
||||
item,
|
||||
resourcesFromProps,
|
||||
true,
|
||||
);
|
||||
|
||||
if (parentPrefix) {
|
||||
const meta = pickNotDeprecated(item.meta, item.options);
|
||||
route = `${parentPrefix}/${meta?.route ?? item.name}`;
|
||||
} else {
|
||||
route = item.options?.route ?? item.name;
|
||||
}
|
||||
|
||||
return `/${route.replace(/^\//, "")}`;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { checkBySegments } from "../check-by-segments";
|
||||
|
||||
describe("checkBySegments", () => {
|
||||
it("should return true if the route and resourceRoute match by segments", () => {
|
||||
const result = checkBySegments("/users/edit/123", "/users/edit/:id");
|
||||
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false if the route and resourceRoute don't match by segments", () => {
|
||||
const result = checkBySegments("/users/edit/123", "/posts/edit/:id/");
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return false if segments are not equal", () => {
|
||||
const result = checkBySegments("/users/edit/123", "/users/edit/:id/step");
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { composeRoute } from "../compose-route";
|
||||
|
||||
describe("composeRoute", () => {
|
||||
it("should compose single slash", () => {
|
||||
const result = composeRoute("/");
|
||||
|
||||
expect(result).toEqual("/");
|
||||
});
|
||||
|
||||
it("should return the route as same if no params are found", () => {
|
||||
const result = composeRoute("/users", { id: 1 });
|
||||
|
||||
expect(result).toEqual("/users");
|
||||
});
|
||||
|
||||
it("should return the route with params replaced", () => {
|
||||
const result = composeRoute("/users/:id", { id: 1 });
|
||||
|
||||
expect(result).toEqual("/users/1");
|
||||
});
|
||||
|
||||
it("should return the route with multiple params replaced", () => {
|
||||
const result = composeRoute(
|
||||
"/users/:id/:name",
|
||||
{ name: "Jane" },
|
||||
{ id: 5 },
|
||||
{
|
||||
id: 1,
|
||||
name: "John",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual("/users/1/John");
|
||||
});
|
||||
|
||||
it("should return the route with multiple params by prioritizing meta params", () => {
|
||||
const result = composeRoute(
|
||||
"/users/:id/:name",
|
||||
{},
|
||||
{ id: 1 },
|
||||
{ name: "Doe" },
|
||||
);
|
||||
|
||||
expect(result).toEqual("/users/1/Doe");
|
||||
});
|
||||
|
||||
it("should return route when not match object", () => {
|
||||
const result = composeRoute("/users/:other", {});
|
||||
|
||||
expect(result).toEqual("/users/:other");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
import { getActionRoutesFromResource } from "../get-action-routes-from-resource";
|
||||
|
||||
describe("getActionRoutesFromResource", () => {
|
||||
it("should return empty array if no actions are found", () => {
|
||||
const result = getActionRoutesFromResource(
|
||||
{
|
||||
name: "users",
|
||||
meta: {},
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return the default routes for a given resource", () => {
|
||||
const result = getActionRoutesFromResource(
|
||||
{
|
||||
name: "users",
|
||||
meta: {},
|
||||
list: () => null,
|
||||
create: () => null,
|
||||
edit: () => null,
|
||||
show: () => null,
|
||||
clone: () => null,
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
action: "list",
|
||||
route: "/users",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
action: "create",
|
||||
route: "/users/create",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
action: "edit",
|
||||
route: "/users/edit/:id",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
action: "show",
|
||||
route: "/users/show/:id",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
action: "clone",
|
||||
route: "/users/clone/:id",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the default routes for a given resource with parent prefix [legacy]", () => {
|
||||
const result = getActionRoutesFromResource(
|
||||
{
|
||||
name: "users",
|
||||
meta: {
|
||||
parent: "orgs",
|
||||
},
|
||||
edit: () => null,
|
||||
},
|
||||
[],
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
action: "edit",
|
||||
route: "/orgs/users/edit/:id",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the default routes for a given resource without parent prefix", () => {
|
||||
const result = getActionRoutesFromResource(
|
||||
{
|
||||
name: "users",
|
||||
meta: {
|
||||
parent: "orgs",
|
||||
},
|
||||
edit: () => null,
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
action: "edit",
|
||||
route: "/users/edit/:id",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not include parent prefix if route is explicitly defined", () => {
|
||||
const result = getActionRoutesFromResource(
|
||||
{
|
||||
name: "users",
|
||||
meta: {
|
||||
parent: "orgs",
|
||||
},
|
||||
edit: {
|
||||
path: "edit/:id",
|
||||
component: () => null,
|
||||
},
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
action: "edit",
|
||||
route: "/edit/:id",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should use deprecated route prop if legacy is set to true", () => {
|
||||
const result = getActionRoutesFromResource(
|
||||
{
|
||||
name: "users",
|
||||
parentName: "orgs",
|
||||
options: {
|
||||
route: "custom-users",
|
||||
},
|
||||
list: () => null,
|
||||
},
|
||||
[],
|
||||
true,
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
action: "list",
|
||||
route: "/orgs/custom-users",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should use the specific route instead of the default one with no component", () => {
|
||||
const result = getActionRoutesFromResource(
|
||||
{
|
||||
name: "users",
|
||||
meta: {},
|
||||
list: "/super-cool-nesting/users/list",
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
action: "list",
|
||||
route: "/super-cool-nesting/users/list",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { getDefaultActionPath } from "../get-default-action-path";
|
||||
|
||||
describe("getDefaultActionPath", () => {
|
||||
it("should return the default path for a given action and resource", () => {
|
||||
expect(getDefaultActionPath("users", "list")).toBe("/users");
|
||||
expect(getDefaultActionPath("users", "create")).toBe("/users/create");
|
||||
expect(getDefaultActionPath("users", "edit")).toBe("/users/edit/:id");
|
||||
expect(getDefaultActionPath("users", "show")).toBe("/users/show/:id");
|
||||
expect(getDefaultActionPath("users", "clone")).toBe("/users/clone/:id");
|
||||
});
|
||||
|
||||
it("should return the default path for a given action and resource with parent prefix", () => {
|
||||
expect(getDefaultActionPath("users", "list", "/orgs")).toBe("/orgs/users");
|
||||
expect(getDefaultActionPath("users", "create", "/orgs")).toBe(
|
||||
"/orgs/users/create",
|
||||
);
|
||||
expect(getDefaultActionPath("users", "edit", "/orgs")).toBe(
|
||||
"/orgs/users/edit/:id",
|
||||
);
|
||||
expect(getDefaultActionPath("users", "show", "/orgs")).toBe(
|
||||
"/orgs/users/show/:id",
|
||||
);
|
||||
expect(getDefaultActionPath("users", "clone", "/orgs")).toBe(
|
||||
"/orgs/users/clone/:id",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the default path for a given action and resource with multiple parent prefixes", () => {
|
||||
expect(getDefaultActionPath("users", "list", "/orgs/posts")).toBe(
|
||||
"/orgs/posts/users",
|
||||
);
|
||||
expect(getDefaultActionPath("users", "create", "/orgs/posts")).toBe(
|
||||
"/orgs/posts/users/create",
|
||||
);
|
||||
expect(getDefaultActionPath("users", "edit", "/orgs/posts")).toBe(
|
||||
"/orgs/posts/users/edit/:id",
|
||||
);
|
||||
expect(getDefaultActionPath("users", "show", "/orgs/posts")).toBe(
|
||||
"/orgs/posts/users/show/:id",
|
||||
);
|
||||
expect(getDefaultActionPath("users", "clone", "/orgs/posts")).toBe(
|
||||
"/orgs/posts/users/clone/:id",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { getParentPrefixForResource } from "../get-parent-prefix-for-resource";
|
||||
|
||||
describe("getParentPrefixForResource", () => {
|
||||
it("should return undefined if no parent is found", () => {
|
||||
const resources = [
|
||||
{
|
||||
name: "users",
|
||||
},
|
||||
];
|
||||
|
||||
expect(getParentPrefixForResource(resources[0], resources)).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should return the parent prefix", () => {
|
||||
const resources = [
|
||||
{
|
||||
name: "users",
|
||||
},
|
||||
{
|
||||
name: "posts",
|
||||
meta: {
|
||||
parent: "users",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(getParentPrefixForResource(resources[1], resources)).toBe("/users");
|
||||
});
|
||||
|
||||
it("should return the parent prefix for deeply nested resources", () => {
|
||||
const resources = [
|
||||
{
|
||||
name: "users",
|
||||
},
|
||||
{
|
||||
name: "posts",
|
||||
meta: {
|
||||
parent: "users",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "comments",
|
||||
meta: {
|
||||
parent: "posts",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(getParentPrefixForResource(resources[2], resources)).toBe(
|
||||
"/users/posts",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the parent prefix for deeply nested resources with legacy routes", () => {
|
||||
const resources = [
|
||||
{
|
||||
name: "users",
|
||||
options: {
|
||||
route: "custom-users",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "posts",
|
||||
meta: {
|
||||
parent: "users",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "comments",
|
||||
meta: {
|
||||
parent: "posts",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(getParentPrefixForResource(resources[2], resources, true)).toBe(
|
||||
"/custom-users/posts",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { getParentResource } from "../get-parent-resource";
|
||||
|
||||
describe("getParentResource", () => {
|
||||
it("should return undefined if no parent is given", () => {
|
||||
const result = getParentResource(
|
||||
{
|
||||
name: "users",
|
||||
meta: {},
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("should return the parent resource if parent is given", () => {
|
||||
const result = getParentResource(
|
||||
{
|
||||
name: "users",
|
||||
meta: {
|
||||
parent: "orgs",
|
||||
},
|
||||
},
|
||||
[
|
||||
{
|
||||
name: "orgs",
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: "orgs",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return the parent resource if parent is given even if the parent is not defined", () => {
|
||||
const result = getParentResource(
|
||||
{
|
||||
name: "users",
|
||||
options: {
|
||||
parent: "orgs",
|
||||
},
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: "orgs",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import { isParameter } from "../is-parameter";
|
||||
|
||||
describe("isParameter", () => {
|
||||
it("should return true for a parameter", () => {
|
||||
expect(isParameter(":id")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for a non-parameter", () => {
|
||||
expect(isParameter("id")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { isSegmentCountsSame } from "../is-segment-counts-same";
|
||||
|
||||
describe("isSegmentCountsSame", () => {
|
||||
it("should return true if the route and resourceRoute have the same number of segments", () => {
|
||||
const result = isSegmentCountsSame("/users/edit/123", "/users/edit/:id");
|
||||
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false if the route and resourceRoute don't have the same number of segments", () => {
|
||||
const result = isSegmentCountsSame(
|
||||
"/users/edit/123",
|
||||
"users/posts/edit/:id/",
|
||||
);
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { matchResourceFromRoute } from "../match-resource-from-route";
|
||||
|
||||
describe("matchResourceFromRoute", () => {
|
||||
it("should return found false if no resource is given", () => {
|
||||
const result = matchResourceFromRoute("/users", []);
|
||||
|
||||
expect(result.found).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return found false if no route is given", () => {
|
||||
const result = matchResourceFromRoute("", [
|
||||
{
|
||||
name: "users",
|
||||
edit: {
|
||||
path: "/users/edit/:id",
|
||||
component: () => null,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result.found).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return found true if route is found", () => {
|
||||
const result = matchResourceFromRoute("/users/edit/123", [
|
||||
{
|
||||
name: "users",
|
||||
edit: {
|
||||
path: "/users/edit/:id",
|
||||
component: () => null,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result.found).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return the best one if multiple routes are found", () => {
|
||||
const result = matchResourceFromRoute("/users/orgs/edit/123", [
|
||||
{
|
||||
name: "users",
|
||||
edit: {
|
||||
path: "/users/:type/edit/:id",
|
||||
component: () => null,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "org-users",
|
||||
edit: {
|
||||
path: "/users/orgs/edit/:id",
|
||||
component: () => null,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(result.found).toEqual(true);
|
||||
expect(result.matchedRoute).toEqual("/users/orgs/edit/:id");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { pickMatchedRoute } from "../pick-matched-route";
|
||||
|
||||
describe("pickMatchedRoute", () => {
|
||||
it("should return the route with no params", () => {
|
||||
const routes = [
|
||||
{
|
||||
route: "/users/edit/123",
|
||||
action: "edit" as const,
|
||||
resource: { name: "users" },
|
||||
},
|
||||
{
|
||||
route: "users/:action/:id/",
|
||||
action: "edit" as const,
|
||||
resource: { name: "users" },
|
||||
},
|
||||
{
|
||||
route: "/users/:action/:id",
|
||||
action: "edit" as const,
|
||||
resource: { name: "users" },
|
||||
},
|
||||
{
|
||||
route: "/users/edit/:id",
|
||||
action: "edit" as const,
|
||||
resource: { name: "users" },
|
||||
},
|
||||
];
|
||||
|
||||
const picked = pickMatchedRoute(routes);
|
||||
|
||||
expect(picked?.route).toEqual("/users/edit/123");
|
||||
});
|
||||
|
||||
it("should return the route with least params", () => {
|
||||
const routes = [
|
||||
{
|
||||
route: "/users/:action/:id",
|
||||
action: "edit" as const,
|
||||
resource: { name: "users" },
|
||||
},
|
||||
{
|
||||
route: "/users/edit/:id",
|
||||
action: "edit" as const,
|
||||
resource: { name: "users" },
|
||||
},
|
||||
];
|
||||
|
||||
const picked = pickMatchedRoute(routes);
|
||||
|
||||
expect(picked?.route).toEqual("/users/edit/:id");
|
||||
});
|
||||
|
||||
it("should return the latest parametrized route", () => {
|
||||
const routes = [
|
||||
{
|
||||
route: "/users/page/:action/:id",
|
||||
action: "edit" as const,
|
||||
resource: { name: "users" },
|
||||
},
|
||||
{
|
||||
route: "/users/page/list/:id/",
|
||||
action: "edit" as const,
|
||||
resource: { name: "users" },
|
||||
},
|
||||
];
|
||||
|
||||
const picked = pickMatchedRoute(routes);
|
||||
|
||||
expect(picked?.route).toEqual("/users/page/list/:id/");
|
||||
});
|
||||
|
||||
it("should return the latest parametrized route with single", () => {
|
||||
const routes = [
|
||||
{
|
||||
route: "/users/:org/list/123",
|
||||
action: "edit" as const,
|
||||
resource: { name: "users" },
|
||||
},
|
||||
{
|
||||
route: "/users/refine/list/:id",
|
||||
action: "edit" as const,
|
||||
resource: { name: "users" },
|
||||
},
|
||||
];
|
||||
|
||||
const picked = pickMatchedRoute(routes);
|
||||
|
||||
expect(picked?.route).toEqual("/users/refine/list/:id");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { pickRouteParams } from "../pick-route-params";
|
||||
|
||||
describe("pickRouteParams", () => {
|
||||
it("should extract route params from a path", () => {
|
||||
expect(pickRouteParams("/users/:id/posts/:postId")).toEqual([
|
||||
"id",
|
||||
"postId",
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return an empty array if no route params are given", () => {
|
||||
expect(pickRouteParams("/users/list")).toEqual([]);
|
||||
});
|
||||
|
||||
it("should extract route params from a path", () => {
|
||||
expect(pickRouteParams("users/:id")).toEqual(["id"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { prepareRouteParams } from "../prepare-route-params";
|
||||
|
||||
describe("prepareRouteParams", () => {
|
||||
it("should return an empty object if no route params are given", () => {
|
||||
expect(prepareRouteParams([])).toEqual({});
|
||||
});
|
||||
|
||||
it("should return `id` property when params array contains `id`", () => {
|
||||
expect(prepareRouteParams(["id"], { id: "1" })).toEqual({ id: "1" });
|
||||
});
|
||||
|
||||
it("should prioritize meta over params", () => {
|
||||
expect(prepareRouteParams(["id"], { id: "2" })).toEqual({
|
||||
id: "2",
|
||||
});
|
||||
});
|
||||
|
||||
it("should combine params and meta", () => {
|
||||
expect(
|
||||
prepareRouteParams(["id", "action"], {
|
||||
...{ id: "1" },
|
||||
...{ action: "2" },
|
||||
}),
|
||||
).toEqual({ id: "1", action: "2" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { removeLeadingTrailingSlashes } from "../remove-leading-trailing-slashes";
|
||||
|
||||
describe("removeLeadingTrailingSlashes", () => {
|
||||
it("should remove leading and trailing slashes", () => {
|
||||
expect(removeLeadingTrailingSlashes("/a/b/c")).toEqual("a/b/c");
|
||||
});
|
||||
|
||||
it("should remove leading and trailing slashes", () => {
|
||||
expect(removeLeadingTrailingSlashes("a/b/c")).toEqual("a/b/c");
|
||||
});
|
||||
|
||||
it("should remove leading and trailing slashes", () => {
|
||||
expect(removeLeadingTrailingSlashes("/a/b/c/")).toEqual("a/b/c");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { splitToSegments } from "../split-to-segments";
|
||||
|
||||
describe("splitToSegments", () => {
|
||||
it("should split a path to segments", () => {
|
||||
expect(splitToSegments("/a/b/c")).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
|
||||
it("should split a path to segments", () => {
|
||||
expect(splitToSegments("a/b/c")).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
|
||||
it("should split a path to segments", () => {
|
||||
expect(splitToSegments("/a/b/c/")).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { isParameter } from "./is-parameter";
|
||||
import { isSegmentCountsSame } from "./is-segment-counts-same";
|
||||
import { removeLeadingTrailingSlashes } from "./remove-leading-trailing-slashes";
|
||||
import { splitToSegments } from "./split-to-segments";
|
||||
|
||||
/**
|
||||
* This function if the route and resourceRoute match by segments.
|
||||
* - First, trailing and leading slashes are removed
|
||||
* - Then, the route and resourceRoute are split to segments and checked if they have the same number of segments
|
||||
* - Then, each segment is checked if it is a parameter or if it matches the resourceRoute segment
|
||||
* - If all segments match, the function returns true, otherwise false
|
||||
*/
|
||||
export const checkBySegments = (route: string, resourceRoute: string) => {
|
||||
const stdRoute = removeLeadingTrailingSlashes(route);
|
||||
const stdResourceRoute = removeLeadingTrailingSlashes(resourceRoute);
|
||||
// we need to check if the route and resourceRoute have the same number of segments
|
||||
// if not, we can't match them
|
||||
if (!isSegmentCountsSame(stdRoute, stdResourceRoute)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const routeSegments = splitToSegments(stdRoute);
|
||||
const resourceRouteSegments = splitToSegments(stdResourceRoute);
|
||||
|
||||
return resourceRouteSegments.every((segment, index) => {
|
||||
return isParameter(segment) || segment === routeSegments[index];
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { MetaQuery } from "../../../contexts/data/types";
|
||||
import type { ParseResponse } from "../../../contexts/router/types";
|
||||
import { pickRouteParams } from "./pick-route-params";
|
||||
import { prepareRouteParams } from "./prepare-route-params";
|
||||
|
||||
/**
|
||||
* This function will compose a route with the given params and meta.
|
||||
* - A route can have parameters like (eg: /users/:id)
|
||||
* - First we pick the route params from the route (eg: [id])
|
||||
* - Then we prepare the route params with the given params and meta (eg: { id: 1 })
|
||||
* - Then we replace the route params with the prepared route params (eg: /users/1)
|
||||
*/
|
||||
export const composeRoute = (
|
||||
designatedRoute: string,
|
||||
resourceMeta: MetaQuery = {},
|
||||
parsed: ParseResponse = {},
|
||||
meta: Record<string, unknown> = {},
|
||||
): string => {
|
||||
// pickRouteParams (from the route)
|
||||
const routeParams = pickRouteParams(designatedRoute);
|
||||
// prepareRouteParams (from route params, params and meta)
|
||||
const preparedRouteParams = prepareRouteParams(routeParams, {
|
||||
...resourceMeta,
|
||||
...(typeof parsed?.id !== "undefined" ? { id: parsed.id } : {}),
|
||||
...(typeof parsed?.action !== "undefined" ? { action: parsed.action } : {}),
|
||||
...(typeof parsed?.resource !== "undefined"
|
||||
? { resource: parsed.resource }
|
||||
: {}),
|
||||
...parsed?.params,
|
||||
...meta,
|
||||
});
|
||||
// replace route params with prepared route params
|
||||
return designatedRoute.replace(/:([^\/]+)/g, (match, key) => {
|
||||
const fromParams = preparedRouteParams[key];
|
||||
if (typeof fromParams !== "undefined") {
|
||||
return `${fromParams}`;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { IResourceItem } from "../../../contexts/resource/types";
|
||||
import type { Action } from "../../../contexts/router/types";
|
||||
import { getDefaultActionPath } from "./get-default-action-path";
|
||||
import { getParentPrefixForResource } from "./get-parent-prefix-for-resource";
|
||||
|
||||
export type ResourceActionRoute = {
|
||||
action: Action;
|
||||
resource: IResourceItem;
|
||||
route: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* This function returns all the routes for available actions for a resource.
|
||||
* - If the action is a function, it means we're fallbacking to default path for the action
|
||||
* - If the action is a string, it means we don't have the component, but we have the route
|
||||
* - If the action is an object, it means we have the component and the route
|
||||
* - It will return an array of objects with the action, the resource and the route
|
||||
*/
|
||||
export const getActionRoutesFromResource = (
|
||||
resource: IResourceItem,
|
||||
resources: IResourceItem[],
|
||||
/**
|
||||
* Uses legacy route if true (`options.route`)
|
||||
*/
|
||||
legacy?: boolean,
|
||||
) => {
|
||||
const actions: ResourceActionRoute[] = [];
|
||||
|
||||
const actionList: Action[] = ["list", "show", "edit", "create", "clone"];
|
||||
|
||||
const parentPrefix = getParentPrefixForResource(resource, resources, legacy);
|
||||
|
||||
actionList.forEach((action) => {
|
||||
const item =
|
||||
legacy && action === "clone" ? resource.create : resource[action];
|
||||
|
||||
let route: string | undefined = undefined;
|
||||
|
||||
if (typeof item === "function" || legacy) {
|
||||
// means we're fallbacking to default path for the action
|
||||
route = getDefaultActionPath(
|
||||
legacy
|
||||
? resource.meta?.route ?? resource.options?.route ?? resource.name
|
||||
: resource.name,
|
||||
action,
|
||||
legacy ? parentPrefix : undefined,
|
||||
);
|
||||
} else if (typeof item === "string") {
|
||||
// means we don't have the component, but we have the route
|
||||
route = item;
|
||||
} else if (typeof item === "object") {
|
||||
// means we have the component and the route
|
||||
route = item.path;
|
||||
}
|
||||
|
||||
if (route) {
|
||||
actions.push({
|
||||
action,
|
||||
resource,
|
||||
route: `/${route.replace(/^\//, "")}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return actions;
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { Action } from "../../../contexts/router/types";
|
||||
import { removeLeadingTrailingSlashes } from "./remove-leading-trailing-slashes";
|
||||
|
||||
/**
|
||||
* This helper function returns the default path for a given action and resource.
|
||||
* It also applies the parentPrefix if provided.
|
||||
* This is used by the legacy router and the new router if the resource doesn't provide a custom path.
|
||||
*/
|
||||
export const getDefaultActionPath = (
|
||||
resourceName: string,
|
||||
action: Action,
|
||||
parentPrefix?: string,
|
||||
): string => {
|
||||
const cleanParentPrefix = removeLeadingTrailingSlashes(parentPrefix || "");
|
||||
|
||||
let path = `${cleanParentPrefix}${
|
||||
cleanParentPrefix ? "/" : ""
|
||||
}${resourceName}`;
|
||||
|
||||
if (action === "list") {
|
||||
path = `${path}`;
|
||||
} else if (action === "create") {
|
||||
path = `${path}/create`;
|
||||
} else if (action === "edit") {
|
||||
path = `${path}/edit/:id`;
|
||||
} else if (action === "show") {
|
||||
path = `${path}/show/:id`;
|
||||
} else if (action === "clone") {
|
||||
path = `${path}/clone/:id`;
|
||||
}
|
||||
|
||||
return `/${path.replace(/^\//, "")}`;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { ResourceProps } from "../../../contexts/resource/types";
|
||||
import { getParentResource } from "./get-parent-resource";
|
||||
import { removeLeadingTrailingSlashes } from "./remove-leading-trailing-slashes";
|
||||
|
||||
/**
|
||||
* Returns the parent prefix for a resource
|
||||
* - If `legacy` is provided, the computation is based on the `route` option of the resource
|
||||
*/
|
||||
export const getParentPrefixForResource = (
|
||||
resource: ResourceProps,
|
||||
resources: ResourceProps[],
|
||||
/**
|
||||
* Uses legacy route if true (`options.route`)
|
||||
*/
|
||||
legacy?: boolean,
|
||||
): string | undefined => {
|
||||
const parents: ResourceProps[] = [];
|
||||
|
||||
let parent = getParentResource(resource, resources);
|
||||
|
||||
while (parent) {
|
||||
parents.push(parent);
|
||||
parent = getParentResource(parent, resources);
|
||||
}
|
||||
|
||||
if (parents.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `/${parents
|
||||
.reverse()
|
||||
.map((parent) => {
|
||||
const v = legacy ? parent.options?.route ?? parent.name : parent.name;
|
||||
return removeLeadingTrailingSlashes(v);
|
||||
})
|
||||
.join("/")}`;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { IResourceItem } from "../../../contexts/resource/types";
|
||||
import { pickNotDeprecated } from "../pickNotDeprecated";
|
||||
|
||||
/**
|
||||
* Returns the parent resource of the given resource.
|
||||
* Works both with the deprecated `parentName` and the new `parent` property.
|
||||
*/
|
||||
export const getParentResource = (
|
||||
resource: IResourceItem,
|
||||
resources: IResourceItem[],
|
||||
): IResourceItem | undefined => {
|
||||
const parentName = pickNotDeprecated(
|
||||
resource.meta?.parent,
|
||||
resource.options?.parent,
|
||||
resource.parentName,
|
||||
);
|
||||
|
||||
if (!parentName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parentResource = resources.find(
|
||||
(resource) => (resource.identifier ?? resource.name) === parentName,
|
||||
);
|
||||
|
||||
/**
|
||||
* If the parent resource is not found, we return a resource object with the name of the parent resource.
|
||||
* Because we still want to have nesting and prefixing for the resource even if the parent is not explicitly defined.
|
||||
*/
|
||||
return parentResource ?? { name: parentName };
|
||||
};
|
||||
10
packages/core/src/definitions/helpers/router/index.ts
Normal file
10
packages/core/src/definitions/helpers/router/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { checkBySegments } from "./check-by-segments";
|
||||
export { getActionRoutesFromResource } from "./get-action-routes-from-resource";
|
||||
export { getDefaultActionPath } from "./get-default-action-path";
|
||||
export { getParentPrefixForResource } from "./get-parent-prefix-for-resource";
|
||||
export { getParentResource } from "./get-parent-resource";
|
||||
export { isParameter } from "./is-parameter";
|
||||
export { isSegmentCountsSame } from "./is-segment-counts-same";
|
||||
export { removeLeadingTrailingSlashes } from "./remove-leading-trailing-slashes";
|
||||
|
||||
export { matchResourceFromRoute } from "./match-resource-from-route";
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Check if a segment is a parameter. (e.g. :id)
|
||||
*/
|
||||
export const isParameter = (segment: string) => {
|
||||
return segment.startsWith(":");
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { splitToSegments } from "./split-to-segments";
|
||||
|
||||
/**
|
||||
* Checks if the both routes have the same number of segments.
|
||||
*/
|
||||
export const isSegmentCountsSame = (route: string, resourceRoute: string) => {
|
||||
const routeSegments = splitToSegments(route);
|
||||
const resourceRouteSegments = splitToSegments(resourceRoute);
|
||||
|
||||
return routeSegments.length === resourceRouteSegments.length;
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { IResourceItem } from "../../../contexts/resource/types";
|
||||
import type { Action } from "../../../contexts/router/types";
|
||||
import { checkBySegments } from "./check-by-segments";
|
||||
import { getActionRoutesFromResource } from "./get-action-routes-from-resource";
|
||||
import { pickMatchedRoute } from "./pick-matched-route";
|
||||
|
||||
/**
|
||||
* Match the resource from the route
|
||||
* - It will calculate all possible routes for resources and their actions
|
||||
* - It will check if the route matches any of the possible routes
|
||||
* - It will return the most eligible resource and action
|
||||
*/
|
||||
export const matchResourceFromRoute = (
|
||||
route: string,
|
||||
resources: IResourceItem[],
|
||||
): {
|
||||
found: boolean;
|
||||
resource?: IResourceItem;
|
||||
action?: Action;
|
||||
matchedRoute?: string;
|
||||
} => {
|
||||
const allActionRoutes = resources.flatMap((resource) => {
|
||||
return getActionRoutesFromResource(resource, resources);
|
||||
});
|
||||
|
||||
const allFound = allActionRoutes.filter((actionRoute) => {
|
||||
return checkBySegments(route, actionRoute.route);
|
||||
});
|
||||
|
||||
const mostEligible = pickMatchedRoute(allFound);
|
||||
|
||||
return {
|
||||
found: !!mostEligible,
|
||||
resource: mostEligible?.resource,
|
||||
action: mostEligible?.action,
|
||||
matchedRoute: mostEligible?.route,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { ResourceActionRoute } from "./get-action-routes-from-resource";
|
||||
import { isParameter } from "./is-parameter";
|
||||
import { removeLeadingTrailingSlashes } from "./remove-leading-trailing-slashes";
|
||||
import { splitToSegments } from "./split-to-segments";
|
||||
|
||||
/**
|
||||
* Picks the most eligible route from the given matched routes.
|
||||
* - If there's only one route, it returns it.
|
||||
* - If there's more than one route, it picks the best non-greedy match.
|
||||
*/
|
||||
export const pickMatchedRoute = (
|
||||
routes: ResourceActionRoute[],
|
||||
): ResourceActionRoute | undefined => {
|
||||
// these routes are all matched, we should pick the least parametrized one
|
||||
|
||||
// no routes, no match
|
||||
if (routes.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// no need to calculate the route segments if there's only one route
|
||||
if (routes.length === 1) {
|
||||
return routes[0];
|
||||
}
|
||||
|
||||
// remove trailing and leading slashes
|
||||
// split them to segments
|
||||
const sanitizedRoutes = routes.map((route) => ({
|
||||
...route,
|
||||
splitted: splitToSegments(removeLeadingTrailingSlashes(route.route)),
|
||||
}));
|
||||
|
||||
// at this point, before calling this function, we already checked for segment lenghts and expect all of them to be the same
|
||||
const segmentsCount = sanitizedRoutes[0]?.splitted.length ?? 0;
|
||||
|
||||
let eligibleRoutes: Array<(typeof sanitizedRoutes)[number]> = [
|
||||
...sanitizedRoutes,
|
||||
];
|
||||
|
||||
// loop through the segments
|
||||
for (let i = 0; i < segmentsCount; i++) {
|
||||
const nonParametrizedRoutes = eligibleRoutes.filter(
|
||||
(route) => !isParameter(route.splitted[i]),
|
||||
);
|
||||
|
||||
if (nonParametrizedRoutes.length === 0) {
|
||||
// keep the eligible routes as they are
|
||||
continue;
|
||||
}
|
||||
if (nonParametrizedRoutes.length === 1) {
|
||||
// no need to continue, we found the route
|
||||
eligibleRoutes = nonParametrizedRoutes;
|
||||
break;
|
||||
}
|
||||
|
||||
// we have more than one non-parametrized route, we need to check the next segment
|
||||
eligibleRoutes = nonParametrizedRoutes;
|
||||
}
|
||||
|
||||
return eligibleRoutes[0];
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { splitToSegments } from "./split-to-segments";
|
||||
import { removeLeadingTrailingSlashes } from "./remove-leading-trailing-slashes";
|
||||
import { isParameter } from "./is-parameter";
|
||||
|
||||
/**
|
||||
* Picks the route parameters from the given route.
|
||||
* (e.g. /users/:id/posts/:postId => ['id', 'postId'])
|
||||
*/
|
||||
export const pickRouteParams = (route: string) => {
|
||||
const segments = splitToSegments(removeLeadingTrailingSlashes(route));
|
||||
|
||||
return segments.flatMap((s) => {
|
||||
if (isParameter(s)) {
|
||||
return [s.slice(1)];
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Prepares the route params by checking the existing params and meta data.
|
||||
* Meta data is prioritized over params.
|
||||
* Params are prioritized over predetermined id, action and resource.
|
||||
* This means, we can use `meta` for user supplied params (both manually or from the query string)
|
||||
*/
|
||||
export const prepareRouteParams = <
|
||||
TRouteParams extends Record<string, unknown> = Record<string, unknown>,
|
||||
>(
|
||||
routeParams: (keyof TRouteParams)[],
|
||||
meta: Record<string, unknown> = {},
|
||||
): Partial<TRouteParams> => {
|
||||
return routeParams.reduce(
|
||||
(acc, key) => {
|
||||
const value = meta[key as string];
|
||||
if (typeof value !== "undefined") {
|
||||
acc[key] = value as TRouteParams[keyof TRouteParams];
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Partial<TRouteParams>,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Remove leading and trailing slashes from a route.
|
||||
*/
|
||||
export const removeLeadingTrailingSlashes = (route: string) => {
|
||||
return route.replace(/^\/|\/$/g, "");
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Split a path to segments.
|
||||
*/
|
||||
export const splitToSegments = (path: string) => {
|
||||
const segments = path.split("/").filter((segment) => segment !== "");
|
||||
return segments;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { safeTranslate } from ".";
|
||||
|
||||
describe("safeTranslate", () => {
|
||||
it("should return the translated string", () => {
|
||||
const translate = jest.fn().mockReturnValue("translated");
|
||||
const result = safeTranslate(translate, "key");
|
||||
|
||||
expect(result).toEqual("translated");
|
||||
});
|
||||
|
||||
it("should return the default message if the key is not translated (undefined)", () => {
|
||||
const translate = jest.fn().mockReturnValue(undefined);
|
||||
const result = safeTranslate(translate, "key", "default");
|
||||
|
||||
expect(result).toEqual("default");
|
||||
});
|
||||
|
||||
it("should return the default message if the translated string is the same as the key", () => {
|
||||
const translate = jest.fn().mockReturnValue("key");
|
||||
const result = safeTranslate(translate, "key", "default");
|
||||
|
||||
expect(result).toEqual("default");
|
||||
});
|
||||
|
||||
it("should return the key if the key is not translated and there is no default message", () => {
|
||||
const translate = jest.fn().mockReturnValue(undefined);
|
||||
const result = safeTranslate(translate, "key");
|
||||
|
||||
expect(result).toEqual("key");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { useTranslate } from "@hooks/i18n";
|
||||
|
||||
export const safeTranslate = (
|
||||
translate: ReturnType<typeof useTranslate>,
|
||||
key: string,
|
||||
defaultMessage?: string,
|
||||
options?: any,
|
||||
) => {
|
||||
const translated = options
|
||||
? translate(key, options, defaultMessage)
|
||||
: translate(key, defaultMessage);
|
||||
|
||||
const fallback = defaultMessage ?? key;
|
||||
|
||||
if (translated === key || typeof translated === "undefined") {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return translated;
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { sanitizeResource } from ".";
|
||||
|
||||
describe("sanitizeResource", () => {
|
||||
it("should remove icon property", () => {
|
||||
expect(
|
||||
sanitizeResource({
|
||||
name: "posts",
|
||||
icon: "icon",
|
||||
}),
|
||||
).toEqual({
|
||||
name: "posts",
|
||||
});
|
||||
});
|
||||
it("should remove meta.icon property", () => {
|
||||
expect(
|
||||
sanitizeResource({
|
||||
name: "posts",
|
||||
meta: {
|
||||
icon: "meta-icon",
|
||||
label: "meta-label",
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
name: "posts",
|
||||
meta: {
|
||||
label: "meta-label",
|
||||
},
|
||||
});
|
||||
});
|
||||
it("should remove options.icon property", () => {
|
||||
expect(
|
||||
sanitizeResource({
|
||||
name: "posts",
|
||||
options: {
|
||||
icon: "options-icon",
|
||||
label: "options-label",
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
name: "posts",
|
||||
options: {
|
||||
label: "options-label",
|
||||
},
|
||||
});
|
||||
});
|
||||
it("should return undefined if resource is not passed", () => {
|
||||
expect(sanitizeResource()).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { IResourceItem } from "../../../contexts/resource/types";
|
||||
|
||||
/**
|
||||
* Remove all properties that are non-serializable from a resource object.
|
||||
*/
|
||||
export const sanitizeResource = (
|
||||
resource?: Partial<IResourceItem> &
|
||||
Required<Pick<IResourceItem, "name">> & { children?: unknown },
|
||||
):
|
||||
| (Partial<IResourceItem> & Required<Pick<IResourceItem, "name">>)
|
||||
| undefined => {
|
||||
if (!resource) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const {
|
||||
icon,
|
||||
list,
|
||||
edit,
|
||||
create,
|
||||
show,
|
||||
clone,
|
||||
children,
|
||||
meta,
|
||||
options,
|
||||
...restResource
|
||||
} = resource;
|
||||
|
||||
const { icon: _metaIcon, ...restMeta } = meta ?? {};
|
||||
const { icon: _optionsIcon, ...restOptions } = options ?? {};
|
||||
|
||||
return {
|
||||
...restResource,
|
||||
...(meta ? { meta: restMeta } : {}),
|
||||
...(options ? { options: restOptions } : {}),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { sequentialPromises } from ".";
|
||||
|
||||
describe("sequentialPromises", () => {
|
||||
it("should resolve all promises", async () => {
|
||||
const result = await sequentialPromises(
|
||||
[
|
||||
() => Promise.resolve("1"),
|
||||
() => Promise.resolve("2"),
|
||||
() => Promise.reject("3"),
|
||||
],
|
||||
(response) => {
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
return error;
|
||||
},
|
||||
);
|
||||
expect(result).toEqual(["1", "2", "3"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
type EachResolve<TResolve, Response> = (
|
||||
result: TResolve,
|
||||
index: number,
|
||||
) => Response;
|
||||
type EachReject<TReject, Response> = (
|
||||
error: TReject,
|
||||
index: number,
|
||||
) => Response;
|
||||
|
||||
export const sequentialPromises = async <
|
||||
TResolve = unknown,
|
||||
TReject = unknown,
|
||||
TResolveResponse = unknown,
|
||||
TRejectResponse = unknown,
|
||||
>(
|
||||
promises: (() => Promise<TResolve>)[],
|
||||
onEachResolve: EachResolve<TResolve, TResolveResponse>,
|
||||
onEachReject: EachReject<TReject, TRejectResponse>,
|
||||
): Promise<(TResolveResponse | TRejectResponse)[]> => {
|
||||
const results = [];
|
||||
// @ts-expect-error Remove this when we enable `downLevelIterations`
|
||||
for (const [index, promise] of promises.entries()) {
|
||||
try {
|
||||
const result = await promise();
|
||||
|
||||
results.push(onEachResolve(result, index));
|
||||
} catch (error) {
|
||||
results.push(onEachReject(error as TReject, index));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { createTreeView } from ".";
|
||||
import type {
|
||||
IResourceItem,
|
||||
ITreeMenu,
|
||||
} from "../../../../contexts/resource/types";
|
||||
|
||||
const mockResources: IResourceItem[] = [
|
||||
{
|
||||
name: "cms",
|
||||
},
|
||||
{
|
||||
name: "content",
|
||||
parentName: "cms",
|
||||
},
|
||||
{
|
||||
name: "posts",
|
||||
parentName: "content",
|
||||
},
|
||||
{
|
||||
name: "categories-route",
|
||||
route: "/categories-route",
|
||||
},
|
||||
{
|
||||
name: "categories",
|
||||
route: "/categories",
|
||||
},
|
||||
{
|
||||
name: "categories",
|
||||
meta: {
|
||||
label: "asd",
|
||||
route: "bitti/son/sonson",
|
||||
},
|
||||
parentName: "categories-route",
|
||||
},
|
||||
{
|
||||
name: "users",
|
||||
route: "/users",
|
||||
},
|
||||
];
|
||||
|
||||
const expectedMockResources: ITreeMenu[] = [
|
||||
{
|
||||
name: "categories-route",
|
||||
route: "/categories-route",
|
||||
children: [
|
||||
{
|
||||
name: "categories",
|
||||
meta: {
|
||||
label: "asd",
|
||||
route: "bitti/son/sonson",
|
||||
},
|
||||
parentName: "categories-route",
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "categories",
|
||||
route: "/categories",
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
name: "users",
|
||||
route: "/users",
|
||||
children: [],
|
||||
},
|
||||
];
|
||||
|
||||
describe("createTreeView", () => {
|
||||
const tree: ITreeMenu[] = createTreeView(mockResources);
|
||||
it("should return an tree which has three member", async () => {
|
||||
expect(tree.length).toBe(3);
|
||||
});
|
||||
it("should be equal expectedMockLocation", async () => {
|
||||
expect(tree).toEqual(expectedMockResources);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { pickNotDeprecated } from "@definitions/helpers/pickNotDeprecated";
|
||||
|
||||
import type {
|
||||
IMenuItem,
|
||||
IResourceItem,
|
||||
ITreeMenu,
|
||||
} from "../../../../contexts/resource/types";
|
||||
|
||||
/**
|
||||
* @deprecated This helper is deprecated. Please use `createTree` instead.
|
||||
*/
|
||||
export const createTreeView = (
|
||||
resources: IResourceItem[] | IMenuItem[],
|
||||
): ITreeMenu[] | ITreeMenu[] => {
|
||||
const tree = [];
|
||||
const resourcesRouteObject: { [key: string]: any } = {};
|
||||
const resourcesNameObject: { [key: string]: any } = {};
|
||||
let parent: IResourceItem | IMenuItem;
|
||||
let child: ITreeMenu;
|
||||
|
||||
for (let i = 0; i < resources.length; i++) {
|
||||
parent = resources[i];
|
||||
|
||||
const route =
|
||||
parent.route ??
|
||||
pickNotDeprecated(parent?.meta, parent.options)?.route ??
|
||||
"";
|
||||
|
||||
resourcesRouteObject[route] = parent;
|
||||
resourcesRouteObject[route]["children"] = [];
|
||||
|
||||
resourcesNameObject[parent.name] = parent;
|
||||
resourcesNameObject[parent.name]["children"] = [];
|
||||
}
|
||||
|
||||
for (const name in resourcesRouteObject) {
|
||||
if (Object.hasOwn(resourcesRouteObject, name)) {
|
||||
child = resourcesRouteObject[name];
|
||||
|
||||
if (child.parentName && resourcesNameObject[child.parentName]) {
|
||||
resourcesNameObject[child.parentName]["children"].push(child);
|
||||
} else {
|
||||
tree.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tree;
|
||||
};
|
||||
1
packages/core/src/definitions/helpers/treeView/index.ts
Normal file
1
packages/core/src/definitions/helpers/treeView/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { createTreeView } from "./createTreeView";
|
||||
@@ -0,0 +1,67 @@
|
||||
import { TestWrapper } from "@test/index";
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import { useActiveAuthProvider } from ".";
|
||||
|
||||
/**
|
||||
* NOTE: Will be removed in v5
|
||||
*/
|
||||
describe("useActiveAuthProvider", () => {
|
||||
it("returns new authProvider", async () => {
|
||||
const { result } = renderHook(() => useActiveAuthProvider(), {
|
||||
wrapper: TestWrapper({
|
||||
authProvider: {
|
||||
login: () => Promise.resolve({ success: true }),
|
||||
check: () => Promise.resolve({ authenticated: true }),
|
||||
onError: () => Promise.resolve({}),
|
||||
logout: () => Promise.resolve({ success: true }),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.current?.isLegacy).toBeFalsy();
|
||||
});
|
||||
|
||||
it("returns v3LegacyAuthProviderCompatible authProvider", async () => {
|
||||
const { result } = renderHook(() => useActiveAuthProvider(), {
|
||||
wrapper: TestWrapper({
|
||||
legacyAuthProvider: {
|
||||
login: () => Promise.resolve(),
|
||||
checkAuth: () => Promise.resolve(),
|
||||
logout: () => Promise.resolve(),
|
||||
checkError: () => Promise.resolve(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.current?.isLegacy).toBeTruthy();
|
||||
});
|
||||
|
||||
it("returns new authProvider when both provided", async () => {
|
||||
const { result } = renderHook(() => useActiveAuthProvider(), {
|
||||
wrapper: TestWrapper({
|
||||
legacyAuthProvider: {
|
||||
login: () => Promise.resolve(),
|
||||
checkAuth: () => Promise.resolve(),
|
||||
logout: () => Promise.resolve(),
|
||||
checkError: () => Promise.resolve(),
|
||||
},
|
||||
authProvider: {
|
||||
login: () => Promise.resolve({ success: true }),
|
||||
check: () => Promise.resolve({ authenticated: true }),
|
||||
onError: () => Promise.resolve({}),
|
||||
logout: () => Promise.resolve({ success: true }),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.current?.isLegacy).toBeFalsy();
|
||||
});
|
||||
|
||||
it("returns null", async () => {
|
||||
const { result } = renderHook(() => useActiveAuthProvider(), {
|
||||
wrapper: TestWrapper({}),
|
||||
});
|
||||
|
||||
expect(result.current).toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useAuthBindingsContext, useLegacyAuthContext } from "@contexts/auth";
|
||||
|
||||
/**
|
||||
* @returns authProvider or legacyAuthProvider if provided, otherwise null
|
||||
* @internal
|
||||
* NOTE: Will be removed in v5
|
||||
*/
|
||||
export const useActiveAuthProvider = () => {
|
||||
const legacyAuthProvider = useLegacyAuthContext();
|
||||
const authProvider = useAuthBindingsContext();
|
||||
|
||||
if (authProvider.isProvided) {
|
||||
return { isLegacy: false, ...authProvider };
|
||||
}
|
||||
|
||||
if (legacyAuthProvider.isProvided) {
|
||||
// legacyAuthProvider interface is different from authProvider interface
|
||||
// we need to convert it to authProvider interface for simple usage
|
||||
// in the future, we will remove legacyAuthProvider
|
||||
return {
|
||||
isLegacy: true,
|
||||
...legacyAuthProvider,
|
||||
check: legacyAuthProvider.checkAuth,
|
||||
onError: legacyAuthProvider.checkError,
|
||||
getIdentity: legacyAuthProvider.getUserIdentity,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { getNextPageParam, getPreviousPageParam } from "./index";
|
||||
|
||||
describe("useInfiniteList pagination helper", () => {
|
||||
describe("getNextPageParam", () => {
|
||||
it("default page size", () => {
|
||||
const hasNextPage = getNextPageParam({
|
||||
data: [],
|
||||
total: 10,
|
||||
});
|
||||
expect(hasNextPage).toBe(undefined);
|
||||
});
|
||||
it("custom pageSize and current page", () => {
|
||||
const hasNextPage = getNextPageParam({
|
||||
data: [],
|
||||
total: 10,
|
||||
pagination: {
|
||||
current: 2,
|
||||
pageSize: 3,
|
||||
},
|
||||
});
|
||||
expect(hasNextPage).toBe(3);
|
||||
});
|
||||
it("cursor", () => {
|
||||
const hasNextPage = getNextPageParam({
|
||||
data: [],
|
||||
total: 10,
|
||||
cursor: {
|
||||
next: 2,
|
||||
},
|
||||
});
|
||||
expect(hasNextPage).toBe(2);
|
||||
});
|
||||
});
|
||||
describe("getPreviousPageParam", () => {
|
||||
it("custom pageSize and current page", () => {
|
||||
const hasPreviousPage = getPreviousPageParam({
|
||||
data: [],
|
||||
total: 10,
|
||||
pagination: {
|
||||
current: 2,
|
||||
pageSize: 3,
|
||||
},
|
||||
});
|
||||
expect(hasPreviousPage).toBe(1);
|
||||
});
|
||||
it("hasPreviousPage false", () => {
|
||||
const hasPreviousPage = getPreviousPageParam({
|
||||
data: [],
|
||||
total: 10,
|
||||
});
|
||||
expect(hasPreviousPage).toBe(undefined);
|
||||
});
|
||||
it("cursor", () => {
|
||||
const hasPreviousPage = getPreviousPageParam({
|
||||
data: [],
|
||||
total: 10,
|
||||
cursor: {
|
||||
next: 2,
|
||||
prev: 1,
|
||||
},
|
||||
});
|
||||
expect(hasPreviousPage).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { GetListResponse } from "../../../contexts/data/types";
|
||||
|
||||
export const getNextPageParam = (lastPage: GetListResponse) => {
|
||||
const { pagination, cursor } = lastPage;
|
||||
|
||||
// cursor pagination
|
||||
if (cursor?.next) {
|
||||
return cursor.next;
|
||||
}
|
||||
|
||||
const current = pagination?.current || 1;
|
||||
|
||||
const pageSize = pagination?.pageSize || 10;
|
||||
const totalPages = Math.ceil((lastPage.total || 0) / pageSize);
|
||||
|
||||
return current < totalPages ? Number(current) + 1 : undefined;
|
||||
};
|
||||
|
||||
export const getPreviousPageParam = (lastPage: GetListResponse) => {
|
||||
const { pagination, cursor } = lastPage;
|
||||
|
||||
// cursor pagination
|
||||
if (cursor?.prev) {
|
||||
return cursor.prev;
|
||||
}
|
||||
|
||||
const current = pagination?.current || 1;
|
||||
|
||||
return current === 1 ? undefined : current - 1;
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { renderHook } from "@testing-library/react";
|
||||
|
||||
import { TestWrapper } from "@test/index";
|
||||
import { useMediaQuery } from ".";
|
||||
|
||||
describe("useMediaQuery Helper Hook", () => {
|
||||
it("should return false if the media query does not match", () => {
|
||||
const { result } = renderHook(() => useMediaQuery("(min-width: 600px)"), {
|
||||
wrapper: TestWrapper({}),
|
||||
});
|
||||
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
it("should return", () => {
|
||||
window.matchMedia = jest.fn().mockImplementation((query) => ({
|
||||
matches: query !== "(max-width: 1024px)",
|
||||
media: "",
|
||||
onchange: null,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
}));
|
||||
|
||||
const { result } = renderHook(() => useMediaQuery("(max-width: 600px)"), {
|
||||
wrapper: TestWrapper({}),
|
||||
});
|
||||
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
});
|
||||
17
packages/core/src/definitions/helpers/useMediaQuery/index.ts
Normal file
17
packages/core/src/definitions/helpers/useMediaQuery/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export const useMediaQuery = (query: string) => {
|
||||
const [matches, setMatches] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia(query);
|
||||
if (media.matches !== matches) {
|
||||
setMatches(media.matches);
|
||||
}
|
||||
const listener = () => setMatches(media.matches);
|
||||
window.addEventListener("resize", listener);
|
||||
return () => window.removeEventListener("resize", listener);
|
||||
}, [matches, query]);
|
||||
|
||||
return matches;
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import { renderHook } from "@testing-library/react";
|
||||
|
||||
import { useUserFriendlyName } from ".";
|
||||
import { defaultRefineOptions } from "@contexts/refine";
|
||||
import { TestWrapper } from "@test/index";
|
||||
|
||||
describe("useUserFriendlyName helper hook", () => {
|
||||
describe("with default options", () => {
|
||||
it("should convert kebab-case to humanizeString with plural", async () => {
|
||||
const singularKebapCase = "red-tomato";
|
||||
|
||||
const { result } = renderHook(() => useUserFriendlyName());
|
||||
|
||||
expect(result.current(singularKebapCase, "plural")).toBe("Red tomatoes");
|
||||
});
|
||||
|
||||
it("should convert kebab-case to humanizeString with singular", async () => {
|
||||
const singularKebapCase = "red-tomato";
|
||||
|
||||
const { result } = renderHook(() => useUserFriendlyName());
|
||||
|
||||
expect(result.current(singularKebapCase, "singular")).toBe("Red tomato");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with custom options", () => {
|
||||
it.each(["singular", "plural"] as const)(
|
||||
"should not convert any texts",
|
||||
async (type) => {
|
||||
const singularKebapCase = "red-tomato";
|
||||
|
||||
const { result } = renderHook(() => useUserFriendlyName(), {
|
||||
wrapper: TestWrapper({
|
||||
refineProvider: {
|
||||
options: {
|
||||
...defaultRefineOptions,
|
||||
textTransformers: {
|
||||
humanize: (text: string) => text,
|
||||
plural: (text: string) => text,
|
||||
singular: (text: string) => text,
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.current(singularKebapCase, type)).toBe("red-tomato");
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useRefineContext } from "@hooks/refine";
|
||||
|
||||
/**
|
||||
* A method that the internal uses
|
||||
* @internal
|
||||
*/
|
||||
export const useUserFriendlyName = () => {
|
||||
const {
|
||||
options: { textTransformers },
|
||||
} = useRefineContext();
|
||||
|
||||
const getFriendlyName = (name = "", type: "singular" | "plural"): string => {
|
||||
const humanizeName = textTransformers.humanize(name);
|
||||
if (type === "singular") {
|
||||
return textTransformers.singular(humanizeName);
|
||||
}
|
||||
return textTransformers.plural(humanizeName);
|
||||
};
|
||||
|
||||
return getFriendlyName;
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { userFriendlyResourceName } from "@definitions";
|
||||
|
||||
describe("userFriendlyResourceName Helper", () => {
|
||||
it("should convert kebab-case to humanizeString with plural", async () => {
|
||||
const singularKebapCase = "red-tomato";
|
||||
|
||||
const result = userFriendlyResourceName(singularKebapCase, "plural");
|
||||
|
||||
expect(result).toBe("Red tomatoes");
|
||||
});
|
||||
|
||||
it("should convert kebab-case to humanizeString with singular", async () => {
|
||||
const singularKebapCase = "red-tomato";
|
||||
|
||||
const result = userFriendlyResourceName(singularKebapCase, "singular");
|
||||
|
||||
expect(result).toBe("Red tomato");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import pluralize from "pluralize";
|
||||
import { humanizeString } from "@definitions";
|
||||
|
||||
/**
|
||||
* A method that the internal uses
|
||||
* @internal
|
||||
* @deprecated use `useUserFriendlyName` instead.
|
||||
*/
|
||||
export const userFriendlyResourceName = (
|
||||
resource = "",
|
||||
type: "singular" | "plural",
|
||||
): string => {
|
||||
const humanizeResource = humanizeString(resource);
|
||||
if (type === "singular") {
|
||||
return pluralize.singular(humanizeResource);
|
||||
}
|
||||
return pluralize.plural(humanizeResource);
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { userFriendlySecond } from "@definitions";
|
||||
|
||||
describe("userFriendlySecond Helper", () => {
|
||||
it("converts milliseconds to seconds correctly", async () => {
|
||||
const miliseconds = 5000;
|
||||
|
||||
const seconds = userFriendlySecond(miliseconds);
|
||||
|
||||
expect(seconds).toBe(5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export const userFriendlySecond = (miliseconds: number): number => {
|
||||
return miliseconds / 1000; //convert to seconds
|
||||
};
|
||||
3
packages/core/src/definitions/index.ts
Normal file
3
packages/core/src/definitions/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./table";
|
||||
export * from "./helpers";
|
||||
export * from "./upload";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user