Skip to content

Commit 47dad31

Browse files
✨ Save and show the certified version of an answer (#376)
This ensures that the latest certified answer is always accessible, even after the answer has been modified. Also: - Adds a confirm modal when editing a certified answer - Adds the date of certification to the Certified flag - Upgrade date-fns - Fixes Algolia synonyms in some cases
1 parent bdbce5c commit 47dad31

File tree

17 files changed

+201
-41
lines changed

17 files changed

+201
-41
lines changed

client/package-lock.json

Lines changed: 14 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"babel-runtime": "^6.26.0",
1111
"classnames": "^2.2.5",
1212
"copy-to-clipboard": "^3.2.0",
13-
"date-fns": "^1.29.0",
13+
"date-fns": "^2.30.0",
1414
"eslint-config-react-app": "^7.0.1",
1515
"graphql": "^14.7.0",
1616
"graphql-tag": "^2.8.0",

client/src/components/Flags/Flag.jsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import capitalize from 'lodash/capitalize'
44
import cn from 'classnames'
55

66
import { getIntl } from 'services'
7+
import { format } from 'date-fns'
78

89
const flagMeta = {
910
duplicate: {
@@ -40,7 +41,16 @@ const Flag = ({ flag, withlabel, style, ...otherProps }) => {
4041
{...otherProps}
4142
>
4243
<i className="material-icons">{flagMeta[flag.type].icon}</i>
43-
{withlabel && <span className="label">{capitalize(intl(flag.type))}</span>}
44+
{withlabel && (
45+
<span className="label">
46+
{capitalize(intl(flag.type))}
47+
{flag.type === 'certified' &&
48+
`
49+
${intl('certifiedAdd')}
50+
${format(new Date(flag.createdAt), 'P')}
51+
`}
52+
</span>
53+
)}
4454
</div>
4555
)
4656
}
@@ -57,14 +67,16 @@ Flag.translations = {
5767
outdated: 'outdated',
5868
incomplete: 'incomplete',
5969
unanswered: 'unanswered',
60-
certified: 'certified'
70+
certified: 'certified',
71+
certifiedAdd: ' on '
6172
},
6273
fr: {
6374
duplicate: 'doublon',
6475
outdated: 'obsolète',
6576
incomplete: 'incomplète',
6677
unanswered: 'sans réponse',
67-
certified: 'certifiée'
78+
certified: 'certifiée',
79+
certifiedAdd: ' le '
6880
}
6981
}
7082

client/src/components/Flags/Flags.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
33
import clone from 'lodash/clone'
44
import find from 'lodash/find'
55
import map from 'lodash/map'
6-
import format from 'date-fns/format'
6+
import { format } from 'date-fns'
77

88
import { getIntl } from 'services'
99

@@ -29,7 +29,7 @@ const Flags = ({ node, withLabels }) => {
2929
let tooltip
3030

3131
if (withLabels && flag.user) {
32-
tooltip = intl('tooltip')(flag.user.name, format(flag.createdAt, 'D MMM YYYY'))
32+
tooltip = intl('tooltip')(flag.user.name, format(new Date(flag.createdAt), 'P'))
3333
} else {
3434
tooltip = flagIntl(flag.type).toUpperCase()
3535
}

client/src/helpers/history.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import compact from 'lodash/compact'
2-
import subMonths from 'date-fns/sub_months'
3-
import differenceInMilliseconds from 'date-fns/difference_in_milliseconds'
4-
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now'
5-
import format from 'date-fns/format'
2+
import { format, formatDistanceToNow, differenceInMilliseconds, subMonths } from 'date-fns'
63

74
import { markdown } from 'services'
85

@@ -84,12 +81,13 @@ export const formatHistoryAction = (historyAction, options = { relative: true })
8481

8582
const monthOld = subMonths(new Date(), 1)
8683

84+
const createdAtDate = new Date(createdAt)
8785
const date =
88-
differenceInMilliseconds(createdAt, monthOld) > 0
89-
? distanceInWordsToNow(createdAt, {
86+
differenceInMilliseconds(createdAtDate, monthOld) > 0
87+
? formatDistanceToNow(createdAtDate, {
9088
addSuffix: true
9189
})
92-
: format(createdAt, 'D MMM YYYY, HH:mm')
90+
: format(createdAtDate, 'Pp')
9391

9492
return {
9593
date,

client/src/scenes/App/App.jsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import React from 'react'
22
import { Helmet } from 'react-helmet'
3+
import { setDefaultOptions } from 'date-fns'
4+
import { enUS, fr } from 'date-fns/locale'
35

46
import { ConfigurationProvider, AuthProvider, UserProvider } from 'contexts'
5-
67
import { AlertStack, AlertProvider } from 'components'
8+
import { getNavigatorLanguage } from 'helpers'
79

810
import Navbar from './components/Navbar'
911
import Footer from './components/Footer'
@@ -12,6 +14,10 @@ import AppBody from './AppBody'
1214

1315
import 'styles'
1416

17+
setDefaultOptions({
18+
locale: getNavigatorLanguage() === 'en' ? enUS : fr
19+
})
20+
1521
const App = () => (
1622
<div className="app theme">
1723
<Helmet>

client/src/scenes/Question/Question.container.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import { query } from 'services/apollo'
44

55
import { withError } from 'components'
66

7-
import { getNode } from './queries'
7+
import { GET_NODE } from './queries'
88

99
export const withNode = compose(
10-
query(getNode, {
10+
query(GET_NODE, {
1111
skip: props => !props.match.params.slug,
1212
variables: props => ({ id: routing.getUIDFromSlug(props.match) })
1313
}),

client/src/scenes/Question/queries.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const zNodeFragment = `
2424
id
2525
content
2626
language
27+
certified
2728
translation {
2829
language
2930
text
@@ -59,10 +60,16 @@ export const zNodeFragment = `
5960
name
6061
}
6162
}
63+
history {
64+
id
65+
meta
66+
action
67+
model
68+
}
6269
`
6370

64-
export const getNode = gql`
65-
query($id: ID!) {
71+
export const GET_NODE = gql`
72+
query getNode($id: ID!) {
6673
zNode(where: { id: $id }) {
6774
${zNodeFragment}
6875
}

client/src/scenes/Question/scenes/Read/Read.jsx

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import Dropdown, { DropdownItem } from 'components/Dropdown'
1717
import { getNavigatorLanguage, handleTranslation } from 'helpers'
1818
import { ActionMenu } from '../../components'
1919
import { FlagsDropdown, History, LanguageDropdown, Meta, Share, Sources, Views } from './components'
20+
import { useUser } from 'contexts'
2021

2122
const Read = ({ history, match, zNode, loading }) => {
2223
const [loaded, setLoaded] = useState(false)
@@ -26,10 +27,12 @@ const Read = ({ history, match, zNode, loading }) => {
2627
const [answerContent, setAnswerContent] = useState('')
2728
const [isTranslated, setIsTranslated] = useState(false)
2829

29-
const [createFlag] = useMutation(CREATE_FLAG)
30+
const [createFlag] = useMutation(CREATE_FLAG, { refetchQueries: ['getNode'] })
3031
const [removeFlag] = useMutation(REMOVE_FLAG)
3132
const [incrementViewsCounter] = useMutation(INCREMENT_VIEWS_COUNTER)
3233

34+
const user = useUser()
35+
3336
const originalQuestionLanguage = zNode?.question.language
3437
const navigatorLanguage = getNavigatorLanguage()
3538

@@ -61,6 +64,20 @@ const Read = ({ history, match, zNode, loading }) => {
6164
return <NotFound />
6265
}
6366

67+
const preventAnswerEdit = () => {
68+
const certified = zNode?.flags.find(flag => flag.type === 'certified')
69+
const isSpecialist = user.specialties.some(specialty =>
70+
zNode.tags.some(tag => specialty.name === tag.label.name)
71+
)
72+
if (certified && !isSpecialist) {
73+
if (window.confirm(intl('alert'))) {
74+
return history.push(`/q/${match.params.slug}/answer`)
75+
}
76+
} else {
77+
return history.push(`/q/${match.params.slug}/answer`)
78+
}
79+
}
80+
6481
/* Redirect to correct URL if old slug used */
6582
const correctSlug = zNode.question.slug + '-' + zNode.id
6683
if (match.params.slug !== correctSlug) {
@@ -82,10 +99,7 @@ const Read = ({ history, match, zNode, loading }) => {
8299
<DropdownItem icon="edit" onClick={() => history.push(`/q/${match.params.slug}/edit`)}>
83100
{intl('menu.edit.question')}
84101
</DropdownItem>
85-
<DropdownItem
86-
icon="question_answer"
87-
onClick={() => history.push(`/q/${match.params.slug}/answer`)}
88-
>
102+
<DropdownItem icon="question_answer" onClick={preventAnswerEdit}>
89103
{intl('menu.edit.answer')}
90104
</DropdownItem>
91105
</Dropdown>
@@ -125,9 +139,24 @@ const Read = ({ history, match, zNode, loading }) => {
125139
/>
126140
)}
127141
</CardTitle>
142+
{zNode.answer?.certified && (
143+
<CardText style={{ border: '2px solid #caac00', marginTop: '0.5rem' }}>
144+
<p className="small-text centered" style={{ padding: '0.5rem', margin: '0' }}>
145+
{intl('certified_answer')}
146+
</p>
147+
<div style={{ padding: '0.5rem', margin: '0' }}>
148+
{markdown.html(zNode.answer.certified)}
149+
</div>
150+
</CardText>
151+
)}
128152
<CardText>
129153
{zNode.answer ? (
130154
<>
155+
{zNode.answer.certified && (
156+
<p className="small-text centered" style={{ padding: '0.5rem', margin: '0' }}>
157+
{intl('recent_answer')}
158+
</p>
159+
)}
131160
<div style={{ padding: '0.5rem', marginBottom: '0.5rem' }}>
132161
{markdown.html(answerContent)}
133162
</div>
@@ -177,9 +206,14 @@ Read.translations = {
177206
answer: 'Answer'
178207
}
179208
},
209+
certified: 'Certified on',
180210
auto_translated: 'Automatic translation',
181211
no_answer: 'No answer yet...',
182-
answer: 'Answer the question'
212+
answer: 'Answer the question',
213+
certified_answer: 'Certified answer',
214+
recent_answer: 'Latest answer',
215+
alert:
216+
'This answer has been certified by a specialist in this field.\nAre you sure that you want to modify it ?\nThis version will still be kept'
183217
},
184218
fr: {
185219
menu: {
@@ -190,9 +224,14 @@ Read.translations = {
190224
answer: 'Réponse'
191225
}
192226
},
227+
certified: 'Certifiée le',
193228
auto_translated: 'Traduction automatique',
194229
no_answer: 'Pas encore de réponse...',
195-
answer: 'Répondre à la question'
230+
answer: 'Répondre à la question',
231+
certified_answer: 'Réponse certifiée',
232+
recent_answer: 'Dernière réponse',
233+
alert:
234+
'Cette réponse a été certifiée par une personne spécialisée dans le domaine.\nEtes-vous sûr de vouloir la modifier ?\nCelle-ci sera tout de même conservée'
196235
}
197236
}
198237

client/src/scenes/Question/scenes/Read/components/Meta/Meta.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ const Meta = ({ node }) => {
2121
<div>
2222
{intl('asked')} {node.question.user.name}
2323
<br />
24-
{format(node.question.createdAt, 'D MMM YYYY')}
24+
{format(new Date(node.question.createdAt), 'P')}
2525
</div>
2626
</div>
2727
{node.answer && (
2828
<div className="answered">
2929
<div>
3030
{intl('answered')} {node.answer.user.name}
3131
<br />
32-
{format(node.answer.createdAt, 'D MMM YYYY')}
32+
{format(new Date(node.answer.createdAt), 'P')}
3333
</div>
3434
<Avatar
3535
image={node.answer.user.picture}

0 commit comments

Comments
 (0)