qrcode: move data items to a new element (#86853)

This commit is contained in:
Corentin Sechet 2024-02-22 17:18:50 +01:00 committed by Corentin Sechet
parent 8e7056f4de
commit 631fd54f30
4 changed files with 184 additions and 118 deletions

View File

@ -91,7 +91,7 @@ qrcode-reader {
}
&.invalid-qrcode {
qrcode-reader-validity {
qrcode-reader-validity, qrcode-reader-data {
display: none;
}
}
@ -116,28 +116,6 @@ qrcode-reader {
margin: 10px;
}
&--data-items, &--metadata-items {
display: grid;
grid-template-columns: auto 1fr;
margin-top: 5px;
padding-top: 5px;
border-top: 1px solid var(--gray-light);
&.error {
color: #{$red};
}
}
&--data-item-label {
font-weight: bold;
justify-self: end;
}
&--data-item-value {
display: flex;
flex-wrap: wrap;
}
&--close-popup-button {
margin: 5px 10px 10px 10px;
padding: 5px;
@ -174,6 +152,7 @@ qrcode-reader {
--background-color: #{$green-light};
display: grid;
gap: 3px;
margin-bottom: 10px;
&--header {
padding: 0 5px;
@ -191,10 +170,19 @@ qrcode-reader {
fill: var(--color);
}
&--ok-icon, &--error-icon {
&--spinner, &--ok-icon, &--error-icon {
grid-area: 1 / 1 / 2 / 2;
}
&--spinner {
display: none;
}
&--spinner-animation {
transform-origin:center;
animation:spinner-keyframes .75s infinite linear;
}
&--error-icon {
display: none;
}
@ -221,6 +209,7 @@ qrcode-reader {
.qrcode-reader-section--errors {
display: block;
}
.qrcode-reader-section--ok-icon {
display: none;
}
@ -229,6 +218,23 @@ qrcode-reader {
display: block;
}
}
&.loading {
--color: black;
--background-color: #{$gray-light};
.qrcode-reader-section--ok-icon {
display: none;
}
.qrcode-reader-section--error-icon {
display: none;
}
.qrcode-reader-section--spinner {
display: block;
}
}
}
qrcode-reader-validity {
@ -243,3 +249,22 @@ qrcode-reader-validity {
font-weight: bold;
}
}
qrcode-reader-data {
.qrcode-reader-section--content {
display: grid;
grid-template-columns: auto 1fr;
}
.qrcode-reader-data {
&--item-label {
font-weight: bold;
justify-self: end;
}
&--item-value {
display: flex;
flex-wrap: wrap;
}
}
}

View File

@ -47,22 +47,16 @@ const readerTemplate = template(`
<div class="qrcode-reader--popup closed">
<div class="qrcode-reader--popup-errors"></div>
<qrcode-reader-validity></qrcode-reader-validity>
<qrcode-reader-data></qrcode-reader-data>
<div class="qrcode-reader--popup-content"></div>
<button class="qrcode-reader--close-popup-button">${translate('close')}</button>
</div>
`)
const dataTemplate = template(`
<div class="qrcode-reader--data-items"></div>
<span class="qrcode-reader--metadata-items"></span>
<span class="qrcode-reader--tally-status"></span>
`)
const dataItemTemplate = template(`
<span class="qrcode-reader--data-item-label">{label}&nbsp;:&nbsp;</span>
<span class="qrcode-reader--data-item-value">{value}</span>
`)
const spinnerTemplate = template(`
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="qrcode-reader--spinner">
<path d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity=".25"/>
@ -146,6 +140,7 @@ class QRCodeReader extends window.HTMLElement {
#errors
#currentCertificate
#validity
#data
constructor () {
super()
@ -161,6 +156,7 @@ class QRCodeReader extends window.HTMLElement {
this.#popupContent = this.querySelector('.qrcode-reader--popup-content')
this.#errors = this.querySelector('.qrcode-reader--popup-errors')
this.#validity = this.querySelector('qrcode-reader-validity')
this.#data = this.querySelector('qrcode-reader-data')
const closePopupButton = this.querySelector('.qrcode-reader--close-popup-button')
closePopupButton.addEventListener('click', () => {
@ -251,22 +247,15 @@ class QRCodeReader extends window.HTMLElement {
this.#validity.refresh(validityStart, validityEnd)
const dataElement = dataTemplate.cloneNode(true)
const dataItems = dataElement.content.querySelector('.qrcode-reader--data-items')
const metadataItems = dataElement.content.querySelector('.qrcode-reader--metadata-items')
const dataElement = dataTemplate.cloneNode(true)
const tallyStatus = dataElement.content.querySelector('.qrcode-reader--tally-status')
for (const [key, value] of Object.entries(data)) {
const dataItem = dataItemTemplate.cloneNode(true)
dataItem.innerHTML = dataItem.innerHTML.replace('{label}', key).replace('{value}', value)
dataItems.append(dataItem.content)
}
this.#popupContent.append(dataElement.content)
await Promise.all([
this.#showMetadata(metadataItems, certificateUUID),
this.#data.refresh(data, this.getAttribute('metadata-url'), certificateUUID),
this.#showTally(tallyStatus, certificateUUID)
])
@ -275,56 +264,6 @@ class QRCodeReader extends window.HTMLElement {
}
}
async #showMetadata(itemsElement, certificateUUID) {
if(this.getAttribute('metadata-url') === null) {
return
}
const spinner = spinnerTemplate.cloneNode(true)
itemsElement.appendChild(spinner.content)
const metadata = await this.#fetchMetadata(itemsElement, certificateUUID)
if(!metadata) {
return
}
itemsElement.innerText = ""
for (const [key, value] of Object.entries(metadata)) {
const dataItem = dataItemTemplate.cloneNode(true)
dataItem.innerHTML = dataItem.innerHTML.replace('{label}', key).replace('{value}', value)
itemsElement.append(dataItem.content)
}
}
async #fetchMetadata(itemsElement, certificateUUID) {
const url = `${this.getAttribute('metadata-url')}?certificate=${certificateUUID}`
let metadata = undefined
try {
const response = await fetch(url)
const content = await response.json()
if(response.ok && content.err == 0) {
metadata = content.data
}
else {
itemsElement.classList.add('error')
itemsElement.innerHTML = `
<p>${translate('metadata_api_error')}</p>
`
}
} catch(err) {
if(err.name == 'NetworkError') {
itemsElement.classList.add('error')
itemsElement.innerHTML = `<p>${translate('metadata_network_error')}</p>`
}
else {
throw err
}
}
return metadata
}
async #showTally(statusElement, certificateUUID) {
if(this.getAttribute('tally-url') === null) {
@ -405,6 +344,17 @@ window.customElements.define('qrcode-reader', QRCodeReader)
const sectionTemplate = template(`
<div class="qrcode-reader-section">
<div class="qrcode-reader-section--header">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="16"
height="16"
class="qrcode-reader-section--spinner">
<path d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity=".25"/>
<path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="qrcode-reader-section--spinner-animation"/>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
@ -448,6 +398,8 @@ class QRCodeReaderSection extends HTMLElement {
this.#errors = this.querySelector('.qrcode-reader-section--errors')
}
get contentElement() { return this.#content }
setTitle(title) {
this.#title.innerText = title
}
@ -461,10 +413,21 @@ class QRCodeReaderSection extends HTMLElement {
this.#wrapper.classList.add('error')
this.#errors.innerHTML = message
}
reset() {
this.#wrapper.classList.remove('error')
this.#content.innerHTML = ''
}
async load(callback) {
this.#wrapper.classList.add('loading')
try {
await callback()
}
finally {
this.#wrapper.classList.remove('loading')
}
}
}
const validityTemplate = template(`
@ -511,3 +474,78 @@ class QRCodeReaderValidity extends QRCodeReaderSection {
}
window.customElements.define('qrcode-reader-validity', QRCodeReaderValidity)
const dataItemTemplate = template(`
<span class="qrcode-reader-data--item-label">{label}&nbsp;:&nbsp;</span>
<span class="qrcode-reader-data--item-value">{value}</span>
`)
class QRCodeReaderData extends QRCodeReaderSection {
connectedCallback() {
super.connectedCallback()
this.setTitle(translate('data_section_title'))
}
async refresh(data, metadataUrl, certificateUUID) {
this.reset()
this.contentElement.innerHTML = ''
for (const [key, value] of Object.entries(data)) {
this.#addItem(key, value)
}
await this.load(async () => {
await this.#showMetadata(metadataUrl, certificateUUID)
})
}
#addItem(label, value) {
const dataItem = dataItemTemplate.cloneNode(true)
dataItem.innerHTML = dataItem.innerHTML.replace('{label}', label).replace('{value}', value)
this.contentElement.append(dataItem.content)
}
async #showMetadata(metadataUrl, certificateUUID) {
if(metadataUrl === null) {
return
}
const metadata = await this.#fetchMetadata(metadataUrl, certificateUUID)
if(!metadata) {
return
}
for (const [key, value] of Object.entries(metadata)) {
this.#addItem(key, value)
}
}
async #fetchMetadata(metadataUrl, certificateUUID) {
const url = `${metadataUrl}?certificate=${certificateUUID}`
let metadata = undefined
try {
const response = await fetch(url)
const content = await response.json()
if(response.ok && content.err === 0) {
metadata = content.data
}
else {
this.showError(translate('metadata_api_error'))
}
} catch(err) {
if(err.name === 'NetworkError') {
this.showError(translate('metadata_network_error'))
}
else {
throw err
}
}
return metadata
}
}
window.customElements.define('qrcode-reader-data', QRCodeReaderData)

View File

@ -28,6 +28,7 @@
"to": "{% trans 'To' %}",
"valid": "{% trans 'Valid QR code' %}",
"validity_section_title": "{% trans 'Validity' %}",
"data_section_title": "{% trans 'Data' %}"
}
</script>
<link rel="stylesheet" href="{% static 'qrcode/css/style.css' %}">

View File

@ -110,14 +110,14 @@ qrcodeReaderTest('qrcode reader shows valid qrcode informations', async ({mock})
expect(validity.innerText).toMatch(/from :\s*10\/31\/2023, 11:00:00 PM\s*to :\s*12\/1\/2023, 10:59:59 PM/)
const labels = popup.querySelectorAll('.qrcode-reader--data-item-label')
const labels = popup.querySelectorAll('.qrcode-reader-data--item-label')
expect(labels.length).toBe(3)
expect(labels[0].innerText).toMatch('last_name')
expect(labels[1].innerText).toMatch('first_name')
expect(labels[2].innerText).toMatch('license_plate')
const values = popup.querySelectorAll('.qrcode-reader--data-item-value')
const values = popup.querySelectorAll('.qrcode-reader-data--item-value')
expect(values.length).toBe(3)
expect(values[0].innerText).toMatch('Abitbol')
@ -261,8 +261,8 @@ qrcodeReaderMetadataTest('qrcode reader fetches metadata', async ({mock, loadMet
const {reader} = mock
await loadMetadata(okCodeData, async (url) => {
expect(url).toBe('https://orson-welles.io?certificate=853b9497-6c1a-4ff1-8604-6dc7cca9003c')
const spinner = reader.querySelector('.qrcode-reader--spinner')
expect(spinner).not.toBe(null)
const section = reader.querySelector('qrcode-reader-data .qrcode-reader-section')
expect(section.classList.contains('loading')).toBe(true)
return {
ok: true,
json: () => new Promise((resolve) => {
@ -271,20 +271,20 @@ qrcodeReaderMetadataTest('qrcode reader fetches metadata', async ({mock, loadMet
}
})
const spinner = reader.querySelector('.qrcode-reader--spinner')
expect(spinner).toBe(null)
const section = reader.querySelector('qrcode-reader-data .qrcode-reader-section')
expect(section.classList.contains('loading')).toBe(false)
const labels = reader.querySelectorAll('.qrcode-reader--metadata-items .qrcode-reader--data-item-label')
const labels = reader.querySelectorAll('.qrcode-reader-data--item-label')
expect(labels.length).toBe(2)
expect(labels[0].innerText).toMatch('Classe')
expect(labels[1].innerText).toMatch('Ravioles')
expect(labels.length).toBe(5)
expect(labels[3].innerText).toMatch('Classe')
expect(labels[4].innerText).toMatch('Ravioles')
const values = reader.querySelectorAll('.qrcode-reader--metadata-items .qrcode-reader--data-item-value')
const values = reader.querySelectorAll('.qrcode-reader-data--item-value')
expect(values.length).toBe(2)
expect(values[0].innerText).toMatch('Oui')
expect(values[1].innerText).toMatch('Non')
expect(values.length).toBe(5)
expect(values[3].innerText).toMatch('Oui')
expect(values[4].innerText).toMatch('Non')
})
@ -297,9 +297,10 @@ qrcodeReaderMetadataTest('qrcode reader show feedback on metadata network error'
const spinner = reader.querySelector('.qrcode-reader--spinner')
expect(spinner).toBe(null)
const items = reader.querySelector('.qrcode-reader--metadata-items')
expect(items.classList.contains('error')).toBe(true)
expect(items.innerText).toBe('metadata_network_error')
const section = reader.querySelector('qrcode-reader-data .qrcode-reader-section')
const errors = reader.querySelector('qrcode-reader-data .qrcode-reader-section--errors')
expect(section.classList.contains('error')).toBe(true)
expect(errors.innerText).toBe('metadata_network_error')
})
qrcodeReaderMetadataTest('qrcode reader show feedback on http error', async ({mock, loadMetadata}) => {
@ -314,9 +315,10 @@ qrcodeReaderMetadataTest('qrcode reader show feedback on http error', async ({mo
const spinner = reader.querySelector('.qrcode-reader--spinner')
expect(spinner).toBe(null)
const items = reader.querySelector('.qrcode-reader--metadata-items')
expect(items.classList.contains('error')).toBe(true)
expect(items.innerText.trim()).toBe('metadata_api_error')
const section = reader.querySelector('qrcode-reader-data .qrcode-reader-section')
const errors = reader.querySelector('qrcode-reader-data .qrcode-reader-section--errors')
expect(section.classList.contains('error')).toBe(true)
expect(errors.innerText.trim()).toBe('metadata_api_error')
})
qrcodeReaderMetadataTest('qrcode reader show feedback on api error', async ({mock, loadMetadata}) => {
@ -334,9 +336,10 @@ qrcodeReaderMetadataTest('qrcode reader show feedback on api error', async ({moc
const spinner = reader.querySelector('.qrcode-reader--spinner')
expect(spinner).toBe(null)
const items = reader.querySelector('.qrcode-reader--metadata-items')
expect(items.classList.contains('error')).toBe(true)
expect(items.innerText.trim()).toBe('metadata_api_error')
const section = reader.querySelector('qrcode-reader-data .qrcode-reader-section')
const errors = reader.querySelector('qrcode-reader-data .qrcode-reader-section--errors')
expect(section.classList.contains('error')).toBe(true)
expect(errors.innerText.trim()).toBe('metadata_api_error')
})
qrcodeReaderMetadataTest("qrcode reader doesn't refetch metadata for the same certificate if popup is opened", async ({mock, loadMetadata}) => {
@ -349,14 +352,14 @@ qrcodeReaderMetadataTest("qrcode reader doesn't refetch metadata for the same ce
}
})
let items = reader.querySelector('.qrcode-reader--metadata-items')
expect(reader.querySelectorAll('.qrcode-reader-data--item-value').length).toStrictEqual(5)
// loading the same certificate doesn't refeches metadata
await loadMetadata(okCodeData, async (url) => {
expect.unreachable()
})
expect(reader.querySelector('.qrcode-reader--metadata-items')).toBe(items)
expect(reader.querySelectorAll('.qrcode-reader-data--item-value').length).toStrictEqual(5)
// loading another certificate refetches metadata
await loadMetadata(certificateWithoutValidity, async (url) => {
@ -366,8 +369,7 @@ qrcodeReaderMetadataTest("qrcode reader doesn't refetch metadata for the same ce
}
})
expect(reader.querySelector('.qrcode-reader--metadata-items')).not.toBe(items)
items = reader.querySelector('.qrcode-reader--metadata-items')
expect(reader.querySelectorAll('.qrcode-reader-data--item-value').length).toStrictEqual(3)
const closeButton = reader.querySelector('.qrcode-reader--close-popup-button')
closeButton.dispatchEvent(new Event('click'))
@ -380,7 +382,7 @@ qrcodeReaderMetadataTest("qrcode reader doesn't refetch metadata for the same ce
}
})
expect(reader.querySelector('.qrcode-reader--metadata-items')).not.toBe(items)
expect(reader.querySelectorAll('.qrcode-reader-data--item-value').length).toStrictEqual(3)
})
const qrcodeReaderTallyTest = qrcodeReaderTest.extend({