mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 14:46:13 +00:00
05cf9986b0
* refactor: optimize page tabs routing * test: add e2e test for page tabs * feat: add popup routing * fix: resolve nested issue * refactor: rename file utils to pagePopupUtils * perf: enhance animation and overall performance * fix: fix filterByTK * fix(sourceId): resolve error when sourceId is undefined * fix: fix List and GridCard * fix: fix params not fresh * fix: fix parent record * fix: resolve the issue on block data not refreshing after popup closure * feat: bind tab with URL in popups * feat(sub-page): enable popup to open in page mode * chore: optimize * feat: support association fields * fix: address the issue of no data in associaiton field * fix: resolve the issue with opening nested dialog in association field * fix: fix the issue of dialog content not refreshing * perf: use useNavigateNoUpdate to replace useNavigate * perf: enhance popups performance by avoiding unnecessary rendering * fix: fix tab page * fix: fix bulk edit action * chore: fix unit test * chore: fix unit tests * fix: fix bug to pass e2e tests * chore: fix build * fix: fix bugs to pass e2e tests * chore: avoid crashing * chore: make e2e tests pass * chore: make e2e tests pass * chore: fix unit tests * fix(multi-app): fix known issues * fix(Duplicate): should no page mode * chore: fix build * fix(mobile): fix known issues * fix: fix open mode of Add new * refactor: rename 'popupUid' to 'popupuid' * refactor: rename 'subPageUid' tp 'subpageuid' * refactor(subpage): simplify configuration of router * fix(variable): refresh data after value change * test: add e2e test for sub page * refactor: refactor and add tests * fix: fix association field * refactor(subPage): avoid blank page occurrences * chore: fix unit tests * fix: correct first-click context setting for association fields * refactor: use Action's uid for subpage * refactor: rename x-nb-popup-context to x-action-context and move it to Action schema * feat: add context during the creation of actions * chore: fix build * chore: make e2e tests pass * fix(addChild): fix context of Add child * fix: avoid loss or query string * fix: avoid including 'popups' in the path * fix: resolve issue with popup variables and add tests * chore(e2e): fix e2e test * fix(sideMenu): resolve the disappearing sidebar issue and add tests * chore(e2e): fix e2e test * fix: should refresh block data after mutiple popups closed * chore: fix e2e test * fix(associationField): fix wrong context * fix: address issue with special characters
393 lines
8.7 KiB
TypeScript
393 lines
8.7 KiB
TypeScript
/**
|
|
* This file is part of the NocoBase (R) project.
|
|
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
* Authors: NocoBase Team.
|
|
*
|
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
*/
|
|
|
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosRequestHeaders, AxiosResponse } from 'axios';
|
|
import qs from 'qs';
|
|
|
|
export interface ActionParams {
|
|
filterByTk?: any;
|
|
[key: string]: any;
|
|
}
|
|
|
|
type ResourceActionOptions<P = any> = {
|
|
resource?: string;
|
|
resourceOf?: any;
|
|
action?: string;
|
|
params?: P;
|
|
};
|
|
|
|
type ResourceAction = (params?: ActionParams) => Promise<any>;
|
|
|
|
export type IResource = {
|
|
[key: string]: ResourceAction;
|
|
};
|
|
|
|
export class Auth {
|
|
protected api: APIClient;
|
|
|
|
get storagePrefix() {
|
|
return this.api.storagePrefix;
|
|
}
|
|
|
|
get KEYS() {
|
|
const defaults = {
|
|
locale: this.storagePrefix + 'LOCALE',
|
|
role: this.storagePrefix + 'ROLE',
|
|
token: this.storagePrefix + 'TOKEN',
|
|
authenticator: this.storagePrefix + 'AUTH',
|
|
theme: this.storagePrefix + 'THEME',
|
|
};
|
|
|
|
if (this.api['app']) {
|
|
const appName = this.api['app']?.getName?.();
|
|
if (appName) {
|
|
defaults['role'] = `${appName.toUpperCase()}_` + defaults['role'];
|
|
defaults['locale'] = `${appName.toUpperCase()}_` + defaults['locale'];
|
|
}
|
|
}
|
|
|
|
return defaults;
|
|
}
|
|
|
|
protected options = {
|
|
locale: null,
|
|
role: null,
|
|
authenticator: null,
|
|
token: null,
|
|
};
|
|
|
|
constructor(api: APIClient) {
|
|
this.api = api;
|
|
this.api.axios.interceptors.request.use(this.middleware.bind(this));
|
|
}
|
|
|
|
get locale() {
|
|
return this.getLocale();
|
|
}
|
|
|
|
set locale(value: string) {
|
|
this.setLocale(value);
|
|
}
|
|
|
|
get role() {
|
|
return this.getRole();
|
|
}
|
|
|
|
set role(value: string) {
|
|
this.setRole(value);
|
|
}
|
|
|
|
get token() {
|
|
return this.getToken();
|
|
}
|
|
|
|
set token(value: string) {
|
|
this.setToken(value);
|
|
}
|
|
|
|
get authenticator() {
|
|
return this.getAuthenticator();
|
|
}
|
|
|
|
set authenticator(value: string) {
|
|
this.setAuthenticator(value);
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
getOption(key: string) {
|
|
if (!this.KEYS[key]) {
|
|
return;
|
|
}
|
|
return this.api.storage.getItem(this.KEYS[key]);
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
setOption(key: string, value?: string) {
|
|
if (!this.KEYS[key]) {
|
|
return;
|
|
}
|
|
this.options[key] = value;
|
|
return this.api.storage.setItem(this.KEYS[key], value || '');
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
* use {@link Auth#locale} instead
|
|
*/
|
|
getLocale() {
|
|
return this.getOption('locale');
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
* use {@link Auth#locale} instead
|
|
*/
|
|
setLocale(locale: string) {
|
|
this.setOption('locale', locale);
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
* use {@link Auth#role} instead
|
|
*/
|
|
getRole() {
|
|
return this.getOption('role');
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
* use {@link Auth#role} instead
|
|
*/
|
|
setRole(role: string) {
|
|
this.setOption('role', role);
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
* use {@link Auth#token} instead
|
|
*/
|
|
getToken() {
|
|
return this.getOption('token');
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
* use {@link Auth#token} instead
|
|
*/
|
|
setToken(token: string) {
|
|
this.setOption('token', token);
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
* use {@link Auth#authenticator} instead
|
|
*/
|
|
getAuthenticator() {
|
|
return this.getOption('authenticator');
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
* use {@link Auth#authenticator} instead
|
|
*/
|
|
setAuthenticator(authenticator: string) {
|
|
this.setOption('authenticator', authenticator);
|
|
}
|
|
|
|
middleware(config: AxiosRequestConfig) {
|
|
if (this.locale) {
|
|
config.headers['X-Locale'] = this.locale;
|
|
}
|
|
if (this.role) {
|
|
config.headers['X-Role'] = this.role;
|
|
}
|
|
if (this.authenticator && !config.headers['X-Authenticator']) {
|
|
config.headers['X-Authenticator'] = this.authenticator;
|
|
}
|
|
if (this.token) {
|
|
config.headers['Authorization'] = `Bearer ${this.token}`;
|
|
}
|
|
return config;
|
|
}
|
|
|
|
async signIn(values: any, authenticator?: string): Promise<AxiosResponse<any>> {
|
|
const response = await this.api.request({
|
|
method: 'post',
|
|
url: 'auth:signIn',
|
|
data: values,
|
|
headers: {
|
|
'X-Authenticator': authenticator,
|
|
},
|
|
});
|
|
const data = response?.data?.data;
|
|
this.setToken(data?.token);
|
|
this.setAuthenticator(authenticator);
|
|
return response;
|
|
}
|
|
|
|
async signUp(values: any, authenticator?: string): Promise<AxiosResponse<any>> {
|
|
return await this.api.request({
|
|
method: 'post',
|
|
url: 'auth:signUp',
|
|
data: values,
|
|
headers: {
|
|
'X-Authenticator': authenticator,
|
|
},
|
|
});
|
|
}
|
|
|
|
async signOut() {
|
|
const response = await this.api.request({
|
|
method: 'post',
|
|
url: 'auth:signOut',
|
|
});
|
|
this.setToken(null);
|
|
this.setRole(null);
|
|
this.setAuthenticator(null);
|
|
return response;
|
|
}
|
|
}
|
|
|
|
export abstract class Storage {
|
|
abstract clear(): void;
|
|
abstract getItem(key: string): string | null;
|
|
abstract removeItem(key: string): void;
|
|
abstract setItem(key: string, value: string): void;
|
|
}
|
|
|
|
export class MemoryStorage extends Storage {
|
|
items = new Map();
|
|
|
|
clear() {
|
|
this.items.clear();
|
|
}
|
|
|
|
getItem(key: string) {
|
|
return this.items.get(key);
|
|
}
|
|
|
|
setItem(key: string, value: string) {
|
|
return this.items.set(key, value);
|
|
}
|
|
|
|
removeItem(key: string) {
|
|
return this.items.delete(key);
|
|
}
|
|
}
|
|
|
|
interface ExtendedOptions {
|
|
authClass?: any;
|
|
storageClass?: any;
|
|
storagePrefix?: string;
|
|
}
|
|
|
|
export type APIClientOptions = AxiosInstance | (AxiosRequestConfig & ExtendedOptions);
|
|
|
|
export class APIClient {
|
|
axios: AxiosInstance;
|
|
auth: Auth;
|
|
storage: Storage;
|
|
storagePrefix = 'NOCOBASE_';
|
|
|
|
getHeaders() {
|
|
const headers = {};
|
|
if (this.auth.locale) {
|
|
headers['X-Locale'] = this.auth.locale;
|
|
}
|
|
if (this.auth.role) {
|
|
headers['X-Role'] = this.auth.role;
|
|
}
|
|
if (this.auth.authenticator) {
|
|
headers['X-Authenticator'] = this.auth.authenticator;
|
|
}
|
|
if (this.auth.token) {
|
|
headers['Authorization'] = `Bearer ${this.auth.token}`;
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
constructor(instance?: APIClientOptions) {
|
|
if (typeof instance === 'function') {
|
|
this.axios = instance;
|
|
} else {
|
|
const { authClass, storageClass, storagePrefix = 'NOCOBASE_', ...others } = instance || {};
|
|
this.storagePrefix = storagePrefix;
|
|
this.axios = axios.create(others);
|
|
this.initStorage(storageClass);
|
|
if (authClass) {
|
|
this.auth = new authClass(this);
|
|
}
|
|
}
|
|
if (!this.storage) {
|
|
this.initStorage();
|
|
}
|
|
if (!this.auth) {
|
|
this.auth = new Auth(this);
|
|
}
|
|
this.interceptors();
|
|
}
|
|
|
|
private initStorage(storage?: any) {
|
|
if (storage) {
|
|
this.storage = new storage(this);
|
|
} else if (typeof localStorage !== 'undefined') {
|
|
this.storage = localStorage;
|
|
} else {
|
|
this.storage = new MemoryStorage();
|
|
}
|
|
}
|
|
|
|
interceptors() {
|
|
this.axios.interceptors.request.use((config) => {
|
|
config.paramsSerializer = (params) => {
|
|
return qs.stringify(params, {
|
|
strictNullHandling: true,
|
|
arrayFormat: 'brackets',
|
|
});
|
|
};
|
|
return config;
|
|
});
|
|
}
|
|
|
|
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D> | ResourceActionOptions): Promise<R> {
|
|
const { resource, resourceOf, action, params, headers } = config as any;
|
|
if (resource) {
|
|
return this.resource(resource, resourceOf, headers)[action](params);
|
|
}
|
|
return this.axios.request<T, R, D>(config);
|
|
}
|
|
|
|
resource(name: string, of?: any, headers?: AxiosRequestHeaders, cancel?: boolean): IResource {
|
|
const target = {};
|
|
const handler = {
|
|
get: (_: any, actionName: string) => {
|
|
if (cancel) {
|
|
return;
|
|
}
|
|
|
|
let url = name.split('.').join(`/${encodeURIComponent(of) || '_'}/`);
|
|
url += `:${actionName}`;
|
|
const config: AxiosRequestConfig = { url };
|
|
if (['get', 'list'].includes(actionName)) {
|
|
config['method'] = 'get';
|
|
} else {
|
|
config['method'] = 'post';
|
|
}
|
|
return async (params?: ActionParams, opts?: any) => {
|
|
const { values, filter, ...others } = params || {};
|
|
config['params'] = others;
|
|
if (filter) {
|
|
if (typeof filter === 'string') {
|
|
config['params']['filter'] = filter;
|
|
} else {
|
|
if (filter['*']) {
|
|
delete filter['*'];
|
|
}
|
|
config['params']['filter'] = JSON.stringify(filter);
|
|
}
|
|
}
|
|
if (config.method !== 'get') {
|
|
config['data'] = values || {};
|
|
}
|
|
return await this.request({
|
|
...config,
|
|
...opts,
|
|
headers,
|
|
});
|
|
};
|
|
},
|
|
};
|
|
return new Proxy(target, handler);
|
|
}
|
|
}
|