mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(Jira Software Node): Use resource locator component (#5090)
* ⚡️Issue -> Create -> parameter Project RLC * 🔥removed unused loadOptions getProjects * ⚡️Issue -> Create -> parameter Issue Type RLC * 🔥removed unused loadOptions getIssueTypes * ⚡️Issue -> Create/Update -> parameter Assignee RLC * ⚡️Issue -> Create/Update -> parameter Reporter RLC * ⚡️Issue -> Create/Update -> parameter Priority RLC * 🔥removed unused loadOptions getPriorities * ⚡️Issue -> Update -> parameter Status RLC * 🔥removed unused loadOptions getTransitions * 🎨 fix typos * ⚡️Issue -> Create/Update -> Custom Fields parameter Field RLC * 🔥removed unused loadOptions getCustomFields * 🥅 throw custom error for "Field priority cannot be set" * 🚨 fix linter error * ⚡ removed ts-ignore * ⚡ removed ts-ignore Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
@@ -7,6 +7,8 @@ import {
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
INodeListSearchItems,
|
||||
INodeListSearchResult,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
@@ -14,6 +16,7 @@ import {
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
filterSortSearchListItems,
|
||||
jiraSoftwareCloudApiRequest,
|
||||
jiraSoftwareCloudApiRequestAllItems,
|
||||
simplifyIssueOutput,
|
||||
@@ -129,11 +132,14 @@ export class Jira implements INodeType {
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
listSearch: {
|
||||
// Get all the projects to display them to user so that he can
|
||||
// select them easily
|
||||
async getProjects(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
async getProjects(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const returnData: INodeListSearchItems[] = [];
|
||||
const jiraVersion = this.getCurrentNodeParameter('jiraVersion') as string;
|
||||
let endpoint = '';
|
||||
let projects;
|
||||
@@ -163,24 +169,14 @@ export class Jira implements INodeType {
|
||||
});
|
||||
}
|
||||
|
||||
returnData.sort((a, b) => {
|
||||
if (a.name < b.name) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return returnData;
|
||||
return { results: filterSortSearchListItems(returnData, filter) };
|
||||
},
|
||||
|
||||
// Get all the issue types to display them to user so that he can
|
||||
// select them easily
|
||||
async getIssueTypes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const projectId = this.getCurrentNodeParameter('project');
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
async getIssueTypes(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
|
||||
const projectId = this.getCurrentNodeParameter('project', { extractValue: true });
|
||||
const returnData: INodeListSearchItems[] = [];
|
||||
const { issueTypes } = await jiraSoftwareCloudApiRequest.call(
|
||||
this,
|
||||
`/api/2/project/${projectId}`,
|
||||
@@ -204,9 +200,146 @@ export class Jira implements INodeType {
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return returnData;
|
||||
return { results: returnData };
|
||||
},
|
||||
|
||||
// Get all the users to display them to user so that he can
|
||||
// select them easily
|
||||
async getUsers(this: ILoadOptionsFunctions, filter?: string): Promise<INodeListSearchResult> {
|
||||
const jiraVersion = this.getCurrentNodeParameter('jiraVersion') as string;
|
||||
const query: IDataObject = {};
|
||||
let endpoint = '/api/2/users/search';
|
||||
|
||||
if (jiraVersion === 'server') {
|
||||
endpoint = '/api/2/user/search';
|
||||
query.username = "'";
|
||||
}
|
||||
|
||||
const users = await jiraSoftwareCloudApiRequest.call(this, endpoint, 'GET', {}, query);
|
||||
const returnData: INodeListSearchItems[] = users.reduce(
|
||||
(activeUsers: INodeListSearchItems[], user: IDataObject) => {
|
||||
if (user.active) {
|
||||
activeUsers.push({
|
||||
name: user.displayName as string,
|
||||
value: (user.accountId ?? user.name) as string,
|
||||
});
|
||||
}
|
||||
return activeUsers;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return { results: filterSortSearchListItems(returnData, filter) };
|
||||
},
|
||||
|
||||
// Get all the priorities to display them to user so that he can
|
||||
// select them easily
|
||||
async getPriorities(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
|
||||
const returnData: INodeListSearchItems[] = [];
|
||||
|
||||
const priorities = await jiraSoftwareCloudApiRequest.call(this, '/api/2/priority', 'GET');
|
||||
|
||||
for (const priority of priorities) {
|
||||
const priorityName = priority.name;
|
||||
const priorityId = priority.id;
|
||||
|
||||
returnData.push({
|
||||
name: priorityName,
|
||||
value: priorityId,
|
||||
});
|
||||
}
|
||||
|
||||
returnData.sort((a, b) => {
|
||||
if (a.name < b.name) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return { results: returnData };
|
||||
},
|
||||
|
||||
// Get all the transitions (status) to display them to user so that he can
|
||||
// select them easily
|
||||
async getTransitions(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
|
||||
const returnData: INodeListSearchItems[] = [];
|
||||
|
||||
const issueKey = this.getCurrentNodeParameter('issueKey');
|
||||
const transitions = await jiraSoftwareCloudApiRequest.call(
|
||||
this,
|
||||
`/api/2/issue/${issueKey}/transitions`,
|
||||
'GET',
|
||||
);
|
||||
|
||||
for (const transition of transitions.transitions) {
|
||||
returnData.push({
|
||||
name: transition.name,
|
||||
value: transition.id,
|
||||
});
|
||||
}
|
||||
|
||||
returnData.sort((a, b) => {
|
||||
if (a.name < b.name) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return { results: returnData };
|
||||
},
|
||||
|
||||
// Get all the custom fields to display them to user so that he can
|
||||
// select them easily
|
||||
async getCustomFields(this: ILoadOptionsFunctions): Promise<INodeListSearchResult> {
|
||||
const returnData: INodeListSearchItems[] = [];
|
||||
const operation = this.getCurrentNodeParameter('operation') as string;
|
||||
let projectId: string;
|
||||
let issueTypeId: string;
|
||||
if (operation === 'create') {
|
||||
projectId = this.getCurrentNodeParameter('project', { extractValue: true }) as string;
|
||||
issueTypeId = this.getCurrentNodeParameter('issueType', { extractValue: true }) as string;
|
||||
} else {
|
||||
const issueKey = this.getCurrentNodeParameter('issueKey') as string;
|
||||
const res = await jiraSoftwareCloudApiRequest.call(
|
||||
this,
|
||||
`/api/2/issue/${issueKey}`,
|
||||
'GET',
|
||||
{},
|
||||
{},
|
||||
);
|
||||
projectId = res.fields.project.id;
|
||||
issueTypeId = res.fields.issuetype.id;
|
||||
}
|
||||
|
||||
const res = await jiraSoftwareCloudApiRequest.call(
|
||||
this,
|
||||
`/api/2/issue/createmeta?projectIds=${projectId}&issueTypeIds=${issueTypeId}&expand=projects.issuetypes.fields`,
|
||||
'GET',
|
||||
);
|
||||
|
||||
const fields = res.projects
|
||||
.find((o: any) => o.id === projectId)
|
||||
.issuetypes.find((o: any) => o.id === issueTypeId).fields;
|
||||
|
||||
for (const key of Object.keys(fields)) {
|
||||
const field = fields[key];
|
||||
if (field.schema && Object.keys(field.schema).includes('customId')) {
|
||||
returnData.push({
|
||||
name: field.name,
|
||||
value: field.key || field.fieldId,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { results: returnData };
|
||||
},
|
||||
},
|
||||
loadOptions: {
|
||||
// Get all the labels to display them to user so that he can
|
||||
// select them easily
|
||||
async getLabels(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
@@ -237,36 +370,6 @@ export class Jira implements INodeType {
|
||||
return returnData;
|
||||
},
|
||||
|
||||
// Get all the priorities to display them to user so that he can
|
||||
// select them easily
|
||||
async getPriorities(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
|
||||
const priorities = await jiraSoftwareCloudApiRequest.call(this, '/api/2/priority', 'GET');
|
||||
|
||||
for (const priority of priorities) {
|
||||
const priorityName = priority.name;
|
||||
const priorityId = priority.id;
|
||||
|
||||
returnData.push({
|
||||
name: priorityName,
|
||||
value: priorityId,
|
||||
});
|
||||
}
|
||||
|
||||
returnData.sort((a, b) => {
|
||||
if (a.name < b.name) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return returnData;
|
||||
},
|
||||
|
||||
// Get all the users to display them to user so that he can
|
||||
// select them easily
|
||||
async getUsers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
@@ -326,90 +429,12 @@ export class Jira implements INodeType {
|
||||
return returnData;
|
||||
},
|
||||
|
||||
// Get all the groups to display them to user so that he can
|
||||
// select them easily
|
||||
async getTransitions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
|
||||
const issueKey = this.getCurrentNodeParameter('issueKey');
|
||||
const transitions = await jiraSoftwareCloudApiRequest.call(
|
||||
this,
|
||||
`/api/2/issue/${issueKey}/transitions`,
|
||||
'GET',
|
||||
);
|
||||
|
||||
for (const transition of transitions.transitions) {
|
||||
returnData.push({
|
||||
name: transition.name,
|
||||
value: transition.id,
|
||||
});
|
||||
}
|
||||
|
||||
returnData.sort((a, b) => {
|
||||
if (a.name < b.name) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return returnData;
|
||||
},
|
||||
|
||||
// Get all the custom fields to display them to user so that he can
|
||||
// select them easily
|
||||
async getCustomFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const operation = this.getCurrentNodeParameter('operation') as string;
|
||||
let projectId: string;
|
||||
let issueTypeId: string;
|
||||
if (operation === 'create') {
|
||||
projectId = this.getCurrentNodeParameter('project') as string;
|
||||
issueTypeId = this.getCurrentNodeParameter('issueType') as string;
|
||||
} else {
|
||||
const issueKey = this.getCurrentNodeParameter('issueKey') as string;
|
||||
const res = await jiraSoftwareCloudApiRequest.call(
|
||||
this,
|
||||
`/api/2/issue/${issueKey}`,
|
||||
'GET',
|
||||
{},
|
||||
{},
|
||||
);
|
||||
projectId = res.fields.project.id;
|
||||
issueTypeId = res.fields.issuetype.id;
|
||||
}
|
||||
|
||||
const res = await jiraSoftwareCloudApiRequest.call(
|
||||
this,
|
||||
`/api/2/issue/createmeta?projectIds=${projectId}&issueTypeIds=${issueTypeId}&expand=projects.issuetypes.fields`,
|
||||
'GET',
|
||||
);
|
||||
|
||||
const fields = res.projects
|
||||
|
||||
.find((o: any) => o.id === projectId)
|
||||
|
||||
.issuetypes.find((o: any) => o.id === issueTypeId).fields;
|
||||
for (const key of Object.keys(fields)) {
|
||||
const field = fields[key];
|
||||
if (field.schema && Object.keys(field.schema).includes('customId')) {
|
||||
returnData.push({
|
||||
name: field.name,
|
||||
value: field.key || field.fieldId,
|
||||
});
|
||||
}
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
|
||||
// Get all the components to display them to user so that he can
|
||||
// select them easily
|
||||
async getProjectComponents(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
|
||||
const project = this.getCurrentNodeParameter('project');
|
||||
const project = this.getCurrentNodeParameter('project', { extractValue: true });
|
||||
const { values: components } = await jiraSoftwareCloudApiRequest.call(
|
||||
this,
|
||||
`/api/2/project/${project}/component`,
|
||||
@@ -454,9 +479,29 @@ export class Jira implements INodeType {
|
||||
if (operation === 'create') {
|
||||
for (let i = 0; i < length; i++) {
|
||||
const summary = this.getNodeParameter('summary', i) as string;
|
||||
const projectId = this.getNodeParameter('project', i) as string;
|
||||
const issueTypeId = this.getNodeParameter('issueType', i) as string;
|
||||
const projectId = this.getNodeParameter('project', i, '', {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
const issueTypeId = this.getNodeParameter('issueType', i, '', {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i);
|
||||
|
||||
const assignee = this.getNodeParameter('additionalFields.assignee', i, '', {
|
||||
extractValue: true,
|
||||
});
|
||||
if (assignee) additionalFields.assignee = assignee;
|
||||
|
||||
const reporter = this.getNodeParameter('additionalFields.reporter', i, '', {
|
||||
extractValue: true,
|
||||
});
|
||||
if (reporter) additionalFields.reporter = reporter;
|
||||
|
||||
const priority = this.getNodeParameter('additionalFields.priority', i, '', {
|
||||
extractValue: true,
|
||||
});
|
||||
if (priority) additionalFields.priority = priority;
|
||||
|
||||
const body: IIssue = {};
|
||||
const fields: IFields = {
|
||||
summary,
|
||||
@@ -513,6 +558,12 @@ export class Jira implements INodeType {
|
||||
const customFields = (additionalFields.customFieldsUi as IDataObject)
|
||||
.customFieldsValues as IDataObject[];
|
||||
if (customFields) {
|
||||
// resolve resource locator fieldId value
|
||||
customFields.forEach((cf) => {
|
||||
if (typeof cf.fieldId !== 'string') {
|
||||
cf.fieldId = ((cf.fieldId as IDataObject).value as string).trim();
|
||||
}
|
||||
});
|
||||
const data = customFields.reduce(
|
||||
(obj, value) => Object.assign(obj, { [`${value.fieldId}`]: value.fieldValue }),
|
||||
{},
|
||||
@@ -560,6 +611,27 @@ export class Jira implements INodeType {
|
||||
for (let i = 0; i < length; i++) {
|
||||
const issueKey = this.getNodeParameter('issueKey', i) as string;
|
||||
const updateFields = this.getNodeParameter('updateFields', i);
|
||||
|
||||
const assignee = this.getNodeParameter('updateFields.assignee', i, '', {
|
||||
extractValue: true,
|
||||
});
|
||||
if (assignee) updateFields.assignee = assignee;
|
||||
|
||||
const reporter = this.getNodeParameter('updateFields.reporter', i, '', {
|
||||
extractValue: true,
|
||||
});
|
||||
if (reporter) updateFields.reporter = reporter;
|
||||
|
||||
const priority = this.getNodeParameter('updateFields.priority', i, '', {
|
||||
extractValue: true,
|
||||
});
|
||||
if (priority) updateFields.priority = priority;
|
||||
|
||||
const statusId = this.getNodeParameter('updateFields.statusId', i, '', {
|
||||
extractValue: true,
|
||||
});
|
||||
if (statusId) updateFields.statusId = statusId;
|
||||
|
||||
const body: IIssue = {};
|
||||
const fields: IFields = {};
|
||||
if (updateFields.summary) {
|
||||
@@ -610,6 +682,12 @@ export class Jira implements INodeType {
|
||||
const customFields = (updateFields.customFieldsUi as IDataObject)
|
||||
.customFieldsValues as IDataObject[];
|
||||
if (customFields) {
|
||||
// resolve resource locator fieldId value
|
||||
customFields.forEach((cf) => {
|
||||
if (typeof cf.fieldId !== 'string') {
|
||||
cf.fieldId = ((cf.fieldId as IDataObject).value as string).trim();
|
||||
}
|
||||
});
|
||||
const data = customFields.reduce(
|
||||
(obj, value) => Object.assign(obj, { [`${value.fieldId}`]: value.fieldValue }),
|
||||
{},
|
||||
@@ -825,42 +903,39 @@ export class Jira implements INodeType {
|
||||
if (!jsonActive) {
|
||||
const notificationRecipientsValues = (
|
||||
this.getNodeParameter('notificationRecipientsUi', i) as IDataObject
|
||||
).notificationRecipientsValues as IDataObject[];
|
||||
).notificationRecipientsValues as IDataObject;
|
||||
const notificationRecipients: INotificationRecipients = {};
|
||||
if (notificationRecipientsValues) {
|
||||
// @ts-ignore
|
||||
if (notificationRecipientsValues.reporter) {
|
||||
// @ts-ignore
|
||||
notificationRecipients.reporter = notificationRecipientsValues.reporter as boolean;
|
||||
}
|
||||
// @ts-ignore
|
||||
|
||||
if (notificationRecipientsValues.assignee) {
|
||||
// @ts-ignore
|
||||
notificationRecipients.assignee = notificationRecipientsValues.assignee as boolean;
|
||||
}
|
||||
// @ts-ignore
|
||||
|
||||
if (notificationRecipientsValues.assignee) {
|
||||
// @ts-ignore
|
||||
notificationRecipients.watchers = notificationRecipientsValues.watchers as boolean;
|
||||
}
|
||||
// @ts-ignore
|
||||
|
||||
if (notificationRecipientsValues.voters) {
|
||||
// @ts-ignore
|
||||
notificationRecipients.watchers = notificationRecipientsValues.voters as boolean;
|
||||
}
|
||||
// @ts-ignore
|
||||
if (notificationRecipientsValues.users.length > 0) {
|
||||
// @ts-ignore
|
||||
notificationRecipients.users = notificationRecipientsValues.users.map((user) => {
|
||||
|
||||
if (((notificationRecipientsValues.users as IDataObject[]) || []).length > 0) {
|
||||
notificationRecipients.users = (
|
||||
notificationRecipientsValues.users as IDataObject[]
|
||||
).map((user) => {
|
||||
return {
|
||||
accountId: user,
|
||||
};
|
||||
});
|
||||
}
|
||||
// @ts-ignore
|
||||
if (notificationRecipientsValues.groups.length > 0) {
|
||||
// @ts-ignore
|
||||
notificationRecipients.groups = notificationRecipientsValues.groups.map((group) => {
|
||||
|
||||
if (((notificationRecipientsValues.groups as IDataObject[]) || []).length > 0) {
|
||||
notificationRecipients.groups = (
|
||||
notificationRecipientsValues.groups as IDataObject[]
|
||||
).map((group) => {
|
||||
return {
|
||||
name: group,
|
||||
};
|
||||
@@ -870,18 +945,20 @@ export class Jira implements INodeType {
|
||||
body.to = notificationRecipients;
|
||||
const notificationRecipientsRestrictionsValues = (
|
||||
this.getNodeParameter('notificationRecipientsRestrictionsUi', i) as IDataObject
|
||||
).notificationRecipientsRestrictionsValues as IDataObject[];
|
||||
).notificationRecipientsRestrictionsValues as IDataObject;
|
||||
const notificationRecipientsRestrictions: NotificationRecipientsRestrictions = {};
|
||||
if (notificationRecipientsRestrictionsValues) {
|
||||
// @ts-ignore
|
||||
if (notificationRecipientsRestrictionsValues.groups.length > 0) {
|
||||
notificationRecipientsRestrictions.groups =
|
||||
// @ts-ignore
|
||||
notificationRecipientsRestrictionsValues.groups.map((group) => {
|
||||
return {
|
||||
name: group,
|
||||
};
|
||||
});
|
||||
if (
|
||||
((notificationRecipientsRestrictionsValues.groups as IDataObject[]) || []).length >
|
||||
0
|
||||
) {
|
||||
notificationRecipientsRestrictions.groups = (
|
||||
notificationRecipientsRestrictionsValues.groups as IDataObject[]
|
||||
).map((group) => {
|
||||
return {
|
||||
name: group,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
body.restrict = notificationRecipientsRestrictions;
|
||||
@@ -1075,18 +1152,16 @@ export class Jira implements INodeType {
|
||||
'GET',
|
||||
{},
|
||||
{},
|
||||
// @ts-ignore
|
||||
attachment?.json.content,
|
||||
attachment?.json.content as string,
|
||||
{ json: false, encoding: null },
|
||||
);
|
||||
//@ts-ignore
|
||||
returnData[index].binary[binaryPropertyName] = await this.helpers.prepareBinaryData(
|
||||
buffer,
|
||||
// @ts-ignore
|
||||
attachment.json.filename,
|
||||
// @ts-ignore
|
||||
attachment.json.mimeType,
|
||||
);
|
||||
|
||||
(returnData[index].binary as IBinaryKeyData)[binaryPropertyName] =
|
||||
await this.helpers.prepareBinaryData(
|
||||
buffer,
|
||||
attachment.json.filename as string,
|
||||
attachment.json.mimeType as string,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1122,25 +1197,21 @@ export class Jira implements INodeType {
|
||||
const binaryPropertyName = this.getNodeParameter('binaryProperty', 0);
|
||||
for (const [index, attachment] of returnData.entries()) {
|
||||
returnData[index].binary = {};
|
||||
//@ts-ignore
|
||||
const buffer = await jiraSoftwareCloudApiRequest.call(
|
||||
this,
|
||||
'',
|
||||
'GET',
|
||||
{},
|
||||
{},
|
||||
// @ts-ignore
|
||||
attachment.json.content,
|
||||
attachment.json.content as string,
|
||||
{ json: false, encoding: null },
|
||||
);
|
||||
//@ts-ignore
|
||||
returnData[index].binary[binaryPropertyName] = await this.helpers.prepareBinaryData(
|
||||
buffer,
|
||||
// @ts-ignore
|
||||
attachment.json.filename,
|
||||
// @ts-ignore
|
||||
attachment.json.mimeType,
|
||||
);
|
||||
(returnData[index].binary as IBinaryKeyData)[binaryPropertyName] =
|
||||
await this.helpers.prepareBinaryData(
|
||||
buffer,
|
||||
attachment.json.filename as string,
|
||||
attachment.json.mimeType as string,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user