Skip to content

Commit

Permalink
- Changes useCurrentFeature hook so that it picks
Browse files Browse the repository at this point in the history
up the latest query accurately
- Creates asset detail component with styles
- Creates asset detail placeholders for questionnaire and pointcloud
- Need to add meaningful test
  • Loading branch information
sophia-massie committed Dec 12, 2024
1 parent 8c6c0da commit a14fb68
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 15 deletions.
74 changes: 74 additions & 0 deletions react/src/components/AssetDetail/AssetDetail.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
.root {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
}

.topSection {
flex: 0 0 auto;
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: large;
}

.middleSection {
flex: 1 1 auto;
overflow: hidden;
min-height: 0;
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
}
.middleSection > div {
flex: 1;
overflow: auto;
}
.bottomSection {
display: block;
flex: 0 0 auto;
overflow-x: hidden;
justify-items: flex-end;
align-items: flex-end;
width: 100%;
}
.metadataTable {
flex-grow: 1;
width: 100%;
}
.metadataTable table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
margin: 5px;
padding: 5px;
}
.metadataTable thead,
th {
background: #d0d0d0;
}
.metadataTable tbody {
display: block;
max-height: 300px;
overflow-y: auto;
scroll-padding: 5px;
}
.metadataTable tr {
display: table;
width: 100%;
table-layout: fixed;
text-overflow: ellipsis;
overflow: hidden;
white-space: wrap;
max-height: 30px;
}
.metadataTable td {
word-wrap: break-word;
word-break: break-word;
white-space: normal;
text-overflow: clip;
}
10 changes: 10 additions & 0 deletions react/src/components/AssetDetail/AssetDetail.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React, { act } from 'react';

Check failure on line 1 in react/src/components/AssetDetail/AssetDetail.test.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

'React' is defined but never used

Check failure on line 1 in react/src/components/AssetDetail/AssetDetail.test.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

'act' is defined but never used
import { render, screen, fireEvent } from '@testing-library/react';

Check failure on line 2 in react/src/components/AssetDetail/AssetDetail.test.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

'render' is defined but never used

Check failure on line 2 in react/src/components/AssetDetail/AssetDetail.test.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

'screen' is defined but never used

Check failure on line 2 in react/src/components/AssetDetail/AssetDetail.test.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

'fireEvent' is defined but never used
import AssetDetail from './AssetDetail';

Check failure on line 3 in react/src/components/AssetDetail/AssetDetail.test.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

'AssetDetail' is defined but never used
import { featureCollection } from '@hazmapper/__fixtures__/featuresFixture';

Check failure on line 4 in react/src/components/AssetDetail/AssetDetail.test.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

'featureCollection' is defined but never used
import { projectMock } from '@hazmapper/__fixtures__/projectFixtures';

Check failure on line 5 in react/src/components/AssetDetail/AssetDetail.test.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

'projectMock' is defined but never used
import { useFeatures } from '@hazmapper/hooks';

Check failure on line 6 in react/src/components/AssetDetail/AssetDetail.test.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

'useFeatures' is defined but never used

jest.mock('@hazmapper/hooks', () => ({
useFeatures: jest.fn(),
}));
172 changes: 172 additions & 0 deletions react/src/components/AssetDetail/AssetDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import React, { Suspense, useState, useEffect } from 'react';
import _ from 'lodash';
import { useAppConfiguration, useFeatureSelection } from '@hazmapper/hooks';
import { useLocation, useNavigate } from 'react-router-dom';
import { Asset, FeatureTypeNullable } from '@hazmapper/types';
import { FeatureIcon } from '@hazmapper/components/FeatureIcon';
import { Button, LoadingSpinner, SectionMessage } from '@tacc/core-components';
import styles from './AssetDetail.module.css';
import { faCameraRetro, faPhotoFilm } from '@fortawesome/free-solid-svg-icons';

Check failure on line 9 in react/src/components/AssetDetail/AssetDetail.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

'faCameraRetro' is defined but never used
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

type AssetModalProps = {
isOpen: boolean;
projectId: number;
isPublicView: boolean;
};

const AssetDetail: React.FC<AssetModalProps> = ({
isOpen,
projectId,
isPublicView,
}) => {
const { selectedFeature, selectedFeatureId, setSelectedFeatureId } =
useFeatureSelection();

const navigate = useNavigate();
const location = useLocation();

const closePanel = () => {
const params = new URLSearchParams(location.search);
params.delete('selectedFeature');
navigate(`${location.pathname}?${params.toString()}`);
};

const config = useAppConfiguration();
const geoapiUrl = config.geoapiUrl;

const [selectedFeatureAsset, setSelectedFeatureAsset] = useState<
Asset | undefined
>(selectedFeature?.assets[0]);

useEffect(() => {
const featureAsset = selectedFeature?.assets[0];
setSelectedFeatureAsset(featureAsset);
}, [selectedFeature]);

const featureSource: string | undefined =
geoapiUrl + '/assets/' + selectedFeatureAsset?.path;

const fileType: string | undefined = selectedFeatureAsset?.asset_type;

const AssetRenderer = React.memo(
({
type,
source,
}: {
type: string | undefined;
source: string | undefined;
}) => {
switch (type) {
case 'image':
return <img src={source} alt="Asset" loading="lazy" width="100%" />;
case 'video':
return (
<video src={source} controls preload="metadata" height="90%" />
);
case 'point_cloud':
/*TODO Add pointcloud */
return <div> source={source}</div>;
case 'questionnaire':
/*TODO Add questionnaire */
return <div> source={source}</div>;
default:
return null;
}
}
);

return (
<div className={styles.root}>
<div className={styles.topSection}>
<FeatureIcon featureType={fileType as FeatureTypeNullable} />
{selectedFeature?.id}
<Button type="link" iconNameAfter="close" onClick={closePanel}></Button>
</div>
<div className={styles.middleSection}>
{fileType ? (
<>
<Suspense fallback={<LoadingSpinner />}>
<AssetRenderer type={fileType} source={featureSource} />
</Suspense>
<Button /*TODO Download Action */>Download</Button>
</>
) : (
<>
<SectionMessage type="info">Feature has no asset.</SectionMessage>

<Button type="primary" /* TODO Add asset to a feature */>
Add asset from DesignSafe
</Button>
</>
)}
</div>
<div className={styles.bottomSection}>
<div className={styles.metadataTable}>
<table>
<thead>
<tr>
<th colSpan={2}>Metadata</th>
</tr>
</thead>
<tbody>
{selectedFeature?.properties &&
Object.keys(selectedFeature.properties).length > 0 ? (
Object.entries(selectedFeature.properties)
.filter(([key]) => !key.startsWith('_hazmapper'))
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB)) // Alphabetizes metadata
.map(([propKey, propValue]) => (
<tr key={propKey}>
<td>{_.startCase(propKey)}</td>
<td>
{propKey.startsWith('description') ? (
<code>{propValue}</code>
) : (
_.trim(JSON.stringify(propValue), '"')
)}
</td>
</tr>
))
) : (
<tr>
<td colSpan={2}>
There are no metadata properties for this feature.
</td>
</tr>
)}
</tbody>
</table>
<table>
<thead>
<tr>
<th colSpan={2}>Geometry</th>
</tr>
</thead>
<tbody>
{selectedFeature?.geometry &&
Object.entries(selectedFeature.geometry)
.map(
([propKey, propValue]) => (
propValue && propValue !== undefined && propValue.toString().trim() !== '' && propValue.toString() !== 'null' && (
<tr key={propKey}>
<td>{_.trim(_.startCase(propKey.toString()), '"')}</td>
<td>
{' '}
{Array.isArray(propValue) && propValue.length === 2
? `Latitude: ${propValue[0].toString()},
Longitude: ${propValue[1].toString()}`
: _.trim(JSON.stringify(propValue), '"')}
</td>
</tr>
)
)
)}
</tbody>
</table>
</div>
</div>
</div>
);
};

export default AssetDetail;
1 change: 1 addition & 0 deletions react/src/components/AssetDetail/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './AssetDetail';
30 changes: 17 additions & 13 deletions react/src/hooks/features/useFeatures.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useQueryClient, UseQueryResult } from 'react-query';
import { useQueryClient, UseQueryResult, QueryKey } from 'react-query';
import { FeatureCollection } from '@hazmapper/types';
import { useGet } from '@hazmapper/requests';

Expand Down Expand Up @@ -40,22 +40,26 @@ export const useFeatures = ({
});
return query;
};

export const useCurrentFeatures = (): UseQueryResult<FeatureCollection> => {
const queryClient = useQueryClient();

// Get all existing queries that match the KEY_USE_FEATURES prefix
const queries = queryClient.getQueriesData<FeatureCollection>([
KEY_USE_FEATURES,
]);

// Find first query with data - getQueriesData returns [queryKey, data] tuples
const activeQuery = queries.find(([, queryData]) => !!queryData);
const currentData = activeQuery ? activeQuery[1] : undefined;
const latestQuery = queryClient
.getQueriesData<FeatureCollection>([KEY_USE_FEATURES])
.filter(([, value]) => Boolean(value))
.reduce<[QueryKey, FeatureCollection] | null>((latest, current) => {
const currentState = queryClient.getQueryState(current[0]);
const latestState = latest ? queryClient.getQueryState(latest[0]) : null;
if (
!latestState ||
(currentState && currentState.dataUpdatedAt > latestState.dataUpdatedAt)
) {
return current;
}
return latest;
}, null);

return {
data: currentData,
isSuccess: !!currentData,
data: latestQuery?.[1],
isSuccess: !!latestQuery?.[1],
isLoading: false,
isError: false,
error: null,
Expand Down
4 changes: 2 additions & 2 deletions react/src/pages/MapProject/MapProject.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@
padding: 5px;
right: 10px;
top: 75px;
bottom: 10px;
z-index: 900;
bottom: 20px;
z-index: 5000;
}

.map {
Expand Down

0 comments on commit a14fb68

Please sign in to comment.