mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-05 06:18:34 +00:00
* csa reducing rerenders * perf optimisation : reducing rerenders for rest of the components * cleanup * fix :: child components exposed variables * fix :: table breaking in subcontainer , review fixes * fix :: modal bug * fix :: clear text not working for textarea
675 lines
22 KiB
JavaScript
675 lines
22 KiB
JavaScript
/* eslint-disable import/no-named-as-default */
|
|
import React, { useCallback, useState, useEffect, useRef } from 'react';
|
|
import { useDrop, useDragLayer } from 'react-dnd';
|
|
import { ItemTypes } from './ItemTypes';
|
|
import { DraggableBox } from './DraggableBox';
|
|
import update from 'immutability-helper';
|
|
const produce = require('immer').default;
|
|
import _ from 'lodash';
|
|
import { componentTypes } from './WidgetManager/components';
|
|
import { addNewWidgetToTheEditor } from '@/_helpers/appUtils';
|
|
import { resolveReferences } from '@/_helpers/utils';
|
|
import { toast } from 'react-hot-toast';
|
|
import { restrictedWidgetsObj } from '@/Editor/WidgetManager/restrictedWidgetsConfig';
|
|
import { useCurrentState } from '@/_stores/currentStateStore';
|
|
import { useAppVersionStore } from '@/_stores/appVersionStore';
|
|
import { shallow } from 'zustand/shallow';
|
|
import { useMounted } from '@/_hooks/use-mount';
|
|
import { useEditorStore } from '@/_stores/editorStore';
|
|
// eslint-disable-next-line import/no-unresolved
|
|
import { diff } from 'deep-object-diff';
|
|
|
|
const NO_OF_GRIDS = 43;
|
|
|
|
export const SubContainer = ({
|
|
mode,
|
|
snapToGrid,
|
|
onComponentClick,
|
|
onEvent,
|
|
appDefinition,
|
|
appDefinitionChanged,
|
|
onComponentOptionChanged,
|
|
onComponentOptionsChanged,
|
|
appLoading,
|
|
zoomLevel,
|
|
parent,
|
|
parentRef,
|
|
setSelectedComponent,
|
|
deviceWindowWidth,
|
|
selectedComponent,
|
|
currentLayout,
|
|
removeComponent,
|
|
darkMode,
|
|
containerCanvasWidth,
|
|
readOnly,
|
|
customResolvables,
|
|
parentComponent,
|
|
onComponentHover,
|
|
hoveredComponent,
|
|
sideBarDebugger,
|
|
onOptionChange,
|
|
exposedVariables,
|
|
addDefaultChildren = false,
|
|
height = '100%',
|
|
currentPageId,
|
|
childComponents = null,
|
|
listmode = null,
|
|
columns = 1,
|
|
}) => {
|
|
//Todo add custom resolve vars for other widgets too
|
|
const mounted = useMounted();
|
|
const widgetResolvables = Object.freeze({
|
|
Listview: 'listItem',
|
|
});
|
|
|
|
const customResolverVariable = widgetResolvables[parentComponent?.component];
|
|
const currentState = useCurrentState();
|
|
const { enableReleasedVersionPopupState, isVersionReleased } = useAppVersionStore(
|
|
(state) => ({
|
|
enableReleasedVersionPopupState: state.actions.enableReleasedVersionPopupState,
|
|
isVersionReleased: state.isVersionReleased,
|
|
}),
|
|
shallow
|
|
);
|
|
|
|
const gridWidth = getContainerCanvasWidth() / NO_OF_GRIDS;
|
|
|
|
const [_containerCanvasWidth, setContainerCanvasWidth] = useState(0);
|
|
useEffect(() => {
|
|
if (parentRef.current) {
|
|
const canvasWidth = getContainerCanvasWidth();
|
|
setContainerCanvasWidth(canvasWidth);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [parentRef, getContainerCanvasWidth(), listmode]);
|
|
|
|
zoomLevel = zoomLevel || 1;
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
const allComponents = appDefinition ? appDefinition.pages[currentPageId].components : {};
|
|
const isParentModal =
|
|
(allComponents[parent]?.component?.component === 'Modal' ||
|
|
allComponents[parent]?.component?.component === 'Form' ||
|
|
allComponents[parent]?.component?.component === 'Container') ??
|
|
false;
|
|
|
|
const getChildWidgets = (components) => {
|
|
let childWidgets = {};
|
|
Object.keys(components).forEach((key) => {
|
|
const componentParent = components[key].component.parent;
|
|
if (componentParent === parent) {
|
|
childWidgets[key] = { ...components[key], component: { ...components[key]['component'], parent } };
|
|
}
|
|
});
|
|
|
|
return childWidgets;
|
|
};
|
|
|
|
const [boxes, setBoxes] = useState(allComponents);
|
|
const [childWidgets, setChildWidgets] = useState([]);
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const [isResizing, setIsResizing] = useState(false);
|
|
// const [subContainerHeight, setSubContainerHeight] = useState('100%'); //used to determine the height of the sub container for modal
|
|
const subContainerHeightRef = useRef(height ?? '100%');
|
|
|
|
useEffect(() => {
|
|
setBoxes(allComponents);
|
|
setChildWidgets(() => getChildWidgets(allComponents));
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [allComponents, parent]);
|
|
|
|
useEffect(() => {
|
|
if (mounted) {
|
|
//find children with parent prop
|
|
const children = Object.keys(allComponents).filter((key) => {
|
|
if (key === parent) return false;
|
|
return allComponents[key].parent === parent;
|
|
});
|
|
|
|
if (children.length === 0 && addDefaultChildren === true) {
|
|
const defaultChildren = _.cloneDeep(parentComponent)['defaultChildren'];
|
|
const childrenBoxes = {};
|
|
const parentId =
|
|
parentComponent.component !== 'Tabs'
|
|
? parentRef.current.id
|
|
: parentRef.current.id?.substring(0, parentRef.current.id.lastIndexOf('-'));
|
|
|
|
const _allComponents = JSON.parse(JSON.stringify(allComponents));
|
|
|
|
defaultChildren.forEach((child) => {
|
|
const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles } = child;
|
|
|
|
const componentMeta = _.cloneDeep(componentTypes.find((component) => component.component === componentName));
|
|
const componentData = JSON.parse(JSON.stringify(componentMeta));
|
|
|
|
const width = layout.width ? layout.width : (componentMeta.defaultSize.width * 100) / NO_OF_GRIDS;
|
|
const height = layout.height ? layout.height : componentMeta.defaultSize.height;
|
|
const newComponentDefinition = {
|
|
...componentData.definition.properties,
|
|
};
|
|
|
|
if (_.isArray(properties) && properties.length > 0) {
|
|
properties.forEach((prop) => {
|
|
const accessor = customResolverVariable
|
|
? `{{${customResolverVariable}.${accessorKey}}}`
|
|
: defaultValue[prop] || '';
|
|
|
|
_.set(newComponentDefinition, prop, {
|
|
value: accessor,
|
|
});
|
|
});
|
|
_.set(componentData, 'definition.properties', newComponentDefinition);
|
|
}
|
|
|
|
if (_.isArray(styles) && styles.length > 0) {
|
|
styles.forEach((prop) => {
|
|
const accessor = customResolverVariable
|
|
? `{{${customResolverVariable}.${accessorKey}}}`
|
|
: defaultValue[prop] || '';
|
|
|
|
_.set(newComponentDefinition, prop, {
|
|
value: accessor,
|
|
});
|
|
});
|
|
_.set(componentData, 'definition.styles', newComponentDefinition);
|
|
}
|
|
|
|
const newComponent = addNewWidgetToTheEditor(
|
|
componentData,
|
|
{},
|
|
{ ..._allComponents, ...childrenBoxes },
|
|
{},
|
|
currentLayout,
|
|
snapToGrid,
|
|
zoomLevel,
|
|
true,
|
|
true
|
|
);
|
|
|
|
_.set(childrenBoxes, newComponent.id, {
|
|
component: {
|
|
...newComponent.component,
|
|
parent: parentComponent.component === 'Tabs' ? parentId + '-' + tab : parentId,
|
|
},
|
|
|
|
layouts: {
|
|
[currentLayout]: {
|
|
...layout,
|
|
width: incrementWidth ? width * incrementWidth : width,
|
|
height: height,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
_allComponents[parentId] = {
|
|
...allComponents[parentId],
|
|
withDefaultChildren: false,
|
|
};
|
|
setBoxes({
|
|
..._allComponents,
|
|
...childrenBoxes,
|
|
});
|
|
}
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [mounted]);
|
|
|
|
const moveBox = useCallback(
|
|
(id, left, top) => {
|
|
setBoxes(
|
|
update(boxes, {
|
|
[id]: {
|
|
$merge: { left, top },
|
|
},
|
|
})
|
|
);
|
|
},
|
|
[boxes]
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (appDefinitionChanged) {
|
|
const newDefinition = {
|
|
...appDefinition,
|
|
pages: {
|
|
...appDefinition.pages,
|
|
[currentPageId]: {
|
|
...appDefinition.pages[currentPageId],
|
|
components: boxes,
|
|
},
|
|
},
|
|
};
|
|
|
|
const oldComponents = appDefinition.pages[currentPageId]?.components ?? {};
|
|
const newComponents = boxes;
|
|
|
|
const componendAdded = Object.keys(newComponents).length > Object.keys(oldComponents).length;
|
|
|
|
const opts = { containerChanges: true };
|
|
|
|
if (componendAdded) {
|
|
opts.componentAdded = true;
|
|
}
|
|
|
|
const shouldUpdate = !_.isEmpty(diff(appDefinition, newDefinition));
|
|
|
|
if (shouldUpdate) {
|
|
appDefinitionChanged(newDefinition, opts);
|
|
}
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [boxes]);
|
|
|
|
const { draggingState } = useDragLayer((monitor) => {
|
|
// TODO: Need to move to a performant version of the block below
|
|
if (monitor.getItem()) {
|
|
if (monitor.getItem().id === undefined) {
|
|
if (parentRef.current) {
|
|
const currentOffset = monitor.getSourceClientOffset();
|
|
if (currentOffset) {
|
|
const canvasBoundingRect = parentRef?.current
|
|
?.getElementsByClassName('real-canvas')[0]
|
|
?.getBoundingClientRect();
|
|
if (!canvasBoundingRect) return { draggingState: false };
|
|
if (
|
|
currentOffset.x > canvasBoundingRect.x &&
|
|
currentOffset.x < canvasBoundingRect.x + canvasBoundingRect.width
|
|
) {
|
|
return { draggingState: true };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (monitor.isDragging() && monitor.getItem().parent) {
|
|
if (monitor.getItem().parent === parent) {
|
|
return { draggingState: true };
|
|
} else {
|
|
return { draggingState: false };
|
|
}
|
|
} else {
|
|
return { draggingState: false };
|
|
}
|
|
});
|
|
|
|
//!Todo: need to check: this never gets called as draggingState is always false
|
|
useEffect(() => {
|
|
setIsDragging(draggingState);
|
|
}, [draggingState]);
|
|
|
|
function convertXToPercentage(x, canvasWidth) {
|
|
return (x * 100) / canvasWidth;
|
|
}
|
|
|
|
const [, drop] = useDrop(
|
|
() => ({
|
|
accept: ItemTypes.BOX,
|
|
drop(item, monitor) {
|
|
const componentMeta = _.cloneDeep(
|
|
componentTypes.find((component) => component.component === item.component.component)
|
|
);
|
|
const canvasBoundingRect = parentRef.current.getElementsByClassName('real-canvas')[0].getBoundingClientRect();
|
|
const parentComp =
|
|
parentComponent?.component === 'Kanban'
|
|
? parent.includes('modal')
|
|
? 'Kanban_popout'
|
|
: 'Kanban_card'
|
|
: parentComponent.component;
|
|
if (!restrictedWidgetsObj[parentComp].includes(componentMeta?.component)) {
|
|
const newComponent = addNewWidgetToTheEditor(
|
|
componentMeta,
|
|
monitor,
|
|
boxes,
|
|
canvasBoundingRect,
|
|
item.currentLayout,
|
|
snapToGrid,
|
|
zoomLevel,
|
|
true
|
|
);
|
|
|
|
setBoxes({
|
|
...boxes,
|
|
[newComponent.id]: {
|
|
component: {
|
|
...newComponent.component,
|
|
parent: parentRef.current.id,
|
|
},
|
|
layouts: {
|
|
...newComponent.layout,
|
|
},
|
|
withDefaultChildren: newComponent.withDefaultChildren,
|
|
},
|
|
});
|
|
|
|
setSelectedComponent(newComponent.id, newComponent.component);
|
|
|
|
return undefined;
|
|
} else {
|
|
toast.error(
|
|
` ${componentMeta?.component} is not compatible as a child component of ${parentComp
|
|
.replace(/_/g, ' ')
|
|
.toLowerCase()}`,
|
|
{
|
|
style: {
|
|
wordBreak: 'break-word',
|
|
},
|
|
}
|
|
);
|
|
}
|
|
},
|
|
}),
|
|
[moveBox]
|
|
);
|
|
|
|
function getContainerCanvasWidth() {
|
|
if (containerCanvasWidth !== undefined) {
|
|
if (listmode == 'grid') return containerCanvasWidth / columns - 2;
|
|
else return containerCanvasWidth - 2;
|
|
}
|
|
|
|
let width = 0;
|
|
if (parentRef.current) {
|
|
const realCanvas = parentRef.current.getElementsByClassName('real-canvas')[0];
|
|
if (realCanvas) {
|
|
const canvasBoundingRect = realCanvas.getBoundingClientRect();
|
|
width = canvasBoundingRect.width;
|
|
}
|
|
}
|
|
return width;
|
|
}
|
|
|
|
function onDragStop(e, componentId, direction, currentLayout) {
|
|
if (isVersionReleased) {
|
|
enableReleasedVersionPopupState();
|
|
return;
|
|
}
|
|
|
|
const canvasWidth = getContainerCanvasWidth();
|
|
const nodeBounds = direction.node.getBoundingClientRect();
|
|
|
|
const canvasBounds = parentRef.current.getElementsByClassName('real-canvas')[0].getBoundingClientRect();
|
|
|
|
// Computing the left offset
|
|
const leftOffset = nodeBounds.x - canvasBounds.x;
|
|
const currentLeftOffset = boxes[componentId].layouts[currentLayout].left;
|
|
const leftDiff = currentLeftOffset - convertXToPercentage(leftOffset, canvasWidth);
|
|
|
|
const topDiff = boxes[componentId].layouts[currentLayout].top - (nodeBounds.y - canvasBounds.y);
|
|
|
|
let newBoxes = { ...boxes };
|
|
|
|
const subContainerHeight = canvasBounds.height - 30;
|
|
const selectedComponents = useEditorStore.getState().selectedComponents;
|
|
|
|
if (selectedComponents) {
|
|
for (const selectedComponent of selectedComponents) {
|
|
newBoxes = produce(newBoxes, (draft) => {
|
|
const topOffset = draft[selectedComponent.id].layouts[currentLayout].top;
|
|
const leftOffset = draft[selectedComponent.id].layouts[currentLayout].left;
|
|
|
|
draft[selectedComponent.id].layouts[currentLayout].top = topOffset - topDiff;
|
|
draft[selectedComponent.id].layouts[currentLayout].left = leftOffset - leftDiff;
|
|
});
|
|
|
|
const componentBottom =
|
|
newBoxes[selectedComponent.id].layouts[currentLayout].top +
|
|
newBoxes[selectedComponent.id].layouts[currentLayout].height;
|
|
|
|
if (isParentModal && subContainerHeight <= componentBottom) {
|
|
subContainerHeightRef.current = subContainerHeight + 100;
|
|
}
|
|
}
|
|
}
|
|
|
|
setChildWidgets(() => getChildWidgets(newBoxes));
|
|
setBoxes(newBoxes);
|
|
}
|
|
|
|
function onResizeStop(id, e, direction, ref, d, position) {
|
|
if (isVersionReleased) {
|
|
enableReleasedVersionPopupState();
|
|
return;
|
|
}
|
|
|
|
const deltaWidth = Math.round(d.width / gridWidth) * gridWidth; //rounding of width of element to nearest mulitple of gridWidth
|
|
const deltaHeight = d.height;
|
|
|
|
if (deltaWidth === 0 && deltaHeight === 0) {
|
|
return;
|
|
}
|
|
|
|
let { x, y } = position;
|
|
x = Math.round(x / gridWidth) * gridWidth;
|
|
|
|
const defaultData = {
|
|
top: 100,
|
|
left: 0,
|
|
width: 445,
|
|
height: 500,
|
|
};
|
|
|
|
let { left, top, width, height } = boxes[id]['layouts'][currentLayout] || defaultData;
|
|
|
|
top = y;
|
|
if (deltaWidth !== 0) {
|
|
// onResizeStop is triggered for a single click on the border, therefore this conditional logic
|
|
// should not be removed.
|
|
left = (x * 100) / _containerCanvasWidth;
|
|
}
|
|
|
|
//round the width to nearest multiple of gridwidth before converting to %
|
|
let currentWidth = (_containerCanvasWidth * width) / NO_OF_GRIDS;
|
|
|
|
if (currentWidth > _containerCanvasWidth) {
|
|
currentWidth = _containerCanvasWidth;
|
|
}
|
|
|
|
let newWidth = currentWidth + deltaWidth;
|
|
newWidth = Math.round(newWidth / gridWidth) * gridWidth;
|
|
width = (newWidth * NO_OF_GRIDS) / _containerCanvasWidth;
|
|
|
|
height = height + deltaHeight;
|
|
|
|
let newBoxes = {
|
|
...boxes,
|
|
[id]: {
|
|
...boxes[id],
|
|
layouts: {
|
|
...boxes[id]['layouts'],
|
|
[currentLayout]: {
|
|
...boxes[id]['layouts'][currentLayout],
|
|
width,
|
|
height,
|
|
top,
|
|
left,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
setBoxes(newBoxes);
|
|
}
|
|
|
|
function paramUpdated(id, param, value) {
|
|
if (Object.keys(value).length > 0) {
|
|
setBoxes((boxes) => {
|
|
return update(boxes, {
|
|
[id]: {
|
|
$merge: {
|
|
component: {
|
|
...boxes[id].component,
|
|
definition: {
|
|
...boxes[id].component.definition,
|
|
properties: {
|
|
...boxes[id].component.definition.properties,
|
|
[param]: value,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
const styles = {
|
|
width: '100%',
|
|
height: subContainerHeightRef.current,
|
|
position: 'absolute',
|
|
backgroundSize: `${gridWidth}px 10px`,
|
|
};
|
|
|
|
//check if parent is listview or form return false is so
|
|
const checkParent = (box) => {
|
|
let isListView = false,
|
|
isForm = false;
|
|
try {
|
|
isListView =
|
|
appDefinition.pages[currentPageId].components[box?.component?.parent]?.component?.component === 'Listview';
|
|
isForm = appDefinition.pages[currentPageId].components[box?.component?.parent]?.component?.component === 'Form';
|
|
} catch {
|
|
console.log('error');
|
|
}
|
|
if (!isListView && !isForm) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
function onComponentOptionChangedForSubcontainer(component, optionName, value, componentId = '') {
|
|
if (typeof value === 'function' && _.findKey(exposedVariables, optionName)) {
|
|
return Promise.resolve();
|
|
}
|
|
onOptionChange && onOptionChange({ component, optionName, value, componentId });
|
|
return onComponentOptionChanged(component, optionName, value);
|
|
}
|
|
|
|
function customRemoveComponent(component) {
|
|
removeComponent(component);
|
|
}
|
|
|
|
function checkParentVisibility() {
|
|
let elem = parentRef.current;
|
|
if (elem?.className === 'tab-content') {
|
|
elem = parentRef.current?.parentElement;
|
|
}
|
|
if (elem?.style?.display !== 'none') return true;
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
ref={drop}
|
|
style={styles}
|
|
id={`canvas-${parent}`}
|
|
className={`real-canvas ${(isDragging || isResizing) && !readOnly ? 'show-grid' : ''}`}
|
|
>
|
|
{checkParentVisibility() &&
|
|
Object.keys(childWidgets).map((key) => {
|
|
const addDefaultChildren = childWidgets[key]['withDefaultChildren'] || false;
|
|
const box = childWidgets[key];
|
|
|
|
const canShowInCurrentLayout =
|
|
box.component.definition.others[currentLayout === 'mobile' ? 'showOnMobile' : 'showOnDesktop'].value;
|
|
|
|
if (box.component.parent && resolveReferences(canShowInCurrentLayout, currentState)) {
|
|
return (
|
|
<DraggableBox
|
|
onComponentClick={onComponentClick}
|
|
onEvent={onEvent}
|
|
onComponentOptionChanged={
|
|
checkParent(box)
|
|
? onComponentOptionChangedForSubcontainer
|
|
: (component, optionName, value, componentId = '') => {
|
|
if (typeof value === 'function' && _.findKey(exposedVariables, optionName)) {
|
|
return Promise.resolve();
|
|
}
|
|
onOptionChange && onOptionChange({ component, optionName, value, componentId });
|
|
}
|
|
}
|
|
onComponentOptionsChanged={(component, variableSet, id) => {
|
|
checkParent(box)
|
|
? onComponentOptionsChanged(component, variableSet)
|
|
: variableSet.map((item) => {
|
|
onOptionChange &&
|
|
onOptionChange({
|
|
component,
|
|
optionName: item[0],
|
|
value: item[1],
|
|
componentId: id,
|
|
});
|
|
});
|
|
}}
|
|
key={key}
|
|
onResizeStop={onResizeStop}
|
|
onDragStop={onDragStop}
|
|
paramUpdated={paramUpdated}
|
|
id={key}
|
|
allComponents={allComponents}
|
|
{...childWidgets[key]}
|
|
mode={mode}
|
|
resizingStatusChanged={(status) => setIsResizing(status)}
|
|
draggingStatusChanged={(status) => setIsDragging(status)}
|
|
inCanvas={true}
|
|
zoomLevel={zoomLevel}
|
|
setSelectedComponent={setSelectedComponent}
|
|
selectedComponent={selectedComponent}
|
|
deviceWindowWidth={deviceWindowWidth}
|
|
removeComponent={customRemoveComponent}
|
|
canvasWidth={_containerCanvasWidth}
|
|
readOnly={readOnly}
|
|
darkMode={darkMode}
|
|
customResolvables={customResolvables}
|
|
onComponentHover={onComponentHover}
|
|
hoveredComponent={hoveredComponent}
|
|
parentId={parentComponent?.name}
|
|
parent={parent}
|
|
sideBarDebugger={sideBarDebugger}
|
|
exposedVariables={exposedVariables ?? {}}
|
|
childComponents={childComponents[key]}
|
|
containerProps={{
|
|
mode,
|
|
snapToGrid,
|
|
onComponentClick,
|
|
onEvent,
|
|
appDefinition,
|
|
appDefinitionChanged,
|
|
currentState,
|
|
onComponentOptionChanged,
|
|
onComponentOptionsChanged,
|
|
appLoading,
|
|
zoomLevel,
|
|
setSelectedComponent,
|
|
removeComponent,
|
|
currentLayout,
|
|
deviceWindowWidth,
|
|
darkMode,
|
|
readOnly,
|
|
onComponentHover,
|
|
hoveredComponent,
|
|
sideBarDebugger,
|
|
addDefaultChildren,
|
|
currentPageId,
|
|
childComponents,
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
})}
|
|
{appLoading && (
|
|
<div className="mx-auto mt-5 w-50 p-5">
|
|
<center>
|
|
<div className="progress progress-sm w-50">
|
|
<div className="progress-bar progress-bar-indeterminate"></div>
|
|
</div>
|
|
</center>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|