@eeacms/volto-slate-footnote 6.3.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env ADDED
@@ -0,0 +1,3 @@
1
+ # Jest configuration variables
2
+ # - possible values: ON, OFF
3
+ JEST_USE_SETUP=OFF
package/.eslintrc.js ADDED
@@ -0,0 +1,65 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const projectRootPath = fs.realpathSync(__dirname + '/../../../');
4
+
5
+ let voltoPath = path.join(projectRootPath, 'node_modules/@plone/volto');
6
+ let configFile;
7
+ if (fs.existsSync(`${projectRootPath}/tsconfig.json`))
8
+ configFile = `${projectRootPath}/tsconfig.json`;
9
+ else if (fs.existsSync(`${projectRootPath}/jsconfig.json`))
10
+ configFile = `${projectRootPath}/jsconfig.json`;
11
+
12
+ if (configFile) {
13
+ const jsConfig = require(configFile).compilerOptions;
14
+ const pathsConfig = jsConfig.paths;
15
+ if (pathsConfig['@plone/volto'])
16
+ voltoPath = `./${jsConfig.baseUrl}/${pathsConfig['@plone/volto'][0]}`;
17
+ }
18
+
19
+ const AddonConfigurationRegistry = require(`${voltoPath}/addon-registry.js`);
20
+ const reg = new AddonConfigurationRegistry(projectRootPath);
21
+
22
+ // Extends ESlint configuration for adding the aliases to `src` directories in Volto addons
23
+ const addonAliases = Object.keys(reg.packages).map((o) => [
24
+ o,
25
+ reg.packages[o].modulePath,
26
+ ]);
27
+
28
+ const addonExtenders = reg.getEslintExtenders().map((m) => require(m));
29
+
30
+ const defaultConfig = {
31
+ extends: `${voltoPath}/.eslintrc`,
32
+ settings: {
33
+ 'import/resolver': {
34
+ alias: {
35
+ map: [
36
+ ['@plone/volto', '@plone/volto/src'],
37
+ ['@plone/volto-slate', '@plone/volto/packages/volto-slate/src'],
38
+ ...addonAliases,
39
+ ['@package', `${__dirname}/src`],
40
+ ['@root', `${__dirname}/src`],
41
+ ['~', `${__dirname}/src`],
42
+ ],
43
+ extensions: ['.js', '.jsx', '.json'],
44
+ },
45
+ 'babel-plugin-root-import': {
46
+ rootPathSuffix: 'src',
47
+ },
48
+ },
49
+ },
50
+ rules: {
51
+ 'react/jsx-no-target-blank': [
52
+ 'error',
53
+ {
54
+ allowReferrer: true,
55
+ },
56
+ ],
57
+ }
58
+ };
59
+
60
+ const config = addonExtenders.reduce(
61
+ (acc, extender) => extender.modify(acc),
62
+ defaultConfig,
63
+ );
64
+
65
+ module.exports = config;
package/CHANGELOG.md CHANGED
@@ -4,7 +4,15 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
- ### [6.3.0](https://github.com/eea/volto-slate-footnote/compare/6.2.3...6.3.0) - 27 March 2024
7
+ ### [7.0.0](https://github.com/eea/volto-slate-footnote/compare/6.3.0...7.0.0) - 22 April 2024
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat: Release 7.0.0 - Volto 17 support [alin - [`9d04cb5`](https://github.com/eea/volto-slate-footnote/commit/9d04cb54fca5e925785511883573fab7a5ff4c76)]
12
+
13
+ #### :hammer_and_wrench: Others
14
+
15
+ ### [6.3.0](https://github.com/eea/volto-slate-footnote/compare/6.2.3...6.3.0) - 28 March 2024
8
16
 
9
17
  #### :hammer_and_wrench: Others
10
18
 
package/Jenkinsfile CHANGED
@@ -9,11 +9,12 @@ pipeline {
9
9
  environment {
10
10
  GIT_NAME = "volto-slate-footnote"
11
11
  NAMESPACE = "@eeacms"
12
- SONARQUBE_TAGS = "volto.eea.europa.eu,biodiversity.europa.eu,www.eea.europa.eu-ims,climate-energy.eea.europa.eu,sustainability.eionet.europa.eu,forest.eea.europa.eu,clms.land.copernicus.eu,industry.eea.europa.eu,water.europa.eu-freshwater,demo-www.eea.europa.eu,clmsdemo.devel6cph.eea.europa.eu,water.europa.eu-marine,climate-adapt.eea.europa.eu,climate-advisory-board.devel4cph.eea.europa.eu,climate-advisory-board.europa.eu,www.eea.europa.eu-en,insitu-frontend.eionet.europa.eu"
12
+ SONARQUBE_TAGS = "volto.eea.europa.eu,biodiversity.europa.eu,www.eea.europa.eu-ims,climate-energy.eea.europa.eu,forest.eea.europa.eu,clms.land.copernicus.eu,industry.eea.europa.eu,water.europa.eu-freshwater,demo-www.eea.europa.eu,clmsdemo.devel6cph.eea.europa.eu,water.europa.eu-marine,climate-adapt.eea.europa.eu,climate-advisory-board.devel4cph.eea.europa.eu,climate-advisory-board.europa.eu,www.eea.europa.eu-en,insitu-frontend.eionet.europa.eu,insitu.copernicus.eu"
13
13
  DEPENDENCIES = ""
14
14
  BACKEND_PROFILES = "eea.kitkat:testing"
15
15
  BACKEND_ADDONS = ""
16
- VOLTO = "16"
16
+ VOLTO = "17"
17
+ VOLTO16_BREAKING_CHANGES = "no"
17
18
  IMAGE_NAME = BUILD_TAG.toLowerCase()
18
19
  }
19
20
 
@@ -44,6 +45,7 @@ pipeline {
44
45
  }
45
46
  steps {
46
47
  script {
48
+ checkout scm
47
49
  withCredentials([string(credentialsId: 'eea-jenkins-token', variable: 'GITHUB_TOKEN')]) {
48
50
  check_result = sh script: '''docker run --pull always -i --rm --name="$IMAGE_NAME-gitflow-check" -e GIT_TOKEN="$GITHUB_TOKEN" -e GIT_BRANCH="$BRANCH_NAME" -e GIT_ORG="$GIT_ORG" -e GIT_NAME="$GIT_NAME" eeacms/gitflow /check_if_testing_needed.sh''', returnStatus: true
49
51
 
@@ -61,7 +63,6 @@ pipeline {
61
63
  allOf {
62
64
  not { environment name: 'CHANGE_ID', value: '' }
63
65
  environment name: 'CHANGE_TARGET', value: 'develop'
64
- environment name: 'SKIP_TESTS', value: ''
65
66
  }
66
67
  allOf {
67
68
  environment name: 'CHANGE_ID', value: ''
@@ -69,25 +70,27 @@ pipeline {
69
70
  not { changelog '.*^Automated release [0-9\\.]+$' }
70
71
  branch 'master'
71
72
  }
72
- environment name: 'SKIP_TESTS', value: ''
73
73
  }
74
74
  }
75
75
  }
76
- stages {
77
- stage('Build test image') {
78
- steps {
79
- checkout scm
80
- sh '''docker build --pull --build-arg="VOLTO_VERSION=$VOLTO" --build-arg="ADDON_NAME=$NAMESPACE/$GIT_NAME" --build-arg="ADDON_PATH=$GIT_NAME" . -t $IMAGE_NAME-frontend'''
76
+ parallel {
77
+
78
+ stage('Volto 17') {
79
+ agent { node { label 'docker-1.13'} }
80
+ stages {
81
+ stage('Build test image') {
82
+ steps {
83
+ sh '''docker build --pull --build-arg="VOLTO_VERSION=$VOLTO" --build-arg="ADDON_NAME=$NAMESPACE/$GIT_NAME" --build-arg="ADDON_PATH=$GIT_NAME" . -t $IMAGE_NAME-frontend'''
84
+ }
81
85
  }
82
- }
83
-
84
- stage('Fix code') {
85
- when {
86
+
87
+ stage('Fix code') {
88
+ when {
86
89
  environment name: 'CHANGE_ID', value: ''
87
90
  not { branch 'master' }
88
- }
89
- steps {
90
- script {
91
+ }
92
+ steps {
93
+ script {
91
94
  fix_result = sh(script: '''docker run --name="$IMAGE_NAME-fix" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend ci-fix''', returnStatus: true)
92
95
  sh '''docker cp $IMAGE_NAME-fix:/app/src/addons/$GIT_NAME/src .'''
93
96
  sh '''docker rm -v $IMAGE_NAME-fix'''
@@ -105,31 +108,31 @@ pipeline {
105
108
  sh '''exit 1'''
106
109
  }
107
110
  }
111
+ }
108
112
  }
109
- }
110
113
 
111
- stage('ES lint') {
112
- steps {
113
- sh '''docker run --rm --name="$IMAGE_NAME-eslint" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend lint'''
114
+ stage('ES lint') {
115
+ when { environment name: 'SKIP_TESTS', value: '' }
116
+ steps {
117
+ sh '''docker run --rm --name="$IMAGE_NAME-eslint" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend lint'''
118
+ }
114
119
  }
115
- }
116
120
 
117
- stage('Style lint') {
118
- steps {
119
- sh '''docker run --rm --name="$IMAGE_NAME-stylelint" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend stylelint'''
121
+ stage('Style lint') {
122
+ when { environment name: 'SKIP_TESTS', value: '' }
123
+ steps {
124
+ sh '''docker run --rm --name="$IMAGE_NAME-stylelint" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend stylelint'''
125
+ }
120
126
  }
121
- }
122
127
 
123
- stage('Prettier') {
124
- steps {
125
- sh '''docker run --rm --name="$IMAGE_NAME-prettier" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend prettier'''
128
+ stage('Prettier') {
129
+ when { environment name: 'SKIP_TESTS', value: '' }
130
+ steps {
131
+ sh '''docker run --rm --name="$IMAGE_NAME-prettier" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend prettier'''
132
+ }
126
133
  }
127
- }
128
-
129
- stage('Coverage Tests') {
130
- parallel {
131
-
132
- stage('Unit tests') {
134
+ stage('Unit tests') {
135
+ when { environment name: 'SKIP_TESTS', value: '' }
133
136
  steps {
134
137
  script {
135
138
  try {
@@ -155,17 +158,24 @@ pipeline {
155
158
  }
156
159
  }
157
160
  }
158
- }
161
+ }
159
162
 
160
- stage('Integration tests') {
163
+ stage('Integration tests') {
164
+ when { environment name: 'SKIP_TESTS', value: '' }
161
165
  steps {
162
166
  script {
163
167
  try {
164
168
  sh '''docker run --pull always --rm -d --name="$IMAGE_NAME-plone" -e SITE="Plone" -e PROFILES="$BACKEND_PROFILES" -e ADDONS="$BACKEND_ADDONS" eeacms/plone-backend'''
165
- sh '''docker run -d --shm-size=3g --link $IMAGE_NAME-plone:plone --name="$IMAGE_NAME-cypress" -e "RAZZLE_INTERNAL_API_PATH=http://plone:8080/Plone" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend start-ci'''
169
+ sh '''docker run -d --shm-size=4g --link $IMAGE_NAME-plone:plone --name="$IMAGE_NAME-cypress" -e "RAZZLE_INTERNAL_API_PATH=http://plone:8080/Plone" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend start-ci'''
170
+ frontend = sh script:'''docker exec --workdir=/app/src/addons/${GIT_NAME} $IMAGE_NAME-cypress make check-ci''', returnStatus: true
171
+ if ( frontend != 0 ) {
172
+ sh '''docker logs $IMAGE_NAME-cypress; exit 1'''
173
+ }
174
+
166
175
  sh '''timeout -s 9 1800 docker exec --workdir=/app/src/addons/${GIT_NAME} $IMAGE_NAME-cypress make cypress-ci'''
167
176
  } finally {
168
177
  try {
178
+ if ( frontend == 0 ) {
169
179
  sh '''rm -rf cypress-videos cypress-results cypress-coverage cypress-screenshots'''
170
180
  sh '''mkdir -p cypress-videos cypress-results cypress-coverage cypress-screenshots'''
171
181
  videos = sh script: '''docker cp $IMAGE_NAME-cypress:/app/src/addons/$GIT_NAME/cypress/videos cypress-videos/''', returnStatus: true
@@ -189,6 +199,7 @@ pipeline {
189
199
  sh '''for file in $(find cypress-results -name *.xml); do if [ $(grep -E 'failures="[1-9].*"' $file | wc -l) -eq 0 ]; then testname=$(grep -E 'file=.*failures="0"' $file | sed 's#.* file=".*\\/\\(.*\\.[jsxt]\\+\\)" time.*#\\1#' ); rm -f cypress-videos/videos/$testname.mp4; fi; done'''
190
200
  archiveArtifacts artifacts: 'cypress-videos/**/*.mp4', fingerprint: true, allowEmptyArchive: true
191
201
  }
202
+ }
192
203
  } finally {
193
204
  catchError(buildResult: 'SUCCESS', stageResult: 'SUCCESS') {
194
205
  junit testResults: 'cypress-results/**/*.xml', allowEmptyResults: true
@@ -204,16 +215,7 @@ pipeline {
204
215
  }
205
216
  }
206
217
  }
207
- }
208
218
  }
209
- }
210
- }
211
- post {
212
- always {
213
- sh script: "docker rmi $IMAGE_NAME-frontend", returnStatus: true
214
- }
215
- }
216
- }
217
219
 
218
220
  stage('Report to SonarQube') {
219
221
  when {
@@ -221,9 +223,11 @@ pipeline {
221
223
  allOf {
222
224
  not { environment name: 'CHANGE_ID', value: '' }
223
225
  environment name: 'CHANGE_TARGET', value: 'develop'
226
+ environment name: 'SKIP_TESTS', value: ''
224
227
  }
225
228
  allOf {
226
229
  environment name: 'CHANGE_ID', value: ''
230
+ environment name: 'SKIP_TESTS', value: ''
227
231
  anyOf {
228
232
  allOf {
229
233
  branch 'develop'
@@ -248,14 +252,107 @@ pipeline {
248
252
  }
249
253
  }
250
254
 
255
+
256
+ }
257
+ }
258
+
259
+ stage('Volto 16') {
260
+ agent { node { label 'integration'} }
261
+ when {
262
+ environment name: 'SKIP_TESTS', value: ''
263
+ not { environment name: 'VOLTO16_BREAKING_CHANGES', value: 'yes' }
264
+ }
265
+ stages {
266
+ stage('Build test image') {
267
+ steps {
268
+ sh '''docker build --pull --build-arg="VOLTO_VERSION=16" --build-arg="ADDON_NAME=$NAMESPACE/$GIT_NAME" --build-arg="ADDON_PATH=$GIT_NAME" . -t $IMAGE_NAME-frontend16'''
269
+ }
270
+ }
271
+
272
+ stage('Unit tests Volto 16') {
273
+ steps {
274
+ script {
275
+ try {
276
+ sh '''docker run --name="$IMAGE_NAME-volto16" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend16 test-ci'''
277
+ sh '''rm -rf xunit-reports16'''
278
+ sh '''mkdir -p xunit-reports16'''
279
+ sh '''docker cp $IMAGE_NAME-volto16:/app/junit.xml xunit-reports16/'''
280
+ } finally {
281
+ catchError(buildResult: 'SUCCESS', stageResult: 'SUCCESS') {
282
+ junit testResults: 'xunit-reports16/junit.xml', allowEmptyResults: true
283
+ }
284
+ sh script: '''docker rm -v $IMAGE_NAME-volto16''', returnStatus: true
285
+ }
286
+ }
287
+ }
288
+ }
289
+
290
+ stage('Integration tests Volto 16') {
291
+ steps {
292
+ script {
293
+ try {
294
+ sh '''docker run --pull always --rm -d --name="$IMAGE_NAME-plone16" -e SITE="Plone" -e PROFILES="$BACKEND_PROFILES" -e ADDONS="$BACKEND_ADDONS" eeacms/plone-backend'''
295
+ sh '''docker run -d --shm-size=4g --link $IMAGE_NAME-plone16:plone --name="$IMAGE_NAME-cypress16" -e "RAZZLE_INTERNAL_API_PATH=http://plone:8080/Plone" --entrypoint=make --workdir=/app/src/addons/$GIT_NAME $IMAGE_NAME-frontend16 start-ci'''
296
+ frontend = sh script:'''docker exec --workdir=/app/src/addons/${GIT_NAME} $IMAGE_NAME-cypress16 make check-ci''', returnStatus: true
297
+ if ( frontend != 0 ) {
298
+ sh '''docker logs $IMAGE_NAME-cypress16; exit 1'''
299
+ }
300
+ sh '''timeout -s 9 1800 docker exec --workdir=/app/src/addons/${GIT_NAME} $IMAGE_NAME-cypress16 make cypress-ci'''
301
+ } finally {
302
+ try {
303
+ if ( frontend == 0 ) {
304
+ sh '''rm -rf cypress-videos16 cypress-results16 cypress-coverage16 cypress-screenshots16'''
305
+ sh '''mkdir -p cypress-videos16 cypress-results16 cypress-coverage16 cypress-screenshots16'''
306
+ videos = sh script: '''docker cp $IMAGE_NAME-cypress16:/app/src/addons/$GIT_NAME/cypress/videos cypress-videos16/''', returnStatus: true
307
+ sh '''docker cp $IMAGE_NAME-cypress16:/app/src/addons/$GIT_NAME/cypress/reports cypress-results16/'''
308
+ screenshots = sh script: '''docker cp $IMAGE_NAME-cypress16:/app/src/addons/$GIT_NAME/cypress/screenshots cypress-screenshots16''', returnStatus: true
309
+
310
+ archiveArtifacts artifacts: 'cypress-screenshots16/**', fingerprint: true, allowEmptyArchive: true
311
+
312
+ if ( videos == 0 ) {
313
+ sh '''for file in $(find cypress-results16 -name *.xml); do if [ $(grep -E 'failures="[1-9].*"' $file | wc -l) -eq 0 ]; then testname=$(grep -E 'file=.*failures="0"' $file | sed 's#.* file=".*\\/\\(.*\\.[jsxt]\\+\\)" time.*#\\1#' ); rm -f cypress-videos16/videos/$testname.mp4; fi; done'''
314
+ archiveArtifacts artifacts: 'cypress-videos16/**/*.mp4', fingerprint: true, allowEmptyArchive: true
315
+ }
316
+ }
317
+ } finally {
318
+ catchError(buildResult: 'SUCCESS', stageResult: 'SUCCESS') {
319
+ junit testResults: 'cypress-results16/**/*.xml', allowEmptyResults: true
320
+ }
321
+ catchError(buildResult: 'SUCCESS', stageResult: 'SUCCESS') {
322
+ sh '''docker logs $IMAGE_NAME-cypress16'''
323
+ }
324
+ sh script: "docker stop $IMAGE_NAME-cypress16", returnStatus: true
325
+ sh script: "docker stop $IMAGE_NAME-plone16", returnStatus: true
326
+ sh script: "docker rm -v $IMAGE_NAME-plone16", returnStatus: true
327
+ sh script: "docker rm -v $IMAGE_NAME-cypress16", returnStatus: true
328
+ }
329
+ }
330
+ }
331
+ }
332
+ }
333
+
334
+ }
335
+ }
336
+ }
337
+ post {
338
+ always {
339
+ sh script: "docker rmi $IMAGE_NAME-frontend", returnStatus: true
340
+ sh script: "docker rmi $IMAGE_NAME-frontend16", returnStatus: true
341
+ }
342
+ }
343
+ }
344
+
345
+
251
346
  stage('SonarQube compare to master') {
252
347
  when {
253
348
  anyOf {
254
349
  allOf {
255
350
  not { environment name: 'CHANGE_ID', value: '' }
256
351
  environment name: 'CHANGE_TARGET', value: 'develop'
352
+ environment name: 'SKIP_TESTS', value: ''
257
353
  }
258
354
  allOf {
355
+ environment name: 'SKIP_TESTS', value: ''
259
356
  environment name: 'CHANGE_ID', value: ''
260
357
  branch 'develop'
261
358
  not { changelog '.*^Automated release [0-9\\.]+$' }
@@ -316,3 +413,4 @@ pipeline {
316
413
  }
317
414
  }
318
415
  }
416
+
package/Makefile CHANGED
@@ -46,7 +46,7 @@ endif
46
46
  DIR=$(shell basename $$(pwd))
47
47
  NODE_MODULES?="../../../node_modules"
48
48
  PLONE_VERSION?=6
49
- VOLTO_VERSION?=16
49
+ VOLTO_VERSION?=17
50
50
  ADDON_PATH="${DIR}"
51
51
  ADDON_NAME="@eeacms/${ADDON_PATH}"
52
52
  DOCKER_COMPOSE=PLONE_VERSION=${PLONE_VERSION} VOLTO_VERSION=${VOLTO_VERSION} ADDON_NAME=${ADDON_NAME} ADDON_PATH=${ADDON_PATH} docker compose
@@ -86,7 +86,7 @@ cypress-open: ## Open cypress integration tests
86
86
 
87
87
  .PHONY: cypress-run
88
88
  cypress-run: ## Run cypress integration tests
89
- CYPRESS_API_PATH="${RAZZLE_DEV_PROXY_API_PATH}" NODE_ENV=development $(NODE_MODULES)/cypress/bin/cypress run --browser chromium
89
+ CYPRESS_API_PATH="${RAZZLE_DEV_PROXY_API_PATH}" NODE_ENV=development $(NODE_MODULES)/cypress/bin/cypress run
90
90
 
91
91
  .PHONY: test
92
92
  test: ## Run jest tests
@@ -98,7 +98,7 @@ test-update: ## Update jest tests snapshots
98
98
 
99
99
  .PHONY: stylelint
100
100
  stylelint: ## Stylelint
101
- $(NODE_MODULES)/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}'
101
+ $(NODE_MODULES)/.bin/stylelint --allow-empty-input 'src/**/*.{css,less}'
102
102
 
103
103
  .PHONY: stylelint-overrides
104
104
  stylelint-overrides:
@@ -106,7 +106,7 @@ stylelint-overrides:
106
106
 
107
107
  .PHONY: stylelint-fix
108
108
  stylelint-fix: ## Fix stylelint
109
- $(NODE_MODULES)/stylelint/bin/stylelint.js --allow-empty-input 'src/**/*.{css,less}' --fix
109
+ $(NODE_MODULES)/.bin/stylelint --allow-empty-input 'src/**/*.{css,less}' --fix
110
110
  $(NODE_MODULES)/.bin/stylelint --custom-syntax less --allow-empty-input 'theme/**/*.overrides' 'src/**/*.overrides' --fix
111
111
 
112
112
  .PHONY: prettier
@@ -119,11 +119,11 @@ prettier-fix: ## Fix prettier
119
119
 
120
120
  .PHONY: lint
121
121
  lint: ## ES Lint
122
- $(NODE_MODULES)/eslint/bin/eslint.js --max-warnings=0 'src/**/*.{js,jsx}'
122
+ $(NODE_MODULES)/.bin/eslint --max-warnings=0 'src/**/*.{js,jsx}'
123
123
 
124
124
  .PHONY: lint-fix
125
125
  lint-fix: ## Fix ES Lint
126
- $(NODE_MODULES)/eslint/bin/eslint.js --fix 'src/**/*.{js,jsx}'
126
+ $(NODE_MODULES)/.bin/eslint --fix 'src/**/*.{js,jsx}'
127
127
 
128
128
  .PHONY: i18n
129
129
  i18n: ## i18n
@@ -155,8 +155,11 @@ start-ci:
155
155
  cd ../..
156
156
  yarn start
157
157
 
158
+ .PHONY: check-ci
159
+ check-ci:
160
+ $(NODE_MODULES)/.bin/wait-on -t 240000 http://localhost:3000
161
+
158
162
  .PHONY: cypress-ci
159
163
  cypress-ci:
160
164
  $(NODE_MODULES)/.bin/wait-on -t 240000 http://localhost:3000
161
- NODE_ENV=development make cypress-run
162
-
165
+ CYPRESS_API_PATH="${RAZZLE_DEV_PROXY_API_PATH}" NODE_ENV=development $(NODE_MODULES)/cypress/bin/cypress run --browser chromium
@@ -35,10 +35,6 @@ export const slateBeforeEach = (contentType = 'Document') => {
35
35
  path: 'cypress',
36
36
  });
37
37
  cy.visit('/cypress/my-page');
38
- cy.waitForResourceToLoad('@navigation');
39
- cy.waitForResourceToLoad('@breadcrumbs');
40
- cy.waitForResourceToLoad('@actions');
41
- cy.waitForResourceToLoad('@types');
42
38
  cy.waitForResourceToLoad('my-page');
43
39
  cy.navigate('/cypress/my-page/edit');
44
40
  };
@@ -1,3 +1,5 @@
1
+ require('dotenv').config({ path: __dirname + '/.env' })
2
+
1
3
  module.exports = {
2
4
  testMatch: ['**/src/addons/**/?(*.)+(spec|test).[jt]s?(x)'],
3
5
  collectCoverageFrom: [
@@ -9,18 +11,26 @@ module.exports = {
9
11
  '@plone/volto/cypress': '<rootDir>/node_modules/@plone/volto/cypress',
10
12
  '@plone/volto/babel': '<rootDir>/node_modules/@plone/volto/babel',
11
13
  '@plone/volto/(.*)$': '<rootDir>/node_modules/@plone/volto/src/$1',
12
- '@package/(.*)$': '<rootDir>/src/$1',
13
- '@root/(.*)$': '<rootDir>/src/$1',
14
+ '@package/(.*)$': '<rootDir>/node_modules/@plone/volto/src/$1',
15
+ '@root/(.*)$': '<rootDir>/node_modules/@plone/volto/src/$1',
14
16
  '@plone/volto-quanta/(.*)$': '<rootDir>/src/addons/volto-quanta/src/$1',
17
+ '@eeacms/search/(.*)$': '<rootDir>/src/addons/volto-searchlib/searchlib/$1',
18
+ '@eeacms/search': '<rootDir>/src/addons/volto-searchlib/searchlib',
15
19
  '@eeacms/(.*?)/(.*)$': '<rootDir>/node_modules/@eeacms/$1/src/$2',
16
- '@plone/volto-slate':
20
+ '@plone/volto-slate$':
17
21
  '<rootDir>/node_modules/@plone/volto/packages/volto-slate/src',
22
+ '@plone/volto-slate/(.*)$':
23
+ '<rootDir>/node_modules/@plone/volto/packages/volto-slate/src/$1',
18
24
  '~/(.*)$': '<rootDir>/src/$1',
19
25
  'load-volto-addons':
20
26
  '<rootDir>/node_modules/@plone/volto/jest-addons-loader.js',
21
27
  },
28
+ transformIgnorePatterns: [
29
+ '/node_modules/(?!(@plone|@root|@package|@eeacms)/).*/',
30
+ ],
22
31
  transform: {
23
32
  '^.+\\.js(x)?$': 'babel-jest',
33
+ '^.+\\.ts(x)?$': 'babel-jest',
24
34
  '^.+\\.(png)$': 'jest-file',
25
35
  '^.+\\.(jpg)$': 'jest-file',
26
36
  '^.+\\.(svg)$': './node_modules/@plone/volto/jest-svgsystem-transform.js',
@@ -33,4 +43,9 @@ module.exports = {
33
43
  statements: 5,
34
44
  },
35
45
  },
36
- };
46
+ ...(process.env.JEST_USE_SETUP === 'ON' && {
47
+ setupFilesAfterEnv: [
48
+ '<rootDir>/node_modules/@eeacms/volto-slate-footnote/jest.setup.js',
49
+ ],
50
+ }),
51
+ }
package/jest.setup.js ADDED
@@ -0,0 +1,65 @@
1
+ import { jest } from '@jest/globals';
2
+ import configureStore from 'redux-mock-store';
3
+ import thunk from 'redux-thunk';
4
+ import { blocksConfig } from '@plone/volto/config/Blocks';
5
+ import installSlate from '@plone/volto-slate/index';
6
+
7
+ var mockSemanticComponents = jest.requireActual('semantic-ui-react');
8
+ var mockComponents = jest.requireActual('@plone/volto/components');
9
+ var config = jest.requireActual('@plone/volto/registry').default;
10
+
11
+ config.blocks.blocksConfig = {
12
+ ...blocksConfig,
13
+ ...config.blocks.blocksConfig,
14
+ };
15
+
16
+ jest.doMock('semantic-ui-react', () => ({
17
+ __esModule: true,
18
+ ...mockSemanticComponents,
19
+ Popup: ({ content, trigger }) => {
20
+ return (
21
+ <div className="popup">
22
+ <div className="trigger">{trigger}</div>
23
+ <div className="content">{content}</div>
24
+ </div>
25
+ );
26
+ },
27
+ }));
28
+
29
+ jest.doMock('@plone/volto/components', () => {
30
+ return {
31
+ __esModule: true,
32
+ ...mockComponents,
33
+ SidebarPortal: ({ children }) => <div id="sidebar">{children}</div>,
34
+ };
35
+ });
36
+
37
+ jest.doMock('@plone/volto/registry', () =>
38
+ [installSlate].reduce((acc, apply) => apply(acc), config),
39
+ );
40
+
41
+ const mockStore = configureStore([thunk]);
42
+
43
+ global.fetch = jest.fn(() =>
44
+ Promise.resolve({
45
+ json: () => Promise.resolve({}),
46
+ }),
47
+ );
48
+
49
+ global.store = mockStore({
50
+ intl: {
51
+ locale: 'en',
52
+ messages: {},
53
+ formatMessage: jest.fn(),
54
+ },
55
+ content: {
56
+ create: {},
57
+ subrequests: [],
58
+ },
59
+ connected_data_parameters: {},
60
+ screen: {
61
+ page: {
62
+ width: 768,
63
+ },
64
+ },
65
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-slate-footnote",
3
- "version": "6.3.0",
3
+ "version": "7.0.0",
4
4
  "description": "volto-slate-footnote: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -23,6 +23,7 @@
23
23
  "@cypress/code-coverage": "^3.10.0",
24
24
  "@plone/scripts": "*",
25
25
  "babel-plugin-transform-class-properties": "^6.24.1",
26
+ "dotenv": "^16.3.2",
26
27
  "husky": "^8.0.3",
27
28
  "lint-staged": "^14.0.1",
28
29
  "md5": "^2.3.0"
@@ -28,139 +28,139 @@ const messages = defineMessages({
28
28
  },
29
29
  });
30
30
 
31
- const MultiSelectSearchWidget = injectLazyLibs('reactSelectAsyncCreateable')(
32
- (props) => {
33
- const [selectedOption, setSelectedOption] = useState([]);
34
- const [defaultOptions, setDefaultOptions] = useState([]);
35
- const [parentFootnote, setParentFootnote] = useState(props.value);
36
-
37
- useEffect(() => {
38
- if (props.value) {
39
- const parentFootnoteCurrent = props.value;
40
-
41
- const extraValues =
42
- parentFootnoteCurrent && props.value.extra ? props.value.extra : [];
43
- const selectedOptionCurrent = parentFootnoteCurrent.value
44
- ? [...[parentFootnoteCurrent], ...extraValues]
45
- : [];
46
- setSelectedOption(selectedOptionCurrent);
47
-
48
- // from choices (list of all footnotes available including current in value) get all not
49
- // found in current in value
50
- // consider that new footnotes have value and footnote undefined
51
- const defaultOptions = (props.choices || []).filter(
52
- (item) =>
53
- !selectedOption.find(({ label }) => label === item.label) &&
54
- item.value,
55
- );
56
- setDefaultOptions(defaultOptions);
57
- setParentFootnote(props.value);
58
- }
59
- }, [props]); // eslint-disable-line
60
-
61
- /**
62
- * evaluate on Regex to filter results
63
- * @param {Object} e - event
64
- * @param {Object} data
65
- */
66
- const loadOptions = (search) => {
67
- const re = new RegExp(escapeRegExp(search), 'i');
68
- const isMatch = (result) => re.test(result.value);
69
- const resultsFiltered = filter(props.choices, isMatch);
70
-
71
- return Promise.resolve(resultsFiltered);
31
+ const MultiSelectSearchWidget = injectLazyLibs('reactSelectAsyncCreateable')((
32
+ props,
33
+ ) => {
34
+ const [selectedOption, setSelectedOption] = useState([]);
35
+ const [defaultOptions, setDefaultOptions] = useState([]);
36
+ const [parentFootnote, setParentFootnote] = useState(props.value);
37
+
38
+ useEffect(() => {
39
+ if (props.value) {
40
+ const parentFootnoteCurrent = props.value;
41
+
42
+ const extraValues =
43
+ parentFootnoteCurrent && props.value.extra ? props.value.extra : [];
44
+ const selectedOptionCurrent = parentFootnoteCurrent.value
45
+ ? [...[parentFootnoteCurrent], ...extraValues]
46
+ : [];
47
+ setSelectedOption(selectedOptionCurrent);
48
+
49
+ // from choices (list of all footnotes available including current in value) get all not
50
+ // found in current in value
51
+ // consider that new footnotes have value and footnote undefined
52
+ const defaultOptions = (props.choices || []).filter(
53
+ (item) =>
54
+ !selectedOption.find(({ label }) => label === item.label) &&
55
+ item.value,
56
+ );
57
+ setDefaultOptions(defaultOptions);
58
+ setParentFootnote(props.value);
59
+ }
60
+ }, [props]); // eslint-disable-line
61
+
62
+ /**
63
+ * evaluate on Regex to filter results
64
+ * @param {Object} e - event
65
+ * @param {Object} data
66
+ */
67
+ const loadOptions = (search) => {
68
+ const re = new RegExp(escapeRegExp(search), 'i');
69
+ const isMatch = (result) => re.test(result.value);
70
+ const resultsFiltered = filter(props.choices, isMatch);
71
+
72
+ return Promise.resolve(resultsFiltered);
73
+ };
74
+
75
+ /**
76
+ * If the list is empty or the first is not parent, return true
77
+ * @param {Object[]} optionsList list of objects - footnotes
78
+ * @returns {boolean}
79
+ */
80
+ const isParetFootnoteRemoved = (optionsList) =>
81
+ !optionsList[0] || optionsList[0].value !== parentFootnote.value;
82
+
83
+ /**
84
+ * replace all parentFootnote data except uid, with the first from the list
85
+ * @param {Object[]} optionsList list of objects - footnotes
86
+ * @returns {Object}
87
+ */
88
+ const setParentFootnoteFromExtra = (optionsList) => {
89
+ const { footnote, label, value } = optionsList[0] || [];
90
+
91
+ return {
92
+ ...parentFootnote,
93
+ footnote: footnote || optionsList[0]?.value,
94
+ label,
95
+ value,
96
+ extra: optionsList.slice(1),
72
97
  };
73
-
74
- /**
75
- * If the list is empty or the first is not parent, return true
76
- * @param {Object[]} optionsList list of objects - footnotes
77
- * @returns {boolean}
78
- */
79
- const isParetFootnoteRemoved = (optionsList) =>
80
- !optionsList[0] || optionsList[0].value !== parentFootnote.value;
81
-
82
- /**
83
- * replace all parentFootnote data except uid, with the first from the list
84
- * @param {Object[]} optionsList list of objects - footnotes
85
- * @returns {Object}
86
- */
87
- const setParentFootnoteFromExtra = (optionsList) => {
88
- const { footnote, label, value } = optionsList[0] || [];
89
-
90
- return {
91
- ...parentFootnote,
92
- footnote: footnote || optionsList[0]?.value,
93
- label,
94
- value,
95
- extra: optionsList.slice(1),
98
+ };
99
+
100
+ /**
101
+ * Will make the footnotes object, that will be saved as first from optionsList
102
+ * the rest will be added to extra
103
+ * @param {Object[]} optionsList
104
+ * @returns
105
+ */
106
+ const setFootnoteFromSelection = (optionsList) => {
107
+ const extra = optionsList.slice(1).map((item) => {
108
+ const obj = {
109
+ ...item,
110
+ footnote: item.value,
96
111
  };
97
- };
98
-
99
- /**
100
- * Will make the footnotes object, that will be saved as first from optionsList
101
- * the rest will be added to extra
102
- * @param {Object[]} optionsList
103
- * @returns
104
- */
105
- const setFootnoteFromSelection = (optionsList) => {
106
- const extra = optionsList.slice(1).map((item) => {
107
- const obj = {
108
- ...item,
109
- footnote: item.value,
110
- };
111
- const { __isNew__: remove, extra, ...rest } = obj;
112
- return rest;
113
- });
114
- return { ...parentFootnote, extra };
115
- };
116
-
117
- /**
118
- * Handle the field change, will remake the result based on the new selected list
119
- * @method handleChange
120
- * @param {array} optionsList The selected options (already aggregated).
121
- * @returns {undefined}
122
- */
123
- const handleChange = (optionsList) => {
124
- const formattedSelectedOptions = optionsList.map((option) => ({
125
- footnoteId: nanoid(5), // to be overwritten if already exists (keep as a reference to same text)
126
- ...option,
127
- uid: nanoid(5), // overwrite existing, thus creating new record for the same text
128
- footnote: option.value,
129
- }));
130
- setSelectedOption(formattedSelectedOptions);
131
-
132
- // manage case if parent footnotes (first from the options) was removed
133
- const resultSelected = isParetFootnoteRemoved(formattedSelectedOptions)
134
- ? setParentFootnoteFromExtra(formattedSelectedOptions)
135
- : setFootnoteFromSelection(formattedSelectedOptions);
136
-
137
- props.onChange({
138
- footnote: resultSelected,
139
- });
140
- };
141
-
142
- const AsyncCreatableSelect = props.reactSelectAsyncCreateable.default;
143
- return (
144
- <FormFieldWrapper {...props}>
145
- <AsyncCreatableSelect
146
- isDisabled={props.isDisabled}
147
- className="react-select-container"
148
- classNamePrefix="react-select"
149
- defaultOptions={defaultOptions}
150
- styles={customSelectStyles}
151
- theme={selectTheme}
152
- components={{ DropdownIndicator, Option }}
153
- isMulti
154
- options={defaultOptions}
155
- value={selectedOption || []}
156
- loadOptions={loadOptions}
157
- onChange={handleChange}
158
- placeholder={props.intl.formatMessage(messages.select)}
159
- noOptionsMessage={() => props.intl.formatMessage(messages.no_options)}
160
- />
161
- </FormFieldWrapper>
162
- );
163
- },
164
- );
112
+ const { __isNew__: remove, extra, ...rest } = obj;
113
+ return rest;
114
+ });
115
+ return { ...parentFootnote, extra };
116
+ };
117
+
118
+ /**
119
+ * Handle the field change, will remake the result based on the new selected list
120
+ * @method handleChange
121
+ * @param {array} optionsList The selected options (already aggregated).
122
+ * @returns {undefined}
123
+ */
124
+ const handleChange = (optionsList) => {
125
+ const formattedSelectedOptions = optionsList.map((option) => ({
126
+ footnoteId: nanoid(5), // to be overwritten if already exists (keep as a reference to same text)
127
+ ...option,
128
+ uid: nanoid(5), // overwrite existing, thus creating new record for the same text
129
+ footnote: option.value,
130
+ }));
131
+ setSelectedOption(formattedSelectedOptions);
132
+
133
+ // manage case if parent footnotes (first from the options) was removed
134
+ const resultSelected = isParetFootnoteRemoved(formattedSelectedOptions)
135
+ ? setParentFootnoteFromExtra(formattedSelectedOptions)
136
+ : setFootnoteFromSelection(formattedSelectedOptions);
137
+
138
+ props.onChange({
139
+ footnote: resultSelected,
140
+ });
141
+ };
142
+
143
+ const AsyncCreatableSelect = props.reactSelectAsyncCreateable.default;
144
+ return (
145
+ <FormFieldWrapper {...props}>
146
+ <AsyncCreatableSelect
147
+ isDisabled={props.isDisabled}
148
+ className="react-select-container"
149
+ classNamePrefix="react-select"
150
+ defaultOptions={defaultOptions}
151
+ styles={customSelectStyles}
152
+ theme={selectTheme}
153
+ components={{ DropdownIndicator, Option }}
154
+ isMulti
155
+ options={defaultOptions}
156
+ value={selectedOption || []}
157
+ loadOptions={loadOptions}
158
+ onChange={handleChange}
159
+ placeholder={props.intl.formatMessage(messages.select)}
160
+ noOptionsMessage={() => props.intl.formatMessage(messages.no_options)}
161
+ />
162
+ </FormFieldWrapper>
163
+ );
164
+ });
165
165
 
166
166
  export default MultiSelectSearchWidget;
@@ -1,10 +1,70 @@
1
1
  import React from 'react';
2
- import renderer from 'react-test-renderer';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
3
  import SearchWidget from './SearchWidget';
4
- describe('Test SearchWidget', () => {
5
- it('check html content', () => {
6
- const component = renderer.create(<SearchWidget />);
7
- const json = component.toJSON();
8
- expect(json).toMatchSnapshot();
4
+ import '@testing-library/jest-dom/extend-expect';
5
+
6
+ describe('SearchWidget', () => {
7
+ const choices = [
8
+ { footnote: 'Citation 1' },
9
+ { footnote: 'Citation 2' },
10
+ { footnote: 'Citation 3' },
11
+ ];
12
+
13
+ it('renders the search input and displays the choices', () => {
14
+ const onChange = jest.fn();
15
+ render(<SearchWidget choices={choices} onChange={onChange} value="" />);
16
+
17
+ expect(screen.getByRole('textbox')).toBeInTheDocument();
18
+ expect(screen.getByText('Citation')).toBeInTheDocument();
19
+ expect(screen.queryAllByRole('option')).toHaveLength(0);
20
+ expect(screen.getByText('No results found.')).toBeInTheDocument();
21
+ });
22
+
23
+ it('filters the choices based on the search input', async () => {
24
+ const onChange = jest.fn();
25
+ const { container } = render(
26
+ <SearchWidget choices={choices} onChange={onChange} value="" />,
27
+ );
28
+
29
+ const searchInput = screen.getByRole('textbox');
30
+ fireEvent.change(searchInput, { target: { value: 'Citation 2' } });
31
+
32
+ await waitFor(() => {
33
+ expect(container.querySelectorAll('.result')).toHaveLength(1);
34
+ expect(
35
+ container.querySelector('div[footnote="Citation 2"]'),
36
+ ).toBeInTheDocument();
37
+ });
38
+ });
39
+
40
+ it('calls the onChange callback when a choice is selected', () => {
41
+ const onChange = jest.fn();
42
+ render(<SearchWidget choices={choices} onChange={onChange} value="" />);
43
+
44
+ const searchInput = screen.getByRole('textbox');
45
+ fireEvent.change(searchInput, { target: { value: 'Citation 2' } });
46
+
47
+ waitFor(() => {
48
+ const option = screen.getByText('Citation 2');
49
+ fireEvent.click(option);
50
+ expect(onChange).toHaveBeenCalledWith({ footnote: 'Citation 2' });
51
+ });
52
+ });
53
+
54
+ it('updates the value prop when the search input changes', () => {
55
+ const onChange = jest.fn();
56
+ render(
57
+ <SearchWidget
58
+ choices={choices}
59
+ onChange={onChange}
60
+ value="Initial value"
61
+ />,
62
+ );
63
+
64
+ const searchInput = screen.getByRole('textbox');
65
+ expect(searchInput).toHaveValue('Initial value');
66
+
67
+ fireEvent.change(searchInput, { target: { value: 'New value' } });
68
+ expect(onChange).toHaveBeenCalledWith({ footnote: 'New value' });
9
69
  });
10
70
  });
@@ -20,8 +20,8 @@ span[aria-describedby='footnote-label'] {
20
20
 
21
21
  .footnote-edit-node {
22
22
  padding: 0px 4px;
23
- background-color: var(--bg-color, #e6f3ff);
24
23
  border-radius: 4px;
24
+ background-color: var(--bg-color, #e6f3ff);
25
25
  }
26
26
 
27
27
  .footnote-edit-node::after,
@@ -61,7 +61,7 @@ describe('getAllBlocksAndSlateFields', () => {
61
61
  });
62
62
 
63
63
  it('handles metadataSection correctly', () => {
64
- const properties = { '1': ['test'] };
64
+ const properties = { 1: ['test'] };
65
65
  const blocks = [
66
66
  {
67
67
  '@type': 'metadataSection',
@@ -98,7 +98,7 @@ describe('getAllBlocksAndSlateFields', () => {
98
98
  });
99
99
 
100
100
  it('handles metadata correctly', () => {
101
- const properties = { '1': ['metadata test'] };
101
+ const properties = { 1: ['metadata test'] };
102
102
  const blocks = [
103
103
  {
104
104
  '@type': 'metadata',
@@ -1,48 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
- const projectRootPath = fs.existsSync('./project')
5
- ? fs.realpathSync('./project')
6
- : fs.realpathSync('./../../../');
7
- const packageJson = require(path.join(projectRootPath, 'package.json'));
8
- const jsConfig = require(path.join(projectRootPath, 'jsconfig.json')).compilerOptions;
9
-
10
- const pathsConfig = jsConfig.paths;
11
-
12
- let voltoPath = path.join(projectRootPath, 'node_modules/@plone/volto');
13
-
14
- Object.keys(pathsConfig).forEach(pkg => {
15
- if (pkg === '@plone/volto') {
16
- voltoPath = `./${jsConfig.baseUrl}/${pathsConfig[pkg][0]}`;
17
- }
18
- });
19
- const AddonConfigurationRegistry = require(`${voltoPath}/addon-registry.js`);
20
- const reg = new AddonConfigurationRegistry(projectRootPath);
21
-
22
- // Extends ESlint configuration for adding the aliases to `src` directories in Volto addons
23
- const addonAliases = Object.keys(reg.packages).map(o => [
24
- o,
25
- reg.packages[o].modulePath,
26
- ]);
27
-
28
-
29
- module.exports = {
30
- extends: `${projectRootPath}/node_modules/@plone/volto/.eslintrc`,
31
- settings: {
32
- 'import/resolver': {
33
- alias: {
34
- map: [
35
- ['@plone/volto', '@plone/volto/src'],
36
- ...addonAliases,
37
- ['@package', `${__dirname}/src`],
38
- ['~', `${__dirname}/src`],
39
- ],
40
- extensions: ['.js', '.jsx', '.json'],
41
- },
42
- 'babel-plugin-root-import': {
43
- rootPathSuffix: 'src',
44
- },
45
- },
46
- },
47
- };
48
-
@@ -1,72 +0,0 @@
1
- // Jest Snapshot v1, https://goo.gl/fbAQLP
2
-
3
- exports[`Test SearchWidget check html content 1`] = `
4
- <div
5
- id="blockform-fieldset-default"
6
- >
7
- <div
8
- className="ui segment attached"
9
- fluid={true}
10
- >
11
- <div
12
- className="ui fluid card"
13
- onClick={[Function]}
14
- >
15
- <div
16
- className="content"
17
- fluid={true}
18
- >
19
- <div
20
- className="header"
21
- >
22
- Citation
23
- </div>
24
- <div
25
- className="description"
26
- fluid={true}
27
- >
28
- <div
29
- className="ui fluid search"
30
- onBlur={[Function]}
31
- onFocus={[Function]}
32
- onMouseDown={[Function]}
33
- >
34
- <div
35
- className="ui fluid right icon input"
36
- >
37
- <input
38
- autoComplete="off"
39
- className="prompt"
40
- id="field-footnote"
41
- onChange={[Function]}
42
- onClick={[Function]}
43
- tabIndex="0"
44
- type="text"
45
- value=""
46
- />
47
- <i
48
- aria-hidden="true"
49
- className="search icon"
50
- onClick={[Function]}
51
- />
52
- </div>
53
- <div
54
- className="results transition"
55
- >
56
- <div
57
- className="message empty"
58
- >
59
- <div
60
- className="header"
61
- >
62
- No results found.
63
- </div>
64
- </div>
65
- </div>
66
- </div>
67
- </div>
68
- </div>
69
- </div>
70
- </div>
71
- </div>
72
- `;