Compare commits

...

10 Commits

Author SHA1 Message Date
Maksim Eltyshev
f6ea10df97 chore: Update version 2024-10-31 14:58:29 +01:00
Maksim Eltyshev
d9e8c24c3f fix: Save description when clicking outside 2024-10-31 14:56:11 +01:00
Maksim Eltyshev
f75b0237d3 fix: Include previous data state in webhook
Closes #809
2024-10-31 00:48:49 +01:00
Maksim Eltyshev
38bc4cb0a0 ref: Refactoring 2024-10-30 22:28:25 +01:00
Elllone
cc95032e74 feat: Telegram bot notifications (#928) 2024-10-30 22:11:52 +01:00
Maksim Eltyshev
1d2193c381 chore: Update version 2024-10-27 22:05:18 +01:00
dependabot[bot]
70f40e26af chore(deps): Bump http-proxy-middleware from 2.0.6 to 2.0.7 in /client (#922)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.6 to 2.0.7.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.7/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.6...v2.0.7)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-27 21:18:12 +01:00
Holden Hu
814b5810ac feat: Improve UX of comment actions (#924)
Closes #915
2024-10-27 21:17:08 +01:00
Zananok
f372113def feat: Add multiple task creation with Ctrl+Enter (#921) 2024-10-27 21:03:59 +01:00
leroyloren
14dff96434 fix: Update Czech translation (#920) 2024-10-24 15:39:20 +02:00
35 changed files with 365 additions and 79 deletions

View File

@@ -15,13 +15,13 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.2.12
version: 0.2.14
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.23.3"
appVersion: "1.23.5"
dependencies:
- alias: postgresql

View File

@@ -10956,9 +10956,9 @@
}
},
"node_modules/http-proxy-middleware": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
"integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==",
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
"integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
"dependencies": {
"@types/http-proxy": "^1.17.8",
"http-proxy": "^1.18.1",

View File

@@ -10,7 +10,7 @@ import { focusEnd } from '../../../utils/element-helpers';
import styles from './CommentEdit.module.scss';
const CommentEdit = React.forwardRef(({ children, defaultData, onUpdate }, ref) => {
const CommentEdit = React.forwardRef(({ defaultData, onUpdate, text, actions }, ref) => {
const [t] = useTranslation();
const [isOpened, setIsOpened] = useState(false);
const [data, handleFieldChange, setData] = useForm(null);
@@ -76,7 +76,12 @@ const CommentEdit = React.forwardRef(({ children, defaultData, onUpdate }, ref)
}, [isOpened]);
if (!isOpened) {
return children;
return (
<>
{actions}
{text}
</>
);
}
return (
@@ -101,9 +106,10 @@ const CommentEdit = React.forwardRef(({ children, defaultData, onUpdate }, ref)
});
CommentEdit.propTypes = {
children: PropTypes.element.isRequired,
defaultData: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onUpdate: PropTypes.func.isRequired,
text: PropTypes.element.isRequired,
actions: PropTypes.element.isRequired,
};
export default React.memo(CommentEdit);

View File

@@ -31,44 +31,51 @@ const ItemComment = React.memo(
<User name={user.name} avatarUrl={user.avatarUrl} />
</span>
<div className={classNames(styles.content)}>
<div className={styles.title}>
<span className={styles.author}>{user.name}</span>
<span className={styles.date}>
{t(`format:${getDateFormat(createdAt)}`, {
postProcess: 'formatDate',
value: createdAt,
})}
</span>
</div>
<CommentEdit ref={commentEdit} defaultData={data} onUpdate={onUpdate}>
<>
<CommentEdit
ref={commentEdit}
defaultData={data}
onUpdate={onUpdate}
text={
<div className={styles.text}>
<Markdown linkTarget="_blank">{data.text}</Markdown>
</div>
{canEdit && (
<Comment.Actions>
<Comment.Action
as="button"
content={t('action.edit')}
disabled={!isPersisted}
onClick={handleEditClick}
/>
<DeletePopup
title="common.deleteComment"
content="common.areYouSureYouWantToDeleteThisComment"
buttonContent="action.deleteComment"
onConfirm={onDelete}
>
}
actions={
<div className={styles.title}>
<span>
<span className={styles.author}>{user.name}</span>
<span className={styles.date}>
{t(`format:${getDateFormat(createdAt)}`, {
postProcess: 'formatDate',
value: createdAt,
})}
</span>
</span>
{canEdit && (
<Comment.Actions>
<Comment.Action
as="button"
content={t('action.delete')}
content={t('action.edit')}
disabled={!isPersisted}
onClick={handleEditClick}
/>
</DeletePopup>
</Comment.Actions>
)}
</>
</CommentEdit>
<DeletePopup
title="common.deleteComment"
content="common.areYouSureYouWantToDeleteThisComment"
buttonContent="action.deleteComment"
onConfirm={onDelete}
>
<Comment.Action
as="button"
content={t('action.delete')}
disabled={!isPersisted}
/>
</DeletePopup>
</Comment.Actions>
)}
</div>
}
/>
</div>
</Comment>
);

View File

@@ -38,6 +38,12 @@
.title {
padding-bottom: 4px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: -1em;
background: #f5f6f7;
}
.user {

View File

@@ -1,8 +1,11 @@
import React, { useCallback, useImperativeHandle, useMemo, useState } from 'react';
import React, { useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Button, Form } from 'semantic-ui-react';
import SimpleMDE from 'react-simplemde-editor';
import { useClickAwayListener } from '../../lib/hooks';
import { useNestedRef } from '../../hooks';
import styles from './DescriptionEdit.module.scss';
@@ -11,6 +14,10 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
const [isOpened, setIsOpened] = useState(false);
const [value, setValue] = useState(null);
const editorWrapperRef = useRef(null);
const codemirrorRef = useRef(null);
const [buttonRef, handleButtonRef] = useNestedRef();
const open = useCallback(() => {
setIsOpened(true);
setValue(defaultValue || '');
@@ -55,6 +62,28 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
close();
}, [close]);
const handleAwayClick = useCallback(() => {
if (!isOpened) {
return;
}
close();
}, [isOpened, close]);
const handleClickAwayCancel = useCallback(() => {
codemirrorRef.current.focus();
}, []);
const clickAwayProps = useClickAwayListener(
[editorWrapperRef, buttonRef],
handleAwayClick,
handleClickAwayCancel,
);
const handleGetCodemirrorInstance = useCallback((codemirror) => {
codemirrorRef.current = codemirror;
}, []);
const mdEditorOptions = useMemo(
() => ({
autoDownloadFontAwesome: false,
@@ -92,16 +121,20 @@ const DescriptionEdit = React.forwardRef(({ children, defaultValue, onUpdate },
return (
<Form onSubmit={handleSubmit}>
<SimpleMDE
value={value}
options={mdEditorOptions}
placeholder={t('common.enterDescription')}
className={styles.field}
onKeyDown={handleFieldKeyDown}
onChange={setValue}
/>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<div {...clickAwayProps} ref={editorWrapperRef}>
<SimpleMDE
value={value}
options={mdEditorOptions}
placeholder={t('common.enterDescription')}
className={styles.field}
getCodemirrorInstance={handleGetCodemirrorInstance}
onKeyDown={handleFieldKeyDown}
onChange={setValue}
/>
</div>
<div className={styles.controls}>
<Button positive content={t('action.save')} />
<Button positive ref={handleButtonRef} content={t('action.save')} />
</div>
</Form>
);

View File

@@ -13,6 +13,8 @@ const DEFAULT_DATA = {
name: '',
};
const MULTIPLE_REGEX = /\s*\r?\n\s*/;
const Add = React.forwardRef(({ children, onCreate }, ref) => {
const [t] = useTranslation();
const [isOpened, setIsOpened] = useState(false);
@@ -29,22 +31,34 @@ const Add = React.forwardRef(({ children, onCreate }, ref) => {
setIsOpened(false);
}, []);
const submit = useCallback(() => {
const cleanData = {
...data,
name: data.name.trim(),
};
const submit = useCallback(
(isMultiple = false) => {
const cleanData = {
...data,
name: data.name.trim(),
};
if (!cleanData.name) {
nameField.current.ref.current.select();
return;
}
if (!cleanData.name) {
nameField.current.ref.current.select();
return;
}
onCreate(cleanData);
if (isMultiple) {
cleanData.name.split(MULTIPLE_REGEX).forEach((name) => {
onCreate({
...cleanData,
name,
});
});
} else {
onCreate(cleanData);
}
setData(DEFAULT_DATA);
focusNameField();
}, [onCreate, data, setData, focusNameField]);
setData(DEFAULT_DATA);
focusNameField();
},
[onCreate, data, setData, focusNameField],
);
useImperativeHandle(
ref,
@@ -63,8 +77,7 @@ const Add = React.forwardRef(({ children, onCreate }, ref) => {
(event) => {
if (event.key === 'Enter') {
event.preventDefault();
submit();
submit(event.ctrlKey);
}
},
[submit],

View File

@@ -1,7 +1,8 @@
import useNestedRef from './use-nested-ref';
import useField from './use-field';
import useForm from './use-form';
import useSteps from './use-steps';
import useModal from './use-modal';
import useClosableForm from './use-closable-form';
export { useField, useForm, useSteps, useModal, useClosableForm };
export { useNestedRef, useField, useForm, useSteps, useModal, useClosableForm };

View File

@@ -0,0 +1,14 @@
import { useCallback, useRef } from 'react';
export default (nestedRefName = 'ref') => {
const ref = useRef(null);
const handleRef = useCallback(
(element) => {
ref.current = element?.[nestedRefName].current;
},
[nestedRefName],
);
return [ref, handleRef];
};

View File

@@ -2,5 +2,6 @@ import usePrevious from './use-previous';
import useToggle from './use-toggle';
import useForceUpdate from './use-force-update';
import useDidUpdate from './use-did-update';
import useClickAwayListener from './use-click-away-listener';
export { usePrevious, useToggle, useForceUpdate, useDidUpdate };
export { usePrevious, useToggle, useForceUpdate, useDidUpdate, useClickAwayListener };

View File

@@ -0,0 +1,45 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
export default (elementRefs, onAwayClick, onCancel) => {
const pressedElement = useRef(null);
const handlePress = useCallback((event) => {
pressedElement.current = event.target;
}, []);
useEffect(() => {
const handleEvent = (event) => {
const element = elementRefs.find(({ current }) => current?.contains(event.target))?.current;
if (element) {
if (!pressedElement.current || pressedElement.current !== element) {
onCancel();
}
} else if (pressedElement.current) {
onCancel();
} else {
onAwayClick();
}
pressedElement.current = null;
};
document.addEventListener('mouseup', handleEvent, true);
document.addEventListener('touchend', handleEvent, true);
return () => {
document.removeEventListener('mouseup', handleEvent, true);
document.removeEventListener('touchend', handleEvent, true);
};
}, [onAwayClick, onCancel]); // eslint-disable-line react-hooks/exhaustive-deps
const props = useMemo(
() => ({
onMouseDown: handlePress,
onTouchStart: handlePress,
}),
[handlePress],
);
return props;
};

View File

@@ -184,6 +184,7 @@ export default {
addTask: 'Přidat úkol',
addToCard: 'Přidat na kartu',
addUser: 'Přidat uživatele',
copyLink_title: 'Zkopírovat odkaz',
createBoard: 'Vytvořit tabuli',
createFile: 'Vytvořit soubor',
createLabel: 'Vytvořit štítek',

View File

@@ -3,6 +3,7 @@ export default {
common: {
emailOrUsername: 'E-mail nebo uživatelské jméno',
invalidEmailOrUsername: 'Nesprávný e-mail nebo uživatelské jméno',
invalidCredentials: 'Neplatné přihlašovací údaje',
invalidPassword: 'Nesprávné heslo',
logInToPlanka: 'Přihlásit se do Planka',
noInternetConnection: 'Bez připojení k internetu',
@@ -11,6 +12,7 @@ export default {
projectManagement: 'Správa projektu',
serverConnectionFailed: 'Připojení k serveru selhalo',
unknownError: 'Neznámá chyba, zkuste to později',
useSingleSignOn: 'Použít jednorázové přihlášení',
},
action: {

View File

@@ -1 +1 @@
export default '1.23.3';
export default '1.23.5';

View File

@@ -67,6 +67,12 @@ services:
# - SLACK_BOT_TOKEN=
# - SLACK_CHANNEL_ID=
# - GOOGLE_CHAT_WEBHOOK_URL=
# - TELEGRAM_BOT_TOKEN=
# - TELEGRAM_CHAT_ID=
# - TELEGRAM_THREAD_ID=
working_dir: /app
command: ["sh", "-c", "npm run start"]
depends_on:

View File

@@ -74,7 +74,12 @@ services:
# - SLACK_BOT_TOKEN=
# - SLACK_CHANNEL_ID=
# - GOOGLE_CHAT_WEBHOOK_URL=
# - TELEGRAM_BOT_TOKEN=
# - TELEGRAM_CHAT_ID=
# - TELEGRAM_THREAD_ID=
depends_on:
postgres:
condition: service_healthy

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "planka",
"version": "1.23.3",
"version": "1.23.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "planka",
"version": "1.23.3",
"version": "1.23.5",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "planka",
"version": "1.23.3",
"version": "1.23.5",
"private": true,
"homepage": "https://plankanban.github.io/planka",
"repository": {
@@ -16,7 +16,7 @@
"client:test": "npm test --prefix client",
"docker:build": "docker build -t ghcr.io/plankanban/planka:local -f Dockerfile .",
"docker:build:base": "docker build -t ghcr.io/plankanban/planka:base-local -f Dockerfile.base .",
"gv": "genversion --source ./ --template client/version-template.ejs client/src/version.js",
"gv": "npm i --package-lock-only --ignore-scripts && genversion --source ./ --template client/version-template.ejs client/src/version.js",
"postinstall": "(cd server && npm i && cd ../client && npm i)",
"lint": "npm run server:lint && npm run client:lint",
"prepare": "husky",

View File

@@ -65,8 +65,13 @@ SECRET_KEY=notsecretkey
# SLACK_BOT_TOKEN=
# SLACK_CHANNEL_ID=
# GOOGLE_CHAT_WEBHOOK_URL=
# TELEGRAM_BOT_TOKEN=
# TELEGRAM_CHAT_ID=
# TELEGRAM_THREAD_ID=
## Do not edit this
TZ=UTC

View File

@@ -14,7 +14,10 @@ const valuesValidator = (value) => {
return true;
};
const buildAndSendMessage = async (card, action, actorUser, send) => {
const truncateString = (string, maxLength = 30) =>
string.length > maxLength ? `${string.substring(0, 30)}...` : string;
const buildAndSendMarkdownMessage = async (card, action, actorUser, send) => {
const cardLink = `<${sails.config.custom.baseUrl}/cards/${card.id}|${card.name}>`;
let markdown;
@@ -28,6 +31,7 @@ const buildAndSendMessage = async (card, action, actorUser, send) => {
break;
case Action.Types.COMMENT_CARD:
// TODO: truncate text?
markdown = `*${actorUser.name}* commented on ${cardLink}:\n>${action.data.text}`;
break;
@@ -38,6 +42,31 @@ const buildAndSendMessage = async (card, action, actorUser, send) => {
await send(markdown);
};
const buildAndSendHtmlMessage = async (card, action, actorUser, send) => {
const cardLink = `<a href="${sails.config.custom.baseUrl}/cards/${card.id}">${card.name}</a>`;
let html;
switch (action.type) {
case Action.Types.CREATE_CARD:
html = `${cardLink} was created by ${actorUser.name} in <b>${action.data.list.name}</b>`;
break;
case Action.Types.MOVE_CARD:
html = `${cardLink} was moved by ${actorUser.name} to <b>${action.data.toList.name}</b>`;
break;
case Action.Types.COMMENT_CARD: {
html = `<b>${actorUser.name}</b> commented on ${cardLink}:\n<i>${truncateString(action.data.text)}</i>`;
break;
}
default:
return;
}
await send(html);
};
module.exports = {
inputs: {
values: {
@@ -116,17 +145,32 @@ module.exports = {
);
if (sails.config.custom.slackBotToken) {
buildAndSendMessage(values.card, action, values.user, sails.helpers.utils.sendSlackMessage);
buildAndSendMarkdownMessage(
values.card,
action,
values.user,
sails.helpers.utils.sendSlackMessage,
);
}
if (sails.config.custom.googleChatWebhookUrl) {
buildAndSendMessage(
buildAndSendMarkdownMessage(
values.card,
action,
values.user,
sails.helpers.utils.sendGoogleChatMessage,
);
}
if (sails.config.custom.telegramBotToken) {
buildAndSendHtmlMessage(
values.card,
action,
values.user,
sails.helpers.utils.sendTelegramMessage,
);
}
return action;
},
};

View File

@@ -59,6 +59,9 @@ module.exports = {
cards: [inputs.card],
},
},
prevData: {
item: inputs.record,
},
user: inputs.actorUser,
});
}

View File

@@ -59,6 +59,9 @@ module.exports = {
cards: [inputs.card],
},
},
prevData: {
item: inputs.record,
},
user: inputs.actorUser,
});
}

View File

@@ -62,6 +62,9 @@ module.exports = {
boards: [inputs.board],
},
},
prevData: {
item: inputs.record,
},
user: inputs.actorUser,
});
}

View File

@@ -100,6 +100,9 @@ module.exports = {
projects: [inputs.project],
},
},
prevData: {
item: inputs.record,
},
user: inputs.actorUser,
});
}

View File

@@ -1,7 +1,11 @@
const buildAndSendMessage = async (card, actorUser, send) => {
const buildAndSendMarkdownMessage = async (card, actorUser, send) => {
await send(`*${card.name}* was deleted by ${actorUser.name}`);
};
const buildAndSendHtmlMessage = async (card, actorUser, send) => {
await send(`<b>${card.name}</b> was deleted by ${actorUser.name}`);
};
module.exports = {
inputs: {
record: {
@@ -56,11 +60,19 @@ module.exports = {
});
if (sails.config.custom.slackBotToken) {
buildAndSendMessage(card, inputs.actorUser, sails.helpers.utils.sendSlackMessage);
buildAndSendMarkdownMessage(card, inputs.actorUser, sails.helpers.utils.sendSlackMessage);
}
if (sails.config.custom.googleChatWebhookUrl) {
buildAndSendMessage(card, inputs.actorUser, sails.helpers.utils.sendGoogleChatMessage);
buildAndSendMarkdownMessage(
card,
inputs.actorUser,
sails.helpers.utils.sendGoogleChatMessage,
);
}
if (sails.config.custom.telegramBotToken) {
buildAndSendHtmlMessage(card, inputs.actorUser, sails.helpers.utils.sendTelegramMessage);
}
}

View File

@@ -264,6 +264,9 @@ module.exports = {
lists: [list],
},
},
prevData: {
item: inputs.record,
},
user: inputs.actorUser,
});

View File

@@ -91,6 +91,9 @@ module.exports = {
boards: [inputs.board],
},
},
prevData: {
item: inputs.record,
},
user: inputs.actorUser,
});
}

View File

@@ -91,6 +91,9 @@ module.exports = {
boards: [inputs.board],
},
},
prevData: {
item: inputs.record,
},
user: inputs.actorUser,
});
}

View File

@@ -48,6 +48,7 @@ module.exports = {
inputs.request,
);
// TODO: with prevData?
sails.helpers.utils.sendWebhooks.with({
event: 'notificationUpdate',
data: {

View File

@@ -118,6 +118,9 @@ module.exports = {
data: {
item: project,
},
prevData: {
item: inputs.record,
},
user: inputs.actorUser,
});
}

View File

@@ -101,6 +101,9 @@ module.exports = {
cards: [inputs.card],
},
},
prevData: {
item: inputs.record,
},
user: inputs.actorUser,
});
}

View File

@@ -159,6 +159,9 @@ module.exports = {
data: {
item: user,
},
prevData: {
item: inputs.record,
},
user: inputs.actorUser,
});
}

View File

@@ -0,0 +1,44 @@
const buildSendMessageApiUrl = (telegramBotToken) =>
`https://api.telegram.org/bot${telegramBotToken}/sendMessage`;
module.exports = {
inputs: {
html: {
type: 'string',
required: true,
},
},
async fn(inputs) {
const headers = {
'Content-Type': 'application/json; charset=utf-8',
};
const body = {
chat_id: sails.config.custom.telegramChatId,
text: inputs.html,
parse_mode: 'HTML',
};
if (sails.config.custom.telegramThreadId) {
body.message_thread_id = sails.config.custom.telegramThreadId;
}
let response;
try {
response = await fetch(buildSendMessageApiUrl(sails.config.custom.telegramBotToken), {
headers,
method: 'POST',
body: JSON.stringify(body),
});
} catch (error) {
sails.log.error(`Error sending to Telegram: ${error}`);
return;
}
if (!response.ok) {
const responseJson = await response.json();
sails.log.error(`Error sending to Telegram: ${responseJson.description}`);
}
},
};

View File

@@ -97,10 +97,11 @@ const jsonifyData = (data) => {
* @param {*} webhook - Webhook configuration.
* @param {string} event - The event (see {@link EVENT_TYPES}).
* @param {Data} data - The data object containing event data and optionally included data.
* @param {Data} [prevData] - The data object containing previous state of data (optional).
* @param {ref} user - User object associated with the event.
* @returns {Promise<void>}
*/
async function sendWebhook(webhook, event, data, user) {
async function sendWebhook(webhook, event, data, prevData, user) {
const headers = {
'Content-Type': 'application/json',
'User-Agent': `planka (+${sails.config.custom.baseUrl})`,
@@ -113,6 +114,7 @@ async function sendWebhook(webhook, event, data, user) {
const body = JSON.stringify({
event,
data: jsonifyData(data),
prevData: prevData && jsonifyData(prevData),
user: sails.helpers.utils.jsonifyRecord(user),
});
@@ -148,6 +150,9 @@ module.exports = {
type: 'ref',
required: true,
},
prevData: {
type: 'ref',
},
user: {
type: 'ref',
required: true,
@@ -172,7 +177,7 @@ module.exports = {
return;
}
sendWebhook(webhook, inputs.event, inputs.data, inputs.user);
sendWebhook(webhook, inputs.event, inputs.data, inputs.prevData, inputs.user);
});
},
};

View File

@@ -82,5 +82,10 @@ module.exports.custom = {
slackBotToken: process.env.SLACK_BOT_TOKEN,
slackChannelId: process.env.SLACK_CHANNEL_ID,
googleChatWebhookUrl: process.env.GOOGLE_CHAT_WEBHOOK_URL,
telegramBotToken: process.env.TELEGRAM_BOT_TOKEN,
telegramChatId: process.env.TELEGRAM_CHAT_ID,
telegramThreadId: process.env.TELEGRAM_THREAD_ID,
};