diff --git a/openapi-codegen.config.ts b/openapi-codegen.config.ts index 948b14281..e1297e325 100644 --- a/openapi-codegen.config.ts +++ b/openapi-codegen.config.ts @@ -27,6 +27,7 @@ type EnvironmentName = (typeof ENVIRONMENT_NAMES)[number]; type Environment = { apiBaseUrl: string; userServiceUrl: string; + jobServiceUrl: string; entityServiceUrl: string; }; @@ -34,26 +35,31 @@ const ENVIRONMENTS: { [Property in EnvironmentName]: Environment } = { local: { apiBaseUrl: "http://localhost:8080", userServiceUrl: "http://localhost:4010", + jobServiceUrl: "http://localhost:4020", entityServiceUrl: "http://localhost:4050" }, dev: { apiBaseUrl: "https://api-dev.terramatch.org", userServiceUrl: "https://api-dev.terramatch.org", + jobServiceUrl: "https://api-dev.terramatch.org", entityServiceUrl: "https://api-dev.terramatch.org" }, test: { apiBaseUrl: "https://api-test.terramatch.org", userServiceUrl: "https://api-test.terramatch.org", + jobServiceUrl: "https://api-test.terramatch.org", entityServiceUrl: "https://api-test.terramatch.org" }, staging: { apiBaseUrl: "https://api-staging.terramatch.org", userServiceUrl: "https://api-staging.terramatch.org", + jobServiceUrl: "https://api-staging.terramatch.org", entityServiceUrl: "https://api-staging.terramatch.org" }, prod: { apiBaseUrl: "https://api.terramatch.org", userServiceUrl: "https://api.terramatch.org", + jobServiceUrl: "https://api.terramatch.org", entityServiceUrl: "https://api.terramatch.org" } }; @@ -66,6 +72,7 @@ if (!ENVIRONMENT_NAMES.includes(declaredEnv as EnvironmentName)) { const DEFAULTS = ENVIRONMENTS[declaredEnv]; const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL ?? DEFAULTS.apiBaseUrl; const userServiceUrl = process.env.NEXT_PUBLIC_USER_SERVICE_URL ?? DEFAULTS.userServiceUrl; +const jobServiceUrl = process.env.NEXT_PUBLIC_JOB_SERVICE_URL ?? DEFAULTS.jobServiceUrl; const entityServiceUrl = process.env.NEXT_PUBLIC_ENTITY_SERVICE_URL ?? DEFAULTS.entityServiceUrl; // The services defined in the v3 Node BE codebase. Although the URL path for APIs in the v3 space @@ -74,6 +81,7 @@ const entityServiceUrl = process.env.NEXT_PUBLIC_ENTITY_SERVICE_URL ?? DEFAULTS. // the associated BE code is for a given FE API integration. const SERVICES = { "user-service": userServiceUrl, + "job-service": jobServiceUrl, "entity-service": entityServiceUrl }; diff --git a/package.json b/package.json index d184e19fb..22e502fb1 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,10 @@ "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "generate:api": "openapi-codegen gen api", + "generate:jobService": "openapi-codegen gen jobService", "generate:userService": "openapi-codegen gen userService", "generate:entityService": "openapi-codegen gen entityService", - "generate:services": "yarn generate:userService && yarn generate:entityService", + "generate:services": "yarn generate:userService && yarn generate:entityService && yarn generate:jobService", "tx:push": "eval $(grep '^TRANSIFEX_TOKEN' .env) && eval $(grep '^TRANSIFEX_SECRET' .env) && txjs-cli push --key-generator=hash src/ --token=$TRANSIFEX_TOKEN --secret=$TRANSIFEX_SECRET", "tx:pull": "eval $(grep '^TRANSIFEX_TOKEN' .env) && eval $(grep '^TRANSIFEX_SECRET' .env) && txjs-cli pull --token=$TRANSIFEX_TOKEN --secret=$TRANSIFEX_SECRET" }, 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 3dff79d6f..bb8af6a2c 100644 --- a/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/components/VersionHistory.tsx +++ b/src/admin/components/ResourceTabs/PolygonReviewTab/components/PolygonDrawer/components/VersionHistory.tsx @@ -362,7 +362,8 @@ const VersionHistory = ({ placeholder="Select Polygon Version" options={polygonVersionData ?? []} optionVariant="text-12-light" - titleClassname="one-line-text !w-[96%] !text-nowrap" + titleClassname="one-line-text !w-full !text-nowrap" + titleContainerClassName="!w-[calc(100%-25px)] !text-nowrap" defaultValue={[selectPolygonVersion?.uuid ?? selectedPolygon?.uuid] as string[]} onChange={e => { const polygonVersionData = (data as SitePolygonsDataResponse)?.find(item => item.uuid === e[0]); diff --git a/src/admin/components/ResourceTabs/PolygonReviewTab/index.tsx b/src/admin/components/ResourceTabs/PolygonReviewTab/index.tsx index 9117e6456..9376b94ce 100644 --- a/src/admin/components/ResourceTabs/PolygonReviewTab/index.tsx +++ b/src/admin/components/ResourceTabs/PolygonReviewTab/index.tsx @@ -629,8 +629,15 @@ const PolygonReviewTab: FC = props => {
Site Status - - + + @@ -640,8 +647,15 @@ const PolygonReviewTab: FC = props => {
Polygon Overview - - + + @@ -660,9 +674,6 @@ const PolygonReviewTab: FC = props => {
Add or Edit Polygons - - - Add, remove or edit polygons that are associated to a site. Polygons may be edited in the map @@ -742,7 +753,7 @@ const PolygonReviewTab: FC = props => { pagination: { pageSize: 10000000 } }} columns={[ - { header: "Polygon Name", accessorKey: "polygon-name" }, + { header: "Polygon Name", accessorKey: "polygon-name", meta: { style: { width: "14.63%" } } }, { header: "Restoration Practice", accessorKey: "restoration-practice", @@ -751,15 +762,28 @@ const PolygonReviewTab: FC = props => { return ( ); - } + }, + meta: { style: { width: "17.63%" } } + }, + { + header: "Target Land Use System", + accessorKey: "target-land-use-system", + meta: { style: { width: "20.63%" } } + }, + { + header: "Tree Distribution", + accessorKey: "tree-distribution", + meta: { style: { width: "15.63%" } } + }, + { + header: "Planting Start Date", + accessorKey: "planting-start-date", + meta: { style: { width: "17.63%" } } }, - { header: "Target Land Use System", accessorKey: "target-land-use-system" }, - { header: "Tree Distribution", accessorKey: "tree-distribution" }, - { header: "Planting Start Date", accessorKey: "planting-start-date" }, - { header: "Source", accessorKey: "source" }, + { header: "Source", accessorKey: "source", meta: { style: { width: "10.63%" } } }, { header: "", accessorKey: "ellipse", diff --git a/src/components/elements/Inputs/Dropdown/Dropdown.tsx b/src/components/elements/Inputs/Dropdown/Dropdown.tsx index b6815374e..dff695d13 100644 --- a/src/components/elements/Inputs/Dropdown/Dropdown.tsx +++ b/src/components/elements/Inputs/Dropdown/Dropdown.tsx @@ -60,6 +60,7 @@ export interface DropdownProps { onInternalError?: (error: ErrorOption) => void; showSelectAll?: boolean; titleClassname?: string; + titleContainerClassName?: string; } const otherKey = "other#value#key"; const getAllowedValues = (values: OptionValue[], options: Option[]) => @@ -206,7 +207,13 @@ const Dropdown = (props: PropsWithChildren) => { )} > {props.prefix} -
+
void }) => { + const t = useT(); + const { closeModal } = useModalContext(); + + return ( + + + + + } + /> + ); +}; + +export default NonScientificConfirmationModal; diff --git a/src/components/elements/Inputs/TreeSpeciesInput/RHFSeedingTableInput.tsx b/src/components/elements/Inputs/TreeSpeciesInput/RHFSeedingTableInput.tsx index 447eadfbe..5dd44724d 100644 --- a/src/components/elements/Inputs/TreeSpeciesInput/RHFSeedingTableInput.tsx +++ b/src/components/elements/Inputs/TreeSpeciesInput/RHFSeedingTableInput.tsx @@ -9,7 +9,7 @@ export interface RHFSeedingTableInputProps UseControllerProps { collection?: string; onChangeCapture?: () => void; - formHook?: UseFormReturn; + formHook: UseFormReturn; } /** @@ -19,12 +19,14 @@ export interface RHFSeedingTableInputProps const RHFSeedingTableInput = (props: PropsWithChildren) => { const t = useT(); const { - field: { value, onChange } + field: { onChange } } = useController(props); const { formHook, collection } = props; + const value = formHook.watch(props.name); + const clearErrors = useCallback(() => { - formHook?.clearErrors(props.name); + formHook.clearErrors(props.name); }, [formHook, props.name]); return ( @@ -39,7 +41,7 @@ const RHFSeedingTableInput = (props: PropsWithChildren - props.formHook?.setError(props.name, { message: t("One or more values are missing"), type: "required" }) + props.formHook.setError(props.name, { message: t("One or more values are missing"), type: "required" }) } /> ); diff --git a/src/components/elements/Inputs/TreeSpeciesInput/RHFTreeSpeciesInput.tsx b/src/components/elements/Inputs/TreeSpeciesInput/RHFTreeSpeciesInput.tsx index 3bec5353a..4a2dc829b 100644 --- a/src/components/elements/Inputs/TreeSpeciesInput/RHFTreeSpeciesInput.tsx +++ b/src/components/elements/Inputs/TreeSpeciesInput/RHFTreeSpeciesInput.tsx @@ -7,7 +7,7 @@ import TreeSpeciesInput, { TreeSpeciesInputProps } from "./TreeSpeciesInput"; export interface RHFTreeSpeciesInputProps extends Omit, UseControllerProps { - formHook?: UseFormReturn; + formHook: UseFormReturn; } /** @@ -17,18 +17,20 @@ export interface RHFTreeSpeciesInputProps const RHFTreeSpeciesInput = (props: PropsWithChildren) => { const t = useT(); const { - field: { value, onChange } + field: { onChange } } = useController(props); const { formHook, collection } = props; + const value = formHook.watch(props.name); + const clearErrors = useCallback(() => { - formHook?.clearErrors(props.name); + formHook.clearErrors(props.name); }, [formHook, props.name]); return ( ) collection={collection} clearErrors={clearErrors} onError={() => - props.formHook?.setError(props.name, { message: t("One or more values are missing"), type: "required" }) + props.formHook.setError(props.name, { message: t("One or more values are missing"), type: "required" }) } /> ); diff --git a/src/components/elements/Inputs/TreeSpeciesInput/SpeciesAlreadyExistsModal.tsx b/src/components/elements/Inputs/TreeSpeciesInput/SpeciesAlreadyExistsModal.tsx new file mode 100644 index 000000000..a9036af1e --- /dev/null +++ b/src/components/elements/Inputs/TreeSpeciesInput/SpeciesAlreadyExistsModal.tsx @@ -0,0 +1,27 @@ +import { useT } from "@transifex/react"; + +import Button from "@/components/elements/Button/Button"; +import TreeSpeciesModal from "@/components/elements/Inputs/TreeSpeciesInput/TreeSpeciesModal"; +import { ModalId } from "@/components/extensive/Modal/ModalConst"; +import { useModalContext } from "@/context/modal.provider"; + +const SpeciesAlreadyExistsModal = ({ speciesName }: { speciesName: string }) => { + const { closeModal } = useModalContext(); + const t = useT(); + + return ( + + + + } + /> + ); +}; + +export default SpeciesAlreadyExistsModal; diff --git a/src/components/elements/Inputs/TreeSpeciesInput/TreeSpeciesInput.tsx b/src/components/elements/Inputs/TreeSpeciesInput/TreeSpeciesInput.tsx index 8677e0eba..4187c8b11 100644 --- a/src/components/elements/Inputs/TreeSpeciesInput/TreeSpeciesInput.tsx +++ b/src/components/elements/Inputs/TreeSpeciesInput/TreeSpeciesInput.tsx @@ -6,6 +6,8 @@ import { FieldError, FieldErrors } from "react-hook-form"; import { Else, If, Then, When } from "react-if"; import { v4 as uuidv4 } from "uuid"; +import NonScientificConfirmationModal from "@/components/elements/Inputs/TreeSpeciesInput/NonScientificConfirmationModal"; +import SpeciesAlreadyExistsModal from "@/components/elements/Inputs/TreeSpeciesInput/SpeciesAlreadyExistsModal"; import { useAutocompleteSearch } from "@/components/elements/Inputs/TreeSpeciesInput/useAutocompleteSearch"; import Icon, { IconNames } from "@/components/extensive/Icon/Icon"; import List from "@/components/extensive/List/List"; @@ -28,6 +30,7 @@ import InputWrapper, { InputWrapperProps } from "../InputElements/InputWrapper"; export interface TreeSpeciesInputProps extends Omit { title: string; + label?: string; buttonCaptionSuffix: string; withNumbers?: boolean; withPreviousCounts: boolean; @@ -41,45 +44,12 @@ export interface TreeSpeciesInputProps extends Omit error?: FieldErrors[]; } -export type TreeSpeciesValue = { uuid?: string; name?: string; taxon_id?: string; amount?: number }; - -const NonScientificConfirmationModal = ({ onConfirm }: { onConfirm: () => void }) => { - const t = useT(); - const { closeModal } = useModalContext(); - - return ( -
-
- - - {t("Your input is a not a scientific name")} - -
-
-
-
- - {t("You can add this species, but it will be pending review from Admin.")} - -
-
-
- - -
-
-
- ); +export type TreeSpeciesValue = { + uuid?: string; + name?: string; + collection?: string; + taxon_id?: string; + amount?: number; }; const TreeSpeciesInput = (props: TreeSpeciesInputProps) => { @@ -110,7 +80,11 @@ const TreeSpeciesInput = (props: TreeSpeciesInputProps) => { const entity = (handleBaseEntityTrees ? entityName : undefined) as EstablishmentEntityType; const uuid = handleBaseEntityTrees ? entityUuid : undefined; - const [establishmentLoaded, { establishmentTrees, previousPlantingCounts }] = useEstablishmentTrees({ entity, uuid }); + const [establishmentLoaded, { establishmentTrees, previousPlantingCounts }] = useEstablishmentTrees({ + entity, + uuid, + collection + }); const shouldPrepopulate = value.length == 0 && Object.values(previousPlantingCounts ?? {}).length > 0; useValueChanged(shouldPrepopulate, function () { if (shouldPrepopulate) { @@ -119,7 +93,8 @@ const TreeSpeciesInput = (props: TreeSpeciesInputProps) => { uuid: uuidv4(), name, taxon_id: previousCount.taxonId, - amount: 0 + amount: 0, + collection: props.collection })) ); } @@ -137,10 +112,10 @@ const TreeSpeciesInput = (props: TreeSpeciesInputProps) => { const handleCreate = useDebounce( useCallback( (treeValue: TreeSpeciesValue) => { - onChange([...value, { ...treeValue, collection }]); + onChange([...value, { ...treeValue }]); clearErrors(); }, - [onChange, value, collection, clearErrors] + [onChange, value, clearErrors] ) ); @@ -176,15 +151,19 @@ const TreeSpeciesInput = (props: TreeSpeciesInputProps) => { uuid: uuidv4(), name: valueAutoComplete, taxon_id: props.useTaxonomicBackbone ? taxonId : undefined, - amount: props.withNumbers ? 0 : undefined + amount: props.withNumbers ? 0 : undefined, + collection }); setValueAutoComplete(""); lastInputRef.current?.focus(); }; - if (!isEmpty(searchResult) && taxonId == null) { - // In this case the use had valid values to choose from, but decided to add a value that isn't + if (value.find(({ name }) => name === valueAutoComplete) != null) { + openModal(ModalId.ERROR_MODAL, ); + setValueAutoComplete(""); + } else if (!isEmpty(searchResult) && taxonId == null) { + // In this case the user had valid values to choose from, but decided to add a value that isn't // on the list, so they haven't been shown the warning yet. openModal(ModalId.ERROR_MODAL, ); } else { @@ -225,7 +204,7 @@ const TreeSpeciesInput = (props: TreeSpeciesInputProps) => { return ( {
- {isReport ? t("SPECIES PLANTED:") : t("TREES TO BE PLANTED:")} + {isReport ? t("TOTAL PLANTED THIS REPORT:") : t("TREES TO BE PLANTED:")} {props.withNumbers ? props.value.reduce((total, v) => total + (v.amount || 0), 0).toLocaleString() : "0"} diff --git a/src/components/elements/Inputs/TreeSpeciesInput/TreeSpeciesModal.tsx b/src/components/elements/Inputs/TreeSpeciesInput/TreeSpeciesModal.tsx new file mode 100644 index 000000000..11076cdf1 --- /dev/null +++ b/src/components/elements/Inputs/TreeSpeciesInput/TreeSpeciesModal.tsx @@ -0,0 +1,33 @@ +import { ReactNode } from "react"; + +import Text from "@/components/elements/Text/Text"; +import Icon, { IconNames } from "@/components/extensive/Icon/Icon"; + +type TreeSpeciesModalProps = { + title: string; + content: string; + buttons: ReactNode; +}; + +const TreeSpeciesModal = ({ title, content, buttons }: TreeSpeciesModalProps) => ( +
+
+ + + {title} + +
+
+
+
+ + {content} + +
+
+
{buttons}
+
+
+); + +export default TreeSpeciesModal; diff --git a/src/components/elements/Inputs/TreeSpeciesInput/__snapshots__/TreeSpeciesInput.stories.storyshot b/src/components/elements/Inputs/TreeSpeciesInput/__snapshots__/TreeSpeciesInput.stories.storyshot index 9810e81fc..bd502d0a0 100644 --- a/src/components/elements/Inputs/TreeSpeciesInput/__snapshots__/TreeSpeciesInput.stories.storyshot +++ b/src/components/elements/Inputs/TreeSpeciesInput/__snapshots__/TreeSpeciesInput.stories.storyshot @@ -9,7 +9,7 @@ exports[`Storyshots Components/Elements/Inputs/TreeSpeciesInput Default 1`] = ` data-testid="txt" htmlFor=":r23:" > - ADD TREE SPECIES * + Tree Species Grown *

- ADD TREE SPECIES * + Tree Species Grown *

({ const [sorting, setSorting] = useState(initialTableState?.sorting ?? []); const [filters, setFilters] = useState([]); + const spanRefs = useRef([]); + const iconRefs = useRef([]); + const { getHeaderGroups, getRowModel, @@ -183,7 +186,7 @@ function Table({ } >

({ fontFamily: "inherit" }} > - {flexRender(header.column.columnDef.header, header.getContext())} - - - +
+ { + if ( + el && + !spanRefs.current.includes(el) && + flexRender(header.column.columnDef.header, header.getContext()) + ) { + spanRefs.current.push(el); + } + }} + > + {flexRender(header.column.columnDef.header, header.getContext())} + + + { + if (el && !iconRefs.current.includes(el)) { + iconRefs.current.push(el); + } + }} + className="absolute left-[calc(100%+10px)] top-1/2 z-auto -translate-y-1/2 transform" + style={{ left: `${spanRefs.current[header.index]?.getBoundingClientRect().width}px` }} + > + + + +
); diff --git a/src/components/elements/Table/TableVariants.ts b/src/components/elements/Table/TableVariants.ts index fd80fb984..a444c5694 100644 --- a/src/components/elements/Table/TableVariants.ts +++ b/src/components/elements/Table/TableVariants.ts @@ -97,9 +97,9 @@ export const VARIANT_TABLE_SITE_POLYGON_REVIEW = { table: "border-collapse", name: "border-airtable", tableWrapper: "border border-neutral-200 rounded-lg overflow-hidden", - trHeader: "bg-neutral-150", + trHeader: "bg-neutral-150 sticky top-0 z-auto", thHeader: - "first:pl-4 first:pr-2 last:pl-2 last:pr-4 border-y border-neutral-200 text-14-semibold whitespace-normal px-2 border-t-0", + "first:pl-4 first:pr-2 last:pl-2 last:pr-4 border-y border-neutral-200 text-14-semibold whitespace-normal px-2 border-t-0 sticky top-0 z-auto", tBody: "", trBody: "bg-white border-y border-neutral-200 last:border-b-0", tdBody: "text-14-light px-2 py-3 first:pl-4 first:pr-2 last:pl-2 last:pr-4 ", diff --git a/src/components/elements/Table/__snapshots__/Table.stories.storyshot b/src/components/elements/Table/__snapshots__/Table.stories.storyshot index 4049211ad..ebac4acaa 100644 --- a/src/components/elements/Table/__snapshots__/Table.stories.storyshot +++ b/src/components/elements/Table/__snapshots__/Table.stories.storyshot @@ -124,7 +124,7 @@ exports[`Storyshots Components/Elements/Table Default 1`] = ` } >
- Funding type +
+ + Funding type + +
- Funding source
+ > + + Funding source + + +
+ +
- Funding amount
+ > + + Funding amount + + +
+ +
- Status +
+ + Status + +
+ > +
+ +
+
@@ -987,7 +1070,7 @@ exports[`Storyshots Components/Elements/Table Primary 1`] = ` } >
- Funding type +
+ + Funding type + +
- Funding source
+ > + + Funding source + + +
+ +
- Funding amount
+ > + + Funding amount + + +
+ +
- Status +
+ + Status + +
+ > +
+ +
+
@@ -1850,7 +2016,7 @@ exports[`Storyshots Components/Elements/Table Secundary White 1`] = ` } >
- Funding type +
+ + Funding type + +
- Funding source
+ > + + Funding source + + +
+ +
- Funding amount
+ > + + Funding amount + + +
+ +
- Status +
+ + Status + +
+ > +
+ +
+
@@ -2713,7 +2962,7 @@ exports[`Storyshots Components/Elements/Table Table Airtable 1`] = ` } >
- Funding type +
+ + Funding type + +
- Funding source
+ > + + Funding source + + +
+ +
- Funding amount
+ > + + Funding amount + + +
+ +
- Status +
+ + Status + +
+ > +
+ +
+
@@ -3579,7 +3911,7 @@ exports[`Storyshots Components/Elements/Table Table Airtable Dashboard 1`] = ` } >
- Funding type +
+ + Funding type + +
- Funding source
+ > + + Funding source + + +
+ +
- Funding amount
+ > + + Funding amount + + +
+ +
- Status +
+ + Status + +
+ > +
+ +
+
@@ -4443,7 +4858,7 @@ exports[`Storyshots Components/Elements/Table Table Border 1`] = ` } >
- Funding type +
+ + Funding type + +
- Funding source
+ > + + Funding source + + +
+ +
- Funding amount
+ > + + Funding amount + + +
+ +
- Status +
+ + Status + +
+ > +
+ +
+
@@ -5306,7 +5804,7 @@ exports[`Storyshots Components/Elements/Table Table Border All 1`] = ` } >
- Funding type +
+ + Funding type + +
- Funding source
+ > + + Funding source + + +
+ +
- Funding amount
+ > + + Funding amount + + +
+ +
- Status +
+ + Status + +
+ > +
+ +
+
@@ -6172,7 +6753,7 @@ exports[`Storyshots Components/Elements/Table Table Dashboard Countries 1`] = ` } >
- Funding type +
+ + Funding type + +
- Funding source
+ > + + Funding source + + +
+ +
- Funding amount
+ > + + Funding amount + + +
+ +
- Status +
+ + Status + +
+ > +
+ +
+
@@ -7039,7 +7703,7 @@ exports[`Storyshots Components/Elements/Table Table Dashboard Countries Modal 1` } >
- Funding type +
+ + Funding type + +
- Funding source
+ > + + Funding source + + +
+ +
- Funding amount
+ > + + Funding amount + + +
+ +
- Status +
+ + Status + +
+ > +
+ +
+
@@ -7906,7 +8653,7 @@ exports[`Storyshots Components/Elements/Table Table Organization 1`] = ` } >
- Funding type +
+ + Funding type + +
- Funding source
+ > + + Funding source + + +
+ +
- Funding amount
+ > + + Funding amount + + +
+ +
- Status +
+ + Status + +
+ > +
+ +
+
@@ -8756,11 +9586,11 @@ exports[`Storyshots Components/Elements/Table Table Site Polygon 1`] = ` className="bg-blueCustom-100" >
- Funding type +
+ + Funding type + +
- Funding source
+ > + + Funding source + + +
+ +
- Funding amount
+ > + + Funding amount + + +
+ +
- Status +
+ + Status + +
+ > +
+ +
+
@@ -9636,7 +10549,7 @@ exports[`Storyshots Components/Elements/Table Table Version 1`] = ` } >
- Funding type +
+ + Funding type + +
- Funding source
+ > + + Funding source + + +
+ +
- Funding amount
+ > + + Funding amount + + +
+ +
- Status +
+ + Status + +
+ > +
+ +
+
diff --git a/src/components/extensive/WizardForm/FormHeader.tsx b/src/components/extensive/WizardForm/FormHeader.tsx index b479cb014..2b9c61ae9 100644 --- a/src/components/extensive/WizardForm/FormHeader.tsx +++ b/src/components/extensive/WizardForm/FormHeader.tsx @@ -11,11 +11,15 @@ export interface WizardFormHeaderProps { errorMessage?: string; onClickSaveAndCloseButton?: () => void; title?: string; + subtitle?: string; } export const WizardFormHeader = (props: WizardFormHeaderProps) => { const t = useT(); + const subtitle = + props.subtitle ?? t("Progress: {number} steps complete", { number: `${props.currentStep}/${props.numberOfSteps}` }); + return (
@@ -27,9 +31,9 @@ export const WizardFormHeader = (props: WizardFormHeaderProps) => { )}
- + {subtitle} - + {t("Unsaved")} {t("Saving…")} diff --git a/src/components/extensive/WizardForm/index.tsx b/src/components/extensive/WizardForm/index.tsx index 4cea6fcb1..aeb1452c7 100644 --- a/src/components/extensive/WizardForm/index.tsx +++ b/src/components/extensive/WizardForm/index.tsx @@ -32,6 +32,7 @@ export interface WizardFormProps { formStatus?: "saving" | "saved"; title?: string; + subtitle?: string; errors?: ErrorWrapper; summaryOptions?: FormSummaryOptions & { downloadButtonText?: string; @@ -267,6 +268,7 @@ function WizardForm(props: WizardFormProps) { errorMessage={props.errors && t("Something went wrong")} onClickSaveAndCloseButton={!props.hideSaveAndCloseButton ? onClickSaveAndClose : undefined} title={props.title} + subtitle={props.subtitle} />
diff --git a/src/components/extensive/WizardForm/utils.ts b/src/components/extensive/WizardForm/utils.ts index 13893a348..e7f21f0a0 100644 --- a/src/components/extensive/WizardForm/utils.ts +++ b/src/components/extensive/WizardForm/utils.ts @@ -44,7 +44,7 @@ export const getSchemaFields = (fields: FormField[]) => { } }); } else { - schema[field.name] = field.validation?.nullable().label(field.label); + schema[field.name] = field.validation?.nullable().label(" "); } if (field.fieldProps.required) schema[field.name] = schema[field.name].required(); diff --git a/src/connections/EstablishmentTrees.ts b/src/connections/EstablishmentTrees.ts index ca30a376d..7cd0ecb73 100644 --- a/src/connections/EstablishmentTrees.ts +++ b/src/connections/EstablishmentTrees.ts @@ -12,13 +12,15 @@ import { connectionHook } from "@/utils/connectionShortcuts"; import { selectorCache } from "@/utils/selectorCache"; type EstablishmentTreesConnection = { - establishmentTrees?: EstablishmentsTreesDto["establishmentTrees"]; - previousPlantingCounts?: EstablishmentsTreesDto["previousPlantingCounts"]; + establishmentTrees?: EstablishmentsTreesDto["establishmentTrees"][string]; + previousPlantingCounts?: Exclude[string]; establishmentTreesLoadFailed: boolean; }; -type EstablishmentTreesProps = Partial; +type EstablishmentTreesProps = Partial & { + collection?: string; +}; export type EstablishmentEntityType = EstablishmentTreesFindPathParams["entity"] | undefined; const establishmentTreesSelector = @@ -32,8 +34,8 @@ const establishmentTreesLoadFailed = const connectionIsLoaded = ( { establishmentTrees, establishmentTreesLoadFailed }: EstablishmentTreesConnection, - { entity, uuid }: EstablishmentTreesProps -) => entity == null || uuid == null || establishmentTrees != null || establishmentTreesLoadFailed; + { entity, uuid, collection }: EstablishmentTreesProps +) => collection == null || entity == null || uuid == null || establishmentTrees != null || establishmentTreesLoadFailed; const establishmentTreesConnection: Connection = { load: (connection, props) => { @@ -45,15 +47,18 @@ const establishmentTreesConnection: Connection `${entity}|${uuid}`, - ({ entity, uuid }) => + ({ entity, uuid, collection }) => `${entity}|${uuid}|${collection}`, + ({ entity, uuid, collection }) => createSelector( [establishmentTreesSelector(entity, uuid), establishmentTreesLoadFailed(entity, uuid)], - (treesDto, establishmentTreesLoadFailed) => ({ - establishmentTrees: treesDto?.attributes?.establishmentTrees, - previousPlantingCounts: treesDto?.attributes?.previousPlantingCounts, - establishmentTreesLoadFailed - }) + (treesDto, establishmentTreesLoadFailed) => { + const loadComplete = treesDto?.attributes?.establishmentTrees != null; + const establishmentTrees = + collection == null || !loadComplete ? undefined : treesDto.attributes.establishmentTrees[collection] ?? []; + const previousPlantingCounts = + collection == null || !loadComplete ? undefined : treesDto.attributes.previousPlantingCounts?.[collection]; + return { establishmentTrees, previousPlantingCounts, establishmentTreesLoadFailed }; + } ) ) }; diff --git a/src/generated/apiFetcher.ts b/src/generated/apiFetcher.ts index 9c489d679..0b08d922b 100644 --- a/src/generated/apiFetcher.ts +++ b/src/generated/apiFetcher.ts @@ -193,11 +193,11 @@ async function processDelayedJob(signal: AbortSignal | undefined, delayed jobResult = await loadJob(signal, delayedJobId) ) { //@ts-ignore - const { total_content, processed_content, progress_message } = jobResult.data?.attributes; - if (total_content != null) { - ApiSlice.addTotalContent(total_content); - ApiSlice.addProgressContent(processed_content); - ApiSlice.addProgressMessage(progress_message); + const { totalContent, processedContent, progressMessage } = jobResult.data?.attributes; + if (totalContent != null) { + ApiSlice.addTotalContent(totalContent); + ApiSlice.addProgressContent(processedContent); + ApiSlice.addProgressMessage(progressMessage); } if (signal?.aborted || ApiSlice.apiDataStore.abort_delayed_job) throw new Error("Aborted"); diff --git a/src/generated/v3/entityService/entityServiceSchemas.ts b/src/generated/v3/entityService/entityServiceSchemas.ts index 9734a4f86..9b8edcaf1 100644 --- a/src/generated/v3/entityService/entityServiceSchemas.ts +++ b/src/generated/v3/entityService/entityServiceSchemas.ts @@ -25,15 +25,21 @@ export type ScientificNameDto = { export type EstablishmentsTreesDto = { /** - * The species that were specified at the establishment of the parent entity. + * The species that were specified at the establishment of the parent entity keyed by collection + * + * @example {"tree-planted":["Aster Peraliens","Circium carniolicum"],"non-tree":["Coffee"]} */ - establishmentTrees: string[]; + establishmentTrees: { + [key: string]: string[]; + }; /** - * If the entity in this request is a report, the sum totals of previous planting by species. + * If the entity in this request is a report, the sum totals of previous planting by species by collection. * - * @example {"Aster persaliens":{"amount":256},"Cirsium carniolicum":{"taxonId":"wfo-0000130112","amount":1024}} + * @example {"tree-planted":{"Aster persaliens":{"amount":256},"Cirsium carniolicum":{"taxonId":"wfo-0000130112","amount":1024}},"non-tree":{"Coffee":{"amount":2048}}} */ previousPlantingCounts: { - [key: string]: PreviousPlantingCountDto; + [key: string]: { + [key: string]: PreviousPlantingCountDto; + }; } | null; }; diff --git a/src/generated/v3/jobService/jobServiceComponents.ts b/src/generated/v3/jobService/jobServiceComponents.ts new file mode 100644 index 000000000..bdece4f35 --- /dev/null +++ b/src/generated/v3/jobService/jobServiceComponents.ts @@ -0,0 +1,203 @@ +/** + * Generated by @openapi-codegen + * + * @version 1.0 + */ +import type * as Fetcher from "./jobServiceFetcher"; +import { jobServiceFetch } from "./jobServiceFetcher"; +import type * as Schemas from "./jobServiceSchemas"; + +export type ListDelayedJobsError = Fetcher.ErrorWrapper<{ + status: 401; + payload: { + /** + * @example 401 + */ + statusCode: number; + /** + * @example Unauthorized + */ + message: string; + /** + * @example Unauthorized + */ + error?: string; + }; +}>; + +export type ListDelayedJobsResponse = { + data?: { + /** + * @example delayedJobs + */ + type?: string; + /** + * @format uuid + */ + id?: string; + attributes?: Schemas.DelayedJobDto; + }; +}; + +/** + * Retrieve a list of all delayed jobs. + */ +export const listDelayedJobs = (signal?: AbortSignal) => + jobServiceFetch({ + url: "/jobs/v3/delayedJobs", + method: "get", + signal + }); + +export type DelayedJobsFindPathParams = { + uuid: string; +}; + +export type DelayedJobsFindError = Fetcher.ErrorWrapper< + | { + status: 401; + payload: { + /** + * @example 401 + */ + statusCode: number; + /** + * @example Unauthorized + */ + message: string; + /** + * @example Unauthorized + */ + error?: string; + }; + } + | { + status: 404; + payload: { + /** + * @example 404 + */ + statusCode: number; + /** + * @example Not Found + */ + message: string; + /** + * @example Not Found + */ + error?: string; + }; + } +>; + +export type DelayedJobsFindResponse = { + data?: { + /** + * @example delayedJobs + */ + type?: string; + /** + * @format uuid + */ + id?: string; + attributes?: Schemas.DelayedJobDto; + }; +}; + +export type DelayedJobsFindVariables = { + pathParams: DelayedJobsFindPathParams; +}; + +/** + * Get the current status and potentially payload or error from a delayed job. + */ +export const delayedJobsFind = (variables: DelayedJobsFindVariables, signal?: AbortSignal) => + jobServiceFetch({ + url: "/jobs/v3/delayedJobs/{uuid}", + method: "get", + ...variables, + signal + }); + +export type BulkUpdateJobsError = Fetcher.ErrorWrapper< + | { + status: 400; + payload: { + /** + * @example 400 + */ + statusCode: number; + /** + * @example Bad Request + */ + message: string; + /** + * @example Bad Request + */ + error?: string; + }; + } + | { + status: 401; + payload: { + /** + * @example 401 + */ + statusCode: number; + /** + * @example Unauthorized + */ + message: string; + /** + * @example Unauthorized + */ + error?: string; + }; + } + | { + status: 404; + payload: { + /** + * @example 404 + */ + statusCode: number; + /** + * @example Not Found + */ + message: string; + /** + * @example Not Found + */ + error?: string; + }; + } +>; + +export type BulkUpdateJobsResponse = { + data?: { + /** + * @example delayedJobs + */ + type?: string; + /** + * @format uuid + */ + id?: string; + attributes?: Schemas.DelayedJobDto; + }; +}; + +export type BulkUpdateJobsVariables = { + body: Schemas.DelayedJobBulkUpdateBodyDto; +}; + +/** + * Accepts a JSON:API-compliant payload to bulk update jobs, allowing each job's isAcknowledged attribute to be set to true or false. + */ +export const bulkUpdateJobs = (variables: BulkUpdateJobsVariables, signal?: AbortSignal) => + jobServiceFetch({ + url: "/jobs/v3/delayedJobs/bulk-update", + method: "patch", + ...variables, + signal + }); diff --git a/src/generated/v3/jobService/jobServiceFetcher.ts b/src/generated/v3/jobService/jobServiceFetcher.ts new file mode 100644 index 000000000..97ea202d3 --- /dev/null +++ b/src/generated/v3/jobService/jobServiceFetcher.ts @@ -0,0 +1,6 @@ +// This type is imported in the auto generated `jobServiceComponents` file, so it needs to be +// exported from this file. +export type { ErrorWrapper } from "../utils"; + +// The serviceFetch method is the shared fetch method for all service fetchers. +export { serviceFetch as jobServiceFetch } from "../utils"; diff --git a/src/generated/v3/jobService/jobServicePredicates.ts b/src/generated/v3/jobService/jobServicePredicates.ts new file mode 100644 index 000000000..fdac678cb --- /dev/null +++ b/src/generated/v3/jobService/jobServicePredicates.ts @@ -0,0 +1,26 @@ +import { isFetching, fetchFailed } from "../utils"; +import { ApiDataStore } from "@/store/apiSlice"; +import { DelayedJobsFindPathParams, DelayedJobsFindVariables } from "./jobServiceComponents"; + +export const listDelayedJobsIsFetching = (store: ApiDataStore) => + isFetching<{}, {}>({ store, url: "/jobs/v3/delayedJobs", method: "get" }); + +export const listDelayedJobsFetchFailed = (store: ApiDataStore) => + fetchFailed<{}, {}>({ store, url: "/jobs/v3/delayedJobs", method: "get" }); + +export const delayedJobsFindIsFetching = (variables: DelayedJobsFindVariables) => (store: ApiDataStore) => + isFetching<{}, DelayedJobsFindPathParams>({ store, url: "/jobs/v3/delayedJobs/{uuid}", method: "get", ...variables }); + +export const delayedJobsFindFetchFailed = (variables: DelayedJobsFindVariables) => (store: ApiDataStore) => + fetchFailed<{}, DelayedJobsFindPathParams>({ + store, + url: "/jobs/v3/delayedJobs/{uuid}", + method: "get", + ...variables + }); + +export const bulkUpdateJobsIsFetching = (store: ApiDataStore) => + isFetching<{}, {}>({ store, url: "/jobs/v3/delayedJobs/bulk-update", method: "patch" }); + +export const bulkUpdateJobsFetchFailed = (store: ApiDataStore) => + fetchFailed<{}, {}>({ store, url: "/jobs/v3/delayedJobs/bulk-update", method: "patch" }); diff --git a/src/generated/v3/jobService/jobServiceSchemas.ts b/src/generated/v3/jobService/jobServiceSchemas.ts new file mode 100644 index 000000000..23a8612a7 --- /dev/null +++ b/src/generated/v3/jobService/jobServiceSchemas.ts @@ -0,0 +1,71 @@ +/** + * Generated by @openapi-codegen + * + * @version 1.0 + */ +export type DelayedJobDto = { + /** + * The current status of the job. If the status is not pending, the payload and statusCode will be provided. + */ + status: "pending" | "failed" | "succeeded"; + /** + * If the job is out of pending state, this is the HTTP status code for the completed process + */ + statusCode: number | null; + /** + * If the job is out of pending state, this is the JSON payload for the completed process + */ + payload: Record | null; + /** + * If the job is in progress, this is the total content to process + */ + totalContent: number | null; + /** + * If the job is in progress, this is the total content processed + */ + processedContent: number | null; + /** + * If the job is in progress, this is the progress message + */ + progressMessage: string | null; + /** + * Indicates whether the jobs have been acknowledged (cleared) + */ + isAcknowledged: boolean | null; +}; + +export type DelayedJobAttributes = { + /** + * Value to set for isAcknowledged + * + * @example true + */ + isAcknowledged: boolean; +}; + +export type DelayedJobData = { + /** + * Type of the resource + * + * @example delayedJobs + */ + type: "delayedJobs"; + /** + * UUID of the job + * + * @format uuid + * @example uuid-1 + */ + uuid: string; + /** + * Attributes to update for the job + */ + attributes: DelayedJobAttributes; +}; + +export type DelayedJobBulkUpdateBodyDto = { + /** + * List of jobs to update isAcknowledged + */ + data: DelayedJobData[]; +}; diff --git a/src/pages/entity/[entityName]/edit/[uuid]/EditEntityForm.tsx b/src/pages/entity/[entityName]/edit/[uuid]/EditEntityForm.tsx index 8b7876904..c6832cb0d 100644 --- a/src/pages/entity/[entityName]/edit/[uuid]/EditEntityForm.tsx +++ b/src/pages/entity/[entityName]/edit/[uuid]/EditEntityForm.tsx @@ -64,7 +64,12 @@ const EditEntityForm = ({ entityName, entityUUID, entity, formData }: EditEntity ); const reportingWindow = useReportingWindow(entity?.due_at); - const formTitle = `${formData.form?.title} ${isReport ? reportingWindow : ""}`; + const formTitle = + entityName === "site-reports" + ? t("{siteName} Site Report", { siteName: entity.site.name }) + : `${formData.form?.title} ${isReport ? reportingWindow : ""}`; + const formSubtitle = + entityName === "site-reports" ? t("Reporting Period: {reportingWindow}", { reportingWindow }) : undefined; const saveAndCloseModalMapping: any = { projects: t( @@ -117,6 +122,7 @@ const EditEntityForm = ({ entityName, entityUUID, entity, formData }: EditEntity submitButtonDisable={isSubmitting} defaultValues={defaultValues} title={formTitle} + subtitle={formSubtitle} tabOptions={{ markDone: true, disableFutureTabs: true diff --git a/src/pages/reports/site-report/[uuid].page.tsx b/src/pages/reports/site-report/[uuid].page.tsx index 8cb7ad215..54133fdf5 100644 --- a/src/pages/reports/site-report/[uuid].page.tsx +++ b/src/pages/reports/site-report/[uuid].page.tsx @@ -139,6 +139,16 @@ const SiteReportDetailPage = () => { {siteReport.public_narrative} + {siteReport.pct_survival_to_date} + + {siteReport.survival_calculation} + + + {siteReport.survival_description} + + + {siteReport.maintenance_activities} + { uuid={siteReportUUID} entity={"site-report" as EstablishmentEntityType} total={siteReport.total_non_tree_species_planted_count} - totalText={t("TOTAL SEEDS PLANTED (ON REPORT)")} + totalText={t("TOTAL NON-TREES SPECIES PLANTED (ON REPORT)")} title={t("Non-Trees Planted")} countColumnName={t("NON-TREE COUNT")} collection="non-tree" /> - - - {t("Assisted Natural Regeneration")} - - +
@@ -175,6 +189,11 @@ const SiteReportDetailPage = () => {
+ + + {t("Assisted Natural Regeneration")} + + {t("DESCRIPTION OF ANR ACTIVITIES")}: @@ -192,7 +211,7 @@ const SiteReportDetailPage = () => { /> - + = Record; establishmentTrees: StoreResourceMap; logins: StoreResourceMap; organisations: StoreResourceMap; diff --git a/src/styles/extended-utilities.css b/src/styles/extended-utilities.css index a1ff1c221..13ee9b42f 100644 --- a/src/styles/extended-utilities.css +++ b/src/styles/extended-utilities.css @@ -674,6 +674,7 @@ -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; + text-wrap: nowrap; } .two-line-text{