mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
rename workflow mappings inputs to local resource mappings
This commit is contained in:
parent
46b662fa03
commit
5efdde1f6d
|
@ -93,17 +93,15 @@ export class DynamicNodeParametersController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/workflow-input-mapping-fields')
|
@Post('/local-resource-mapper-fields')
|
||||||
async getWorkflowInputMappingFields(
|
async getLocalResourceMappingFields(req: DynamicNodeParametersRequest.ResourceMapperFields) {
|
||||||
req: DynamicNodeParametersRequest.WorkflowInputMappingFields,
|
|
||||||
) {
|
|
||||||
const { path, methodName, credentials, currentNodeParameters, nodeTypeAndVersion } = req.body;
|
const { path, methodName, credentials, currentNodeParameters, nodeTypeAndVersion } = req.body;
|
||||||
|
|
||||||
if (!methodName) throw new BadRequestError('Missing `methodName` in request body');
|
if (!methodName) throw new BadRequestError('Missing `methodName` in request body');
|
||||||
|
|
||||||
const additionalData = await getBase(req.user.id, currentNodeParameters);
|
const additionalData = await getBase(req.user.id, currentNodeParameters);
|
||||||
|
|
||||||
return await this.service.getWorkflowInputMappingFields(
|
return await this.service.getLocalResourceMappingFields(
|
||||||
methodName,
|
methodName,
|
||||||
path,
|
path,
|
||||||
additionalData,
|
additionalData,
|
||||||
|
|
|
@ -385,11 +385,6 @@ export declare namespace DynamicNodeParametersRequest {
|
||||||
methodName: string;
|
methodName: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
/** POST dynamic-node-parameters/workflow-input-mapping-fields */
|
|
||||||
type WorkflowInputMappingFields = BaseRequest<{
|
|
||||||
methodName: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
/** POST /dynamic-node-parameters/action-result */
|
/** POST /dynamic-node-parameters/action-result */
|
||||||
type ActionResult = BaseRequest<{
|
type ActionResult = BaseRequest<{
|
||||||
handler: string;
|
handler: string;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { LoadOptionsContext, NodeExecuteFunctions, WorkflowInputsContext } from 'n8n-core';
|
import { LoadOptionsContext, NodeExecuteFunctions, LocalLoadOptionsContext } from 'n8n-core';
|
||||||
import type {
|
import type {
|
||||||
ILoadOptions,
|
ILoadOptions,
|
||||||
ILoadOptionsFunctions,
|
ILoadOptionsFunctions,
|
||||||
|
@ -17,15 +17,15 @@ import type {
|
||||||
INodeTypeNameVersion,
|
INodeTypeNameVersion,
|
||||||
NodeParameterValueType,
|
NodeParameterValueType,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
IWorkflowInputsLoadOptionsFunctions,
|
ILocalLoadOptionsFunctions,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { Workflow, RoutingNode, ApplicationError } from 'n8n-workflow';
|
import { Workflow, RoutingNode, ApplicationError } from 'n8n-workflow';
|
||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
import { NodeTypes } from '@/node-types';
|
import { NodeTypes } from '@/node-types';
|
||||||
|
|
||||||
type WorkflowInputsMappingMethod = (
|
type LocalResourceMappingMethod = (
|
||||||
this: IWorkflowInputsLoadOptionsFunctions,
|
this: ILocalLoadOptionsFunctions,
|
||||||
) => Promise<ResourceMapperFields>;
|
) => Promise<ResourceMapperFields>;
|
||||||
type ListSearchMethod = (
|
type ListSearchMethod = (
|
||||||
this: ILoadOptionsFunctions,
|
this: ILoadOptionsFunctions,
|
||||||
|
@ -40,7 +40,7 @@ type ActionHandlerMethod = (
|
||||||
type ResourceMappingMethod = (this: ILoadOptionsFunctions) => Promise<ResourceMapperFields>;
|
type ResourceMappingMethod = (this: ILoadOptionsFunctions) => Promise<ResourceMapperFields>;
|
||||||
|
|
||||||
type NodeMethod =
|
type NodeMethod =
|
||||||
| WorkflowInputsMappingMethod
|
| LocalResourceMappingMethod
|
||||||
| ListSearchMethod
|
| ListSearchMethod
|
||||||
| LoadOptionsMethod
|
| LoadOptionsMethod
|
||||||
| ActionHandlerMethod
|
| ActionHandlerMethod
|
||||||
|
@ -183,7 +183,7 @@ export class DynamicNodeParametersService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the available workflow input mapping fields for the ResourceMapper component */
|
/** Returns the available workflow input mapping fields for the ResourceMapper component */
|
||||||
async getWorkflowInputMappingFields(
|
async getLocalResourceMappingFields(
|
||||||
methodName: string,
|
methodName: string,
|
||||||
path: string,
|
path: string,
|
||||||
additionalData: IWorkflowExecuteAdditionalData,
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
|
@ -192,9 +192,9 @@ export class DynamicNodeParametersService {
|
||||||
credentials?: INodeCredentials,
|
credentials?: INodeCredentials,
|
||||||
): Promise<ResourceMapperFields> {
|
): Promise<ResourceMapperFields> {
|
||||||
const nodeType = this.getNodeType(nodeTypeAndVersion);
|
const nodeType = this.getNodeType(nodeTypeAndVersion);
|
||||||
const method = this.getMethod('workflowInputsMapping', methodName, nodeType);
|
const method = this.getMethod('localResourceMapping', methodName, nodeType);
|
||||||
const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials);
|
const workflow = this.getWorkflow(nodeTypeAndVersion, currentNodeParameters, credentials);
|
||||||
const thisArgs = this.getWorkflowInputsContext(path, additionalData, workflow);
|
const thisArgs = this.getLocalLoadOptionsContext(path, additionalData, workflow);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
return method.call(thisArgs);
|
return method.call(thisArgs);
|
||||||
}
|
}
|
||||||
|
@ -223,10 +223,10 @@ export class DynamicNodeParametersService {
|
||||||
nodeType: INodeType,
|
nodeType: INodeType,
|
||||||
): ResourceMappingMethod;
|
): ResourceMappingMethod;
|
||||||
private getMethod(
|
private getMethod(
|
||||||
type: 'workflowInputsMapping',
|
type: 'localResourceMapping',
|
||||||
methodName: string,
|
methodName: string,
|
||||||
nodeType: INodeType,
|
nodeType: INodeType,
|
||||||
): WorkflowInputsMappingMethod;
|
): LocalResourceMappingMethod;
|
||||||
private getMethod(type: 'listSearch', methodName: string, nodeType: INodeType): ListSearchMethod;
|
private getMethod(type: 'listSearch', methodName: string, nodeType: INodeType): ListSearchMethod;
|
||||||
private getMethod(
|
private getMethod(
|
||||||
type: 'loadOptions',
|
type: 'loadOptions',
|
||||||
|
@ -241,7 +241,7 @@ export class DynamicNodeParametersService {
|
||||||
private getMethod(
|
private getMethod(
|
||||||
type:
|
type:
|
||||||
| 'resourceMapping'
|
| 'resourceMapping'
|
||||||
| 'workflowInputsMapping'
|
| 'localResourceMapping'
|
||||||
| 'listSearch'
|
| 'listSearch'
|
||||||
| 'loadOptions'
|
| 'loadOptions'
|
||||||
| 'actionHandler',
|
| 'actionHandler',
|
||||||
|
@ -297,12 +297,12 @@ export class DynamicNodeParametersService {
|
||||||
return new LoadOptionsContext(workflow, node, additionalData, path);
|
return new LoadOptionsContext(workflow, node, additionalData, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getWorkflowInputsContext(
|
private getLocalLoadOptionsContext(
|
||||||
path: string,
|
path: string,
|
||||||
additionalData: IWorkflowExecuteAdditionalData,
|
additionalData: IWorkflowExecuteAdditionalData,
|
||||||
workflow: Workflow,
|
workflow: Workflow,
|
||||||
) {
|
) {
|
||||||
const node = workflow.nodes['Temp-Node'];
|
const node = workflow.nodes['Temp-Node'];
|
||||||
return new WorkflowInputsContext(workflow, node, additionalData, path);
|
return new LocalLoadOptionsContext(workflow, node, additionalData, path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ export { ExecuteContext } from './execute-context';
|
||||||
export { ExecuteSingleContext } from './execute-single-context';
|
export { ExecuteSingleContext } from './execute-single-context';
|
||||||
export { HookContext } from './hook-context';
|
export { HookContext } from './hook-context';
|
||||||
export { LoadOptionsContext } from './load-options-context';
|
export { LoadOptionsContext } from './load-options-context';
|
||||||
export { WorkflowInputsContext } from './workflow-inputs-context';
|
export { LocalLoadOptionsContext } from './local-load-options-context';
|
||||||
export { PollContext } from './poll-context';
|
export { PollContext } from './poll-context';
|
||||||
export { SupplyDataContext } from './supply-data-context';
|
export { SupplyDataContext } from './supply-data-context';
|
||||||
export { TriggerContext } from './trigger-context';
|
export { TriggerContext } from './trigger-context';
|
||||||
|
|
|
@ -7,17 +7,17 @@ import type {
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
NodeParameterValueType,
|
NodeParameterValueType,
|
||||||
Workflow,
|
Workflow,
|
||||||
IWorkflowInputsLoadOptionsFunctions,
|
ILocalLoadOptionsFunctions,
|
||||||
FieldType,
|
FieldValueOption,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { extractValue } from '@/ExtractValue';
|
import { extractValue } from '@/ExtractValue';
|
||||||
|
|
||||||
import { NodeExecutionContext } from './node-execution-context';
|
import { NodeExecutionContext } from './node-execution-context';
|
||||||
|
|
||||||
export class WorkflowInputsContext
|
export class LocalLoadOptionsContext
|
||||||
extends NodeExecutionContext
|
extends NodeExecutionContext
|
||||||
implements IWorkflowInputsLoadOptionsFunctions
|
implements ILocalLoadOptionsFunctions
|
||||||
{
|
{
|
||||||
constructor(
|
constructor(
|
||||||
workflow: Workflow,
|
workflow: Workflow,
|
||||||
|
@ -28,12 +28,12 @@ export class WorkflowInputsContext
|
||||||
super(workflow, node, additionalData, 'internal');
|
super(workflow, node, additionalData, 'internal');
|
||||||
}
|
}
|
||||||
|
|
||||||
getWorkflowInputValues(): Array<{ name: string; type: FieldType }> {
|
getWorkflowInputValues(): FieldValueOption[] {
|
||||||
const { value } = this.getCurrentNodeParameter('workflowId') as INodeParameterResourceLocator;
|
const { value } = this.getCurrentNodeParameter('workflowId') as INodeParameterResourceLocator;
|
||||||
|
|
||||||
const workflowId = value as string;
|
const workflowId = value as string;
|
||||||
if (!workflowId) {
|
if (!workflowId) {
|
||||||
throw new ApplicationError('No workflowId defined on node!');
|
throw new ApplicationError('No workflowId parameter defined on node!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: load the inputs from the workflow
|
// TODO: load the inputs from the workflow
|
||||||
|
@ -41,6 +41,7 @@ export class WorkflowInputsContext
|
||||||
{ name: 'field1', type: 'string' as const },
|
{ name: 'field1', type: 'string' as const },
|
||||||
{ name: 'field2', type: 'number' as const },
|
{ name: 'field2', type: 'number' as const },
|
||||||
{ name: 'field3', type: 'boolean' as const },
|
{ name: 'field3', type: 'boolean' as const },
|
||||||
|
{ name: 'field4', type: 'any' as const },
|
||||||
];
|
];
|
||||||
|
|
||||||
return dummyFields;
|
return dummyFields;
|
|
@ -1275,10 +1275,6 @@ export declare namespace DynamicNodeParameters {
|
||||||
methodName: string;
|
methodName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkflowInputMappingFieldsRequest extends BaseRequest {
|
|
||||||
methodName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ActionResultRequest extends BaseRequest {
|
interface ActionResultRequest extends BaseRequest {
|
||||||
handler: string;
|
handler: string;
|
||||||
payload: IDataObject | string | undefined;
|
payload: IDataObject | string | undefined;
|
||||||
|
|
|
@ -59,14 +59,14 @@ export async function getResourceMapperFields(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getWorkflowInputFields(
|
export async function getLocalResourceMapperFields(
|
||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
sendData: DynamicNodeParameters.WorkflowInputMappingFieldsRequest,
|
sendData: DynamicNodeParameters.ResourceMapperFieldsRequest,
|
||||||
): Promise<ResourceMapperFields> {
|
): Promise<ResourceMapperFields> {
|
||||||
return await makeRestApiRequest(
|
return await makeRestApiRequest(
|
||||||
context,
|
context,
|
||||||
'POST',
|
'POST',
|
||||||
'/dynamic-node-parameters/workflow-input-mapping-fields',
|
'/dynamic-node-parameters/local-resource-mapper-fields',
|
||||||
sendData,
|
sendData,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -245,8 +245,8 @@ async function loadFieldsToMap(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceMapperMethod = props.parameter.typeOptions?.resourceMapper?.resourceMapperMethod;
|
const resourceMapperMethod = props.parameter.typeOptions?.resourceMapper?.resourceMapperMethod;
|
||||||
const workflowInputsMappingMethod =
|
const localResourceMapperMethod =
|
||||||
props.parameter.typeOptions?.resourceMapper?.workflowInputsMappingMethod;
|
props.parameter.typeOptions?.resourceMapper?.localResourceMapperMethod;
|
||||||
|
|
||||||
let fetchedFields = null;
|
let fetchedFields = null;
|
||||||
|
|
||||||
|
@ -266,8 +266,8 @@ async function loadFieldsToMap(): Promise<void> {
|
||||||
};
|
};
|
||||||
fetchedFields = await nodeTypesStore.getResourceMapperFields(requestParams);
|
fetchedFields = await nodeTypesStore.getResourceMapperFields(requestParams);
|
||||||
} else {
|
} else {
|
||||||
if (typeof workflowInputsMappingMethod === 'string') {
|
if (typeof localResourceMapperMethod === 'string') {
|
||||||
const requestParams: DynamicNodeParameters.WorkflowInputMappingFieldsRequest = {
|
const requestParams: DynamicNodeParameters.ResourceMapperFieldsRequest = {
|
||||||
nodeTypeAndVersion: {
|
nodeTypeAndVersion: {
|
||||||
name: props.node?.type,
|
name: props.node?.type,
|
||||||
version: props.node.typeVersion,
|
version: props.node.typeVersion,
|
||||||
|
@ -277,9 +277,9 @@ async function loadFieldsToMap(): Promise<void> {
|
||||||
props.node.parameters,
|
props.node.parameters,
|
||||||
) as INodeParameters,
|
) as INodeParameters,
|
||||||
path: props.path,
|
path: props.path,
|
||||||
methodName: workflowInputsMappingMethod,
|
methodName: localResourceMapperMethod,
|
||||||
};
|
};
|
||||||
fetchedFields = await nodeTypesStore.getWorkflowInputFields(requestParams);
|
fetchedFields = await nodeTypesStore.getLocalResourceMapperFields(requestParams);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -302,11 +302,11 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getWorkflowInputFields = async (
|
const getLocalResourceMapperFields = async (
|
||||||
sendData: DynamicNodeParameters.WorkflowInputMappingFieldsRequest,
|
sendData: DynamicNodeParameters.ResourceMapperFieldsRequest,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
return await nodeTypesApi.getWorkflowInputFields(rootStore.restApiContext, sendData);
|
return await nodeTypesApi.getLocalResourceMapperFields(rootStore.restApiContext, sendData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -336,7 +336,7 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
|
||||||
visibleNodeTypesByInputConnectionTypeNames,
|
visibleNodeTypesByInputConnectionTypeNames,
|
||||||
isConfigurableNode,
|
isConfigurableNode,
|
||||||
getResourceMapperFields,
|
getResourceMapperFields,
|
||||||
getWorkflowInputFields,
|
getLocalResourceMapperFields,
|
||||||
getNodeParameterActionResult,
|
getNodeParameterActionResult,
|
||||||
getResourceLocatorResults,
|
getResourceLocatorResults,
|
||||||
getNodeParameterOptions,
|
getNodeParameterOptions,
|
||||||
|
|
|
@ -201,7 +201,7 @@ export class ExecuteWorkflow implements INodeType {
|
||||||
typeOptions: {
|
typeOptions: {
|
||||||
loadOptionsDependsOn: ['workflowId.value'],
|
loadOptionsDependsOn: ['workflowId.value'],
|
||||||
resourceMapper: {
|
resourceMapper: {
|
||||||
workflowInputsMappingMethod: 'getWorkflowInputs',
|
localResourceMapperMethod: 'getWorkflowInputs',
|
||||||
valuesLabel: 'Workflow Inputs',
|
valuesLabel: 'Workflow Inputs',
|
||||||
mode: 'add',
|
mode: 'add',
|
||||||
fieldWords: {
|
fieldWords: {
|
||||||
|
@ -265,7 +265,7 @@ export class ExecuteWorkflow implements INodeType {
|
||||||
};
|
};
|
||||||
|
|
||||||
methods = {
|
methods = {
|
||||||
workflowInputsMapping: {
|
localResourceMapping: {
|
||||||
getWorkflowInputs,
|
getWorkflowInputs,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,24 +1,30 @@
|
||||||
import type {
|
import type {
|
||||||
FieldType,
|
FieldValueOption,
|
||||||
IWorkflowInputsLoadOptionsFunctions,
|
ILocalLoadOptionsFunctions,
|
||||||
ResourceMapperField,
|
ResourceMapperField,
|
||||||
ResourceMapperFields,
|
ResourceMapperFields,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
export async function getWorkflowInputs(
|
export async function getWorkflowInputs(
|
||||||
this: IWorkflowInputsLoadOptionsFunctions,
|
this: ILocalLoadOptionsFunctions,
|
||||||
): Promise<ResourceMapperFields> {
|
): Promise<ResourceMapperFields> {
|
||||||
const workflowInputs = this.getWorkflowInputValues() as Array<{ name: string; type: FieldType }>;
|
const workflowInputFields = this.getWorkflowInputValues() as FieldValueOption[];
|
||||||
|
|
||||||
const fields: ResourceMapperField[] = workflowInputs.map((currentWorkflowInput) => ({
|
const fields: ResourceMapperField[] = workflowInputFields.map((currentWorkflowInput) => {
|
||||||
|
const field: ResourceMapperField = {
|
||||||
id: currentWorkflowInput.name,
|
id: currentWorkflowInput.name,
|
||||||
displayName: currentWorkflowInput.name,
|
displayName: currentWorkflowInput.name,
|
||||||
required: false,
|
required: false,
|
||||||
defaultMatch: true,
|
defaultMatch: true,
|
||||||
display: true,
|
display: true,
|
||||||
type: currentWorkflowInput.type,
|
|
||||||
canBeUsedToMatch: true,
|
canBeUsedToMatch: true,
|
||||||
}));
|
};
|
||||||
|
|
||||||
|
if (currentWorkflowInput.type !== 'any') {
|
||||||
|
field.type = currentWorkflowInput.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return field;
|
||||||
|
});
|
||||||
return { fields };
|
return { fields };
|
||||||
}
|
}
|
||||||
|
|
|
@ -1072,7 +1072,7 @@ export interface ILoadOptionsFunctions extends FunctionsBase {
|
||||||
|
|
||||||
export type FieldValueOption = { name: string; type: FieldType | 'any' };
|
export type FieldValueOption = { name: string; type: FieldType | 'any' };
|
||||||
|
|
||||||
export interface IWorkflowInputsLoadOptionsFunctions {
|
export interface ILocalLoadOptionsFunctions {
|
||||||
getWorkflowInputValues(): FieldValueOption[];
|
getWorkflowInputValues(): FieldValueOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1374,14 +1374,14 @@ export interface ResourceMapperTypeOptionsBase {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enforce at least one of resourceMapperMethod or workflowInputsMappingMethod
|
// Enforce at least one of resourceMapperMethod or localResourceMapperMethod
|
||||||
export type ResourceMapperTypeOptions =
|
export type ResourceMapperTypeOptions =
|
||||||
| (ResourceMapperTypeOptionsBase & {
|
| (ResourceMapperTypeOptionsBase & {
|
||||||
resourceMapperMethod: string;
|
resourceMapperMethod: string;
|
||||||
workflowInputsMappingMethod?: never;
|
localResourceMapperMethod?: never;
|
||||||
})
|
})
|
||||||
| (ResourceMapperTypeOptionsBase & {
|
| (ResourceMapperTypeOptionsBase & {
|
||||||
workflowInputsMappingMethod: string;
|
localResourceMapperMethod: string;
|
||||||
resourceMapperMethod?: never;
|
resourceMapperMethod?: never;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1647,10 +1647,8 @@ export interface INodeType {
|
||||||
resourceMapping?: {
|
resourceMapping?: {
|
||||||
[functionName: string]: (this: ILoadOptionsFunctions) => Promise<ResourceMapperFields>;
|
[functionName: string]: (this: ILoadOptionsFunctions) => Promise<ResourceMapperFields>;
|
||||||
};
|
};
|
||||||
workflowInputsMapping?: {
|
localResourceMapping?: {
|
||||||
[functionName: string]: (
|
[functionName: string]: (this: ILocalLoadOptionsFunctions) => Promise<ResourceMapperFields>;
|
||||||
this: IWorkflowInputsLoadOptionsFunctions,
|
|
||||||
) => Promise<ResourceMapperFields>;
|
|
||||||
};
|
};
|
||||||
actionHandler?: {
|
actionHandler?: {
|
||||||
[functionName: string]: (
|
[functionName: string]: (
|
||||||
|
|
Loading…
Reference in a new issue