fronts-client/src/components/FrontsEdit/CollectionComponents/Collection.tsx (439 lines of code) (raw):
import { Dispatch } from 'types/Store';
import React, { Fragment } from 'react';
import { connect } from 'react-redux';
import { HeadlineContentButton } from 'components/CollectionDisplay';
import CollectionDisplay from 'components/CollectionDisplay';
import CollectionNotification from 'components/CollectionNotification';
import Button from 'components/inputs/ButtonDefault';
import { AlsoOnDetail } from 'types/Collection';
import {
publishCollection,
discardDraftChangesToCollection,
openCollectionsAndFetchTheirArticles,
} from 'actions/Collections';
import { actions, selectors } from 'bundles/collectionsBundle';
import {
selectHasUnpublishedChanges,
selectCollectionHasPrefill,
selectCollectionIsHidden,
selectCollectionDisplayName,
selectCollectionCanMoveToRelativeIndex,
selectCollectionTargetedRegions,
} from 'selectors/frontsSelectors';
import { selectIsCollectionLocked } from 'selectors/collectionSelectors';
import type { State } from 'types/State';
import { CardSets, Group } from 'types/Collection';
import {
createSelectCollectionStageGroups,
createSelectCollectionEditWarning,
createSelectPreviouslyLiveArticlesInCollection,
} from 'selectors/shared';
import {
selectIsCollectionOpen,
editorCloseCollections,
selectHasMultipleFrontsOpen,
createSelectDoesCollectionHaveOpenForms,
} from 'bundles/frontsUI';
import { fetchCardReferencedEntitiesForCollections } from 'actions/Collections';
import { cardSets } from 'constants/fronts';
import CollectionMetaContainer from 'components/collection/CollectionMetaContainer';
import ButtonCircularCaret from 'components/inputs/ButtonCircularCaret';
import { theme, styled } from 'constants/theme';
import EditModeVisibility from 'components/util/EditModeVisibility';
import { fetchPrefill } from 'bundles/capiFeedBundle';
import LoadingGif from 'images/icons/loading.gif';
import OpenFormsWarning from './OpenFormsWarning';
import { selectors as editionsIssueSelectors } from '../../../bundles/editionsIssueBundle';
import { moveFrontCollection } from '../../../actions/Editions';
interface CollectionPropsBeforeState {
id: string;
children: (
group: Group,
isUneditable: boolean,
groupIds: string[],
groupsData: Group[],
showGroupName?: boolean,
) => React.ReactNode;
alsoOn: { [id: string]: AlsoOnDetail };
frontId: string;
browsingStage: CardSets;
priority: string;
isFeast?: boolean;
}
type CollectionProps = CollectionPropsBeforeState & {
publishCollection: (collectionId: string, frontId: string) => Promise<void>;
discardDraftChangesToCollection: (
collectionId: string,
) => Promise<void | string[]>;
hasUnpublishedChanges: boolean;
canPublish: boolean;
groups: Group[];
previousGroup: Group;
displayEditWarning: boolean;
isCollectionLocked: boolean;
isOpen: boolean;
hasOpenForms: boolean;
hasContent: boolean;
hasMultipleFrontsOpen: boolean;
onChangeOpenState: (id: string, isOpen: boolean) => void;
fetchPreviousCollectionArticles: (id: string) => void;
fetchPrefill: (id: string) => void;
hasPrefill: boolean;
setHidden: (id: string, isHidden: boolean) => void;
isHidden: boolean;
displayName: string;
targetedRegions: string[];
canMoveUp: boolean;
canMoveDown: boolean;
moveFrontCollection: (
frontId: string,
id: string,
direction: 'up' | 'down',
) => void;
};
interface CollectionState {
showOpenFormsWarning: boolean;
isPreviouslyOpen: boolean;
isLaunching: boolean;
}
const PreviouslyCollectionContainer = styled.div``;
const PreviouslyCollectionToggle = styled(CollectionMetaContainer)`
align-items: center;
font-size: 14px;
font-weight: normal;
padding-top: 0.25em;
justify-content: unset;
border-top: 1px solid ${theme.colors.greyMediumLight};
`;
const PreviouslyGroupsWrapper = styled.div`
padding-top: 0.25em;
opacity: 0.5;
`;
const PreviouslyCollectionInfo = styled.div`
background: ${theme.colors.greyVeryLight};
color: ${theme.colors.blackDark};
padding: 4px 6px;
font-size: 14px;
`;
const LoadingImageBox = styled.div`
min-width: 50px;
`;
const PreviouslyCircularCaret = styled(ButtonCircularCaret)`
height: 15px;
width: 15px;
background-color: ${theme.colors.greyMediumLight};
margin-left: 6px;
svg {
height: 15px;
width: 15px;
}
`;
const OpenFormsWarningContainer = styled.div`
position: absolute;
top: 100%;
left: 0;
border: 1px solid ${theme.base.colors.brandColor};
background-color: ${theme.base.colors.brandColorLight};
padding: 10px;
font-family: TS3TextSans;
font-weight: normal;
font-size: 12px;
line-height: 14px;
`;
const ActionButtonsContainer = styled.div`
display: flex;
`;
const MoveButtonsContainer = styled.div`
margin-right: 8px;
display: flex;
gap: 3px;
`;
class Collection extends React.Component<CollectionProps, CollectionState> {
public state = {
isPreviouslyOpen: false,
isLaunching: false,
showOpenFormsWarning: false,
};
// added to prevent setState call on unmounted component
public isComponentMounted = false;
public componentDidMount() {
this.isComponentMounted = true;
}
public componentWillUnmount() {
this.isComponentMounted = false;
}
public togglePreviouslyOpen = () => {
const { isPreviouslyOpen } = this.state;
if (!isPreviouslyOpen) {
this.props.fetchPreviousCollectionArticles(this.props.id);
}
this.setState({ isPreviouslyOpen: !isPreviouslyOpen });
};
public startPublish = (id: string, frontId: string) => {
this.setState({ isLaunching: true });
this.props.publishCollection(id, frontId).then((res) => {
if (this.isComponentMounted) {
this.setState({ isLaunching: false });
}
});
};
public render() {
const {
id,
frontId,
children,
alsoOn,
groups,
previousGroup: previousGroup,
browsingStage,
hasUnpublishedChanges,
canPublish = true,
displayEditWarning,
isCollectionLocked,
isOpen,
onChangeOpenState,
hasMultipleFrontsOpen,
discardDraftChangesToCollection: discardDraftChanges,
hasPrefill,
isHidden,
targetedRegions,
hasContent,
hasOpenForms,
isFeast,
} = this.props;
const { isPreviouslyOpen, isLaunching } = this.state;
const isUneditable = isCollectionLocked || browsingStage !== cardSets.draft;
const groupIds = groups.map((group) => group.uuid);
return (
<>
<CollectionDisplay
frontId={frontId}
id={id}
browsingStage={browsingStage}
isUneditable={isUneditable}
isLocked={isCollectionLocked}
isOpen={isOpen}
hasMultipleFrontsOpen={hasMultipleFrontsOpen}
onChangeOpenState={() => onChangeOpenState(id, isOpen)}
headlineContent={
hasUnpublishedChanges &&
canPublish && (
<Fragment>
<EditModeVisibility visibleMode="editions">
{!isFeast && (
<HeadlineContentButton
priority="default"
onClick={() => this.props.setHidden(id, !isHidden)}
title="Toggle the visibility of this container in this issue."
>
{isHidden ? 'Unhide' : 'Hide'}
</HeadlineContentButton>
)}
{isFeast && (
<>
{targetedRegions?.length > 0 ? '🇺🇸 ' : ''}{' '}
<MoveButtonsContainer>
<ButtonCircularCaret
small
openDir="up"
disabled={!this.props.canMoveUp}
onClick={() =>
this.props.moveFrontCollection(
this.props.frontId,
this.props.id,
'up',
)
}
/>
<ButtonCircularCaret
small
disabled={!this.props.canMoveDown}
onClick={() =>
this.props.moveFrontCollection(
this.props.frontId,
this.props.id,
'down',
)
}
/>
</MoveButtonsContainer>
</>
)}
{hasPrefill && (
<HeadlineContentButton
data-testid="prefill-button"
priority="default"
onClick={() => this.props.fetchPrefill(id)}
title="Get suggested articles for this collection"
>
Suggest
</HeadlineContentButton>
)}
</EditModeVisibility>
<ActionButtonsContainer
onMouseEnter={this.showOpenFormsWarning}
onMouseLeave={this.hideOpenFormsWarning}
>
{hasOpenForms && this.state.showOpenFormsWarning && (
<OpenFormsWarningContainer>
<OpenFormsWarning collectionId={id} frontId={frontId} />
</OpenFormsWarningContainer>
)}
<EditModeVisibility visibleMode="fronts">
<Button
size="l"
priority="default"
onClick={() => discardDraftChanges(id)}
tabIndex={-1}
data-testid="collection-discard-button"
>
Discard
</Button>
<Button
size="l"
priority="primary"
onClick={() => this.startPublish(id, frontId)}
tabIndex={-1}
disabled={isLaunching}
data-testid="collection-launch-button"
>
{isLaunching ? (
<LoadingImageBox>
<img src={LoadingGif} />
</LoadingImageBox>
) : (
'Launch'
)}
</Button>
</EditModeVisibility>
</ActionButtonsContainer>
</Fragment>
)
}
metaContent={
alsoOn[id].fronts.length || displayEditWarning ? (
<CollectionNotification
displayEditWarning={displayEditWarning}
alsoOn={alsoOn[id]}
/>
) : null
}
>
{groups.map((group) =>
children(group, isUneditable, groupIds, groups, true),
)}
{hasContent && (
<EditModeVisibility visibleMode="fronts">
<PreviouslyCollectionContainer data-testid="previously">
<PreviouslyCollectionToggle
onClick={this.togglePreviouslyOpen}
data-testid="previously-toggle"
>
Recently removed from launched front
<PreviouslyCircularCaret active={isPreviouslyOpen} />
</PreviouslyCollectionToggle>
{isPreviouslyOpen && (
<>
<PreviouslyCollectionInfo>
This contains the 5 most recently deleted articles from
the live front. If the deleted articles were never
launched they will not appear here.
</PreviouslyCollectionInfo>
<PreviouslyGroupsWrapper>
{children(previousGroup, true, groupIds, groups, false)}
</PreviouslyGroupsWrapper>
</>
)}
</PreviouslyCollectionContainer>
</EditModeVisibility>
)}
</CollectionDisplay>
</>
);
}
private showOpenFormsWarning = () =>
this.setState({ showOpenFormsWarning: true });
private hideOpenFormsWarning = () =>
this.setState({ showOpenFormsWarning: false });
}
const createMapStateToProps = () => {
const selectCollectionStageGroups = createSelectCollectionStageGroups();
const selectEditWarning = createSelectCollectionEditWarning();
const selectPreviously = createSelectPreviouslyLiveArticlesInCollection();
const selectHasOpenForms = createSelectDoesCollectionHaveOpenForms();
return (
state: State,
{
browsingStage,
id: collectionId,
priority,
frontId,
}: CollectionPropsBeforeState,
) => ({
canMoveUp: selectCollectionCanMoveToRelativeIndex(
state,
frontId,
collectionId,
-1,
),
canMoveDown: selectCollectionCanMoveToRelativeIndex(
state,
frontId,
collectionId,
1,
),
isHidden: selectCollectionIsHidden(state, collectionId),
displayName: selectCollectionDisplayName(state, collectionId),
targetedRegions: selectCollectionTargetedRegions(state, collectionId),
hasPrefill: selectCollectionHasPrefill(state, collectionId),
hasUnpublishedChanges: selectHasUnpublishedChanges(state, {
collectionId,
}),
isCollectionLocked: selectIsCollectionLocked(state, collectionId),
groups: selectCollectionStageGroups(state, {
collectionSet: browsingStage,
collectionId,
}),
previousGroup: selectPreviously(state, {
collectionId,
}),
displayEditWarning: selectEditWarning(state, {
collectionId,
}),
isOpen: selectIsCollectionOpen(state, collectionId),
hasMultipleFrontsOpen: selectHasMultipleFrontsOpen(state, priority),
hasContent: !!selectors.selectById(state, collectionId),
hasOpenForms: selectHasOpenForms(state, { collectionId, frontId }),
isFeast: editionsIssueSelectors.selectAll(state)?.platform === 'feast',
});
};
const mapDispatchToProps = (
dispatch: Dispatch,
{ browsingStage, frontId }: CollectionPropsBeforeState,
) => ({
fetchPrefill: (id: string) => dispatch(fetchPrefill(id)),
setHidden: (id: string, isHidden: boolean) =>
dispatch(actions.setHiddenAndPersist(id, isHidden)),
publishCollection: (id: string) => dispatch(publishCollection(id, frontId)),
discardDraftChangesToCollection: (id: string) =>
dispatch(discardDraftChangesToCollection(id)),
onChangeOpenState: (id: string, isOpen: boolean) => {
if (isOpen) {
dispatch(editorCloseCollections(id));
} else {
dispatch(
openCollectionsAndFetchTheirArticles([id], frontId, browsingStage),
);
}
},
fetchPreviousCollectionArticles: (id: string) => {
dispatch(
fetchCardReferencedEntitiesForCollections([id], cardSets.previously),
);
},
moveFrontCollection: (
frontId: string,
id: string,
direction: 'up' | 'down',
) => dispatch(moveFrontCollection(frontId, id, direction)),
});
export default connect(createMapStateToProps, mapDispatchToProps)(Collection);