diff --git a/share/qtcreator/qmldesigner/collectionEditorQmlSource/ImportDialog.qml b/share/qtcreator/qmldesigner/collectionEditorQmlSource/ImportDialog.qml index cb77d567e8c..d98b23c5ba7 100644 --- a/share/qtcreator/qmldesigner/collectionEditorQmlSource/ImportDialog.qml +++ b/share/qtcreator/qmldesigner/collectionEditorQmlSource/ImportDialog.qml @@ -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() diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.cpp b/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.cpp index c4c7972582b..8706a5dab0b 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.cpp +++ b/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.cpp @@ -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"(?:,\"|^\")(?\"\"|[\\w\\W]*?)(?=\",|\"$)" + u"|(?:,(?!\")|^(?!\"))(?[^,]*?)(?=$|,)|(\\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("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); diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.h b/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.h index 95528413f6c..b84c214570a 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.h +++ b/src/plugins/qmldesigner/components/collectioneditor/collectiondetails.h @@ -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, diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp b/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp index d8fd026c7e7..93cfb2f987d 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.cpp @@ -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()) { diff --git a/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.h b/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.h index e52e3e401e7..f4afd98f0da 100644 --- a/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.h +++ b/src/plugins/qmldesigner/components/collectioneditor/collectionwidget.h @@ -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);