@@ -112,18 +117,18 @@ export default function ProductionBoardCard(technician, card, bodyshop, cardSett
)}
-
{`${card.v_model_yr || ""} ${
- card.v_make_desc || ""
- } ${card.v_model_desc || ""}`}
+
{`${card.metadata.v_model_yr || ""} ${
+ card.metadata.v_make_desc || ""
+ } ${card.metadata.v_model_desc || ""}`}
- {cardSettings && cardSettings.ins_co_nm && card.ins_co_nm && (
+ {cardSettings && cardSettings.ins_co_nm && card.metadata.ins_co_nm && (
-
{card.ins_co_nm || ""}
+
{card.metadata.ins_co_nm || ""}
)}
- {cardSettings && cardSettings.clm_no && card.clm_no && (
+ {cardSettings && cardSettings.clm_no && card.metadata.clm_no && (
-
{card.clm_no || ""}
+
{card.metadata.clm_no || ""}
)}
@@ -132,7 +137,7 @@ export default function ProductionBoardCard(technician, card, bodyshop, cardSett
{`B: ${
employee_body ? `${employee_body.first_name.substr(0, 3)} ${employee_body.last_name.charAt(0)}` : ""
- } ${card.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`}
+ } ${card.metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`}
{`P: ${
employee_prep ? `${employee_prep.first_name.substr(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""
}`}
@@ -140,7 +145,7 @@ export default function ProductionBoardCard(technician, card, bodyshop, cardSett
employee_refinish
? `${employee_refinish.first_name.substr(0, 3)} ${employee_refinish.last_name.charAt(0)}`
: ""
- } ${card.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`}
+ } ${card.metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`}
{`C: ${
employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""
}`}
@@ -151,38 +156,38 @@ export default function ProductionBoardCard(technician, card, bodyshop, cardSett
{`B: ${
- card.labhrs.aggregate.sum.mod_lb_hrs || "?"
+ card.metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"
} hrs`}
{`R: ${
- card.larhrs.aggregate.sum.mod_lb_hrs || "?"
+ card.metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"
} hrs`}
)} */}
- {cardSettings && cardSettings.actual_in && card.actual_in && (
+ {cardSettings && cardSettings.actual_in && card.metadata.actual_in && (
- {card.actual_in}
+ {card.metadata.actual_in}
)}
- {cardSettings && cardSettings.scheduled_completion && card.scheduled_completion && (
+ {cardSettings && cardSettings.scheduled_completion && card.metadata.scheduled_completion && (
- {card.scheduled_completion}
+ {card.metadata.scheduled_completion}
)}
- {cardSettings && cardSettings.ats && card.alt_transport && (
+ {cardSettings && cardSettings.ats && card.metadata.alt_transport && (
- {card.alt_transport || ""}
+ {card.metadata.alt_transport || ""}
)}
{cardSettings && cardSettings.sublets && (
-
+
)}
{cardSettings && cardSettings.production_note && (
@@ -192,7 +197,7 @@ export default function ProductionBoardCard(technician, card, bodyshop, cardSett
)}
{cardSettings && cardSettings.partsstatus && (
-
+
)}
diff --git a/client/src/components/production-board-kanban/production-board-kanban.component.jsx b/client/src/components/production-board-kanban/production-board-kanban.component.jsx
index f191bd339..3d3bc97a5 100644
--- a/client/src/components/production-board-kanban/production-board-kanban.component.jsx
+++ b/client/src/components/production-board-kanban/production-board-kanban.component.jsx
@@ -1,6 +1,6 @@
import { SyncOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
-import Board, { moveCard } from "@asseinfo/react-kanban";
+import Board from "../../components/trello-board/index";
import { Button, Grid, notification, Space, Statistic } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import React, { useEffect, useState } from "react";
@@ -42,9 +42,7 @@ export function ProductionBoardKanbanComponent({
insertAuditTrail,
associationSettings
}) {
- const [boardLanes, setBoardLanes] = useState({
- columns: [{ id: "Loading...", title: "Loading...", cards: [] }]
- });
+ const [boardLanes, setBoardLanes] = useState({ lanes: [] });
const [filter, setFilter] = useState({ search: "", employeeId: null });
@@ -58,7 +56,7 @@ export function ProductionBoardKanbanComponent({
filter
);
- boardData.columns = boardData.columns.map((d) => {
+ boardData.lanes = boardData.lanes.map((d) => {
return { ...d, title: `${d.title} (${d.cards.length})` };
});
setBoardLanes(boardData);
@@ -67,63 +65,55 @@ export function ProductionBoardKanbanComponent({
const client = useApolloClient();
- const handleDragEnd = async (card, source, destination) => {
+ const handleDragEnd = async (cardId, sourceLaneId, targetLaneId, position, cardDetails) => {
logImEXEvent("kanban_drag_end");
setIsMoving(true);
- setBoardLanes(moveCard(boardLanes, source, destination));
- const sameColumnTransfer = source.fromColumnId === destination.toColumnId;
- const sourceColumn = boardLanes.columns.find((x) => x.id === source.fromColumnId);
- const destinationColumn = boardLanes.columns.find((x) => x.id === destination.toColumnId);
- const movedCardWillBeFirst = destination.toPosition === 0;
+ const sameColumnTransfer = sourceLaneId === targetLaneId;
+ const sourceLane = boardLanes.lanes.find((lane) => lane.id === sourceLaneId);
+ const targetLane = boardLanes.lanes.find((lane) => lane.id === targetLaneId);
- const movedCardWillBeLast = destinationColumn.cards.length - destination.toPosition < 1;
+ const movedCardWillBeFirst = position === 0;
+ const movedCardWillBeLast = targetLane.cards.length - position < 1;
- const lastCardInDestinationColumn = destinationColumn.cards[destinationColumn.cards.length - 1];
+ const lastCardInTargetLane = targetLane.cards[targetLane.cards.length - 1];
- const oldChildCard = sourceColumn.cards[source.fromPosition + 1];
+ const oldChildCard = sourceLane.cards[position + 1];
const newChildCard = movedCardWillBeLast
? null
- : destinationColumn.cards[
- sameColumnTransfer
- ? source.fromPosition - destination.toPosition > 0
- ? destination.toPosition
- : destination.toPosition + 1
- : destination.toPosition
- ];
+ : targetLane.cards[sameColumnTransfer ? (position - position > 0 ? position : position + 1) : position];
- const oldChildCardNewParent = oldChildCard ? card.kanbanparent : null;
+ const oldChildCardNewParent = oldChildCard ? cardDetails.kanbanparent : null;
let movedCardNewKanbanParent;
if (movedCardWillBeFirst) {
- //console.log("==> New Card is first.");
movedCardNewKanbanParent = "-1";
} else if (movedCardWillBeLast) {
- // console.log("==> New Card is last.");
- movedCardNewKanbanParent = lastCardInDestinationColumn.id;
+ movedCardNewKanbanParent = lastCardInTargetLane.id;
} else if (!!newChildCard) {
- // console.log("==> New Card is somewhere in the middle");
movedCardNewKanbanParent = newChildCard.kanbanparent;
} else {
console.log("==> !!!!!!Couldn't find a parent.!!!! <==");
}
- const newChildCardNewParent = newChildCard ? card.id : null;
+ const newChildCardNewParent = newChildCard ? cardId : null;
+
const update = await client.mutate({
mutation: generate_UPDATE_JOB_KANBAN(
oldChildCard ? oldChildCard.id : null,
oldChildCardNewParent,
- card.id,
+ cardId,
movedCardNewKanbanParent,
- destination.toColumnId,
+ targetLaneId,
newChildCard ? newChildCard.id : null,
newChildCardNewParent
)
});
+
insertAuditTrail({
- jobid: card.id,
- operation: AuditTrailMapping.jobstatuschange(destination.toColumnId),
+ jobid: cardId,
+ operation: AuditTrailMapping.jobstatuschange(targetLaneId),
type: "jobstatuschange"
});
@@ -134,6 +124,8 @@ export function ProductionBoardKanbanComponent({
})
});
}
+
+ setIsMoving(false);
};
const totalHrs = data
@@ -214,7 +206,6 @@ export function ProductionBoardKanbanComponent({
return (
-
@@ -234,18 +225,20 @@ export function ProductionBoardKanbanComponent({
}
/>
-
{cardSettings.cardcolor && }
-
ProductionBoardCard(technician, card, bodyshop, cardSettings)}
- onCardDragEnd={handleDragEnd}
+ data={boardLanes}
+ draggable
+ canAddLanes
+ handleDragEnd={handleDragEnd}
+ editable
+ style={{ height: "100%", backgroundColor: "transparent" }}
+ renameLane
+ components={{
+ Card: (cardProps) => ProductionBoardCard({ card: cardProps, technician, bodyshop, cardSettings })
+ }}
/>
diff --git a/client/src/components/production-board-kanban/production-board-kanban.utils.js b/client/src/components/production-board-kanban/production-board-kanban.utils.js
index 74b500143..ab286794f 100644
--- a/client/src/components/production-board-kanban/production-board-kanban.utils.js
+++ b/client/src/components/production-board-kanban/production-board-kanban.utils.js
@@ -18,8 +18,8 @@ const sortByParentId = (arr) => {
//console.log("sortByParentId -> byParentsIdsList", byParentsIdsList);
while (byParentsIdsList[parentId]) {
- sortedList.push(...byParentsIdsList[parentId]); //Spread in the whole list in case several items have the same parents.
- parentId = byParentsIdsList[parentId][byParentsIdsList[parentId].length -1].id; //Grab the ID from the last one.
+ sortedList.push(...byParentsIdsList[parentId]); //Spread in the whole list in case several items have the same parents.
+ parentId = byParentsIdsList[parentId][byParentsIdsList[parentId].length - 1].id; //Grab the ID from the last one.
}
if (byParentsIdsList["null"]) byParentsIdsList["null"].map((i) => sortedList.push(i));
@@ -40,15 +40,13 @@ const sortByParentId = (arr) => {
export const createBoardData = (AllStatuses, Jobs, filter) => {
const { search, employeeId } = filter;
- const boardLanes = {
- columns: AllStatuses.map((s) => {
- return {
- id: s,
- title: s,
- cards: []
- };
- })
- };
+ const lanes = AllStatuses.map((s) => {
+ return {
+ id: s,
+ title: s,
+ cards: []
+ };
+ });
const filteredJobs =
(search === "" || !search) && !employeeId
@@ -75,16 +73,25 @@ export const createBoardData = (AllStatuses, Jobs, filter) => {
Object.keys(DataGroupedByStatus).map((statusGroupKey) => {
try {
- const needle = boardLanes.columns.find((l) => l.id === statusGroupKey);
- if (!needle?.cards) return null;
- needle.cards = sortByParentId(DataGroupedByStatus[statusGroupKey]);
+ const lane = lanes.find((l) => l.id === statusGroupKey);
+ if (!lane?.cards) return null;
+ lane.cards = sortByParentId(DataGroupedByStatus[statusGroupKey]).map((job) => {
+ const { id, title, description, due_date, ...metadata } = job;
+ return {
+ id,
+ title,
+ description,
+ label: job.due_date || "",
+ metadata
+ };
+ });
} catch (error) {
console.log("Error while creating board card", error);
}
return null;
});
- return boardLanes;
+ return { lanes };
};
const CheckSearch = (search, job) => {
diff --git a/client/src/components/production-list-columns/production-list-columns.productionnote.component.jsx b/client/src/components/production-list-columns/production-list-columns.productionnote.component.jsx
index b5f5a1556..0119b8cc1 100644
--- a/client/src/components/production-list-columns/production-list-columns.productionnote.component.jsx
+++ b/client/src/components/production-list-columns/production-list-columns.productionnote.component.jsx
@@ -18,7 +18,8 @@ const mapDispatchToProps = (dispatch) => ({
function ProductionListColumnProductionNote({ record, setNoteUpsertContext }) {
const { t } = useTranslation();
-
+ console.log("RECORD");
+ console.dir(record);
const [note, setNote] = useState((record.production_vars && record.production_vars.note) || "");
const [open, setOpen] = useState(false);
diff --git a/client/src/components/production-sublets-manage/production-sublets-manage.component.jsx b/client/src/components/production-sublets-manage/production-sublets-manage.component.jsx
index a7a9ddad9..55aca7a8b 100644
--- a/client/src/components/production-sublets-manage/production-sublets-manage.component.jsx
+++ b/client/src/components/production-sublets-manage/production-sublets-manage.component.jsx
@@ -9,6 +9,8 @@ export default function ProductionSubletsManageComponent({ subletJobLines }) {
const { t } = useTranslation();
const [updateJobLine] = useMutation(UPDATE_JOB_LINE_SUBLET);
const [loading, setLoading] = useState(false);
+ console.log("subletJobLines");
+ console.dir(subletJobLines);
const subletCount = useMemo(() => {
return {
total: subletJobLines.filter((s) => !s.sublet_ignored).length,
diff --git a/client/src/components/trello-board/components/AddCardLink.jsx b/client/src/components/trello-board/components/AddCardLink.jsx
new file mode 100644
index 000000000..df259a9d9
--- /dev/null
+++ b/client/src/components/trello-board/components/AddCardLink.jsx
@@ -0,0 +1,11 @@
+import React from "react";
+import { AddCardLink } from "../styles/Base";
+import { useTranslation } from "react-i18next";
+
+const AddCardLinkComponent = ({ onClick, laneId }) => {
+ const { t } = useTranslation();
+
+ return
{t("trello.labels.add_card")};
+};
+
+export default AddCardLinkComponent;
diff --git a/client/src/components/trello-board/components/Card.jsx b/client/src/components/trello-board/components/Card.jsx
new file mode 100644
index 000000000..d4ef6265d
--- /dev/null
+++ b/client/src/components/trello-board/components/Card.jsx
@@ -0,0 +1,112 @@
+import React, { useCallback } from "react";
+import PropTypes from "prop-types";
+
+import { CardHeader, CardRightContent, CardTitle, Detail, Footer, MovableCardWrapper } from "../styles/Base";
+import InlineInput from "../widgets/InlineInput.jsx";
+import Tag from "./Card/Tag.jsx";
+import DeleteButton from "../widgets/DeleteButton.jsx";
+import { useTranslation } from "react-i18next";
+
+const Card = ({
+ showDeleteButton = true,
+ onDelete = () => {},
+ onClick = () => {},
+ style = {},
+ tagStyle = {},
+ className = "",
+ id,
+ title = "no title",
+ label = "",
+ description = "",
+ tags = [],
+ cardDraggable,
+ editable,
+ onChange
+}) => {
+ const { t } = useTranslation();
+
+ const handleDelete = useCallback(
+ (e) => {
+ onDelete();
+ e.stopPropagation();
+ },
+ [onDelete]
+ );
+
+ const updateCard = (card) => {
+ onChange({ ...card, id });
+ };
+
+ return (
+
+
+
+ {editable ? (
+ updateCard({ title: value })}
+ />
+ ) : (
+ title
+ )}
+
+
+ {editable ? (
+ updateCard({ label: value })}
+ />
+ ) : (
+ label
+ )}
+
+ {showDeleteButton && }
+
+
+ {editable ? (
+ updateCard({ description: value })}
+ />
+ ) : (
+ description
+ )}
+
+ {tags && tags.length > 0 && (
+
+ )}
+
+ );
+};
+
+Card.propTypes = {
+ showDeleteButton: PropTypes.bool,
+ onDelete: PropTypes.func,
+ onClick: PropTypes.func,
+ style: PropTypes.object,
+ tagStyle: PropTypes.object,
+ className: PropTypes.string,
+ id: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired,
+ label: PropTypes.string,
+ description: PropTypes.string,
+ tags: PropTypes.array,
+ cardDraggable: PropTypes.bool,
+ editable: PropTypes.bool,
+ onChange: PropTypes.func.isRequired
+};
+
+export default Card;
diff --git a/client/src/components/trello-board/components/Card/Tag.jsx b/client/src/components/trello-board/components/Card/Tag.jsx
new file mode 100644
index 000000000..78bd78063
--- /dev/null
+++ b/client/src/components/trello-board/components/Card/Tag.jsx
@@ -0,0 +1,21 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { TagSpan } from "../../styles/Base";
+
+const Tag = ({ title, color, bgcolor, tagStyle, ...otherProps }) => {
+ const style = { color: color || "white", backgroundColor: bgcolor || "orange", ...tagStyle };
+ return (
+
+ {title}
+
+ );
+};
+
+Tag.propTypes = {
+ title: PropTypes.string.isRequired,
+ color: PropTypes.string,
+ bgcolor: PropTypes.string,
+ tagStyle: PropTypes.object
+};
+
+export default Tag;
diff --git a/client/src/components/trello-board/components/Lane/LaneFooter.jsx b/client/src/components/trello-board/components/Lane/LaneFooter.jsx
new file mode 100644
index 000000000..ccf6dcdcb
--- /dev/null
+++ b/client/src/components/trello-board/components/Lane/LaneFooter.jsx
@@ -0,0 +1,9 @@
+import React from "react";
+import { LaneFooter } from "../../styles/Base";
+import { CollapseBtn, ExpandBtn } from "../../styles/Elements";
+
+const LaneFooterComponent = ({ onClick, collapsed }) => (
+
{collapsed ? : }
+);
+
+export default LaneFooterComponent;
diff --git a/client/src/components/trello-board/components/Lane/LaneHeader.jsx b/client/src/components/trello-board/components/Lane/LaneHeader.jsx
new file mode 100644
index 000000000..d61afcbf1
--- /dev/null
+++ b/client/src/components/trello-board/components/Lane/LaneHeader.jsx
@@ -0,0 +1,64 @@
+import React from "react";
+import PropTypes from "prop-types";
+import InlineInput from "../../widgets/InlineInput.jsx";
+import { LaneHeader, RightContent, Title } from "../../styles/Base";
+import LaneMenu from "./LaneHeader/LaneMenu.jsx";
+import { useTranslation } from "react-i18next";
+
+const LaneHeaderComponent = ({
+ updateTitle,
+ canAddLanes,
+ onDelete,
+ onDoubleClick,
+ editLaneTitle,
+ label,
+ title,
+ titleStyle,
+ labelStyle,
+ laneDraggable
+}) => {
+ const { t } = useTranslation();
+
+ return (
+
+
+ {editLaneTitle ? (
+
+ ) : (
+ title
+ )}
+
+ {label && (
+
+ {label}
+
+ )}
+ {canAddLanes && }
+
+ );
+};
+
+LaneHeaderComponent.propTypes = {
+ updateTitle: PropTypes.func,
+ editLaneTitle: PropTypes.bool,
+ canAddLanes: PropTypes.bool,
+ laneDraggable: PropTypes.bool,
+ label: PropTypes.string,
+ title: PropTypes.string,
+ onDelete: PropTypes.func,
+ onDoubleClick: PropTypes.func
+};
+
+LaneHeaderComponent.defaultProps = {
+ updateTitle: () => {},
+ editLaneTitle: false,
+ canAddLanes: false
+};
+
+export default LaneHeaderComponent;
diff --git a/client/src/components/trello-board/components/Lane/LaneHeader/LaneMenu.jsx b/client/src/components/trello-board/components/Lane/LaneHeader/LaneMenu.jsx
new file mode 100644
index 000000000..0dc6610bd
--- /dev/null
+++ b/client/src/components/trello-board/components/Lane/LaneHeader/LaneMenu.jsx
@@ -0,0 +1,41 @@
+import React from "react";
+
+import { Popover } from "react-popopo";
+
+import { CustomPopoverContainer, CustomPopoverContent } from "../../../styles/Base";
+
+import {
+ DeleteWrapper,
+ GenDelButton,
+ LaneMenuContent,
+ LaneMenuHeader,
+ LaneMenuItem,
+ LaneMenuTitle,
+ MenuButton
+} from "../../../styles/Elements";
+import { useTranslation } from "react-i18next";
+
+const LaneMenu = ({ onDelete }) => {
+ const { t } = useTranslation();
+
+ return (
+
⋮}
+ >
+
+ {t("trello.labels.lane_actions")}
+
+ ✖
+
+
+
+ {t("trello.labels.delete_lane")}
+
+
+ );
+};
+
+export default LaneMenu;
diff --git a/client/src/components/trello-board/components/Loader.jsx b/client/src/components/trello-board/components/Loader.jsx
new file mode 100644
index 000000000..52b55ea81
--- /dev/null
+++ b/client/src/components/trello-board/components/Loader.jsx
@@ -0,0 +1,13 @@
+import React from 'react'
+import {LoaderDiv, LoadingBar} from '../styles/Loader'
+
+const Loader = () => (
+
+
+
+
+
+
+)
+
+export default Loader
diff --git a/client/src/components/trello-board/components/NewCardForm.jsx b/client/src/components/trello-board/components/NewCardForm.jsx
new file mode 100644
index 000000000..585cc202b
--- /dev/null
+++ b/client/src/components/trello-board/components/NewCardForm.jsx
@@ -0,0 +1,53 @@
+import React, { useState } from "react";
+import PropTypes from "prop-types";
+import { CardForm, CardHeader, CardRightContent, CardTitle, CardWrapper, Detail } from "../styles/Base";
+import { AddButton, CancelButton } from "../styles/Elements";
+import EditableLabel from "../widgets/EditableLabel.jsx";
+import { useTranslation } from "react-i18next";
+
+const NewCardForm = ({ onCancel, onAdd }) => {
+ const [state, setState] = useState({});
+ const { t } = useTranslation();
+
+ const updateField = (field, value) => {
+ setState((prevState) => ({ ...prevState, [field]: value }));
+ };
+
+ const handleAdd = () => {
+ onAdd(state);
+ };
+
+ return (
+
+
+
+
+ updateField("title", val)}
+ autoFocus
+ />
+
+
+ updateField("label", val)} />
+
+
+
+ updateField("description", val)}
+ />
+
+
+ {t("trello.labels.add_card")}
+ {t("trello.labels.cancel")}
+
+ );
+};
+
+NewCardForm.propTypes = {
+ onCancel: PropTypes.func.isRequired,
+ onAdd: PropTypes.func.isRequired
+};
+
+export default NewCardForm;
diff --git a/client/src/components/trello-board/components/NewLaneForm.jsx b/client/src/components/trello-board/components/NewLaneForm.jsx
new file mode 100644
index 000000000..b24a956cc
--- /dev/null
+++ b/client/src/components/trello-board/components/NewLaneForm.jsx
@@ -0,0 +1,56 @@
+import React, { useRef } from "react";
+import PropTypes from "prop-types";
+import { LaneTitle, NewLaneButtons, Section } from "../styles/Base";
+import { AddButton, CancelButton } from "../styles/Elements";
+import NewLaneTitleEditor from "../widgets/NewLaneTitleEditor.jsx";
+import { v1 } from "uuid";
+import { useTranslation } from "react-i18next";
+
+const NewLane = ({ onCancel, onAdd }) => {
+ const refInput = useRef(null);
+ const { t } = useTranslation();
+
+ const handleSubmit = () => {
+ onAdd({
+ id: v1(),
+ title: getValue()
+ });
+ };
+
+ const getValue = () => refInput.current.getValue();
+
+ const onClickOutside = (a, b, c) => {
+ if (getValue().length > 0) {
+ handleSubmit();
+ } else {
+ onCancel();
+ }
+ };
+
+ return (
+
+
+
+
+
+ {t("trello.labels.add_lane")}
+ {t("trello.labels.cancel")}
+
+
+ );
+};
+
+NewLane.propTypes = {
+ onCancel: PropTypes.func.isRequired,
+ onAdd: PropTypes.func.isRequired
+};
+
+export default NewLane;
diff --git a/client/src/components/trello-board/components/NewLaneSection.jsx b/client/src/components/trello-board/components/NewLaneSection.jsx
new file mode 100644
index 000000000..edfbe5434
--- /dev/null
+++ b/client/src/components/trello-board/components/NewLaneSection.jsx
@@ -0,0 +1,16 @@
+import React from "react";
+import { NewLaneSection } from "../styles/Base";
+import { AddLaneLink } from "../styles/Elements";
+import { useTranslation } from "react-i18next";
+
+const NewLaneSectionComponent = ({ onClick }) => {
+ const { t } = useTranslation();
+
+ return (
+
+ {t("trello.labels.add_lane")}
+
+ );
+};
+
+export default NewLaneSectionComponent;
diff --git a/client/src/components/trello-board/components/index.js b/client/src/components/trello-board/components/index.js
new file mode 100644
index 000000000..24858380d
--- /dev/null
+++ b/client/src/components/trello-board/components/index.js
@@ -0,0 +1,24 @@
+import LaneHeader from "./Lane/LaneHeader";
+import LaneFooter from "./Lane/LaneFooter";
+import Card from "./Card";
+import Loader from "./Loader.jsx";
+import NewLaneForm from "./NewLaneForm.jsx";
+import NewCardForm from "./NewCardForm.jsx";
+import AddCardLink from "./AddCardLink";
+import NewLaneSection from "./NewLaneSection.jsx";
+import { BoardWrapper, GlobalStyle, ScrollableLane, Section } from "../styles/Base";
+
+export default {
+ GlobalStyle,
+ BoardWrapper,
+ Loader,
+ ScrollableLane,
+ LaneHeader,
+ LaneFooter,
+ Section,
+ NewLaneForm,
+ NewLaneSection,
+ NewCardForm,
+ Card,
+ AddCardLink
+};
diff --git a/client/src/components/trello-board/controllers/Board.jsx b/client/src/components/trello-board/controllers/Board.jsx
new file mode 100644
index 000000000..879845521
--- /dev/null
+++ b/client/src/components/trello-board/controllers/Board.jsx
@@ -0,0 +1,19 @@
+import { BoardContainer } from "../index.jsx";
+import classNames from "classnames";
+import { useState } from "react";
+import { v1 } from "uuid";
+
+const Board = ({ id, className, components, ...additionalProps }) => {
+ const [storeId] = useState(id || v1());
+
+ const allClassNames = classNames("react-trello-board", className || "");
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export default Board;
diff --git a/client/src/components/trello-board/controllers/BoardContainer.jsx b/client/src/components/trello-board/controllers/BoardContainer.jsx
new file mode 100644
index 000000000..eae0cd51c
--- /dev/null
+++ b/client/src/components/trello-board/controllers/BoardContainer.jsx
@@ -0,0 +1,294 @@
+import React, { Component } from "react";
+import { bindActionCreators } from "redux";
+import { connect } from "react-redux";
+import Container from "../dnd/Container";
+import Draggable from "../dnd/Draggable";
+import PropTypes from "prop-types";
+import pick from "lodash/pick";
+import isEqual from "lodash/isEqual";
+import Lane from "./Lane";
+import { PopoverWrapper } from "react-popopo";
+
+import * as actions from "../../../redux/trello/trello.actions.js";
+
+class BoardContainer extends Component {
+ state = {
+ addLaneMode: false
+ };
+
+ get groupName() {
+ const { id } = this.props;
+ return `TrelloBoard${id}`;
+ }
+
+ componentDidMount() {
+ const { actions, eventBusHandle } = this.props;
+ actions.loadBoard(this.props.data);
+ if (eventBusHandle) {
+ this.wireEventBus();
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ const { data, reducerData, onDataChange, actions } = this.props;
+
+ if (this.props.reducerData && !isEqual(reducerData, prevProps.reducerData)) {
+ onDataChange(this.props.reducerData);
+ }
+
+ if (data && !isEqual(data, prevProps.data)) {
+ actions.loadBoard(data);
+ onDataChange(data);
+ }
+ }
+
+ onDragStart = ({ payload }) => {
+ const { handleLaneDragStart } = this.props;
+ handleLaneDragStart(payload.id);
+ };
+
+ onLaneDrop = ({ removedIndex, addedIndex, payload }) => {
+ const { actions, handleLaneDragEnd } = this.props;
+ if (removedIndex !== addedIndex) {
+ actions.moveLane({ oldIndex: removedIndex, newIndex: addedIndex });
+ handleLaneDragEnd(removedIndex, addedIndex, payload);
+ }
+ };
+
+ getCardDetails = (laneId, cardIndex) => {
+ return this.props.reducerData.lanes.find((lane) => lane.id === laneId).cards[cardIndex];
+ };
+
+ getLaneDetails = (index) => {
+ return this.props.reducerData.lanes[index];
+ };
+
+ wireEventBus = () => {
+ const { actions, eventBusHandle } = this.props;
+ let eventBus = {
+ publish: (event) => {
+ switch (event.type) {
+ case "ADD_CARD":
+ return actions.addCard({ laneId: event.laneId, card: event.card });
+ case "UPDATE_CARD":
+ return actions.updateCard({ laneId: event.laneId, card: event.card });
+ case "REMOVE_CARD":
+ return actions.removeCard({ laneId: event.laneId, cardId: event.cardId });
+ case "REFRESH_BOARD":
+ return actions.loadBoard(event.data);
+ case "MOVE_CARD":
+ return actions.moveCardAcrossLanes({
+ fromLaneId: event.fromLaneId,
+ toLaneId: event.toLaneId,
+ cardId: event.cardId,
+ index: event.index
+ });
+ case "UPDATE_CARDS":
+ return actions.updateCards({ laneId: event.laneId, cards: event.cards });
+ case "UPDATE_CARD":
+ return actions.updateCard({ laneId: event.laneId, updatedCard: event.card });
+ case "UPDATE_LANES":
+ return actions.updateLanes(event.lanes);
+ case "UPDATE_LANE":
+ return actions.updateLane(event.lane);
+ default:
+ return;
+ }
+ }
+ };
+ eventBusHandle(eventBus);
+ };
+
+ // + add
+ hideEditableLane = () => {
+ this.setState({ addLaneMode: false });
+ };
+
+ showEditableLane = () => {
+ this.setState({ addLaneMode: true });
+ };
+
+ addNewLane = (params) => {
+ this.hideEditableLane();
+ this.props.actions.addLane(params);
+ this.props.onLaneAdd(params);
+ };
+
+ render() {
+ const {
+ id,
+ components,
+ reducerData,
+ draggable,
+ laneDraggable,
+ laneDragClass,
+ laneDropClass,
+ style,
+ onDataChange,
+ onCardAdd,
+ onCardUpdate,
+ onCardClick,
+ onBeforeCardDelete,
+ onCardDelete,
+ onLaneScroll,
+ onLaneClick,
+ onLaneAdd,
+ onLaneDelete,
+ onLaneUpdate,
+ editable,
+ canAddLanes,
+ laneStyle,
+ onCardMoveAcrossLanes,
+ t,
+ ...otherProps
+ } = this.props;
+
+ const { addLaneMode } = this.state;
+ // Stick to whitelisting attributes to segregate board and lane props
+ const passthroughProps = pick(this.props, [
+ "onCardMoveAcrossLanes",
+ "onLaneScroll",
+ "onLaneDelete",
+ "onLaneUpdate",
+ "onCardClick",
+ "onBeforeCardDelete",
+ "onCardDelete",
+ "onCardAdd",
+ "onCardUpdate",
+ "onLaneClick",
+ "laneSortFunction",
+ "draggable",
+ "laneDraggable",
+ "cardDraggable",
+ "collapsibleLanes",
+ "canAddLanes",
+ "hideCardDeleteIcon",
+ "tagStyle",
+ "handleDragStart",
+ "handleDragEnd",
+ "cardDragClass",
+ "editLaneTitle",
+ "t"
+ ]);
+
+ return (
+
+
+ this.getLaneDetails(index)}
+ groupName={this.groupName}
+ >
+ {reducerData.lanes.map((lane, index) => {
+ const { id, droppable, ...otherProps } = lane;
+ const laneToRender = (
+
+ );
+ return draggable && laneDraggable ? {laneToRender} : laneToRender;
+ })}
+
+
+ {canAddLanes && (
+
+ {editable && !addLaneMode ? (
+
+ ) : (
+ addLaneMode &&
+ )}
+
+ )}
+
+ );
+ }
+}
+
+BoardContainer.propTypes = {
+ id: PropTypes.string,
+ components: PropTypes.object,
+ actions: PropTypes.object,
+ data: PropTypes.object.isRequired,
+ reducerData: PropTypes.object,
+ onDataChange: PropTypes.func,
+ eventBusHandle: PropTypes.func,
+ onLaneScroll: PropTypes.func,
+ onCardClick: PropTypes.func,
+ onBeforeCardDelete: PropTypes.func,
+ onCardDelete: PropTypes.func,
+ onCardAdd: PropTypes.func,
+ onCardUpdate: PropTypes.func,
+ onLaneAdd: PropTypes.func,
+ onLaneDelete: PropTypes.func,
+ onLaneClick: PropTypes.func,
+ onLaneUpdate: PropTypes.func,
+ laneSortFunction: PropTypes.func,
+ draggable: PropTypes.bool,
+ collapsibleLanes: PropTypes.bool,
+ editable: PropTypes.bool,
+ canAddLanes: PropTypes.bool,
+ hideCardDeleteIcon: PropTypes.bool,
+ handleDragStart: PropTypes.func,
+ handleDragEnd: PropTypes.func,
+ handleLaneDragStart: PropTypes.func,
+ handleLaneDragEnd: PropTypes.func,
+ style: PropTypes.object,
+ tagStyle: PropTypes.object,
+ laneDraggable: PropTypes.bool,
+ cardDraggable: PropTypes.bool,
+ cardDragClass: PropTypes.string,
+ laneDragClass: PropTypes.string,
+ laneDropClass: PropTypes.string,
+ onCardMoveAcrossLanes: PropTypes.func.isRequired
+};
+
+BoardContainer.defaultProps = {
+ t: (v) => v,
+ onDataChange: () => {},
+ handleDragStart: () => {},
+ handleDragEnd: () => {},
+ handleLaneDragStart: () => {},
+ handleLaneDragEnd: () => {},
+ onCardUpdate: () => {},
+ onLaneAdd: () => {},
+ onLaneDelete: () => {},
+ onCardMoveAcrossLanes: () => {},
+ onLaneUpdate: () => {},
+ editable: false,
+ canAddLanes: false,
+ hideCardDeleteIcon: false,
+ draggable: false,
+ collapsibleLanes: false,
+ laneDraggable: true,
+ cardDraggable: true,
+ cardDragClass: "react_trello_dragClass",
+ laneDragClass: "react_trello_dragLaneClass",
+ laneDropClass: ""
+};
+
+const mapStateToProps = (state) => {
+ return state.trello.lanes ? { reducerData: state.trello } : {};
+};
+
+const mapDispatchToProps = (dispatch) => ({
+ actions: bindActionCreators({ ...actions }, dispatch)
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(BoardContainer);
diff --git a/client/src/components/trello-board/controllers/Lane.jsx b/client/src/components/trello-board/controllers/Lane.jsx
new file mode 100644
index 000000000..30802dcf7
--- /dev/null
+++ b/client/src/components/trello-board/controllers/Lane.jsx
@@ -0,0 +1,328 @@
+import React, { Component } from "react";
+import classNames from "classnames";
+import PropTypes from "prop-types";
+import { bindActionCreators } from "redux";
+import { connect } from "react-redux";
+import isEqual from "lodash/isEqual";
+import cloneDeep from "lodash/cloneDeep";
+import { v1 } from "uuid";
+
+import Container from "../dnd/Container.jsx";
+import Draggable from "../dnd/Draggable.jsx";
+
+import * as actions from "../../../redux/trello/trello.actions.js";
+
+class Lane extends Component {
+ state = {
+ loading: false,
+ currentPage: this.props.currentPage,
+ addCardMode: false,
+ collapsed: false,
+ isDraggingOver: false
+ };
+
+ get groupName() {
+ const { boardId } = this.props;
+ return `TrelloBoard${boardId}Lane`;
+ }
+
+ handleScroll = (evt) => {
+ const node = evt.target;
+ const elemScrollPosition = node.scrollHeight - node.scrollTop - node.clientHeight;
+ const { onLaneScroll } = this.props;
+ // In some browsers and/or screen sizes a decimal rest value between 0 and 1 exists, so it should be checked on < 1 instead of < 0
+ if (elemScrollPosition < 1 && onLaneScroll && !this.state.loading) {
+ const { currentPage } = this.state;
+ this.setState({ loading: true });
+ const nextPage = currentPage + 1;
+ onLaneScroll(nextPage, this.props.id).then((moreCards) => {
+ if ((moreCards || []).length > 0) {
+ this.props.actions.paginateLane({
+ laneId: this.props.id,
+ newCards: moreCards,
+ nextPage: nextPage
+ });
+ }
+ this.setState({ loading: false });
+ });
+ }
+ };
+
+ sortCards(cards, sortFunction) {
+ if (!cards) return [];
+ if (!sortFunction) return cards;
+ return cards.concat().sort(function (card1, card2) {
+ return sortFunction(card1, card2);
+ });
+ }
+
+ laneDidMount = (node) => {
+ if (node) {
+ node.addEventListener("scroll", this.handleScroll);
+ }
+ };
+
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ if (!isEqual(this.props.cards, nextProps.cards)) {
+ this.setState({
+ currentPage: nextProps.currentPage
+ });
+ }
+ }
+
+ removeCard = (cardId) => {
+ if (this.props.onBeforeCardDelete && typeof this.props.onBeforeCardDelete === "function") {
+ this.props.onBeforeCardDelete(() => {
+ this.props.onCardDelete && this.props.onCardDelete(cardId, this.props.id);
+ this.props.actions.removeCard({ laneId: this.props.id, cardId: cardId });
+ });
+ } else {
+ this.props.onCardDelete && this.props.onCardDelete(cardId, this.props.id);
+ this.props.actions.removeCard({ laneId: this.props.id, cardId: cardId });
+ }
+ };
+
+ handleCardClick = (e, card) => {
+ const { onCardClick } = this.props;
+ onCardClick && onCardClick(card.id, card.metadata, card.laneId);
+ e.stopPropagation();
+ };
+
+ showEditableCard = () => {
+ this.setState({ addCardMode: true });
+ };
+
+ hideEditableCard = () => {
+ this.setState({ addCardMode: false });
+ };
+
+ addNewCard = (params) => {
+ const laneId = this.props.id;
+ const id = v1();
+ this.hideEditableCard();
+ let card = { id, ...params };
+ this.props.actions.addCard({ laneId, card });
+ this.props.onCardAdd(card, laneId);
+ };
+
+ onDragStart = ({ payload }) => {
+ const { handleDragStart } = this.props;
+ handleDragStart && handleDragStart(payload.id, payload.laneId);
+ };
+
+ shouldAcceptDrop = (sourceContainerOptions) => {
+ return this.props.droppable && sourceContainerOptions.groupName === this.groupName;
+ };
+
+ onDragEnd = (laneId, result) => {
+ const { handleDragEnd } = this.props;
+ const { addedIndex, payload } = result;
+
+ if (this.state.isDraggingOver) {
+ this.setState({ isDraggingOver: false });
+ }
+
+ if (addedIndex != null) {
+ const newCard = { ...cloneDeep(payload), laneId };
+ const response = handleDragEnd ? handleDragEnd(payload.id, payload.laneId, laneId, addedIndex, newCard) : true;
+ if (response === undefined || !!response) {
+ this.props.actions.moveCardAcrossLanes({
+ fromLaneId: payload.laneId,
+ toLaneId: laneId,
+ cardId: payload.id,
+ index: addedIndex
+ });
+ this.props.onCardMoveAcrossLanes(payload.laneId, laneId, payload.id, addedIndex);
+ }
+ return response;
+ }
+ };
+
+ updateCard = (updatedCard) => {
+ this.props.actions.updateCard({ laneId: this.props.id, card: updatedCard });
+ this.props.onCardUpdate(this.props.id, updatedCard);
+ };
+
+ renderDragContainer = (isDraggingOver) => {
+ const {
+ id,
+ cards,
+ laneSortFunction,
+ editable,
+ hideCardDeleteIcon,
+ cardDraggable,
+ cardDragClass,
+ cardDropClass,
+ tagStyle,
+ cardStyle,
+ components,
+ t
+ } = this.props;
+ const { addCardMode, collapsed } = this.state;
+
+ const showableCards = collapsed ? [] : cards;
+
+ const cardList = this.sortCards(showableCards, laneSortFunction).map((card, idx) => {
+ const onDeleteCard = () => this.removeCard(card.id);
+ const cardToRender = (
+
this.handleCardClick(e, card)}
+ onChange={(updatedCard) => this.updateCard(updatedCard)}
+ showDeleteButton={!hideCardDeleteIcon}
+ tagStyle={tagStyle}
+ cardDraggable={cardDraggable}
+ editable={editable}
+ {...card}
+ />
+ );
+ return cardDraggable && (!card.hasOwnProperty("draggable") || card.draggable) ? (
+ {cardToRender}
+ ) : (
+ {cardToRender}
+ );
+ });
+
+ return (
+
+ this.onDragEnd(id, e)}
+ onDragEnter={() => this.setState({ isDraggingOver: true })}
+ onDragLeave={() => this.setState({ isDraggingOver: false })}
+ shouldAcceptDrop={this.shouldAcceptDrop}
+ getChildPayload={(index) => this.props.getCardDetails(id, index)}
+ >
+ {cardList}
+
+ {editable && !addCardMode && }
+ {addCardMode && }
+
+ );
+ };
+
+ removeLane = () => {
+ const { id } = this.props;
+ this.props.actions.removeLane({ laneId: id });
+ this.props.onLaneDelete(id);
+ };
+
+ updateTitle = (value) => {
+ this.props.actions.updateLane({ id: this.props.id, title: value });
+ this.props.onLaneUpdate(this.props.id, { title: value });
+ };
+
+ renderHeader = (pickedProps) => {
+ const { components } = this.props;
+ return (
+
+ );
+ };
+
+ toggleLaneCollapsed = () => {
+ this.props.collapsibleLanes && this.setState((state) => ({ collapsed: !state.collapsed }));
+ };
+
+ render() {
+ const { loading, isDraggingOver, collapsed } = this.state;
+ const {
+ id,
+ cards,
+ collapsibleLanes,
+ components,
+ onLaneClick,
+ onLaneScroll,
+ onCardClick,
+ onCardAdd,
+ onBeforeCardDelete,
+ onCardDelete,
+ onLaneDelete,
+ onLaneUpdate,
+ onCardUpdate,
+ onCardMoveAcrossLanes,
+ ...otherProps
+ } = this.props;
+ const allClassNames = classNames("react-trello-lane", this.props.className || "");
+ const showFooter = collapsibleLanes && cards.length > 0;
+ return (
+ onLaneClick && onLaneClick(id)}
+ draggable={false}
+ className={allClassNames}
+ >
+ {this.renderHeader({ id, cards, ...otherProps })}
+ {this.renderDragContainer(isDraggingOver)}
+ {loading && }
+ {showFooter && }
+
+ );
+ }
+}
+
+Lane.propTypes = {
+ actions: PropTypes.object,
+ id: PropTypes.string.isRequired,
+ boardId: PropTypes.string,
+ title: PropTypes.node,
+ index: PropTypes.number,
+ laneSortFunction: PropTypes.func,
+ style: PropTypes.object,
+ cardStyle: PropTypes.object,
+ tagStyle: PropTypes.object,
+ titleStyle: PropTypes.object,
+ labelStyle: PropTypes.object,
+ cards: PropTypes.array,
+ label: PropTypes.string,
+ currentPage: PropTypes.number,
+ draggable: PropTypes.bool,
+ collapsibleLanes: PropTypes.bool,
+ droppable: PropTypes.bool,
+ onCardMoveAcrossLanes: PropTypes.func,
+ onCardClick: PropTypes.func,
+ onBeforeCardDelete: PropTypes.func,
+ onCardDelete: PropTypes.func,
+ onCardAdd: PropTypes.func,
+ onCardUpdate: PropTypes.func,
+ onLaneDelete: PropTypes.func,
+ onLaneUpdate: PropTypes.func,
+ onLaneClick: PropTypes.func,
+ onLaneScroll: PropTypes.func,
+ editable: PropTypes.bool,
+ laneDraggable: PropTypes.bool,
+ cardDraggable: PropTypes.bool,
+ cardDragClass: PropTypes.string,
+ cardDropClass: PropTypes.string,
+ canAddLanes: PropTypes.bool
+};
+
+Lane.defaultProps = {
+ style: {},
+ titleStyle: {},
+ labelStyle: {},
+ label: undefined,
+ editable: false,
+ onLaneUpdate: () => {},
+ onCardAdd: () => {},
+ onCardUpdate: () => {}
+};
+
+const mapDispatchToProps = (dispatch) => ({
+ actions: bindActionCreators(actions, dispatch)
+});
+
+export default connect(null, mapDispatchToProps)(Lane);
diff --git a/client/src/components/trello-board/dnd/Container.jsx b/client/src/components/trello-board/dnd/Container.jsx
new file mode 100644
index 000000000..6b6883a6d
--- /dev/null
+++ b/client/src/components/trello-board/dnd/Container.jsx
@@ -0,0 +1,139 @@
+import React, {Component} from 'react'
+import ReactDOM from 'react-dom'
+import PropTypes from 'prop-types'
+import container, {dropHandlers} from 'kuika-smooth-dnd'
+
+container.dropHandler = dropHandlers.reactDropHandler().handler
+container.wrapChild = p => p // dont wrap children they will already be wrapped
+
+class Container extends Component {
+ constructor(props) {
+ super(props)
+ this.getContainerOptions = this.getContainerOptions.bind(this)
+ this.setRef = this.setRef.bind(this)
+ this.prevContainer = null
+ }
+
+ componentDidMount() {
+ this.containerDiv = this.containerDiv || ReactDOM.findDOMNode(this)
+ this.prevContainer = this.containerDiv
+ this.container = container(this.containerDiv, this.getContainerOptions())
+ }
+
+ componentWillUnmount() {
+ this.container.dispose()
+ this.container = null
+ }
+
+ componentDidUpdate() {
+ this.containerDiv = this.containerDiv || ReactDOM.findDOMNode(this)
+ if (this.containerDiv) {
+ if (this.prevContainer && this.prevContainer !== this.containerDiv) {
+ this.container.dispose()
+ this.container = container(this.containerDiv, this.getContainerOptions())
+ this.prevContainer = this.containerDiv
+ }
+ }
+ }
+
+ render() {
+ if (this.props.render) {
+ return this.props.render(this.setRef)
+ } else {
+ return (
+
+ {this.props.children}
+
+ )
+ }
+ }
+
+ setRef(element) {
+ this.containerDiv = element
+ }
+
+ getContainerOptions() {
+ const functionProps = {}
+
+ if (this.props.onDragStart) {
+ functionProps.onDragStart = (...p) => this.props.onDragStart(...p)
+ }
+
+ if (this.props.onDragEnd) {
+ functionProps.onDragEnd = (...p) => this.props.onDragEnd(...p)
+ }
+
+ if (this.props.onDrop) {
+ functionProps.onDrop = (...p) => this.props.onDrop(...p)
+ }
+
+ if (this.props.getChildPayload) {
+ functionProps.getChildPayload = (...p) => this.props.getChildPayload(...p)
+ }
+
+ if (this.props.shouldAnimateDrop) {
+ functionProps.shouldAnimateDrop = (...p) => this.props.shouldAnimateDrop(...p)
+ }
+
+ if (this.props.shouldAcceptDrop) {
+ functionProps.shouldAcceptDrop = (...p) => this.props.shouldAcceptDrop(...p)
+ }
+
+ if (this.props.onDragEnter) {
+ functionProps.onDragEnter = (...p) => this.props.onDragEnter(...p)
+ }
+
+ if (this.props.onDragLeave) {
+ functionProps.onDragLeave = (...p) => this.props.onDragLeave(...p)
+ }
+
+ if (this.props.render) {
+ functionProps.render = (...p) => this.props.render(...p)
+ }
+
+ if (this.props.onDropReady) {
+ functionProps.onDropReady = (...p) => this.props.onDropReady(...p)
+ }
+
+ if (this.props.getGhostParent) {
+ functionProps.getGhostParent = (...p) => this.props.getGhostParent(...p)
+ }
+
+ return Object.assign({}, this.props, functionProps)
+ }
+}
+
+Container.propTypes = {
+ behaviour: PropTypes.oneOf(['move', 'copy', 'drag-zone']),
+ groupName: PropTypes.string,
+ orientation: PropTypes.oneOf(['horizontal', 'vertical']),
+ style: PropTypes.object,
+ dragHandleSelector: PropTypes.string,
+ className: PropTypes.string,
+ nonDragAreaSelector: PropTypes.string,
+ dragBeginDelay: PropTypes.number,
+ animationDuration: PropTypes.number,
+ autoScrollEnabled: PropTypes.string,
+ lockAxis: PropTypes.string,
+ dragClass: PropTypes.string,
+ dropClass: PropTypes.string,
+ onDragStart: PropTypes.func,
+ onDragEnd: PropTypes.func,
+ onDrop: PropTypes.func,
+ getChildPayload: PropTypes.func,
+ shouldAnimateDrop: PropTypes.func,
+ shouldAcceptDrop: PropTypes.func,
+ onDragEnter: PropTypes.func,
+ onDragLeave: PropTypes.func,
+ render: PropTypes.func,
+ getGhostParent: PropTypes.func,
+ removeOnDropOut: PropTypes.bool
+}
+
+Container.defaultProps = {
+ behaviour: 'move',
+ orientation: 'vertical',
+ className: 'reactTrelloBoard'
+}
+
+export default Container
diff --git a/client/src/components/trello-board/dnd/Draggable.jsx b/client/src/components/trello-board/dnd/Draggable.jsx
new file mode 100644
index 000000000..080422121
--- /dev/null
+++ b/client/src/components/trello-board/dnd/Draggable.jsx
@@ -0,0 +1,26 @@
+import React, {Component} from 'react'
+import PropTypes from 'prop-types'
+import {constants} from 'kuika-smooth-dnd'
+
+const {wrapperClass} = constants
+
+class Draggable extends Component {
+ render() {
+ if (this.props.render) {
+ return React.cloneElement(this.props.render(), {className: wrapperClass})
+ }
+
+ const clsName = `${this.props.className ? this.props.className + ' ' : ''}`
+ return (
+
+ {this.props.children}
+
+ )
+ }
+}
+
+Draggable.propTypes = {
+ render: PropTypes.func
+}
+
+export default Draggable
diff --git a/client/src/components/trello-board/helpers/LaneHelper.js b/client/src/components/trello-board/helpers/LaneHelper.js
new file mode 100644
index 000000000..b13a65483
--- /dev/null
+++ b/client/src/components/trello-board/helpers/LaneHelper.js
@@ -0,0 +1,135 @@
+import update from "immutability-helper";
+
+const LaneHelper = {
+ initialiseLanes: (state, { lanes }) => {
+ const newLanes = lanes.map((lane) => {
+ lane.currentPage = 1;
+ lane.cards && lane.cards.forEach((c) => (c.laneId = lane.id));
+ return lane;
+ });
+ return update(state, { lanes: { $set: newLanes } });
+ },
+
+ paginateLane: (state, { laneId, newCards, nextPage }) => {
+ const updatedLanes = LaneHelper.appendCardsToLane(state, { laneId: laneId, newCards: newCards });
+ updatedLanes.find((lane) => lane.id === laneId).currentPage = nextPage;
+ return update(state, { lanes: { $set: updatedLanes } });
+ },
+
+ appendCardsToLane: (state, { laneId, newCards, index }) => {
+ const lane = state.lanes.find((lane) => lane.id === laneId);
+ newCards = newCards
+ .map((c) => update(c, { laneId: { $set: laneId } }))
+ .filter((c) => lane.cards.find((card) => card.id === c.id) == null);
+ return state.lanes.map((lane) => {
+ if (lane.id === laneId) {
+ if (index !== undefined) {
+ return update(lane, { cards: { $splice: [[index, 0, ...newCards]] } });
+ } else {
+ const cardsToUpdate = [...lane.cards, ...newCards];
+ return update(lane, { cards: { $set: cardsToUpdate } });
+ }
+ } else {
+ return lane;
+ }
+ });
+ },
+
+ appendCardToLane: (state, { laneId, card, index }) => {
+ const newLanes = LaneHelper.appendCardsToLane(state, { laneId: laneId, newCards: [card], index });
+ return update(state, { lanes: { $set: newLanes } });
+ },
+
+ addLane: (state, lane) => {
+ const newLane = { cards: [], ...lane };
+ return update(state, { lanes: { $push: [newLane] } });
+ },
+
+ updateLane: (state, updatedLane) => {
+ const newLanes = state.lanes.map((lane) => {
+ if (updatedLane.id === lane.id) {
+ return { ...lane, ...updatedLane };
+ } else {
+ return lane;
+ }
+ });
+ return update(state, { lanes: { $set: newLanes } });
+ },
+
+ removeCardFromLane: (state, { laneId, cardId }) => {
+ const lanes = state.lanes.map((lane) => {
+ if (lane.id === laneId) {
+ let newCards = lane.cards.filter((card) => card.id !== cardId);
+ return update(lane, { cards: { $set: newCards } });
+ } else {
+ return lane;
+ }
+ });
+ return update(state, { lanes: { $set: lanes } });
+ },
+
+ moveCardAcrossLanes: (state, { fromLaneId, toLaneId, cardId, index }) => {
+ let cardToMove = null;
+ const interimLanes = state.lanes.map((lane) => {
+ if (lane.id === fromLaneId) {
+ cardToMove = lane.cards.find((card) => card.id === cardId);
+ const newCards = lane.cards.filter((card) => card.id !== cardId);
+ return update(lane, { cards: { $set: newCards } });
+ } else {
+ return lane;
+ }
+ });
+ const updatedState = update(state, { lanes: { $set: interimLanes } });
+ return LaneHelper.appendCardToLane(updatedState, {
+ laneId: toLaneId,
+ card: cardToMove,
+ index: index
+ });
+ },
+
+ updateCardsForLane: (state, { laneId, cards }) => {
+ const lanes = state.lanes.map((lane) => {
+ if (lane.id === laneId) {
+ return update(lane, { cards: { $set: cards } });
+ } else {
+ return lane;
+ }
+ });
+ return update(state, { lanes: { $set: lanes } });
+ },
+
+ updateCardForLane: (state, { laneId, card: updatedCard }) => {
+ const lanes = state.lanes.map((lane) => {
+ if (lane.id === laneId) {
+ const cards = lane.cards.map((card) => {
+ if (card.id === updatedCard.id) {
+ return { ...card, ...updatedCard };
+ } else {
+ return card;
+ }
+ });
+ return update(lane, { cards: { $set: cards } });
+ } else {
+ return lane;
+ }
+ });
+ return update(state, { lanes: { $set: lanes } });
+ },
+
+ updateLanes: (state, lanes) => {
+ return { ...state, ...{ lanes: lanes } };
+ },
+
+ moveLane: (state, { oldIndex, newIndex }) => {
+ const laneToMove = state.lanes[oldIndex];
+ const tempState = update(state, { lanes: { $splice: [[oldIndex, 1]] } });
+ return update(tempState, { lanes: { $splice: [[newIndex, 0, laneToMove]] } });
+ },
+
+ removeLane: (state, { laneId }) => {
+ const updatedLanes = state.lanes.filter((lane) => lane.id !== laneId);
+ return update(state, { lanes: { $set: updatedLanes } });
+ }
+};
+
+export default LaneHelper;
diff --git a/client/src/components/trello-board/helpers/deprecationWarnings.js b/client/src/components/trello-board/helpers/deprecationWarnings.js
new file mode 100644
index 000000000..41d203805
--- /dev/null
+++ b/client/src/components/trello-board/helpers/deprecationWarnings.js
@@ -0,0 +1,24 @@
+const REPLACE_TABLE = {
+ customLaneHeader: 'components.LaneHeader',
+ newLaneTemplate: 'components.NewLaneSection',
+ newCardTemplate: 'components.NewCardForm',
+ children: 'components.Card',
+ customCardLayout: 'components.Card',
+ addLaneTitle: '`t` function with key "Add another lane"',
+ addCardLink: '`t` function with key "Click to add card"'
+}
+
+const warn = prop => {
+ const use = REPLACE_TABLE[prop]
+ console.warn(
+ `react-trello property '${prop}' is removed. Use '${use}' instead. More - https://github.com/rcdexta/react-trello/blob/master/UPGRADE.md`
+ )
+}
+
+export default props => {
+ Object.keys(REPLACE_TABLE).forEach(key => {
+ if (props.hasOwnProperty(key)) {
+ warn(key)
+ }
+ })
+}
diff --git a/client/src/components/trello-board/index.jsx b/client/src/components/trello-board/index.jsx
new file mode 100644
index 000000000..55826bdd2
--- /dev/null
+++ b/client/src/components/trello-board/index.jsx
@@ -0,0 +1,38 @@
+import React from "react";
+
+import Draggable from "./dnd/Draggable.jsx";
+import Container from "./dnd/Container.jsx";
+import BoardContainer from "./controllers/BoardContainer.jsx";
+import Board from "./controllers/Board.jsx";
+import Lane from "./controllers/Lane.jsx";
+import deprecationWarnings from "./helpers/deprecationWarnings";
+import DefaultComponents from "./components";
+
+import widgets from "./widgets/index";
+
+import { StyleSheetManager } from "styled-components";
+import isPropValid from "@emotion/is-prop-valid";
+
+export { Draggable, Container, BoardContainer, Lane, widgets };
+
+export { DefaultComponents as components };
+
+// Enhanced default export using arrow function for simplicity
+const TrelloBoard = ({ components, ...otherProps }) => {
+ deprecationWarnings(otherProps);
+
+ return (
+
+ ;
+
+ );
+};
+
+const shouldForwardProp = (propName, target) => {
+ if (typeof target === "string") {
+ return isPropValid(propName);
+ }
+ return true;
+};
+
+export default TrelloBoard;
diff --git a/client/src/components/trello-board/styles/Base.js b/client/src/components/trello-board/styles/Base.js
new file mode 100644
index 000000000..be2c630e4
--- /dev/null
+++ b/client/src/components/trello-board/styles/Base.js
@@ -0,0 +1,298 @@
+import { PopoverContainer, PopoverContent } from "react-popopo";
+import styled, { createGlobalStyle, css } from "styled-components";
+
+export const GlobalStyle = createGlobalStyle`
+ .comPlainTextContentEditable {
+ -webkit-user-modify: read-write-plaintext-only;
+ cursor: text;
+ }
+
+ .comPlainTextContentEditable--has-placeholder::before {
+ content: attr(placeholder);
+ opacity: 0.5;
+ color: inherit;
+ cursor: text;
+ }
+
+ .react_trello_dragClass {
+ transform: rotate(3deg);
+ }
+
+ .react_trello_dragLaneClass {
+ transform: rotate(3deg);
+ }
+
+ .icon-overflow-menu-horizontal:before {
+ content: "\\E91F";
+ }
+
+ .icon-lg, .icon-sm {
+ color: #798d99;
+ }
+
+ .icon-lg {
+ height: 32px;
+ font-size: 16px;
+ line-height: 32px;
+ width: 32px;
+ }
+`;
+
+export const CustomPopoverContainer = styled(PopoverContainer)`
+ position: absolute;
+ right: 10px;
+ flex-flow: column nowrap;
+`;
+
+export const CustomPopoverContent = styled(PopoverContent)`
+ visibility: hidden;
+ margin-top: -5px;
+ opacity: 0;
+ position: absolute;
+ z-index: 10;
+ box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3);
+ transition: all 0.3s ease 0ms;
+ border-radius: 3px;
+ min-width: 7em;
+ flex-flow: column nowrap;
+ background-color: #fff;
+ color: #000;
+ padding: 5px;
+ left: 50%;
+ transform: translateX(-50%);
+
+ ${(props) =>
+ props.active &&
+ `
+ visibility: visible;
+ opacity: 1;
+ transition-delay: 100ms;
+ `} &::before {
+ visibility: hidden;
+ }
+
+ a {
+ color: rgba(255, 255, 255, 0.56);
+ padding: 0.5em 1em;
+ margin: 0;
+ text-decoration: none;
+
+ &:hover {
+ background-color: #00bcd4 !important;
+ color: #37474f;
+ }
+ }
+`;
+
+export const BoardWrapper = styled.div`
+ background-color: #3179ba;
+ overflow-y: hidden;
+ padding: 5px;
+ color: #393939;
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ height: 100vh;
+`;
+
+export const Header = styled.header`
+ margin-bottom: 10px;
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+`;
+
+export const Section = styled.section`
+ background-color: #e3e3e3;
+ border-radius: 3px;
+ margin: 5px 5px;
+ position: relative;
+ padding: 10px;
+ display: inline-flex;
+ height: auto;
+ max-height: 90%;
+ flex-direction: column;
+`;
+
+export const LaneHeader = styled(Header)`
+ margin-bottom: 0px;
+ ${(props) =>
+ props.editLaneTitle &&
+ css`
+ padding: 0px;
+ line-height: 30px;
+ `} ${(props) =>
+ !props.editLaneTitle &&
+ css`
+ padding: 0px 5px;
+ `};
+`;
+
+export const LaneFooter = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ position: relative;
+ height: 10px;
+`;
+
+export const ScrollableLane = styled.div`
+ flex: 1;
+ overflow-y: auto;
+ min-width: 250px;
+ overflow-x: hidden;
+ align-self: center;
+ max-height: 90vh;
+ min-height: 100px;
+ margin-top: 10px;
+ flex-direction: column;
+ justify-content: space-between;
+`;
+
+export const Title = styled.span`
+ font-weight: bold;
+ font-size: 15px;
+ line-height: 18px;
+ cursor: ${(props) => (props.draggable ? "grab" : `auto`)};
+ width: 70%;
+`;
+
+export const RightContent = styled.span`
+ width: 38%;
+ text-align: right;
+ padding-right: 10px;
+ font-size: 13px;
+`;
+export const CardWrapper = styled.article`
+ border-radius: 3px;
+ border-bottom: 1px solid #ccc;
+ background-color: #fff;
+ position: relative;
+ padding: 10px;
+ cursor: pointer;
+ max-width: 250px;
+ margin-bottom: 7px;
+ min-width: 230px;
+`;
+
+export const MovableCardWrapper = styled(CardWrapper)`
+ &:hover {
+ background-color: #f0f0f0;
+ color: #000;
+ }
+`;
+
+export const CardHeader = styled(Header)`
+ border-bottom: 1px solid #eee;
+ padding-bottom: 6px;
+ color: #000;
+`;
+
+export const CardTitle = styled(Title)`
+ font-size: 14px;
+`;
+
+export const CardRightContent = styled(RightContent)`
+ font-size: 10px;
+`;
+
+export const Detail = styled.div`
+ font-size: 12px;
+ color: #4d4d4d;
+ white-space: pre-wrap;
+`;
+
+export const Footer = styled.div`
+ border-top: 1px solid #eee;
+ padding-top: 6px;
+ text-align: right;
+ display: flex;
+ justify-content: flex-end;
+ flex-direction: row;
+ flex-wrap: wrap;
+`;
+
+export const TagSpan = styled.span`
+ padding: 2px 3px;
+ border-radius: 3px;
+ margin: 2px 5px;
+ font-size: 70%;
+`;
+
+export const AddCardLink = styled.a`
+ border-radius: 0 0 3px 3px;
+ color: #838c91;
+ display: block;
+ padding: 5px 2px;
+ margin-top: 10px;
+ position: relative;
+ text-decoration: none;
+ cursor: pointer;
+
+ &:hover {
+ //background-color: #cdd2d4;
+ color: #4d4d4d;
+ text-decoration: underline;
+ }
+`;
+
+export const LaneTitle = styled.div`
+ font-size: 15px;
+ width: 268px;
+ height: auto;
+`;
+
+export const LaneSection = styled.section`
+ background-color: #2b6aa3;
+ border-radius: 3px;
+ margin: 5px;
+ position: relative;
+ padding: 5px;
+ display: inline-flex;
+ height: auto;
+ flex-direction: column;
+`;
+
+export const NewLaneSection = styled(LaneSection)`
+ width: 200px;
+`;
+
+export const NewLaneButtons = styled.div`
+ margin-top: 10px;
+`;
+
+export const CardForm = styled.div`
+ background-color: #e3e3e3;
+`;
+
+export const InlineInput = styled.textarea`
+ overflow-x: hidden; /* for Firefox (issue #5) */
+ word-wrap: break-word;
+ min-height: 18px;
+ max-height: 112px; /* optional, but recommended */
+ resize: none;
+ width: 100%;
+ height: 18px;
+ font-size: inherit;
+ font-weight: inherit;
+ line-height: inherit;
+ text-align: inherit;
+ background-color: transparent;
+ box-shadow: none;
+ box-sizing: border-box;
+ border-radius: 3px;
+ border: 0;
+ padding: 0 8px;
+ outline: 0;
+
+ ${(props) =>
+ props.border &&
+ css`
+ &:focus {
+ box-shadow: inset 0 0 0 2px #0079bf;
+ }
+ `} &:focus {
+ background-color: white;
+ }
+`;
diff --git a/client/src/components/trello-board/styles/Elements.js b/client/src/components/trello-board/styles/Elements.js
new file mode 100644
index 000000000..06e361d5c
--- /dev/null
+++ b/client/src/components/trello-board/styles/Elements.js
@@ -0,0 +1,251 @@
+import styled from 'styled-components'
+import {CardWrapper, MovableCardWrapper} from './Base'
+
+export const DeleteWrapper = styled.div`
+ text-align: center;
+ position: absolute;
+ top: -1px;
+ right: 2px;
+ cursor: pointer;
+`
+
+export const GenDelButton = styled.button`
+ transition: all 0.5s ease;
+ display: inline-block;
+ border: none;
+ font-size: 15px;
+ height: 15px;
+ padding: 0;
+ margin-top: 5px;
+ text-align: center;
+ width: 15px;
+ background: inherit;
+ cursor: pointer;
+`
+
+export const DelButton = styled.button`
+ transition: all 0.5s ease;
+ display: inline-block;
+ border: none;
+ font-size: 8px;
+ height: 15px;
+ line-height: 1px;
+ margin: 0 0 8px;
+ padding: 0;
+ text-align: center;
+ width: 15px;
+ background: inherit;
+ cursor: pointer;
+ opacity: 0;
+
+ ${MovableCardWrapper}:hover & {
+ opacity: 1;
+ }
+`
+
+export const MenuButton = styled.button`
+ transition: all 0.5s ease;
+ display: inline-block;
+ border: none;
+ outline: none;
+ font-size: 16px;
+ font-weight: bold;
+ height: 15px;
+ line-height: 1px;
+ margin: 0 0 8px;
+ padding: 0;
+ text-align: center;
+ width: 15px;
+ background: inherit;
+ cursor: pointer;
+`
+
+export const LaneMenuHeader = styled.div`
+ position: relative;
+ margin-bottom: 4px;
+ text-align: center;
+`
+
+export const LaneMenuContent = styled.div`
+ overflow-x: hidden;
+ overflow-y: auto;
+ padding: 0 12px 12px;
+`
+
+export const LaneMenuItem = styled.div`
+ cursor: pointer;
+ display: block;
+ font-weight: 700;
+ padding: 6px 12px;
+ position: relative;
+ margin: 0 -12px;
+ text-decoration: none;
+
+ &:hover {
+ background-color: #3179ba;
+ color: #fff;
+ }
+`
+
+export const LaneMenuTitle = styled.span`
+ box-sizing: border-box;
+ color: #6b808c;
+ display: block;
+ line-height: 30px;
+ border-bottom: 1px solid rgba(9, 45, 66, 0.13);
+ margin: 0 6px;
+ overflow: hidden;
+ padding: 0 32px;
+ position: relative;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ z-index: 1;
+`
+
+export const DeleteIcon = styled.span`
+ position: relative;
+ display: inline-block;
+ width: 4px;
+ height: 4px;
+ opacity: 1;
+ overflow: hidden;
+ border: 1px solid #83bd42;
+ border-radius: 50%;
+ padding: 4px;
+ background-color: #83bd42;
+
+ ${CardWrapper}:hover & {
+ opacity: 1;
+ }
+
+ &:hover::before,
+ &:hover::after {
+ background: red;
+ }
+
+ &:before,
+ &:after {
+ content: '';
+ position: absolute;
+ height: 2px;
+ width: 60%;
+ top: 45%;
+ left: 20%;
+ background: #fff;
+ border-radius: 5px;
+ }
+
+ &:before {
+ -webkit-transform: rotate(45deg);
+ -moz-transform: rotate(45deg);
+ -o-transform: rotate(45deg);
+ transform: rotate(45deg);
+ }
+
+ &:after {
+ -webkit-transform: rotate(-45deg);
+ -moz-transform: rotate(-45deg);
+ -o-transform: rotate(-45deg);
+ transform: rotate(-45deg);
+ }
+`
+
+export const ExpandCollapseBase = styled.span`
+ width: 36px;
+ margin: 0 auto;
+ font-size: 14px;
+ position: relative;
+ cursor: pointer;
+`
+
+export const CollapseBtn = styled(ExpandCollapseBase)`
+ &:before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ border-bottom: 7px solid #444;
+ border-left: 7px solid transparent;
+ border-right: 7px solid transparent;
+ border-radius: 6px;
+ }
+
+ &:after {
+ content: '';
+ position: absolute;
+ left: 4px;
+ top: 4px;
+ border-bottom: 3px solid #e3e3e3;
+ border-left: 3px solid transparent;
+ border-right: 3px solid transparent;
+ }
+`
+
+export const ExpandBtn = styled(ExpandCollapseBase)`
+ &:before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ border-top: 7px solid #444;
+ border-left: 7px solid transparent;
+ border-right: 7px solid transparent;
+ border-radius: 6px;
+ }
+
+ &:after {
+ content: '';
+ position: absolute;
+ left: 4px;
+ top: 0px;
+ border-top: 3px solid #e3e3e3;
+ border-left: 3px solid transparent;
+ border-right: 3px solid transparent;
+ }
+`
+
+export const AddButton = styled.button`
+ background: #5aac44;
+ color: #fff;
+ transition: background 0.3s ease;
+ min-height: 32px;
+ padding: 4px 16px;
+ vertical-align: top;
+ margin-top: 0;
+ margin-right: 8px;
+ font-weight: bold;
+ border-radius: 3px;
+ font-size: 14px;
+ cursor: pointer;
+ margin-bottom: 0;
+`
+
+export const CancelButton = styled.button`
+ background: #999999;
+ color: #fff;
+ transition: background 0.3s ease;
+ min-height: 32px;
+ padding: 4px 16px;
+ vertical-align: top;
+ margin-top: 0;
+ font-weight: bold;
+ border-radius: 3px;
+ font-size: 14px;
+ cursor: pointer;
+ margin-bottom: 0;
+`
+export const AddLaneLink = styled.button`
+ background: #2b6aa3;
+ border: none;
+ color: #fff;
+ transition: background 0.3s ease;
+ min-height: 32px;
+ padding: 4px 16px;
+ vertical-align: top;
+ margin-top: 0;
+ margin-right: 0px;
+ border-radius: 4px;
+ font-size: 13px;
+ cursor: pointer;
+ margin-bottom: 0;
+`
diff --git a/client/src/components/trello-board/styles/Loader.js b/client/src/components/trello-board/styles/Loader.js
new file mode 100644
index 000000000..2d7545399
--- /dev/null
+++ b/client/src/components/trello-board/styles/Loader.js
@@ -0,0 +1,43 @@
+import styled, {keyframes} from 'styled-components'
+
+const keyframeAnimation = keyframes`
+ 0% {
+ transform: scale(1);
+ }
+ 20% {
+ transform: scale(1, 2.2);
+ }
+ 40% {
+ transform: scale(1);
+ }
+`
+export const LoaderDiv = styled.div`
+ text-align: center;
+ margin: 15px 0;
+`
+
+export const LoadingBar = styled.div`
+ display: inline-block;
+ margin: 0 2px;
+ width: 4px;
+ height: 18px;
+ border-radius: 4px;
+ animation: ${keyframeAnimation} 1s ease-in-out infinite;
+ background-color: #777;
+
+ &:nth-child(1) {
+ animation-delay: 0.0001s;
+ }
+
+ &:nth-child(2) {
+ animation-delay: 0.09s;
+ }
+
+ &:nth-child(3) {
+ animation-delay: 0.18s;
+ }
+
+ &:nth-child(4) {
+ animation-delay: 0.27s;
+ }
+`
diff --git a/client/src/components/trello-board/widgets/DeleteButton.jsx b/client/src/components/trello-board/widgets/DeleteButton.jsx
new file mode 100644
index 000000000..a89b854b9
--- /dev/null
+++ b/client/src/components/trello-board/widgets/DeleteButton.jsx
@@ -0,0 +1,12 @@
+import React from "react";
+import { DelButton, DeleteWrapper } from "../styles/Elements";
+
+const DeleteButton = (props) => {
+ return (
+
+ ✖
+
+ );
+};
+
+export default DeleteButton;
diff --git a/client/src/components/trello-board/widgets/EditableLabel.jsx b/client/src/components/trello-board/widgets/EditableLabel.jsx
new file mode 100644
index 000000000..13564234e
--- /dev/null
+++ b/client/src/components/trello-board/widgets/EditableLabel.jsx
@@ -0,0 +1,87 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+class EditableLabel extends React.Component {
+ constructor({value}) {
+ super()
+ this.state = {value: value}
+ }
+
+ getText = el => {
+ return el.innerText
+ }
+
+ onTextChange = ev => {
+ const value = this.getText(ev.target)
+ this.setState({value: value})
+ }
+
+ componentDidMount() {
+ if (this.props.autoFocus) {
+ this.refDiv.focus()
+ }
+ }
+
+ onBlur = () => {
+ this.props.onChange(this.state.value)
+ }
+
+ onPaste = ev => {
+ ev.preventDefault()
+ const value = ev.clipboardData.getData('text')
+ document.execCommand('insertText', false, value)
+ }
+
+ getClassName = () => {
+ const placeholder = this.state.value === '' ? 'comPlainTextContentEditable--has-placeholder' : ''
+ return `comPlainTextContentEditable ${placeholder}`
+ }
+
+ onKeyDown = e => {
+ if (e.keyCode === 13) {
+ this.props.onChange(this.state.value)
+ this.refDiv.blur()
+ e.preventDefault()
+ }
+ if (e.keyCode === 27) {
+ this.refDiv.value = this.props.value
+ this.setState({value: this.props.value})
+ // this.refDiv.blur()
+ e.preventDefault()
+ e.stopPropagation()
+ }
+ }
+
+ render() {
+ const placeholder = this.props.value.length > 0 ? false : this.props.placeholder
+ return (
+ (this.refDiv = ref)}
+ contentEditable="true"
+ className={this.getClassName()}
+ onPaste={this.onPaste}
+ onBlur={this.onBlur}
+ onInput={this.onTextChange}
+ onKeyDown={this.onKeyDown}
+ placeholder={placeholder}
+ />
+ )
+ }
+}
+
+EditableLabel.propTypes = {
+ onChange: PropTypes.func,
+ placeholder: PropTypes.string,
+ autoFocus: PropTypes.bool,
+ inline: PropTypes.bool,
+ value: PropTypes.string
+}
+
+EditableLabel.defaultProps = {
+ onChange: () => {},
+ placeholder: '',
+ autoFocus: false,
+ inline: false,
+ value: ''
+}
+export default EditableLabel
diff --git a/client/src/components/trello-board/widgets/InlineInput.jsx b/client/src/components/trello-board/widgets/InlineInput.jsx
new file mode 100644
index 000000000..56c311931
--- /dev/null
+++ b/client/src/components/trello-board/widgets/InlineInput.jsx
@@ -0,0 +1,106 @@
+import React, { useEffect, useRef, useState } from "react";
+import PropTypes from "prop-types";
+import { InlineInput } from "../styles/Base";
+import autosize from "autosize";
+
+const InlineInputController = ({ onSave, border, placeholder, value, autoFocus, resize, onCancel }) => {
+ const inputRef = useRef(null);
+ const [inputValue, setInputValue] = useState(value);
+
+ // Effect for autosizing and initial autoFocus
+ useEffect(() => {
+ if (inputRef.current && resize !== "none") {
+ autosize(inputRef.current);
+ }
+ if (inputRef.current && autoFocus) {
+ inputRef.current.focus();
+ }
+ }, [resize, autoFocus]);
+
+ // Effect to update value when props change
+ useEffect(() => {
+ setInputValue(value);
+ }, [value]);
+
+ const handleFocus = (e) => e.target.select();
+
+ const handleMouseDown = (e) => {
+ if (document.activeElement !== e.target) {
+ e.preventDefault();
+ inputRef.current.focus();
+ }
+ };
+
+ const handleBlur = () => {
+ updateValue();
+ };
+
+ const handleKeyDown = (e) => {
+ if (e.keyCode === 13) {
+ // Enter
+ inputRef.current.blur();
+ e.preventDefault();
+ } else if (e.keyCode === 27) {
+ // Escape
+ setInputValue(value); // Reset to initial value
+ inputRef.current.blur();
+ e.preventDefault();
+ } else if (e.keyCode === 9) {
+ // Tab
+ if (inputValue.length === 0) {
+ onCancel();
+ }
+ inputRef.current.blur();
+ e.preventDefault();
+ }
+ };
+
+ const updateValue = () => {
+ if (inputValue !== value) {
+ onSave(inputValue);
+ }
+ };
+
+ return (
+ setInputValue(e.target.value)}
+ autoComplete="off"
+ autoCorrect="off"
+ autoCapitalize="off"
+ spellCheck="false"
+ dataGramm="false"
+ rows={1}
+ autoFocus={autoFocus}
+ />
+ );
+};
+
+InlineInputController.propTypes = {
+ onSave: PropTypes.func,
+ onCancel: PropTypes.func,
+ border: PropTypes.bool,
+ placeholder: PropTypes.string,
+ value: PropTypes.string,
+ autoFocus: PropTypes.bool,
+ resize: PropTypes.oneOf(["none", "vertical", "horizontal"])
+};
+
+InlineInputController.defaultProps = {
+ onSave: () => {},
+ onCancel: () => {},
+ placeholder: "",
+ value: "",
+ border: false,
+ autoFocus: false,
+ resize: "none"
+};
+
+export default InlineInputController;
diff --git a/client/src/components/trello-board/widgets/NewLaneTitleEditor.jsx b/client/src/components/trello-board/widgets/NewLaneTitleEditor.jsx
new file mode 100644
index 000000000..572046d70
--- /dev/null
+++ b/client/src/components/trello-board/widgets/NewLaneTitleEditor.jsx
@@ -0,0 +1,94 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { InlineInput } from "../styles/Base";
+import autosize from "autosize";
+
+class NewLaneTitleEditor extends React.Component {
+ onKeyDown = (e) => {
+ if (e.keyCode === 13) {
+ this.refInput.blur();
+ this.props.onSave();
+ e.preventDefault();
+ }
+ if (e.keyCode === 27) {
+ this.cancel();
+ e.preventDefault();
+ }
+
+ if (e.keyCode === 9) {
+ if (this.getValue().length === 0) {
+ this.cancel();
+ } else {
+ this.props.onSave();
+ }
+ e.preventDefault();
+ }
+ };
+
+ cancel = () => {
+ this.setValue("");
+ this.props.onCancel();
+ this.refInput.blur();
+ };
+
+ getValue = () => this.refInput.value;
+ setValue = (value) => (this.refInput.value = value);
+
+ saveValue = () => {
+ if (this.getValue() !== this.props.value) {
+ this.props.onSave(this.getValue());
+ }
+ };
+
+ focus = () => this.refInput.focus();
+
+ setRef = (ref) => {
+ this.refInput = ref;
+ if (this.props.resize !== "none") {
+ autosize(this.refInput);
+ }
+ };
+
+ render() {
+ const { autoFocus, resize, border, autoResize, value, placeholder } = this.props;
+
+ return (
+
+ );
+ }
+}
+
+NewLaneTitleEditor.propTypes = {
+ onSave: PropTypes.func,
+ onCancel: PropTypes.func,
+ border: PropTypes.bool,
+ placeholder: PropTypes.string,
+ value: PropTypes.string,
+ autoFocus: PropTypes.bool,
+ autoResize: PropTypes.bool,
+ resize: PropTypes.oneOf(["none", "vertical", "horizontal"])
+};
+
+NewLaneTitleEditor.defaultProps = {
+ inputRef: () => {},
+ onSave: () => {},
+ onCancel: () => {},
+ placeholder: "",
+ value: "",
+ border: false,
+ autoFocus: false,
+ autoResize: false,
+ resize: "none"
+};
+
+export default NewLaneTitleEditor;
diff --git a/client/src/components/trello-board/widgets/index.jsx b/client/src/components/trello-board/widgets/index.jsx
new file mode 100644
index 000000000..c9139bc09
--- /dev/null
+++ b/client/src/components/trello-board/widgets/index.jsx
@@ -0,0 +1,9 @@
+import DeleteButton from "./DeleteButton";
+import EditableLabel from "./EditableLabel";
+import InlineInput from "./InlineInput";
+
+export default {
+ DeleteButton,
+ EditableLabel,
+ InlineInput
+};
diff --git a/client/src/redux/root.reducer.js b/client/src/redux/root.reducer.js
index 48113382b..ad3a1c0cb 100644
--- a/client/src/redux/root.reducer.js
+++ b/client/src/redux/root.reducer.js
@@ -9,6 +9,7 @@ import messagingReducer from "./messaging/messaging.reducer";
import modalsReducer from "./modals/modals.reducer";
import techReducer from "./tech/tech.reducer";
import userReducer from "./user/user.reducer";
+import trelloReducer from "./trello/trello.reducer";
// const persistConfig = {
// key: "root",
@@ -30,11 +31,8 @@ const rootReducer = combineReducers({
modals: modalsReducer,
application: persistReducer(applicationPersistConfig, applicationReducer),
tech: techReducer,
- media: mediaReducer
+ media: mediaReducer,
+ trello: trelloReducer
});
-export default withReduxStateSync(
- // persistReducer(persistConfig,
- rootReducer
- //)
-);
+export default withReduxStateSync(rootReducer);
diff --git a/client/src/redux/trello/trello.actions.js b/client/src/redux/trello/trello.actions.js
new file mode 100644
index 000000000..ad0ed7990
--- /dev/null
+++ b/client/src/redux/trello/trello.actions.js
@@ -0,0 +1,14 @@
+import { createAction } from "redux-actions";
+
+export const loadBoard = createAction("LOAD_BOARD");
+export const addLane = createAction("ADD_LANE");
+export const addCard = createAction("ADD_CARD");
+export const updateCard = createAction("UPDATE_CARD");
+export const removeCard = createAction("REMOVE_CARD");
+export const moveCardAcrossLanes = createAction("MOVE_CARD");
+export const updateCards = createAction("UPDATE_CARDS");
+export const updateLanes = createAction("UPDATE_LANES");
+export const updateLane = createAction("UPDATE_LANE");
+export const paginateLane = createAction("PAGINATE_LANE");
+export const moveLane = createAction("MOVE_LANE");
+export const removeLane = createAction("REMOVE_LANE");
diff --git a/client/src/redux/trello/trello.reducer.js b/client/src/redux/trello/trello.reducer.js
new file mode 100644
index 000000000..96418ce75
--- /dev/null
+++ b/client/src/redux/trello/trello.reducer.js
@@ -0,0 +1,35 @@
+import Lh from "../../components/trello-board/helpers/LaneHelper";
+
+const boardReducer = (state = { lanes: [] }, action) => {
+ const { payload, type } = action;
+ switch (type) {
+ case "LOAD_BOARD":
+ return Lh.initialiseLanes(state, payload);
+ case "ADD_CARD":
+ return Lh.appendCardToLane(state, payload);
+ case "REMOVE_CARD":
+ return Lh.removeCardFromLane(state, payload);
+ case "MOVE_CARD":
+ return Lh.moveCardAcrossLanes(state, payload);
+ case "UPDATE_CARDS":
+ return Lh.updateCardsForLane(state, payload);
+ case "UPDATE_CARD":
+ return Lh.updateCardForLane(state, payload);
+ case "UPDATE_LANES":
+ return Lh.updateLanes(state, payload);
+ case "UPDATE_LANE":
+ return Lh.updateLane(state, payload);
+ case "PAGINATE_LANE":
+ return Lh.paginateLane(state, payload);
+ case "MOVE_LANE":
+ return Lh.moveLane(state, payload);
+ case "REMOVE_LANE":
+ return Lh.removeLane(state, payload);
+ case "ADD_LANE":
+ return Lh.addLane(state, payload);
+ default:
+ return state;
+ }
+};
+
+export default boardReducer;
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index 0011cede6..2a121c51b 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -3460,6 +3460,18 @@
"validation": {
"unique_vendor_name": "You must enter a unique vendor name."
}
- }
+ },
+ "trello": {
+ "labels": {
+ "add_card": "Add Card",
+ "add_lane": "Add Lane",
+ "delete_lane": "Delete Lane",
+ "lane_actions": "Lane Actions",
+ "title": "Title",
+ "description": "Description",
+ "label": "Label",
+ "cancel": "Cancel"
+ }
+ }
}
}
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index 463e310e6..d265b4a42 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -3460,6 +3460,18 @@
"validation": {
"unique_vendor_name": ""
}
- }
+ },
+ "trello": {
+ "labels": {
+ "add_card": "",
+ "add_lane": "",
+ "delete_lane": "",
+ "lane_actions": "",
+ "title": "",
+ "description": "",
+ "label": "",
+ "cancel": ""
+ }
+ }
}
}
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index 8ddbeef43..f8de4a336 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -3460,6 +3460,18 @@
"validation": {
"unique_vendor_name": ""
}
- }
+ },
+ "trello": {
+ "labels": {
+ "add_card": "",
+ "add_lane": "",
+ "delete_lane": "",
+ "lane_actions": "",
+ "title": "",
+ "description": "",
+ "label": "",
+ "cancel": ""
+ }
+ }
}
}
diff --git a/client/vite.config.js b/client/vite.config.js
index 3d9f3fb7b..9208812a0 100644
--- a/client/vite.config.js
+++ b/client/vite.config.js
@@ -5,7 +5,7 @@ import * as path from "path";
import * as url from "url";
import { defineConfig } from "vite";
import { ViteEjsPlugin } from "vite-plugin-ejs";
-import eslint from 'vite-plugin-eslint';
+import eslint from "vite-plugin-eslint";
//import CompressionPlugin from 'vite-plugin-compression';
import { VitePWA } from "vite-plugin-pwa";
@@ -103,7 +103,7 @@ export default defineConfig({
}),
reactVirtualized(),
react(),
- eslint(),
+ eslint()
// CompressionPlugin(), //Cloudfront already compresses assets, so not needed.
],
define: {