mirror of
https://github.com/plankanban/planka.git
synced 2025-07-27 17:19:43 +02:00
parent
ad7fb51cfa
commit
2ee1166747
1557 changed files with 76832 additions and 47042 deletions
|
@ -1,3 +1,8 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import http from './http';
|
||||
|
||||
/* Actions */
|
||||
|
@ -5,13 +10,13 @@ import http from './http';
|
|||
const createAccessToken = (data, headers) =>
|
||||
http.post('/access-tokens?withHttpOnlyToken=true', data, headers);
|
||||
|
||||
const exchangeForAccessTokenUsingOidc = (data, headers) =>
|
||||
http.post('/access-tokens/exchange-using-oidc?withHttpOnlyToken=true', data, headers);
|
||||
const exchangeForAccessTokenWithOidc = (data, headers) =>
|
||||
http.post('/access-tokens/exchange-with-oidc?withHttpOnlyToken=true', data, headers);
|
||||
|
||||
const deleteCurrentAccessToken = (headers) => http.delete('/access-tokens/me', undefined, headers);
|
||||
|
||||
export default {
|
||||
createAccessToken,
|
||||
exchangeForAccessTokenUsingOidc,
|
||||
exchangeForAccessTokenWithOidc,
|
||||
deleteCurrentAccessToken,
|
||||
};
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
/*!
|
||||
* 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';
|
||||
import { transformUser } from './users';
|
||||
|
||||
/* Transformers */
|
||||
|
||||
export const transformActivity = (activity) => ({
|
||||
...activity,
|
||||
createdAt: new Date(activity.createdAt),
|
||||
...(activity.createdAt && {
|
||||
createdAt: new Date(activity.createdAt),
|
||||
}),
|
||||
});
|
||||
|
||||
/* Actions */
|
||||
|
@ -14,10 +20,6 @@ const getActivities = (cardId, data, headers) =>
|
|||
socket.get(`/cards/${cardId}/actions`, data, headers).then((body) => ({
|
||||
...body,
|
||||
items: body.items.map(transformActivity),
|
||||
included: {
|
||||
...body.included,
|
||||
users: body.included.users.map(transformUser),
|
||||
},
|
||||
}));
|
||||
|
||||
/* Event handlers */
|
||||
|
@ -29,13 +31,7 @@ const makeHandleActivityCreate = (next) => (body) => {
|
|||
});
|
||||
};
|
||||
|
||||
const makeHandleActivityUpdate = makeHandleActivityCreate;
|
||||
|
||||
const makeHandleActivityDelete = makeHandleActivityCreate;
|
||||
|
||||
export default {
|
||||
getActivities,
|
||||
makeHandleActivityCreate,
|
||||
makeHandleActivityUpdate,
|
||||
makeHandleActivityDelete,
|
||||
};
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import http from './http';
|
||||
import socket from './socket';
|
||||
|
||||
|
@ -5,17 +10,34 @@ import socket from './socket';
|
|||
|
||||
export const transformAttachment = (attachment) => ({
|
||||
...attachment,
|
||||
createdAt: new Date(attachment.createdAt),
|
||||
...(attachment.createdAt && {
|
||||
createdAt: new Date(attachment.createdAt),
|
||||
}),
|
||||
});
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createAttachment = (cardId, data, requestId, headers) =>
|
||||
http.post(`/cards/${cardId}/attachments?requestId=${requestId}`, data, headers).then((body) => ({
|
||||
const createAttachment = (cardId, data, headers) =>
|
||||
socket.post(`/cards/${cardId}/attachments`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformAttachment(body.item),
|
||||
}));
|
||||
|
||||
const createAttachmentWithFile = (cardId, { file, ...data }, requestId, headers) =>
|
||||
http
|
||||
.post(
|
||||
`/cards/${cardId}/attachments?requestId=${requestId}`,
|
||||
{
|
||||
...data,
|
||||
file,
|
||||
},
|
||||
headers,
|
||||
)
|
||||
.then((body) => ({
|
||||
...body,
|
||||
item: transformAttachment(body.item),
|
||||
}));
|
||||
|
||||
const updateAttachment = (id, data, headers) =>
|
||||
socket.patch(`/attachments/${id}`, data, headers).then((body) => ({
|
||||
...body,
|
||||
|
@ -43,6 +65,7 @@ const makeHandleAttachmentDelete = makeHandleAttachmentCreate;
|
|||
|
||||
export default {
|
||||
createAttachment,
|
||||
createAttachmentWithFile,
|
||||
updateAttachment,
|
||||
deleteAttachment,
|
||||
makeHandleAttachmentCreate,
|
||||
|
|
27
client/src/api/background-images.js
Normal file
27
client/src/api/background-images.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import http from './http';
|
||||
import socket from './socket';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createBackgroundImage = (projectId, { file, ...data }, requestId, headers) =>
|
||||
http.post(
|
||||
`/projects/${projectId}/background-images?requestId=${requestId}`,
|
||||
{
|
||||
...data,
|
||||
file,
|
||||
},
|
||||
headers,
|
||||
);
|
||||
|
||||
const deleteBackgroundImage = (id, headers) =>
|
||||
socket.delete(`/background-images/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createBackgroundImage,
|
||||
deleteBackgroundImage,
|
||||
};
|
23
client/src/api/base-custom-field-groups.js
Executable file
23
client/src/api/base-custom-field-groups.js
Executable 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 createBaseCustomFieldGroup = (projectId, data, headers) =>
|
||||
socket.post(`/projects/${projectId}/base-custom-field-groups`, data, headers);
|
||||
|
||||
const updateBaseCustomFieldGroup = (id, data, headers) =>
|
||||
socket.patch(`/base-custom-field-groups/${id}`, data, headers);
|
||||
|
||||
const deleteBaseCustomFieldGroup = (id, headers) =>
|
||||
socket.delete(`/base-custom-field-groups/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createBaseCustomFieldGroup,
|
||||
updateBaseCustomFieldGroup,
|
||||
deleteBaseCustomFieldGroup,
|
||||
};
|
|
@ -1,50 +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';
|
||||
|
||||
/* Transformers */
|
||||
|
||||
export const transformBoardMembership = (boardMembership) => ({
|
||||
...boardMembership,
|
||||
createdAt: new Date(boardMembership.createdAt),
|
||||
});
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createBoardMembership = (boardId, data, headers) =>
|
||||
socket.post(`/boards/${boardId}/memberships`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformBoardMembership(body.item),
|
||||
}));
|
||||
socket.post(`/boards/${boardId}/board-memberships`, data, headers);
|
||||
|
||||
const updateBoardMembership = (id, data, headers) =>
|
||||
socket.patch(`/board-memberships/${id}`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformBoardMembership(body.item),
|
||||
}));
|
||||
socket.patch(`/board-memberships/${id}`, data, headers);
|
||||
|
||||
const deleteBoardMembership = (id, headers) =>
|
||||
socket.delete(`/board-memberships/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformBoardMembership(body.item),
|
||||
}));
|
||||
|
||||
/* Event handlers */
|
||||
|
||||
const makeHandleBoardMembershipCreate = (next) => (body) => {
|
||||
next({
|
||||
...body,
|
||||
item: transformBoardMembership(body.item),
|
||||
});
|
||||
};
|
||||
|
||||
const makeHandleBoardMembershipUpdate = makeHandleBoardMembershipCreate;
|
||||
|
||||
const makeHandleBoardMembershipDelete = makeHandleBoardMembershipCreate;
|
||||
socket.delete(`/board-memberships/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createBoardMembership,
|
||||
updateBoardMembership,
|
||||
deleteBoardMembership,
|
||||
makeHandleBoardMembershipCreate,
|
||||
makeHandleBoardMembershipUpdate,
|
||||
makeHandleBoardMembershipDelete,
|
||||
};
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
import socket from './socket';
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import http from './http';
|
||||
import { transformUser } from './users';
|
||||
import { transformBoardMembership } from './board-memberships';
|
||||
import socket from './socket';
|
||||
import { transformCard } from './cards';
|
||||
import { transformAttachment } from './attachments';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createBoard = (projectId, data, headers) =>
|
||||
socket.post(`/projects/${projectId}/boards`, data, headers).then((body) => ({
|
||||
...body,
|
||||
included: {
|
||||
...body.included,
|
||||
boardMemberships: body.included.boardMemberships.map(transformBoardMembership),
|
||||
},
|
||||
}));
|
||||
socket.post(`/projects/${projectId}/boards`, data, headers);
|
||||
|
||||
const createBoardWithImport = (projectId, data, requestId, headers) =>
|
||||
http.post(`/projects/${projectId}/boards?requestId=${requestId}`, data, headers);
|
||||
|
@ -26,8 +23,6 @@ const getBoard = (id, subscribe, headers) =>
|
|||
...body,
|
||||
included: {
|
||||
...body.included,
|
||||
users: body.included.users.map(transformUser),
|
||||
boardMemberships: body.included.boardMemberships.map(transformBoardMembership),
|
||||
cards: body.included.cards.map(transformCard),
|
||||
attachments: body.included.attachments.map(transformAttachment),
|
||||
},
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
/*!
|
||||
* 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 createCardLabel = (cardId, data, headers) =>
|
||||
socket.post(`/cards/${cardId}/labels`, data, headers);
|
||||
socket.post(`/cards/${cardId}/card-labels`, data, headers);
|
||||
|
||||
const deleteCardLabel = (cardId, labelId, headers) =>
|
||||
socket.delete(`/cards/${cardId}/labels/${labelId}`, undefined, headers);
|
||||
socket.delete(`/cards/${cardId}/card-labels/labelId:${labelId}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createCardLabel,
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
/*!
|
||||
* 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 createCardMembership = (cardId, data, headers) =>
|
||||
socket.post(`/cards/${cardId}/memberships`, data, headers);
|
||||
socket.post(`/cards/${cardId}/card-memberships`, data, headers);
|
||||
|
||||
const deleteCardMembership = (cardId, userId, headers) =>
|
||||
socket.delete(`/cards/${cardId}/memberships?userId=${userId}`, undefined, headers);
|
||||
socket.delete(`/cards/${cardId}/card-memberships/userId:${userId}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createCardMembership,
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import omit from 'lodash/omit';
|
||||
|
||||
import socket from './socket';
|
||||
import { transformAttachment } from './attachments';
|
||||
import { transformActivity } from './activities';
|
||||
import { transformNotification } from './notifications';
|
||||
|
||||
/* Transformers */
|
||||
|
||||
|
@ -16,6 +25,12 @@ export const transformCard = (card) => ({
|
|||
}),
|
||||
},
|
||||
}),
|
||||
...(card.createdAt && {
|
||||
createdAt: new Date(card.createdAt),
|
||||
}),
|
||||
...(card.listChangedAt && {
|
||||
listChangedAt: new Date(card.listChangedAt),
|
||||
}),
|
||||
});
|
||||
|
||||
export const transformCardData = (data) => ({
|
||||
|
@ -35,6 +50,16 @@ export const transformCardData = (data) => ({
|
|||
|
||||
/* Actions */
|
||||
|
||||
const getCards = (listId, data, headers) =>
|
||||
socket.get(`/lists/${listId}/cards`, data, headers).then((body) => ({
|
||||
...body,
|
||||
items: body.items.map(transformCard),
|
||||
included: {
|
||||
...body.included,
|
||||
attachments: body.included.attachments.map(transformAttachment),
|
||||
},
|
||||
}));
|
||||
|
||||
const createCard = (listId, data, headers) =>
|
||||
socket.post(`/lists/${listId}/cards`, transformCardData(data), headers).then((body) => ({
|
||||
...body,
|
||||
|
@ -61,6 +86,20 @@ const duplicateCard = (id, data, headers) =>
|
|||
socket.post(`/cards/${id}/duplicate`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformCard(body.item),
|
||||
included: {
|
||||
...body.included,
|
||||
attachments: body.included.attachments.map(transformAttachment),
|
||||
},
|
||||
}));
|
||||
|
||||
const readCardNotifications = (id, headers) =>
|
||||
socket.post(`/cards/${id}/read-notifications`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformCard(body.item),
|
||||
included: {
|
||||
...body.included,
|
||||
notifications: body.included.notifications.map(transformNotification),
|
||||
},
|
||||
}));
|
||||
|
||||
const deleteCard = (id, headers) =>
|
||||
|
@ -71,6 +110,17 @@ const deleteCard = (id, headers) =>
|
|||
|
||||
/* Event handlers */
|
||||
|
||||
const makeHandleCardsUpdate = (next) => (body) => {
|
||||
next({
|
||||
...body,
|
||||
items: body.items.map(transformCard),
|
||||
included: body.included && {
|
||||
...omit(body.included, 'actions'),
|
||||
activities: body.included.actions.map(transformActivity),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const makeHandleCardCreate = (next) => (body) => {
|
||||
next({
|
||||
...body,
|
||||
|
@ -80,14 +130,17 @@ const makeHandleCardCreate = (next) => (body) => {
|
|||
|
||||
const makeHandleCardUpdate = makeHandleCardCreate;
|
||||
|
||||
const makeHandleCardDelete = makeHandleCardCreate;
|
||||
const makeHandleCardDelete = makeHandleCardUpdate;
|
||||
|
||||
export default {
|
||||
getCards,
|
||||
createCard,
|
||||
getCard,
|
||||
updateCard,
|
||||
deleteCard,
|
||||
duplicateCard,
|
||||
readCardNotifications,
|
||||
deleteCard,
|
||||
makeHandleCardsUpdate,
|
||||
makeHandleCardCreate,
|
||||
makeHandleCardUpdate,
|
||||
makeHandleCardDelete,
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
import socket from './socket';
|
||||
import { transformActivity } from './activities';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createCommentActivity = (cardId, data, headers) =>
|
||||
socket.post(`/cards/${cardId}/comment-actions`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformActivity(body.item),
|
||||
}));
|
||||
|
||||
const updateCommentActivity = (id, data, headers) =>
|
||||
socket.patch(`/comment-actions/${id}`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformActivity(body.item),
|
||||
}));
|
||||
|
||||
const deleteCommentActivity = (id, headers) =>
|
||||
socket.delete(`/comment-actions/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformActivity(body.item),
|
||||
}));
|
||||
|
||||
export default {
|
||||
createCommentActivity,
|
||||
updateCommentActivity,
|
||||
deleteCommentActivity,
|
||||
};
|
64
client/src/api/comments.js
Normal file
64
client/src/api/comments.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*!
|
||||
* 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';
|
||||
|
||||
/* Transformers */
|
||||
|
||||
export const transformComment = (comment) => ({
|
||||
...comment,
|
||||
...(comment.createdAt && {
|
||||
createdAt: new Date(comment.createdAt),
|
||||
}),
|
||||
});
|
||||
|
||||
/* Actions */
|
||||
|
||||
const getComments = (cardId, data, headers) =>
|
||||
socket.get(`/cards/${cardId}/comments`, data, headers).then((body) => ({
|
||||
...body,
|
||||
items: body.items.map(transformComment),
|
||||
}));
|
||||
|
||||
const createComment = (cardId, data, headers) =>
|
||||
socket.post(`/cards/${cardId}/comments`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformComment(body.item),
|
||||
}));
|
||||
|
||||
const updateComment = (id, data, headers) =>
|
||||
socket.patch(`/comments/${id}`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformComment(body.item),
|
||||
}));
|
||||
|
||||
const deleteComment = (id, headers) =>
|
||||
socket.delete(`/comments/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformComment(body.item),
|
||||
}));
|
||||
|
||||
/* Event handlers */
|
||||
|
||||
const makeHandleCommentCreate = (next) => (body) => {
|
||||
next({
|
||||
...body,
|
||||
item: transformComment(body.item),
|
||||
});
|
||||
};
|
||||
|
||||
const makeHandleCommentUpdate = makeHandleCommentCreate;
|
||||
|
||||
const makeHandleCommentDelete = makeHandleCommentUpdate;
|
||||
|
||||
export default {
|
||||
getComments,
|
||||
createComment,
|
||||
updateComment,
|
||||
deleteComment,
|
||||
makeHandleCommentCreate,
|
||||
makeHandleCommentUpdate,
|
||||
makeHandleCommentDelete,
|
||||
};
|
14
client/src/api/config.js
Normal file
14
client/src/api/config.js
Normal 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
|
||||
*/
|
||||
|
||||
import http from './http';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const getConfig = (headers) => http.get('/config', undefined, headers);
|
||||
|
||||
export default {
|
||||
getConfig,
|
||||
};
|
31
client/src/api/custom-field-groups.js
Executable file
31
client/src/api/custom-field-groups.js
Executable file
|
@ -0,0 +1,31 @@
|
|||
/*!
|
||||
* 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 createCustomFieldGroupInBoard = (cardId, data, headers) =>
|
||||
socket.post(`/boards/${cardId}/custom-field-groups`, data, headers);
|
||||
|
||||
const createCustomFieldGroupInCard = (cardId, data, headers) =>
|
||||
socket.post(`/cards/${cardId}/custom-field-groups`, data, headers);
|
||||
|
||||
const getCustomFieldGroup = (id, headers) =>
|
||||
socket.get(`/custom-field-groups/${id}`, undefined, headers);
|
||||
|
||||
const updateCustomFieldGroup = (id, data, headers) =>
|
||||
socket.patch(`/custom-field-groups/${id}`, data, headers);
|
||||
|
||||
const deleteCustomFieldGroup = (id, headers) =>
|
||||
socket.delete(`/custom-field-groups/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createCustomFieldGroupInBoard,
|
||||
createCustomFieldGroupInCard,
|
||||
getCustomFieldGroup,
|
||||
updateCustomFieldGroup,
|
||||
deleteCustomFieldGroup,
|
||||
};
|
27
client/src/api/custom-field-values.js
Normal file
27
client/src/api/custom-field-values.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*!
|
||||
* 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 updateCustomFieldValue = (cardId, customFieldGroupId, customFieldId, data, headers) =>
|
||||
socket.patch(
|
||||
`/cards/${cardId}/custom-field-values/customFieldGroupId:${customFieldGroupId}:customFieldId:${customFieldId}`,
|
||||
data,
|
||||
headers,
|
||||
);
|
||||
|
||||
const deleteCustomFieldValue = (cardId, customFieldGroupId, customFieldId, headers) =>
|
||||
socket.delete(
|
||||
`/cards/${cardId}/custom-field-values/customFieldGroupId:${customFieldGroupId}:customFieldId:${customFieldId}`,
|
||||
undefined,
|
||||
headers,
|
||||
);
|
||||
|
||||
export default {
|
||||
updateCustomFieldValue,
|
||||
deleteCustomFieldValue,
|
||||
};
|
27
client/src/api/custom-fields.js
Executable file
27
client/src/api/custom-fields.js
Executable file
|
@ -0,0 +1,27 @@
|
|||
/*!
|
||||
* 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 createCustomFieldInBaseGroup = (baseCustomFieldGroupId, data, headers) =>
|
||||
socket.post(`/base-custom-field-groups/${baseCustomFieldGroupId}/custom-fields`, data, headers);
|
||||
|
||||
const createCustomFieldInGroup = (customFieldGroupId, data, headers) =>
|
||||
socket.post(`/custom-field-groups/${customFieldGroupId}/custom-fields`, data, headers);
|
||||
|
||||
const updateCustomField = (id, data, headers) =>
|
||||
socket.patch(`/custom-fields/${id}`, data, headers);
|
||||
|
||||
const deleteCustomField = (id, headers) =>
|
||||
socket.delete(`/custom-fields/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createCustomFieldInBaseGroup,
|
||||
createCustomFieldInGroup,
|
||||
updateCustomField,
|
||||
deleteCustomField,
|
||||
};
|
|
@ -1,4 +1,7 @@
|
|||
import { fetch } from 'whatwg-fetch';
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import Config from '../constants/Config';
|
||||
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import http from './http';
|
||||
import socket from './socket';
|
||||
import root from './root';
|
||||
import config from './config';
|
||||
import accessTokens from './access-tokens';
|
||||
import users from './users';
|
||||
import projects from './projects';
|
||||
import projectManagers from './project-managers';
|
||||
import backgroundImages from './background-images';
|
||||
import baseCustomFieldGroups from './base-custom-field-groups';
|
||||
import boards from './boards';
|
||||
import boardMemberships from './board-memberships';
|
||||
import labels from './labels';
|
||||
|
@ -12,20 +19,27 @@ import lists from './lists';
|
|||
import cards from './cards';
|
||||
import cardMemberships from './card-memberships';
|
||||
import cardLabels from './card-labels';
|
||||
import taskLists from './task-lists';
|
||||
import tasks from './tasks';
|
||||
import attachments from './attachments';
|
||||
import customFieldGroups from './custom-field-groups';
|
||||
import customFields from './custom-fields';
|
||||
import customFieldValues from './custom-field-values';
|
||||
import comments from './comments';
|
||||
import activities from './activities';
|
||||
import commentActivities from './comment-activities';
|
||||
import notifications from './notifications';
|
||||
import notificationServices from './notification-services';
|
||||
|
||||
export { http, socket };
|
||||
|
||||
export default {
|
||||
...root,
|
||||
...config,
|
||||
...accessTokens,
|
||||
...users,
|
||||
...projects,
|
||||
...projectManagers,
|
||||
...backgroundImages,
|
||||
...baseCustomFieldGroups,
|
||||
...boards,
|
||||
...boardMemberships,
|
||||
...labels,
|
||||
|
@ -33,9 +47,14 @@ export default {
|
|||
...cards,
|
||||
...cardMemberships,
|
||||
...cardLabels,
|
||||
...taskLists,
|
||||
...tasks,
|
||||
...attachments,
|
||||
...customFieldGroups,
|
||||
...customFields,
|
||||
...customFieldValues,
|
||||
...comments,
|
||||
...activities,
|
||||
...commentActivities,
|
||||
...notifications,
|
||||
...notificationServices,
|
||||
};
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
/*!
|
||||
* 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 */
|
||||
|
|
|
@ -1,11 +1,30 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import omit from 'lodash/omit';
|
||||
|
||||
import socket from './socket';
|
||||
import { transformCard } from './cards';
|
||||
import { transformAttachment } from './attachments';
|
||||
import { transformActivity } from './activities';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createList = (boardId, data, headers) =>
|
||||
socket.post(`/boards/${boardId}/lists`, data, headers);
|
||||
|
||||
const getList = (id, headers) =>
|
||||
socket.get(`/lists/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
included: {
|
||||
...body.included,
|
||||
cards: body.included.cards.map(transformCard),
|
||||
attachments: body.included.attachments.map(transformAttachment),
|
||||
},
|
||||
}));
|
||||
|
||||
const updateList = (id, data, headers) => socket.patch(`/lists/${id}`, data, headers);
|
||||
|
||||
const sortList = (id, data, headers) =>
|
||||
|
@ -17,11 +36,30 @@ const sortList = (id, data, headers) =>
|
|||
},
|
||||
}));
|
||||
|
||||
const deleteList = (id, headers) => socket.delete(`/lists/${id}`, undefined, headers);
|
||||
const moveListCards = (id, data, headers) =>
|
||||
socket.post(`/lists/${id}/move-cards`, data, headers).then((body) => ({
|
||||
...body,
|
||||
included: {
|
||||
...omit(body.included, 'actions'),
|
||||
cards: body.included.cards.map(transformCard),
|
||||
activities: body.included.actions.map(transformActivity),
|
||||
},
|
||||
}));
|
||||
|
||||
const clearList = (id, headers) => socket.post(`/lists/${id}/clear`, undefined, headers);
|
||||
|
||||
const deleteList = (id, headers) =>
|
||||
socket.delete(`/lists/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
included: {
|
||||
...body.included,
|
||||
cards: body.included.cards.map(transformCard),
|
||||
},
|
||||
}));
|
||||
|
||||
/* Event handlers */
|
||||
|
||||
const makeHandleListSort = (next) => (body) => {
|
||||
const makeHandleListDelete = (next) => (body) => {
|
||||
next({
|
||||
...body,
|
||||
included: {
|
||||
|
@ -33,8 +71,11 @@ const makeHandleListSort = (next) => (body) => {
|
|||
|
||||
export default {
|
||||
createList,
|
||||
getList,
|
||||
updateList,
|
||||
sortList,
|
||||
moveListCards,
|
||||
clearList,
|
||||
deleteList,
|
||||
makeHandleListSort,
|
||||
makeHandleListDelete,
|
||||
};
|
||||
|
|
31
client/src/api/notification-services.js
Executable file
31
client/src/api/notification-services.js
Executable file
|
@ -0,0 +1,31 @@
|
|||
/*!
|
||||
* 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 createNotificationServiceInUser = (userId, data, headers) =>
|
||||
socket.post(`/users/${userId}/notification-services`, data, headers);
|
||||
|
||||
const createNotificationServiceInBoard = (boardId, data, headers) =>
|
||||
socket.post(`/boards/${boardId}/notification-services`, data, headers);
|
||||
|
||||
const updateNotificationService = (id, data, headers) =>
|
||||
socket.patch(`/notification-services/${id}`, data, headers);
|
||||
|
||||
const testNotificationService = (id, headers) =>
|
||||
socket.post(`/notification-services/${id}/test`, undefined, headers);
|
||||
|
||||
const deleteNotificationService = (id, headers) =>
|
||||
socket.delete(`/notification-services/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createNotificationServiceInUser,
|
||||
createNotificationServiceInBoard,
|
||||
updateNotificationService,
|
||||
testNotificationService,
|
||||
deleteNotificationService,
|
||||
};
|
|
@ -1,15 +1,21 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import omit from 'lodash/omit';
|
||||
|
||||
import socket from './socket';
|
||||
import { transformUser } from './users';
|
||||
import { transformCard } from './cards';
|
||||
import { transformActivity } from './activities';
|
||||
|
||||
/* Transformers */
|
||||
|
||||
export const transformNotification = (notification) => ({
|
||||
...omit(notification, 'actionId'),
|
||||
activityId: notification.actionId,
|
||||
...(notification.actionId
|
||||
? {
|
||||
...omit(notification, 'actionId'),
|
||||
activityId: notification.actionId,
|
||||
}
|
||||
: notification),
|
||||
});
|
||||
|
||||
/* Actions */
|
||||
|
@ -18,28 +24,25 @@ const getNotifications = (headers) =>
|
|||
socket.get('/notifications', undefined, headers).then((body) => ({
|
||||
...body,
|
||||
items: body.items.map(transformNotification),
|
||||
included: {
|
||||
...omit(body.included, 'actions'),
|
||||
users: body.included.users.map(transformUser),
|
||||
cards: body.included.cards.map(transformCard),
|
||||
activities: body.included.actions.map(transformActivity),
|
||||
},
|
||||
}));
|
||||
|
||||
const getNotification = (id, headers) =>
|
||||
/* const getNotification = (id, headers) =>
|
||||
socket.get(`/notifications/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformNotification(body.item),
|
||||
included: {
|
||||
...omit(body.included, 'actions'),
|
||||
users: body.included.users.map(transformUser),
|
||||
cards: body.included.cards.map(transformCard),
|
||||
activities: body.included.actions.map(transformActivity),
|
||||
},
|
||||
})); */
|
||||
|
||||
const updateNotification = (id, data, headers) =>
|
||||
socket.patch(`/notifications/${id}`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformNotification(body.item),
|
||||
}));
|
||||
|
||||
const updateNotifications = (ids, data, headers) =>
|
||||
socket.patch(`/notifications/${ids.join(',')}`, data, headers).then((body) => ({
|
||||
const readAllNotifications = (headers) =>
|
||||
socket.post('/notifications/read-all', undefined, headers).then((body) => ({
|
||||
...body,
|
||||
items: body.items.map(transformNotification),
|
||||
}));
|
||||
|
@ -57,8 +60,9 @@ const makeHandleNotificationUpdate = makeHandleNotificationCreate;
|
|||
|
||||
export default {
|
||||
getNotifications,
|
||||
getNotification,
|
||||
updateNotifications,
|
||||
// getNotification,
|
||||
updateNotification,
|
||||
readAllNotifications,
|
||||
makeHandleNotificationCreate,
|
||||
makeHandleNotificationUpdate,
|
||||
};
|
||||
|
|
|
@ -1,40 +1,19 @@
|
|||
/*!
|
||||
* 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';
|
||||
|
||||
/* Transformers */
|
||||
|
||||
export const transformProjectManager = (projectManager) => ({
|
||||
...projectManager,
|
||||
createdAt: new Date(projectManager.createdAt),
|
||||
});
|
||||
|
||||
/* Actions */
|
||||
|
||||
const createProjectManager = (projectId, data, headers) =>
|
||||
socket.post(`/projects/${projectId}/managers`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformProjectManager(body.item),
|
||||
}));
|
||||
socket.post(`/projects/${projectId}/project-managers`, data, headers);
|
||||
|
||||
const deleteProjectManager = (id, headers) =>
|
||||
socket.delete(`/project-managers/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformProjectManager(body.item),
|
||||
}));
|
||||
|
||||
/* Event handlers */
|
||||
|
||||
const makeHandleProjectManagerCreate = (next) => (body) => {
|
||||
next({
|
||||
...body,
|
||||
item: transformProjectManager(body.item),
|
||||
});
|
||||
};
|
||||
|
||||
const makeHandleProjectManagerDelete = makeHandleProjectManagerCreate;
|
||||
socket.delete(`/project-managers/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createProjectManager,
|
||||
deleteProjectManager,
|
||||
makeHandleProjectManagerCreate,
|
||||
makeHandleProjectManagerDelete,
|
||||
};
|
||||
|
|
|
@ -1,47 +1,20 @@
|
|||
import http from './http';
|
||||
/*!
|
||||
* 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';
|
||||
import { transformUser } from './users';
|
||||
import { transformProjectManager } from './project-managers';
|
||||
import { transformBoardMembership } from './board-memberships';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const getProjects = (headers) =>
|
||||
socket.get('/projects', undefined, headers).then((body) => ({
|
||||
...body,
|
||||
included: {
|
||||
...body.included,
|
||||
users: body.included.users.map(transformUser),
|
||||
projectManagers: body.included.projectManagers.map(transformProjectManager),
|
||||
boardMemberships: body.included.boardMemberships.map(transformBoardMembership),
|
||||
},
|
||||
}));
|
||||
const getProjects = (headers) => socket.get('/projects', undefined, headers);
|
||||
|
||||
const createProject = (data, headers) =>
|
||||
socket.post('/projects', data, headers).then((body) => ({
|
||||
...body,
|
||||
included: {
|
||||
...body.included,
|
||||
projectManagers: body.included.projectManagers.map(transformProjectManager),
|
||||
},
|
||||
}));
|
||||
const createProject = (data, headers) => socket.post('/projects', data, headers);
|
||||
|
||||
const getProject = (id, headers) =>
|
||||
socket.get(`/projects/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
included: {
|
||||
...body.included,
|
||||
users: body.included.users.map(transformUser),
|
||||
projectManagers: body.included.projectManagers.map(transformProjectManager),
|
||||
boardMemberships: body.included.boardMemberships.map(transformBoardMembership),
|
||||
},
|
||||
}));
|
||||
const getProject = (id, headers) => socket.get(`/projects/${id}`, undefined, headers);
|
||||
|
||||
const updateProject = (id, data, headers) => socket.patch(`/projects/${id}`, data, headers);
|
||||
|
||||
const updateProjectBackgroundImage = (id, data, headers) =>
|
||||
http.post(`/projects/${id}/background-image`, data, headers);
|
||||
|
||||
const deleteProject = (id, headers) => socket.delete(`/projects/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
|
@ -49,6 +22,5 @@ export default {
|
|||
createProject,
|
||||
getProject,
|
||||
updateProject,
|
||||
updateProjectBackgroundImage,
|
||||
deleteProject,
|
||||
};
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import http from './http';
|
||||
|
||||
/* Actions */
|
||||
|
||||
const getConfig = (headers) => http.get('/config', undefined, headers);
|
||||
|
||||
export default {
|
||||
getConfig,
|
||||
};
|
|
@ -1,3 +1,8 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import socketIOClient from 'socket.io-client';
|
||||
import sailsIOClient from 'sails.io.js';
|
||||
|
||||
|
@ -5,15 +10,14 @@ import Config from '../constants/Config';
|
|||
|
||||
const io = sailsIOClient(socketIOClient);
|
||||
|
||||
io.sails.url = Config.SERVER_HOST_NAME;
|
||||
io.sails.url = Config.SERVER_BASE_URL;
|
||||
io.sails.autoConnect = false;
|
||||
io.sails.reconnection = true;
|
||||
io.sails.useCORSRouteToGetCookie = false;
|
||||
io.sails.environment = process.env.NODE_ENV;
|
||||
io.sails.environment = import.meta.env.MODE;
|
||||
|
||||
const { socket } = io;
|
||||
|
||||
socket.path = `${Config.SERVER_BASE_PATH}/socket.io`;
|
||||
socket.connect = socket._connect; // eslint-disable-line no-underscore-dangle
|
||||
|
||||
['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].forEach((method) => {
|
||||
|
|
24
client/src/api/task-lists.js
Normal file
24
client/src/api/task-lists.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*!
|
||||
* 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 createTaskList = (cardId, data, headers) =>
|
||||
socket.post(`/cards/${cardId}/task-lists`, data, headers);
|
||||
|
||||
const getTaskList = (id, headers) => socket.get(`/task-lists/${id}`, undefined, headers);
|
||||
|
||||
const updateTaskList = (id, data, headers) => socket.patch(`/task-lists/${id}`, data, headers);
|
||||
|
||||
const deleteTaskList = (id, headers) => socket.delete(`/task-lists/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
createTaskList,
|
||||
getTaskList,
|
||||
updateTaskList,
|
||||
deleteTaskList,
|
||||
};
|
|
@ -1,8 +1,14 @@
|
|||
/*!
|
||||
* 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 createTask = (cardId, data, headers) => socket.post(`/cards/${cardId}/tasks`, data, headers);
|
||||
const createTask = (taskListId, data, headers) =>
|
||||
socket.post(`/task-lists/${taskListId}/tasks`, data, headers);
|
||||
|
||||
const updateTask = (id, data, headers) => socket.patch(`/tasks/${id}`, data, headers);
|
||||
|
||||
|
|
|
@ -1,92 +1,44 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
import http from './http';
|
||||
import socket from './socket';
|
||||
|
||||
/* Transformers */
|
||||
|
||||
export const transformUser = (user) => ({
|
||||
...user,
|
||||
createdAt: new Date(user.createdAt),
|
||||
});
|
||||
|
||||
/* Actions */
|
||||
|
||||
const getUsers = (headers) =>
|
||||
socket.get('/users', undefined, headers).then((body) => ({
|
||||
...body,
|
||||
items: body.items.map(transformUser),
|
||||
}));
|
||||
const getUsers = (headers) => socket.get('/users', undefined, headers);
|
||||
|
||||
const createUser = (data, headers) =>
|
||||
socket.post('/users', data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
}));
|
||||
const createUser = (data, headers) => socket.post('/users', data, headers);
|
||||
|
||||
const getUser = (id, headers) =>
|
||||
/* const getUser = (id, headers) =>
|
||||
socket.get(`/users/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
}));
|
||||
})); */
|
||||
|
||||
const getCurrentUser = (subscribe, headers) =>
|
||||
socket.get(`/users/me${subscribe ? '?subscribe=true' : ''}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
}));
|
||||
socket.get(`/users/me${subscribe ? '?subscribe=true' : ''}`, undefined, headers);
|
||||
|
||||
const updateUser = (id, data, headers) =>
|
||||
socket.patch(`/users/${id}`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
}));
|
||||
const updateUser = (id, data, headers) => socket.patch(`/users/${id}`, data, headers);
|
||||
|
||||
const updateUserEmail = (id, data, headers) =>
|
||||
socket.patch(`/users/${id}/email`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
}));
|
||||
const updateUserEmail = (id, data, headers) => socket.patch(`/users/${id}/email`, data, headers);
|
||||
|
||||
const updateUserPassword = (id, data, headers) =>
|
||||
socket.patch(`/users/${id}/password`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
}));
|
||||
socket.patch(`/users/${id}/password`, data, headers);
|
||||
|
||||
const updateUserUsername = (id, data, headers) =>
|
||||
socket.patch(`/users/${id}/username`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
}));
|
||||
socket.patch(`/users/${id}/username`, data, headers);
|
||||
|
||||
const updateUserAvatar = (id, data, headers) =>
|
||||
http.post(`/users/${id}/avatar`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
}));
|
||||
const updateUserAvatar = (id, data, headers) => http.post(`/users/${id}/avatar`, data, headers);
|
||||
|
||||
const deleteUser = (id, headers) =>
|
||||
socket.delete(`/users/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
}));
|
||||
|
||||
/* Event handlers */
|
||||
|
||||
const makeHandleUserCreate = (next) => (body) => {
|
||||
next({
|
||||
...body,
|
||||
item: transformUser(body.item),
|
||||
});
|
||||
};
|
||||
|
||||
const makeHandleUserUpdate = makeHandleUserCreate;
|
||||
|
||||
const makeHandleUserDelete = makeHandleUserCreate;
|
||||
const deleteUser = (id, headers) => socket.delete(`/users/${id}`, undefined, headers);
|
||||
|
||||
export default {
|
||||
getUsers,
|
||||
createUser,
|
||||
getUser,
|
||||
// getUser,
|
||||
getCurrentUser,
|
||||
updateUser,
|
||||
updateUserEmail,
|
||||
|
@ -94,7 +46,4 @@ export default {
|
|||
updateUserUsername,
|
||||
updateUserAvatar,
|
||||
deleteUser,
|
||||
makeHandleUserCreate,
|
||||
makeHandleUserUpdate,
|
||||
makeHandleUserDelete,
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue