mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 09:28:31 +00:00
Internal storage - Pagination for Database table (#5040)
* init * footer component with pagintion ui basic styles * pagination: apply a limit and offset rows through query params * open create row drawer from footer button * border color for dark theme footer button * cleaned * pagination * fixes: input value * moved functions to component level
This commit is contained in:
parent
bfceb37da9
commit
6712bd2fb3
12 changed files with 265 additions and 14 deletions
8
frontend/assets/images/icons/add-row.svg
Normal file
8
frontend/assets/images/icons/add-row.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0.390524 1.21767C0.640573 0.967625 0.979711 0.827148 1.33333 0.827148H10.6667C11.0203 0.827148 11.3594 0.967624 11.6095 1.21767C11.8595 1.46772 12 1.80686 12 2.16048V4.82715C12 5.18077 11.8595 5.51991 11.6095 5.76996C11.3594 6.02001 11.0203 6.16048 10.6667 6.16048H1.33333C0.979711 6.16048 0.640573 6.02001 0.390524 5.76996C0.140476 5.51991 0 5.18077 0 4.82715V2.16048C0 1.80686 0.140476 1.46772 0.390524 1.21767ZM10.6667 2.16048H1.33333L1.33333 4.82715H10.6667V2.16048ZM6 7.49381C6.36819 7.49381 6.66667 7.79229 6.66667 8.16048V8.82715H7.33333C7.70152 8.82715 8 9.12562 8 9.49381C8 9.862 7.70152 10.1605 7.33333 10.1605H6.66667V10.8271C6.66667 11.1953 6.36819 11.4938 6 11.4938C5.63181 11.4938 5.33333 11.1953 5.33333 10.8271V10.1605H4.66667C4.29848 10.1605 4 9.862 4 9.49381C4 9.12562 4.29848 8.82715 4.66667 8.82715H5.33333V8.16048C5.33333 7.79229 5.63181 7.49381 6 7.49381Z"
|
||||
fill="#3E63DD"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
4
frontend/assets/images/icons/chevron-left.svg
Normal file
4
frontend/assets/images/icons/chevron-left.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-chevron-left" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<polyline points="15 6 9 12 15 18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 329 B |
4
frontend/assets/images/icons/chevron-right.svg
Normal file
4
frontend/assets/images/icons/chevron-right.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-chevron-right" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#000000" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<polyline points="9 6 15 12 9 18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 329 B |
|
|
@ -1,13 +1,12 @@
|
|||
import React, { useState, useContext } from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import Drawer from '@/_ui/Drawer';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import CreateRowForm from '../../Forms/RowForm';
|
||||
import { TooljetDatabaseContext } from '../../index';
|
||||
import { tooljetDatabaseService } from '@/_services';
|
||||
|
||||
const CreateRowDrawer = () => {
|
||||
const CreateRowDrawer = ({ isCreateRowDrawerOpen, setIsCreateRowDrawerOpen }) => {
|
||||
const { organizationId, selectedTable, setSelectedTableData } = useContext(TooljetDatabaseContext);
|
||||
const [isCreateRowDrawerOpen, setIsCreateRowDrawerOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
119
frontend/src/TooljetDatabase/Table/Footer.jsx
Normal file
119
frontend/src/TooljetDatabase/Table/Footer.jsx
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Button } from '@/_ui/LeftSidebar';
|
||||
import Select from '@/_ui/Select';
|
||||
import Pagination from './Paginations';
|
||||
|
||||
const Footer = ({ darkMode, openCreateRowDrawer, totalRecords, fetchTableData }) => {
|
||||
const selectOptions = [
|
||||
{ label: '50 records', value: '50 per page' },
|
||||
{ label: '100 records', value: '100 per page' },
|
||||
{ label: '200 records', value: '200 per page' },
|
||||
{ label: '500 records', value: '500 per page' },
|
||||
{ label: '1000 records', value: '1000 per page' },
|
||||
];
|
||||
|
||||
const RecordEnum = Object.freeze({
|
||||
'50 per page': 50,
|
||||
'100 per page': 100,
|
||||
'200 per page': 200,
|
||||
'500 per page': 500,
|
||||
'1000 per page': 1000,
|
||||
});
|
||||
|
||||
const [selectedOption, setSelectedOption] = useState('50 per page');
|
||||
const [pageCount, setPageCount] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(RecordEnum[selectedOption]);
|
||||
|
||||
const handleSelectChange = (value) => {
|
||||
setSelectedOption(value);
|
||||
setPageSize(RecordEnum[value]);
|
||||
|
||||
setPageCount(1);
|
||||
fetchTableData(`?limit=${RecordEnum[value]}&offset=0`, RecordEnum[value], 1);
|
||||
};
|
||||
|
||||
const handlePageCountChange = (value) => {
|
||||
setPageCount(value);
|
||||
|
||||
const limit = RecordEnum[selectedOption];
|
||||
const offset = value === 1 ? 0 : (value - 1) * RecordEnum[selectedOption];
|
||||
|
||||
fetchTableData(`?limit=${limit}&offset=${offset}`, limit, value);
|
||||
};
|
||||
|
||||
const gotoNextPage = (fromInput = false, value = null) => {
|
||||
if (fromInput && value) {
|
||||
return handlePageCountChange(value);
|
||||
}
|
||||
|
||||
setPageCount((prev) => {
|
||||
return prev + 1;
|
||||
});
|
||||
|
||||
const limit = RecordEnum[selectedOption];
|
||||
const offset = pageCount * RecordEnum[selectedOption];
|
||||
|
||||
fetchTableData(`?limit=${limit}&offset=${offset}`, limit, pageCount + 1);
|
||||
};
|
||||
|
||||
const gotoPreviousPage = () => {
|
||||
setPageCount((prev) => {
|
||||
if (prev - 1 < 1) {
|
||||
return prev;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
|
||||
const limit = RecordEnum[selectedOption];
|
||||
const offset = (pageCount - 2) * RecordEnum[selectedOption];
|
||||
|
||||
fetchTableData(`?limit=${limit}&offset=${offset}`, limit, pageCount - 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="toojet-db-table-footer card-footer d-flex align-items-center jet-table-footer justify-content-center">
|
||||
<div className="table-footer row gx-0">
|
||||
<div className="col-5">
|
||||
<Button
|
||||
onClick={openCreateRowDrawer}
|
||||
darkMode={darkMode}
|
||||
size="sm"
|
||||
styles={{ width: '118px', fontSize: '12px', fontWeight: 700, borderColor: darkMode && 'transparent' }}
|
||||
>
|
||||
<Button.Content title={'Add new row'} iconSrc={'assets/images/icons/add-row.svg'} direction="right" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col d-flex align-items-center justify-content-end">
|
||||
<div className="col">
|
||||
<Pagination
|
||||
darkMode={darkMode}
|
||||
gotoNextPage={gotoNextPage}
|
||||
gotoPreviousPage={gotoPreviousPage}
|
||||
currentPage={pageCount}
|
||||
totalPage={pageSize}
|
||||
/>
|
||||
</div>
|
||||
<div className="col mx-2">
|
||||
<Select
|
||||
className={`${darkMode ? 'select-search-dark' : 'select-search'}`}
|
||||
options={selectOptions}
|
||||
value={selectedOption}
|
||||
search={false}
|
||||
onChange={(value) => handleSelectChange(value)}
|
||||
placeholder={'Select page'}
|
||||
useMenuPortal={false}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-4 mx-2">
|
||||
<span>
|
||||
{pageCount}-{pageSize} of {totalRecords} Records
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
65
frontend/src/TooljetDatabase/Table/Paginations.jsx
Normal file
65
frontend/src/TooljetDatabase/Table/Paginations.jsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Button } from '@/_ui/LeftSidebar';
|
||||
|
||||
const Pagination = ({ darkMode, gotoNextPage, gotoPreviousPage, currentPage, totalPage }) => {
|
||||
const [currentPageNumber, setCurrentPageNumber] = React.useState(currentPage);
|
||||
|
||||
const handleOnChange = (value) => {
|
||||
const parsedValue = parseInt(value, 10);
|
||||
|
||||
if (parsedValue > 0 && parsedValue <= totalPage && parsedValue !== currentPage) {
|
||||
gotoNextPage(true, event.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPageNumber(currentPage);
|
||||
}, [currentPage]);
|
||||
|
||||
return (
|
||||
<div className="tooljet-db-pagination-container d-flex">
|
||||
<Button.UnstyledButton
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
gotoPreviousPage();
|
||||
}}
|
||||
classNames={darkMode ? 'dark' : 'nothing'}
|
||||
styles={{ height: '20px', width: '20px' }}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<Button.Content iconSrc={'assets/images/icons/chevron-left.svg'} />
|
||||
</Button.UnstyledButton>
|
||||
|
||||
<div className="d-flex">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control mx-1"
|
||||
value={currentPageNumber}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleOnChange(event.target.value);
|
||||
}
|
||||
}}
|
||||
onChange={(event) => {
|
||||
setCurrentPageNumber(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<span className="mx-1">/ {totalPage}</span>
|
||||
</div>
|
||||
|
||||
<Button.UnstyledButton
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
gotoNextPage();
|
||||
}}
|
||||
classNames={darkMode && 'dark'}
|
||||
styles={{ height: '20px', width: '20px' }}
|
||||
disabled={currentPage === totalPage}
|
||||
>
|
||||
<Button.Content iconSrc={'assets/images/icons/chevron-right.svg'} />
|
||||
</Button.UnstyledButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable react/jsx-key */
|
||||
import React, { useEffect, useState, useContext } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { useTable, useRowSelect } from 'react-table';
|
||||
|
|
@ -11,14 +10,17 @@ import Skeleton from 'react-loading-skeleton';
|
|||
import IndeterminateCheckbox from '@/_ui/IndeterminateCheckbox';
|
||||
import Drawer from '@/_ui/Drawer';
|
||||
import EditColumnForm from '../Forms/ColumnForm';
|
||||
import TableFooter from './Footer';
|
||||
|
||||
const Table = () => {
|
||||
const Table = ({ openCreateRowDrawer }) => {
|
||||
const { organizationId, columns, selectedTable, selectedTableData, setSelectedTableData, setColumns } =
|
||||
useContext(TooljetDatabaseContext);
|
||||
const [isEditColumnDrawerOpen, setIsEditColumnDrawerOpen] = useState(false);
|
||||
const [selectedColumn, setSelectedColumn] = useState();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [totalRecords, setTotalRecords] = useState(0);
|
||||
|
||||
const fetchTableMetadata = () => {
|
||||
tooljetDatabaseService.viewTable(organizationId, selectedTable).then(({ data = [], error }) => {
|
||||
if (error) {
|
||||
|
|
@ -40,15 +42,18 @@ const Table = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const fetchTableData = () => {
|
||||
const fetchTableData = (queryParams = '', pagesize = 50, pagecount = 1) => {
|
||||
const defaultQueryParams = `limit=${pagesize}&offset=${(pagecount - 1) * pagesize}`;
|
||||
const params = queryParams ? queryParams : defaultQueryParams;
|
||||
setLoading(true);
|
||||
tooljetDatabaseService.findOne(organizationId, selectedTable).then(({ data = [], error }) => {
|
||||
tooljetDatabaseService.findOne(organizationId, selectedTable, params).then(({ headers, data = [], error }) => {
|
||||
setLoading(false);
|
||||
if (error) {
|
||||
toast.error(error?.message ?? `Error fetching table "${selectedTable}" data`);
|
||||
return;
|
||||
}
|
||||
|
||||
const totalRecords = headers['content-range'].split('/')[1] || 0;
|
||||
setTotalRecords(totalRecords);
|
||||
setSelectedTableData(data);
|
||||
});
|
||||
};
|
||||
|
|
@ -58,6 +63,7 @@ const Table = () => {
|
|||
fetchTableData();
|
||||
fetchTableMetadata();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedTable]);
|
||||
|
||||
const tableData = React.useMemo(
|
||||
|
|
@ -214,9 +220,14 @@ const Table = () => {
|
|||
prepareRow(row);
|
||||
return (
|
||||
<tr {...row.getRowProps()} key={index}>
|
||||
{row.cells.map((cell) => {
|
||||
{row.cells.map((cell, index) => {
|
||||
return (
|
||||
<td title={cell.value || ''} className="table-cell" {...cell.getCellProps()}>
|
||||
<td
|
||||
key={`cell.value-${index}`}
|
||||
title={cell.value || ''}
|
||||
className="table-cell"
|
||||
{...cell.getCellProps()}
|
||||
>
|
||||
{isBoolean(cell?.value) ? cell?.value?.toString() : cell.render('Cell')}
|
||||
</td>
|
||||
);
|
||||
|
|
@ -226,6 +237,12 @@ const Table = () => {
|
|||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<TableFooter
|
||||
darkMode={darkMode}
|
||||
openCreateRowDrawer={openCreateRowDrawer}
|
||||
totalRecords={totalRecords}
|
||||
fetchTableData={fetchTableData}
|
||||
/>
|
||||
</div>
|
||||
<Drawer isOpen={isEditColumnDrawerOpen} onClose={() => setIsEditColumnDrawerOpen(false)} position="right">
|
||||
<EditColumnForm
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useContext, useRef } from 'react';
|
||||
import React, { useState, useContext, useRef } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
|
@ -68,6 +68,7 @@ const TooljetDatabasePage = () => {
|
|||
};
|
||||
|
||||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
const [isCreateRowDrawerOpen, setIsCreateRowDrawerOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="row gx-0">
|
||||
|
|
@ -88,7 +89,10 @@ const TooljetDatabasePage = () => {
|
|||
<>
|
||||
<Filter onClose={handleBuildFilterQuery} />
|
||||
<Sort onClose={handleBuildSortQuery} />
|
||||
<CreateRowDrawer />
|
||||
<CreateRowDrawer
|
||||
isCreateRowDrawerOpen={isCreateRowDrawerOpen}
|
||||
setIsCreateRowDrawerOpen={setIsCreateRowDrawerOpen}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -96,7 +100,7 @@ const TooljetDatabasePage = () => {
|
|||
</div>
|
||||
</div>
|
||||
<div className={cx('col')}>
|
||||
<Table />
|
||||
<Table openCreateRowDrawer={() => setIsCreateRowDrawerOpen(true)} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,14 @@ $btn-dark-color: #FFFFFF;
|
|||
line-height: 20px;
|
||||
}
|
||||
|
||||
.unstyled-button.dark {
|
||||
color: #FFFFFF;
|
||||
|
||||
img {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.page-handle-button-container {
|
||||
border-radius: $base-border-radius;
|
||||
|
|
|
|||
|
|
@ -8488,3 +8488,22 @@ tbody {
|
|||
// ONBOARDING-SELF-HOST STYLES END------->
|
||||
|
||||
//ONBOARD STYLES END---------------------------->>>>>
|
||||
.toojet-db-table-footer {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
width: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.tooljet-db-pagination-container {
|
||||
display: flex;
|
||||
padding: 0px;
|
||||
height: 20px;
|
||||
|
||||
.form-control {
|
||||
padding: 0 4px;
|
||||
width: fit-content;
|
||||
max-width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ export const SelectComponent = ({ options = [], value, onChange, ...restProps })
|
|||
useMenuPortal = true, // todo: deperecate this prop, use menuPortalTarget instead
|
||||
maxMenuHeight = 250,
|
||||
menuPortalTarget = null,
|
||||
menuPlacement = 'auto',
|
||||
} = restProps;
|
||||
|
||||
const customStyles = defaultStyles(darkMode, width, height, styles);
|
||||
|
|
@ -56,7 +57,7 @@ export const SelectComponent = ({ options = [], value, onChange, ...restProps })
|
|||
placeholder={placeholder}
|
||||
styles={customStyles}
|
||||
formatOptionLabel={(option) => renderCustomOption(option)}
|
||||
menuPlacement="auto"
|
||||
menuPlacement={menuPlacement}
|
||||
maxMenuHeight={maxMenuHeight}
|
||||
menuPortalTarget={useMenuPortal ? document.body : menuPortalTarget}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ export class PostgrestProxyService {
|
|||
const authToken = 'Bearer ' + this.signJwtPayload(this.configService.get<string>('PG_USER'));
|
||||
req.headers = {};
|
||||
req.headers['Authorization'] = authToken;
|
||||
req.headers['Prefer'] = 'count=exact'; // To get the total count of records
|
||||
|
||||
res.set('Access-Control-Expose-Headers', 'Content-Range');
|
||||
|
||||
return this.httpProxy(req, res, next);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue