Files
bodyshop/client/src/components/job-watcher-toggle/job-watcher-toggle.container.jsx

226 lines
7.2 KiB
JavaScript

import { useCallback, useEffect, useMemo, useState } from "react";
import { useMutation, useQuery } from "@apollo/client/react";
import { ADD_JOB_WATCHER, GET_JOB_WATCHERS, REMOVE_JOB_WATCHER } from "../../graphql/jobs.queries.js";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement";
import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx";
import { useIsEmployee } from "../../utils/useIsEmployee.js";
const EMPTY_ARRAY = Object.freeze([]);
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
currentUser: selectCurrentUser
});
function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
const {
treatments: { Enhanced_Payroll }
} = useTreatmentsWithConfig({
attributes: {},
names: ["Enhanced_Payroll"],
splitKey: bodyshop && bodyshop.imexshopid
});
const isEmployee = useIsEmployee(bodyshop, currentUser);
const [open, setOpen] = useState(false);
const [selectedWatcher, setSelectedWatcher] = useState(null);
const [selectedTeam, setSelectedTeam] = useState(null);
const userEmail = currentUser?.email;
const jobid = job?.id;
const watcherVars = useMemo(() => ({ jobid }), [jobid]);
const {
data: watcherData,
loading: watcherLoading,
refetch
} = useQuery(GET_JOB_WATCHERS, {
variables: watcherVars,
skip: !jobid,
fetchPolicy: "cache-first",
notifiyOnNetworkStatusChange: true
});
// Refetch jobWatchers when the popover opens
useEffect(() => {
if (!jobid) return;
if (open) {
refetch().catch((err) =>
console.error(`Something went wrong fetching Notification Watchers on popover open: ${err?.message}`, {
stack: err?.stack
})
);
}
}, [open, refetch, jobid]);
// Do NOT clone arrays; keep referential stability for React Compiler and to reduce rerenders.
const jobWatchers = watcherData?.job_watchers ?? EMPTY_ARRAY;
const watcherEmailSet = useMemo(
() => new Set((jobWatchers ?? EMPTY_ARRAY).map((w) => w?.user_email).filter(Boolean)),
[jobWatchers]
);
const isWatching = !!userEmail && watcherEmailSet.has(userEmail);
const [addWatcher, { loading: adding }] = useMutation(ADD_JOB_WATCHER, {
onError: (err) => {
if (err.graphQLErrors && err.graphQLErrors.length > 0) {
const errorMessage = err.graphQLErrors[0].message;
if (
errorMessage.includes("Uniqueness violation") ||
errorMessage.includes("idx_job_watchers_jobid_user_email_unique")
) {
console.warn("Watcher already exists for this job and user.");
// Only refetch for this edge case to ensure UI is accurate
refetch().catch((e) =>
console.error(
`Something went wrong fetching Notification Watchers after uniqueness violation: ${e?.message}`,
{ stack: e?.stack }
)
);
} else {
console.error(`Error adding job watcher: ${errorMessage}`);
}
} else {
console.error(`Unexpected error adding job watcher: ${err.message || JSON.stringify(err)}`);
}
},
update(cache, { data }) {
const inserted = data?.insert_job_watchers_one;
if (!inserted) return;
cache.updateQuery({ query: GET_JOB_WATCHERS, variables: watcherVars }, (existing) => {
const prev = existing?.job_watchers ?? [];
if (prev.some((w) => w.user_email === inserted.user_email)) return existing;
return {
...existing,
job_watchers: [...prev, inserted]
};
});
}
});
const [removeWatcher, { loading: removing }] = useMutation(REMOVE_JOB_WATCHER, {
onError: (err) => console.error(`Error removing job watcher: ${err.message}`),
update(cache, { data }) {
const deleted = data?.delete_job_watchers?.returning?.[0];
if (!deleted?.user_email) return;
cache.updateQuery({ query: GET_JOB_WATCHERS, variables: watcherVars }, (existing) => {
const prev = existing?.job_watchers ?? [];
return {
...existing,
job_watchers: prev.filter((w) => w.user_email !== deleted.user_email)
};
});
}
});
const handleToggleSelf = useCallback(async () => {
if (!jobid || !userEmail) return;
if (adding || removing || !isEmployee) return;
if (isWatching) {
await removeWatcher({ variables: { jobid, userEmail } });
} else {
await addWatcher({ variables: { jobid, userEmail } });
}
}, [isWatching, addWatcher, removeWatcher, jobid, userEmail, adding, removing, isEmployee]);
const handleRemoveWatcher = useCallback(
async (email) => {
if (!jobid) return;
if (removing) return;
if (!email) return;
await removeWatcher({ variables: { jobid, userEmail: email } });
},
[removeWatcher, jobid, removing]
);
const handleWatcherSelect = useCallback(
async (selectedUser) => {
if (!jobid) return;
if (adding || removing) return;
const employee = bodyshop?.employees?.find((e) => e.id === selectedUser);
if (!employee?.user_email) return;
const email = employee.user_email;
const isAlreadyWatching = watcherEmailSet.has(email);
if (isAlreadyWatching) {
await handleRemoveWatcher(email);
} else {
await addWatcher({ variables: { jobid, userEmail: email } });
}
setSelectedWatcher(null);
},
[watcherEmailSet, addWatcher, handleRemoveWatcher, jobid, bodyshop, adding, removing]
);
const handleTeamSelect = useCallback(
async (team) => {
if (!jobid) return;
if (adding) return;
let selectedTeamMembers = [];
try {
selectedTeamMembers = Array.isArray(team) ? team : JSON.parse(team);
} catch {
selectedTeamMembers = [];
}
const newWatchers = (selectedTeamMembers ?? []).filter(Boolean).filter((email) => !watcherEmailSet.has(email));
if (newWatchers.length === 0) {
console.warn("All selected team members are already watchers.");
setSelectedTeam(null);
return;
}
await Promise.all(
newWatchers.map((email) =>
addWatcher({
variables: {
jobid,
userEmail: email
}
})
)
);
setSelectedTeam(null);
},
[watcherEmailSet, addWatcher, jobid, adding]
);
return (
<JobWatcherToggleComponent
jobWatchers={jobWatchers}
isWatching={isWatching}
watcherLoading={watcherLoading}
adding={adding}
removing={removing}
open={open}
setOpen={setOpen}
selectedWatcher={selectedWatcher}
setSelectedWatcher={setSelectedWatcher}
selectedTeam={selectedTeam}
setSelectedTeam={setSelectedTeam}
bodyshop={bodyshop}
Enhanced_Payroll={Enhanced_Payroll}
handleToggleSelf={handleToggleSelf}
handleRemoveWatcher={handleRemoveWatcher}
handleWatcherSelect={handleWatcherSelect}
handleTeamSelect={handleTeamSelect}
currentUser={currentUser}
isEmployee={isEmployee}
/>
);
}
export default connect(mapStateToProps)(JobWatcherToggleContainer);