Skip to content

Commit

Permalink
Add active/paused toggle (#535)
Browse files Browse the repository at this point in the history
* Add active/paused toggle

* not active if start is nil

* use not is_nil()

* Default to paused as false when creating a new pa message

* PR feedabck

* fix toast

* Change active to current internally

* test

* reuse existing query

* Add better response handling for updating existing message
  • Loading branch information
PaulJKim authored Oct 29, 2024
1 parent a7fa4e0 commit a26b200
Show file tree
Hide file tree
Showing 12 changed files with 230 additions and 34 deletions.
17 changes: 17 additions & 0 deletions assets/css/dashboard/new-pa-message/new-pa-message.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,23 @@
font-weight: 500;
line-height: 48px;
text-align: left;
display: flex;
}

.paused-pill {
padding: 8px 16px;
margin-left: 16px;
background-color: $theme-secondary;
border-radius: 200px;
height: 40px;
align-self: center;
font-size: 24px;
line-height: 24px;
text-align: center;

text {
position: relative;
}
}

.cancel-button {
Expand Down
36 changes: 36 additions & 0 deletions assets/css/dashboard/pa-messages-page.scss
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,40 @@
align-items: center;
margin-top: 48px;
}

.pause-active-switch-container {
position: relative;

.pause-active-switch {
margin-bottom: 8px;
width: 86px;

.form-check-input {
width: 86px;
height: 25px;
}

.form-check-input:not(:checked) {
background-color: $dark-bg;
border-color: $border-primary;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba(255,255,255,.25)'/></svg>");
}
}

.switch-text {
font-size: 14px;
line-height: 21px;
position: absolute;
top: 2px;
}

.paused {
left: 28px;
}

.active {
color: black;
left: 8px;
}
}
}
18 changes: 10 additions & 8 deletions assets/js/components/Dashboard/EditPaMessage/EditPaMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,19 @@ const EditPaMessage = ({ paMessage, alert }: Props) => {
defaultValues={paMessage}
defaultAlert={alert ?? paMessage.alert_id}
defaultAudioState={AudioPreview.Reviewed}
paused={paMessage.paused}
onSubmit={async (data) => {
const result = await updateExistingPaMessage(paMessage.id, data);

if (result.status === 200) {
try {
await updateExistingPaMessage(paMessage.id, data);
mutate(`/api/pa-messages/${paMessage.id}`);
navigate("/pa-messages");
} else if (result.status === 422) {
setErrorMessage("Correct the following errors:");
setErrors(Object.keys(result.body.errors));
} else {
setErrorMessage("Something went wrong. Please try again.");
} catch (error) {
if (Array.isArray(error)) {
setErrorMessage("Correct the following errors:");
setErrors(error);
} else {
setErrorMessage((error as Error).message);
}
}
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const NewPaMessage = () => {
errorMessage={errorMessage}
onError={setErrorMessage}
onErrorsChange={setErrors}
paused={false}
onSubmit={async (formData) => {
const { status, errors } = await createNewPaMessage(formData);

Expand Down
8 changes: 7 additions & 1 deletion assets/js/components/Dashboard/PaMessageForm/MainForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ interface Props {
setAudioState: Dispatch<SetStateAction<AudioPreview>>;
audioState: AudioPreview;
hide: boolean;
paused: boolean;
}

const MainForm = ({
Expand Down Expand Up @@ -85,6 +86,7 @@ const MainForm = ({
hide,
audioState,
setAudioState,
paused,
}: Props) => {
const navigate = useNavigate();
const [validated, setValidated] = useState(false);
Expand Down Expand Up @@ -140,7 +142,11 @@ const MainForm = ({
return (
<div className="new-pa-message-page">
<Form onSubmit={handleSubmit} noValidate>
<div className="header">{title}</div>
<div className="header">
{title}
{paused && <div className="paused-pill">Paused</div>}
</div>

<Container fluid>
<NewPaMessageHeader
associatedAlert={associatedAlert}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface Props {
defaultValues?: Partial<PaMessageFormData>;
defaultAlert?: Alert | string | null;
defaultAudioState?: AudioPreview;
paused: boolean;
}

const PaMessageForm = ({
Expand All @@ -44,6 +45,7 @@ const PaMessageForm = ({
defaultValues,
defaultAlert,
defaultAudioState,
paused,
}: Props) => {
const [page, setPage] = useState<Page>(Page.MAIN);
const now = moment();
Expand Down Expand Up @@ -191,6 +193,7 @@ const PaMessageForm = ({
busRoutes,
audioState,
setAudioState,
paused,
}}
/>
{[Page.STATIONS, Page.ZONES].includes(page) && (
Expand Down
108 changes: 90 additions & 18 deletions assets/js/components/Dashboard/PaMessagesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ import {
ButtonGroup,
Button,
Spinner,
FormCheck,
} from "react-bootstrap";
import { BoxArrowUpRight, PlusCircleFill } from "react-bootstrap-icons";
import { PaMessage } from "Models/pa_message";
import { Link, useNavigate, useSearchParams } from "react-router-dom";
import cx from "classnames";
import useSWR from "swr";
import useSWR, { mutate } from "swr";
import { useRouteToRouteIDsMap } from "Hooks/useRouteToRouteIDsMap";
import { updateExistingPaMessage } from "Utils/api";
import { UpdatePaMessageBody } from "Models/pa_message";
import Toast from "Components/Toast";

type StateFilter = "active" | "future" | "past";
type StateFilter = "current" | "future" | "past";

type ServiceType =
| "Green"
Expand Down Expand Up @@ -94,7 +98,7 @@ const useDelayedLoadingState = (value: boolean, delay = 250) => {
const PaMessagesPage: ComponentType = () => {
const [params, setParams] = useSearchParams();
const [stateFilter, setStateFilter] = useState<StateFilter>(
() => (params.get("state") as StateFilter) ?? "active",
() => (params.get("state") as StateFilter) ?? "current",
);
const [serviceTypes, setServiceTypes] = useState<Array<ServiceType>>(
() => (params.getAll("serviceTypes[]") as Array<ServiceType>) ?? [],
Expand All @@ -115,6 +119,20 @@ const PaMessagesPage: ComponentType = () => {
const { data, isLoading } = usePaMessages({ serviceTypes, stateFilter });
const shouldShowLoadingState = useDelayedLoadingState(isLoading);

const updatePaMessage = (updatedPaMessage: PaMessage) => {
if (!data) return;

const updatedData = data.map((paMessage) => {
return paMessage.id === updatedPaMessage.id
? updatedPaMessage
: paMessage;
});

mutate(`/api/pa-messages?${params.toString()}`, updatedData, false);
};

const [errorMessage, setErrorMessage] = useState<string | null>(null);

return (
<>
<div className="pa-message-page-header">PA/ESS Messages</div>
Expand All @@ -138,10 +156,12 @@ const PaMessagesPage: ComponentType = () => {
<header>Filter by message state</header>
<ButtonGroup className="button-group" vertical>
<Button
className={cx("button", { active: stateFilter === "active" })}
onClick={() => setStateFilter("active")}
className={cx("button", {
active: stateFilter === "current",
})}
onClick={() => setStateFilter("current")}
>
Active
Now
</Button>
<Button
className={cx("button", { active: stateFilter === "future" })}
Expand Down Expand Up @@ -188,23 +208,39 @@ const PaMessagesPage: ComponentType = () => {
<PaMessageTable
paMessages={data ?? []}
isLoading={shouldShowLoadingState}
stateFilter={stateFilter}
updatePaMessage={updatePaMessage}
setErrorMessage={setErrorMessage}
/>
</Row>
</Col>
</Row>
</Container>
<Toast
variant="warning"
message={errorMessage}
onClose={() => {
setErrorMessage(null);
}}
/>
</>
);
};

interface PaMessageTableProps {
paMessages: PaMessage[];
isLoading: boolean;
stateFilter: StateFilter;
updatePaMessage: (paMessage: PaMessage) => void;
setErrorMessage: (message: string | null) => void;
}

const PaMessageTable: ComponentType<PaMessageTableProps> = ({
paMessages,
isLoading,
stateFilter,
updatePaMessage,
setErrorMessage,
}: PaMessageTableProps) => {
const data = isLoading ? [] : paMessages;

Expand All @@ -216,11 +252,20 @@ const PaMessageTable: ComponentType<PaMessageTableProps> = ({
<th>Message</th>
<th>Interval</th>
<th className="pa-message-table__start-end">Start-End</th>
{stateFilter == "current" && <th>Actions</th>}
</tr>
</thead>
<tbody>
{data.map((paMessage: PaMessage) => {
return <PaMessageRow key={paMessage.id} paMessage={paMessage} />;
return (
<PaMessageRow
key={paMessage.id}
paMessage={paMessage}
stateFilter={stateFilter}
updatePaMessage={updatePaMessage}
setErrorMessage={setErrorMessage}
/>
);
})}
</tbody>
</table>
Expand All @@ -243,16 +288,34 @@ const PaMessageTable: ComponentType<PaMessageTableProps> = ({

interface PaMessageRowProps {
paMessage: PaMessage;
stateFilter: StateFilter;
updatePaMessage: (paMessage: PaMessage) => void;
setErrorMessage: (message: string | null) => void;
}

const PaMessageRow: ComponentType<PaMessageRowProps> = ({
paMessage,
stateFilter,
updatePaMessage,
setErrorMessage,
}: PaMessageRowProps) => {
const navigate = useNavigate();
const start = new Date(paMessage.start_datetime);
const end =
paMessage.end_datetime === null ? null : new Date(paMessage.end_datetime);

const togglePaused = async (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
try {
await updateExistingPaMessage(paMessage.id, {
paused: !paMessage.paused,
} as UpdatePaMessageBody);
updatePaMessage({ ...paMessage, paused: !paMessage.paused });
} catch (error) {
setErrorMessage((error as Error).message);
}
};

return (
<tr onClick={() => navigate(`/pa-messages/${paMessage.id}/edit`)}>
<td>{paMessage.visual_text}</td>
Expand All @@ -262,17 +325,26 @@ const PaMessageRow: ComponentType<PaMessageRowProps> = ({
<br />
{end && end.toLocaleString().replace(",", "")}
</td>
{/* <td>
<FormCheck />
</td>
<td className="pa-message-table__actions">
<a href="/pa-messages">
<u>Pause</u>
</a>
<a href="/pa-messages">
<u>Copy</u>
</a>
</td> */}
{stateFilter == "current" && (
<td>
<div className="pause-active-switch-container" onClick={togglePaused}>
<FormCheck
className={"pause-active-switch"}
type="switch"
checked={!paMessage.paused}
onChange={() => {}}
/>
<div
className={cx("switch-text", {
paused: paMessage.paused,
active: !paMessage.paused,
})}
>
{paMessage.paused ? "Paused" : "Active"}
</div>
</div>
</td>
)}
</tr>
);
};
Expand Down
11 changes: 11 additions & 0 deletions assets/js/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,17 @@ export const updateExistingPaMessage = async (
body: JSON.stringify(updates),
});

if (response.status === 422) {
const body = await response.json();
const error = Object.keys(body.errors);

throw error;
} else if (!response.ok) {
const error = new Error(`Error: ${response.status} ${response.statusText}`);

throw error;
}

return {
status: response.status,
body: await response.json(),
Expand Down
Loading

0 comments on commit a26b200

Please sign in to comment.