Support Usager : Implémentation de la messagerie Crisp (#2329)
* feat: Ajoute l'iframe crisp ainsi que le bloc custom html * wip * feat: Renomme le dossier standup.. + ajoute la fonction crisp * fix: Install not broken version + add update meta * feat: Ajoute le formulaire * fix: Corrige htmlFor et id * feat: Ajoute la logique pour récupérer nombre de réponses et les issues * fix: uncomment stuff * fix: Retire log * chore: Renomme fonction * chore: Renomme fonction * chore: Renomme fonction * feat: Retire commentaires * fix: Refacto urlParams * chore : Nettoyage de reliquats * fix : Ajoute TextAreaField et utilise TextField * fix: Style issues * fix: Améliore types * feat: Cleaning * feat: Ajoute variable d'env website id * feat: Met à jour README * wip placeholder url * feat: Ajoute une fonction de validation du body * feat: Ajoute validation * chore: update yarn.lock * fix: Add missing secret ref + cleaningpull/2351/head
parent
492f6f9026
commit
a51920b44c
|
@ -50,6 +50,9 @@ jobs:
|
|||
ATINTERNET_API_SECRET_KEY: ${{ secrets.ATINTERNET_API_SECRET_KEY }}
|
||||
ATINTERNET_API_ACCESS_KEY: ${{ secrets.ATINTERNET_API_ACCESS_KEY }}
|
||||
PLAUSIBLE_API_KEY: ${{ secrets.PLAUSIBLE_API_KEY }}
|
||||
CRISP_API_IDENTIFIER: ${{ secrets.CRISP_API_IDENTIFIER }}
|
||||
CRISP_API_KEY: ${{ secrets.CRISP_API_KEY }}
|
||||
CRISP_WEBSITE_ID: ${{ secrets.CRISP_WEBSITE_ID }}
|
||||
- name: Build app
|
||||
run: yarn workspace site build
|
||||
env:
|
||||
|
@ -101,6 +104,9 @@ jobs:
|
|||
ATINTERNET_API_SECRET_KEY: ${{ secrets.ATINTERNET_API_SECRET_KEY }}
|
||||
ATINTERNET_API_ACCESS_KEY: ${{ secrets.ATINTERNET_API_ACCESS_KEY }}
|
||||
PLAUSIBLE_API_KEY: ${{ secrets.PLAUSIBLE_API_KEY }}
|
||||
CRISP_API_IDENTIFIER: ${{ secrets.CRISP_API_IDENTIFIER }}
|
||||
CRISP_API_KEY: ${{ secrets.CRISP_API_KEY }}
|
||||
CRISP_WEBSITE_ID: ${{ secrets.CRISP_WEBSITE_ID }}
|
||||
- name: Build Storybook
|
||||
run: yarn workspace site build:storybook
|
||||
- uses: actions/upload-artifact@v2
|
||||
|
|
|
@ -25,6 +25,9 @@ jobs:
|
|||
ATINTERNET_API_SECRET_KEY: ${{ secrets.ATINTERNET_API_SECRET_KEY }}
|
||||
ATINTERNET_API_ACCESS_KEY: ${{ secrets.ATINTERNET_API_ACCESS_KEY }}
|
||||
PLAUSIBLE_API_KEY: ${{ secrets.PLAUSIBLE_API_KEY }}
|
||||
CRISP_API_IDENTIFIER: ${{ secrets.CRISP_API_IDENTIFIER }}
|
||||
CRISP_API_KEY: ${{ secrets.CRISP_API_KEY }}
|
||||
CRISP_WEBSITE_ID: ${{ secrets.CRISP_WEBSITE_ID }}
|
||||
- name: Remove temporary algolia index
|
||||
run: yarn workspace site algolia:clean
|
||||
env:
|
||||
|
|
|
@ -21,6 +21,9 @@ jobs:
|
|||
ATINTERNET_API_SECRET_KEY: ${{ secrets.ATINTERNET_API_SECRET_KEY }}
|
||||
ATINTERNET_API_ACCESS_KEY: ${{ secrets.ATINTERNET_API_ACCESS_KEY }}
|
||||
PLAUSIBLE_API_KEY: ${{ secrets.PLAUSIBLE_API_KEY }}
|
||||
CRISP_API_IDENTIFIER: ${{ secrets.CRISP_API_IDENTIFIER }}
|
||||
CRISP_API_KEY: ${{ secrets.CRISP_API_KEY }}
|
||||
CRISP_WEBSITE_ID: ${{ secrets.CRISP_WEBSITE_ID }}
|
||||
- run: yarn test:type
|
||||
|
||||
unit:
|
||||
|
|
|
@ -11,12 +11,12 @@
|
|||
"exoneration-covid",
|
||||
"api",
|
||||
"site",
|
||||
"standup-mattermost-bot"
|
||||
"server"
|
||||
],
|
||||
"scripts": {
|
||||
"scalingo-postbuild": "echo \"$APP_DIR\" ; CI=true ; yarn test:type && yarn \"build:$APP_DIR\" && yarn workspaces focus \"$APP_DIR\" --production",
|
||||
"build:api": "yarn workspaces focus api && yarn workspace api run build && yarn workspaces focus --all && yarn test",
|
||||
"build:standup-mattermost-bot": "yarn workspaces focus standup-mattermost-bot && yarn workspace standup-mattermost-bot run build",
|
||||
"build:server": "yarn workspaces focus server && yarn workspace server run build",
|
||||
"lint:eslintrc": "npx eslint-config-prettier .eslintrc.cjs",
|
||||
"lint:eslint": "NODE_OPTIONS='--max-old-space-size=4096' eslint .",
|
||||
"lint:eslint:fix": "yarn lint:eslint --fix",
|
||||
|
@ -34,7 +34,7 @@
|
|||
"i18n:check": "yarn workspace site i18n:check",
|
||||
"i18n:translate": "yarn workspace site i18n:translate"
|
||||
},
|
||||
"//": "Resolve conflicts with @types/... in api and standup-mattermost-bot",
|
||||
"//": "Resolve conflicts with @types/... in api and server",
|
||||
"resolutions": {
|
||||
"@types/cacheable-request": "8.3.1",
|
||||
"@types/responselike": "^1.0.0",
|
||||
|
|
|
@ -2,3 +2,6 @@ CLIENT_ID=
|
|||
CLIENT_SECRET=
|
||||
MONGO_URL=mongodb://root:example@localhost:27017/
|
||||
ORIGIN=http://localhost:4000
|
||||
CRISP_API_IDENTIFIER=
|
||||
CRISP_API_KEY=
|
||||
CRISP_WEBSITE_ID=
|
|
@ -1,4 +1,9 @@
|
|||
# Bot Mattermost pour stand-up asynchrone
|
||||
# Server : serveur back-end de Mon-Entreprise
|
||||
|
||||
Serveur hébergé sur Scalingo, utilisé pour faire tourner le bot Stand-up ainsi que pour mettre à disposition des fonctions back-end Node.js.
|
||||
|
||||
## Jobs
|
||||
### Bot Mattermost pour stand-up asynchrone
|
||||
|
||||
Ce bot envoie une notification périodique sur un canal Mattermost dédié afin que chaque membre de l'équipe puisse écrire en réponse où il en est dans ses tâches.
|
||||
|
||||
|
@ -6,7 +11,13 @@ Il désigne également qui sera l'animateur pour chaque jour de la semaine à ve
|
|||
|
||||
Et peut-être plus à venir...
|
||||
|
||||
# Détail technique
|
||||
#### Détail technique
|
||||
|
||||
Le bot peut être installé facilement sur Scalingo, vous pouvez aller voir le `Procfile` à la racine du projet.
|
||||
Il nécessite une base de données Mongodb pour stocker les token d'authentification et la liste des animateurs de la semaine.
|
||||
|
||||
## Functions
|
||||
|
||||
### Send Crisp message
|
||||
|
||||
Cette fonction utilise l'API Crisp pour rediriger les messages en provenance de notre formulaire de suggestions vers la messagerie Crisp afin de prendre en charge ces retours.
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "standup-mattermost-bot",
|
||||
"name": "server",
|
||||
"license": "MIT",
|
||||
"version": "2.0.0",
|
||||
"description": "Code source du standup-mattermost-bot",
|
||||
"description": "Code source du serveur backend mon-entreprise",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/betagouv/mon-entreprise.git",
|
||||
"directory": "standup-mattermost-bot"
|
||||
"directory": "server"
|
||||
},
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
@ -20,9 +20,11 @@
|
|||
"@koa/cors": "^3.4.1",
|
||||
"@koa/router": "^12.0.0",
|
||||
"bree": "^9.1.2",
|
||||
"crisp-api": "6.3.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"got": "^12.5.1",
|
||||
"koa": "^2.13.4",
|
||||
"koa-body": "^5.0.0",
|
||||
"mongodb": "^4.10.0",
|
||||
"nodemon": "^2.0.20"
|
||||
},
|
|
@ -0,0 +1,85 @@
|
|||
import Crisp from 'crisp-api'
|
||||
|
||||
export type BodyType = {
|
||||
subject: string
|
||||
message: string
|
||||
email: string
|
||||
}
|
||||
|
||||
type SendMessageParamsType = {
|
||||
type: string
|
||||
content: string
|
||||
from: string
|
||||
origin: string
|
||||
subject: string
|
||||
}
|
||||
|
||||
type ConversationMetaType = {
|
||||
email: string
|
||||
subject: string
|
||||
nickname: string
|
||||
}
|
||||
|
||||
type CrispType = {
|
||||
authenticateTier: (
|
||||
type: string,
|
||||
identifier: string | undefined,
|
||||
key: string | undefined
|
||||
) => void
|
||||
website: {
|
||||
createNewConversation: (id: string) => Promise<{ session_id: string }>
|
||||
sendMessageInConversation: (
|
||||
website_id: string,
|
||||
session_id: string,
|
||||
params: SendMessageParamsType
|
||||
) => void
|
||||
updateConversationMetas: (
|
||||
website_id: string,
|
||||
session_id: string,
|
||||
meta: ConversationMetaType
|
||||
) => Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
export const sendCrispMessage = async (body: BodyType) => {
|
||||
try {
|
||||
const { message, email, subject } = body || {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
const CrispClient = new Crisp() as unknown as CrispType
|
||||
|
||||
CrispClient.authenticateTier(
|
||||
'plugin',
|
||||
process.env.CRISP_API_IDENTIFIER,
|
||||
process.env.CRISP_API_KEY
|
||||
)
|
||||
|
||||
const WEBSITE_ID = process.env.CRISP_WEBSITE_ID as string
|
||||
|
||||
const result = await CrispClient.website.createNewConversation(WEBSITE_ID)
|
||||
|
||||
const { session_id: sessionId } = result
|
||||
|
||||
await CrispClient.website.updateConversationMetas(WEBSITE_ID, sessionId, {
|
||||
email,
|
||||
nickname: email,
|
||||
subject,
|
||||
})
|
||||
|
||||
CrispClient.website.sendMessageInConversation(
|
||||
WEBSITE_ID,
|
||||
sessionId,
|
||||
|
||||
{
|
||||
type: 'text',
|
||||
content: message,
|
||||
subject,
|
||||
from: 'operator',
|
||||
origin: 'chat',
|
||||
}
|
||||
)
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e)
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ import cors from '@koa/cors'
|
|||
import Router from '@koa/router'
|
||||
import 'dotenv/config'
|
||||
import Koa from 'koa'
|
||||
import koaBody from 'koa-body'
|
||||
import {
|
||||
clientId,
|
||||
clientSecret,
|
||||
|
@ -9,10 +10,11 @@ import {
|
|||
redirectUri,
|
||||
serverUrl,
|
||||
} from './config.js'
|
||||
import { BodyType, sendCrispMessage } from './functions/send-crisp-message.js'
|
||||
import { bree } from './jobs.js'
|
||||
import { initMongodb } from './mongodb.js'
|
||||
import { getAccessToken } from './oauth.js'
|
||||
import { snakeToCamelCaseKeys } from './utils.js'
|
||||
import { snakeToCamelCaseKeys, validateCrispBody } from './utils.js'
|
||||
|
||||
const mongo = await initMongodb()
|
||||
|
||||
|
@ -81,6 +83,21 @@ router.get('/oauth', async (ctx) => {
|
|||
}
|
||||
})
|
||||
|
||||
router.post('/send-crisp-message', koaBody(), async (ctx) => {
|
||||
try {
|
||||
const body = validateCrispBody(ctx.request.body as BodyType)
|
||||
|
||||
await sendCrispMessage(body)
|
||||
|
||||
ctx.status = 200
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err)
|
||||
|
||||
ctx.status = 400
|
||||
}
|
||||
})
|
||||
|
||||
app.use(router.routes())
|
||||
app.use(router.allowedMethods())
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import { BodyType } from './functions/send-crisp-message.js'
|
||||
|
||||
type CamelCase<S extends string> =
|
||||
S extends `${infer P1}_${infer P2}${infer P3}`
|
||||
? `${Lowercase<P1>}${Uppercase<P2>}${CamelCase<P3>}`
|
||||
|
@ -33,3 +35,23 @@ export const shuffleArray = <T>(array: T[]) => {
|
|||
|
||||
return shuffle
|
||||
}
|
||||
|
||||
const isStringAndNotEmpty = (value: string) =>
|
||||
value !== undefined && value !== '' && typeof value === 'string'
|
||||
|
||||
const SHORT_MAX_LENGTH = 254
|
||||
|
||||
export const validateCrispBody = (body: BodyType): BodyType => {
|
||||
const { subject, message, email } = body || {}
|
||||
if (
|
||||
isStringAndNotEmpty(subject) &&
|
||||
subject.length <= SHORT_MAX_LENGTH &&
|
||||
isStringAndNotEmpty(message) &&
|
||||
isStringAndNotEmpty(email) &&
|
||||
email.length <= SHORT_MAX_LENGTH
|
||||
) {
|
||||
return body
|
||||
}
|
||||
|
||||
throw Error('Body validation failed.')
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
[[headers]]
|
||||
for = "/*"
|
||||
[headers.values]
|
||||
Content-Security-Policy = "default-src 'self' mon-entreprise.fr; style-src 'self' 'unsafe-inline' mon-entreprise.zammad.com; connect-src 'self' *.incubateur.net raw.githubusercontent.com tm.urssaf.fr mon-entreprise.zammad.com api.recherche-entreprises.fabrique.social.gouv.fr geo.api.gouv.fr *.algolia.net *.algolianet.com polyfill.io code.jquery.com jedonnemonavis.numerique.gouv.fr user-images.githubusercontent.com; form-action 'self' *.sibforms.com *.incubateur.net mon-entreprise.zammad.com; script-src 'self' 'unsafe-inline' 'unsafe-eval' tm.urssaf.fr *.incubateur.net stonly.com code.jquery.com mon-entreprise.zammad.com polyfill.io; img-src 'self' data: mon-entreprise.urssaf.fr tm.urssaf.fr user-images.githubusercontent.com jedonnemonavis.numerique.gouv.fr; frame-src 'self' https://www.youtube-nocookie.com https://codesandbox.io https://place-des-entreprises.beta.gouv.fr https://reso-staging.osc-fr1.scalingo.io https://stackblitz.com"
|
||||
Content-Security-Policy = "default-src 'self' mon-entreprise.fr; style-src 'self' 'unsafe-inline' mon-entreprise.zammad.com; connect-src 'self' *.incubateur.net raw.githubusercontent.com tm.urssaf.fr mon-entreprise.zammad.com api.recherche-entreprises.fabrique.social.gouv.fr geo.api.gouv.fr *.algolia.net *.algolianet.com polyfill.io jedonnemonavis.numerique.gouv.fr user-images.githubusercontent.com; form-action 'self' *.sibforms.com *.incubateur.net mon-entreprise.zammad.com; script-src 'self' 'unsafe-inline' 'unsafe-eval' tm.urssaf.fr *.incubateur.net stonly.com code.jquery.com mon-entreprise.zammad.com polyfill.io; img-src 'self' data: mon-entreprise.urssaf.fr tm.urssaf.fr user-images.githubusercontent.com jedonnemonavis.numerique.gouv.fr; frame-src 'self' https://www.youtube-nocookie.com https://codesandbox.io https://place-des-entreprises.beta.gouv.fr https://reso-staging.osc-fr1.scalingo.io https://stackblitz.com"
|
||||
|
||||
[dev]
|
||||
autoLaunch = false
|
||||
|
|
|
@ -311,7 +311,6 @@ async function fetchDailyVisits() {
|
|||
}
|
||||
|
||||
async function fetchMonthlyVisits() {
|
||||
console.log('fetchMonthlyVisits')
|
||||
const pages = uniformiseData([
|
||||
...flattenPage(await fetchApi(buildSimulateursQuery(last36Months, 'M'))),
|
||||
...flattenPage(await fetchApi(buildCreerQuery(last36Months, 'M'))),
|
||||
|
@ -341,7 +340,80 @@ async function fetchMonthlyVisits() {
|
|||
}
|
||||
}
|
||||
|
||||
async function fetchUserAnswersStats() {
|
||||
const getISODatesStartEndPreviousMonth = () => {
|
||||
const dateFirstDayPreviousMonth = new Date()
|
||||
// On prend le premier jour du mois dernier
|
||||
dateFirstDayPreviousMonth.setMonth(dateFirstDayPreviousMonth.getMonth() - 1)
|
||||
dateFirstDayPreviousMonth.setDate(1)
|
||||
dateFirstDayPreviousMonth.setUTCHours(0, 0, 0, 0)
|
||||
const dateLastDayPreviousMonth = new Date(dateFirstDayPreviousMonth)
|
||||
// Ici l'index 0 permet de récupérer le dernier jour du mois précédent
|
||||
dateLastDayPreviousMonth.setDate(0)
|
||||
dateLastDayPreviousMonth.setUTCHours(23, 59, 59, 999)
|
||||
|
||||
return {
|
||||
startISODatePreviousMonth: dateFirstDayPreviousMonth.toISOString(),
|
||||
endISODatePreviousMonth: dateLastDayPreviousMonth.toISOString(),
|
||||
}
|
||||
}
|
||||
async function fetchPaginatedCrispConversations(pageNumber, urlParams) {
|
||||
const response = await fetch(
|
||||
`https://api.crisp.chat/v1/website/d8247abb-cac5-4db6-acb2-cea0c00d8524/conversations/${pageNumber}${
|
||||
urlParams ? `?${urlParams}` : ''
|
||||
}`,
|
||||
{
|
||||
method: 'get',
|
||||
headers: {
|
||||
Authorization: `Basic ${btoa(
|
||||
`${process.env.CRISP_API_IDENTIFIER}:${process.env.CRISP_API_KEY}`
|
||||
)}`,
|
||||
'X-Crisp-Tier': 'plugin',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
return result?.data
|
||||
}
|
||||
|
||||
async function fetchAllCrispConversations({ urlParams }) {
|
||||
try {
|
||||
let isEndPagination = false
|
||||
let pageCount = 1
|
||||
const dataConversations = []
|
||||
|
||||
while (!isEndPagination) {
|
||||
const paginatedData = await fetchPaginatedCrispConversations(
|
||||
pageCount,
|
||||
urlParams
|
||||
)
|
||||
// Array vide : plus rien à fetch de plus
|
||||
if (paginatedData.length === 0) {
|
||||
isEndPagination = true
|
||||
}
|
||||
|
||||
dataConversations.push(...(paginatedData || []))
|
||||
pageCount++
|
||||
}
|
||||
|
||||
return dataConversations
|
||||
} catch (e) {
|
||||
console.log('error', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCrispAnsweredConversationsLastMonth() {
|
||||
const { startISODatePreviousMonth, endISODatePreviousMonth } =
|
||||
getISODatesStartEndPreviousMonth()
|
||||
|
||||
const conversations = await fetchAllCrispConversations({
|
||||
urlParams: `filter_resolved=1&filter_date_start=${startISODatePreviousMonth}&filter_date_end=${endISODatePreviousMonth}`,
|
||||
})
|
||||
return conversations?.length ?? 0
|
||||
}
|
||||
|
||||
async function fetchZammadUserAnswersStats() {
|
||||
const ticketLists = await fetch(
|
||||
'https://mon-entreprise.zammad.com/api/v1/ticket_overviews?view=tickets_repondus_le_mois_dernier',
|
||||
{
|
||||
|
@ -354,20 +426,21 @@ async function fetchUserAnswersStats() {
|
|||
return answer.index.count
|
||||
}
|
||||
|
||||
async function fetchUserFeedbackIssues() {
|
||||
const tags = await fetch(
|
||||
'https://mon-entreprise.zammad.com/api/v1/tag_list',
|
||||
{
|
||||
headers: new Headers({
|
||||
Authorization: `Token token=${process.env.ZAMMAD_API_SECRET_KEY}`,
|
||||
}),
|
||||
}
|
||||
).then((r) => r.json())
|
||||
const sortedTags = tags
|
||||
.sort((t1, t2) => t2.count - t1.count)
|
||||
.filter(({ name }) => /#[\d]+/.exec(name))
|
||||
async function fetchAllUserAnswerStats() {
|
||||
const zammadAnswersCount = await fetchZammadUserAnswersStats()
|
||||
const cripsAnswersCount = await fetchCrispAnsweredConversationsLastMonth()
|
||||
|
||||
return zammadAnswersCount + cripsAnswersCount
|
||||
}
|
||||
|
||||
async function fetchGithubIssuesFromTags(tags) {
|
||||
if (!tags || tags?.length === 0) {
|
||||
console.error(`❌ Error: no tags to fetch issues from`)
|
||||
return []
|
||||
}
|
||||
|
||||
const query = `query {
|
||||
repository(owner:"betagouv", name:"mon-entreprise") {${sortedTags
|
||||
repository(owner:"betagouv", name:"mon-entreprise") {${tags
|
||||
.map(
|
||||
({ name, count }, i) =>
|
||||
`
|
||||
|
@ -402,13 +475,79 @@ async function fetchUserFeedbackIssues() {
|
|||
.filter(([, value]) => !!value)
|
||||
.map(([k, value]) => ({ ...value, count: +/[\d]+$/.exec(k)[0] }))
|
||||
|
||||
return issues
|
||||
}
|
||||
|
||||
async function fetchCrispUserFeedbackIssues() {
|
||||
const conversationsResolved = await fetchAllCrispConversations({
|
||||
urlParams: 'filter_resolved=1&search_query=issue&search_type=segment',
|
||||
})
|
||||
|
||||
const sortedSegments = conversationsResolved
|
||||
?.reduce((acc, conversation) => {
|
||||
const newAcc = [...acc]
|
||||
const conversationSegments = conversation.meta.segments
|
||||
|
||||
conversationSegments
|
||||
.filter((segment) => /#[\d]+/.exec(segment))
|
||||
.forEach((segment) => {
|
||||
const segmentObjectIndex = newAcc.findIndex(
|
||||
(segmentObject) => segmentObject.name === segment
|
||||
)
|
||||
if (segmentObjectIndex < 0) {
|
||||
newAcc.push({ name: segment, count: 1 })
|
||||
} else {
|
||||
newAcc[segmentObjectIndex].count += 1
|
||||
}
|
||||
})
|
||||
return newAcc
|
||||
}, [])
|
||||
?.sort((t1, t2) => t2.count - t1.count)
|
||||
|
||||
return fetchGithubIssuesFromTags(sortedSegments)
|
||||
}
|
||||
|
||||
async function fetchZammadUserFeedbackIssues() {
|
||||
const tags = await fetch(
|
||||
'https://mon-entreprise.zammad.com/api/v1/tag_list',
|
||||
{
|
||||
headers: new Headers({
|
||||
Authorization: `Token token=${process.env.ZAMMAD_API_SECRET_KEY}`,
|
||||
}),
|
||||
}
|
||||
).then((r) => r.json())
|
||||
const sortedTags = tags
|
||||
.sort((t1, t2) => t2.count - t1.count)
|
||||
.filter(({ name }) => /#[\d]+/.exec(name))
|
||||
|
||||
return fetchGithubIssuesFromTags(sortedTags)
|
||||
}
|
||||
|
||||
async function fetchAllUserFeedbackIssues() {
|
||||
const crispFeedbackIssues = await fetchCrispUserFeedbackIssues()
|
||||
const zammadFeedbackIssues = await fetchZammadUserFeedbackIssues()
|
||||
|
||||
const allIssues = [...(crispFeedbackIssues || [])]
|
||||
|
||||
zammadFeedbackIssues.forEach((zammadIssue) => {
|
||||
const issueIndex = allIssues.findIndex(
|
||||
(issue) => issue.number === zammadIssue.number
|
||||
)
|
||||
if (issueIndex > 0) {
|
||||
allIssues[issueIndex].count += zammadIssue.count
|
||||
} else {
|
||||
allIssues.push(zammadIssue)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
open: issues.filter((s) => !s.closedAt),
|
||||
closed: issues
|
||||
open: allIssues.filter((s) => !s.closedAt),
|
||||
closed: allIssues
|
||||
.filter((s) => s.closedAt)
|
||||
.sort((i1, i2) => new Date(i2.closedAt) - new Date(i1.closedAt)),
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
createDataDir()
|
||||
// In case we cannot fetch the release (the API is down or the Authorization
|
||||
|
@ -443,8 +582,8 @@ async function main() {
|
|||
fetchDailyVisits(),
|
||||
fetchMonthlyVisits(),
|
||||
fetchApi(buildSatisfactionQuery()),
|
||||
fetchUserFeedbackIssues(),
|
||||
fetchUserAnswersStats(),
|
||||
fetchAllUserFeedbackIssues(),
|
||||
fetchAllUserAnswerStats(),
|
||||
])
|
||||
const satisfaction = uniformiseData(flattenPage(await rawSatisfaction)).map(
|
||||
(page) => {
|
||||
|
|
|
@ -1,77 +1,97 @@
|
|||
import { ScrollToElement } from '@/components/utils/Scroll'
|
||||
import { useEffect } from 'react'
|
||||
import { TextAreaField, TextField } from '@/design-system'
|
||||
import { Button } from '@/design-system/buttons'
|
||||
import { Body } from '@/design-system/typography/paragraphs'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
declare global {
|
||||
interface JQuery {
|
||||
ZammadForm(options: any): void
|
||||
}
|
||||
}
|
||||
const SERVER_URL = import.meta.env.DEV
|
||||
? 'http://localhost:4000'
|
||||
: 'https://mon-entreprise-server.osc-fr1.scalingo.io'
|
||||
|
||||
const SHORT_MAX_LENGTH = 254
|
||||
|
||||
// TODO: we could implement the form logic ourselves to avoid including
|
||||
// https://mon-entreprise.zammad.com and https://code.jquery.com scripts
|
||||
export default function FeedbackForm() {
|
||||
// const tracker = useContext(TrackerContext)
|
||||
const [isSubmittedSuccessfully, setIsSubmittedSuccessfully] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const pathname = useLocation().pathname
|
||||
const page = pathname.split('/').slice(-1)[0]
|
||||
const isSimulateur = pathname.includes('simulateurs')
|
||||
const lang = useTranslation().i18n.language
|
||||
|
||||
useEffect(() => {
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://code.jquery.com/jquery-2.1.4.min.js'
|
||||
const { t } = useTranslation()
|
||||
|
||||
document.body.appendChild(script)
|
||||
setTimeout(() => {
|
||||
const script = document.createElement('script')
|
||||
script.id = 'zammad_form_script'
|
||||
script.async = true
|
||||
script.onload = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
$('#feedback-form').ZammadForm({
|
||||
messageTitle: `Remarque sur ${
|
||||
isSimulateur ? 'le simulateur' : 'la page'
|
||||
} ${page}`,
|
||||
messageSubmit: 'Envoyer',
|
||||
messageThankYou: 'Merci de votre retour !',
|
||||
lang,
|
||||
attributes: [
|
||||
{
|
||||
display:
|
||||
"Que pouvons-nous améliorer afin de mieux répondre à vos attentes ? (ne pas mettre d'informations personnelles)",
|
||||
name: 'body',
|
||||
tag: 'textarea',
|
||||
placeholder: 'Your Message...',
|
||||
defaultValue: '',
|
||||
rows: 7,
|
||||
},
|
||||
{
|
||||
display: 'Nom',
|
||||
name: 'name',
|
||||
tag: 'input',
|
||||
type: 'text',
|
||||
defaultValue: '-',
|
||||
},
|
||||
{
|
||||
display: 'Email (pour recevoir une réponse)',
|
||||
name: 'email',
|
||||
tag: 'input',
|
||||
type: 'email',
|
||||
placeholder: 'Your Email',
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
script.src = 'https://mon-entreprise.zammad.com/assets/form/form.js'
|
||||
document.body.appendChild(script)
|
||||
}, 100)
|
||||
}, [])
|
||||
const sendMessage = async () => {
|
||||
setIsLoading(true)
|
||||
const messageValue = (
|
||||
document.getElementById('message') as HTMLTextAreaElement
|
||||
)?.value
|
||||
const emailValue = (document.getElementById('email') as HTMLInputElement)
|
||||
?.value
|
||||
|
||||
try {
|
||||
await fetch(`${SERVER_URL}/send-crisp-message`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subject: `Suggestion sur la page : ${page}`,
|
||||
message: messageValue,
|
||||
email: emailValue,
|
||||
}),
|
||||
})
|
||||
setIsSubmittedSuccessfully(true)
|
||||
} catch (e) {
|
||||
// Show error message
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollToElement onlyIfNotVisible>
|
||||
<StyledFeedback id="feedback-form"></StyledFeedback>
|
||||
{isSubmittedSuccessfully && (
|
||||
<StyledBody>Merci de votre retour !</StyledBody>
|
||||
)}
|
||||
{!isSubmittedSuccessfully && (
|
||||
<StyledFeedback>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
void sendMessage()
|
||||
}}
|
||||
>
|
||||
<Body>
|
||||
Que pouvons-nous améliorer pour mieux répondre à vos attentes ?
|
||||
</Body>
|
||||
<StyledTextArea
|
||||
name="message"
|
||||
label={t('Votre message')}
|
||||
description={t(
|
||||
'Éviter de communiquer des informations personnelles'
|
||||
)}
|
||||
id="message"
|
||||
rows={7}
|
||||
isDisabled={isLoading}
|
||||
/>
|
||||
<StyledDiv>
|
||||
<StyledTextField
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
label={t('Votre adresse e-mail')}
|
||||
description={t(
|
||||
'Renseigner une adresse e-mail pour recevoir une réponse'
|
||||
)}
|
||||
isDisabled={isLoading}
|
||||
maxLength={SHORT_MAX_LENGTH}
|
||||
/>
|
||||
</StyledDiv>
|
||||
<StyledButton isDisabled={isLoading} type="submit">
|
||||
{t('Envoyer')}
|
||||
</StyledButton>
|
||||
</form>
|
||||
</StyledFeedback>
|
||||
)}
|
||||
</ScrollToElement>
|
||||
)
|
||||
}
|
||||
|
@ -82,15 +102,39 @@ const StyledFeedback = styled.div`
|
|||
font-family: ${({ theme }) => theme.fonts.main};
|
||||
text-align: left;
|
||||
|
||||
textarea,
|
||||
input {
|
||||
width: 100%;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
padding: ${({ theme }) => theme.spacings.sm};
|
||||
margin-top: ${({ theme }) => theme.spacings.xs};
|
||||
border-radius: ${({ theme }) => theme.box.borderRadius};
|
||||
border: 1px solid ${({ theme }) => theme.colors.extended.grey[500]};
|
||||
label {
|
||||
margin-bottom: 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledTextArea = styled(TextAreaField)`
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
padding: ${({ theme }) => theme.spacings.sm};
|
||||
border-radius: ${({ theme }) => theme.box.borderRadius};
|
||||
font-family: ${({ theme }) => theme.fonts.main};
|
||||
`
|
||||
|
||||
const StyledTextField = styled(TextField)`
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
padding: ${({ theme }) => theme.spacings.sm};
|
||||
font-family: ${({ theme }) => theme.fonts.main};
|
||||
`
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
margin-top: 1rem;
|
||||
`
|
||||
|
||||
const StyledBody = styled(Body)`
|
||||
font-size: 1.25rem;
|
||||
font-family: ${({ theme }) => theme.fonts.main};
|
||||
text-align: center;
|
||||
padding: 1rem 0;
|
||||
`
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
margin-top: 1rem;
|
||||
`
|
||||
|
|
|
@ -145,7 +145,7 @@ export default function PageFeedback({ customMessage }: PageFeedbackProps) {
|
|||
{state.showForm && (
|
||||
<Popover
|
||||
isOpen
|
||||
title="Votre avis nous interesse"
|
||||
title="Votre avis nous intéresse"
|
||||
isDismissable
|
||||
onClose={() => setState({ showThanks: true, showForm: false })}
|
||||
small
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
import { AriaTextFieldOptions, useTextField } from '@react-aria/textfield'
|
||||
import { ExtraSmallBody } from '@/design-system/typography/paragraphs'
|
||||
import { HTMLAttributes, RefObject, useRef } from 'react'
|
||||
import styled, { css } from 'styled-components'
|
||||
|
||||
const LABEL_HEIGHT = '1rem'
|
||||
|
||||
type TextAreaFieldProps = AriaTextFieldOptions<'textarea'> & {
|
||||
inputRef?: RefObject<HTMLTextAreaElement>
|
||||
small?: boolean
|
||||
rows?: number | undefined
|
||||
}
|
||||
|
||||
export default function TextAreaField(props: TextAreaFieldProps) {
|
||||
const ref = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const { labelProps, inputProps, descriptionProps, errorMessageProps } =
|
||||
useTextField(
|
||||
{ ...props, inputElementType: 'textarea' },
|
||||
props.inputRef || ref
|
||||
)
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledTextAreaContainer
|
||||
hasError={!!props.errorMessage || props.validationState === 'invalid'}
|
||||
hasLabel={!!props.label && !props.small}
|
||||
>
|
||||
<StyledTextArea
|
||||
{...(props as HTMLAttributes<HTMLTextAreaElement>)}
|
||||
{...(inputProps as HTMLAttributes<HTMLTextAreaElement>)}
|
||||
placeholder={inputProps.placeholder ?? ''}
|
||||
ref={props.inputRef || ref}
|
||||
/>
|
||||
{props.label && (
|
||||
<StyledLabel className={props.small ? 'sr-only' : ''} {...labelProps}>
|
||||
{props.label}
|
||||
</StyledLabel>
|
||||
)}
|
||||
</StyledTextAreaContainer>
|
||||
{props.errorMessage && (
|
||||
<StyledErrorMessage {...errorMessageProps}>
|
||||
{props.errorMessage}
|
||||
</StyledErrorMessage>
|
||||
)}
|
||||
{props.description && (
|
||||
<StyledDescription {...descriptionProps}>
|
||||
{props.description}
|
||||
</StyledDescription>
|
||||
)}
|
||||
</StyledContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export const StyledContainer = styled.div`
|
||||
width: 100%;
|
||||
`
|
||||
export const StyledTextArea = styled.textarea`
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
border: none;
|
||||
width: 100%;
|
||||
background: none;
|
||||
font-family: ${({ theme }) => theme.fonts.main};
|
||||
height: 100%;
|
||||
outline: none;
|
||||
transition: color 0.2s;
|
||||
::placeholder {
|
||||
${({ theme }) =>
|
||||
theme.darkMode &&
|
||||
css`
|
||||
opacity: 0.6;
|
||||
`}
|
||||
color: ${({ theme }) =>
|
||||
theme.colors.extended.grey[theme.darkMode ? 200 : 600]};
|
||||
}
|
||||
${({ theme }) =>
|
||||
theme.darkMode &&
|
||||
css`
|
||||
@media not print {
|
||||
color: ${theme.colors.extended.grey[100]} !important;
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
export const StyledLabel = styled.label`
|
||||
top: 0%;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
transform: translateY(0%);
|
||||
font-size: 0.75rem;
|
||||
line-height: ${LABEL_HEIGHT};
|
||||
font-family: ${({ theme }) => theme.fonts.main};
|
||||
padding: ${({ theme }) => `${theme.spacings.xxs} ${theme.spacings.sm}`};
|
||||
position: absolute;
|
||||
will-change: transform top font-size line-height color;
|
||||
transition: all 0.1s;
|
||||
${({ theme }) =>
|
||||
theme.darkMode &&
|
||||
css`
|
||||
@media not print {
|
||||
color: ${theme.colors.extended.grey[100]} !important;
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
export const StyledDescription = styled(ExtraSmallBody)`
|
||||
padding: ${({ theme }) => `${theme.spacings.xxs} ${theme.spacings.sm}`};
|
||||
will-change: color;
|
||||
transition: color 0.2s;
|
||||
margin-top: 0;
|
||||
`
|
||||
|
||||
export const StyledErrorMessage = styled(StyledDescription)`
|
||||
color: ${({ theme }) => theme.colors.extended.error[400]} !important;
|
||||
`
|
||||
|
||||
export const StyledSuffix = styled.span`
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
font-family: ${({ theme }) => theme.fonts.main};
|
||||
`
|
||||
|
||||
export const StyledTextAreaContainer = styled.div<{
|
||||
hasError: boolean
|
||||
hasLabel: boolean
|
||||
small?: boolean
|
||||
}>`
|
||||
border-radius: ${({ theme }) => theme.box.borderRadius};
|
||||
border: ${({ theme }) =>
|
||||
`${theme.box.borderWidth} solid ${
|
||||
theme.darkMode
|
||||
? theme.colors.extended.grey[100]
|
||||
: theme.colors.extended.grey[700]
|
||||
}`};
|
||||
outline: transparent solid 1px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
background-color: ${({ theme }) =>
|
||||
theme.darkMode
|
||||
? 'rgba(255, 255, 255, 20%)'
|
||||
: theme.colors.extended.grey[100]};
|
||||
align-items: center;
|
||||
transition: all 0.2s;
|
||||
|
||||
:focus-within {
|
||||
outline-color: ${({ theme, hasError }) =>
|
||||
hasError
|
||||
? theme.colors.extended.error[400]
|
||||
: theme.darkMode
|
||||
? theme.colors.bases.primary[100]
|
||||
: theme.colors.bases.primary[700]};
|
||||
}
|
||||
:focus-within ${StyledLabel} {
|
||||
color: ${({ theme }) => theme.colors.bases.primary[800]};
|
||||
}
|
||||
|
||||
:focus-within + ${StyledDescription} {
|
||||
${({ theme }) =>
|
||||
!theme.darkMode &&
|
||||
css`
|
||||
color: ${theme.colors.bases.primary[800]};
|
||||
`}
|
||||
}
|
||||
|
||||
${({ hasLabel }) =>
|
||||
hasLabel &&
|
||||
css`
|
||||
${StyledTextArea}:not(:focus):placeholder-shown {
|
||||
color: transparent;
|
||||
}
|
||||
${StyledTextArea}:not(:focus):placeholder-shown + ${StyledSuffix} {
|
||||
color: transparent;
|
||||
}
|
||||
`}
|
||||
|
||||
${StyledTextArea}:not(:focus):placeholder-shown:not(:empty) + ${StyledLabel} {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
${({ theme, hasError }) =>
|
||||
hasError &&
|
||||
css`
|
||||
&& {
|
||||
border-color: ${theme.colors.extended.error[400]};
|
||||
}
|
||||
&&& label {
|
||||
color: ${theme.colors.extended.error[400]};
|
||||
}
|
||||
`}
|
||||
|
||||
${StyledTextArea}, ${StyledSuffix} {
|
||||
padding: ${({ hasLabel, theme, small }) =>
|
||||
small
|
||||
? css`
|
||||
${theme.spacings.xxs} ${theme.spacings.xs}
|
||||
`
|
||||
: css`calc(${hasLabel ? LABEL_HEIGHT : '0rem'} + ${
|
||||
theme.spacings.xs
|
||||
}) ${theme.spacings.sm} ${theme.spacings.xs}`};
|
||||
}
|
||||
|
||||
${({ small }) =>
|
||||
small &&
|
||||
css`
|
||||
${StyledSuffix}, ${StyledTextArea} {
|
||||
font-size: 1rem;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
`}
|
||||
`
|
|
@ -104,6 +104,7 @@ export const StyledDescription = styled(ExtraSmallBody)`
|
|||
padding: ${({ theme }) => `${theme.spacings.xxs} ${theme.spacings.sm}`};
|
||||
will-change: color;
|
||||
transition: color 0.2s;
|
||||
margin-top: 0;
|
||||
`
|
||||
|
||||
export const StyledErrorMessage = styled(StyledDescription)`
|
||||
|
|
|
@ -4,3 +4,4 @@ export { default as DateField } from './DateField'
|
|||
export { default as NumberField } from './NumberField'
|
||||
export { default as SearchField } from './SearchField'
|
||||
export { default as TextField } from './TextField'
|
||||
export { default as TextAreaField } from './TextAreaField'
|
||||
|
|
262
yarn.lock
262
yarn.lock
|
@ -6481,6 +6481,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@socket.io/component-emitter@npm:~3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "@socket.io/component-emitter@npm:3.0.0"
|
||||
checksum: b5e909dbb16bcf27958d1bfb8319f3255f3a50f62fde78ecf9a584f39f916b928fdc5661519892eea912da082c6413d671c1e67bde70725c75ee62956aa67c26
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@storybook/addon-actions@npm:6.5.0-alpha.55, @storybook/addon-actions@npm:^6.5.0-alpha.49":
|
||||
version: 6.5.0-alpha.55
|
||||
resolution: "@storybook/addon-actions@npm:6.5.0-alpha.55"
|
||||
|
@ -10419,7 +10426,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"asap@npm:^2.0.0":
|
||||
"asap@npm:^2.0.0, asap@npm:~2.0.3":
|
||||
version: 2.0.6
|
||||
resolution: "asap@npm:2.0.6"
|
||||
checksum: b296c92c4b969e973260e47523207cd5769abd27c245a68c26dc7a0fe8053c55bb04360237cb51cab1df52be939da77150ace99ad331fb7fb13b3423ed73ff3d
|
||||
|
@ -10843,6 +10850,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"backo2@npm:~1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "backo2@npm:1.0.2"
|
||||
checksum: fda8d0a0f4810068d23715f2f45153146d6ee8f62dd827ce1e0b6cc3c8328e84ad61e11399a83931705cef702fe7cbb457856bf99b9bd10c4ed57b0786252385
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"backoff@npm:^2.5.0":
|
||||
version: 2.5.0
|
||||
resolution: "backoff@npm:2.5.0"
|
||||
|
@ -12960,6 +12974,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"crisp-api@npm:6.3.0":
|
||||
version: 6.3.0
|
||||
resolution: "crisp-api@npm:6.3.0"
|
||||
dependencies:
|
||||
fbemitter: 3.0.0
|
||||
got: 9.6.0
|
||||
socket.io-client: 4.4.1
|
||||
checksum: 684000db238933c59f752aed87833ca27f1da7a3fe23b1f46d9c8b166cd8a7f99f35417d5fa48bb3aa23f39206527de91b26568d5147ccbe238419189de99cb3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cron-parser@npm:^4.1.0, cron-parser@npm:^4.2.1":
|
||||
version: 4.5.0
|
||||
resolution: "cron-parser@npm:4.5.0"
|
||||
|
@ -13408,7 +13433,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4":
|
||||
"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2":
|
||||
version: 4.3.4
|
||||
resolution: "debug@npm:4.3.4"
|
||||
dependencies:
|
||||
|
@ -14354,6 +14379,30 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"engine.io-client@npm:~6.1.1":
|
||||
version: 6.1.1
|
||||
resolution: "engine.io-client@npm:6.1.1"
|
||||
dependencies:
|
||||
"@socket.io/component-emitter": ~3.0.0
|
||||
debug: ~4.3.1
|
||||
engine.io-parser: ~5.0.0
|
||||
has-cors: 1.1.0
|
||||
parseqs: 0.0.6
|
||||
parseuri: 0.0.6
|
||||
ws: ~8.2.3
|
||||
xmlhttprequest-ssl: ~2.0.0
|
||||
yeast: 0.1.2
|
||||
checksum: c2e1cec87ac8cf45842527bd072d1b2c5f14fbf9e57f110b4120335ed7bf5310a86da0d33b5906dd4774094ee499d534a498db467d3c1cb53c7a1109a593b05d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"engine.io-parser@npm:~5.0.0":
|
||||
version: 5.0.4
|
||||
resolution: "engine.io-parser@npm:5.0.4"
|
||||
checksum: d4ad0cef6ff63c350e35696da9bb3dbd180f67b56e93e90375010cc40393e6c0639b780d5680807e1d93a7e2e3d7b4a1c3b27cf75db28eb8cbf605bc1497da03
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"enhanced-resolve@npm:^4.5.0":
|
||||
version: 4.5.0
|
||||
resolution: "enhanced-resolve@npm:4.5.0"
|
||||
|
@ -16103,6 +16152,37 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fbemitter@npm:3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "fbemitter@npm:3.0.0"
|
||||
dependencies:
|
||||
fbjs: ^3.0.0
|
||||
checksum: 069690b8cdff3521ade3c9beb92ba0a38d818a86ef36dff8690e66749aef58809db4ac0d6938eb1cacea2dbef5f2a508952d455669590264cdc146bbe839f605
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fbjs-css-vars@npm:^1.0.0":
|
||||
version: 1.0.2
|
||||
resolution: "fbjs-css-vars@npm:1.0.2"
|
||||
checksum: 72baf6d22c45b75109118b4daecb6c8016d4c83c8c0f23f683f22e9d7c21f32fff6201d288df46eb561e3c7d4bb4489b8ad140b7f56444c453ba407e8bd28511
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fbjs@npm:^3.0.0":
|
||||
version: 3.0.4
|
||||
resolution: "fbjs@npm:3.0.4"
|
||||
dependencies:
|
||||
cross-fetch: ^3.1.5
|
||||
fbjs-css-vars: ^1.0.0
|
||||
loose-envify: ^1.0.0
|
||||
object-assign: ^4.1.0
|
||||
promise: ^7.1.1
|
||||
setimmediate: ^1.0.5
|
||||
ua-parser-js: ^0.7.30
|
||||
checksum: 8b23a3550fcda8a9109fca9475a3416590c18bb6825ea884192864ed686f67fcd618e308a140c9e5444fbd0168732e1ff3c092ba3d0c0ae1768969f32ba280c7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fd-slicer@npm:~1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "fd-slicer@npm:1.1.0"
|
||||
|
@ -17335,6 +17415,25 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"got@npm:9.6.0, got@npm:^9.6.0":
|
||||
version: 9.6.0
|
||||
resolution: "got@npm:9.6.0"
|
||||
dependencies:
|
||||
"@sindresorhus/is": ^0.14.0
|
||||
"@szmarczak/http-timer": ^1.1.2
|
||||
cacheable-request: ^6.0.0
|
||||
decompress-response: ^3.3.0
|
||||
duplexer3: ^0.1.4
|
||||
get-stream: ^4.1.0
|
||||
lowercase-keys: ^1.0.1
|
||||
mimic-response: ^1.0.1
|
||||
p-cancelable: ^1.0.0
|
||||
to-readable-stream: ^1.0.0
|
||||
url-parse-lax: ^3.0.0
|
||||
checksum: 941807bd9704bacf5eb401f0cc1212ffa1f67c6642f2d028fd75900471c221b1da2b8527f4553d2558f3faeda62ea1cf31665f8b002c6137f5de8732f07370b0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"got@npm:^10.0.0, got@npm:^10.7.0":
|
||||
version: 10.7.0
|
||||
resolution: "got@npm:10.7.0"
|
||||
|
@ -17402,25 +17501,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"got@npm:^9.6.0":
|
||||
version: 9.6.0
|
||||
resolution: "got@npm:9.6.0"
|
||||
dependencies:
|
||||
"@sindresorhus/is": ^0.14.0
|
||||
"@szmarczak/http-timer": ^1.1.2
|
||||
cacheable-request: ^6.0.0
|
||||
decompress-response: ^3.3.0
|
||||
duplexer3: ^0.1.4
|
||||
get-stream: ^4.1.0
|
||||
lowercase-keys: ^1.0.1
|
||||
mimic-response: ^1.0.1
|
||||
p-cancelable: ^1.0.0
|
||||
to-readable-stream: ^1.0.0
|
||||
url-parse-lax: ^3.0.0
|
||||
checksum: 941807bd9704bacf5eb401f0cc1212ffa1f67c6642f2d028fd75900471c221b1da2b8527f4553d2558f3faeda62ea1cf31665f8b002c6137f5de8732f07370b0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"graceful-fs@npm:^4.0.0, graceful-fs@npm:^4.1.10, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.3, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9":
|
||||
version: 4.2.10
|
||||
resolution: "graceful-fs@npm:4.2.10"
|
||||
|
@ -17489,6 +17569,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"has-cors@npm:1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "has-cors@npm:1.1.0"
|
||||
checksum: 549ce94113fd23895b22d71ade9809918577b8558cd4d701fe79045d8b1d58d87eba870260b28f6a3229be933a691c55653afd496d0fc52e98fd2ff577f01197
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"has-flag@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "has-flag@npm:3.0.0"
|
||||
|
@ -23256,6 +23343,20 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"parseqs@npm:0.0.6":
|
||||
version: 0.0.6
|
||||
resolution: "parseqs@npm:0.0.6"
|
||||
checksum: 7fc4ff4ba59764060bb8529875f6d4313056ea6939ff579b22dd7bd6f6033035e1fd2d6a559ab48ef0a7fa29a9d7731c982bfd1594e9115141fe1c328485ce9e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"parseuri@npm:0.0.6":
|
||||
version: 0.0.6
|
||||
resolution: "parseuri@npm:0.0.6"
|
||||
checksum: fa430e40f0c75293a28e5f1023da5f51a5038d5e34c48c517b0d5187143f6bcc67d3091a062b68765db4a22757e488c7d15854f9d1921f2c2b9afa5ca0629a84
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"parseurl@npm:^1.3.2, parseurl@npm:~1.3.2, parseurl@npm:~1.3.3":
|
||||
version: 1.3.3
|
||||
resolution: "parseurl@npm:1.3.3"
|
||||
|
@ -23929,6 +24030,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"promise@npm:^7.1.1":
|
||||
version: 7.3.1
|
||||
resolution: "promise@npm:7.3.1"
|
||||
dependencies:
|
||||
asap: ~2.0.3
|
||||
checksum: 475bb069130179fbd27ed2ab45f26d8862376a137a57314cf53310bdd85cc986a826fd585829be97ebc0aaf10e9d8e68be1bfe5a4a0364144b1f9eedfa940cf1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prompts@npm:^2.4.0":
|
||||
version: 2.4.2
|
||||
resolution: "prompts@npm:2.4.2"
|
||||
|
@ -25951,6 +26061,31 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"server@workspace:server":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "server@workspace:server"
|
||||
dependencies:
|
||||
"@breejs/later": ^4.1.0
|
||||
"@koa/cors": ^3.4.1
|
||||
"@koa/router": ^12.0.0
|
||||
"@types/koa": ^2.13.5
|
||||
"@types/koa__cors": ^3.3.0
|
||||
"@types/koa__router": ^12.0.0
|
||||
"@types/node": ^18.8.3
|
||||
"@types/safe-timers": ^1.1.0
|
||||
bree: ^9.1.2
|
||||
crisp-api: 6.3.0
|
||||
dotenv: ^16.0.3
|
||||
got: ^12.5.1
|
||||
koa: ^2.13.4
|
||||
koa-body: ^5.0.0
|
||||
mongodb: ^4.10.0
|
||||
nodemon: ^2.0.20
|
||||
ts-node: ^10.9.1
|
||||
typescript: ^4.8.4
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"set-blocking@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "set-blocking@npm:2.0.0"
|
||||
|
@ -25970,7 +26105,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"setimmediate@npm:^1.0.4":
|
||||
"setimmediate@npm:^1.0.4, setimmediate@npm:^1.0.5":
|
||||
version: 1.0.5
|
||||
resolution: "setimmediate@npm:1.0.5"
|
||||
checksum: c9a6f2c5b51a2dabdc0247db9c46460152ffc62ee139f3157440bd48e7c59425093f42719ac1d7931f054f153e2d26cf37dfeb8da17a794a58198a2705e527fd
|
||||
|
@ -26312,6 +26447,30 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"socket.io-client@npm:4.4.1":
|
||||
version: 4.4.1
|
||||
resolution: "socket.io-client@npm:4.4.1"
|
||||
dependencies:
|
||||
"@socket.io/component-emitter": ~3.0.0
|
||||
backo2: ~1.0.2
|
||||
debug: ~4.3.2
|
||||
engine.io-client: ~6.1.1
|
||||
parseuri: 0.0.6
|
||||
socket.io-parser: ~4.1.1
|
||||
checksum: 4c7f77f439b72f851fa162603b45bdabc94154ad6bd7b1d1c1658b45cd25d7f495293c08f71cd3ce96bccc477045d99840c10a92e1da9c13e9a5ff939edde30d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"socket.io-parser@npm:~4.1.1":
|
||||
version: 4.1.2
|
||||
resolution: "socket.io-parser@npm:4.1.2"
|
||||
dependencies:
|
||||
"@socket.io/component-emitter": ~3.0.0
|
||||
debug: ~4.3.1
|
||||
checksum: cd13cdbda929cce610b39fbf7f2c6aa59e55cfc58f13b38c592d7eb45b19d5110bcb81150607a88f8644959f5d0a384467a2083d29c12e224c010a406377649b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"socks-proxy-agent@npm:^7.0.0":
|
||||
version: 7.0.0
|
||||
resolution: "socks-proxy-agent@npm:7.0.0"
|
||||
|
@ -26636,29 +26795,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"standup-mattermost-bot@workspace:standup-mattermost-bot":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "standup-mattermost-bot@workspace:standup-mattermost-bot"
|
||||
dependencies:
|
||||
"@breejs/later": ^4.1.0
|
||||
"@koa/cors": ^3.4.1
|
||||
"@koa/router": ^12.0.0
|
||||
"@types/koa": ^2.13.5
|
||||
"@types/koa__cors": ^3.3.0
|
||||
"@types/koa__router": ^12.0.0
|
||||
"@types/node": ^18.8.3
|
||||
"@types/safe-timers": ^1.1.0
|
||||
bree: ^9.1.2
|
||||
dotenv: ^16.0.3
|
||||
got: ^12.5.1
|
||||
koa: ^2.13.4
|
||||
mongodb: ^4.10.0
|
||||
nodemon: ^2.0.20
|
||||
ts-node: ^10.9.1
|
||||
typescript: ^4.8.4
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"state-toggle@npm:^1.0.0":
|
||||
version: 1.0.3
|
||||
resolution: "state-toggle@npm:1.0.3"
|
||||
|
@ -28270,6 +28406,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ua-parser-js@npm:^0.7.30":
|
||||
version: 0.7.31
|
||||
resolution: "ua-parser-js@npm:0.7.31"
|
||||
checksum: e2f8324a83d1715601576af85b2b6c03890699aaa7272950fc77ea925c70c5e4f75060ae147dc92124e49f7f0e3d6dd2b0a91e7f40d267e92df8894be967ba8b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uglify-js@npm:^3.1.4":
|
||||
version: 3.16.2
|
||||
resolution: "uglify-js@npm:3.16.2"
|
||||
|
@ -29937,6 +30080,21 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ws@npm:~8.2.3":
|
||||
version: 8.2.3
|
||||
resolution: "ws@npm:8.2.3"
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: ^5.0.2
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
checksum: c869296ccb45f218ac6d32f8f614cd85b50a21fd434caf11646008eef92173be53490810c5c23aea31bc527902261fbfd7b062197eea341b26128d4be56a85e4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"x-default-browser@npm:^0.4.0":
|
||||
version: 0.4.0
|
||||
resolution: "x-default-browser@npm:0.4.0"
|
||||
|
@ -29975,6 +30133,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"xmlhttprequest-ssl@npm:~2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "xmlhttprequest-ssl@npm:2.0.0"
|
||||
checksum: 1e98df67f004fec15754392a131343ea92e6ab5ac4d77e842378c5c4e4fd5b6a9134b169d96842cc19422d77b1606b8df84a5685562b3b698cb68441636f827e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"xtend@npm:^4.0.0, xtend@npm:^4.0.1, xtend@npm:~4.0.0, xtend@npm:~4.0.1":
|
||||
version: 4.0.2
|
||||
resolution: "xtend@npm:4.0.2"
|
||||
|
@ -30107,6 +30272,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yeast@npm:0.1.2":
|
||||
version: 0.1.2
|
||||
resolution: "yeast@npm:0.1.2"
|
||||
checksum: 81a250b69f601fed541e9518eb2972e75631dd81231689503d7f288612d4eec793b29c208d6807fd6bfc4c2a43614d0c6db233739a4ae6223e244aaed6a885c0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ylru@npm:^1.2.0":
|
||||
version: 1.3.2
|
||||
resolution: "ylru@npm:1.3.2"
|
||||
|
|
Loading…
Reference in New Issue