QmlDesigner: Prompt to consider first row as header for CSV files

* Also, a bug is fixed for reading the quoted texts within CSV rows

Fixes: QDS-11667
Fixes: QDS-11834
Change-Id: I74242148e38c8e71edeb45f3543308259358ee1a
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
This commit is contained in:
Ali Kianian
2024-02-27 12:31:16 +02:00
parent 18c2a8e02c
commit d473f9aabb
5 changed files with 66 additions and 21 deletions

View File

@@ -180,6 +180,17 @@ StudioControls.Dialog {
Spacer {}
StudioControls.CheckBox {
id: csvFirstRowIsHeader
visible: root.fileExists && fileName.text.endsWith(".csv")
text: qsTr("Consider first row as headers")
checked: true
actionIndicatorVisible: false
}
Spacer {}
RowLayout {
spacing: StudioTheme.Values.sectionRowSpacing
@@ -194,7 +205,8 @@ StudioControls.Dialog {
onClicked: {
let collectionImported = root.backendValue.importFile(
collectionName.text,
fileName.text)
fileName.text,
csvFirstRowIsHeader.checked)
if (collectionImported)
root.accept()

View File

@@ -264,6 +264,29 @@ inline static bool isEmptyJsonValue(const QJsonValue &value)
return value.isNull() || value.isUndefined() || (value.isString() && value.toString().isEmpty());
}
QStringList csvReadLine(const QString &line)
{
constexpr QStringView linePattern = u"(?:,\"|^\")(?<value>\"\"|[\\w\\W]*?)(?=\",|\"$)"
u"|(?:,(?!\")|^(?!\"))(?<quote>[^,]*?)(?=$|,)|(\\r\\n|\\n)";
static const QRegularExpression lineRegex(linePattern.toString());
static const int valueIndex = lineRegex.namedCaptureGroups().indexOf("value");
static const int quoteIndex = lineRegex.namedCaptureGroups().indexOf("quote");
Q_ASSERT(valueIndex > 0 && quoteIndex > 0);
QStringList result;
QRegularExpressionMatchIterator iterator = lineRegex.globalMatch(line, 0);
while (iterator.hasNext()) {
const QRegularExpressionMatch match = iterator.next();
if (match.hasCaptured(valueIndex))
result.append(match.captured(2));
else if (match.hasCaptured(quoteIndex))
result.append(match.captured(quoteIndex));
}
return result;
}
class PropertyOrderFinder : public QmlJS::AST::Visitor
{
public:
@@ -706,31 +729,35 @@ void CollectionDetails::registerDeclarativeType()
qmlRegisterUncreatableType<DataTypeWarning>("CollectionDetails", 1, 0, "Warning", "Enum type");
}
CollectionDetails CollectionDetails::fromImportedCsv(const QByteArray &document)
CollectionDetails CollectionDetails::fromImportedCsv(const QByteArray &document,
const bool &firstRowIsHeader)
{
QStringList headers;
QJsonArray importedArray;
QTextStream stream(document);
if (!stream.atEnd())
headers = stream.readLine().split(',');
if (firstRowIsHeader && !stream.atEnd()) {
headers = Utils::transform(csvReadLine(stream.readLine()),
[](const QString &value) -> QString { return value.trimmed(); });
}
for (QString &header : headers)
header = header.trimmed();
if (!headers.isEmpty()) {
while (!stream.atEnd()) {
const QStringList recordDataList = stream.readLine().split(',');
int column = -1;
QJsonObject recordData;
for (const QString &cellData : recordDataList) {
if (++column == headers.size())
break;
recordData.insert(headers.at(column), cellData);
while (!stream.atEnd()) {
const QStringList recordDataList = csvReadLine(stream.readLine());
int column = -1;
QJsonObject recordData;
for (const QString &cellData : recordDataList) {
if (++column == headers.size()) {
QString proposalName;
int proposalId = column;
do
proposalName = QString("Column %1").arg(++proposalId);
while (headers.contains(proposalName));
headers.append(proposalName);
}
importedArray.append(recordData);
recordData.insert(headers.at(column), cellData);
}
importedArray.append(recordData);
}
return fromImportedJson(importedArray, headers);

View File

@@ -125,7 +125,8 @@ public:
static void registerDeclarativeType();
static CollectionDetails fromImportedCsv(const QByteArray &document);
static CollectionDetails fromImportedCsv(const QByteArray &document,
const bool &firstRowIsHeader = true);
static CollectionDetails fromImportedJson(const QByteArray &json,
QJsonParseError *error = nullptr);
static CollectionDetails fromLocalJson(const QJsonDocument &document,

View File

@@ -202,7 +202,9 @@ bool CollectionWidget::isValidUrlToImport(const QUrl &url) const
return false;
}
bool CollectionWidget::importFile(const QString &collectionName, const QUrl &url)
bool CollectionWidget::importFile(const QString &collectionName,
const QUrl &url,
const bool &firstRowIsHeader)
{
using Utils::FilePath;
m_view->ensureDataStoreExists();
@@ -244,7 +246,7 @@ bool CollectionWidget::importFile(const QString &collectionName, const QUrl &url
} else if (fileInfo.suffix() == "csv") {
if (!loadUrlContent())
return false;
loadedCollection = CollectionDetails::fromImportedCsv(fileContent);
loadedCollection = CollectionDetails::fromImportedCsv(fileContent, firstRowIsHeader);
}
if (loadedCollection.columns()) {

View File

@@ -40,7 +40,10 @@ public:
Q_INVOKABLE bool isCsvFile(const QUrl &url) const;
Q_INVOKABLE bool isValidUrlToImport(const QUrl &url) const;
Q_INVOKABLE bool importFile(const QString &collectionName, const QUrl &url);
Q_INVOKABLE bool importFile(const QString &collectionName,
const QUrl &url,
const bool &firstRowIsHeader = true);
Q_INVOKABLE bool addCollectionToDataStore(const QString &collectionName);
Q_INVOKABLE void assignCollectionToSelectedNode(const QString collectionName);
Q_INVOKABLE void openCollection(const QString &collectionName);