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 {} 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 { RowLayout {
spacing: StudioTheme.Values.sectionRowSpacing spacing: StudioTheme.Values.sectionRowSpacing
@@ -194,7 +205,8 @@ StudioControls.Dialog {
onClicked: { onClicked: {
let collectionImported = root.backendValue.importFile( let collectionImported = root.backendValue.importFile(
collectionName.text, collectionName.text,
fileName.text) fileName.text,
csvFirstRowIsHeader.checked)
if (collectionImported) if (collectionImported)
root.accept() 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()); 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 class PropertyOrderFinder : public QmlJS::AST::Visitor
{ {
public: public:
@@ -706,32 +729,36 @@ void CollectionDetails::registerDeclarativeType()
qmlRegisterUncreatableType<DataTypeWarning>("CollectionDetails", 1, 0, "Warning", "Enum type"); 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; QStringList headers;
QJsonArray importedArray; QJsonArray importedArray;
QTextStream stream(document); QTextStream stream(document);
if (!stream.atEnd()) if (firstRowIsHeader && !stream.atEnd()) {
headers = stream.readLine().split(','); 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()) { while (!stream.atEnd()) {
const QStringList recordDataList = stream.readLine().split(','); const QStringList recordDataList = csvReadLine(stream.readLine());
int column = -1; int column = -1;
QJsonObject recordData; QJsonObject recordData;
for (const QString &cellData : recordDataList) { for (const QString &cellData : recordDataList) {
if (++column == headers.size()) if (++column == headers.size()) {
break; QString proposalName;
int proposalId = column;
do
proposalName = QString("Column %1").arg(++proposalId);
while (headers.contains(proposalName));
headers.append(proposalName);
}
recordData.insert(headers.at(column), cellData); recordData.insert(headers.at(column), cellData);
} }
importedArray.append(recordData); importedArray.append(recordData);
} }
}
return fromImportedJson(importedArray, headers); return fromImportedJson(importedArray, headers);
} }

View File

@@ -125,7 +125,8 @@ public:
static void registerDeclarativeType(); 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, static CollectionDetails fromImportedJson(const QByteArray &json,
QJsonParseError *error = nullptr); QJsonParseError *error = nullptr);
static CollectionDetails fromLocalJson(const QJsonDocument &document, static CollectionDetails fromLocalJson(const QJsonDocument &document,

View File

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

View File

@@ -40,7 +40,10 @@ public:
Q_INVOKABLE bool isCsvFile(const QUrl &url) const; Q_INVOKABLE bool isCsvFile(const QUrl &url) const;
Q_INVOKABLE bool isValidUrlToImport(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 bool addCollectionToDataStore(const QString &collectionName);
Q_INVOKABLE void assignCollectionToSelectedNode(const QString collectionName); Q_INVOKABLE void assignCollectionToSelectedNode(const QString collectionName);
Q_INVOKABLE void openCollection(const QString &collectionName); Q_INVOKABLE void openCollection(const QString &collectionName);