Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5acf84c20b | ||
|
|
6cd9da844f | ||
|
|
d7847fe89b | ||
|
|
d033520927 | ||
|
|
d911030831 | ||
|
|
9d95ed6c41 | ||
|
|
1a5a853fa3 |
@@ -15,13 +15,13 @@ type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.19
|
||||
version: 0.1.23
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "1.16.0"
|
||||
appVersion: "1.16.2"
|
||||
|
||||
dependencies:
|
||||
- alias: postgresql
|
||||
|
||||
@@ -46,7 +46,7 @@ helm install planka . --set secretkey=$SECRETKEY \
|
||||
--set admin_email="demo@demo.demo" \
|
||||
--set admin_password="demo" \
|
||||
--set admin_name="Demo Demo" \
|
||||
--set admin_username="demo"
|
||||
--set admin_username="demo" \
|
||||
--set ingress.enabled=true \
|
||||
--set ingress.hosts[0].host=planka.example.dev \
|
||||
|
||||
@@ -55,7 +55,7 @@ helm install planka . --set secretkey=$SECRETKEY \
|
||||
--set admin_email="demo@demo.demo" \
|
||||
--set admin_password="demo" \
|
||||
--set admin_name="Demo Demo" \
|
||||
--set admin_username="demo"
|
||||
--set admin_username="demo" \
|
||||
--set ingress.enabled=true \
|
||||
--set ingress.hosts[0].host=planka.example.dev \
|
||||
--set ingress.tls[0].secretName=planka-tls \
|
||||
@@ -76,6 +76,7 @@ admin_name: "Demo Demo"
|
||||
admin_username: "demo"
|
||||
# Admin user
|
||||
|
||||
# Ingress
|
||||
ingress:
|
||||
enabled: true
|
||||
hosts:
|
||||
|
||||
@@ -58,11 +58,16 @@ spec:
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
env:
|
||||
{{- if not .Values.postgresql.enabled }}
|
||||
- name: DATABASE_URL
|
||||
value: {{ required "If the included postgresql deployment is disabled you need to define a Database URL in 'dburl'" .Values.dburl }}
|
||||
{{- else }}
|
||||
- name: DATABASE_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: planka-postgresql-svcbind-custom-user
|
||||
key: uri
|
||||
{{- end }}
|
||||
- name: BASE_URL
|
||||
{{- if .Values.baseUrl }}
|
||||
value: {{ .Values.baseUrl }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2beta1
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "planka.fullname" . }}
|
||||
@@ -17,12 +17,16 @@ spec:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
@@ -105,12 +105,14 @@ postgresql:
|
||||
serviceBindings:
|
||||
enabled: true
|
||||
|
||||
## Set this if you disable the built-in postgresql deployment
|
||||
dburl:
|
||||
|
||||
## PVC-based data storage configuration
|
||||
persistence:
|
||||
enabled: false
|
||||
# existingClaim: netbox-data
|
||||
# storageClass: "-"
|
||||
|
||||
accessMode: ReadWriteOnce
|
||||
size: 10Gi
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
REACT_APP_VERSION=1.16.0
|
||||
REACT_APP_VERSION=1.16.2
|
||||
|
||||
@@ -23,10 +23,14 @@ createCard.failure = (localId, error) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const handleCardCreate = (card) => ({
|
||||
const handleCardCreate = (card, cardMemberships, cardLabels, tasks, attachments) => ({
|
||||
type: ActionTypes.CARD_CREATE_HANDLE,
|
||||
payload: {
|
||||
card,
|
||||
cardMemberships,
|
||||
cardLabels,
|
||||
tasks,
|
||||
attachments,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -60,6 +64,34 @@ const handleCardUpdate = (card) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const duplicateCard = (id, card, taskIds) => ({
|
||||
type: ActionTypes.CARD_DUPLICATE,
|
||||
payload: {
|
||||
id,
|
||||
card,
|
||||
taskIds,
|
||||
},
|
||||
});
|
||||
|
||||
duplicateCard.success = (localId, card, cardMemberships, cardLabels, tasks) => ({
|
||||
type: ActionTypes.CARD_DUPLICATE__SUCCESS,
|
||||
payload: {
|
||||
localId,
|
||||
card,
|
||||
cardMemberships,
|
||||
cardLabels,
|
||||
tasks,
|
||||
},
|
||||
});
|
||||
|
||||
duplicateCard.failure = (id, error) => ({
|
||||
type: ActionTypes.CARD_DUPLICATE__FAILURE,
|
||||
payload: {
|
||||
id,
|
||||
error,
|
||||
},
|
||||
});
|
||||
|
||||
const deleteCard = (id) => ({
|
||||
type: ActionTypes.CARD_DELETE,
|
||||
payload: {
|
||||
@@ -94,6 +126,7 @@ export default {
|
||||
handleCardCreate,
|
||||
updateCard,
|
||||
handleCardUpdate,
|
||||
duplicateCard,
|
||||
deleteCard,
|
||||
handleCardDelete,
|
||||
};
|
||||
|
||||
@@ -57,6 +57,12 @@ const updateCard = (id, data, headers) =>
|
||||
item: transformCard(body.item),
|
||||
}));
|
||||
|
||||
const duplicateCard = (id, data, headers) =>
|
||||
socket.post(`/cards/${id}/duplicate`, data, headers).then((body) => ({
|
||||
...body,
|
||||
item: transformCard(body.item),
|
||||
}));
|
||||
|
||||
const deleteCard = (id, headers) =>
|
||||
socket.delete(`/cards/${id}`, undefined, headers).then((body) => ({
|
||||
...body,
|
||||
@@ -81,6 +87,7 @@ export default {
|
||||
getCard,
|
||||
updateCard,
|
||||
deleteCard,
|
||||
duplicateCard,
|
||||
makeHandleCardCreate,
|
||||
makeHandleCardUpdate,
|
||||
makeHandleCardDelete,
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
max-height: 60vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
width: 100%;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
|
||||
@@ -85,7 +85,6 @@
|
||||
height: 56px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&:hover {
|
||||
height: 38px;
|
||||
|
||||
@@ -36,6 +36,7 @@ const ActionsStep = React.memo(
|
||||
onUpdate,
|
||||
onMove,
|
||||
onTransfer,
|
||||
onDuplicate,
|
||||
onDelete,
|
||||
onUserAdd,
|
||||
onUserRemove,
|
||||
@@ -76,6 +77,11 @@ const ActionsStep = React.memo(
|
||||
openStep(StepTypes.MOVE);
|
||||
}, [openStep]);
|
||||
|
||||
const handleDuplicateClick = useCallback(() => {
|
||||
onDuplicate();
|
||||
onClose();
|
||||
}, [onDuplicate, onClose]);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
openStep(StepTypes.DELETE);
|
||||
}, [openStep]);
|
||||
@@ -207,6 +213,11 @@ const ActionsStep = React.memo(
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleDuplicateClick}>
|
||||
{t('action.duplicateCard', {
|
||||
context: 'title',
|
||||
})}
|
||||
</Menu.Item>
|
||||
<Menu.Item className={styles.menuItem} onClick={handleDeleteClick}>
|
||||
{t('action.deleteCard', {
|
||||
context: 'title',
|
||||
@@ -232,6 +243,7 @@ ActionsStep.propTypes = {
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
onMove: PropTypes.func.isRequired,
|
||||
onTransfer: PropTypes.func.isRequired,
|
||||
onDuplicate: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onUserAdd: PropTypes.func.isRequired,
|
||||
onUserRemove: PropTypes.func.isRequired,
|
||||
|
||||
@@ -41,6 +41,7 @@ const Card = React.memo(
|
||||
onUpdate,
|
||||
onMove,
|
||||
onTransfer,
|
||||
onDuplicate,
|
||||
onDelete,
|
||||
onUserAdd,
|
||||
onUserRemove,
|
||||
@@ -185,6 +186,7 @@ const Card = React.memo(
|
||||
onUpdate={onUpdate}
|
||||
onMove={onMove}
|
||||
onTransfer={onTransfer}
|
||||
onDuplicate={onDuplicate}
|
||||
onDelete={onDelete}
|
||||
onUserAdd={onUserAdd}
|
||||
onUserRemove={onUserRemove}
|
||||
@@ -238,6 +240,7 @@ Card.propTypes = {
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
onMove: PropTypes.func.isRequired,
|
||||
onTransfer: PropTypes.func.isRequired,
|
||||
onDuplicate: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onUserAdd: PropTypes.func.isRequired,
|
||||
onUserRemove: PropTypes.func.isRequired,
|
||||
|
||||
@@ -55,6 +55,7 @@ const CardModal = React.memo(
|
||||
onUpdate,
|
||||
onMove,
|
||||
onTransfer,
|
||||
onDuplicate,
|
||||
onDelete,
|
||||
onUserAdd,
|
||||
onUserRemove,
|
||||
@@ -140,6 +141,11 @@ const CardModal = React.memo(
|
||||
});
|
||||
}, [isSubscribed, onUpdate]);
|
||||
|
||||
const handleDuplicateClick = useCallback(() => {
|
||||
onDuplicate();
|
||||
onClose();
|
||||
}, [onDuplicate, onClose]);
|
||||
|
||||
const handleGalleryOpen = useCallback(() => {
|
||||
isGalleryOpened.current = true;
|
||||
}, []);
|
||||
@@ -496,6 +502,10 @@ const CardModal = React.memo(
|
||||
{t('action.move')}
|
||||
</Button>
|
||||
</CardMovePopup>
|
||||
<Button fluid className={styles.actionButton} onClick={handleDuplicateClick}>
|
||||
<Icon name="copy outline" className={styles.actionIcon} />
|
||||
{t('action.duplicate')}
|
||||
</Button>
|
||||
<DeletePopup
|
||||
title="common.deleteCard"
|
||||
content="common.areYouSureYouWantToDeleteThisCard"
|
||||
@@ -555,6 +565,7 @@ CardModal.propTypes = {
|
||||
onUpdate: PropTypes.func.isRequired,
|
||||
onMove: PropTypes.func.isRequired,
|
||||
onTransfer: PropTypes.func.isRequired,
|
||||
onDuplicate: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onUserAdd: PropTypes.func.isRequired,
|
||||
onUserRemove: PropTypes.func.isRequired,
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
max-height: 60vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
max-height: 60vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
max-height: calc(100vh - 268px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
width: 290px;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
max-height: 60vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
|
||||
@@ -194,6 +194,9 @@ export default {
|
||||
CARD_TRANSFER: 'CARD_TRANSFER',
|
||||
CARD_TRANSFER__SUCCESS: 'CARD_TRANSFER__SUCCESS',
|
||||
CARD_TRANSFER__FAILURE: 'CARD_TRANSFER__FAILURE',
|
||||
CARD_DUPLICATE: 'CARD_DUPLICATE',
|
||||
CARD_DUPLICATE__SUCCESS: 'CARD_DUPLICATE__SUCCESS',
|
||||
CARD_DUPLICATE__FAILURE: 'CARD_DUPLICATE__FAILURE',
|
||||
CARD_DELETE: 'CARD_DELETE',
|
||||
CARD_DELETE__SUCCESS: 'CARD_DELETE__SUCCESS',
|
||||
CARD_DELETE__FAILURE: 'CARD_DELETE__FAILURE',
|
||||
|
||||
@@ -132,6 +132,8 @@ export default {
|
||||
CURRENT_CARD_MOVE: `${PREFIX}/CURRENT_CARD_MOVE`,
|
||||
CARD_TRANSFER: `${PREFIX}/CARD_TRANSFER`,
|
||||
CURRENT_CARD_TRANSFER: `${PREFIX}/CURRENT_CARD_TRANSFER`,
|
||||
CARD_DUPLICATE: `${PREFIX}/CARD_DUPLICATE`,
|
||||
CURRENT_CARD_DUPLICATE: `${PREFIX}/CURRENT_CARD_DUPLICATE`,
|
||||
CARD_DELETE: `${PREFIX}/CARD_DELETE`,
|
||||
CURRENT_CARD_DELETE: `${PREFIX}/CURRENT_CARD_DELETE`,
|
||||
CARD_DELETE_HANDLE: `${PREFIX}/CARD_DELETE_HANDLE`,
|
||||
|
||||
@@ -62,6 +62,7 @@ const mapDispatchToProps = (dispatch, { id }) =>
|
||||
onUpdate: (data) => entryActions.updateCard(id, data),
|
||||
onMove: (listId, index) => entryActions.moveCard(id, listId, index),
|
||||
onTransfer: (boardId, listId) => entryActions.transferCard(id, boardId, listId),
|
||||
onDuplicate: () => entryActions.duplicateCard(id),
|
||||
onDelete: () => entryActions.deleteCard(id),
|
||||
onUserAdd: (userId) => entryActions.addUserToCard(userId, id),
|
||||
onUserRemove: (userId) => entryActions.removeUserFromCard(userId, id),
|
||||
|
||||
@@ -78,6 +78,7 @@ const mapDispatchToProps = (dispatch) =>
|
||||
onUpdate: entryActions.updateCurrentCard,
|
||||
onMove: entryActions.moveCurrentCard,
|
||||
onTransfer: entryActions.transferCurrentCard,
|
||||
onDuplicate: entryActions.duplicateCurrentCard,
|
||||
onDelete: entryActions.deleteCurrentCard,
|
||||
onUserAdd: entryActions.addUserToCurrentCard,
|
||||
onUserRemove: entryActions.removeUserFromCurrentCard,
|
||||
|
||||
@@ -74,6 +74,18 @@ const transferCurrentCard = (boardId, listId, index = 0) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const duplicateCard = (id) => ({
|
||||
type: EntryActionTypes.CARD_DUPLICATE,
|
||||
payload: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
const duplicateCurrentCard = () => ({
|
||||
type: EntryActionTypes.CURRENT_CARD_DUPLICATE,
|
||||
payload: {},
|
||||
});
|
||||
|
||||
const deleteCard = (id) => ({
|
||||
type: EntryActionTypes.CARD_DELETE,
|
||||
payload: {
|
||||
@@ -103,6 +115,8 @@ export default {
|
||||
moveCurrentCard,
|
||||
transferCard,
|
||||
transferCurrentCard,
|
||||
duplicateCard,
|
||||
duplicateCurrentCard,
|
||||
deleteCard,
|
||||
deleteCurrentCard,
|
||||
handleCardDelete,
|
||||
|
||||
@@ -51,6 +51,7 @@ export default {
|
||||
cardNotFound_title: 'Card Not Found',
|
||||
cardOrActionAreDeleted: 'Card or action are deleted.',
|
||||
color: 'Color',
|
||||
copy_inline: 'copy',
|
||||
createBoard_title: 'Create Board',
|
||||
createLabel_title: 'Create Label',
|
||||
createNewOneOrSelectExistingOne: 'Create a new one or select<br />an existing one.',
|
||||
@@ -196,6 +197,8 @@ export default {
|
||||
deleteTask: 'Delete task',
|
||||
deleteTask_title: 'Delete Task',
|
||||
deleteUser: 'Delete user',
|
||||
duplicate: 'Duplicate',
|
||||
duplicateCard_title: 'Duplicate Card',
|
||||
edit: 'Edit',
|
||||
editDueDate_title: 'Edit Due Date',
|
||||
editDescription_title: 'Edit Description',
|
||||
|
||||
@@ -169,6 +169,7 @@ export default {
|
||||
deleteTask: 'Supprimer la tâche',
|
||||
deleteTask_title: 'Supprimer la tâche',
|
||||
deleteUser: "Supprimer l'utilisateur",
|
||||
duplicate: 'Dupliquer',
|
||||
edit: 'Modifier',
|
||||
editDueDate_title: "Modifier la date d'échéance",
|
||||
editDescription_title: 'Éditer la description',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import pick from 'lodash/pick';
|
||||
import { attr, fk, many, oneToOne } from 'redux-orm';
|
||||
|
||||
import BaseModel from './BaseModel';
|
||||
@@ -165,7 +166,6 @@ export default class extends BaseModel {
|
||||
|
||||
break;
|
||||
case ActionTypes.CARD_CREATE:
|
||||
case ActionTypes.CARD_CREATE_HANDLE:
|
||||
case ActionTypes.CARD_UPDATE__SUCCESS:
|
||||
case ActionTypes.CARD_UPDATE_HANDLE:
|
||||
Card.upsert(payload.card);
|
||||
@@ -176,10 +176,63 @@ export default class extends BaseModel {
|
||||
Card.upsert(payload.card);
|
||||
|
||||
break;
|
||||
case ActionTypes.CARD_CREATE_HANDLE: {
|
||||
const cardModel = Card.upsert(payload.card);
|
||||
|
||||
payload.cardMemberships.forEach(({ userId }) => {
|
||||
cardModel.users.add(userId);
|
||||
});
|
||||
|
||||
payload.cardLabels.forEach(({ labelId }) => {
|
||||
cardModel.labels.add(labelId);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CARD_UPDATE:
|
||||
Card.withId(payload.id).update(payload.data);
|
||||
|
||||
break;
|
||||
case ActionTypes.CARD_DUPLICATE: {
|
||||
const cardModel = Card.withId(payload.id);
|
||||
|
||||
const nextCardModel = Card.upsert({
|
||||
...pick(cardModel.ref, [
|
||||
'boardId',
|
||||
'listId',
|
||||
'position',
|
||||
'name',
|
||||
'description',
|
||||
'dueDate',
|
||||
'stopwatch',
|
||||
]),
|
||||
...payload.card,
|
||||
});
|
||||
|
||||
cardModel.users.toRefArray().forEach(({ id }) => {
|
||||
nextCardModel.users.add(id);
|
||||
});
|
||||
|
||||
cardModel.labels.toRefArray().forEach(({ id }) => {
|
||||
nextCardModel.labels.add(id);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CARD_DUPLICATE__SUCCESS: {
|
||||
Card.withId(payload.localId).deleteWithRelated();
|
||||
const cardModel = Card.upsert(payload.card);
|
||||
|
||||
payload.cardMemberships.forEach(({ userId }) => {
|
||||
cardModel.users.add(userId);
|
||||
});
|
||||
|
||||
payload.cardLabels.forEach(({ labelId }) => {
|
||||
cardModel.labels.add(labelId);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case ActionTypes.CARD_DELETE:
|
||||
Card.withId(payload.id).deleteWithRelated();
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { attr, fk } from 'redux-orm';
|
||||
|
||||
import { createLocalId } from '../utils/local-id';
|
||||
import BaseModel from './BaseModel';
|
||||
import ActionTypes from '../constants/ActionTypes';
|
||||
|
||||
@@ -44,10 +45,24 @@ export default class extends BaseModel {
|
||||
|
||||
break;
|
||||
case ActionTypes.BOARD_FETCH__SUCCESS:
|
||||
case ActionTypes.CARD_CREATE_HANDLE:
|
||||
case ActionTypes.CARD_DUPLICATE__SUCCESS:
|
||||
payload.tasks.forEach((task) => {
|
||||
Task.upsert(task);
|
||||
});
|
||||
|
||||
break;
|
||||
case ActionTypes.CARD_DUPLICATE:
|
||||
payload.taskIds.forEach((taskId, index) => {
|
||||
const taskModel = Task.withId(taskId);
|
||||
|
||||
Task.upsert({
|
||||
...taskModel.ref,
|
||||
id: `${createLocalId()}-${index}`, // TODO: hack?
|
||||
cardId: payload.card.id,
|
||||
});
|
||||
});
|
||||
|
||||
break;
|
||||
case ActionTypes.TASK_CREATE:
|
||||
case ActionTypes.TASK_CREATE_HANDLE:
|
||||
|
||||
@@ -5,6 +5,7 @@ import request from '../request';
|
||||
import selectors from '../../../selectors';
|
||||
import actions from '../../../actions';
|
||||
import api from '../../../api';
|
||||
import i18n from '../../../i18n';
|
||||
import { createLocalId } from '../../../utils/local-id';
|
||||
|
||||
export function* createCard(listId, data, autoOpen) {
|
||||
@@ -41,8 +42,23 @@ export function* createCard(listId, data, autoOpen) {
|
||||
}
|
||||
}
|
||||
|
||||
export function* handleCardCreate(card) {
|
||||
yield put(actions.handleCardCreate(card));
|
||||
export function* handleCardCreate({ id }) {
|
||||
let card;
|
||||
let cardMemberships;
|
||||
let cardLabels;
|
||||
let tasks;
|
||||
let attachments;
|
||||
|
||||
try {
|
||||
({
|
||||
item: card,
|
||||
included: { cardMemberships, cardLabels, tasks, attachments },
|
||||
} = yield call(request, api.getCard, id));
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield put(actions.handleCardCreate(card, cardMemberships, cardLabels, tasks, attachments));
|
||||
}
|
||||
|
||||
export function* updateCard(id, data) {
|
||||
@@ -106,6 +122,55 @@ export function* transferCurrentCard(boardId, listId, index) {
|
||||
yield call(transferCard, cardId, boardId, listId, index);
|
||||
}
|
||||
|
||||
export function* duplicateCard(id) {
|
||||
const { listId, name } = yield select(selectors.selectCardById, id);
|
||||
const index = yield select(selectors.selectCardIndexById, id);
|
||||
|
||||
const nextData = {
|
||||
position: yield select(selectors.selectNextCardPosition, listId, index + 1),
|
||||
name: `${name} (${i18n.t('common.copy', {
|
||||
context: 'inline',
|
||||
})})`,
|
||||
};
|
||||
|
||||
const localId = yield call(createLocalId);
|
||||
const taskIds = yield select(selectors.selectTaskIdsByCardId, id);
|
||||
|
||||
yield put(
|
||||
actions.duplicateCard(
|
||||
id,
|
||||
{
|
||||
...nextData,
|
||||
id: localId,
|
||||
},
|
||||
taskIds,
|
||||
),
|
||||
);
|
||||
|
||||
let card;
|
||||
let cardMemberships;
|
||||
let cardLabels;
|
||||
let tasks;
|
||||
|
||||
try {
|
||||
({
|
||||
item: card,
|
||||
included: { cardMemberships, cardLabels, tasks },
|
||||
} = yield call(request, api.duplicateCard, id, nextData));
|
||||
} catch (error) {
|
||||
yield put(actions.duplicateCard.failure(localId, error));
|
||||
return;
|
||||
}
|
||||
|
||||
yield put(actions.duplicateCard.success(localId, card, cardMemberships, cardLabels, tasks));
|
||||
}
|
||||
|
||||
export function* duplicateCurrentCard() {
|
||||
const { cardId } = yield select(selectors.selectPath);
|
||||
|
||||
yield call(duplicateCard, cardId);
|
||||
}
|
||||
|
||||
export function* deleteCard(id) {
|
||||
const { cardId, boardId } = yield select(selectors.selectPath);
|
||||
|
||||
@@ -147,11 +212,13 @@ export default {
|
||||
handleCardCreate,
|
||||
updateCard,
|
||||
updateCurrentCard,
|
||||
handleCardUpdate,
|
||||
moveCard,
|
||||
moveCurrentCard,
|
||||
transferCard,
|
||||
transferCurrentCard,
|
||||
handleCardUpdate,
|
||||
duplicateCard,
|
||||
duplicateCurrentCard,
|
||||
deleteCard,
|
||||
deleteCurrentCard,
|
||||
handleCardDelete,
|
||||
|
||||
@@ -32,6 +32,8 @@ export default function* cardsWatchers() {
|
||||
takeEvery(EntryActionTypes.CURRENT_CARD_TRANSFER, ({ payload: { boardId, listId, index } }) =>
|
||||
services.transferCurrentCard(boardId, listId, index),
|
||||
),
|
||||
takeEvery(EntryActionTypes.CARD_DUPLICATE, ({ payload: { id } }) => services.duplicateCard(id)),
|
||||
takeEvery(EntryActionTypes.CURRENT_CARD_DUPLICATE, () => services.duplicateCurrentCard()),
|
||||
takeEvery(EntryActionTypes.CARD_DELETE, ({ payload: { id } }) => services.deleteCard(id)),
|
||||
takeEvery(EntryActionTypes.CURRENT_CARD_DELETE, () => services.deleteCurrentCard()),
|
||||
takeEvery(EntryActionTypes.CARD_DELETE_HANDLE, ({ payload: { card } }) =>
|
||||
|
||||
@@ -33,10 +33,10 @@ export function* authenticateUsingOidc() {
|
||||
const oidcConfig = yield select(selectors.selectOidcConfig);
|
||||
|
||||
const state = nanoid();
|
||||
window.sessionStorage.setItem('oidc-state', state);
|
||||
window.localStorage.setItem('oidc-state', state);
|
||||
|
||||
const nonce = nanoid();
|
||||
window.sessionStorage.setItem('oidc-nonce', nonce);
|
||||
window.localStorage.setItem('oidc-nonce', nonce);
|
||||
|
||||
let redirectUrl = `${oidcConfig.authorizationUrl}`;
|
||||
redirectUrl += `&state=${encodeURIComponent(state)}`;
|
||||
@@ -49,11 +49,11 @@ export function* authenticateUsingOidcCallback() {
|
||||
// https://github.com/plankanban/planka/issues/511#issuecomment-1771385639
|
||||
const params = new URLSearchParams(window.location.hash.substring(1) || window.location.search);
|
||||
|
||||
const state = window.sessionStorage.getItem('oidc-state');
|
||||
window.sessionStorage.removeItem('oidc-state');
|
||||
const state = window.localStorage.getItem('oidc-state');
|
||||
window.localStorage.removeItem('oidc-state');
|
||||
|
||||
const nonce = window.sessionStorage.getItem('oidc-nonce');
|
||||
window.sessionStorage.removeItem('oidc-nonce');
|
||||
const nonce = window.localStorage.getItem('oidc-nonce');
|
||||
window.localStorage.removeItem('oidc-nonce');
|
||||
|
||||
yield put(replace(Paths.LOGIN));
|
||||
|
||||
|
||||
@@ -26,6 +26,24 @@ export const makeSelectCardById = () =>
|
||||
|
||||
export const selectCardById = makeSelectCardById();
|
||||
|
||||
export const makeSelectCardIndexById = () =>
|
||||
createSelector(
|
||||
orm,
|
||||
(_, id) => id,
|
||||
({ Card }, id) => {
|
||||
const cardModel = Card.withId(id);
|
||||
|
||||
if (!cardModel) {
|
||||
return cardModel;
|
||||
}
|
||||
|
||||
const cardModels = cardModel.list.getFilteredOrderedCardsModelArray();
|
||||
return cardModels.findIndex((cardModelItem) => cardModelItem.id === cardModel.id);
|
||||
},
|
||||
);
|
||||
|
||||
export const selectCardIndexById = makeSelectCardIndexById();
|
||||
|
||||
export const makeSelectUsersByCardId = () =>
|
||||
createSelector(
|
||||
orm,
|
||||
@@ -60,6 +78,26 @@ export const makeSelectLabelsByCardId = () =>
|
||||
|
||||
export const selectLabelsByCardId = makeSelectLabelsByCardId();
|
||||
|
||||
export const makeSelectTaskIdsByCardId = () =>
|
||||
createSelector(
|
||||
orm,
|
||||
(_, id) => id,
|
||||
({ Card }, id) => {
|
||||
const cardModel = Card.withId(id);
|
||||
|
||||
if (!cardModel) {
|
||||
return cardModel;
|
||||
}
|
||||
|
||||
return cardModel
|
||||
.getOrderedTasksQuerySet()
|
||||
.toRefArray()
|
||||
.map((task) => task.id);
|
||||
},
|
||||
);
|
||||
|
||||
export const selectTaskIdsByCardId = makeSelectTaskIdsByCardId();
|
||||
|
||||
export const makeSelectTasksByCardId = () =>
|
||||
createSelector(
|
||||
orm,
|
||||
@@ -286,10 +324,14 @@ export const selectNotificationIdsForCurrentCard = createSelector(
|
||||
export default {
|
||||
makeSelectCardById,
|
||||
selectCardById,
|
||||
makeSelectCardIndexById,
|
||||
selectCardIndexById,
|
||||
makeSelectUsersByCardId,
|
||||
selectUsersByCardId,
|
||||
makeSelectLabelsByCardId,
|
||||
selectLabelsByCardId,
|
||||
makeSelectTaskIdsByCardId,
|
||||
selectTaskIdsByCardId,
|
||||
makeSelectTasksByCardId,
|
||||
selectTasksByCardId,
|
||||
makeSelectLastActivityIdByCardId,
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "planka",
|
||||
"version": "1.16.0",
|
||||
"version": "1.16.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "planka",
|
||||
"version": "1.16.0",
|
||||
"version": "1.16.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "planka",
|
||||
"version": "1.16.0",
|
||||
"version": "1.16.2",
|
||||
"private": true,
|
||||
"homepage": "https://plankanban.github.io/planka",
|
||||
"repository": {
|
||||
|
||||
Executable
+82
@@ -0,0 +1,82 @@
|
||||
const Errors = {
|
||||
NOT_ENOUGH_RIGHTS: {
|
||||
notEnoughRights: 'Not enough rights',
|
||||
},
|
||||
CARD_NOT_FOUND: {
|
||||
cardNotFound: 'Card not found',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
regex: /^[0-9]+$/,
|
||||
required: true,
|
||||
},
|
||||
position: {
|
||||
type: 'number',
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
|
||||
exits: {
|
||||
notEnoughRights: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
cardNotFound: {
|
||||
responseType: 'notFound',
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const { currentUser } = this.req;
|
||||
|
||||
const { card, list, board } = await sails.helpers.cards
|
||||
.getProjectPath(inputs.id)
|
||||
.intercept('pathNotFound', () => Errors.CARD_NOT_FOUND);
|
||||
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: card.boardId,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (!boardMembership) {
|
||||
throw Errors.CARD_NOT_FOUND; // Forbidden
|
||||
}
|
||||
|
||||
if (boardMembership.role !== BoardMembership.Roles.EDITOR) {
|
||||
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||
}
|
||||
|
||||
const values = _.pick(inputs, ['position', 'name']);
|
||||
|
||||
const {
|
||||
card: nextCard,
|
||||
cardMemberships,
|
||||
cardLabels,
|
||||
tasks,
|
||||
} = await sails.helpers.cards.duplicateOne.with({
|
||||
board,
|
||||
list,
|
||||
record: card,
|
||||
values: {
|
||||
...values,
|
||||
creatorUser: currentUser,
|
||||
},
|
||||
request: this.req,
|
||||
});
|
||||
|
||||
return {
|
||||
item: nextCard,
|
||||
included: {
|
||||
cardMemberships,
|
||||
cardLabels,
|
||||
tasks,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
const POSITION_GAP = 65535; // TODO: move to config
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
user: {
|
||||
@@ -124,7 +126,7 @@ module.exports = {
|
||||
getUsedTrelloLabels().map(async (trelloLabel, index) => {
|
||||
const plankaLabel = await Label.create({
|
||||
boardId: inputs.board.id,
|
||||
position: 65535 * (index + 1), // TODO: move to config
|
||||
position: POSITION_GAP * (index + 1),
|
||||
name: trelloLabel.name || null,
|
||||
color: getPlankaLabelColor(trelloLabel.color),
|
||||
}).fetch();
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
const valuesValidator = (value) => {
|
||||
if (!_.isPlainObject(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_.isUndefined(value.position) && !_.isFinite(value.position)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_.isPlainObject(value.creatorUser)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
record: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
values: {
|
||||
type: 'ref',
|
||||
custom: valuesValidator,
|
||||
required: true,
|
||||
},
|
||||
board: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
list: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
request: {
|
||||
type: 'ref',
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const { values } = inputs;
|
||||
|
||||
const cards = await sails.helpers.lists.getCards(inputs.record.listId);
|
||||
|
||||
const { position, repositions } = sails.helpers.utils.insertToPositionables(
|
||||
values.position,
|
||||
cards,
|
||||
);
|
||||
|
||||
repositions.forEach(async ({ id, position: nextPosition }) => {
|
||||
await Card.update({
|
||||
id,
|
||||
listId: inputs.record.listId,
|
||||
}).set({
|
||||
position: nextPosition,
|
||||
});
|
||||
|
||||
sails.sockets.broadcast(`board:${inputs.record.boardId}`, 'cardUpdate', {
|
||||
item: {
|
||||
id,
|
||||
position: nextPosition,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const card = await Card.create({
|
||||
..._.pick(inputs.record, [
|
||||
'boardId',
|
||||
'listId',
|
||||
'name',
|
||||
'description',
|
||||
'dueDate',
|
||||
'stopwatch',
|
||||
]),
|
||||
...values,
|
||||
position,
|
||||
creatorUserId: values.creatorUser.id,
|
||||
}).fetch();
|
||||
|
||||
const cardMemberships = await sails.helpers.cards.getCardMemberships(inputs.record.id);
|
||||
const cardMembershipsValues = cardMemberships.map((cardMembership) => ({
|
||||
..._.pick(cardMembership, ['userId']),
|
||||
cardId: card.id,
|
||||
}));
|
||||
const nextCardMemberships = await CardMembership.createEach(cardMembershipsValues).fetch();
|
||||
|
||||
const cardLabels = await sails.helpers.cards.getCardLabels(inputs.record.id);
|
||||
const cardLabelsValues = cardLabels.map((cardLabel) => ({
|
||||
..._.pick(cardLabel, ['labelId']),
|
||||
cardId: card.id,
|
||||
}));
|
||||
const nextCardLabels = await CardLabel.createEach(cardLabelsValues).fetch();
|
||||
|
||||
const tasks = await sails.helpers.cards.getTasks(inputs.record.id);
|
||||
const tasksValues = tasks.map((task) => ({
|
||||
..._.pick(task, ['position', 'name', 'isCompleted']),
|
||||
cardId: card.id,
|
||||
}));
|
||||
const nextTasks = await Task.createEach(tasksValues).fetch();
|
||||
|
||||
sails.sockets.broadcast(
|
||||
`board:${card.boardId}`,
|
||||
'cardCreate',
|
||||
{
|
||||
item: card,
|
||||
},
|
||||
inputs.request,
|
||||
);
|
||||
|
||||
if (values.creatorUser.subscribeToOwnCards) {
|
||||
await CardSubscription.create({
|
||||
cardId: card.id,
|
||||
userId: card.creatorUserId,
|
||||
}).tolerate('E_UNIQUE');
|
||||
|
||||
sails.sockets.broadcast(`user:${card.creatorUserId}`, 'cardUpdate', {
|
||||
item: {
|
||||
id: card.id,
|
||||
isSubscribed: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await sails.helpers.actions.createOne.with({
|
||||
values: {
|
||||
card,
|
||||
type: Action.Types.CREATE_CARD, // TODO: introduce separate type?
|
||||
data: {
|
||||
list: _.pick(inputs.list, ['id', 'name']),
|
||||
},
|
||||
user: values.creatorUser,
|
||||
},
|
||||
board: inputs.board,
|
||||
});
|
||||
|
||||
return {
|
||||
card,
|
||||
cardMemberships: nextCardMemberships,
|
||||
cardLabels: nextCardLabels,
|
||||
tasks: nextTasks,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -55,6 +55,7 @@ module.exports.routes = {
|
||||
'POST /api/lists/:listId/cards': 'cards/create',
|
||||
'GET /api/cards/:id': 'cards/show',
|
||||
'PATCH /api/cards/:id': 'cards/update',
|
||||
'POST /api/cards/:id/duplicate': 'cards/duplicate',
|
||||
'DELETE /api/cards/:id': 'cards/delete',
|
||||
'POST /api/cards/:cardId/memberships': 'card-memberships/create',
|
||||
'DELETE /api/cards/:cardId/memberships': 'card-memberships/delete',
|
||||
|
||||
Reference in New Issue
Block a user