mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-18 05:10:45 +00:00
QQQ-37 checkpoint - cleanup of various kinds on process validation & result
This commit is contained in:
@ -48,15 +48,17 @@ function QProcessSummaryResults({
|
|||||||
qInstance, process, table = null, processValues, step,
|
qInstance, process, table = null, processValues, step,
|
||||||
}: Props): JSX.Element
|
}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
|
const sourceTable = qInstance.tables.get(processValues.sourceTable);
|
||||||
|
|
||||||
const resultValidationList = (
|
const resultValidationList = (
|
||||||
<List sx={{mt: 2}}>
|
<List sx={{mt: 2}}>
|
||||||
{
|
{
|
||||||
processValues?.recordCount && table && (
|
processValues?.recordCount !== undefined && sourceTable && (
|
||||||
<ListItem sx={{my: 2}}>
|
<ListItem sx={{my: 2}}>
|
||||||
<ListItemText primaryTypographyProps={{fontSize: 16}}>
|
<ListItemText primaryTypographyProps={{fontSize: 16}}>
|
||||||
{processValues.recordCount.toLocaleString()}
|
{processValues.recordCount.toLocaleString()}
|
||||||
{" "}
|
{" "}
|
||||||
{table.label}
|
{sourceTable.label}
|
||||||
{" "}
|
{" "}
|
||||||
records were processed.
|
records were processed.
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
@ -65,7 +67,7 @@ function QProcessSummaryResults({
|
|||||||
}
|
}
|
||||||
<List>
|
<List>
|
||||||
{
|
{
|
||||||
processValues.processResults && processValues.processResults.map((processSummaryLine: ProcessSummaryLine, i: number) => (new ProcessSummaryLine(processSummaryLine).getProcessSummaryListItem(i, table, qInstance, true)))
|
processValues.processResults && processValues.processResults.map((processSummaryLine: ProcessSummaryLine, i: number) => (new ProcessSummaryLine(processSummaryLine).getProcessSummaryListItem(i, sourceTable, qInstance, true)))
|
||||||
}
|
}
|
||||||
</List>
|
</List>
|
||||||
</List>
|
</List>
|
||||||
@ -79,8 +81,8 @@ function QProcessSummaryResults({
|
|||||||
<MDBox border="1px solid rgb(70%, 70%, 70%)" borderRadius="lg" p={2} mt={2}>
|
<MDBox border="1px solid rgb(70%, 70%, 70%)" borderRadius="lg" p={2} mt={2}>
|
||||||
<MDBox mt={-5} p={1} sx={{width: "fit-content"}} bgColor="success" borderRadius=".25em" width="initial" color="white">
|
<MDBox mt={-5} p={1} sx={{width: "fit-content"}} bgColor="success" borderRadius=".25em" width="initial" color="white">
|
||||||
<MDBox display="flex" alignItems="center" color="white">
|
<MDBox display="flex" alignItems="center" color="white">
|
||||||
<Icon fontSize="medium" sx={{mr: 1}}>{process.iconName}</Icon>
|
{process.iconName && <Icon fontSize="medium" sx={{mr: 1}}>{process.iconName}</Icon>}
|
||||||
{`${process.label} : ${step.label}`}
|
Process Summary
|
||||||
</MDBox>
|
</MDBox>
|
||||||
</MDBox>
|
</MDBox>
|
||||||
{resultValidationList}
|
{resultValidationList}
|
||||||
|
@ -64,6 +64,7 @@ function QValidationReview({
|
|||||||
}: Props): JSX.Element
|
}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const [previewRecordIndex, setPreviewRecordIndex] = useState(0);
|
const [previewRecordIndex, setPreviewRecordIndex] = useState(0);
|
||||||
|
const sourceTable = qInstance.tables.get(processValues.sourceTable);
|
||||||
|
|
||||||
const updatePreviewRecordIndex = (offset: number) =>
|
const updatePreviewRecordIndex = (offset: number) =>
|
||||||
{
|
{
|
||||||
@ -89,15 +90,44 @@ function QValidationReview({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const buildDoFullValidationRadioListItem = (value: "true" | "false", labelText: string, tooltipHTML: JSX.Element): JSX.Element =>
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// split up the label into words - then we'll display the last word by itself with a non-breaking space, no-wrap-glued to the button. //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const labelWords = labelText.split(" ");
|
||||||
|
const lastWord = labelWords[labelWords.length - 1];
|
||||||
|
labelWords.splice(labelWords.length - 1, 1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem sx={{pl: 2}}>
|
||||||
|
<FormControlLabel
|
||||||
|
value={value}
|
||||||
|
control={<Radio />}
|
||||||
|
label={(
|
||||||
|
<ListItemText primaryTypographyProps={{fontSize: 16, pt: 0.625}}>
|
||||||
|
{`${labelWords.join(" ")} `}
|
||||||
|
<span style={{whiteSpace: "nowrap"}}>
|
||||||
|
{/* eslint-disable-next-line react/jsx-one-expression-per-line */}
|
||||||
|
{lastWord}. <CustomWidthTooltip title={tooltipHTML}>
|
||||||
|
<IconButton sx={{py: 0}}><Icon fontSize="small">info_outlined</Icon></IconButton>
|
||||||
|
{/* eslint-disable-next-line react/jsx-closing-tag-location */}
|
||||||
|
</CustomWidthTooltip>
|
||||||
|
</span>
|
||||||
|
</ListItemText>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const preValidationList = (
|
const preValidationList = (
|
||||||
<List sx={{mt: 2}}>
|
<List sx={{mt: 2}}>
|
||||||
{
|
{
|
||||||
processValues?.recordCount && table && (
|
processValues?.recordCount !== undefined && sourceTable && (
|
||||||
<ListItem sx={{my: 2}}>
|
<ListItem sx={{my: 2}}>
|
||||||
<ListItemText primaryTypographyProps={{fontSize: 16}}>
|
<ListItemText primaryTypographyProps={{fontSize: 16}}>
|
||||||
You selected
|
{`Input: ${processValues.recordCount.toLocaleString()} ${sourceTable?.label} record${processValues.recordCount === 1 ? "" : "s"}.`}
|
||||||
{` ${processValues.recordCount.toLocaleString()} ${table?.label} `}
|
|
||||||
records.
|
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)
|
)
|
||||||
@ -108,59 +138,36 @@ function QValidationReview({
|
|||||||
<ListItem sx={{mb: 1, mt: 6}}>
|
<ListItem sx={{mb: 1, mt: 6}}>
|
||||||
<ListItemText primaryTypographyProps={{fontSize: 16}}>How would you like to proceed?</ListItemText>
|
<ListItemText primaryTypographyProps={{fontSize: 16}}>How would you like to proceed?</ListItemText>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<List>
|
<List className="doFullValidationRadios">
|
||||||
<RadioGroup name="doFullValidation" value={formValues.doFullValidation} onChange={doFullValidationRadioChangedHandler}>
|
<RadioGroup name="doFullValidation" value={formValues.doFullValidation} onChange={doFullValidationRadioChangedHandler}>
|
||||||
<ListItem sx={{pl: 2}}>
|
{buildDoFullValidationRadioListItem(
|
||||||
<FormControlLabel
|
"true",
|
||||||
value="true"
|
"Perform Validation on all records before processing", (
|
||||||
control={<Radio />}
|
<div>
|
||||||
label={(
|
If you choose this option, a Validation step will run on all of the input records.
|
||||||
<ListItemText primaryTypographyProps={{fontSize: 16}}>
|
You will then be told how many can process successfully, and how many have issues.
|
||||||
Perform Validation on all records before processing.
|
<br />
|
||||||
<CustomWidthTooltip
|
<br />
|
||||||
title={(
|
Running this validation may take several minutes, depending on the complexity of the work, and the number of records.
|
||||||
<div>
|
<br />
|
||||||
If you choose this option, a Validation step will run on all of the records that you selected.
|
<br />
|
||||||
You will then be told how many can process successfully, and how many have issues.
|
Choose this option if you want more information about what will happen, and you are willing to wait for that information.
|
||||||
<br />
|
</div>
|
||||||
<br />
|
),
|
||||||
Running this validation may take several minutes, depending on the complexity of the work, and the number of records.
|
)}
|
||||||
<br />
|
|
||||||
<br />
|
{buildDoFullValidationRadioListItem(
|
||||||
Choose this option if you want more information about what will happen, and you are willing to wait for that information.
|
"false",
|
||||||
</div>
|
"Skip Validation. Submit the records for immediate processing", (
|
||||||
)}
|
<div>
|
||||||
>
|
If you choose this option, the records input records will immediately be processed.
|
||||||
<IconButton sx={{py: 0}}><Icon fontSize="small">info_outlined</Icon></IconButton>
|
You will be told how many records were successfully processed, and which ones had issues after the processing is completed.
|
||||||
</CustomWidthTooltip>
|
<br />
|
||||||
</ListItemText>
|
<br />
|
||||||
)}
|
Choose this option if you feel that you do not need this information, or are not willing to wait for it.
|
||||||
/>
|
</div>
|
||||||
</ListItem>
|
),
|
||||||
<ListItem sx={{pl: 2}}>
|
)}
|
||||||
<FormControlLabel
|
|
||||||
value="false"
|
|
||||||
control={<Radio />}
|
|
||||||
label={(
|
|
||||||
<ListItemText primaryTypographyProps={{fontSize: 16}}>
|
|
||||||
Skip Validation. Submit the records for immediate processing.
|
|
||||||
<CustomWidthTooltip
|
|
||||||
title={(
|
|
||||||
<div>
|
|
||||||
If you choose this option, the records you selected will immediately be processed.
|
|
||||||
You will be told how many records were successfully processed, and which ones had issues after the processing is completed.
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
Choose this option if you feel that you do not need this information, or are not willing to wait for it.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<IconButton sx={{py: 0}}><Icon fontSize="small">info_outlined</Icon></IconButton>
|
|
||||||
</CustomWidthTooltip>
|
|
||||||
</ListItemText>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</List>
|
</List>
|
||||||
</>
|
</>
|
||||||
@ -172,11 +179,11 @@ function QValidationReview({
|
|||||||
const postValidationList = (
|
const postValidationList = (
|
||||||
<List sx={{mt: 2}}>
|
<List sx={{mt: 2}}>
|
||||||
{
|
{
|
||||||
processValues?.recordCount && table && (
|
processValues?.recordCount !== undefined && sourceTable && (
|
||||||
<ListItem sx={{my: 2}}>
|
<ListItem sx={{my: 2}}>
|
||||||
<ListItemText primaryTypographyProps={{fontSize: 16}}>
|
<ListItemText primaryTypographyProps={{fontSize: 16}}>
|
||||||
Validation complete on
|
Validation complete on
|
||||||
{` ${processValues.recordCount.toLocaleString()} ${table?.label} `}
|
{` ${processValues.recordCount.toLocaleString()} ${sourceTable?.label} `}
|
||||||
records.
|
records.
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@ -184,7 +191,7 @@ function QValidationReview({
|
|||||||
}
|
}
|
||||||
<List>
|
<List>
|
||||||
{
|
{
|
||||||
processValues.validationSummary && processValues.validationSummary.map((processSummaryLine: ProcessSummaryLine, i: number) => (new ProcessSummaryLine(processSummaryLine).getProcessSummaryListItem(i, table, qInstance)))
|
processValues.validationSummary && processValues.validationSummary.map((processSummaryLine: ProcessSummaryLine, i: number) => (new ProcessSummaryLine(processSummaryLine).getProcessSummaryListItem(i, sourceTable, qInstance)))
|
||||||
}
|
}
|
||||||
</List>
|
</List>
|
||||||
</List>
|
</List>
|
||||||
@ -199,9 +206,9 @@ function QValidationReview({
|
|||||||
<MDTypography color="body" variant="body2" component="div" mb={2}>
|
<MDTypography color="body" variant="body2" component="div" mb={2}>
|
||||||
<MDBox display="flex">
|
<MDBox display="flex">
|
||||||
{
|
{
|
||||||
previewRecords && previewRecords.length > 0 ? (
|
processValues?.previewMessage && previewRecords && previewRecords.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<i>This is a preview of the records that will be created.</i>
|
<i>{processValues?.previewMessage}</i>
|
||||||
<CustomWidthTooltip
|
<CustomWidthTooltip
|
||||||
title={(
|
title={(
|
||||||
<div>
|
<div>
|
||||||
|
@ -21,7 +21,9 @@
|
|||||||
|
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
|
|
||||||
import {CircularProgress, TablePagination} from "@mui/material";
|
import {
|
||||||
|
Button, CircularProgress, Icon, TablePagination,
|
||||||
|
} from "@mui/material";
|
||||||
import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro";
|
import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro";
|
||||||
// formik components
|
// formik components
|
||||||
import {Form, Formik} from "formik";
|
import {Form, Formik} from "formik";
|
||||||
@ -96,8 +98,13 @@ function ProcessRun({process}: Props): JSX.Element
|
|||||||
const [lastProcessResponse, setLastProcessResponse] = useState(
|
const [lastProcessResponse, setLastProcessResponse] = useState(
|
||||||
null as QJobStarted | QJobComplete | QJobError | QJobRunning,
|
null as QJobStarted | QJobComplete | QJobError | QJobRunning,
|
||||||
);
|
);
|
||||||
|
const [showErrorDetail, setShowErrorDetail] = useState(false);
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// the validation screen - it can change whether next is actually the final step or not... so, use this state field to track that. //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const [overrideOnLastStep, setOverrideOnLastStep] = useState(null as boolean);
|
const [overrideOnLastStep, setOverrideOnLastStep] = useState(null as boolean);
|
||||||
|
|
||||||
const onLastStep = activeStepIndex === steps.length - 2;
|
const onLastStep = activeStepIndex === steps.length - 2;
|
||||||
const noMoreSteps = activeStepIndex === steps.length - 1;
|
const noMoreSteps = activeStepIndex === steps.length - 1;
|
||||||
|
|
||||||
@ -199,6 +206,11 @@ function ProcessRun({process}: Props): JSX.Element
|
|||||||
return (<span>{value}</span>);
|
return (<span>{value}</span>);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleShowErrorDetail = () =>
|
||||||
|
{
|
||||||
|
setShowErrorDetail(!showErrorDetail);
|
||||||
|
};
|
||||||
|
|
||||||
////////////////////////////////////////////////////
|
////////////////////////////////////////////////////
|
||||||
// generate the main form body content for a step //
|
// generate the main form body content for a step //
|
||||||
////////////////////////////////////////////////////
|
////////////////////////////////////////////////////
|
||||||
@ -216,11 +228,24 @@ function ProcessRun({process}: Props): JSX.Element
|
|||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MDTypography color="error" variant="h5" component="div">
|
<MDTypography color="error" variant="h3" component="div">
|
||||||
Error
|
Error
|
||||||
</MDTypography>
|
</MDTypography>
|
||||||
<MDTypography color="body" variant="button">
|
<MDTypography color="body" variant="button">
|
||||||
{processError}
|
An error occurred while running the process:
|
||||||
|
{" "}
|
||||||
|
{process.label}
|
||||||
|
<MDBox mt={3} display="flex" justifyContent="center">
|
||||||
|
<MDBox display="flex" flexDirection="column" alignItems="center">
|
||||||
|
<Button onClick={toggleShowErrorDetail} startIcon={<Icon>{showErrorDetail ? "expand_less" : "expand_more"}</Icon>}>
|
||||||
|
{showErrorDetail ? "Hide " : "Show "}
|
||||||
|
detailed error message
|
||||||
|
</Button>
|
||||||
|
<MDBox mt={1} style={{display: showErrorDetail ? "block" : "none"}}>
|
||||||
|
{processError}
|
||||||
|
</MDBox>
|
||||||
|
</MDBox>
|
||||||
|
</MDBox>
|
||||||
</MDTypography>
|
</MDTypography>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -326,8 +351,7 @@ function ProcessRun({process}: Props): JSX.Element
|
|||||||
//////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////
|
||||||
setFieldValue("doFullValidation", value);
|
setFieldValue("doFullValidation", value);
|
||||||
|
|
||||||
// eslint-disable-next-line no-unneeded-ternary
|
setOverrideOnLastStep(value !== "true");
|
||||||
setOverrideOnLastStep(value === "true" ? false : true);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@ -823,6 +847,7 @@ function ProcessRun({process}: Props): JSX.Element
|
|||||||
|
|
||||||
setProcessValues({});
|
setProcessValues({});
|
||||||
setRecords([]);
|
setRecords([]);
|
||||||
|
setOverrideOnLastStep(null);
|
||||||
setLastProcessResponse(new QJobRunning({message: "Working..."}));
|
setLastProcessResponse(new QJobRunning({message: "Working..."}));
|
||||||
|
|
||||||
setTimeout(async () =>
|
setTimeout(async () =>
|
||||||
@ -848,7 +873,7 @@ function ProcessRun({process}: Props): JSX.Element
|
|||||||
|
|
||||||
const mainCardStyles: any = {};
|
const mainCardStyles: any = {};
|
||||||
mainCardStyles.minHeight = "calc(100vh - 400px)";
|
mainCardStyles.minHeight = "calc(100vh - 400px)";
|
||||||
if (qJobRunning || activeStep === null)
|
if (!processError && (qJobRunning || activeStep === null))
|
||||||
{
|
{
|
||||||
mainCardStyles.background = "none";
|
mainCardStyles.background = "none";
|
||||||
mainCardStyles.boxShadow = "none";
|
mainCardStyles.boxShadow = "none";
|
||||||
|
@ -59,24 +59,37 @@ export class ProcessSummaryLine
|
|||||||
|
|
||||||
getProcessSummaryListItem(i: number, table: QTableMetaData, qInstance: QInstance, isResultScreen: boolean = false): JSX.Element
|
getProcessSummaryListItem(i: number, table: QTableMetaData, qInstance: QInstance, isResultScreen: boolean = false): JSX.Element
|
||||||
{
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// split up the message into words - then we'll display the last word by itself with a non-breaking space, no-wrap-glued to the button. //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const messageWords = this.message ? this.message.split(" ") : [];
|
||||||
|
const lastWord = messageWords.length > 1 ? messageWords[messageWords.length - 1] : "";
|
||||||
|
if (messageWords.length > 1)
|
||||||
|
{
|
||||||
|
messageWords.splice(messageWords.length - 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem key={i} sx={{pl: 4, my: 2}}>
|
<ListItem key={i} sx={{pl: 4, my: 2}}>
|
||||||
<MDBox display="flex" alignItems="top">
|
<MDBox display="flex" alignItems="top">
|
||||||
<Icon fontSize="medium" sx={{mr: 1}} color={this.getColor()}>{this.getIcon(isResultScreen)}</Icon>
|
<Icon fontSize="medium" sx={{mr: 1}} color={this.getColor()}>{this.getIcon(isResultScreen)}</Icon>
|
||||||
<ListItemText primaryTypographyProps={{fontSize: 16}}>
|
<ListItemText primaryTypographyProps={{fontSize: 16}}>
|
||||||
{this.count.toLocaleString()}
|
{/* work hard to prevent the icon from falling down to the next line by itself... */}
|
||||||
{" "}
|
{`${this.count.toLocaleString()} ${messageWords.join(" ")} `}
|
||||||
{this.message}
|
{
|
||||||
|
(table && this.primaryKeys) ? (
|
||||||
|
<span style={{whiteSpace: "nowrap"}}>
|
||||||
|
{/* eslint-disable-next-line react/jsx-one-expression-per-line */}
|
||||||
|
{lastWord} <Link target="_blank" to={this.getLinkToRecords(table, qInstance)}>
|
||||||
|
<Tooltip title="See these records in a new tab" sx={{py: 0}}>
|
||||||
|
<IconButton sx={{py: 0}}><Icon fontSize="small">open_in_new</Icon></IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
{/* eslint-disable-next-line react/jsx-closing-tag-location */}
|
||||||
|
</Link>
|
||||||
|
</span>
|
||||||
|
) : <span>{lastWord}</span>
|
||||||
|
}
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
{
|
|
||||||
table && this.primaryKeys && (
|
|
||||||
<Link target="_blank" to={this.getLinkToRecords(table, qInstance)}>
|
|
||||||
<Tooltip title="See these records in a new tab" sx={{py: 0}}>
|
|
||||||
<IconButton sx={{py: 0}}><Icon fontSize="small">open_in_new</Icon></IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</MDBox>
|
</MDBox>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
@ -131,8 +144,6 @@ export class ProcessSummaryLine
|
|||||||
{
|
{
|
||||||
const tablePath = qInstance.getTablePath(table);
|
const tablePath = qInstance.getTablePath(table);
|
||||||
const filter = new QQueryFilter([new QFilterCriteria(table.primaryKeyField, QCriteriaOperator.IN, this.primaryKeys)]);
|
const filter = new QQueryFilter([new QFilterCriteria(table.primaryKeyField, QCriteriaOperator.IN, this.primaryKeys)]);
|
||||||
console.log("Link to records:");
|
|
||||||
console.log(filter);
|
|
||||||
return (`${tablePath}?filter=${JSON.stringify(filter)}`);
|
return (`${tablePath}?filter=${JSON.stringify(filter)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,3 +110,10 @@
|
|||||||
margin-left: 40px;
|
margin-left: 40px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Help make the radio, text, and icon wrap in a good way */
|
||||||
|
.doFullValidationRadios label
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user