Skip to content

Commit b3c93f8

Browse files
committed
add CRUD for user pdfs
new component for organization links new routes for pdf CRUD create PdfForm to work with edit + add create UserSelector, update OrganizationSelector with some improvements tell apollo to use uuid as id for caching
1 parent 283205d commit b3c93f8

11 files changed

+468
-19
lines changed

src/components/OrgLink.js

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react';
2+
import { string } from 'prop-types';
3+
4+
import { Link } from 'react-router-dom';
5+
6+
import { slugify } from '../utils';
7+
8+
const OrgLink = ({ name, uuid }) => (
9+
<Link to={`/organizations/${slugify(name)}/${uuid}`}>
10+
<strong>{name}</strong>
11+
</Link>
12+
);
13+
14+
OrgLink.propTypes = {
15+
name: string.isRequired,
16+
uuid: string.isRequired,
17+
};
18+
19+
export default OrgLink;

src/components/PdfForm.js

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import React, { useState } from 'react';
2+
3+
import { bool, func, number, shape, string } from 'prop-types';
4+
5+
import OrganizationSelector from '../containers/OrganizationSelector';
6+
import UserSelector from '../containers/UserSelector';
7+
8+
const onNumericChange = (currentValue, setter) => (e) => {
9+
const parsed = parseInt(e.target.value, 10);
10+
isNaN(parsed) ? setter(currentValue) : setter(parsed);
11+
};
12+
13+
const PdfForm = ({ onSubmit, defaultValues = {} }) => {
14+
const [organizationNameMatch, setOrganizationNameMatch] = useState(
15+
defaultValues.organization && {
16+
label: defaultValues.organization.name,
17+
value: defaultValues.organization.uuid,
18+
}
19+
);
20+
const [organization, setOrganization] = useState(
21+
defaultValues.organization && defaultValues.organization.uuid
22+
);
23+
const [userNameMatch, setUserNameMatch] = useState(
24+
defaultValues.user && {
25+
label: defaultValues.user.name,
26+
value: defaultValues.user.uuid,
27+
}
28+
);
29+
const [user, setUser] = useState(
30+
defaultValues.user && defaultValues.user.uuid
31+
);
32+
const [year, setYear] = useState(defaultValues.year);
33+
const [url, setUrl] = useState(defaultValues.url);
34+
const [currentPage, setCurrentPage] = useState(defaultValues.currentPage);
35+
const [done, setDone] = useState(defaultValues.done);
36+
37+
const handleSubmit = (e) => {
38+
e.preventDefault();
39+
onSubmit({
40+
organization,
41+
user,
42+
url,
43+
done: !!done,
44+
year,
45+
current_page: currentPage,
46+
});
47+
};
48+
49+
const onYearChange = onNumericChange(year, setYear);
50+
const onCurrentPageChange = onNumericChange(currentPage, setCurrentPage);
51+
52+
return (
53+
<form onSubmit={handleSubmit}>
54+
<div className="form-group">
55+
<label htmlFor="organization">Organization</label>
56+
<OrganizationSelector
57+
value={organizationNameMatch}
58+
setValue={setOrganizationNameMatch}
59+
onOrgSelected={setOrganization}
60+
/>
61+
</div>
62+
63+
<div className="form-group">
64+
<label htmlFor="user">User</label>
65+
<UserSelector
66+
value={userNameMatch}
67+
setValue={setUserNameMatch}
68+
onUserSelected={setUser}
69+
/>
70+
</div>
71+
72+
<div className="form-group">
73+
<label htmlFor="year">Year</label>
74+
<input
75+
value={year}
76+
onChange={onYearChange}
77+
name="year"
78+
type="number"
79+
min={1900}
80+
max={new Date().getUTCFullYear() + 1}
81+
/>
82+
</div>
83+
84+
<div className="form-group">
85+
<label htmlFor="url">Url</label>
86+
<input
87+
value={url}
88+
onChange={(e) => setUrl(e.target.value)}
89+
name="url"
90+
type="url"
91+
/>
92+
</div>
93+
94+
<div className="form-group">
95+
<label htmlFor="currentPage">Current Page</label>
96+
<input
97+
type="number"
98+
min={1}
99+
value={currentPage}
100+
name="currentPage"
101+
onChange={onCurrentPageChange}
102+
/>
103+
</div>
104+
105+
<div className="form-group">
106+
<label htmlFor="done">Done?</label>
107+
<input
108+
type="checkbox"
109+
checked={!!done}
110+
name="done"
111+
onChange={(e) => setDone(e.target.checked)}
112+
/>
113+
</div>
114+
115+
<button type="submit">{defaultValues.uuid ? 'Update' : 'Add'} PDF</button>
116+
</form>
117+
);
118+
};
119+
120+
PdfForm.propTypes = {
121+
onSubmit: func.isRequired,
122+
defaultValues: shape({
123+
organization: shape({
124+
uuid: string,
125+
name: string,
126+
}),
127+
user: shape({
128+
uuid: string,
129+
name: string,
130+
}),
131+
url: string,
132+
done: bool,
133+
year: number,
134+
currentPage: number,
135+
}),
136+
};
137+
138+
PdfForm.defaultProps = {
139+
defaultValues: {
140+
url: '',
141+
done: false,
142+
year: new Date().getUTCFullYear(),
143+
currentPage: 1,
144+
},
145+
};
146+
147+
export default PdfForm;

src/containers/AdminLinks.js

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const AdminLinks = ({ user }) =>
77
user ? (
88
<>
99
<NavbarLink title="Add grant" href="/admin/grants/add" />
10+
<NavbarLink title="Manage PDFs" href="/admin/pdfs" />
1011
<NavbarLink title="Account" href="/admin" />
1112
</>
1213
) : (

src/containers/EditPdf.js

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React from 'react';
2+
import { string } from 'prop-types';
3+
4+
import { useParams } from 'react-router-dom';
5+
6+
import { gql, useMutation, useQuery } from '@apollo/client';
7+
8+
import PdfForm from '../components/PdfForm';
9+
10+
const GET_PDF = gql`
11+
query getPdf($uuid: String) {
12+
pdf(uuid: $uuid) {
13+
organization {
14+
name
15+
uuid
16+
}
17+
user {
18+
uuid
19+
name
20+
}
21+
url
22+
uuid
23+
year
24+
done
25+
}
26+
}
27+
`;
28+
29+
const UPDATE_PDF = gql`
30+
mutation updatePdf($input: PdfInput!) {
31+
upsertPdf(input: $input) {
32+
uuid
33+
}
34+
}
35+
`;
36+
37+
const EditPdf = () => {
38+
const { uuid } = useParams();
39+
40+
const { loading, error, data } = useQuery(GET_PDF, {
41+
variables: { uuid },
42+
});
43+
44+
const [
45+
updatePdf,
46+
{ loading: updatePdfLoading, error: updatePdfError, data: updatePdfData },
47+
] = useMutation(UPDATE_PDF);
48+
49+
if (loading) return <>Loading...</>;
50+
51+
if (error) throw new Error('todo catch these?');
52+
53+
const updateError = updatePdfError && <p>{updatePdfError.message}</p>;
54+
const updated = updatePdfData && <p>Updated PDF</p>;
55+
56+
const onSubmit = (input) => {
57+
updatePdf({
58+
variables: {
59+
input: { ...input, uuid },
60+
},
61+
});
62+
};
63+
64+
return (
65+
<>
66+
<PdfForm defaultValues={data.pdf} onSubmit={onSubmit} />
67+
{updated}
68+
{updatePdfLoading && 'Loading...'}
69+
{updateError}
70+
</>
71+
);
72+
};
73+
74+
EditPdf.propTypes = { uuid: string.isRequired };
75+
76+
export default EditPdf;

src/containers/ListPdfs.js

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React from 'react';
2+
import { gql, useQuery } from '@apollo/client';
3+
4+
import { Link } from 'react-router-dom';
5+
6+
import OrgLink from '../components/OrgLink';
7+
8+
const LIST_PDFS_QUERY = gql`
9+
query pdfs {
10+
pdfs(limit: 9999, limitToCurrentUser: false) {
11+
organization {
12+
name
13+
uuid
14+
}
15+
user {
16+
uuid
17+
name
18+
}
19+
url
20+
uuid
21+
year
22+
done
23+
}
24+
}
25+
`;
26+
27+
const ListPdfs = () => {
28+
const { loading, error, data } = useQuery(LIST_PDFS_QUERY);
29+
30+
if (loading) return <>Loading...</>;
31+
32+
if (error) throw new Error('todo catch these?');
33+
34+
return (
35+
<table>
36+
<thead>
37+
<tr>
38+
<th>User</th>
39+
<th>Organization</th>
40+
<th>PDF URL</th>
41+
<th>Year</th>
42+
<th>Done?</th>
43+
<th></th>
44+
</tr>
45+
</thead>
46+
<tbody>
47+
{data.pdfs.map(({ url, uuid, year, done, organization, user }) => (
48+
<tr key={uuid}>
49+
<td>{user ? user.name : '-'}</td>
50+
<td>
51+
<OrgLink name={organization.name} uuid={organization.uuid} />
52+
</td>
53+
<td>
54+
<a href={url}>{url}</a>
55+
</td>
56+
<td>{year}</td>
57+
<td>{done ? '✔️' : ''}</td>
58+
<td>
59+
<Link to={`/admin/pdfs/${uuid}`}>edit</Link>
60+
</td>
61+
</tr>
62+
))}
63+
</tbody>
64+
</table>
65+
);
66+
};
67+
68+
export default ListPdfs;

src/containers/ManagePdfs.js

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React from 'react';
2+
3+
import { gql, useMutation } from '@apollo/client';
4+
5+
import PdfForm from '../components/PdfForm';
6+
7+
import ListPdfs from './ListPdfs';
8+
9+
const ADD_PDF = gql`
10+
mutation addPdf($input: PdfInput!) {
11+
upsertPdf(input: $input) {
12+
uuid
13+
}
14+
}
15+
`;
16+
17+
const ADD_PDF_OPTS = {
18+
update(cache, { data: { upsertPdf } }) {
19+
cache.modify({
20+
fields: {
21+
pdfs(existingPdfs = []) {
22+
const newPdfRef = cache.writeFragment({
23+
data: upsertPdf,
24+
fragment: gql`
25+
fragment NewPdf on Pdf {
26+
id
27+
type
28+
}
29+
`,
30+
});
31+
return [...existingPdfs, newPdfRef];
32+
},
33+
},
34+
});
35+
},
36+
};
37+
38+
const ManagePdfs = () => {
39+
const [
40+
addPdf,
41+
{ loading: addPdfLoading, error: addPdfError, data: addPdfData },
42+
] = useMutation(ADD_PDF, ADD_PDF_OPTS);
43+
44+
if (addPdfError) {
45+
console.log(
46+
'xxx',
47+
addPdfError.message,
48+
addPdfError.graphQLErrors,
49+
addPdfError.extraInfo,
50+
addPdfError.networkError,
51+
addPdfError.errors,
52+
addPdfData
53+
);
54+
}
55+
const error = addPdfError && <p>{addPdfError.message}</p>;
56+
const created = addPdfData && <p>Added PDF {addPdfData.upsertPdf.uuid}</p>;
57+
58+
const onSubmit = (input) => {
59+
addPdf({
60+
variables: {
61+
input,
62+
},
63+
});
64+
};
65+
66+
return (
67+
<div className="managePdfs">
68+
<ListPdfs />
69+
<PdfForm onSubmit={onSubmit} />
70+
{created}
71+
{addPdfLoading && 'Loading...'}
72+
{error}
73+
</div>
74+
);
75+
};
76+
77+
export default ManagePdfs;

0 commit comments

Comments
 (0)