mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(editor): Fix SQL editor issue (#7236)
Github issue / Community forum post (link here to close automatically):
This commit is contained in:
parent
e8e44f6b6e
commit
647fc6c555
|
@ -1,9 +1,9 @@
|
||||||
// import {
|
import {
|
||||||
// HTTP_REQUEST_NODE_NAME,
|
HTTP_REQUEST_NODE_NAME,
|
||||||
// MANUAL_TRIGGER_NODE_NAME,
|
MANUAL_TRIGGER_NODE_NAME,
|
||||||
// PIPEDRIVE_NODE_NAME,
|
PIPEDRIVE_NODE_NAME,
|
||||||
// EDIT_FIELDS_SET_NODE_NAME,
|
EDIT_FIELDS_SET_NODE_NAME,
|
||||||
// } from '../constants';
|
} from '../constants';
|
||||||
import { WorkflowPage, NDV } from '../pages';
|
import { WorkflowPage, NDV } from '../pages';
|
||||||
|
|
||||||
const workflowPage = new WorkflowPage();
|
const workflowPage = new WorkflowPage();
|
||||||
|
@ -69,35 +69,33 @@ describe('Data pinning', () => {
|
||||||
ndv.getters.outputTbodyCell(1, 0).should('include.text', 1);
|
ndv.getters.outputTbodyCell(1, 0).should('include.text', 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
//TODO: Update Edit Fields (Set) node to a new version
|
it('Should be able to reference paired items in a node located before pinned data', () => {
|
||||||
// it('Should be able to reference paired items in a node located before pinned data', () => {
|
workflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||||
// workflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
workflowPage.actions.addNodeToCanvas(HTTP_REQUEST_NODE_NAME, true, true);
|
||||||
// workflowPage.actions.addNodeToCanvas(HTTP_REQUEST_NODE_NAME, true, true);
|
ndv.actions.setPinnedData([{ http: 123 }]);
|
||||||
// ndv.actions.setPinnedData([{ http: 123 }]);
|
ndv.actions.close();
|
||||||
// ndv.actions.close();
|
|
||||||
|
|
||||||
// workflowPage.actions.addNodeToCanvas(PIPEDRIVE_NODE_NAME, true, true);
|
workflowPage.actions.addNodeToCanvas(PIPEDRIVE_NODE_NAME, true, true);
|
||||||
// ndv.actions.setPinnedData(Array(3).fill({ pipedrive: 123 }));
|
ndv.actions.setPinnedData(Array(3).fill({ pipedrive: 123 }));
|
||||||
// ndv.actions.close();
|
ndv.actions.close();
|
||||||
|
|
||||||
// workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
|
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
|
||||||
// setExpressionOnStringValueInSet(`{{ $('${HTTP_REQUEST_NODE_NAME}').item`);
|
setExpressionOnStringValueInSet(`{{ $('${HTTP_REQUEST_NODE_NAME}').item`);
|
||||||
|
|
||||||
// const output = '[Object: {"json": {"http": 123}, "pairedItem": {"item": 0}}]';
|
const output = '[Object: {"json": {"http": 123}, "pairedItem": {"item": 0}}]';
|
||||||
|
|
||||||
// cy.get('div').contains(output).should('be.visible');
|
cy.get('div').contains(output).should('be.visible');
|
||||||
// });
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// function setExpressionOnStringValueInSet(expression: string) {
|
function setExpressionOnStringValueInSet(expression: string) {
|
||||||
// cy.get('button').contains('Execute node').click();
|
cy.get('button').contains('Execute node').click();
|
||||||
// cy.get('input[placeholder="Add Value"]').click();
|
cy.get('.fixed-collection-parameter > :nth-child(2) > .button > span').click();
|
||||||
// cy.get('span').contains('String').click();
|
|
||||||
|
|
||||||
// ndv.getters.nthParam(3).contains('Expression').invoke('show').click();
|
ndv.getters.nthParam(4).contains('Expression').invoke('show').click();
|
||||||
|
|
||||||
// ndv.getters
|
ndv.getters
|
||||||
// .inlineExpressionEditorInput()
|
.inlineExpressionEditorInput()
|
||||||
// .clear()
|
.clear()
|
||||||
// .type(expression, { parseSpecialCharSequences: false });
|
.type(expression, { parseSpecialCharSequences: false });
|
||||||
// }
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { WorkflowPage, NDV, CredentialsModal } from '../pages';
|
import { WorkflowPage, NDV, CredentialsModal } from '../pages';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
// import { cowBase64 } from '../support/binaryTestFiles';
|
import { cowBase64 } from '../support/binaryTestFiles';
|
||||||
import { BACKEND_BASE_URL } from '../constants';
|
import { BACKEND_BASE_URL, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
|
||||||
import { getVisibleSelect } from '../utils';
|
import { getVisibleSelect } from '../utils';
|
||||||
|
|
||||||
const workflowPage = new WorkflowPage();
|
const workflowPage = new WorkflowPage();
|
||||||
|
@ -102,39 +102,31 @@ describe('Webhook Trigger node', async () => {
|
||||||
simpleWebhookCall({ method: 'PUT', webhookPath: uuid(), executeNow: true });
|
simpleWebhookCall({ method: 'PUT', webhookPath: uuid(), executeNow: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
//TODO: Update Edit Fields (Set) node to a new version
|
it('should listen for a GET request and respond with Respond to Webhook node', () => {
|
||||||
// it('should listen for a GET request and respond with Respond to Webhook node', () => {
|
const webhookPath = uuid();
|
||||||
// const webhookPath = uuid();
|
simpleWebhookCall({
|
||||||
// simpleWebhookCall({
|
method: 'GET',
|
||||||
// method: 'GET',
|
webhookPath,
|
||||||
// webhookPath,
|
executeNow: false,
|
||||||
// executeNow: false,
|
respondWith: 'Respond to Webhook',
|
||||||
// respondWith: 'Respond to Webhook',
|
});
|
||||||
// });
|
|
||||||
|
|
||||||
// ndv.getters.backToCanvas().click();
|
ndv.getters.backToCanvas().click();
|
||||||
|
|
||||||
// workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
addEditFields();
|
||||||
// workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
|
|
||||||
// cy.get('.add-option').click();
|
|
||||||
// getVisibleSelect().find('.el-select-dropdown__item').contains('Number').click();
|
|
||||||
// cy.get('.fixed-collection-parameter')
|
|
||||||
// .getByTestId('parameter-input-name')
|
|
||||||
// .clear()
|
|
||||||
// .type('MyValue');
|
|
||||||
// cy.get('.fixed-collection-parameter').getByTestId('parameter-input-value').clear().type('1234');
|
|
||||||
// ndv.getters.backToCanvas().click({ force: true });
|
|
||||||
|
|
||||||
// workflowPage.actions.addNodeToCanvas('Respond to Webhook');
|
ndv.getters.backToCanvas().click({ force: true });
|
||||||
|
|
||||||
// workflowPage.actions.executeWorkflow();
|
workflowPage.actions.addNodeToCanvas('Respond to Webhook');
|
||||||
// cy.wait(waitForWebhook);
|
|
||||||
|
|
||||||
// cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => {
|
workflowPage.actions.executeWorkflow();
|
||||||
// expect(response.status).to.eq(200);
|
cy.wait(waitForWebhook);
|
||||||
// expect(response.body.MyValue).to.eq(1234);
|
|
||||||
// });
|
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => {
|
||||||
// });
|
expect(response.status).to.eq(200);
|
||||||
|
expect(response.body.MyValue).to.eq(1234);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should listen for a GET request and respond custom status code 201', () => {
|
it('should listen for a GET request and respond custom status code 201', () => {
|
||||||
const webhookPath = uuid();
|
const webhookPath = uuid();
|
||||||
|
@ -153,83 +145,64 @@ describe('Webhook Trigger node', async () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
//TODO: Update Edit Fields (Set) node to a new version
|
it('should listen for a GET request and respond with last node', () => {
|
||||||
// it('should listen for a GET request and respond with last node', () => {
|
const webhookPath = uuid();
|
||||||
// const webhookPath = uuid();
|
simpleWebhookCall({
|
||||||
// simpleWebhookCall({
|
method: 'GET',
|
||||||
// method: 'GET',
|
webhookPath,
|
||||||
// webhookPath,
|
executeNow: false,
|
||||||
// executeNow: false,
|
respondWith: 'Last Node',
|
||||||
// respondWith: 'Last Node',
|
});
|
||||||
// });
|
ndv.getters.backToCanvas().click();
|
||||||
// ndv.getters.backToCanvas().click();
|
|
||||||
|
|
||||||
// workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
addEditFields();
|
||||||
// workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
|
|
||||||
// cy.get('.add-option').click();
|
|
||||||
// getVisibleSelect().find('.el-select-dropdown__item').contains('Number').click();
|
|
||||||
// cy.get('.fixed-collection-parameter')
|
|
||||||
// .getByTestId('parameter-input-name')
|
|
||||||
// .find('input')
|
|
||||||
// .clear()
|
|
||||||
// .type('MyValue');
|
|
||||||
// cy.get('.fixed-collection-parameter')
|
|
||||||
// .getByTestId('parameter-input-value')
|
|
||||||
// .find('input')
|
|
||||||
// .clear()
|
|
||||||
// .type('1234');
|
|
||||||
// ndv.getters.backToCanvas().click({ force: true });
|
|
||||||
|
|
||||||
// workflowPage.actions.executeWorkflow();
|
ndv.getters.backToCanvas().click({ force: true });
|
||||||
// cy.wait(waitForWebhook);
|
|
||||||
|
|
||||||
// cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => {
|
workflowPage.actions.executeWorkflow();
|
||||||
// expect(response.status).to.eq(200);
|
cy.wait(waitForWebhook);
|
||||||
// expect(response.body.MyValue).to.eq(1234);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
//TODO: Update Edit Fields (Set) node to a new version
|
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => {
|
||||||
// it('should listen for a GET request and respond with last node binary data', () => {
|
expect(response.status).to.eq(200);
|
||||||
// const webhookPath = uuid();
|
expect(response.body.MyValue).to.eq(1234);
|
||||||
// simpleWebhookCall({
|
});
|
||||||
// method: 'GET',
|
});
|
||||||
// webhookPath,
|
|
||||||
// executeNow: false,
|
|
||||||
// respondWith: 'Last Node',
|
|
||||||
// responseData: 'First Entry Binary',
|
|
||||||
// });
|
|
||||||
// ndv.getters.backToCanvas().click();
|
|
||||||
|
|
||||||
// workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
it('should listen for a GET request and respond with last node binary data', () => {
|
||||||
// workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
|
const webhookPath = uuid();
|
||||||
// cy.get('.add-option').click();
|
simpleWebhookCall({
|
||||||
// getVisibleSelect().find('.el-select-dropdown__item').contains('String').click();
|
method: 'GET',
|
||||||
// cy.get('.fixed-collection-parameter').getByTestId('parameter-input-name').clear().type('data');
|
webhookPath,
|
||||||
// cy.get('.fixed-collection-parameter')
|
executeNow: false,
|
||||||
// .getByTestId('parameter-input-value')
|
respondWith: 'Last Node',
|
||||||
// .clear()
|
responseData: 'First Entry Binary',
|
||||||
// .find('input')
|
});
|
||||||
// .invoke('val', cowBase64)
|
ndv.getters.backToCanvas().click();
|
||||||
// .trigger('blur');
|
|
||||||
// ndv.getters.backToCanvas().click();
|
|
||||||
|
|
||||||
// workflowPage.actions.addNodeToCanvas('Move Binary Data');
|
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||||
// workflowPage.actions.zoomToFit();
|
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
|
||||||
|
cy.get('.fixed-collection-parameter > :nth-child(2) > .button > span').click();
|
||||||
|
ndv.getters.nthParam(2).type('data');
|
||||||
|
ndv.getters.nthParam(4).invoke('val', cowBase64).trigger('blur');
|
||||||
|
|
||||||
// workflowPage.actions.openNode('Move Binary Data');
|
ndv.getters.backToCanvas().click();
|
||||||
// cy.getByTestId('parameter-input-mode').click();
|
|
||||||
// getVisibleSelect().find('.option-headline').contains('JSON to Binary').click();
|
|
||||||
// ndv.getters.backToCanvas().click();
|
|
||||||
|
|
||||||
// workflowPage.actions.executeWorkflow();
|
workflowPage.actions.addNodeToCanvas('Move Binary Data');
|
||||||
// cy.wait(waitForWebhook);
|
workflowPage.actions.zoomToFit();
|
||||||
|
|
||||||
// cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => {
|
workflowPage.actions.openNode('Move Binary Data');
|
||||||
// expect(response.status).to.eq(200);
|
cy.getByTestId('parameter-input-mode').click();
|
||||||
// expect(Object.keys(response.body).includes('data')).to.be.true;
|
getVisibleSelect().find('.option-headline').contains('JSON to Binary').click();
|
||||||
// });
|
ndv.getters.backToCanvas().click();
|
||||||
// });
|
|
||||||
|
workflowPage.actions.executeWorkflow();
|
||||||
|
cy.wait(waitForWebhook);
|
||||||
|
|
||||||
|
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => {
|
||||||
|
expect(response.status).to.eq(200);
|
||||||
|
expect(Object.keys(response.body).includes('data')).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should listen for a GET request and respond with an empty body', () => {
|
it('should listen for a GET request and respond with an empty body', () => {
|
||||||
const webhookPath = uuid();
|
const webhookPath = uuid();
|
||||||
|
@ -332,3 +305,13 @@ describe('Webhook Trigger node', async () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const addEditFields = () => {
|
||||||
|
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||||
|
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
|
||||||
|
cy.get('.fixed-collection-parameter > :nth-child(2) > .button > span').click();
|
||||||
|
ndv.getters.nthParam(2).type('MyValue');
|
||||||
|
ndv.getters.nthParam(3).click();
|
||||||
|
cy.get('div').contains('Number').click();
|
||||||
|
ndv.getters.nthParam(4).type('1234');
|
||||||
|
};
|
||||||
|
|
26
cypress/e2e/29-sql-editor.cy.ts
Normal file
26
cypress/e2e/29-sql-editor.cy.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { WorkflowPage, NDV } from '../pages';
|
||||||
|
|
||||||
|
const workflowPage = new WorkflowPage();
|
||||||
|
const ndv = new NDV();
|
||||||
|
|
||||||
|
describe('SQL editors', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
workflowPage.actions.visit();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve changes when opening-closing Postgres node', () => {
|
||||||
|
workflowPage.actions.addInitialNodeToCanvas('Manual');
|
||||||
|
workflowPage.actions.addNodeToCanvas('Postgres');
|
||||||
|
workflowPage.actions.openNode('Postgres');
|
||||||
|
ndv.getters.parameterInput('operation').click();
|
||||||
|
cy.get('div').contains('Execute Query').click();
|
||||||
|
cy.get('div.cm-activeLine').type('SELECT * FROM `testTable`');
|
||||||
|
ndv.actions.close();
|
||||||
|
workflowPage.actions.openNode('Postgres');
|
||||||
|
cy.get('div.cm-activeLine').type('{end} LIMIT 10');
|
||||||
|
ndv.actions.close();
|
||||||
|
workflowPage.actions.openNode('Postgres');
|
||||||
|
|
||||||
|
cy.get('div.cm-activeLine').contains('SELECT * FROM `testTable` LIMIT 10');
|
||||||
|
});
|
||||||
|
});
|
|
@ -320,7 +320,7 @@ export default defineComponent({
|
||||||
...readOnlyEditorExtensions,
|
...readOnlyEditorExtensions,
|
||||||
EditorState.readOnly.of(isReadOnly),
|
EditorState.readOnly.of(isReadOnly),
|
||||||
EditorView.editable.of(!isReadOnly),
|
EditorView.editable.of(!isReadOnly),
|
||||||
codeNodeEditorTheme({ isReadOnly }),
|
codeNodeEditorTheme({ isReadOnly, customMinHeight: this.rows }),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!isReadOnly) {
|
if (!isReadOnly) {
|
||||||
|
@ -354,16 +354,8 @@ export default defineComponent({
|
||||||
const [languageSupport, ...otherExtensions] = this.languageExtensions;
|
const [languageSupport, ...otherExtensions] = this.languageExtensions;
|
||||||
extensions.push(this.languageCompartment.of(languageSupport), ...otherExtensions);
|
extensions.push(this.languageCompartment.of(languageSupport), ...otherExtensions);
|
||||||
|
|
||||||
let doc = this.modelValue ?? this.placeholder;
|
|
||||||
|
|
||||||
const lines = doc.split('\n');
|
|
||||||
|
|
||||||
if (lines.length < this.rows) {
|
|
||||||
doc += '\n'.repeat(this.rows - lines.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = EditorState.create({
|
const state = EditorState.create({
|
||||||
doc,
|
doc: this.modelValue ?? this.placeholder,
|
||||||
extensions,
|
extensions,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -32,9 +32,14 @@ const cssStyleDeclaration = getComputedStyle(document.documentElement);
|
||||||
interface ThemeSettings {
|
interface ThemeSettings {
|
||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
customMaxHeight?: string;
|
customMaxHeight?: string;
|
||||||
|
customMinHeight?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codeNodeEditorTheme = ({ isReadOnly, customMaxHeight }: ThemeSettings) => [
|
export const codeNodeEditorTheme = ({
|
||||||
|
isReadOnly,
|
||||||
|
customMaxHeight,
|
||||||
|
customMinHeight,
|
||||||
|
}: ThemeSettings) => [
|
||||||
EditorView.theme({
|
EditorView.theme({
|
||||||
'&': {
|
'&': {
|
||||||
'font-size': BASE_STYLING.fontSize,
|
'font-size': BASE_STYLING.fontSize,
|
||||||
|
@ -82,7 +87,9 @@ export const codeNodeEditorTheme = ({ isReadOnly, customMaxHeight }: ThemeSettin
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
|
|
||||||
maxHeight: customMaxHeight ?? '100%',
|
maxHeight: customMaxHeight ?? '100%',
|
||||||
...(isReadOnly ? {} : { minHeight: '1.3em' }),
|
...(isReadOnly
|
||||||
|
? {}
|
||||||
|
: { minHeight: customMinHeight ? `${Number(customMinHeight) * 1.3}em` : '10em' }),
|
||||||
},
|
},
|
||||||
'.cm-diagnosticAction': {
|
'.cm-diagnosticAction': {
|
||||||
backgroundColor: BASE_STYLING.diagnosticButton.backgroundColor,
|
backgroundColor: BASE_STYLING.diagnosticButton.backgroundColor,
|
||||||
|
|
|
@ -120,7 +120,7 @@
|
||||||
:dialect="getArgument('sqlDialect')"
|
:dialect="getArgument('sqlDialect')"
|
||||||
:isReadOnly="isReadOnly"
|
:isReadOnly="isReadOnly"
|
||||||
:rows="getArgument('rows')"
|
:rows="getArgument('rows')"
|
||||||
@valueChanged="valueChangedDebounced"
|
@update:modelValue="valueChangedDebounced"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<code-node-editor
|
<code-node-editor
|
||||||
|
|
|
@ -141,7 +141,11 @@ export default defineComponent({
|
||||||
const extensions = [
|
const extensions = [
|
||||||
sqlWithN8nLanguageSupport(),
|
sqlWithN8nLanguageSupport(),
|
||||||
expressionInputHandler(),
|
expressionInputHandler(),
|
||||||
codeNodeEditorTheme({ isReadOnly: this.isReadOnly, customMaxHeight: '350px' }),
|
codeNodeEditorTheme({
|
||||||
|
isReadOnly: this.isReadOnly,
|
||||||
|
customMaxHeight: '350px',
|
||||||
|
customMinHeight: this.rows,
|
||||||
|
}),
|
||||||
lineNumbers(),
|
lineNumbers(),
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
EditorState.readOnly.of(this.isReadOnly),
|
EditorState.readOnly.of(this.isReadOnly),
|
||||||
|
@ -188,15 +192,7 @@ export default defineComponent({
|
||||||
mounted() {
|
mounted() {
|
||||||
if (!this.isReadOnly) codeNodeEditorEventBus.on('error-line-number', this.highlightLine);
|
if (!this.isReadOnly) codeNodeEditorEventBus.on('error-line-number', this.highlightLine);
|
||||||
|
|
||||||
let doc = this.modelValue;
|
const state = EditorState.create({ doc: this.modelValue, extensions: this.extensions });
|
||||||
|
|
||||||
const lines = doc.split('\n');
|
|
||||||
|
|
||||||
if (lines.length < this.rows) {
|
|
||||||
doc += '\n'.repeat(this.rows - lines.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = EditorState.create({ doc, extensions: this.extensions });
|
|
||||||
|
|
||||||
this.editor = new EditorView({ parent: this.$refs.sqlEditor as HTMLDivElement, state });
|
this.editor = new EditorView({ parent: this.$refs.sqlEditor as HTMLDivElement, state });
|
||||||
this.editorState = this.editor.state;
|
this.editorState = this.editor.state;
|
||||||
|
|
Loading…
Reference in a new issue