forked from qt-creator/qt-creator
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:
@@ -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()
|
||||||
|
|||||||
@@ -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,31 +729,35 @@ 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)
|
while (!stream.atEnd()) {
|
||||||
header = header.trimmed();
|
const QStringList recordDataList = csvReadLine(stream.readLine());
|
||||||
|
int column = -1;
|
||||||
if (!headers.isEmpty()) {
|
QJsonObject recordData;
|
||||||
while (!stream.atEnd()) {
|
for (const QString &cellData : recordDataList) {
|
||||||
const QStringList recordDataList = stream.readLine().split(',');
|
if (++column == headers.size()) {
|
||||||
int column = -1;
|
QString proposalName;
|
||||||
QJsonObject recordData;
|
int proposalId = column;
|
||||||
for (const QString &cellData : recordDataList) {
|
do
|
||||||
if (++column == headers.size())
|
proposalName = QString("Column %1").arg(++proposalId);
|
||||||
break;
|
while (headers.contains(proposalName));
|
||||||
recordData.insert(headers.at(column), cellData);
|
headers.append(proposalName);
|
||||||
}
|
}
|
||||||
importedArray.append(recordData);
|
recordData.insert(headers.at(column), cellData);
|
||||||
}
|
}
|
||||||
|
importedArray.append(recordData);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fromImportedJson(importedArray, headers);
|
return fromImportedJson(importedArray, headers);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user