diff --git a/CHANGELOG.md b/CHANGELOG.md index b86cc849b41..12538bf5d46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [2.17.1](https://github.com/erxes/erxes/compare/2.17.0...2.17.1) (2025-09-10) + +### Features + +* enhance automation field generation and related value retrieval ([361f7de](https://github.com/erxes/erxes/commit/361f7de7a84fe8bf2411b92fbaa10f55da60a945)) +* **tickets:** allow system or owner users to see all tickets ([7441c63](https://github.com/erxes/erxes/commit/7441c63081842fa91e7888781962f5ad235769c4)) + +### Bug Fixes + +* correct string interpolation and formatting in Docker utility functions ([358b0fd](https://github.com/erxes/erxes/commit/358b0fdb3ef88ad4857294a6c2eb20dcb87d6425)) + ## [2.17.0](https://github.com/erxes/erxes/compare/2.16.7...2.17.0) (2025-09-09) ### Features diff --git a/cli/commands/docker/utils.js b/cli/commands/docker/utils.js index a9057765b04..877ef0caa6d 100644 --- a/cli/commands/docker/utils.js +++ b/cli/commands/docker/utils.js @@ -35,7 +35,7 @@ const commonEnvs = configs => { rabbitmq.server_address || db_server_address || (isSwarm ? "erxes-dbs_rabbitmq" : "rabbitmq") - }:${db_server_address ? RABBITMQ_PORT : 5672}/${rabbitmq.vhost}`; + }:${db_server_address ? RABBITMQ_PORT : 5672}/${rabbitmq.vhost}`; return { ...be_env, @@ -48,7 +48,7 @@ const commonEnvs = configs => { REDIS_PORT: db_server_address ? REDIS_PORT : 6379, REDIS_PASSWORD: redis.password || "", RABBITMQ_HOST: rabbitmq_host, - ELASTICSEARCH_URL: `http://elastic:${elasticsearch.password}${ + ELASTICSEARCH_URL: `http://elastic:${elasticsearch.password}@${ db_server_address || (isSwarm ? "erxes-dbs_elasticsearch" : "elasticsearch") }:9200`, @@ -82,7 +82,7 @@ const mongoEnv = (configs, plugin) => { db_server_address || (isSwarm ? "erxes-dbs_mongo" : "mongo") }:${ db_server_address ? MONGO_PORT : 27017 - }/${db_name}?authSource=admin&replicaSet=rs0`; + }/${db_name}?authSource=admin&replicaSet=rs0`; return mongo_url; }; @@ -347,7 +347,7 @@ const deployDbs = async () => { environment: { "discovery.type": "single-node", "xpack.security.enabled": "true", - "ELASTIC_PASSWORD": configs.elasticsearch.password + ELASTIC_PASSWORD: configs.elasticsearch.password }, ports: ["9200:9200"], networks: ["erxes"], @@ -582,10 +582,10 @@ const up = async ({ uis, downloadLocales, fromInstaller }) => { dockerComposeConfig.services.essyncer = { image: `erxes/essyncer:${essyncer_tag}`, environment: { - ELASTICSEARCH_URL: `http://elastic:${configs.elasticsearch.password}${ + ELASTICSEARCH_URL: `http://elastic:${configs.elasticsearch.password}@${ configs.db_server_address || (isSwarm ? "erxes-dbs_elasticsearch" : "elasticsearch") - }:9200`, + }:9200`, MONGO_URL: `${mongoEnv(configs)}${ (configs.essyncer || {}).mongoOptions || "" }` @@ -718,7 +718,8 @@ const up = async ({ uis, downloadLocales, fromInstaller }) => { { name: "form_submissions", schema: "{ 'value': { 'type': 'text' } }", - script: "if (ns.indexOf('form_submissions') > -1) { if (doc.value && typeof doc.value === 'object') { doc.value = JSON.stringify(doc.value) }}" + script: + "if (ns.indexOf('form_submissions') > -1) { if (doc.value && typeof doc.value === 'object') { doc.value = JSON.stringify(doc.value) }}" }, { name: "customers", diff --git a/cli/package.json b/cli/package.json index 72b2c1da8d9..b223d94a214 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "erxes", - "version": "2.0.3", + "version": "2.0.5", "description": "Source available experience management infrastructure. Pioneering the future of experiences with XOS (Experience Operating System)", "homepage": "https://erxes.io", "repository": "https://github.com/erxes/erxes", diff --git a/package.json b/package.json index a1c6d57f154..ea05a2b9eb0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "erxes", "private": true, - "version": "2.17.0", + "version": "2.17.1", "workspaces": [ "packages/*", "scripts" diff --git a/packages/api-utils/src/automations.ts b/packages/api-utils/src/automations.ts index d15caaef22b..266ebbe0e41 100644 --- a/packages/api-utils/src/automations.ts +++ b/packages/api-utils/src/automations.ts @@ -31,6 +31,20 @@ export const replacePlaceHolders = async ({ ); for (const fieldKey of fieldKeys) { + const replacedValue = await getRelatedValue( + models, + subdomain, + target, + fieldKey, + relatedValueProps + ); + + if (replacedValue) { + actionData[actionDataKey] = actionData[actionDataKey].replace( + `{{ ${fieldKey} }}`, + replacedValue + ); + } const targetKey = targetKeys.find( (targetKey) => targetKey === fieldKey ); diff --git a/packages/plugin-automations-api/src/common/email/generateEmailPayload.ts b/packages/plugin-automations-api/src/common/email/generateEmailPayload.ts index 7f63351b4eb..09e63c29226 100644 --- a/packages/plugin-automations-api/src/common/email/generateEmailPayload.ts +++ b/packages/plugin-automations-api/src/common/email/generateEmailPayload.ts @@ -49,7 +49,7 @@ export const generateEmailPayload = async ({ replacedContent = await replaceDocuments(subdomain, replacedContent, target); - const { subject, content } = await sendCommonMessage({ + const { subject, content = "" } = await sendCommonMessage({ subdomain, serviceName, action: "automations.replacePlaceHolders", @@ -81,6 +81,6 @@ export const generateEmailPayload = async ({ fromEmail: formatFromEmail(sender, fromUserEmail), toEmails: filterOutSenderEmail(toEmails, fromUserEmail), ccEmails: filterOutSenderEmail(ccEmails, fromUserEmail), - customHtml: content, + customHtml: content.replace(/{{\s*([^}]+)\s*}}/g, "-"), }; }; diff --git a/packages/plugin-tickets-api/src/automations/getRelatedValue.ts b/packages/plugin-tickets-api/src/automations/getRelatedValue.ts index e96fc9e92ca..50ca787f0cb 100644 --- a/packages/plugin-tickets-api/src/automations/getRelatedValue.ts +++ b/packages/plugin-tickets-api/src/automations/getRelatedValue.ts @@ -4,11 +4,20 @@ import { sendCommonMessage, sendCoreMessage } from "../messageBroker"; import { getEnv } from "@erxes/api-utils/src"; import * as moment from "moment"; +function resolvePlaceholder(obj, placeholder) { + const path = placeholder.replace(/[{}]/g, "").trim(); + + const keys = path.split("."); + + // Walk through object + return keys.reduce((acc, key) => acc && acc[key], obj); +} + export const getRelatedValue = async ( models: IModels, subdomain: string, target, - targetKey, + targetKey: string, relatedValueProps: any = {} ) => { if ( @@ -159,19 +168,69 @@ export const getRelatedValue = async ( } } - if ((targetKey || "").includes("createdBy.")) { - return await generateCreatedByFieldValue({ subdomain, target, targetKey }); + if ( + ["createdBy.", "modifiedBy."].some((key) => (targetKey || "").includes(key)) + ) { + const [targetField, userField, userSubField] = targetKey.split("."); + + const targetFieldMap = { + createdBy: "userId", + modifiedBy: "modifiedBy", + }; + + const user = (await sendCoreMessage({ + subdomain, + action: "users.findOne", + data: { _id: target[targetFieldMap[targetField]] }, + isRPC: true, + })) as { positionIds: string[] } & IUser; + return await generateUserFieldValue({ + subdomain, + user, + targetKey, + }); + } + + if ( + ["assignedUsers.", "watchedUsers."].some((key) => + (targetKey || "").includes(key) + ) + ) { + // your logic here + const [targetField, userField, userSubField] = targetKey.split("."); + + const fieldsMap = { + watchedUsers: "watchedUserIds", + assignedUsers: "assignedUserIds", + }; + + const users = (await sendCoreMessage({ + subdomain, + action: "users.find", + data: { _id: { $in: target[fieldsMap[targetField]] } }, + isRPC: true, + })) as ({ positionIds: string[] } & IUser)[]; + + return await Promise.all( + (users || []).map( + async (user) => + await generateUserFieldValue({ + subdomain, + user, + targetKey, + }) + ) + ); } if (targetKey.includes("customers.")) { - const result = await generateCustomersFielValue({ + return await generateCustomersFielValue({ target, targetKey, subdomain, }); - return result; } - if (targetKey.includes("customFieldsData.")) { + if (targetKey.startsWith("customFieldsData.")) { const [_, fieldId] = targetKey.split("customFieldsData."); return await generateCustomFieldsDataValue({ @@ -279,10 +338,6 @@ const generateCustomFieldsDataValue = async ({ data: { query: { _id: fieldId, - $or: [ - { type: "users" }, - { type: "input", validation: { $in: ["date", "datetime"] } }, - ], }, }, isRPC: true, @@ -344,7 +399,7 @@ const generateCustomersFielValue = async ({ subdomain: string; target: any; }) => { - const [_, fieldName, fieldId] = targetKey.split("."); + const [targetField, fieldName, fieldId] = targetKey.split("."); const customerIds = await sendCoreMessage({ subdomain, @@ -357,7 +412,6 @@ const generateCustomersFielValue = async ({ isRPC: true, defaultValue: [], }); - const customers: any[] = (await sendCoreMessage({ subdomain, @@ -395,42 +449,44 @@ const generateCustomersFielValue = async ({ } if (fieldName === "customFieldsData" && fieldId) { - return customers - .map((customer) => - generateCustomFieldsDataValue({ + const results = await Promise.all( + customers.map(async (customer) => { + return await generateCustomFieldsDataValue({ subdomain, fieldId, target: customer, targetKey, relatedValueProps: null, - }) - ) - .filter(Boolean) - .join(", "); + }); + }) + ); + + return results.filter(Boolean).join(", "); } return customers - .map((customer) => customer[fieldName]) + .map((customer) => + resolvePlaceholder( + customer, + targetKey.replace(new RegExp(`^${targetField}\\.`), "") + ) + ) .filter(Boolean) .join(", "); }; -const generateCreatedByFieldValue = async ({ +const generateUserFieldValue = async ({ targetKey, subdomain, - target, + + user, }: { targetKey: string; subdomain: string; - target: any; + + user: { positionIds: string[] } & IUser; }) => { - const [_, userField, userSubField] = targetKey.split("."); - const user = (await sendCoreMessage({ - subdomain, - action: "users.findOne", - data: { _id: target?.userId }, - isRPC: true, - })) as { positionIds: string[] } & IUser; + const [targetField, userField, userSubField] = targetKey.split("."); if (userField === "branch") { const branches = await sendCoreMessage({ @@ -482,10 +538,6 @@ const generateCreatedByFieldValue = async ({ return `${details?.operatorPhone || ""}`; } - if (userField === "email") { - return `${user?.email || "-"}`; - } - if (userField === "fullName") { const { details, username } = user || {}; return ( @@ -511,8 +563,30 @@ const generateCreatedByFieldValue = async ({ .filter(Boolean) .join(", "); } + return user?.details?.position; } + + if (userField === "customFieldsData" && userSubField) { + const result = await generateCustomFieldsDataValue({ + subdomain, + fieldId: userSubField, + target: user, + targetKey, + relatedValueProps: null, + }); + + return result; + } + + const replacedValue = resolvePlaceholder( + user, + targetKey.replace(new RegExp(`^${targetField}\\.`), "") + ); + + if (replacedValue) { + return replacedValue; + } }; const generateTotalAmount = (productsData) => { diff --git a/packages/plugin-tickets-api/src/automations/index.ts b/packages/plugin-tickets-api/src/automations/index.ts index 3888780fdb4..67655d9d8b2 100644 --- a/packages/plugin-tickets-api/src/automations/index.ts +++ b/packages/plugin-tickets-api/src/automations/index.ts @@ -82,7 +82,6 @@ export default { ['customers.fullName']: '-', ['branches.title']: '-', ['branches.parent']: '-', - link: '-', pipelineLabels: '-' }, diff --git a/packages/plugin-tickets-api/src/fieldUtils.ts b/packages/plugin-tickets-api/src/fieldUtils.ts index ec4c615a269..92f60dc9b19 100644 --- a/packages/plugin-tickets-api/src/fieldUtils.ts +++ b/packages/plugin-tickets-api/src/fieldUtils.ts @@ -305,62 +305,18 @@ export const generateFields = async ({ subdomain, data }) => { defaultValue: [], }); + const userFields = await sendCoreMessage({ + subdomain, + isRPC: true, + action: `fieldsCombinedByContentType`, + data: { + contentType: 'core:user', + }, + defaultValue: [], + }); + fields = [ ...fields, - { - _id: Math.random(), - name: 'createdBy.email', - label: 'Created by Email', - type: 'String', - }, - { - _id: Math.random(), - name: 'createdBy.fullName', - label: 'Created by Full Name', - type: 'String', - }, - { - _id: Math.random(), - name: 'createdBy.phone', - label: 'Created by Phone', - type: 'String', - }, - { - _id: Math.random(), - name: 'createdBy.branch', - label: 'Created by Branch', - type: 'String', - }, - { - _id: Math.random(), - name: 'createdBy.department', - label: 'Created by Department', - type: 'String', - }, - { - _id: Math.random(), - name: 'createdBy.position', - label: 'Created by Position', - type: 'String', - }, - { - _id: Math.random(), - name: 'customers.email', - label: 'Customers Email', - type: 'String', - }, - { - _id: Math.random(), - name: 'customers.phone', - label: 'Customers phone', - type: 'String', - }, - { - _id: Math.random(), - name: 'customers.fullName', - label: 'Customers FullName', - type: 'String', - }, { _id: Math.random(), name: 'link', @@ -397,6 +353,26 @@ export const generateFields = async ({ subdomain, data }) => { name: `customers.${customerField.name}`, label: `Customers ${customerField.label}`, })), + (userFields || []).map((userField) => ({ + ...userField, + name: `modifiedBy.${userField.name}`, + label: `Modified By ${userField.label}`, + })), + (userFields || []).map((userField) => ({ + ...userField, + name: `createdBy.${userField.name}`, + label: `Created By ${userField.label}`, + })), + (userFields || []).map((userField) => ({ + ...userField, + name: `assignedUsers.${userField.name}`, + label: `Assigned users ${userField.label}`, + })), + (userFields || []).map((userField) => ({ + ...userField, + name: `watchedUsers.${userField.name}`, + label: `Watched users ${userField.label}`, + })), ); } diff --git a/packages/plugin-tickets-api/src/graphql/resolvers/queries/utils.ts b/packages/plugin-tickets-api/src/graphql/resolvers/queries/utils.ts index 3b47986ecd4..45c6c5013ad 100644 --- a/packages/plugin-tickets-api/src/graphql/resolvers/queries/utils.ts +++ b/packages/plugin-tickets-api/src/graphql/resolvers/queries/utils.ts @@ -500,9 +500,12 @@ export const generateCommonFilters = async ( supervisorDepartmentIds.filter((id) => pipelineDepartmentIds.includes(id) ) || []; - const isEligibleSeeAllCards = (pipeline.excludeCheckUserIds || []).includes( - currentUserId - ); + + const isEligibleSeeAllCards = + user.role === "system" || + user.isOwner || + (pipeline.excludeCheckUserIds || []).includes(currentUserId); + if ( commonIds?.length > 0 && (pipeline.isCheckUser || pipeline.isCheckDepartment) &&