1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-18 20:59:44 +02:00

feat: Move webhooks configuration from environment variable to UI

This commit is contained in:
Maksim Eltyshev 2025-07-04 22:04:11 +02:00
parent f0680831c2
commit b22dba0d11
128 changed files with 2077 additions and 206 deletions

View file

@ -8,6 +8,7 @@ import ActionTypes from '../constants/ActionTypes';
const initializeCore = (
user,
board,
webhooks,
users,
projects,
projectManagers,
@ -33,6 +34,7 @@ const initializeCore = (
payload: {
user,
board,
webhooks,
users,
projects,
projectManagers,

View file

@ -8,6 +8,7 @@ import socket from './socket';
import login from './login';
import core from './core';
import modals from './modals';
import webhooks from './webhooks';
import users from './users';
import projects from './projects';
import projectManagers from './project-managers';
@ -35,6 +36,7 @@ export default {
...login,
...core,
...modals,
...webhooks,
...users,
...projects,
...projectManagers,

View file

@ -14,6 +14,7 @@ const handleSocketReconnect = (
config,
user,
board,
webhooks,
users,
projects,
projectManagers,
@ -40,6 +41,7 @@ const handleSocketReconnect = (
config,
user,
board,
webhooks,
users,
projects,
projectManagers,

View file

@ -67,6 +67,7 @@ const handleUserUpdate = (
boardIds,
config,
board,
webhooks,
users,
projects,
projectManagers,
@ -95,6 +96,7 @@ const handleUserUpdate = (
boardIds,
config,
board,
webhooks,
users,
projects,
projectManagers,

View file

@ -0,0 +1,104 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import ActionTypes from '../constants/ActionTypes';
const createWebhook = (webhook) => ({
type: ActionTypes.WEBHOOK_CREATE,
payload: {
webhook,
},
});
createWebhook.success = (localId, webhook) => ({
type: ActionTypes.WEBHOOK_CREATE__SUCCESS,
payload: {
localId,
webhook,
},
});
createWebhook.failure = (localId, error) => ({
type: ActionTypes.WEBHOOK_CREATE__FAILURE,
payload: {
localId,
error,
},
});
const handleWebhookCreate = (webhook) => ({
type: ActionTypes.WEBHOOK_CREATE_HANDLE,
payload: {
webhook,
},
});
const updateWebhook = (id, data) => ({
type: ActionTypes.WEBHOOK_UPDATE,
payload: {
id,
data,
},
});
updateWebhook.success = (webhook) => ({
type: ActionTypes.WEBHOOK_UPDATE__SUCCESS,
payload: {
webhook,
},
});
updateWebhook.failure = (id, error) => ({
type: ActionTypes.WEBHOOK_UPDATE__FAILURE,
payload: {
id,
error,
},
});
const handleWebhookUpdate = (webhook) => ({
type: ActionTypes.WEBHOOK_UPDATE_HANDLE,
payload: {
webhook,
},
});
const deleteWebhook = (id) => ({
type: ActionTypes.WEBHOOK_DELETE,
payload: {
id,
},
});
deleteWebhook.success = (webhook) => ({
type: ActionTypes.WEBHOOK_DELETE__SUCCESS,
payload: {
webhook,
},
});
deleteWebhook.failure = (id, error) => ({
type: ActionTypes.WEBHOOK_DELETE__FAILURE,
payload: {
id,
error,
},
});
const handleWebhookDelete = (webhook) => ({
type: ActionTypes.WEBHOOK_DELETE_HANDLE,
payload: {
webhook,
},
});
export default {
createWebhook,
handleWebhookCreate,
updateWebhook,
handleWebhookUpdate,
deleteWebhook,
handleWebhookDelete,
};

View file

@ -7,6 +7,7 @@ import http from './http';
import socket from './socket';
import config from './config';
import accessTokens from './access-tokens';
import webhooks from './webhooks';
import users from './users';
import projects from './projects';
import projectManagers from './project-managers';
@ -35,6 +36,7 @@ export { http, socket };
export default {
...config,
...accessTokens,
...webhooks,
...users,
...projects,
...projectManagers,

23
client/src/api/webhooks.js Executable file
View file

@ -0,0 +1,23 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import socket from './socket';
/* Actions */
const getWebhooks = (headers) => socket.get('/webhooks', undefined, headers);
const createWebhook = (data, headers) => socket.post('/webhooks', data, headers);
const updateWebhook = (id, data, headers) => socket.patch(`/webhooks/${id}`, data, headers);
const deleteWebhook = (id, headers) => socket.delete(`/webhooks/${id}`, undefined, headers);
export default {
getWebhooks,
createWebhook,
updateWebhook,
deleteWebhook,
};

View file

@ -12,6 +12,7 @@ import { Modal, Tab } from 'semantic-ui-react';
import entryActions from '../../../entry-actions';
import { useClosableModal } from '../../../hooks';
import UsersPane from './UsersPane';
import WebhooksPane from './WebhooksPane';
import styles from './AdministrationModal.module.scss';
@ -37,6 +38,12 @@ const AdministrationModal = React.memo(() => {
}),
render: () => <UsersPane />,
},
{
menuItem: t('common.webhooks', {
context: 'title',
}),
render: () => <WebhooksPane />,
},
];
const isUsersPaneActive = activeTabIndex === 0;

View file

@ -0,0 +1,35 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Tab } from 'semantic-ui-react';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import Webhooks from '../../webhooks/Webhooks';
import styles from './WebhooksPane.module.scss';
const WebhooksPane = React.memo(() => {
const webhookIds = useSelector(selectors.selectWebhookIds);
const dispatch = useDispatch();
const handleCreate = useCallback(
(data) => {
dispatch(entryActions.createWebhook(data));
},
[dispatch],
);
return (
<Tab.Pane attached={false} className={styles.wrapper}>
<Webhooks ids={webhookIds} onCreate={handleCreate} />
</Tab.Pane>
);
});
export default WebhooksPane;

View file

@ -0,0 +1,11 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
:global(#app) {
.wrapper {
border: none;
box-shadow: none;
}
}

View file

@ -28,7 +28,10 @@ const ConfirmationStep = React.memo(
const [nameFieldRef, handleNameFieldRef] = useNestedRef('inputRef');
const handleSubmit = useCallback(() => {
const handleSubmit = useCallback(
(event) => {
event.stopPropagation();
if (typeValue) {
const cleanData = {
...data,
@ -42,7 +45,9 @@ const ConfirmationStep = React.memo(
}
onConfirm();
}, [typeValue, onConfirm, data, nameFieldRef]);
},
[typeValue, onConfirm, data, nameFieldRef],
);
useEffect(() => {
if (typeValue) {

View file

@ -125,7 +125,7 @@ const Item = React.memo(({ id }) => {
dispatch(entryActions.deleteNotificationService(id));
}, [id, dispatch]);
const handleUpdateSubmit = useCallback(() => {
const handleSubmit = useCallback(() => {
urlFieldRef.current.blur();
}, [urlFieldRef]);
@ -153,7 +153,7 @@ const Item = React.memo(({ id }) => {
const ConfirmationPopup = usePopupInClosableContext(ConfirmationStep);
return (
<Form className={styles.wrapper} onSubmit={handleUpdateSubmit}>
<Form className={styles.wrapper} onSubmit={handleSubmit}>
<Input
ref={handleUrlFieldRef}
name="url"

View file

@ -0,0 +1,152 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import React, { useCallback, useImperativeHandle } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Dropdown, Input } from 'semantic-ui-react';
import { useNestedRef } from '../../../hooks';
import WEBHOOK_EVENTS from '../../../constants/WebhookEvents';
import styles from './Editor.module.scss';
const Editor = React.forwardRef(({ data, isReadOnly, onFieldChange }, ref) => {
const [t] = useTranslation();
const [nameFieldRef, handleNameFieldRef] = useNestedRef('inputRef');
const [urlFieldRef, handleUrlFieldRef] = useNestedRef('inputRef');
const focusNameField = useCallback(() => {
nameFieldRef.current.focus({
preventScroll: true,
});
}, [nameFieldRef]);
const selectNameField = useCallback(() => {
nameFieldRef.current.select();
}, [nameFieldRef]);
const selectUrlField = useCallback(() => {
urlFieldRef.current.select();
}, [urlFieldRef]);
useImperativeHandle(
ref,
() => ({
focusNameField,
selectNameField,
selectUrlField,
}),
[focusNameField, selectNameField, selectUrlField],
);
return (
<>
<div className={styles.text}>{t('common.title')}</div>
<Input
fluid
ref={handleNameFieldRef}
name="name"
value={data.name}
maxLength={128}
readOnly={isReadOnly}
className={styles.field}
onChange={onFieldChange}
/>
<div className={styles.text}>{t('common.url')}</div>
<Input
fluid
ref={handleUrlFieldRef}
name="url"
value={data.url}
maxLength={2048}
readOnly={isReadOnly}
className={styles.field}
onChange={onFieldChange}
/>
<div className={styles.text}>
{t('common.accessToken')} (
{t('common.optional', {
context: 'inline',
})}
)
</div>
<Input
fluid
name="accessToken"
value={data.accessToken}
maxLength={512}
readOnly={isReadOnly}
className={styles.field}
onChange={onFieldChange}
/>
{data.excludedEvents.length === 0 && (
<>
<div className={styles.text}>
{t('common.events')} (
{t('common.optional', {
context: 'inline',
})}
)
</div>
<Dropdown
selection
multiple
fluid
name="events"
options={WEBHOOK_EVENTS.map((event) => ({
text: event,
value: event,
}))}
value={data.events}
placeholder="All"
readOnly={isReadOnly}
className={styles.field}
onChange={onFieldChange}
/>
</>
)}
{data.events.length === 0 && (
<>
<div className={styles.text}>
{t('common.excludedEvents')} (
{t('common.optional', {
context: 'inline',
})}
)
</div>
<Dropdown
selection
multiple
fluid
name="excludedEvents"
options={WEBHOOK_EVENTS.map((event) => ({
text: event,
value: event,
}))}
value={data.excludedEvents}
placeholder="None"
readOnly={isReadOnly}
className={styles.field}
onChange={onFieldChange}
/>
</>
)}
</>
);
});
Editor.propTypes = {
data: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
isReadOnly: PropTypes.bool,
onFieldChange: PropTypes.func.isRequired,
};
Editor.defaultProps = {
isReadOnly: false,
};
export default React.memo(Editor);

View file

@ -0,0 +1,17 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
:global(#app) {
.field {
margin-bottom: 8px;
}
.text {
color: #444444;
font-size: 12px;
font-weight: bold;
padding-bottom: 6px;
}
}

View file

@ -0,0 +1,137 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { dequal } from 'dequal';
import React, { useCallback, useMemo, useRef } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Accordion, Button, Form, Icon } from 'semantic-ui-react';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import { useForm, usePopupInClosableContext } from '../../../hooks';
import { isUrl } from '../../../utils/validator';
import Editor from './Editor';
import ConfirmationStep from '../../common/ConfirmationStep';
import styles from './Item.module.scss';
import { useToggle } from '../../../lib/hooks';
const Item = React.memo(({ id }) => {
const selectWebhookById = useMemo(() => selectors.makeSelectWebhookById(), []);
const webhook = useSelector((state) => selectWebhookById(state, id));
const dispatch = useDispatch();
const [t] = useTranslation();
const [isOpened, toggleOpened] = useToggle();
const defaultData = useMemo(
() => ({
name: webhook.name,
url: webhook.url,
accessToken: webhook.accessToken,
events: webhook.events,
excludedEvents: webhook.excludedEvents,
}),
[webhook],
);
const [data, handleFieldChange] = useForm(() => ({
name: '',
url: '',
...defaultData,
accessToken: defaultData.accessToken || '',
events: defaultData.events || [],
excludedEvents: defaultData.excludedEvents || [],
}));
const cleanData = useMemo(
() => ({
...data,
name: data.name.trim(),
url: data.url.trim(),
accessToken: data.accessToken.trim() || null,
events: data.events.length === 0 ? null : data.events,
excludedEvents: data.excludedEvents.length === 0 ? null : data.excludedEvents,
}),
[data],
);
const editorRef = useRef(null);
const handleDeleteConfirm = useCallback(() => {
dispatch(entryActions.deleteWebhook(id));
}, [id, dispatch]);
const handleSubmit = useCallback(() => {
if (!cleanData.name) {
editorRef.current.selectNameField();
return;
}
if (!cleanData.url || !isUrl(cleanData.url)) {
editorRef.current.selectUrlField();
return;
}
dispatch(entryActions.updateWebhook(id, cleanData));
}, [id, dispatch, cleanData]);
const handleOpenClick = useCallback(() => {
toggleOpened();
}, [toggleOpened]);
const ConfirmationPopup = usePopupInClosableContext(ConfirmationStep);
return (
<>
<Accordion.Title active={isOpened} className={styles.title} onClick={handleOpenClick}>
<Icon name="dropdown" />
{defaultData.name}
</Accordion.Title>
<Accordion.Content active={isOpened}>
<div>
<Form onSubmit={handleSubmit}>
<Editor
ref={editorRef}
data={data}
isReadOnly={!webhook.isPersisted}
onFieldChange={handleFieldChange}
/>
<div className={styles.controls}>
<Button
positive
disabled={dequal(cleanData, defaultData)}
content={t('action.save')}
/>
<ConfirmationPopup
title="common.deleteWebhook"
content="common.areYouSureYouWantToDeleteThisWebhook"
buttonContent="action.deleteWebhook"
onConfirm={handleDeleteConfirm}
>
<Button
type="button"
disabled={!webhook.isPersisted}
className={styles.deleteButton}
>
{t('action.delete')}
</Button>
</ConfirmationPopup>
</div>
</Form>
</div>
</Accordion.Content>
</>
);
});
Item.propTypes = {
id: PropTypes.string.isRequired,
};
export default Item;

View file

@ -0,0 +1,22 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
:global(#app) {
.controls {
display: flex;
justify-content: space-between;
}
.deleteButton {
box-shadow: 0 1px 0 #cbcccc;
margin-right: 0;
}
.title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}

View file

@ -0,0 +1,96 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import React, { useCallback, useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { Accordion, Button, Form, Segment } from 'semantic-ui-react';
import { useDidUpdate, useToggle } from '../../../lib/hooks';
import { useForm } from '../../../hooks';
import { isUrl } from '../../../utils/validator';
import Item from './Item';
import Editor from './Editor';
const DEFAULT_DATA = {
name: '',
url: '',
accessToken: '',
events: [],
excludedEvents: [],
};
const Webhooks = React.memo(({ ids, onCreate }) => {
const [t] = useTranslation();
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
const [focusNameFieldState, focusNameField] = useToggle();
const editorRef = useRef(null);
const handleCreateSubmit = useCallback(() => {
const cleanData = {
...data,
name: data.name.trim(),
url: data.url.trim(),
accessToken: data.accessToken.trim() || null,
events: data.events.length === 0 ? null : data.events,
excludedEvents: data.excludedEvents.length === 0 ? null : data.excludedEvents,
};
if (!cleanData.name) {
editorRef.current.selectNameField();
return;
}
if (!cleanData.url || !isUrl(cleanData.url)) {
editorRef.current.selectUrlField();
return;
}
onCreate(cleanData);
setData(DEFAULT_DATA);
focusNameField();
}, [onCreate, data, setData, focusNameField]);
useEffect(() => {
if (editorRef.current) {
editorRef.current.focusNameField();
}
}, []);
useDidUpdate(() => {
if (editorRef.current) {
editorRef.current.focusNameField();
}
}, [focusNameFieldState]);
return (
<>
{ids.length > 0 && (
<Accordion styled fluid>
{ids.map((id) => (
<Item key={id} id={id} />
))}
</Accordion>
)}
{ids.length < 10 && (
<Segment>
<Form onSubmit={handleCreateSubmit}>
<Editor ref={editorRef} data={data} onFieldChange={handleFieldChange} />
<Button positive>{t('action.addWebhook')}</Button>
</Form>
</Segment>
)}
</>
);
});
Webhooks.propTypes = {
ids: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
onCreate: PropTypes.func.isRequired,
};
export default Webhooks;

View file

@ -0,0 +1,8 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import Webhooks from './Webhooks';
export default Webhooks;

View file

@ -42,6 +42,21 @@ export default {
MODAL_OPEN: 'MODAL_OPEN',
MODAL_CLOSE: 'MODAL_CLOSE',
/* Webhooks */
WEBHOOK_CREATE: 'WEBHOOK_CREATE',
WEBHOOK_CREATE__SUCCESS: 'WEBHOOK_CREATE__SUCCESS',
WEBHOOK_CREATE__FAILURE: 'WEBHOOK_CREATE__FAILURE',
WEBHOOK_CREATE_HANDLE: 'WEBHOOK_CREATE_HANDLE',
WEBHOOK_UPDATE: 'WEBHOOK_UPDATE',
WEBHOOK_UPDATE__SUCCESS: 'WEBHOOK_UPDATE__SUCCESS',
WEBHOOK_UPDATE__FAILURE: 'WEBHOOK_UPDATE__FAILURE',
WEBHOOK_UPDATE_HANDLE: 'WEBHOOK_UPDATE_HANDLE',
WEBHOOK_DELETE: 'WEBHOOK_DELETE',
WEBHOOK_DELETE__SUCCESS: 'WEBHOOK_DELETE__SUCCESS',
WEBHOOK_DELETE__FAILURE: 'WEBHOOK_DELETE__FAILURE',
WEBHOOK_DELETE_HANDLE: 'WEBHOOK_DELETE_HANDLE',
/* Users */
USER_CREATE: 'USER_CREATE',

View file

@ -31,6 +31,15 @@ export default {
MODAL_OPEN: `${PREFIX}/MODAL_OPEN`,
MODAL_CLOSE: `${PREFIX}/MODAL_CLOSE`,
/* Webhooks */
WEBHOOK_CREATE: `${PREFIX}/WEBHOOK_CREATE`,
WEBHOOK_CREATE_HANDLE: `${PREFIX}/WEBHOOK_CREATE_HANDLE`,
WEBHOOK_UPDATE: `${PREFIX}/WEBHOOK_UPDATE`,
WEBHOOK_UPDATE_HANDLE: `${PREFIX}/WEBHOOK_UPDATE_HANDLE`,
WEBHOOK_DELETE: `${PREFIX}/WEBHOOK_DELETE`,
WEBHOOK_DELETE_HANDLE: `${PREFIX}/WEBHOOK_DELETE_HANDLE`,
/* Users */
USER_CREATE: `${PREFIX}/USER_CREATE`,

View file

@ -0,0 +1,91 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
export default [
'actionCreate',
'attachmentCreate',
'attachmentUpdate',
'attachmentDelete',
'backgroundImageCreate',
'backgroundImageDelete',
'baseCustomFieldGroupCreate',
'baseCustomFieldGroupUpdate',
'baseCustomFieldGroupDelete',
'boardCreate',
'boardUpdate',
'boardDelete',
'boardMembershipCreate',
'boardMembershipUpdate',
'boardMembershipDelete',
'cardCreate',
'cardUpdate',
'cardDelete',
'cardLabelCreate',
'cardLabelDelete',
'cardMembershipCreate',
'cardMembershipDelete',
'commentCreate',
'commentUpdate',
'commentDelete',
'customFieldCreate',
'customFieldUpdate',
'customFieldDelete',
'customFieldGroupCreate',
'customFieldGroupUpdate',
'customFieldGroupDelete',
'customFieldValueUpdate',
'customFieldValueDelete',
'labelCreate',
'labelUpdate',
'labelDelete',
'listCreate',
'listUpdate',
'listClear',
'listDelete',
'notificationCreate',
'notificationUpdate',
'notificationServiceCreate',
'notificationServiceUpdate',
'notificationServiceDelete',
'projectCreate',
'projectUpdate',
'projectDelete',
'projectManagerCreate',
'projectManagerDelete',
'taskCreate',
'taskUpdate',
'taskDelete',
'taskListCreate',
'taskListUpdate',
'taskListDelete',
'userCreate',
'userUpdate',
'userDelete',
'webhookCreate',
'webhookUpdate',
'webhookDelete',
];

View file

@ -7,6 +7,7 @@ import socket from './socket';
import login from './login';
import core from './core';
import modals from './modals';
import webhooks from './webhooks';
import users from './users';
import projects from './projects';
import projectManagers from './project-managers';
@ -33,6 +34,7 @@ export default {
...login,
...core,
...modals,
...webhooks,
...users,
...projects,
...projectManagers,

View file

@ -0,0 +1,58 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import EntryActionTypes from '../constants/EntryActionTypes';
const createWebhook = (data) => ({
type: EntryActionTypes.WEBHOOK_CREATE,
payload: {
data,
},
});
const handleWebhookCreate = (webhook) => ({
type: EntryActionTypes.WEBHOOK_CREATE_HANDLE,
payload: {
webhook,
},
});
const updateWebhook = (id, data) => ({
type: EntryActionTypes.WEBHOOK_UPDATE,
payload: {
id,
data,
},
});
const handleWebhookUpdate = (webhook) => ({
type: EntryActionTypes.WEBHOOK_UPDATE_HANDLE,
payload: {
webhook,
},
});
const deleteWebhook = (id) => ({
type: EntryActionTypes.WEBHOOK_DELETE,
payload: {
id,
},
});
const handleWebhookDelete = (webhook) => ({
type: EntryActionTypes.WEBHOOK_DELETE_HANDLE,
payload: {
webhook,
},
});
export default {
createWebhook,
handleWebhookCreate,
updateWebhook,
handleWebhookUpdate,
deleteWebhook,
handleWebhookDelete,
};

View file

@ -21,6 +21,7 @@ export default {
translation: {
common: {
aboutPlanka: 'About PLANKA',
accessToken: 'Access token',
account: 'Account',
actions: 'Actions',
activateUser_title: 'Activate User',
@ -69,6 +70,7 @@ export default {
areYouSureYouWantToDeleteThisTask: 'Are you sure you want to delete this task?',
areYouSureYouWantToDeleteThisTaskList: 'Are you sure you want to delete this task list?',
areYouSureYouWantToDeleteThisUser: 'Are you sure you want to delete this user?',
areYouSureYouWantToDeleteThisWebhook: 'Are you sure you want to delete this webhook?',
areYouSureYouWantToEmptyTrash: 'Are you sure you want to empty the trash?',
areYouSureYouWantToLeaveBoard: 'Are you sure you want to leave the board?',
areYouSureYouWantToLeaveProject: 'Are you sure you want to leave the project?',
@ -152,6 +154,7 @@ export default {
deleteTask_title: 'Delete Task',
deleteUser_title: 'Delete User',
deletedUser_title: 'Deleted User',
deleteWebhook_title: 'Delete Webhook',
description: 'Description',
detectAutomatically: 'Detect automatically',
display: 'Display',
@ -182,6 +185,8 @@ export default {
enterFilename: 'Enter filename',
enterListTitle: 'Enter list title...',
enterTaskDescription: 'Enter task description...',
events: 'Events',
excludedEvents: 'Excluded events',
filterByLabels_title: 'Filter By Labels',
filterByMembers_title: 'Filter By Members',
forPersonalProjects: 'For personal projects.',
@ -286,6 +291,7 @@ export default {
typeTitleToConfirm: 'Type the title to confirm.',
unsavedChanges: 'Unsaved changes',
uploadedImages: 'Uploaded images',
url: 'URL',
userActions_title: 'User Actions',
userAddedCardToList: '<0>{{user}}</0> added <2>{{card}}</2> to {{list}}',
userAddedThisCardToList: '<0>{{user}}</0> added this card to {{list}}',
@ -313,6 +319,7 @@ export default {
userRemovedUserFromThisCard: '<0>{{actorUser}}</0> removed {{removedUser}} from this card',
username: 'Username',
users: 'Users',
webhooks: 'Webhooks',
viewer: 'Viewer',
viewers: 'Viewers',
visualTaskManagementWithLists: 'Visual task management with lists.',
@ -338,6 +345,7 @@ export default {
addTaskList: 'Add task list',
addToCard: 'Add to card',
addUser: 'Add user',
addWebhook: 'Add webhook',
archive: 'Archive',
archiveCard: 'Archive card',
archiveCard_title: 'Archive Card',
@ -378,6 +386,7 @@ export default {
deleteTask_title: 'Delete Task',
deleteUser: 'Delete user',
deleteUser_title: 'Delete User',
deleteWebhook: 'Delete webhook',
dismissAll: 'Dismiss all',
duplicate: 'Duplicate',
duplicateCard_title: 'Duplicate Card',

View file

@ -16,6 +16,7 @@ export default {
translation: {
common: {
aboutPlanka: 'About PLANKA',
accessToken: 'Access token',
account: 'Account',
actions: 'Actions',
activateUser_title: 'Activate User',
@ -64,6 +65,7 @@ export default {
areYouSureYouWantToDeleteThisTask: 'Are you sure you want to delete this task?',
areYouSureYouWantToDeleteThisTaskList: 'Are you sure you want to delete this task list?',
areYouSureYouWantToDeleteThisUser: 'Are you sure you want to delete this user?',
areYouSureYouWantToDeleteThisWebhook: 'Are you sure you want to delete this webhook?',
areYouSureYouWantToEmptyTrash: 'Are you sure you want to empty the trash?',
areYouSureYouWantToLeaveBoard: 'Are you sure you want to leave the board?',
areYouSureYouWantToLeaveProject: 'Are you sure you want to leave the project?',
@ -147,6 +149,7 @@ export default {
deleteTask_title: 'Delete Task',
deleteUser_title: 'Delete User',
deletedUser_title: 'Deleted User',
deleteWebhook_title: 'Delete Webhook',
description: 'Description',
detectAutomatically: 'Detect automatically',
display: 'Display',
@ -177,6 +180,8 @@ export default {
enterFilename: 'Enter filename',
enterListTitle: 'Enter list title...',
enterTaskDescription: 'Enter task description...',
events: 'Events',
excludedEvents: 'Excluded events',
filterByLabels_title: 'Filter By Labels',
filterByMembers_title: 'Filter By Members',
forPersonalProjects: 'For personal projects.',
@ -281,6 +286,7 @@ export default {
typeTitleToConfirm: 'Type the title to confirm.',
unsavedChanges: 'Unsaved changes',
uploadedImages: 'Uploaded images',
url: 'URL',
userActions_title: 'User Actions',
userAddedCardToList: '<0>{{user}}</0> added <2>{{card}}</2> to {{list}}',
userAddedThisCardToList: '<0>{{user}}</0> added this card to {{list}}',
@ -308,6 +314,7 @@ export default {
userRemovedUserFromThisCard: '<0>{{actorUser}}</0> removed {{removedUser}} from this card',
username: 'Username',
users: 'Users',
webhooks: 'Webhooks',
viewer: 'Viewer',
viewers: 'Viewers',
visualTaskManagementWithLists: 'Visual task management with lists.',
@ -333,6 +340,7 @@ export default {
addTaskList: 'Add task list',
addToCard: 'Add to card',
addUser: 'Add user',
addWebhook: 'Add webhook',
archive: 'Archive',
archiveCard: 'Archive card',
archiveCard_title: 'Archive Card',
@ -373,6 +381,7 @@ export default {
deleteTask_title: 'Delete Task',
deleteUser: 'Delete user',
deleteUser_title: 'Delete User',
deleteWebhook: 'Delete webhook',
dismissAll: 'Dismiss all',
duplicate: 'Duplicate',
duplicateCard_title: 'Duplicate Card',

View file

@ -0,0 +1,88 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { attr, fk } from 'redux-orm';
import BaseModel from './BaseModel';
import ActionTypes from '../constants/ActionTypes';
export default class extends BaseModel {
static modelName = 'Webhook';
static fields = {
id: attr(),
name: attr(),
url: attr(),
accessToken: attr(),
events: attr(),
excludedEvents: attr(),
boardId: fk({
to: 'Board',
as: 'board',
relatedName: 'webhooks',
}),
};
static reducer({ type, payload }, Webhook) {
switch (type) {
case ActionTypes.SOCKET_RECONNECT_HANDLE:
Webhook.all().delete();
payload.webhooks.forEach((webhook) => {
Webhook.upsert(webhook);
});
break;
case ActionTypes.CORE_INITIALIZE:
case ActionTypes.USER_UPDATE_HANDLE:
if (payload.webhooks) {
payload.webhooks.forEach((webhook) => {
Webhook.upsert(webhook);
});
}
break;
case ActionTypes.WEBHOOK_CREATE:
case ActionTypes.WEBHOOK_CREATE_HANDLE:
case ActionTypes.WEBHOOK_UPDATE__SUCCESS:
case ActionTypes.WEBHOOK_UPDATE_HANDLE:
Webhook.upsert(payload.webhook);
break;
case ActionTypes.WEBHOOK_CREATE__SUCCESS:
Webhook.withId(payload.localId).delete();
Webhook.upsert(payload.webhook);
break;
case ActionTypes.WEBHOOK_CREATE__FAILURE:
Webhook.withId(payload.localId).delete();
break;
case ActionTypes.WEBHOOK_UPDATE:
Webhook.withId(payload.id).update(payload.data);
break;
case ActionTypes.WEBHOOK_DELETE:
Webhook.withId(payload.id).delete();
break;
case ActionTypes.WEBHOOK_DELETE__SUCCESS:
case ActionTypes.WEBHOOK_DELETE_HANDLE: {
const webhookModel = Webhook.withId(payload.webhook.id);
if (webhookModel) {
webhookModel.delete();
}
break;
}
default:
}
}
static getAllQuerySet() {
return this.orderBy(['id.length', 'id']);
}
}

View file

@ -3,6 +3,7 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import Webhook from './Webhook';
import User from './User';
import Project from './Project';
import ProjectManager from './ProjectManager';
@ -25,6 +26,7 @@ import Notification from './Notification';
import NotificationService from './NotificationService';
export {
Webhook,
User,
Project,
ProjectManager,

View file

@ -26,6 +26,7 @@ import {
Task,
TaskList,
User,
Webhook,
} from './models';
const orm = new ORM({
@ -33,6 +34,7 @@ const orm = new ORM({
});
orm.register(
Webhook,
User,
Project,
ProjectManager,

View file

@ -10,6 +10,7 @@ import request from '../request';
import api from '../../../api';
import mergeRecords from '../../../utils/merge-records';
import { isUserAdminOrProjectOwner } from '../../../utils/record-helpers';
import { UserRoles } from '../../../constants/Enums';
export function* fetchCore() {
const {
@ -17,6 +18,11 @@ export function* fetchCore() {
included: { notificationServices: notificationServices1 },
} = yield call(request, api.getCurrentUser, true);
let webhooks;
if (user.role === UserRoles.ADMIN) {
({ items: webhooks } = yield call(request, api.getWebhooks));
}
let users1;
if (isUserAdminOrProjectOwner(user)) {
({ items: users1 } = yield call(request, api.getUsers));
@ -101,6 +107,7 @@ export function* fetchCore() {
return {
user,
board,
webhooks,
projectManagers,
backgroundImages,
baseCustomFieldGroups,

View file

@ -21,6 +21,7 @@ export function* initializeCore() {
const {
user,
board,
webhooks,
users,
projects,
projectManagers,
@ -50,6 +51,7 @@ export function* initializeCore() {
actions.initializeCore(
user,
board,
webhooks,
users,
projects,
projectManagers,

View file

@ -7,6 +7,7 @@ import router from './router';
import socket from './socket';
import core from './core';
import modals from './modals';
import webhooks from './webhooks';
import users from './users';
import projects from './projects';
import projectManagers from './project-managers';
@ -33,6 +34,7 @@ export default {
...socket,
...core,
...modals,
...webhooks,
...users,
...projects,
...projectManagers,

View file

@ -24,6 +24,7 @@ export function* handleSocketReconnect() {
let config;
let user;
let board;
let webhooks;
let users;
let projects;
let projectManagers;
@ -51,6 +52,7 @@ export function* handleSocketReconnect() {
({
user,
board,
webhooks,
users,
projects,
projectManagers,
@ -81,6 +83,7 @@ export function* handleSocketReconnect() {
config,
user,
board,
webhooks,
users,
projects,
projectManagers,

View file

@ -70,6 +70,7 @@ export function* handleUserUpdate(user) {
let config;
let board;
let webhooks;
let users1;
let users2;
let users3;
@ -102,6 +103,7 @@ export function* handleUserUpdate(user) {
if (user.role === UserRoles.ADMIN) {
({ item: config } = yield call(request, api.getConfig));
({ items: webhooks } = yield call(request, api.getWebhooks));
({
items: projects,
@ -164,6 +166,7 @@ export function* handleUserUpdate(user) {
boardIds,
config,
board,
webhooks,
mergeRecords(users1, users2, users3),
projects,
projectManagers,

View file

@ -0,0 +1,89 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { call, put } from 'redux-saga/effects';
import request from '../request';
import actions from '../../../actions';
import api from '../../../api';
import { createLocalId } from '../../../utils/local-id';
export function* createWebhook(data) {
const localId = yield call(createLocalId);
yield put(
actions.createWebhook({
...data,
id: localId,
}),
);
let webhook;
try {
({ item: webhook } = yield call(request, api.createWebhook, {
...data,
events: data.events && data.events.join(','),
excludedEvents: data.excludedEvents && data.excludedEvents.join(','),
}));
} catch (error) {
yield put(actions.createWebhook.failure(localId, error));
return;
}
yield put(actions.createWebhook.success(localId, webhook));
}
export function* handleWebhookCreate(webhook) {
yield put(actions.handleWebhookCreate(webhook));
}
export function* updateWebhook(id, data) {
yield put(actions.updateWebhook(id, data));
let webhook;
try {
({ item: webhook } = yield call(request, api.updateWebhook, id, {
...data,
events: data.events && data.events.join(','),
excludedEvents: data.excludedEvents && data.excludedEvents.join(','),
}));
} catch (error) {
yield put(actions.updateWebhook.failure(id, error));
return;
}
yield put(actions.updateWebhook.success(webhook));
}
export function* handleWebhookUpdate(webhook) {
yield put(actions.handleWebhookUpdate(webhook));
}
export function* deleteWebhook(id) {
yield put(actions.deleteWebhook(id));
let webhook;
try {
({ item: webhook } = yield call(request, api.deleteWebhook, id));
} catch (error) {
yield put(actions.deleteWebhook.failure(id, error));
return;
}
yield put(actions.deleteWebhook.success(webhook));
}
export function* handleWebhookDelete(webhook) {
yield put(actions.handleWebhookDelete(webhook));
}
export default {
createWebhook,
handleWebhookCreate,
updateWebhook,
handleWebhookUpdate,
deleteWebhook,
handleWebhookDelete,
};

View file

@ -7,6 +7,7 @@ import router from './router';
import socket from './socket';
import core from './core';
import modals from './modals';
import webhooks from './webhooks';
import users from './users';
import projects from './projects';
import projectManagers from './project-managers';
@ -33,6 +34,7 @@ export default [
socket,
core,
modals,
webhooks,
users,
projects,
projectManagers,

View file

@ -0,0 +1,30 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { all, takeEvery } from 'redux-saga/effects';
import services from '../services';
import EntryActionTypes from '../../../constants/EntryActionTypes';
export default function* webhooksWatchers() {
yield all([
takeEvery(EntryActionTypes.WEBHOOK_CREATE, ({ payload: { data } }) =>
services.createWebhook(data),
),
takeEvery(EntryActionTypes.WEBHOOK_CREATE_HANDLE, ({ payload: { webhook } }) =>
services.handleWebhookCreate(webhook),
),
takeEvery(EntryActionTypes.WEBHOOK_UPDATE, ({ payload: { id, data } }) =>
services.updateWebhook(id, data),
),
takeEvery(EntryActionTypes.WEBHOOK_UPDATE_HANDLE, ({ payload: { webhook } }) =>
services.handleWebhookUpdate(webhook),
),
takeEvery(EntryActionTypes.WEBHOOK_DELETE, ({ payload: { id } }) => services.deleteWebhook(id)),
takeEvery(EntryActionTypes.WEBHOOK_DELETE_HANDLE, ({ payload: { webhook } }) =>
services.handleWebhookDelete(webhook),
),
]);
}

View file

@ -8,6 +8,7 @@ import common from './common';
import core from './core';
import modals from './modals';
import positioning from './positioning';
import webhooks from './webhooks';
import users from './users';
import projects from './projects';
import projectManagers from './project-managers';
@ -35,6 +36,7 @@ export default {
...core,
...modals,
...positioning,
...webhooks,
...users,
...projects,
...projectManagers,

View file

@ -0,0 +1,41 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { createSelector } from 'redux-orm';
import orm from '../orm';
import { isLocalId } from '../utils/local-id';
export const makeSelectWebhookById = () =>
createSelector(
orm,
(_, id) => id,
({ Webhook }, id) => {
const webhookModel = Webhook.withId(id);
if (!webhookModel) {
return webhookModel;
}
return {
...webhookModel.ref,
isPersisted: !isLocalId(webhookModel.id),
};
},
);
export const selectWebhookById = makeSelectWebhookById();
export const selectWebhookIds = createSelector(orm, ({ Webhook }) =>
Webhook.getAllQuerySet()
.toRefArray()
.map((webhook) => webhook.id),
);
export default {
makeSelectWebhookById,
selectWebhookById,
selectWebhookIds,
};

View file

@ -80,15 +80,6 @@ services:
# - SMTP_PASSWORD=
# - SMTP_FROM="Demo Demo" <demo@demo.demo>
# - SMTP_TLS_REJECT_UNAUTHORIZED=false
# Optional fields: accessToken, events, excludedEvents
# - |
# WEBHOOKS=[{
# "url": "http://localhost:3001",
# "accessToken": "notaccesstoken",
# "events": ["cardCreate", "cardUpdate", "cardDelete"],
# "excludedEvents": ["notificationCreate", "notificationUpdate"]
# }]
depends_on:
postgres:
condition: service_healthy

View file

@ -100,15 +100,6 @@ services:
# - SMTP_PASSWORD__FILE=/run/secrets/smtp_password
# - SMTP_FROM="Demo Demo" <demo@demo.demo>
# - SMTP_TLS_REJECT_UNAUTHORIZED=false
# Optional fields: accessToken, events, excludedEvents
# - |
# WEBHOOKS=[{
# "url": "http://localhost:3001",
# "accessToken": "notaccesstoken",
# "events": ["cardCreate", "cardUpdate", "cardDelete"],
# "excludedEvents": ["notificationCreate", "notificationUpdate"]
# }]
depends_on:
postgres:
condition: service_healthy

View file

@ -72,14 +72,6 @@ SECRET_KEY=notsecretkey
# SMTP_FROM="Demo Demo" <demo@demo.demo>
# SMTP_TLS_REJECT_UNAUTHORIZED=false
# Optional fields: accessToken, events, excludedEvents
# WEBHOOKS='[{
# "url": "http://localhost:3001",
# "accessToken": "notaccesstoken",
# "events": ["cardCreate", "cardUpdate", "cardDelete"],
# "excludedEvents": ["notificationCreate", "notificationUpdate"]
# }]'
## Do not edit this
TZ=UTC

View file

@ -0,0 +1,76 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
const { isUrl } = require('../../../utils/validators');
const Errors = {
LIMIT_REACHED: {
limitReached: 'Limit reached',
},
};
module.exports = {
inputs: {
name: {
type: 'string',
maxLength: 128,
required: true,
},
url: {
type: 'string',
maxLength: 2048,
custom: isUrl,
required: true,
},
accessToken: {
type: 'string',
isNotEmptyString: true,
maxLength: 512,
allowNull: true,
},
events: {
type: 'string',
isNotEmptyString: true,
maxLength: 2048,
allowNull: true,
},
excludedEvents: {
type: 'string',
isNotEmptyString: true,
maxLength: 2048,
allowNull: true,
},
},
exits: {
limitReached: {
responseType: 'conflict',
},
},
async fn(inputs) {
const { currentUser } = this.req;
const values = _.pick(inputs, ['name', 'url', 'accessToken']);
const events = inputs.events && inputs.events.split(',');
const excludedEvents = inputs.excludedEvents && inputs.excludedEvents.split(',');
const webhook = await sails.helpers.webhooks.createOne
.with({
values: {
...values,
events,
excludedEvents,
},
actorUser: currentUser,
request: this.req,
})
.intercept('limitReached', () => Errors.LIMIT_REACHED);
return {
item: webhook,
};
},
};

View file

@ -0,0 +1,51 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
const { idInput } = require('../../../utils/inputs');
const Errors = {
WEBHOOK_NOT_FOUND: {
webhookNotFound: 'Webhook not found',
},
};
module.exports = {
inputs: {
id: {
...idInput,
required: true,
},
},
exits: {
webhookNotFound: {
responseType: 'notFound',
},
},
async fn(inputs) {
const { currentUser } = this.req;
let webhook = await Webhook.qm.getOneById(inputs.id);
if (!webhook) {
throw Errors.WEBHOOK_NOT_FOUND;
}
webhook = await sails.helpers.webhooks.deleteOne.with({
record: webhook,
actorUser: currentUser,
request: this.req,
});
if (!webhook) {
throw Errors.WEBHOOK_NOT_FOUND;
}
return {
item: webhook,
};
},
};

View file

@ -0,0 +1,14 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
module.exports = {
async fn() {
const webhooks = await Webhook.qm.getAll();
return {
items: webhooks,
};
},
};

View file

@ -0,0 +1,89 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
const { isUrl } = require('../../../utils/validators');
const { idInput } = require('../../../utils/inputs');
const Errors = {
WEBHOOK_NOT_FOUND: {
webhookNotFound: 'Webhook not found',
},
};
module.exports = {
inputs: {
id: {
...idInput,
required: true,
},
name: {
type: 'string',
isNotEmptyString: true,
maxLength: 128,
},
url: {
type: 'string',
maxLength: 2048,
custom: isUrl,
},
accessToken: {
type: 'string',
isNotEmptyString: true,
maxLength: 512,
allowNull: true,
},
events: {
type: 'string',
isNotEmptyString: true,
maxLength: 2048,
allowNull: true,
},
excludedEvents: {
type: 'string',
isNotEmptyString: true,
maxLength: 2048,
allowNull: true,
},
},
exits: {
webhookNotFound: {
responseType: 'notFound',
},
},
async fn(inputs) {
const { currentUser } = this.req;
let webhook = await Webhook.qm.getOneById(inputs.id);
if (!webhook) {
throw Errors.WEBHOOK_NOT_FOUND;
}
const values = _.pick(inputs, ['name', 'url', 'accessToken']);
const events = inputs.events && inputs.events.split(',');
const excludedEvents = inputs.excludedEvents && inputs.excludedEvents.split(',');
webhook = await sails.helpers.webhooks.updateOne.with({
record: webhook,
values: {
...values,
events,
excludedEvents,
},
actorUser: currentUser,
request: this.req,
});
if (!webhook) {
throw Errors.WEBHOOK_NOT_FOUND;
}
return {
item: webhook,
};
},
};

View file

@ -105,6 +105,10 @@ module.exports = {
type: 'ref',
required: true,
},
webhooks: {
type: 'ref',
required: true,
},
request: {
type: 'ref',
},
@ -130,7 +134,8 @@ module.exports = {
);
sails.helpers.utils.sendWebhooks.with({
event: 'actionCreate',
webhooks: inputs.webhooks,
event: Webhook.Events.ACTION_CREATE,
buildData: () => ({
item: action,
included: {
@ -158,6 +163,7 @@ module.exports = {
project: inputs.project,
board: inputs.board,
list: inputs.list,
webhooks: inputs.webhooks,
});
}
} else {
@ -187,6 +193,7 @@ module.exports = {
project: inputs.project,
board: inputs.board,
list: inputs.list,
webhooks: inputs.webhooks,
}),
),
);

View file

@ -48,8 +48,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'attachmentCreate',
webhooks,
event: Webhook.Events.ATTACHMENT_CREATE,
buildData: () => ({
item: sails.helpers.attachments.presentOne(attachment),
included: {
@ -65,6 +68,7 @@ module.exports = {
if (!values.card.coverAttachmentId) {
if (attachment.type === Attachment.Types.FILE && attachment.data.image) {
await sails.helpers.cards.updateOne.with({
webhooks,
record: values.card,
values: {
coverAttachmentId: attachment.id,

View file

@ -37,6 +37,7 @@ module.exports = {
async fn(inputs) {
if (inputs.record.id === inputs.card.coverAttachmentId) {
await sails.helpers.cards.updateOne.with({
webhooks,
record: inputs.card,
values: {
coverAttachmentId: null,
@ -66,8 +67,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'attachmentDelete',
webhooks,
event: Webhook.Events.ATTACHMENT_DELETE,
buildData: () => ({
item: sails.helpers.attachments.presentOne(attachment),
included: {

View file

@ -53,8 +53,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'attachmentUpdate',
webhooks,
event: Webhook.Events.ATTACHMENT_UPDATE,
buildData: () => ({
item: sails.helpers.attachments.presentOne(attachment),
included: {

View file

@ -47,8 +47,11 @@ module.exports = {
);
});
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'backgroundImageCreate',
webhooks,
event: Webhook.Events.BACKGROUND_IMAGE_CREATE,
buildData: () => ({
item: sails.helpers.backgroundImages.presentOne(backgroundImage),
included: {
@ -60,6 +63,7 @@ module.exports = {
await sails.helpers.projects.updateOne.with({
scoper,
webhooks,
record: values.project,
values: {
backgroundImage,

View file

@ -31,6 +31,7 @@ module.exports = {
if (inputs.record.id === inputs.project.backgroundImageId) {
await sails.helpers.projects.updateOne.with({
scoper,
webhooks,
record: inputs.project,
values: {
backgroundType: null,
@ -58,8 +59,11 @@ module.exports = {
);
});
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'backgroundImageDelete',
webhooks,
event: Webhook.Events.BACKGROUND_IMAGE_DELETE,
buildData: () => ({
item: sails.helpers.backgroundImages.presentOne(backgroundImage),
included: {

View file

@ -43,8 +43,11 @@ module.exports = {
);
});
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'baseCustomFieldGroupCreate',
webhooks,
event: Webhook.Events.BASE_CUSTOM_FIELD_GROUP_CREATE,
buildData: () => ({
item: baseCustomFieldGroup,
included: {

View file

@ -45,8 +45,11 @@ module.exports = {
);
});
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'baseCustomFieldGroupDelete',
webhooks,
event: Webhook.Events.BASE_CUSTOM_FIELD_GROUP_DELETE,
buildData: () => ({
item: baseCustomFieldGroup,
included: {

View file

@ -49,8 +49,11 @@ module.exports = {
);
});
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'baseCustomFieldGroupUpdate',
webhooks,
event: Webhook.Events.BASE_CUSTOM_FIELD_GROUP_UPDATE,
buildData: () => ({
item: baseCustomFieldGroup,
included: {

View file

@ -86,8 +86,11 @@ module.exports = {
});
});
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'boardMembershipCreate',
webhooks,
event: Webhook.Events.BOARD_MEMBERSHIP_CREATE,
buildData: () => ({
item: boardMembership,
included: {

View file

@ -106,8 +106,11 @@ module.exports = {
});
});
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'boardMembershipDelete',
webhooks,
event: Webhook.Events.BOARD_MEMBERSHIP_DELETE,
buildData: () => ({
item: boardMembership,
included: {

View file

@ -75,8 +75,11 @@ module.exports = {
});
});
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'boardMembershipUpdate',
webhooks,
event: Webhook.Events.BOARD_MEMBERSHIP_UPDATE,
buildData: () => ({
item: boardMembership,
included: {

View file

@ -108,8 +108,11 @@ module.exports = {
);
});
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'boardCreate',
webhooks,
event: Webhook.Events.BOARD_CREATE,
buildData: () => ({
item: board,
included: {

View file

@ -49,8 +49,11 @@ module.exports = {
);
});
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'boardDelete',
webhooks,
event: Webhook.Events.BOARD_DELETE,
buildData: () => ({
item: board,
included: {

View file

@ -104,8 +104,11 @@ module.exports = {
);
});
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'boardUpdate',
webhooks,
event: Webhook.Events.BOARD_UPDATE,
buildData: () => ({
item: board,
included: {

View file

@ -61,8 +61,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'cardLabelCreate',
webhooks,
event: Webhook.Events.CARD_LABEL_CREATE,
buildData: () => ({
item: cardLabel,
included: {

View file

@ -47,8 +47,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'cardLabelDelete',
webhooks,
event: Webhook.Events.CARD_LABEL_DELETE,
buildData: () => ({
item: cardLabel,
included: {

View file

@ -61,8 +61,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'cardMembershipCreate',
webhooks,
event: Webhook.Events.CARD_MEMBERSHIP_CREATE,
buildData: () => ({
item: cardMembership,
included: {
@ -106,6 +109,7 @@ module.exports = {
}
await sails.helpers.actions.createOne.with({
webhooks,
values: {
type: Action.Types.ADD_MEMBER_TO_CARD,
data: {

View file

@ -51,8 +51,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'cardMembershipDelete',
webhooks,
event: Webhook.Events.CARD_MEMBERSHIP_DELETE,
buildData: () => ({
item: cardMembership,
included: {
@ -82,6 +85,7 @@ module.exports = {
}
await sails.helpers.actions.createOne.with({
webhooks,
values: {
type: Action.Types.REMOVE_MEMBER_FROM_CARD,
data: {

View file

@ -84,8 +84,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'cardCreate',
webhooks,
event: Webhook.Events.CARD_CREATE,
buildData: () => ({
item: card,
included: {
@ -120,6 +123,7 @@ module.exports = {
}
await sails.helpers.actions.createOne.with({
webhooks,
values: {
card,
type: Action.Types.CREATE_CARD,

View file

@ -45,8 +45,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'cardDelete',
webhooks,
event: Webhook.Events.CARD_DELETE,
buildData: () => ({
item: card,
included: {

View file

@ -228,8 +228,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'cardCreate',
webhooks,
event: Webhook.Events.CARD_CREATE,
buildData: () => ({
item: card,
included: {
@ -272,6 +275,7 @@ module.exports = {
}
await sails.helpers.actions.createOne.with({
webhooks,
values: {
card,
type: Action.Types.CREATE_CARD, // TODO: introduce separate type?

View file

@ -31,6 +31,8 @@ module.exports = {
},
);
const webhooks = await Webhook.qm.getAll();
notifications.forEach((notification) => {
sails.sockets.broadcast(
`user:${notification.userId}`,
@ -43,7 +45,8 @@ module.exports = {
// TODO: with prevData?
sails.helpers.utils.sendWebhooks.with({
event: 'notificationUpdate',
webhooks,
event: Webhook.Events.NOTIFICATION_UPDATE,
buildData: () => ({
item: notification,
}),

View file

@ -31,6 +31,9 @@ module.exports = {
type: 'ref',
required: true,
},
webhooks: {
type: 'ref',
},
request: {
type: 'ref',
},
@ -104,6 +107,8 @@ module.exports = {
if (_.isEmpty(values)) {
card = inputs.record;
} else {
const { webhooks = await Webhook.qm.getAll() } = inputs;
if (!_.isNil(values.position)) {
const cards = await Card.qm.getByListId(list.id, {
exceptIdOrIds: inputs.record.id,
@ -402,6 +407,7 @@ module.exports = {
const { id } = await sails.helpers.labels.createOne.with({
project,
webhooks,
values: {
..._.omit(label, ['id', 'boardId', 'createdAt', 'updatedAt']),
board,
@ -459,6 +465,7 @@ module.exports = {
if (values.list) {
await sails.helpers.actions.createOne.with({
webhooks,
values: {
card,
type: Action.Types.MOVE_CARD,
@ -477,7 +484,8 @@ module.exports = {
}
sails.helpers.utils.sendWebhooks.with({
event: 'cardUpdate',
webhooks,
event: Webhook.Events.CARD_UPDATE,
buildData: () => ({
item: card,
included: {

View file

@ -79,8 +79,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'commentCreate',
webhooks,
event: Webhook.Events.COMMENT_CREATE,
buildData: () => ({
item: comment,
included: {
@ -125,6 +128,7 @@ module.exports = {
await Promise.all(
notifiableUserIds.map((userId) =>
sails.helpers.notifications.createOne.with({
webhooks,
values: {
userId,
comment,

View file

@ -47,8 +47,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'commentDelete',
webhooks,
event: Webhook.Events.COMMENT_DELETE,
buildData: () => ({
item: comment,
included: {

View file

@ -53,8 +53,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'commentUpdate',
webhooks,
event: Webhook.Events.COMMENT_UPDATE,
buildData: () => ({
item: comment,
included: {

View file

@ -87,8 +87,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'customFieldGroupCreate',
webhooks,
event: Webhook.Events.CUSTOM_FIELD_GROUP_CREATE,
buildData: () => ({
item: customFieldGroup,
included: {

View file

@ -95,8 +95,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'customFieldGroupCreate',
webhooks,
event: Webhook.Events.CUSTOM_FIELD_GROUP_CREATE,
buildData: () => ({
item: customFieldGroup,
included: {

View file

@ -41,8 +41,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'customFieldGroupDelete',
webhooks,
event: Webhook.Events.CUSTOM_FIELD_GROUP_DELETE,
buildData: () => ({
item: customFieldGroup,
included: {

View file

@ -49,8 +49,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'customFieldGroupDelete',
webhooks,
event: Webhook.Events.CUSTOM_FIELD_GROUP_DELETE,
buildData: () => ({
item: customFieldGroup,
included: {

View file

@ -91,8 +91,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'customFieldGroupUpdate',
webhooks,
event: Webhook.Events.CUSTOM_FIELD_GROUP_UPDATE,
buildData: () => ({
item: customFieldGroup,
included: {

View file

@ -99,8 +99,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'customFieldGroupUpdate',
webhooks,
event: Webhook.Events.CUSTOM_FIELD_GROUP_UPDATE,
buildData: () => ({
item: customFieldGroup,
included: {

View file

@ -49,9 +49,12 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
// TODO: with prevData?
sails.helpers.utils.sendWebhooks.with({
event: 'customFieldValueUpdate',
webhooks,
event: Webhook.Events.CUSTOM_FIELD_VALUE_UPDATE,
buildData: () => ({
item: customFieldValue,
included: {

View file

@ -51,8 +51,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'customFieldValueDelete',
webhooks,
event: Webhook.Events.CUSTOM_FIELD_VALUE_DELETE,
buildData: () => ({
item: customFieldValue,
included: {

View file

@ -93,8 +93,11 @@ module.exports = {
);
});
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'customFieldCreate',
webhooks,
event: Webhook.Events.CUSTOM_FIELD_CREATE,
buildData: () => ({
item: customField,
included: {

View file

@ -103,8 +103,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'customFieldCreate',
webhooks,
event: Webhook.Events.CUSTOM_FIELD_CREATE,
buildData: () => ({
item: customField,
included: {

View file

@ -49,8 +49,11 @@ module.exports = {
);
});
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'customFieldDelete',
webhooks,
event: Webhook.Events.CUSTOM_FIELD_DELETE,
buildData: () => ({
item: customField,
included: {

View file

@ -66,8 +66,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'customFieldDelete',
webhooks,
event: Webhook.Events.CUSTOM_FIELD_DELETE,
buildData: () => ({
item: customField,
included: {

View file

@ -94,8 +94,11 @@ module.exports = {
);
});
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'customFieldUpdate',
webhooks,
event: Webhook.Events.CUSTOM_FIELD_UPDATE,
buildData: () => ({
item: customField,
included: {

View file

@ -109,8 +109,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'customFieldUpdate',
webhooks,
event: Webhook.Events.CUSTOM_FIELD_UPDATE,
buildData: () => ({
item: customField,
included: {

View file

@ -17,6 +17,9 @@ module.exports = {
type: 'ref',
required: true,
},
webhooks: {
type: 'ref',
},
request: {
type: 'ref',
},
@ -70,8 +73,11 @@ module.exports = {
inputs.request,
);
const { webhooks = await Webhook.qm.getAll() } = inputs;
sails.helpers.utils.sendWebhooks.with({
event: 'labelCreate',
webhooks,
event: Webhook.Events.LABEL_CREATE,
buildData: () => ({
item: label,
included: {

View file

@ -41,8 +41,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'labelDelete',
webhooks,
event: Webhook.Events.LABEL_DELETE,
buildData: () => ({
item: label,
included: {

View file

@ -81,8 +81,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'labelUpdate',
webhooks,
event: Webhook.Events.LABEL_UPDATE,
buildData: () => ({
item: label,
included: {

View file

@ -38,8 +38,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'listClear',
webhooks,
event: Webhook.Events.LIST_CLEAR,
buildData: () => ({
item: inputs.record,
included: {

View file

@ -72,8 +72,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'listCreate',
webhooks,
event: Webhook.Events.LIST_CREATE,
buildData: () => ({
item: list,
included: {

View file

@ -57,8 +57,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'listDelete',
webhooks,
event: Webhook.Events.LIST_DELETE,
buildData: () => ({
item: list,
included: {

View file

@ -91,10 +91,13 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
cards.forEach((card) => {
// TODO: with prevData?
sails.helpers.utils.sendWebhooks.with({
event: 'cardUpdate',
webhooks,
event: Webhook.Events.CARD_UPDATE,
buildData: () => ({
item: card,
included: {

View file

@ -100,10 +100,13 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
cards.forEach((card) => {
// TODO: with prevData?
sails.helpers.utils.sendWebhooks.with({
event: 'cardUpdate',
webhooks,
event: Webhook.Events.CARD_UPDATE,
buildData: () => ({
item: card,
included: {

View file

@ -84,8 +84,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'listUpdate',
webhooks,
event: Webhook.Events.LIST_UPDATE,
buildData: () => ({
item: list,
included: {

View file

@ -62,8 +62,11 @@ module.exports = {
);
});
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'notificationServiceCreate',
webhooks,
event: Webhook.Events.NOTIFICATION_SERVICE_CREATE,
buildData: () => ({
item: notificationService,
included: {

View file

@ -48,8 +48,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'notificationServiceCreate',
webhooks,
event: Webhook.Events.NOTIFICATION_SERVICE_CREATE,
buildData: () => ({
item: notificationService,
included: {

View file

@ -49,8 +49,11 @@ module.exports = {
);
});
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'notificationServiceDelete',
webhooks,
event: Webhook.Events.NOTIFICATION_SERVICE_DELETE,
buildData: () => ({
item: notificationService,
included: {

View file

@ -35,8 +35,11 @@ module.exports = {
inputs.request,
);
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'notificationServiceDelete',
webhooks,
event: Webhook.Events.NOTIFICATION_SERVICE_DELETE,
buildData: () => ({
item: notificationService,
included: {

View file

@ -55,8 +55,11 @@ module.exports = {
);
});
const webhooks = await Webhook.qm.getAll();
sails.helpers.utils.sendWebhooks.with({
event: 'notificationServiceUpdate',
webhooks,
event: Webhook.Events.NOTIFICATION_SERVICE_UPDATE,
buildData: () => ({
item: notificationService,
included: {

Some files were not shown because too many files have changed in this diff Show more