diff --git a/.eslintrc.js b/.eslintrc.js index 87c63fac4..1554564c4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -53,7 +53,7 @@ module.exports = { } ], "no-unused-vars": "off", - "react-hooks/exhaustive-deps": "warn", + "react-hooks/exhaustive-deps": "error", "prettier/prettier": [ "error", {}, diff --git a/package.json b/package.json index 19c8ecd99..a781cfd09 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "nookies": "^2.5.2", "prettier-plugin-tailwindcss": "^0.2.2", "query-string": "^7.1.1", + "quill": "2.0.3", "ra-input-rich-text": "^4.12.2", "react": "18.2.0", "react-admin": "^4.7.4", diff --git a/public/images/Impact Story - Landing Page.png b/public/images/Impact Story - Landing Page.png new file mode 100644 index 000000000..9813ef1f2 Binary files /dev/null and b/public/images/Impact Story - Landing Page.png differ diff --git a/public/images/impact-story-1.png b/public/images/impact-story-1.png new file mode 100644 index 000000000..c63d41ceb Binary files /dev/null and b/public/images/impact-story-1.png differ diff --git a/public/images/impact-story-2.png b/public/images/impact-story-2.png new file mode 100644 index 000000000..f3f1d2324 Binary files /dev/null and b/public/images/impact-story-2.png differ diff --git a/public/images/mask.png b/public/images/mask.png new file mode 100644 index 000000000..d669a3cbb Binary files /dev/null and b/public/images/mask.png differ diff --git a/public/images/maskRight.png b/public/images/maskRight.png new file mode 100644 index 000000000..8b04f7acf Binary files /dev/null and b/public/images/maskRight.png differ diff --git a/src/admin/apiProvider/authProvider.ts b/src/admin/apiProvider/authProvider.ts index 0b44e787e..05c3aabde 100644 --- a/src/admin/apiProvider/authProvider.ts +++ b/src/admin/apiProvider/authProvider.ts @@ -1,9 +1,11 @@ -import { AuthProvider } from "react-admin"; +import { AuthProvider, UserIdentity } from "react-admin"; import { loadLogin, logout } from "@/connections/Login"; import { loadMyUser } from "@/connections/User"; import Log from "@/utils/log"; +export type TMUserIdentity = UserIdentity & { primaryRole: string }; + export const authProvider: AuthProvider = { login: async () => { Log.error("Admin app does not support direct login"); @@ -29,7 +31,7 @@ export const authProvider: AuthProvider = { const { user } = await loadMyUser(); if (user == null) throw "No user logged in."; - return { id: user.uuid, fullName: user.fullName ?? undefined, primaryRole: user.primaryRole }; + return { id: user.uuid, fullName: user.fullName ?? undefined, primaryRole: user.primaryRole } as TMUserIdentity; }, // get the user permissions (optional) diff --git a/src/admin/apiProvider/dataProviders/fundingProgrammeDataProvider.ts b/src/admin/apiProvider/dataProviders/fundingProgrammeDataProvider.ts index f7da66c1e..e53ef68a4 100644 --- a/src/admin/apiProvider/dataProviders/fundingProgrammeDataProvider.ts +++ b/src/admin/apiProvider/dataProviders/fundingProgrammeDataProvider.ts @@ -152,7 +152,6 @@ export const fundingProgrammeDataProvider: FundingDataProvider = { } }); } - await handleUploads(params, uploadKeys, { //@ts-ignore uuid: resp.data.uuid, diff --git a/src/admin/apiProvider/dataProviders/impactStoriesDataProvider.ts b/src/admin/apiProvider/dataProviders/impactStoriesDataProvider.ts new file mode 100644 index 000000000..87e4203ea --- /dev/null +++ b/src/admin/apiProvider/dataProviders/impactStoriesDataProvider.ts @@ -0,0 +1,115 @@ +import lo from "lodash"; +import { DataProvider } from "react-admin"; + +import { + DeleteV2AdminImpactStoriesIdError, + fetchDeleteV2AdminImpactStoriesId, + fetchGetV2AdminImpactStories, + fetchGetV2AdminImpactStoriesId, + fetchPostV2AdminImpactStories, + fetchPostV2AdminImpactStoriesBulkDelete, + fetchPutV2AdminImpactStoriesId, + GetV2AdminImpactStoriesError, + GetV2AdminImpactStoriesIdError, + PostV2AdminImpactStoriesBulkDeleteError, + PostV2AdminImpactStoriesError, + PutV2AdminImpactStoriesIdError +} from "@/generated/apiComponents"; + +import { getFormattedErrorForRA } from "../utils/error"; +import { apiListResponseToRAListResult, raListParamsToQueryParams } from "../utils/listing"; +import { handleUploads } from "../utils/upload"; + +// @ts-ignore +export const impactStoriesDataProvider: DataProvider = { + async getList(_, params) { + try { + const response = await fetchGetV2AdminImpactStories({ + queryParams: raListParamsToQueryParams(params, []) + }); + return apiListResponseToRAListResult(response); + } catch (err) { + throw getFormattedErrorForRA(err as GetV2AdminImpactStoriesError); + } + }, + // @ts-ignore + async getOne(_, params) { + try { + const list = await fetchGetV2AdminImpactStoriesId({ + pathParams: { id: params.id } + }); + const response = { data: list }; + //@ts-ignore + return { data: { ...response.data, id: response.data.id } }; + } catch (err) { + throw getFormattedErrorForRA(err as GetV2AdminImpactStoriesIdError); + } + }, + //@ts-ignore + async create(__, params) { + const uploadKeys = ["thumbnail"]; + const body: any = lo.omit(params.data, uploadKeys); + try { + const response = await fetchPostV2AdminImpactStories({ + body: body + }); + // @ts-expect-error + const uuid = response.data.uuid as string; + await handleUploads(params, uploadKeys, { + uuid, + model: "impact-story" + }); + // @ts-expect-error + return { data: { ...response.data, id: response.id } }; + } catch (err) { + throw getFormattedErrorForRA(err as PostV2AdminImpactStoriesError); + } + }, + //@ts-ignore + async update(__, params) { + const uuid = params.id as string; + const uploadKeys = ["thumbnail"]; + const body = lo.omit(params.data, uploadKeys); + + try { + await handleUploads(params, uploadKeys, { + uuid, + model: "impact-story" + }); + + const response = await fetchPutV2AdminImpactStoriesId({ + body, + pathParams: { id: uuid } + }); + + console.log("Params", params.data); + // @ts-expect-error + return { data: { ...response.data, id: response.data.uuid } }; + } catch (err) { + throw getFormattedErrorForRA(err as PutV2AdminImpactStoriesIdError); + } + }, + + //@ts-ignore + async delete(__, params) { + try { + await fetchDeleteV2AdminImpactStoriesId({ + pathParams: { id: params.id as string } + }); + return { data: { id: params.id } }; + } catch (err) { + throw getFormattedErrorForRA(err as DeleteV2AdminImpactStoriesIdError); + } + }, + // @ts-ignore + async deleteMany(_, params) { + try { + await fetchPostV2AdminImpactStoriesBulkDelete({ + body: { uuids: params.ids.map(String) } + }); + return { data: params.ids }; + } catch (err) { + throw getFormattedErrorForRA(err as PostV2AdminImpactStoriesBulkDeleteError); + } + } +}; diff --git a/src/admin/apiProvider/dataProviders/index.ts b/src/admin/apiProvider/dataProviders/index.ts index 3ca5cfc3f..328aee823 100644 --- a/src/admin/apiProvider/dataProviders/index.ts +++ b/src/admin/apiProvider/dataProviders/index.ts @@ -8,6 +8,7 @@ import { applicationDataProvider } from "./applicationDataProvider"; import { auditDataProvider } from "./auditDataProvider"; import { formDataProvider } from "./formDataProvider"; import { fundingProgrammeDataProvider } from "./fundingProgrammeDataProvider"; +import { impactStoriesDataProvider } from "./impactStoriesDataProvider"; import { nurseryDataProvider } from "./nurseryDataProvider"; import { nurseryReportDataProvider } from "./nurseryReportDataProvider"; import { organisationDataProvider } from "./organisationDataProvider"; @@ -70,6 +71,9 @@ export const dataProvider = combineDataProviders(resource => { case modules.audit.ResourceName: return auditDataProvider; + case modules.impactStories.ResourceName: + return impactStoriesDataProvider; + default: throw new Error(`Unknown resource: ${resource}`); } diff --git a/src/admin/apiProvider/dataProviders/organisationDataProvider.ts b/src/admin/apiProvider/dataProviders/organisationDataProvider.ts index c59a8f835..6ff056a43 100644 --- a/src/admin/apiProvider/dataProviders/organisationDataProvider.ts +++ b/src/admin/apiProvider/dataProviders/organisationDataProvider.ts @@ -107,7 +107,6 @@ export const organisationDataProvider: OrganisationDataProvider = { const uuid = params.id as string; const uploadKeys = ["logo", "cover", "legal_registration", "reference", "additional"]; const body = lo.omit(params.data, uploadKeys); - await handleUploads(params, uploadKeys, { uuid, model: "organisation" diff --git a/src/admin/components/Actions/ListActionsImpactStories.tsx b/src/admin/components/Actions/ListActionsImpactStories.tsx new file mode 100644 index 000000000..986021379 --- /dev/null +++ b/src/admin/components/Actions/ListActionsImpactStories.tsx @@ -0,0 +1,19 @@ +import DownloadIcon from "@mui/icons-material/GetApp"; +import { Button, CreateButton, FilterButton, TopToolbar } from "react-admin"; +import { When } from "react-if"; + +interface ListActionsProps { + onExport?: () => void; +} + +const ListActionsImpactStories = (props: ListActionsProps) => ( + + + + } onClick={props.onExport} /> + + + +); + +export default ListActionsImpactStories; diff --git a/src/admin/components/Actions/ShowActions.tsx b/src/admin/components/Actions/ShowActions.tsx index 20e3e7e56..298a241d4 100644 --- a/src/admin/components/Actions/ShowActions.tsx +++ b/src/admin/components/Actions/ShowActions.tsx @@ -1,13 +1,12 @@ import { Box, Typography } from "@mui/material"; -import { get } from "lodash"; import { Button, DeleteWithConfirmButton, DeleteWithConfirmButtonProps, EditButton, Link, - RaRecord, TopToolbar, + useGetRecordRepresentation, useRecordContext, useResourceContext } from "react-admin"; @@ -19,8 +18,6 @@ import Icon, { IconNames } from "@/components/extensive/Icon/Icon"; import ShowTitle from "../ShowTitle"; interface IProps { - titleSource?: string; - getTitle?: (record: RaRecord) => string; resourceName?: string; moduleName?: string; hasDelete?: boolean; @@ -30,8 +27,6 @@ interface IProps { } const ShowActions = ({ - titleSource, - getTitle, resourceName, moduleName, hasDelete = true, @@ -41,11 +36,10 @@ const ShowActions = ({ }: IProps) => { const record = useRecordContext(); const resource = useResourceContext(); + const title = useGetRecordRepresentation(resource)(record); - const title = titleSource ? get(record, titleSource) : ""; - - if (titleSource && resourceName) { - deleteProps.confirmTitle = `Delete ${resourceName} ${record?.[titleSource]}`; + if (resourceName != null) { + deleteProps.confirmTitle = `Delete ${resourceName} ${title}`; deleteProps.confirmContent = `You are about to delete this ${resourceName}. This action will permanently remove the item from the system, and it cannot be undone. Are you sure you want to delete this item?`; } @@ -58,9 +52,9 @@ const ShowActions = ({ - + - title} /> + diff --git a/src/admin/components/Alerts/DelayedJobsProgressAlert.tsx b/src/admin/components/Alerts/DelayedJobsProgressAlert.tsx deleted file mode 100644 index 1a8c1bfef..000000000 --- a/src/admin/components/Alerts/DelayedJobsProgressAlert.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Alert, AlertTitle, CircularProgress } from "@mui/material"; -import { FC, useEffect, useState } from "react"; -import { useStore } from "react-redux"; - -import ApiSlice from "@/store/apiSlice"; -import { AppStore } from "@/store/store"; - -type DelayedJobsProgressAlertProps = { - show: boolean; - title?: string; - setIsLoadingDelayedJob?: (value: boolean) => void; -}; - -const DelayedJobsProgressAlert: FC = ({ show, title, setIsLoadingDelayedJob }) => { - const [delayedJobProcessing, setDelayedJobProcessing] = useState(0); - const [delayedJobTotal, setDalayedJobTotal] = useState(0); - const [progressMessage, setProgressMessage] = useState("Running 0 out of 0 polygons (0%)"); - - const store = useStore(); - useEffect(() => { - let intervalId: any; - if (show) { - intervalId = setInterval(() => { - const { total_content, processed_content, progress_message } = store.getState().api; - setDalayedJobTotal(total_content); - setDelayedJobProcessing(processed_content); - if (progress_message != "") { - setProgressMessage(progress_message); - } - }, 1000); - } - - return () => { - if (intervalId) { - setDelayedJobProcessing(0); - setDalayedJobTotal(0); - setProgressMessage("Running 0 out of 0 polygons (0%)"); - clearInterval(intervalId); - } - }; - }, [show]); - - const abortDelayedJob = () => { - ApiSlice.abortDelayedJob(true); - ApiSlice.addTotalContent(0); - ApiSlice.addProgressContent(0); - ApiSlice.addProgressMessage("Running 0 out of 0 polygons (0%)"); - setDelayedJobProcessing(0); - setDalayedJobTotal(0); - setIsLoadingDelayedJob?.(false); - }; - - if (!show) return null; - - const calculatedProgress = delayedJobTotal! > 0 ? Math.round((delayedJobProcessing! / delayedJobTotal!) * 100) : 0; - - const severity = calculatedProgress >= 75 ? "success" : calculatedProgress >= 50 ? "info" : "warning"; - - return ( - - } - action={ - - Cancel - - } - > - {title} - {progressMessage ?? "Running 0 out of 0 polygons (0%)"} - - - ); -}; - -export default DelayedJobsProgressAlert; diff --git a/src/admin/components/App.tsx b/src/admin/components/App.tsx index 2a07ea914..fb936d702 100644 --- a/src/admin/components/App.tsx +++ b/src/admin/components/App.tsx @@ -36,6 +36,7 @@ const App = () => { edit={modules.user.Edit} create={modules.user.Create} icon={() => } + recordRepresentation={record => `${record?.first_name} ${record?.last_name}`} /> { show={modules.organisation.Show} edit={modules.organisation.Edit} icon={() => } + recordRepresentation={record => record?.name} /> { show={modules.pitch.Show} edit={modules.pitch.Edit} icon={() => } + recordRepresentation={record => record?.project_name} /> { create={modules.fundingProgramme.Create} icon={() => } options={{ label: "Funding Programmes" }} + recordRepresentation={record => `Funding Programme "${record?.name}"`} /> { list={modules.application.List} show={modules.application.Show} icon={() => } + recordRepresentation={record => `${record?.id}`} /> { edit={modules.form.Edit} icon={() => } create={modules.form.Create} + recordRepresentation={record => record?.project_name} /> > )} @@ -96,6 +102,7 @@ const App = () => { show={modules.project.Show} edit={modules.project.Edit} icon={() => } + recordRepresentation={record => record?.name ?? ""} /> { show={modules.site.Show} edit={modules.site.Edit} icon={() => } + recordRepresentation={record => record?.name ?? ""} /> { show={modules.nursery.Show} edit={modules.nursery.Edit} icon={() => } + recordRepresentation={record => record?.name ?? ""} /> { show={modules.task.Show} icon={SummarizeIcon} options={{ label: "Tasks" }} + recordRepresentation={record => record?.project?.name} /> { edit={modules.projectReport.Edit} icon={() => } options={{ label: "Project Reports" }} + recordRepresentation={record => record?.title} /> { edit={modules.siteReport.Edit} icon={() => } options={{ label: "Site Reports" }} + recordRepresentation={record => record?.title} /> { edit={modules.nurseryReport.Edit} icon={() => } options={{ label: "Nursery Reports" }} + recordRepresentation={record => record?.title} /> {isAdmin && ( <> @@ -153,6 +166,14 @@ const App = () => { /> > )} + } + options={{ label: "Impact Stories" }} + /> ); diff --git a/src/admin/components/AppBar.tsx b/src/admin/components/AppBar.tsx index 574d689df..d691ae61f 100644 --- a/src/admin/components/AppBar.tsx +++ b/src/admin/components/AppBar.tsx @@ -1,14 +1,28 @@ -import { Typography } from "@mui/material"; -import { AppBar as RaAppBar, AppBarProps, Link } from "react-admin"; +import { AppBar as RaAppBar, AppBarProps, Link, Logout, MenuItemLink, UserMenu } from "react-admin"; -export const AppBar = (props: AppBarProps) => ( - - - - - - +import Icon, { IconNames } from "@/components/extensive/Icon/Icon"; +export const AppBar = (props: AppBarProps) => { + const CustomUserMenu = (props: any) => ( + + } + onClick={() => { + window.location.href = "/dashboard"; + }} + /> + + + ); - - -); + return ( + }> + + + + + + + ); +}; diff --git a/src/admin/components/AppMenu.tsx b/src/admin/components/AppMenu.tsx index fc99bb471..30867b678 100644 --- a/src/admin/components/AppMenu.tsx +++ b/src/admin/components/AppMenu.tsx @@ -60,6 +60,9 @@ const AppMenu = () => { + + + ); }; diff --git a/src/admin/components/Dialogs/FrameworkSelectionDialog.tsx b/src/admin/components/Dialogs/FrameworkSelectionDialog.tsx index 8d09ab237..49fd06ce8 100644 --- a/src/admin/components/Dialogs/FrameworkSelectionDialog.tsx +++ b/src/admin/components/Dialogs/FrameworkSelectionDialog.tsx @@ -84,7 +84,7 @@ export function useFrameworkExport(entity: EntityName, choices: any[]) { setModalOpen(false); }, - [entity, choices] + [entity, role] ); return { @@ -95,7 +95,7 @@ export function useFrameworkExport(entity: EntityName, choices: any[]) { } else { onExport(choices[0].id); } - }, [choices]), + }, [choices, onExport]), frameworkDialogProps: { open: modalOpen, onCancel: useCallback(() => setModalOpen(false), []), diff --git a/src/admin/components/EntityEdit/EntityEdit.tsx b/src/admin/components/EntityEdit/EntityEdit.tsx index a7f271d0c..a98f3cbb3 100644 --- a/src/admin/components/EntityEdit/EntityEdit.tsx +++ b/src/admin/components/EntityEdit/EntityEdit.tsx @@ -1,4 +1,5 @@ import { notFound } from "next/navigation"; +import { useMemo } from "react"; import { useCreatePath, useResourceContext } from "react-admin"; import { useNavigate, useParams } from "react-router-dom"; @@ -7,7 +8,7 @@ import WizardForm from "@/components/extensive/WizardForm"; import LoadingContainer from "@/components/generic/Loading/LoadingContainer"; import EntityProvider from "@/context/entity.provider"; import FrameworkProvider, { Framework } from "@/context/framework.provider"; -import { GetV2FormsENTITYUUIDResponse, useGetV2FormsENTITYUUID } from "@/generated/apiComponents"; +import { GetV2FormsENTITYUUIDResponse, useGetV2ENTITYUUID, useGetV2FormsENTITYUUID } from "@/generated/apiComponents"; import { normalizedFormData } from "@/helpers/customForms"; import { pluralEntityNameToSingular } from "@/helpers/entity"; import { useFormUpdate } from "@/hooks/useFormUpdate"; @@ -44,6 +45,8 @@ export const EntityEdit = () => { isError: loadError } = useGetV2FormsENTITYUUID({ pathParams: { entity: entityName, uuid: entityUUID } }); + const { data: entityValue } = useGetV2ENTITYUUID({ pathParams: { entity: entityName, uuid: entityUUID } }); + // @ts-ignore const formData = (formResponse?.data ?? {}) as GetV2FormsENTITYUUIDResponse; @@ -67,6 +70,15 @@ export const EntityEdit = () => { return notFound(); } + const bannerTitle = useMemo(() => { + if (entityName === "site-reports") { + return `${entityValue?.data?.site?.name} ${title}`; + } else if (entityName === "nursery-reports") { + return `${entityValue?.data?.nursery?.name} ${title}`; + } + return title; + }, [entityName, entityValue, title]); + return ( @@ -80,7 +92,7 @@ export const EntityEdit = () => { formStatus={isSuccess ? "saved" : isUpdating ? "saving" : undefined} onSubmit={() => navigate(createPath({ resource, id, type: "show" }))} defaultValues={defaultValues} - title={title} + title={bannerTitle} tabOptions={{ markDone: true, disableFutureTabs: true diff --git a/src/admin/components/Fields/ChipFieldArray.tsx b/src/admin/components/Fields/ChipFieldArray.tsx new file mode 100644 index 000000000..a48b74a4c --- /dev/null +++ b/src/admin/components/Fields/ChipFieldArray.tsx @@ -0,0 +1,38 @@ +import classNames from "classnames"; +import React from "react"; +import { ArrayField, ArrayFieldProps, ChipField, FunctionField, SingleFieldList } from "react-admin"; + +interface ChipFieldArrayProps extends Omit { + data: { id: string; label: string; className?: string }[]; + emptyText?: string; +} + +const ChipFieldArray: React.FC = ({ data, emptyText, ...props }) => { + if (!data.length) { + return ( + + {emptyText ?? "Not Provided"} + + ); + } + + return ( + + + + record ? ( + + ) : null + } + /> + + + ); +}; + +export default ChipFieldArray; diff --git a/src/admin/components/Fields/MapField.tsx b/src/admin/components/Fields/MapField.tsx index b110c5163..e282d4564 100644 --- a/src/admin/components/Fields/MapField.tsx +++ b/src/admin/components/Fields/MapField.tsx @@ -34,16 +34,17 @@ const MapField = ({ source, emptyText = "Not Provided" }: MapFieldProps) => { } ); - const setBbboxAndZoom = async () => { - if (projectPolygon?.project_polygon?.poly_uuid) { - const bbox = await fetchGetV2TerrafundPolygonBboxUuid({ - pathParams: { uuid: projectPolygon.project_polygon?.poly_uuid } - }); - const bounds: any = bbox.bbox; - setPolygonBbox(bounds); - } - }; useEffect(() => { + const setBbboxAndZoom = async () => { + if (projectPolygon?.project_polygon?.poly_uuid) { + const bbox = await fetchGetV2TerrafundPolygonBboxUuid({ + pathParams: { uuid: projectPolygon.project_polygon?.poly_uuid } + }); + const bounds: any = bbox.bbox; + setPolygonBbox(bounds); + } + }; + const getDataProjectPolygon = async () => { if (!projectPolygon?.project_polygon) { setPolygonDataMap({ [FORM_POLYGONS]: [] }); diff --git a/src/admin/components/ResourceTabs/AuditLogTab/AuditLogTab.tsx b/src/admin/components/ResourceTabs/AuditLogTab/AuditLogTab.tsx index 0dcc8dc09..9d365b30a 100644 --- a/src/admin/components/ResourceTabs/AuditLogTab/AuditLogTab.tsx +++ b/src/admin/components/ResourceTabs/AuditLogTab/AuditLogTab.tsx @@ -57,6 +57,7 @@ const AuditLogTab: FC = ({ label, entity, ...rest }) => { useEffect(() => { refetch(); loadEntityList(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [buttonToggle]); const isSite = buttonToggle === AuditLogButtonStates.SITE; diff --git a/src/admin/components/ResourceTabs/ChangeRequestsTab/ChangeRequestsTab.tsx b/src/admin/components/ResourceTabs/ChangeRequestsTab/ChangeRequestsTab.tsx index 67fba8d43..7a2225294 100644 --- a/src/admin/components/ResourceTabs/ChangeRequestsTab/ChangeRequestsTab.tsx +++ b/src/admin/components/ResourceTabs/ChangeRequestsTab/ChangeRequestsTab.tsx @@ -56,7 +56,10 @@ const ChangeRequestsTab: FC = ({ label, entity, singularEntity, ...rest // @ts-ignore const form = currentValues?.data?.form; - const formSteps = useMemo(() => (form == null ? [] : getCustomFormSteps(form, t, undefined, framework)), [form, t]); + const formSteps = useMemo( + () => (form == null ? [] : getCustomFormSteps(form, t, undefined, framework)), + [form, framework, t] + ); const formChanges = useFormChanges(current, changes, formSteps ?? []); const numFieldsAffected = useMemo( () => diff --git a/src/admin/components/ResourceTabs/InformationTab/components/ProjectInformationAside/QuickActions.tsx b/src/admin/components/ResourceTabs/InformationTab/components/ProjectInformationAside/QuickActions.tsx index 896d1e261..de47cee3a 100644 --- a/src/admin/components/ResourceTabs/InformationTab/components/ProjectInformationAside/QuickActions.tsx +++ b/src/admin/components/ResourceTabs/InformationTab/components/ProjectInformationAside/QuickActions.tsx @@ -27,9 +27,14 @@ const QuickActions: FC = () => { } }).then((response: any) => { if (entity === "shapefiles") { - const jsonString = JSON.stringify(response, null, 2); - const fileBlob = new Blob([jsonString], { type: "application/geo+json" }); - downloadFileBlob(fileBlob, `${record.name}_polygons.geojson`); + const exportName = `${record.name}_polygons.geojson`; + if (response instanceof Blob) { + downloadFileBlob(response, exportName); + } else { + const jsonString = JSON.stringify(response, null, 2); + const fileBlob = new Blob([jsonString], { type: "application/geo+json" }); + downloadFileBlob(fileBlob, exportName); + } } else { downloadFileBlob(response, `${record.name} ${entity.replace("-reports", "")} reports.csv`); } diff --git a/src/admin/components/ResourceTabs/InformationTab/components/ReportInformationAside/HighLevelMetrics.tsx b/src/admin/components/ResourceTabs/InformationTab/components/ReportInformationAside/HighLevelMetrics.tsx index 54cfd7036..34dbfbf40 100644 --- a/src/admin/components/ResourceTabs/InformationTab/components/ReportInformationAside/HighLevelMetrics.tsx +++ b/src/admin/components/ResourceTabs/InformationTab/components/ReportInformationAside/HighLevelMetrics.tsx @@ -56,7 +56,7 @@ const HighLevelMetics: FC = () => { diff --git a/src/admin/components/ResourceTabs/MonitoredTab/components/DataCard.tsx b/src/admin/components/ResourceTabs/MonitoredTab/components/DataCard.tsx index 36599b0a1..67d7bebb5 100644 --- a/src/admin/components/ResourceTabs/MonitoredTab/components/DataCard.tsx +++ b/src/admin/components/ResourceTabs/MonitoredTab/components/DataCard.tsx @@ -656,7 +656,7 @@ const DataCard = ({ if (selectPolygonFromMap?.isOpen) { setSelectPolygonFromMap?.({ isOpen: false, uuid: "" }); } - }, [selectPolygonFromMap]); + }, [selectPolygonFromMap, setSelectPolygonFromMap]); const dateRunIndicator = polygonsIndicator?.[polygonsIndicator.length - 1] ? format(new Date(polygonsIndicator?.[polygonsIndicator.length - 1]?.created_at!), "dd/MM/yyyy") diff --git a/src/admin/components/ResourceTabs/MonitoredTab/components/MonitoredCharts.tsx b/src/admin/components/ResourceTabs/MonitoredTab/components/MonitoredCharts.tsx index db9f8afb7..53fd944ff 100644 --- a/src/admin/components/ResourceTabs/MonitoredTab/components/MonitoredCharts.tsx +++ b/src/admin/components/ResourceTabs/MonitoredTab/components/MonitoredCharts.tsx @@ -6,13 +6,15 @@ import { When } from "react-if"; import SimpleBarChart from "@/pages/dashboard/charts/SimpleBarChart"; import GraphicIconDashboard from "@/pages/dashboard/components/GraphicIconDashboard"; import SecDashboard from "@/pages/dashboard/components/SecDashboard"; -import { TOTAL_HECTARES_UNDER_RESTORATION_TOOLTIP } from "@/pages/dashboard/constants/tooltips"; import EcoRegionDoughnutChart from "./EcoRegionDoughnutChart"; import { LoadingState } from "./MonitoredLoading"; import { NoDataState } from "./NoDataState"; import TreeLossBarChart from "./TreesLossBarChart"; +const TOTAL_HECTARES_UNDER_RESTORATION_TOOLTIP = + "Total land area measured in hectares with active restoration interventions, tallied by the total area of polygons submitted by projects."; + const ChartContainer = ({ children, isLoading, diff --git a/src/admin/components/ResourceTabs/MonitoredTab/hooks/useMonitoredData.ts b/src/admin/components/ResourceTabs/MonitoredTab/hooks/useMonitoredData.ts index 00d374bd9..4a96ac3b4 100644 --- a/src/admin/components/ResourceTabs/MonitoredTab/hooks/useMonitoredData.ts +++ b/src/admin/components/ResourceTabs/MonitoredTab/hooks/useMonitoredData.ts @@ -234,16 +234,16 @@ export const useMonitoredData = (entity?: EntityName, entity_uuid?: string) => { ? totalPolygonsApproved : totalPolygonsApproved! - Object?.keys(dataToMissingPolygonVerify ?? {})?.length; - const verifySlug = async (slug: string) => - fetchGetV2IndicatorsEntityUuidSlugVerify({ - pathParams: { - entity: entity!, - uuid: entity_uuid!, - slug: slug! - } - }); - useEffect(() => { + const verifySlug = async (slug: string) => + fetchGetV2IndicatorsEntityUuidSlugVerify({ + pathParams: { + entity: entity!, + uuid: entity_uuid!, + slug: slug! + } + }); + const fetchSlugs = async () => { setIsLoadingVerify(true); const slugVerify = await Promise.all(SLUGS_INDICATORS.map(verifySlug)); @@ -269,13 +269,13 @@ export const useMonitoredData = (entity?: EntityName, entity_uuid?: string) => { }); }; setAnalysisToSlug(slugToAnalysis); - await setDropdownAnalysisOptions(updateTitleDropdownOptions); + setDropdownAnalysisOptions(updateTitleDropdownOptions); setIsLoadingVerify(false); }; if (modalOpened(ModalId.MODAL_RUN_ANALYSIS)) { fetchSlugs(); } - }, [entity]); + }, [entity, entity_uuid, modalOpened]); return { polygonsIndicator: filteredPolygons, diff --git a/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/PolygonDrawer.tsx b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/PolygonDrawer.tsx index 213efaaa2..8c69703e0 100644 --- a/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/PolygonDrawer.tsx +++ b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/PolygonDrawer.tsx @@ -170,6 +170,7 @@ const PolygonDrawer = ({ showLoader(); getValidations({ queryParams: { uuid: polygonSelected } }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [checkPolygonValidation]); useEffect(() => { @@ -195,7 +196,7 @@ const PolygonDrawer = ({ setSelectedPolygonData({}); setStatusSelectedPolygon(""); } - }, [polygonSelected, sitePolygonData]); + }, [polygonSelected, setStatusSelectedPolygon, sitePolygonData]); useEffect(() => { if (openEditNewPolygon) { setButtonToogle(true); @@ -230,7 +231,7 @@ const PolygonDrawer = ({ fetchCriteriaValidation(); setSelectPolygonVersion(selectedPolygonData); - }, [buttonToogle, selectedPolygonData]); + }, [buttonToogle, polygonSelected, selectedPolygonData]); const { data: polygonVersions, @@ -252,13 +253,13 @@ const PolygonDrawer = ({ setIsLoadingDropdown(false); }; onLoading(); - }, [isOpenPolygonDrawer]); + }, [isOpenPolygonDrawer, refetchPolygonVersions]); useEffect(() => { if (selectedPolygonData && isEmpty(selectedPolygonData as SitePolygon) && isEmpty(polygonSelected)) { setSelectedPolygonData(selectPolygonVersion); } - }, [selectPolygonVersion]); + }, [polygonSelected, selectPolygonVersion, selectedPolygonData]); const runFixPolygonOverlaps = () => { if (polygonSelected) { diff --git a/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/components/PolygonValidation.tsx b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/components/PolygonValidation.tsx index 1cb2c8a8b..6bf203020 100644 --- a/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/components/PolygonValidation.tsx +++ b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/components/PolygonValidation.tsx @@ -99,7 +99,7 @@ const PolygonValidation = (props: ICriteriaCheckProps) => { Last check at {formattedDate(lastValidationDate)} - + {menu.map(item => ( diff --git a/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/components/VersionHistory.tsx b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/components/VersionHistory.tsx index bb8af6a2c..058d852c3 100644 --- a/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/components/VersionHistory.tsx +++ b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/components/VersionHistory.tsx @@ -53,7 +53,7 @@ const VersionHistory = ({ setStatusSelectedPolygon?: any; data: GetV2SitePolygonUuidVersionsResponse | []; isLoadingVersions: boolean; - refetch: () => void; + refetch: () => Promise; isLoadingDropdown: boolean; setIsLoadingDropdown: Dispatch>; setPolygonFromMap: Dispatch>; @@ -72,6 +72,7 @@ const VersionHistory = ({ useEffect(() => { refetch(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectPolygonVersion]); useEffect(() => { @@ -79,6 +80,7 @@ const VersionHistory = ({ uploadFiles(); setSaveFlags(false); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [files, saveFlags]); const getFileType = (file: UploadedFile) => { @@ -305,7 +307,7 @@ const VersionHistory = ({ }; reloadVersionList(); } - }, [polygonFromMap]); + }, [polygonFromMap, refetch, setIsLoadingDropdown]); return ( diff --git a/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonItem.tsx b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonItem.tsx index 6a431a492..d95c7d50f 100644 --- a/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonItem.tsx +++ b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonItem.tsx @@ -66,7 +66,7 @@ const PolygonItem = ({ } else if (criteriaData?.criteria_list && criteriaData.criteria_list.length === 0) { setValidationStatus("notChecked"); } - }, [polygonMap]); + }, [polygonMap, uuid]); const handleCheckboxClick = () => { onCheckboxChange(uuid, !isChecked); diff --git a/src/admin/components/ResourceTabs/PolygonReviewTab/index.tsx b/src/admin/components/ResourceTabs/PolygonReviewTab/index.tsx index 7550902de..e40e77e37 100644 --- a/src/admin/components/ResourceTabs/PolygonReviewTab/index.tsx +++ b/src/admin/components/ResourceTabs/PolygonReviewTab/index.tsx @@ -3,7 +3,7 @@ import Box from "@mui/material/Box"; import LinearProgress from "@mui/material/LinearProgress"; import { useT } from "@transifex/react"; import { LngLatBoundsLike } from "mapbox-gl"; -import { FC, useEffect, useState } from "react"; +import { FC, useCallback, useEffect, useState } from "react"; import { TabbedShowLayout, TabProps, useShowContext } from "react-admin"; import { Else, If, Then } from "react-if"; @@ -167,16 +167,39 @@ const PolygonReviewTab: FC = props => { const { openNotification } = useNotificationContext(); + const onSave = (geojson: any, record: any) => { + storePolygon(geojson, record, refetch, setPolygonFromMap, refreshEntity); + }; + const mapFunctions = useMap(onSave); + + const flyToPolygonBounds = useCallback( + async (uuid: string) => { + const bbox: PolygonBboxResponse = await fetchGetV2TerrafundPolygonBboxUuid({ pathParams: { uuid } }); + const bboxArray = bbox?.bbox; + const { map } = mapFunctions; + if (bboxArray && map?.current) { + const bounds: LngLatBoundsLike = [ + [bboxArray[0], bboxArray[1]], + [bboxArray[2], bboxArray[3]] + ]; + map.current.fitBounds(bounds, { + padding: 100, + linear: false + }); + } else { + Log.error("Bounding box is not in the expected format"); + } + }, + [mapFunctions] + ); + useEffect(() => { if (selectPolygonFromMap?.uuid) { setPolygonFromMap(selectPolygonFromMap); flyToPolygonBounds(selectPolygonFromMap.uuid); } - }, [polygonList]); - const onSave = (geojson: any, record: any) => { - storePolygon(geojson, record, refetch, setPolygonFromMap, refreshEntity); - }; - const mapFunctions = useMap(onSave); + }, [flyToPolygonBounds, polygonList, selectPolygonFromMap]); + const { data: sitePolygonData, refetch, @@ -234,24 +257,6 @@ const PolygonReviewTab: FC = props => { const { openModal, closeModal } = useModalContext(); - const flyToPolygonBounds = async (uuid: string) => { - const bbox: PolygonBboxResponse = await fetchGetV2TerrafundPolygonBboxUuid({ pathParams: { uuid } }); - const bboxArray = bbox?.bbox; - const { map } = mapFunctions; - if (bboxArray && map?.current) { - const bounds: LngLatBoundsLike = [ - [bboxArray[0], bboxArray[1]], - [bboxArray[2], bboxArray[3]] - ]; - map.current.fitBounds(bounds, { - padding: 100, - linear: false - }); - } else { - Log.error("Bounding box is not in the expected format"); - } - }; - const deletePolygon = (uuid: string) => { fetchDeleteV2TerrafundPolygonUuid({ pathParams: { uuid } }) .then((response: DeletePolygonProps | undefined) => { @@ -288,6 +293,7 @@ const PolygonReviewTab: FC = props => { uploadFiles(); setSaveFlags(false); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [files, saveFlags]); useEffect(() => { @@ -295,22 +301,22 @@ const PolygonReviewTab: FC = props => { openNotification("error", t("Error uploading file"), t(errorMessage)); setErrorMessage(null); } - }, [errorMessage]); + }, [errorMessage, openNotification, t]); useEffect(() => { setPolygonData(sitePolygonData); - }, [loading]); + }, [loading, setPolygonData, sitePolygonData]); useEffect(() => { setPolygonCriteriaMap(polygonCriteriaMap); - }, [polygonCriteriaMap]); + }, [polygonCriteriaMap, setPolygonCriteriaMap]); useEffect(() => { if (shouldRefetchValidation) { refetch(); setShouldRefetchValidation(false); } - }, [shouldRefetchValidation]); + }, [refetch, setShouldRefetchValidation, shouldRefetchValidation]); const uploadFiles = async () => { const uploadPromises = []; closeModal(ModalId.ADD_POLYGON); @@ -578,9 +584,7 @@ const PolygonReviewTab: FC = props => { ); }; - const isLoading = ctxLoading; - - if (isLoading) return null; + if (ctxLoading) return null; const tableItemMenu = (props: TableItemMenuProps) => [ { diff --git a/src/admin/components/ShowTitle/index.tsx b/src/admin/components/ShowTitle/index.tsx index f9bcaca35..939440eef 100644 --- a/src/admin/components/ShowTitle/index.tsx +++ b/src/admin/components/ShowTitle/index.tsx @@ -1,5 +1,5 @@ import { Chip } from "@mui/material"; -import { Link, RaRecord, useRecordContext, useResourceContext, useShowContext } from "react-admin"; +import { Link, useGetRecordRepresentation, useRecordContext, useResourceContext, useShowContext } from "react-admin"; import { Else, If, Then, When } from "react-if"; import Text from "@/components/elements/Text/Text"; @@ -7,18 +7,17 @@ import Icon, { IconNames } from "@/components/extensive/Icon/Icon"; interface IProps { moduleName?: string; - getTitle: (record: RaRecord) => string; } const ShowTitle = (props: IProps) => { const record = useRecordContext(); const { isLoading } = useShowContext(); - const titleText = props.getTitle(record); const resource = useResourceContext(); + const titleGetter = useGetRecordRepresentation(resource); - const title = titleText && ( + const title = ( <> - {titleText} + {titleGetter(record)} {record?.is_test && } > ); diff --git a/src/admin/hooks/useCanUserEdit.ts b/src/admin/hooks/useCanUserEdit.ts index 1dd24429d..31da7db52 100644 --- a/src/admin/hooks/useCanUserEdit.ts +++ b/src/admin/hooks/useCanUserEdit.ts @@ -1,29 +1,30 @@ import { useMemo } from "react"; -import { useGetUserRole } from "./useGetUserRole"; +import { getRoleData, useGetUserRole } from "./useGetUserRole"; -export const useCanUserEdit = (record: any, resource: string) => { - const { isFrameworkAdmin, isSuperAdmin, role } = useGetUserRole(); - - const canEdit = useMemo(() => { - if (resource === "user") { - if (isSuperAdmin) { - return true; - } - if (record?.role === "admin-super") { - return false; - } - if (record?.role === "project-developer" || record?.role === "project-manager") { - return true; - } - if (isFrameworkAdmin) { - if (record?.role === role) { - return true; - } - return false; - } +export const userCanEdit = ( + record: any, + resource: string, + { role, isSuperAdmin, isFrameworkAdmin }: ReturnType +) => { + if (resource === "user") { + if (isSuperAdmin) { + return true; + } + if (record?.role === "admin-super") { + return false; + } + if (record?.role === "project-developer" || record?.role === "project-manager") { + return true; } - return !!record; - }, [record, resource, isFrameworkAdmin, isSuperAdmin]); - return canEdit; + if (isFrameworkAdmin) { + return record?.role === role; + } + } + return record != null; +}; + +export const useCanUserEdit = (record: any, resource: string) => { + const roleData = useGetUserRole(); + return useMemo(() => userCanEdit(record, resource, roleData), [record, resource, roleData]); }; diff --git a/src/admin/hooks/useGetUserRole.ts b/src/admin/hooks/useGetUserRole.ts index 51fba7984..cb3c47450 100644 --- a/src/admin/hooks/useGetUserRole.ts +++ b/src/admin/hooks/useGetUserRole.ts @@ -1,14 +1,17 @@ +import { useMemo } from "react"; import { useGetIdentity } from "react-admin"; -export const useGetUserRole = () => { - const { data } = useGetIdentity(); - const user: any = data || {}; +import { TMUserIdentity } from "@/admin/apiProvider/authProvider"; + +export const getRoleData = (primaryRole?: string) => ({ + role: primaryRole, + isSuperAdmin: primaryRole === "admin-super", + isPPCAdmin: primaryRole === "admin-ppc", + isPPCTerrafundAdmin: primaryRole === "admin-terrafund", + isFrameworkAdmin: primaryRole?.includes("admin-") +}); - return { - role: user.primaryRole, - isSuperAdmin: user.primaryRole === "admin-super", - isPPCAdmin: user.primaryRole === "admin-ppc", - isPPCTerrafundAdmin: user.primaryRole === "admin-terrafund", - isFrameworkAdmin: user.primaryRole && user.primaryRole.includes("admin-") - }; +export const useGetUserRole = () => { + const user = useGetIdentity().data as TMUserIdentity | undefined; + return useMemo(() => getRoleData(user?.primaryRole), [user?.primaryRole]); }; diff --git a/src/admin/modules/application/components/ApplicationShow.tsx b/src/admin/modules/application/components/ApplicationShow.tsx index b5eb1177f..9b8300e07 100644 --- a/src/admin/modules/application/components/ApplicationShow.tsx +++ b/src/admin/modules/application/components/ApplicationShow.tsx @@ -1,20 +1,15 @@ import { Show } from "react-admin"; import ShowActions from "@/admin/components/Actions/ShowActions"; -import ShowTitle from "@/admin/components/ShowTitle"; import ApplicationShowAside from "./ApplicationShowAside"; import { ApplicationTabs } from "./ApplicationTabs"; -export const ApplicationShow = () => { - return ( - <> - `${item?.id}`} />} - actions={} - aside={} - > - - - > - ); -}; + +export const ApplicationShow = () => ( + } + aside={} + > + + +); diff --git a/src/admin/modules/form/components/CopyFormToOtherEnv.tsx b/src/admin/modules/form/components/CopyFormToOtherEnv.tsx index 20e76da82..22c254926 100644 --- a/src/admin/modules/form/components/CopyFormToOtherEnv.tsx +++ b/src/admin/modules/form/components/CopyFormToOtherEnv.tsx @@ -40,7 +40,7 @@ export const CopyFormToOtherEnv = () => { } }); const { register, handleSubmit, formState, getValues } = formHook; - Log.info(getValues(), formState.errors); + Log.info("Copy form values", { ...getValues(), formErrors: formState.errors }); const copyToDestinationEnv = async ({ env: baseUrl, title: formTitle, framework_key, ...body }: any) => { const linkedFieldsData: any = await fetchGetV2FormsLinkedFieldListing({}); diff --git a/src/admin/modules/form/components/FormBuilder/QuestionArrayInput.tsx b/src/admin/modules/form/components/FormBuilder/QuestionArrayInput.tsx index 73ecb08f1..3a3616a4c 100644 --- a/src/admin/modules/form/components/FormBuilder/QuestionArrayInput.tsx +++ b/src/admin/modules/form/components/FormBuilder/QuestionArrayInput.tsx @@ -9,6 +9,7 @@ import { FormDataConsumer, FormDataConsumerRenderParams, minLength, + NumberInput, required, TextInput, useInput @@ -109,6 +110,28 @@ export const QuestionArrayInput = ({ height="75px" /> )} + + {({ scopedFormData, getSource }: FormDataConsumerRenderParams) => { + if (!scopedFormData || !getSource) return null; + const field = getFieldByUUID(scopedFormData.linked_field_key); + return field?.input_type == "long-text" ? ( + <> + + + > + ) : ( + <>> + ); + }} + { return !record?.published ? ( { ) : null; }; -export const FormEdit = () => { - return ( - } - title={ record?.title} moduleName="Form" />} - sx={{ marginBottom: 2 }} - > - - } noValidate paddingY="1.5rem"> - - - - ); -}; +export const FormEdit = () => ( + } + title={} + sx={{ marginBottom: 2 }} + > + + } noValidate paddingY="1.5rem"> + + + +); diff --git a/src/admin/modules/fundingProgrammes/components/FundingProgrammeShow.tsx b/src/admin/modules/fundingProgrammes/components/FundingProgrammeShow.tsx index da0ae3a5d..272e14409 100644 --- a/src/admin/modules/fundingProgrammes/components/FundingProgrammeShow.tsx +++ b/src/admin/modules/fundingProgrammes/components/FundingProgrammeShow.tsx @@ -1,31 +1,29 @@ import { Divider } from "@mui/material"; import { ImageField, Show, SimpleShowLayout, TabbedShowLayout, TextField } from "react-admin"; +import ShowActions from "@/admin/components/Actions/ShowActions"; import FundingProgrammeStages from "@/admin/modules/fundingProgrammes/components/FundingProgrammeStages"; import FundingProgrammeOrganisations from "./FundingProgrammeOrganisations"; -import FundingProgrammeTitle from "./FundingProgrammeTitle"; -export const FundingProgrammeShow = () => { - return ( - } sx={{ mb: 2 }}> - - - - - - - - - - - - - - - - - - - ); -}; +export const FundingProgrammeShow = () => ( + } sx={{ mb: 2 }}> + + + + + + + + + + + + + + + + + + +); diff --git a/src/admin/modules/impactStories/components/ImpactStories.tsx b/src/admin/modules/impactStories/components/ImpactStories.tsx new file mode 100644 index 000000000..88fa3c9d9 --- /dev/null +++ b/src/admin/modules/impactStories/components/ImpactStories.tsx @@ -0,0 +1,46 @@ +import { Create, Edit, SimpleForm } from "react-admin"; + +import ImpactStoryForm from "./ImpactStoryForm"; + +const transformData = (data: any) => { + const transformedData = { + organization_id: data.organization?.uuid, + title: data.title, + date: data.date, + category: data.category, + content: data.content, + status: data.status, + thumbnail: data.thumbnail + }; + + return Object.fromEntries(Object.entries(transformedData).filter(([_, value]) => value != null)); +}; +export const ImpactStoriesCreate: React.FC = () => ( + + + + + +); + +export const ImpactStoriesEdit: React.FC = () => ( + + + + + +); diff --git a/src/admin/modules/impactStories/components/ImpactStoriesEditForm.tsx b/src/admin/modules/impactStories/components/ImpactStoriesEditForm.tsx new file mode 100644 index 000000000..93c9e1bd9 --- /dev/null +++ b/src/admin/modules/impactStories/components/ImpactStoriesEditForm.tsx @@ -0,0 +1,118 @@ +// import { t } from "@transifex/native"; +import React from "react"; + +import Button from "@/components/elements/Button/Button"; +import Dropdown from "@/components/elements/Inputs/Dropdown/Dropdown"; +import { VARIANT_DROPDOWN_IMPACT_STORY } from "@/components/elements/Inputs/Dropdown/DropdownVariant"; +import FileInput from "@/components/elements/Inputs/FileInput/FileInput"; +import { VARIANT_FILE_INPUT_IMPACT_STORY } from "@/components/elements/Inputs/FileInput/FileInputVariants"; +import Input from "@/components/elements/Inputs/Input/Input"; +import Text from "@/components/elements/Text/Text"; + +// import { ModalId } from "@/components/extensive/Modal/ModalConst"; +// import ModalStory from "@/components/extensive/Modal/ModalStory"; +// import { useModalContext } from "@/context/modal.provider"; +import QuillEditor from "./QuillEditor"; + +const ImpactStoriesEditForm = () => { + // const { openModal } = useModalContext(); + const ModalStoryOpen = (uuid: string) => { + // openModal(ModalId.MODAL_STORY, ); + }; + return ( + + + Edit Impact Story + + + + + + + {}} + labelClassName="capitalize text-14-bold" + className="text-14-light" + multiSelect={true} + variant={VARIANT_DROPDOWN_IMPACT_STORY} + /> + + + {}} + variant={VARIANT_FILE_INPUT_IMPACT_STORY} + files={[]} + allowMultiple={true} + label="Thumbnail" + labelClassName="capitalize text-14-bold" + classNameTextOr="hidden" + descriptionInput={ + + documents or images to help reviewer + + } + /> + + + + + Content + + + + + + Delete + + Save as draft + ModalStoryOpen("impact-story-1")}> + Preview + + Publish + + + + + ); +}; + +export default ImpactStoriesEditForm; diff --git a/src/admin/modules/impactStories/components/ImpactStoriesList.tsx b/src/admin/modules/impactStories/components/ImpactStoriesList.tsx new file mode 100644 index 000000000..cd06c75a1 --- /dev/null +++ b/src/admin/modules/impactStories/components/ImpactStoriesList.tsx @@ -0,0 +1,173 @@ +import { Stack } from "@mui/material"; +import { FC } from "react"; +import { + AutocompleteInput, + Datagrid, + DateField, + EditButton, + FunctionField, + List, + ReferenceInput, + SearchInput, + SelectInput, + TextField, + WrapperField +} from "react-admin"; + +import ListActionsImpactStories from "@/admin/components/Actions/ListActionsImpactStories"; +import CustomDeleteWithConfirmButton from "@/admin/components/Buttons/CustomDeleteWithConfirmButton"; +import CustomChipField from "@/admin/components/Fields/CustomChipField"; +import Menu from "@/components/elements/Menu/Menu"; +import { MENU_PLACEMENT_BOTTOM_LEFT } from "@/components/elements/Menu/MenuVariant"; +import Text from "@/components/elements/Text/Text"; +import Icon, { IconNames } from "@/components/extensive/Icon/Icon"; +import { getCountriesOptions } from "@/constants/options/countries"; +import { getChangeRequestStatusOptions, getPolygonOptions, getStatusOptions } from "@/constants/options/status"; +import { useUserFrameworkChoices } from "@/constants/options/userFrameworksChoices"; +import { optionToChoices } from "@/utils/options"; + +import modules from "../.."; + +const monitoringDataChoices = [ + { + id: "0", + name: "No" + }, + { + id: "1", + name: "Yes" + } +]; +const tableMenu = [ + { + id: "1", + render: () => + }, + { + id: "2", + render: () => { + return ( + + + + ); + } + } +]; + +const ImpactStoriesDataGrid: FC = () => { + return ( + + + { + return ; + }} + /> + + + record.organization?.countries?.length > 0 + ? record.organization.countries.map((c: any) => c.label).join(", ") + : "No country" + } + /> + + + + + + ); +}; + +export const ImpactStoriesList: FC = () => { + const frameworkInputChoices = useUserFrameworkChoices(); + + const filters = [ + , + , + + + , + + + , + , + , + , + , + + ]; + + return ( + <> + + + Impact Stories + + + + } filters={filters}> + + + > + ); +}; diff --git a/src/admin/modules/impactStories/components/ImpactStoryForm.tsx b/src/admin/modules/impactStories/components/ImpactStoryForm.tsx new file mode 100644 index 000000000..773cebadc --- /dev/null +++ b/src/admin/modules/impactStories/components/ImpactStoryForm.tsx @@ -0,0 +1,210 @@ +import { Box } from "@mui/material"; +import { memo } from "react"; +import { ReferenceInput, required } from "react-admin"; +import { useFormContext } from "react-hook-form"; + +import { maxFileSize } from "@/admin/utils/forms"; +import Button from "@/components/elements/Button/Button"; +import Dropdown from "@/components/elements/Inputs/Dropdown/Dropdown"; +import { VARIANT_DROPDOWN_IMPACT_STORY } from "@/components/elements/Inputs/Dropdown/DropdownVariant"; +import Input from "@/components/elements/Inputs/Input/Input"; +import Text from "@/components/elements/Text/Text"; +import Icon, { IconNames } from "@/components/extensive/Icon/Icon"; +import { ModalId } from "@/components/extensive/Modal/ModalConst"; +import ModalStory from "@/components/extensive/Modal/ModalStory"; +import { useLoading } from "@/context/loaderAdmin.provider"; +import { useModalContext } from "@/context/modal.provider"; +import { useOnMount } from "@/hooks/useOnMount"; + +import modules from "../.."; +import { useImpactStoryForm } from "../hooks/useImpactStoryForm"; +import QuillEditor from "./QuillEditor"; +import StyledFileUploadInput from "./StyledFileUploadInput"; +import { StyledAutocompleteInput, StyledReferenceInput } from "./StyledInputs"; + +export interface ImpactCategory { + title: string; + value: string; +} + +export interface ImpactStoryFormProps { + mode: "create" | "edit"; +} + +export const IMPACT_CATEGORIES: ImpactCategory[] = [ + { title: "Business development/fundraising", value: "business-dev-fund" }, + { title: "Community benefits", value: "community-benefits" }, + { title: "Livelihoods strengthening", value: "livelihoods-strengthening" }, + { title: "Gender equity", value: "gender-equity" }, + { title: "Youth engagement", value: "youth-engagement" }, + { title: "Ecosystem services", value: "ecosystem-services" }, + { title: "Climate resilience", value: "climate-resilience" }, + { title: "Institutional capacity", value: "institutional-capacity" }, + { title: "Technical capacity", value: "technical-capacity" } +]; + +const ImpactStoryForm: React.FC = memo(({ mode }) => { + const { initialValues, handlers } = useImpactStoryForm(mode); + const { openModal } = useModalContext(); + const { getValues, trigger } = useFormContext(); + const { showLoader, hideLoader } = useLoading(); + useOnMount(() => hideLoader); + const handlePreviewClick = () => { + const formValues = getValues(); + const previewData = { + uuid: formValues.uuid ? formValues.uuid : formValues?.data?.uuid, + title: formValues.title ? formValues.title : formValues?.data?.title, + date: formValues.date ? formValues.date : formValues?.data?.date, + content: formValues.content ? JSON.parse(formValues.content) : JSON.parse(formValues.data?.content), + category: formValues.category ? formValues.category : formValues.data?.category, + thumbnail: + formValues.thumbnail instanceof File ? URL.createObjectURL(formValues.thumbnail) : formValues.thumbnail || "", + organization: { + name: formValues?.organization?.name + ? formValues?.organization?.name + : formValues?.data?.organization.name ?? "", + category: formValues?.category ? formValues?.category : formValues?.data?.category, + country: + formValues?.organization?.countries?.length > 0 + ? formValues.organization.countries.map((c: any) => c.label).join(", ") + : formValues?.data?.organization?.countries?.length > 0 + ? formValues.data.organization.countries.map((c: any) => c.label).join(", ") + : "No country", + facebook_url: formValues?.organization?.facebook_url + ? formValues?.organization?.facebook_url + : formValues?.data?.organization?.facebook_url, + instagram_url: formValues?.organization?.instagram_url + ? formValues?.organization?.instagram_url + : formValues?.data?.organization?.instagram_url, + linkedin_url: formValues?.organization?.linkedin_url + ? formValues?.organization?.linkedin_url + : formValues?.data?.organization?.linkedin_url, + twitter_url: formValues?.organization?.twitter_url + ? formValues?.organization?.twitter_url + : formValues?.data?.organization?.twitter_url + }, + status: formValues?.status ? formValues?.status : formValues?.data?.status + }; + + openModal(ModalId.MODAL_STORY, ); + }; + const handleSave = async (status: "draft" | "published") => { + const isValid = await trigger(); + if (!isValid) { + return; + } + showLoader(); + handlers.handleStatusChange(status); + }; + return ( + + + {mode === "create" ? "Create Impact Story" : "Edit Impact Story"} + + + + + handlers.handleTitleChange(e.target.value)} + required + /> + handlers.handleDateChange(e.target.value)} + required + /> + + + + + + + + + handlers.handleImpactCategoryChange(e as string[])} + labelClassName="capitalize text-14-bold" + className="text-14-light" + multiSelect={true} + variant={VARIANT_DROPDOWN_IMPACT_STORY} + required + /> + + + + + + Click to upload + + + documents or images to help reviewer + + + } + /> + Uploaded + + + + + Content + + + + + + {mode === "edit" && ( + + Delete + + )} + + handleSave("draft")}> + Save as draft + + + Preview + + handleSave("published")}> + Publish + + + + + + ); +}); + +ImpactStoryForm.displayName = "ImpactStoryForm"; + +export default ImpactStoryForm; diff --git a/src/admin/modules/impactStories/components/QuillEditor.tsx b/src/admin/modules/impactStories/components/QuillEditor.tsx new file mode 100644 index 000000000..4023dcbb5 --- /dev/null +++ b/src/admin/modules/impactStories/components/QuillEditor.tsx @@ -0,0 +1,96 @@ +import React, { Component, createRef } from "react"; + +let Quill: any = null; + +if (typeof window !== "undefined") { + Quill = require("quill").default; + require("quill/dist/quill.snow.css"); +} + +interface QuillEditorProps { + value?: string; + onChange?: (content: string) => void; +} + +class QuillEditor extends Component { + private editorRef = createRef(); + private quill?: any; + + componentDidMount() { + if (typeof window !== "undefined" && Quill) { + this.initializeQuill(); + } + } + + componentDidUpdate(prevProps: QuillEditorProps) { + if (this.quill && prevProps.value !== this.props.value) { + this.quill.root.innerHTML = this.props.value || ""; + } + } + + initializeQuill() { + if (this.editorRef.current && !this.quill && Quill) { + this.quill = new Quill(this.editorRef.current, { + theme: "snow", + modules: { + toolbar: [ + [{ header: [1, 2, 3, false] }], + ["bold", "italic", "underline"], + ["link", "blockquote"], + [{ list: "ordered" }, { list: "bullet" }], + ["video"] + ] + }, + bounds: document.body + }); + + this.quill.on("text-change", () => { + if (this.quill && this.props.onChange) { + this.props.onChange(this.quill.root.innerHTML); + } + }); + + if (this.props.value) { + this.quill.root.innerHTML = this.props.value; + } + } + } + + render() { + return ( + + + + + ); + } +} + +export default QuillEditor; diff --git a/src/admin/modules/impactStories/components/StyledFileUploadInput.tsx b/src/admin/modules/impactStories/components/StyledFileUploadInput.tsx new file mode 100644 index 000000000..8c9cf8074 --- /dev/null +++ b/src/admin/modules/impactStories/components/StyledFileUploadInput.tsx @@ -0,0 +1,56 @@ +import React from "react"; + +import { FileUploadInput } from "@/admin/components/Inputs/FileUploadInput"; + +const StyledFileUploadInput = (props: any) => { + return ( + + ); +}; + +export default StyledFileUploadInput; diff --git a/src/admin/modules/impactStories/components/StyledInputs.tsx b/src/admin/modules/impactStories/components/StyledInputs.tsx new file mode 100644 index 000000000..dfb190b09 --- /dev/null +++ b/src/admin/modules/impactStories/components/StyledInputs.tsx @@ -0,0 +1,51 @@ +import { ReactNode } from "react"; +import { AutocompleteInput } from "react-admin"; + +import Text from "@/components/elements/Text/Text"; + +export interface StyledReferenceInputProps { + label: string; + children: ReactNode; +} + +export const StyledAutocompleteInput = (props: any) => ( + +); + +export const StyledReferenceInput: React.FC = ({ label, children }) => ( + + + {label} + + {children} + +); diff --git a/src/admin/modules/impactStories/hooks/useImpactStoryForm.ts b/src/admin/modules/impactStories/hooks/useImpactStoryForm.ts new file mode 100644 index 000000000..da795b8b1 --- /dev/null +++ b/src/admin/modules/impactStories/hooks/useImpactStoryForm.ts @@ -0,0 +1,85 @@ +import { useCallback } from "react"; +import { useNotify, useRecordContext, useRedirect } from "react-admin"; +import { useFormContext } from "react-hook-form"; + +export const useImpactStoryForm = (mode: "create" | "edit") => { + const { setValue, getValues, watch } = useFormContext(); + const status = watch("status"); + const record = useRecordContext(); + const notify = useNotify(); + const redirect = useRedirect(); + + const currentData = mode === "edit" && record?.data ? record.data : record; + const initialValues = { + content: currentData?.content ? JSON.parse(currentData.content) : "", + title: currentData?.title || "", + date: currentData?.date || "", + thumbnail: currentData?.thumbnail, + categories: currentData?.category ? currentData.category : "", + orgUuid: mode === "edit" ? currentData?.organization?.uuid : record?.organization?.uuid + }; + + const handleImpactCategoryChange = useCallback( + (selectedValues: string[]) => { + setValue("category", selectedValues); + }, + [setValue] + ); + + const handleContentChange = useCallback( + (content: string) => { + setValue("content", JSON.stringify(content)); + }, + [setValue] + ); + + const handleTitleChange = useCallback( + (value: string) => { + setValue("title", value); + }, + [setValue] + ); + + const handleDateChange = useCallback( + (value: string) => { + setValue("date", value); + }, + [setValue] + ); + + const handleStatusChange = useCallback( + (status: "draft" | "published") => { + setValue("status", status); + }, + [setValue] + ); + + const handlePreview = useCallback(() => { + const values = getValues(); + notify("Preview mode activated"); + console.log("values", values); + }, [getValues, notify]); + + const handleDelete = useCallback(async () => { + try { + notify("Story deleted successfully"); + redirect("list", "impactStories"); + } catch (error) { + notify("Error deleting story", { type: "error" }); + } + }, [notify, redirect]); + + return { + initialValues, + handlers: { + handleImpactCategoryChange, + handleContentChange, + handleTitleChange, + handleDateChange, + handleStatusChange, + handlePreview, + handleDelete + }, + status + }; +}; diff --git a/src/admin/modules/index.tsx b/src/admin/modules/index.tsx index 98c103770..2bcef56ec 100644 --- a/src/admin/modules/index.tsx +++ b/src/admin/modules/index.tsx @@ -19,6 +19,8 @@ import FundingProgrammeCreate from "./fundingProgrammes/components/FundingProgra import FundingProgrammeEdit from "./fundingProgrammes/components/FundingProgrammeEdit"; import { FundingProgrammeList } from "./fundingProgrammes/components/FundingProgrammeList"; import { FundingProgrammeShow } from "./fundingProgrammes/components/FundingProgrammeShow"; +import { ImpactStoriesCreate, ImpactStoriesEdit } from "./impactStories/components/ImpactStories"; +import { ImpactStoriesList } from "./impactStories/components/ImpactStoriesList"; import { NurseriesList } from "./nurseries/components/NurseriesList"; import NurseryShow from "./nurseries/components/NurseryShow"; import NurseryReportShow from "./nurseryReports/components/NurseryReportShow"; @@ -157,6 +159,13 @@ const validatePolygonFile = { List: ValidatePolygonFileShow }; +const impactStories = { + ResourceName: "impactStories", + List: ImpactStoriesList, + Create: ImpactStoriesCreate, + Edit: ImpactStoriesEdit +}; + const modules = { user, organisation, @@ -174,7 +183,8 @@ const modules = { siteReport, nurseryReport, audit, - validatePolygonFile + validatePolygonFile, + impactStories }; export default modules; diff --git a/src/admin/modules/nurseries/components/NurseryShow.tsx b/src/admin/modules/nurseries/components/NurseryShow.tsx index 0e2c9d6ce..d6d004ae0 100644 --- a/src/admin/modules/nurseries/components/NurseryShow.tsx +++ b/src/admin/modules/nurseries/components/NurseryShow.tsx @@ -1,4 +1,3 @@ -import { FC } from "react"; import { Show, TabbedShowLayout } from "react-admin"; import ShowActions from "@/admin/components/Actions/ShowActions"; @@ -8,27 +7,20 @@ import ChangeRequestsTab from "@/admin/components/ResourceTabs/ChangeRequestsTab import DocumentTab from "@/admin/components/ResourceTabs/DocumentTab/DocumentTab"; import GalleryTab from "@/admin/components/ResourceTabs/GalleryTab/GalleryTab"; import InformationTab from "@/admin/components/ResourceTabs/InformationTab"; -import ShowTitle from "@/admin/components/ShowTitle"; import { RecordFrameworkProvider } from "@/context/framework.provider"; -const NurseryShow: FC = () => { - return ( - record?.name} />} - actions={} - className="-mt-[50px] bg-neutral-100" - > - - - - - - - - - - - ); -}; +const NurseryShow = () => ( + } className="-mt-[50px] bg-neutral-100"> + + + + + + + + + + +); export default NurseryShow; diff --git a/src/admin/modules/nurseryReports/components/NurseryReportShow.tsx b/src/admin/modules/nurseryReports/components/NurseryReportShow.tsx index c21797ef5..378112bb8 100644 --- a/src/admin/modules/nurseryReports/components/NurseryReportShow.tsx +++ b/src/admin/modules/nurseryReports/components/NurseryReportShow.tsx @@ -1,4 +1,3 @@ -import { FC } from "react"; import { Show, TabbedShowLayout } from "react-admin"; import ShowActions from "@/admin/components/Actions/ShowActions"; @@ -8,27 +7,20 @@ import ChangeRequestsTab from "@/admin/components/ResourceTabs/ChangeRequestsTab import DocumentTab from "@/admin/components/ResourceTabs/DocumentTab/DocumentTab"; import GalleryTab from "@/admin/components/ResourceTabs/GalleryTab/GalleryTab"; import InformationTab from "@/admin/components/ResourceTabs/InformationTab"; -import ShowTitle from "@/admin/components/ShowTitle"; import { RecordFrameworkProvider } from "@/context/framework.provider"; -const NurseryReportShow: FC = () => { - return ( - record?.title} />} - actions={} - className="-mt-[50px] bg-neutral-100" - > - - - - - - - - - - - ); -}; +const NurseryReportShow = () => ( + } className="-mt-[50px] bg-neutral-100"> + + + + + + + + + + +); export default NurseryReportShow; diff --git a/src/admin/modules/organisations/components/OrganisationShow.tsx b/src/admin/modules/organisations/components/OrganisationShow.tsx index 1e8cf6302..2334eecf4 100644 --- a/src/admin/modules/organisations/components/OrganisationShow.tsx +++ b/src/admin/modules/organisations/components/OrganisationShow.tsx @@ -21,7 +21,6 @@ import ShowActions from "@/admin/components/Actions/ShowActions"; import { FileArrayField } from "@/admin/components/Fields/FileArrayField"; import MapField from "@/admin/components/Fields/MapField"; import SimpleChipFieldArray from "@/admin/components/Fields/SimpleChipFieldArray"; -import ShowTitle from "@/admin/components/ShowTitle"; import { getCountriesOptions } from "@/constants/options/countries"; import { getFarmersEngagementStrategyOptions, @@ -59,11 +58,7 @@ export const OrganisationShow = () => { return ( <> - } - title={ record?.name} />} - aside={} - > + } aside={}> diff --git a/src/admin/modules/pitch/components/PitchShow.tsx b/src/admin/modules/pitch/components/PitchShow.tsx index 4f6fce2b4..819ff8209 100644 --- a/src/admin/modules/pitch/components/PitchShow.tsx +++ b/src/admin/modules/pitch/components/PitchShow.tsx @@ -15,7 +15,6 @@ import ShowActions from "@/admin/components/Actions/ShowActions"; import { FileArrayField } from "@/admin/components/Fields/FileArrayField"; import MapField from "@/admin/components/Fields/MapField"; import SimpleChipFieldArray from "@/admin/components/Fields/SimpleChipFieldArray"; -import ShowTitle from "@/admin/components/ShowTitle"; import { getCapacityBuildingNeedOptions } from "@/constants/options/capacityBuildingNeeds"; import { getCountriesOptions } from "@/constants/options/countries"; import { getLandTenureOptions } from "@/constants/options/landTenure"; @@ -25,249 +24,239 @@ import { optionToChoices } from "@/utils/options"; import { PitchAside } from "./PitchAside"; -export const PitchShow = () => { - return ( - record?.project_name} />} - actions={} - aside={} - > - - - Objectives - - - - - - - - - - - +export const PitchShow = () => ( + } aside={}> + + + Objectives + + + + + + + + + + + - - - Proposed Project Area - - - - - + + + Proposed Project Area + + + + + - - - Timeline - - - - - - + + + Timeline + + + + + + - - - Land Tenure Strategy - - - + + + Land Tenure Strategy + + + - - - + + + - - - More Information - - - - - + + + More Information + + + + + - + - - - + + + - - - Environmental Impact - - - - - - - - - - - - - - + + + Environmental Impact + + + + + + + + + + + + + + - - - Biophysical characteristics of the project area Sources of tree seedlings for the project - - - - - + + + Biophysical characteristics of the project area Sources of tree seedlings for the project + + + + + - - - Sources of tree seedlings for the project - - - - + + + Sources of tree seedlings for the project + + + + - - - Social Impact - - - New jobs breakdown - - - - - - - - Project beneficiaries breakdown - - - - - - - - + + + Social Impact + + + New jobs breakdown + + + + + + + + Project beneficiaries breakdown + + + + + + + + - - - More Details on Social Impact of Project - - - - - - - - ); -}; + + + More Details on Social Impact of Project + + + + + + + +); diff --git a/src/admin/modules/projectReports/components/ProjectReportShow.tsx b/src/admin/modules/projectReports/components/ProjectReportShow.tsx index abad8a533..fd8af3ee7 100644 --- a/src/admin/modules/projectReports/components/ProjectReportShow.tsx +++ b/src/admin/modules/projectReports/components/ProjectReportShow.tsx @@ -1,4 +1,3 @@ -import { FC } from "react"; import { Show, TabbedShowLayout } from "react-admin"; import ShowActions from "@/admin/components/Actions/ShowActions"; @@ -8,27 +7,20 @@ import ChangeRequestsTab from "@/admin/components/ResourceTabs/ChangeRequestsTab import DocumentTab from "@/admin/components/ResourceTabs/DocumentTab/DocumentTab"; import GalleryTab from "@/admin/components/ResourceTabs/GalleryTab/GalleryTab"; import InformationTab from "@/admin/components/ResourceTabs/InformationTab"; -import ShowTitle from "@/admin/components/ShowTitle"; import { RecordFrameworkProvider } from "@/context/framework.provider"; -const ProjectReportShow: FC = () => { - return ( - record?.title} />} - actions={} - className="-mt-[50px] bg-neutral-100" - > - - - - - - - - - - - ); -}; +const ProjectReportShow = () => ( + } className="-mt-[50px] bg-neutral-100"> + + + + + + + + + + +); export default ProjectReportShow; diff --git a/src/admin/modules/projects/components/ProjectShow.tsx b/src/admin/modules/projects/components/ProjectShow.tsx index 40fca8a9d..17fcc75cf 100644 --- a/src/admin/modules/projects/components/ProjectShow.tsx +++ b/src/admin/modules/projects/components/ProjectShow.tsx @@ -10,7 +10,6 @@ import DocumentTab from "@/admin/components/ResourceTabs/DocumentTab/DocumentTab import GalleryTab from "@/admin/components/ResourceTabs/GalleryTab/GalleryTab"; import InformationTab from "@/admin/components/ResourceTabs/InformationTab"; import MonitoredTab from "@/admin/components/ResourceTabs/MonitoredTab/MonitoredTab"; -import ShowTitle from "@/admin/components/ShowTitle"; import { RecordFrameworkProvider } from "@/context/framework.provider"; import { usePutV2AdminProjectsUUID } from "@/generated/apiComponents"; @@ -33,8 +32,7 @@ const ProjectShow = () => { return ( record?.name} />} - actions={} + actions={} className="-mt-[50px] bg-neutral-100" > diff --git a/src/admin/modules/siteReports/components/SiteReportShow.tsx b/src/admin/modules/siteReports/components/SiteReportShow.tsx index 07b8e1f8e..05b59b648 100644 --- a/src/admin/modules/siteReports/components/SiteReportShow.tsx +++ b/src/admin/modules/siteReports/components/SiteReportShow.tsx @@ -1,4 +1,3 @@ -import { FC } from "react"; import { Show, TabbedShowLayout } from "react-admin"; import ShowActions from "@/admin/components/Actions/ShowActions"; @@ -8,27 +7,20 @@ import ChangeRequestsTab from "@/admin/components/ResourceTabs/ChangeRequestsTab import DocumentTab from "@/admin/components/ResourceTabs/DocumentTab/DocumentTab"; import GalleryTab from "@/admin/components/ResourceTabs/GalleryTab/GalleryTab"; import InformationTab from "@/admin/components/ResourceTabs/InformationTab"; -import ShowTitle from "@/admin/components/ShowTitle"; import { RecordFrameworkProvider } from "@/context/framework.provider"; -const SiteReportShow: FC = () => { - return ( - record?.title} />} - actions={} - className="-mt-[50px] bg-neutral-100" - > - - - - - - - - - - - ); -}; +const SiteReportShow = () => ( + } className="-mt-[50px] bg-neutral-100"> + + + + + + + + + + +); export default SiteReportShow; diff --git a/src/admin/modules/sites/components/SiteShow.tsx b/src/admin/modules/sites/components/SiteShow.tsx index 7662bf547..28fae8eb2 100644 --- a/src/admin/modules/sites/components/SiteShow.tsx +++ b/src/admin/modules/sites/components/SiteShow.tsx @@ -1,4 +1,3 @@ -import { FC } from "react"; import { Show, TabbedShowLayout } from "react-admin"; import ShowActions from "@/admin/components/Actions/ShowActions"; @@ -10,34 +9,27 @@ import GalleryTab from "@/admin/components/ResourceTabs/GalleryTab/GalleryTab"; import InformationTab from "@/admin/components/ResourceTabs/InformationTab"; import MonitoredTab from "@/admin/components/ResourceTabs/MonitoredTab/MonitoredTab"; import PolygonReviewTab from "@/admin/components/ResourceTabs/PolygonReviewTab"; -import ShowTitle from "@/admin/components/ShowTitle"; import { RecordFrameworkProvider } from "@/context/framework.provider"; import { MapAreaProvider } from "@/context/mapArea.provider"; -const SiteShow: FC = () => { - return ( - record?.name} />} - actions={} - className="-mt-[50px] bg-neutral-100" - > - - - - - - - - - - - - - - - - - ); -}; +const SiteShow = () => ( + } className="-mt-[50px] bg-neutral-100"> + + + + + + + + + + + + + + + + +); export default SiteShow; diff --git a/src/admin/modules/tasks/components/TaskShow.tsx b/src/admin/modules/tasks/components/TaskShow.tsx index 76240bed9..9be56093e 100644 --- a/src/admin/modules/tasks/components/TaskShow.tsx +++ b/src/admin/modules/tasks/components/TaskShow.tsx @@ -12,10 +12,9 @@ import { import { grey } from "@mui/material/colors"; import { useT } from "@transifex/react"; import { camelCase } from "lodash"; -import { FC, useMemo } from "react"; +import { useMemo } from "react"; import { RaRecord, Show, ShowButton, useShowContext } from "react-admin"; -import ShowTitle from "@/admin/components/ShowTitle"; import { useGetV2TasksUUIDReports } from "@/generated/apiComponents"; import { useDate } from "@/hooks/useDate"; @@ -129,8 +128,8 @@ function ShowReports() { ); } -const TaskShow: FC = () => ( - record?.project?.name} />}> +const TaskShow = () => ( + ); diff --git a/src/admin/modules/user/components/UserCreate.tsx b/src/admin/modules/user/components/UserCreate.tsx index 0efae8f5b..3100fd875 100644 --- a/src/admin/modules/user/components/UserCreate.tsx +++ b/src/admin/modules/user/components/UserCreate.tsx @@ -46,7 +46,7 @@ const UserCreate = () => { } return [...frameworkAdminPrimaryRoleChoices, userPrimaryRoleChoices.find(choice => choice.id === role)]; - }, [isFrameworkAdmin]); + }, [isSuperAdmin, role]); return ( diff --git a/src/admin/modules/user/components/UserEdit.tsx b/src/admin/modules/user/components/UserEdit.tsx index c1694ea63..daab9aec9 100644 --- a/src/admin/modules/user/components/UserEdit.tsx +++ b/src/admin/modules/user/components/UserEdit.tsx @@ -40,7 +40,7 @@ const UserEdit = () => { } return [...frameworkAdminPrimaryRoleChoices, userPrimaryRoleChoices.find(choice => choice.id === role)]; - }, [isFrameworkAdmin]); + }, [isSuperAdmin, role]); return ( } mutationMode="pessimistic" actions={false}> diff --git a/src/admin/modules/user/components/UserList.tsx b/src/admin/modules/user/components/UserList.tsx index f30f0ed05..6c9d69684 100644 --- a/src/admin/modules/user/components/UserList.tsx +++ b/src/admin/modules/user/components/UserList.tsx @@ -19,7 +19,7 @@ import { import { UserDataProvider } from "@/admin/apiProvider/dataProviders/userDataProvider"; import ListActionsCreateFilter from "@/admin/components/Actions/ListActionsCreateFilter"; import ExportProcessingAlert from "@/admin/components/Alerts/ExportProcessingAlert"; -import { useCanUserEdit } from "@/admin/hooks/useCanUserEdit"; +import { userCanEdit } from "@/admin/hooks/useCanUserEdit"; import { useGetUserRole } from "@/admin/hooks/useGetUserRole"; import Menu from "@/components/elements/Menu/Menu"; import { MENU_PLACEMENT_BOTTOM_LEFT } from "@/components/elements/Menu/MenuVariant"; @@ -65,6 +65,8 @@ const readOnlyMenu = [ShowItem]; const adminMenu = [EditItem, ShowItem]; const UserDataGrid = () => { + const roleData = useGetUserRole(); + return ( { { - const canEdit = useCanUserEdit(record, "user"); + const canEdit = userCanEdit(record, "user", roleData); return ( diff --git a/src/admin/modules/user/components/UserShow.tsx b/src/admin/modules/user/components/UserShow.tsx index e1f481c62..e8f347450 100644 --- a/src/admin/modules/user/components/UserShow.tsx +++ b/src/admin/modules/user/components/UserShow.tsx @@ -12,7 +12,6 @@ import { } from "react-admin"; import ShowActions from "@/admin/components/Actions/ShowActions"; -import ShowTitle from "@/admin/components/ShowTitle"; import { V2AdminOrganisationRead } from "@/generated/apiSchemas"; import modules from "../.."; @@ -42,16 +41,7 @@ const renderFrameworks = (property: string) => (record: any) => { }; export const UserShow = () => ( - `${record?.first_name} ${record?.last_name}`} - deleteProps={{ confirmTitle: "Delete User" }} - /> - } - title={ `${record?.first_name} ${record?.last_name}`} />} - aside={} - > + } aside={}> diff --git a/src/admin/modules/validationPolygonFile/components/ValidationPolygonFileShow.tsx b/src/admin/modules/validationPolygonFile/components/ValidationPolygonFileShow.tsx index bec6d42a5..cf647eae4 100644 --- a/src/admin/modules/validationPolygonFile/components/ValidationPolygonFileShow.tsx +++ b/src/admin/modules/validationPolygonFile/components/ValidationPolygonFileShow.tsx @@ -1,6 +1,6 @@ import { Stack } from "@mui/material"; import { useT } from "@transifex/react"; -import { FC, useEffect, useState } from "react"; +import { FC, useState } from "react"; import Button from "@/components/elements/Button/Button"; import Text from "@/components/elements/Text/Text"; @@ -14,6 +14,7 @@ import { fetchPostV2TerrafundUploadKmlValidate, fetchPostV2TerrafundUploadShapefileValidate } from "@/generated/apiComponents"; +import { useValueChanged } from "@/hooks/useValueChanged"; import { FileType, UploadedFile } from "@/types/common"; import { getErrorMessageFromPayload } from "@/utils/errors"; @@ -25,12 +26,12 @@ const ValidatePolygonFileShow: FC = () => { const { openNotification } = useNotificationContext(); const t = useT(); - useEffect(() => { + useValueChanged(saveFlags, () => { if (file && saveFlags) { uploadFile(); setSaveFlags(false); } - }, [saveFlags]); + }); const getFileType = (file: UploadedFile) => { const fileType = file?.file_name.split(".").pop()?.toLowerCase(); diff --git a/src/assets/icons/arrow-up-right.svg b/src/assets/icons/arrow-up-right.svg new file mode 100644 index 000000000..7a3facc84 --- /dev/null +++ b/src/assets/icons/arrow-up-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/briefcase.svg b/src/assets/icons/briefcase.svg new file mode 100644 index 000000000..dfb0f58af --- /dev/null +++ b/src/assets/icons/briefcase.svg @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/icons/check-laguages.svg b/src/assets/icons/check-laguages.svg new file mode 100644 index 000000000..9094ba8b1 --- /dev/null +++ b/src/assets/icons/check-laguages.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/icons/check.svg b/src/assets/icons/check.svg new file mode 100644 index 000000000..da9b057a4 --- /dev/null +++ b/src/assets/icons/check.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/clear-dashboard.svg b/src/assets/icons/clear-dashboard.svg new file mode 100644 index 000000000..0efa2c6d1 --- /dev/null +++ b/src/assets/icons/clear-dashboard.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/dashboard-impact-story.svg b/src/assets/icons/dashboard-impact-story.svg new file mode 100644 index 000000000..1a0aeaee8 --- /dev/null +++ b/src/assets/icons/dashboard-impact-story.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/icons/earth-dashboard.svg b/src/assets/icons/earth-dashboard.svg new file mode 100644 index 000000000..57de2ebcb --- /dev/null +++ b/src/assets/icons/earth-dashboard.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/facebook.svg b/src/assets/icons/facebook.svg index d009cc4b7..6311ae3dd 100644 --- a/src/assets/icons/facebook.svg +++ b/src/assets/icons/facebook.svg @@ -1,3 +1,5 @@ - - + + \ No newline at end of file diff --git a/src/assets/icons/filter.svg b/src/assets/icons/filter.svg index 1941e4280..ed8f4c6be 100644 --- a/src/assets/icons/filter.svg +++ b/src/assets/icons/filter.svg @@ -1,7 +1,5 @@ - - - - - - + + + + diff --git a/src/assets/icons/ic-menu.svg b/src/assets/icons/ic-menu.svg new file mode 100644 index 000000000..35269a540 --- /dev/null +++ b/src/assets/icons/ic-menu.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/assets/icons/ic-switch.svg b/src/assets/icons/ic-switch.svg new file mode 100644 index 000000000..f60a74206 --- /dev/null +++ b/src/assets/icons/ic-switch.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/ic-user.svg b/src/assets/icons/ic-user.svg index 026ce70e9..a915cfab3 100644 --- a/src/assets/icons/ic-user.svg +++ b/src/assets/icons/ic-user.svg @@ -1,10 +1,12 @@ - - - - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/instagram.svg b/src/assets/icons/instagram.svg index 18c2195f6..e079911f0 100644 --- a/src/assets/icons/instagram.svg +++ b/src/assets/icons/instagram.svg @@ -1,3 +1,5 @@ - - + + \ No newline at end of file diff --git a/src/assets/icons/linkedin.svg b/src/assets/icons/linkedin.svg index 0557402e8..dd34e130a 100644 --- a/src/assets/icons/linkedin.svg +++ b/src/assets/icons/linkedin.svg @@ -1,3 +1,5 @@ - - + + \ No newline at end of file diff --git a/src/assets/icons/logout.svg b/src/assets/icons/logout.svg new file mode 100644 index 000000000..b4c9b4004 --- /dev/null +++ b/src/assets/icons/logout.svg @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/my-account.svg b/src/assets/icons/my-account.svg new file mode 100644 index 000000000..660491e25 --- /dev/null +++ b/src/assets/icons/my-account.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/assets/icons/no-check-laguages.svg b/src/assets/icons/no-check-laguages.svg new file mode 100644 index 000000000..c99806875 --- /dev/null +++ b/src/assets/icons/no-check-laguages.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/share-impact-story.svg b/src/assets/icons/share-impact-story.svg new file mode 100644 index 000000000..9cddfff04 --- /dev/null +++ b/src/assets/icons/share-impact-story.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/src/assets/icons/twitter.svg b/src/assets/icons/twitter.svg new file mode 100644 index 000000000..044f61c31 --- /dev/null +++ b/src/assets/icons/twitter.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/components/elements/Cards/ItemMonitoringCard/__snapshots__/ItemMonitoringCard.stories.storyshot b/src/components/elements/Cards/ItemMonitoringCard/__snapshots__/ItemMonitoringCard.stories.storyshot index 7d340fda7..b6a7dafe4 100644 --- a/src/components/elements/Cards/ItemMonitoringCard/__snapshots__/ItemMonitoringCard.stories.storyshot +++ b/src/components/elements/Cards/ItemMonitoringCard/__snapshots__/ItemMonitoringCard.stories.storyshot @@ -2,7 +2,7 @@ exports[`Storyshots Components/Elements/Cards/ItemMonitoringCards Default 1`] = ` { + useValueChanged(sortOrder, () => { setSortLabel(sortOrder === "asc" ? t("Oldest to Newest") : t("Newest to Oldest")); - }, [sortOrder]); + }); const tabs = [ { key: "0", render: "All Images" }, @@ -321,9 +323,10 @@ const ImageGallery = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [pageIndex, pageSize, modelName]); - useEffect(() => { + useValueChanged(activeIndex, () => { onChangeGeotagged(activeIndex); - }, [activeIndex]); + }); + return ( <> @@ -416,7 +419,7 @@ const ImageGallery = ({ > )} , UseControllerProps { @@ -41,7 +42,7 @@ const ConditionalInput = (props: ConditionalInputProps) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.value, formHook]); - useEffect(() => { + useValueChanged(valueCondition, () => { if (valueCondition == true) { field.onChange(true); return; @@ -71,7 +72,7 @@ const ConditionalInput = (props: ConditionalInputProps) => { if (fieldsCount == fields?.length) { field.onChange(false); } - }, [valueCondition]); + }); return ( <> diff --git a/src/components/elements/Inputs/DataTable/DataTable.tsx b/src/components/elements/Inputs/DataTable/DataTable.tsx index 9156f0a42..acd9c5f45 100644 --- a/src/components/elements/Inputs/DataTable/DataTable.tsx +++ b/src/components/elements/Inputs/DataTable/DataTable.tsx @@ -67,7 +67,7 @@ function DataTable(props: DataTablePro handleCreate?.(fieldValues); closeModal(ModalId.FORM_MODAL); }, - [generateUuids, value, onChange, handleCreate, closeModal] + [generateUuids, onChange, value, handleCreate, closeModal, additionalValues] ); const onDeleteEntry = useCallback( diff --git a/src/components/elements/Inputs/Dropdown/Dropdown.tsx b/src/components/elements/Inputs/Dropdown/Dropdown.tsx index dff695d13..f15f595a2 100644 --- a/src/components/elements/Inputs/Dropdown/Dropdown.tsx +++ b/src/components/elements/Inputs/Dropdown/Dropdown.tsx @@ -272,7 +272,8 @@ const Dropdown = (props: PropsWithChildren) => { as="div" className={tw( "border-light absolute mt-2 max-h-[235px] min-w-full overflow-auto rounded-lg bg-white outline-none lg:max-h-[250px] wide:max-h-[266px]", - props.optionsClassName + props.optionsClassName, + variant.optionsClassName )} > diff --git a/src/components/elements/Inputs/Dropdown/DropdownVariant.tsx b/src/components/elements/Inputs/Dropdown/DropdownVariant.tsx index 2f7c8c071..e25eaf8e2 100644 --- a/src/components/elements/Inputs/Dropdown/DropdownVariant.tsx +++ b/src/components/elements/Inputs/Dropdown/DropdownVariant.tsx @@ -10,6 +10,7 @@ export interface DropdownVariant { iconNameClear?: IconNames; iconClearContainerClassName?: string; iconClearClassName?: string; + optionsClassName?: string; optionCheckboxClassName?: string; optionLabelClassName?: string; optionClassName?: string; @@ -63,3 +64,32 @@ export const VARIANT_DROPDOWN_SIMPLE: DropdownVariant = { optionLabelClassName: "text-14-semibold whitespace-nowrap", optionClassName: "gap-2" }; + +export const VARIANT_DROPDOWN_IMPACT_STORY: DropdownVariant = { + containerClassName: "relative", + className: "gap-2 !text-black border border-neutral-200 ", + iconClassName: "w-3 h-[9px] fill-trasparent", + iconName: IconNames.CHEVRON_DOWN_DASH, + iconNameClear: IconNames.CLEAR, + iconClearClassName: "w-3 h-3", + iconClearContainerClassName: "p-1 border border-neutral-200 rounded", + titleContainerClassName: "flex-1 overflow-hidden", + titleClassname: "leading-normal text-ellipsis whitespace-nowrap overflow-hidden", + optionCheckboxClassName: "checked:text-blueCustom-700", + optionLabelClassName: "text-14-semibold whitespace-nowrap", + optionClassName: "gap-2" +}; + +export const VARIANT_DROPDOWN_COLLAPSE: DropdownVariant = { + containerClassName: "", + className: "", + iconClassName: "w-4", + iconClearClassName: "w-4", + iconName: undefined, + titleContainerClassName: "flex items-center gap-2", + titleClassname: "hidden", + optionsClassName: "static bg-neutral-40 rounded-none", + optionClassName: "flex-row w-full", + optionCheckboxClassName: "w-4 h-4", + optionLabelClassName: "text-14-light" +}; diff --git a/src/components/elements/Inputs/FileInput/FileInput.tsx b/src/components/elements/Inputs/FileInput/FileInput.tsx index 75a6e7a07..1f26811c6 100644 --- a/src/components/elements/Inputs/FileInput/FileInput.tsx +++ b/src/components/elements/Inputs/FileInput/FileInput.tsx @@ -1,4 +1,5 @@ import { useT } from "@transifex/react"; +import classNames from "classnames"; import { ChangeEvent, Fragment, ReactNode, useId, useMemo, useRef } from "react"; import { useDropzone } from "react-dropzone"; import { UseFormReturn } from "react-hook-form"; @@ -34,6 +35,7 @@ export type FileInputProps = InputWrapperProps & { formHook?: UseFormReturn; updateFile?: (file: Partial) => void; entityData?: any; + classNameTextOr?: string; }; export interface FileStatus { @@ -148,7 +150,7 @@ const FileInput = (props: FileInputProps) => { {t("Click to upload")} - + {t("or")} diff --git a/src/components/elements/Inputs/FileInput/FileInputVariants.ts b/src/components/elements/Inputs/FileInput/FileInputVariants.ts index 00277208f..ab5f8b2f4 100644 --- a/src/components/elements/Inputs/FileInput/FileInputVariants.ts +++ b/src/components/elements/Inputs/FileInput/FileInputVariants.ts @@ -125,3 +125,11 @@ export const VARIANT_FILE_INPUT_MODAL_ADD_IMAGES_WITH_MAP: FileInputVariant = { listPreviewDescription: "flex items-center justify-between", filePreviewVariant: VARIANT_FILE_PREVIEW_CARD_MODAL_ADD_IMAGES_WITH_MAP }; + +export const VARIANT_FILE_INPUT_IMPACT_STORY: FileInputVariant = { + container: "flex flex-col items-center justify-center rounded-lg border border-grey-750 py-8", + snapshotPanel: true, + listPreview: "flex flex-col gap-4 w-full mt-2 mb-4 hidden", + listPreviewDescription: "flex items-center justify-between hidden", + filePreviewVariant: VARIANT_FILE_PREVIEW_CARD_MODAL_ADD_IMAGES_WITH_MAP +}; diff --git a/src/components/elements/Inputs/LanguageDropdown/LanguagesDropdown.tsx b/src/components/elements/Inputs/LanguageDropdown/LanguagesDropdown.tsx index 2f5f93271..a501de7e6 100644 --- a/src/components/elements/Inputs/LanguageDropdown/LanguagesDropdown.tsx +++ b/src/components/elements/Inputs/LanguageDropdown/LanguagesDropdown.tsx @@ -1,4 +1,5 @@ import { Popover } from "@headlessui/react"; +import { useMediaQuery } from "@mui/material"; import { useT } from "@transifex/react"; import classNames from "classnames"; import { useRouter } from "next/router"; @@ -10,10 +11,13 @@ import List from "@/components/extensive/List/List"; import { useValueChanged } from "@/hooks/useValueChanged"; import { Option, OptionValue } from "@/types/common"; +import { LanguagesDropdownVariant, VARIANT_LANGUAGES_DROPDOWN } from "./LanguagesDropdownVariant"; + export interface DropdownProps { defaultValue?: OptionValue; onChange?: (value: string) => void; className?: string; + variant?: LanguagesDropdownVariant; } const LANGUAGES: Option[] = [ @@ -28,8 +32,11 @@ const languageForLocale = (locale?: string | null) => LANGUAGES.find(({ value }) const LanguagesDropdown = (props: PropsWithChildren) => { const t = useT(); const router = useRouter(); - + const variantClass = props.variant ?? VARIANT_LANGUAGES_DROPDOWN; + const isMobile = useMediaQuery("(max-width: 1200px)"); const [selected, setSelected] = useState(languageForLocale(router.locale)); + + const [selectedIndex, setSelectedIndex] = useState(LANGUAGES.findIndex(lang => lang.value === selected.value) || 0); let buttonRef = useRef(); useValueChanged(router.locale, () => { @@ -42,35 +49,65 @@ const LanguagesDropdown = (props: PropsWithChildren) => { buttonRef.current?.click(); }; - return ( - - - - - {t(selected?.title)} - - - + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "Enter" || event.key === " ") { + onChange(LANGUAGES[selectedIndex]); + } + if (event.key === "ArrowDown") { + setSelectedIndex(prev => (prev + 1) % LANGUAGES.length); + } else if (event.key === "ArrowUp") { + setSelectedIndex(prev => (prev - 1 + LANGUAGES.length) % LANGUAGES.length); + } + }; + + const mobileLanguages = LANGUAGES.filter(lang => lang.value !== "pt-BR" && lang.value !== "es-MX"); - - ( - onChange(item)} - > - {t(item.title)} - - )} - /> - - + return ( + + + + + + + {t(selected?.title.slice(0, 2))} + + + + {t(selected?.title)} + + + + ( + onChange(item)} + > + {(isMobile || selected.value === item.value) && ( + + )} + {t(isMobile ? item.title.slice(0, 2) : item.title)} + + )} + className={variantClass.classList} + /> + + + ); }; diff --git a/src/components/elements/Inputs/LanguageDropdown/LanguagesDropdownVariant.ts b/src/components/elements/Inputs/LanguageDropdown/LanguagesDropdownVariant.ts new file mode 100644 index 000000000..7cb239526 --- /dev/null +++ b/src/components/elements/Inputs/LanguageDropdown/LanguagesDropdownVariant.ts @@ -0,0 +1,58 @@ +import { IconNames } from "@/components/extensive/Icon/Icon"; + +export interface LanguagesDropdownVariant { + classIcon: string; + classButtonPopover?: string; + classText: string; + icon: IconNames; + arrowIcon: IconNames; + arrowDashboardClass: string; + arrowNavbarClass: string; + classPanel: string; + classList: string; + classItem: string; + classIconSelected: string; + classContent: string; + classContentOpen: string; + classTextDashboard: string; + iconSelected?: IconNames; +} + +export const VARIANT_LANGUAGES_DROPDOWN: LanguagesDropdownVariant = { + classIcon: "mr-2 fill-neutral-700", + classButtonPopover: "flex items-center justify-between p-2", + classText: "text-14-light mr-2 whitespace-nowrap text-sm uppercase text-darkCustom", + icon: IconNames.EARTH, + arrowIcon: IconNames.TRIANGLE_DOWN, + arrowDashboardClass: "hidden", + arrowNavbarClass: "transition fill-neutral-700 ui-open:rotate-180 ui-open:transform", + classPanel: "border-1 absolute right-0 z-50 mt-4 w-[130px] border border-neutral-300 bg-white shadow", + classList: "", + classItem: "px-3 py-1 uppercase text-neutral-900 first:pt-2 last:pb-2 hover:bg-neutral-200 cursor-pointer", + classIconSelected: "hidden", + classContent: "relative w-fit", + classContentOpen: "", + classTextDashboard: "hidden" +}; + +export const VARIANT_LANGUAGES_DROPDOWN_SECONDARY: LanguagesDropdownVariant = { + classIcon: "text-white w-8 h-8 mobile:hidden", + classButtonPopover: "flex flex-col items-start outline-none opacity-50 aria-expanded:opacity-100", + classText: "hidden", + icon: IconNames.EARTH_DASHBOARD, + arrowIcon: IconNames.CHEVRON_DOWN, + arrowDashboardClass: + "transition fill-white ui-open:rotate-180 ui-open:transform h-2.5 w-2.5 min-w-2.5 mt-3 mobile:m-0", + arrowNavbarClass: "hidden", + classPanel: + "shadow-all border-1 absolute sm:bottom-0 sm:left-full z-50 ml-3 w-[140px] bg-white shadow rounded-lg overflow-hidden mobile:top-full mobile:w-auto mobile:left-0 mobile:m-0 mobile:mt-2", + classList: "divide-y divide-grey-950", + classItem: + "py-2 px-3 hover:bg-neutral-200 text-black !font-normal flex items-center gap-2 cursor-pointer mobile:uppercase mobile:justify-between", + classIconSelected: "text-black mobile:order-last mobile:h-4 mobile:w-4", + iconSelected: IconNames.CHECK, + classContent: + "relative w-fit mobile:px-1.5 mobile:py-0.5 mobile:bg-white mobile:bg-opacity-20 mobile:border mobile:border-white mobile:rounded-lg mobile:border-opacity-40", + classContentOpen: "!opacity-100", + classTextDashboard: "text-12 whitespace-nowrap uppercase text-white mobile:text-14" +}; diff --git a/src/components/elements/Inputs/LanguageDropdown/__snapshots__/LanguagesDropdown.stories.storyshot b/src/components/elements/Inputs/LanguageDropdown/__snapshots__/LanguagesDropdown.stories.storyshot index ed7ee5205..323d27aa3 100644 --- a/src/components/elements/Inputs/LanguageDropdown/__snapshots__/LanguagesDropdown.stories.storyshot +++ b/src/components/elements/Inputs/LanguageDropdown/__snapshots__/LanguagesDropdown.stories.storyshot @@ -5,42 +5,65 @@ exports[`Storyshots Components/Elements/Inputs/LanguagesDropdown Default 1`] = ` className="flex h-[300px] justify-end" > - - - - English - - + + + + En + + + + + + English + + - + /> + + `; diff --git a/src/components/elements/Inputs/Map/RHFMap.tsx b/src/components/elements/Inputs/Map/RHFMap.tsx index 2c5bc2301..c8c672a36 100644 --- a/src/components/elements/Inputs/Map/RHFMap.tsx +++ b/src/components/elements/Inputs/Map/RHFMap.tsx @@ -72,16 +72,18 @@ const RHFMap = ({ cacheTime: 0 } ); - const setBbboxAndZoom = async () => { - if (projectPolygon?.project_polygon?.poly_uuid) { - const bbox = await fetchGetV2TerrafundPolygonBboxUuid({ - pathParams: { uuid: projectPolygon.project_polygon?.poly_uuid ?? "" } - }); - const bounds: any = bbox.bbox; - setPolygonBbox(bounds); - } - }; + useEffect(() => { + const setBbboxAndZoom = async () => { + if (projectPolygon?.project_polygon?.poly_uuid) { + const bbox = await fetchGetV2TerrafundPolygonBboxUuid({ + pathParams: { uuid: projectPolygon.project_polygon?.poly_uuid ?? "" } + }); + const bounds: any = bbox.bbox; + setPolygonBbox(bounds); + } + }; + const getDataProjectPolygon = async () => { if (!projectPolygon?.project_polygon) { setPolygonDataMap({ [FORM_POLYGONS]: [] }); @@ -93,8 +95,9 @@ const RHFMap = ({ setPolygonFromMap({ isOpen: true, uuid: projectPolygon?.project_polygon?.poly_uuid }); } }; + getDataProjectPolygon(); - }, [projectPolygon, isRefetching]); + }, [projectPolygon, isRefetching, setSelectPolygonFromMap]); useEffect(() => { if (entity) { diff --git a/src/components/elements/Inputs/MyAccountDropdown/MyAccountDropdown.tsx b/src/components/elements/Inputs/MyAccountDropdown/MyAccountDropdown.tsx new file mode 100644 index 000000000..2676a02b0 --- /dev/null +++ b/src/components/elements/Inputs/MyAccountDropdown/MyAccountDropdown.tsx @@ -0,0 +1,124 @@ +import { Popover } from "@headlessui/react"; +import { useT } from "@transifex/react"; +import classNames from "classnames"; +import { useRouter } from "next/router"; +import { PropsWithChildren, useMemo, useRef, useState } from "react"; + +import { removeAccessToken } from "@/admin/apiProvider/utils/token"; +import Icon, { IconNames } from "@/components/extensive/Icon/Icon"; +import List from "@/components/extensive/List/List"; +import { useMyUser } from "@/connections/User"; + +import Text from "../../Text/Text"; +import { MyAccountDropdownVariant, VARIANT_MY_ACCOUNT_DROPDOWN } from "./MyAccountDropdownVariant"; + +export interface MyAccountDropdownProps { + variant?: MyAccountDropdownVariant; + className?: string; + isLoggedIn?: boolean; +} + +const MyAccountDropdown = (props: PropsWithChildren) => { + const t = useT(); + const router = useRouter(); + const rootPath = router.asPath.split("?")[0].split("/")[1]; + const isOnDashboard = rootPath === "dashboard"; + const [loaded, { isAdmin }] = useMyUser(); + const variantClass = props.variant ?? VARIANT_MY_ACCOUNT_DROPDOWN; + const [selectedIndex, setSelectedIndex] = useState(0); + + let buttonRef = useRef(); + + const OptionMyAccount = useMemo(() => { + return props.isLoggedIn + ? [ + { + value: isOnDashboard ? (isAdmin ? "Admin view" : "Project Developer view") : "Dashboard", + title: isOnDashboard ? (isAdmin ? "Admin view" : "Project Developer view") : "Dashboard", + icon: IconNames.IC_SWITCH + }, + { + value: "Logout", + title: "Logout", + icon: IconNames.LOGOUT + } + ] + : [ + { + value: "Go To Login", + title: "Go To Login", + icon: IconNames.LOGOUT + } + ]; + }, [props.isLoggedIn, isOnDashboard, isAdmin]); + + const onChange = (item: any) => { + if (item.value === "Go To Login") { + router.push("/auth/login"); + } else if (item.value === "Logout") { + removeAccessToken(); + router.push("/auth/login"); + } else { + if (!loaded) return; + if (isOnDashboard) { + if (isAdmin) { + router.push("/admin"); + } + router.push("/home"); + } else { + router.push("/dashboard"); + } + } + setTimeout(() => { + router.reload(); + }, 1000); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "Enter" || event.key === " ") { + onChange(OptionMyAccount[selectedIndex]); + } + if (event.key === "ArrowDown") { + setSelectedIndex(prev => (prev + 1) % OptionMyAccount.length); + } else if (event.key === "ArrowUp") { + setSelectedIndex(prev => (prev - 1 + OptionMyAccount.length) % OptionMyAccount.length); + } + }; + + return ( + + + + + + + + {t("MY ACCOUNT")} + + + + + ( + onChange(item)} + > + + + {t(item.title)} + + )} + className={variantClass.classList} + /> + + + + ); +}; + +export default MyAccountDropdown; diff --git a/src/components/elements/Inputs/MyAccountDropdown/MyAccountDropdownVariant.ts b/src/components/elements/Inputs/MyAccountDropdown/MyAccountDropdownVariant.ts new file mode 100644 index 000000000..7ae79df9c --- /dev/null +++ b/src/components/elements/Inputs/MyAccountDropdown/MyAccountDropdownVariant.ts @@ -0,0 +1,51 @@ +import { IconNames } from "@/components/extensive/Icon/Icon"; + +export interface MyAccountDropdownVariant { + classIcon: string; + classButtonPopover?: string; + classText: string; + icon: IconNames; + arrowIcon: IconNames; + arrowDashboardClass: string; + arrowNavbarClass: string; + classPanel: string; + classList: string; + classItem: string; + classIconSelected: string; + classContent: string; + classContentOpen: string; +} + +export const VARIANT_MY_ACCOUNT_DROPDOWN: MyAccountDropdownVariant = { + classIcon: "mr-2 text-neutral-700", + classButtonPopover: "flex items-center justify-between p-2", + classText: "text-14-light mr-2 whitespace-nowrap text-sm uppercase text-darkCustom", + icon: IconNames.MY_ACCOUNT, + arrowIcon: IconNames.TRIANGLE_DOWN, + arrowDashboardClass: "hidden", + arrowNavbarClass: "transition fill-neutral-700 ui-open:rotate-180 ui-open:transform", + classPanel: "border-1 absolute right-0 z-50 mt-4 w-auto border border-neutral-300 bg-white shadow", + classList: "", + classItem: + "px-3 py-1 text-neutral-900 first:pt-2 last:pb-2 hover:bg-neutral-200 whitespace-nowrap flex items-center gap-2 cursor-pointer", + classIconSelected: "w-3 h-3", + classContent: "relative w-fit", + classContentOpen: "" +}; + +export const VARIANT_MY_ACCOUNT_DROPDOWN_SECONDARY: MyAccountDropdownVariant = { + classIcon: "text-white w-8 h-8", + classButtonPopover: "flex flex-col items-start outline-none opacity-50 aria-expanded:opacity-100", + classText: "hidden", + icon: IconNames.IC_USER, + arrowIcon: IconNames.CHEVRON_DOWN, + arrowDashboardClass: "transition fill-white ui-open:rotate-180 ui-open:transform h-2.5 w-2.5 min-w-2.5", + arrowNavbarClass: "hidden", + classPanel: "border-1 absolute bottom-0 left-full z-50 ml-4 w-auto bg-white shadow rounded-lg overflow-hidden", + classList: "divide-y divide-grey-950", + classItem: + "py-2 px-3 hover:bg-neutral-200 text-black !font-normal flex items-center gap-2 whitespace-nowrap cursor-pointer", + classIconSelected: "w-3 h-3", + classContent: "relative w-fit", + classContentOpen: "!opacity-100" +}; diff --git a/src/components/elements/Inputs/textArea/TextArea.tsx b/src/components/elements/Inputs/textArea/TextArea.tsx index bea8c4742..cd9927ddf 100644 --- a/src/components/elements/Inputs/textArea/TextArea.tsx +++ b/src/components/elements/Inputs/textArea/TextArea.tsx @@ -64,7 +64,7 @@ const TextArea = ({ formHook, className, onChange: externalOnChange, ...inputWra formHook?.trigger(inputWrapperProps.name) })} id={id} className={inputClasses} /> diff --git a/src/components/elements/Map-mapbox/Map.tsx b/src/components/elements/Map-mapbox/Map.tsx index dd3882aef..16552cbcf 100644 --- a/src/components/elements/Map-mapbox/Map.tsx +++ b/src/components/elements/Map-mapbox/Map.tsx @@ -34,6 +34,8 @@ import { usePutV2TerrafundPolygonUuid } from "@/generated/apiComponents"; import { DashboardGetProjectsData, SitePolygonsDataResponse } from "@/generated/apiSchemas"; +import { useOnMount } from "@/hooks/useOnMount"; +import { useValueChanged } from "@/hooks/useValueChanged"; import Log from "@/utils/log"; import { ImageGalleryItemData } from "../ImageGallery/ImageGalleryItem"; @@ -222,7 +224,7 @@ export const MapContainer = ({ const { map, mapContainer, draw, onCancel, styleLoaded, initMap, setStyleLoaded, setChangeStyle, changeStyle } = mapFunctions; - useEffect(() => { + useOnMount(() => { initMap(!!isDashboard); return () => { if (map.current) { @@ -231,7 +233,7 @@ export const MapContainer = ({ map.current = null; } }; - }, []); + }); useEffect(() => { if (!map) return; @@ -240,7 +242,7 @@ export const MapContainer = ({ } }, [map, location]); - useEffect(() => { + useValueChanged(isUserDrawingEnabled, () => { if (map?.current && draw?.current) { if (isUserDrawingEnabled) { startDrawing(draw.current, map.current); @@ -251,7 +253,7 @@ export const MapContainer = ({ stopDrawing(draw.current, map.current); } } - }, [isUserDrawingEnabled]); + }); useEffect(() => { if (map?.current && (isDashboard || !_.isEmpty(polygonsData))) { @@ -292,25 +294,26 @@ export const MapContainer = ({ }); } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [sitePolygonData, polygonsData, showPopups, centroids, styleLoaded]); - useEffect(() => { + useValueChanged(currentStyle, () => { if (currentStyle) { setChangeStyle(false); } - }, [currentStyle]); + }); - useEffect(() => { + useValueChanged(changeStyle, () => { if (!changeStyle) { setStyleLoaded(false); } - }, [changeStyle]); + }); - useEffect(() => { + useValueChanged(bbox, () => { if (bbox && map.current && map && shouldBboxZoom) { zoomToBbox(bbox, map.current, hasControls); } - }, [bbox]); + }); useEffect(() => { if (!map.current || !sourcesAdded) return; const setupBorders = () => { @@ -327,6 +330,7 @@ export const MapContainer = ({ setupBorders(); }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedCountry, styleLoaded, sourcesAdded]); useEffect(() => { if (!map.current || !sourcesAdded) return; @@ -344,6 +348,7 @@ export const MapContainer = ({ setupBorders(); }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedLandscapes, styleLoaded, sourcesAdded]); useEffect(() => { if (!map.current || !projectUUID) return; @@ -354,6 +359,7 @@ export const MapContainer = ({ setMapStyle(MapStyle.Satellite, map.current, setCurrentStyle, currentStyle); }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [projectUUID, styleLoaded]); useEffect(() => { const projectUUID = router.query.uuid as string; @@ -453,13 +459,14 @@ export const MapContainer = ({ removeMediaLayer(map.current); } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [props?.modelFilesData, showMediaPopups, styleLoaded]); - useEffect(() => { + useValueChanged(showMediaPopups, () => { if (geojson && map.current && draw.current) { addGeojsonToDraw(geojson, "", () => {}, draw.current, map.current); } - }, [showMediaPopups]); + }); function handleAddGeojsonToDraw(polygonuuid: string) { if (polygonsData && map.current && draw.current) { @@ -490,6 +497,7 @@ export const MapContainer = ({ newPolygonData ); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedPolygonsInCheckbox, styleLoaded]); const handleEditPolygon = async () => { @@ -576,7 +584,7 @@ export const MapContainer = ({ drawTemporaryPolygon(polygonGeojson?.geojson, () => {}, map.current, selectedPolyVersion); }; - useEffect(() => { + useValueChanged(selectedPolyVersion, () => { if (map?.current?.getSource("temp-polygon-source") || map?.current?.getLayer("temp-polygon-source-line")) { map?.current.removeLayer("temp-polygon-source-line"); map?.current?.removeLayer("temp-polygon-source"); @@ -586,7 +594,7 @@ export const MapContainer = ({ if (selectedPolyVersion) { addGeometryVersion(); } - }, [selectedPolyVersion]); + }); return ( diff --git a/src/components/elements/Map-mapbox/MapControls/CheckIndividualPolygonControl.tsx b/src/components/elements/Map-mapbox/MapControls/CheckIndividualPolygonControl.tsx index b60108a7c..bcda1385d 100644 --- a/src/components/elements/Map-mapbox/MapControls/CheckIndividualPolygonControl.tsx +++ b/src/components/elements/Map-mapbox/MapControls/CheckIndividualPolygonControl.tsx @@ -1,5 +1,5 @@ import { useT } from "@transifex/react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { When } from "react-if"; import { useLoading } from "@/context/loaderAdmin.provider"; @@ -11,6 +11,7 @@ import { usePostV2TerrafundValidationPolygon } from "@/generated/apiComponents"; import { ClippedPolygonResponse, SitePolygonsDataResponse } from "@/generated/apiSchemas"; +import { useValueChanged } from "@/hooks/useValueChanged"; import Log from "@/utils/log"; import Button from "../../Button/Button"; @@ -83,12 +84,12 @@ const CheckIndividualPolygonControl = ({ viewRequestSuport }: { viewRequestSupor } }); - useEffect(() => { + useValueChanged(clickedValidation, () => { if (clickedValidation) { showLoader(); getValidations({ queryParams: { uuid: editPolygon.uuid } }); } - }, [clickedValidation]); + }); return ( diff --git a/src/components/elements/Map-mapbox/MapControls/CheckPolygonControl.tsx b/src/components/elements/Map-mapbox/MapControls/CheckPolygonControl.tsx index 6997f450b..b8a85d310 100644 --- a/src/components/elements/Map-mapbox/MapControls/CheckPolygonControl.tsx +++ b/src/components/elements/Map-mapbox/MapControls/CheckPolygonControl.tsx @@ -22,8 +22,9 @@ import { usePostV2TerrafundClipPolygonsSiteUuid, usePostV2TerrafundValidationSitePolygons } from "@/generated/apiComponents"; -import { ClippedPolygonResponse, SitePolygon } from "@/generated/apiSchemas"; -import ApiSlice from "@/store/apiSlice"; +import { ClippedPolygonResponse, SitePolygon, SitePolygonsDataResponse } from "@/generated/apiSchemas"; +import { useValueChanged } from "@/hooks/useValueChanged"; +import JobsSlice from "@/store/jobsSlice"; import Log from "@/utils/log"; import Button from "../../Button/Button"; @@ -54,6 +55,40 @@ interface TransformedData { showWarning: boolean; } +const getTransformedData = ( + sitePolygonData: SitePolygonsDataResponse | undefined, + currentValidationSite: CheckedPolygon[] +) => { + return currentValidationSite.map((checkedPolygon, index) => { + const matchingPolygon = Array.isArray(sitePolygonData) + ? sitePolygonData.find((polygon: SitePolygon) => polygon.poly_id === checkedPolygon.uuid) + : null; + const excludedFromValidationCriterias = [ + COMPLETED_DATA_CRITERIA_ID, + ESTIMATED_AREA_CRITERIA_ID, + WITHIN_COUNTRY_CRITERIA_ID + ]; + const nonValidCriteriasIds = checkedPolygon?.nonValidCriteria?.map(r => r.criteria_id); + const failingCriterias = nonValidCriteriasIds?.filter(r => !excludedFromValidationCriterias.includes(r)); + let isValid = false; + if (checkedPolygon?.nonValidCriteria?.length === 0) { + isValid = true; + } else if (failingCriterias?.length === 0) { + isValid = true; + } + return { + id: index + 1, + valid: checkedPolygon.checked && isValid, + checked: checkedPolygon.checked, + label: matchingPolygon?.poly_name ?? null, + showWarning: + nonValidCriteriasIds?.includes(COMPLETED_DATA_CRITERIA_ID) || + nonValidCriteriasIds?.includes(ESTIMATED_AREA_CRITERIA_ID) || + nonValidCriteriasIds?.includes(WITHIN_COUNTRY_CRITERIA_ID) + }; + }); +}; + const CheckPolygonControl = (props: CheckSitePolygonProps) => { const { siteRecord, polygonCheck, setIsLoadingDelayedJob, isLoadingDelayedJob, setAlertTitle } = props; const siteUuid = siteRecord?.uuid; @@ -98,28 +133,13 @@ const CheckPolygonControl = (props: CheckSitePolygonProps) => { t("Success! TerraMatch reviewed all polygons") ); setIsLoadingDelayedJob?.(false); - ApiSlice.addTotalContent(0); - ApiSlice.addProgressContent(0); - ApiSlice.addProgressMessage(""); + JobsSlice.reset(); }, onError: () => { hideLoader(); setIsLoadingDelayedJob?.(false); setClickedValidation(false); - if (ApiSlice.apiDataStore.abort_delayed_job) { - displayNotification( - t("The Check Polygons processing was cancelled."), - "warning", - t("You can try again later.") - ); - - ApiSlice.abortDelayedJob(false); - ApiSlice.addTotalContent(0); - ApiSlice.addProgressContent(0); - ApiSlice.addProgressMessage(""); - } else { - displayNotification(t("Please try again later."), "error", t("Error! TerraMatch could not review polygons")); - } + displayNotification(t("Please try again later."), "error", t("Error! TerraMatch could not review polygons")); } }); @@ -147,52 +167,13 @@ const CheckPolygonControl = (props: CheckSitePolygonProps) => { closeModal(ModalId.FIX_POLYGONS); }, onError: error => { - if (ApiSlice.apiDataStore.abort_delayed_job) { - displayNotification(t("The Fix Polygons processing was cancelled."), "warning", t("You can try again later.")); - ApiSlice.abortDelayedJob(false); - ApiSlice.addTotalContent(0); - ApiSlice.addProgressContent(0); - ApiSlice.addProgressMessage(""); - } else { - Log.error("Error clipping polygons:", error); - displayNotification(t("An error occurred while fixing polygons. Please try again."), "error", t("Error")); - } + Log.error("Error clipping polygons:", error); + displayNotification(t("An error occurred while fixing polygons. Please try again."), "error", t("Error")); hideLoader(); setIsLoadingDelayedJob?.(false); } }); - const getTransformedData = (currentValidationSite: CheckedPolygon[]) => { - return currentValidationSite.map((checkedPolygon, index) => { - const matchingPolygon = Array.isArray(sitePolygonData) - ? sitePolygonData.find((polygon: SitePolygon) => polygon.poly_id === checkedPolygon.uuid) - : null; - const excludedFromValidationCriterias = [ - COMPLETED_DATA_CRITERIA_ID, - ESTIMATED_AREA_CRITERIA_ID, - WITHIN_COUNTRY_CRITERIA_ID - ]; - const nonValidCriteriasIds = checkedPolygon?.nonValidCriteria?.map(r => r.criteria_id); - const failingCriterias = nonValidCriteriasIds?.filter(r => !excludedFromValidationCriterias.includes(r)); - let isValid = false; - if (checkedPolygon?.nonValidCriteria?.length === 0) { - isValid = true; - } else if (failingCriterias?.length === 0) { - isValid = true; - } - return { - id: index + 1, - valid: checkedPolygon.checked && isValid, - checked: checkedPolygon.checked, - label: matchingPolygon?.poly_name ?? null, - showWarning: - nonValidCriteriasIds?.includes(COMPLETED_DATA_CRITERIA_ID) || - nonValidCriteriasIds?.includes(ESTIMATED_AREA_CRITERIA_ID) || - nonValidCriteriasIds?.includes(WITHIN_COUNTRY_CRITERIA_ID) - }; - }); - }; - const checkHasOverlaps = (currentValidationSite: CheckedPolygon[]) => { for (const record of currentValidationSite) { for (const criteria of record.nonValidCriteria) { @@ -244,24 +225,24 @@ const CheckPolygonControl = (props: CheckSitePolygonProps) => { useEffect(() => { if (currentValidationSite) { setHasOverlaps(checkHasOverlaps(currentValidationSite)); - const transformedData = getTransformedData(currentValidationSite); + const transformedData = getTransformedData(sitePolygonData, currentValidationSite); setSitePolygonCheckData(transformedData); } }, [currentValidationSite, sitePolygonData]); - useEffect(() => { + useValueChanged(sitePolygonData, () => { if (sitePolygonData) { reloadSitePolygonValidation(); } - }, [sitePolygonData]); + }); - useEffect(() => { + useValueChanged(clickedValidation, () => { if (clickedValidation) { setIsLoadingDelayedJob?.(true); setAlertTitle?.("Check Polygons"); getValidations({ queryParams: { uuid: siteUuid ?? "" } }); } - }, [clickedValidation]); + }); return ( diff --git a/src/components/elements/Map-mapbox/MapControls/EditControl.tsx b/src/components/elements/Map-mapbox/MapControls/EditControl.tsx index 093ee2a20..9e27543b0 100644 --- a/src/components/elements/Map-mapbox/MapControls/EditControl.tsx +++ b/src/components/elements/Map-mapbox/MapControls/EditControl.tsx @@ -1,8 +1,9 @@ import { useT } from "@transifex/react"; -import React, { useEffect } from "react"; +import React from "react"; import { When } from "react-if"; import { useMapAreaContext } from "@/context/mapArea.provider"; +import { useOnMount } from "@/hooks/useOnMount"; import Button from "../../Button/Button"; import Text from "../../Text/Text"; @@ -11,11 +12,11 @@ const EditControl = ({ onClick, onSave, onCancel }: { onClick?: any; onSave?: an const t = useT(); const [isEditing, setIsEditing] = React.useState(false); const { selectedPolyVersion } = useMapAreaContext(); - useEffect(() => { + useOnMount(() => { return () => { onCancel(); }; - }, []); + }); const handleSaveButton = () => { onSave(); setIsEditing(false); diff --git a/src/components/elements/Map-mapbox/MapControls/PolygonHandler.tsx b/src/components/elements/Map-mapbox/MapControls/PolygonHandler.tsx index 78565d6fb..c0f054fc5 100644 --- a/src/components/elements/Map-mapbox/MapControls/PolygonHandler.tsx +++ b/src/components/elements/Map-mapbox/MapControls/PolygonHandler.tsx @@ -39,6 +39,7 @@ export const PolygonHandler = () => { uploadFile(); setSaveFlags(false); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [file, saveFlags]); const uploadFile = async () => { diff --git a/src/components/elements/Map-mapbox/MapControls/ProcessBulkPolygonsControl.tsx b/src/components/elements/Map-mapbox/MapControls/ProcessBulkPolygonsControl.tsx index b267f6622..6b391f04a 100644 --- a/src/components/elements/Map-mapbox/MapControls/ProcessBulkPolygonsControl.tsx +++ b/src/components/elements/Map-mapbox/MapControls/ProcessBulkPolygonsControl.tsx @@ -16,7 +16,7 @@ import { usePostV2TerrafundValidationPolygons } from "@/generated/apiComponents"; import { SitePolygon } from "@/generated/apiSchemas"; -import ApiSlice from "@/store/apiSlice"; +import JobsSlice from "@/store/jobsSlice"; const ProcessBulkPolygonsControl = ({ entityData, @@ -127,9 +127,7 @@ const ProcessBulkPolygonsControl = ({ const processedNames = response?.processed?.map(item => item.poly_name).join(", "); setIsLoadingDelayedJob?.(false); - ApiSlice.addTotalContent(0); - ApiSlice.addProgressContent(0); - ApiSlice.addProgressMessage(""); + JobsSlice.reset(); if (processedNames) { openNotification( "success", @@ -144,19 +142,7 @@ const ProcessBulkPolygonsControl = ({ onError: () => { hideLoader(); setIsLoadingDelayedJob?.(false); - if (ApiSlice.apiDataStore.abort_delayed_job) { - openNotification( - "warning", - t("The Fix Polygons processing was cancelled."), - t("You can try again later.") - ); - ApiSlice.abortDelayedJob(false); - ApiSlice.addTotalContent(0); - ApiSlice.addProgressContent(0); - ApiSlice.addProgressMessage(""); - } else { - openNotification("error", t("Error!"), t("Failed to fix polygons")); - } + openNotification("error", t("Error!"), t("Failed to fix polygons")); } } ); @@ -187,26 +173,12 @@ const ProcessBulkPolygonsControl = ({ openNotification("success", t("Success!"), t("Polygons checked successfully")); hideLoader(); setIsLoadingDelayedJob?.(false); - ApiSlice.addTotalContent(0); - ApiSlice.addProgressContent(0); - ApiSlice.addProgressMessage(""); + JobsSlice.reset(); }, onError: () => { hideLoader(); setIsLoadingDelayedJob?.(false); - if (ApiSlice.apiDataStore.abort_delayed_job) { - openNotification( - "warning", - t("The Check Polygons processing was cancelled."), - t("You can try again later.") - ); - ApiSlice.abortDelayedJob(false); - ApiSlice.addTotalContent(0); - ApiSlice.addProgressContent(0); - ApiSlice.addProgressMessage(""); - } else { - openNotification("error", t("Error!"), t("Failed to check polygons")); - } + openNotification("error", t("Error!"), t("Failed to check polygons")); } } ); diff --git a/src/components/elements/Map-mapbox/MapControls/ViewImageCarousel.tsx b/src/components/elements/Map-mapbox/MapControls/ViewImageCarousel.tsx index 242b5d14f..ac0ff6148 100644 --- a/src/components/elements/Map-mapbox/MapControls/ViewImageCarousel.tsx +++ b/src/components/elements/Map-mapbox/MapControls/ViewImageCarousel.tsx @@ -1,9 +1,10 @@ import { useT } from "@transifex/react"; -import { useEffect, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import Icon, { IconNames } from "@/components/extensive/Icon/Icon"; import ModalImageGallery, { TabImagesItem } from "@/components/extensive/Modal/ModalImageGallery"; import { GetV2MODELUUIDFilesResponse } from "@/generated/apiComponents"; +import { useOnMount } from "@/hooks/useOnMount"; import Button from "../../Button/Button"; import Text from "../../Text/Text"; @@ -50,7 +51,7 @@ const ViewImageCarousel = ({ })) } ]; - }, [modelFilesData]); + }, [modelFilesData, t]); const [openModal, setOpenModal] = useState(false); @@ -91,12 +92,12 @@ const ViewImageCarousel = ({ scrollToGalleryElement(); }; - useEffect(() => { + useOnMount(() => { if (sessionStorage.getItem("scrollToElement") === "true") { scrollToGalleryElement(); sessionStorage.removeItem("scrollToElement"); } - }, []); + }); return ( diff --git a/src/components/elements/Map-mapbox/components/DashboardPopup.tsx b/src/components/elements/Map-mapbox/components/DashboardPopup.tsx index 47fd2af18..2ef90453e 100644 --- a/src/components/elements/Map-mapbox/components/DashboardPopup.tsx +++ b/src/components/elements/Map-mapbox/components/DashboardPopup.tsx @@ -113,6 +113,7 @@ export const DashboardPopup = (event: any) => { } else if (itemUuid && layerName === LAYERS_NAMES.POLYGON_GEOMETRY) { fetchPolygonData(); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isoCountry, layerName, itemUuid]); const learnMoreEvent = () => { if (isoCountry && layerName === LAYERS_NAMES.WORLD_COUNTRIES) { diff --git a/src/components/elements/Map-mapbox/components/OverviewMapArea.tsx b/src/components/elements/Map-mapbox/components/OverviewMapArea.tsx index 986f58248..8ac386b49 100644 --- a/src/components/elements/Map-mapbox/components/OverviewMapArea.tsx +++ b/src/components/elements/Map-mapbox/components/OverviewMapArea.tsx @@ -18,6 +18,7 @@ import { import { SitePolygonsDataResponse } from "@/generated/apiSchemas"; import useLoadCriteriaSite from "@/hooks/paginated/useLoadCriteriaSite"; import { useDate } from "@/hooks/useDate"; +import { useValueChanged } from "@/hooks/useValueChanged"; import { createQueryParams } from "@/utils/dashboardUtils"; import MapPolygonPanel from "../../MapPolygonPanel/MapPolygonPanel"; @@ -79,7 +80,7 @@ const OverviewMapArea = ({ loading } = useLoadCriteriaSite(entityModel.uuid, type, checkedValues.join(","), sortOrder); - useEffect(() => { + useValueChanged(loading, () => { setPolygonCriteriaMap(polygonCriteriaMap); setPolygonData(polygonsData); if (loading) { @@ -90,9 +91,10 @@ const OverviewMapArea = ({ } else { callCountryBBox(); } - }, [loading]); + }); useEffect(() => { refetch(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [checkedValues, sortOrder]); const callEntityBbox = async () => { if (type === "sites") { @@ -129,7 +131,7 @@ const OverviewMapArea = ({ if (entityBbox !== null) { setShouldRefetchPolygonData(false); } - }, [entityBbox, polygonsData]); + }, [entityBbox, polygonsData, setShouldRefetchPolygonData]); useEffect(() => { const { isOpen, uuid } = editPolygon; @@ -137,20 +139,20 @@ const OverviewMapArea = ({ if (isOpen) { setSelectedPolygonsInCheckbox([]); } - }, [editPolygon]); + }, [editPolygon, setSelectedPolygonsInCheckbox]); - useEffect(() => { + useValueChanged(shouldRefetchPolygonData, () => { if (shouldRefetchPolygonData) { reloadSiteData?.(); refetch(); } - }, [shouldRefetchPolygonData]); - useEffect(() => { + }); + useValueChanged(shouldRefetchValidation, () => { if (shouldRefetchValidation) { refetch(); setShouldRefetchValidation(false); } - }, [shouldRefetchValidation]); + }); useEffect(() => { if (polygonsData?.length > 0) { const dataMap = parsePolygonData(polygonsData); diff --git a/src/components/elements/Map-mapbox/hooks/useMap.ts b/src/components/elements/Map-mapbox/hooks/useMap.ts index 10ae4b372..942a646c9 100644 --- a/src/components/elements/Map-mapbox/hooks/useMap.ts +++ b/src/components/elements/Map-mapbox/hooks/useMap.ts @@ -11,7 +11,7 @@ import type { ControlType } from "../Map.d"; import { MapStyle } from "../MapControls/types"; import { addFilterOfPolygonsData, convertToGeoJSON } from "../utils"; -const INITIAL_ZOOM = 2.5; +const INITIAL_ZOOM = 2.0; export const useMap = (onSave?: (geojson: any, record: any) => void) => { const { record } = useShowContext(); @@ -45,6 +45,7 @@ export const useMap = (onSave?: (geojson: any, record: any) => void) => { container: mapContainer.current as HTMLDivElement, style: isDashboard ? MapStyle.Street : MapStyle.Satellite, zoom: zoom, + minZoom: 2.0, accessToken: mapboxToken }); diff --git a/src/components/elements/MapPolygonPanel/MapEditPolygonPanel.tsx b/src/components/elements/MapPolygonPanel/MapEditPolygonPanel.tsx index 98a371dd3..d935fde7c 100644 --- a/src/components/elements/MapPolygonPanel/MapEditPolygonPanel.tsx +++ b/src/components/elements/MapPolygonPanel/MapEditPolygonPanel.tsx @@ -1,11 +1,13 @@ import { useT } from "@transifex/react"; import classNames from "classnames"; -import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import { Dispatch, SetStateAction, useState } from "react"; import { When } from "react-if"; import Icon, { IconNames } from "@/components/extensive/Icon/Icon"; import { useMapAreaContext } from "@/context/mapArea.provider"; import { SitePolygon, SitePolygonsDataResponse, V2TerrafundCriteriaData } from "@/generated/apiSchemas"; +import { useOnMount } from "@/hooks/useOnMount"; +import { useValueChanged } from "@/hooks/useValueChanged"; import Button from "../Button/Button"; import Text from "../Text/Text"; @@ -46,9 +48,9 @@ const MapEditPolygonPanel = ({ setHasOverlaps } = useMapAreaContext(); const { onCancel } = mapFunctions; - useEffect(() => { + useOnMount(() => { setTabEditPolygon("Attributes"); - }, []); + }); const handleClose = () => { setEditPolygon?.({ isOpen: false, uuid: "", primary_uuid: "" }); setHasOverlaps(false); @@ -72,13 +74,13 @@ const MapEditPolygonPanel = ({ return false; }; - useEffect(() => { + useValueChanged(polygonMap, () => { const criteriaDataPolygon = polygonMap[editPolygon?.uuid ?? ""]; if (criteriaDataPolygon) { setHasOverlaps(hasOverlaps(criteriaDataPolygon)); setCriteriaData(criteriaDataPolygon); } - }, [polygonMap]); + }); return ( <> diff --git a/src/components/elements/MapPolygonPanel/MapMenuPanelItem.tsx b/src/components/elements/MapPolygonPanel/MapMenuPanelItem.tsx index f84d8d37e..4e8b5db5b 100644 --- a/src/components/elements/MapPolygonPanel/MapMenuPanelItem.tsx +++ b/src/components/elements/MapPolygonPanel/MapMenuPanelItem.tsx @@ -73,7 +73,7 @@ const MapMenuPanelItem = ({ } else { setValidationStatus(undefined); } - }, [polygonMap, setValidationStatus]); + }, [poly_id, polygonMap, setValidationStatus]); const openFormModalHandlerConfirm = () => { openModal( diff --git a/src/components/elements/MapPolygonPanel/MapPolygonCheckPanel.tsx b/src/components/elements/MapPolygonPanel/MapPolygonCheckPanel.tsx index 5b6e61032..b1b20344a 100644 --- a/src/components/elements/MapPolygonPanel/MapPolygonCheckPanel.tsx +++ b/src/components/elements/MapPolygonPanel/MapPolygonCheckPanel.tsx @@ -68,7 +68,6 @@ const MapPolygonCheckPanel = ({ emptyText, onLoadMore, selected, mapFunctions }: const { siteData } = useMapAreaContext(); const context = useSitePolygonData(); const [polygonsValidationData, setPolygonsValidationData] = useState([]); - const sitePolygonData = context?.sitePolygonData ?? []; const { data: currentValidationSite } = useGetV2TerrafundValidationSite( { @@ -83,10 +82,11 @@ const MapPolygonCheckPanel = ({ emptyText, onLoadMore, selected, mapFunctions }: useEffect(() => { if (currentValidationSite) { + const sitePolygonData = context?.sitePolygonData ?? []; const data = parseData(sitePolygonData, currentValidationSite, validationLabels); setPolygonsValidationData(data); } - }, [currentValidationSite, sitePolygonData]); + }, [context?.sitePolygonData, currentValidationSite]); return ( <> diff --git a/src/components/elements/MapPolygonPanel/VersionInformation.tsx b/src/components/elements/MapPolygonPanel/VersionInformation.tsx index 3441e2211..d7b0ea6ac 100644 --- a/src/components/elements/MapPolygonPanel/VersionInformation.tsx +++ b/src/components/elements/MapPolygonPanel/VersionInformation.tsx @@ -22,6 +22,7 @@ import { usePutV2SitePolygonUuidMakeActive } from "@/generated/apiComponents"; import { SitePolygon, SitePolygonsDataResponse } from "@/generated/apiSchemas"; +import { useValueChanged } from "@/hooks/useValueChanged"; import { FileType, UploadedFile } from "@/types/common"; import { getErrorMessageFromPayload } from "@/utils/errors"; @@ -91,6 +92,7 @@ const VersionInformation = ({ uploadFiles(); setSaveFlags(false); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [files, saveFlags]); const getFileType = (file: UploadedFile) => { @@ -267,9 +269,9 @@ const VersionInformation = ({ }); }; - useEffect(() => { + useValueChanged(selectedPolyVersion, () => { recallEntityData?.(); - }, [selectedPolyVersion]); + }); const makeActivePolygon = async (polygon: any) => { const versionActive = (polygonVersionData as SitePolygonsDataResponse)?.find( diff --git a/src/components/elements/MapSidePanel/MapSidePanel.tsx b/src/components/elements/MapSidePanel/MapSidePanel.tsx index a62011fce..1a86412cb 100644 --- a/src/components/elements/MapSidePanel/MapSidePanel.tsx +++ b/src/components/elements/MapSidePanel/MapSidePanel.tsx @@ -116,6 +116,7 @@ const MapSidePanel = ({ } setClickedButton(""); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [clickedButton, selected]); useEffect(() => { @@ -222,6 +223,7 @@ const MapSidePanel = ({ itemAs={Fragment} render={item => ( { + const firstRender = useRef(true); const t = useT(); const [openModalNotification, setOpenModalNotification] = useState(false); const [isLoaded, { delayedJobs }] = useDelayedJobs(); const [notAcknowledgedJobs, setNotAcknowledgedJobs] = useState([]); + const clearJobs = () => { if (delayedJobs === undefined) return; const newJobsData: DelayedJobData[] = delayedJobs @@ -42,16 +36,23 @@ const FloatNotification = () => { }); triggerBulkUpdate(newJobsData); }; - useEffect(() => { - if (delayedJobs === undefined) return; - const notAcknowledgedJobs = delayedJobs.filter((job: DelayedJobDto) => !job.isAcknowledged); - setNotAcknowledgedJobs(notAcknowledgedJobs); - }, [delayedJobs]); - useEffect(() => { - if (!notAcknowledgedJobs.length) { + + useValueChanged(delayedJobs, () => { + if (!delayedJobs) return; + + setNotAcknowledgedJobs(delayedJobs); + if (delayedJobs.length > notAcknowledgedJobs.length && !firstRender.current) { + setOpenModalNotification(true); + } + firstRender.current = false; + }); + + useValueChanged(notAcknowledgedJobs.length, () => { + if (notAcknowledgedJobs.length === 0) { setOpenModalNotification(false); } - }, [notAcknowledgedJobs]); + }); + const listOfPolygonsFixed = (data: Record | null) => { if (data?.updated_polygons) { const updatedPolygonNames = data.updated_polygons @@ -72,20 +73,27 @@ const FloatNotification = () => { - + {t("Notifications")} - + - - {t("Actions Taken")} + + {t("Uploads")} - + {t("Clear completed")} @@ -93,17 +101,22 @@ const FloatNotification = () => { {isLoaded && notAcknowledgedJobs && notAcknowledgedJobs.map((item, index) => ( - - + + {item.name} + {/* + + + + */} Site: {item.entityName} - + {item.status === "failed" ? ( {item.payload ? t(getErrorMessageFromPayload(item.payload)) : t("Failed to complete")} @@ -146,8 +159,8 @@ const FloatNotification = () => { )} - {item.status === "succeeded" && ( - + {item.status === "succeeded" && listOfPolygonsFixed(item.payload) && ( + {listOfPolygonsFixed(item.payload)} )} @@ -158,7 +171,7 @@ const FloatNotification = () => { 0}> - + {notAcknowledgedJobs?.length} @@ -167,7 +180,7 @@ const FloatNotification = () => { setOpenModalNotification(!openModalNotification); }} className={classNames( - "z-10 flex h-15 w-15 items-center justify-center rounded-full border border-grey-950 bg-primary duration-300 hover:scale-105", + "z-10 flex h-13 w-13 items-center justify-center rounded-full border border-grey-950 bg-primary duration-300 hover:scale-105", { hidden: (notAcknowledgedJobs?.length ?? 0) === 0, visible: (notAcknowledgedJobs?.length ?? 0) > 0 @@ -177,8 +190,8 @@ const FloatNotification = () => { diff --git a/src/components/elements/ServerSideTable/ServerSideTable.tsx b/src/components/elements/ServerSideTable/ServerSideTable.tsx index 244610031..241cc5158 100644 --- a/src/components/elements/ServerSideTable/ServerSideTable.tsx +++ b/src/components/elements/ServerSideTable/ServerSideTable.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; import Table, { TableProps } from "@/components/elements/Table/Table"; import { FilterValue } from "@/components/elements/TableFilters/TableFilter"; import Pagination from "@/components/extensive/Pagination"; +import { VARIANT_PAGINATION_DASHBOARD } from "@/components/extensive/Pagination/PaginationVariant"; import { getQueryParams } from "@/helpers/api"; import { useDebounce } from "@/hooks/useDebounce"; @@ -58,6 +59,7 @@ export function ServerSideTable({ {props.meta?.last_page > 1 && ( page < props.meta?.last_page!} getCanPreviousPage={() => page > 1} getPageCount={() => props.meta?.last_page || 1} diff --git a/src/components/elements/Table/Table.tsx b/src/components/elements/Table/Table.tsx index e04925083..4563fc4ae 100644 --- a/src/components/elements/Table/Table.tsx +++ b/src/components/elements/Table/Table.tsx @@ -58,6 +58,7 @@ export interface TableProps contentClassName?: string; classNameTableWrapper?: string; galleryType?: string; + classPagination?: string; } export interface TableState { @@ -93,6 +94,7 @@ function Table({ onRowClick, contentClassName, galleryType, + classPagination, ...props }: TableProps) { const t = useT(); @@ -305,7 +307,7 @@ function Table({ setPageIndex={setPageIndex} setPageSize={setPageSize} defaultPageSize={defaultPageSize} - containerClassName="mt-6" + containerClassName={classNames("mt-6", classPagination)} hasPageSizeSelector={rowCount > defaultPageSize} invertSelect={invertSelectPagination} galleryType={galleryType} diff --git a/src/components/elements/Table/TableVariants.ts b/src/components/elements/Table/TableVariants.ts index 54f24dd19..fedc54b3e 100644 --- a/src/components/elements/Table/TableVariants.ts +++ b/src/components/elements/Table/TableVariants.ts @@ -136,32 +136,35 @@ export const VARIANT_TABLE_ORGANISATION = { }; export const VARIANT_TABLE_DASHBOARD_COUNTRIES = { - table: "border-collapse", + table: "border-collapse mobile:border-separate mobile:bg-neutral-200 border-spacing-x-0 !border-spacing-y-px", name: "border-airtable", - tableWrapper: "border border-neutral-200 rounded-lg overflow-auto max-h-[260px] lg:max-h-[303px] wide:max-h-[321px]", - trHeader: "bg-neutral-150 sticky top-0 z-[1]", - thHeader: "text-nowrap first:pl-3 first:pr-2 last:pl-2 last:pr-3 border-y border-neutral-200 text-14 px-3 border-t-0", + tableWrapper: + "border border-neutral-200 rounded-lg overflow-auto max-h-[260px] lg:max-h-[303px] wide:max-h-[321px] mobile:border-0", + trHeader: "bg-neutral-150 sticky top-0 z-[1] mobile:bg-white mobile:border-t mobile:border-neutral-200", + thHeader: + "text-nowrap first:pl-3 first:pr-2 last:pl-2 last:pr-3 border-y border-neutral-200 text-14 mobile:text-12-semibold px-3 border-t-0 mobile:py-3 mobile:first:pl-0 mobile:last:pr-0 mobile:border-b-2", tBody: "", trBody: "bg-white border-y border-neutral-200 last:border-b-0", - tdBody: "text-14-light px-3 py-3 first:pl-4 first:pr-2 last:pl-2 last:pr-4", + tdBody: "text-14-light px-3 py-3 first:pl-4 first:pr-2 last:pl-2 last:pr-4 mobile:first:pl-0 mobile:last:pr-0", thead: "bg-blueCustom-100 ", paginationVariant: VARIANT_PAGINATION_DASHBOARD }; export const VARIANT_TABLE_DASHBOARD_COUNTRIES_MODAL = { - table: "border-collapse", + table: "border-collapse mobile:border-separate mobile:bg-neutral-200 border-spacing-x-0 !border-spacing-y-px", name: "border-airtable", - tableWrapper: "border border-neutral-200 rounded-lg overflow-auto max-h-[calc(90vh-208px)]", - trHeader: "bg-neutral-150 sticky top-0 z-10 ", - thHeader: "text-nowrap first:pl-3 first:pr-2 last:pl-2 last:pr-3 border-y border-neutral-200 text-12 px-3 border-t-0", + tableWrapper: "border border-neutral-200 rounded-lg overflow-auto max-h-[calc(90vh-208px)] mobile:border-0", + trHeader: "bg-neutral-150 sticky top-0 z-10 mobile:bg-white mobile:border-t mobile:border-neutral-200", + thHeader: + "text-nowrap first:pl-3 first:pr-2 last:pl-2 last:pr-3 border-y border-neutral-200 mobile:text-12-semibold text-12 px-3 border-t-0 mobile:py-3 mobile:first:pl-0 mobile:last:pr-0", tBody: "", trBody: "bg-white border-y border-neutral-200 last:border-b-0", - tdBody: "text-12-light px-3 py-3 first:pl-4 first:pr-2 last:pl-2 last:pr-4", + tdBody: "text-14-light px-3 py-3 first:pl-4 first:pr-2 last:pl-2 last:pr-4 mobile:first:pl-0 mobile:last:pr-0", thead: "bg-blueCustom-100 ", paginationVariant: VARIANT_PAGINATION_DASHBOARD }; -export const VARIANT_TABLE_DASHBOARD = { +export const VARIANT_TABLE_DASHBOARD_LIST = { className: "h-full", table: "border-collapse", name: "border-airtable", @@ -172,7 +175,7 @@ export const VARIANT_TABLE_DASHBOARD = { tBody: "", trBody: "bg-white border-y border-neutral-200 last:border-b-0", tdBody: "text-14-light px-2 py-4 first:pl-4 first:pr-2 last:pl-2 last:pr-4", - thead: "text-14-semibold bg-blueCustom-100", + thead: "text-14-semibold bg-blueCustom-100 mobile:hidden", paginationVariant: VARIANT_PAGINATION_DASHBOARD }; diff --git a/src/components/elements/Table/__snapshots__/Table.stories.storyshot b/src/components/elements/Table/__snapshots__/Table.stories.storyshot index ebac4acaa..38290f23a 100644 --- a/src/components/elements/Table/__snapshots__/Table.stories.storyshot +++ b/src/components/elements/Table/__snapshots__/Table.stories.storyshot @@ -6730,20 +6730,20 @@ exports[`Storyshots Components/Elements/Table Table Dashboard Countries 1`] = ` Cash VISA 2000 Cash Master Card 1000 Credit American Express 1500 Cash VISA 7500 Credit American Express 1900 Credit Master Card 5000 Credit VISA 100000 Cash VISA 60000 Credit Master Card 15000 Cash VISA 1510000 Cash VISA 2000 Cash Master Card 1000 Credit American Express 1500 Cash VISA 7500 Credit American Express 1900 Credit Master Card 5000 Credit VISA 100000 Cash VISA 60000 Credit Master Card 15000 Cash VISA 1510000 void; } @@ -22,6 +23,7 @@ const FilterSearchBox = ({ value, onChange, className, + suffix, ...props }: PropsWithChildren) => { return ( @@ -34,6 +36,7 @@ const FilterSearchBox = ({ className={variant.input} value={value} /> + {suffix} ); }; diff --git a/src/components/elements/TableFilters/Inputs/FilterSearchBoxVariants.ts b/src/components/elements/TableFilters/Inputs/FilterSearchBoxVariants.ts index 0f8b67558..980f1b7fe 100644 --- a/src/components/elements/TableFilters/Inputs/FilterSearchBoxVariants.ts +++ b/src/components/elements/TableFilters/Inputs/FilterSearchBoxVariants.ts @@ -30,3 +30,12 @@ export const FILTER_SEARCH_MONITORING = { iconClassName: "w-5 h-5 text-darkCustom", input: "text-14-light w-full p-0 border-0 placeholder:text-darkCustom focus:ring-0 bg-transparent text-darkCustom" }; + +export const FILTER_SEARCH_IMPACT_STORY = { + container: + "flex items-center gap-2 rounded-full pl-6 z-10 relative border-neutral-200 bg-white hover:shadow-monitored border", + icon: IconNames.SEARCH_PA, + iconClassName: "hidden", + input: + "text-16-light w-full px-0 py-3 border-0 placeholder:text-darkCustom focus:ring-0 bg-transparent text-darkCustom" +}; diff --git a/src/components/elements/Tabs/Secondary/SecondaryTabs.tsx b/src/components/elements/Tabs/Secondary/SecondaryTabs.tsx index 9ed2abc91..b5669f7f3 100644 --- a/src/components/elements/Tabs/Secondary/SecondaryTabs.tsx +++ b/src/components/elements/Tabs/Secondary/SecondaryTabs.tsx @@ -1,12 +1,16 @@ import { Tab as HTab } from "@headlessui/react"; +import { useMediaQuery } from "@mui/material"; import classNames from "classnames"; import { useRouter } from "next/router"; -import { DetailedHTMLProps, Fragment, HTMLAttributes, ReactElement, useEffect } from "react"; +import { DetailedHTMLProps, Fragment, HTMLAttributes, ReactElement, useRef, useState } from "react"; import Text from "@/components/elements/Text/Text"; +import Icon, { IconNames } from "@/components/extensive/Icon/Icon"; import List from "@/components/extensive/List/List"; import { Framework, useFrameworkContext } from "@/context/framework.provider"; +import { useValueChanged } from "@/hooks/useValueChanged"; +import Button from "../../Button/Button"; import { SecundaryTabsVariants, VARIANT_TABS_PRIMARY } from "./SecuandaryTabsVariants"; export interface SecondaryTabsProps extends DetailedHTMLProps, HTMLDivElement> { @@ -15,6 +19,7 @@ export interface SecondaryTabsProps extends DetailedHTMLProps void; variant?: SecundaryTabsVariants; selectedIndex?: number; + scrollable?: boolean; } export interface TabItem { @@ -36,11 +41,16 @@ const SecondaryTabs = ({ containerClassName, setSelectedIndex, selectedIndex, + scrollable = false, variant = VARIANT_TABS_PRIMARY, ...divProps }: SecondaryTabsProps) => { const router = useRouter(); const { framework } = useFrameworkContext(); + const ContentListRef = useRef(null); + const [scrollLeft, setScrollLeft] = useState(0); + const [selectedIndexTab, setSelectedIndexTab] = useState(0); + const isMobile = useMediaQuery("(max-width: 1200px)"); const tabItems = _tabItems.filter(item => { if (item.show != null) { return item.show.includes(framework); @@ -58,7 +68,7 @@ const SecondaryTabs = ({ const onTabChange = (index: number) => { const key = tabItems[index].key; - + setSelectedIndexTab(index); if (key) { router.query.tab = key; router.push(router, undefined, { shallow: true }); @@ -66,49 +76,122 @@ const SecondaryTabs = ({ setSelectedIndex && setSelectedIndex(index); }; - useEffect(() => { + useValueChanged(selectedIndex, () => { if (selectedIndex !== undefined) { onTabChange(selectedIndex); } - }, [selectedIndex]); + }); + + const handleScrollNext = () => { + if (ContentListRef.current) { + if (isMobile) { + const scrollWidth = ContentListRef.current.scrollWidth; + ContentListRef.current.scrollLeft = ContentListRef.current.scrollLeft + scrollWidth / tabItems.length; + onTabChange(selectedIndexTab + 1); + } else { + ContentListRef.current.scrollLeft = ContentListRef.current.scrollLeft + 75; + } + } + }; + + const handleScrollPrev = () => { + if (ContentListRef.current) { + if (isMobile) { + const scrollWidth = ContentListRef.current.scrollWidth; + ContentListRef.current.scrollLeft = ContentListRef.current.scrollLeft - scrollWidth / tabItems.length; + onTabChange(selectedIndexTab - 1); + } else { + ContentListRef.current.scrollLeft = ContentListRef.current.scrollLeft - 75; + } + } + }; + + const handleScroll = (event: React.UIEvent) => { + setScrollLeft(event.currentTarget.scrollLeft); + }; return ( - - - ( - - {({ selected }) => ( - - - {item.title} - - + + + {scrollable && scrollLeft > 0 && ( + + + onClick={handleScrollPrev} + > + + + + )} + + ( + + {({ selected }) => ( + + + {item.title} + + + )} + + )} + /> + + {scrollable && + scrollLeft < (ContentListRef.current?.scrollWidth ?? 0) - (ContentListRef.current?.clientWidth ?? 0) - 2 && ( + + + + + )} - /> - - item.body} /> - + item.body} /> + + ); }; diff --git a/src/components/elements/Tabs/Secondary/SecuandaryTabsVariants.ts b/src/components/elements/Tabs/Secondary/SecuandaryTabsVariants.ts index d7857e804..ce2c15c16 100644 --- a/src/components/elements/Tabs/Secondary/SecuandaryTabsVariants.ts +++ b/src/components/elements/Tabs/Secondary/SecuandaryTabsVariants.ts @@ -19,10 +19,21 @@ export const VARIANT_TABS_PRIMARY: SecundaryTabsVariants = { }; export const VARIANT_TABS_ABOUT_US: SecundaryTabsVariants = { - classNameContentList: "border-b-2 border-neutral-200 bg-white", - listClassName: "grid grid-cols-5 gap-4", - itemTabClassName: " px-2 py-5 text-center border-b-4 text-neutral-700", - selectedTabClassName: "border-black !text-black", + classNameContentList: "border-b-2 border-neutral-200 bg-white mobile:overflow-scroll mobile:border-none", + listClassName: "grid grid-cols-5 gap-4 mobile:flex mobile:w-full mobile:gap-0", + itemTabClassName: " px-2 py-5 text-center border-b-4 text-neutral-700 mobile:w-full mobile:min-w-full mobile:p-0", + selectedTabClassName: "border-black !text-black mobile:border-none", + textVariantSelected: "text-18", + textVariantNotSelected: "text-18" +}; + +export const VARIANT_TABS_IMPACT_STORY: SecundaryTabsVariants = { + classNameContentList: " bg-transparent overflow-scroll mobile:overflow-scroll mobile:border-none", + listClassName: + "flex gap-4 overflow-scroll w-max border-b-2 border-neutral-200 !overflow-visible mobile:w-full mobile:gap-0 mobile:border-none", + itemTabClassName: + "px-7 py-2 lg:py-3 text-center border-b-4 !mb-[-1.5px] text-neutral-700 bg-transparent overflow-visible z-10 mobile:p-0 mobile:w-full mobile:min-w-full", + selectedTabClassName: "border-black !text-black mobile:border-none", textVariantSelected: "text-18", textVariantNotSelected: "text-18" }; diff --git a/src/components/elements/Tabs/Secondary/__snapshots__/SecondaryTabs.stories.storyshot b/src/components/elements/Tabs/Secondary/__snapshots__/SecondaryTabs.stories.storyshot index a63fcab49..71f534535 100644 --- a/src/components/elements/Tabs/Secondary/__snapshots__/SecondaryTabs.stories.storyshot +++ b/src/components/elements/Tabs/Secondary/__snapshots__/SecondaryTabs.stories.storyshot @@ -5,120 +5,125 @@ exports[`Storyshots Components/Elements/Tabs Secondary 1`] = ` className="h-[40px] w-full bg-neutral-150" > - - - Overview - - - - + Overview + + + - Environmental Impact - - - - + Environmental Impact + + + - Social Impact - - - - - - - - step 1 body + + Social Impact + + - + + + step 1 body + + + - + + tabIndex={-1} + /> + `; diff --git a/src/components/elements/Toggle/ToggleVariants.ts b/src/components/elements/Toggle/ToggleVariants.ts index 4fc897a89..08cc11b29 100644 --- a/src/components/elements/Toggle/ToggleVariants.ts +++ b/src/components/elements/Toggle/ToggleVariants.ts @@ -15,9 +15,9 @@ export const VARIANT_TOGGLE_PRIMARY = { }; export const VARIANT_TOGGLE_DASHBOARD = { - container: "bg-white border-b border-grey-1000 p-0", + container: "bg-white border-b border-grey-1000 p-0 mobile:w-full mobile:mb-2", activeToggle: "uppercase bg-transparent border-b-2 border-blueCustom-700 h-[calc(100%_-_2px)] left-1", - textActive: "text-12-bold text-blueCustom-700 pb-1 uppercase", - textInactive: "text-12-light text-darkCustom-40 pb-1 uppercase", + textActive: "text-12-bold text-blueCustom-700 pb-1 uppercase mobile:text-14-bold mobile:w-1/2", + textInactive: "text-12-light text-darkCustom-40 pb-1 uppercase mobile:text-14-light mobile:w-1/2", heightBtn: 2 }; diff --git a/src/components/elements/Tooltip/Tooltip.tsx b/src/components/elements/Tooltip/Tooltip.tsx index 25054f167..7fe25b88b 100644 --- a/src/components/elements/Tooltip/Tooltip.tsx +++ b/src/components/elements/Tooltip/Tooltip.tsx @@ -4,6 +4,7 @@ import { ReactNode, useEffect, useRef, useState } from "react"; import { When } from "react-if"; import { twMerge as tw } from "tailwind-merge"; +import { useOnMount } from "@/hooks/useOnMount"; import { TextVariants } from "@/types/common"; import Text from "../Text/Text"; @@ -57,12 +58,11 @@ const ToolTip = ({ document.removeEventListener("mousedown", handleClickOutside); }; }); + const handleScroll = () => { + setIsVisible(false); + }; useEffect(() => { - const handleScroll = () => { - setIsVisible(false); - }; - window.addEventListener("scroll", handleScroll, true); return () => { window.removeEventListener("scroll", handleScroll, true); @@ -84,7 +84,7 @@ const ToolTip = ({ } }; - useEffect(() => { + useOnMount(() => { const handleResize = () => { updateTooltipPosition(); }; @@ -93,7 +93,7 @@ const ToolTip = ({ return () => { window.removeEventListener("resize", handleResize); }; - }, []); + }); const updateTooltipPosition = () => { const position = contentRef.current?.getBoundingClientRect(); diff --git a/src/components/extensive/EntityMapAndGalleryCard/EntityMapAndGalleryCard.tsx b/src/components/extensive/EntityMapAndGalleryCard/EntityMapAndGalleryCard.tsx index 2d9377f5f..10438f691 100644 --- a/src/components/extensive/EntityMapAndGalleryCard/EntityMapAndGalleryCard.tsx +++ b/src/components/extensive/EntityMapAndGalleryCard/EntityMapAndGalleryCard.tsx @@ -1,6 +1,6 @@ import { useT } from "@transifex/react"; import { useRouter } from "next/router"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import { Else, If, Then } from "react-if"; import Button from "@/components/elements/Button/Button"; @@ -25,6 +25,7 @@ import { } from "@/generated/apiComponents"; import { getCurrentPathEntity } from "@/helpers/entity"; import { useGetImagesGeoJSON } from "@/hooks/useImageGeoJSON"; +import { useValueChanged } from "@/hooks/useValueChanged"; import { EntityName, FileType } from "@/types/common"; import Log from "@/utils/log"; @@ -134,12 +135,12 @@ const EntityMapAndGalleryCard = ({ return mapping?.[modelName] || []; }, [modelName, t]); - useEffect(() => { + useValueChanged(shouldRefetchMediaData, () => { if (shouldRefetchMediaData) { refetch(); setShouldRefetchMediaData(false); } - }, [shouldRefetchMediaData]); + }); const openFormModalHandlerUploadImages = () => { openModal( diff --git a/src/components/extensive/Form/Form.tsx b/src/components/extensive/Form/Form.tsx index 5df06d9e7..67f857863 100644 --- a/src/components/extensive/Form/Form.tsx +++ b/src/components/extensive/Form/Form.tsx @@ -20,13 +20,14 @@ interface FormFooterProps extends DetailedHTMLProps { +const Form = ({ children, formType, className, ...rest }: FormProps) => { return ( {children} diff --git a/src/components/extensive/Icon/Icon.tsx b/src/components/extensive/Icon/Icon.tsx index 9b8723a55..498318611 100644 --- a/src/components/extensive/Icon/Icon.tsx +++ b/src/components/extensive/Icon/Icon.tsx @@ -25,6 +25,8 @@ export enum IconNames { PROFILE = "profile", MENU = "menu", TREE = "tree", + LOGOUT = "logout", + IC_SWITCH = "ic-switch", MAIL_OPEN = "mail-open", MAIL_SENT = "mail-sent", CHECK_CIRCLE = "check-circle", @@ -47,7 +49,8 @@ export enum IconNames { CALENDAR = "calendar", BRANCH_CIRCLE = "branch-circle", LIGHT_BULB_CIRCLE = "light-bulb-circle", - LINKEDLIN = "linkedin", + LINKEDIN = "linkedin", + TWITTER = "twitter", INFO_CIRCLE = "info-circle", INFO_CIRCLE_ALT = "info-circle-alt", INSTAGRAM = "instagram", @@ -101,6 +104,7 @@ export enum IconNames { USER_INVESTOR = "ic-investor", NO_SUCCESS = "no-success", CLEAR = "clear", + CLEAR_DASHBOARD = "clear-dashboard", IC_ERROR = "ic-error", IC_ERROR_PANEL = "ic-error-panel", IC_WARNING = "ic-warning", @@ -175,6 +179,7 @@ export enum IconNames { PROJECT_PROFILE = "project-profile", DASHBOARD_AIRTABLE = "dashboard-airtable", DASHBOARD_REPORTS = "dashboard-reports", + DASHBOARD_IMPACT_STORY = "dashboard-impact-story", ABOUT_US = "about-us", EXPAND = "expand", COLLAPSE = "collapse", @@ -192,6 +197,7 @@ export enum IconNames { PIN = "pin", LINK_AIRTABLE = "link-aritable", IC_USER = "ic-user", + MY_ACCOUNT = "my-account", MONITORING_PROFILE = "monitoring-profile", DASHBOARD = "dashboard", RUN_ALALYSIS = "run-analysis", @@ -223,7 +229,15 @@ export enum IconNames { NON_TREES_PLANTED_CIRCLE = "non-trees-planted-circle", SEED_PLANTED = "seed-planted", TREES_REGENERATING = "trees-regenerating", - NON_TRESS_PLANTED = "non-trees-planted" + NON_TRESS_PLANTED = "non-trees-planted", + ARROW_UP_RIGHT = "arrow-up-right", + SHARE_IMPACT_STORY = "share-impact-story", + BRIEFCASE = "briefcase", + EARTH_DASHBOARD = "earth-dashboard", + CHECK = "check", + IC_MENU = "ic-menu", + CHECK_LANGUAGES = "check-laguages", + NO_CHECK_LANGUAGES = "no-check-laguages" } export interface IconProps { diff --git a/src/components/extensive/Icon/IconSocial.tsx b/src/components/extensive/Icon/IconSocial.tsx index 5daf7a1cc..d2e4b423a 100644 --- a/src/components/extensive/Icon/IconSocial.tsx +++ b/src/components/extensive/Icon/IconSocial.tsx @@ -11,14 +11,15 @@ export type IconSocialProps = { | IconNames.SOCIAL_TWITTER | IconNames.EARTH; url?: string; + className?: string; }; -const IconSocial = ({ name, url }: IconSocialProps) => { +const IconSocial = ({ name, url, className }: IconSocialProps) => { return ( - + diff --git a/src/components/extensive/Icon/IconSocialImpactStory.tsx b/src/components/extensive/Icon/IconSocialImpactStory.tsx new file mode 100644 index 000000000..4956a1c3b --- /dev/null +++ b/src/components/extensive/Icon/IconSocialImpactStory.tsx @@ -0,0 +1,24 @@ +import classNames from "classnames"; +import Link from "next/link"; +import { When } from "react-if"; + +import Icon, { IconNames } from "./Icon"; + +export type IconSocialImpactStoryProps = { + name: IconNames.FACEBOOK | IconNames.INSTAGRAM | IconNames.LINKEDIN | IconNames.TWITTER; + + url?: string; + className?: string; +}; + +const IconSocialImpactStory = ({ name, url, className }: IconSocialImpactStoryProps) => { + return ( + + + + + + ); +}; + +export default IconSocialImpactStory; diff --git a/src/components/extensive/Modal/ModalConst.ts b/src/components/extensive/Modal/ModalConst.ts index 8faf471aa..f7c521343 100644 --- a/src/components/extensive/Modal/ModalConst.ts +++ b/src/components/extensive/Modal/ModalConst.ts @@ -50,5 +50,7 @@ export const ModalId = { WELCOME_MODAL: "WelcomeModal", WARNING: "Warning", MODAL_RUN_ANALYSIS: "ModalRunAnalysis", - MODAL_NOTES: "ModalNotes" + MODAL_NOTES: "ModalNotes", + MODAL_SHARE_IMPACT_STORY: "ModalShareImpactStory", + MODAL_STORY: "ModalStory" }; diff --git a/src/components/extensive/Modal/ModalExpand.tsx b/src/components/extensive/Modal/ModalExpand.tsx index c2ec3735e..27532785a 100644 --- a/src/components/extensive/Modal/ModalExpand.tsx +++ b/src/components/extensive/Modal/ModalExpand.tsx @@ -1,3 +1,4 @@ +import { useMediaQuery } from "@mui/material"; import { useT } from "@transifex/react"; import { DetailedHTMLProps, FC, HTMLAttributes } from "react"; import { When } from "react-if"; @@ -19,28 +20,31 @@ export interface ModalExpandProps extends ModalBaseProps { const ModalExpand: FC = ({ id, title, children, popUpContent, closeModal, ...rest }) => { const t = useT(); + const isMobile = useMediaQuery("(max-width: 1200px)"); ; return ( - + - + {t(title)} - + - closeModal(id)}> + closeModal(id)}> - - {t("Collapse")} - + {!isMobile && ( + + {t("Collapse")} + + )} diff --git a/src/components/extensive/Modal/ModalFixOverlaps.tsx b/src/components/extensive/Modal/ModalFixOverlaps.tsx index 66aa76adb..e596a6e79 100644 --- a/src/components/extensive/Modal/ModalFixOverlaps.tsx +++ b/src/components/extensive/Modal/ModalFixOverlaps.tsx @@ -116,7 +116,7 @@ const ModalFixOverlaps: FC = ({ }; }) ); - }, [polygonList, polygonsCriteriaData, t, memoizedCheckValidCriteria]); + }, [polygonList, polygonsCriteriaData, t, memoizedCheckValidCriteria, selectedUUIDs]); return ( diff --git a/src/components/extensive/Modal/ModalImageDetails.tsx b/src/components/extensive/Modal/ModalImageDetails.tsx index 27ae96f67..b7ceab004 100644 --- a/src/components/extensive/Modal/ModalImageDetails.tsx +++ b/src/components/extensive/Modal/ModalImageDetails.tsx @@ -1,6 +1,6 @@ import { useT } from "@transifex/react"; import Image from "next/image"; -import React, { FC, useEffect, useState } from "react"; +import React, { FC, useState } from "react"; import Button from "@/components/elements/Button/Button"; import Input from "@/components/elements/Inputs/Input/Input"; @@ -14,6 +14,7 @@ import Modal from "@/components/extensive/Modal/Modal"; import { useModalContext } from "@/context/modal.provider"; import { useNotificationContext } from "@/context/notification.provider"; import { usePatchV2MediaProjectProjectMediaUuid, usePatchV2MediaUuid } from "@/generated/apiComponents"; +import { useOnMount } from "@/hooks/useOnMount"; import Log from "@/utils/log"; import Icon, { IconNames } from "../Icon/Icon"; @@ -66,9 +67,9 @@ const ModalImageDetails: FC = ({ const { mutate: updateMedia, isLoading: isUpdating } = usePatchV2MediaUuid(); const { mutateAsync: updateIsCoverAsync, isLoading: isUpdatingCover } = usePatchV2MediaProjectProjectMediaUuid(); - useEffect(() => { + useOnMount(() => { setInitialFormData({ ...formData }); - }, []); + }); const handleInputChange = (name: string, value: string | boolean) => { setFormData(prev => ({ ...prev, [name]: value })); diff --git a/src/components/extensive/Modal/ModalRoot.tsx b/src/components/extensive/Modal/ModalRoot.tsx index 433aded4e..05587e40c 100644 --- a/src/components/extensive/Modal/ModalRoot.tsx +++ b/src/components/extensive/Modal/ModalRoot.tsx @@ -36,7 +36,9 @@ const ModalRoot = () => { leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > - + {modal.content} diff --git a/src/components/extensive/Modal/ModalShareImpactStory.tsx b/src/components/extensive/Modal/ModalShareImpactStory.tsx new file mode 100644 index 000000000..22686b8c6 --- /dev/null +++ b/src/components/extensive/Modal/ModalShareImpactStory.tsx @@ -0,0 +1,90 @@ +import { useT } from "@transifex/react"; +import { FC, useCallback } from "react"; +import { twMerge as tw } from "tailwind-merge"; + +import Button from "@/components/elements/Button/Button"; +import Text from "@/components/elements/Text/Text"; + +import Icon, { IconNames } from "../Icon/Icon"; +import IconSocialImpactStory from "../Icon/IconSocialImpactStory"; +import { ModalBase, ModalProps } from "./Modal"; + +export interface ModalShareImpactStoryProps extends ModalProps { + onClose: () => void; + onCopySuccess: () => void; + shareUrl: string; +} + +const ModalShareImpactStory: FC = ({ + onClose, + onCopySuccess, + shareUrl, + className, + ...rest +}) => { + const t = useT(); + + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(shareUrl); + onCopySuccess(); + onClose(); + } catch (err) { + console.error("Failed to copy:", err); + } + }, [shareUrl, onCopySuccess, onClose]); + + const encodedUrl = encodeURIComponent(shareUrl); + + return ( + + + + + + + + + {t("Share this project")} + + + {t("Invite your team to review collaborate on this project.")} + + + + + {t("Share on Social media")} + + + + + + + + + + + {t("Share on Social media")} + + + + {shareUrl} + + + {t("Copy")} + + + + + + + ); +}; + +export default ModalShareImpactStory; diff --git a/src/components/extensive/Modal/ModalStory.tsx b/src/components/extensive/Modal/ModalStory.tsx new file mode 100644 index 000000000..07981214d --- /dev/null +++ b/src/components/extensive/Modal/ModalStory.tsx @@ -0,0 +1,97 @@ +import { useMediaQuery } from "@mui/material"; +import { When } from "react-if"; +import { twMerge as tw } from "tailwind-merge"; + +import Text from "@/components/elements/Text/Text"; +import { useModalContext } from "@/context/modal.provider"; +import SectionShare from "@/pages/dashboard/impact-story/components/SectionShare"; + +import Icon, { IconNames } from "../Icon/Icon"; +import { ModalBase, ModalProps } from "./Modal"; +import { ModalId } from "./ModalConst"; + +export interface ImpactStoryData { + uuid: string; + title: string; + date: string; + content: string; + category: string[]; + thumbnail?: any; + organization?: { + name?: string; + country?: string; + }; + status: string; +} + +export interface ModalStoryProps extends ModalProps { + data: ImpactStoryData; + preview?: boolean; +} + +const ModalStory = ({ className, preview, data, ...rest }: ModalStoryProps) => { + const { closeModal } = useModalContext(); + const handleClose = () => { + closeModal(ModalId.MODAL_STORY); + }; + const isMobile = useMediaQuery("(max-width: 1200px)"); + + return ( + + + + + + + + + + {data?.title} + + + {`Date Added: `} + {new Date(data?.date).toLocaleDateString("en-GB", { + day: "numeric", + month: "long", + year: "numeric", + timeZone: "UTC" + })} + + + {data?.content} + + + + + + + + + + + + + Impact Story Preview + + + + ); +}; + +export default ModalStory; diff --git a/src/components/extensive/Modal/ModalsBases.tsx b/src/components/extensive/Modal/ModalsBases.tsx index e27d4effd..4f8b603d8 100644 --- a/src/components/extensive/Modal/ModalsBases.tsx +++ b/src/components/extensive/Modal/ModalsBases.tsx @@ -13,7 +13,7 @@ const createModalComponent = ); const commonBaseClasses = - "m-auto flex max-h-full flex-col items-center justify-start overflow-y-auto rounded-lg border-2 border-neutral-100 bg-white"; + "m-auto flex max-h-full flex-col items-center justify-start overflow-y-auto rounded-lg border-2 border-neutral-100 bg-white mobile:h-[calc(100%-60px)] mobile:w-full mobile:rounded-none mobile:mt-[60px]"; export const ExpandModalBase = createModalComponent(commonBaseClasses + " h-[90vh] w-[95vw]"); diff --git a/src/components/extensive/Modal/__snapshots__/FormModal.stories.storyshot b/src/components/extensive/Modal/__snapshots__/FormModal.stories.storyshot index 8bf54f6bc..d57bdafe4 100644 --- a/src/components/extensive/Modal/__snapshots__/FormModal.stories.storyshot +++ b/src/components/extensive/Modal/__snapshots__/FormModal.stories.storyshot @@ -5,7 +5,7 @@ exports[`Storyshots Components/Extensive/Modal/FormModal Default 1`] = ` className="flex items-center justify-center bg-primary-400 p-8" > + + diff --git a/src/components/extensive/PageElements/Card/PageCard.tsx b/src/components/extensive/PageElements/Card/PageCard.tsx index 2c0612a15..74bf7a7fa 100644 --- a/src/components/extensive/PageElements/Card/PageCard.tsx +++ b/src/components/extensive/PageElements/Card/PageCard.tsx @@ -33,6 +33,7 @@ export interface PageCardProps iconClassName?: string; widthTooltip?: string; isUserAllowed?: boolean; + collapseChildren?: boolean; } const PageCard = ({ @@ -51,10 +52,12 @@ const PageCard = ({ tooltip, widthTooltip, isUserAllowed = true, + collapseChildren = false, ...props }: PageCardProps) => { const [collapseSubtile, setCollapseSubtile] = useState(true); const [subtitleText, setSubtitleText] = useState(subtitle); + const [openCollapseChildren, setOpenCollapseChildren] = useState(true); const t = useT(); const [, { user }] = useMyUser(); @@ -97,34 +100,62 @@ const PageCard = ({ {headerChildren} - - - - - - {subtitleText} - - maxLength}> - setCollapseSubtile(!collapseSubtile)} - > - - {collapseSubtile ? t("...See More") : t("See Less")} + + setOpenCollapseChildren(!openCollapseChildren)}> + - - - {isEmpty && !!emptyStateProps ? : children} - - + + + + + {subtitleText} + + maxLength}> + setCollapseSubtile(!collapseSubtile)} + > + + {collapseSubtile ? t("...See More") : t("See Less")} + + + + + + + + {isEmpty && !!emptyStateProps ? : children} + + + ); diff --git a/src/components/extensive/PageElements/Card/__snapshots__/PageCard.stories.storyshot b/src/components/extensive/PageElements/Card/__snapshots__/PageCard.stories.storyshot index 323672232..d90bc5ca7 100644 --- a/src/components/extensive/PageElements/Card/__snapshots__/PageCard.stories.storyshot +++ b/src/components/extensive/PageElements/Card/__snapshots__/PageCard.stories.storyshot @@ -18,14 +18,18 @@ exports[`Storyshots Components/Extensive/Page/Card Default 1`] = ` - - Card subtitle - + + Card subtitle + + diff --git a/src/components/extensive/PageElements/Column/__snapshots__/PageColumn.stories.storyshot b/src/components/extensive/PageElements/Column/__snapshots__/PageColumn.stories.storyshot index 88c1df76d..fa3352e8e 100644 --- a/src/components/extensive/PageElements/Column/__snapshots__/PageColumn.stories.storyshot +++ b/src/components/extensive/PageElements/Column/__snapshots__/PageColumn.stories.storyshot @@ -17,6 +17,9 @@ exports[`Storyshots Components/Extensive/Page/Column Default 1`] = ` card title + + + `; diff --git a/src/components/extensive/PageElements/Footer/PageFooter.tsx b/src/components/extensive/PageElements/Footer/PageFooter.tsx index d31e4db5b..f3c350642 100644 --- a/src/components/extensive/PageElements/Footer/PageFooter.tsx +++ b/src/components/extensive/PageElements/Footer/PageFooter.tsx @@ -3,10 +3,11 @@ import Icon, { IconNames } from "../../Icon/Icon"; const PageFooter = () => ( - - - + + + + © TerraMatch 2024 diff --git a/src/components/extensive/PageElements/Row/PageRow.tsx b/src/components/extensive/PageElements/Row/PageRow.tsx index e76e02942..382a8c46f 100644 --- a/src/components/extensive/PageElements/Row/PageRow.tsx +++ b/src/components/extensive/PageElements/Row/PageRow.tsx @@ -8,7 +8,13 @@ export interface PageRowProps PropsWithChildren {} const PageRow = ({ children, className, ...props }: PageRowProps) => ( - + {children} ); diff --git a/src/components/extensive/PageElements/Row/__snapshots__/PageRow.stories.storyshot b/src/components/extensive/PageElements/Row/__snapshots__/PageRow.stories.storyshot index 7feab45e2..70a4399af 100644 --- a/src/components/extensive/PageElements/Row/__snapshots__/PageRow.stories.storyshot +++ b/src/components/extensive/PageElements/Row/__snapshots__/PageRow.stories.storyshot @@ -2,7 +2,7 @@ exports[`Storyshots Components/Extensive/Page/Row Default 1`] = ` + + diff --git a/src/components/extensive/Pagination/PageSelector.tsx b/src/components/extensive/Pagination/PageSelector.tsx index 8abe98bc3..093567be7 100644 --- a/src/components/extensive/Pagination/PageSelector.tsx +++ b/src/components/extensive/Pagination/PageSelector.tsx @@ -48,17 +48,21 @@ function PageSelector({ className={classNames(className, "flex items-center justify-center gap-5", variant?.contentPageSelector)} > - - - {t("Page")} - - - {currentPage} - - - {t("of")} {getPageCount()} - - + + {t("Page")} + + + {currentPage} + + + {t("of")} {getPageCount()} + previousPage()} disabled={!getCanPreviousPage()} - className={variant?.iconContentPagination} + className={classNames(variant?.iconContentPagination, "mobile:order-1")} /> {getPaginationItems(currentPage, getPageCount()).map(pageNumber => { return ( @@ -95,7 +99,7 @@ function PageSelector({ }} onClick={() => nextPage()} disabled={!getCanNextPage()} - className={variant?.iconContentPagination} + className={classNames(variant?.iconContentPagination, "mobile:order-5")} /> ); diff --git a/src/components/extensive/Pagination/PaginationVariant.ts b/src/components/extensive/Pagination/PaginationVariant.ts index 095e63b24..8f5798352 100644 --- a/src/components/extensive/Pagination/PaginationVariant.ts +++ b/src/components/extensive/Pagination/PaginationVariant.ts @@ -31,7 +31,7 @@ export const VARIANT_PAGINATION_TEXT_16: VariantPagination = { export const VARIANT_PAGINATION_DASHBOARD: VariantPagination = { VariantPageText: "text-12", VariantPrePageText: "text-12-bold", - label: "order-1", + label: "order-1 mobile:hidden", value: "!w-14 !h-8 lg:!h-9 lg:!w-[68px] order-2 border border-grey-350 bg-white rounded-lg shadow-none", labelText: "Rows per page:", iconContent: "bg-white", @@ -43,7 +43,8 @@ export const VARIANT_PAGINATION_DASHBOARD: VariantPagination = { "bg-white !w-8 !h-8 lg:!h-9 lg:!w-9 rounded-lg border border-grey-350 flex items-center justify-center", contentPageSelector: "!gap-2 items-center", textNumberNoSelected: "!font-bold", - textNumberSelected: "!font-normal" + textNumberSelected: "!font-normal", + containerClassName: "mobile:justify-center" }; export const VARIANT_PAGINATION_MONITORED: VariantPagination = { diff --git a/src/components/extensive/Pagination/PerPageSelector.tsx b/src/components/extensive/Pagination/PerPageSelector.tsx index fbf75a3f2..04f1a26f7 100644 --- a/src/components/extensive/Pagination/PerPageSelector.tsx +++ b/src/components/extensive/Pagination/PerPageSelector.tsx @@ -30,7 +30,12 @@ const PerPageSelector = (props: PropsWithChildren) => { }; return ( - + {({ open, value }) => ( <> diff --git a/src/components/extensive/Pagination/__snapshots__/PerPageSelector.stories.storyshot b/src/components/extensive/Pagination/__snapshots__/PerPageSelector.stories.storyshot index 04ec672b3..d590f85e3 100644 --- a/src/components/extensive/Pagination/__snapshots__/PerPageSelector.stories.storyshot +++ b/src/components/extensive/Pagination/__snapshots__/PerPageSelector.stories.storyshot @@ -5,7 +5,7 @@ exports[`Storyshots Components/Elements/Table/PerPageSelector Default 1`] = ` className="w-fit" > { const mapFunctions = useMap(); return useMemo( () => getFormEntries(props, t, entityPolygonData, bbox, mapFunctions), + // eslint-disable-next-line react-hooks/exhaustive-deps [props, t, entityPolygonData, bbox] ); }; diff --git a/src/components/generic/Layout/DashboardLayout.tsx b/src/components/generic/Layout/DashboardLayout.tsx index 0445c3297..4a6d0bfb4 100644 --- a/src/components/generic/Layout/DashboardLayout.tsx +++ b/src/components/generic/Layout/DashboardLayout.tsx @@ -1,3 +1,4 @@ +import { useMediaQuery } from "@mui/material"; import { useRouter } from "next/router"; import { cloneElement, @@ -8,6 +9,7 @@ import { useEffect, useState } from "react"; +import { When } from "react-if"; import { DashboardProvider } from "@/context/dashboard.provider"; import { useLoading } from "@/context/loaderAdmin.provider"; @@ -51,12 +53,13 @@ const DashboardLayout = (props: PropsWithChildren) => { } }, [dashboardCountries, router.asPath]); + const isImpactStoryPage = router.pathname.includes("dashboard/impact-story"); const isProjectInsightsPage = router.pathname.includes("dashboard/project-insights"); const isProjectListPage = router.pathname === "/dashboard/project-list"; const isProjectPage = router.pathname === "dashboard/project"; const isHomepage = router.pathname === "/dashboard/learn-more"; const childrenWithProps = props.children ? cloneElement(props.children as ReactElement, { selectedCountry }) : null; - + const isMobile = useMediaQuery("(max-width: 1200px)"); return ( {loading && ( @@ -64,20 +67,23 @@ const DashboardLayout = (props: PropsWithChildren) => { )} - + - + {dashboardCountries && ( <> - + + + {childrenWithProps} > )} diff --git a/src/components/generic/Layout/__snapshots__/MainLayout.stories.storyshot b/src/components/generic/Layout/__snapshots__/MainLayout.stories.storyshot index aa79b10e2..8562b2b8c 100644 --- a/src/components/generic/Layout/__snapshots__/MainLayout.stories.storyshot +++ b/src/components/generic/Layout/__snapshots__/MainLayout.stories.storyshot @@ -34,42 +34,65 @@ exports[`Storyshots Components/Generic/Layouts/MainLayout Default 1`] = ` className="absolute top-4 left-[50%] translate-x-[-50%]" > - - - - English - - + + + + En + + + + + + English + + - + /> + + @@ -174,42 +197,65 @@ exports[`Storyshots Components/Generic/Layouts/MainLayout Default 1`] = ` - - - - English - - + + + + En + + + + + + English + + - + /> + + { 0}> - + - - {t("Logout")} - - - {t("Sign in")} - + + + + diff --git a/src/components/generic/Navbar/__snapshots__/Navbar.stories.storyshot b/src/components/generic/Navbar/__snapshots__/Navbar.stories.storyshot index c2de5734a..fbf7816d7 100644 --- a/src/components/generic/Navbar/__snapshots__/Navbar.stories.storyshot +++ b/src/components/generic/Navbar/__snapshots__/Navbar.stories.storyshot @@ -28,42 +28,65 @@ exports[`Storyshots Components/Generic/Navbar Logged In 1`] = ` className="absolute top-4 left-[50%] translate-x-[-50%]" > - - - - English - - + + + + En + + + + + + English + + - + /> + + @@ -93,71 +116,119 @@ exports[`Storyshots Components/Generic/Navbar Logged In 1`] = ` className="hidden h-4 w-[1px] bg-neutral-500 sm:mx-2 sm:block" /> - - + + + + + + MY ACCOUNT + - - Logout - - - + + - - - - English - - + + + + En + + + + + + English + + - + /> + + - - - - English - - + + + + En + + + + + + English + + - + /> + + @@ -346,42 +440,65 @@ exports[`Storyshots Components/Generic/Navbar Logged Out 1`] = ` - - - - English - - + + + + En + + + + + + English + + - + /> + + { const router = useRouter(); const [, { isLoggedIn }] = useLogin(); - + const [, { setLocale }] = useMyUser(); const t = useT(); - return ( - - - - - - - - - {t("DASHBOARDS")} - - - - - - - {t("PROJECT")} {t("LIST")} - - - + const isMobile = useMediaQuery("(max-width: 1200px)"); + const [isOpen, setIsOpen] = useState(!isMobile); - - - - - {t("PROJECT")} - {t("INSIGHTS")} - - - + const NAV_ITEMS: NavItem[] = [ + { + path: "/dashboard", + icon: IconNames.DASHBOARDS, + label: "Dashboards" + }, + { + path: "/dashboard/project-list", + icon: IconNames.PROJECT_PROFILE, + label: "ProjectList" + }, + ...(isMobile + ? [] + : [ + { + path: "/dashboard/project-insights", + icon: IconNames.DASHBOARD_AIRTABLE, + label: "ProjectInsights", + disabled: true + } + ]), + { + path: "/dashboard/impact-story", + icon: IconNames.DASHBOARD_IMPACT_STORY, + label: "ImpactStory" + }, + { + path: "/dashboard/learn-more", + icon: IconNames.ABOUT_US, + label: "Learn More" + } + ]; + const changeLanguageHandler = (lang: string) => { + if (setLocale) { + setLocale(lang as ValidLocale); + window.location.reload(); + } else { + router.push({ pathname: router.pathname, query: router.query }, router.asPath, { locale: lang }); + setTimeout(() => window.location.reload(), 1000); + } + }; + + useEffect(() => { + if (!isMobile) setIsOpen(true); + }, [isMobile]); - - { + if (!isMobile) { + return ( + + ", " "))} + placement="right" + className="uppercase" > - - {t("LEARN MORE")} - - - - ( - - {isLoggedIn ? t("Sign out") : t("Sign in")} + + + + {t(label)} - ) + + + + ); + } + return ( + + + + + {t(label.replace("", " "))} + + + + + ); + }; + + return ( + + + + + + + + {NAV_ITEMS.map(item => renderNavItem(item))} + + { + removeAccessToken(); + router.push("/auth/login"); + }} + > + + + + {t("Logout")} + + + + + + + + + - - + + {!isMobile && } + {isMobile && ( + setIsOpen(!isOpen)}> + + + )} + ); }; diff --git a/src/connections/DelayedJob.ts b/src/connections/DelayedJob.ts index 1890e9ee4..1ff178efc 100644 --- a/src/connections/DelayedJob.ts +++ b/src/connections/DelayedJob.ts @@ -1,11 +1,16 @@ -import { useEffect } from "react"; +import { useCallback, useEffect, useRef } from "react"; +import { useSelector } from "react-redux"; import { createSelector } from "reselect"; +import { useLogin } from "@/connections/Login"; import { bulkUpdateJobs, listDelayedJobs } from "@/generated/v3/jobService/jobServiceComponents"; import { bulkUpdateJobsFetchFailed, bulkUpdateJobsIsFetching } from "@/generated/v3/jobService/jobServicePredicates"; import { DelayedJobData, DelayedJobDto } from "@/generated/v3/jobService/jobServiceSchemas"; import { useConnection } from "@/hooks/useConnection"; +import { useValueChanged } from "@/hooks/useValueChanged"; import { ApiDataStore } from "@/store/apiSlice"; +import { JobsDataStore } from "@/store/jobsSlice"; +import { AppStore } from "@/store/store"; import { Connection } from "@/types/connection"; type DelayedJobCombinedConnection = { @@ -14,56 +19,76 @@ type DelayedJobCombinedConnection = { delayedJobsHasFailed: boolean; bulkUpdateJobsIsLoading: boolean; bulkUpdateJobsHasFailed: boolean; - updatedJobsResponse?: DelayedJobDto[]; }; -const delayedJobsSelector = (store: ApiDataStore) => store.delayedJobs; +const delayedJobsSelector = (store: ApiDataStore) => + Object.values(store.delayedJobs ?? {}) + .map(resource => resource.attributes) + .filter(({ isAcknowledged }) => !isAcknowledged); const combinedSelector = createSelector( [delayedJobsSelector, bulkUpdateJobsIsFetching, bulkUpdateJobsFetchFailed], (delayedJobs, bulkUpdateJobsIsLoading, bulkUpdateJobsFailure) => ({ - delayedJobs: Object.values(delayedJobs ?? {}).map(resource => resource.attributes), + delayedJobs, delayedJobsIsLoading: delayedJobs == null && !bulkUpdateJobsFailure, delayedJobsHasFailed: Boolean(bulkUpdateJobsFailure), bulkUpdateJobsIsLoading, - bulkUpdateJobsHasFailed: bulkUpdateJobsFailure != null, - updatedJobsResponse: Object.values(delayedJobs ?? {}).map(resource => resource.attributes as DelayedJobDto) + bulkUpdateJobsHasFailed: bulkUpdateJobsFailure != null }) ); -const combinedLoad = (connection: DelayedJobCombinedConnection) => { - if (!combinedIsLoaded(connection)) { - listDelayedJobs(); - } -}; - -const combinedIsLoaded = ({ - delayedJobs, - delayedJobsHasFailed, - bulkUpdateJobsIsLoading, - bulkUpdateJobsHasFailed -}: DelayedJobCombinedConnection) => - (delayedJobs != null || delayedJobsHasFailed) && !bulkUpdateJobsIsLoading && !bulkUpdateJobsHasFailed; - const delayedJobsCombinedConnection: Connection = { - load: combinedLoad, - isLoaded: combinedIsLoaded, selector: combinedSelector }; +export const useJobProgress = () => useSelector(({ jobs }) => jobs); + export const useDelayedJobs = () => { const connection = useConnection(delayedJobsCombinedConnection); + const { totalContent } = useJobProgress(); + const intervalRef = useRef(); + const [, { isLoggedIn }] = useLogin(); + const stopPolling = useCallback(() => { + if (intervalRef.current != null) { + clearInterval(intervalRef.current); + intervalRef.current = undefined; + } + }, []); + const startPolling = useCallback(() => { + if (intervalRef.current == null) { + intervalRef.current = setInterval(() => { + listDelayedJobs(); + }, 1500); + } + }, []); + + // Make sure to stop polling when we unmount. + useEffect(() => stopPolling, [stopPolling]); + + const hasJobs = (connection[1].delayedJobs ?? []).length > 0; useEffect(() => { - const intervalId = setInterval(() => { - listDelayedJobs(); - }, 1500); + if (totalContent > 0) { + startPolling(); + // Don't process the connection content because we need it to poll once before giving up; the + // currently cached poll result is going to claim there are no jobs. + // Note: this is a little fragile because it depends on some code somewhere to call + // JobsSlice.reset() when it's done watching the job, but that's better than accidentally not + // polling when we're supposed to. + return; + } - return () => { - clearInterval(intervalId); - }; - }, []); + if (hasJobs) startPolling(); + else stopPolling(); + }, [hasJobs, startPolling, stopPolling, totalContent]); + + useValueChanged(isLoggedIn, () => { + // make sure we call the listDelayedJobs request at least once when we first mount if we're + // logged in, or when we log in with a fresh user. + if (isLoggedIn) listDelayedJobs(); + }); return connection; }; + export const triggerBulkUpdate = (jobs: DelayedJobData[]) => bulkUpdateJobs({ body: { data: jobs } }); diff --git a/src/connections/Login.ts b/src/connections/Login.ts index 15e4578dd..1e1db1684 100644 --- a/src/connections/Login.ts +++ b/src/connections/Login.ts @@ -20,8 +20,6 @@ export const logout = () => { // When we log out, remove all cached API resources so that when we log in again, these resources // are freshly fetched from the BE. ApiSlice.clearApiCache(); - ApiSlice.queryClient?.getQueryCache()?.clear(); - ApiSlice.queryClient?.clear(); }; export const selectFirstLogin = (store: ApiDataStore) => Object.values(store.logins)?.[0]?.attributes; diff --git a/src/connections/ResetPassword.ts b/src/connections/ResetPassword.ts new file mode 100644 index 000000000..e32e12261 --- /dev/null +++ b/src/connections/ResetPassword.ts @@ -0,0 +1,82 @@ +import { createSelector } from "reselect"; + +import { requestPasswordReset, resetPassword } from "@/generated/v3/userService/userServiceComponents"; +import { + requestPasswordResetFetchFailed, + requestPasswordResetIsFetching, + resetPasswordFetchFailed, + resetPasswordIsFetching +} from "@/generated/v3/userService/userServicePredicates"; +import { ApiDataStore, PendingErrorState } from "@/store/apiSlice"; +import { Connection } from "@/types/connection"; +import { connectionHook } from "@/utils/connectionShortcuts"; +import { selectorCache } from "@/utils/selectorCache"; + +export const sendRequestPasswordReset = (emailAddress: string, callbackUrl: string) => + requestPasswordReset({ body: { emailAddress, callbackUrl } }); + +export const selectResetPassword = (store: ApiDataStore) => Object.values(store.passwordResets)?.[0]?.attributes; + +type RequestResetPasswordConnection = { + isLoading: boolean; + requestFailed: PendingErrorState | null; + isSuccess: boolean; + requestEmail: string; +}; + +const requestPasswordConnection: Connection = { + selector: createSelector( + [requestPasswordResetIsFetching, requestPasswordResetFetchFailed, selectResetPassword], + (isLoading, requestFailed, selector) => { + return { + isLoading: isLoading, + requestFailed: requestFailed, + isSuccess: selector?.emailAddress != null, + requestEmail: selector?.emailAddress + }; + } + ) +}; + +type ResetPasswordConnection = { + isLoading: boolean; + requestFailed: PendingErrorState | null; + isSuccess: boolean; + resetPassword: (password: string) => void; +}; + +type ResetPasswordProps = { + token: string; +}; + +const resetPasswordConnection: Connection = { + selector: selectorCache( + ({ token }) => token, + ({ token }) => + createSelector( + [ + resetPasswordIsFetching({ pathParams: { token } }), + resetPasswordFetchFailed({ pathParams: { token } }), + selectResetPassword + ], + (isLoading, requestFailed, selector) => ({ + isLoading, + requestFailed, + isSuccess: selector?.emailAddress != null, + resetPassword: (password: string) => + resetPassword({ + body: { + newPassword: password + }, + pathParams: { + token: token + } + }) + }) + ) + ) +}; + +export const useRequestPassword = connectionHook(requestPasswordConnection); + +export const useResetPassword = connectionHook(resetPasswordConnection); diff --git a/src/context/mapArea.provider.tsx b/src/context/mapArea.provider.tsx index a9d14b38f..f9b55cc0d 100644 --- a/src/context/mapArea.provider.tsx +++ b/src/context/mapArea.provider.tsx @@ -1,4 +1,4 @@ -import React, { createContext, ReactNode, useContext, useState } from "react"; +import React, { createContext, ReactNode, useCallback, useContext, useState } from "react"; import { fetchGetV2DashboardViewProjectUuid } from "@/generated/apiComponents"; import { SitePolygon } from "@/generated/apiSchemas"; @@ -138,7 +138,7 @@ export const MapAreaProvider: React.FC<{ children: ReactNode }> = ({ children }) setOpenEditNewPolygon(isOpen); }; - const checkIsMonitoringPartner = async (projectUuid: string) => { + const checkIsMonitoringPartner = useCallback(async (projectUuid: string) => { try { const isMonitoringPartner: any = await fetchGetV2DashboardViewProjectUuid({ pathParams: { uuid: projectUuid } @@ -148,7 +148,7 @@ export const MapAreaProvider: React.FC<{ children: ReactNode }> = ({ children }) Log.error("Failed to check if monitoring partner:", error); setIsMonitoring(false); } - }; + }, []); const contextValue: MapAreaType = { isMonitoring, diff --git a/src/context/modal.provider.tsx b/src/context/modal.provider.tsx index 31a01301e..5f6f5f9a9 100644 --- a/src/context/modal.provider.tsx +++ b/src/context/modal.provider.tsx @@ -62,6 +62,9 @@ const ModalProvider = ({ children }: ModalProviderProps) => { setModalLoading, modalOpened }), + // Only regenerate the context value if the set of current modals changes. This is more + // efficient than wrapping every callback above in useCallback() + // eslint-disable-next-line react-hooks/exhaustive-deps [modals] ); diff --git a/src/context/notification.provider.tsx b/src/context/notification.provider.tsx index f5f9df02f..cebec6504 100644 --- a/src/context/notification.provider.tsx +++ b/src/context/notification.provider.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useMemo, useState } from "react"; +import React, { useCallback, useContext, useMemo, useState } from "react"; import Notification from "@/components/elements/Notification/Notification"; @@ -38,13 +38,16 @@ const NotificationProvider = ({ children }: NotificationProviderProps) => { open: false }); - const openNotification = (type: Exclude, title: string, message?: string | any) => { - setNotificationProps({ type, title, message: message ?? undefined, open: true }); - }; + const openNotification = useCallback( + (type: Exclude, title: string, message?: string | any) => { + setNotificationProps({ type, title, message: message ?? undefined, open: true }); + }, + [] + ); - const closeNotification = () => { + const closeNotification = useCallback(() => { setNotificationProps(prev => ({ ...prev, open: false })); - }; + }, []); const value = useMemo( () => ({ @@ -52,7 +55,7 @@ const NotificationProvider = ({ children }: NotificationProviderProps) => { closeNotification, notificationProps }), - [notificationProps] + [closeNotification, notificationProps, openNotification] ); return ( diff --git a/src/generated/apiComponents.ts b/src/generated/apiComponents.ts index c60812f42..8db47e4f9 100644 --- a/src/generated/apiComponents.ts +++ b/src/generated/apiComponents.ts @@ -38795,6 +38795,652 @@ export const useGetV2IndicatorsEntityUuidSlugExport = ( ); }; +export type GetV2AdminImpactStoriesQueryParams = { + /** + * Search term to use on the collection + */ + search?: string; + /** + * Multiple filters can be applied. Syntax: ?filter[status]=published + */ + filter?: string; + /** + * Sorting can be applied, default is ascending or use - for descending. Example: ?sort=-created_at + */ + sort?: string; + /** + * Number of results per page + */ + per_page?: number; + /** + * Page number for results + */ + page?: number; +}; + +export type GetV2AdminImpactStoriesError = Fetcher.ErrorWrapper; + +export type GetV2AdminImpactStoriesResponse = { + data?: { + /** + * @example 123e4567-e89b-12d3-a456-426614174000 + */ + uuid?: string; + /** + * @example Empowering Local Communities + */ + title?: string; + /** + * @example This is an inspiring story of impact... + */ + content?: string; + /** + * @example published + */ + status?: "draft" | "published" | "archived"; + /** + * @format date-time + * @example 2024-09-02T15:04:05Z + */ + created_at?: string; + /** + * @format date-time + * @example 2024-09-02T15:04:05Z + */ + updated_at?: string; + }[]; + links?: { + first?: string; + last?: string; + prev?: string; + next?: string; + }; + meta?: { + from?: number; + to?: number; + current_page?: number; + last_page?: number; + per_page?: number; + total?: number; + path?: string; + links?: { + url?: string; + label?: string; + active?: boolean; + }[]; + }; +}; + +export type GetV2AdminImpactStoriesVariables = { + queryParams?: GetV2AdminImpactStoriesQueryParams; +} & ApiContext["fetcherOptions"]; + +/** + * Fetches a list of impact stories with filtering and sorting options. + */ +export const fetchGetV2AdminImpactStories = (variables: GetV2AdminImpactStoriesVariables, signal?: AbortSignal) => + apiFetch< + GetV2AdminImpactStoriesResponse, + GetV2AdminImpactStoriesError, + undefined, + {}, + GetV2AdminImpactStoriesQueryParams, + {} + >({ url: "/v2/admin/impact-stories", method: "get", ...variables, signal }); + +/** + * Fetches a list of impact stories with filtering and sorting options. + */ +export const useGetV2AdminImpactStories = ( + variables: GetV2AdminImpactStoriesVariables, + options?: Omit< + reactQuery.UseQueryOptions, + "queryKey" | "queryFn" + > +) => { + const { fetcherOptions, queryOptions, queryKeyFn } = useApiContext(options); + return reactQuery.useQuery( + queryKeyFn({ path: "/v2/admin/impact-stories", operationId: "getV2AdminImpactStories", variables }), + ({ signal }) => fetchGetV2AdminImpactStories({ ...fetcherOptions, ...variables }, signal), + { + ...options, + ...queryOptions + } + ); +}; + +export type PostV2AdminImpactStoriesError = Fetcher.ErrorWrapper; + +export type PostV2AdminImpactStoriesResponse = { + /** + * @example 123e4567-e89b-12d3-a456-426614174000 + */ + uuid?: string; + /** + * @example Empowering Local Communities + */ + title?: string; + /** + * @example This is an inspiring story of impact... + */ + content?: string; + /** + * @example published + */ + status?: "draft" | "published" | "archived"; + /** + * @format date-time + * @example 2024-09-02T15:04:05Z + */ + created_at?: string; + /** + * @format date-time + * @example 2024-09-02T15:04:05Z + */ + updated_at?: string; +}; + +export type PostV2AdminImpactStoriesRequestBody = { + /** + * @example Empowering Local Communities + */ + title: string; + /** + * @example This is an inspiring story of impact... + */ + content: string; + /** + * @example draft + */ + status?: "draft" | "published" | "archived"; +}; + +export type PostV2AdminImpactStoriesVariables = { + body: PostV2AdminImpactStoriesRequestBody; +} & ApiContext["fetcherOptions"]; + +/** + * Creates a new impact story. + */ +export const fetchPostV2AdminImpactStories = (variables: PostV2AdminImpactStoriesVariables, signal?: AbortSignal) => + apiFetch< + PostV2AdminImpactStoriesResponse, + PostV2AdminImpactStoriesError, + PostV2AdminImpactStoriesRequestBody, + {}, + {}, + {} + >({ url: "/v2/admin/impact-stories", method: "post", ...variables, signal }); + +/** + * Creates a new impact story. + */ +export const usePostV2AdminImpactStories = ( + options?: Omit< + reactQuery.UseMutationOptions< + PostV2AdminImpactStoriesResponse, + PostV2AdminImpactStoriesError, + PostV2AdminImpactStoriesVariables + >, + "mutationFn" + > +) => { + const { fetcherOptions } = useApiContext(); + return reactQuery.useMutation< + PostV2AdminImpactStoriesResponse, + PostV2AdminImpactStoriesError, + PostV2AdminImpactStoriesVariables + >( + (variables: PostV2AdminImpactStoriesVariables) => + fetchPostV2AdminImpactStories({ ...fetcherOptions, ...variables }), + options + ); +}; + +export type GetV2AdminImpactStoriesIdPathParams = { + /** + * UUID of the impact story + */ + id: string; +}; + +export type GetV2AdminImpactStoriesIdError = Fetcher.ErrorWrapper; + +export type GetV2AdminImpactStoriesIdResponse = { + /** + * @example 123e4567-e89b-12d3-a456-426614174000 + */ + uuid?: string; + /** + * @example Empowering Local Communities + */ + title?: string; + /** + * @example This is an inspiring story of impact... + */ + content?: string; + /** + * @example published + */ + status?: "draft" | "published" | "archived"; + /** + * @format date-time + * @example 2024-09-02T15:04:05Z + */ + created_at?: string; + /** + * @format date-time + * @example 2024-09-02T15:04:05Z + */ + updated_at?: string; +}; + +export type GetV2AdminImpactStoriesIdVariables = { + pathParams: GetV2AdminImpactStoriesIdPathParams; +} & ApiContext["fetcherOptions"]; + +/** + * Retrieves details of a single impact story. + */ +export const fetchGetV2AdminImpactStoriesId = (variables: GetV2AdminImpactStoriesIdVariables, signal?: AbortSignal) => + apiFetch< + GetV2AdminImpactStoriesIdResponse, + GetV2AdminImpactStoriesIdError, + undefined, + {}, + {}, + GetV2AdminImpactStoriesIdPathParams + >({ url: "/v2/admin/impact-stories/{id}", method: "get", ...variables, signal }); + +/** + * Retrieves details of a single impact story. + */ +export const useGetV2AdminImpactStoriesId = ( + variables: GetV2AdminImpactStoriesIdVariables, + options?: Omit< + reactQuery.UseQueryOptions, + "queryKey" | "queryFn" + > +) => { + const { fetcherOptions, queryOptions, queryKeyFn } = useApiContext(options); + return reactQuery.useQuery( + queryKeyFn({ path: "/v2/admin/impact-stories/{id}", operationId: "getV2AdminImpactStoriesId", variables }), + ({ signal }) => fetchGetV2AdminImpactStoriesId({ ...fetcherOptions, ...variables }, signal), + { + ...options, + ...queryOptions + } + ); +}; + +export type PutV2AdminImpactStoriesIdPathParams = { + /** + * UUID of the impact story to update + */ + id: string; +}; + +export type PutV2AdminImpactStoriesIdError = Fetcher.ErrorWrapper; + +export type PutV2AdminImpactStoriesIdResponse = { + /** + * @example 123e4567-e89b-12d3-a456-426614174000 + */ + uuid?: string; + /** + * @example Empowering Local Communities + */ + title?: string; + /** + * @example This is an inspiring story of impact... + */ + content?: string; + /** + * @example published + */ + status?: "draft" | "published" | "archived"; + /** + * @format date-time + * @example 2024-09-02T15:04:05Z + */ + created_at?: string; + /** + * @format date-time + * @example 2024-09-02T15:04:05Z + */ + updated_at?: string; +}; + +export type PutV2AdminImpactStoriesIdRequestBody = { + /** + * @example Updated Title + */ + title?: string; + /** + * @example Updated content of the impact story. + */ + content?: string; + /** + * @example published + */ + status?: "draft" | "published" | "archived"; +}; + +export type PutV2AdminImpactStoriesIdVariables = { + body?: PutV2AdminImpactStoriesIdRequestBody; + pathParams: PutV2AdminImpactStoriesIdPathParams; +} & ApiContext["fetcherOptions"]; + +/** + * Updates the details of an existing impact story. + */ +export const fetchPutV2AdminImpactStoriesId = (variables: PutV2AdminImpactStoriesIdVariables, signal?: AbortSignal) => + apiFetch< + PutV2AdminImpactStoriesIdResponse, + PutV2AdminImpactStoriesIdError, + PutV2AdminImpactStoriesIdRequestBody, + {}, + {}, + PutV2AdminImpactStoriesIdPathParams + >({ url: "/v2/admin/impact-stories/{id}", method: "put", ...variables, signal }); + +/** + * Updates the details of an existing impact story. + */ +export const usePutV2AdminImpactStoriesId = ( + options?: Omit< + reactQuery.UseMutationOptions< + PutV2AdminImpactStoriesIdResponse, + PutV2AdminImpactStoriesIdError, + PutV2AdminImpactStoriesIdVariables + >, + "mutationFn" + > +) => { + const { fetcherOptions } = useApiContext(); + return reactQuery.useMutation< + PutV2AdminImpactStoriesIdResponse, + PutV2AdminImpactStoriesIdError, + PutV2AdminImpactStoriesIdVariables + >( + (variables: PutV2AdminImpactStoriesIdVariables) => + fetchPutV2AdminImpactStoriesId({ ...fetcherOptions, ...variables }), + options + ); +}; + +export type DeleteV2AdminImpactStoriesIdPathParams = { + id: string; +}; + +export type DeleteV2AdminImpactStoriesIdError = Fetcher.ErrorWrapper; + +export type DeleteV2AdminImpactStoriesIdVariables = { + pathParams: DeleteV2AdminImpactStoriesIdPathParams; +} & ApiContext["fetcherOptions"]; + +/** + * Deletes an existing impact story. + */ +export const fetchDeleteV2AdminImpactStoriesId = ( + variables: DeleteV2AdminImpactStoriesIdVariables, + signal?: AbortSignal +) => + apiFetch({ + url: "/v2/admin/impact-stories/{id}", + method: "delete", + ...variables, + signal + }); + +/** + * Deletes an existing impact story. + */ +export const useDeleteV2AdminImpactStoriesId = ( + options?: Omit< + reactQuery.UseMutationOptions, + "mutationFn" + > +) => { + const { fetcherOptions } = useApiContext(); + return reactQuery.useMutation( + (variables: DeleteV2AdminImpactStoriesIdVariables) => + fetchDeleteV2AdminImpactStoriesId({ ...fetcherOptions, ...variables }), + options + ); +}; + +export type PostV2AdminImpactStoriesBulkDeleteError = Fetcher.ErrorWrapper; + +export type PostV2AdminImpactStoriesBulkDeleteRequestBody = { + uuids: string[]; +}; + +export type PostV2AdminImpactStoriesBulkDeleteVariables = { + body: PostV2AdminImpactStoriesBulkDeleteRequestBody; +} & ApiContext["fetcherOptions"]; + +export const fetchPostV2AdminImpactStoriesBulkDelete = ( + variables: PostV2AdminImpactStoriesBulkDeleteVariables, + signal?: AbortSignal +) => + apiFetch< + undefined, + PostV2AdminImpactStoriesBulkDeleteError, + PostV2AdminImpactStoriesBulkDeleteRequestBody, + {}, + {}, + {} + >({ url: "/v2/admin/impact-stories/bulk-delete", method: "post", ...variables, signal }); + +export const usePostV2AdminImpactStoriesBulkDelete = ( + options?: Omit< + reactQuery.UseMutationOptions< + undefined, + PostV2AdminImpactStoriesBulkDeleteError, + PostV2AdminImpactStoriesBulkDeleteVariables + >, + "mutationFn" + > +) => { + const { fetcherOptions } = useApiContext(); + return reactQuery.useMutation< + undefined, + PostV2AdminImpactStoriesBulkDeleteError, + PostV2AdminImpactStoriesBulkDeleteVariables + >( + (variables: PostV2AdminImpactStoriesBulkDeleteVariables) => + fetchPostV2AdminImpactStoriesBulkDelete({ ...fetcherOptions, ...variables }), + options + ); +}; + +export type GetV2ImpactStoriesQueryParams = { + /** + * Search term to use on the collection + */ + search?: string; + /** + * Multiple filters can be applied. Syntax: ?filter[status]=published + */ + filter?: string; + /** + * Sorting can be applied, default is ascending or use - for descending. Example: ?sort=-created_at + */ + sort?: string; + /** + * Number of results per page + */ + per_page?: number; + /** + * Page number for results + */ + page?: number; +}; + +export type GetV2ImpactStoriesError = Fetcher.ErrorWrapper; + +export type GetV2ImpactStoriesResponse = { + data?: { + /** + * @example 123e4567-e89b-12d3-a456-426614174000 + */ + uuid?: string; + /** + * @example Empowering Local Communities + */ + title?: string; + /** + * @example This is an inspiring story of impact... + */ + content?: string; + /** + * @example published + */ + status?: "draft" | "published" | "archived"; + /** + * @format date-time + * @example 2024-09-02T15:04:05Z + */ + created_at?: string; + /** + * @format date-time + * @example 2024-09-02T15:04:05Z + */ + updated_at?: string; + }[]; + links?: { + first?: string; + last?: string; + prev?: string; + next?: string; + }; + meta?: { + from?: number; + to?: number; + current_page?: number; + last_page?: number; + per_page?: number; + total?: number; + path?: string; + links?: { + url?: string; + label?: string; + active?: boolean; + }[]; + }; +}; + +export type GetV2ImpactStoriesVariables = { + queryParams?: GetV2ImpactStoriesQueryParams; +} & ApiContext["fetcherOptions"]; + +/** + * Fetches a list of impact stories with filtering and sorting options. + */ +export const fetchGetV2ImpactStories = (variables: GetV2ImpactStoriesVariables, signal?: AbortSignal) => + apiFetch({ + url: "/v2/impact-stories", + method: "get", + ...variables, + signal + }); + +/** + * Fetches a list of impact stories with filtering and sorting options. + */ +export const useGetV2ImpactStories = ( + variables: GetV2ImpactStoriesVariables, + options?: Omit< + reactQuery.UseQueryOptions, + "queryKey" | "queryFn" + > +) => { + const { fetcherOptions, queryOptions, queryKeyFn } = useApiContext(options); + return reactQuery.useQuery( + queryKeyFn({ path: "/v2/impact-stories", operationId: "getV2ImpactStories", variables }), + ({ signal }) => fetchGetV2ImpactStories({ ...fetcherOptions, ...variables }, signal), + { + ...options, + ...queryOptions + } + ); +}; + +export type GetV2ImpactStoriesIdPathParams = { + /** + * UUID of the impact story + */ + id: string; +}; + +export type GetV2ImpactStoriesIdError = Fetcher.ErrorWrapper; + +export type GetV2ImpactStoriesIdResponse = { + /** + * @example 123e4567-e89b-12d3-a456-426614174000 + */ + uuid?: string; + /** + * @example Empowering Local Communities + */ + title?: string; + /** + * @example This is an inspiring story of impact... + */ + content?: string; + /** + * @example published + */ + status?: "draft" | "published" | "archived"; + /** + * @format date-time + * @example 2024-09-02T15:04:05Z + */ + created_at?: string; + /** + * @format date-time + * @example 2024-09-02T15:04:05Z + */ + updated_at?: string; +}; + +export type GetV2ImpactStoriesIdVariables = { + pathParams: GetV2ImpactStoriesIdPathParams; +} & ApiContext["fetcherOptions"]; + +/** + * Retrieves details of a single impact story. + */ +export const fetchGetV2ImpactStoriesId = (variables: GetV2ImpactStoriesIdVariables, signal?: AbortSignal) => + apiFetch({ + url: "/v2/impact-stories/{id}", + method: "get", + ...variables, + signal + }); + +/** + * Retrieves details of a single impact story. + */ +export const useGetV2ImpactStoriesId = ( + variables: GetV2ImpactStoriesIdVariables, + options?: Omit< + reactQuery.UseQueryOptions, + "queryKey" | "queryFn" + > +) => { + const { fetcherOptions, queryOptions, queryKeyFn } = useApiContext(options); + return reactQuery.useQuery( + queryKeyFn({ path: "/v2/impact-stories/{id}", operationId: "getV2ImpactStoriesId", variables }), + ({ signal }) => fetchGetV2ImpactStoriesId({ ...fetcherOptions, ...variables }, signal), + { + ...options, + ...queryOptions + } + ); +}; + export type QueryOperation = | { path: "/v2/tree-species/{entity}/{UUID}"; @@ -39540,4 +40186,24 @@ export type QueryOperation = path: "/v2/indicators/{entity}/{uuid}/{slug}/export"; operationId: "getV2IndicatorsEntityUuidSlugExport"; variables: GetV2IndicatorsEntityUuidSlugExportVariables; + } + | { + path: "/v2/admin/impact-stories"; + operationId: "getV2AdminImpactStories"; + variables: GetV2AdminImpactStoriesVariables; + } + | { + path: "/v2/admin/impact-stories/{id}"; + operationId: "getV2AdminImpactStoriesId"; + variables: GetV2AdminImpactStoriesIdVariables; + } + | { + path: "/v2/impact-stories"; + operationId: "getV2ImpactStories"; + variables: GetV2ImpactStoriesVariables; + } + | { + path: "/v2/impact-stories/{id}"; + operationId: "getV2ImpactStoriesId"; + variables: GetV2ImpactStoriesIdVariables; }; diff --git a/src/generated/apiFetcher.ts b/src/generated/apiFetcher.ts index 0b08d922b..2fcc320d6 100644 --- a/src/generated/apiFetcher.ts +++ b/src/generated/apiFetcher.ts @@ -4,7 +4,8 @@ import FormData from "form-data"; import Log from "@/utils/log"; import { resolveUrl as resolveV3Url } from "./v3/utils"; import { apiBaseUrl } from "@/constants/environment"; -import ApiSlice from "@/store/apiSlice"; +import JobsSlice from "@/store/jobsSlice"; +import { DelayedJobDto } from "./v3/jobService/jobServiceSchemas"; const baseUrl = `${apiBaseUrl}/api`; @@ -127,15 +128,7 @@ const resolveUrl = (url: string, queryParams: Record = {}, pathP const JOB_POLL_TIMEOUT = 500; // in ms -type JobResult = { - data: { - attributes: { - status: "pending" | "failed" | "succeeded"; - statusCode: number | null; - payload: object | null; - }; - }; -}; +type JobResult = { data: { attributes: DelayedJobDto } }; async function loadJob(signal: AbortSignal | undefined, delayedJobId: string, retries = 3): Promise { let response, error; @@ -192,15 +185,12 @@ async function processDelayedJob(signal: AbortSignal | undefined, delayed jobResult.data?.attributes?.status === "pending"; jobResult = await loadJob(signal, delayedJobId) ) { - //@ts-ignore const { totalContent, processedContent, progressMessage } = jobResult.data?.attributes; - if (totalContent != null) { - ApiSlice.addTotalContent(totalContent); - ApiSlice.addProgressContent(processedContent); - ApiSlice.addProgressMessage(progressMessage); + if (totalContent != null && processedContent != null) { + JobsSlice.setJobsProgress(totalContent, processedContent, progressMessage); } - if (signal?.aborted || ApiSlice.apiDataStore.abort_delayed_job) throw new Error("Aborted"); + if (signal?.aborted) throw new Error("Aborted"); await new Promise(resolve => setTimeout(resolve, JOB_POLL_TIMEOUT)); } diff --git a/src/generated/apiSchemas.ts b/src/generated/apiSchemas.ts index 01aef7556..1074b1bd2 100644 --- a/src/generated/apiSchemas.ts +++ b/src/generated/apiSchemas.ts @@ -23776,3 +23776,62 @@ export type IndicatorPolygonsStatus = { approved?: number; ["needs-more-information"]?: number; }; + +export type V2ImpactStoryRead = { + /** + * @example 123e4567-e89b-12d3-a456-426614174000 + */ + uuid?: string; + /** + * @example Empowering Local Communities + */ + title?: string; + /** + * @example This is an inspiring story of impact... + */ + content?: string; + /** + * @example published + */ + status?: "draft" | "published" | "archived"; + /** + * @format date-time + * @example 2024-09-02T15:04:05Z + */ + created_at?: string; + /** + * @format date-time + * @example 2024-09-02T15:04:05Z + */ + updated_at?: string; +}; + +export type V2ImpactStoryUpdate = { + /** + * @example Updated Title + */ + title?: string; + /** + * @example Updated content of the impact story. + */ + content?: string; + /** + * @example published + */ + status?: "draft" | "published" | "archived"; +}; + +export type V2ImpactStoryCreate = { + /** + * @example Empowering Local Communities + */ + title: string; + /** + * @example This is an inspiring story of impact... + */ + content: string; + /** + * @example draft + */ + status?: "draft" | "published" | "archived"; +}; diff --git a/src/generated/v3/entityService/entityServiceComponents.ts b/src/generated/v3/entityService/entityServiceComponents.ts index 964c60fcf..554d7e750 100644 --- a/src/generated/v3/entityService/entityServiceComponents.ts +++ b/src/generated/v3/entityService/entityServiceComponents.ts @@ -64,10 +64,6 @@ export type EstablishmentTreesFindError = Fetcher.ErrorWrapper< * @example Bad Request */ message: string; - /** - * @example Bad Request - */ - error?: string; }; } | { @@ -81,10 +77,6 @@ export type EstablishmentTreesFindError = Fetcher.ErrorWrapper< * @example Unauthorized */ message: string; - /** - * @example Unauthorized - */ - error?: string; }; } >; diff --git a/src/generated/v3/jobService/jobServiceComponents.ts b/src/generated/v3/jobService/jobServiceComponents.ts index bdece4f35..cff82641b 100644 --- a/src/generated/v3/jobService/jobServiceComponents.ts +++ b/src/generated/v3/jobService/jobServiceComponents.ts @@ -18,10 +18,6 @@ export type ListDelayedJobsError = Fetcher.ErrorWrapper<{ * @example Unauthorized */ message: string; - /** - * @example Unauthorized - */ - error?: string; }; }>; @@ -65,10 +61,6 @@ export type DelayedJobsFindError = Fetcher.ErrorWrapper< * @example Unauthorized */ message: string; - /** - * @example Unauthorized - */ - error?: string; }; } | { @@ -82,10 +74,6 @@ export type DelayedJobsFindError = Fetcher.ErrorWrapper< * @example Not Found */ message: string; - /** - * @example Not Found - */ - error?: string; }; } >; @@ -131,10 +119,6 @@ export type BulkUpdateJobsError = Fetcher.ErrorWrapper< * @example Bad Request */ message: string; - /** - * @example Bad Request - */ - error?: string; }; } | { @@ -148,10 +132,6 @@ export type BulkUpdateJobsError = Fetcher.ErrorWrapper< * @example Unauthorized */ message: string; - /** - * @example Unauthorized - */ - error?: string; }; } | { @@ -165,10 +145,6 @@ export type BulkUpdateJobsError = Fetcher.ErrorWrapper< * @example Not Found */ message: string; - /** - * @example Not Found - */ - error?: string; }; } >; diff --git a/src/generated/v3/userService/userServiceComponents.ts b/src/generated/v3/userService/userServiceComponents.ts index 7d9bf187e..038808932 100644 --- a/src/generated/v3/userService/userServiceComponents.ts +++ b/src/generated/v3/userService/userServiceComponents.ts @@ -18,10 +18,6 @@ export type AuthLoginError = Fetcher.ErrorWrapper<{ * @example Unauthorized */ message: string; - /** - * @example Unauthorized - */ - error?: string; }; }>; @@ -32,7 +28,7 @@ export type AuthLoginResponse = { */ type?: string; /** - * @pattern ^\d{5}$ + * @format uuid */ id?: string; attributes?: Schemas.LoginDto; @@ -56,7 +52,7 @@ export const authLogin = (variables: AuthLoginVariables, signal?: AbortSignal) = export type UsersFindPathParams = { /** - * A valid user uuid or "me" + * A valid user UUID or "me" * * @example me */ @@ -75,10 +71,6 @@ export type UsersFindError = Fetcher.ErrorWrapper< * @example Unauthorized */ message: string; - /** - * @example Unauthorized - */ - error?: string; }; } | { @@ -92,10 +84,6 @@ export type UsersFindError = Fetcher.ErrorWrapper< * @example Not Found */ message: string; - /** - * @example Not Found - */ - error?: string; }; } >; @@ -145,7 +133,7 @@ export type UsersFindVariables = { }; /** - * Fetch a user by ID, or with the 'me' identifier + * Fetch a user by UUID, or with the 'me' identifier */ export const usersFind = (variables: UsersFindVariables, signal?: AbortSignal) => userServiceFetch({ @@ -174,10 +162,6 @@ export type UserUpdateError = Fetcher.ErrorWrapper< * @example Bad Request */ message: string; - /** - * @example Bad Request - */ - error?: string; }; } | { @@ -191,10 +175,6 @@ export type UserUpdateError = Fetcher.ErrorWrapper< * @example Unauthorized */ message: string; - /** - * @example Unauthorized - */ - error?: string; }; } | { @@ -208,10 +188,6 @@ export type UserUpdateError = Fetcher.ErrorWrapper< * @example Not Found */ message: string; - /** - * @example Not Found - */ - error?: string; }; } >; @@ -262,7 +238,7 @@ export type UserUpdateVariables = { }; /** - * Update a user by ID + * Update a user by UUID */ export const userUpdate = (variables: UserUpdateVariables, signal?: AbortSignal) => userServiceFetch({ @@ -271,3 +247,96 @@ export const userUpdate = (variables: UserUpdateVariables, signal?: AbortSignal) ...variables, signal }); + +export type RequestPasswordResetError = Fetcher.ErrorWrapper<{ + status: 400; + payload: { + /** + * @example 400 + */ + statusCode: number; + /** + * @example Bad Request + */ + message: string; + }; +}>; + +export type RequestPasswordResetResponse = { + data?: { + /** + * @example passwordResets + */ + type?: string; + /** + * @format uuid + */ + id?: string; + attributes?: Schemas.ResetPasswordResponseDto; + }; +}; + +export type RequestPasswordResetVariables = { + body: Schemas.ResetPasswordRequest; +}; + +/** + * Send password reset email with a token + */ +export const requestPasswordReset = (variables: RequestPasswordResetVariables, signal?: AbortSignal) => + userServiceFetch({ + url: "/auth/v3/passwordResets", + method: "post", + ...variables, + signal + }); + +export type ResetPasswordPathParams = { + token: string; +}; + +export type ResetPasswordError = Fetcher.ErrorWrapper<{ + status: 400; + payload: { + /** + * @example 400 + */ + statusCode: number; + /** + * @example Bad Request + */ + message: string; + }; +}>; + +export type ResetPasswordResponse = { + data?: { + /** + * @example passwordResets + */ + type?: string; + /** + * @format uuid + */ + id?: string; + attributes?: Schemas.ResetPasswordResponseDto; + }; +}; + +export type ResetPasswordVariables = { + body?: Schemas.ResetPasswordDto; + pathParams: ResetPasswordPathParams; +}; + +/** + * Reset password using the provided token + */ +export const resetPassword = (variables: ResetPasswordVariables, signal?: AbortSignal) => + userServiceFetch< + ResetPasswordResponse, + ResetPasswordError, + Schemas.ResetPasswordDto, + {}, + {}, + ResetPasswordPathParams + >({ url: "/auth/v3/passwordResets/{token}", method: "put", ...variables, signal }); diff --git a/src/generated/v3/userService/userServicePredicates.ts b/src/generated/v3/userService/userServicePredicates.ts index 1994d7c0d..f80d76b8c 100644 --- a/src/generated/v3/userService/userServicePredicates.ts +++ b/src/generated/v3/userService/userServicePredicates.ts @@ -4,7 +4,9 @@ import { UsersFindPathParams, UsersFindVariables, UserUpdatePathParams, - UserUpdateVariables + UserUpdateVariables, + ResetPasswordPathParams, + ResetPasswordVariables } from "./userServiceComponents"; export const authLoginIsFetching = (store: ApiDataStore) => @@ -24,3 +26,25 @@ export const userUpdateIsFetching = (variables: Omit) => (store: ApiDataStore) => fetchFailed<{}, UserUpdatePathParams>({ store, url: "/users/v3/users/{uuid}", method: "patch", ...variables }); + +export const requestPasswordResetIsFetching = (store: ApiDataStore) => + isFetching<{}, {}>({ store, url: "/auth/v3/passwordResets", method: "post" }); + +export const requestPasswordResetFetchFailed = (store: ApiDataStore) => + fetchFailed<{}, {}>({ store, url: "/auth/v3/passwordResets", method: "post" }); + +export const resetPasswordIsFetching = (variables: Omit) => (store: ApiDataStore) => + isFetching<{}, ResetPasswordPathParams>({ + store, + url: "/auth/v3/passwordResets/{token}", + method: "put", + ...variables + }); + +export const resetPasswordFetchFailed = (variables: Omit) => (store: ApiDataStore) => + fetchFailed<{}, ResetPasswordPathParams>({ + store, + url: "/auth/v3/passwordResets/{token}", + method: "put", + ...variables + }); diff --git a/src/generated/v3/userService/userServiceSchemas.ts b/src/generated/v3/userService/userServiceSchemas.ts index a39b977ef..1695198bc 100644 --- a/src/generated/v3/userService/userServiceSchemas.ts +++ b/src/generated/v3/userService/userServiceSchemas.ts @@ -73,3 +73,19 @@ export type UserUpdate = { export type UserUpdateBodyDto = { data: UserUpdate; }; + +export type ResetPasswordResponseDto = { + /** + * User email + * + * @example user@example.com + */ + emailAddress: string; +}; + +export type ResetPasswordRequest = { + emailAddress: string; + callbackUrl: string; +}; + +export type ResetPasswordDto = {}; diff --git a/src/generated/v3/utils.ts b/src/generated/v3/utils.ts index 2e7a3669f..13b8c86a0 100644 --- a/src/generated/v3/utils.ts +++ b/src/generated/v3/utils.ts @@ -71,7 +71,7 @@ export function fetchFailed({ return isErrorState(pending) ? pending : null; } -const isPending = (method: Method, fullUrl: string) => ApiSlice.apiDataStore.meta.pending[method][fullUrl] != null; +const isPending = (method: Method, fullUrl: string) => ApiSlice.currentState.meta.pending[method][fullUrl] != null; async function dispatchRequest(url: string, requestInit: RequestInit) { const actionPayload = { url, method: requestInit.method as Method }; diff --git a/src/helpers/customForms.ts b/src/helpers/customForms.ts index 7bc9302f5..b1914e68c 100644 --- a/src/helpers/customForms.ts +++ b/src/helpers/customForms.ts @@ -182,6 +182,8 @@ export const apiFormQuestionToFormField = ( condition: question.show_on_parent_condition, is_parent_conditional_default: question.is_parent_conditional_default, parent_id: question.parent_id, + min_character_limit: question.min_character_limit, + max_character_limit: question.max_character_limit, feedbackRequired }; @@ -571,6 +573,8 @@ const getFieldValidation = (question: FormQuestionRead, t: typeof useT, framewor const required = question.validation?.required || false; const max = question.validation?.max; const min = question.validation?.min; + const limitMin = question.min_character_limit; + const limitMax = question.max_character_limit; switch (question.input_type) { case "text": @@ -590,6 +594,18 @@ const getFieldValidation = (question: FormQuestionRead, t: typeof useT, framewor if (isNumber(min)) validation = validation.min(min); if (max) validation = validation.max(max); if (required) validation = validation.required(); + if (limitMin) + validation = validation.min( + limitMin, + t(`Your answer does not meet the minimum required characters ${limitMin} for this field.`) + ); + if (limitMax) + validation = validation.max( + limitMax, + t( + `Your answer length exceeds the maximum number of characters ${limitMax} allowed for this field. Please edit your answer to fit within the required number of characters for this field.` + ) + ); return validation; } diff --git a/src/hooks/AuditStatus/useAuditLogActions.ts b/src/hooks/AuditStatus/useAuditLogActions.ts index 4cc6b1481..f4fd66575 100644 --- a/src/hooks/AuditStatus/useAuditLogActions.ts +++ b/src/hooks/AuditStatus/useAuditLogActions.ts @@ -138,7 +138,7 @@ const useAuditLogActions = ({ fetchCriteriaValidation(); fetchCheckPolygons(); } - }, [entityType, record, selected]); + }, [entityType, isPolygon, isSite, isSiteProject, record, selected, verifyEntity]); const isValidCriteriaData = (criteriaData: any) => { if (!criteriaData?.criteria_list?.length) { @@ -190,6 +190,7 @@ const useAuditLogActions = ({ useEffect(() => { refetch(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [buttonToggle, record, entityListItem, selected]); const getValuesStatusEntity = (() => { diff --git a/src/hooks/paginated/useLoadCriteriaSite.ts b/src/hooks/paginated/useLoadCriteriaSite.ts index be82cce6f..545e0b01f 100644 --- a/src/hooks/paginated/useLoadCriteriaSite.ts +++ b/src/hooks/paginated/useLoadCriteriaSite.ts @@ -1,10 +1,11 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { fetchGetV2EntityPolygons, fetchGetV2EntityPolygonsCount, fetchPostV2TerrafundValidationCriteriaData } from "@/generated/apiComponents"; +import { useOnMount } from "@/hooks/useOnMount"; interface LoadCriteriaSiteHook { data: any[]; @@ -100,9 +101,7 @@ const useLoadCriteriaSite = ( }); }; - useEffect(() => { - loadInBatches(); - }, []); + useOnMount(loadInBatches); return { data, diff --git a/src/hooks/useDemographicData.tsx b/src/hooks/useDemographicData.tsx index 8e9971007..04330265c 100644 --- a/src/hooks/useDemographicData.tsx +++ b/src/hooks/useDemographicData.tsx @@ -135,6 +135,6 @@ export default function useDemographicData( const title = t(`${titlePrefix} - {total}`, { total: total ?? "...loading" }); return { grids, title }; }, - [data, collections, framework, t, titlePrefix] + [data, collections, framework, t, titlePrefix, variant, demographicalType] ); } diff --git a/src/hooks/useGetCustomFormSteps/__snapshots__/useGetCustomFormSteps.test.ts.snap b/src/hooks/useGetCustomFormSteps/__snapshots__/useGetCustomFormSteps.test.ts.snap index b8ada2996..f34058a18 100644 --- a/src/hooks/useGetCustomFormSteps/__snapshots__/useGetCustomFormSteps.test.ts.snap +++ b/src/hooks/useGetCustomFormSteps/__snapshots__/useGetCustomFormSteps.test.ts.snap @@ -1014,6 +1014,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "Headquarters address Country", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "3e34aab6-f6fe-45f2-9d2c-581a69dbd1bc", "parent_id": undefined, "placeholder": undefined, @@ -2057,6 +2059,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "What Countries is your organisation legally registered in?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "a90630bf-b77f-4b6e-a828-013f4d0ac1fb", "parent_id": undefined, "placeholder": undefined, @@ -2170,6 +2174,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "Organization Mission", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "d64877a6-fc33-4bd8-9f75-43e77a5112ab", "parent_id": undefined, "placeholder": undefined, @@ -2224,6 +2230,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "On what date was your organisation founded?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "ced1d792-2f5f-40b0-8686-86e048c98406", "parent_id": undefined, "placeholder": undefined, @@ -2311,6 +2319,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "In what languages can your organisation communicate?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "6827e8c1-632c-47cf-a45b-43b8bfba78f2", "parent_id": undefined, "placeholder": undefined, @@ -2517,6 +2527,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "What interventions do you intend to use to restore land?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "9bfe6b76-2ff1-4658-9763-54bc6242cce6", "parent_id": undefined, "placeholder": undefined, @@ -2632,6 +2644,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "Please provide at least two letters of reference", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "6303cc5c-cbd7-4f46-bed6-d339c4c2f00b", "parent_id": undefined, "placeholder": undefined, @@ -2679,6 +2693,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "Organization Twitter URL(optional)", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "981edb54-739e-4971-b078-4e2a971d9ffd", "parent_id": undefined, "placeholder": undefined, @@ -2726,6 +2742,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "List the people who have a ownership stake in your company", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "4388ff23-a5e7-486a-9b37-7ad41cce5e77", "parent_id": undefined, "placeholder": undefined, @@ -2819,6 +2837,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "How does your organization engage with farmers? ", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "3be9ae6a-c769-44e7-a8e6-46cd9b7d7ff2", "parent_id": undefined, "placeholder": undefined, @@ -2975,6 +2995,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "How does your organization engage with women?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "32873c74-5ec4-4508-90a1-bb05b9139ab2", "parent_id": undefined, "placeholder": undefined, @@ -3131,6 +3153,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "How does your organization engage with people younger than 35 years old?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "bb1c3fd3-be90-46db-9996-5148ead42376", "parent_id": undefined, "placeholder": undefined, @@ -3253,6 +3277,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "What were your organization's revenues in USD in 2020?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "2fc2ddcb-65c4-401f-ad8b-2464127995e1", "parent_id": undefined, "placeholder": undefined, @@ -3308,6 +3334,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "Please upload your organization's 2020 income statement and balance sheet.", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "dfc8b46f-a2b5-4c47-9d86-6eaf02f38ff4", "parent_id": undefined, "placeholder": undefined, @@ -3355,6 +3383,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "What were your organization's revenues in USD in 2021?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "a5ff9d12-249f-4752-8c56-93d6c21ff7b6", "parent_id": undefined, "placeholder": undefined, @@ -3410,6 +3440,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "Please upload your organization's 2021 income statement and balance sheet.", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "4519d674-4110-4523-a745-f999741622cd", "parent_id": undefined, "placeholder": undefined, @@ -3457,6 +3489,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "What were your organization's revenues in USD in 2022?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "c1537262-4802-4223-b694-9088d76a2d11", "parent_id": undefined, "placeholder": undefined, @@ -3512,6 +3546,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "Please upload your organization's 2022 income statement and balance sheet.", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "eae46eca-8e4e-4cec-a7a0-cd9d93af8054", "parent_id": undefined, "placeholder": undefined, @@ -3559,6 +3595,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "What are your organization's projected revenues in USD for 2023", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "ae621f3b-552c-4f5f-8616-5d7821299959", "parent_id": undefined, "placeholder": undefined, @@ -3612,6 +3650,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "How many years of restoration experience does your organization have?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "003a59f0-d262-45b1-b4ef-c46c62f79341", "parent_id": undefined, "placeholder": undefined, @@ -3659,6 +3699,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "How many hectares of degraded land has your organisation restored since it was founded? ", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "c8ba24a7-3532-4665-99d0-8d1b10e85f27", "parent_id": undefined, "placeholder": undefined, @@ -3706,6 +3748,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "How many hectares of degraded land has your organization restored in the past 36 months?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "12e2645b-ab2d-4c69-ab21-379408a1e743", "parent_id": undefined, "placeholder": undefined, @@ -3753,6 +3797,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "How many trees has your organization restored or naturally regenerated since it was founded? ", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "1817a476-e340-467c-8c6b-8918c200654e", "parent_id": undefined, "placeholder": undefined, @@ -3800,6 +3846,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "How many trees has your organization planted, naturally regenerated or otherwise restored in the past 36 months?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "4c0e0a9f-636a-45b0-ad5c-80f5722c7cfe", "parent_id": undefined, "placeholder": undefined, @@ -3853,6 +3901,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "What is the name of your proposed project?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "2f114af5-a0b9-405d-926b-7689a9fd5a25", "parent_id": undefined, "placeholder": undefined, @@ -3897,6 +3947,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "What are the objectives of your proposed project?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "917fd2b0-74c2-42c8-b65e-b98144322f6d", "parent_id": undefined, "placeholder": undefined, @@ -4940,6 +4992,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "In what country will your project operate? ", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "875d923a-f1e4-4516-a77a-96fcdf099f66", "parent_id": undefined, "placeholder": undefined, @@ -4987,6 +5041,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "In which subnational jurisdictions would you carry out this project? ", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "59388280-52bd-48f8-97a6-8de30b509494", "parent_id": undefined, "placeholder": undefined, @@ -5034,6 +5090,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "How many hectares of land do you intend to restore through this project?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "5c2b9303-302d-4140-beba-95d79fb2d060", "parent_id": undefined, "placeholder": undefined, @@ -5081,6 +5139,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "How many trees do you intend to restore through this project?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "790370ce-a8e4-4185-a6ef-99a64b2e4bc5", "parent_id": undefined, "placeholder": undefined, @@ -5127,6 +5187,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "What tree species do you intend to grow through this project?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "90acff1b-27f1-4de7-a9a3-caf6be137604", "parent_id": undefined, "placeholder": undefined, @@ -5372,6 +5434,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "What is your proposed project budget in USD? ", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "09475088-63b9-4634-bb13-d0ad6c9ecf14", "parent_id": undefined, "placeholder": undefined, @@ -5436,6 +5500,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "Upload any additional documents", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "e55c81f1-e0ba-42a3-9887-d22f853687c2", "parent_id": undefined, "placeholder": undefined, @@ -5633,6 +5699,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "On which of the following topics would you request technical assistance from a team of experts?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "3d456c52-8e68-45ff-ae88-37a96263130e", "parent_id": undefined, "placeholder": undefined, @@ -5899,6 +5967,8 @@ exports[`test useGetCustomFormSteps hook snapShot test 1`] = ` }, "is_parent_conditional_default": undefined, "label": "How did you hear about this opportunity on TerraMatch?", + "max_character_limit": undefined, + "min_character_limit": undefined, "name": "6ed56eff-b1c3-4df3-bfcc-98db47e02fb0", "parent_id": undefined, "placeholder": undefined, diff --git a/src/hooks/useHideOnScroll.ts b/src/hooks/useHideOnScroll.ts index cbf0a352f..2d2a95940 100644 --- a/src/hooks/useHideOnScroll.ts +++ b/src/hooks/useHideOnScroll.ts @@ -17,6 +17,7 @@ const useHideOnScroll = (ref: RefObject, container?: HTMLElement | container?.removeEventListener("scroll", hideElement); window.removeEventListener("scroll", hideElement); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [container]); }; diff --git a/src/hooks/useMessageValidations.ts b/src/hooks/useMessageValidations.ts index 84b088c1d..a0c371df2 100644 --- a/src/hooks/useMessageValidations.ts +++ b/src/hooks/useMessageValidations.ts @@ -24,17 +24,19 @@ interface ExtraInfoItem { error?: string; } +const FIELDS_TO_VALIDATE: Record = { + poly_name: "Polygon Name", + plantstart: "Plant Start Date", + plantend: "Plant End Date", + practice: "Restoration Practice", + target_sys: "Target Land Use System", + distr: "Tree Distribution", + num_trees: "Number of Trees" +}; + export const useMessageValidators = () => { const t = useT(); - const fieldsToValidate: any = { - poly_name: "Polygon Name", - plantstart: "Plant Start Date", - plantend: "Plant End Date", - practice: "Restoration Practice", - target_sys: "Target Land Use System", - distr: "Tree Distribution", - num_trees: "Number of Trees" - }; + const getIntersectionMessages = useMemo( () => (extraInfo: any): string[] => { @@ -91,28 +93,28 @@ export const useMessageValidators = () => { return infoArray .map(info => { if (!info.exists) { - return t("{field} is missing", { field: fieldsToValidate[info.field] }); + return t("{field} is missing", { field: FIELDS_TO_VALIDATE[info.field] }); } switch (info.field) { case "target_sys": return t( "{field}: {error} is not a valid {field} because it is not one of ['agroforest', 'natural-forest', 'mangrove', 'peatland', 'riparian-area-or-wetland', 'silvopasture', 'woodlot-or-plantation', 'urban-forest']", - { field: fieldsToValidate[info.field], error: info.error } + { field: FIELDS_TO_VALIDATE[info.field], error: info.error } ); case "distr": return t( "{field}: {error} is not a valid {field} because it is not one of ['single-line', 'partial', 'full']", - { field: fieldsToValidate[info.field], error: info.error } + { field: FIELDS_TO_VALIDATE[info.field], error: info.error } ); case "num_trees": return t("{field} {error} is not a valid number", { - field: fieldsToValidate[info.field], + field: FIELDS_TO_VALIDATE[info.field], error: info.error }); case "practice": return t( "{field}: {error} is not a valid {field} because it is not one of ['tree-planting', 'direct-seeding', 'assisted-natural-regeneration']", - { field: fieldsToValidate[info.field], error: info.error } + { field: FIELDS_TO_VALIDATE[info.field], error: info.error } ); default: return null; diff --git a/src/hooks/useProcessRecordData.ts b/src/hooks/useProcessRecordData.ts index c6adcc0f3..ef0ab630f 100644 --- a/src/hooks/useProcessRecordData.ts +++ b/src/hooks/useProcessRecordData.ts @@ -26,5 +26,5 @@ export function useProcessRecordData(modelUUID: string, modelName: string, input } return true; - }, [record, inputType, modelUUID, modelName]); + }, [record, inputType]); } diff --git a/src/hooks/useReportingWindow.ts b/src/hooks/useReportingWindow.ts index 82cf96251..42ea53d55 100644 --- a/src/hooks/useReportingWindow.ts +++ b/src/hooks/useReportingWindow.ts @@ -40,5 +40,6 @@ export const useReportingWindow = (dueDate: string) => { } return `${start} - ${end} ${year}`; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [dueDate]); }; diff --git a/src/hooks/useValueChanged.ts b/src/hooks/useValueChanged.ts index d06a0f793..8c73a79f1 100644 --- a/src/hooks/useValueChanged.ts +++ b/src/hooks/useValueChanged.ts @@ -1,10 +1,11 @@ -import { useEffect, useRef } from "react"; +import { useEffect } from "react"; /** * A hook useful for executing a side effect after component render (in an effect) if the given - * value changes. Uses strict equals. The primary use of this hook is to prevent a side effect from - * being executed multiple times if the component re-renders after the value has transitioned to its - * action state. + * value changes. Uses strict equals. The primary use of this hook are: + * * to prevent a side effect from being executed multiple times if the component re-renders after + * the value has transitioned to its action state. + * * to take some action every time a given value changes * * Callback is guaranteed to execute on the first render of the component. This is intentional. A * consumer of this hook is expected to check the current state of the value and take action based @@ -12,18 +13,20 @@ import { useEffect, useRef } from "react"; * want the resulting side effect to take place immediately, rather than only when the value has * changed. * - * Example: + * Examples: * * useValueChanged(isLoggedIn, () => { * if (isLoggedIn) router.push("/home"); * } + * + * useValueChanged(buttonToggle, () => { + * refetch(); + * loadEntityList(); + * }); */ export function useValueChanged(value: T, callback: () => void) { - const ref = useRef(); useEffect(() => { - if (ref.current !== value) { - ref.current = value; - callback(); - } - }); + callback(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]); } diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index aa90ec5c3..605433210 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -5,6 +5,7 @@ export default function Document() { return ( +
- Overview -
+ Overview +
+ Environmental Impact +
+ Social Impact +
© TerraMatch 2024