feat: Support row virtualization using react-window in Table widget (#16872)

This commit is contained in:
balajisoundar 2022-10-06 15:02:09 +05:30 committed by GitHub
parent 4bae04ea64
commit 81458035d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 447 additions and 108 deletions

View File

@ -0,0 +1,73 @@
import { ObjectsRegistry } from "../../../../../support/Objects/Registry";
const PropertyPane = ObjectsRegistry.PropertyPane;
const totalRows = 100;
describe("Table Widget Virtualized Row", function() {
before(() => {
cy.dragAndDropToCanvas("tablewidgetv2", { x: 300, y: 600 });
const row = {
step: "#3",
task: "Bind the query using => fetch_users.data",
status: "--",
action: "",
};
const rows = new Array(totalRows).fill("").map((d, i) => ({
...row,
step: i,
}));
PropertyPane.UpdatePropertyFieldValue("Table Data", JSON.stringify(rows));
PropertyPane.ToggleOnOrOff("Server Side Pagination", "On");
PropertyPane.ToggleOnOrOff("Show Pagination", "Off");
});
it("1. should check that row is getting rendered", () => {
cy.get(".tr[data-rowindex]").should("exist");
cy.get(".td[data-rowindex]").should("exist");
});
it("2. should check that virtual rows are getting rendered when scrolling through the table", () => {
cy.get(".tr[data-rowindex]").should("not.have.length", totalRows);
cy.get(".tr[data-rowindex='0']").should("exist");
cy.get(".tbody > div").scrollTo("bottom");
cy.wait(500);
cy.get(".tr[data-rowindex='0']").should("not.exist");
cy.get(".tr[data-rowindex='98']").should("exist");
cy.get(".tbody > div").scrollTo("top");
cy.wait(500);
cy.get(".tr[data-rowindex='0']").should("exist");
cy.get(".tr[data-rowindex='98']").should("not.exist");
cy.get(".t--virtual-row").should("exist");
});
it("3. should check that virtual rows feature is turned off when cell wrapping is enabled", () => {
cy.editColumn("step");
cy.wait(500);
PropertyPane.ToggleOnOrOff("Cell Wrapping", "On");
cy.get(".tr[data-rowindex]").should("have.length", totalRows);
cy.get(".tr[data-rowindex='0']").should("exist");
cy.get(".tr[data-rowindex='98']").should("exist");
cy.get(".tbody").scrollTo("bottom");
cy.wait(500);
cy.get(".tr[data-rowindex='0']").should("exist");
cy.get(".tr[data-rowindex='98']").should("exist");
cy.get(".tbody").scrollTo("top");
cy.wait(500);
cy.get(".tr[data-rowindex='0']").should("exist");
cy.get(".tr[data-rowindex='98']").should("exist");
cy.get(".t--virtual-row").should("not.exist");
});
it("4. should check that virtual rows feature is turned off when server side pagination is disabled", () => {
PropertyPane.ToggleOnOrOff("Cell Wrapping", "Off");
PropertyPane.NavigateBackToPropertyPane();
cy.wait(500);
PropertyPane.ToggleOnOrOff("Show Pagination", "On");
cy.wait(500);
PropertyPane.ToggleOnOrOff("Server Side Pagination", "Off");
cy.get(".tr[data-rowindex]").should("have.length", 5);
cy.get(".t--virtual-row").should("not.exist");
});
});

View File

@ -53,6 +53,7 @@
"exceljs-lightweight": "^1.14.0", "exceljs-lightweight": "^1.14.0",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-xml-parser": "^3.17.5", "fast-xml-parser": "^3.17.5",
"fastdom": "^1.0.11",
"flow-bin": "^0.148.0", "flow-bin": "^0.148.0",
"focus-trap-react": "^8.9.2", "focus-trap-react": "^8.9.2",
"fuse.js": "^3.4.5", "fuse.js": "^3.4.5",

View File

@ -464,3 +464,5 @@ export const scrollbarOnHoverCSS = `
} }
} }
`; `;
export const MULTISELECT_CHECKBOX_WIDTH = 40;

View File

@ -6,7 +6,7 @@ import {
useBlockLayout, useBlockLayout,
useResizeColumns, useResizeColumns,
useRowSelect, useRowSelect,
Row, Row as ReactTableRowType,
} from "react-table"; } from "react-table";
import { import {
TableWrapper, TableWrapper,
@ -28,12 +28,10 @@ import { ScrollIndicator } from "design-system";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import { Scrollbars } from "react-custom-scrollbars"; import { Scrollbars } from "react-custom-scrollbars";
import { renderEmptyRows } from "./cellComponents/EmptyCell"; import { renderEmptyRows } from "./cellComponents/EmptyCell";
import { import { renderHeaderCheckBoxCell } from "./cellComponents/SelectionCheckboxCell";
renderBodyCheckBoxCell,
renderHeaderCheckBoxCell,
} from "./cellComponents/SelectionCheckboxCell";
import { HeaderCell } from "./cellComponents/HeaderCell"; import { HeaderCell } from "./cellComponents/HeaderCell";
import { EditableCell } from "../constants"; import { EditableCell } from "../constants";
import { TableBody } from "./TableBody";
interface TableProps { interface TableProps {
width: number; width: number;
@ -68,7 +66,7 @@ interface TableProps {
enableDrag: () => void; enableDrag: () => void;
toggleAllRowSelect: ( toggleAllRowSelect: (
isSelect: boolean, isSelect: boolean,
pageData: Row<Record<string, unknown>>[], pageData: ReactTableRowType<Record<string, unknown>>[],
) => void; ) => void;
triggerRowSelection: boolean; triggerRowSelection: boolean;
searchTableData: (searchKey: any) => void; searchTableData: (searchKey: any) => void;
@ -85,6 +83,7 @@ interface TableProps {
boxShadow?: string; boxShadow?: string;
onBulkEditDiscard: () => void; onBulkEditDiscard: () => void;
onBulkEditSave: () => void; onBulkEditSave: () => void;
primaryColumnId?: string;
} }
const defaultColumn = { const defaultColumn = {
@ -185,7 +184,6 @@ export function Table(props: TableProps) {
endIndex = props.data.length; endIndex = props.data.length;
} }
const subPage = page.slice(startIndex, endIndex); const subPage = page.slice(startIndex, endIndex);
const selectedRowIndex = props.selectedRowIndex;
const selectedRowIndices = props.selectedRowIndices || []; const selectedRowIndices = props.selectedRowIndices || [];
const tableSizes = TABLE_SIZES[props.compactMode || CompactModeTypes.DEFAULT]; const tableSizes = TABLE_SIZES[props.compactMode || CompactModeTypes.DEFAULT];
const tableWrapperRef = useRef<HTMLDivElement | null>(null); const tableWrapperRef = useRef<HTMLDivElement | null>(null);
@ -228,6 +226,12 @@ export function Table(props: TableProps) {
[props.width], [props.width],
); );
const shouldUseVirtual =
props.serverSidePaginationEnabled &&
!props.columns.some(
(column) => !!column.columnProperties.allowCellWrapping,
);
return ( return (
<TableWrapper <TableWrapper
accentColor={props.accentColor} accentColor={props.accentColor}
@ -354,73 +358,32 @@ export function Table(props: TableProps) {
props.columns, props.columns,
props.width, props.width,
subPage, subPage,
prepareRow,
props.multiRowSelection, props.multiRowSelection,
props.accentColor, props.accentColor,
props.borderRadius, props.borderRadius,
{},
prepareRow,
)} )}
</div> </div>
<div <TableBody
{...getTableBodyProps()} accentColor={props.accentColor}
className={`tbody ${ borderRadius={props.borderRadius}
props.pageSize > subPage.length ? "no-scroll" : "" columns={props.columns}
}`} getTableBodyProps={getTableBodyProps}
height={props.height}
multiRowSelection={!!props.multiRowSelection}
pageSize={props.pageSize}
prepareRow={prepareRow}
primaryColumnId={props.primaryColumnId}
ref={tableBodyRef} ref={tableBodyRef}
> rows={subPage}
{subPage.map((row, rowIndex) => { selectTableRow={props.selectTableRow}
prepareRow(row); selectedRowIndex={props.selectedRowIndex}
const rowProps = { selectedRowIndices={props.selectedRowIndices}
...row.getRowProps(), tableSizes={tableSizes}
style: { display: "flex" }, useVirtual={shouldUseVirtual}
}; width={props.width}
const isRowSelected = props.multiRowSelection />
? selectedRowIndices.includes(row.index)
: row.index === selectedRowIndex;
return (
<div
{...rowProps}
className={"tr" + `${isRowSelected ? " selected-row" : ""}`}
key={rowIndex}
onClick={(e) => {
row.toggleRowSelected();
props.selectTableRow(row);
e.stopPropagation();
}}
>
{props.multiRowSelection &&
renderBodyCheckBoxCell(
isRowSelected,
props.accentColor,
props.borderRadius,
)}
{row.cells.map((cell, cellIndex) => {
return (
<div
{...cell.getCellProps()}
className="td"
data-colindex={cellIndex}
data-rowindex={rowIndex}
key={cellIndex}
>
{cell.render("Cell")}
</div>
);
})}
</div>
);
})}
{props.pageSize > subPage.length &&
renderEmptyRows(
props.pageSize - subPage.length,
props.columns,
props.width,
subPage,
prepareRow,
props.multiRowSelection,
props.accentColor,
props.borderRadius,
)}
</div>
</div> </div>
</Scrollbars> </Scrollbars>
</div> </div>

View File

@ -0,0 +1,129 @@
import React, { CSSProperties, Key, useContext } from "react";
import { Row as ReactTableRowType } from "react-table";
import { ListChildComponentProps } from "react-window";
import { BodyContext } from ".";
import { renderEmptyRows } from "../cellComponents/EmptyCell";
import { renderBodyCheckBoxCell } from "../cellComponents/SelectionCheckboxCell";
type RowType = {
className?: string;
index: number;
row: ReactTableRowType<Record<string, unknown>>;
style?: ListChildComponentProps["style"];
};
export function Row(props: RowType) {
const {
accentColor,
borderRadius,
multiRowSelection,
prepareRow,
primaryColumnId,
selectedRowIndex,
selectedRowIndices,
selectTableRow,
} = useContext(BodyContext);
prepareRow?.(props.row);
const rowProps = {
...props.row.getRowProps(),
style: {
display: "flex",
...(props.style || {}),
},
};
const isRowSelected = multiRowSelection
? selectedRowIndices.includes(props.row.index)
: props.row.index === selectedRowIndex;
const key =
(primaryColumnId && (props.row.original[primaryColumnId] as Key)) ||
props.index;
return (
<div
{...rowProps}
className={`tr ${isRowSelected ? "selected-row" : ""} ${props.className ||
""}`}
data-rowindex={props.index}
key={key}
onClick={(e) => {
props.row.toggleRowSelected();
selectTableRow?.(props.row);
e.stopPropagation();
}}
role="button"
>
{multiRowSelection &&
renderBodyCheckBoxCell(isRowSelected, accentColor, borderRadius)}
{props.row.cells.map((cell, cellIndex) => {
return (
<div
{...cell.getCellProps()}
className="td"
data-colindex={cellIndex}
data-rowindex={props.index}
key={cellIndex}
>
{cell.render("Cell")}
</div>
);
})}
</div>
);
}
export const EmptyRows = (props: {
style?: CSSProperties;
rowCount: number;
}) => {
const {
accentColor,
borderRadius,
columns,
multiRowSelection,
prepareRow,
rows,
width,
} = useContext(BodyContext);
return (
<>
{renderEmptyRows(
props.rowCount,
columns,
width,
rows,
multiRowSelection,
accentColor,
borderRadius,
props.style,
prepareRow,
)}
</>
);
};
export const EmptyRow = (props: { style?: CSSProperties }) => {
const {
accentColor,
borderRadius,
columns,
multiRowSelection,
prepareRow,
rows,
width,
} = useContext(BodyContext);
return renderEmptyRows(
1,
columns,
width,
rows,
multiRowSelection,
accentColor,
borderRadius,
props.style,
prepareRow,
)?.[0];
};

View File

@ -0,0 +1,155 @@
import React, { Ref } from "react";
import {
Row as ReactTableRowType,
TableBodyPropGetter,
TableBodyProps,
} from "react-table";
import { FixedSizeList, ListChildComponentProps, areEqual } from "react-window";
import { WIDGET_PADDING } from "constants/WidgetConstants";
import { EmptyRows, EmptyRow, Row } from "./Row";
import { ReactTableColumnProps, TableSizes } from "../Constants";
type BodyContextType = {
accentColor: string;
borderRadius: string;
multiRowSelection: boolean;
prepareRow?(row: ReactTableRowType<Record<string, unknown>>): void;
selectTableRow?: (row: {
original: Record<string, unknown>;
index: number;
}) => void;
selectedRowIndex: number;
selectedRowIndices: number[];
columns: ReactTableColumnProps[];
width: number;
rows: ReactTableRowType<Record<string, unknown>>[];
primaryColumnId?: string;
};
export const BodyContext = React.createContext<BodyContextType>({
accentColor: "",
borderRadius: "",
multiRowSelection: false,
selectedRowIndex: -1,
selectedRowIndices: [],
columns: [],
width: 0,
rows: [],
primaryColumnId: "",
});
const rowRenderer = React.memo((rowProps: ListChildComponentProps) => {
const { data, index, style } = rowProps;
if (index < data.length) {
const row = data[index];
return (
<Row
className="t--virtual-row"
index={index}
key={index}
row={row}
style={style}
/>
);
} else {
return <EmptyRow style={style} />;
}
}, areEqual);
type BodyPropsType = {
getTableBodyProps(
propGetter?: TableBodyPropGetter<Record<string, unknown>> | undefined,
): TableBodyProps;
pageSize: number;
rows: ReactTableRowType<Record<string, unknown>>[];
height: number;
tableSizes: TableSizes;
};
const TableVirtualBodyComponent = React.forwardRef(
(props: BodyPropsType, ref: Ref<HTMLDivElement>) => {
return (
<div {...props.getTableBodyProps()} className="tbody no-scroll">
<FixedSizeList
height={
props.height -
props.tableSizes.TABLE_HEADER_HEIGHT -
props.tableSizes.COLUMN_HEADER_HEIGHT -
2 * WIDGET_PADDING // Top and bottom padding
}
itemCount={Math.max(props.rows.length, props.pageSize)}
itemData={props.rows}
itemSize={props.tableSizes.ROW_HEIGHT}
outerRef={ref}
width={`calc(100% + ${2 * WIDGET_PADDING}px`}
>
{rowRenderer}
</FixedSizeList>
</div>
);
},
);
const TableBodyComponent = React.forwardRef(
(props: BodyPropsType, ref: Ref<HTMLDivElement>) => {
return (
<div {...props.getTableBodyProps()} className="tbody" ref={ref}>
{props.rows.map((row, index) => {
return <Row index={index} key={index} row={row} />;
})}
{props.pageSize > props.rows.length && (
<EmptyRows rowCount={props.pageSize - props.rows.length} />
)}
</div>
);
},
);
export const TableBody = React.forwardRef(
(
props: BodyPropsType & BodyContextType & { useVirtual: boolean },
ref: Ref<HTMLDivElement>,
) => {
const {
accentColor,
borderRadius,
columns,
multiRowSelection,
prepareRow,
primaryColumnId,
rows,
selectedRowIndex,
selectedRowIndices,
selectTableRow,
useVirtual,
width,
...restOfProps
} = props;
return (
<BodyContext.Provider
value={{
accentColor,
borderRadius,
multiRowSelection,
prepareRow,
primaryColumnId,
selectTableRow,
selectedRowIndex,
selectedRowIndices,
columns,
width,
rows,
}}
>
{useVirtual ? (
<TableVirtualBodyComponent ref={ref} rows={rows} {...restOfProps} />
) : (
<TableBodyComponent ref={ref} rows={rows} {...restOfProps} />
)}
</BodyContext.Provider>
);
},
);

View File

@ -11,6 +11,7 @@ import {
CellAlignment, CellAlignment,
VerticalAlignment, VerticalAlignment,
scrollbarOnHoverCSS, scrollbarOnHoverCSS,
MULTISELECT_CHECKBOX_WIDTH,
} from "./Constants"; } from "./Constants";
import { Colors, Color } from "constants/Colors"; import { Colors, Color } from "constants/Colors";
import { hideScrollbar, invisible } from "constants/DefaultTheme"; import { hideScrollbar, invisible } from "constants/DefaultTheme";
@ -81,6 +82,9 @@ export const TableWrapper = styled.div<{
overflow-y: auto; overflow-y: auto;
${hideScrollbar}; ${hideScrollbar};
} }
.tbody.no-scroll {
overflow: hidden;
}
.tr { .tr {
overflow: hidden; overflow: hidden;
cursor: ${(props) => props.triggerRowSelection && "pointer"}; cursor: ${(props) => props.triggerRowSelection && "pointer"};
@ -429,11 +433,11 @@ export const CellWrapper = styled.div<{
export const CellCheckboxWrapper = styled(CellWrapper)<{ export const CellCheckboxWrapper = styled(CellWrapper)<{
isChecked?: boolean; isChecked?: boolean;
accentColor: string; accentColor?: string;
borderRadius: string; borderRadius?: string;
}>` }>`
justify-content: center; justify-content: center;
width: 40px; width: ${MULTISELECT_CHECKBOX_WIDTH}px;
height: auto; height: auto;
& > div { & > div {
border-radius: ${({ borderRadius }) => borderRadius}; border-radius: ${({ borderRadius }) => borderRadius};

View File

@ -39,15 +39,14 @@ function useToolTip(
const [showTooltip, updateToolTip] = useState(false); const [showTooltip, updateToolTip] = useState(false);
useEffect(() => { useEffect(() => {
requestAnimationFrame(() => {
const element = ref.current?.querySelector("div") as HTMLDivElement; const element = ref.current?.querySelector("div") as HTMLDivElement;
if (element && element.offsetWidth < element.scrollWidth) { if (element && element.offsetWidth < element.scrollWidth) {
updateToolTip(true); updateToolTip(true);
} else { } else {
updateToolTip(false); updateToolTip(false);
} }
}); }, [children]);
}, [children, ref.current]);
return showTooltip && children ? ( return showTooltip && children ? (
<Tooltip <Tooltip

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { CSSProperties } from "react";
import { Cell, Row } from "react-table"; import { Cell, Row } from "react-table";
import { ReactTableColumnProps } from "../Constants"; import { ReactTableColumnProps } from "../Constants";
import { EmptyCell, EmptyRow } from "../TableStyledWrappers"; import { EmptyCell, EmptyRow } from "../TableStyledWrappers";
@ -9,10 +9,11 @@ export const renderEmptyRows = (
columns: ReactTableColumnProps[], columns: ReactTableColumnProps[],
tableWidth: number, tableWidth: number,
page: Row<Record<string, unknown>>[], page: Row<Record<string, unknown>>[],
prepareRow: (row: Row<Record<string, unknown>>) => void,
multiRowSelection = false, multiRowSelection = false,
accentColor: string, accentColor: string,
borderRadius: string, borderRadius: string,
style?: CSSProperties,
prepareRow?: (row: Row<Record<string, unknown>>) => void,
) => { ) => {
const rows: string[] = new Array(rowCount).fill(""); const rows: string[] = new Array(rowCount).fill("");
@ -20,10 +21,13 @@ export const renderEmptyRows = (
const row = page[0]; const row = page[0];
return rows.map((item: string, index: number) => { return rows.map((item: string, index: number) => {
prepareRow(row); prepareRow?.(row);
const rowProps = { const rowProps = {
...row.getRowProps(), ...row.getRowProps(),
style: { display: "flex" }, style: {
display: "flex",
...style,
},
}; };
return ( return (
<div {...rowProps} className="tr" key={index}> <div {...rowProps} className="tr" key={index}>
@ -41,26 +45,18 @@ export const renderEmptyRows = (
? columns ? columns
: new Array(3).fill({ width: tableWidth / 3, isHidden: false }); : new Array(3).fill({ width: tableWidth / 3, isHidden: false });
return rows.map((row: string, index: number) => {
return ( return (
<> <EmptyRow className="tr" key={index} style={style}>
{rows.map((row: string, index: number) => {
return (
<EmptyRow className="tr" key={index}>
{multiRowSelection && {multiRowSelection &&
renderBodyCheckBoxCell(false, accentColor, borderRadius)} renderBodyCheckBoxCell(false, accentColor, borderRadius)}
{tableColumns.map((column: any, colIndex: number) => { {tableColumns.map((column: any, colIndex: number) => {
return ( return (
<EmptyCell <EmptyCell className="td" key={colIndex} width={column.width} />
className="td"
key={colIndex}
width={column.width}
/>
); );
})} })}
</EmptyRow> </EmptyRow>
); );
})} });
</>
);
} }
}; };

View File

@ -7,8 +7,8 @@ import { CheckboxState } from "../Constants";
export const renderBodyCheckBoxCell = ( export const renderBodyCheckBoxCell = (
isChecked: boolean, isChecked: boolean,
accentColor: string, accentColor?: string,
borderRadius: string, borderRadius?: string,
) => ( ) => (
<CellCheckboxWrapper <CellCheckboxWrapper
accentColor={accentColor} accentColor={accentColor}

View File

@ -83,6 +83,7 @@ interface ReactTableComponentProps {
borderRadius: string; borderRadius: string;
boxShadow?: string; boxShadow?: string;
isEditableCellValid?: boolean; isEditableCellValid?: boolean;
primaryColumnId?: string;
} }
function ReactTableComponent(props: ReactTableComponentProps) { function ReactTableComponent(props: ReactTableComponentProps) {
@ -113,6 +114,7 @@ function ReactTableComponent(props: ReactTableComponentProps) {
pageNo, pageNo,
pageSize, pageSize,
prevPageClick, prevPageClick,
primaryColumnId,
searchKey, searchKey,
searchTableData, searchTableData,
selectAllRow, selectAllRow,
@ -293,6 +295,7 @@ function ReactTableComponent(props: ReactTableComponentProps) {
pageNo={pageNo - 1} pageNo={pageNo - 1}
pageSize={pageSize || 1} pageSize={pageSize || 1}
prevPageClick={prevPageClick} prevPageClick={prevPageClick}
primaryColumnId={primaryColumnId}
searchKey={searchKey} searchKey={searchKey}
searchTableData={searchTableData} searchTableData={searchTableData}
selectTableRow={selectTableRow} selectTableRow={selectTableRow}
@ -354,6 +357,7 @@ export default React.memo(ReactTableComponent, (prev, next) => {
// and we are not changing the columns manually. // and we are not changing the columns manually.
JSON.stringify(prev.columns) === JSON.stringify(next.columns) && JSON.stringify(prev.columns) === JSON.stringify(next.columns) &&
equal(prev.editableCell, next.editableCell) && equal(prev.editableCell, next.editableCell) &&
prev.isEditableCellValid === next.isEditableCellValid prev.isEditableCellValid === next.isEditableCellValid &&
prev.primaryColumnId === next.primaryColumnId
); );
}); });

View File

@ -858,6 +858,7 @@ class TableWidgetV2 extends BaseWidget<TableWidgetProps, WidgetState> {
isVisibleHeaderOptions ? Math.max(1, pageSize) : pageSize + 1 isVisibleHeaderOptions ? Math.max(1, pageSize) : pageSize + 1
} }
prevPageClick={this.handlePrevPageClick} prevPageClick={this.handlePrevPageClick}
primaryColumnId={this.props.primaryColumnId}
searchKey={this.props.searchText} searchKey={this.props.searchText}
searchTableData={this.handleSearchTable} searchTableData={this.handleSearchTable}
selectAllRow={this.handleAllRowSelect} selectAllRow={this.handleAllRowSelect}

View File

@ -7376,6 +7376,13 @@ fast-xml-parser@^3.17.5:
version "3.17.5" version "3.17.5"
resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.17.5.tgz" resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.17.5.tgz"
fastdom@^1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/fastdom/-/fastdom-1.0.11.tgz#f22984f9df6b9a6081e5ce2e49cfb5525daf198a"
integrity sha512-jl9MwXDFxhg354W4E3s1UMsLh3HWFuVMQiRUlXpHckcHRXQvUe76yzBf1Z7b+x5Ci4TUJ1KmynI9alGUXG95IQ==
dependencies:
strictdom "^1.0.1"
fastq@^1.6.0: fastq@^1.6.0:
version "1.13.0" version "1.13.0"
resolved "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz" resolved "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz"
@ -14262,6 +14269,11 @@ strict-event-emitter@^0.2.0:
dependencies: dependencies:
events "^3.3.0" events "^3.3.0"
strictdom@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/strictdom/-/strictdom-1.0.1.tgz#189de91649f73d44d59b8432efa68ef9d2659460"
integrity sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg==
string-argv@^0.3.1: string-argv@^0.3.1:
version "0.3.1" version "0.3.1"
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"