Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

task/WG-74: Create Map React Modal #193

Merged
merged 21 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13,819 changes: 5,137 additions & 8,682 deletions react/package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@testing-library/react": "^13.4.0",
"@types/leaflet.markercluster": "^1.5.1",
"axios": "^1.6.2",
"bootstrap": "^5.3.2",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can bootstrap package be removed now that we are using the cdn?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it should still work with just the cdn. Let me test that though

"eslint-config-prettier": "^8.5.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-prettier": "^4.2.1",
Expand All @@ -45,7 +46,8 @@
"react-router-dom": "^6.3.0",
"react-table": "^7.8.0",
"reactstrap": "^9.2.1",
"uuid": "^9.0.1"
"uuid": "^9.0.1",
"yup": "^1.3.3"
},
"devDependencies": {
"@redux-devtools/core": "^3.13.1",
Expand Down
30 changes: 30 additions & 0 deletions react/src/components/MapModal/MapModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { useState } from 'react';
import { Button } from 'reactstrap';
import MapModal from './MapModal';

const ModalTestPage: React.FC = () => {
const [isModalOpen, setIsModalOpen] = useState(false);

const toggleModal = () => setIsModalOpen(!isModalOpen);

// Dummy onSubmit function for testing
const dummyOnSubmit = () => {
// No operation (noop)
};

return (
<div>
<Button color="secondary" onClick={toggleModal}>
Open Map Modal
</Button>
<MapModal
isOpen={isModalOpen}
toggle={toggleModal}
onSubmit={dummyOnSubmit}
isCreating={false}
/>
</div>
);
};

export default ModalTestPage;
156 changes: 156 additions & 0 deletions react/src/components/MapModal/MapModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import React from 'react';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: what about making the name more specific? something likeMapCreateModal instead of MapModal? 🤷‍♂️

or NewMapModal? or CreateMapModal

import {
Button,
Modal,
ModalHeader,
ModalBody,
ModalFooter,
FormGroup,
Label,
Input,
} from 'reactstrap';
import { Formik, Form, Field, ErrorMessage } from 'formik';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider using the form-related components in core-components. Originally from CEP (but probably improved via TUP) the `react/src/core-components/Form has all the labels and required label etc for the formik fields. what about we create a follow-on ticket for using components from core-components/Form in this modal?

import * as Yup from 'yup';

type MapModalProps = {
isOpen: boolean;
toggle: () => void;
onSubmit: (values: any) => void;
isCreating: boolean;
};

// Yup validation schema
const validationSchema = Yup.object({
name: Yup.string().required('Name is required'),
description: Yup.string().required('Description is required'),
system_file: Yup.string()
.matches(
/^[A-Za-z0-9-_]+$/,
'Only letters, numbers, hyphens, and underscores are allowed'
)
.required('Custom file name is required'),
});

const MapModal: React.FC<MapModalProps> = ({
isOpen,
toggle,
onSubmit,
isCreating,
}) => {
return (
<Modal isOpen={isOpen} toggle={toggle}>
<ModalHeader toggle={toggle}>Create a New Map</ModalHeader>
<ModalBody>
<Formik
initialValues={{
name: '',
description: '',
system_file: '',
syncFolder: false,
}}
validationSchema={validationSchema}
onSubmit={(values, actions) => {
console.log('Form data', values);
onSubmit(values);
actions.setSubmitting(false);
}}
>
{({ errors, touched, values, handleChange }) => (
<Form>
<FormGroup>
<Label for="name">Name</Label>
<Field
name="name"
as={Input}
invalid={touched.name && !!errors.name}
/>
<ErrorMessage
name="name"
component="div"
className="invalid-feedback"
/>
</FormGroup>
<FormGroup>
<Label for="description">Description</Label>
<Field
name="description"
as={Input}
invalid={touched.description && !!errors.description}
/>
<ErrorMessage
name="description"
component="div"
className="invalid-feedback"
/>
</FormGroup>
<FormGroup>
<Label for="system_file">Custom File Name</Label>
<div className="input-group">
<Field
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we match the behavior on the current app where the user inputs the map name and the file initially matches the map name? Lets add a follow on Jira issue

name="system_file"
as={Input}
className="col-sm-8"
invalid={touched.system_file && !!errors.system_file}
/>
<span className="input-group-text col-sm-4">.hazmapper</span>
</div>
{/* Alt solution to render error message bc input group was causing text to not display properly */}
{touched.system_file && errors.system_file && (
<div
className="custom-error-message"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is custom-error-message a class somewhere? or is that the desired class to contain the margin-top, font-size and color?

We should put some custom style in a MapModul.module.css file like:

.foo-bar {
    margin-top: '0.25rem';
    font-size: '0.80em';
    color: '#dc3545';
}

Then import styles from './MapModal.module.css' and use that class here:

               className={`custom-error-message ${styles['foo-bar']}`}

style={{
marginTop: '0.25rem',
fontSize: '0.875em',
color: 'var(--bs-danger)',
}}
>
{errors.system_file}
</div>
)}
</FormGroup>
<FormGroup className="row align-items-center">
<Label className="col-sm-4">Save Location:</Label>
<div className="col-sm-8 text-primary">/tgrafft</div>
</FormGroup>
<FormGroup className="row mb-2">
<Label className="col-sm-4 col-form-label pt-0">
Sync Folder:
</Label>
<div className="col-sm-8">
<div className="form-check">
<Field
type="checkbox"
name="syncFolder"
id="syncFolder"
className="form-check-input"
checked={values.syncFolder}
onChange={handleChange}
/>
</div>
<Label
className="form-check-label"
for="syncFolder"
style={{ fontStyle: 'italic' }}
>
When enabled, files in this folder are automatically synced
into the map periodically.
</Label>
</div>
</FormGroup>
<ModalFooter className="justify-content-start">
<Button color="warning" type="button" onClick={toggle}>
Close
</Button>
<Button color="primary" type="submit" disabled={isCreating}>
{isCreating ? 'Creating...' : 'Create'}
</Button>
</ModalFooter>
</Form>
)}
</Formik>
</ModalBody>
</Modal>
);
};

export default MapModal;
1 change: 1 addition & 0 deletions react/src/components/MapModal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './MapModal';
33 changes: 33 additions & 0 deletions react/src/hooks/projects/useCreateProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { usePost } from '../../requests';
import { Project } from '../../types';

type ProjectData = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about renaming to CreateProjectData? we can always refactor again when we complete TACC-Cloud/geoapi#173

Should this be moved into src/types/projects.ts

// TO-DO: Make sure project data structure is correct for API
name: string;
description: string;
system_file: string;
};

// type ProjectData = {
// id: number;
// name: string;
// description: string;
// public: boolean;
// uuid: string;
// system_file: string;
// system_id: string;
// system_path: string;
// deletable: boolean;
// };

const useCreateProject = () => {
const baseUrl = 'https://agave.designsafe-ci.org/geo/v2';
const endpoint = '/projects/';

return usePost<ProjectData, Project>({
endpoint,
baseUrl,
});
};

export default useCreateProject;
39 changes: 38 additions & 1 deletion react/src/pages/MainMenu/MainMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
import React from 'react';
import React, { useState } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import {
LoadingSpinner,
InlineMessage,
SectionHeader,
} from '../../core-components';
import { useProjects } from '../../hooks';
import MapModal from '../../components/MapModal';
import { Button } from 'reactstrap';
import useCreateProject from '../../hooks/projects/useCreateProject';

function MainMenu() {
const { data, isLoading, error } = useProjects();
const { mutate: createProject, isLoading: isCreatingProject } =
useCreateProject();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could line 14 and the handleCreateProject (20-33) be moved into the MapModal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

const [isModalOpen, setIsModalOpen] = useState(false);

const toggleModal = () => setIsModalOpen(!isModalOpen);

const handleCreateProject = (projectData) => {
createProject(projectData, {
onSuccess: () => {
// TO-ADD: Handle success (e.g., show success message, refetch projects, etc.)
console.log('Data after submit', projectData);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we want to navigate to the new map as now we know the new uuid:

import { useNavigate } from 'react-router-dom';
import { Project } from '../../types';

....

      onSuccess: (newProject: Project) => {
        navigate(`/project/${newProject.uuid}`);
      },

this /project/uuid route is already supported in react/src/AppRouter.tsx and bring us to a MapProject page (with fixtures for map data)

toggleModal();
},
onError: (err) => {
// Handle error (e.g., show error message)
console.error('Error creating project:', err);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to not let the modal close and then show an error in the modal for users to see.

we should match the current behavior where we show the error message or the text " That folder is already syncing with a different map!"

},
});
};

if (isLoading) {
return (
<>
Expand All @@ -27,6 +51,19 @@ function MainMenu() {
return (
<>
<SectionHeader isNestedHeader>Main Menu</SectionHeader>
<Button
color="primary"
onClick={toggleModal}
disabled={isCreatingProject}
>
Create Map
</Button>
<MapModal
isOpen={isModalOpen}
toggle={toggleModal}
onSubmit={handleCreateProject}
isCreating={isCreatingProject}
/>

<table>
<thead>Projects</thead>
Expand Down
37 changes: 36 additions & 1 deletion react/src/requests.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import axios from 'axios';
import store from './redux/store';
import { AxiosError } from 'axios';
import { useQuery, UseQueryOptions, QueryKey } from 'react-query';
import {
useQuery,
useMutation,
UseQueryOptions,
UseMutationOptions,
QueryKey,
} from 'react-query';

type UseGetParams<ResponseType> = {
endpoint: string;
Expand All @@ -13,6 +19,12 @@ type UseGetParams<ResponseType> = {
baseUrl: string;
};

type UsePostParams<RequestType, ResponseType> = {
endpoint: string;
options?: UseMutationOptions<ResponseType, AxiosError, RequestType>;
baseUrl: string;
};

export function useGet<ResponseType>({
endpoint,
key,
Expand All @@ -35,3 +47,26 @@ export function useGet<ResponseType>({
};
return useQuery<ResponseType, AxiosError>(key, () => getUtil(), options);
}

export function usePost<RequestType, ResponseType>({
endpoint,
options = {},
baseUrl,
}: UsePostParams<RequestType, ResponseType>) {
const client = axios;
const state = store.getState();
const token = state.auth.token?.token;

const postUtil = async (requestData: RequestType) => {
const response = await client.post<ResponseType>(
`${baseUrl}${endpoint}`,
requestData,
{
headers: { Authorization: `Bearer ${token}` },
}
);
return response.data;
};

return useMutation<ResponseType, AxiosError, RequestType>(postUtil, options);
}
Loading