diff --git a/docs/features/cmdb-import.md b/docs/features/cmdb-import.md index 76760cbfc..14460b91a 100644 --- a/docs/features/cmdb-import.md +++ b/docs/features/cmdb-import.md @@ -373,6 +373,14 @@ Nextcloud admins only) answers, per pack, where the import takes it from link to its editor in OpenRegister. Reading it changes nothing: it does not store or update a pack. +**In the settings.** Open **Administration settings → Stackiq → CMDB import** +and press **Mapping** under the import form. One table lists the sheets the +import reads and its match, name, required and date columns; a second table +lists the four packs, each with where the import takes it from, its version +and, for a pack kept in OpenRegister, an **Edit in OpenRegister** link. The +municipality pack is not listed: it maps the municipality you choose for the +import, not a column. Opening the page changes nothing. + Each pack has a list of field mappings, one per column: the column name in the export, the field it goes to, whether it is required, and a transformation such as trim, a date, or a lookup table of export values to @@ -405,15 +413,27 @@ rules, so it checks every pack before it reads a row. It refuses a pack, with defaults of the vendor and municipality packs stay as shipped. The mapping endpoint answers the same reason, with the link to the pack's -editor in `details.editUrl`, so you can correct it there and try again. +editor in `details.editUrl`, and the **Mapping** block shows it with an +**Edit in OpenRegister** link, so you can correct the pack there. After saving +it, reload the settings page: **Mapping** then shows the new version, or the +reason the pack is still refused. + +**What the Mapping block shows.** Below the table of packs, each pack has a +collapsible block "Columns and fields": which column of the export goes to +which field, whether the mapping is required and which transform it uses, and +the rules for what the import writes (the values it sets, the fields written +only on a new record, the fields it never writes on an existing record). When +an unedited pack in OpenRegister is older than the shipped one, the row says +"Update pending": the next import replaces it with the shipped version. **What a pack does not change.** Which sheets are read, the APPID and "Applicatie Naam" columns, the values that mean empty (`NB`), the constant -each sheet adds (`Beheer geregeld: ja` or `nee`), the limits, and which fields -are written only on create or never on an existing record are part of the app -(`lib/Settings/cmdb-import/topdesk-profile.json`) and cannot be changed. The +each sheet adds (`Beheer geregeld: ja` or `nee`) and the limits are part of +the app (`lib/Settings/cmdb-import/topdesk-profile.json`) and cannot be +changed; the **Mapping** block shows the sheets and the match, name, required +and date columns, not the empty values, the constants or the limits. The municipality is mapped from the name you choose for the import, not from the -export; that pack is not stored in OpenRegister. +export; that pack is not stored in OpenRegister and the block does not list it. **Reading more columns.** The import reads only the columns the mapping names. When you map a new column in a pack, the import reads that column from @@ -428,7 +448,7 @@ endpoint already shows the new pack, with `updatePending` true, before the next import runs). A pack you changed is never overwritten, also not when you save it while an import is updating it. When an update ships a newer version of a pack you changed, the mapping -endpoint reports it (`newerShipped`); compare the two and copy what you need +endpoint reports it (`newerShipped`) and the **Mapping** block says so; compare the two and copy what you need into your pack in OpenRegister. **Back to the shipped pack.** Delete the pack in OpenRegister. The next diff --git a/l10n/en.js b/l10n/en.js index 0c34d605b..96ba8c174 100644 --- a/l10n/en.js +++ b/l10n/en.js @@ -974,7 +974,6 @@ OC.L10N.register( "Error code: {code}": "Error code: {code}", "Excel workbook (.xlsx), at most 10 MB, with the sheet \"Onbeh Applicaties CMDB\" or \"Beheerde Applicaties CMDB\".": "Excel workbook (.xlsx), at most 10 MB, with the sheet \"Onbeh Applicaties CMDB\" or \"Beheerde Applicaties CMDB\".", "Existing municipalities could not be loaded. You can still type the name of a municipality.": "Existing municipalities could not be loaded. You can still type the name of a municipality.", - "Expected a sheet named \"{first}\" or \"{second}\". Sheet names must match exactly.": "Expected a sheet named \"{first}\" or \"{second}\". Sheet names must match exactly.", "Failed": "Failed", "Import": "Import", "Import a TOPdesk CMDB export (.xlsx) as the applications one municipality uses": "Import a TOPdesk CMDB export (.xlsx) as the applications one municipality uses", @@ -991,10 +990,9 @@ OC.L10N.register( "No rows with this outcome": "No rows with this outcome", "Nothing more is known on this page; the Nextcloud log has the details.": "Nothing more is known on this page; the Nextcloud log has the details.", "Only Nextcloud administrators can import a CMDB export.": "Only Nextcloud administrators can import a CMDB export.", - "OpenRegister's mapping engine is missing or a mapping file is invalid. Update OpenRegister and check the Nextcloud log.": "OpenRegister's mapping engine is missing or a mapping file is invalid. Update OpenRegister and check the Nextcloud log.", + "OpenRegister's mapping engine is missing, or a mapping pack is invalid or breaks a rule of the import. The reason below names the pack: correct it in OpenRegister, or check the Nextcloud log.": "OpenRegister's mapping engine is missing, or a mapping pack is invalid or breaks a rule of the import. The reason below names the pack: correct it in OpenRegister, or check the Nextcloud log.", "Outcome": "Outcome", "Pick an existing municipality or type the name of a new one.": "Pick an existing municipality or type the name of a new one.", - "Pick an existing organisation of type Municipality, or type a new name and press Enter.": "Pick an existing organisation of type Municipality, or type a new name and press Enter.", "Pick an organisation of type Municipality, or type the name of a new one.": "Pick an organisation of type Municipality, or type the name of a new one.", "Reasons and warnings": "Reasons and warnings", "Records missing from the export can only be kept.": "Records missing from the export can only be kept.", @@ -1091,8 +1089,6 @@ OC.L10N.register( "The file is larger than the server accepts.": "The file is larger than the server accepts.", "A source sheet may hold at most {limit} rows. Split the export and import the parts one after the other.": "A source sheet may hold at most {limit} rows. Split the export and import the parts one after the other.", "Split the export and import the parts one after the other.": "Split the export and import the parts one after the other.", - "Excel workbook (.xlsx) with the sheet \"{first}\" or \"{second}\". By default the file may be at most {size}.": "Excel workbook (.xlsx) with the sheet \"{first}\" or \"{second}\". By default the file may be at most {size}.", - "The sheets \"{first}\" (applications without arranged maintenance) and \"{second}\" (with arranged maintenance) are read; other sheets, including the \"Invoer\" sheets, are ignored.": "The sheets \"{first}\" (applications without arranged maintenance) and \"{second}\" (with arranged maintenance) are read; other sheets, including the \"Invoer\" sheets, are ignored.", "The page got no answer from the import.": "The page got no answer from the import.", "The connection was cut off before the import answered, so it may still be running or may have finished. Wait a few minutes and check the applications of the municipality before you import again. Importing the same file again creates no duplicates.": "The connection was cut off before the import answered, so it may still be running or may have finished. Wait a few minutes and check the applications of the municipality before you import again. Importing the same file again creates no duplicates.", "The import cannot be cancelled now: the server has not started its rows yet, or has already finished them. If the import keeps running, press Cancel import again in a moment.": "The import cannot be cancelled now: the server has not started its rows yet, or has already finished them. If the import keeps running, press Cancel import again in a moment.", @@ -1131,12 +1127,61 @@ OC.L10N.register( "Split the rows over two copies of the export and import them one after the other.": "Split the rows over two copies of the export and import them one after the other.", "The sheet \"{sheet}\" is too large to import: unpacked it is {size}, and the import reads at most {limit} of one sheet.": "The sheet \"{sheet}\" is too large to import: unpacked it is {size}, and the import reads at most {limit} of one sheet.", "The part {part} of the workbook is too large to import: unpacked it is {size}, and the import reads at most {limit} of one part.": "The part {part} of the workbook is too large to import: unpacked it is {size}, and the import reads at most {limit} of one part.", - "Delete the sheets the import does not read from a copy of the export (it reads only \"{first}\" and \"{second}\"), and import that copy.": "Delete the sheets the import does not read from a copy of the export (it reads only \"{first}\" and \"{second}\"), and import that copy.", "The workbook holds more than {count} different texts, the most the import reads.": "The workbook holds more than {count} different texts, the most the import reads.", "Together, the cells of the workbook reference more than {size} of shared text, the most the import reads.": "Together, the cells of the workbook reference more than {size} of shared text, the most the import reads.", "In development since": "In development since", "In use since": "In use since", - "The import mapping could not be read. The details are in the Nextcloud log.": "The import mapping could not be read. The details are in the Nextcloud log." + "Application (module)": "Application (module)", + "Supplier organisation (Vendor)": "Supplier organisation (Vendor)", + "Business owner (contact person)": "Business owner (contact person)", + "Loading the mapping…": "Loading the mapping…", + "The mapping could not be loaded.": "The mapping could not be loaded.", + "Sheets read": "Sheets read", + "Match column": "Match column", + "Name column": "Name column", + "Required columns": "Required columns", + "Date columns": "Date columns", + "The import mapping could not be read. The details are in the Nextcloud log.": "The import mapping could not be read. The details are in the Nextcloud log.", + "Mapping": "Mapping", + "OpenRegister's pack store is not available, so the import uses the mapping shipped with stackiq. Update or enable OpenRegister to view and edit the mapping there.": "OpenRegister's pack store is not available, so the import uses the mapping shipped with stackiq. Update or enable OpenRegister to view and edit the mapping there.", + "OpenRegister": "OpenRegister", + "Shipped file": "Shipped file", + "Version {version}": "Version {version}", + "Version {version} (shipped: {shipped})": "Version {version} (shipped: {shipped})", + "Edited": "Edited", + "Edit in OpenRegister": "Edit in OpenRegister", + "Edit in OpenRegister: {target} (opens in a new tab)": "Edit in OpenRegister: {target} (opens in a new tab)", + "Stackiq ships a newer version of this pack ({version}). It was not applied, because the pack was edited in OpenRegister. Delete the pack there to use the shipped version from the next import on; your edits are then lost.": "Stackiq ships a newer version of this pack ({version}). It was not applied, because the pack was edited in OpenRegister. Delete the pack there to use the shipped version from the next import on; your edits are then lost.", + "Excel workbook (.xlsx) with the sheet {sheets}. By default the file may be at most {size}.": "Excel workbook (.xlsx) with the sheet {sheets}. By default the file may be at most {size}.", + "Excel workbook (.xlsx) with one of the sheets {sheets}. By default the file may be at most {size}.": "Excel workbook (.xlsx) with one of the sheets {sheets}. By default the file may be at most {size}.", + "The sheet {sheets} is read; other sheets, including the \"Invoer\" sheets, are ignored.": "The sheet {sheets} is read; other sheets, including the \"Invoer\" sheets, are ignored.", + "The sheets {sheets} are read; other sheets, including the \"Invoer\" sheets, are ignored.": "The sheets {sheets} are read; other sheets, including the \"Invoer\" sheets, are ignored.", + "Expected the sheet {sheets}. Sheet names must match exactly.": "Expected the sheet {sheets}. Sheet names must match exactly.", + "Expected one of the sheets {sheets}. Sheet names must match exactly.": "Expected one of the sheets {sheets}. Sheet names must match exactly.", + "Delete the sheets the import does not read from a copy of the export (it reads only {sheets}), and import that copy.": "Delete the sheets the import does not read from a copy of the export (it reads only {sheets}), and import that copy.", + "A pack deleted in OpenRegister is restored from the shipped file at the next import.": "A pack deleted in OpenRegister is restored from the shipped file at the next import.", + "Choose a municipality, or type a new name and press Enter.": "Choose a municipality, or type a new name and press Enter.", + "Kind of record": "Kind of record", + "New applications become visible to everyone, also without logging in.": "New applications become visible to everyone, also without logging in.", + "Overwrites applications imported before with the values from the export. The (i) button above lists the fields.": "Overwrites applications imported before with the values from the export. The (i) button above lists the fields.", + "Stored in": "Stored in", + "The export": "The export", + "Which columns of the export the next import reads, and where each column goes.": "Which columns of the export the next import reads, and where each column goes.", + "Packs": "Packs", + "Update pending: shipped {version} replaces stored {storedVersion} at the next import.": "Update pending: shipped {version} replaces stored {storedVersion} at the next import.", + "Columns and fields: {target}": "Columns and fields: {target}", + "Which column of the export goes to which field": "Which column of the export goes to which field", + "Column of the export": "Column of the export", + "Field": "Field", + "Transform": "Transform", + "Yes": "Yes", + "No": "No", + "What the import writes": "What the import writes", + "Set on every record": "Set on every record", + "Set on a new record only": "Set on a new record only", + "Written on a new record, or on an existing one only when empty": "Written on a new record, or on an existing one only when empty", + "Never written on an existing record": "Never written on an existing record", + "The mapping could not be loaded. Open Mapping for the reason.": "The mapping could not be loaded. Open Mapping for the reason." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/en.json b/l10n/en.json index 0ed57538e..247f79a2a 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -973,7 +973,6 @@ "Error code: {code}": "Error code: {code}", "Excel workbook (.xlsx), at most 10 MB, with the sheet \"Onbeh Applicaties CMDB\" or \"Beheerde Applicaties CMDB\".": "Excel workbook (.xlsx), at most 10 MB, with the sheet \"Onbeh Applicaties CMDB\" or \"Beheerde Applicaties CMDB\".", "Existing municipalities could not be loaded. You can still type the name of a municipality.": "Existing municipalities could not be loaded. You can still type the name of a municipality.", - "Expected a sheet named \"{first}\" or \"{second}\". Sheet names must match exactly.": "Expected a sheet named \"{first}\" or \"{second}\". Sheet names must match exactly.", "Failed": "Failed", "Import": "Import", "Import a TOPdesk CMDB export (.xlsx) as the applications one municipality uses": "Import a TOPdesk CMDB export (.xlsx) as the applications one municipality uses", @@ -990,10 +989,9 @@ "No rows with this outcome": "No rows with this outcome", "Nothing more is known on this page; the Nextcloud log has the details.": "Nothing more is known on this page; the Nextcloud log has the details.", "Only Nextcloud administrators can import a CMDB export.": "Only Nextcloud administrators can import a CMDB export.", - "OpenRegister's mapping engine is missing or a mapping file is invalid. Update OpenRegister and check the Nextcloud log.": "OpenRegister's mapping engine is missing or a mapping file is invalid. Update OpenRegister and check the Nextcloud log.", + "OpenRegister's mapping engine is missing, or a mapping pack is invalid or breaks a rule of the import. The reason below names the pack: correct it in OpenRegister, or check the Nextcloud log.": "OpenRegister's mapping engine is missing, or a mapping pack is invalid or breaks a rule of the import. The reason below names the pack: correct it in OpenRegister, or check the Nextcloud log.", "Outcome": "Outcome", "Pick an existing municipality or type the name of a new one.": "Pick an existing municipality or type the name of a new one.", - "Pick an existing organisation of type Municipality, or type a new name and press Enter.": "Pick an existing organisation of type Municipality, or type a new name and press Enter.", "Pick an organisation of type Municipality, or type the name of a new one.": "Pick an organisation of type Municipality, or type the name of a new one.", "Reasons and warnings": "Reasons and warnings", "Records missing from the export can only be kept.": "Records missing from the export can only be kept.", @@ -1090,8 +1088,6 @@ "The file is larger than the server accepts.": "The file is larger than the server accepts.", "A source sheet may hold at most {limit} rows. Split the export and import the parts one after the other.": "A source sheet may hold at most {limit} rows. Split the export and import the parts one after the other.", "Split the export and import the parts one after the other.": "Split the export and import the parts one after the other.", - "Excel workbook (.xlsx) with the sheet \"{first}\" or \"{second}\". By default the file may be at most {size}.": "Excel workbook (.xlsx) with the sheet \"{first}\" or \"{second}\". By default the file may be at most {size}.", - "The sheets \"{first}\" (applications without arranged maintenance) and \"{second}\" (with arranged maintenance) are read; other sheets, including the \"Invoer\" sheets, are ignored.": "The sheets \"{first}\" (applications without arranged maintenance) and \"{second}\" (with arranged maintenance) are read; other sheets, including the \"Invoer\" sheets, are ignored.", "The page got no answer from the import.": "The page got no answer from the import.", "The connection was cut off before the import answered, so it may still be running or may have finished. Wait a few minutes and check the applications of the municipality before you import again. Importing the same file again creates no duplicates.": "The connection was cut off before the import answered, so it may still be running or may have finished. Wait a few minutes and check the applications of the municipality before you import again. Importing the same file again creates no duplicates.", "The import cannot be cancelled now: the server has not started its rows yet, or has already finished them. If the import keeps running, press Cancel import again in a moment.": "The import cannot be cancelled now: the server has not started its rows yet, or has already finished them. If the import keeps running, press Cancel import again in a moment.", @@ -1130,11 +1126,60 @@ "Split the rows over two copies of the export and import them one after the other.": "Split the rows over two copies of the export and import them one after the other.", "The sheet \"{sheet}\" is too large to import: unpacked it is {size}, and the import reads at most {limit} of one sheet.": "The sheet \"{sheet}\" is too large to import: unpacked it is {size}, and the import reads at most {limit} of one sheet.", "The part {part} of the workbook is too large to import: unpacked it is {size}, and the import reads at most {limit} of one part.": "The part {part} of the workbook is too large to import: unpacked it is {size}, and the import reads at most {limit} of one part.", - "Delete the sheets the import does not read from a copy of the export (it reads only \"{first}\" and \"{second}\"), and import that copy.": "Delete the sheets the import does not read from a copy of the export (it reads only \"{first}\" and \"{second}\"), and import that copy.", "The workbook holds more than {count} different texts, the most the import reads.": "The workbook holds more than {count} different texts, the most the import reads.", "Together, the cells of the workbook reference more than {size} of shared text, the most the import reads.": "Together, the cells of the workbook reference more than {size} of shared text, the most the import reads.", "In development since": "In development since", "In use since": "In use since", - "The import mapping could not be read. The details are in the Nextcloud log.": "The import mapping could not be read. The details are in the Nextcloud log." + "Application (module)": "Application (module)", + "Supplier organisation (Vendor)": "Supplier organisation (Vendor)", + "Business owner (contact person)": "Business owner (contact person)", + "Loading the mapping…": "Loading the mapping…", + "The mapping could not be loaded.": "The mapping could not be loaded.", + "Sheets read": "Sheets read", + "Match column": "Match column", + "Name column": "Name column", + "Required columns": "Required columns", + "Date columns": "Date columns", + "The import mapping could not be read. The details are in the Nextcloud log.": "The import mapping could not be read. The details are in the Nextcloud log.", + "Mapping": "Mapping", + "OpenRegister's pack store is not available, so the import uses the mapping shipped with stackiq. Update or enable OpenRegister to view and edit the mapping there.": "OpenRegister's pack store is not available, so the import uses the mapping shipped with stackiq. Update or enable OpenRegister to view and edit the mapping there.", + "OpenRegister": "OpenRegister", + "Shipped file": "Shipped file", + "Version {version}": "Version {version}", + "Version {version} (shipped: {shipped})": "Version {version} (shipped: {shipped})", + "Edited": "Edited", + "Edit in OpenRegister": "Edit in OpenRegister", + "Edit in OpenRegister: {target} (opens in a new tab)": "Edit in OpenRegister: {target} (opens in a new tab)", + "Stackiq ships a newer version of this pack ({version}). It was not applied, because the pack was edited in OpenRegister. Delete the pack there to use the shipped version from the next import on; your edits are then lost.": "Stackiq ships a newer version of this pack ({version}). It was not applied, because the pack was edited in OpenRegister. Delete the pack there to use the shipped version from the next import on; your edits are then lost.", + "Excel workbook (.xlsx) with the sheet {sheets}. By default the file may be at most {size}.": "Excel workbook (.xlsx) with the sheet {sheets}. By default the file may be at most {size}.", + "Excel workbook (.xlsx) with one of the sheets {sheets}. By default the file may be at most {size}.": "Excel workbook (.xlsx) with one of the sheets {sheets}. By default the file may be at most {size}.", + "The sheet {sheets} is read; other sheets, including the \"Invoer\" sheets, are ignored.": "The sheet {sheets} is read; other sheets, including the \"Invoer\" sheets, are ignored.", + "The sheets {sheets} are read; other sheets, including the \"Invoer\" sheets, are ignored.": "The sheets {sheets} are read; other sheets, including the \"Invoer\" sheets, are ignored.", + "Expected the sheet {sheets}. Sheet names must match exactly.": "Expected the sheet {sheets}. Sheet names must match exactly.", + "Expected one of the sheets {sheets}. Sheet names must match exactly.": "Expected one of the sheets {sheets}. Sheet names must match exactly.", + "Delete the sheets the import does not read from a copy of the export (it reads only {sheets}), and import that copy.": "Delete the sheets the import does not read from a copy of the export (it reads only {sheets}), and import that copy.", + "A pack deleted in OpenRegister is restored from the shipped file at the next import.": "A pack deleted in OpenRegister is restored from the shipped file at the next import.", + "Choose a municipality, or type a new name and press Enter.": "Choose a municipality, or type a new name and press Enter.", + "Kind of record": "Kind of record", + "New applications become visible to everyone, also without logging in.": "New applications become visible to everyone, also without logging in.", + "Overwrites applications imported before with the values from the export. The (i) button above lists the fields.": "Overwrites applications imported before with the values from the export. The (i) button above lists the fields.", + "Stored in": "Stored in", + "The export": "The export", + "Which columns of the export the next import reads, and where each column goes.": "Which columns of the export the next import reads, and where each column goes.", + "Packs": "Packs", + "Update pending: shipped {version} replaces stored {storedVersion} at the next import.": "Update pending: shipped {version} replaces stored {storedVersion} at the next import.", + "Columns and fields: {target}": "Columns and fields: {target}", + "Which column of the export goes to which field": "Which column of the export goes to which field", + "Column of the export": "Column of the export", + "Field": "Field", + "Transform": "Transform", + "Yes": "Yes", + "No": "No", + "What the import writes": "What the import writes", + "Set on every record": "Set on every record", + "Set on a new record only": "Set on a new record only", + "Written on a new record, or on an existing one only when empty": "Written on a new record, or on an existing one only when empty", + "Never written on an existing record": "Never written on an existing record", + "The mapping could not be loaded. Open Mapping for the reason.": "The mapping could not be loaded. Open Mapping for the reason." } } diff --git a/l10n/nl.js b/l10n/nl.js index 3f2953500..84e936752 100644 --- a/l10n/nl.js +++ b/l10n/nl.js @@ -1042,7 +1042,6 @@ OC.L10N.register( "Error code: {code}": "Foutcode: {code}", "Excel workbook (.xlsx), at most 10 MB, with the sheet \"Onbeh Applicaties CMDB\" or \"Beheerde Applicaties CMDB\".": "Excel-werkmap (.xlsx), hoogstens 10 MB, met het tabblad \"Onbeh Applicaties CMDB\" of \"Beheerde Applicaties CMDB\".", "Existing municipalities could not be loaded. You can still type the name of a municipality.": "Bestaande gemeenten konden niet worden geladen. U kunt nog steeds de naam van een gemeente typen.", - "Expected a sheet named \"{first}\" or \"{second}\". Sheet names must match exactly.": "Verwacht werd een tabblad met de naam \"{first}\" of \"{second}\". De naam van het tabblad moet precies overeenkomen.", "Failed": "Mislukt", "Import": "Importeren", "Import a TOPdesk CMDB export (.xlsx) as the applications one municipality uses": "Importeer een TOPdesk CMDB-export (.xlsx) als de applicaties die één gemeente gebruikt", @@ -1059,10 +1058,9 @@ OC.L10N.register( "No rows with this outcome": "Geen rijen met dit resultaat", "Nothing more is known on this page; the Nextcloud log has the details.": "Op deze pagina is niet meer bekend; het Nextcloud-logboek bevat de details.", "Only Nextcloud administrators can import a CMDB export.": "Alleen Nextcloud-beheerders kunnen een CMDB-export importeren.", - "OpenRegister's mapping engine is missing or a mapping file is invalid. Update OpenRegister and check the Nextcloud log.": "De mapping-engine van OpenRegister ontbreekt of een mappingbestand is ongeldig. Werk OpenRegister bij en bekijk het Nextcloud-logboek.", + "OpenRegister's mapping engine is missing, or a mapping pack is invalid or breaks a rule of the import. The reason below names the pack: correct it in OpenRegister, or check the Nextcloud log.": "De mapping-engine van OpenRegister ontbreekt, of een mappingpakket is ongeldig of breekt een regel van de import. De reden hieronder noemt het pakket: herstel het in OpenRegister, of bekijk het Nextcloud-logboek.", "Outcome": "Resultaat", "Pick an existing municipality or type the name of a new one.": "Kies een bestaande gemeente of typ de naam van een nieuwe.", - "Pick an existing organisation of type Municipality, or type a new name and press Enter.": "Kies een bestaande organisatie van het type Gemeente, of typ een nieuwe naam en druk op Enter.", "Pick an organisation of type Municipality, or type the name of a new one.": "Kies een organisatie van het type Gemeente, of typ de naam van een nieuwe.", "Reasons and warnings": "Redenen en waarschuwingen", "Records missing from the export can only be kept.": "Records die in de export ontbreken, kunnen alleen worden behouden.", @@ -1159,8 +1157,6 @@ OC.L10N.register( "The file is larger than the server accepts.": "Het bestand is groter dan de server accepteert.", "A source sheet may hold at most {limit} rows. Split the export and import the parts one after the other.": "Een brontabblad mag hoogstens {limit} rijen bevatten. Splits de export en importeer de delen na elkaar.", "Split the export and import the parts one after the other.": "Splits de export en importeer de delen na elkaar.", - "Excel workbook (.xlsx) with the sheet \"{first}\" or \"{second}\". By default the file may be at most {size}.": "Excel-werkmap (.xlsx) met het tabblad \"{first}\" of \"{second}\". Standaard mag het bestand hoogstens {size} zijn.", - "The sheets \"{first}\" (applications without arranged maintenance) and \"{second}\" (with arranged maintenance) are read; other sheets, including the \"Invoer\" sheets, are ignored.": "De tabbladen \"{first}\" (applicaties zonder geregeld beheer) en \"{second}\" (met geregeld beheer) worden gelezen; andere tabbladen, ook de \"Invoer\"-tabbladen, worden genegeerd.", "The page got no answer from the import.": "De pagina kreeg geen antwoord van de import.", "The connection was cut off before the import answered, so it may still be running or may have finished. Wait a few minutes and check the applications of the municipality before you import again. Importing the same file again creates no duplicates.": "De verbinding werd verbroken voordat de import antwoordde, dus de import kan nog bezig zijn of al klaar zijn. Wacht een paar minuten en controleer de applicaties van de gemeente voordat u opnieuw importeert. Hetzelfde bestand opnieuw importeren maakt geen dubbele records aan.", "The import cannot be cancelled now: the server has not started its rows yet, or has already finished them. If the import keeps running, press Cancel import again in a moment.": "De import kan nu niet worden geannuleerd: de server is nog niet aan de rijen begonnen, of is er al mee klaar. Loopt de import door, druk dan zo dadelijk opnieuw op Import annuleren.", @@ -1199,12 +1195,61 @@ OC.L10N.register( "Split the rows over two copies of the export and import them one after the other.": "Verdeel de rijen over twee kopieën van de export en importeer die na elkaar.", "The sheet \"{sheet}\" is too large to import: unpacked it is {size}, and the import reads at most {limit} of one sheet.": "Het tabblad \"{sheet}\" is te groot om te importeren: uitgepakt is het {size}, en de import leest van één tabblad hooguit {limit}.", "The part {part} of the workbook is too large to import: unpacked it is {size}, and the import reads at most {limit} of one part.": "Het onderdeel {part} van de werkmap is te groot om te importeren: uitgepakt is het {size}, en de import leest van één onderdeel hooguit {limit}.", - "Delete the sheets the import does not read from a copy of the export (it reads only \"{first}\" and \"{second}\"), and import that copy.": "Verwijder in een kopie van de export de tabbladen die de import niet leest (hij leest alleen \"{first}\" en \"{second}\"), en importeer die kopie.", "The workbook holds more than {count} different texts, the most the import reads.": "De werkmap bevat meer dan {count} verschillende teksten, het maximum dat de import leest.", "Together, the cells of the workbook reference more than {size} of shared text, the most the import reads.": "Samen verwijzen de cellen van de werkmap naar meer dan {size} gedeelde tekst, het maximum dat de import leest.", "In development since": "In ontwikkeling sinds", "In use since": "In gebruik sinds", - "The import mapping could not be read. The details are in the Nextcloud log.": "De mapping van de import kon niet worden gelezen. De details staan in het Nextcloud-logboek." + "Application (module)": "Applicatie (module)", + "Supplier organisation (Vendor)": "Leveranciersorganisatie (Vendor)", + "Business owner (contact person)": "Business owner (contactpersoon)", + "Loading the mapping…": "Mapping laden…", + "The mapping could not be loaded.": "De mapping kon niet worden geladen.", + "Sheets read": "Gelezen tabbladen", + "Match column": "Koppelkolom", + "Name column": "Naamkolom", + "Required columns": "Verplichte kolommen", + "Date columns": "Datumkolommen", + "The import mapping could not be read. The details are in the Nextcloud log.": "De mapping van de import kon niet worden gelezen. De details staan in het Nextcloud-logboek.", + "Mapping": "Mapping", + "OpenRegister's pack store is not available, so the import uses the mapping shipped with stackiq. Update or enable OpenRegister to view and edit the mapping there.": "De packopslag van OpenRegister is niet beschikbaar, dus de import gebruikt de mapping die met stackiq wordt meegeleverd. Werk OpenRegister bij of schakel het in om de mapping daar te bekijken en te bewerken.", + "OpenRegister": "OpenRegister", + "Shipped file": "Meegeleverd bestand", + "Version {version}": "Versie {version}", + "Version {version} (shipped: {shipped})": "Versie {version} (meegeleverd: {shipped})", + "Edited": "Bewerkt", + "Edit in OpenRegister": "Bewerken in OpenRegister", + "Edit in OpenRegister: {target} (opens in a new tab)": "Bewerken in OpenRegister: {target} (opent in een nieuw tabblad)", + "Stackiq ships a newer version of this pack ({version}). It was not applied, because the pack was edited in OpenRegister. Delete the pack there to use the shipped version from the next import on; your edits are then lost.": "Stackiq levert een nieuwere versie van dit pack mee ({version}). Die is niet toegepast, omdat het pack in OpenRegister is bewerkt. Verwijder het pack daar om vanaf de volgende import de meegeleverde versie te gebruiken; de wijzigingen gaan dan verloren.", + "Excel workbook (.xlsx) with the sheet {sheets}. By default the file may be at most {size}.": "Excel-werkmap (.xlsx) met het tabblad {sheets}. Standaard mag het bestand hoogstens {size} zijn.", + "Excel workbook (.xlsx) with one of the sheets {sheets}. By default the file may be at most {size}.": "Excel-werkmap (.xlsx) met een van de tabbladen {sheets}. Standaard mag het bestand hoogstens {size} zijn.", + "The sheet {sheets} is read; other sheets, including the \"Invoer\" sheets, are ignored.": "Het tabblad {sheets} wordt gelezen; andere tabbladen, ook de \"Invoer\"-tabbladen, worden genegeerd.", + "The sheets {sheets} are read; other sheets, including the \"Invoer\" sheets, are ignored.": "De tabbladen {sheets} worden gelezen; andere tabbladen, ook de \"Invoer\"-tabbladen, worden genegeerd.", + "Expected the sheet {sheets}. Sheet names must match exactly.": "Verwacht werd het tabblad {sheets}. De naam van het tabblad moet precies overeenkomen.", + "Expected one of the sheets {sheets}. Sheet names must match exactly.": "Verwacht werd een van de tabbladen {sheets}. De namen van de tabbladen moeten precies overeenkomen.", + "Delete the sheets the import does not read from a copy of the export (it reads only {sheets}), and import that copy.": "Verwijder in een kopie van de export de tabbladen die de import niet leest (hij leest alleen {sheets}), en importeer die kopie.", + "A pack deleted in OpenRegister is restored from the shipped file at the next import.": "Een pack dat in OpenRegister is verwijderd, wordt bij de volgende import hersteld uit het meegeleverde bestand.", + "Choose a municipality, or type a new name and press Enter.": "Kies een gemeente, of typ een nieuwe naam en druk op Enter.", + "Kind of record": "Soort gegeven", + "New applications become visible to everyone, also without logging in.": "Nieuwe applicaties worden voor iedereen zichtbaar, ook zonder in te loggen.", + "Overwrites applications imported before with the values from the export. The (i) button above lists the fields.": "Overschrijft eerder geïmporteerde applicaties met de waarden uit de export. De (i)-knop bovenaan noemt de velden.", + "Stored in": "Opgeslagen in", + "The export": "De export", + "Which columns of the export the next import reads, and where each column goes.": "Welke kolommen van de export de volgende import leest, en waar elke kolom heen gaat.", + "Packs": "Packs", + "Update pending: shipped {version} replaces stored {storedVersion} at the next import.": "Update in afwachting: de meegeleverde versie {version} vervangt de opgeslagen versie {storedVersion} bij de volgende import.", + "Columns and fields: {target}": "Kolommen en velden: {target}", + "Which column of the export goes to which field": "Welke kolom van de export naar welk veld gaat", + "Column of the export": "Kolom van de export", + "Field": "Veld", + "Transform": "Omzetting", + "Yes": "Ja", + "No": "Nee", + "What the import writes": "Wat de import schrijft", + "Set on every record": "Op elk record gezet", + "Set on a new record only": "Alleen op een nieuw record gezet", + "Written on a new record, or on an existing one only when empty": "Op een nieuw record geschreven, of op een bestaand record alleen als de waarde leeg is", + "Never written on an existing record": "Nooit geschreven op een bestaand record", + "The mapping could not be loaded. Open Mapping for the reason.": "De mapping kon niet worden geladen. Open Mapping voor de reden." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/nl.json b/l10n/nl.json index ca9c80731..2c5238793 100644 --- a/l10n/nl.json +++ b/l10n/nl.json @@ -1041,7 +1041,6 @@ "Error code: {code}": "Foutcode: {code}", "Excel workbook (.xlsx), at most 10 MB, with the sheet \"Onbeh Applicaties CMDB\" or \"Beheerde Applicaties CMDB\".": "Excel-werkmap (.xlsx), hoogstens 10 MB, met het tabblad \"Onbeh Applicaties CMDB\" of \"Beheerde Applicaties CMDB\".", "Existing municipalities could not be loaded. You can still type the name of a municipality.": "Bestaande gemeenten konden niet worden geladen. U kunt nog steeds de naam van een gemeente typen.", - "Expected a sheet named \"{first}\" or \"{second}\". Sheet names must match exactly.": "Verwacht werd een tabblad met de naam \"{first}\" of \"{second}\". De naam van het tabblad moet precies overeenkomen.", "Failed": "Mislukt", "Import": "Importeren", "Import a TOPdesk CMDB export (.xlsx) as the applications one municipality uses": "Importeer een TOPdesk CMDB-export (.xlsx) als de applicaties die één gemeente gebruikt", @@ -1058,10 +1057,9 @@ "No rows with this outcome": "Geen rijen met dit resultaat", "Nothing more is known on this page; the Nextcloud log has the details.": "Op deze pagina is niet meer bekend; het Nextcloud-logboek bevat de details.", "Only Nextcloud administrators can import a CMDB export.": "Alleen Nextcloud-beheerders kunnen een CMDB-export importeren.", - "OpenRegister's mapping engine is missing or a mapping file is invalid. Update OpenRegister and check the Nextcloud log.": "De mapping-engine van OpenRegister ontbreekt of een mappingbestand is ongeldig. Werk OpenRegister bij en bekijk het Nextcloud-logboek.", + "OpenRegister's mapping engine is missing, or a mapping pack is invalid or breaks a rule of the import. The reason below names the pack: correct it in OpenRegister, or check the Nextcloud log.": "De mapping-engine van OpenRegister ontbreekt, of een mappingpakket is ongeldig of breekt een regel van de import. De reden hieronder noemt het pakket: herstel het in OpenRegister, of bekijk het Nextcloud-logboek.", "Outcome": "Resultaat", "Pick an existing municipality or type the name of a new one.": "Kies een bestaande gemeente of typ de naam van een nieuwe.", - "Pick an existing organisation of type Municipality, or type a new name and press Enter.": "Kies een bestaande organisatie van het type Gemeente, of typ een nieuwe naam en druk op Enter.", "Pick an organisation of type Municipality, or type the name of a new one.": "Kies een organisatie van het type Gemeente, of typ de naam van een nieuwe.", "Reasons and warnings": "Redenen en waarschuwingen", "Records missing from the export can only be kept.": "Records die in de export ontbreken, kunnen alleen worden behouden.", @@ -1158,8 +1156,6 @@ "The file is larger than the server accepts.": "Het bestand is groter dan de server accepteert.", "A source sheet may hold at most {limit} rows. Split the export and import the parts one after the other.": "Een brontabblad mag hoogstens {limit} rijen bevatten. Splits de export en importeer de delen na elkaar.", "Split the export and import the parts one after the other.": "Splits de export en importeer de delen na elkaar.", - "Excel workbook (.xlsx) with the sheet \"{first}\" or \"{second}\". By default the file may be at most {size}.": "Excel-werkmap (.xlsx) met het tabblad \"{first}\" of \"{second}\". Standaard mag het bestand hoogstens {size} zijn.", - "The sheets \"{first}\" (applications without arranged maintenance) and \"{second}\" (with arranged maintenance) are read; other sheets, including the \"Invoer\" sheets, are ignored.": "De tabbladen \"{first}\" (applicaties zonder geregeld beheer) en \"{second}\" (met geregeld beheer) worden gelezen; andere tabbladen, ook de \"Invoer\"-tabbladen, worden genegeerd.", "The page got no answer from the import.": "De pagina kreeg geen antwoord van de import.", "The connection was cut off before the import answered, so it may still be running or may have finished. Wait a few minutes and check the applications of the municipality before you import again. Importing the same file again creates no duplicates.": "De verbinding werd verbroken voordat de import antwoordde, dus de import kan nog bezig zijn of al klaar zijn. Wacht een paar minuten en controleer de applicaties van de gemeente voordat u opnieuw importeert. Hetzelfde bestand opnieuw importeren maakt geen dubbele records aan.", "The import cannot be cancelled now: the server has not started its rows yet, or has already finished them. If the import keeps running, press Cancel import again in a moment.": "De import kan nu niet worden geannuleerd: de server is nog niet aan de rijen begonnen, of is er al mee klaar. Loopt de import door, druk dan zo dadelijk opnieuw op Import annuleren.", @@ -1198,11 +1194,60 @@ "Split the rows over two copies of the export and import them one after the other.": "Verdeel de rijen over twee kopieën van de export en importeer die na elkaar.", "The sheet \"{sheet}\" is too large to import: unpacked it is {size}, and the import reads at most {limit} of one sheet.": "Het tabblad \"{sheet}\" is te groot om te importeren: uitgepakt is het {size}, en de import leest van één tabblad hooguit {limit}.", "The part {part} of the workbook is too large to import: unpacked it is {size}, and the import reads at most {limit} of one part.": "Het onderdeel {part} van de werkmap is te groot om te importeren: uitgepakt is het {size}, en de import leest van één onderdeel hooguit {limit}.", - "Delete the sheets the import does not read from a copy of the export (it reads only \"{first}\" and \"{second}\"), and import that copy.": "Verwijder in een kopie van de export de tabbladen die de import niet leest (hij leest alleen \"{first}\" en \"{second}\"), en importeer die kopie.", "The workbook holds more than {count} different texts, the most the import reads.": "De werkmap bevat meer dan {count} verschillende teksten, het maximum dat de import leest.", "Together, the cells of the workbook reference more than {size} of shared text, the most the import reads.": "Samen verwijzen de cellen van de werkmap naar meer dan {size} gedeelde tekst, het maximum dat de import leest.", "In development since": "In ontwikkeling sinds", "In use since": "In gebruik sinds", - "The import mapping could not be read. The details are in the Nextcloud log.": "De mapping van de import kon niet worden gelezen. De details staan in het Nextcloud-logboek." + "Application (module)": "Applicatie (module)", + "Supplier organisation (Vendor)": "Leveranciersorganisatie (Vendor)", + "Business owner (contact person)": "Business owner (contactpersoon)", + "Loading the mapping…": "Mapping laden…", + "The mapping could not be loaded.": "De mapping kon niet worden geladen.", + "Sheets read": "Gelezen tabbladen", + "Match column": "Koppelkolom", + "Name column": "Naamkolom", + "Required columns": "Verplichte kolommen", + "Date columns": "Datumkolommen", + "The import mapping could not be read. The details are in the Nextcloud log.": "De mapping van de import kon niet worden gelezen. De details staan in het Nextcloud-logboek.", + "Mapping": "Mapping", + "OpenRegister's pack store is not available, so the import uses the mapping shipped with stackiq. Update or enable OpenRegister to view and edit the mapping there.": "De packopslag van OpenRegister is niet beschikbaar, dus de import gebruikt de mapping die met stackiq wordt meegeleverd. Werk OpenRegister bij of schakel het in om de mapping daar te bekijken en te bewerken.", + "OpenRegister": "OpenRegister", + "Shipped file": "Meegeleverd bestand", + "Version {version}": "Versie {version}", + "Version {version} (shipped: {shipped})": "Versie {version} (meegeleverd: {shipped})", + "Edited": "Bewerkt", + "Edit in OpenRegister": "Bewerken in OpenRegister", + "Edit in OpenRegister: {target} (opens in a new tab)": "Bewerken in OpenRegister: {target} (opent in een nieuw tabblad)", + "Stackiq ships a newer version of this pack ({version}). It was not applied, because the pack was edited in OpenRegister. Delete the pack there to use the shipped version from the next import on; your edits are then lost.": "Stackiq levert een nieuwere versie van dit pack mee ({version}). Die is niet toegepast, omdat het pack in OpenRegister is bewerkt. Verwijder het pack daar om vanaf de volgende import de meegeleverde versie te gebruiken; de wijzigingen gaan dan verloren.", + "Excel workbook (.xlsx) with the sheet {sheets}. By default the file may be at most {size}.": "Excel-werkmap (.xlsx) met het tabblad {sheets}. Standaard mag het bestand hoogstens {size} zijn.", + "Excel workbook (.xlsx) with one of the sheets {sheets}. By default the file may be at most {size}.": "Excel-werkmap (.xlsx) met een van de tabbladen {sheets}. Standaard mag het bestand hoogstens {size} zijn.", + "The sheet {sheets} is read; other sheets, including the \"Invoer\" sheets, are ignored.": "Het tabblad {sheets} wordt gelezen; andere tabbladen, ook de \"Invoer\"-tabbladen, worden genegeerd.", + "The sheets {sheets} are read; other sheets, including the \"Invoer\" sheets, are ignored.": "De tabbladen {sheets} worden gelezen; andere tabbladen, ook de \"Invoer\"-tabbladen, worden genegeerd.", + "Expected the sheet {sheets}. Sheet names must match exactly.": "Verwacht werd het tabblad {sheets}. De naam van het tabblad moet precies overeenkomen.", + "Expected one of the sheets {sheets}. Sheet names must match exactly.": "Verwacht werd een van de tabbladen {sheets}. De namen van de tabbladen moeten precies overeenkomen.", + "Delete the sheets the import does not read from a copy of the export (it reads only {sheets}), and import that copy.": "Verwijder in een kopie van de export de tabbladen die de import niet leest (hij leest alleen {sheets}), en importeer die kopie.", + "A pack deleted in OpenRegister is restored from the shipped file at the next import.": "Een pack dat in OpenRegister is verwijderd, wordt bij de volgende import hersteld uit het meegeleverde bestand.", + "Choose a municipality, or type a new name and press Enter.": "Kies een gemeente, of typ een nieuwe naam en druk op Enter.", + "Kind of record": "Soort gegeven", + "New applications become visible to everyone, also without logging in.": "Nieuwe applicaties worden voor iedereen zichtbaar, ook zonder in te loggen.", + "Overwrites applications imported before with the values from the export. The (i) button above lists the fields.": "Overschrijft eerder geïmporteerde applicaties met de waarden uit de export. De (i)-knop bovenaan noemt de velden.", + "Stored in": "Opgeslagen in", + "The export": "De export", + "Which columns of the export the next import reads, and where each column goes.": "Welke kolommen van de export de volgende import leest, en waar elke kolom heen gaat.", + "Packs": "Packs", + "Update pending: shipped {version} replaces stored {storedVersion} at the next import.": "Update in afwachting: de meegeleverde versie {version} vervangt de opgeslagen versie {storedVersion} bij de volgende import.", + "Columns and fields: {target}": "Kolommen en velden: {target}", + "Which column of the export goes to which field": "Welke kolom van de export naar welk veld gaat", + "Column of the export": "Kolom van de export", + "Field": "Veld", + "Transform": "Omzetting", + "Yes": "Ja", + "No": "Nee", + "What the import writes": "Wat de import schrijft", + "Set on every record": "Op elk record gezet", + "Set on a new record only": "Alleen op een nieuw record gezet", + "Written on a new record, or on an existing one only when empty": "Op een nieuw record geschreven, of op een bestaand record alleen als de waarde leeg is", + "Never written on an existing record": "Nooit geschreven op een bestaand record", + "The mapping could not be loaded. Open Mapping for the reason.": "De mapping kon niet worden geladen. Open Mapping voor de reden." } } diff --git a/openspec/changes/cmdb-import-mapping-view/design.md b/openspec/changes/cmdb-import-mapping-view/design.md index c77046774..dfe9e40ab 100644 --- a/openspec/changes/cmdb-import-mapping-view/design.md +++ b/openspec/changes/cmdb-import-mapping-view/design.md @@ -54,7 +54,7 @@ The method carries no auth attribute (Nextcloud's default: admins with a CSRF to ### D4. The section takes the sheet names from the endpoint, with the constant as fallback -Unchanged from the first version: the help text computes the sheet names from the endpoint's answer, or from `PROFILE_DEFAULTS.sheets` until it arrives or when it fails. +Unchanged from the first version: the help text computes the sheet names from the endpoint's answer, or from `PROFILE_DEFAULTS.sheets` until it arrives or when it fails. The help text, the info text and the missing-sheet and too-large hints name every sheet the profile lists, whatever their number: they are built from the list (`quotedSheetList`), not from its first two entries, and say nothing that depends on a sheet's position. ### D5. The packs live in OpenRegister's migration-pack store, found by slug diff --git a/openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.md b/openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.md index b37ea6b6d..527c2802c 100644 --- a/openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.md +++ b/openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.md @@ -22,7 +22,7 @@ Nextcloud OCP interfaces used: `OCP\IL10N` (messages), `OCP\IURLGenerator` (the - `packs`: one entry per target in the order module, manufacturer, municipality, usage, businessOwner, each with `target`, `file`, `id`, `name`, `version`, `description`, `fieldMappings`, the write rules `defaults`, `createOnlyDefaults`, `createOnlyFields` and `neverWritten`, and where the pack comes from: `source` (`openregister` or `file`), `packId` (the OpenRegister row id, or null), `storedVersion` (or null), `shippedVersion`, `edited` (whether the stored pack differs from what stackiq seeded; null when unknown or for a file), `newerShipped` (an edited pack kept a newer shipped pack out), `editable` (false for the municipality pack), `updatePending` (an unedited stored pack that the next import run replaces with the shipped one; then `version`, `fieldMappings`, `defaults` and the other pack fields show the shipped pack and `storedVersion` what OpenRegister holds) and `editUrl` (the absolute link to the pack's editor in OpenRegister, built by Nextcloud's URL generator so an instance that needs `/index.php` gets it, or null when the pack is not stored there or the link cannot be built); - `openRegisterPacks`: whether OpenRegister's migration-pack store is available. -Each field mapping SHALL carry `source`, `target`, `required` (a boolean) and `transform` as the pack stores it. `createOnlyDefaults` holds values the import sets on create only, `createOnlyFields` the mapped fields written on create or, on an existing object, only when the stored value is empty (a key of `createOnlyDefaults` only when it is also a mapped field), and `neverWritten` the fields the import never writes on an existing object. When the validator is missing, a pack is unreadable, invalid or breaks a rule of the import, or the store that holds the packs is gone, the endpoint SHALL answer 503 `MAPPING_UNAVAILABLE` in the import's error envelope (`success`, `error`, `message`, `details`) with the loader's reason in `details.reason` (its first line, at most 500 characters), and for a pack stored in OpenRegister its slug in `details.pack`, its row id in `details.packId` and the link to its editor in `details.editUrl` (null when the link cannot be built). Reading the mapping SHALL change nothing: it SHALL NOT seed, update or delete a pack in OpenRegister and SHALL NOT write stackiq's app config; a pack the store does not hold yet SHALL be answered from its shipped file (`source` `file`), and an unedited stored pack that a newer shipped pack would replace SHALL be answered as the import will run it, that is as the shipped pack with `updatePending` `true`, without writing anything; a stored pack of which OpenRegister reports no last-saved time is kept by the import and answered as stored. +Each field mapping SHALL carry `source`, `target`, `required` (a boolean) and `transform` as the pack stores it. `createOnlyDefaults` holds values the import sets on create only, `createOnlyFields` the mapped fields written on create or, on an existing object, only when the stored value is empty (a key of `createOnlyDefaults` only when it is also a mapped field), and `neverWritten` the fields the import never writes on an existing object. When the validator is missing, a pack is unreadable, invalid or breaks a rule of the import, or the store that holds the packs is gone, the endpoint SHALL answer 503 `MAPPING_UNAVAILABLE` in the import's error envelope (`success`, `error`, `message`, `details`) with the loader's reason in `details.reason` (its first line, at most 500 characters), and for a pack stored in OpenRegister its slug in `details.pack`, its row id in `details.packId` and the link to its editor in `details.editUrl` (null when the link cannot be built). Reading the mapping SHALL change nothing: it SHALL NOT seed, update or delete a pack in OpenRegister and SHALL NOT write stackiq's app config; a pack the store does not hold yet SHALL be answered from its shipped file (`source` `file`), and an unedited stored pack that a newer shipped pack would replace SHALL be answered as the import will run it, that is as the shipped pack with `updatePending` `true`, without writing anything; a stored pack of which OpenRegister reports no last-saved time is kept by the import and answered as stored. The "CMDB import" section SHALL show, per pack, its source, its version, a hint when a newer shipped pack was not applied, a hint when an update is pending (`updatePending`: the shipped version replaces the stored one at the next import), and a link "Edit in OpenRegister" when the pack has an `editUrl`; when the endpoint answers 503 for a stored pack, the error SHALL carry the same link from `details.editUrl`. Per sheet pack it SHALL also show, as text in accessible tables, the field mappings (`source`, `target`, `required`, the transform's type) and the write rules that have values (`defaults`, `createOnlyDefaults`, `createOnlyFields`, `neverWritten`). It SHALL offer no editing itself. #### Scenario: An admin reads the mapping the import uses @e2e tests/e2e/spec-coverage/cmdb-import-packs.spec.ts @@ -92,10 +92,8 @@ OpenRegister's store SHALL count as available only when its pack writes require - **GIVEN** an administrator deleted the business-owner pack in OpenRegister - **WHEN** a Nextcloud admin requests the mapping -- **THEN** the business-owner pack SHALL have `source` `file`, and OpenRegister SHALL still hold no `stackiq-topdesk-business-owner` -- **WHEN** the next import runs -- **THEN** OpenRegister SHALL hold `stackiq-topdesk-business-owner` again as a built-in pack -- **AND** the mapping SHALL show it with `source` `openregister`, a new `packId` and `edited` `false` +- **THEN** the business-owner pack SHALL have `source` `openregister`, a new `packId` and `edited` `false` +- **AND** OpenRegister SHALL hold `stackiq-topdesk-business-owner` again as a built-in pack ### Requirement: Stackiq SHALL seed its mapping packs into OpenRegister and keep unedited ones current (REQ-CMDB-022) @@ -154,13 +152,55 @@ The service SHALL map each normalised row with OpenRegister's `MigrationPack\Map - **AND** the "Onbeh" row's "Applicatiesoort" `Webapplicatie`, which is not a hosting model, SHALL be stored as `applicationType` and SHALL leave `cloudDienstverleningsmodel` empty without a warning - **AND** "BNN Classificatie" `1`, `2`, `2+` SHALL be `BBN1`, `BBN2`, `BBN2+` +### Requirement: The admin settings SHALL offer a CMDB import section (REQ-CMDB-014) + +Stackiq's admin settings page SHALL show a section "CMDB import", rendered by the settings page and not registered as an in-app route. The section SHALL let the admin choose an existing municipality or type the name of a new one, choose an `.xlsx` file, and start the import. While the import runs it SHALL show a progress bar and a Cancel button. Afterwards it SHALL show the summary and a report table that can be filtered by outcome. The section SHALL also show the mapping the import uses (REQ-CMDB-020), as tables: the export's sheets and columns, and per sheet pack its source, its version, a hint when a newer shipped pack was not applied, and a link "Edit in OpenRegister" when the pack has an `editUrl`; the municipality pack, which maps the import options and no column, SHALL NOT be listed. When the endpoint answers 503 for a stored pack, the error SHALL carry the same link from `details.editUrl`. The section SHALL offer no editing of the mapping itself. The sheet names in its help text SHALL come from that mapping, every sheet it names and in its order, whatever their number, with the shipped names as the fallback until the mapping has loaded or when it cannot be loaded. Every control SHALL have a visible label, and every string SHALL be translatable. + +(Previously: the section had no mapping block, and the sheet names in its help text were a constant in the page, which could drift from the profile on the server.) + +#### Scenario: The admin runs an import from the settings page +@e2e tests/e2e/spec-coverage/cmdb-import.spec.ts + +- **GIVEN** a Nextcloud admin on stackiq's admin settings page +- **WHEN** they choose "Gemeente Voorbeeldstad", choose the anonymised export and press "Import" +- **THEN** a progress bar SHALL appear while the import runs +- **AND** afterwards the summary and the report table SHALL be shown +- **AND** filtering the table on `created` SHALL show the two imported rows + +#### Scenario: The sheet names in the help come from the server +@e2e tests/e2e/spec-coverage/cmdb-import-mapping.spec.ts + +- **GIVEN** a Nextcloud admin on stackiq's admin settings page +- **WHEN** the mapping endpoint has answered +- **THEN** the help text under the file control SHALL name the sheets the endpoint returned +- **AND** a profile with one, two or three sheets SHALL give a help text and an info text that name exactly those sheets, with no sheet the profile does not name and no description tied to a sheet's position +- **AND** when the endpoint fails, it SHALL name the shipped sheets and the mapping block SHALL show the error + +#### Scenario: The section lists the mapping and links each pack to its editor +@e2e tests/e2e/spec-coverage/cmdb-import-mapping.spec.ts + +- **GIVEN** a Nextcloud admin on stackiq's admin settings page +- **WHEN** the mapping block of the "CMDB import" section has loaded +- **THEN** a table of the export SHALL list the sheets read and the match, name, required and date columns, and a table of the packs SHALL show each of the four sheet packs with its source and version and a link "Edit in OpenRegister" to its `editUrl` +- **AND** the municipality pack, which maps the import options and no column, SHALL NOT be listed +- **AND** when the endpoint answers 503 for a stored pack, the error SHALL show a link "Edit in OpenRegister" to its `details.editUrl` +- **AND** the block SHALL hold no input field + +#### Scenario: An edited pack whose newer shipped version was not applied says so +@e2e exclude Needs a stackiq release that ships a newer pack than the one installed; tests/vitest/cmdbImportMapping.spec.js mounts the block with an edited usage pack whose `newerShipped` is true and asserts the hint, and its absence for the unedited module pack. + +- **GIVEN** the mapping endpoint answers an edited usage pack with `newerShipped` `true` and `shippedVersion` `1.1.0` +- **WHEN** the mapping block renders +- **THEN** the usage row SHALL say that stackiq ships version `1.1.0`, that it was not applied because the pack was edited, and how to use the shipped version +- **AND** a row whose `newerShipped` is `false` SHALL show no such hint + ## Non-Functional Requirements -- **Performance:** loading the mapping reads seven small JSON files and the register definition, and makes at most one OpenRegister lookup per sheet pack; it SHALL answer within the time an admin settings page loads. Reading the mapping writes nothing; only the import run and the seed step seed or update a pack. +- **Performance:** loading the mapping reads seven small JSON files and the register definition, and makes at most one OpenRegister lookup per sheet pack; it SHALL answer within the time an admin settings page loads. Writes happen only to seed or update a pack. - **Security:** admin-only with CSRF, as the import routes; OpenRegister's pack writes are admin-only with CSRF, and stackiq uses no store without those guards. The import writes with OpenRegister's RBAC and multitenancy off, so every pack SHALL follow the import's rules (REQ-CMDB-021). An edit that maps a new column makes the import read that column. The answer and the reasons hold column names, field names, lookup tables, pack names and ids; no object data and no person data. A store failure is named by its exception class, never its message. - **Reliability:** an edit is never overwritten by an upgrade and never silently replaced by the shipped files (REQ-CMDB-021, REQ-CMDB-022). -- **Accessibility:** not applicable to this part: the endpoint, the import rules and the seed step have no user interface. The section that shows the mapping is part of #1289. -- **Internationalization:** Dutch and English MUST be supported (ADR-005) for the messages and the import warning. +- **Accessibility:** Target WCAG 2.2 AA. The mapping block's link has a visible, descriptive label (SC 2.4.4, 4.1.2; gate `link-name`); a status hint is text, not colour alone (SC 1.4.1). New in 2.2: 2.4.11 Focus Not Obscured applies (the block is in the page flow); 2.5.7 Dragging Movements does not apply; 2.5.8 Target Size applies to the link (Nextcloud default); 3.2.6 Consistent Help does not apply; 3.3.7 Redundant Entry does not apply (nothing is entered); 3.3.8 Accessible Authentication does not apply. +- **Internationalization:** Dutch and English MUST be supported (ADR-005) for the block, the messages and the import warning. ## Acceptance Criteria @@ -168,7 +208,7 @@ The service SHALL map each normalised row with OpenRegister's `MigrationPack\Map - [ ] A Nextcloud admin edits a lookup value of the usage pack in OpenRegister, and the next CMDB import uses it. - [ ] A pack edited to map a field the import manages makes the import answer 503 naming the pack; the mapping endpoint answers the same 503. - [ ] Deleting the stored pack in OpenRegister makes the next import seed the shipped pack again. -- [ ] The mapping endpoint answers per pack its source and the link that opens the pack's editor (the section that shows them is #1289). +- [ ] The "CMDB import" section shows per pack its source and an "Edit in OpenRegister" link that opens the pack's editor. - [ ] A signed-in user who is not a Nextcloud admin gets 403 from the endpoint. ## Notes diff --git a/openspec/changes/cmdb-import-mapping-view/tasks.md b/openspec/changes/cmdb-import-mapping-view/tasks.md index fe0f07745..f5cb4abf1 100644 --- a/openspec/changes/cmdb-import-mapping-view/tasks.md +++ b/openspec/changes/cmdb-import-mapping-view/tasks.md @@ -40,14 +40,18 @@ Spec: `openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.m - [x] Test ### Task 4 (#1289): The section's mapping block shows the source and the editor link per pack -- **spec_ref**: `SPEC#requirement-the-admin-settings-shall-show-the-mapping-the-import-uses-req-cmdb-020` (cmdb-export-import#REQ-CMDB-020); the section requirement is changed in #1289 -- **files**: `src/views/settings/sections/CmdbImportMapping.vue`, `src/views/settings/sections/CmdbImport.vue`, `src/utils/cmdbImport.js`, `tests/vitest/cmdbImportMapping.spec.js` +- **spec_ref**: `SPEC#requirement-the-admin-settings-shall-offer-a-cmdb-import-section-req-cmdb-014` (cmdb-export-import#REQ-CMDB-014) and `SPEC#requirement-the-admin-settings-shall-show-the-mapping-the-import-uses-req-cmdb-020` (cmdb-export-import#REQ-CMDB-020) +- **files**: `src/views/settings/sections/CmdbImportMapping.vue`, `src/views/settings/sections/CmdbImport.vue`, `src/utils/cmdbImport.js`, `tests/vitest/cmdbImportMapping.spec.js`, `src/views/settings/sections/CmdbImport.spec.js`, `src/utils/cmdbImport.spec.js` - **acceptance_criteria**: - - GIVEN the endpoint's answer WHEN the block renders THEN each pack shows its source, its version, a hint when `newerShipped`, and a link to its editor (`editUrl`) when there is one - - GIVEN a 503 THEN the block shows the error, the code and the reason + - GIVEN the endpoint's answer WHEN the block renders THEN each pack shows its source, its version, a hint when `newerShipped`, and an "Edit in OpenRegister" link to `editUrl` when there is one + - GIVEN a pack with `updatePending` THEN the block says the shipped version replaces the stored one at the next import + - GIVEN a sheet pack THEN a collapsible block lists its field mappings (column, field, required, transform) and the write rules that have values, as text in tables with captions and `th` scopes + - GIVEN the block is collapsed while the answer loads THEN the loading announcement is in a live region outside the hidden block + - GIVEN a 503 THEN the block shows the error, the code and the reason, and for a stored pack an "Edit in OpenRegister" link to `details.editUrl` - GIVEN the answer THEN the help text names the endpoint's sheets; before and on failure `PROFILE_DEFAULTS.sheets` -- [ ] Implement -- [ ] Test + - GIVEN a profile with one, two or three sheets WHEN the section renders its help text and its info text THEN both name exactly those sheets in the profile's order +- [x] Implement +- [x] Test ### Task 5: Translations and documentation - **spec_ref**: `SPEC#requirement-the-admin-settings-shall-show-the-mapping-the-import-uses-req-cmdb-020` (cmdb-export-import#REQ-CMDB-020) @@ -55,7 +59,7 @@ Spec: `openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.m - **acceptance_criteria**: - GIVEN the new strings WHEN `npm run test:l10n` and `npm run check:l10n-js` run THEN both pass, with Dutch for every string - GIVEN the docs WHEN an administrator reads "Viewing and editing the mapping" THEN they know where to edit a pack, what the import refuses, what happens on an app update and how to go back to the shipped pack -- [x] Implement (backend strings and documentation; the section's strings come with Task 4) +- [x] Implement (backend strings, the section's strings and documentation) - [x] Test ### Task 6: End-to-end coverage of the endpoint, the import and the seed @@ -75,9 +79,9 @@ Spec: `openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.m - **spec_ref**: `SPEC#requirement-the-admin-settings-shall-show-the-mapping-the-import-uses-req-cmdb-020` (cmdb-export-import#REQ-CMDB-020); the section itself is specified in #1289 - **files**: `tests/e2e/spec-coverage/cmdb-import-mapping.spec.ts` - **acceptance_criteria**: - - GIVEN an admin on the settings page WHEN the mapping has loaded THEN every sheet pack shows its source and a link to its editor -- [ ] Implement -- [ ] Test + - GIVEN an admin on the settings page WHEN the mapping has loaded THEN every sheet pack shows its source and an "Edit in OpenRegister" link, and the error of a broken stored pack links to its editor +- [x] Implement +- [x] Test ### Task 8: Archive - **spec_ref**: `SPEC#requirement-the-import-shall-run-the-mapping-packs-stored-in-openregister-req-cmdb-021` (cmdb-export-import#REQ-CMDB-021) diff --git a/src/utils/cmdbImport.js b/src/utils/cmdbImport.js index ab7fa7080..edfed8923 100644 --- a/src/utils/cmdbImport.js +++ b/src/utils/cmdbImport.js @@ -490,10 +490,11 @@ export function isKnownError(code) { * to which sheet or part, and what to do about it. * * @param {object} details The error details: `maxPartBytes`, `part`, `size` and, for a sheet, `sheet`; `maxSharedStrings`; `maxReferencedStringBytes`; or `maxUncompressedBytes` + * @param {Array} sheetNames The sheets the import reads, for the hint * @return {{title: string, hint: string}} The words * @spec openspec/changes/cmdb-export-import/specs/cmdb-export-import/spec.md#requirement-the-workbook-shall-be-read-as-stored-data-without-evaluating-formulas-or-following-links-req-cmdb-002 */ -function workbookTooLarge(details) { +function workbookTooLarge(details, sheetNames) { const compressed = t( 'stackiq', 'An .xlsx file is compressed, so a file of a few MB can be much larger once unpacked. Nothing was imported.', @@ -532,11 +533,8 @@ function workbookTooLarge(details) { + ' ' + t( 'stackiq', - 'Delete the sheets the import does not read from a copy of the export (it reads only "{first}" and "{second}"), and import that copy.', - { - first: PROFILE_DEFAULTS.sheets[0], - second: PROFILE_DEFAULTS.sheets[1], - }, + 'Delete the sheets the import does not read from a copy of the export (it reads only {sheets}), and import that copy.', + { sheets: quotedSheetList(sheetNames) }, AS_TEXT, ) if (Number(details.maxSharedStrings) > 0) { @@ -584,10 +582,11 @@ function workbookTooLarge(details) { * when the server's message is not. * * @param {{error: string, message?: string, details?: object}} error The error in the server's shape + * @param {Array} [sheetNames] The sheets the import reads, from mappingSheetNames(); the shipped names when left out. Hints name all of them, whatever their number * @return {{title: string, hint: string}} The words * @spec openspec/changes/cmdb-export-import/specs/cmdb-export-import/spec.md#requirement-columns-shall-be-resolved-by-header-name-and-a-missing-required-column-shall-stop-the-import-with-422-req-cmdb-003 */ -export function errorText(error) { +export function errorText(error, sheetNames = PROFILE_DEFAULTS.sheets) { const details = error?.details || {} switch (error?.error) { case 'NO_FILE_UPLOADED': @@ -682,22 +681,28 @@ export function errorText(error) { case 'NO_SOURCE_SHEET': { const expected = Array.isArray(details.expected) && details.expected.length > 0 - ? details.expected - : PROFILE_DEFAULTS.sheets + ? details.expected.map((name) => String(name)) + : sheetNames + const values = { sheets: quotedSheetList(expected) } return { title: t( 'stackiq', 'The workbook has none of the sheets the import reads.', ), - hint: t( - 'stackiq', - 'Expected a sheet named "{first}" or "{second}". Sheet names must match exactly.', - { - first: String(expected[0] ?? PROFILE_DEFAULTS.sheets[0]), - second: String(expected[1] ?? PROFILE_DEFAULTS.sheets[1]), - }, - AS_TEXT, - ), + hint: + expected.length === 1 + ? t( + 'stackiq', + 'Expected the sheet {sheets}. Sheet names must match exactly.', + values, + AS_TEXT, + ) + : t( + 'stackiq', + 'Expected one of the sheets {sheets}. Sheet names must match exactly.', + values, + AS_TEXT, + ), } } case 'MISSING_COLUMN': @@ -747,7 +752,7 @@ export function errorText(error) { title: t('stackiq', 'The import mapping cannot run.'), hint: t( 'stackiq', - "OpenRegister's mapping engine is missing or a mapping file is invalid. Update OpenRegister and check the Nextcloud log.", + "OpenRegister's mapping engine is missing, or a mapping pack is invalid or breaks a rule of the import. The reason below names the pack: correct it in OpenRegister, or check the Nextcloud log.", ), } case 'READER_UNAVAILABLE': @@ -767,7 +772,7 @@ export function errorText(error) { ), } case 'WORKBOOK_TOO_LARGE': - return workbookTooLarge(details) + return workbookTooLarge(details, sheetNames) case 'SCHEMA_OUTDATED': return { title: details.schema @@ -984,3 +989,296 @@ export function reportRows(rows) { moduleUuid: row.moduleUuid ? String(row.moduleUuid) : '', })) } + +/** + * The URL of the mapping overview endpoint. + * + * @return {string} The URL + * @spec openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.md#requirement-the-admin-settings-shall-show-the-mapping-the-import-uses-req-cmdb-020 + */ +export function mappingUrl() { + return generateUrl('/apps/stackiq/api/settings/cmdb-import/mapping') +} + +/** + * Read the mapping the import uses: the profile and one entry per pack. + * + * @param {object} options The options + * @param {object} options.http An axios-like client with get + * @return {Promise<{profile: object, packs: Array}>} The server's answer + * @spec openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.md#requirement-the-admin-settings-shall-show-the-mapping-the-import-uses-req-cmdb-020 + */ +export async function loadCmdbMapping({ http }) { + const response = await http.get(mappingUrl()) + return response.data +} + +/** + * The names of the source sheets: the profile's when the mapping has loaded + * and names at least one sheet, the shipped defaults until then or when it + * could not be loaded. Every sheet the profile names is returned, in the + * profile's order, so the help texts and the block show the same list. + * + * @param {object|null} mapping The mapping endpoint's answer, or null + * @return {Array} The sheet names, never empty + * @spec openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.md#requirement-the-admin-settings-shall-offer-a-cmdb-import-section-req-cmdb-014 + */ +export function mappingSheetNames(mapping) { + const sheets = mapping?.profile?.sheets + const names = (Array.isArray(sheets) ? sheets : []) + .map((sheet) => + typeof sheet === 'string' ? sheet : String(sheet?.name ?? ''), + ) + .filter((name) => name !== '') + return names.length >= 1 ? names : [...PROFILE_DEFAULTS.sheets] +} + +/** + * The sheet names as one quoted list: "A", "B", "C". + * + * @param {Array} names The sheet names + * @return {string} The list + * @spec openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.md#requirement-the-admin-settings-shall-offer-a-cmdb-import-section-req-cmdb-014 + */ +export function quotedSheetList(names) { + return names.map((name) => `"${name}"`).join(', ') +} + +/** + * The help text under the file chooser: which sheets the workbook needs and + * the default size limit. It names every sheet the profile reads, whatever + * their number, and does not depend on their order. + * + * @param {Array} names The sheet names, from mappingSheetNames() + * @param {number} maxFileBytes The default size limit in bytes + * @return {string} The text + * @spec openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.md#requirement-the-admin-settings-shall-offer-a-cmdb-import-section-req-cmdb-014 + */ +export function fileHelpText(names, maxFileBytes) { + const values = { + sheets: quotedSheetList(names), + size: formatMegabytes(maxFileBytes), + } + if (names.length === 1) { + return t( + 'stackiq', + 'Excel workbook (.xlsx) with the sheet {sheets}. By default the file may be at most {size}.', + values, + AS_TEXT, + ) + } + return t( + 'stackiq', + 'Excel workbook (.xlsx) with one of the sheets {sheets}. By default the file may be at most {size}.', + values, + AS_TEXT, + ) +} + +/** + * The sentence of the info panel that says which sheets are read. + * + * @param {Array} names The sheet names, from mappingSheetNames() + * @return {string} The text + * @spec openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.md#requirement-the-admin-settings-shall-offer-a-cmdb-import-section-req-cmdb-014 + */ +export function sheetsReadText(names) { + const values = { sheets: quotedSheetList(names) } + if (names.length === 1) { + return t( + 'stackiq', + 'The sheet {sheets} is read; other sheets, including the "Invoer" sheets, are ignored.', + values, + AS_TEXT, + ) + } + return t( + 'stackiq', + 'The sheets {sheets} are read; other sheets, including the "Invoer" sheets, are ignored.', + values, + AS_TEXT, + ) +} + +/** + * The words for a pack's target: what the pack's rows become. + * + * @param {string} target The target as the profile names it (module, manufacturer, …) + * @return {string} The translated name, or the target itself when it is unknown + * @spec openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.md#requirement-the-admin-settings-shall-show-the-mapping-the-import-uses-req-cmdb-020 + */ +export function packTargetLabel(target) { + switch (target) { + case 'module': + return t('stackiq', 'Application (module)') + case 'manufacturer': + return t('stackiq', 'Supplier organisation (Vendor)') + case 'usage': + return t('stackiq', 'Usage') + case 'businessOwner': + return t('stackiq', 'Business owner (contact person)') + default: + return String(target ?? '') + } +} + +/** + * The field mappings of a pack as rows of text: the column of the export, the + * field it goes to, whether a failing row is skipped without it, and what the + * pack does with the value (the transform's type; a lookup also its size). + * + * @param {object} pack One entry of the endpoint's `packs` + * @return {Array<{source: string, target: string, required: boolean, transform: string}>} One row per mapping, in the pack's order + * @spec openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.md#requirement-the-admin-settings-shall-show-the-mapping-the-import-uses-req-cmdb-020 + */ +export function packFieldRows(pack) { + const mappings = Array.isArray(pack?.fieldMappings) ? pack.fieldMappings : [] + return mappings.map((mapping) => { + const transform = mapping?.transform + let text = '' + if (typeof transform === 'string') { + text = transform + } else if (transform && typeof transform === 'object') { + text = String(transform.type ?? '') + if ( + text === 'lookup' + && transform.map + && typeof transform.map === 'object' + ) { + text += ' (' + Object.keys(transform.map).length + ')' + } + } + return { + source: String(mapping?.source ?? ''), + target: String(mapping?.target ?? ''), + required: mapping?.required === true, + transform: text, + } + }) +} + +/** + * The write rules of a pack as rows of text: the values it sets, and the + * fields written only on create or never on an existing record. A rule the + * pack does not have gives no row. + * + * @param {object} pack One entry of the endpoint's `packs` + * @return {Array<{key: string, label: string, values: Array}>} One row per rule that has values + * @spec openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.md#requirement-the-admin-settings-shall-show-the-mapping-the-import-uses-req-cmdb-020 + */ +export function packWriteRules(pack) { + const pairs = (value) => + value && typeof value === 'object' && !Array.isArray(value) + ? Object.entries(value).map( + ([key, val]) => + key + + ': ' + + (typeof val === 'string' ? val : JSON.stringify(val)), + ) + : [] + const list = (value) => (Array.isArray(value) ? value.map(String) : []) + return [ + { + key: 'defaults', + label: t('stackiq', 'Set on every record'), + values: pairs(pack?.defaults), + }, + { + key: 'createOnlyDefaults', + label: t('stackiq', 'Set on a new record only'), + values: pairs(pack?.createOnlyDefaults), + }, + { + key: 'createOnlyFields', + label: t( + 'stackiq', + 'Written on a new record, or on an existing one only when empty', + ), + values: list(pack?.createOnlyFields), + }, + { + key: 'neverWritten', + label: t('stackiq', 'Never written on an existing record'), + values: list(pack?.neverWritten), + }, + ].filter((rule) => rule.values.length > 0) +} + +/** + * The words for where the import takes a pack from: OpenRegister's pack + * store, or the file shipped with stackiq. + * + * @param {string} source The pack's `source` from the endpoint (`openregister` or `file`) + * @return {string} The translated name, or the source itself when it is unknown + * @spec openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.md#requirement-the-admin-settings-shall-show-the-mapping-the-import-uses-req-cmdb-020 + */ +export function packSourceLabel(source) { + switch (source) { + case 'openregister': + return t('stackiq', 'OpenRegister') + case 'file': + return t('stackiq', 'Shipped file') + default: + return String(source ?? '') + } +} + +/** + * The version of a pack as the import runs it: the stored version for a + * pack from OpenRegister, with the shipped version next to it when the two + * differ, and the shipped version for a pack read from the file. + * + * @param {object} pack One entry of the endpoint's `packs` + * @return {string} The text, empty when the pack names no version + * @spec openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.md#requirement-the-admin-settings-shall-show-the-mapping-the-import-uses-req-cmdb-020 + */ +export function packVersionText(pack) { + const shipped = String(pack?.shippedVersion ?? pack?.version ?? '') + const stored = String(pack?.storedVersion ?? '') + if (pack?.source === 'openregister' && stored !== '') { + if (shipped !== '' && shipped !== stored) { + return t( + 'stackiq', + 'Version {version} (shipped: {shipped})', + { version: stored, shipped }, + AS_TEXT, + ) + } + return t('stackiq', 'Version {version}', { version: stored }, AS_TEXT) + } + if (shipped === '') { + return '' + } + return t('stackiq', 'Version {version}', { version: shipped }, AS_TEXT) +} + +/** + * An editor address from the server, when the page may link to it: an + * http(s) or server-relative address. Anything else (a `javascript:` + * address, a protocol-relative one, no address at all) gives no link. + * + * @param {unknown} url The address the server gave + * @return {string|null} The address, or null for no link + * @spec openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.md#requirement-the-admin-settings-shall-show-the-mapping-the-import-uses-req-cmdb-020 + */ +export function editorUrl(url) { + if (typeof url !== 'string') { + return null + } + return /^(https?:\/\/|\/(?!\/))/i.test(url) ? url : null +} + +/** + * The address of OpenRegister's editor for a pack, when the page may link + * to it: the pack is editable and its address passes editorUrl(). + * + * @param {object} pack One entry of the endpoint's `packs` + * @return {string|null} The address, or null for no link + * @spec openspec/changes/cmdb-import-mapping-view/specs/cmdb-export-import/spec.md#requirement-the-admin-settings-shall-show-the-mapping-the-import-uses-req-cmdb-020 + */ +export function packEditUrl(pack) { + if (pack?.editable !== true) { + return null + } + return editorUrl(pack.editUrl) +} diff --git a/src/utils/cmdbImport.spec.js b/src/utils/cmdbImport.spec.js index 8f566d6cb..2d3a8266f 100644 --- a/src/utils/cmdbImport.spec.js +++ b/src/utils/cmdbImport.spec.js @@ -363,12 +363,47 @@ describe('errorText', () => { error: 'NO_SOURCE_SHEET', details: { expected: ['Blad A', 'Blad B'] }, }).hint, - ).toContain('"Blad A" or "Blad B"') + ).toContain('one of the sheets "Blad A", "Blad B"') expect(errorText({ error: 'NO_SOURCE_SHEET', details: {} }).hint).toContain( - PROFILE_DEFAULTS.sheets[1], + '"' + PROFILE_DEFAULTS.sheets.join('", "') + '"', ) }) + it.each([ + [1, ['Blad A'], 'Expected the sheet "Blad A".'], + [ + 3, + ['Blad A', 'Blad B', 'Blad C'], + 'one of the sheets "Blad A", "Blad B", "Blad C".', + ], + ])( + 'names all %i sheets of the profile in the missing-sheet and too-large hints', + (count, names, expected) => { + const quoted = names.map((name) => '"' + name + '"').join(', ') + + const missing = errorText( + { error: 'NO_SOURCE_SHEET', details: {} }, + names, + ) + expect(missing.hint).toContain(expected) + for (const shipped of PROFILE_DEFAULTS.sheets) { + expect(missing.hint).not.toContain(shipped) + } + + const tooLarge = errorText( + { + error: 'WORKBOOK_TOO_LARGE', + details: { maxSharedStrings: 200000 }, + }, + names, + ) + expect(tooLarge.hint).toContain('(it reads only ' + quoted + ')') + for (const shipped of PROFILE_DEFAULTS.sheets) { + expect(tooLarge.hint).not.toContain(shipped) + } + }, + ) + it('names the unpacked size limit the server applied', () => { expect( errorText({ @@ -408,7 +443,7 @@ describe('errorText', () => { expect( errorText({ error: 'WORKBOOK_TOO_LARGE', details }).hint, ).toContain( - '(it reads only "Onbeh Applicaties CMDB" and "Beheerde Applicaties CMDB")', + '(it reads only "Onbeh Applicaties CMDB", "Beheerde Applicaties CMDB")', ) } }) diff --git a/src/views/settings/sections/CmdbImport.spec.js b/src/views/settings/sections/CmdbImport.spec.js index 931f7edbc..667243d95 100644 --- a/src/views/settings/sections/CmdbImport.spec.js +++ b/src/views/settings/sections/CmdbImport.spec.js @@ -10,7 +10,9 @@ import axios from '@nextcloud/axios' import { flushPromises, shallowMount } from '@vue/test-utils' +import { h } from 'vue' import CmdbImport from './CmdbImport.vue' +import CmdbImportMapping from './CmdbImportMapping.vue' // Virtual: the package's `exports` field declares only the `import` condition, which jest does not resolve. jest.mock( @@ -55,6 +57,15 @@ jest.mock('vue-material-design-icons/DatabaseImport.vue', () => ({ jest.mock('vue-material-design-icons/TrayArrowUp.vue', () => ({ render: () => null, })) +jest.mock('vue-material-design-icons/ChevronDown.vue', () => ({ + render: () => null, +})) +jest.mock('vue-material-design-icons/ChevronUp.vue', () => ({ + render: () => null, +})) +jest.mock('vue-material-design-icons/OpenInNew.vue', () => ({ + render: () => null, +})) jest.mock('../../../components/AlwaysVisibleSection.vue', () => ({ render: () => null, })) @@ -91,7 +102,7 @@ const ORGANISATIONS = [ * * @return {Promise} The wrapper, after the municipalities loaded */ -async function mountSection() { +async function mountSection(options = {}) { axios.get.mockImplementation((url) => Promise.resolve( url.includes('/voorzieningen/config') @@ -99,7 +110,7 @@ async function mountSection() { : { data: { results: ORGANISATIONS } }, ), ) - const wrapper = shallowMount(CmdbImport) + const wrapper = shallowMount(CmdbImport, options) await flushPromises() return wrapper } @@ -147,3 +158,120 @@ describe('CmdbImport municipality chooser', () => { expect(wrapper.vm.report).toBe(null) }) }) + +// shallowMount stubs the section's wrapper without its slots; this one renders them, so the help text and the info panel are in the DOM. +const SLOT_RENDERING_SECTION = { + render() { + return h('div', [this.$slots.default?.(), this.$slots['info-content']?.()]) + }, +} + +/** + * Mount the section with the wrapper rendering its slots. + * + * @return {Promise} The wrapper + */ +function mountWithSlots() { + return mountSection({ + global: { stubs: { AlwaysVisibleSection: SLOT_RENDERING_SECTION } }, + }) +} + +describe('CmdbImport sheet names in the help texts', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + const SHIPPED = ['Onbeh Applicaties CMDB', 'Beheerde Applicaties CMDB'] + + /** + * The mapping endpoint's answer for a profile with the given sheets. + * + * @param {Array} names The sheet names + * @return {object} The answer, reduced to what the section reads + */ + function mappingOf(names) { + return { + profile: { sheets: names.map((name) => ({ name })) }, + packs: [], + } + } + + /** + * The help text under the file chooser and the info panel's sheet sentence. + * + * @param {object} wrapper The mounted section + * @return {{help: string, info: string}} Both texts, as rendered + */ + function renderedTexts(wrapper) { + return { + help: wrapper.find('#cmdb-import-file-help').text(), + info: wrapper + .findAll('.cmdb-import-info li') + .map((li) => li.text()) + .find((text) => /sheets? .* (is|are) read/.test(text)), + } + } + + it('names the shipped sheets until the mapping block has loaded', async () => { + const wrapper = await mountWithSlots() + + const { help, info } = renderedTexts(wrapper) + expect(help).toContain('"' + SHIPPED[0] + '", "' + SHIPPED[1] + '"') + expect(info).toContain('"' + SHIPPED[0] + '", "' + SHIPPED[1] + '"') + }) + + it('names the sheets the mapping block emits, in the help text and the info panel', async () => { + const wrapper = await mountWithSlots() + + wrapper + .findComponent(CmdbImportMapping) + .vm.$emit('loaded', mappingOf(['Alpha', 'Beta', 'Gamma'])) + await flushPromises() + + const { help, info } = renderedTexts(wrapper) + expect(help).toContain('"Alpha", "Beta", "Gamma"') + expect(info).toContain('"Alpha", "Beta", "Gamma"') + for (const shipped of SHIPPED) { + expect(help).not.toContain(shipped) + expect(info).not.toContain(shipped) + } + }) + + it('names only the one sheet of a profile with one sheet', async () => { + const wrapper = await mountWithSlots() + + wrapper + .findComponent(CmdbImportMapping) + .vm.$emit('loaded', mappingOf(['Alpha'])) + await flushPromises() + + const { help, info } = renderedTexts(wrapper) + expect(help).toContain('with the sheet "Alpha".') + expect(info).toContain('The sheet "Alpha" is read') + for (const shipped of SHIPPED) { + expect(help).not.toContain(shipped) + } + }) + + it("names the profile's sheets in the section's missing-sheet and too-large errors", async () => { + const wrapper = await mountWithSlots() + wrapper + .findComponent(CmdbImportMapping) + .vm.$emit('loaded', mappingOf(['Alpha', 'Beta', 'Gamma'])) + await flushPromises() + + await wrapper.setData({ error: { error: 'NO_SOURCE_SHEET', details: {} } }) + expect(wrapper.vm.errorView.hint).toContain('"Alpha", "Beta", "Gamma"') + + await wrapper.setData({ + error: { + error: 'WORKBOOK_TOO_LARGE', + details: { maxUncompressedBytes: 50 * 1024 * 1024 }, + }, + }) + expect(wrapper.vm.errorView.hint).toContain( + '(it reads only "Alpha", "Beta", "Gamma")', + ) + }) +}) diff --git a/src/views/settings/sections/CmdbImport.vue b/src/views/settings/sections/CmdbImport.vue index f8bbc1e4b..6ce595d12 100644 --- a/src/views/settings/sections/CmdbImport.vue +++ b/src/views/settings/sections/CmdbImport.vue @@ -47,7 +47,7 @@ {{ t( 'stackiq', - 'Pick an existing organisation of type Municipality, or type a new name and press Enter.', + 'Choose a municipality, or type a new name and press Enter.', ) }}

@@ -84,18 +84,7 @@ }}

- {{ - t( - 'stackiq', - 'Excel workbook (.xlsx) with the sheet "{first}" or "{second}". By default the file may be at most {size}.', - { - first: profileDefaults.sheets[0], - second: profileDefaults.sheets[1], - size: formatMegabytes(profileDefaults.maxFileBytes), - }, - asText, - ) - }} + {{ fileHelp }}

@@ -111,15 +100,7 @@ {{ t( 'stackiq', - 'When off, applications imported before are left as they are and reported as skipped.', - ) - }} -

-

- {{ - t( - 'stackiq', - "When on, a re-import overwrites the application's name, descriptions, application type, hosting model, BBN level, source fields and supplier, and the usage's status, phase-out date and business owner, with the values from the export. The usage's TIME classification and internal note are only set when the usage is created or the field is empty, so changes made in stackiq stay.", + 'Overwrites applications imported before with the values from the export. The (i) button above lists the fields.', ) }}

@@ -138,7 +119,7 @@ {{ t( 'stackiq', - 'A published application is visible to anyone, including anonymous visitors of OpenCatalogi. When off, the applications this import creates stay unpublished until you publish them by hand. Applications imported before keep their publication as it is.', + 'New applications become visible to everyone, also without logging in.', ) }}

@@ -361,6 +342,9 @@ + + +