- Optimization and Edgecases

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-07-02 20:50:34 -04:00
parent 61569d97cb
commit 162d8bfffe
10 changed files with 394 additions and 465 deletions

View File

@@ -6,49 +6,31 @@ import {
PauseCircleOutlined
} from "@ant-design/icons";
import { Card, Col, Row, Space, Tooltip } from "antd";
import React, { useMemo, useCallback } from "react";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
import "./production-board-card.styles.scss";
import dayjs from "../../utils/day";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
/**
* Get the color of the card based on the total hours
* @param ssbuckets
* @param totalHrs
* @returns {{r: number, b: number, g: number}}
*/
const cardColor = (ssbuckets, totalHrs) => {
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
return bucket && bucket.color ? bucket.color.rgb || bucket.color : { r: 255, g: 255, b: 255 };
};
/**
* Get the contrast color based on the background color
* @param bgColor
* @returns {string}
*/
const getContrastYIQ = (bgColor) =>
(bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000 >= 128 ? "black" : "white";
/**
* Production Board Card component
* @param technician
* @param card
* @param bodyshop
* @param cardSettings
* @returns {Element}
* @constructor
*/
export default function ProductionBoardCard({ technician, card, bodyshop, cardSettings }) {
const { t } = useTranslation();
const { metadata } = card;
const employee_body = useMemo(
@@ -90,9 +72,8 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
className="react-trello-card"
size="small"
style={{
backgroundColor:
cardSettings && cardSettings.cardcolor && `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
color: cardSettings && cardSettings.cardcolor && contrastYIQ,
backgroundColor: cardSettings?.cardcolor && `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
color: cardSettings?.cardcolor && contrastYIQ,
maxWidth: "250px",
margin: "5px"
}}

View File

@@ -3,7 +3,7 @@ import { useApolloClient } from "@apollo/client";
import Board from "../../components/trello-board/index";
import { Button, notification, Skeleton, Space, Statistic } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useMemo, useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -49,13 +49,11 @@ export function ProductionBoardKanbanComponent({
associationSettings
}) {
const [boardLanes, setBoardLanes] = useState({ lanes: [] });
const [filter, setFilter] = useState({ search: "", employeeId: null });
const [loading, setLoading] = useState(true);
const [isMoving, setIsMoving] = useState(false);
const orientation = associationSettings?.kanban_settings?.orientation ? "vertical" : "horizontal";
const { t } = useTranslation();
useEffect(() => {
@@ -65,13 +63,12 @@ export function ProductionBoardKanbanComponent({
}, [associationSettings]);
useEffect(() => {
const newBoardData = createFakeBoardData(
const newBoardData = createBoardData(
[...bodyshop.md_ro_statuses.production_statuses, ...(bodyshop.md_ro_statuses.additional_board_statuses || [])],
data,
filter
);
// Build Board Lanes Data
newBoardData.lanes = newBoardData.lanes.map((lane) => ({
...lane,
title: `${lane.title} (${lane.cards.length})`
@@ -89,13 +86,7 @@ export function ProductionBoardKanbanComponent({
const client = useApolloClient();
/**
* Get Card By ID
* @param data
* @param cardId
* @returns {*|any|null}
*/
const getCardByID = (data, cardId) => {
const getCardByID = useCallback((data, cardId) => {
for (const lane of data.lanes) {
for (const card of lane.cards) {
if (card.id === cardId) {
@@ -104,88 +95,90 @@ export function ProductionBoardKanbanComponent({
}
}
return null;
};
}, []);
const onDragEnd = async ({ type, source, destination, draggableId }) => {
logImEXEvent("kanban_drag_end");
const onDragEnd = useCallback(
async ({ type, source, destination, draggableId }) => {
logImEXEvent("kanban_drag_end");
// Early gate, also if the card is already moving bail
if (!type || type !== "lane" || !source || !destination || isMoving) return;
if (!type || type !== "lane" || !source || !destination || isMoving) return;
setIsMoving(true);
setIsMoving(true);
const targetLane = boardLanes.lanes.find((lane) => lane.id === destination.droppableId);
const sourceLane = boardLanes.lanes.find((lane) => lane.id === source.droppableId);
const targetLane = boardLanes.lanes.find((lane) => lane.id === destination.droppableId);
const sourceLane = boardLanes.lanes.find((lane) => lane.id === source.droppableId);
const sameColumnTransfer = source.droppableId === destination.droppableId;
const sourceCard = getCardByID(boardLanes, draggableId);
const sameColumnTransfer = source.droppableId === destination.droppableId;
const sourceCard = getCardByID(boardLanes, draggableId);
const movedCardWillBeFirst = destination.index === 0;
const movedCardWillBeLast = destination.index > targetLane.cards.length - 1;
const movedCardWillBeFirst = destination.index === 0;
const movedCardWillBeLast = destination.index > targetLane.cards.length - 1;
const lastCardInTargetLane = targetLane.cards[targetLane.cards.length - 1];
const lastCardInTargetLane = targetLane.cards[targetLane.cards.length - 1];
const oldChildCard = sourceLane.cards[destination.index + 1];
const oldChildCard = sourceLane.cards[destination.index + 1];
const newChildCard = movedCardWillBeLast
? null
: targetLane.cards[
sameColumnTransfer
? destination.index - destination.index > 0
? destination.index
: destination.index + 1
: destination.index
];
const newChildCard = movedCardWillBeLast
? null
: targetLane.cards[
sameColumnTransfer
? destination.index - destination.index > 0
? destination.index
: destination.index + 1
: destination.index
];
const oldChildCardNewParent = oldChildCard ? sourceCard.metadata.kanbanparent : null;
const oldChildCardNewParent = oldChildCard ? sourceCard.metadata.kanbanparent : null;
let movedCardNewKanbanParent;
if (movedCardWillBeFirst) {
movedCardNewKanbanParent = "-1";
} else if (movedCardWillBeLast) {
movedCardNewKanbanParent = lastCardInTargetLane.id;
} else if (!!newChildCard) {
movedCardNewKanbanParent = newChildCard.metadata.kanbanparent;
} else {
console.log("==> !!!!!!Couldn't find a parent.!!!! <==");
}
const newChildCardNewParent = newChildCard ? draggableId : null;
try {
const update = await client.mutate({
mutation: generate_UPDATE_JOB_KANBAN(
oldChildCard ? oldChildCard.id : null,
oldChildCardNewParent,
draggableId,
movedCardNewKanbanParent,
targetLane.id,
newChildCard ? newChildCard.id : null,
newChildCardNewParent
)
});
let movedCardNewKanbanParent;
if (movedCardWillBeFirst) {
movedCardNewKanbanParent = "-1";
} else if (movedCardWillBeLast) {
movedCardNewKanbanParent = lastCardInTargetLane.id;
} else if (!!newChildCard) {
movedCardNewKanbanParent = newChildCard.metadata.kanbanparent;
} else {
console.log("==> !!!!!!Couldn't find a parent.!!!! <==");
}
const newChildCardNewParent = newChildCard ? draggableId : null;
try {
const update = await client.mutate({
mutation: generate_UPDATE_JOB_KANBAN(
oldChildCard ? oldChildCard.id : null,
oldChildCardNewParent,
draggableId,
movedCardNewKanbanParent,
targetLane.id,
newChildCard ? newChildCard.id : null,
newChildCardNewParent
)
});
insertAuditTrail({
jobid: draggableId,
operation: AuditTrailMapping.jobstatuschange(targetLane.id),
type: "jobstatuschange"
});
insertAuditTrail({
jobid: draggableId,
operation: AuditTrailMapping.jobstatuschange(targetLane.id),
type: "jobstatuschange"
});
if (update.errors) {
if (update.errors) {
notification["error"]({
message: t("production.errors.boardupdate", {
message: JSON.stringify(update.errors)
})
});
}
} catch (error) {
notification["error"]({
message: t("production.errors.boardupdate", {
message: JSON.stringify(update.errors)
message: error.message
})
});
} finally {
setIsMoving(false);
}
} catch (error) {
notification["error"]({
message: t("production.errors.boardupdate", {
message: error.message
})
});
} finally {
setIsMoving(false);
}
};
},
[boardLanes, client, getCardByID, insertAuditTrail, isMoving, t]
);
const totalHrs = useMemo(
() =>
@@ -209,36 +202,45 @@ export function ProductionBoardKanbanComponent({
[data]
);
const Header = ({ title }) => (
<div className="react-trello-column-header" style={{ backgroundColor: "#e3e3e3" }}>
<UnorderedListOutlined style={{ marginRight: "5px" }} /> {title}
</div>
const Header = useCallback(
({ title }) => (
<div className="react-trello-column-header" style={{ backgroundColor: "#e3e3e3" }}>
<UnorderedListOutlined style={{ marginRight: "5px" }} /> {title}
</div>
),
[]
);
const cardSettings =
associationSettings &&
associationSettings.kanban_settings &&
Object.keys(associationSettings.kanban_settings).length > 0
? associationSettings.kanban_settings
: {
ats: true,
clm_no: true,
compact: false,
ownr_nm: true,
sublets: true,
ins_co_nm: true,
production_note: true,
employeeassignments: true,
scheduled_completion: true,
stickyheader: false,
cardcolor: false,
orientation: false
};
const cardSettings = useMemo(
() =>
associationSettings?.kanban_settings && Object.keys(associationSettings.kanban_settings).length > 0
? associationSettings.kanban_settings
: {
ats: true,
clm_no: true,
compact: false,
ownr_nm: true,
sublets: true,
ins_co_nm: true,
production_note: true,
employeeassignments: true,
scheduled_completion: true,
stickyheader: false,
cardcolor: false,
orientation: false
},
[associationSettings]
);
const components = {
Card: (cardProps) => ProductionBoardCard({ card: cardProps, technician, bodyshop, cardSettings }),
LaneHeader: Header
};
const components = useMemo(
() => ({
Card: (cardProps) => (
<ProductionBoardCard card={cardProps} technician={technician} bodyshop={bodyshop} cardSettings={cardSettings} />
),
LaneHeader: Header
}),
[Header, bodyshop, cardSettings, technician]
);
if (loading) {
return <Skeleton active />;

View File

@@ -1,7 +1,7 @@
import { ExclamationCircleFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Dropdown } from "antd";
import React from "react";
import React, { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -21,9 +21,8 @@ export function ProductionListColumnAlert({ record, insertAuditTrail }) {
const [updateAlert] = useMutation(UPDATE_JOB);
const handleAlertToggle = (e) => {
const handleAlertToggle = useCallback(() => {
logImEXEvent("production_toggle_alert");
//e.stopPropagation();
updateAlert({
variables: {
jobId: record.id,
@@ -44,10 +43,10 @@ export function ProductionListColumnAlert({ record, insertAuditTrail }) {
}).then(() => {
if (record.refetch) record.refetch();
});
};
}, [updateAlert, insertAuditTrail, record]);
const menu = {
items: [
const menuItems = useMemo(
() => [
{
key: "toggleAlert",
label:
@@ -56,17 +55,13 @@ export function ProductionListColumnAlert({ record, insertAuditTrail }) {
: t("production.labels.alerton"),
onClick: handleAlertToggle
}
]
};
],
[record.production_vars, t, handleAlertToggle]
);
return (
<Dropdown menu={menu} trigger={["contextMenu"]}>
<div
style={{
//width: "100%",
height: "19px"
}}
>
<Dropdown menu={{ items: menuItems }} trigger={["contextMenu"]}>
<div style={{ height: "19px" }}>
{record.production_vars && record.production_vars.alert ? (
<ExclamationCircleFilled className="production-alert" />
) : null}

View File

@@ -1,7 +1,7 @@
import Icon from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, Input, Popover, Space } from "antd";
import React, { useState } from "react";
import React, { useCallback, useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { FaRegStickyNote } from "react-icons/fa";
import { logImEXEvent } from "../../firebase/firebase.utils";
@@ -19,77 +19,81 @@ const mapDispatchToProps = (dispatch) => ({
function ProductionListColumnProductionNote({ record, setNoteUpsertContext }) {
const { t } = useTranslation();
const [note, setNote] = useState((record.production_vars && record.production_vars.note) || "");
const [open, setOpen] = useState(false);
const [updateAlert] = useMutation(UPDATE_JOB);
const handleSaveNote = (e) => {
logImEXEvent("production_add_note");
e.stopPropagation();
setOpen(false);
updateAlert({
variables: {
jobId: record.id,
job: {
production_vars: {
...record.production_vars,
note: note
const handleSaveNote = useCallback(
(e) => {
logImEXEvent("production_add_note");
e.stopPropagation();
setOpen(false);
updateAlert({
variables: {
jobId: record.id,
job: {
production_vars: {
...record.production_vars,
note: note
}
}
}
}
}).then(() => {
if (record.refetch) record.refetch();
});
};
}).then(() => {
if (record.refetch) record.refetch();
});
},
[updateAlert, record, note]
);
const handleChange = (e) => {
const handleChange = useCallback((e) => {
e.stopPropagation();
setNote(e.target.value);
};
}, []);
const handleOpenChange = (flag) => {
setOpen(flag);
if (flag) setNote((record.production_vars && record.production_vars.note) || "");
};
const handleOpenChange = useCallback(
(flag) => {
setOpen(flag);
if (flag) setNote((record.production_vars && record.production_vars.note) || "");
},
[record]
);
const popoverContent = useMemo(
() => (
<div style={{ width: "30em" }}>
<Input.TextArea
rows={5}
value={note}
onChange={handleChange}
autoFocus
allowClear
style={{ marginBottom: "1em" }}
/>
<Space>
<Button onClick={handleSaveNote} type="primary">
{t("general.actions.save")}
</Button>
<Button
onClick={() => {
setOpen(false);
setNoteUpsertContext({
context: {
jobId: record.id,
text: note
}
});
}}
>
{t("notes.actions.savetojobnotes")}
</Button>
</Space>
</div>
),
[note, handleSaveNote, handleChange, record, setNoteUpsertContext, t]
);
return (
<Popover
onOpenChange={handleOpenChange}
open={open}
content={
<div style={{ width: "30em" }}>
<Input.TextArea
rows={5}
value={note}
onChange={handleChange}
// onPressEnter={handleSaveNote}
autoFocus
allowClear
style={{ marginBottom: "1em" }}
/>
<Space>
<Button onClick={handleSaveNote} type="primary">
{t("general.actions.save")}
</Button>
<Button
onClick={() => {
setOpen(false);
setNoteUpsertContext({
context: {
jobId: record.id,
text: note
}
});
}}
>
{t("notes.actions.savetojobnotes")}
</Button>
</Space>
</div>
}
trigger={["click"]}
>
<Popover onOpenChange={handleOpenChange} open={open} content={popoverContent} trigger={["click"]}>
<div
style={{
width: "100%",

View File

@@ -1,6 +1,6 @@
import { CheckCircleFilled, EyeInvisibleFilled } from "@ant-design/icons";
import { Button, List, notification, Popover } from "antd";
import React, { useMemo, useState } from "react";
import React, { useMemo, useState, useCallback } from "react";
import { useMutation } from "@apollo/client";
import { useTranslation } from "react-i18next";
import { UPDATE_JOB_LINE_SUBLET } from "../../graphql/jobs-lines.queries";
@@ -10,79 +10,86 @@ export default function ProductionSubletsManageComponent({ subletJobLines }) {
const [updateJobLine] = useMutation(UPDATE_JOB_LINE_SUBLET);
const [loading, setLoading] = useState(false);
const subletCount = useMemo(() => {
return {
const subletCount = useMemo(
() => ({
total: subletJobLines.filter((s) => !s.sublet_ignored).length,
outstanding: subletJobLines.filter((s) => !s.sublet_ignored && !s.sublet_completed).length
};
}, [subletJobLines]);
}),
[subletJobLines]
);
const handleSubletMark = async (sublet, action) => {
setLoading(true);
const handleSubletMark = useCallback(
async (sublet, action) => {
setLoading(true);
const result = await updateJobLine({
variables: {
jobId: sublet.jobid,
now: new Date(),
lineId: sublet.id,
line: {
sublet_completed: action === "complete" ? !sublet.sublet_completed : false,
sublet_ignored: action === "ignore" ? !sublet.sublet_ignored : false
const result = await updateJobLine({
variables: {
jobId: sublet.jobid,
now: new Date(),
lineId: sublet.id,
line: {
sublet_completed: action === "complete" ? !sublet.sublet_completed : false,
sublet_ignored: action === "ignore" ? !sublet.sublet_ignored : false
}
}
});
if (result.errors) {
notification["error"]({
message: t("joblines.errors.updating", {
message: JSON.stringify(result.errors)
})
});
} else {
notification["success"]({
message: t("joblines.successes.updated")
});
}
});
setLoading(false);
},
[updateJobLine, t]
);
if (!!result.errors) {
notification["error"]({
message: t("joblines.errors.updating", {
message: JSON.stringify(result.errors)
})
});
} else {
notification["success"]({
message: t("joblines.successes.updated")
});
}
setLoading(false);
};
const popContent = (
<div style={{ minWidth: "20rem" }}>
<List
onClick={(e) => e.stopPropagation()}
dataSource={subletJobLines}
renderItem={(s) => (
<List.Item
actions={[
<Button
key="complete"
loading={loading}
onClick={(e) => {
e.stopPropagation();
handleSubletMark(s, "complete");
}}
type={s.sublet_completed ? "primary" : "ghost"}
>
<CheckCircleFilled color={s.sublet_completed ? "green" : null} />
</Button>,
<Button
key="sublet"
loading={loading}
onClick={(e) => {
e.stopPropagation();
handleSubletMark(s, "ignore");
}}
type={s.sublet_ignored ? "primary" : "ghost"}
>
<EyeInvisibleFilled color={s.sublet_ignored ? "tomato" : null} />
</Button>
]}
>
<List.Item.Meta title={s.line_desc} />
</List.Item>
)}
/>
</div>
const popContent = useMemo(
() => (
<div style={{ minWidth: "20rem" }}>
<List
onClick={(e) => e.stopPropagation()}
dataSource={subletJobLines}
renderItem={(s) => (
<List.Item
actions={[
<Button
key="complete"
loading={loading}
onClick={(e) => {
e.stopPropagation();
handleSubletMark(s, "complete");
}}
type={s.sublet_completed ? "primary" : "ghost"}
>
<CheckCircleFilled style={{ color: s.sublet_completed ? "green" : undefined }} />
</Button>,
<Button
key="sublet"
loading={loading}
onClick={(e) => {
e.stopPropagation();
handleSubletMark(s, "ignore");
}}
type={s.sublet_ignored ? "primary" : "ghost"}
>
<EyeInvisibleFilled style={{ color: s.sublet_ignored ? "tomato" : undefined }} />
</Button>
]}
>
<List.Item.Meta title={s.line_desc} />
</List.Item>
)}
/>
</div>
),
[subletJobLines, loading, handleSubletMark]
);
return (
@@ -93,9 +100,9 @@ export default function ProductionSubletsManageComponent({ subletJobLines }) {
placement="bottom"
title={t("production.labels.sublets")}
>
<span style={{ color: subletCount.outstanding > 0 ? "tomato" : "" }}>{`${
subletCount.total - subletCount.outstanding
} / ${subletCount.total}`}</span>
<span
style={{ color: subletCount.outstanding > 0 ? "tomato" : undefined }}
>{`${subletCount.total - subletCount.outstanding} / ${subletCount.total}`}</span>
</Popover>
);
}

View File

@@ -2,20 +2,12 @@ import React, { useEffect, useState } from "react";
const HeightPreservingItem = ({ children, ...props }) => {
const [size, setSize] = useState(0);
const { "data-known-size": knownSize = 0 } = props;
const knownSize = props["data-known-size"];
useEffect(() => {
if (knownSize !== 0) {
setSize(knownSize);
}
}, [knownSize]);
//
// useEffect(() => {
// if (knownSize !== 0 && knownSize !== size) {
// setSize(knownSize);
// }
// }, [knownSize, size]);
setSize((prevSize) => {
return knownSize === 0 ? prevSize : knownSize;
});
}, [setSize, knownSize]);
return (
<div
{...props}

View File

@@ -1,13 +1,16 @@
import { BoardContainer } from "../index";
import classNames from "classnames";
import { useState } from "react";
import { useMemo, useState } from "react";
import { v1 } from "uuid";
const Board = ({ id, className, components, orientation, ...additionalProps }) => {
const [storeId] = useState(id || v1());
const allClassNames = classNames("react-trello-board", className || "");
const OrientationStyle = orientation === "horizontal" ? components.StyleHorizontal : components.StyleVertical;
const allClassNames = useMemo(() => classNames("react-trello-board", className || ""), [className]);
const OrientationStyle = useMemo(
() => (orientation === "horizontal" ? components.StyleHorizontal : components.StyleVertical),
[orientation, components.StyleHorizontal, components.StyleVertical]
);
return (
<>

View File

@@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback, useEffect, useState, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { DragDropContext, Droppable } from "../dnd/lib";
import PropTypes from "prop-types";
import pick from "lodash/pick";
import isEqual from "lodash/isEqual";
@@ -35,7 +34,6 @@ import * as actions from "../../../redux/trello/trello.actions.js";
* @param {boolean} props.editable - Whether the board is editable
* @param {boolean} props.canAddLanes - Whether lanes can be added to the board
* @param {Object} props.laneStyle - The CSS styles to apply to the lanes
* @param {Function} props.onCardMoveAcrossLanes - Callback function when a card is moved across lanes
* @param {string} props.orientation - The orientation of the board ("horizontal" or "vertical")
* @param {Function} props.eventBusHandle - Function to handle events from the event bus
* @param {Object} props.reducerData - The initial data for the Redux reducer
@@ -64,7 +62,6 @@ const BoardContainer = ({
editable = false,
canAddLanes = false,
laneStyle,
onCardMoveAcrossLanes = () => {},
orientation = "horizontal",
eventBusHandle,
reducerData,
@@ -129,43 +126,15 @@ const BoardContainer = ({
}
}, [currentReducerData, reducerData, onDataChange]);
/**
* onDragUpdate
* @param draggableId
* @param type
* @param source
* @param mode
* @param combine
* @param destination
*/
const onDragUpdate = ({ draggableId, type, source, mode, combine, destination }) => {};
const onDragUpdate = () => {};
/**
* onDragStart
* @type {(function({draggableId: *, type: *, source: *, mode: *}): void)|*}
*/
const onDragStart = useCallback(
({ draggableId, type, source, mode }) => {
setIsDragging(true);
},
[setIsDragging]
);
const onDragStart = useCallback(() => {
setIsDragging(true);
}, []);
/**
* onBeforeDragStart
* @param draggableId
* @param type
* @param source
* @param mode
*/
const onBeforeDragStart = ({ draggableId, type, source, mode }) => {};
const onBeforeDragStart = () => {};
/**
* onBeforeCapture
* @param draggableId
* @param mode
*/
const onBeforeCapture = ({ draggableId, mode }) => {};
const onBeforeCapture = () => {};
const getCardDetails = useCallback(
(laneId, cardIndex) => {
@@ -174,22 +143,77 @@ const BoardContainer = ({
[currentReducerData]
);
const hideEditableLane = () => {
const hideEditableLane = useCallback(() => {
setAddLaneMode(false);
};
}, []);
const showEditableLane = () => {
const showEditableLane = useCallback(() => {
setAddLaneMode(true);
};
}, []);
const addNewLane = (params) => {
hideEditableLane();
dispatch(actions.addLane(params));
onLaneAdd(params);
};
const addNewLane = useCallback(
(params) => {
hideEditableLane();
dispatch(actions.addLane(params));
onLaneAdd(params);
},
[dispatch, hideEditableLane, onLaneAdd]
);
const passThroughProps = pick(
{
const passThroughProps = useMemo(
() =>
pick(
{
id,
components,
data,
draggable,
style,
onDataChange,
onCardAdd,
onCardUpdate,
onCardClick,
onBeforeCardDelete,
onCardDelete,
onLaneScroll,
onLaneClick,
onLaneAdd,
onLaneDelete,
onLaneUpdate,
editable,
canAddLanes,
laneStyle,
orientation,
eventBusHandle,
reducerData,
cardStyle,
...otherProps
},
[
"onLaneScroll",
"onLaneDelete",
"onLaneUpdate",
"onCardClick",
"onBeforeCardDelete",
"onCardDelete",
"onCardAdd",
"onCardUpdate",
"onLaneClick",
"laneSortFunction",
"draggable",
"cardDraggable",
"collapsibleLanes",
"canAddLanes",
"hideCardDeleteIcon",
"tagStyle",
"handleDragStart",
"handleDragEnd",
"cardDragClass",
"editLaneTitle",
"orientation"
]
),
[
id,
components,
data,
@@ -209,66 +233,44 @@ const BoardContainer = ({
editable,
canAddLanes,
laneStyle,
onCardMoveAcrossLanes,
orientation,
eventBusHandle,
reducerData,
cardStyle,
...otherProps
},
[
// "onCardMoveAcrossLanes",
"onLaneScroll",
"onLaneDelete",
"onLaneUpdate",
"onCardClick",
"onBeforeCardDelete",
"onCardDelete",
"onCardAdd",
"onCardUpdate",
"onLaneClick",
"laneSortFunction",
"draggable",
"cardDraggable",
"collapsibleLanes",
"canAddLanes",
"hideCardDeleteIcon",
"tagStyle",
"handleDragStart",
"handleDragEnd",
"cardDragClass",
"editLaneTitle",
"orientation"
otherProps
]
);
const onLaneDrag = async ({ draggableId, type, source, reason, mode, destination, combine }) => {
setIsDragging(false);
const onLaneDrag = useCallback(
async ({ draggableId, type, source, reason, mode, destination, combine }) => {
setIsDragging(false);
if (!type || type !== "lane" || !source || !destination) return;
if (!type || type !== "lane" || !source || !destination) return;
setIsProcessing(true);
setIsProcessing(true);
dispatch(
actions.moveCardAcrossLanes({
fromLaneId: source.droppableId,
toLaneId: destination.droppableId,
cardId: draggableId,
index: destination.index
})
);
dispatch(
actions.moveCardAcrossLanes({
fromLaneId: source.droppableId,
toLaneId: destination.droppableId,
cardId: draggableId,
index: destination.index
})
);
onDragEnd({ draggableId, type, source, reason, mode, destination, combine })
.catch((err) => {
try {
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
} catch (err) {
console.error("Error in onLaneDrag", err);
})
.finally(() => {
} finally {
setIsProcessing(false);
});
};
}
},
[dispatch, onDragEnd]
);
return (
<components.BoardWrapper style={style} orientation={orientation} draggable={false}>
<components.BoardWrapper style={style} orientation={orientation}>
<PopoverWrapper>
<DragDropContext
onDragEnd={onLaneDrag}
@@ -346,7 +348,6 @@ BoardContainer.propTypes = {
tagStyle: PropTypes.object,
cardDraggable: PropTypes.bool,
cardDragClass: PropTypes.string,
onCardMoveAcrossLanes: PropTypes.func,
orientation: PropTypes.string,
cardStyle: PropTypes.object
};

View File

@@ -1,9 +1,8 @@
import React, { forwardRef, useCallback, useEffect, useRef, useState } from "react";
import React, { forwardRef, useCallback, useMemo, useState, useEffect } 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 { v1 } from "uuid";
import * as actions from "../../../redux/trello/trello.actions.js";
@@ -17,7 +16,6 @@ const Lane = ({
boardId,
title,
index,
isDragging,
isProcessing,
laneSortFunction,
style = {},
@@ -54,52 +52,15 @@ const Lane = ({
currentPage,
...otherProps
}) => {
const [loading, setLoading] = useState(false);
const [currentPageFinal, setCurrentPageFinal] = useState(currentPage);
const [addCardMode, setAddCardMode] = useState(false);
const [collapsed, setCollapsed] = useState(false);
const laneRef = useRef(null);
const [isVisible, setIsVisible] = useState(true);
useEffect(() => {
if (!isEqual(cards, currentPageFinal)) {
setCurrentPageFinal(currentPage);
}
}, [cards, currentPage, currentPageFinal]);
const handleScroll = useCallback(
(evt) => {
const node = evt.target;
const elemScrollPosition = node.scrollHeight - node.scrollTop - node.clientHeight;
if (elemScrollPosition < 1 && onLaneScroll && !loading) {
const nextPage = currentPageFinal + 1;
setLoading(true);
onLaneScroll(nextPage, id).then((moreCards) => {
if ((moreCards || []).length > 0) {
actions.paginateLane({
laneId: id,
newCards: moreCards,
nextPage: nextPage
});
}
setLoading(false);
});
}
},
[currentPageFinal, loading, onLaneScroll, id, actions]
);
useEffect(() => {
const node = laneRef.current;
if (node) {
node.addEventListener("scroll", handleScroll);
}
return () => {
if (node) {
node.removeEventListener("scroll", handleScroll);
}
};
}, [handleScroll]);
setIsVisible(false);
const timer = setTimeout(() => setIsVisible(true), 0);
return () => clearTimeout(timer);
}, [cards.length]);
const sortCards = useCallback((cards, sortFunction) => {
if (!cards) return [];
@@ -177,7 +138,6 @@ const Lane = ({
const Card = React.memo(({ provided, item: card, isDragging }) => {
const onDeleteCard = () => removeCard(card.id);
return (
<div
{...provided.draggableProps}
@@ -204,12 +164,6 @@ const Lane = ({
);
});
/**
* Render the draggable component
* @param index
* @param item
* @returns {React.JSX.Element}
*/
const renderDraggable = (index, item) => {
if (!item) {
console.log("null Item");
@@ -217,18 +171,18 @@ const Lane = ({
}
return (
<Draggable draggableId={item.id} index={index} key={item.id}>
<Draggable draggableId={item.id} index={index} key={item.id} isDragDisabled={isProcessing}>
{(provided, snapshot) => <Card provided={provided} item={item} isDragging={snapshot.isDragging} />}
</Draggable>
);
};
const renderAddCardLink = useCallback(
const renderAddCardLink = useMemo(
() => editable && !addCardMode && <components.AddCardLink onClick={showEditableCard} laneId={id} />,
[editable, addCardMode, showEditableCard, id]
);
const renderNewCardForm = useCallback(
const renderNewCardForm = useMemo(
() => addCardMode && <components.NewCardForm onCancel={hideEditableCard} laneId={id} onAdd={addNewCard} />,
[addCardMode, hideEditableCard, addNewCard, id]
);
@@ -246,12 +200,6 @@ const Lane = ({
</div>
);
/**
* Render the droppable component with the provided cards and the provided props from react-beautiful-dnd
* @param provided
* @param renderedCards
* @returns {Element}
*/
const renderDroppable = (provided, renderedCards) => {
const Component = orientation === "vertical" ? VirtuosoGrid : Virtuoso;
const FinalComponent = collapsed ? "div" : Component;
@@ -286,11 +234,8 @@ const Lane = ({
<div
{...props}
style={{
width: 152, // This is required and is pegged to .react-trello-card (Vertical)=
display: "flex",
flex: "none",
alignContent: "stretch",
boxSizing: "border-box"
width: 152,
display: "flex"
}}
>
{children}
@@ -306,23 +251,22 @@ const Lane = ({
reverse: 22
},
components: { Item: HeightPreservingItem },
itemContent: (index, item) => renderDraggable(index, item),
scrollerRef: provided.innerRef
itemContent: (index, item) => renderDraggable(index, item)
};
const finalComponentProps = collapsed ? {} : componentProps;
return (
<div>
<div style={{ height: "100%" }}>
<div
{...provided.droppableProps}
ref={provided.innerRef}
className={allClassNames}
style={{ ...provided.droppableProps.style }}
>
<FinalComponent {...finalComponentProps} />
{/*{provided.placeholder}*/}
{isVisible && <FinalComponent {...finalComponentProps} />}
{(orientation === "horizontal" || renderedCards.length === 0 || collapsed) && provided.placeholder}
</div>
{renderAddCardLink()}
{renderNewCardForm()}
{renderAddCardLink}
{renderNewCardForm}
</div>
);
};
@@ -358,7 +302,6 @@ const Lane = ({
};
const allClassNames = classNames("react-trello-lane", collapsed ? "lane-collapsed" : "", className || "");
const showFooter = collapsibleLanes && cards.length > 0;
const passedProps = {
actions,
@@ -392,7 +335,6 @@ const Lane = ({
currentPage,
...otherProps
};
return (
<components.Section
key={id}
@@ -402,8 +344,7 @@ const Lane = ({
>
{renderHeader({ id, cards, ...passedProps })}
{renderDragContainer()}
{loading && <components.Loader />}
{showFooter && <components.LaneFooter onClick={toggleLaneCollapsed} collapsed={collapsed} />}
{collapsibleLanes && <components.LaneFooter onClick={toggleLaneCollapsed} collapsed={collapsed} />}
</components.Section>
);
};

View File

@@ -21,7 +21,7 @@ const getSectionStyles = (props) => {
display: inline-flex;
flex-direction: column;
white-space: nowrap;
overflow-y: none;
// overflow-y: none;
`;
}
return `
@@ -75,7 +75,6 @@ export const StyleHorizontal = styled.div`
.react-trello-lane {
min-width: 250px;
min-height: 25px;
margin-bottom: 10px;
}
.react-trello-lane.lane-collapsed {
@@ -84,6 +83,9 @@ export const StyleHorizontal = styled.div`
.ant-card-body {
padding: 4px;
}
.react-trello-card {
height: auto;
}
`;
export const StyleVertical = styled.div`
@@ -167,6 +169,7 @@ export const CustomPopoverContent = styled(PopoverContent)`
export const BoardWrapper = styled.div`
color: #393939;
height: 100%;
overflow-x: auto;
overflow-y: hidden;
${getBoardWrapperStyles};
@@ -183,7 +186,7 @@ export const Section = styled.section`
background-color: #e3e3e3;
border-radius: 3px;
margin: 2px 2px;
padding: 2px;
height: 100%;
${getSectionStyles};
`;
@@ -319,7 +322,7 @@ export const LaneSection = styled.section`
position: relative;
padding: 5px;
display: inline-flex;
height: auto;
height: 100%;
flex-direction: column;
`;