Skip to content

Commit fe9cbef

Browse files
committed
feat(project): Added project listing and edit add form
1 parent 37acde2 commit fe9cbef

File tree

6 files changed

+445
-2
lines changed

6 files changed

+445
-2
lines changed

app/components/FileUpload/styles.module.css

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@
66
@media (max-width: 768px) {
77
flex-direction: column;
88
}
9-
}
9+
p {
10+
word-break: break-all;
11+
}
12+
}

app/root/config/routes.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,27 @@ const addVacancy: RouteConfig = {
202202
visibility: 'is-authenticated',
203203
};
204204

205+
const project: RouteConfig = {
206+
index: true,
207+
path: 'projects',
208+
load: () => import('#views/Project/ProjectList'),
209+
visibility: 'is-authenticated',
210+
};
211+
212+
const editProject: RouteConfig = {
213+
index: true,
214+
path: 'projects/:id/edit',
215+
load: () => import('#views/Project/ProjectForm'),
216+
visibility: 'is-authenticated',
217+
};
218+
219+
const addProject: RouteConfig = {
220+
index: true,
221+
path: 'projects/add',
222+
load: () => import('#views/Project/ProjectForm'),
223+
visibility: 'is-authenticated',
224+
};
225+
205226
const routes = {
206227
login,
207228
home,
@@ -232,6 +253,9 @@ const routes = {
232253
vacancy,
233254
addVacancy,
234255
editVacancy,
256+
project,
257+
addProject,
258+
editProject,
235259
};
236260

237261
export type RouteKeys = keyof typeof routes;

app/views/PrivateLayout/index.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ const navigation = [
5757
title: 'Procurements',
5858
variant: 'leaf' as const,
5959

60+
},
61+
{
62+
to: '/projects',
63+
title: 'Projects',
64+
variant: 'leaf' as const,
65+
6066
},
6167
{
6268
to: '/radio-programs',
@@ -73,7 +79,11 @@ const navigation = [
7379
variant: 'leaf' as const,
7480
title: 'Major Responsibilities',
7581
},
76-
{ to: '/about/strategic/goal', title: 'Goal', variant: 'leaf' as const },
82+
{
83+
to: '/strategic/strategic-directive',
84+
title: 'Strategic Directives',
85+
variant: 'leaf' as const,
86+
},
7787
],
7888
},
7989
{
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import {
2+
useCallback,
3+
useEffect,
4+
} from 'react';
5+
import {
6+
useNavigate,
7+
useParams,
8+
} from 'react-router';
9+
import {
10+
Button,
11+
Heading,
12+
SelectInput,
13+
TextArea,
14+
TextInput,
15+
} from '@ifrc-go/ui';
16+
import {
17+
createSubmitHandler,
18+
getErrorObject,
19+
ObjectSchema,
20+
PartialForm,
21+
requiredStringCondition,
22+
useForm,
23+
} from '@togglecorp/toggle-form';
24+
25+
import ContainerWrapper from '#components/ContainerWrapper';
26+
import FileUpload from '#components/FileUpload';
27+
import FormSection from '#components/FormSection';
28+
import Page from '#components/Page';
29+
import {
30+
ProjectCreateInput,
31+
useCreateProjectMutation,
32+
useDepartmentsQuery,
33+
useProjectDetailQuery,
34+
useUpdateProjectMutation,
35+
} from '#generated/types/graphql';
36+
import urlToFile from '#utils/urlToFile';
37+
38+
type PartialFormType = PartialForm<ProjectCreateInput> &
39+
{ createdBy: string, modifiedBy: string }
40+
41+
type FormSchema = ObjectSchema<PartialFormType>;
42+
type FormSchemaFields = ReturnType<FormSchema['fields']>;
43+
44+
const EditBlogSchema: FormSchema = {
45+
fields: (): FormSchemaFields => ({
46+
title: {
47+
required: true,
48+
requiredValidation: requiredStringCondition,
49+
},
50+
description: {
51+
required: true,
52+
requiredValidation: requiredStringCondition,
53+
},
54+
department: {
55+
required: true,
56+
},
57+
coverImage: {
58+
required: true,
59+
},
60+
createdBy: {},
61+
modifiedBy: {},
62+
63+
}),
64+
};
65+
66+
const defaultEditFormValue: PartialFormType = {
67+
createdBy: '',
68+
modifiedBy: '',
69+
};
70+
function ProjectForm() {
71+
const { id } = useParams();
72+
const navigate = useNavigate();
73+
const [{ data }] = useProjectDetailQuery({
74+
variables: { id: id || '' }, pause: !id,
75+
});
76+
const [{ data: departments }] = useDepartmentsQuery();
77+
78+
const [{ fetching: createPending }, createProjectMutate] = useCreateProjectMutation();
79+
const [{ fetching: updatePending }, updateProjectMutate] = useUpdateProjectMutation();
80+
const {
81+
setFieldValue,
82+
error: formError,
83+
value,
84+
validate,
85+
setError,
86+
} = useForm(EditBlogSchema, { value: defaultEditFormValue });
87+
88+
const error = getErrorObject(formError);
89+
90+
const handleFormSubmit = useCallback(() => {
91+
const handler = createSubmitHandler(
92+
validate,
93+
setError,
94+
async (val) => {
95+
const mutateData = {
96+
coverImage: val.coverImage ?? null,
97+
title: val.title ?? '',
98+
department: val.department ?? null,
99+
description: val.description ?? '',
100+
};
101+
if (id) {
102+
const res = await updateProjectMutate({
103+
pk: id,
104+
data: mutateData,
105+
});
106+
if (res.data?.updateProject?.ok) {
107+
navigate('/projects');
108+
} else if (res.data?.updateProject.errors) {
109+
setError(res.data.updateProject.errors);
110+
}
111+
} else {
112+
const res = await createProjectMutate({
113+
data: mutateData,
114+
});
115+
116+
if (res.data?.createProject.ok) {
117+
navigate('/projects');
118+
} else if (res.data?.createProject?.errors) {
119+
setError(res.data.createProject.errors);
120+
}
121+
}
122+
},
123+
);
124+
handler();
125+
}, [setError, validate, id, createProjectMutate, updateProjectMutate, navigate]);
126+
127+
useEffect(() => {
128+
if (data?.project) {
129+
const { project } = data;
130+
if (project.coverImage) {
131+
urlToFile(project?.coverImage?.url, project?.coverImage?.name)
132+
.then((file) => {
133+
setFieldValue(file, 'coverImage');
134+
});
135+
}
136+
setFieldValue(project?.title, 'title');
137+
setFieldValue(project?.description, 'description');
138+
setFieldValue(project?.department?.id, 'department');
139+
setFieldValue(`${project.modifiedBy?.firstName} ${project.modifiedBy?.lastName}`, 'modifiedBy');
140+
setFieldValue(`${project.createdBy?.firstName} ${project.createdBy?.lastName}`, 'createdBy');
141+
}
142+
}, [data, setFieldValue]);
143+
144+
const departmentOptions = departments?.departments.results.map(
145+
(dept) => ({
146+
id: dept.id,
147+
name: dept.title,
148+
}),
149+
) ?? [];
150+
151+
return (
152+
<Page>
153+
<ContainerWrapper>
154+
<FormSection headingLevel={3} label="VACANCY DETAILS" />
155+
{(value.createdBy && value.modifiedBy) && (
156+
<FormSection>
157+
<Heading level={6}>
158+
Created by:
159+
{' '}
160+
{value.createdBy}
161+
</Heading>
162+
<Heading level={6}>
163+
Modified by:
164+
{' '}
165+
{value.createdBy}
166+
</Heading>
167+
</FormSection>
168+
)}
169+
<FormSection label="Title" description="Enter the Title" withAsteriskOnTitle>
170+
<TextInput
171+
name="title"
172+
value={value.title}
173+
error={error?.title as string}
174+
onChange={setFieldValue}
175+
/>
176+
</FormSection>
177+
<FormSection label="Cover Image" description="Add a Cover Image, which will be attached and shown on Project" withAsteriskOnTitle>
178+
<FileUpload
179+
name="coverImage"
180+
onChange={(files) => setFieldValue(files, 'coverImage')}
181+
value={value.coverImage}
182+
/>
183+
</FormSection>
184+
<FormSection label="Description" description="Enter the Description" withAsteriskOnTitle>
185+
<TextArea
186+
name="description"
187+
value={value.description}
188+
error={error?.description as string}
189+
onChange={setFieldValue}
190+
/>
191+
</FormSection>
192+
<FormSection label="Type" description="Add type to either Tuesday Program or Radio Red Cross" withAsteriskOnTitle>
193+
<SelectInput
194+
name="department"
195+
options={departmentOptions}
196+
value={value.department}
197+
keySelector={(o) => o.id}
198+
labelSelector={(o) => o.name}
199+
onChange={setFieldValue}
200+
placeholder="Select Status"
201+
error={error?.department}
202+
/>
203+
</FormSection>
204+
<FormSection>
205+
<Button name="save" onClick={handleFormSubmit} variant="primary">
206+
{createPending || updatePending ? 'Saving' : 'Save'}
207+
</Button>
208+
</FormSection>
209+
</ContainerWrapper>
210+
</Page>
211+
);
212+
}
213+
214+
export default ProjectForm;
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import {
2+
useCallback,
3+
useMemo,
4+
} from 'react';
5+
import { useNavigate } from 'react-router';
6+
import {
7+
Button,
8+
Pager,
9+
Table,
10+
} from '@ifrc-go/ui';
11+
import {
12+
createElementColumn,
13+
createNumberColumn,
14+
createStringColumn,
15+
} from '@ifrc-go/ui/utils';
16+
17+
import ContainerWrapper from '#components/ContainerWrapper';
18+
import Page from '#components/Page';
19+
import TableActions, { TableActionsProps } from '#components/TableAction';
20+
import {
21+
ProjectQuery,
22+
useDeleteProjectMutation,
23+
useProjectQuery,
24+
} from '#generated/types/graphql';
25+
import usePagination from '#hooks/usePagination';
26+
27+
type ProjectListItem = NonNullable<ProjectQuery['projects']>['results'][number];
28+
29+
function ProjectList() {
30+
const navigate = useNavigate();
31+
const {
32+
page,
33+
setPage,
34+
pageSize,
35+
variables,
36+
getFormattedData,
37+
} = usePagination();
38+
39+
const [{ fetching, data }, reExecuteQuery] = useProjectQuery({ variables });
40+
const [{ fetching: deletePending }, deleteProject] = useDeleteProjectMutation();
41+
42+
const tableData = useMemo(
43+
() => getFormattedData<ProjectListItem>(data?.projects.results),
44+
[data, getFormattedData],
45+
);
46+
47+
const handleDelete = useCallback(
48+
(id: string, closeModal: () => void) => {
49+
deleteProject({ id }).then((resp) => {
50+
if (resp.data?.deleteProject) {
51+
reExecuteQuery();
52+
closeModal();
53+
}
54+
});
55+
},
56+
[deleteProject, reExecuteQuery],
57+
);
58+
59+
const columns = useMemo(() => [
60+
createNumberColumn<ProjectListItem & { sn: number }, string | number>('sn', 'S.N.', (item) => item.sn, { columnWidth: 60 }),
61+
createStringColumn<ProjectListItem, string | number>('title', 'Tile', (dept) => dept.title),
62+
createStringColumn<ProjectListItem, string | number>('department', 'Department', (dept) => dept?.department?.title),
63+
createElementColumn<ProjectListItem, string | number, TableActionsProps>(
64+
'actions',
65+
'Actions',
66+
TableActions,
67+
(_, datum) => ({
68+
id: datum.id,
69+
handleConfirmButtonChange: handleDelete,
70+
confirmPending: deletePending,
71+
itemTitle: datum.title,
72+
}),
73+
{ columnWidth: 150 },
74+
),
75+
], [handleDelete, deletePending]);
76+
return (
77+
<Page>
78+
<ContainerWrapper
79+
withPadding
80+
heading="Project"
81+
actions={(
82+
<Button name={undefined} variant="primary" disabled={false} onClick={() => navigate('add')}>
83+
Add Project
84+
</Button>
85+
)}
86+
footerActions={(
87+
<Pager
88+
activePage={page}
89+
itemsCount={data?.projects.totalCount ?? 0}
90+
maxItemsPerPage={pageSize}
91+
onActivePageChange={setPage}
92+
/>
93+
)}
94+
>
95+
<Table
96+
keySelector={(item) => item.id}
97+
columns={columns}
98+
data={tableData}
99+
filtered={false}
100+
pending={fetching}
101+
/>
102+
</ContainerWrapper>
103+
</Page>
104+
);
105+
}
106+
107+
export default ProjectList;

0 commit comments

Comments
 (0)