Skip to content

Commit

Permalink
Merge pull request #1637 from Vizzuality/MRXN23-566-user-should-be-ab…
Browse files Browse the repository at this point in the history
…le-to-filter

[MRXN23-566]: allows editing target/spf of custom selection
  • Loading branch information
agnlez authored Feb 6, 2024
2 parents 9cb0e18 + e65a975 commit b2ce411
Show file tree
Hide file tree
Showing 6 changed files with 311 additions and 37 deletions.
5 changes: 0 additions & 5 deletions app/hooks/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,6 @@ export function useSelectedFeatures(
const { data: session } = useSession();
const { search } = filters;

const queryClient = useQueryClient();

// const featureColorQueryState =
// queryClient.getQueryState<{ id: Feature['id']; color: string }[]>('feature-colors');

const featureColors = useColorFeatures(pid, sid);

const fetchFeatures = () =>
Expand Down
21 changes: 5 additions & 16 deletions app/hooks/gap-analysis/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useQuery } from 'react-query';

import { sortBy } from 'lodash';
import { useSession } from 'next-auth/react';

import { ItemProps as RawItemProps } from 'components/gap-analysis/item/component';
Expand Down Expand Up @@ -70,18 +69,9 @@ export function usePreGapAnalysis(sId: Scenario['id'], options: UseFeaturesOptio

return useQuery(['pre-gap-analysis', sId, JSON.stringify(options)], fetchFeatures, {
select: ({ data }) =>
sortBy(
data.map((d): AllItemProps => {
const {
id,
name,
featureClassName,
met,
metArea,
coverageTarget,
coverageTargetArea,
onTarget,
} = d;
data
.map((d): AllItemProps => {
const { id, name, featureClassName, met, metArea, coverageTarget, onTarget } = d;

return {
id,
Expand All @@ -94,9 +84,8 @@ export function usePreGapAnalysis(sId: Scenario['id'], options: UseFeaturesOptio
percent: coverageTarget / 100,
},
};
}),
['name']
),
})
.sort((a, b) => a['name']?.localeCompare(b['name'])),
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import Icon from 'components/icon';
import Modal from 'components/modal/component';
import { Feature } from 'types/api/feature';

import EDIT_SVG from 'svgs/ui/edit.svg?sprite';
import DELETE_SVG from 'svgs/ui/new-layout/delete.svg?sprite';

import DeleteModal from './modals/delete';
import EditModal from './modals/edit';

const BUTTON_CLASSES =
'col-span-1 flex items-center space-x-2 rounded-lg bg-gray-800 px-4 text-xs text-gray-100';
Expand Down Expand Up @@ -40,14 +42,24 @@ const SplitFeaturesBulkActionMenu = ({

return (
<>
<div className="grid w-full grid-cols-2 items-center space-x-2 rounded-xl bg-gray-600 p-1">
<div className="grid w-full grid-cols-3 items-center space-x-2 rounded-xl bg-gray-600 p-1">
<span className="col-span-1 flex items-center justify-center space-x-2">
<span className="block w-[20px] rounded-[4px] bg-blue-500/25 px-1 text-center text-xs font-semibold text-blue-500">
{selectedFeatureIds.length}
</span>
<span className="text-xs text-gray-100">Selected</span>
</span>

<Button
theme="secondary"
size="base"
className={BUTTON_CLASSES}
onClick={() => handleModal('edit', true)}
>
<Icon icon={EDIT_SVG} className={ICON_CLASSES} />
<span>Edit</span>
</Button>

<Button
theme="secondary"
size="base"
Expand All @@ -71,6 +83,24 @@ const SplitFeaturesBulkActionMenu = ({
>
<DeleteModal features={features} onDone={onDone} selectedFeaturesIds={selectedFeatureIds} />
</Modal>

<Modal
id="edit-split-features-modal"
open={modalState.edit}
size="narrow"
dismissable
onDismiss={() => {
handleModal('edit', false);
}}
>
<EditModal
selectedFeatures={features.filter((feature) => selectedFeatureIds.includes(feature.id))}
onDone={onDone}
handleModal={() => {
handleModal('edit', false);
}}
/>
</Modal>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import React, { useCallback, useRef, useMemo } from 'react';

import { Form as FormRFF, Field as FieldRFF, FormProps } from 'react-final-form';

import { useRouter } from 'next/router';

import { useSaveSelectedFeatures, useSelectedFeatures } from 'hooks/features';
import { useToasts } from 'hooks/toast';

import Button from 'components/button';
import Field from 'components/forms/field';
import Label from 'components/forms/label';
import { composeValidators } from 'components/forms/validations';
import { Feature } from 'types/api/feature';

export type FormValues = {
target: number;
spf: number;
};

const EditModal = ({
selectedFeatures,
handleModal,
onDone,
}: {
selectedFeatures: (Feature & { name: string })[];
handleModal: (modalKey: 'split' | 'edit' | 'delete', isVisible: boolean) => void;
onDone?: () => void;
}): JSX.Element => {
const { addToast } = useToasts();
const { query } = useRouter();
const { sid } = query as { pid: string; sid: string };

const formRef = useRef<FormProps<FormValues>['form']>(null);
const selectedFeaturesMutation = useSaveSelectedFeatures({});

const selectedFeaturesQuery = useSelectedFeatures(
sid,
{},
{
keepPreviousData: true,
}
);

const targetedFeatures = useMemo(() => {
let parsedData = [];
const formState = formRef.current?.getState();

if (!formState?.values) return [];

selectedFeaturesQuery.data?.forEach((feature) => {
if (feature.splitFeaturesSelected?.length > 0) {
const splitFeatures = feature.splitFeaturesSelected.map((splitFeature) => ({
...splitFeature,
id: `${feature.id}-${splitFeature.name}`,
parentId: feature.id,
}));

parsedData = [...parsedData, ...splitFeatures];
} else {
parsedData = [
...parsedData,
{
...feature,
},
];
}
});

return parsedData;
}, [selectedFeaturesQuery.data, formRef]);

const onEditSubmit = useCallback(
(values: FormValues) => {
const { target, spf = 1 } = values;

const data = {
status: 'created',
features: selectedFeaturesQuery.data.map((sf) => {
const { featureId, kind, geoprocessingOperations } = sf;

if (kind === 'withGeoprocessing') {
return {
featureId,
kind,
geoprocessingOperations: geoprocessingOperations.map((go) => {
const { splits } = go;

return {
...go,
splits: splits
.filter((s) => {
return targetedFeatures.find((f) => {
return f.parentId === featureId && f.value === s.value;
});
})
.map((s) => {
const {
marxanSettings: { prop, fpf },
} = targetedFeatures.find((f) => {
return f.parentId === featureId && f.value === s.value;
});

return {
...s,
marxanSettings: {
prop: prop / 100,
fpf,
},
};
}),
};
}),
};
}

return {
featureId,
kind,
marxanSettings: selectedFeatures.find((f) => f.id === featureId)
? {
prop: target / 100 || 0.5,
fpf: +spf,
}
: sf.marxanSettings,
};
}),
};

selectedFeaturesMutation.mutate(
{
id: sid,
data,
},
{
onSuccess: () => {
onDone?.();
handleModal('edit', false);

addToast(
'success-edit-features',
<>
<h2 className="font-medium">Success!</h2>
<p className="text-sm">Features edited</p>
</>,
{
level: 'success',
}
);
},
onError: () => {
addToast(
'error-edit-features',
<>
<h2 className="font-medium">Error!</h2>
<p className="text-sm">It is not possible to edit this feature</p>
</>,
{
level: 'error',
}
);
},
}
);
},
[
addToast,
selectedFeaturesQuery.data,
targetedFeatures,
selectedFeatures,
selectedFeaturesMutation,
handleModal,
sid,
onDone,
]
);

return (
<FormRFF<FormValues>
initialValues={{
target: 50,
spf: 1,
}}
ref={formRef}
onSubmit={onEditSubmit}
render={({ form, handleSubmit }) => {
formRef.current = form;

return (
<form onSubmit={handleSubmit} className="relative">
<div className="flex flex-col space-y-5 px-8 py-1">
<h2 className="font-heading font-bold text-black">Edit selected features</h2>

<div className="flex w-full space-x-2">
<FieldRFF<FormValues['target']>
name="target"
validate={composeValidators([{ presence: true }])}
>
{(fprops) => (
<Field id="target" {...fprops} className="flex-1">
<Label theme="light" className="mb-3 text-xs font-semibold uppercase">
Target (%)
</Label>

<input
{...fprops.input}
type="number"
className="h-10 w-full rounded-md border border-gray-400 px-3 text-gray-900 focus:border-none focus:outline-none focus:ring-1 focus:ring-blue-600"
defaultValue={fprops.input.value}
min={0}
max={100}
/>
</Field>
)}
</FieldRFF>

<FieldRFF<FormValues['spf']>
name="spf"
validate={composeValidators([{ presence: true }])}
>
{(fprops) => (
<Field id="spf" {...fprops} className="flex-1">
<Label theme="light" className="mb-3 text-xs font-semibold uppercase">
SPF
</Label>

<input
{...fprops.input}
type="number"
className="h-10 w-full rounded-md border border-gray-400 px-3 text-gray-900 focus:border-none focus:outline-none focus:ring-1 focus:ring-blue-600"
defaultValue={fprops.input.value}
min={0}
max={1}
step="0.01"
/>
</Field>
)}
</FieldRFF>
</div>

<div className="mt-16 flex justify-center space-x-6">
<Button theme="secondary" size="xl" onClick={() => handleModal('edit', false)}>
Cancel
</Button>

<Button theme="primary" size="xl" type="submit">
Save
</Button>
</div>
</div>
</form>
);
}}
/>
);
};

export default EditModal;
Loading

0 comments on commit b2ce411

Please sign in to comment.