Skip to content

Commit

Permalink
add export to ODS (keep CSV for now...)
Browse files Browse the repository at this point in the history
  • Loading branch information
prigaux committed Nov 6, 2023
1 parent 21db9b5 commit 53bf736
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 7 deletions.
1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"array-includes": "^3.0.3",
"axios": "^0.17.1",
"bootstrap": "^3.3.7",
"client-zip": "^2.3.1",
"croppie": "^2.6.3",
"exif-js": "^2.3.0",
"load-script-once": "^1.1.2",
Expand Down
34 changes: 27 additions & 7 deletions app/src/attrs/ReadOnlyObjectItems.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
<template>
<div class="ReadOnlyObjectItems" v-if="v_array.length">
<a class="btn btn-default export" @click="export_csv" href="#" download="comptes.csv" v-if="opts.uiOptions && opts.uiOptions.object_items_export_csv">
<span class="glyphicon glyphicon-export"></span>
Exporter
</a>
<div v-if="opts.uiOptions && opts.uiOptions.object_items_export_csv">
Exporter :
<template v-for="(name, extension) in { ods: 'Tableur', csv: 'CSV' }">
<a class="btn btn-default export" @click.prevent="export_(extension)" title="Exporter">
<span class="glyphicon glyphicon-export"></span>
{{name}}
</a>
</template>
</div>

<div class="table-responsive">
<table class="table table-striped">
Expand Down Expand Up @@ -43,9 +48,24 @@ export default Vue.extend({
},
methods: {
formatValue,
export_csv(event) {
const csv = Helpers.to_csv(this.v_array, this.attrs)
event.target.href = "data:text/csv;charset=utf-8," + encodeURIComponent(csv)
async export_(extension) {
const blob = extension === 'csv' ?
new Blob(
[Helpers.to_csv(this.v_array, this.attrs)],
{ type: 'text/csv;charset=utf-8' }
) :
await (
await import('../services/spreadsheet')
).to_ods(this.v_array, this.attrs)
if (this.export_ods_link) {
// revoke previous blob which should not be useful anymore
URL.revokeObjectURL(this.export_ods_link.href)
}
const link = this.export_ods_link ||= document.createElement("a")
link.href = URL.createObjectURL(blob)
link.download = "comptes." + extension
link.click()
},
},
})
Expand Down
81 changes: 81 additions & 0 deletions app/src/services/spreadsheet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// NB: it would be easier to generate a "Single OpenDocument XML" http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#element-office_document
// alas Microsoft 365 still does not implement it in 2023

import { makeZip } from "client-zip"
import * as _ from 'lodash'
import { V, StepAttrsOption } from "./ws"

const XML_CHAR_MAP = { '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&apos;' }
const escapeXml = (unsafe: string) => (unsafe || '').replace(/[<>&"]/g, c => XML_CHAR_MAP[c])

async function export_spreadsheet_raw(table_rows: string) {
const mimetype = "application/vnd.oasis.opendocument.spreadsheet"

const entries = [
{ name: "mimetype", input: mimetype },
{ name: "META-INF/manifest.xml", input :
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0">
<manifest:file-entry manifest:full-path="/" manifest:media-type="${mimetype}"/>
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
</manifest:manifest>
` },
{ name: "content.xml", input:
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<office:document-content office:version="1.1"
xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"
xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"
xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0">
<office:automatic-styles>
<style:style style:name="mydate" style:family="table-cell" style:parent-style-name="Default" style:data-style-name="DD_MM_YYYY_"/>
<style:style style:name="mytitle" style:family="table-cell" style:parent-style-name="Default">
<style:paragraph-properties fo:text-align="center"/>
<style:text-properties fo:font-weight="bold"/>
</style:style>
<number:date-style style:name="DD_MM_YYYY_" number:automatic-order="true">
<number:day number:style="long"/> <number:text>/</number:text>
<number:month number:style="long"/> <number:text>/</number:text>
<number:year number:style="long"/>
</number:date-style>
</office:automatic-styles>
<office:body>
<office:spreadsheet>
<table:table>
<table:table-column /> <!-- needed for schema compliance -->
${table_rows}
</table:table>
</office:spreadsheet>
</office:body>
</office:document-content>` },
]

return new Response(
makeZip(entries),
{ headers: { "Content-Type": mimetype } }
).blob()
}

export async function to_ods(l: V[], attrs: StepAttrsOption) {
const format_table_cell_header = txt => (
` <table:table-cell table:style-name="mytitle"><text:p>${escapeXml(txt)}</text:p></table:table-cell>`
)
const format_table_cell = cell => (
typeof cell === 'number' ?
` <table:table-cell office:value-type="float" office:value="${cell}" />` :
cell instanceof Date ?
` <table:table-cell office:value-type="date" office:date-value="${cell.toISOString()}" table:style-name="mydate" />` :
` <table:table-cell office:value-type="string" office:string-value="${escapeXml(cell)}" />`
)
const format_table_row = (v: V) => `
<table:table-row>
${_.map(attrs, (_, attr) => format_table_cell(v[attr])).join("\n")}
</table:table-row>
`
return await export_spreadsheet_raw([
`<table:table-row>${_.map(attrs, (opts, _)=> format_table_cell_header(opts.title)).join("\n")}</table:table-row>`,
...l.map(v => format_table_row(v)),
].join(""))
}

0 comments on commit 53bf736

Please sign in to comment.