nocobase/packages/core/sdk/src/APIClient.ts
Zeke Zhang 05cf9986b0
feat: enable direct dialog opening via URL and support for page mode (#4706)
* 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
2024-06-30 23:25:01 +08:00

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);
}
}