Skip to content

Commit 43182a7

Browse files
authored
Reusable components (#414)
* Replace repeated code with re-usable components * Built in confirm dialogues and permission checks
1 parent e3b145a commit 43182a7

182 files changed

Lines changed: 3432 additions & 4445 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/attendance/components/AttendanceSetup.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Link } from "react-router-dom";
55
import { Icon, Table, TableBody, TableCell, TableRow, TableHead, Paper, Box, Typography, Button, Stack } from "@mui/material";
66
import { Add as AddIcon } from "@mui/icons-material";
77
import { AppIconButton } from "../../components/ui/AppIconButton";
8+
import { hoverRowSx } from "../../components/ui";
89
import {
910
type AttendanceInterface,
1011
type CampusInterface,
@@ -230,7 +231,7 @@ export const AttendanceSetup = memo(() => {
230231
const result = (
231232
<TableRow
232233
key={key}
233-
sx={{ "&:hover": { backgroundColor: "action.hover" } }}>
234+
sx={hoverRowSx}>
234235
<TableCell sx={{ py: 0.5, border: 0 }}>{campusHtml}</TableCell>
235236
<TableCell sx={{ py: 0.5, border: 0 }}>{serviceHtml}</TableCell>
236237
<TableCell sx={{ py: 0.5, border: 0 }}>{serviceTimeHtml}</TableCell>

src/attendance/components/ServiceEdit.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import React from "react";
2-
import { Alert, Box, FormControl, Grid, InputLabel, MenuItem, Select, TextField } from "@mui/material";
2+
import { Box, FormControl, Grid, InputLabel, MenuItem, Select, TextField } from "@mui/material";
33
import { useForm, Controller } from "react-hook-form";
44
import { type ServiceInterface } from "@churchapps/helpers";
5-
import { ApiHelper, UniqueIdHelper, Locale } from "@churchapps/apphelper";
5+
import { ApiHelper, UniqueIdHelper, Locale, ErrorMessages } from "@churchapps/apphelper";
66
import { FormCard } from "../../components/ui";
77
import { useCampuses } from "../../hooks/useCampuses";
8+
import { useConfirmDelete, useErrorSummary } from "../../hooks";
89

910
interface Props {
1011
service: ServiceInterface;
@@ -21,9 +22,8 @@ export const ServiceEdit: React.FC<Props> = (props) => {
2122

2223
const { control, register, handleSubmit, reset, formState } = useForm<AnyRecord>({ defaultValues: { name: "", campusId: "" } });
2324
const e = formState.errors as any;
24-
const summaryErrors: string[] = [];
25-
if (e.name?.message) summaryErrors.push(e.name.message);
26-
if (e.campusId?.message) summaryErrors.push(e.campusId.message);
25+
const summaryErrors = useErrorSummary(formState.errors, ["name", "campusId"]);
26+
const { confirm, ConfirmDialogElement } = useConfirmDelete();
2727

2828
const onValid = (values: AnyRecord) => {
2929
setIsSubmitting(true);
@@ -33,8 +33,8 @@ export const ServiceEdit: React.FC<Props> = (props) => {
3333
.finally(() => { setIsSubmitting(false); });
3434
};
3535

36-
const handleDelete = () => {
37-
if (window.confirm(Locale.label("attendance.serviceEdit.confirmDelete"))) ApiHelper.delete("/services/" + props.service.id, "AttendanceApi").then(props.updatedFunction);
36+
const handleDelete = async () => {
37+
if (await confirm(Locale.label("attendance.serviceEdit.confirmDelete"))) ApiHelper.delete("/services/" + props.service.id, "AttendanceApi").then(props.updatedFunction);
3838
};
3939

4040
const loadData = React.useCallback(() => {
@@ -48,6 +48,7 @@ export const ServiceEdit: React.FC<Props> = (props) => {
4848

4949
return (
5050
<Box data-cy="service-box">
51+
{ConfirmDialogElement}
5152
<FormCard
5253
id="serviceBox"
5354
onCancel={props.updatedFunction}
@@ -57,7 +58,7 @@ export const ServiceEdit: React.FC<Props> = (props) => {
5758
icon="calendar_month"
5859
isSubmitting={isSubmitting}
5960
help="docs/b1-admin/attendance/">
60-
{summaryErrors.length > 0 && <Alert severity="error" sx={{ mb: 2 }}>{summaryErrors.map((msg) => <div key={msg}>{msg}</div>)}</Alert>}
61+
<ErrorMessages errors={summaryErrors} />
6162
<Grid container spacing={2}>
6263
<Grid size={{ xs: 12, sm: 6 }}>
6364
<FormControl fullWidth>

src/attendance/components/ServiceTimeEdit.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import React from "react";
2-
import { Alert, Box, FormControl, Grid, InputLabel, MenuItem, Select } from "@mui/material";
2+
import { Box, FormControl, Grid, InputLabel, MenuItem, Select } from "@mui/material";
33
import { TextField } from "@mui/material";
44
import { useForm, Controller } from "react-hook-form";
55
import { type ServiceTimeInterface, type ServiceInterface } from "@churchapps/helpers";
6-
import { useMountedState, ApiHelper, Locale } from "@churchapps/apphelper";
6+
import { useMountedState, ApiHelper, Locale, ErrorMessages } from "@churchapps/apphelper";
77
import { FormCard } from "../../components/ui";
8+
import { useErrorSummary } from "../../hooks";
89

910
interface Props {
1011
serviceTime: ServiceTimeInterface;
@@ -21,9 +22,7 @@ export const ServiceTimeEdit: React.FC<Props> = (props) => {
2122

2223
const { control, register, handleSubmit, reset, formState } = useForm<AnyRecord>({ defaultValues: { name: "", serviceId: "" } });
2324
const e = formState.errors as any;
24-
const summaryErrors: string[] = [];
25-
if (e.name?.message) summaryErrors.push(e.name.message);
26-
if (e.serviceId?.message) summaryErrors.push(e.serviceId.message);
25+
const summaryErrors = useErrorSummary(formState.errors, ["name", "serviceId"]);
2726

2827
const onValid = (values: AnyRecord) => {
2928
setIsSubmitting(true);
@@ -59,7 +58,7 @@ export const ServiceTimeEdit: React.FC<Props> = (props) => {
5958
isSubmitting={isSubmitting}
6059
icon="schedule"
6160
help="docs/b1-admin/attendance/">
62-
{summaryErrors.length > 0 && <Alert severity="error" sx={{ mb: 2 }}>{summaryErrors.map((msg) => <div key={msg}>{msg}</div>)}</Alert>}
61+
<ErrorMessages errors={summaryErrors} />
6362
<Grid container spacing={2}>
6463
<Grid size={{ xs: 12, sm: 6 }}>
6564
<FormControl fullWidth>

src/calendars/AvailabilityPage.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ApiHelper, UserHelper, EventHelper, Loading, PageHeader, Locale } from
66
import { Permissions } from "@churchapps/helpers";
77
import { Box, MenuItem, Stack, TextField } from "@mui/material";
88
import { Add as AddIcon, EventAvailable as AvailabilityIcon } from "@mui/icons-material";
9-
import { PermissionDenied } from "../components";
9+
import { useRequirePermission } from "../hooks";
1010
import { NewEventModal } from "./components/NewEventModal";
1111
import { HeaderPrimaryButton } from "../components/ui/headerButtons";
1212
import { type CalendarBlockoutInterface, type EventBookingInterface, type ResourceInterface, type RoomInterface } from "./interfaces";
@@ -21,6 +21,7 @@ export const AvailabilityPage = () => {
2121
const [filter, setFilter] = useState("");
2222
const [showBook, setShowBook] = useState(false);
2323
const [loading, setLoading] = useState(true);
24+
const denied = useRequirePermission(Permissions.contentApi.content.edit);
2425

2526
const localizer = dayjsLocalizer(dayjs);
2627

@@ -104,7 +105,7 @@ export const AvailabilityPage = () => {
104105
return { style: { backgroundColor: bg, borderColor: bg } };
105106
};
106107

107-
if (!UserHelper.checkAccess(Permissions.contentApi.content.edit)) return <PermissionDenied permissions={[Permissions.contentApi.content.edit]} />;
108+
if (denied) return denied;
108109

109110
const churchId = UserHelper.currentUserChurch?.church?.id || "";
110111
const bookRoomId = filter.startsWith("room:") ? filter.slice(5) : undefined;

src/calendars/CalendarPage.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import {
1515
import { Delete as DeleteIcon, CalendarMonth as CalendarIcon, Groups as GroupsIcon, Add as AddIcon, Print as PrintIcon, UploadFile as ImportIcon } from "@mui/icons-material";
1616
import { ApiHelper, UserHelper, Loading, PageHeader, Locale, Permissions } from "@churchapps/apphelper";
1717
import { type CuratedCalendarInterface, type GroupInterface, type CuratedEventInterface } from "@churchapps/helpers";
18-
import { PermissionDenied } from "../components";
18+
import { useConfirmDelete, useRequirePermission } from "../hooks";
1919
import { CuratedCalendar } from "./components/CuratedCalendar";
2020
import { NewEventModal } from "./components/NewEventModal";
2121
import { ImportIcsModal } from "./components/ImportIcsModal";
2222
import { AppIconButton } from "../components/ui/AppIconButton";
23-
import { CountChip } from "../components/ui";
23+
import { CountChip, EmptyState } from "../components/ui";
2424
import { HeaderPrimaryButton, HeaderSecondaryButton } from "../components/ui/headerButtons";
2525

2626
const printStyles = `@media print {
@@ -39,6 +39,8 @@ export const CalendarPage = () => {
3939
const [refresh, refresher] = useState({});
4040
const [showNewEvent, setShowNewEvent] = useState(false);
4141
const [showImport, setShowImport] = useState(false);
42+
const { confirm, ConfirmDialogElement } = useConfirmDelete();
43+
const denied = useRequirePermission(Permissions.contentApi.content.edit);
4244

4345
const curatedCalendarId = params.id;
4446

@@ -60,8 +62,8 @@ export const CalendarPage = () => {
6062
});
6163
};
6264

63-
const handleGroupDelete = (groupId: string) => {
64-
if (confirm(Locale.label("calendars.calendarPage.confirmRemoveGroup"))) {
65+
const handleGroupDelete = async (groupId: string) => {
66+
if (await confirm(Locale.label("calendars.calendarPage.confirmRemoveGroup"))) {
6567
ApiHelper.delete("/curatedEvents/calendar/" + curatedCalendarId + "/group/" + groupId, "ContentApi").then(() => {
6668
loadData();
6769
refresher({});
@@ -76,10 +78,11 @@ export const CalendarPage = () => {
7678
}, [curatedCalendarId]);
7779

7880
if (!curatedCalendarId) return null;
79-
if (!UserHelper.checkAccess(Permissions.contentApi.content.edit)) return <PermissionDenied permissions={[Permissions.contentApi.content.edit]} />;
81+
if (denied) return denied;
8082

8183
return (
8284
<>
85+
{ConfirmDialogElement}
8386
<style>{printStyles}</style>
8487
<PageHeader
8588
icon={<CalendarIcon />}
@@ -152,14 +155,13 @@ export const CalendarPage = () => {
152155
<Loading data-testid="groups-loading" />
153156
</Box>
154157
) : addedGroups.length === 0 ? (
155-
<Box sx={{ p: 3, textAlign: "center" }}>
156-
<GroupsIcon sx={{ fontSize: 48, color: "grey.400", mb: 1 }} />
157-
<Typography variant="body2" color="text.secondary">
158-
{Locale.label("calendars.calendarPage.noGroupsAdded")}
159-
</Typography>
160-
<Typography variant="caption" color="text.secondary">
161-
{Locale.label("calendars.calendarPage.addEventsHint")}
162-
</Typography>
158+
<Box sx={{ p: 2 }}>
159+
<EmptyState
160+
variant="card"
161+
icon={<GroupsIcon />}
162+
title={Locale.label("calendars.calendarPage.noGroupsAdded")}
163+
description={Locale.label("calendars.calendarPage.addEventsHint")}
164+
/>
163165
</Box>
164166
) : (
165167
<Table size="small">

src/calendars/CalendarsPage.tsx

Lines changed: 16 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { useState, useEffect } from "react";
2-
import { ApiHelper, UserHelper, Loading, PageHeader, Locale } from "@churchapps/apphelper";
1+
import { useState } from "react";
2+
import { useQuery } from "@tanstack/react-query";
3+
import { UserHelper, Loading, PageHeader, Locale } from "@churchapps/apphelper";
34
import { Permissions, type CuratedCalendarInterface } from "@churchapps/helpers";
45
import { Link, useNavigate } from "react-router-dom";
56
import {
@@ -26,34 +27,22 @@ import {
2627
Description as DescriptionIcon
2728
} from "@mui/icons-material";
2829
import { CalendarEdit } from "./components";
29-
import { PermissionDenied } from "../components";
30+
import { useRequirePermission } from "../hooks";
3031
import { EmptyState } from "../components/ui/EmptyState";
32+
import { hoverRowSx } from "../components/ui/tableStyles";
3133
import { AppIconButton } from "../components/ui/AppIconButton";
3234
import { HeaderPrimaryButton } from "../components/ui/headerButtons";
3335

3436
export const CalendarsPage = () => {
35-
const [calendars, setCalendars] = useState<CuratedCalendarInterface[]>([]);
37+
const calendarsQuery = useQuery<CuratedCalendarInterface[]>({ queryKey: ["/curatedCalendars", "ContentApi"], placeholderData: [] });
3638
const [currentCalendar, setCurrentCalendar] = useState<CuratedCalendarInterface | null>(null);
37-
const [loading, setLoading] = useState(true);
3839
const navigate = useNavigate();
40+
const denied = useRequirePermission(Permissions.contentApi.content.edit);
3941

40-
const loadData = () => {
41-
setLoading(true);
42-
ApiHelper.get("/curatedCalendars", "ContentApi").then((data: any) => {
43-
setCalendars(data);
44-
setLoading(false);
45-
}).catch(() => {
46-
setLoading(false);
47-
});
48-
};
49-
50-
const getRows = () => calendars.map((calendar) => (
42+
const getRows = () => calendarsQuery.data.map((calendar) => (
5143
<TableRow
5244
key={calendar.id}
53-
sx={{
54-
"&:hover": { backgroundColor: "action.hover" },
55-
transition: "background-color 0.2s ease"
56-
}}
45+
sx={hoverRowSx}
5746
>
5847
<TableCell>
5948
<Stack direction="row" spacing={2} alignItems="center">
@@ -117,20 +106,16 @@ export const CalendarsPage = () => {
117106
</TableRow>
118107
));
119108

120-
useEffect(() => {
121-
loadData();
122-
}, []);
123-
124-
if (!UserHelper.checkAccess(Permissions.contentApi.content.edit)) return <PermissionDenied permissions={[Permissions.contentApi.content.edit]} />;
109+
if (denied) return denied;
125110

126111
return (
127112
<>
128113
<PageHeader
129114
icon={<CalendarIcon />}
130115
title={Locale.label("calendars.calendarList.title")}
131116
subtitle={
132-
calendars.length > 0
133-
? Locale.label("calendars.calendarList.subtitleWithCount", `${calendars.length} ${calendars.length === 1 ? Locale.label("calendars.calendarList.calendar") : Locale.label("calendars.calendarList.calendars")}`)
117+
calendarsQuery.data.length > 0
118+
? Locale.label("calendars.calendarList.subtitleWithCount", `${calendarsQuery.data.length} ${calendarsQuery.data.length === 1 ? Locale.label("calendars.calendarList.calendar") : Locale.label("calendars.calendarList.calendars")}`)
134119
: Locale.label("calendars.calendarList.subtitleEmpty")
135120
}
136121
>
@@ -152,15 +137,15 @@ export const CalendarsPage = () => {
152137
calendar={currentCalendar}
153138
updatedCallback={() => {
154139
setCurrentCalendar(null);
155-
loadData();
140+
calendarsQuery.refetch();
156141
}}
157142
/>
158143
</Box>
159144
)}
160145

161-
{loading ? (
146+
{calendarsQuery.isLoading ? (
162147
<Loading data-testid="calendars-loading" />
163-
) : calendars.length === 0 ? (
148+
) : calendarsQuery.data.length === 0 ? (
164149
<EmptyState
165150
icon={<CalendarIcon />}
166151
title={Locale.label("calendars.calendarList.noCalendars")}
@@ -197,7 +182,7 @@ export const CalendarsPage = () => {
197182
</TableContainer>
198183
)}
199184

200-
{calendars.length > 0 && !currentCalendar && (
185+
{calendarsQuery.data.length > 0 && !currentCalendar && (
201186
<Card sx={{ mt: 3, borderRadius: 2, border: "1px solid", borderColor: "divider" }}>
202187
<CardContent>
203188
<Stack direction="row" spacing={2} alignItems="flex-start">

src/calendars/RoomsResourcesPage.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { useState, useEffect, useCallback } from "react";
2-
import { ApiHelper, UserHelper, Loading, PageHeader, Locale } from "@churchapps/apphelper";
2+
import { ApiHelper, Loading, PageHeader, Locale } from "@churchapps/apphelper";
33
import { Permissions, type GroupInterface } from "@churchapps/helpers";
44
import { Box, Grid, Table, TableBody, TableCell, TableHead, TableRow, TableContainer, Paper } from "@mui/material";
55
import { Add as AddIcon, Edit as EditIcon, MeetingRoom as RoomIcon } from "@mui/icons-material";
6-
import { PermissionDenied } from "../components";
6+
import { useRequirePermission } from "../hooks";
77
import { EmptyState } from "../components/ui/EmptyState";
88
import { AppIconButton } from "../components/ui/AppIconButton";
99
import { NavigationTabs } from "../components/ui/NavigationTabs";
@@ -26,6 +26,7 @@ export const RoomsResourcesPage = () => {
2626
const [groups, setGroups] = useState<GroupInterface[]>([]);
2727
const [editing, setEditing] = useState<Editing>(null);
2828
const [loading, setLoading] = useState(true);
29+
const denied = useRequirePermission(Permissions.contentApi.content.edit);
2930

3031
const loadData = useCallback(() => {
3132
setLoading(true);
@@ -144,7 +145,7 @@ export const RoomsResourcesPage = () => {
144145
</TableContainer>
145146
));
146147

147-
if (!UserHelper.checkAccess(Permissions.contentApi.content.edit)) return <PermissionDenied permissions={[Permissions.contentApi.content.edit]} />;
148+
if (denied) return denied;
148149

149150
const getTable = () => {
150151
switch (tab) {

0 commit comments

Comments
 (0)