-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(frontend): add confirmation before update docs published status (#…
…353) * feat(frontend): add confirmation before update a document published status * fix: review * fix: review * fix: review * fix: review * fix(frontend): handle onsubmit for search filter * test * revert previosu
- Loading branch information
Lionel
authored
Mar 11, 2021
1 parent
b62c7fd
commit eb3525f
Showing
7 changed files
with
587 additions
and
360 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import PropTypes from "prop-types"; | ||
import { useState } from "react"; | ||
import { useSelectionContext } from "src/pages/contenus"; | ||
import { Box, Text } from "theme-ui"; | ||
|
||
import { Button } from "../button"; | ||
import { Dialog } from "../dialog"; | ||
import { Inline } from "../layout/Inline"; | ||
import { Stack } from "../layout/Stack"; | ||
|
||
export function DocumentsListActions({ onUpdatePublication }) { | ||
const [selectedItems] = useSelectionContext(); | ||
const [showPublishDialog, setPublishDialogVisible] = useState(false); | ||
const openPublishDialog = () => setPublishDialogVisible(true); | ||
const closePublishDialog = () => setPublishDialogVisible(false); | ||
|
||
function updatePublication() { | ||
const docEntries = Object.entries(selectedItems); | ||
onUpdatePublication(docEntries); | ||
closePublishDialog(); | ||
} | ||
|
||
return ( | ||
<Box> | ||
<Dialog | ||
isOpen={showPublishDialog} | ||
onDismiss={closePublishDialog} | ||
aria-label="Modifier le statut de publication" | ||
> | ||
<Stack> | ||
<Stack> | ||
<Text> | ||
Êtes vous sûr de vouloir modifier la publication des | ||
contenus ? | ||
</Text> | ||
<Recap publications={selectedItems} /> | ||
</Stack> | ||
<Inline> | ||
<Button onClick={updatePublication} size="small"> | ||
Modifier la publication des contenus | ||
</Button> | ||
<Button variant="link" onClick={closePublishDialog} size="small"> | ||
Annuler | ||
</Button> | ||
</Inline> | ||
</Stack> | ||
</Dialog> | ||
<Button | ||
type="button" | ||
outline | ||
size="small" | ||
variant="secondary" | ||
disabled={Object.keys(selectedItems).length === 0} | ||
onClick={openPublishDialog} | ||
> | ||
Modifier | ||
</Button> | ||
</Box> | ||
); | ||
} | ||
DocumentsListActions.propTypes = { | ||
onUpdatePublication: PropTypes.func.isRequired, | ||
}; | ||
function Recap({ publications }) { | ||
const items = Object.entries(publications).reduce( | ||
(state, [, published]) => { | ||
if (published) { | ||
state.published += 1; | ||
} else { | ||
state.unpublished += 1; | ||
} | ||
return state; | ||
}, | ||
{ published: 0, unpublished: 0 } | ||
); | ||
return ( | ||
<Box> | ||
<Text sx={{ fontWeight: "heading" }}>Détails</Text> | ||
<ul> | ||
{items.published > 0 && ( | ||
<li> | ||
<strong>{items.published}</strong> éléments à publier | ||
</li> | ||
)} | ||
{items.unpublished > 0 && ( | ||
<li> | ||
<strong>{items.unpublished}</strong> éléments à dépublier | ||
</li> | ||
)} | ||
</ul> | ||
</Box> | ||
); | ||
} | ||
|
||
Recap.propTypes = { | ||
publications: PropTypes.object, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import Link from "next/link"; | ||
import { useRouter } from "next/router"; | ||
import PropTypes from "prop-types"; | ||
import { useCallback, useMemo } from "react"; | ||
import { IoMdAdd } from "react-icons/io"; | ||
import { useSelectionContext } from "src/pages/contenus"; | ||
import { Card, Flex, Message } from "theme-ui"; | ||
import { useMutation, useQuery } from "urql"; | ||
|
||
import { Button } from "../button"; | ||
import { Stack } from "../layout/Stack"; | ||
import { Pagination } from "../pagination"; | ||
import { DocumentsListActions } from "./Actions"; | ||
import { DocumentList } from "./List"; | ||
import { SearchFilters } from "./SearchFilters"; | ||
|
||
export function DocumentListContainer({ initialFilterValues }) { | ||
const router = useRouter(); | ||
const context = useMemo(() => ({ additionalTypenames: ["documents"] }), []); | ||
const [, setSelectedItems] = useSelectionContext(); | ||
const [, updatePublication] = useMutation(updatePublicationMutation); | ||
const updatePublicationStatus = useCallback( | ||
(docEntries) => { | ||
docEntries.forEach(([cdtnId, isPublished]) => { | ||
updatePublication({ cdtnId, isPublished }); | ||
}); | ||
setSelectedItems({}); | ||
}, | ||
[updatePublication, setSelectedItems] | ||
); | ||
|
||
const updateUrl = useCallback( | ||
(filterValues) => { | ||
// we reset changed published value if search critera change | ||
setSelectedItems({}); | ||
const query = { ...filterValues, page: 0 }; | ||
router.push({ pathname: router.route, query }, undefined, { | ||
shallow: true, | ||
}); | ||
}, | ||
[router, setSelectedItems] | ||
); | ||
|
||
const [result] = useQuery({ | ||
context, | ||
query: searchDocumentQuery, | ||
variables: { | ||
limit: initialFilterValues.itemsPerPage, | ||
offset: initialFilterValues.page * initialFilterValues.itemsPerPage, | ||
published: | ||
initialFilterValues.published === "yes" | ||
? [true] | ||
: initialFilterValues.published === "no" | ||
? [false] | ||
: [true, false], | ||
search: `%${initialFilterValues.q}%`, | ||
source: initialFilterValues.source || null, | ||
}, | ||
}); | ||
|
||
const { fetching, error, data } = result; | ||
|
||
if (error) { | ||
return <Message variant="primary">{error.message}</Message>; | ||
} | ||
return ( | ||
<Stack> | ||
<Flex sx={{ justifyContent: "flex-end" }}> | ||
<Link href="/contenus/create/" passHref> | ||
<Button as="a" size="small" outline variant="secondary"> | ||
<IoMdAdd | ||
sx={{ | ||
height: "iconSmall", | ||
mr: "xxsmall", | ||
width: "iconSmall", | ||
}} | ||
/> | ||
Ajouter un contenu | ||
</Button> | ||
</Link> | ||
</Flex> | ||
<Card sx={{ position: "sticky", top: 0 }} bg="white"> | ||
<SearchFilters | ||
initialValues={initialFilterValues} | ||
onSearchUpdate={updateUrl} | ||
/> | ||
</Card> | ||
{fetching ? ( | ||
<>chargement...</> | ||
) : data.documents.length ? ( | ||
<form> | ||
<DocumentList documents={data.documents} /> | ||
<DocumentsListActions onUpdatePublication={updatePublicationStatus} /> | ||
<Pagination | ||
count={data.documents_aggregate.aggregate.count} | ||
currentPage={initialFilterValues.page} | ||
pageSize={initialFilterValues.itemsPerPage} | ||
/> | ||
</form> | ||
) : ( | ||
<p>Pas de résultats.</p> | ||
)} | ||
</Stack> | ||
); | ||
} | ||
|
||
DocumentListContainer.propTypes = { | ||
initialFilterValues: PropTypes.shape({ | ||
itemsPerPage: PropTypes.number, | ||
page: PropTypes.number, | ||
published: PropTypes.oneOf(["all", "yes", "no"]), | ||
q: PropTypes.string, | ||
source: PropTypes.string, | ||
}), | ||
}; | ||
|
||
const searchDocumentQuery = ` | ||
query documents($source: String, $search: String!, $published: [Boolean!]!, $offset: Int = 0, $limit: Int = 50) { | ||
documents(where: { | ||
_not: { | ||
document: {_has_key: "split"} | ||
} | ||
_and: { | ||
source: {_eq: $source, _neq: "code_du_travail"} | ||
title: {_ilike: $search} | ||
is_published: {_in: $published} | ||
} | ||
}, | ||
offset: $offset, limit: $limit, order_by: [{source: asc}, {slug: asc}]) { | ||
id: initial_id | ||
cdtnId: cdtn_id | ||
title | ||
source | ||
isPublished: is_published | ||
} | ||
documents_aggregate(where: { | ||
_not: { | ||
document: {_has_key: "split"} | ||
} | ||
_and: { | ||
source: {_eq: $source, _neq: "code_du_travail"} | ||
title: {_ilike: $search}, | ||
is_published: {_in: $published} | ||
} | ||
}) { | ||
aggregate { | ||
count | ||
} | ||
} | ||
} | ||
`; | ||
|
||
const updatePublicationMutation = ` | ||
mutation publication($cdtnId:String!, $isPublished:Boolean!) { | ||
document: update_documents_by_pk( | ||
_set: {is_published: $isPublished} | ||
pk_columns: { cdtn_id: $cdtnId } | ||
) { | ||
cdtnId:cdtn_id, isPublished:is_published | ||
} | ||
} | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
/** @jsxImportSource theme-ui */ | ||
|
||
import { SOURCES } from "@socialgouv/cdtn-sources"; | ||
import Link from "next/link"; | ||
import PropTypes from "prop-types"; | ||
import { IoIosCheckmark, IoIosClose } from "react-icons/io"; | ||
import { useSelectionContext } from "src/pages/contenus"; | ||
import { theme } from "src/theme"; | ||
import { Box, NavLink } from "theme-ui"; | ||
|
||
export function DocumentList({ documents }) { | ||
return ( | ||
<table> | ||
<thead> | ||
<tr> | ||
<th /> | ||
<th sx={{ textAlign: "left" }}>Document</th> | ||
<th sx={{ textAlign: "left" }}>Publié</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{documents.map((doc) => ( | ||
<DocumentRow key={doc.cdtnId} document={doc} /> | ||
))} | ||
</tbody> | ||
</table> | ||
); | ||
} | ||
DocumentList.propTypes = { | ||
documents: PropTypes.arrayOf( | ||
PropTypes.shape({ | ||
cdtnId: PropTypes.string.isRequired, | ||
isPublished: PropTypes.bool.isRequired, | ||
source: PropTypes.string.isRequired, | ||
title: PropTypes.string.isRequired, | ||
}) | ||
).isRequired, | ||
}; | ||
|
||
const DocumentRow = function DocumentRow({ | ||
document: { cdtnId, source, title, isPublished }, | ||
}) { | ||
const [selectedItems, setSelectedItems] = useSelectionContext(); | ||
const updatePublishedRef = () => { | ||
// eslint-disable-next-line no-prototype-builtins | ||
if (selectedItems.hasOwnProperty(cdtnId)) { | ||
delete selectedItems[cdtnId]; | ||
setSelectedItems({ ...selectedItems }); | ||
} else { | ||
setSelectedItems({ ...selectedItems, [cdtnId]: !isPublished }); | ||
} | ||
}; | ||
|
||
return ( | ||
<tr> | ||
<td> | ||
<input | ||
name={cdtnId} | ||
onChange={updatePublishedRef} | ||
defaultChecked={ | ||
// eslint-disable-next-line no-prototype-builtins | ||
selectedItems.hasOwnProperty(cdtnId) ? !isPublished : isPublished | ||
} | ||
sx={checkboxStyles} | ||
type="checkbox" | ||
/> | ||
</td> | ||
<td> | ||
<Link href={sourceToRoute({ cdtnId, source })} passHref shallow> | ||
<NavLink> | ||
<span | ||
sx={{ | ||
color: isPublished ? theme.colors.link : theme.colors.muted, | ||
}} | ||
> | ||
{source} › {title} | ||
</span> | ||
</NavLink> | ||
</Link> | ||
</td> | ||
<td sx={{ textAlign: "center" }}> | ||
{isPublished ? ( | ||
<Box sx={{ color: "muted" }}> | ||
<IoIosCheckmark /> | ||
</Box> | ||
) : ( | ||
<Box sx={{ color: "critical" }}> | ||
<IoIosClose /> | ||
</Box> | ||
)} | ||
</td> | ||
</tr> | ||
); | ||
}; | ||
|
||
DocumentRow.propTypes = { | ||
document: PropTypes.shape({ | ||
cdtnId: PropTypes.string.isRequired, | ||
isPublished: PropTypes.bool.isRequired, | ||
source: PropTypes.string.isRequired, | ||
title: PropTypes.string.isRequired, | ||
}).isRequired, | ||
}; | ||
|
||
const sourceToRoute = ({ cdtnId, source }) => { | ||
switch (source) { | ||
case SOURCES.EDITORIAL_CONTENT: | ||
case SOURCES.HIGHLIGHTS: | ||
case SOURCES.PREQUALIFIED: | ||
return `/contenus/edit/${cdtnId}`; | ||
default: | ||
return `/contenus/${cdtnId}`; | ||
} | ||
}; | ||
|
||
const checkboxStyles = { | ||
cursor: "pointer", | ||
display: "block", | ||
m: "0 0 0 small", | ||
padding: 0, | ||
}; |
Oops, something went wrong.