Refactor Widget loading

This commit is contained in:
Hetu Nandu 2020-01-30 13:23:04 +00:00
parent 97dbc156f0
commit 0dd25ba2ef
29 changed files with 361 additions and 584 deletions

View File

@ -46,7 +46,6 @@
"husky": "^3.0.5",
"interweave": "^12.1.1",
"interweave-autolink": "^4.0.1",
"jsonpath-plus": "^1.0.0",
"lint-staged": "^9.2.5",
"localforage": "^1.7.3",
"lodash": "^4.17.11",

View File

@ -4,7 +4,6 @@ import {
ReduxActionErrorTypes,
} from "constants/ReduxActionConstants";
import { RestAction } from "api/ActionAPI";
import { ActionWidgetIdsMap } from "sagas/ActionWidgetMapSagas";
export const createActionRequest = (payload: Partial<RestAction>) => {
return {
@ -128,13 +127,6 @@ export const copyActionError = (payload: {
};
};
export const actionToWidgetIdMapSuccess = (
map: ActionWidgetIdsMap,
): ReduxAction<ActionWidgetIdsMap> => ({
type: ReduxActionTypes.CREATE_UPDATE_ACTION_WIDGETIDS_MAP_SUCCESS,
payload: map,
});
export default {
createAction: createActionRequest,
fetchActions,

View File

@ -1,17 +0,0 @@
import {
ReduxAction,
ReduxActionTypes,
ReduxActionWithoutPayload,
} from "constants/ReduxActionConstants";
import { NamePathBindingMap } from "constants/BindingsConstants";
export const initBindingMapListener = (): ReduxActionWithoutPayload => ({
type: ReduxActionTypes.CREATE_UPDATE_BINDINGS_MAP_LISTENER_INIT,
});
export const bindingsMapSuccess = (
map: NamePathBindingMap,
): ReduxAction<NamePathBindingMap> => ({
type: ReduxActionTypes.CREATE_UPDATE_BINDINGS_MAP_SUCCESS,
payload: map,
});

View File

@ -1,5 +1,13 @@
import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants";
import { ActionPayload, PageAction } from "constants/ActionConstants";
import {
ReduxActionTypes,
ReduxAction,
ReduxActionErrorTypes,
} from "constants/ReduxActionConstants";
import {
ActionPayload,
ExecuteErrorPayload,
PageAction,
} from "constants/ActionConstants";
export const executeAction = (
actionPayloads: ActionPayload[],
@ -10,6 +18,13 @@ export const executeAction = (
};
};
export const executeActionError = (
executeErrorPayload: ExecuteErrorPayload,
): ReduxAction<ExecuteErrorPayload> => ({
type: ReduxActionErrorTypes.EXECUTE_ACTION_ERROR,
payload: executeErrorPayload,
});
export const executePageLoadActions = (
payload: PageAction[][],
): ReduxAction<PageAction[][]> => ({
@ -17,19 +32,6 @@ export const executePageLoadActions = (
payload,
});
export const loadingAction = (
areLoading: boolean,
widgetIds: string[],
): ReduxAction<WidgetLoadingState> => {
return {
type: ReduxActionTypes.LOADING_ACTION,
payload: {
areLoading: areLoading,
widgetIds: widgetIds,
},
};
};
export const disableDragAction = (
disable: boolean,
): ReduxAction<{ disable: boolean }> => {

View File

@ -12,6 +12,7 @@ import { APIEditorRouteParams } from "constants/routes";
import { ApiPaneReduxState } from "reducers/uiReducers/apiPaneReducer";
import LoadingOverlayScreen from "components/editorComponents/LoadingOverlayScreen";
import CodeEditor from "components/editorComponents/CodeEditor";
import { getActionResponses } from "selectors/entitiesSelector";
const ResponseWrapper = styled.div`
position: relative;
@ -49,9 +50,7 @@ const TableWrapper = styled.div`
`;
interface ReduxStateProps {
responses: {
[id: string]: ActionResponse;
};
responses: Record<string, ActionResponse | undefined>;
apiPane: ApiPaneReduxState;
}
@ -100,7 +99,7 @@ const ApiResponseView = (props: Props) => {
let response: ActionResponse = EMPTY_RESPONSE;
let isRunning = false;
if (apiId && apiId in responses) {
response = responses[apiId];
response = responses[apiId] || EMPTY_RESPONSE;
isRunning = apiPane.isRunning[apiId];
}
return (
@ -160,7 +159,7 @@ const ApiResponseView = (props: Props) => {
};
const mapStateToProps = (state: AppState): ReduxStateProps => ({
responses: state.entities.apiData,
responses: getActionResponses(state),
apiPane: state.ui.apiPane,
});

View File

@ -155,7 +155,7 @@ export function FinalActionSelector(props: FinalActionSelectorProps) {
updateLabel={props.updateLabel}
actions={props.actions}
labelEditable={props.labelEditable}
></ActionSelector>
/>
{selectedActionLabel !== DEFAULT_ACTION_LABEL && (
<ResolutionActionContainer>
<ActionSelector
@ -169,7 +169,7 @@ export function FinalActionSelector(props: FinalActionSelectorProps) {
updateActions={props.updateActions}
updateLabel={props.updateLabel}
actions={props.actions}
></ActionSelector>
/>
<ActionSelector
allActions={allActions}
actionTypeOptions={actionTypeOptions}
@ -181,7 +181,7 @@ export function FinalActionSelector(props: FinalActionSelectorProps) {
updateActions={props.updateActions}
updateLabel={props.updateLabel}
actions={props.actions}
></ActionSelector>
/>
</ResolutionActionContainer>
)}
</ControlWrapper>
@ -189,7 +189,7 @@ export function FinalActionSelector(props: FinalActionSelectorProps) {
}
class ActionSelectorControl extends BaseControl<
ControlProps & ActionDataState
ControlProps & { data: RestAction[] }
> {
render() {
return (
@ -199,7 +199,7 @@ class ActionSelectorControl extends BaseControl<
actions={this.props.propertyValue}
identifier={this.props.propertyName}
updateActions={this.updateActions}
></FinalActionSelector>
/>
);
}
@ -216,8 +216,8 @@ export interface ActionSelectorControlProps extends ControlProps {
propertyValue: ActionPayload[];
}
const mapStateToProps = (state: AppState): ActionDataState => ({
...state.entities.actions,
const mapStateToProps = (state: AppState): { data: RestAction[] } => ({
data: state.entities.actions.map(a => a.config),
});
export default connect(mapStateToProps)(ActionSelectorControl);

View File

@ -12,6 +12,7 @@ import { generateReactKey } from "utils/generators";
import styled from "constants/DefaultTheme";
import { AnyStyledComponent } from "styled-components";
import { FormIcons } from "icons/FormIcons";
import { RestAction } from "api/ActionAPI";
export interface ColumnAction {
label: string;
id: string;
@ -27,7 +28,7 @@ const StyledDeleteIcon = styled(FormIcons.DELETE_ICON as AnyStyledComponent)`
`;
class ColumnActionSelectorControl extends BaseControl<
ColumnActionSelectorControlProps & ActionDataState
ColumnActionSelectorControlProps & { data: RestAction[] }
> {
render() {
return (
@ -51,12 +52,12 @@ class ColumnActionSelectorControl extends BaseControl<
label={columnAction.label}
labelEditable={true}
updateLabel={this.updateLabel}
></FinalActionSelector>
/>
<StyledDeleteIcon
height={20}
width={20}
onClick={this.removeColumnAction.bind(this, index)}
></StyledDeleteIcon>
/>
</div>
);
},
@ -67,7 +68,7 @@ class ColumnActionSelectorControl extends BaseControl<
color={"#FFFFFF"}
minimal={true}
onClick={this.addColumnAction}
></StyledPropertyPaneButton>
/>
</ControlWrapper>
);
}
@ -141,8 +142,8 @@ class ColumnActionSelectorControl extends BaseControl<
export type ColumnActionSelectorControlProps = ControlProps;
const mapStateToProps = (state: AppState): ActionDataState => ({
...state.entities.actions,
const mapStateToProps = (state: AppState): { data: RestAction[] } => ({
data: state.entities.actions.map(a => a.config),
});
export default connect(mapStateToProps)(ColumnActionSelectorControl);

View File

@ -86,3 +86,8 @@ export interface PageAction {
export interface TableAction extends BaseActionPayload {
actionName: string;
}
export interface ExecuteErrorPayload {
actionId: string;
error: any;
}

View File

@ -31,7 +31,6 @@ export const ReduxActionTypes: { [key: string]: string } = {
RUN_API_SUCCESS: "RUN_API_SUCCESS",
EXECUTE_ACTION: "EXECUTE_ACTION",
EXECUTE_ACTION_SUCCESS: "EXECUTE_ACTION_SUCCESS",
EXECUTE_ACTION_ERROR: "EXECUTE_ACTION_ERROR",
LOAD_CANVAS_ACTIONS: "LOAD_CANVAS_ACTIONS",
SAVE_PAGE_INIT: "SAVE_PAGE_INIT",
SAVE_PAGE_SUCCESS: "SAVE_PAGE_SUCCESS",
@ -76,11 +75,6 @@ export const ReduxActionTypes: { [key: string]: string } = {
FETCH_APPLICATION_LIST_SUCCESS: "FETCH_APPLICATION_LIST_SUCCESS",
CREATE_APPLICATION_INIT: "CREATE_APPLICATION_INIT",
CREATE_APPLICATION_SUCCESS: "CREATE_APPLICATION_SUCCESS",
CREATE_UPDATE_BINDINGS_MAP_LISTENER_INIT:
"CREATE_UPDATE_BINDINGS_MAP_LISTENER_INIT",
CREATE_UPDATE_BINDINGS_MAP_SUCCESS: "CREATE_UPDATE_BINDINGS_MAP_SUCCESS",
CREATE_UPDATE_ACTION_WIDGETIDS_MAP_SUCCESS:
"CREATE_UPDATE_ACTION_WIDGETIDS_MAP_SUCCESS",
UPDATE_WIDGET_PROPERTY_VALIDATION: "UPDATE_WIDGET_PROPERTY_VALIDATION",
HIDE_PROPERTY_PANE: "HIDE_PROPERTY_PANE",
INIT_API_PANE: "INIT_API_PANE",

View File

@ -0,0 +1,2 @@
export const ENTITY_TYPE_ACTION = "ACTION";
export const ENTITY_TYPE_WIDGET = "WIDGET";

View File

@ -88,15 +88,15 @@ class ApiSidebar extends React.Component<Props> {
) {
return true;
}
return nextProps.actions.data !== this.props.actions.data;
return nextProps.actions !== this.props.actions;
}
handleCreateNew = () => {
const { actions } = this.props;
const { pageId } = this.props.match.params;
const pageApiNames = actions.data
.filter(a => a.pageId === pageId)
.map(a => a.name);
const pageApiNames = actions
.filter(a => a.config.pageId === pageId)
.map(a => a.config.name);
const newName = getNextEntityName("Api", pageApiNames);
this.props.createAction({ ...DEFAULT_API_ACTION, name: newName, pageId });
};
@ -106,23 +106,28 @@ class ApiSidebar extends React.Component<Props> {
};
handleMove = (itemId: string, destinationPageId: string) => {
const action = this.props.actions.data.filter(a => a.id === itemId)[0];
const pageApiNames = this.props.actions.data
.filter(a => a.pageId === destinationPageId)
.map(a => a.name);
let name = action.name;
if (pageApiNames.indexOf(action.name) > -1) {
const action = this.props.actions.filter(a => a.config.id === itemId)[0];
const pageApiNames = this.props.actions
.filter(a => a.config.pageId === destinationPageId)
.map(a => a.config.name);
let name = action.config.name;
if (pageApiNames.indexOf(action.config.name) > -1) {
name = getNextEntityName(name, pageApiNames);
}
this.props.moveAction(itemId, destinationPageId, name, action.pageId);
this.props.moveAction(
itemId,
destinationPageId,
name,
action.config.pageId,
);
};
handleCopy = (itemId: string, destinationPageId: string) => {
const action = this.props.actions.data.filter(a => a.id === itemId)[0];
const pageApiNames = this.props.actions.data
.filter(a => a.pageId === destinationPageId)
.map(a => a.name);
let name = `${action.name}Copy`;
const action = this.props.actions.filter(a => a.config.id === itemId)[0];
const pageApiNames = this.props.actions
.filter(a => a.config.pageId === destinationPageId)
.map(a => a.config.name);
let name = `${action.config.name}Copy`;
if (pageApiNames.indexOf(name) > -1) {
name = getNextEntityName(name, pageApiNames);
}
@ -154,10 +159,11 @@ class ApiSidebar extends React.Component<Props> {
match: {
params: { apiId },
},
actions: { data },
actions,
pluginId,
} = this.props;
if (!pluginId) return null;
const data = actions.map(a => a.config);
return (
<EditorSidebar
isLoading={isFetching}

View File

@ -4,87 +4,139 @@ import {
ReduxAction,
ReduxActionErrorTypes,
} from "constants/ReduxActionConstants";
import { RestAction } from "api/ActionAPI";
import { ActionWidgetIdsMap } from "sagas/ActionWidgetMapSagas";
import { ActionResponse, RestAction } from "api/ActionAPI";
import { ActionPayload, ExecuteErrorPayload } from "constants/ActionConstants";
import _ from "lodash";
const initialState: ActionDataState = {
data: [],
actionToWidgetIdMap: {},
};
export interface ActionDataState {
data: RestAction[];
actionToWidgetIdMap: ActionWidgetIdsMap;
interface ActionData {
isLoading: boolean;
config: RestAction;
data?: ActionResponse;
}
export type ActionDataState = ActionData[];
const initialState: ActionDataState = [];
const actionsReducer = createReducer(initialState, {
[ReduxActionTypes.FETCH_ACTIONS_SUCCESS]: (
state: ActionDataState,
action: ReduxAction<RestAction[]>,
) => ({
...state,
data: action.payload,
isFetching: false,
}),
[ReduxActionErrorTypes.FETCH_ACTIONS_ERROR]: (state: ActionDataState) => ({
...state,
data: [],
}),
): ActionDataState =>
action.payload.map(a => ({
isLoading: false,
config: a,
})),
[ReduxActionErrorTypes.FETCH_ACTIONS_ERROR]: () => initialState,
[ReduxActionTypes.CREATE_ACTION_INIT]: (
state: ActionDataState,
action: ReduxAction<RestAction>,
) => ({
...state,
data: state.data.concat([action.payload]),
}),
): ActionDataState =>
state.concat([{ config: action.payload, isLoading: false }]),
[ReduxActionTypes.CREATE_ACTION_SUCCESS]: (
state: ActionDataState,
action: ReduxAction<RestAction>,
) => ({
...state,
data: state.data.map(a => {
): ActionDataState =>
state.map(a => {
if (
a.pageId === action.payload.pageId &&
a.name === action.payload.name
a.config.pageId === action.payload.pageId &&
a.config.name === action.payload.name
) {
return action.payload;
return { ...a, config: action.payload };
}
return a;
}),
}),
[ReduxActionTypes.CREATE_ACTION_ERROR]: (
state: ActionDataState,
action: ReduxAction<RestAction>,
) => ({
...state,
data: state.data.filter(
a => a.name !== action.payload.name && a.pageId !== action.payload.pageId,
): ActionDataState =>
state.filter(
a =>
a.config.name !== action.payload.name &&
a.config.pageId !== action.payload.pageId,
),
}),
[ReduxActionTypes.UPDATE_ACTION_SUCCESS]: (
state: ActionDataState,
action: ReduxAction<{ data: RestAction }>,
) => ({
...state,
data: state.data.map(d => {
if (d.id === action.payload.data.id) return action.payload.data;
return d;
): ActionDataState =>
state.map(a => {
if (a.config.id === action.payload.data.id)
return { ...a, config: action.payload.data };
return a;
}),
}),
[ReduxActionTypes.DELETE_ACTION_SUCCESS]: (
state: ActionDataState,
action: ReduxAction<{ id: string }>,
) => ({
...state,
data: state.data.filter(d => d.id !== action.payload.id),
}),
[ReduxActionTypes.CREATE_UPDATE_ACTION_WIDGETIDS_MAP_SUCCESS]: (
): ActionDataState => state.filter(a => a.config.id !== action.payload.id),
[ReduxActionTypes.EXECUTE_ACTION]: (
state: ActionDataState,
action: ReduxAction<ActionWidgetIdsMap>,
) => ({
...state,
actionToWidgetIdMap: action.payload,
}),
action: ReduxAction<ActionPayload[]>,
): ActionDataState =>
state.map(a => {
if (_.find(action.payload, { actionId: a.config.id })) {
return {
...a,
isLoading: true,
};
}
return a;
}),
[ReduxActionTypes.EXECUTE_ACTION_SUCCESS]: (
state: ActionDataState,
action: ReduxAction<{ [id: string]: ActionResponse }>,
): ActionDataState => {
const actionId = Object.keys(action.payload)[0];
return state.map(a => {
if (a.config.id === actionId) {
return { ...a, isLoading: false, data: action.payload[actionId] };
}
return a;
});
},
[ReduxActionErrorTypes.EXECUTE_ACTION_ERROR]: (
state: ActionDataState,
action: ReduxAction<ExecuteErrorPayload>,
): ActionDataState =>
state.map(a => {
if (a.config.id === action.payload.actionId) {
return { ...a, isLoading: false };
}
return a;
}),
[ReduxActionTypes.RUN_API_REQUEST]: (
state: ActionDataState,
action: ReduxAction<string>,
): ActionDataState =>
state.map(a => {
if (action.payload === a.config.id) {
return {
...a,
isLoading: true,
};
}
return a;
}),
[ReduxActionTypes.RUN_API_SUCCESS]: (
state: ActionDataState,
action: ReduxAction<{ [id: string]: ActionResponse }>,
): ActionDataState => {
const actionId = Object.keys(action.payload)[0];
return state.map(a => {
if (a.config.id === actionId) {
return { ...a, isLoading: false, data: action.payload[actionId] };
}
return a;
});
},
[ReduxActionErrorTypes.RUN_API_ERROR]: (
state: ActionDataState,
action: ReduxAction<{ id: string }>,
): ActionDataState =>
state.map(a => {
if (a.config.id === action.payload.id) {
return { ...a, isLoading: false };
}
return a;
}),
[ReduxActionTypes.MOVE_ACTION_INIT]: (
state: ActionDataState,
action: ReduxAction<{
@ -92,46 +144,46 @@ const actionsReducer = createReducer(initialState, {
destinationPageId: string;
name: string;
}>,
) => ({
...state,
data: state.data.map(restAction => {
if (restAction.id === action.payload.id) {
): ActionDataState =>
state.map(a => {
if (a.config.id === action.payload.id) {
return {
...restAction,
name: action.payload.name,
pageId: action.payload.destinationPageId,
...a,
config: {
...a.config,
name: action.payload.name,
pageId: action.payload.destinationPageId,
},
};
}
return restAction;
return a;
}),
}),
[ReduxActionTypes.MOVE_ACTION_SUCCESS]: (
state: ActionDataState,
action: ReduxAction<RestAction>,
) => ({
...state,
data: state.data.map(restAction => {
if (restAction.id === action.payload.id) {
return action.payload;
): ActionDataState =>
state.map(a => {
if (a.config.id === action.payload.id) {
return { ...a, config: action.payload };
}
return restAction;
return a;
}),
}),
[ReduxActionErrorTypes.MOVE_ACTION_ERROR]: (
state: ActionDataState,
action: ReduxAction<{ id: string; originalPageId: string }>,
) => ({
...state,
data: state.data.map(restAction => {
if (restAction.id === action.payload.id) {
): ActionDataState =>
state.map(a => {
if (a.config.id === action.payload.id) {
return {
...restAction,
pageId: action.payload.originalPageId,
...a,
config: {
...a.config,
pageId: action.payload.originalPageId,
},
};
}
return restAction;
return a;
}),
}),
[ReduxActionTypes.COPY_ACTION_INIT]: (
state: ActionDataState,
action: ReduxAction<{
@ -139,34 +191,35 @@ const actionsReducer = createReducer(initialState, {
destinationPageId: string;
name: string;
}>,
) => ({
...state,
data: state.data.concat(
state.data
.filter(a => a.id === action.payload.id)
): ActionDataState =>
state.concat(
state
.filter(a => a.config.id === action.payload.id)
.map(a => ({
...a,
name: action.payload.name,
pageId: action.payload.destinationPageId,
config: {
...a.config,
name: action.payload.name,
pageId: action.payload.destinationPageId,
},
})),
),
}),
[ReduxActionTypes.COPY_ACTION_SUCCESS]: (
state: ActionDataState,
action: ReduxAction<RestAction>,
) => ({
...state,
data: state.data.map(a => {
): ActionDataState =>
state.map(a => {
if (
a.pageId === action.payload.pageId &&
a.name === action.payload.name
a.config.pageId === action.payload.pageId &&
a.config.name === action.payload.name
) {
return action.payload;
return {
...a,
config: action.payload,
};
}
return a;
}),
}),
[ReduxActionErrorTypes.COPY_ACTION_ERROR]: (
state: ActionDataState,
action: ReduxAction<{
@ -174,18 +227,16 @@ const actionsReducer = createReducer(initialState, {
destinationPageId: string;
name: string;
}>,
) => ({
...state,
data: state.data.filter(a => {
if (a.pageId === action.payload.destinationPageId) {
if (a.id === action.payload.id) {
return a.name !== action.payload.name;
): ActionDataState =>
state.filter(a => {
if (a.config.pageId === action.payload.destinationPageId) {
if (a.config.id === action.payload.id) {
return a.config.name !== action.payload.name;
}
return true;
}
return true;
}),
}),
});
export default actionsReducer;

View File

@ -1,26 +0,0 @@
import { createReducer } from "utils/AppsmithUtils";
import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants";
import { ActionResponse } from "api/ActionAPI";
import { ActionDataState } from "./actionsReducer";
import _ from "lodash";
const initialState: APIDataState = {};
export type APIDataState = Record<string, ActionResponse>;
const apiDataReducer = createReducer(initialState, {
[ReduxActionTypes.EXECUTE_ACTION_SUCCESS]: (
state: ActionDataState,
action: ReduxAction<{ [id: string]: ActionResponse }>,
) => ({ ...state, ...action.payload }),
[ReduxActionTypes.RUN_API_SUCCESS]: (
state: ActionDataState,
action: ReduxAction<{ [id: string]: ActionResponse }>,
) => ({ ...state, ...action.payload }),
[ReduxActionTypes.DELETE_ACTION_SUCCESS]: (
state: ActionDataState,
action: ReduxAction<{ id: string }>,
) => _.omit(state, action.payload.id),
});
export default apiDataReducer;

View File

@ -1,16 +0,0 @@
import { createReducer } from "utils/AppsmithUtils";
import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants";
import { NamePathBindingMap } from "constants/BindingsConstants";
export type BindingsDataState = NamePathBindingMap;
const initialState: BindingsDataState = {};
const bindingsReducer = createReducer(initialState, {
[ReduxActionTypes.CREATE_UPDATE_BINDINGS_MAP_SUCCESS]: (
state: BindingsDataState,
action: ReduxAction<NamePathBindingMap>,
) => action.payload,
});
export default bindingsReducer;

View File

@ -28,19 +28,6 @@ const canvasWidgetsReducer = createReducer(initialState, {
) => {
return { ...action.payload.widgets };
},
[ReduxActionTypes.WIDGETS_LOADING]: (
state: CanvasWidgetsReduxState,
action: ReduxAction<WidgetLoadingState>,
) => {
const finalState = { ...state };
action.payload.widgetIds.forEach(widgetId => {
const widget = state[widgetId];
widget.isLoading = action.payload.areLoading;
finalState[widgetId] = widget;
});
return finalState;
},
[ReduxActionTypes.UPDATE_WIDGET_PROPERTY]: (
state: CanvasWidgetsReduxState,
action: ReduxAction<UpdateWidgetPropertyPayload>,

View File

@ -1,25 +1,21 @@
import { combineReducers } from "redux";
import canvasWidgetsReducer from "./canvasWidgetsReducer";
import apiDataReducer from "./apiDataReducer";
import queryDataReducer from "./queryDataReducer";
import widgetConfigReducer from "./widgetConfigReducer";
import actionsReducer from "./actionsReducer";
import propertyPaneConfigReducer from "./propertyPaneConfigReducer";
import datasourceReducer from "./datasourceReducer";
import bindingsReducer from "./bindingsReducer";
import pageListReducer from "./pageListReducer";
import jsExecutionsReducer from "./jsExecutionsReducer";
import pluginsReducer from "reducers/entityReducers/pluginsReducer";
const entityReducer = combineReducers({
canvasWidgets: canvasWidgetsReducer,
apiData: apiDataReducer,
queryData: queryDataReducer,
widgetConfig: widgetConfigReducer,
actions: actionsReducer,
propertyConfig: propertyPaneConfigReducer,
datasources: datasourceReducer,
nameBindings: bindingsReducer,
pageList: pageListReducer,
jsExecutions: jsExecutionsReducer,
plugins: pluginsReducer,

View File

@ -5,7 +5,6 @@ import { reducer as formReducer } from "redux-form";
import { CanvasWidgetsReduxState } from "./entityReducers/canvasWidgetsReducer";
import { EditorReduxState } from "./uiReducers/editorReducer";
import { ErrorReduxState } from "./uiReducers/errorReducer";
import { APIDataState } from "./entityReducers/apiDataReducer";
import { QueryDataState } from "./entityReducers/queryDataReducer";
import { ActionDataState } from "./entityReducers/actionsReducer";
import { PropertyPaneConfigState } from "./entityReducers/propertyPaneConfigReducer";
@ -15,7 +14,6 @@ import { WidgetSidebarReduxState } from "./uiReducers/widgetSidebarReducer";
import { DatasourceDataState } from "./entityReducers/datasourceReducer";
import { AppViewReduxState } from "./uiReducers/appViewReducer";
import { ApplicationsReduxState } from "./uiReducers/applicationsReducer";
import { BindingsDataState } from "./entityReducers/bindingsReducer";
import { PageListReduxState } from "./entityReducers/pageListReducer";
import { ApiPaneReduxState } from "./uiReducers/apiPaneReducer";
import { RoutesParamsReducerState } from "reducers/uiReducers/routesParamsReducer";
@ -50,13 +48,11 @@ export interface AppState {
};
entities: {
canvasWidgets: CanvasWidgetsReduxState;
apiData: APIDataState;
queryData: QueryDataState;
actions: ActionDataState;
propertyConfig: PropertyPaneConfigState;
widgetConfig: WidgetConfigReducerState;
datasources: DatasourceDataState;
nameBindings: BindingsDataState;
pageList: PageListReduxState;
plugins: PluginDataState;
};

View File

@ -25,7 +25,7 @@ import ActionAPI, {
Property,
RestAction,
} from "api/ActionAPI";
import { AppState, DataTree } from "reducers";
import { AppState } from "reducers";
import _ from "lodash";
import { mapToPropList } from "utils/AppsmithUtils";
import AppToaster from "components/editorComponents/ToastComponent";
@ -48,23 +48,24 @@ import {
removeBindingsFromObject,
} from "utils/DynamicBindingUtils";
import { validateResponse } from "./ErrorSagas";
import { getDataTree } from "selectors/entitiesSelector";
import {
ERROR_MESSAGE_SELECT_ACTION,
ERROR_MESSAGE_SELECT_ACTION_TYPE,
} from "constants/messages";
import { getFormData } from "selectors/formSelectors";
import { API_EDITOR_FORM_NAME } from "constants/forms";
import { executeAction } from "actions/widgetActions";
import { executeAction, executeActionError } from "actions/widgetActions";
import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton";
import { getParsedDataTree } from "selectors/nameBindingsWithDataSelector";
import { transformRestAction } from "transformers/RestActionTransformer";
import { getActionResponses } from "selectors/entitiesSelector";
export const getAction = (
state: AppState,
actionId: string,
): RestAction | undefined => {
return _.find(state.entities.actions.data, { id: actionId });
const action = _.find(state.entities.actions, a => a.config.id === actionId);
return action ? action.config : undefined;
};
const createActionResponse = (response: ActionApiResponse): ActionResponse => ({
@ -126,10 +127,12 @@ export function* executeAPIQueryActionSaga(apiAction: ActionPayload) {
try {
const api: PageAction = yield select(getAction, apiAction.actionId);
if (!api) {
yield put({
type: ReduxActionTypes.EXECUTE_ACTION_ERROR,
payload: "No action selected",
});
yield put(
executeActionError({
actionId: apiAction.actionId,
error: "No action selected",
}),
);
return;
}
const params: Property[] = yield call(getActionParams, api.jsonPathKeys);
@ -137,26 +140,9 @@ export function* executeAPIQueryActionSaga(apiAction: ActionPayload) {
action: { id: apiAction.actionId },
params,
};
const dataTree: DataTree = yield select(getDataTree);
yield put({
type: ReduxActionTypes.WIDGETS_LOADING,
payload: {
widgetIds:
dataTree.actions.actionToWidgetIdMap[apiAction.actionId] || [],
areLoading: true,
},
});
const response: ActionApiResponse = yield ActionAPI.executeAction(
executeActionRequest,
);
yield put({
type: ReduxActionTypes.WIDGETS_LOADING,
payload: {
widgetIds:
dataTree.actions.actionToWidgetIdMap[apiAction.actionId] || [],
areLoading: false,
},
});
let payload = createActionResponse(response);
if (response.responseMeta && response.responseMeta.error) {
payload = createActionErrorResponse(response);
@ -166,10 +152,12 @@ export function* executeAPIQueryActionSaga(apiAction: ActionPayload) {
payload: apiAction.onError,
});
}
yield put({
type: ReduxActionTypes.EXECUTE_ACTION_ERROR,
payload: { [apiAction.actionId]: payload },
});
yield put(
executeActionError({
actionId: apiAction.actionId,
error: response.responseMeta.error,
}),
);
} else {
if (apiAction.onSuccess) {
yield put({
@ -184,10 +172,12 @@ export function* executeAPIQueryActionSaga(apiAction: ActionPayload) {
}
return response;
} catch (error) {
yield put({
type: ReduxActionTypes.EXECUTE_ACTION_ERROR,
payload: { error },
});
yield put(
executeActionError({
actionId: apiAction.actionId,
error,
}),
);
}
}
@ -241,10 +231,12 @@ export function* executeReduxActionSaga(action: ReduxAction<ActionPayload[]>) {
if (!_.isNil(action.payload)) {
yield* executeActionSaga(action.payload);
} else {
yield put({
type: ReduxActionTypes.EXECUTE_ACTION_ERROR,
payload: "No action payload",
});
yield put(
executeActionError({
actionId: "",
error: "No action payload",
}),
);
}
}
@ -390,9 +382,7 @@ export function* runApiActionSaga(action: ReduxAction<string>) {
function* executePageLoadActionsSaga(action: ReduxAction<PageAction[][]>) {
const pageActions = action.payload;
const apiResponses = yield select(
(state: AppState) => state.entities.apiData,
);
const apiResponses = yield select(getActionResponses);
const actionPayloads: ActionPayload[][] = pageActions.map(actionSet =>
actionSet.map(action => ({
actionId: action.id,

View File

@ -1,155 +0,0 @@
import { all, select, takeLatest, put } from "redux-saga/effects";
import { ReduxActionTypes, ReduxAction } from "constants/ReduxActionConstants";
import { actionToWidgetIdMapSuccess } from "actions/actionActions";
import { AppState } from "reducers";
import { getDynamicBindings } from "utils/DynamicBindingUtils";
import { UpdateWidgetPropertyPayload } from "actions/controlActions";
import _ from "lodash";
import { RestAction } from "api/ActionAPI";
export type ActionWidgetIdsMap = Record<string, string[]>;
function* init() {
const data: AppState = yield select();
const actionToWidgetIdMap: any = {};
Object.keys(data.entities.canvasWidgets).forEach(widgetId => {
const widget = data.entities.canvasWidgets[widgetId];
const dynamicBindings = widget.dynamicBindings || {};
Object.keys(dynamicBindings).forEach(key => {
const dynamicBindingString = widget[key];
const binding = getDynamicBindings(dynamicBindingString);
const firstElementInPathList = binding.paths.map(
path => path.split(".")[0],
);
firstElementInPathList.forEach(name => {
const action = data.entities.actions.data.find(
action => action.name === name,
);
if (action) {
if (actionToWidgetIdMap[action.id] === undefined) {
actionToWidgetIdMap[action.id] = [];
}
if (!_.includes(actionToWidgetIdMap[action.id], widgetId)) {
actionToWidgetIdMap[action.id].push(widgetId);
}
}
});
});
});
yield put(actionToWidgetIdMapSuccess(actionToWidgetIdMap));
}
function getActionsForPaths(
paths: string[],
actions: RestAction[],
): RestAction[] {
// Let's say dynamic binding = {{UsersTable.selectedRow}}
return (
paths // Get UsersTable
.map(path => path.split(".")[0])
// Check if the UsersTable is an action or not
.map(apiName => {
const action = actions.find(action => action.name === apiName);
return action ? action : null;
})
// Filter the null generated by non actions
.filter(action => action !== null) as RestAction[]
);
}
function removeWidgetForActionId(
actionToWidgetIdMap: ActionWidgetIdsMap,
actionId: string,
widgetId: string,
) {
actionToWidgetIdMap[actionId] = actionToWidgetIdMap[actionId].filter(
actionWidgetId => actionWidgetId !== widgetId,
);
}
function removeWidgetFromUnboundAction(
boundActions: RestAction[],
actionToWidgetIdMap: ActionWidgetIdsMap,
widgetId: string,
) {
const mapActionIds = Object.keys(actionToWidgetIdMap);
mapActionIds.forEach(mapActionId => {
const actionFound = boundActions.find(boundAction => {
return boundAction.id === mapActionId;
});
if (!actionFound) {
removeWidgetForActionId(actionToWidgetIdMap, mapActionId, widgetId);
}
});
}
function addWidgetToBoundAction(
boundActions: RestAction[],
actionToWidgetIdMap: ActionWidgetIdsMap,
widgetId: string,
) {
boundActions.forEach(boundAction => {
// Initiate if undefined
if (actionToWidgetIdMap[boundAction.id] === undefined) {
actionToWidgetIdMap[boundAction.id] = [];
}
// Make sure you don't push any duplicate widgetIds inside the map
if (!_.includes(actionToWidgetIdMap[boundAction.id], widgetId)) {
actionToWidgetIdMap[boundAction.id].push(widgetId);
}
});
}
function* updateAction(reduxAction: ReduxAction<UpdateWidgetPropertyPayload>) {
const data: AppState = yield select();
const actionsData = data.entities.actions;
const allActions = actionsData.data;
const actionToWidgetIdMap = {
...actionsData.actionToWidgetIdMap,
};
const { widgetId } = reduxAction.payload;
const widget = data.entities.canvasWidgets[widgetId];
const widgetDynamicBindings = widget.dynamicBindings as Record<
string,
boolean
>;
if (widgetDynamicBindings !== undefined) {
const paths = Object.keys(widgetDynamicBindings)
.map(propName => {
const dynamicBindingString = widget[propName];
return getDynamicBindings(dynamicBindingString);
})
.map(obj => obj.paths)
.flat();
const boundActions = getActionsForPaths(paths, allActions);
addWidgetToBoundAction(boundActions, actionToWidgetIdMap, widgetId);
removeWidgetFromUnboundAction(boundActions, actionToWidgetIdMap, widgetId);
}
yield put(
actionToWidgetIdMapSuccess({
...data.entities.actions.actionToWidgetIdMap,
...actionToWidgetIdMap,
}),
);
}
export function* watchPropertyAndBindingUpdate() {
yield takeLatest(
[
ReduxActionTypes.UPDATE_WIDGET_PROPERTY,
ReduxActionTypes.UPDATE_WIDGET_DYNAMIC_PROPERTY,
],
updateAction,
);
}
export default function* watchActionWidgetMapSagas() {
yield all([takeLatest(ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS, init)]);
}

View File

@ -35,7 +35,8 @@ const getApiDraft = (state: AppState, id: string) => {
return {};
};
const getActions = (state: AppState) => state.entities.actions.data;
const getActions = (state: AppState) =>
state.entities.actions.map(a => a.config);
const getLastUsedAction = (state: AppState) => state.ui.apiPane.lastUsed;

View File

@ -1,44 +0,0 @@
import { all, select, takeLatest, put, call, take } from "redux-saga/effects";
import { ReduxActionTypes } from "constants/ReduxActionConstants";
import { AppState } from "reducers";
import { bindingsMapSuccess } from "actions/bindingActions";
function* createUpdateBindingsMapData() {
const data: AppState = yield select();
const map: Record<string, string> = {};
data.entities.actions.data.forEach(action => {
map[action.name] = `$.apiData.${action.id}.body`;
});
Object.keys(data.entities.canvasWidgets).forEach(widgetId => {
const name = data.entities.canvasWidgets[widgetId].widgetName;
map[name] = `$.canvasWidgets.${widgetId}`;
});
yield put(bindingsMapSuccess(map));
}
// The listener will keep track of any action
// that requires an update of the action and
// then call the update function again
function* initListener() {
while (true) {
// list all actions types here
yield take([
ReduxActionTypes.INITIALIZE_EDITOR_SUCCESS,
ReduxActionTypes.CREATE_ACTION_SUCCESS,
ReduxActionTypes.UPDATE_ACTION_SUCCESS,
ReduxActionTypes.DELETE_ACTION_SUCCESS,
ReduxActionTypes.UPDATE_CANVAS,
ReduxActionTypes.SAVE_PAGE_INIT,
]);
yield call(createUpdateBindingsMapData);
}
}
export default function* watchBindingsSagas() {
yield all([
takeLatest(
ReduxActionTypes.CREATE_UPDATE_BINDINGS_MAP_LISTENER_INIT,
initListener,
),
]);
}

View File

@ -8,7 +8,6 @@ import {
import { fetchEditorConfigs } from "actions/configsActions";
import { fetchPageList } from "actions/pageActions";
import { fetchDatasources } from "actions/datasourcesActions";
import { initBindingMapListener } from "actions/bindingActions";
import { fetchPlugins } from "actions/pluginActions";
import { fetchActions } from "actions/actionActions";
@ -21,7 +20,6 @@ function* initializeEditorSaga(
put(fetchPlugins()),
put(fetchPageList(applicationId)),
put(fetchEditorConfigs()),
put(initBindingMapListener()),
put(fetchActions(applicationId)),
put(fetchDatasources()),
]);
@ -44,7 +42,6 @@ export function* initializeAppViewerSaga(
) {
const { applicationId } = action.payload;
yield all([
put(initBindingMapListener()),
put(fetchActions(applicationId)),
put(fetchPageList(applicationId)),
]);

View File

@ -8,10 +8,6 @@ import configsSagas from "./ConfigsSagas";
import applicationSagas from "./ApplicationSagas";
import { watchDatasourcesSagas } from "./DatasourcesSagas";
import initSagas from "./InitSagas";
import bindingsSagas from "./BindingsSagas";
import watchActionWidgetMapSagas, {
watchPropertyAndBindingUpdate,
} from "./ActionWidgetMapSagas";
import apiPaneSagas from "./ApiPaneSagas";
import userSagas from "./userSagas";
import pluginSagas from "./PluginSagas";
@ -28,9 +24,6 @@ export function* rootSaga() {
spawn(configsSagas),
spawn(watchDatasourcesSagas),
spawn(applicationSagas),
spawn(bindingsSagas),
spawn(watchActionWidgetMapSagas),
spawn(watchPropertyAndBindingUpdate),
spawn(apiPaneSagas),
spawn(userSagas),
spawn(pluginSagas),

View File

@ -1,11 +1,9 @@
import { AppState, DataTree } from "reducers";
import { ActionDataState } from "reducers/entityReducers/actionsReducer";
import { ActionResponse } from "api/ActionAPI";
export const getDataTree = (state: AppState): DataTree => state.entities;
export const getDynamicNames = (state: AppState): DataTree["nameBindings"] =>
state.entities.nameBindings;
export const getPluginIdOfName = (
state: AppState,
name: string,
@ -17,5 +15,15 @@ export const getPluginIdOfName = (
return plugin.id;
};
export const getActions = (state: AppState): ActionDataState["data"] =>
state.entities.actions.data;
export const getActions = (state: AppState): ActionDataState =>
state.entities.actions;
export const getActionResponses = (
state: AppState,
): Record<string, ActionResponse | undefined> => {
const responses: Record<string, ActionResponse | undefined> = {};
state.entities.actions.forEach(a => {
responses[a.config.id] = a.data;
});
return responses;
};

View File

@ -1,23 +1,33 @@
import { AppState, DataTree } from "reducers";
import { JSONPath } from "jsonpath-plus";
import { createSelector } from "reselect";
import { getActions, getDataTree } from "./entitiesSelector";
import { ActionDataState } from "reducers/entityReducers/actionsReducer";
import createCachedSelector from "re-reselect";
import { getEvaluatedDataTree } from "utils/DynamicBindingUtils";
import {
ENTITY_TYPE_ACTION,
ENTITY_TYPE_WIDGET,
} from "constants/entityConstants";
export type NameBindingsWithData = Record<string, object>;
export type NameBindingsWithData = { [key: string]: any };
export const getNameBindingsWithData = createSelector(
getDataTree,
(dataTree: DataTree): NameBindingsWithData => {
const nameBindingsWithData: Record<string, object> = {};
Object.keys(dataTree.nameBindings).forEach(key => {
const nameBindings = dataTree.nameBindings[key];
nameBindingsWithData[key] = JSONPath({
path: nameBindings,
json: dataTree,
})[0];
dataTree.actions.forEach(a => {
nameBindingsWithData[a.config.name] = {
...a,
data: a.data ? a.data.body : {},
__type: ENTITY_TYPE_ACTION,
};
});
Object.keys(dataTree.canvasWidgets).forEach(w => {
const widget = dataTree.canvasWidgets[w];
nameBindingsWithData[widget.widgetName] = {
...widget,
__type: ENTITY_TYPE_WIDGET,
};
});
return nameBindingsWithData;
},
@ -35,19 +45,21 @@ export const getParsedDataTree = createSelector(
export const getNameBindingsForAutocomplete = createCachedSelector(
getParsedDataTree,
getActions,
(dataTree: NameBindingsWithData, actions: ActionDataState["data"]) => {
(dataTree: NameBindingsWithData, actions: ActionDataState) => {
const cachedResponses: Record<string, any> = {};
if (actions && actions.length) {
actions.forEach(action => {
if (!(action.name in dataTree) && action.cacheResponse) {
if (!(action.config.name in dataTree) && action.config.cacheResponse) {
try {
cachedResponses[action.name] = JSON.parse(action.cacheResponse);
cachedResponses[action.config.name] = JSON.parse(
action.config.cacheResponse,
);
} catch (e) {
cachedResponses[action.name] = action.cacheResponse;
cachedResponses[action.config.name] = action.config.cacheResponse;
}
}
});
}
return { ...dataTree, ...cachedResponses };
},
)((state: AppState) => state.entities.actions.data.length);
)((state: AppState) => state.entities.actions.length);

View File

@ -6,6 +6,7 @@ import JSExecutionManagerSingleton from "jsExecution/JSExecutionManagerSingleton
import unescapeJS from "unescape-js";
import { NameBindingsWithData } from "selectors/nameBindingsWithDataSelector";
import toposort from "toposort";
import { ENTITY_TYPE_ACTION } from "constants/entityConstants";
export const removeBindingsFromObject = (obj: object) => {
const string = JSON.stringify(obj);
@ -207,10 +208,11 @@ export const getEvaluatedDataTree = (
dynamicDependencyMap,
parseValues,
);
const treeWithLoading = setTreeLoading(evaluatedTree, dynamicDependencyMap);
if (parseValues) {
return getParsedTree(evaluatedTree);
return getParsedTree(treeWithLoading);
} else {
return evaluatedTree;
return treeWithLoading;
}
};
@ -268,6 +270,59 @@ const calculateSubDependencies = (
return subDeps;
};
export const setTreeLoading = (
dataTree: NameBindingsWithData,
dependencyMap: Array<[string, string]>,
) => {
const result = _.cloneDeep(dataTree);
Object.keys(dataTree)
.filter(
e => dataTree[e].__type === ENTITY_TYPE_ACTION && dataTree[e].isLoading,
)
.reduce(
(allEntities: string[], curr) =>
allEntities.concat(getEntityDependencies(dependencyMap, curr)),
[],
)
.forEach(w => (result[w].isLoading = true));
return result;
};
export const getEntityDependencies = (
dependencyMap: Array<[string, string]>,
entity: string,
): Array<string> => {
const entityDeps: Record<string, string[]> = dependencyMap
.map(d => [d[1].split(".")[0], d[0].split(".")[0]])
.filter(d => d[0] !== d[1])
.reduce((deps: Record<string, string[]>, dep) => {
const key: string = dep[0];
const value: string = dep[1];
return {
...deps,
[key]: deps[key] ? deps[key].concat(value) : [value],
};
}, {});
if (entity in entityDeps) {
const recFind = (
keys: Array<string>,
deps: Record<string, string[]>,
): Array<string> => {
let allDeps: string[] = [];
keys.forEach(e => {
allDeps = allDeps.concat([e]);
if (e in deps) {
allDeps = allDeps.concat([...recFind(deps[e], deps)]);
}
});
return allDeps;
};
return recFind(entityDeps[entity], entityDeps);
}
return [];
};
export function dependencySortedEvaluateDataTree(
dataTree: NameBindingsWithData,
dependencyTree: Array<[string, string]>,

View File

@ -11,6 +11,7 @@ jest.mock("jsExecution/RealmExecutor", () => {
import {
dependencySortedEvaluateDataTree,
getDynamicValue,
getEntityDependencies,
parseDynamicString,
} from "./DynamicBindingUtils";
import { getNameBindingsWithData } from "selectors/nameBindingsWithDataSelector";
@ -23,35 +24,11 @@ beforeAll(() => {
it("Gets the value from the data tree", () => {
const dynamicBinding = "{{GetUsers.data}}";
const dataTree: Partial<DataTree> = {
apiData: {
id: {
body: {
data: "correct data",
},
headers: {},
statusCode: "0",
duration: "0",
size: "0",
},
someOtherId: {
body: {
data: "wrong data",
},
headers: {},
statusCode: "0",
duration: "0",
size: "0",
},
},
nameBindings: {
GetUsers: "$.apiData.id.body",
const nameBindingsWithData = {
GetUsers: {
data: "correct data",
},
};
const appState: Partial<AppState> = {
entities: dataTree as DataTree,
};
const nameBindingsWithData = getNameBindingsWithData(appState as AppState);
const actualValue = "correct data";
const value = getDynamicValue(dynamicBinding, nameBindingsWithData);
@ -88,44 +65,6 @@ describe.each([
});
});
it("Parse the dynamic string", () => {
const dynamicBinding = "{{GetUsers.data}}";
const dataTree: Partial<DataTree> = {
apiData: {
id: {
body: {
data: "correct data",
},
headers: {},
statusCode: "0",
duration: "0",
size: "0",
},
someOtherId: {
body: {
data: "wrong data",
},
headers: {},
statusCode: "0",
duration: "0",
size: "0",
},
},
nameBindings: {
GetUsers: "$.apiData.id.body",
},
};
const appState: Partial<AppState> = {
entities: dataTree as DataTree,
};
const nameBindingsWithData = getNameBindingsWithData(appState as AppState);
const actualValue = "correct data";
const value = getDynamicValue(dynamicBinding, nameBindingsWithData);
expect(value).toEqual(actualValue);
});
it("evaluates the data tree", () => {
const input = {
widget1: {
@ -165,3 +104,19 @@ it("evaluates the data tree", () => {
const result = dependencySortedEvaluateDataTree(input, dynamicBindings);
expect(result).toEqual(output);
});
it("finds dependencies of a entity", () => {
const depMap: Array<[string, string]> = [
["Widget5.text", "Widget2.data.visible"],
["Widget1.options", "Action1.data"],
["Widget2.text", "Widget1.selectedOption"],
["Widget3.text", "Widget4.selectedRow.name"],
["Widget6.label", "Action1.data.label"],
];
const entity = "Action1";
const result = ["Widget1", "Widget2", "Widget5", "Widget6"];
const actual = getEntityDependencies(depMap, entity);
expect(actual).toEqual(result);
});

View File

@ -256,7 +256,6 @@ export interface WidgetProps extends WidgetDataProps {
key?: string;
renderMode: RenderMode;
dynamicBindings?: Record<string, boolean>;
isLoading: boolean;
invalidProps?: Record<string, boolean>;
validationMessages?: Record<string, string>;
[key: string]: any;

View File

@ -9197,11 +9197,6 @@ jsonify@~0.0.0:
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
jsonpath-plus@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/jsonpath-plus/-/jsonpath-plus-1.1.0.tgz#7caaea4db88b761a0a3b55d715cb01eaa469dfa5"
integrity sha512-ydqTBOuLcFCUr9e7AxJlKCFgxzEQ03HjnIim0hJSdk2NxD8MOsaMOrRgP6XWEm5q3VuDY5+cRT1DM9vLlGo/qA==
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"