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

Feature/deseng668: Added authoring side nav and bottom nav, modified routing, added authoring context, added base page for banner #2581

Merged
merged 12 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## September 3, 2024
- **Feature** New authoring content section [🎟️ DESENG-668](https://citz-gdx.atlassian.net/browse/DESENG-668)
- Added skeletons

## August 23, 2024
- **Feature** New authoring content section [🎟️ DESENG-668](https://citz-gdx.atlassian.net/browse/DESENG-668)
- Implemented authoring side nav
Expand Down
1 change: 0 additions & 1 deletion met-web/src/components/common/RichTextEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ const RichTextEditor = ({
onEditorStateChange={handleChange}
handlePastedText={() => false}
editorStyle={{
height: '10em',
padding: '1em',
resize: 'vertical',
}}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,45 +1,29 @@
import React, { Suspense, useState, useMemo } from 'react';
import { Await, useAsyncValue, useParams } from 'react-router-dom';
import React from 'react';
import { AppBar, Theme, ThemeProvider, Box, useMediaQuery, Select, MenuItem, SelectChangeEvent } from '@mui/material';
import { Palette, colors, DarkTheme, BaseTheme } from 'styles/Theme';
import { When, Unless } from 'react-if';
import { BodyText } from 'components/common/Typography';
import { elevations } from 'components/common';
import { Button } from 'components/common/Input';
import { Link } from 'components/common/Navigation';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCheck } from '@fortawesome/pro-regular-svg-icons';
import { useAppSelector } from 'hooks';
import { getAuthoringRoutes } from './AuthoringNavElements';
import { getTenantLanguages } from 'services/languageService';
import { Language } from 'models/language';
import { StatusCircle } from '../../view/AuthoringTab';
import pagePreview from 'assets/images/pagePreview.png';
import { AuthoringBottomNavProps, LanguageSelectorProps } from './types';
import { getLanguageValue } from './AuthoringTemplate';

const AuthoringBottomNav = (props: AuthoringBottomNavProps) => {
const { isDirty, isValid, isSubmitting } = props;
const AuthoringBottomNav = ({
isDirty,
isValid,
isSubmitting,
currentLanguage,
setCurrentLanguage,
languages,
pageTitle,
}: AuthoringBottomNavProps) => {
const isMediumScreenOrLarger = useMediaQuery((theme: Theme) => theme.breakpoints.up('md'));
const padding = { xs: '1rem 1rem', md: '1rem 1.5rem 1rem 2rem', lg: '1rem 3rem 1rem 2rem' };

const tenant = useAppSelector((state) => state.tenant);
const { engagementId } = useParams() as { engagementId: string };
const location = window.location.href;
const locationArray = location.split('/');
const slug = locationArray[locationArray.length - 1];

const languages = useMemo(() => getTenantLanguages(tenant.id), [tenant.id]); // todo: Using tenant language list until language data is integrated with the engagement.
const [currentLanguage, setCurrentLanguage] = useState(useAppSelector((state) => state.language.id));

const getPageValue = () => {
const authoringRoutes = getAuthoringRoutes(Number(engagementId), tenant);
return authoringRoutes.find((route) => route.path.includes(slug))?.name;
};

const getLanguageValue = (currentLanguage: string, languages: Language[]) => {
return languages.find((language) => language.code === currentLanguage)?.name;
};

const buttonStyles = {
height: '2.6rem',
borderRadius: '8px',
Expand Down Expand Up @@ -80,24 +64,18 @@ const AuthoringBottomNav = (props: AuthoringBottomNavProps) => {
<ThemeProvider theme={DarkTheme}>
<Box
sx={{
width: '18.75rem',
minWidth: '18.75rem',
width: '18.8rem',
minWidth: '18.8rem',
marginBottom: isMediumScreenOrLarger ? '0' : '1rem',
}}
>
<BodyText sx={{ fontWeight: 'bold' }}>Currently Authoring</BodyText>
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
<BodyText sx={{ fontWeight: 'bold' }}>Currently Authoring</BodyText>
<BodyText bold>Currently Authoring</BodyText>

<BodyText sx={{ fontSize: '0.7rem', alignItems: 'center', marginTop: '-5px', display: 'flex' }}>
<span>{getPageValue()}</span>
<span>{pageTitle}</span>
<span style={{ fontSize: '0.4rem', paddingLeft: '0.4rem', paddingRight: '0.4rem' }}>
{'\u2B24'}
</span>
<Suspense>
<Await resolve={languages}>
{(languages: Language[]) => (
<span>{getLanguageValue(currentLanguage, languages)}</span>
)}
</Await>
</Suspense>
{getLanguageValue(currentLanguage, languages)}
</BodyText>
</Box>
<Box
Expand All @@ -108,14 +86,13 @@ const AuthoringBottomNav = (props: AuthoringBottomNavProps) => {
}}
>
<ThemeProvider theme={BaseTheme}>
<Suspense>
<Await resolve={languages}>
<LanguageSelector
currentLanguage={currentLanguage}
setCurrentLanguage={setCurrentLanguage}
/>
</Await>
</Suspense>
<LanguageSelector
currentLanguage={currentLanguage}
setCurrentLanguage={setCurrentLanguage}
languages={languages}
isDirty={isDirty}
isSubmitting={isSubmitting}
/>
</ThemeProvider>

<Button
Expand All @@ -128,11 +105,10 @@ const AuthoringBottomNav = (props: AuthoringBottomNavProps) => {
margin: '0 1.2rem',
}}
>
<Link sx={{ color: colors.surface.gray[60] }} href="#">
Save Section
</Link>
Save Section
</Button>
<Button
disabled={!isValid || !isDirty || isSubmitting}
type="submit"
name="request_type"
value="preview"
Expand All @@ -141,7 +117,14 @@ const AuthoringBottomNav = (props: AuthoringBottomNavProps) => {
marginLeft: 'auto',
}}
>
<img style={{ paddingRight: '0.3rem' }} src={pagePreview} alt="page preview icon" />
<img
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should be an icon instead of an image. Also for screenreaders, as long as it reads something like "preview," "page preview," or "editing preview" then we should be good as far as labelling goes.

Jess should have been using FontAwesome icons so it should be available to us

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure how to make it into an icon since this is a custom graphic that Jessica created for the button. I exported it from Figma and included it in our assets. If we have a process for converting images to icons then let me know and I will do it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I appreciate that you took the trouble of exactly replicating her design. Is it a PNG? We may still be able to use it as an icon then. You could also switch to a fontawesome icon button with one of these icons here, some of which are close enough to hers: https://fontawesome.com/search?q=magnifying&o=r

However you do it, the image tag should be removed here - it'll cause confusion to screen readers. Practically speaking, all they should hear is "button, editing preview" or something like this. Hearing "image" in this context might throw them off.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I looked it up and apparently I can add alt="" and aria-hidden="true" in order to keep it invisible to a reader.

Copy link
Collaborator

@Baelx Baelx Sep 4, 2024

Choose a reason for hiding this comment

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

I'm alright with this as a quick fix this time. Could you please just test it once you implement? Ideally with VoiceOver

style={{
paddingRight: '0.3rem',
filter: !isValid || !isDirty || isSubmitting ? 'opacity(40%)' : 'opacity(100%)',
}}
src={pagePreview}
alt="page preview icon"
/>
Preview
</Button>
</Box>
Expand All @@ -152,10 +135,24 @@ const AuthoringBottomNav = (props: AuthoringBottomNavProps) => {
);
};

const LanguageSelector = ({ currentLanguage, setCurrentLanguage }: LanguageSelectorProps) => {
const languages = useAsyncValue() as Language[];
const LanguageSelector = ({
currentLanguage,
setCurrentLanguage,
languages,
isDirty,
isSubmitting,
}: LanguageSelectorProps) => {
const handleSelectChange = (event: SelectChangeEvent<string>) => {
const newLanguageCode = event.target.value;
if (isDirty && !isSubmitting)
// todo: Replace this message with our stylized modal message.
window.confirm(
`Are you sure you want to switch to ${
getLanguageValue(newLanguageCode, languages) || 'another language'
}? You have unsaved changes for the ${
getLanguageValue(currentLanguage, languages) || 'current'
} language.`,
);
setCurrentLanguage(newLanguageCode);
};
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import React from 'react';
import { Dayjs } from 'dayjs';
import dayjs, { Dayjs } from 'dayjs';
import { FormProvider, useForm } from 'react-hook-form';
import { createSearchParams, useFetcher, Outlet } from 'react-router-dom';

export interface EngagementUpdateData {
id: number;
status_id: number;
taxon_id: number;
content_id: number;
name: string;
start_date: Dayjs;
end_date: Dayjs;
status_id: number;
description: string;
rich_description: string;
banner_filename: string;
status_block: string[];
title: string;
icon_name: string;
metadata_value: string;
taxon_id: number;
send_report: boolean;
slug: string;
request_type: string;
Expand All @@ -26,55 +27,67 @@ export const AuthoringContext = () => {
const fetcher = useFetcher();
const locationArray = window.location.href.split('/');
const slug = locationArray[locationArray.length - 1];
const defaultDateValue = dayjs(new Date(1970, 0, 1));
const engagementUpdateForm = useForm<EngagementUpdateData>({
defaultValues: {
id: 0,
name: '',
start_date: undefined,
end_date: undefined,
status_id: 0,
taxon_id: 0,
content_id: 0,
name: '',
start_date: defaultDateValue,
end_date: defaultDateValue,
description: '',
rich_description: '',
banner_filename: '',
status_block: [],
title: '',
icon_name: '',
metadata_value: '',
taxon_id: 0,
send_report: false,
send_report: undefined,
slug: '',
request_type: '',
},
mode: 'onSubmit',
reValidateMode: 'onChange',
});

const onSubmit = async (data: EngagementUpdateData) => {
fetcher.submit(
createSearchParams({
id: data.id.toString(),
id: 0 === data.id ? '' : data.id.toString(),
status_id: 0 === data.status_id ? '' : data.status_id.toString(),
taxon_id: 0 === data.taxon_id ? '' : data.taxon_id.toString(),
content_id: 0 === data.content_id ? '' : data.content_id.toString(),
name: data.name,
start_date: data.start_date.format('YYYY-MM-DD'),
end_date: data.end_date.format('YYYY-MM-DD'),
start_date:
'1970-01-01' === data.start_date.format('YYYY-MM-DD') ? '' : data.start_date.format('YYYY-MM-DD'),
end_date:
'1970-01-01' === data.start_date.format('YYYY-MM-DD') ? '' : data.end_date.format('YYYY-MM-DD'),
description: data.description,
rich_description: data.rich_description,
banner_filename: data.banner_filename,
status_block: data.status_block,
title: data.title,
icon_name: data.icon_name,
metadata_value: data.metadata_value,
taxon_id: data.taxon_id.toString(),
send_report: data.send_report ? 'true' : 'false',
send_report: getSendReportValue(data.send_report),
slug: data.slug,
request_type: data.request_type,
}),
{
method: 'post',
action: `/engagements/${data.id}/authoring/${slug}`,
action: `/engagements/${data.id}/details/authoring/${slug}`,
},
);
};

const getSendReportValue = (valueToInterpret: boolean) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this like a utility function to convert incompatible data to that used in an HTTP request? Do we not have something for this already?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So the data in createSearchParams all needs to be converted to strings. In this particular case, I just needed to convert a boolean value to a string equivalent as 'true' or 'false'. It's quite possible that there is another utility function that does this, I just don't know about it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Booleans can't be used with truthy checks because they can return false if their value is false, even though I want that conditional to pass on a false value. So I need to check if the value is undefined separately, then give a value that is either 'true' or 'false'.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah I was just curious how this operation for creating an HTTP request gets handled elsewhere and if we could use that. No worries if you want to just stick with this function you've created

if (undefined === valueToInterpret) {
return '';
}
return valueToInterpret ? 'true' : 'false';
};

return (
<FormProvider {...engagementUpdateForm}>
<Outlet context={{ onSubmit }} />
Expand Down
Loading