Skip to content

Commit

Permalink
feat: add themes (#126)
Browse files Browse the repository at this point in the history
  • Loading branch information
UnbearableBear committed Oct 7, 2020
1 parent 9cdd3d0 commit db38184
Show file tree
Hide file tree
Showing 38 changed files with 3,230 additions and 775 deletions.
17 changes: 17 additions & 0 deletions shared/id-generator/package.json
@@ -0,0 +1,17 @@
{
"name": "@shared/id-generator",
"version": "1.0.0",
"dependencies": {
"uuid": "^8.3.0",
"xxhashjs": "^0.2.2"
},
"main": "src/index.js",
"module": "src/index.js",
"types": "src/index.d.ts",
"private": true,
"scripts": {},
"devDependencies": {
"@types/uuid": "^8.3.0",
"@types/xxhashjs": "^0.2.2"
}
}
8 changes: 8 additions & 0 deletions shared/id-generator/src/index.d.ts
@@ -0,0 +1,8 @@
export as namespace idGenerator

export type generatedId = {
cdtn_id: string
initial_id: string
};


35 changes: 35 additions & 0 deletions shared/id-generator/src/index.js
@@ -0,0 +1,35 @@
import { v4 as uuidv4 } from "uuid";
import * as XXH from "xxhashjs";

const H = XXH.h64(0x1e7f);


export const MAX_ID_LENGTH = 10;

// use xxhash to hash source + newly generated UUID
/**
*
* @param {string} content
* @param {number} maxIdLength
* @returns {string}
*/
export const generateCdtnId = (content, maxIdLength = MAX_ID_LENGTH) =>
// save 64bits hash as Hexa string up to maxIdLength chars (can be changed later in case of collision)
// as the xxhash function ensure distribution property
H.update(content).digest().toString(16).slice(0, maxIdLength);

export const generateInitialId = uuidv4;

// Beware, you might be generating an already existing cdtn_id
/**
* @param {string} source
* @param {number} maxIdLength
* @returns {idGenerator.generatedId}
*/
export const generateIds = (source, maxIdLength = MAX_ID_LENGTH) => {
const uuid = uuidv4();
return {
cdtn_id: generateCdtnId(source + uuid, maxIdLength),
initial_id: generateInitialId(),
};
};
15 changes: 15 additions & 0 deletions shared/id-generator/tsconfig.json
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"downlevelIteration": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"lib": ["es2019"],
"baseUrl": ".",
}
}
1 change: 1 addition & 0 deletions targets/frontend/Dockerfile
Expand Up @@ -4,6 +4,7 @@ WORKDIR /app

COPY package.json yarn.lock /app/
COPY shared/graphql-client/package.json /app/shared/graphql-client/
COPY shared/id-generator/package.json /app/shared/id-generator/
COPY targets/frontend/package.json /app/targets/frontend/

RUN npx @socialgouv/yarn-workspace-focus-install --cwd targets/frontend --production -- --cache-folder /dev/shm/yarn
Expand Down
8 changes: 8 additions & 0 deletions targets/frontend/package.json
Expand Up @@ -13,12 +13,17 @@
"@sentry/integrations": "^5.23.0",
"@sentry/node": "^5.23.0",
"@shared/graphql-client": "^1.0.0",
"@shared/id-generator": "^1.0.0",
"@socialgouv/cdtn-slugify": "^4.30.0",
"@socialgouv/cdtn-sources": "^4.30.0",
"@socialgouv/matomo-next": "^1.1.2",
"@socialgouv/react-ui": "^4.17.0",
"@zeit/next-source-maps": "0.0.4-canary.1",
"ace-builds": "^1.4.12",
"argon2": "^0.27.0",
"cookie": "^0.4.1",
"d3": "^6.1.1",
"d3-hierarchy": "^2.0.0",
"diff": "^4.0.2",
"graphql": "^15.3.0",
"http-proxy-middleware": "^1.0.5",
Expand All @@ -31,12 +36,15 @@
"polished": "^3.6.7",
"react": "^16.13.1",
"react-ace": "^9.1.4",
"react-autosuggest": "^10.0.2",
"react-dom": "^16.13.1",
"react-hook-form": "^6.8.6",
"react-icons": "^3.11.0",
"react-is": "^16.13.1",
"react-sortable-hoc": "^1.11.0",
"semver": "^7.3.2",
"sentry-testkit": "^3.2.1",
"styled-components": "^5.1.1",
"theme-ui": "^0.3.1",
"unist-util-parents": "^1.0.3",
"unist-util-select": "^3.0.1",
Expand Down
4 changes: 2 additions & 2 deletions targets/frontend/src/components/alerts/AlertTitle.js
Expand Up @@ -25,7 +25,7 @@ export function AlertTitle({ alertId, info, ...props }) {
<IconButton variant="secondary">
<IoIosLink
aria-label="Voir la convention sur legifrance"
style={{ height: "1em", width: "1em" }}
style={{ height: "iconXSmall", width: "iconXSmall" }}
/>
</IconButton>
</a>
Expand All @@ -38,7 +38,7 @@ export function AlertTitle({ alertId, info, ...props }) {
>
<IoMdChatbubbles
aria-label="Voir les commentaires"
style={{ height: "1em", width: "1em" }}
style={{ height: "iconXSmall", width: "iconXSmall" }}
/>
</IconButton>
{showComment && <Comments alertId={alertId} />}
Expand Down
6 changes: 4 additions & 2 deletions targets/frontend/src/components/alerts/Status.js
Expand Up @@ -33,10 +33,12 @@ export function AlertStatus({ alertId }) {
En cours
</MenuItem>
<MenuItem onSelect={() => updateStatus("done")}>
<IoIosCheckmark style={{ height: "1.5em", width: "1.5em" }} /> Traité
<IoIosCheckmark style={{ height: "iconSmall", width: "iconSmall" }} />{" "}
Traité
</MenuItem>
<MenuItem onSelect={() => updateStatus("rejected")}>
<IoIosClose style={{ height: "1.5em", width: "1.5em" }} /> Rejeté
<IoIosClose style={{ height: "iconSmall", width: "iconSmall" }} />{" "}
Rejeté
</MenuItem>
</MenuButton>
);
Expand Down
Expand Up @@ -90,5 +90,6 @@ FicheTravailDiffchange.propTypes = {
})
),
title: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
}),
};
11 changes: 9 additions & 2 deletions targets/frontend/src/components/dialog/index.js
Expand Up @@ -9,13 +9,20 @@ import { css, jsx } from "theme-ui";
import { IconButton } from "../button";
import { Stack } from "../layout/Stack";

export function Dialog({ ariaLabel, isOpen = false, onDismiss, children }) {
export function Dialog({
ariaLabel,
isOpen = false,
onDismiss,
children,
...props
}) {
return (
<ReachDialog
css={styles.dialog}
isOpen={isOpen}
onDismiss={onDismiss}
aria-label={ariaLabel}
{...props}
>
<IconButton css={styles.closeBt} variant="secondary" onClick={onDismiss}>
<VisuallyHidden>Close</VisuallyHidden>
Expand All @@ -28,7 +35,7 @@ export function Dialog({ ariaLabel, isOpen = false, onDismiss, children }) {

const styles = {
closeBt: css({
position: "absolute",
position: "fixed",
right: "xxsmall",
top: "xxsmall",
}),
Expand Down
201 changes: 201 additions & 0 deletions targets/frontend/src/components/forms/ContentPicker/ContentSearch.js
@@ -0,0 +1,201 @@
/** @jsx jsx */

import { getLabelBySource, 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 { Input, jsx } from "theme-ui";
import { useQuery } from "urql";

const sources = [
SOURCES.SHEET_MT_PAGE,
SOURCES.SHEET_SP,
SOURCES.LETTERS,
SOURCES.TOOLS,
SOURCES.CONTRIBUTIONS,
SOURCES.EXTERNALS,
SOURCES.THEMATIC_FILES,
SOURCES.EDITORIAL_CONTENT,
SOURCES.CDT,
SOURCES.THEMES,
];

const AUTOSUGGEST_MAX_RESULTS = 15;

const searchDocumentsQuery = `
query searchDocuments($sources: [String!]! = "", $search: String = "") {
documents(where: {title: {_ilike: $search}, source: {_in: $sources}}, limit: ${AUTOSUGGEST_MAX_RESULTS}) {
source
title
cdtnId: cdtn_id
}
}
`;

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

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

useEffect(() => {
const allDocuments = results.data?.documents || [];
const documents = allDocuments.filter(
(document) =>
document.source !== SOURCES.THEMES && document.source !== SOURCES.CDT
);
documents.forEach((document) => {
document.category = "document";
});
const themes = allDocuments.filter(
(document) => document.source === SOURCES.THEMES
);
const articles = allDocuments.filter(
(document) => document.source === SOURCES.CDT
);
setSuggestions([
{
suggestions: documents,
title: "Documents",
},
{ suggestions: articles, title: "Articles" },
{ suggestions: themes, title: "Thèmes" },
]);
}, [results.data]);

const onSearchValueChange = (event, { newValue }) => {
setInputSearchValue(newValue);
setDebouncedSearchValue(newValue);
};
const onSuggestionSelected = (
event,
{ suggestion: { cdtnId, source, title = null } }
) => {
if (contents.find((content) => content.cdtnId === cdtnId)) {
return;
}
onChange(contents.concat([{ cdtnId, source, title }]));
setInputSearchValue("");
setSuggestions([]);
};

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

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

const inputProps = {
onChange: onSearchValueChange,
placeholder: "Rechercher et ajouter un contenu",
value: inputSearchValue,
};

return (
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
onSuggestionsClearRequested={onSuggestionsClearRequested}
onSuggestionSelected={onSuggestionSelected}
getSuggestionValue={getSuggestionValue}
getSectionSuggestions={getSectionSuggestions}
multiSection={true}
shouldRenderSuggestions={shouldRenderSuggestions}
renderInputComponent={renderInputComponent}
renderSuggestion={renderSuggestion}
renderSuggestionsContainer={renderSuggestionsContainer}
renderSectionTitle={renderSectionTitle}
inputProps={inputProps}
/>
);
};

ContentSearch.propTypes = {
contents: PropTypes.array,
onChange: PropTypes.func.isRequired,
};

const renderInputComponent = (inputProps) => (
<Input {...inputProps} ref={inputProps.ref} />
);

function shouldRenderSuggestions(value) {
return value.trim().length > 2;
}
function renderSectionTitle(section) {
return section.suggestions.length ? (
<div
sx={{
bg: "neutral",
fontWeight: "bold",
p: "xxsmall",
}}
>
{section.title}
</div>
) : null;
}

function getSectionSuggestions(section) {
return section.suggestions;
}

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

const renderSuggestion = (content) => (
<div>
{content.title}
{content.category === "document" && (
<strong>{getLabelBySource(content.source)}</strong>
)}
</div>
);

const renderSuggestionsContainer = ({ containerProps, children }) => (
<div
sx={{
'&[class*="container--open"]': {
border: "1px solid #ddd",
borderRadius: "4px",
maxHeight: "300px",
overflow: "scroll",
position: "relative",
top: "4px",
},
li: {
'&[role="option"]:hover': {
bg: "#dde",
},
":nth-of-type(2n + 1)": {
bg: "highlight",
},
bg: "white",
cursor: "pointer",
m: "0",
p: "xxsmall",
zIndex: 2,
},
ul: {
listStyleType: "none",
m: "0",
p: "0",
width: "100%",
},
}}
{...containerProps}
>
{children}
</div>
);

0 comments on commit db38184

Please sign in to comment.