Skip to content

Commit

Permalink
Merge branch 'development' into 4391-pan-european-reasonableness-chec…
Browse files Browse the repository at this point in the history
…k-32-table-fix
  • Loading branch information
minotogna authored Feb 26, 2025
2 parents ed1d662 + 6c67926 commit 52e5cda
Show file tree
Hide file tree
Showing 193 changed files with 3,764 additions and 737 deletions.
4 changes: 4 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ module.exports = {
'react/function-component-definition': 'off',
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',

// Not possible to enforce destructureInSignature: 'never' - only 'ignore' is possible
'react/require-default-props': 'off',

// simple-import-sort rules

'simple-import-sort/exports': 'error',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@
"d3-tip": "^0.9.1",
"date-fns": "^2.28.0",
"diff": "^5.2.0",
"diff-dom": "^5.1.4",
"dotenv": "^16.0.0",
"emoji-mart-lite": "^0.6.1",
"express": "^4.17.3",
Expand Down
17 changes: 17 additions & 0 deletions src/client/components/DiffDOM/DiffDOM.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, { useMemo, useRef } from 'react'

import { useDOMChanges } from './hooks/useDOMChanges'
import { cleanDOM } from './hooks/utils'
import { DiffDOMProps } from './types'

const DiffDOM: React.FC<DiffDOMProps> = (props) => {
const { current = '', prev = '' } = props

const ref = useRef<HTMLDivElement>()
const __html = useMemo<string>(() => cleanDOM(prev), [prev])
useDOMChanges({ current, prev, ref })

return <div ref={ref} className="editorWYSIWYG jodit-wysiwyg diff-text" dangerouslySetInnerHTML={{ __html }} />
}

export default DiffDOM
8 changes: 8 additions & 0 deletions src/client/components/DiffDOM/hooks/_addElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { DiffInfoAddElement, DiffType } from 'client/components/DiffDOM/types'

import { applyDiffClassToElement } from './utils'

export const addElement = (info: DiffInfoAddElement) => {
// eslint-disable-next-line no-param-reassign
info.diff.element = applyDiffClassToElement(info.diff.element, DiffType.added)
}
26 changes: 26 additions & 0 deletions src/client/components/DiffDOM/hooks/_getTextDiffNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as Diff from 'diff'
import { Strings } from 'utils/strings'

export const getTextDiffNode = (currentValue: string, newValue: string): HTMLDivElement => {
const changes = Diff.diffLines(Strings.nbspToUnicode(currentValue), Strings.nbspToUnicode(newValue))

const newNode = document.createElement('div')

changes.forEach((change, i) => {
const { added, removed, value } = change

value.split('\n\r').forEach((text) => {
const span = document.createElement('span')
if (added) span.classList.add('added')
if (removed) span.classList.add('removed')
span.textContent = text
newNode.appendChild(span)

if (i < changes.length - 1) {
newNode.appendChild(document.createElement('br'))
}
})
})

return newNode
}
49 changes: 49 additions & 0 deletions src/client/components/DiffDOM/hooks/_postApplyRemoveElements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { MutableRefObject } from 'react'

import { DiffDOM } from 'diff-dom'
import { objToNode } from 'diff-dom/src/diffDOM/dom/fromVirtual'

import { applyDiffClassToElement } from 'client/components/DiffDOM/hooks/utils'
import { DiffInfoRemoveElement, DiffType } from 'client/components/DiffDOM/types'

type Props = {
diffDOM: DiffDOM
diffs: Array<DiffInfoRemoveElement>
ref: MutableRefObject<HTMLDivElement>
}

export const postApplyRemoveElements = (props: Props): void => {
const { diffDOM, diffs, ref } = props
// record of deleted nodes count by parent route
const removedCounts: Record<string, number> = {}

diffs.forEach((diffInfo) => {
const { diff } = diffInfo
const { element, route } = diff

// 1. add 'removed' class and convert removed element to node
const obj = applyDiffClassToElement(element, DiffType.removed)
const node = objToNode(obj, false, diffDOM.options)

// 2. get parent node from route
const parentRoute = route.slice(0, -1)
let parentNode: ChildNode = ref.current
parentRoute.forEach((routeIndex, iterationIndex) => {
const _parentRoute = route.slice(0, iterationIndex)
const _removedKey = JSON.stringify(_parentRoute)
const _removedCount = removedCounts[_removedKey] ?? 0
parentNode = parentNode?.childNodes?.[routeIndex + _removedCount]
})

// 3. insert removed node at new index
const removedKey = JSON.stringify(parentRoute)
const removedCount = removedCounts[removedKey] ?? 0
if (parentNode) {
const childIndex = route.slice(-1)?.at(0)
parentNode.insertBefore(node, parentNode.childNodes[childIndex + removedCount])
}

// 4. update parent route removed counts
removedCounts[removedKey] = removedCount + 1
})
}
19 changes: 19 additions & 0 deletions src/client/components/DiffDOM/hooks/_replaceElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { DiffElementNode, DiffInfoReplaceElement, DiffType } from 'client/components/DiffDOM/types'

import { applyDiffClassToElement } from './utils'

export const replaceElement = (info: DiffInfoReplaceElement) => {
const { oldValue } = info.diff
const { newValue } = info.diff

const newValueUpdated: DiffElementNode = {
nodeName: 'div',
attributes: { class: 'diff-text' },
childNodes: [
applyDiffClassToElement(oldValue, DiffType.removed),
applyDiffClassToElement(newValue, DiffType.added),
],
}
// eslint-disable-next-line no-param-reassign
info.diff.newValue = newValueUpdated
}
65 changes: 65 additions & 0 deletions src/client/components/DiffDOM/hooks/useDOMChanges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useLayoutEffect } from 'react'

import { DiffDOM, stringToObj } from 'diff-dom'

import {
DiffAction,
DiffDOMProps,
DiffInfo,
DiffInfoAddElement,
DiffInfoRemoveElement,
DiffInfoReplaceElement,
} from 'client/components/DiffDOM/types'

import { addElement } from './_addElement'
import { getTextDiffNode } from './_getTextDiffNode'
import { postApplyRemoveElements } from './_postApplyRemoveElements'
import { replaceElement } from './_replaceElement'
import { normalizeDiffDOM } from './utils'

type Props = DiffDOMProps & {
ref: React.MutableRefObject<HTMLDivElement>
}

export const useDOMChanges = (props: Props) => {
const { current, prev, ref } = props

useLayoutEffect(() => {
const removedDiffs: Array<DiffInfoRemoveElement> = []

const diffDOM = new DiffDOM({
caseSensitive: true,
preDiffApply: (info: DiffInfo<DiffAction, unknown>): boolean => {
switch (info.diff.action) {
case DiffAction.replaceElement:
replaceElement(info as DiffInfoReplaceElement)
return false
case DiffAction.addElement:
addElement(info as DiffInfoAddElement)
return false
case DiffAction.removeElement:
removedDiffs.push(info as DiffInfoRemoveElement)
return false
default:
return false
}
},
textDiff(node, currentValue, _expectedValue, newValue) {
if (node instanceof Text) {
node.replaceWith(getTextDiffNode(currentValue, newValue))
}
},
})

const normalizedPrev = normalizeDiffDOM(prev)
const normalizedCurrent = normalizeDiffDOM(current)

const objDiffPrev = stringToObj(normalizedPrev)
const objDiffCurrent = stringToObj(normalizedCurrent)
const diff = diffDOM.diff(objDiffPrev, objDiffCurrent)
diffDOM.apply(ref.current, diff)

// add removed nodes to dom
postApplyRemoveElements({ diffDOM, diffs: removedDiffs, ref })
}, [current, prev, ref])
}
39 changes: 39 additions & 0 deletions src/client/components/DiffDOM/hooks/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Strings } from 'utils/strings'

import { DiffElement, DiffElementNode, DiffElementText, DiffType } from 'client/components/DiffDOM/types'

export const cleanDOM = (value: string): string => {
return (
value
.replace(/[\n\t\r]/g, '')
// .replace(/\u00a0|&nbsp;/g, ' ')
.toString()
)
}

export const normalizeDiffDOM = (value: string): string => {
const cleanedValue = cleanDOM(value)

const parser = new DOMParser()
const doc = parser.parseFromString(`<div>${cleanedValue}</div>`, 'text/html')
const element = doc.body.firstElementChild as HTMLElement
element?.normalize()

return element ? `<div class="___diff">${element.innerHTML}</div>` : ''
}

export const applyDiffClassToElement = (element: DiffElement, type: DiffType): DiffElementNode => {
if (['#comment', '#text'].includes(element.nodeName)) {
// replace space with Unicode char
const data = Strings.nbspToUnicode((element as DiffElementText).data)
const textNode = { nodeName: element.nodeName, data }

return { nodeName: 'span', attributes: { class: type }, childNodes: [textNode] }
}

const elementNode = element as DiffElementNode
return {
...elementNode,
childNodes: elementNode.childNodes?.map((child) => applyDiffClassToElement(child, type)) || [],
}
}
1 change: 1 addition & 0 deletions src/client/components/DiffDOM/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './DiffDOM'
49 changes: 49 additions & 0 deletions src/client/components/DiffDOM/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export type DiffDOMProps = {
current: string
prev: string
}

export enum DiffType {
added = 'added',
removed = 'removed',
}

export type DiffElement = { nodeName: string }

export type DiffElementNode = DiffElement & {
attributes?: Record<string, string>
checked?: boolean
childNodes?: DiffElement[]
selected?: boolean
value?: string | number
}

export type DiffElementText = DiffElement & {
data: string
}

export enum DiffAction {
addElement = 'addElement',
removeElement = 'removeElement',
replaceElement = 'replaceElement',
}

type Diff<Action extends DiffAction, Props> = {
action: Action
route: Array<number>
} & Props

export type DiffInfo<
Action extends DiffAction = DiffAction,
Props extends Record<string, unknown> | void | unknown = void
> = {
diff: Diff<Action, Props>
node: HTMLElement
}

export type DiffInfoAddElement = DiffInfo<DiffAction.addElement, { element: DiffElement }>
export type DiffInfoRemoveElement = DiffInfo<DiffAction.removeElement, { element: DiffElement }>
export type DiffInfoReplaceElement = DiffInfo<
DiffAction.replaceElement,
{ newValue: DiffElement; oldValue: DiffElement }
>
21 changes: 21 additions & 0 deletions src/client/components/DiffText/DiffText.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@import 'src/client/style/partials';

.diff-text {
span {
word-break: break-word;
}

.added,
.removed {
font-weight: 600;
}

.added {
color: darken($ui-status-accepted, 10%);
}

.removed {
color: $ui-destructive;
text-decoration: line-through;
}
}
36 changes: 36 additions & 0 deletions src/client/components/DiffText/DiffText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import './DiffText.scss'
import React from 'react'

import classNames from 'classnames'
import { Change } from 'diff'

interface Props {
changes: Array<Change>
className?: string
}

const DiffText: React.FC<Props> = (props) => {
const { changes, className = '' } = props

return (
<div className={classNames('diff-text', className)}>
{changes?.map((change, i) => {
const { added, removed, value } = change
const key = `${value}_${String(i)}`

return (
<React.Fragment key={key}>
{value.split('\n\r').map((text, j) => (
<React.Fragment key={`${key}_${String(j)}`}>
<span className={classNames({ added, removed })}>{text}</span>
<br />
</React.Fragment>
))}
</React.Fragment>
)
})}
</div>
)
}

export default DiffText
1 change: 1 addition & 0 deletions src/client/components/DiffText/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './DiffText'
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ActivityLog } from 'meta/assessment'
import { HistoryTarget } from 'meta/cycleData'
import { HistoryTarget } from 'meta/cycleData/historyActivities'

export type Props = {
datum: ActivityLog<never>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useMemo } from 'react'

import { ActivityLog } from 'meta/assessment'
import { HistoryTarget } from 'meta/cycleData'
import { HistoryTarget } from 'meta/cycleData/historyActivities'

import Item from 'client/components/Navigation/NavAssessment/History/Items/Item'
import { Column } from 'client/components/TablePaginated'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HistoryTarget } from 'meta/cycleData'
import { HistoryTarget } from 'meta/cycleData/historyActivities'

// Utility functions to get the path to the state that needs to be updated

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ActivityLogDescription, CommentableDescriptionValue } from 'meta/assessment'
import { HistoryTarget } from 'meta/cycleData'
import { HistoryTarget } from 'meta/cycleData/historyActivities'

export const getTargetValue: Record<
HistoryTarget,
Expand Down
Loading

0 comments on commit 52e5cda

Please sign in to comment.