Skip to content

Commit

Permalink
Merge pull request #1447 from Vizzuality/MRXN23-284-inventory-panel-l…
Browse files Browse the repository at this point in the history
…ist-v2-new

[MRXN23-284]: Unify lists for features / protected areas / cost surface
  • Loading branch information
agnlez authored Aug 24, 2023
2 parents c292881 + b0c97a5 commit 20a4cba
Show file tree
Hide file tree
Showing 31 changed files with 667 additions and 1,166 deletions.
2 changes: 2 additions & 0 deletions app/layout/project/navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export type NavigationTreeCategories =
| 'gridSetup'
| 'solutions'
| 'advancedSettings';

export type NavigationInventoryTabs = 'protected-areas' | 'cost-surface' | 'features';
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useCallback } from 'react';

import { ArrowDown, ArrowUp } from 'lucide-react';

import { cn } from 'utils/cn';

import { HeaderItem } from './types';

const HeaderItem = ({
className,
text,
name,
columns,
sorting,
onClick,
}: HeaderItem): JSX.Element => {
const sortingMatches = /^(-?)(.+)$/.exec(sorting);
const sortField = sortingMatches[2];
const sortOrder = sortingMatches[1] === '-' ? 'desc' : 'asc';

const isActive = columns[name] === sortField;

const handleClick = useCallback(() => {
onClick(columns[name]);
}, [onClick, columns, name]);

return (
<button
type="button"
className={cn({
'inline-flex items-center space-x-2': true,
[className]: !!className,
})}
onClick={handleClick}
>
<span
className={cn({
'text-xs font-semibold uppercase leading-none text-gray-400': true,
'text-white': isActive,
[className]: !!className,
})}
>
{text}
</span>
{sortOrder === 'asc' && isActive ? (
<ArrowDown className={isActive ? 'text-blue-400' : 'text-gray-400'} size={20} />
) : (
<ArrowUp className={isActive ? 'text-blue-400' : 'text-gray-400'} size={20} />
)}
</button>
);
};

export default HeaderItem;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type HeaderItem = {
className?: string;
text: string;
name: string;
columns: {
[key: string]: string;
};
sorting: string;
onClick?: (field: string) => void;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import Checkbox from 'components/forms/checkbox';
import Loading from 'components/loading';

import HeaderItem from './header-item';
import RowItem from './row-item';
import { InventoryTable } from './types';

const InventoryTable = ({
loading,
data,
noDataMessage,
columns,
sorting,
selectedIds,
onSortChange,
onToggleSeeOnMap,
onSelectRow,
onSelectAll,
ActionsComponent,
}: InventoryTable): JSX.Element => {
const noData = !loading && data?.length === 0;

return (
<>
{loading && !data.length && (
<div className="relative min-h-[200px]">
<Loading
visible={true}
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"
/>
</div>
)}
{noData && <div className="flex h-[200px] items-center justify-center">{noDataMessage}</div>}
{!!data?.length && (
<table className="w-full table-auto space-y-2">
<thead className="text-left text-xs font-semibold uppercase">
<tr className="flex w-full items-center pl-1">
<th>
<Checkbox
id="select-all"
theme="light"
className="block h-4 w-4 checked:bg-blue-400"
onChange={onSelectAll}
/>
</th>
<th className="flex-1 pl-2">
<HeaderItem
text={'Name'}
name={'name'}
columns={columns}
sorting={sorting}
onClick={onSortChange}
/>
</th>
<th className="flex flex-1 justify-start py-2 pl-14">
<HeaderItem
className="justify-center"
text={'Type'}
name={'tag'}
columns={columns}
sorting={sorting}
onClick={onSortChange}
/>
</th>
</tr>
</thead>
<tbody className="block max-h-[calc(100vh-430px)] divide-y divide-gray-400 overflow-y-auto overflow-x-hidden pl-1 align-baseline text-sm">
{data.map((item) => (
<RowItem
key={item.id}
item={item}
selectedIds={selectedIds}
onSelectRow={onSelectRow}
onToggleSeeOnMap={onToggleSeeOnMap}
ActionsComponent={ActionsComponent}
/>
))}
</tbody>
</table>
)}
</>
);
};

export { type DataItem } from './types';

export default InventoryTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { MoreHorizontal } from 'lucide-react';

import Checkbox from 'components/forms/checkbox';
import Icon from 'components/icon';
import { Popover, PopoverContent, PopoverTrigger } from 'components/popover';
import { cn } from 'utils/cn';

import HIDE_SVG from 'svgs/ui/hide.svg?sprite';
import SHOW_SVG from 'svgs/ui/show.svg?sprite';

import { RowItem } from './types';

const RowItem = ({
item,
selectedIds,
onSelectRow,
onToggleSeeOnMap,
ActionsComponent,
}: RowItem) => {
const { id, name, scenarios, tag, isVisibleOnMap } = item;

return (
<tr key={id} className="align-top">
<td className="pb-2 pr-1 pt-5">
<Checkbox
id={`select-${id}`}
theme="light"
className="block h-4 w-4 checked:bg-blue-400"
onChange={onSelectRow}
value={id}
checked={selectedIds.includes(id)}
/>
</td>
<td className="px-1 pb-2 pt-5">
<span className="inline-flex">{name}</span>
<div className="mt-1.5 text-xs text-gray-300">
Currently in use in
<span className="rounded bg-blue-500 bg-opacity-10 px-1 text-blue-500">
{scenarios}
</span>{' '}
scenarios.
</div>
</td>
<td className="px-6 pb-2 pt-5 text-xs">
{tag && (
<div className="flex justify-center">
<span className="whitespace-nowrap rounded-full bg-yellow-600 bg-opacity-10 px-2 py-1 text-yellow-600">
{tag}
</span>
</div>
)}
</td>
<td className="pb-2 pl-1 pr-2 pt-5">
<div className="flex gap-6">
<button type="button" onClick={() => onToggleSeeOnMap(id)}>
<Icon
className={cn({
'h-5 w-5 text-gray-400': true,
'text-blue-400': isVisibleOnMap,
})}
icon={isVisibleOnMap ? SHOW_SVG : HIDE_SVG}
/>
</button>
<Popover>
<PopoverTrigger asChild>
<button type="button" className="h-5 w-5">
<MoreHorizontal className="h-4 w-4 text-white" />
</button>
</PopoverTrigger>
<PopoverContent
className="w-auto rounded-2xl border-transparent p-0"
side="bottom"
sideOffset={5}
align="start"
>
<ActionsComponent item={item} />
</PopoverContent>
</Popover>
</div>
</td>
</tr>
);
};

export default RowItem;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ChangeEvent } from 'react';

import { DataItem } from '../types';

export type RowItem = {
item: DataItem;
selectedIds: string[];
onSelectRow: (evt: ChangeEvent<HTMLInputElement>) => void;
onToggleSeeOnMap: (id: string) => void;
ActionsComponent: ({ item }) => JSX.Element;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ChangeEvent } from 'react';

export type DataItem = {
id: string;
name: string;
scenarios: number;
tag: string;
isVisibleOnMap: boolean;
};

export type InventoryTable = {
loading: boolean;
data: DataItem[];
noDataMessage: string;
columns: {
[key: string]: string;
};
sorting: string;
selectedIds: string[];
onSortChange: (field: string) => void;
onToggleSeeOnMap: (id: string) => void;
onSelectRow: (evt: ChangeEvent<HTMLInputElement>) => void;
onSelectAll: (evt: ChangeEvent<HTMLInputElement>) => void;
ActionsComponent: ({ item }) => JSX.Element;
};
37 changes: 37 additions & 0 deletions app/layout/project/sidebar/project/inventory-panel/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NavigationInventoryTabs } from 'layout/project/navigation/types';

import CostSurfaceTable from './cost-surface';
import CostSurfaceInfo from './cost-surface/info';
import FeaturesTable from './features';
import FeaturesInfo from './features/info';
import FeatureUploadModal from './features/modals/upload';
import ProtectedAreasTable from './protected-areas';
import ProtectedAreasFooter from './protected-areas/footer';
import { InventoryPanel } from './types';

export const INVENTORY_TABS = {
'protected-areas': {
title: 'Protected Areas',
search: 'Search protected areas',
noData: 'No protected areas found.',
TableComponent: ProtectedAreasTable,
FooterComponent: ProtectedAreasFooter,
},
'cost-surface': {
title: 'Cost Surface',
search: 'Search cost surfaces',
noData: 'No cost surfaces found.',
InfoComponent: CostSurfaceInfo,
TableComponent: CostSurfaceTable,
},
features: {
title: 'Features',
search: 'Search features',
noData: 'No features found.',
InfoComponent: FeaturesInfo,
UploadModalComponent: FeatureUploadModal,
TableComponent: FeaturesTable,
},
} satisfies {
[key in NavigationInventoryTabs]: InventoryPanel;
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import Section from 'layout/section';

const InventoryPanelCostSurface = (): JSX.Element => {
return <Section>InventoryPanelCostSurface</Section>;
const InventoryPanelCostSurface = ({ noData: noDataMessage }: { noData: string }): JSX.Element => {
return <div className="flex h-[200px] items-center justify-center">{noDataMessage}</div>;
};

export default InventoryPanelCostSurface;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import COST_LAND_IMG from 'images/info-buttons/img_cost_surface_marine.png';
import COST_SEA_IMG from 'images/info-buttons/img_cost_surface_terrestrial.png';

const CostSurfaceInfo = (): JSX.Element => {
return (
<>
<h4 className="mb-2.5 font-heading text-lg">What is a Cost Surface?</h4>
<div className="space-y-2">
<p>
Marxan aims to minimize socio-economic impacts and conflicts between uses through what is
called the “cost” surface. In conservation planning, cost data may reflect acquisition,
management, or opportunity costs ($), but may also reflect non-monetary impacts. Proxies
are commonly used in absence of fine-scale socio-economic information. A default value for
cost will be the planning unit area but you can upload your cost surface.
</p>
<p>
In the examples below, we illustrate how distance from a city, road or port can be used as
a proxy cost surface. In these examples, areas with many competing activities will make a
planning unit cost more than areas further away with less competition for access.
</p>
<img src={COST_SEA_IMG} alt="Cost sea" />
<img src={COST_LAND_IMG} alt="Cost Land" />
</div>
</>
);
};

export default CostSurfaceInfo;
Loading

0 comments on commit 20a4cba

Please sign in to comment.