Skip to content

Commit

Permalink
feat(frontend): handle unthemed documents (#287)
Browse files Browse the repository at this point in the history
* wip

* feat(frontend): handle unthemed documents

* rename

* rename

* fix(frontend): update layout

* fix(frontend): review

* fix: review

* fix: review

* fix: review
  • Loading branch information
Lionel authored Feb 4, 2021
1 parent 4fa383b commit 9ba8502
Show file tree
Hide file tree
Showing 7 changed files with 467 additions and 4 deletions.
56 changes: 56 additions & 0 deletions targets/frontend/src/components/forms/ContentPicker/ThemePicker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import PropTypes from "prop-types";
import React from "react";
import { Controller } from "react-hook-form";
import { Alert, Close, Text } from "theme-ui";

import { ThemeSearch } from "./ThemeSearch";

function ThemePicker({ ...props }) {
return (
<Controller
{...props}
// eslint-disable-next-line no-unused-vars
render={({ ref, ...renderProps }) => {
if (renderProps.value) {
return (
<Alert
variant="highlight"
sx={{
minWidth: 0,
p: "xxsmall",
paddingRight: "medium",
position: "relative",
}}
>
<Text
sx={{
display: "block",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{renderProps.value.title}
</Text>
<Close
sx={{ position: "absolute", right: 0 }}
onClick={() => {
renderProps.onChange("");
}}
/>
</Alert>
);
}
return <ThemeSearch {...renderProps} />;
}}
/>
);
}

ThemePicker.propTypes = {
disabled: PropTypes.bool,
};

const MemoThemePicker = React.memo(ThemePicker);

export { MemoThemePicker as ThemePicker };
169 changes: 169 additions & 0 deletions targets/frontend/src/components/forms/ContentPicker/ThemeSearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/** @jsxImportSource theme-ui */

import { SOURCES } from "@socialgouv/cdtn-sources";
import PropTypes from "prop-types";
import { useEffect, useState } from "react";
import Autosuggest from "react-autosuggest";
import { useDebouncedState } from "src/hooks/index";
import { Box, Input, Text } from "theme-ui";
import { useQuery } from "urql";

const sources = [SOURCES.THEMES];

const AUTOSUGGEST_MAX_RESULTS = 15;

const searchThemesQuery = `
query searchThemes($sources: [String!]! = "", $search: String = "") {
documents(where: {
title: {_ilike: $search},
source: {_in: $sources},
}, limit: ${AUTOSUGGEST_MAX_RESULTS}) {
source
title
cdtnId: cdtn_id
themeDocuments: relation_a_aggregate(where: {type: {_eq: "theme-content"}}) {aggregate{count}}
parentRelation: relation_b(where: {type: {_eq: "theme"}}) {
document: a {
title
}
}
}
}
`;

export function ThemeSearch({ onChange }) {
const [suggestions, setSuggestions] = useState([]);
const [inputSearchValue, setInputSearchValue] = useState("");
const [searchValue, , setDebouncedSearchValue] = useDebouncedState("", 500);

const [results] = useQuery({
pause: searchValue.length < 3,
query: searchThemesQuery,
variables: {
search: `%${searchValue}%`,
sources,
},
});

useEffect(() => {
setSuggestions(results.data?.documents || []);
}, [results.data]);

const onSearchValueChange = (event, { newValue }) => {
setInputSearchValue(newValue);
};

const onSuggestionSelected = (
event,
{ suggestion: { cdtnId, source, title = null, themeDocuments } }
) => {
const position = themeDocuments.aggregate.count;
onChange({ cdtnId, position, source, title });
};

const onSuggestionsFetchRequested = async ({ value }) => {
setDebouncedSearchValue(value);
setInputSearchValue(value);
};

const onSuggestionsClearRequested = () => {
setSuggestions([]);
};

const inputProps = {
onChange: onSearchValueChange,
placeholder: "Entrer le nom d'un thème et sélectionner le (ex: travail)",
value: inputSearchValue,
};
return (
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
onSuggestionsClearRequested={onSuggestionsClearRequested}
onSuggestionSelected={onSuggestionSelected}
getSuggestionValue={getSuggestionValue}
shouldRenderSuggestions={shouldRenderSuggestions}
renderInputComponent={renderInputComponent}
renderSuggestion={renderSuggestion}
renderSuggestionsContainer={renderSuggestionsContainer}
inputProps={inputProps}
alwaysRenderSuggestions
/>
);
}

ThemeSearch.propTypes = {
onChange: PropTypes.func.isRequired,
};

const renderInputComponent = (inputProps) => (
<Input {...inputProps} sx={{ fontSize: "small", padding: "xxsmall" }} />
);

function shouldRenderSuggestions(value) {
return value.trim().length >= 2;
}

const getSuggestionValue = (content) => content.title;

function renderSuggestion(content) {
const parent = content.parentRelation[0]?.document?.title;
const parentTitle = parent;
return (
<Box sx={{ lineHeight: 1.2 }}>
<Text sx={{ color: "muted", fontSize: "small", fontWeight: "300" }}>
{parentTitle}
</Text>
<Text sx={{ display: "block" }}>{content.title}</Text>
</Box>
);
}

function renderSuggestionsContainer({ containerProps, children }) {
return (
<Box
{...containerProps}
sx={{
position: "relative",
}}
>
<Box
sx={{
".react-autosuggest__suggestion--highlighted": {
bg: "info",
},
'[class*="container--open"] &': {
border: "1px solid",
borderColor: "neutral",
borderRadius: "4px",
boxShadow: "medium",
left: 0,
maxHeight: "300px",
overflow: "scroll",
position: "absolute",
right: 0,
top: "4px",
},
bg: "white",
li: {
":nth-of-type(2n + 1):not(.react-autosuggest__suggestion--highlighted)": {
bg: "highlight",
},
cursor: "pointer",
m: "0",
p: "xxsmall",
},

ul: {
listStyleType: "none",
m: "0",
p: "0",
},
zIndex: 1,
}}
>
{children}
</Box>
</Box>
);
}
84 changes: 84 additions & 0 deletions targets/frontend/src/components/home/UnThemedContent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { SOURCES } from "@socialgouv/cdtn-sources";
import Link from "next/link";
import { RELATIONS } from "src/lib/relations";
import { Box, Card, Flex, Message, NavLink, Text } from "theme-ui";
import { useQuery } from "urql";

export const getUnthemedContentQuery = `
query getUnthemed($themeSources: [String!]!) {
documents (where: {
is_available: {_eq: true}
is_published: {_eq: true}
source: {
_in: $themeSources
}
_and: [
{_not: {
relation_b: {type: {_eq: "${RELATIONS.THEME_CONTENT}"} a :{source: {_eq: "${SOURCES.THEMES}"}} }
}}
{_not: {document: {_has_key: "split"}}}
]
}) {
source
slug
title
cdtnId: cdtn_id
}
}
`;

export const THEMABLE_CONTENT = [
SOURCES.CONTRIBUTIONS,
SOURCES.EDITORIAL_CONTENT,
SOURCES.EXTERNALS,
SOURCES.LETTERS,
SOURCES.SHEET_MT_PAGE,
SOURCES.SHEET_SP,
SOURCES.THEMATIC_FILES,
SOURCES.TOOLS,
];

export function UnThemedContent() {
const [result] = useQuery({
query: getUnthemedContentQuery,
variables: {
themeSources: THEMABLE_CONTENT,
},
});

const { data, fetching, error } = result;

if (fetching) {
return null;
}
if (error) {
return (
<Message>
<pre>{JSON.stringify(error, 2)}</pre>
</Message>
);
}
return (
<Link href="/unthemed" passHref>
<NavLink>
<Card>
<Flex sx={{ justifyContent: "flex-end" }}>
<Text
color="secondary"
sx={{
fontSize: "xxlarge",
fontWeight: "600",
}}
>
{data.documents.length}
</Text>
</Flex>
<Box>
<Text> Contenus non thémés</Text>
</Box>
</Card>
</NavLink>
</Link>
);
}
5 changes: 5 additions & 0 deletions targets/frontend/src/components/layout/Nav.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ export function Nav() {
Fichiers
</ActiveLink>
</Li>
<Li>
<ActiveLink href="/unthemed" passHref>
Contenus sans thème
</ActiveLink>
</Li>
</List>
</Box>
</Box>
Expand Down
1 change: 0 additions & 1 deletion targets/frontend/src/components/login/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ const LoginForm = ({ authenticate, resetPassword, onSuccess }) => {
label="Mot de passe"
name="password"
type="password"
placeholder="•••••••••"
defaultValue={password}
onChange={(e) => setPassword(e.target.value)}
/>
Expand Down
12 changes: 9 additions & 3 deletions targets/frontend/src/pages/index.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import { GitlabButton } from "src/components/button/GitlabButton";
import { UnThemedContent } from "src/components/home/UnThemedContent";
import { Layout } from "src/components/layout/auth.layout";
import { Inline } from "src/components/layout/Inline";
import { Stack } from "src/components/layout/Stack";
import { withCustomUrqlClient } from "src/hoc/CustomUrqlClient";
import { withUserProvider } from "src/hoc/UserProvider";
import { Text } from "theme-ui";
import { Heading } from "theme-ui";

export function IndexPage() {
return (
<Layout title="Home">
<Layout title="Administration des contenus et gestion des alertes">
<Stack>
<Text>Administration des contenus et gestion des alertes</Text>
<Inline>
<GitlabButton env="prod">Mettre à jour la prod</GitlabButton>
<GitlabButton env="preprod">Mettre à jour la preprod</GitlabButton>
</Inline>
<Heading as="h2" sx={{ fontSize: "large" }}>
Tableau de bord
</Heading>
<Inline>
<UnThemedContent />
</Inline>
</Stack>
</Layout>
);
Expand Down
Loading

0 comments on commit 9ba8502

Please sign in to comment.