diff --git a/.gitignore b/.gitignore index 3f1b436f..a294589b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ node_modules .env.development .vscode/settings.json storybook-static -_redirects +# _redirects diff --git a/_redirects b/_redirects new file mode 100644 index 00000000..44940377 --- /dev/null +++ b/_redirects @@ -0,0 +1,3 @@ +/proxy-githubusercontent/* https://raw.githubusercontent.com/:splat 200 +/api/* https://journalofdigitalhistory.org/api/:splat 200 +/* /index.html 200 \ No newline at end of file diff --git a/package.json b/package.json index 2d0224ad..10bdb661 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@jonkoops/matomo-tracker-react": "^0.7.0", "@react-spring/web": "^9.7.3", "@tanstack/react-query": "^4.29.19", + "@tanstack/react-table": "^8.10.7", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/src/components/Article/ArticleCell.js b/src/components/Article/ArticleCell.js index 64fab0b2..14c6b4a5 100644 --- a/src/components/Article/ArticleCell.js +++ b/src/components/Article/ArticleCell.js @@ -158,6 +158,7 @@ const ArticleCell = ({ containerClassName={containerClassNames.join(' ')} windowHeight={windowHeight} active={active} + cellType={type} > p { + padding: var(--spacer-2); +} +.ArticleCellDataTable table { + border: 0px solid transparent; + background-color: transparent; + width: 100%; +} +.ArticleCellDataTable table tbody { + box-shadow: rgba(0, 0, 0, 0.06) 0px 2px 4px 0px inset; +} +.ArticleCellDataTable__header { + position: sticky; + top: 100px; +} +.ArticleCellDataTable__pagination { + display: flex; + justify-content: space-between; + align-items: center; +} + +.ArticleCellDataTable .input-group { + z-index: 0; +} +.ArticleCellDataTable__pagination .input-group .input-group-text { + background: transparent; + border-top-color: inherit; + border-bottom-color: inherit; + padding: 0 var(--spacer-2) 0 var(--spacer-1); +} + +.ArticleCellDataTable__pagination .input-group .input-group-text label { + background: var(--dark); + color: white; + border-radius: 3px; + padding: 0 var(--spacer-2); + font-weight: bold; +} + +.ArticleCellDataTable__pagination .form-select { + background-color: transparent; + border-color: inherit; + width: 100px; +} +.ArticleCellDataTable__pagination .form-select:focus { + box-shadow: 0 0 0 0.25rem rgba(0, 0, 0, 0.186); +} +.ArticleCellDataTable table th { + padding: var(--spacer-2); + background-color: transparent; + vertical-align: bottom; + border: 1px solid rgba(0, 0, 0, 0.186); +} +.ArticleCellDataTable table td { + border: 1px solid rgba(0, 0, 0, 0.061); +} +.ArticleCellDataTable table th:first-child, +.ArticleCellDataTable table td:first-child, +.ArticleCellDataTable table td:last-child, +.ArticleCellDataTable table th:last-child { + border-left: 0px solid transparent; +} + +.ArticleCellDataTable table thead { + border-bottom: 1px solid; + border-top: 1px solid rgba(0, 0, 0, 0.186); +} + +.ArticleCellDataTable table tbody tr:nth-of-type(2n + 1) { + background-color: rgba(0, 0, 0, 0.05); +} + +.ArticleCellDataTable .ColumnSorting { + display: block; + width: auto; + min-width: 50px; + margin-top: var(--spacer-1); + opacity: 0.5; +} +.ArticleCellDataTable .ColumnSorting:hover, +.ArticleCellDataTable .ColumnSorting.active { + opacity: 1; +} +.ArticleCellDataTable .ColumnSorting button { + --bs-btn-line-height: 1.15; +} +.ArticleCellDataTable .ColumnSorting button.active { + box-shadow: 0 0 0 0.25rem rgba(0, 0, 0, 0.186); +} diff --git a/src/components/Article/ArticleCellDataTable.js b/src/components/Article/ArticleCellDataTable.js new file mode 100644 index 00000000..2da7dda1 --- /dev/null +++ b/src/components/Article/ArticleCellDataTable.js @@ -0,0 +1,323 @@ +import React from 'react' +import { + ChevronLeft, + ChevronsLeft, + ChevronRight, + ChevronsRight, + ChevronDown, + ChevronUp, +} from 'react-feather' +import { + createColumnHelper, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + // sortingFns, + useReactTable, + // PaginationState, +} from '@tanstack/react-table' +import DebouncedInput from '../DebouncedInput' +import './ArticleCellDataTable.css' +import { useTranslation } from 'react-i18next' +// import { +// RankingInfo, +// rankItem, +// compareItems, +// } from '@tanstack/match-sorter-utils' +const columnHelper = createColumnHelper() + +const ColumnFilter = ({ column }) => { + const columnFilterValue = column.getFilterValue() + return ( + column.setFilterValue(value)} + placeholder={`Search... (${column.getFacetedUniqueValues().size})`} + className="w-36 border shadow rounded" + list={column.id + 'list'} + /> + ) +} + +const ColumnSorting = ({ column }) => { + const columnSortingValue = column.getIsSorted() + const sortBy = (direction) => { + console.debug('[ColumnSorting] sortBy', direction, 'columnSortingValue', columnSortingValue) + if (columnSortingValue === direction) { + column.clearSorting() + } else { + column.toggleSorting(direction !== 'asc') + } + } + return ( +
+ + +
+ ) +} + +const ArticleCellDataTable = ({ + cellIdx = -1, + htmlContent = '', + allowFilterColumn = false, + allowSorting = true, +}) => { + const { t } = useTranslation() + const [columnFilters, setColumnFilters] = React.useState([]) + const [sorting, setSorting] = React.useState([]) + + const [globalFilter, setGlobalFilter] = React.useState('') + // htmlContent is "\n\n\n\n\n\n\n\n\n" + // get the table headers from the htmlContent" + // const tableHeaders = htmlContent.split(''.match(/]*>(.*?)<\/th>/g) + htmlContent + .split('') + .shift() + .match(/]*>(.*?)<\/th>/g) + ?.map((th, i) => { + const v = th.replace(/]*>/g, '').replace(/<\/?th>/g, '') + return typeof v !== 'string' || v.length === 0 ? String('c' + i) : v + }) || [], + [], + ) + // Create the table and pass your options + const columns = React.useMemo(() => thValues.map((v) => columnHelper.accessor(v), [])) + + const data = React.useMemo( + () => + htmlContent + .split('') + .pop() + .match(/]*>[\s\S]*?<\/tr>/g) + ?.map((tr) => { + const v = tr.replace(/]*>/g, '').replace(/<\/?tr>/g, '') + + const tdValues = + v.match(/]*>(.*?)<\/t[dh]>/g)?.reduce((acc, td, i) => { + const v = td.replace(/]*>/g, '').replace(/<\/?t[dh]>/g, '') + const header = thValues[i] + acc[header] = v + return acc + }, {}) || {} + return tdValues + }) || [], + [], + ) + + if (cellIdx === 86) { + console.debug( + '[ArticleCellDataTable] creating table ... \n - cellIdx:', + cellIdx, + '\n - columns:', + columns, + '\n - data:', + data, + ) + } + + const table = useReactTable({ + data, + columns, + state: { + columnFilters, + sorting, + globalFilter, + }, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + onSortingChange: setSorting, + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + }) + + const columnFilterId = table.getState().columnFilters[0]?.id + const isFiltered = columnFilterId !== undefined || globalFilter !== '' + React.useEffect(() => { + console.debug( + '[ArticleCellDataTable] @useEffect \n - cellIdx:', + cellIdx, + '\n - columns:', + columns, + '\n - columnFilterId:', + columnFilterId, + ) + if (table.getState().columnFilters[0]?.id === 'fullName') { + if (table.getState().sorting[0]?.id !== 'fullName') { + table.setSorting([{ id: 'fullName', desc: false }]) + } + } + }, [columnFilterId]) + + const displayedRows = table.getRowModel().rows + const pageSize = table.getState().pagination.pageSize + const currentPage = table.getState().pagination.pageIndex + const startOffset = currentPage * pageSize + 1 + const endOffset = Math.min( + (currentPage + 1) * pageSize, + isFiltered ? displayedRows.length : data.length, + ) + + console.debug('[ArticleCellDataTable] rendered \n - cellIdx:', cellIdx) + return ( +
+ {/* Pagination */} + {data.length > 10 ? ( +
+ {/*

*/} +

+ + + + + + +
+ setGlobalFilter(String(value))} + className="mx-2 border border-dark form-control form-control-sm" + placeholder="Search all columns..." + /> + +
+ ) : null} + {/* Table :) */} +
Use CaseCell TypeContent TypeTagCaption
') + const thValues = React.useMemo( + () => + // e.g. test with 'Titolo
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + ) + })} + + ))} + + + {table.getRowModel().rows.map((row) => { + return ( + + {row.getVisibleCells().map((cell) => { + return ( + + ) + })} + +
+ {header.id} + {allowFilterColumn && header.column.getCanFilter() ? ( + + ) : null} + {allowSorting && header.column.getCanSort() ? ( + + ) : null} +
+ ) + })} +
+ + ) +} + +export default ArticleCellDataTable diff --git a/src/components/Article/ArticleCellFigure.js b/src/components/Article/ArticleCellFigure.js index e9c24e7d..f21877bb 100644 --- a/src/components/Article/ArticleCellFigure.js +++ b/src/components/Article/ArticleCellFigure.js @@ -2,9 +2,10 @@ import React, { useMemo } from 'react' import ArticleCellOutputs from './ArticleCellOutputs' import ArticleFigureCaption from './ArticleFigureCaption' import { markdownParser } from '../../logic/ipynb' -import { BootstrapColumLayout } from '../../constants' +import { BootstrapColumLayout, DataTableRefPrefix } from '../../constants' import { Container, Row, Col } from 'react-bootstrap' import '../../styles/components/Article/ArticleCellFigure.scss' +import ArticleCellDataTable from './ArticleCellDataTable' const ArticleCellFigure = ({ figure, @@ -18,6 +19,7 @@ const ArticleCellFigure = ({ windowHeight = 100, isMagic = false, isolationMode = false, + cellType = 'markdown', // lazy = false, // withTransition = false, active = false, @@ -51,7 +53,7 @@ const ArticleCellFigure = ({ * useMemo hook that processes the outputs of an article figure to extract captions, pictures and other outputs. * It checks if a caption has been added to the cell metadata. * */ - const { captions, pictures, otherOutputs } = useMemo( + const { captions, pictures, otherOutputs, htmlOutputs } = useMemo( () => [{ metadata }].concat(outputs).reduce( (acc, output = {}) => { @@ -65,7 +67,10 @@ const ArticleCellFigure = ({ const isOutputEmpty = outputProps.length === 0 || (outputProps.length === 1 && outputProps.shift() === 'metadata') - + const isHTML = mimetypes.includes('text/html') + if (isHTML) { + acc.htmlOutputs.push(output) + } if (mimetype) { acc.pictures.push({ // ...output, @@ -77,7 +82,7 @@ const ArticleCellFigure = ({ } return acc }, - { captions: [], pictures: [], otherOutputs: [] }, + { captions: [], pictures: [], otherOutputs: [], htmlOutputs: [] }, ), [figure.idx], ) @@ -91,20 +96,42 @@ const ArticleCellFigure = ({ return acc }, BootstrapColumLayout) - console.debug( - '[ArticleCellFigure] \n - idx:', - figure.idx, - '\n - aspectRatio:', - aspectRatio, - '\n - tags:', - tags, - '\n - n.pictures:', - pictures.length, - '\n - active:', - active, - captions, - ) + // console.debug( + // '[ArticleCellFigure] \n - idx:', + // figure.idx, + // '\n - aspectRatio:', + // aspectRatio, + // '\n - tags:', + // tags, + // '\n - n.pictures:', + // pictures.length, + // '\n - active:', + // active, + // captions, + // ) + const isDataTable = figure.refPrefix === DataTableRefPrefix && cellType === 'markdown' + let dataTableContent = '' + if (isDataTable) { + columnLayout = { md: 10, lg: 11 } + + if (htmlOutputs.length > 0) { + dataTableContent = htmlOutputs[0].data['text/html'].join('\n') + } else { + dataTableContent = children?.props?.content + } + } + if (figure.idx === 22) { + console.debug( + '[ArticleCellFigure] \n - idx:', + figure.idx, + cellType, + 'dataTableContent', + dataTableContent.length, + ) + // eslint-disable-next-line no-debugger + // debugger + } return (
@@ -160,7 +187,15 @@ const ArticleCellFigure = ({ withTransition={withTransition} /> )) */} - {children} + {isDataTable ? ( + + ) : ( + children + )} diff --git a/src/components/ArticleV2/ArticleToCSteps.js b/src/components/ArticleV2/ArticleToCSteps.js index 69b55f0f..afbfebc9 100644 --- a/src/components/ArticleV2/ArticleToCSteps.js +++ b/src/components/ArticleV2/ArticleToCSteps.js @@ -166,7 +166,7 @@ const ArticleToCSteps = ({
  • { + const [value, setValue] = useState(initialValue) + + useEffect(() => { + setValue(initialValue) + }, [initialValue]) + + useEffect(() => { + const timeout = setTimeout(() => { + onChange(value) + }, debounce) + + return () => clearTimeout(timeout) + }, [value]) + + return setValue(e.target.value)} /> +} + +export default DebouncedInput diff --git a/src/components/ToCStep.js b/src/components/ToCStep.js index 811d08fc..f4683331 100644 --- a/src/components/ToCStep.js +++ b/src/components/ToCStep.js @@ -6,6 +6,7 @@ import { DialogRefPrefix, FigureRefPrefix, TableRefPrefix, + DataTableRefPrefix, VideoRefPrefix, SoundRefPrefix, LayerNarrative, @@ -16,6 +17,7 @@ const FigureRefPrefixMapping = { [FigureRefPrefix]: MediaImage, [DialogRefPrefix]: MessageText, [TableRefPrefix]: Table, + [DataTableRefPrefix]: Table, [SoundRefPrefix]: SoundMin, [VideoRefPrefix]: MediaVideo, } @@ -76,7 +78,7 @@ const ToCStep = ({ -
    +
    {isHermeneutics && !isFigure && } {!isHermeneutics && !isFigure &&
    } {isFigure && } diff --git a/src/constants.js b/src/constants.js index 7cd4c896..ac050874 100644 --- a/src/constants.js +++ b/src/constants.js @@ -198,6 +198,7 @@ export const FigureDatavis = 'vega' export const FigureRefPrefix = 'figure-' export const CoverRefPrefix = 'cover' export const TableRefPrefix = 'table-' +export const DataTableRefPrefix = 'data-table-' export const QuoteRefPrefix = 'quote-' export const DialogRefPrefix = 'dialog-' export const SoundRefPrefix = 'sound-' @@ -213,6 +214,7 @@ export const AvailableFigureRefPrefixes = [ VideoRefPrefix, GalleryRefPrefix, CoverRefPrefix, + DataTableRefPrefix, ] export const AvailableRefPrefixes = AvailableFigureRefPrefixes.concat([AnchorRefPrefix]) diff --git a/src/translations.json b/src/translations.json index e3b9be00..629ce592 100644 --- a/src/translations.json +++ b/src/translations.json @@ -249,12 +249,19 @@ "figure": "figure {{ n }}", "sound": "sound {{ n }}", "table": "table {{ n }}", + "data-table": "data table {{ n }}", "yExponent": "Power scale exponent (y axis): {{n}}", "errors": "{{count}} errors", "maxAllowedCharactersWithCount": "Max {{count}} characters allowed", "currentCharactersWithCount": "(now: {{count}})", "itemIdx": "(item #{{idx}})", - "articles": "{{ n }} articles" + "articles": "{{ n }} articles", + "datatableRows": "{{ total }} rows", + "datatableRowsFiltered": "{{ n }} / {{ total }} rows (filtered)", + "datatableShowAllRows": "show all rows ({{ n }})", + "datatableShowRows": "show {{ n }} rows", + "datatablePaginatedRows": " of {{ total }} rows", + "datatablePaginatedRowsFiltered": " of {{ n }} rows (filtered)" }, "missingStepsWithCount": "1 step missing", "missingStepsWithCount_plural": "{{count}} steps missing", diff --git a/yarn.lock b/yarn.lock index 3856b821..0b668ddb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3588,6 +3588,18 @@ "@tanstack/query-core" "4.29.19" use-sync-external-store "^1.2.0" +"@tanstack/react-table@^8.10.7": + version "8.10.7" + resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.10.7.tgz#733f4bee8cf5aa19582f944dd0fd3224b21e8c94" + integrity sha512-bXhjA7xsTcsW8JPTTYlUg/FuBpn8MNjiEPhkNhIGCUR6iRQM2+WEco4OBpvDeVcR9SE+bmWLzdfiY7bCbCSVuA== + dependencies: + "@tanstack/table-core" "8.10.7" + +"@tanstack/table-core@8.10.7": + version "8.10.7" + resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.10.7.tgz#577e8a635048875de4c9d6d6a3c21d26ff9f9d08" + integrity sha512-KQk5OMg5OH6rmbHZxuNROvdI+hKDIUxANaHlV+dPlNN7ED3qYQ/WkpY2qlXww1SIdeMlkIhpN/2L00rof0fXFw== + "@testing-library/dom@^8.3.0", "@testing-library/dom@^8.5.0": version "8.19.0" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.19.0.tgz#bd3f83c217ebac16694329e413d9ad5fdcfd785f"