feature/IO-2681-Share-To-Teams-Button - checkpoint

This commit is contained in:
Dave Richer
2025-01-29 18:19:34 -05:00
parent 71a74c5437
commit a064b8e07e
10 changed files with 223 additions and 31 deletions

View File

@@ -31,6 +31,7 @@ import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
import ShareToTeamsButton from "../share-to-teams/share-to-teams-button.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -130,10 +131,10 @@ export function JobsDetailHeaderActions({
const { socket } = useContext(SocketContext);
const {
treatments: { ImEXPay }
treatments: { ImEXPay, Share_To_Teams }
} = useSplitTreatments({
attributes: {},
names: ["ImEXPay"],
names: ["ImEXPay", "Share_To_Teams"],
splitKey: bodyshop && bodyshop.imexshopid
});
@@ -962,6 +963,16 @@ export function JobsDetailHeaderActions({
}
);
// TODO REMEMBER TO REVERT
// if (Share_To_Teams?.treatment === "on") {
if (true) {
menuItems.push({
key: "sharetoteams",
id: "job-actions-sharetoteams",
label: <ShareToTeamsButton noIcon={true} urlOverride={`${window.location.origin}${window.location.pathname}`} />
});
}
menuItems.push({
key: "exportcustdata",
id: "job-actions-exportcustdata",

View File

@@ -19,6 +19,7 @@ import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
import PrintWrapper from "../print-wrapper/print-wrapper.component";
import PartsOrderDrawer from "./parts-order-list-table-drawer.component";
import ShareToTeamsButton from "../share-to-teams/share-to-teams-button.jsx";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -66,19 +67,21 @@ export function PartsOrderListTableComponent({
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
const { refetch } = billsQuery;
// label: <ShareToTeamsButton noIcon={true} urlOverride={`${window.location.origin}${window.location.pathname}`} />
const recordActions = (record, showView = false) => (
<Space direction="horizontal" wrap>
<ShareToTeamsButton
linkText={""}
urlOverride={`${window.location.origin}/manage/jobs/${job.id}?partsorderid=${record.id}&tab=partssublet `}
/>
{showView && (
<Button
icon={<EyeFilled />}
onClick={() => {
handleOnRowClick(record);
}}
>
<EyeFilled />
</Button>
/>
)}
<Button
disabled={jobRO || record.return || record.vendor.id === bodyshop.inhousevendorid}
onClick={() => {
@@ -106,6 +109,7 @@ export function PartsOrderListTableComponent({
</Button>
<Button
title={t("tasks.buttons.create")}
icon={<FaTasks />}
onClick={() => {
setTaskUpsertContext({
context: {
@@ -114,9 +118,7 @@ export function PartsOrderListTableComponent({
}
});
}}
>
<FaTasks />
</Button>
/>
<Popconfirm
title={t("parts_orders.labels.confirmdelete")}
disabled={jobRO}
@@ -137,9 +139,7 @@ export function PartsOrderListTableComponent({
});
}}
>
<Button disabled={jobRO}>
<DeleteFilled />
</Button>
<Button disabled={jobRO} icon={<DeleteFilled />} />
</Popconfirm>
<Button

View File

@@ -20,6 +20,8 @@ import dayjs from "../../utils/day";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ShareToTeamsButton from "../share-to-teams/share-to-teams-button.jsx";
import { SiMicrosoftteams } from "react-icons/si";
const cardColor = (ssbuckets, totalHrs) => {
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
@@ -417,9 +419,20 @@ export default function ProductionBoardCard({ technician, card, bodyshop, cardSe
title={!isBodyEmpty ? headerContent : null}
extra={
!isBodyEmpty && (
<Link to={{ search: `?selected=${card.id}` }}>
<EyeFilled />
</Link>
<Space>
<ShareToTeamsButton
noIcon={true}
linkText={
<div className="share-to-teams-badge">
<SiMicrosoftteams />
</div>
}
urlOverride={`${window.location.origin}/manage/jobs/${card.id}`}
/>
<Link to={{ search: `?selected=${card.id}` }}>
<EyeFilled />
</Link>
</Space>
)
}
>

View File

@@ -10,6 +10,16 @@
.height-preserving-container {
}
.share-to-teams-badge {
background-color: #cccccc;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.react-trello-column-header {
font-weight: bold;
cursor: pointer;

View File

@@ -27,6 +27,7 @@ import ProductionListColumnNote from "./production-list-columns.productionnote.c
import ProductionListColumnCategory from "./production-list-columns.status.category";
import ProductionListColumnStatus from "./production-list-columns.status.component";
import ProductionListColumnTouchTime from "./prodution-list-columns.touchtime.component";
import ShareToTeamsButton from "../share-to-teams/share-to-teams-button.jsx";
const getEmployeeName = (employeeId, employees) => {
const employee = employees.find((e) => e.id === employeeId);
@@ -41,7 +42,17 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
dataIndex: "viewdetail",
key: "viewdetail",
ellipsis: true,
render: (text, record) => <Link to={{ search: `?selected=${record.id}` }}>{i18n.t("general.labels.view")}</Link>
render: (text, record) => (
<Space>
<Link to={{ search: `?selected=${record.id}` }}>{i18n.t("general.labels.view")}</Link>
<ShareToTeamsButton
noIcon={true}
linkText={"Share"}
noIconStyle={{ color: "#1890ff" }}
urlOverride={`${window.location.origin}/manage/jobs/${record.id}`}
/>
</Space>
)
},
...(Enhanced_Payroll.treatment === "on"
? [

View File

@@ -0,0 +1,135 @@
import React, { useMemo } from "react";
import PropTypes from "prop-types";
import { Button } from "antd";
import { useLocation } from "react-router-dom";
import { SiMicrosoftteams } from "react-icons/si";
import { useTranslation } from "react-i18next";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
/**
* ShareToTeamsButton component for sharing content to Microsoft Teams via an HTTP link.
*
* This component creates a button or link that opens the Microsoft Teams share dialog with
* the provided URL, title, and message text through query parameters. The popup window is centered on the screen.
*
* @param {Object} props - The component's props.
* @param {string} [props.messageTextOverride] - Custom message text for sharing.
* @param {string} [props.urlOverride] - Custom URL to share instead of the current page's URL.
* @param {string} [props.pageTitleOverride] - Custom title for the shared page.
* @param {boolean} [props.noIcon=false] - If true, renders as a simple text link instead of a button with an icon.
* @param {Object} [props.noIconStyle={}] - Style object for the text link when noIcon is true.
* @param {Object} [props.buttonStyle={}] - Style object for the Ant Design button.
* @param {Object} [props.buttonIconStyle={}] - Style object for the icon within the button.
* @param {string} [props.linkText] - Text to display on the button or link.
* @param {Object} [props.additionalProps] - Additional props to pass to the rendered component.
* @returns {React.ReactElement} A button or text link for sharing to Microsoft Teams.
*/
const ShareToTeamsButton = ({
bodyshop,
messageTextOverride,
urlOverride,
pageTitleOverride,
noIcon = false,
noIconStyle = {},
buttonStyle = {},
buttonIconStyle = {},
linkText,
...additionalProps
}) => {
const location = useLocation();
const { t } = useTranslation();
const {
treatments: { Share_To_Teams }
} = useSplitTreatments({
attributes: {},
names: ["Share_To_Teams"],
splitKey: bodyshop && bodyshop.imexshopid
});
const currentUrl = useMemo(
() =>
urlOverride ||
encodeURIComponent(`${window.location.origin}${location.pathname}${location.search}${location.hash}`),
[urlOverride, location]
);
const pageTitle = useMemo(
() =>
pageTitleOverride ||
encodeURIComponent(typeof document !== "undefined" ? document.title : t("general.actions.sharetoteams")),
[pageTitleOverride, t]
);
const messageText = useMemo(
() => messageTextOverride || encodeURIComponent(t("general.actions.sharetoteams")),
[messageTextOverride, t]
);
// Construct the Teams share URL with parameters
const teamsShareUrl = `https://teams.microsoft.com/share?href=${currentUrl}&preText=${messageText}&title=${pageTitle}`;
// Function to open the centered share link in a new window/tab
const handleShare = () => {
const screenWidth = window.screen.width;
const screenHeight = window.screen.height;
const windowWidth = 600;
const windowHeight = 400;
const left = screenWidth / 2 - windowWidth / 2;
const top = screenHeight / 2 - windowHeight / 2;
const windowFeatures = `width=${windowWidth},height=${windowHeight},left=${left},top=${top}`;
// noinspection JSIgnoredPromiseFromCall
window.open(teamsShareUrl, "_blank", windowFeatures);
};
// if (Share_To_Teams?.treatment !== "on") {
// return null;
// }
if (noIcon) {
return (
<div style={{ cursor: "pointer", ...noIconStyle }} onClick={handleShare} {...additionalProps}>
{!linkText ? t("general.actions.sharetoteams") : linkText}
</div>
);
}
return (
<Button
style={{
backgroundColor: "#6264A7",
borderColor: "#6264A7",
color: "#FFFFFF",
...buttonStyle
}}
icon={<SiMicrosoftteams style={{ color: "#FFFFFF", ...buttonIconStyle }} />}
onClick={handleShare}
title={linkText === null ? t("general.actions.sharetoteams") : linkText}
{...additionalProps}
/>
);
};
ShareToTeamsButton.propTypes = {
messageTextOverride: PropTypes.string,
urlOverride: PropTypes.string,
pageTitleOverride: PropTypes.string,
noIcon: PropTypes.bool,
noIconStyle: PropTypes.object,
buttonStyle: PropTypes.object,
buttonIconStyle: PropTypes.object,
linkText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
additionalProps: PropTypes.object
};
export default connect(mapStateToProps)(ShareToTeamsButton);

View File

@@ -18,6 +18,7 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import { pageLimit } from "../../utils/config";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter.jsx";
import dayjs from "../../utils/day";
import ShareToTeamsButton from "../share-to-teams/share-to-teams-button.jsx";
/**
* Task List Component
@@ -266,8 +267,13 @@ function TaskListComponent({
width: "8%",
render: (text, record) => (
<Space direction="horizontal">
<ShareToTeamsButton
linkText=""
urlOverride={`https://localhost:3000/manage/tasks/alltasks?taskid=${record.id}`}
/>
<Button
title={t("tasks.buttons.edit")}
icon={<EditFilled />}
onClick={() => {
setTaskUpsertContext({
context: {
@@ -276,18 +282,18 @@ function TaskListComponent({
}
});
}}
>
<EditFilled />
</Button>
/>
<Button
title={t("tasks.buttons.complete")}
onClick={() => toggleCompletedStatus(record.id, record.completed)}
>
{record.completed ? <CheckCircleOutlined /> : <CheckCircleFilled />}
</Button>
<Button title={t("tasks.buttons.delete")} onClick={() => toggleDeletedStatus(record.id, record.deleted)}>
{record.deleted ? <DeleteOutlined /> : <DeleteFilled />}
</Button>
icon={record.completed ? <CheckCircleOutlined /> : <CheckCircleFilled />}
/>
<Button
title={t("tasks.buttons.delete")}
onClick={() => toggleDeletedStatus(record.id, record.deleted)}
icon={record.deleted ? <DeleteOutlined /> : <DeleteFilled />}
/>
</Space>
)
}

View File

@@ -34,7 +34,7 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
const [insertTask] = useMutation(MUTATION_INSERT_NEW_TASK);
const [updateTask] = useMutation(MUTATION_UPDATE_TASK);
const { open, context } = taskUpsert;
const { jobid, joblineid, billid, partsorderid, taskId, existingTask, query } = context;
const { jobid, joblineid, billid, partsorderid, taskId, existingTask, query, view } = context;
const [form] = Form.useForm();
const [selectedJobId, setSelectedJobId] = useState(null);
const [selectedJobDetails, setSelectedJobDetails] = useState(null);
@@ -255,10 +255,11 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
return (
<Modal
title={existingTask ? t("tasks.actions.edit") : t("tasks.actions.new")}
title={view ? t("tasks.actions.view") : existingTask ? t("tasks.actions.edit") : t("tasks.actions.new")}
open={open}
okText={t("general.actions.save")}
width="50%"
cancelText={!isTouched ? t("general.actions.ok") : t("general.actions.cancel")}
onOk={() => {
removeTaskIdFromUrl();
form.submit();
@@ -283,6 +284,7 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
loading={loading || (taskId && taskLoading)}
error={error}
data={data}
view={view}
existingTask={existingTask || taskData?.tasks_by_pk}
selectedJobId={selectedJobId}
setSelectedJobId={setSelectedJobId}

View File

@@ -46,7 +46,8 @@ export function AllTasksPageContainer({ setBreadcrumbs, setSelectedHeader, setTa
if (taskId) {
setTaskUpsertContext({
context: {
taskId
taskId,
view: true
}
});
urlParams.delete("taskid");

View File

@@ -1193,7 +1193,9 @@
"submit": "Submit",
"tryagain": "Try Again",
"view": "View",
"viewreleasenotes": "See What's Changed"
"viewreleasenotes": "See What's Changed",
"sharetoteams": "Share to Teams",
"ok": "Ok"
},
"errors": {
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
@@ -3182,7 +3184,8 @@
"tasks": {
"actions": {
"edit": "Edit Task",
"new": "New Task"
"new": "New Task",
"view": "View Task"
},
"buttons": {
"allTasks": "All",