Merge pull request #1650 from symonbaikov/feature/webhook-scope-filter
feat: filter webhooks per project, board, and account
This commit is contained in:
@@ -15,6 +15,9 @@ import styles from './WebhooksPane.module.scss';
|
|||||||
|
|
||||||
const WebhooksPane = React.memo(() => {
|
const WebhooksPane = React.memo(() => {
|
||||||
const webhookIds = useSelector(selectors.selectWebhookIds);
|
const webhookIds = useSelector(selectors.selectWebhookIds);
|
||||||
|
const projects = useSelector(selectors.selectProjects);
|
||||||
|
const boards = useSelector(selectors.selectBoards);
|
||||||
|
const users = useSelector(selectors.selectUsers);
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
@@ -27,7 +30,13 @@ const WebhooksPane = React.memo(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab.Pane attached={false} className={styles.wrapper}>
|
<Tab.Pane attached={false} className={styles.wrapper}>
|
||||||
<Webhooks ids={webhookIds} onCreate={handleCreate} />
|
<Webhooks
|
||||||
|
ids={webhookIds}
|
||||||
|
projects={projects}
|
||||||
|
boards={boards}
|
||||||
|
users={users}
|
||||||
|
onCreate={handleCreate}
|
||||||
|
/>
|
||||||
</Tab.Pane>
|
</Tab.Pane>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useImperativeHandle } from 'react';
|
import React, { useCallback, useImperativeHandle, useMemo } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Dropdown, Input } from 'semantic-ui-react';
|
import { Dropdown, Input } from 'semantic-ui-react';
|
||||||
@@ -13,7 +13,8 @@ import WEBHOOK_EVENTS from '../../../constants/WebhookEvents';
|
|||||||
|
|
||||||
import styles from './Editor.module.scss';
|
import styles from './Editor.module.scss';
|
||||||
|
|
||||||
const Editor = React.forwardRef(({ data, isReadOnly, onFieldChange }, ref) => {
|
const Editor = React.forwardRef(
|
||||||
|
({ data, projects, boards, users, isReadOnly, onFieldChange }, ref) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
|
|
||||||
const [nameFieldRef, handleNameFieldRef] = useNestedRef('inputRef');
|
const [nameFieldRef, handleNameFieldRef] = useNestedRef('inputRef');
|
||||||
@@ -43,6 +44,34 @@ const Editor = React.forwardRef(({ data, isReadOnly, onFieldChange }, ref) => {
|
|||||||
[focusNameField, selectNameField, selectUrlField],
|
[focusNameField, selectNameField, selectUrlField],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleScopeChange = useCallback(
|
||||||
|
(_, { name, value }) => {
|
||||||
|
onFieldChange(null, { name, value: value === '' ? null : value });
|
||||||
|
},
|
||||||
|
[onFieldChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectOptions = useMemo(
|
||||||
|
() => projects.map((project) => ({ text: project.name, value: project.id })),
|
||||||
|
[projects],
|
||||||
|
);
|
||||||
|
|
||||||
|
const boardOptions = useMemo(() => {
|
||||||
|
const filtered = data.projectId
|
||||||
|
? boards.filter((board) => board.projectId === data.projectId)
|
||||||
|
: boards;
|
||||||
|
return filtered.map((board) => ({ text: board.name, value: board.id }));
|
||||||
|
}, [boards, data.projectId]);
|
||||||
|
|
||||||
|
const userOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
users.map((user) => ({
|
||||||
|
text: user.name || user.username || user.email,
|
||||||
|
value: user.id,
|
||||||
|
})),
|
||||||
|
[users],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.text}>{t('common.title')}</div>
|
<div className={styles.text}>{t('common.title')}</div>
|
||||||
@@ -83,6 +112,63 @@ const Editor = React.forwardRef(({ data, isReadOnly, onFieldChange }, ref) => {
|
|||||||
className={styles.field}
|
className={styles.field}
|
||||||
onChange={onFieldChange}
|
onChange={onFieldChange}
|
||||||
/>
|
/>
|
||||||
|
<div className={styles.text}>
|
||||||
|
{t('common.project')} (
|
||||||
|
{t('common.optional', {
|
||||||
|
context: 'inline',
|
||||||
|
})}
|
||||||
|
)
|
||||||
|
</div>
|
||||||
|
<Dropdown
|
||||||
|
selection
|
||||||
|
clearable
|
||||||
|
fluid
|
||||||
|
name="projectId"
|
||||||
|
options={projectOptions}
|
||||||
|
value={data.projectId || ''}
|
||||||
|
placeholder={t('common.all')}
|
||||||
|
disabled={isReadOnly}
|
||||||
|
className={styles.field}
|
||||||
|
onChange={handleScopeChange}
|
||||||
|
/>
|
||||||
|
<div className={styles.text}>
|
||||||
|
{t('common.board')} (
|
||||||
|
{t('common.optional', {
|
||||||
|
context: 'inline',
|
||||||
|
})}
|
||||||
|
)
|
||||||
|
</div>
|
||||||
|
<Dropdown
|
||||||
|
selection
|
||||||
|
clearable
|
||||||
|
fluid
|
||||||
|
name="boardId"
|
||||||
|
options={boardOptions}
|
||||||
|
value={data.boardId || ''}
|
||||||
|
placeholder={t('common.all')}
|
||||||
|
disabled={isReadOnly}
|
||||||
|
className={styles.field}
|
||||||
|
onChange={handleScopeChange}
|
||||||
|
/>
|
||||||
|
<div className={styles.text}>
|
||||||
|
{t('common.account')} (
|
||||||
|
{t('common.optional', {
|
||||||
|
context: 'inline',
|
||||||
|
})}
|
||||||
|
)
|
||||||
|
</div>
|
||||||
|
<Dropdown
|
||||||
|
selection
|
||||||
|
clearable
|
||||||
|
fluid
|
||||||
|
name="userId"
|
||||||
|
options={userOptions}
|
||||||
|
value={data.userId || ''}
|
||||||
|
placeholder={t('common.all')}
|
||||||
|
disabled={isReadOnly}
|
||||||
|
className={styles.field}
|
||||||
|
onChange={handleScopeChange}
|
||||||
|
/>
|
||||||
{data.excludedEvents.length === 0 && (
|
{data.excludedEvents.length === 0 && (
|
||||||
<>
|
<>
|
||||||
<div className={styles.text}>
|
<div className={styles.text}>
|
||||||
@@ -137,10 +223,14 @@ const Editor = React.forwardRef(({ data, isReadOnly, onFieldChange }, ref) => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
Editor.propTypes = {
|
Editor.propTypes = {
|
||||||
data: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
data: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||||
|
projects: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||||
|
boards: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||||
|
users: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||||
isReadOnly: PropTypes.bool,
|
isReadOnly: PropTypes.bool,
|
||||||
onFieldChange: PropTypes.func.isRequired,
|
onFieldChange: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import ConfirmationStep from '../../common/ConfirmationStep';
|
|||||||
import styles from './Item.module.scss';
|
import styles from './Item.module.scss';
|
||||||
import { useToggle } from '../../../lib/hooks';
|
import { useToggle } from '../../../lib/hooks';
|
||||||
|
|
||||||
const Item = React.memo(({ id }) => {
|
const Item = React.memo(({ id, projects, boards, users }) => {
|
||||||
const selectWebhookById = useMemo(() => selectors.makeSelectWebhookById(), []);
|
const selectWebhookById = useMemo(() => selectors.makeSelectWebhookById(), []);
|
||||||
|
|
||||||
const webhook = useSelector((state) => selectWebhookById(state, id));
|
const webhook = useSelector((state) => selectWebhookById(state, id));
|
||||||
@@ -36,6 +36,9 @@ const Item = React.memo(({ id }) => {
|
|||||||
accessToken: webhook.accessToken,
|
accessToken: webhook.accessToken,
|
||||||
events: webhook.events,
|
events: webhook.events,
|
||||||
excludedEvents: webhook.excludedEvents,
|
excludedEvents: webhook.excludedEvents,
|
||||||
|
projectId: webhook.projectId,
|
||||||
|
boardId: webhook.boardId,
|
||||||
|
userId: webhook.userId,
|
||||||
}),
|
}),
|
||||||
[webhook],
|
[webhook],
|
||||||
);
|
);
|
||||||
@@ -47,6 +50,9 @@ const Item = React.memo(({ id }) => {
|
|||||||
accessToken: defaultData.accessToken || '',
|
accessToken: defaultData.accessToken || '',
|
||||||
events: defaultData.events || [],
|
events: defaultData.events || [],
|
||||||
excludedEvents: defaultData.excludedEvents || [],
|
excludedEvents: defaultData.excludedEvents || [],
|
||||||
|
projectId: defaultData.projectId || null,
|
||||||
|
boardId: defaultData.boardId || null,
|
||||||
|
userId: defaultData.userId || null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const cleanData = useMemo(
|
const cleanData = useMemo(
|
||||||
@@ -57,6 +63,9 @@ const Item = React.memo(({ id }) => {
|
|||||||
accessToken: data.accessToken.trim() || null,
|
accessToken: data.accessToken.trim() || null,
|
||||||
events: data.events.length === 0 ? null : data.events,
|
events: data.events.length === 0 ? null : data.events,
|
||||||
excludedEvents: data.excludedEvents.length === 0 ? null : data.excludedEvents,
|
excludedEvents: data.excludedEvents.length === 0 ? null : data.excludedEvents,
|
||||||
|
projectId: data.projectId || null,
|
||||||
|
boardId: data.boardId || null,
|
||||||
|
userId: data.userId || null,
|
||||||
}),
|
}),
|
||||||
[data],
|
[data],
|
||||||
);
|
);
|
||||||
@@ -99,6 +108,9 @@ const Item = React.memo(({ id }) => {
|
|||||||
<Editor
|
<Editor
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
data={data}
|
data={data}
|
||||||
|
projects={projects}
|
||||||
|
boards={boards}
|
||||||
|
users={users}
|
||||||
isReadOnly={!webhook.isPersisted}
|
isReadOnly={!webhook.isPersisted}
|
||||||
onFieldChange={handleFieldChange}
|
onFieldChange={handleFieldChange}
|
||||||
/>
|
/>
|
||||||
@@ -132,6 +144,9 @@ const Item = React.memo(({ id }) => {
|
|||||||
|
|
||||||
Item.propTypes = {
|
Item.propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
|
projects: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||||
|
boards: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||||
|
users: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Item;
|
export default Item;
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import Item from './Item';
|
|||||||
import Editor from './Editor';
|
import Editor from './Editor';
|
||||||
|
|
||||||
const DEFAULT_DATA = {
|
const DEFAULT_DATA = {
|
||||||
|
projectId: null,
|
||||||
|
boardId: null,
|
||||||
|
userId: null,
|
||||||
name: '',
|
name: '',
|
||||||
url: '',
|
url: '',
|
||||||
accessToken: '',
|
accessToken: '',
|
||||||
@@ -22,7 +25,7 @@ const DEFAULT_DATA = {
|
|||||||
excludedEvents: [],
|
excludedEvents: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const Webhooks = React.memo(({ ids, onCreate }) => {
|
const Webhooks = React.memo(({ ids, projects, boards, users, onCreate }) => {
|
||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
|
|
||||||
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
|
const [data, handleFieldChange, setData] = useForm(DEFAULT_DATA);
|
||||||
@@ -38,6 +41,9 @@ const Webhooks = React.memo(({ ids, onCreate }) => {
|
|||||||
accessToken: data.accessToken.trim() || null,
|
accessToken: data.accessToken.trim() || null,
|
||||||
events: data.events.length === 0 ? null : data.events,
|
events: data.events.length === 0 ? null : data.events,
|
||||||
excludedEvents: data.excludedEvents.length === 0 ? null : data.excludedEvents,
|
excludedEvents: data.excludedEvents.length === 0 ? null : data.excludedEvents,
|
||||||
|
projectId: data.projectId || null,
|
||||||
|
boardId: data.boardId || null,
|
||||||
|
userId: data.userId || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!cleanData.name) {
|
if (!cleanData.name) {
|
||||||
@@ -72,14 +78,21 @@ const Webhooks = React.memo(({ ids, onCreate }) => {
|
|||||||
{ids.length > 0 && (
|
{ids.length > 0 && (
|
||||||
<Accordion styled fluid>
|
<Accordion styled fluid>
|
||||||
{ids.map((id) => (
|
{ids.map((id) => (
|
||||||
<Item key={id} id={id} />
|
<Item key={id} id={id} projects={projects} boards={boards} users={users} />
|
||||||
))}
|
))}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
)}
|
)}
|
||||||
{ids.length < 10 && (
|
{ids.length < 10 && (
|
||||||
<Segment>
|
<Segment>
|
||||||
<Form onSubmit={handleCreateSubmit}>
|
<Form onSubmit={handleCreateSubmit}>
|
||||||
<Editor ref={editorRef} data={data} onFieldChange={handleFieldChange} />
|
<Editor
|
||||||
|
ref={editorRef}
|
||||||
|
data={data}
|
||||||
|
projects={projects}
|
||||||
|
boards={boards}
|
||||||
|
users={users}
|
||||||
|
onFieldChange={handleFieldChange}
|
||||||
|
/>
|
||||||
<Button positive>{t('action.addWebhook')}</Button>
|
<Button positive>{t('action.addWebhook')}</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Segment>
|
</Segment>
|
||||||
@@ -90,6 +103,9 @@ const Webhooks = React.memo(({ ids, onCreate }) => {
|
|||||||
|
|
||||||
Webhooks.propTypes = {
|
Webhooks.propTypes = {
|
||||||
ids: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
ids: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||||
|
projects: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||||
|
boards: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||||
|
users: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||||
onCreate: PropTypes.func.isRequired,
|
onCreate: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,21 @@ export default class extends BaseModel {
|
|||||||
accessToken: attr(),
|
accessToken: attr(),
|
||||||
events: attr(),
|
events: attr(),
|
||||||
excludedEvents: attr(),
|
excludedEvents: attr(),
|
||||||
|
projectId: fk({
|
||||||
|
to: 'Project',
|
||||||
|
as: 'project',
|
||||||
|
relatedName: 'webhooks',
|
||||||
|
}),
|
||||||
boardId: fk({
|
boardId: fk({
|
||||||
to: 'Board',
|
to: 'Board',
|
||||||
as: 'board',
|
as: 'board',
|
||||||
relatedName: 'webhooks',
|
relatedName: 'webhooks',
|
||||||
}),
|
}),
|
||||||
|
userId: fk({
|
||||||
|
to: 'User',
|
||||||
|
as: 'user',
|
||||||
|
relatedName: 'webhooks',
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
static reducer({ type, payload }, Webhook) {
|
static reducer({ type, payload }, Webhook) {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { isLocalId } from '../utils/local-id';
|
|||||||
import { isListArchiveOrTrash } from '../utils/record-helpers';
|
import { isListArchiveOrTrash } from '../utils/record-helpers';
|
||||||
import { ListTypes } from '../constants/Enums';
|
import { ListTypes } from '../constants/Enums';
|
||||||
|
|
||||||
|
export const selectBoards = createSelector(orm, ({ Board }) => Board.all().toRefArray());
|
||||||
|
|
||||||
export const makeSelectBoardById = () =>
|
export const makeSelectBoardById = () =>
|
||||||
createSelector(
|
createSelector(
|
||||||
orm,
|
orm,
|
||||||
@@ -484,6 +486,7 @@ export const selectIsBoardWithIdExists = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
selectBoards,
|
||||||
makeSelectBoardById,
|
makeSelectBoardById,
|
||||||
selectBoardById,
|
selectBoardById,
|
||||||
makeSelectCurrentUserMembershipByBoardId,
|
makeSelectCurrentUserMembershipByBoardId,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { selectPath } from './router';
|
|||||||
import { selectCurrentUserId } from './users';
|
import { selectCurrentUserId } from './users';
|
||||||
import { isLocalId } from '../utils/local-id';
|
import { isLocalId } from '../utils/local-id';
|
||||||
|
|
||||||
|
export const selectProjects = createSelector(orm, ({ Project }) => Project.all().toRefArray());
|
||||||
|
|
||||||
export const makeSelectProjectById = () =>
|
export const makeSelectProjectById = () =>
|
||||||
createSelector(
|
createSelector(
|
||||||
orm,
|
orm,
|
||||||
@@ -311,6 +313,7 @@ export const selectIsCurrentUserManagerForCurrentProject = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
selectProjects,
|
||||||
makeSelectProjectById,
|
makeSelectProjectById,
|
||||||
selectProjectById,
|
selectProjectById,
|
||||||
makeSelectBoardIdsByProjectId,
|
makeSelectBoardIdsByProjectId,
|
||||||
|
|||||||
@@ -22,6 +22,21 @@
|
|||||||
* - name
|
* - name
|
||||||
* - url
|
* - url
|
||||||
* properties:
|
* properties:
|
||||||
|
* projectId:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* description: Optional project scope
|
||||||
|
* example: "1357158568008091264"
|
||||||
|
* boardId:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* description: Optional board scope
|
||||||
|
* example: "1357158568008091264"
|
||||||
|
* userId:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* description: Optional acting-user scope
|
||||||
|
* example: "1357158568008091264"
|
||||||
* name:
|
* name:
|
||||||
* type: string
|
* type: string
|
||||||
* maxLength: 128
|
* maxLength: 128
|
||||||
@@ -72,6 +87,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const { isUrl } = require('../../../utils/validators');
|
const { isUrl } = require('../../../utils/validators');
|
||||||
|
const { idInput } = require('../../../utils/inputs');
|
||||||
|
|
||||||
const Errors = {
|
const Errors = {
|
||||||
LIMIT_REACHED: {
|
LIMIT_REACHED: {
|
||||||
@@ -81,6 +97,18 @@ const Errors = {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
inputs: {
|
inputs: {
|
||||||
|
projectId: {
|
||||||
|
...idInput,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
boardId: {
|
||||||
|
...idInput,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
...idInput,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
name: {
|
name: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
maxLength: 128,
|
maxLength: 128,
|
||||||
@@ -121,7 +149,7 @@ module.exports = {
|
|||||||
async fn(inputs) {
|
async fn(inputs) {
|
||||||
const { currentUser } = this.req;
|
const { currentUser } = this.req;
|
||||||
|
|
||||||
const values = _.pick(inputs, ['name', 'url', 'accessToken']);
|
const values = _.pick(inputs, ['projectId', 'boardId', 'userId', 'name', 'url', 'accessToken']);
|
||||||
const events = inputs.events && inputs.events.split(',');
|
const events = inputs.events && inputs.events.split(',');
|
||||||
const excludedEvents = inputs.excludedEvents && inputs.excludedEvents.split(',');
|
const excludedEvents = inputs.excludedEvents && inputs.excludedEvents.split(',');
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,21 @@
|
|||||||
* schema:
|
* schema:
|
||||||
* type: object
|
* type: object
|
||||||
* properties:
|
* properties:
|
||||||
|
* projectId:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* description: Optional project scope
|
||||||
|
* example: "1357158568008091264"
|
||||||
|
* boardId:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* description: Optional board scope
|
||||||
|
* example: "1357158568008091264"
|
||||||
|
* userId:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* description: Optional acting-user scope
|
||||||
|
* example: "1357158568008091264"
|
||||||
* name:
|
* name:
|
||||||
* type: string
|
* type: string
|
||||||
* maxLength: 128
|
* maxLength: 128
|
||||||
@@ -91,6 +106,18 @@ module.exports = {
|
|||||||
...idInput,
|
...idInput,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
projectId: {
|
||||||
|
...idInput,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
boardId: {
|
||||||
|
...idInput,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
...idInput,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
name: {
|
name: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
isNotEmptyString: true,
|
isNotEmptyString: true,
|
||||||
@@ -136,7 +163,7 @@ module.exports = {
|
|||||||
throw Errors.WEBHOOK_NOT_FOUND;
|
throw Errors.WEBHOOK_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = _.pick(inputs, ['name', 'url', 'accessToken']);
|
const values = _.pick(inputs, ['projectId', 'boardId', 'userId', 'name', 'url', 'accessToken']);
|
||||||
const events = inputs.events && inputs.events.split(',');
|
const events = inputs.events && inputs.events.split(',');
|
||||||
const excludedEvents = inputs.excludedEvents && inputs.excludedEvents.split(',');
|
const excludedEvents = inputs.excludedEvents && inputs.excludedEvents.split(',');
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,48 @@ const { ProxyAgent } = require('undici');
|
|||||||
|
|
||||||
const Webhook = require('../../models/Webhook');
|
const Webhook = require('../../models/Webhook');
|
||||||
|
|
||||||
|
const BOARD_ITEM_EVENTS = new Set([
|
||||||
|
Webhook.Events.BOARD_CREATE,
|
||||||
|
Webhook.Events.BOARD_UPDATE,
|
||||||
|
Webhook.Events.BOARD_DELETE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const PROJECT_ITEM_EVENTS = new Set([
|
||||||
|
Webhook.Events.PROJECT_CREATE,
|
||||||
|
Webhook.Events.PROJECT_UPDATE,
|
||||||
|
Webhook.Events.PROJECT_DELETE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
function resolveScope(event, data, override) {
|
||||||
|
const result = { projectId: null, boardId: null, ...(override || {}) };
|
||||||
|
if (!data) return result;
|
||||||
|
|
||||||
|
const { item } = data;
|
||||||
|
const included = data.included || {};
|
||||||
|
|
||||||
|
if (!result.projectId) {
|
||||||
|
if (included.projects && included.projects[0]) {
|
||||||
|
result.projectId = included.projects[0].id;
|
||||||
|
} else if (item && PROJECT_ITEM_EVENTS.has(event)) {
|
||||||
|
result.projectId = item.id;
|
||||||
|
} else if (item && item.projectId) {
|
||||||
|
result.projectId = item.projectId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.boardId) {
|
||||||
|
if (included.boards && included.boards[0]) {
|
||||||
|
result.boardId = included.boards[0].id;
|
||||||
|
} else if (item && BOARD_ITEM_EVENTS.has(event)) {
|
||||||
|
result.boardId = item.id;
|
||||||
|
} else if (item && item.boardId) {
|
||||||
|
result.boardId = item.boardId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} Included
|
* @typedef {Object} Included
|
||||||
* @property {any[]} [users] - Array of users (optional).
|
* @property {any[]} [users] - Array of users (optional).
|
||||||
@@ -104,10 +146,15 @@ module.exports = {
|
|||||||
type: 'ref',
|
type: 'ref',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
scope: {
|
||||||
|
type: 'ref',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
fn(inputs) {
|
fn(inputs) {
|
||||||
const webhooks = inputs.webhooks.filter((webhook) => {
|
const userId = inputs.user && inputs.user.id;
|
||||||
|
|
||||||
|
const eventFilteredWebhooks = inputs.webhooks.filter((webhook) => {
|
||||||
if (!webhook.url) {
|
if (!webhook.url) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -120,6 +167,31 @@ module.exports = {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (webhook.userId && webhook.userId !== userId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eventFilteredWebhooks.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = inputs.buildData();
|
||||||
|
const prevData = inputs.buildPrevData && inputs.buildPrevData();
|
||||||
|
|
||||||
|
const scope = resolveScope(inputs.event, data, inputs.scope);
|
||||||
|
|
||||||
|
const webhooks = eventFilteredWebhooks.filter((webhook) => {
|
||||||
|
if (webhook.projectId && webhook.projectId !== scope.projectId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (webhook.boardId && webhook.boardId !== scope.boardId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -127,9 +199,6 @@ module.exports = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = inputs.buildData();
|
|
||||||
const prevData = inputs.buildPrevData && inputs.buildPrevData();
|
|
||||||
|
|
||||||
webhooks.forEach((webhook) => {
|
webhooks.forEach((webhook) => {
|
||||||
sendWebhook(
|
sendWebhook(
|
||||||
webhook,
|
webhook,
|
||||||
|
|||||||
@@ -30,6 +30,21 @@
|
|||||||
* type: string
|
* type: string
|
||||||
* description: Unique identifier for the webhook
|
* description: Unique identifier for the webhook
|
||||||
* example: "1357158568008091264"
|
* example: "1357158568008091264"
|
||||||
|
* projectId:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* description: Optional project scope; webhook fires only for events within this project
|
||||||
|
* example: "1357158568008091264"
|
||||||
|
* boardId:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* description: Optional board scope; webhook fires only for events within this board
|
||||||
|
* example: "1357158568008091264"
|
||||||
|
* userId:
|
||||||
|
* type: string
|
||||||
|
* nullable: true
|
||||||
|
* description: Optional acting-user scope; webhook fires only for events triggered by this user
|
||||||
|
* example: "1357158568008091264"
|
||||||
* name:
|
* name:
|
||||||
* type: string
|
* type: string
|
||||||
* description: Name/title of the webhook
|
* description: Name/title of the webhook
|
||||||
@@ -200,9 +215,17 @@ module.exports = {
|
|||||||
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
|
// ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
|
||||||
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
|
// ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
|
||||||
|
|
||||||
|
projectId: {
|
||||||
|
model: 'Project',
|
||||||
|
columnName: 'project_id',
|
||||||
|
},
|
||||||
boardId: {
|
boardId: {
|
||||||
model: 'Board',
|
model: 'Board',
|
||||||
columnName: 'board_id',
|
columnName: 'board_id',
|
||||||
},
|
},
|
||||||
|
userId: {
|
||||||
|
model: 'User',
|
||||||
|
columnName: 'user_id',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.up = (knex) =>
|
||||||
|
knex.schema.alterTable('webhook', (table) => {
|
||||||
|
table.bigInteger('project_id');
|
||||||
|
table.bigInteger('user_id');
|
||||||
|
|
||||||
|
table.index('project_id');
|
||||||
|
table.index('user_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
exports.down = (knex) =>
|
||||||
|
knex.schema.alterTable('webhook', (table) => {
|
||||||
|
table.dropIndex('user_id');
|
||||||
|
table.dropIndex('project_id');
|
||||||
|
|
||||||
|
table.dropColumn('user_id');
|
||||||
|
table.dropColumn('project_id');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user