• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

EcrituresNumeriques / stylo / 26445582777

26 May 2026 09:59AM UTC coverage: 68.916% (+0.4%) from 68.537%
26445582777

Pull #2018

github

web-flow
Merge 38a05f317 into a1b7ad30a
Pull Request #2018: feat(editor): ajout d'un validateur Markdown pour le balisage Métopes

864 of 1382 branches covered (62.52%)

Branch coverage included in aggregate %.

79 of 80 new or added lines in 2 files covered. (98.75%)

46 existing lines in 1 file now uncovered.

4488 of 6384 relevant lines covered (70.3%)

6.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

15.65
/front/src/hooks/user.js
1
import { useCallback, useMemo, useState } from 'react'
2
import { useTranslation } from 'react-i18next'
3
import { useDispatch, useSelector } from 'react-redux'
4
import { useRouteLoaderData } from 'react-router'
5

6
import { applicationConfig } from '../config.js'
7
import { useGraphQLClient } from '../helpers/graphQL.js'
8
import useFetchData, { useMutateData } from './graphql.js'
9

10
import {
11
  deleteAccount as deleteAccountMutation,
12
  logoutMutation,
13
  unsetAuthTokenMutation,
14
} from './Credentials.graphql'
15
import { createTag, getTags, updateTag } from './Tag.graphql'
16

17
/**
18
 * @returns {string|null}
19
 */
20
export function useActiveUserId() {
UNCOV
21
  const { user } = useRouteLoaderData('app')
×
UNCOV
22
  return user?._id
×
23
}
24

25
/**
26
 * @typedef {import('redux').Dispatch} Dispatch
27
 */
28

29
/**
30
 *
31
 * @param {string} key
32
 * @param {'article' | 'user' | 'export' | 'corpus' } namespace
33
 * @returns {{ value: string|boolean|number, setValue: Dispatch, toggleValue: Dispatch }}
34
 */
35
export function usePreferenceItem(key, namespace = 'article') {
×
UNCOV
36
  const dispatch = useDispatch()
×
37
  const value = useSelector((state) => state[`${namespace}Preferences`]?.[key])
×
38

UNCOV
39
  const ns = namespace.toUpperCase()
×
40

UNCOV
41
  return {
×
42
    value,
43
    /**
44
     * @param {boolean | undefined} value
45
     */
46
    setValue(value) {
UNCOV
47
      dispatch({ type: `SET_${ns}_PREFERENCES`, key, value })
×
48
    },
49
    toggleValue() {
UNCOV
50
      dispatch({ type: `${ns}_PREFERENCES_TOGGLE`, key })
×
51
    },
52
  }
53
}
54

55
/**
56
 *
57
 * @param {'humanid' | 'hypothesis' | 'zotero' } service
58
 * @returns {{ link: React.EffectCallback, token: string | undefined, id: string | undefined, isLinked: boolean, error: string, unlink: React.EffectCallback }}
59
 */
60
export function useSetAuthToken(service) {
UNCOV
61
  const dispatch = useDispatch()
×
62
  const { query } = useGraphQLClient()
×
63
  const [error, setError] = useState('')
×
UNCOV
64
  const { backendEndpoint, frontendEndpoint } = applicationConfig
×
65

66
  const token = useSelector(
×
67
    (state) => state.activeUser.authProviders?.[service]?.token
×
68
  )
69

70
  const id = useSelector(
×
UNCOV
71
    (state) => state.activeUser.authProviders?.[service]?.id
×
72
  )
73

74
  const isLinked = useMemo(() => id ?? token, [id, token])
×
75

UNCOV
76
  const link = useCallback(async function handleSetAuthToken() {
×
UNCOV
77
    setError('')
×
UNCOV
78
    const popup = window.open(
×
79
      `${backendEndpoint}/authorize/${service}?returnTo=${frontendEndpoint}/credentials/auth-callback/${service}`,
80
      `auth-${service}`,
81
      'width=660&height=360&menubar=0&toolbar=0'
82
    )
83

84
    async function handleClose({ data, type, source }) {
85
      if (source === popup && type === 'message' && data) {
×
UNCOV
86
        const authProviders = JSON.parse(data ?? '')
×
87

UNCOV
88
        if (authProviders) {
×
UNCOV
89
          dispatch({
×
90
            type: 'UPDATE_ACTIVE_USER_DETAILS',
91
            payload: {
92
              authProviders,
93
            },
94
          })
95
        }
96

97
        popup.close()
×
98
      }
99
    }
100

UNCOV
101
    window.addEventListener('message', handleClose)
×
UNCOV
102
    popup.addEventListener('beforeunload', () =>
×
103
      window.removeEventListener('message', handleClose)
×
104
    )
105
  }, [])
106

UNCOV
107
  const unlink = useCallback(async () => {
×
UNCOV
108
    try {
×
UNCOV
109
      setError('')
×
UNCOV
110
      const { unsetAuthToken } = await query({
×
111
        query: unsetAuthTokenMutation,
112
        variables: { service },
113
      })
114

UNCOV
115
      dispatch({ type: 'UPDATE_ACTIVE_USER_DETAILS', payload: unsetAuthToken })
×
116
    } catch (error) {
117
      setError(error.messages.at(0).extensions.type)
×
118
    }
119
  }, [])
120

UNCOV
121
  return { link, unlink, token, id, isLinked, error }
×
122
}
123

124
export function useUserTagActions() {
125
  const { query } = useGraphQLClient()
3✔
126
  const { mutate } = useMutateData({
3✔
127
    query: getTags,
128
    variables: {},
129
  })
130
  const create = async (tag) => {
3✔
131
    const result = await query({
1✔
132
      query: createTag,
133
      variables: tag,
134
      type: 'mutation',
135
    })
136
    await mutate(
1✔
137
      async (data) => {
138
        const tags = data?.user?.tags ?? []
1✔
139
        return {
1✔
140
          user: {
141
            tags: [...tags, result.createTag],
142
          },
143
        }
144
      },
145
      { revalidate: false }
146
    )
147
    return result.createTag
1✔
148
  }
149
  const update = async (tag) => {
3✔
150
    const result = await query({
1✔
151
      query: updateTag,
152
      variables: tag,
153
      type: 'mutation',
154
    })
155
    await mutate(
1✔
156
      async (data) => ({
1✔
157
        user: {
158
          tags: data.user.tags.map((tag) => {
159
            return tag._id === result.updateTag._id ? result.updateTag : tag
1!
160
          }),
161
        },
162
      }),
163
      { revalidate: false }
164
    )
165
    return result.updateTag
1✔
166
  }
167

168
  return {
3✔
169
    create,
170
    update,
171
  }
172
}
173

174
export function useUserTags() {
UNCOV
175
  const { data, error, isLoading } = useFetchData({
×
176
    query: getTags,
177
    variables: {},
178
  })
179

UNCOV
180
  const tags = data?.user?.tags || []
×
UNCOV
181
  return {
×
182
    tags,
183
    error,
184
    isLoading,
185
  }
186
}
187

188
export function useLogout() {
189
  const dispatch = useDispatch()
×
UNCOV
190
  const { query } = useGraphQLClient()
×
191

UNCOV
192
  return useCallback(async () => {
×
UNCOV
193
    await query({ query: logoutMutation })
×
194

UNCOV
195
    dispatch({ type: 'LOGOUT' })
×
196
  }, [])
197
}
198

199
/**
200
 * @typedef {object} User
201
 * @property {boolean?} deletedAt
202
 * @property {string?} displayName
203
 * @property {string} username
204
 */
205

206
/**
207
 * @returns {(user: User) => string}
208
 */
209
export function useDisplayName() {
210
  const { t } = useTranslation()
×
211

UNCOV
212
  return function displayName(user = {}) {
×
213
    if (user.deletedAt) {
×
UNCOV
214
      return t('user.account.isDeleted.displayName')
×
215
    }
216

UNCOV
217
    return user.displayName || user.username
×
218
  }
219
}
220

221
export function useAccount() {
UNCOV
222
  const sessionToken = useSelector((state) => state.sessionToken)
×
UNCOV
223
  const { query } = useGraphQLClient()
×
224

225
  async function deleteAccount() {
226
    const result = await query({ query: deleteAccountMutation })
×
UNCOV
227
    return result.deleteAccount === true
×
228
  }
229

230
  async function download({
×
231
    scope = 'mine',
×
232
    versions = 'latest',
×
233
    format = 'zip',
×
234
    workspaceId,
235
  } = {}) {
UNCOV
236
    const response = await fetch(
×
237
      applicationConfig.backendEndpoint + '/backup',
238
      {
239
        method: 'POST',
240
        mode: 'cors',
241
        credentials: 'omit',
242
        headers: {
243
          'Content-Type': 'application/json',
244
          ...(sessionToken ? { Authorization: `Bearer ${sessionToken}` } : {}),
×
245
        },
246
        body: JSON.stringify({ scope, versions, format, workspaceId }),
247
      }
248
    )
249

250
    if (!response.ok) {
×
251
      const error = await response.json()
×
252
      throw new Error(error.message ?? 'Backup request failed')
×
253
    }
254

255
    const filename = format === 'zip' ? 'backup.zip' : 'backup.json'
×
UNCOV
256
    const blob = await response.blob()
×
UNCOV
257
    const url = URL.createObjectURL(blob)
×
UNCOV
258
    const a = document.createElement('a')
×
UNCOV
259
    a.href = url
×
UNCOV
260
    a.download = filename
×
UNCOV
261
    a.click()
×
UNCOV
262
    URL.revokeObjectURL(url)
×
263
  }
264

UNCOV
265
  return { download, deleteAccount }
×
266
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc