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 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
7 changes: 7 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## September 3, 2024
- **Feature** New authoring content section [🎟️ DESENG-668](https://citz-gdx.atlassian.net/browse/DESENG-668)
- Implemented authoring side nav
- Implemented authoring bottom nav
- Implemented authoring section context
- Added skeletons for the required sections

## August 28, 2024

- **Bugfix** Fix keyboard focus being trapped on buttons on authoring page [🎟️ DESENG-687](https://citz-gdx.atlassian.net/browse/DESENG-687)
Expand Down
Binary file added met-web/src/assets/images/pagePreview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const engagementUpdateAction: ActionFunction = async ({ request, params }
console.error('Error updating team members', e);
}

return redirect(`/engagements/${engagementId}/view`);
return redirect(`/engagements/${engagementId}/details/config`);
};

export default engagementUpdateAction;
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const ConfigForm = () => {
}),
{
method: 'patch',
action: `/engagements/${engagement.id}/config/`,
action: `/engagements/${engagement.id}/details/config/edit`,
},
);
};
Expand Down Expand Up @@ -131,7 +131,7 @@ const ConfigForm = () => {
>
<Grid container direction="row" item xs={12}>
<Grid xs={12}>
<Header2 sx={{ mb: 0 }}>We're just looking over your configuration.</Header2>
<Header2 sx={{ mb: 0 }}>We're saving your configuration.</Header2>
</Grid>
</Grid>
<Grid container direction="row" item xs={12}>
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCheck } from '@fortawesome/pro-regular-svg-icons';
import { StatusCircle } from '../../view/AuthoringTab';
import pagePreview from 'assets/images/pagePreview.png';
import { AuthoringBottomNavProps, LanguageSelectorProps } from './types';
import { getLanguageValue } from './AuthoringTemplate';

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 buttonStyles = {
height: '2.6rem',
borderRadius: '8px',
border: 'none',
padding: '0 1rem',
minWidth: '8.125rem',
fontSize: '0.9rem',
};

return (
<AppBar
component={'nav'}
position="fixed"
sx={{
backgroundColor: 'transparent',
borderTopRightRadius: '16px',
minHeight: '5rem',
backgroundClip: 'padding-box',
overflow: 'hidden',
top: 'auto',
left: 0,
bottom: 0,
boxShadow: elevations.default,
}}
data-testid="appbar-authoring-bottom-nav"
>
<Box
sx={{
background: colors.surface.blue[90],
minHeight: '5rem',
justifyContent: 'flex-start',
padding: padding,
display: 'flex',
alignItems: 'center',
flexWrap: isMediumScreenOrLarger ? 'nowrap' : 'wrap',
}}
>
<ThemeProvider theme={DarkTheme}>
<Box
sx={{
width: '18.8rem',
minWidth: '18.8rem',
marginBottom: isMediumScreenOrLarger ? '0' : '1rem',
}}
>
<BodyText bold>Currently Authoring</BodyText>
<BodyText sx={{ fontSize: '0.7rem', alignItems: 'center', marginTop: '-5px', display: 'flex' }}>
<span>{pageTitle}</span>
<span style={{ fontSize: '0.4rem', paddingLeft: '0.4rem', paddingRight: '0.4rem' }}>
{'\u2B24'}
Comment on lines +75 to +76
Copy link
Contributor

Choose a reason for hiding this comment

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

Found a different Unicode character that doesn't require a font size adjustment

Suggested change
<span style={{ fontSize: '0.4rem', paddingLeft: '0.4rem', paddingRight: '0.4rem' }}>
{'\u2B24'}
<span style={{ paddingLeft: '0.5rem', paddingRight: '0.5rem', userSelect: 'none' }}>
{'\u25CF'}

</span>
{getLanguageValue(currentLanguage, languages)}
</BodyText>
</Box>
<Box
sx={{
width: '43.75rem',
justifyContent: 'flex-start',
display: 'flex',
}}
>
<ThemeProvider theme={BaseTheme}>
<LanguageSelector
currentLanguage={currentLanguage}
setCurrentLanguage={setCurrentLanguage}
languages={languages}
isDirty={isDirty}
isSubmitting={isSubmitting}
/>
</ThemeProvider>

<Button
disabled={!isValid || !isDirty || isSubmitting}
type="submit"
name="request_type"
value="update"
sx={{
...buttonStyles,
margin: '0 1.2rem',
}}
>
Save Section
</Button>
<Button
disabled={!isValid || !isDirty || isSubmitting}
type="submit"
name="request_type"
value="preview"
sx={{
...buttonStyles,
marginLeft: 'auto',
}}
>
<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=""
aria-hidden="true"
/>
Preview
</Button>
</Box>
<Box style={{ width: '25rem', display: 'flex' }}></Box>
</ThemeProvider>
</Box>
</AppBar>
);
};

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 (
<Select
value={currentLanguage}
onChange={handleSelectChange}
sx={{
height: '2.6rem',
borderRadius: '8px',
width: '9.375rem',
backgroundColor: colors.surface.gray[10],
border: 'none',
color: Palette.text.primary,
fontSize: '0.9rem',
cursor: 'pointer',
'& .MuiSelect-icon': {
color: Palette.text.primary,
},
}}
renderValue={(value) => {
const completed = false; // todo: Replace with real "completed" boolean value once it is available.
return (
<span>
<When condition={completed}>
<FontAwesomeIcon style={{ marginRight: '0.3rem' }} icon={faCheck} />
</When>
{languages.find((language) => language.code === value)?.name}
<Unless condition={completed}>
<StatusCircle required={true} />
</Unless>
</span>
);
}}
>
{languages.map((language) => {
const completed = false; // todo: Replace with the real "completed" boolean values once they are available.
return (
<MenuItem value={language.code} key={language.code}>
<When condition={completed}>
<FontAwesomeIcon style={{ marginRight: '0.3rem' }} icon={faCheck} />
</When>
{language.name}
<Unless condition={completed}>
<StatusCircle required={true} />
</Unless>
</MenuItem>
);
})}
</Select>
);
};

export default AuthoringBottomNav;
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React from 'react';
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;
description: string;
rich_description: string;
banner_filename: string;
status_block: string[];
title: string;
icon_name: string;
metadata_value: string;
send_report: boolean;
slug: string;
request_type: string;
}

export const AuthoringContext = () => {
const fetcher = useFetcher();
const locationArray = window.location.href.split('/');
const slug = locationArray[locationArray.length - 1];
Comment on lines +28 to +29
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if you realized, but you're extracting all this information just to reconstruct your current location - try useLocation() instead.

Suggested change
const locationArray = window.location.href.split('/');
const slug = locationArray[locationArray.length - 1];
const { pathname } = useLocation();

const defaultDateValue = dayjs(new Date(1970, 0, 1));
const engagementUpdateForm = useForm<EngagementUpdateData>({
defaultValues: {
id: 0,
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: '',
send_report: undefined,
slug: '',
request_type: '',
},
mode: 'onSubmit',
reValidateMode: 'onChange',
});
const onSubmit = async (data: EngagementUpdateData) => {
fetcher.submit(
createSearchParams({
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:
'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,
send_report: getSendReportValue(data.send_report),
slug: data.slug,
request_type: data.request_type,
}),
{
method: 'post',
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 }} />
</FormProvider>
);
};
Loading
Loading