Skip to content

Commit a4a83e5

Browse files
committed
feat(partner): Added partner listing and edit add form
1 parent f418e74 commit a4a83e5

File tree

6 files changed

+486
-0
lines changed

6 files changed

+486
-0
lines changed

app/root/config/routes.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,25 @@ const editHighlight: RouteConfig = {
9999
load: () => import('#views/Highlight/HighlightForm'),
100100
visibility: 'is-authenticated',
101101
};
102+
const partner: RouteConfig = {
103+
index: true,
104+
path: 'partners',
105+
load: () => import('#views/Partner/PartnerList'),
106+
visibility: 'is-authenticated',
107+
};
108+
const editPartner: RouteConfig = {
109+
index: true,
110+
path: 'partners/:id/edit',
111+
load: () => import('#views/Partner/PartnerForm'),
112+
visibility: 'is-authenticated',
113+
};
102114

115+
const addPartner: RouteConfig = {
116+
index: true,
117+
path: 'partners/add',
118+
load: () => import('#views/Partner/PartnerForm'),
119+
visibility: 'is-authenticated',
120+
};
103121
const routes = {
104122
login,
105123
home,
@@ -115,6 +133,9 @@ const routes = {
115133
highlight,
116134
addHighlight,
117135
editHighlight,
136+
partner,
137+
editPartner,
138+
addPartner,
118139
};
119140

120141
export type RouteKeys = keyof typeof routes;
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import {
2+
useCallback,
3+
useEffect,
4+
} from 'react';
5+
import {
6+
useNavigate,
7+
useParams,
8+
} from 'react-router';
9+
import {
10+
Button,
11+
Container,
12+
Heading,
13+
NumberInput,
14+
RawFileInput,
15+
SelectInput,
16+
TextArea,
17+
TextInput,
18+
} from '@ifrc-go/ui';
19+
import {
20+
createSubmitHandler,
21+
getErrorObject,
22+
integerCondition,
23+
ObjectSchema,
24+
PartialForm,
25+
requiredStringCondition,
26+
useForm,
27+
} from '@togglecorp/toggle-form';
28+
29+
import FormSection from '#components/FormSection';
30+
import Page from '#components/Page';
31+
import {
32+
PartnerCreateInput,
33+
PartnerScopeEnum,
34+
useCreatePartnerMutation,
35+
usePartnerDetailQuery,
36+
useUpdatePartnerMutation,
37+
} from '#generated/types/graphql';
38+
import urlToFile from '#utils/urlToFile';
39+
40+
import styles from './styles.module.css';
41+
42+
type PartialFormType = PartialForm<PartnerCreateInput> &
43+
{ createdBy: string, modifiedBy: string }
44+
45+
type FormSchema = ObjectSchema<PartialFormType>;
46+
type FormSchemaFields = ReturnType<FormSchema['fields']>;
47+
48+
const EditBlogSchema: FormSchema = {
49+
fields: (): FormSchemaFields => ({
50+
title: {
51+
required: true,
52+
requiredValidation: requiredStringCondition,
53+
},
54+
scope: {
55+
required: true,
56+
requiredValidation: requiredStringCondition,
57+
},
58+
image: {
59+
required: true,
60+
},
61+
createdBy: {},
62+
modifiedBy: {},
63+
64+
}),
65+
};
66+
67+
const defaultEditFormValue: PartialFormType = {
68+
createdBy: '',
69+
modifiedBy: '',
70+
};
71+
function PartnerForm() {
72+
const { id } = useParams();
73+
const navigate = useNavigate();
74+
const [{ data }] = usePartnerDetailQuery({
75+
variables: { id: id || '' }, pause: !id,
76+
});
77+
const [{ fetching: createPending }, createPartnerMutate] = useCreatePartnerMutation();
78+
const [{ fetching: updatePending }, updatePartnerMutate] = useUpdatePartnerMutation();
79+
const {
80+
setFieldValue,
81+
error: formError,
82+
value,
83+
validate,
84+
setError,
85+
} = useForm(EditBlogSchema, { value: defaultEditFormValue });
86+
87+
const error = getErrorObject(formError);
88+
89+
const handleFormSubmit = useCallback(() => {
90+
const handler = createSubmitHandler(
91+
validate,
92+
setError,
93+
async (val) => {
94+
const mutateData = {
95+
image: val.image ?? '',
96+
title: val.title ?? '',
97+
scope: val.scope as PartnerScopeEnum,
98+
};
99+
if (id) {
100+
const res = await updatePartnerMutate({
101+
pk: id,
102+
data: mutateData,
103+
});
104+
if (res.data?.updatePartner?.ok) {
105+
navigate('/partners');
106+
} else if (res.data?.updatePartner.errors) {
107+
setError(res.data.updatePartner.errors);
108+
}
109+
} else {
110+
const res = await createPartnerMutate({
111+
data: mutateData,
112+
});
113+
114+
if (res.data?.createPartner.ok) {
115+
navigate('/partners');
116+
} else if (res.data?.createPartner?.errors) {
117+
setError(res.data.createPartner.errors);
118+
}
119+
}
120+
},
121+
);
122+
handler();
123+
}, [setError, validate, id, createPartnerMutate, updatePartnerMutate, navigate]);
124+
125+
useEffect(() => {
126+
if (data?.partner) {
127+
const { partner } = data;
128+
if (partner.image) {
129+
urlToFile(partner.image.url, partner.image.name)
130+
.then((file) => {
131+
setFieldValue(file, 'image');
132+
});
133+
}
134+
setFieldValue(partner.title, 'title');
135+
setFieldValue(partner.scope, 'scope');
136+
setFieldValue(partner.image, 'image');
137+
setFieldValue(`${partner.modifiedBy.firstName} ${partner.modifiedBy.lastName}`, 'modifiedBy');
138+
setFieldValue(`${partner.createdBy.firstName} ${partner.createdBy.lastName}`, 'createdBy');
139+
}
140+
}, [data, setFieldValue]);
141+
142+
const scopeOptions = Object.values(PartnerScopeEnum).map((scope) => ({
143+
value: scope,
144+
label: scope,
145+
}));
146+
return (
147+
<Page>
148+
<Container
149+
className={styles.container}
150+
childrenContainerClassName={styles.containerChild}
151+
>
152+
<FormSection headingLevel={3} label="Partner DETAIL" />
153+
{(value.createdBy && value.modifiedBy) && (
154+
<FormSection inputClassName={styles.inputClassName}>
155+
<div>
156+
<Heading level={6}>Created by:</Heading>
157+
<Heading level={6}>{value.createdBy}</Heading>
158+
</div>
159+
<div>
160+
<Heading level={6}>Modified by:</Heading>
161+
<Heading level={6}>{value.createdBy}</Heading>
162+
</div>
163+
</FormSection>
164+
)}
165+
<FormSection label="Title*" description="Enter the question">
166+
<TextInput
167+
name="title"
168+
value={value.title}
169+
error={error?.title as string}
170+
onChange={setFieldValue}
171+
/>
172+
</FormSection>
173+
174+
<FormSection label="Status*" description="Add status to either draft, publish or archived">
175+
<SelectInput
176+
name="scope"
177+
options={scopeOptions}
178+
value={value.scope}
179+
keySelector={(o) => o.label}
180+
labelSelector={(o) => o.value}
181+
onChange={setFieldValue}
182+
placeholder="Select Status"
183+
error={error?.scope}
184+
/>
185+
</FormSection>
186+
187+
<FormSection label="Partner Logo*" description="Add a cover photo, which will be displayed on top">
188+
<RawFileInput
189+
name="image"
190+
onChange={(files) => setFieldValue(files, 'image')}
191+
variant="secondary"
192+
>
193+
Upload
194+
</RawFileInput>
195+
{value.image?.name && <p>{value.image.name}</p>}
196+
</FormSection>
197+
198+
<div className={styles.submitBtn}>
199+
<Button name="save" onClick={handleFormSubmit} variant="primary">
200+
{createPending || updatePending ? 'Saving' : 'Save'}
201+
</Button>
202+
</div>
203+
</Container>
204+
</Page>
205+
);
206+
}
207+
208+
export default PartnerForm;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
.container {
2+
width: 100%;
3+
}
4+
.containerChild {
5+
display: flex;
6+
flex-direction: column;
7+
width: 100%;
8+
height: 100%;
9+
gap: var(--go-ui-spacing-sm);
10+
background: var(--go-ui-color-gray-10);
11+
overflow: auto;
12+
}
13+
.containerChild > * {
14+
background: var(--go-ui-color-white);
15+
}
16+
17+
.inputClassName {
18+
display: flex;
19+
flex-direction: row;
20+
width: 100%;
21+
gap: var(--go-ui-spacing-xl);
22+
}
23+
.fileUpload {
24+
display: flex;
25+
align-items: center;
26+
gap: var(--go-ui-spacing-sm);
27+
}
28+
.submitBtn {
29+
display: flex;
30+
align-items: center;
31+
justify-content: center;
32+
padding: var(--go-ui-spacing-lg) var(--go-ui-spacing-xl);
33+
}
34+
35+
.inputClassName > * {
36+
flex: 1;
37+
display: flex;
38+
gap: var(--go-ui-spacing-sm);
39+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import React, {
2+
useCallback,
3+
useMemo,
4+
} from 'react';
5+
import { useNavigate } from 'react-router';
6+
import {
7+
Button,
8+
Container,
9+
Pager,
10+
Table,
11+
} from '@ifrc-go/ui';
12+
import {
13+
createElementColumn,
14+
createNumberColumn,
15+
createStringColumn,
16+
} from '@ifrc-go/ui/utils';
17+
18+
import TableActions, { TableActionsProps } from '#components/TableAction';
19+
import {
20+
PartnerQuery,
21+
useDeletePartnerMutation,
22+
usePartnerQuery,
23+
} from '#generated/types/graphql';
24+
import usePagination from '#hooks/usePagination';
25+
26+
import styles from './styles.module.css';
27+
28+
type PartnerListItem = NonNullable<PartnerQuery['partners']>['results'][number];
29+
30+
function PartnerList() {
31+
const navigate = useNavigate();
32+
const {
33+
page,
34+
setPage,
35+
pageSize,
36+
variables,
37+
getFormattedData,
38+
} = usePagination();
39+
40+
const [{ fetching, data }, reExecuteQuery] = usePartnerQuery({ variables });
41+
const [{ fetching: deletePending }, deletePartner] = useDeletePartnerMutation();
42+
43+
const tableData = useMemo(
44+
() => getFormattedData<PartnerListItem>(data?.partners.results),
45+
[data, getFormattedData],
46+
);
47+
48+
const handleDelete = useCallback(
49+
(id: string, closeModal: () => void) => {
50+
deletePartner({ id }).then((resp) => {
51+
if (resp.data?.deletePartner) {
52+
reExecuteQuery();
53+
closeModal();
54+
}
55+
});
56+
},
57+
[deletePartner, reExecuteQuery],
58+
);
59+
60+
const columns = useMemo(() => [
61+
createNumberColumn<PartnerListItem & { sn: number }, string | number>('sn', 'S.N.', (item) => item.sn, { columnWidth: 60 }),
62+
createStringColumn<PartnerListItem, string | number>('title', 'Tile', (dept) => dept.title),
63+
createStringColumn<PartnerListItem, string | number>('scope', 'Scope', (dept) => dept?.scope),
64+
createElementColumn<PartnerListItem, string | number, TableActionsProps>(
65+
'actions',
66+
'Actions',
67+
TableActions,
68+
(_, datum) => ({
69+
id: datum.id,
70+
handleConfirmButtonChange: handleDelete,
71+
confirmPending: deletePending,
72+
itemTitle: datum.title,
73+
}),
74+
{ columnWidth: 150 },
75+
),
76+
], [handleDelete, deletePending]);
77+
return (
78+
<Container
79+
className={styles.partner}
80+
childrenContainerClassName={styles.content}
81+
heading="Partner"
82+
actions={(
83+
<Button name={undefined} variant="primary" disabled={false} onClick={() => navigate('add')}>
84+
Add Partner
85+
</Button>
86+
)}
87+
footerActions={(
88+
<Pager
89+
activePage={page}
90+
itemsCount={data?.partners.totalCount ?? 0}
91+
maxItemsPerPage={pageSize}
92+
onActivePageChange={setPage}
93+
/>
94+
)}
95+
>
96+
<Table
97+
keySelector={(item) => item.id}
98+
className={styles.table}
99+
columns={columns}
100+
data={tableData}
101+
filtered={false}
102+
pending={fetching}
103+
headerRowClassName={styles.headerRow}
104+
/>
105+
</Container>
106+
);
107+
}
108+
109+
export default PartnerList;

0 commit comments

Comments
 (0)