diff --git a/docs/React4xp.md b/docs/React4xp.md
index c16b2bea6..7b607d544 100644
--- a/docs/React4xp.md
+++ b/docs/React4xp.md
@@ -14,4 +14,6 @@ Hydration is needed for react components with client side interaction. You need
If we want to make more complex components/containers for react we can create classes that extends the React.Component class. This will open of for the possibility to use [state](https://reactjs.org/docs/state-and-lifecycle.html#adding-local-state-to-a-class) and [lifecycles](https://reactjs.org/docs/react-component.html#the-component-lifecycle). Ref. [Header.jsx](/src/main/resources/react4xp/_entries/Header.jsx)
## Entries
+It is important that any file in `react4xp/_entries` is not imported into any other file. An entry is a bridge between XP and React, not a reusable React component. They should only be used through the various render methods in `lib-react4xp` in our controllers.
+
After you've run `enonic project build` or `enonic project deploy` at least once, you'll have a file in your build folder with a list of all react4xp components available. `/build/resources/main/assets/react4xp/entries.json`
diff --git a/src/main/resources/admin/tools/bestbet/bestbet.ts b/src/main/resources/admin/tools/bestbet/bestbet.ts
index a2f50647f..1a9315cc5 100644
--- a/src/main/resources/admin/tools/bestbet/bestbet.ts
+++ b/src/main/resources/admin/tools/bestbet/bestbet.ts
@@ -92,7 +92,7 @@ function renderPart(req: XP.Request): XP.Response {
]
const bestBetComponent = r4XpRender(
- 'bestbet/Bestbet',
+ 'Bestbet',
{
logoUrl: assetUrl({
path: 'SSB_logo_black.svg',
diff --git a/src/main/resources/main.es6 b/src/main/resources/main.es6
index f0909bbfa..85781bec5 100644
--- a/src/main/resources/main.es6
+++ b/src/main/resources/main.es6
@@ -95,10 +95,6 @@ try {
feature: 'datefns-publication-archive',
enabled: false,
},
- {
- feature: 'csr-on-table-accordion',
- enabled: false,
- },
],
},
])
diff --git a/src/main/resources/react4xp/_entries/Accordion.jsx b/src/main/resources/react4xp/_entries/Accordion.jsx
index 91e4693d1..84d84a3d3 100644
--- a/src/main/resources/react4xp/_entries/Accordion.jsx
+++ b/src/main/resources/react4xp/_entries/Accordion.jsx
@@ -1,67 +1,4 @@
import React from 'react'
-import { Accordion as AccordionComponent, NestedAccordion } from '@statisticsnorway/ssb-component-library'
-
-import PropTypes from 'prop-types'
-
-class Accordion extends React.Component {
- renderNestedAccordions(items) {
- return items.map((item, i) => (
-
- {item.body &&
}
-
- ))
- }
-
- createMarkup(html) {
- return {
- __html: html.replace(/ /g, ' '),
- }
- }
-
- render() {
- const location = window.location
- const anchor = location && location.hash !== '' ? location.hash.substr(1) : undefined
-
- const { accordions } = this.props
-
- return (
-
-
- {accordions.map((accordion, index) => (
-
-
- {accordion.body &&
}
- {this.renderNestedAccordions(accordion.items)}
-
-
- ))}
-
-
- )
- }
-}
-
-Accordion.propTypes = {
- accordions: PropTypes.arrayOf(
- PropTypes.shape({
- id: PropTypes.string,
- open: PropTypes.string.isRequired,
- subHeader: PropTypes.string,
- body: PropTypes.string,
- items: PropTypes.arrayOf(
- PropTypes.shape({
- title: PropTypes.string,
- body: PropTypes.string,
- })
- ),
- })
- ),
-}
+import Accordion from '../accordion/Accordion'
export default (props) =>
diff --git a/src/main/resources/react4xp/_entries/AttachmentTablesFigures.jsx b/src/main/resources/react4xp/_entries/AttachmentTablesFigures.jsx
index 8fdb3fd56..af0964452 100644
--- a/src/main/resources/react4xp/_entries/AttachmentTablesFigures.jsx
+++ b/src/main/resources/react4xp/_entries/AttachmentTablesFigures.jsx
@@ -3,7 +3,7 @@ import { Accordion, Button } from '@statisticsnorway/ssb-component-library'
import { ChevronDown, ChevronUp } from 'react-feather'
import PropTypes from 'prop-types'
-import Table from './Table'
+import Table from '../table/Table'
import { addGtagForEvent } from '/react4xp/ReactGA'
function AttachmentTableFigures(props) {
diff --git a/src/main/resources/react4xp/_entries/Bestbet.jsx b/src/main/resources/react4xp/_entries/Bestbet.jsx
new file mode 100644
index 000000000..98f5185ba
--- /dev/null
+++ b/src/main/resources/react4xp/_entries/Bestbet.jsx
@@ -0,0 +1,4 @@
+import React from 'react'
+import Bestbet from '../bestbet/Bestbet'
+
+export default (props) =>
diff --git a/src/main/resources/react4xp/_entries/Table.jsx b/src/main/resources/react4xp/_entries/Table.jsx
index ca0e5c167..ee111a1a6 100644
--- a/src/main/resources/react4xp/_entries/Table.jsx
+++ b/src/main/resources/react4xp/_entries/Table.jsx
@@ -1,650 +1,4 @@
-import React, { useState, useRef, useEffect } from 'react'
-import PropTypes from 'prop-types'
-import { Dropdown, Link } from '@statisticsnorway/ssb-component-library'
-import { default as isEmpty } from 'ramda/es/isEmpty'
-import NumberFormat from 'react-number-format'
-import { Alert, Button } from 'react-bootstrap'
-import { ChevronLeft, ChevronRight } from 'react-feather'
-import { addGtagForEvent } from '/react4xp/ReactGA'
-
-function Table(props) {
- const [prevClientWidth, setPrevClientWidth] = useState(0)
- const [table, setTable] = useState(props.paramShowDraft && props.draftExist ? props.tableDraft : props.table)
- const [fetchUnPublished, setFetchUnPublished] = useState(props.paramShowDraft)
-
- const showPreviewToggle =
- props.showPreviewDraft && (!props.pageTypeStatistic || (props.paramShowDraft && props.pageTypeStatistic))
-
- const captionRef = useRef(null)
- const tableControlsDesktopRef = useRef(null)
- const tableControlsMobileRef = useRef(null)
- const tableRef = useRef(null)
- const tableWrapperRef = useRef(null)
-
- useEffect(() => {
- updateTableControlsDesktop()
-
- const widthCheckInterval = setInterval(() => {
- widthCheck()
- }, 250)
- window.addEventListener('resize', updateTableControlsDesktop)
- return () => {
- clearInterval(widthCheckInterval)
- window.removeEventListener('resize', updateTableControlsDesktop)
- }
- }, [])
-
- function widthCheck() {
- if (tableWrapperRef.current.clientWidth !== prevClientWidth) {
- setPrevClientWidth(tableWrapperRef.current.clientWidth)
- updateTableControlsDesktop()
- }
- }
-
- function updateTableControlsDesktop() {
- const controls = tableControlsDesktopRef.current
- const tableWrapper = tableWrapperRef.current
- const left = controls.children.item(0)
- const right = controls.children.item(1)
-
- // hide controlls if there is no scrollbar
- if (tableWrapper.scrollWidth > tableWrapper.clientWidth || tableWrapper.clientWidth === 0) {
- controls.classList.remove('d-none')
- tableControlsMobileRef.current.classList.remove('d-none')
- // disable left
- if (tableWrapper.scrollLeft <= 0) {
- left.classList.add('disabled')
- } else {
- left.classList.remove('disabled')
- }
-
- // disable right
- if (tableWrapper.scrollLeft + tableWrapper.clientWidth >= tableWrapper.scrollWidth) {
- right.classList.add('disabled')
- } else {
- right.classList.remove('disabled')
- }
-
- // move desktop controls to correct pos
- const captionHalfHeight = captionRef.current.offsetHeight / 2
- const controlsHalfHeight = left.scrollHeight / 2
- left.style.marginTop = `${captionHalfHeight - controlsHalfHeight}px`
- right.style.marginTop = `${captionHalfHeight - controlsHalfHeight}px`
- } else {
- controls.classList.add('d-none')
- tableControlsMobileRef.current.classList.add('d-none')
- }
- }
-
- function scrollLeft() {
- tableWrapperRef.current.scrollLeft -= 100
- updateTableControlsDesktop()
- }
-
- function scrollRight() {
- tableWrapperRef.current.scrollLeft += 100
- updateTableControlsDesktop()
- }
-
- function trimValue(value) {
- if (value && typeof value === 'string') {
- return value.trim()
- }
- return value
- }
-
- function formatNumber(value) {
- const language = props.table.language
- const decimalSeparator = language === 'en' ? '.' : ','
- value = trimValue(value)
- if (value) {
- if (typeof value === 'number' || (typeof value === 'string' && !isNaN(value))) {
- const decimals = value.toString().indexOf('.') > -1 ? value.toString().split('.')[1].length : 0
- return (
-
- )
- }
- }
- return value
- }
-
- function addDownloadTableDropdown(mobile) {
- const { downloadTableLabel, downloadTableTitle, downloadTableOptions } = props
-
- if (downloadTableLabel && downloadTableTitle && downloadTableOptions) {
- const downloadTable = (item) => {
- if (item.id === 'downloadTableAsCSV') {
- {
- downloadTableAsCSV()
- }
- }
-
- if (item.id === 'downloadTableAsXLSX') {
- {
- downloadTableAsExcel()
- }
- }
- }
-
- return (
-
-
-
- )
- }
- }
-
- function downloadTableAsCSV() {
- if (props.GA_TRACKING_ID) {
- addGtagForEvent(props.GA_TRACKING_ID, 'Lastet ned csv tabell', 'Statistikkside tabeller', 'Last ned csv tabell')
- }
-
- if (window && window.downloadTableFile) {
- window.downloadTableFile(tableRef.current, {
- type: 'csv',
- fileName: 'tabell',
- csvSeparator: ';',
- csvEnclosure: '',
- tfootSelector: '',
- })
- }
- }
-
- function downloadTableAsExcel() {
- if (props.GA_TRACKING_ID) {
- addGtagForEvent(
- props.GA_TRACKING_ID,
- 'Lastet ned excell tabell',
- 'Statistikkside tabeller',
- 'Last ned excell tabell'
- )
- }
-
- if (window && window.downloadTableFile) {
- window.downloadTableFile(tableRef.current, {
- type: 'xlsx',
- fileName: 'tabell',
- numbers: {
- html: {
- decimalMark: ',',
- thousandsSeparator: ' ',
- },
- output: {
- decimalMark: '.',
- thousandsSeparator: '',
- },
- },
- })
- }
- }
-
- function createTable() {
- const { tableClass } = props.table
-
- return (
-
- {addCaption()}
- {table.thead.map((t, index) => {
- return (
-
- {addThead(index)}
- {addTbody(index)}
-
- )
- })}
- {addTFoot()}
-
- )
- }
-
- function addCaption() {
- const { caption } = table
- if (caption) {
- const hasNoteRefs = typeof caption === 'object'
- return (
-
-
- {hasNoteRefs ? caption.content : caption}
- {hasNoteRefs ? addNoteRefs(caption.noterefs) : null}
-
-
- )
- }
- }
-
- function createScrollControlsMobile() {
- return (
-
-
-
- )
- }
-
- function createScrollControlsDesktop() {
- return (
-
- scrollLeft()}>
-
-
- scrollRight()}>
-
-
-
- )
- }
-
- function addThead(index) {
- return {createRowsHead(table.thead[index].tr)}
- }
-
- function addTbody(index) {
- return {createRowsBody(table.tbody[index].tr)}
- }
-
- function renderCorrectionNotice() {
- if (table.tfoot.correctionNotice) {
- return (
-
- {table.tfoot.correctionNotice}
-
- )
- }
- return null
- }
-
- function addTFoot() {
- const { footnotes, correctionNotice } = table.tfoot
-
- const noteRefs = table.noteRefs
-
- if ((noteRefs && noteRefs.length > 0) || correctionNotice) {
- return (
-
- {noteRefs.map((note, index) => {
- const current = footnotes && footnotes.find((footnote) => footnote.noteid === note)
- if (current) {
- return (
-
-
- {index + 1}
- {current.content}
-
-
- )
- } else {
- return null
- }
- })}
- {renderCorrectionNotice()}
-
- )
- }
- return null
- }
-
- function createRowsHead(rows) {
- if (rows) {
- return rows.map((row, i) => {
- return {createHeaderCell(row)}
- })
- }
- }
-
- function createRowsBody(rows) {
- if (rows) {
- return rows.map((row, i) => {
- return (
-
- {createBodyTh(row)}
- {createBodyTd(row)}
-
- )
- })
- }
- }
-
- function createHeaderCell(row) {
- return Object.keys(row).map((keyName) => {
- const value = row[keyName]
- if (keyName === 'th') {
- return createHeadTh(value)
- } else if (keyName === 'td') {
- return createHeadTd(value)
- }
- })
- }
-
- function createHeadTh(value) {
- return value.map((cellValue, i) => {
- if (typeof cellValue === 'object') {
- if (Array.isArray(cellValue)) {
- // TODO: Because some values is split into array by xmlParser i have to do this, find better fix
- return {cellValue.join(' ')}
- } else {
- return (
-
- {trimValue(cellValue.content)}
- {addNoteRefs(cellValue.noterefs)}
-
- )
- }
- } else {
- return (
-
- {trimValue(cellValue)}
-
- )
- }
- })
- }
-
- function createHeadTd(value) {
- return value.map((cellValue, i) => {
- if (typeof cellValue === 'object') {
- return (
-
- {trimValue(cellValue.content)}
- {addNoteRefs(cellValue.noterefs)}
-
- )
- } else {
- return {trimValue(cellValue)}
- }
- })
- }
-
- function createBodyTh(row) {
- return Object.keys(row).map((key) => {
- const value = row[key]
- if (key === 'th') {
- return value.map((cellValue, i) => {
- if (typeof cellValue === 'object') {
- return (
-
- {trimValue(cellValue.content)}
- {addNoteRefs(cellValue.noterefs)}
-
- )
- } else {
- return (
-
- {trimValue(cellValue)}
-
- )
- }
- })
- }
- })
- }
-
- function createBodyTd(row) {
- return Object.keys(row).map((keyName) => {
- const value = row[keyName]
- if (keyName === 'td') {
- return value.map((cellValue, i) => {
- if (typeof cellValue === 'object') {
- return (
-
- {formatNumber(cellValue.content)}
-
- )
- } else {
- return {formatNumber(cellValue)}
- }
- })
- }
- })
- }
-
- function addNoteRefs(noteRefId) {
- if (noteRefId) {
- const noteRefs = table.noteRefs
- const noteIDs = noteRefId.split(' ')
- const notesToReturn = noteRefs.reduce((acc, current, index) => {
- // Lag et array av indeksen til alle id-enene i footer
- return noteIDs.some((element) => element === current) ? acc.concat(index) : acc
- }, [])
-
- if (notesToReturn) {
- return {notesToReturn.map((noteRef) => `${noteRef + 1} `)}
- }
- } else return ''
- }
-
- function addStandardSymbols() {
- const { standardSymbol } = props
-
- if (standardSymbol && standardSymbol.href && standardSymbol.text) {
- return (
-
- {standardSymbol.text}
-
- )
- }
- }
-
- function addPreviewButton() {
- if (showPreviewToggle && !props.pageTypeStatistic) {
- return (
-
- {!fetchUnPublished ? 'Vis upubliserte tall' : 'Vis publiserte tall'}
-
- )
- }
- return
- }
-
- function toggleDraft() {
- setFetchUnPublished(!fetchUnPublished)
- setTable(!fetchUnPublished && props.draftExist ? props.tableDraft : props.table)
- }
-
- function addPreviewInfo() {
- if (props.showPreviewDraft) {
- if (fetchUnPublished && props.draftExist) {
- return Tallene i tabellen nedenfor er upublisert
- } else if (fetchUnPublished && !props.draftExist) {
- return Finnes ikke upubliserte tall for denne tabellen
- }
- }
- return
- }
-
- function renderSources() {
- const { sources, sourceLabel, sourceListTables, sourceTableLabel, statBankWebUrl } = props
-
- if ((sourceListTables && sourceListTables.length > 0) || (sources && sources.length > 0)) {
- return (
-
-
-
- {sourceLabel}
-
-
- {sourceListTables.map((tableId, index) => {
- return (
-
-
- {sourceTableLabel + ' ' + tableId}
-
-
- )
- })}
- {sources.map((source, index) => {
- if (source.url && source.urlText) {
- return (
-
-
- {source.urlText}
-
-
- )
- }
- })}
-
- )
- }
- return null
- }
-
- const { hiddenTitle } = props
- return (
-
- {!isEmpty(table) ? (
-
-
- {hiddenTitle}
-
-
- {addPreviewButton()}
- {addDownloadTableDropdown(false)}
- {addPreviewInfo()}
- {createScrollControlsDesktop()}
- {createScrollControlsMobile()}
-
updateTableControlsDesktop()}
- ref={tableWrapperRef}
- >
- {createTable()}
-
- {addDownloadTableDropdown(true)}
- {addStandardSymbols()}
- {renderSources()}
-
-
- ) : (
-
-
Ingen tilknyttet Tabell
-
- )}
-
- )
-}
-
-const tableDataShape = PropTypes.shape({
- caption:
- PropTypes.string |
- PropTypes.shape({
- content: PropTypes.string,
- noterefs: PropTypes.string,
- }),
- tableClass: PropTypes.string,
- thead: PropTypes.arrayOf(
- PropTypes.shape({
- td:
- PropTypes.array |
- PropTypes.number |
- PropTypes.string |
- PropTypes.shape({
- rowspan: PropTypes.number,
- colspan: PropTypes.number,
- content: PropTypes.string,
- class: PropTypes.string,
- }),
- th:
- PropTypes.array |
- PropTypes.number |
- PropTypes.string |
- PropTypes.shape({
- rowspan: PropTypes.number,
- colspan: PropTypes.number,
- content: PropTypes.string,
- class: PropTypes.string,
- noterefs: PropTypes.string,
- }),
- })
- ),
- tbody: PropTypes.arrayOf(
- PropTypes.shape({
- th:
- PropTypes.array |
- PropTypes.number |
- PropTypes.string |
- PropTypes.shape({
- content: PropTypes.string,
- class: PropTypes.string,
- noterefs: PropTypes.string,
- }),
- td:
- PropTypes.array |
- PropTypes.number |
- PropTypes.string |
- PropTypes.shape({
- content: PropTypes.string,
- class: PropTypes.string,
- }),
- })
- ),
- tfoot: PropTypes.shape({
- footnotes: PropTypes.arrayOf(
- PropTypes.shape({
- noteid: PropTypes.string,
- content: PropTypes.string,
- })
- ),
- correctionNotice: PropTypes.string,
- }),
- language: PropTypes.string,
- noteRefs: PropTypes.arrayOf(PropTypes.string),
-})
-
-Table.propTypes = {
- downloadTableLabel: PropTypes.string,
- downloadTableTitle: PropTypes.object,
- downloadTableOptions: PropTypes.arrayOf(
- PropTypes.shape({
- title: PropTypes.string,
- id: PropTypes.string,
- })
- ),
- standardSymbol: PropTypes.shape({
- href: PropTypes.string,
- text: PropTypes.string,
- }),
- sourceLabel: PropTypes.string,
- sources: PropTypes.arrayOf(
- PropTypes.shape({
- urlText: PropTypes.string,
- url: PropTypes.string,
- })
- ),
- iconUrl: PropTypes.string,
- table: tableDataShape,
- tableDraft: tableDataShape,
- showPreviewDraft: PropTypes.bool,
- paramShowDraft: PropTypes.bool,
- draftExist: PropTypes.bool,
- pageTypeStatistic: PropTypes.bool,
- sourceListTables: PropTypes.arrayOf(PropTypes.string),
- sourceTableLabel: PropTypes.string,
- statBankWebUrl: PropTypes.string,
- hiddenTitle: PropTypes.string,
- GA_TRACKING_ID: PropTypes.string,
-}
+import React from 'react'
+import Table from '../table/Table'
export default (props) =>
diff --git a/src/main/resources/react4xp/_entries/Variables.jsx b/src/main/resources/react4xp/_entries/Variables.jsx
new file mode 100644
index 000000000..96907db93
--- /dev/null
+++ b/src/main/resources/react4xp/_entries/Variables.jsx
@@ -0,0 +1,4 @@
+import React from 'react'
+import Variables from '../variables/Variables'
+
+export default (props) =>
diff --git a/src/main/resources/react4xp/accordion/Accordion.jsx b/src/main/resources/react4xp/accordion/Accordion.jsx
new file mode 100644
index 000000000..63d969c63
--- /dev/null
+++ b/src/main/resources/react4xp/accordion/Accordion.jsx
@@ -0,0 +1,67 @@
+import React from 'react'
+import { Accordion as AccordionComponent, NestedAccordion } from '@statisticsnorway/ssb-component-library'
+
+import PropTypes from 'prop-types'
+
+class Accordion extends React.Component {
+ renderNestedAccordions(items) {
+ return items.map((item, i) => (
+
+ {item.body &&
}
+
+ ))
+ }
+
+ createMarkup(html) {
+ return {
+ __html: html.replace(/ /g, ' '),
+ }
+ }
+
+ render() {
+ const location = window.location
+ const anchor = location && location.hash !== '' ? location.hash.substr(1) : undefined
+
+ const { accordions } = this.props
+
+ return (
+
+
+ {accordions.map((accordion, index) => (
+
+
+ {accordion.body &&
}
+ {this.renderNestedAccordions(accordion.items)}
+
+
+ ))}
+
+
+ )
+ }
+}
+
+Accordion.propTypes = {
+ accordions: PropTypes.arrayOf(
+ PropTypes.shape({
+ id: PropTypes.string,
+ open: PropTypes.string.isRequired,
+ subHeader: PropTypes.string,
+ body: PropTypes.string,
+ items: PropTypes.arrayOf(
+ PropTypes.shape({
+ title: PropTypes.string,
+ body: PropTypes.string,
+ })
+ ),
+ })
+ ),
+}
+
+export default Accordion
diff --git a/src/main/resources/react4xp/_entries/bestbet/BestBetForm.jsx b/src/main/resources/react4xp/bestbet/BestBetForm.jsx
similarity index 97%
rename from src/main/resources/react4xp/_entries/bestbet/BestBetForm.jsx
rename to src/main/resources/react4xp/bestbet/BestBetForm.jsx
index 25bf2089b..d3abbedf6 100644
--- a/src/main/resources/react4xp/_entries/bestbet/BestBetForm.jsx
+++ b/src/main/resources/react4xp/bestbet/BestBetForm.jsx
@@ -5,8 +5,8 @@ import { Input, TextArea, Dropdown, Button, Divider, Tabs, RadioGroup } from '@s
import axios from 'axios'
import AsyncSelect from 'react-select/async'
import 'regenerator-runtime'
-import { BestBetContext } from '/react4xp/_entries/bestbet/Bestbet'
-import { customAsyncSelectStyles } from '/react4xp/_entries/bestbet/customAsyncSelectStyles'
+import { BestBetContext } from './Bestbet'
+import { customAsyncSelectStyles } from './customAsyncSelectStyles'
function BestBetForm(props) {
const { formState, dispatch } = useContext(BestBetContext)
@@ -242,4 +242,4 @@ BestBetForm.propTypes = {
handleTag: PropTypes.func,
}
-export default (props) =>
+export default BestBetForm
diff --git a/src/main/resources/react4xp/_entries/bestbet/BestBetModal.jsx b/src/main/resources/react4xp/bestbet/BestBetModal.jsx
similarity index 91%
rename from src/main/resources/react4xp/_entries/bestbet/BestBetModal.jsx
rename to src/main/resources/react4xp/bestbet/BestBetModal.jsx
index 7afb65ace..1e69c4aea 100644
--- a/src/main/resources/react4xp/_entries/bestbet/BestBetModal.jsx
+++ b/src/main/resources/react4xp/bestbet/BestBetModal.jsx
@@ -22,4 +22,4 @@ BestBetModal.propTypes = {
footer: PropTypes.node,
}
-export default (props) =>
+export default BestBetModal
diff --git a/src/main/resources/react4xp/_entries/bestbet/Bestbet.jsx b/src/main/resources/react4xp/bestbet/Bestbet.jsx
similarity index 98%
rename from src/main/resources/react4xp/_entries/bestbet/Bestbet.jsx
rename to src/main/resources/react4xp/bestbet/Bestbet.jsx
index eee9c8154..6483aa0c1 100644
--- a/src/main/resources/react4xp/_entries/bestbet/Bestbet.jsx
+++ b/src/main/resources/react4xp/bestbet/Bestbet.jsx
@@ -3,8 +3,8 @@ import PropTypes from 'prop-types'
import { Container, Row, Col } from 'react-bootstrap'
import { Title, Link, Tag, Button, Divider } from '@statisticsnorway/ssb-component-library'
import { XCircle, Edit, Trash, Plus } from 'react-feather'
-import BestBetModal from '/react4xp/_entries/bestbet/BestBetModal'
-import BestBetForm from '/react4xp/_entries/bestbet/BestBetForm'
+import BestBetModal from './BestBetModal'
+import BestBetForm from './BestBetForm'
import axios from 'axios'
export const BestBetContext = createContext()
@@ -419,4 +419,4 @@ Bestbet.propTypes = {
mainSubjects: PropTypes.array,
}
-export default (props) =>
+export default Bestbet
diff --git a/src/main/resources/react4xp/_entries/bestbet/customAsyncSelectStyles.ts b/src/main/resources/react4xp/bestbet/customAsyncSelectStyles.ts
similarity index 100%
rename from src/main/resources/react4xp/_entries/bestbet/customAsyncSelectStyles.ts
rename to src/main/resources/react4xp/bestbet/customAsyncSelectStyles.ts
diff --git a/src/main/resources/react4xp/table/Table.jsx b/src/main/resources/react4xp/table/Table.jsx
new file mode 100644
index 000000000..bd10ddb1c
--- /dev/null
+++ b/src/main/resources/react4xp/table/Table.jsx
@@ -0,0 +1,650 @@
+import React, { useState, useRef, useEffect } from 'react'
+import PropTypes from 'prop-types'
+import { Dropdown, Link } from '@statisticsnorway/ssb-component-library'
+import { default as isEmpty } from 'ramda/es/isEmpty'
+import NumberFormat from 'react-number-format'
+import { Alert, Button } from 'react-bootstrap'
+import { ChevronLeft, ChevronRight } from 'react-feather'
+import { addGtagForEvent } from '/react4xp/ReactGA'
+
+function Table(props) {
+ const [prevClientWidth, setPrevClientWidth] = useState(0)
+ const [table, setTable] = useState(props.paramShowDraft && props.draftExist ? props.tableDraft : props.table)
+ const [fetchUnPublished, setFetchUnPublished] = useState(props.paramShowDraft)
+
+ const showPreviewToggle =
+ props.showPreviewDraft && (!props.pageTypeStatistic || (props.paramShowDraft && props.pageTypeStatistic))
+
+ const captionRef = useRef(null)
+ const tableControlsDesktopRef = useRef(null)
+ const tableControlsMobileRef = useRef(null)
+ const tableRef = useRef(null)
+ const tableWrapperRef = useRef(null)
+
+ useEffect(() => {
+ updateTableControlsDesktop()
+
+ const widthCheckInterval = setInterval(() => {
+ widthCheck()
+ }, 250)
+ window.addEventListener('resize', updateTableControlsDesktop)
+ return () => {
+ clearInterval(widthCheckInterval)
+ window.removeEventListener('resize', updateTableControlsDesktop)
+ }
+ }, [])
+
+ function widthCheck() {
+ if (tableWrapperRef.current.clientWidth !== prevClientWidth) {
+ setPrevClientWidth(tableWrapperRef.current.clientWidth)
+ updateTableControlsDesktop()
+ }
+ }
+
+ function updateTableControlsDesktop() {
+ const controls = tableControlsDesktopRef.current
+ const tableWrapper = tableWrapperRef.current
+ const left = controls.children.item(0)
+ const right = controls.children.item(1)
+
+ // hide controlls if there is no scrollbar
+ if (tableWrapper.scrollWidth > tableWrapper.clientWidth || tableWrapper.clientWidth === 0) {
+ controls.classList.remove('d-none')
+ tableControlsMobileRef.current.classList.remove('d-none')
+ // disable left
+ if (tableWrapper.scrollLeft <= 0) {
+ left.classList.add('disabled')
+ } else {
+ left.classList.remove('disabled')
+ }
+
+ // disable right
+ if (tableWrapper.scrollLeft + tableWrapper.clientWidth >= tableWrapper.scrollWidth) {
+ right.classList.add('disabled')
+ } else {
+ right.classList.remove('disabled')
+ }
+
+ // move desktop controls to correct pos
+ const captionHalfHeight = captionRef.current.offsetHeight / 2
+ const controlsHalfHeight = left.scrollHeight / 2
+ left.style.marginTop = `${captionHalfHeight - controlsHalfHeight}px`
+ right.style.marginTop = `${captionHalfHeight - controlsHalfHeight}px`
+ } else {
+ controls.classList.add('d-none')
+ tableControlsMobileRef.current.classList.add('d-none')
+ }
+ }
+
+ function scrollLeft() {
+ tableWrapperRef.current.scrollLeft -= 100
+ updateTableControlsDesktop()
+ }
+
+ function scrollRight() {
+ tableWrapperRef.current.scrollLeft += 100
+ updateTableControlsDesktop()
+ }
+
+ function trimValue(value) {
+ if (value && typeof value === 'string') {
+ return value.trim()
+ }
+ return value
+ }
+
+ function formatNumber(value) {
+ const language = props.table.language
+ const decimalSeparator = language === 'en' ? '.' : ','
+ value = trimValue(value)
+ if (value) {
+ if (typeof value === 'number' || (typeof value === 'string' && !isNaN(value))) {
+ const decimals = value.toString().indexOf('.') > -1 ? value.toString().split('.')[1].length : 0
+ return (
+
+ )
+ }
+ }
+ return value
+ }
+
+ function addDownloadTableDropdown(mobile) {
+ const { downloadTableLabel, downloadTableTitle, downloadTableOptions } = props
+
+ if (downloadTableLabel && downloadTableTitle && downloadTableOptions) {
+ const downloadTable = (item) => {
+ if (item.id === 'downloadTableAsCSV') {
+ {
+ downloadTableAsCSV()
+ }
+ }
+
+ if (item.id === 'downloadTableAsXLSX') {
+ {
+ downloadTableAsExcel()
+ }
+ }
+ }
+
+ return (
+
+
+
+ )
+ }
+ }
+
+ function downloadTableAsCSV() {
+ if (props.GA_TRACKING_ID) {
+ addGtagForEvent(props.GA_TRACKING_ID, 'Lastet ned csv tabell', 'Statistikkside tabeller', 'Last ned csv tabell')
+ }
+
+ if (window && window.downloadTableFile) {
+ window.downloadTableFile(tableRef.current, {
+ type: 'csv',
+ fileName: 'tabell',
+ csvSeparator: ';',
+ csvEnclosure: '',
+ tfootSelector: '',
+ })
+ }
+ }
+
+ function downloadTableAsExcel() {
+ if (props.GA_TRACKING_ID) {
+ addGtagForEvent(
+ props.GA_TRACKING_ID,
+ 'Lastet ned excell tabell',
+ 'Statistikkside tabeller',
+ 'Last ned excell tabell'
+ )
+ }
+
+ if (window && window.downloadTableFile) {
+ window.downloadTableFile(tableRef.current, {
+ type: 'xlsx',
+ fileName: 'tabell',
+ numbers: {
+ html: {
+ decimalMark: ',',
+ thousandsSeparator: ' ',
+ },
+ output: {
+ decimalMark: '.',
+ thousandsSeparator: '',
+ },
+ },
+ })
+ }
+ }
+
+ function createTable() {
+ const { tableClass } = props.table
+
+ return (
+
+ {addCaption()}
+ {table.thead.map((t, index) => {
+ return (
+
+ {addThead(index)}
+ {addTbody(index)}
+
+ )
+ })}
+ {addTFoot()}
+
+ )
+ }
+
+ function addCaption() {
+ const { caption } = table
+ if (caption) {
+ const hasNoteRefs = typeof caption === 'object'
+ return (
+
+
+ {hasNoteRefs ? caption.content : caption}
+ {hasNoteRefs ? addNoteRefs(caption.noterefs) : null}
+
+
+ )
+ }
+ }
+
+ function createScrollControlsMobile() {
+ return (
+
+
+
+ )
+ }
+
+ function createScrollControlsDesktop() {
+ return (
+
+ scrollLeft()}>
+
+
+ scrollRight()}>
+
+
+
+ )
+ }
+
+ function addThead(index) {
+ return {createRowsHead(table.thead[index].tr)}
+ }
+
+ function addTbody(index) {
+ return {createRowsBody(table.tbody[index].tr)}
+ }
+
+ function renderCorrectionNotice() {
+ if (table.tfoot.correctionNotice) {
+ return (
+
+ {table.tfoot.correctionNotice}
+
+ )
+ }
+ return null
+ }
+
+ function addTFoot() {
+ const { footnotes, correctionNotice } = table.tfoot
+
+ const noteRefs = table.noteRefs
+
+ if ((noteRefs && noteRefs.length > 0) || correctionNotice) {
+ return (
+
+ {noteRefs.map((note, index) => {
+ const current = footnotes && footnotes.find((footnote) => footnote.noteid === note)
+ if (current) {
+ return (
+
+
+ {index + 1}
+ {current.content}
+
+
+ )
+ } else {
+ return null
+ }
+ })}
+ {renderCorrectionNotice()}
+
+ )
+ }
+ return null
+ }
+
+ function createRowsHead(rows) {
+ if (rows) {
+ return rows.map((row, i) => {
+ return {createHeaderCell(row)}
+ })
+ }
+ }
+
+ function createRowsBody(rows) {
+ if (rows) {
+ return rows.map((row, i) => {
+ return (
+
+ {createBodyTh(row)}
+ {createBodyTd(row)}
+
+ )
+ })
+ }
+ }
+
+ function createHeaderCell(row) {
+ return Object.keys(row).map((keyName) => {
+ const value = row[keyName]
+ if (keyName === 'th') {
+ return createHeadTh(value)
+ } else if (keyName === 'td') {
+ return createHeadTd(value)
+ }
+ })
+ }
+
+ function createHeadTh(value) {
+ return value.map((cellValue, i) => {
+ if (typeof cellValue === 'object') {
+ if (Array.isArray(cellValue)) {
+ // TODO: Because some values is split into array by xmlParser i have to do this, find better fix
+ return {cellValue.join(' ')}
+ } else {
+ return (
+
+ {trimValue(cellValue.content)}
+ {addNoteRefs(cellValue.noterefs)}
+
+ )
+ }
+ } else {
+ return (
+
+ {trimValue(cellValue)}
+
+ )
+ }
+ })
+ }
+
+ function createHeadTd(value) {
+ return value.map((cellValue, i) => {
+ if (typeof cellValue === 'object') {
+ return (
+
+ {trimValue(cellValue.content)}
+ {addNoteRefs(cellValue.noterefs)}
+
+ )
+ } else {
+ return {trimValue(cellValue)}
+ }
+ })
+ }
+
+ function createBodyTh(row) {
+ return Object.keys(row).map((key) => {
+ const value = row[key]
+ if (key === 'th') {
+ return value.map((cellValue, i) => {
+ if (typeof cellValue === 'object') {
+ return (
+
+ {trimValue(cellValue.content)}
+ {addNoteRefs(cellValue.noterefs)}
+
+ )
+ } else {
+ return (
+
+ {trimValue(cellValue)}
+
+ )
+ }
+ })
+ }
+ })
+ }
+
+ function createBodyTd(row) {
+ return Object.keys(row).map((keyName) => {
+ const value = row[keyName]
+ if (keyName === 'td') {
+ return value.map((cellValue, i) => {
+ if (typeof cellValue === 'object') {
+ return (
+
+ {formatNumber(cellValue.content)}
+
+ )
+ } else {
+ return {formatNumber(cellValue)}
+ }
+ })
+ }
+ })
+ }
+
+ function addNoteRefs(noteRefId) {
+ if (noteRefId) {
+ const noteRefs = table.noteRefs
+ const noteIDs = noteRefId.split(' ')
+ const notesToReturn = noteRefs.reduce((acc, current, index) => {
+ // Lag et array av indeksen til alle id-enene i footer
+ return noteIDs.some((element) => element === current) ? acc.concat(index) : acc
+ }, [])
+
+ if (notesToReturn) {
+ return {notesToReturn.map((noteRef) => `${noteRef + 1} `)}
+ }
+ } else return ''
+ }
+
+ function addStandardSymbols() {
+ const { standardSymbol } = props
+
+ if (standardSymbol && standardSymbol.href && standardSymbol.text) {
+ return (
+
+ {standardSymbol.text}
+
+ )
+ }
+ }
+
+ function addPreviewButton() {
+ if (showPreviewToggle && !props.pageTypeStatistic) {
+ return (
+
+ {!fetchUnPublished ? 'Vis upubliserte tall' : 'Vis publiserte tall'}
+
+ )
+ }
+ return
+ }
+
+ function toggleDraft() {
+ setFetchUnPublished(!fetchUnPublished)
+ setTable(!fetchUnPublished && props.draftExist ? props.tableDraft : props.table)
+ }
+
+ function addPreviewInfo() {
+ if (props.showPreviewDraft) {
+ if (fetchUnPublished && props.draftExist) {
+ return Tallene i tabellen nedenfor er upublisert
+ } else if (fetchUnPublished && !props.draftExist) {
+ return Finnes ikke upubliserte tall for denne tabellen
+ }
+ }
+ return
+ }
+
+ function renderSources() {
+ const { sources, sourceLabel, sourceListTables, sourceTableLabel, statBankWebUrl } = props
+
+ if ((sourceListTables && sourceListTables.length > 0) || (sources && sources.length > 0)) {
+ return (
+
+
+
+ {sourceLabel}
+
+
+ {sourceListTables.map((tableId, index) => {
+ return (
+
+
+ {sourceTableLabel + ' ' + tableId}
+
+
+ )
+ })}
+ {sources.map((source, index) => {
+ if (source.url && source.urlText) {
+ return (
+
+
+ {source.urlText}
+
+
+ )
+ }
+ })}
+
+ )
+ }
+ return null
+ }
+
+ const { hiddenTitle } = props
+ return (
+
+ {!isEmpty(table) ? (
+
+
+ {hiddenTitle}
+
+
+ {addPreviewButton()}
+ {addDownloadTableDropdown(false)}
+ {addPreviewInfo()}
+ {createScrollControlsDesktop()}
+ {createScrollControlsMobile()}
+
updateTableControlsDesktop()}
+ ref={tableWrapperRef}
+ >
+ {createTable()}
+
+ {addDownloadTableDropdown(true)}
+ {addStandardSymbols()}
+ {renderSources()}
+
+
+ ) : (
+
+
Ingen tilknyttet Tabell
+
+ )}
+
+ )
+}
+
+const tableDataShape = PropTypes.shape({
+ caption:
+ PropTypes.string |
+ PropTypes.shape({
+ content: PropTypes.string,
+ noterefs: PropTypes.string,
+ }),
+ tableClass: PropTypes.string,
+ thead: PropTypes.arrayOf(
+ PropTypes.shape({
+ td:
+ PropTypes.array |
+ PropTypes.number |
+ PropTypes.string |
+ PropTypes.shape({
+ rowspan: PropTypes.number,
+ colspan: PropTypes.number,
+ content: PropTypes.string,
+ class: PropTypes.string,
+ }),
+ th:
+ PropTypes.array |
+ PropTypes.number |
+ PropTypes.string |
+ PropTypes.shape({
+ rowspan: PropTypes.number,
+ colspan: PropTypes.number,
+ content: PropTypes.string,
+ class: PropTypes.string,
+ noterefs: PropTypes.string,
+ }),
+ })
+ ),
+ tbody: PropTypes.arrayOf(
+ PropTypes.shape({
+ th:
+ PropTypes.array |
+ PropTypes.number |
+ PropTypes.string |
+ PropTypes.shape({
+ content: PropTypes.string,
+ class: PropTypes.string,
+ noterefs: PropTypes.string,
+ }),
+ td:
+ PropTypes.array |
+ PropTypes.number |
+ PropTypes.string |
+ PropTypes.shape({
+ content: PropTypes.string,
+ class: PropTypes.string,
+ }),
+ })
+ ),
+ tfoot: PropTypes.shape({
+ footnotes: PropTypes.arrayOf(
+ PropTypes.shape({
+ noteid: PropTypes.string,
+ content: PropTypes.string,
+ })
+ ),
+ correctionNotice: PropTypes.string,
+ }),
+ language: PropTypes.string,
+ noteRefs: PropTypes.arrayOf(PropTypes.string),
+})
+
+Table.propTypes = {
+ downloadTableLabel: PropTypes.string,
+ downloadTableTitle: PropTypes.object,
+ downloadTableOptions: PropTypes.arrayOf(
+ PropTypes.shape({
+ title: PropTypes.string,
+ id: PropTypes.string,
+ })
+ ),
+ standardSymbol: PropTypes.shape({
+ href: PropTypes.string,
+ text: PropTypes.string,
+ }),
+ sourceLabel: PropTypes.string,
+ sources: PropTypes.arrayOf(
+ PropTypes.shape({
+ urlText: PropTypes.string,
+ url: PropTypes.string,
+ })
+ ),
+ iconUrl: PropTypes.string,
+ table: tableDataShape,
+ tableDraft: tableDataShape,
+ showPreviewDraft: PropTypes.bool,
+ paramShowDraft: PropTypes.bool,
+ draftExist: PropTypes.bool,
+ pageTypeStatistic: PropTypes.bool,
+ sourceListTables: PropTypes.arrayOf(PropTypes.string),
+ sourceTableLabel: PropTypes.string,
+ statBankWebUrl: PropTypes.string,
+ hiddenTitle: PropTypes.string,
+ GA_TRACKING_ID: PropTypes.string,
+}
+
+export default Table
diff --git a/src/main/resources/react4xp/_entries/variables/VariableCard.jsx b/src/main/resources/react4xp/variables/VariableCard.jsx
similarity index 86%
rename from src/main/resources/react4xp/_entries/variables/VariableCard.jsx
rename to src/main/resources/react4xp/variables/VariableCard.jsx
index e20da9939..f0b7dedcf 100644
--- a/src/main/resources/react4xp/_entries/variables/VariableCard.jsx
+++ b/src/main/resources/react4xp/variables/VariableCard.jsx
@@ -1,6 +1,6 @@
import React from 'react'
import { Card, Text } from '@statisticsnorway/ssb-component-library'
-import { variableType } from '/react4xp/_entries/variables/types'
+import { variableType } from './types'
const VariableCard = ({ variable }) => {
const { icon, description, ...rest } = variable
diff --git a/src/main/resources/react4xp/_entries/variables/VariableCardsList.jsx b/src/main/resources/react4xp/variables/VariableCardsList.jsx
similarity index 78%
rename from src/main/resources/react4xp/_entries/variables/VariableCardsList.jsx
rename to src/main/resources/react4xp/variables/VariableCardsList.jsx
index 988c1ca05..68c0b1857 100644
--- a/src/main/resources/react4xp/_entries/variables/VariableCardsList.jsx
+++ b/src/main/resources/react4xp/variables/VariableCardsList.jsx
@@ -1,7 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
-import VariableCard from '/react4xp/_entries/variables/VariableCard.jsx'
-import { variableType } from '/react4xp/_entries/variables/types'
+import VariableCard from './VariableCard.jsx'
+import { variableType } from './types'
class VariableCardsList extends React.Component {
constructor(props) {
diff --git a/src/main/resources/react4xp/_entries/variables/Variables.jsx b/src/main/resources/react4xp/variables/Variables.jsx
similarity index 80%
rename from src/main/resources/react4xp/_entries/variables/Variables.jsx
rename to src/main/resources/react4xp/variables/Variables.jsx
index 220e40b43..eb6dfcbf4 100644
--- a/src/main/resources/react4xp/_entries/variables/Variables.jsx
+++ b/src/main/resources/react4xp/variables/Variables.jsx
@@ -1,8 +1,8 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Text } from '@statisticsnorway/ssb-component-library'
-import VariableCardsList from '/react4xp/_entries/variables/VariableCardsList.jsx'
-import { variableType } from '/react4xp/_entries/variables/types'
+import VariableCardsList from './VariableCardsList.jsx'
+import { variableType } from './types'
export const DISPLAY_TYPE_CARDS = 'CARDS'
export const DISPLAY_TYPE_TABLE = 'TABLE'
diff --git a/src/main/resources/react4xp/_entries/variables/Variables.md b/src/main/resources/react4xp/variables/Variables.md
similarity index 100%
rename from src/main/resources/react4xp/_entries/variables/Variables.md
rename to src/main/resources/react4xp/variables/Variables.md
diff --git a/src/main/resources/react4xp/_entries/variables/types.ts b/src/main/resources/react4xp/variables/types.ts
similarity index 100%
rename from src/main/resources/react4xp/_entries/variables/types.ts
rename to src/main/resources/react4xp/variables/types.ts
diff --git a/src/main/resources/site/parts/accordion/accordion.ts b/src/main/resources/site/parts/accordion/accordion.ts
index bbecb0bdf..155b5bd0f 100644
--- a/src/main/resources/site/parts/accordion/accordion.ts
+++ b/src/main/resources/site/parts/accordion/accordion.ts
@@ -9,7 +9,6 @@ const {
} = __non_webpack_require__('/lib/util')
const { sanitize } = __non_webpack_require__('/lib/xp/common')
const { renderError } = __non_webpack_require__('/lib/ssb/error/error')
-const { isEnabled } = __non_webpack_require__('/lib/featureToggle')
export function get(req: XP.Request): XP.Response {
try {
@@ -38,7 +37,6 @@ export function preview(req: XP.Request, accordionIds: Array | string):
function renderPart(req: XP.Request, accordionIds: Array) {
const accordions: Array = []
- const csrOnTableAccordion: boolean = isEnabled('csr-on-table-accordion', false, 'ssb')
accordionIds.map((key) => {
const accordion: Content | null = key
@@ -93,7 +91,7 @@ function renderPart(req: XP.Request, accordionIds: Array) {
accordions,
}
- return render('Accordion', props, req, { ssr: !csrOnTableAccordion })
+ return render('Accordion', props, req)
}
export interface AccordionData {
diff --git a/src/main/resources/site/parts/omStatistikken/omStatistikken.jsx b/src/main/resources/site/parts/omStatistikken/omStatistikken.jsx
index 331e69ec6..72aad846d 100644
--- a/src/main/resources/site/parts/omStatistikken/omStatistikken.jsx
+++ b/src/main/resources/site/parts/omStatistikken/omStatistikken.jsx
@@ -1,6 +1,6 @@
import React from 'react'
import PropTypes from 'prop-types'
-import Accordion from '/react4xp/_entries/Accordion'
+import Accordion from '/react4xp/accordion/Accordion'
const OmStatistikken = (props) => {
const { ingress, label, accordions } = props
diff --git a/src/main/resources/site/parts/table/table.ts b/src/main/resources/site/parts/table/table.ts
index 7a92f0f2c..7202ae95b 100644
--- a/src/main/resources/site/parts/table/table.ts
+++ b/src/main/resources/site/parts/table/table.ts
@@ -23,7 +23,6 @@ const {
const { getLanguage, getPhrases } = __non_webpack_require__('/lib/ssb/utils/language')
const { DATASET_BRANCH, UNPUBLISHED_DATASET_BRANCH } = __non_webpack_require__('/lib/ssb/repo/dataset')
const { hasWritePermissionsAndPreview } = __non_webpack_require__('/lib/ssb/parts/permissions')
-const { isEnabled } = __non_webpack_require__('/lib/featureToggle')
const view = resolve('./table.html')
@@ -140,7 +139,6 @@ function renderPart(req: XP.Request, tableId?: string): XP.Response {
if (!page) throw Error('No page found')
const phrases: Phrases = getPhrases(page) as Phrases
- const csrOnTableAccordion: boolean = isEnabled('csr-on-table-accordion', false, 'ssb')
if (!tableId) {
if (req.mode === 'edit' && page.type !== `${app.name}:statistics`) {
@@ -160,7 +158,6 @@ function renderPart(req: XP.Request, tableId?: string): XP.Response {
pageContributions: {
bodyEnd: [scriptAsset('js/tableExport.js')],
},
- ssr: !csrOnTableAccordion,
})
}
diff --git a/src/main/resources/site/parts/variables/variables.ts b/src/main/resources/site/parts/variables/variables.ts
index 53cde17c2..b3fdd8452 100644
--- a/src/main/resources/site/parts/variables/variables.ts
+++ b/src/main/resources/site/parts/variables/variables.ts
@@ -47,7 +47,7 @@ function renderVariables(req: XP.Request, variables: Array): XP.Respo
})
return render(
- 'variables/Variables',
+ 'Variables',
{
variables: variables.map(({ title, description, fileHref, fileModifiedDate, href }) => {
return {