# Pull Request Template ## Description Moves the WhatsApp & Twilio content-template logic to the shared [`@chatwoot/utils`](https://github.com/chatwoot/utils) ([PR](https://github.com/chatwoot/utils/pull/62)) package so web and mobile share one implementation. The neutral core takes the raw template and returns `processed_params`, the same shape the web parsers already use, so it's a drop-in with no behavior change. - `templateHelper.js` / `URLHelper.js` → source `MEDIA_FORMATS`, `findComponentByType`, `processVariable`, `buildTemplateParameters`, `extractFilenameFromUrl` from the package - `inboxes.js` → filters with shared `isSendableTemplate` - `WhatsAppTemplateParser.vue` / `ContentTemplateParser.vue` → `isFormInvalid` and Twilio media helpers now use the shared `isWhatsAppComplete` / `isTwilioComplete` / `applyTwilioMediaFilename` > Depends on the `@chatwoot/utils` release adding the shared template API, bump `package.json` from `^0.0.55` to the published version before merge. Fixes [CW-7540](https://linear.app/chatwoot/issue/CW-7540/web-templates-integration-with-utils) ## Type of change - [x] Breaking change (Refactor) --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
163 lines
4.2 KiB
JavaScript
163 lines
4.2 KiB
JavaScript
export const frontendURL = (path, params) => {
|
|
const stringifiedParams = params ? `?${new URLSearchParams(params)}` : '';
|
|
return `/app/${path}${stringifiedParams}`;
|
|
};
|
|
|
|
export const conversationUrl = ({
|
|
accountId,
|
|
activeInbox,
|
|
id,
|
|
label,
|
|
teamId,
|
|
conversationType = '',
|
|
foldersId,
|
|
}) => {
|
|
let url = `accounts/${accountId}/conversations/${id}`;
|
|
if (activeInbox) {
|
|
url = `accounts/${accountId}/inbox/${activeInbox}/conversations/${id}`;
|
|
} else if (label) {
|
|
url = `accounts/${accountId}/label/${label}/conversations/${id}`;
|
|
} else if (teamId) {
|
|
url = `accounts/${accountId}/team/${teamId}/conversations/${id}`;
|
|
} else if (foldersId && foldersId !== 0) {
|
|
url = `accounts/${accountId}/custom_view/${foldersId}/conversations/${id}`;
|
|
} else if (conversationType === 'mention') {
|
|
url = `accounts/${accountId}/mentions/conversations/${id}`;
|
|
} else if (conversationType === 'participating') {
|
|
url = `accounts/${accountId}/participating/conversations/${id}`;
|
|
} else if (conversationType === 'unattended') {
|
|
url = `accounts/${accountId}/unattended/conversations/${id}`;
|
|
}
|
|
return url;
|
|
};
|
|
|
|
export const conversationListPageURL = ({
|
|
accountId,
|
|
conversationType = '',
|
|
inboxId,
|
|
label,
|
|
teamId,
|
|
customViewId,
|
|
}) => {
|
|
let url = `accounts/${accountId}/dashboard`;
|
|
if (label) {
|
|
url = `accounts/${accountId}/label/${label}`;
|
|
} else if (teamId) {
|
|
url = `accounts/${accountId}/team/${teamId}`;
|
|
} else if (inboxId) {
|
|
url = `accounts/${accountId}/inbox/${inboxId}`;
|
|
} else if (customViewId) {
|
|
url = `accounts/${accountId}/custom_view/${customViewId}`;
|
|
} else if (conversationType) {
|
|
const urlMap = {
|
|
mention: 'mentions/conversations',
|
|
participating: 'participating/conversations',
|
|
unattended: 'unattended/conversations',
|
|
};
|
|
url = `accounts/${accountId}/${urlMap[conversationType]}`;
|
|
}
|
|
return frontendURL(url);
|
|
};
|
|
|
|
export const isValidURL = value => {
|
|
/* eslint-disable no-useless-escape */
|
|
const URL_REGEX =
|
|
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;
|
|
return URL_REGEX.test(value);
|
|
};
|
|
|
|
export const getArticleSearchURL = ({
|
|
host,
|
|
portalSlug,
|
|
pageNumber,
|
|
locale,
|
|
status,
|
|
authorId,
|
|
categorySlug,
|
|
sort,
|
|
query,
|
|
}) => {
|
|
const queryParams = new URLSearchParams({});
|
|
|
|
const params = {
|
|
page: pageNumber,
|
|
locale,
|
|
status,
|
|
author_id: authorId,
|
|
category_slug: categorySlug,
|
|
sort,
|
|
query,
|
|
};
|
|
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
if (value !== null && value !== undefined) {
|
|
queryParams.set(key, value);
|
|
}
|
|
});
|
|
|
|
return `${host}/${portalSlug}/articles?${queryParams.toString()}`;
|
|
};
|
|
|
|
export const hasValidAvatarUrl = avatarUrl => {
|
|
try {
|
|
const { host: avatarUrlHost } = new URL(avatarUrl);
|
|
const isFromGravatar = ['www.gravatar.com', 'gravatar'].includes(
|
|
avatarUrlHost
|
|
);
|
|
return avatarUrl && !isFromGravatar;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const timeStampAppendedURL = dataUrl => {
|
|
const url = new URL(dataUrl);
|
|
if (!url.searchParams.has('t')) {
|
|
url.searchParams.append('t', Date.now());
|
|
}
|
|
|
|
return url.toString();
|
|
};
|
|
|
|
export const getHostNameFromURL = url => {
|
|
try {
|
|
return new URL(url).hostname;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// Shared with the mobile app via @chatwoot/utils.
|
|
export { extractFilenameFromUrl } from '@chatwoot/utils';
|
|
|
|
/**
|
|
* Normalizes a comma/newline separated list of domains
|
|
* @param {string} domains - The comma/newline separated list of domains
|
|
* @returns {string} - The normalized list of domains
|
|
* - Converts newlines to commas
|
|
* - Trims whitespace
|
|
* - Lowercases entries
|
|
* - Removes empty values
|
|
* - De-duplicates while preserving original order
|
|
*/
|
|
export const sanitizeAllowedDomains = domains => {
|
|
if (!domains) return '';
|
|
|
|
const tokens = domains
|
|
.replace(/\r\n/g, '\n')
|
|
.replace(/\s*\n\s*/g, ',')
|
|
.split(',')
|
|
.map(d => d.trim().toLowerCase())
|
|
.filter(d => d.length > 0);
|
|
|
|
// De-duplicate while preserving order using Set and filter index
|
|
const seen = new Set();
|
|
const unique = tokens.filter(d => {
|
|
if (seen.has(d)) return false;
|
|
seen.add(d);
|
|
return true;
|
|
});
|
|
|
|
return unique.join(',');
|
|
};
|