Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions app/Root/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,19 @@ const onlineInteractive: RouteConfig = {
load: () => import('#views/OnlineInteractive'),
visibility: 'is-authenticated',
};

const createOnlineInteractive: RouteConfig = {
path: '/online-interactive/create',
load: () => import('#views/OnlineInteractive/OnlineInteractiveForm'),
visibility: 'is-authenticated',
};

const editOnlineInteractive: RouteConfig = {
path: '/online-interactive/:id/edit',
load: () => import('#views/OnlineInteractive/OnlineInteractiveForm'),
visibility: 'is-authenticated',
};

const galleries: RouteConfig = {
index: true,
path: '/galleries',
Expand Down Expand Up @@ -238,6 +251,8 @@ const routes = {
editResourceDashboard,
documents,
onlineInteractive,
createOnlineInteractive,
editOnlineInteractive,
galleries,
editTeamMember,
links,
Expand Down
1 change: 1 addition & 0 deletions app/Root/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ function RootContent() {
userRole: globalEnumsData?.enums.UserRole,
teamMemberSex: globalEnumsData?.enums.TeamMemberSex,
dashboardPage: globalEnumsData?.enums.DashboardPage,
reportContentType: globalEnumsData?.enums.ReportContentType,
}), [globalEnumsData]);

return (
Expand Down
4 changes: 4 additions & 0 deletions app/Root/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export const GLOBAL_ENUMS = gql`
key
label
}
ReportContentType {
key
label
}
}
}
`;
3 changes: 3 additions & 0 deletions app/contexts/GlobalEnumsContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createContext } from 'react';
import type {
AppEnumCollectionDashboardPage,
AppEnumCollectionLinkType,
AppEnumCollectionReportContentType,
AppEnumCollectionTeamMemberSex,
AppEnumCollectionUserRole,
} from '#generated/types/graphql';
Expand All @@ -12,13 +13,15 @@ export interface GlobalEnumsContextInterface {
userRole: AppEnumCollectionUserRole[] | undefined;
teamMemberSex: AppEnumCollectionTeamMemberSex[] | undefined;
dashboardPage: AppEnumCollectionDashboardPage[] | undefined;
reportContentType: AppEnumCollectionReportContentType[] | undefined;
}

const GlobalEnumsContext = createContext<GlobalEnumsContextInterface>({
linkType: undefined,
userRole: undefined,
teamMemberSex: undefined,
dashboardPage: undefined,
reportContentType: undefined,
});

export default GlobalEnumsContext;
22 changes: 22 additions & 0 deletions app/views/OnlineInteractive/OnlineInteractiveFilter/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { TextInput } from '@ifrc-go/ui';
import { type EntriesAsList } from '@togglecorp/toggle-form';

import { type OnlineInteractiveFilterType } from '../index';

export interface Props {
value: OnlineInteractiveFilterType;
onChange: (...args: EntriesAsList<OnlineInteractiveFilterType>) => void;
}

function OnlineInteractiveFilter({ value, onChange }: Props) {
return (
<TextInput
name="search"
placeholder="Search by title"
value={value.search}
onChange={onChange}
/>
);
}

export default OnlineInteractiveFilter;
280 changes: 280 additions & 0 deletions app/views/OnlineInteractive/OnlineInteractiveForm/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
import {
useCallback,
useEffect,
useMemo,
} from 'react';
import { useParams } from 'react-router';
import {
BlockLoading,
Button,
Container,
Description,
InlineLayout,
InputSection,
ListView,
RawFileInput,
TextInput,
} from '@ifrc-go/ui';
import {
isDefined,
isNotDefined,
} from '@togglecorp/fujs';
import {
createSubmitHandler,
getErrorObject,
type ObjectSchema,
type PartialForm,
requiredStringCondition,
useForm,
} from '@togglecorp/toggle-form';

import NonFieldError from '#components/NonFieldError';
import {
ReportContentType,
type ReportCreateInput,
ReportTypeEnum,
type ReportUpdateInput,
useCreateOnlineInteractiveMutation,
useOnlineInteractiveDetailQuery,
useUpdateOnlineInteractiveMutation,
} from '#generated/types/graphql';
import useAlert from '#hooks/useAlert';
import useRouting from '#hooks/useRouting';
import {
errorMessage,
transformToFormError,
} from '#utils/common';

type FormFields = Pick<ReportCreateInput, 'title' | 'file' | 'contentType' | 'reportType'>;
type PartialFormType = PartialForm<FormFields>;
type FormSchema = ObjectSchema<PartialFormType>;
type FormSchemaFields = ReturnType<FormSchema['fields']>;

function getOnlineInteractiveSchema(isEditing: boolean): FormSchema {
return {
fields: (): FormSchemaFields => ({
title: {
required: true,
requiredValidation: requiredStringCondition,
},
contentType: {
required: true,
},
reportType: {
required: true,
},
file: {
required: !isEditing,
},
}),
};
}

const defaultFormValue: PartialFormType = {
contentType: ReportContentType.File,
reportType: ReportTypeEnum.OnlineInteractive,
};

const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB

function OnlineInteractiveForm() {
const { id } = useParams();
const navigate = useRouting();
const alert = useAlert();

const isEditing = isDefined(id);

const schema = useMemo(() => getOnlineInteractiveSchema(isEditing), [isEditing]);

const {
setFieldValue,
error: formError,
value,
validate,
setError,
setValue,
} = useForm(schema, { value: defaultFormValue });

const [{ data: detailData, fetching: detailFetching }] = useOnlineInteractiveDetailQuery({
variables: { id: isDefined(id) ? id : '' },
pause: isNotDefined(id),
});

const [{ fetching: creating }, createOnlineInteractive] = useCreateOnlineInteractiveMutation();
const [{ fetching: updating }, updateOnlineInteractive] = useUpdateOnlineInteractiveMutation();

const pending = creating || updating || detailFetching;

const fileName = value.file instanceof File
? value.file.name
: detailData?.report?.file?.name?.split('/').pop();

const handleFileInputChange = useCallback(
(file: File | undefined, name: 'file') => {
if (isDefined(file) && file.size > MAX_FILE_SIZE) {
alert.show('File must be 5MB or smaller', { variant: 'danger' });
return;
}
setFieldValue(file, name);
},
[alert, setFieldValue],
);

const handleResult = useCallback((
result: {
ok?: boolean | null;
errors?: Parameters<typeof transformToFormError>[0] | null;
} | undefined | null,
successMessage: string,
) => {
if (isDefined(result) && result.ok) {
navigate('onlineInteractive');
alert.show(successMessage, { variant: 'success' });
} else if (isDefined(result) && isDefined(result.errors)) {
setError(transformToFormError(result.errors));
alert.show(errorMessage, { variant: 'danger' });
} else {
alert.show(errorMessage, { variant: 'danger' });
}
}, [navigate, alert, setError]);

const handleCreate = useCallback(async (formValues: PartialFormType) => {
const { file, ...rest } = formValues;

const res = await createOnlineInteractive({
data: {
...rest,
...(isDefined(file) ? { file } : {}),
} as ReportCreateInput,
});

handleResult(res.data?.createReport, 'Online interactive created successfully');
}, [createOnlineInteractive, handleResult]);

const handleUpdate = useCallback(async (formValues: PartialFormType) => {
if (isNotDefined(id)) {
return;
}
const { file, title } = formValues;

const res = await updateOnlineInteractive({
id,
data: {
title,
...(isDefined(file) ? { file } : {}),
} as ReportUpdateInput,
});

handleResult(res.data?.updateReport, 'Online interactive updated successfully');
}, [id, updateOnlineInteractive, handleResult]);

const handleFormSubmit = useCallback(
() => createSubmitHandler(
validate,
setError,
isDefined(id) ? handleUpdate : handleCreate,
)(),
[validate, setError, id, handleUpdate, handleCreate],
);

const handleCancel = useCallback(() => {
navigate('onlineInteractive');
}, [navigate]);

const error = getErrorObject(formError);

useEffect(() => {
if (isNotDefined(detailData?.report)) {
return;
}
setValue({
...defaultFormValue,
title: detailData.report.title,
});
}, [detailData, setValue]);

if (detailFetching) {
return (
<BlockLoading
withoutBorder
compact
message="Loading"
/>
);
}

return (
<Container
heading={isEditing ? 'Edit Online Interactive' : 'Create Online Interactive'}
headerDescription={isEditing
? 'Manage and update the online interactive'
: 'Create a new online interactive'}
withPadding
footerActions={(
<ListView>
<Button
name={undefined}
onClick={handleCancel}
>
Cancel
</Button>
<Button
name={undefined}
onClick={handleFormSubmit}
styleVariant="filled"
disabled={pending}
>
Save
</Button>
</ListView>
)}
>
<ListView layout="block">
<NonFieldError
error={formError}
withFallbackError
/>
<InputSection
title="Title"
description="Enter the title of the online interactive"
withAsteriskOnTitle
>
<TextInput
name="title"
value={value.title}
onChange={setFieldValue}
error={error?.title}
disabled={pending}
/>
</InputSection>
<InputSection
title="File"
description="Upload the online interactive file (max 5MB)"
withAsteriskOnTitle
>
<InlineLayout
spacing="sm"
contentAlignment="center"
before={(
<RawFileInput
name="file"
onChange={handleFileInputChange}
disabled={pending}
styleVariant="outline"
>
{isDefined(fileName) ? 'Change file' : 'Upload file'}
</RawFileInput>
)}
>
<Description>
{isDefined(fileName) ? fileName : 'Please upload a file'}
</Description>
</InlineLayout>
<NonFieldError error={error?.file} />
</InputSection>
</ListView>
</Container>
);
}

export default OnlineInteractiveForm;
Loading
Loading