sw/qa/extras/mailmerge/data/empty-db-field.fodt | 15 + sw/qa/extras/mailmerge/mailmerge.cxx | 298 +++++++++++++++--------- sw/qa/extras/mailmerge/mailmerge2.cxx | 16 + sw/source/core/doc/doc.cxx | 2 sw/source/core/doc/docnew.cxx | 1 sw/source/uibase/dbui/dbmgr.cxx | 5 6 files changed, 231 insertions(+), 106 deletions(-)
New commits: commit ef3b11f811e1fd3852a22881d712afda03a0238f Author: Mike Kaganski <mike.kagan...@collabora.com> AuthorDate: Sun Jun 29 16:44:31 2025 +0500 Commit: Mike Kaganski <mike.kagan...@collabora.com> CommitDate: Sun Jun 29 18:23:16 2025 +0200 tdf#156061: don't hide DB fields when not in mail merge Original implementation (commit db04be037b611e296ef9f2542322c52ed82d7a2b tdf#35798: Hide empty Database fields' paragraphs (+ compat option), 2018-05-20) made empty DB fields serve as "hidden paragraph" field, when the field text is empty. The feature is created for mail merge, where the generated documents adapt to varying set of data in each record. But that broke the case where documents having DB field are used in non-mailmerge context, like viewing/printing such documents. Users expect such documents to show the lines. This patch changes the behavior, so that empty DB field only hides paragraphs when mail merge is being performed. When the resulting document converts DB fields to static text, the hidden paragraphs get removed. But if the generated documents still have DB fields, then the paragraphs in it will not be hidden, when opened later. This matches user expectations, and behavior of other apps, better. This behavior change required to change respective unit tests in sw/qa/extras/mailmerge/mailmerge.cxx from *_FILE_* to *_SHELL_*: the latter creates a static-text output, while the former produces files with fields. (IMO, that is a bug.) Change-Id: Id796ad8fe413372377ce22d8da4931110718c8c2 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/187150 Reviewed-by: Mike Kaganski <mike.kagan...@collabora.com> Tested-by: Jenkins diff --git a/sw/qa/extras/mailmerge/data/empty-db-field.fodt b/sw/qa/extras/mailmerge/data/empty-db-field.fodt new file mode 100644 index 000000000000..ab36c4323b7a --- /dev/null +++ b/sw/qa/extras/mailmerge/data/empty-db-field.fodt @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:ooo="http://openoffice.org/2004/office" xmlns:config="urn:oasis:names:tc:opendocument:xmlns:config:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.4" office:mimetype="application/vnd.oasis.opendocument.text"> + <office:settings> + <config:config-item-set config:name="ooo:configuration-settings"> + <config:config-item config:name="EmptyDbFieldHidesPara" config:type="boolean">true</config:config-item> + </config:config-item-set> + </office:settings> + <office:body> + <office:text> + <text:p>Text <text:database-display text:table-name="" text:column-name="" text:database-name=""/></text:p> + <text:p/> + </office:text> + </office:body> +</office:document> \ No newline at end of file diff --git a/sw/qa/extras/mailmerge/mailmerge.cxx b/sw/qa/extras/mailmerge/mailmerge.cxx index 71eb97fd753e..7acf5f9bb50c 100644 --- a/sw/qa/extras/mailmerge/mailmerge.cxx +++ b/sw/qa/extras/mailmerge/mailmerge.cxx @@ -385,135 +385,223 @@ DECLARE_SHELL_MAILMERGE_TEST(testTdf118113, "tdf118113.odt", "tdf118113.ods", "t } -constexpr char const* const EmptyValuesLegacyData[][8] - = { { "Heading", "Title: ", "First Name: firstname1", "Last Name: lastname1", - "Title: First Name: firstname1", "First Name: firstname1 Last Name: lastname1", - "Title: First Name: firstname1 Last Name: lastname1", "Trailing text" }, - { "Heading", "Title: title2", "First Name: ", "Last Name: lastname2", - "Title: title2 First Name: ", "First Name: Last Name: lastname2", - "Title: title2 First Name: Last Name: lastname2", "Trailing text" }, - { "Heading", "Title: title3", "First Name: firstname3", "Last Name: ", - "Title: title3 First Name: firstname3", "First Name: firstname3 Last Name: ", - "Title: title3 First Name: firstname3 Last Name: ", "Trailing text" }, - { "Heading", "Title: ", "First Name: ", "Last Name: lastname4", - "Title: First Name: ", "First Name: Last Name: lastname4", - "Title: First Name: Last Name: lastname4", "Trailing text" }, - { "Heading", "Title: title5", "First Name: ", "Last Name: ", "Title: title5 First Name: ", - "First Name: Last Name: ", "Title: title5 First Name: Last Name: ", "Trailing text" } }; -constexpr char const* const EmptyValuesNewData[][8] - = { { "Heading", "First Name: firstname1", "Last Name: lastname1", - "Title: First Name: firstname1", "First Name: firstname1 Last Name: lastname1", - "Title: First Name: firstname1 Last Name: lastname1", "Trailing text" }, - { "Heading", "Title: title2", "Last Name: lastname2", - "Title: title2 First Name: ", "First Name: Last Name: lastname2", - "Title: title2 First Name: Last Name: lastname2", "Trailing text" }, - { "Heading", "Title: title3", "First Name: firstname3", - "Title: title3 First Name: firstname3", "First Name: firstname3 Last Name: ", - "Title: title3 First Name: firstname3 Last Name: ", "Trailing text" }, - { "Heading", "Last Name: lastname4", "First Name: Last Name: lastname4", - "Title: First Name: Last Name: lastname4", "Trailing text" }, - { "Heading", "Title: title5", "Title: title5 First Name: ", - "Title: title5 First Name: Last Name: ", "Trailing text" } }; - - -// The following four tests (testEmptyValuesLegacyODT, testEmptyValuesNewODT, (testEmptyValuesLegacyFODT, testEmptyValuesNewFODT) +constexpr OUString EmptyValuesLegacyData[] = { + // Doc 1 [ Title: ""; First Name: "firstname1"; Last Name: "lastname1" ] + u"Heading"_ustr, + // Title: [Title] + u"Title: "_ustr, + // First Name: [First Name] + u"First Name: firstname1"_ustr, + // Last Name: [Last Name] + u"Last Name: lastname1"_ustr, + // Title: [Title] First Name: [First Name] + u"Title: First Name: firstname1"_ustr, + // First Name: [First Name] Last Name: [Last Name] + u"First Name: firstname1 Last Name: lastname1"_ustr, + // Title: [Title] First Name: [First Name] Last Name: [Last Name] + u"Title: First Name: firstname1 Last Name: lastname1"_ustr, + u"Trailing text"_ustr, + + // Doc 2 [ Title: "title2"; First Name: ""; Last Name: "lastname2" ] + u"Heading"_ustr, + // Title: [Title] + u"Title: title2"_ustr, + // First Name: [First Name] + u"First Name: "_ustr, + // Last Name: [Last Name] + u"Last Name: lastname2"_ustr, + // Title: [Title] First Name: [First Name] + u"Title: title2 First Name: "_ustr, + // First Name: [First Name] Last Name: [Last Name] + u"First Name: Last Name: lastname2"_ustr, + // Title: [Title] First Name: [First Name] Last Name: [Last Name] + u"Title: title2 First Name: Last Name: lastname2"_ustr, + u"Trailing text"_ustr, + + // Doc 3 [ Title: "title3"; First Name: "firstname3"; Last Name: "" ] + u"Heading"_ustr, + // Title: [Title] + u"Title: title3"_ustr, + // First Name: [First Name] + u"First Name: firstname3"_ustr, + // Last Name: [Last Name] + u"Last Name: "_ustr, + // Title: [Title] First Name: [First Name] + u"Title: title3 First Name: firstname3"_ustr, + // First Name: [First Name] Last Name: [Last Name] + u"First Name: firstname3 Last Name: "_ustr, + // Title: [Title] First Name: [First Name] Last Name: [Last Name] + u"Title: title3 First Name: firstname3 Last Name: "_ustr, + u"Trailing text"_ustr, + + // Doc 4 [ Title: ""; First Name: ""; Last Name: "lastname4" ] + u"Heading"_ustr, + // Title: [Title] + u"Title: "_ustr, + // First Name: [First Name] + u"First Name: "_ustr, + // Last Name: [Last Name] + u"Last Name: lastname4"_ustr, + // Title: [Title] First Name: [First Name] + u"Title: First Name: "_ustr, + // First Name: [First Name] Last Name: [Last Name] + u"First Name: Last Name: lastname4"_ustr, + // Title: [Title] First Name: [First Name] Last Name: [Last Name] + u"Title: First Name: Last Name: lastname4"_ustr, + u"Trailing text"_ustr, + + // Doc 5 [ Title: "title5"; First Name: ""; Last Name: "" ] + u"Heading"_ustr, + // Title: [Title] + u"Title: title5"_ustr, + // First Name: [First Name] + u"First Name: "_ustr, + // Last Name: [Last Name] + u"Last Name: "_ustr, + // Title: [Title] First Name: [First Name] + u"Title: title5 First Name: "_ustr, + // First Name: [First Name] Last Name: [Last Name] + u"First Name: Last Name: "_ustr, + // Title: [Title] First Name: [First Name] Last Name: [Last Name] + u"Title: title5 First Name: Last Name: "_ustr, + u"Trailing text"_ustr, +}; +constexpr OUString EmptyValuesNewData[] = { + // Doc 1 [ Title: ""; First Name: "firstname1"; Last Name: "lastname1" ] + u"Heading"_ustr, + // Title: [Title] + // First Name: [First Name] + u"First Name: firstname1"_ustr, + // Last Name: [Last Name] + u"Last Name: lastname1"_ustr, + // Title: [Title] First Name: [First Name] + u"Title: First Name: firstname1"_ustr, + // First Name: [First Name] Last Name: [Last Name] + u"First Name: firstname1 Last Name: lastname1"_ustr, + // Title: [Title] First Name: [First Name] Last Name: [Last Name] + u"Title: First Name: firstname1 Last Name: lastname1"_ustr, + u"Trailing text"_ustr, + + // Doc 2 [ Title: "title2"; First Name: ""; Last Name: "lastname2" ] + u"Heading"_ustr, + // Title: [Title] + u"Title: title2"_ustr, + // First Name: [First Name] + // Last Name: [Last Name] + u"Last Name: lastname2"_ustr, + // Title: [Title] First Name: [First Name] + u"Title: title2 First Name: "_ustr, + // First Name: [First Name] Last Name: [Last Name] + u"First Name: Last Name: lastname2"_ustr, + // Title: [Title] First Name: [First Name] Last Name: [Last Name] + u"Title: title2 First Name: Last Name: lastname2"_ustr, + u"Trailing text"_ustr, + + // Doc 3 [ Title: "title3"; First Name: "firstname3"; Last Name: "" ] + u"Heading"_ustr, + // Title: [Title] + u"Title: title3"_ustr, + // First Name: [First Name] + u"First Name: firstname3"_ustr, + // Last Name: [Last Name] + // Title: [Title] First Name: [First Name] + u"Title: title3 First Name: firstname3"_ustr, + // First Name: [First Name] Last Name: [Last Name] + u"First Name: firstname3 Last Name: "_ustr, + // Title: [Title] First Name: [First Name] Last Name: [Last Name] + u"Title: title3 First Name: firstname3 Last Name: "_ustr, + u"Trailing text"_ustr, + + // Doc 4 [ Title: ""; First Name: ""; Last Name: "lastname4" ] + u"Heading"_ustr, + // Title: [Title] + // First Name: [First Name] + // Last Name: [Last Name] + u"Last Name: lastname4"_ustr, + // Title: [Title] First Name: [First Name] + // First Name: [First Name] Last Name: [Last Name] + u"First Name: Last Name: lastname4"_ustr, + // Title: [Title] First Name: [First Name] Last Name: [Last Name] + u"Title: First Name: Last Name: lastname4"_ustr, + u"Trailing text"_ustr, + + // Doc 5 [ Title: "title5"; First Name: ""; Last Name: "" ] + u"Heading"_ustr, + // Title: [Title] + u"Title: title5"_ustr, + // First Name: [First Name] + // Last Name: [Last Name] + // Title: [Title] First Name: [First Name] + u"Title: title5 First Name: "_ustr, + // First Name: [First Name] Last Name: [Last Name] + // Title: [Title] First Name: [First Name] Last Name: [Last Name] + u"Title: title5 First Name: Last Name: "_ustr, + u"Trailing text"_ustr, +}; + + +// The following four tests (testEmptyValuesLegacyODT, testEmptyValuesNewODT, testEmptyValuesLegacyFODT, testEmptyValuesNewFODT) // check that for native documents without "EmptyDbFieldHidesPara" compatibility option, all paragraphs are exported visible, -// while for documents with the option enabled, the paragraphs with all Database fields having empty values are hidden. +// while for documents with the option enabled, the paragraphs with all Database fields having empty values are removed. -DECLARE_FILE_MAILMERGE_TEST(testEmptyValuesLegacyODT, "tdf35798-legacy.odt", "5-with-blanks.ods", - "names") +DECLARE_SHELL_MAILMERGE_TEST(testEmptyValuesLegacyODT, "tdf35798-legacy.odt", "5-with-blanks.ods", + "names") { executeMailMerge(); - for (int doc = 0; doc < 5; ++doc) + CPPUNIT_ASSERT(mxSwTextDocument); + for (size_t i = 0; i < std::size(EmptyValuesLegacyData); ++i) { - loadMailMergeDocument(doc); - SwDoc* pDoc = getSwDoc(); - pDoc->RemoveInvisibleContent(); - CPPUNIT_ASSERT_EQUAL(1, getPages()); - for (int i = 0; i < 8; ++i) - { - auto xPara = getParagraph(i+1); - CPPUNIT_ASSERT_EQUAL_MESSAGE( - OString("in doc " + OString::number(doc) + " paragraph " + OString::number(i + 1)) - .getStr(), - OUString::createFromAscii(EmptyValuesLegacyData[doc][i]), xPara->getString()); - } - CPPUNIT_ASSERT_EQUAL(8, getParagraphs()); + auto xPara = getParagraphOfText(i + 1, mxSwTextDocument->getText()); + CPPUNIT_ASSERT_EQUAL_MESSAGE(OString("paragraph " + OString::number(i + 1)).getStr(), + EmptyValuesLegacyData[i], xPara->getString()); } + CPPUNIT_ASSERT_EQUAL(static_cast<int>(std::size(EmptyValuesLegacyData)), + getParagraphs(mxSwTextDocument->getText())); } -DECLARE_FILE_MAILMERGE_TEST(testEmptyValuesNewODT, "tdf35798-new.odt", "5-with-blanks.ods", - "names") +DECLARE_SHELL_MAILMERGE_TEST(testEmptyValuesNewODT, "tdf35798-new.odt", "5-with-blanks.ods", + "names") { executeMailMerge(); - for (int doc = 0; doc < 5; ++doc) + CPPUNIT_ASSERT(mxSwTextDocument); + for (size_t i = 0; i < std::size(EmptyValuesNewData); ++i) { - loadMailMergeDocument(doc); - SwDoc* pDoc = getSwDoc(); - pDoc->RemoveInvisibleContent(); - CPPUNIT_ASSERT_EQUAL(1, getPages()); - int i; - for (i = 0; i < 8; ++i) - { - const char* pExpected = EmptyValuesNewData[doc][i]; - if (!pExpected) - break; - auto xPara = getParagraph(i + 1); - CPPUNIT_ASSERT_EQUAL_MESSAGE( - OString("in doc " + OString::number(doc) + " paragraph " + OString::number(i + 1)) - .getStr(), - OUString::createFromAscii(pExpected), xPara->getString()); - } - CPPUNIT_ASSERT_EQUAL(i, getParagraphs()); + auto xPara = getParagraphOfText(i + 1, mxSwTextDocument->getText()); + CPPUNIT_ASSERT_EQUAL_MESSAGE(OString("paragraph " + OString::number(i + 1)).getStr(), + EmptyValuesNewData[i], xPara->getString()); } + CPPUNIT_ASSERT_EQUAL(static_cast<int>(std::size(EmptyValuesNewData)), + getParagraphs(mxSwTextDocument->getText())); } -DECLARE_FILE_MAILMERGE_TEST(testEmptyValuesLegacyFODT, "tdf35798-legacy.fodt", "5-with-blanks.ods", - "names") +DECLARE_SHELL_MAILMERGE_TEST(testEmptyValuesLegacyFODT, "tdf35798-legacy.fodt", "5-with-blanks.ods", + "names") { executeMailMerge(); - for (int doc = 0; doc < 5; ++doc) + CPPUNIT_ASSERT(mxSwTextDocument); + for (size_t i = 0; i < std::size(EmptyValuesLegacyData); ++i) { - loadMailMergeDocument(doc); - SwDoc* pDoc = getSwDoc(); - pDoc->RemoveInvisibleContent(); - CPPUNIT_ASSERT_EQUAL(1, getPages()); - for (int i = 0; i < 8; ++i) - { - auto xPara = getParagraph(i + 1); - CPPUNIT_ASSERT_EQUAL_MESSAGE( - OString("in doc " + OString::number(doc) + " paragraph " + OString::number(i + 1)) - .getStr(), - OUString::createFromAscii(EmptyValuesLegacyData[doc][i]), xPara->getString()); - } - CPPUNIT_ASSERT_EQUAL(8, getParagraphs()); + auto xPara = getParagraphOfText(i + 1, mxSwTextDocument->getText()); + CPPUNIT_ASSERT_EQUAL_MESSAGE(OString("paragraph " + OString::number(i + 1)).getStr(), + EmptyValuesLegacyData[i], xPara->getString()); } + CPPUNIT_ASSERT_EQUAL(static_cast<int>(std::size(EmptyValuesLegacyData)), + getParagraphs(mxSwTextDocument->getText())); } -DECLARE_FILE_MAILMERGE_TEST(testEmptyValuesNewFODT, "tdf35798-new.fodt", "5-with-blanks.ods", - "names") +DECLARE_SHELL_MAILMERGE_TEST(testEmptyValuesNewFODT, "tdf35798-new.fodt", "5-with-blanks.ods", + "names") { executeMailMerge(); - for (int doc = 0; doc < 5; ++doc) + CPPUNIT_ASSERT(mxSwTextDocument); + for (size_t i = 0; i < std::size(EmptyValuesNewData); ++i) { - loadMailMergeDocument(doc); - SwDoc* pDoc = getSwDoc(); - pDoc->RemoveInvisibleContent(); - CPPUNIT_ASSERT_EQUAL(1, getPages()); - int i; - for (i = 0; i < 8; ++i) - { - const char* pExpected = EmptyValuesNewData[doc][i]; - if (!pExpected) - break; - auto xPara = getParagraph(i + 1); - CPPUNIT_ASSERT_EQUAL_MESSAGE( - OString("in doc " + OString::number(doc) + " paragraph " + OString::number(i + 1)) - .getStr(), - OUString::createFromAscii(pExpected), xPara->getString()); - } - CPPUNIT_ASSERT_EQUAL(i, getParagraphs()); + auto xPara = getParagraphOfText(i + 1, mxSwTextDocument->getText()); + CPPUNIT_ASSERT_EQUAL_MESSAGE(OString("paragraph " + OString::number(i + 1)).getStr(), + EmptyValuesNewData[i], xPara->getString()); } + CPPUNIT_ASSERT_EQUAL(static_cast<int>(std::size(EmptyValuesNewData)), + getParagraphs(mxSwTextDocument->getText())); } DECLARE_SHELL_MAILMERGE_TEST(testTdf118845, "tdf118845.fodt", "4_v01.ods", "Tabelle1") diff --git a/sw/qa/extras/mailmerge/mailmerge2.cxx b/sw/qa/extras/mailmerge/mailmerge2.cxx index 4fde3c7bcd24..de3c8fb770c6 100644 --- a/sw/qa/extras/mailmerge/mailmerge2.cxx +++ b/sw/qa/extras/mailmerge/mailmerge2.cxx @@ -416,6 +416,22 @@ DECLARE_MAILMERGE_TEST(testGrabBag, "grabbagtest.docx", "onecell.xlsx", "Sheet1" CPPUNIT_ASSERT_EQUAL(u"Arial"_ustr, getProperty<OUString>(xParaA1, u"CharFontName"_ustr)); } +CPPUNIT_TEST_FIXTURE(MMTest2, testTdf156061) +{ + // Given a document with a paragraph with a database field having empty content + createSwDoc("empty-db-field.fodt"); + xmlDocUniquePtr pXmlDoc = parseLayoutDump(); + // The first paragraph is a text followed by the DB field; the second is empty. + // Check that both are visible (have proper height). + assertXPath(pXmlDoc, "//txt", 2); + + // Without the fix, it was "0" + CPPUNIT_ASSERT_GREATER(sal_Int32(260), + getXPath(pXmlDoc, "//txt[1]/infos/bounds", "height").toInt32()); + CPPUNIT_ASSERT_GREATER(sal_Int32(260), + getXPath(pXmlDoc, "//txt[2]/infos/bounds", "height").toInt32()); +} + } // end of anonymous namespace namespace com::sun::star::table { diff --git a/sw/source/core/doc/doc.cxx b/sw/source/core/doc/doc.cxx index 7d5a2f95322c..01c064af5b25 100644 --- a/sw/source/core/doc/doc.cxx +++ b/sw/source/core/doc/doc.cxx @@ -1774,7 +1774,7 @@ bool SwDoc::FieldHidesPara(const SwField& rField) const case SwFieldIds::HiddenPara: return static_cast<const SwHiddenParaField&>(rField).IsHidden(); case SwFieldIds::Database: - return FieldCanHideParaWeight(SwFieldIds::Database) + return IsInMailMerge() && FieldCanHideParaWeight(SwFieldIds::Database) && rField.ExpandField(true, nullptr).isEmpty(); default: return false; diff --git a/sw/source/core/doc/docnew.cxx b/sw/source/core/doc/docnew.cxx index 2f1d99478f3f..0d3d4fc5fcc0 100644 --- a/sw/source/core/doc/docnew.cxx +++ b/sw/source/core/doc/docnew.cxx @@ -917,6 +917,7 @@ rtl::Reference<SfxObjectShell> SwDoc::CreateCopy(bool bCallInitNew, bool bEmpty) { SAL_INFO( "sw.pageframe", "(SwDoc::CreateCopy in" ); rtl::Reference<SwDoc> xRet( new SwDoc ); + xRet->SetInMailMerge(IsInMailMerge()); // we have to use pointer here, since the callee has to decide whether // SfxObjectShellLock or SfxObjectShellRef should be used sometimes the diff --git a/sw/source/uibase/dbui/dbmgr.cxx b/sw/source/uibase/dbui/dbmgr.cxx index ab259b5bc2af..12915e58f976 100644 --- a/sw/source/uibase/dbui/dbmgr.cxx +++ b/sw/source/uibase/dbui/dbmgr.cxx @@ -86,6 +86,7 @@ #include <comphelper/processfactory.hxx> #include <comphelper/property.hxx> #include <comphelper/propertyvalue.hxx> +#include <comphelper/scopeguard.hxx> #include <comphelper/storagehelper.hxx> #include <comphelper/string.hxx> #include <comphelper/types.hxx> @@ -1189,6 +1190,10 @@ bool SwDBManager::MergeMailFiles(SwWrtShell* pSourceShell, std::shared_ptr<weld::GenericDialogController> xProgressDlg; + comphelper::ScopeGuard restoreInMailMerge( + [pSourceShell, val = pSourceShell->GetDoc()->IsInMailMerge()] + { pSourceShell->GetDoc()->SetInMailMerge(val); }); + pSourceShell->GetDoc()->SetInMailMerge(true); try { vcl::Window *pSourceWindow = nullptr;