feat(cibg): adopt the vendored CIBG Bestand-upload component
The upload suite now wraps the vendored CIBG file-upload classes instead of hand-rolling from tokens: - file-input becomes a .file-picker-drop-area with drag-and-drop + a real .btn-upload button and a visually-hidden, focusable <input>; renders an always-visible instruction (allowed types + max size) linked to the input via aria-describedby (pattern requirement). - The file list is a semantic ul.file-list; single-upload is an li[app-single-upload] .file-container (native <li> child) with .actions (retry/.icon-remove) + progress; document-chip renders the .file block (status glyph, .file-name link, .file-meta size/status). - Category validation moves ABOVE the block as .upload-validation > .feedback. - Instruction text uses the default (not subtle) foreground for WCAG AA contrast on the grey drop-area. DocumentUpload's public inputs/outputs are unchanged — both wizards keep working. GREEN: lint, tokens, 181 tests, build, 137 axe stories. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1442
documentation.json
1442
documentation.json
File diff suppressed because one or more lines are too long
@@ -4,9 +4,10 @@ import { DeliveryChannelToggleComponent } from '../delivery-channel-toggle/deliv
|
||||
import { FileInputComponent } from '../file-input/file-input.component';
|
||||
import { SingleUploadComponent } from '../single-upload/single-upload.component';
|
||||
|
||||
/** Organism: one document category — its label/description, an optional delivery
|
||||
channel toggle, and (when digital) a file picker plus the list of uploads. Pure
|
||||
UI: emits selection/removal/retry/delete and channel changes; no HTTP or rules. */
|
||||
/** Organism: one document category (CIBG Bestand-upload) — its label/description, an
|
||||
optional delivery channel toggle, and (when digital) a validation message, the
|
||||
file picker/drop-zone, and the `ul.file-list` of uploads. Pure UI: emits
|
||||
selection/removal/retry/delete and channel changes; no HTTP or rules. */
|
||||
@Component({
|
||||
selector: 'app-document-category',
|
||||
imports: [DeliveryChannelToggleComponent, FileInputComponent, SingleUploadComponent],
|
||||
@@ -15,14 +16,7 @@ import { SingleUploadComponent } from '../single-upload/single-upload.component'
|
||||
.label { font-weight: var(--rhc-text-font-weight-semi-bold); margin-block-end: var(--rhc-space-max-sm); }
|
||||
.req { font-weight: var(--rhc-text-font-weight-regular); color: var(--rhc-color-foreground-subtle); }
|
||||
.desc { color: var(--rhc-color-foreground-subtle); font-size: var(--rhc-text-font-size-sm); margin-block-end: var(--rhc-space-max-md); }
|
||||
.uploads { display: flex; flex-direction: column; gap: var(--rhc-space-max-md); margin-block-start: var(--rhc-space-max-md); }
|
||||
.rejection {
|
||||
color: var(--rhc-color-foreground-default);
|
||||
font-weight: var(--rhc-text-font-weight-semi-bold);
|
||||
margin-block-start: var(--rhc-space-max-sm);
|
||||
border-inline-start: var(--rhc-border-width-md) solid var(--rhc-color-rood-500);
|
||||
padding-inline-start: var(--rhc-space-max-md);
|
||||
}
|
||||
.file-list { list-style: none; padding: 0; margin-block-start: var(--rhc-space-max-md); }
|
||||
`],
|
||||
template: `
|
||||
<div class="label">
|
||||
@@ -40,27 +34,33 @@ import { SingleUploadComponent } from '../single-upload/single-upload.component'
|
||||
}
|
||||
|
||||
@if (channel() === 'digital') {
|
||||
<!-- CIBG: validation sits ABOVE the upload block. -->
|
||||
@if (rejection()) {
|
||||
<div class="upload-validation">
|
||||
<div class="feedback feedback-warning" role="alert">{{ rejection() }}</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<app-file-input
|
||||
[inputId]="category().categoryId + '-file'"
|
||||
[label]="fileInputLabel()"
|
||||
[accept]="category().acceptedTypes"
|
||||
[maxSizeMb]="category().maxSizeMb"
|
||||
[multiple]="category().multiple"
|
||||
(filesSelected)="fileSelected.emit($event)" />
|
||||
|
||||
<div class="uploads">
|
||||
@for (u of uploads(); track u.localId) {
|
||||
<app-single-upload
|
||||
animate.enter="app-item-enter"
|
||||
animate.leave="app-item-leave"
|
||||
[upload]="u"
|
||||
[previewUrlFor]="previewUrlFor()"
|
||||
(remove)="onRemove(u)"
|
||||
(retry)="retryUpload.emit(u.localId)" />
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (rejection()) {
|
||||
<div class="rejection" role="alert">{{ rejection() }}</div>
|
||||
@if (uploads().length) {
|
||||
<ul class="file-list">
|
||||
@for (u of uploads(); track u.localId) {
|
||||
<li app-single-upload
|
||||
animate.enter="app-item-enter"
|
||||
animate.leave="app-item-leave"
|
||||
[upload]="u"
|
||||
[previewUrlFor]="previewUrlFor()"
|
||||
(remove)="onRemove(u)"
|
||||
(retry)="retryUpload.emit(u.localId)"></li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -1,72 +1,55 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
import { Component, computed, input } from '@angular/core';
|
||||
import type { UploadStatus } from '@shared/upload/upload.machine';
|
||||
import { UploadStatusIconComponent } from '../upload-status-icon/upload-status-icon.component';
|
||||
|
||||
/** Atom: a single uploaded-file chip — filename + status glyph + actions. Pure UI:
|
||||
emits `remove`/`retry`; the container decides what they mean. */
|
||||
const STATUS_LABELS: Record<UploadStatus['type'], string> = {
|
||||
idle: '',
|
||||
queued: $localize`:@@upload.status.queued:In wachtrij`,
|
||||
uploading: $localize`:@@upload.status.uploading:Bezig met uploaden`,
|
||||
complete: $localize`:@@upload.status.complete:Geüpload`,
|
||||
failed: $localize`:@@upload.status.failed:Mislukt`,
|
||||
deleting: $localize`:@@upload.status.deleting:Bezig met verwijderen`,
|
||||
deleted: '',
|
||||
};
|
||||
|
||||
/** Atom: the `.file` block of a CIBG Bestand-upload file row — a status glyph, the
|
||||
filename (a download link once complete) and a `.file-meta` line (size + status).
|
||||
`display:contents` so `.file` becomes a flex child of the `.file-container` row.
|
||||
Pure UI. */
|
||||
@Component({
|
||||
selector: 'app-document-chip',
|
||||
imports: [UploadStatusIconComponent],
|
||||
styles: [`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--rhc-space-max-md);
|
||||
padding-block: var(--rhc-space-max-sm);
|
||||
padding-inline: var(--rhc-space-max-md);
|
||||
border: var(--rhc-border-width-sm) solid var(--rhc-color-border-subtle);
|
||||
border-radius: var(--rhc-border-radius-md);
|
||||
background: var(--rhc-color-cool-grey-100);
|
||||
}
|
||||
.name { flex: 1; }
|
||||
.action {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
color: var(--rhc-color-foreground-link);
|
||||
font: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.action:hover { color: var(--rhc-color-foreground-link-hover); }
|
||||
a.action { display: inline-block; }
|
||||
:host { display: contents; }
|
||||
.file { display: flex; flex-direction: column; gap: var(--rhc-space-max-xs); min-inline-size: 0; }
|
||||
.file-name { word-break: break-all; }
|
||||
`],
|
||||
template: `
|
||||
<app-upload-status-icon [status]="status().type" />
|
||||
<span class="name">{{ fileName() }}</span>
|
||||
@if (previewUrl()) {
|
||||
<a class="action" [href]="previewUrl()" target="_blank" rel="noopener" [attr.aria-label]="previewLabel">
|
||||
<span i18n="@@upload.chip.preview">Voorbeeld / Download</span>
|
||||
</a>
|
||||
}
|
||||
@if (status().type === 'failed') {
|
||||
<button type="button" class="action" [attr.aria-label]="retryLabel" (click)="retry.emit()">
|
||||
<span i18n="@@upload.chip.retry">Opnieuw</span>
|
||||
</button>
|
||||
}
|
||||
@if (status().type !== 'deleting') {
|
||||
<button type="button" class="action" [attr.aria-label]="removeLabel" (click)="remove.emit()">
|
||||
<span i18n="@@upload.chip.remove">Verwijderen</span>
|
||||
</button>
|
||||
}
|
||||
<div class="file">
|
||||
<span class="d-inline-flex align-items-center gap-2">
|
||||
<app-upload-status-icon [status]="status().type" />
|
||||
@if (previewUrl()) {
|
||||
<a class="file-name" [href]="previewUrl()" target="_blank" rel="noopener">{{ fileName() }}</a>
|
||||
} @else {
|
||||
<span class="file-name">{{ fileName() }}</span>
|
||||
}
|
||||
</span>
|
||||
<span class="file-meta">{{ meta() }}</span>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class DocumentChipComponent {
|
||||
fileName = input.required<string>();
|
||||
status = input.required<UploadStatus>();
|
||||
/** When set, show a preview/download link (opens the stored bytes). */
|
||||
fileSizeMb = input(0);
|
||||
/** When set, the filename is a preview/download link (opens the stored bytes). */
|
||||
previewUrl = input<string>();
|
||||
|
||||
remove = output<void>();
|
||||
retry = output<void>();
|
||||
|
||||
protected get removeLabel(): string {
|
||||
return $localize`:@@upload.chip.removeAria:${this.fileName()}:fileName: verwijderen`;
|
||||
}
|
||||
protected get previewLabel(): string {
|
||||
return $localize`:@@upload.chip.previewAria:${this.fileName()}:fileName: bekijken of downloaden`;
|
||||
}
|
||||
protected get retryLabel(): string {
|
||||
return $localize`:@@upload.chip.retryAria:${this.fileName()}:fileName: opnieuw uploaden`;
|
||||
}
|
||||
/** `.file-meta`: file size + status word (+ the failure reason when failed). */
|
||||
protected meta = computed(() => {
|
||||
const s = this.status();
|
||||
const size = this.fileSizeMb() > 0 ? `${this.fileSizeMb().toFixed(1)} MB` : '';
|
||||
const label = s.type === 'failed' && s.reason ? s.reason : STATUS_LABELS[s.type];
|
||||
return [size, label].filter(Boolean).join(' · ');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,20 +7,21 @@ const meta: Meta<DocumentChipComponent> = {
|
||||
component: DocumentChipComponent,
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<app-document-chip [fileName]="fileName" [status]="status" [previewUrl]="previewUrl" />`,
|
||||
// The .file/.file-name/.file-meta styling only applies inside ul.file-list > .file-container.
|
||||
template: `<ul class="file-list"><li class="file-container"><app-document-chip [fileName]="fileName" [status]="status" [fileSizeMb]="fileSizeMb" [previewUrl]="previewUrl" /></li></ul>`,
|
||||
}),
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<DocumentChipComponent>;
|
||||
|
||||
export const Complete: Story = {
|
||||
args: { fileName: 'diploma.pdf', status: { type: 'complete', documentId: 'doc-1' } as UploadStatus },
|
||||
args: { fileName: 'diploma.pdf', fileSizeMb: 1.2, status: { type: 'complete', documentId: 'doc-1' } as UploadStatus },
|
||||
};
|
||||
|
||||
export const WithPreview: Story = {
|
||||
args: { fileName: 'diploma.pdf', status: { type: 'complete', documentId: 'doc-1' } as UploadStatus, previewUrl: '/api/v1/uploads/doc-1/content' },
|
||||
args: { fileName: 'diploma.pdf', fileSizeMb: 1.2, status: { type: 'complete', documentId: 'doc-1' } as UploadStatus, previewUrl: '/api/v1/uploads/doc-1/content' },
|
||||
};
|
||||
|
||||
export const Failed: Story = {
|
||||
args: { fileName: 'diploma.pdf', status: { type: 'failed', reason: 'Netwerkfout' } as UploadStatus },
|
||||
args: { fileName: 'diploma.pdf', fileSizeMb: 1.2, status: { type: 'failed', reason: 'Netwerkfout' } as UploadStatus },
|
||||
};
|
||||
|
||||
@@ -1,62 +1,111 @@
|
||||
import { Component, ElementRef, input, output, viewChild } from '@angular/core';
|
||||
import { Component, ElementRef, computed, input, output, signal, viewChild } from '@angular/core';
|
||||
|
||||
/** Atom: a styled file picker. Wraps a native `<input type="file">` and emits the
|
||||
selected files; resets its value after each change so re-picking the same file
|
||||
re-fires. Pure UI — no validation, no upload. */
|
||||
let nextId = 0;
|
||||
|
||||
/** Friendly labels for the MIME types the backend sends in `acceptedTypes`. */
|
||||
const TYPE_LABELS: Record<string, string> = {
|
||||
'application/pdf': 'PDF',
|
||||
'image/jpeg': 'JPG',
|
||||
'image/jpg': 'JPG',
|
||||
'image/png': 'PNG',
|
||||
'application/msword': 'Word',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'Word',
|
||||
};
|
||||
|
||||
/** Atom: the CIBG Huisstijl **Bestand-upload** control
|
||||
(designsystem.cibg.nl/componenten/bestand-upload) — a `.file-picker-drop-area`
|
||||
with an always-visible instruction (allowed types + max size), a real
|
||||
`.btn-upload` button, and a visually-hidden native `<input type="file">`. Supports
|
||||
click-to-pick and drag-and-drop; resets its value after each change so re-picking
|
||||
the same file re-fires. The instruction is linked to the input via
|
||||
`aria-describedby` (pattern requirement). Pure UI — no validation, no upload. */
|
||||
@Component({
|
||||
selector: 'app-file-input',
|
||||
styles: [`
|
||||
:host { display: inline-block; }
|
||||
.field { position: relative; display: inline-flex; }
|
||||
input[type='file'] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type='file']:disabled { cursor: not-allowed; }
|
||||
.label {
|
||||
pointer-events: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--rhc-space-max-sm);
|
||||
}
|
||||
:host { display: block; }
|
||||
.file-picker-drop-area { display: flex; flex-direction: column; align-items: flex-start; gap: var(--rhc-space-max-sm); }
|
||||
/* Default (not subtle) foreground: subtle grey on the grey drop-area fails WCAG AA contrast. */
|
||||
.upload-instructions { margin: 0; color: var(--rhc-color-foreground-default); font-size: var(--rhc-text-font-size-sm); }
|
||||
.btn-upload { cursor: pointer; }
|
||||
.btn-upload.disabled { cursor: not-allowed; }
|
||||
/* The input is visually hidden but still focusable; surface its focus on the button. */
|
||||
input:focus-visible + .btn-upload { outline: var(--rhc-border-width-md) solid var(--rhc-color-foreground-link); outline-offset: 2px; }
|
||||
`],
|
||||
template: `
|
||||
<span class="field">
|
||||
<div class="file-picker-drop-area" [class.drag-over]="dragging()"
|
||||
(dragover)="onDragOver($event)" (dragleave)="onDragLeave($event)" (drop)="onDrop($event)">
|
||||
@if (instructions()) {
|
||||
<p class="upload-instructions" [id]="instructionsId">{{ instructions() }}</p>
|
||||
}
|
||||
<input
|
||||
#fileInput
|
||||
class="visually-hidden"
|
||||
type="file"
|
||||
[id]="inputId()"
|
||||
[attr.aria-label]="label()"
|
||||
[attr.aria-describedby]="instructions() ? instructionsId : null"
|
||||
[accept]="accept().join(',')"
|
||||
[multiple]="multiple()"
|
||||
[disabled]="disabled()"
|
||||
(change)="onChange($event)" />
|
||||
<span class="btn btn-outline-primary label" [class.disabled]="disabled()">
|
||||
<span aria-hidden="true">↑</span>
|
||||
<span>{{ label() }}</span>
|
||||
</span>
|
||||
</span>
|
||||
<label class="btn btn-outline-primary btn-upload" [class.disabled]="disabled()" [attr.for]="inputId()">
|
||||
{{ buttonText() }}
|
||||
</label>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class FileInputComponent {
|
||||
accept = input<string[]>([]);
|
||||
multiple = input(false);
|
||||
disabled = input(false);
|
||||
maxSizeMb = input(0);
|
||||
inputId = input.required<string>();
|
||||
/** Accessible name + button text; the domain caller supplies a per-category label. */
|
||||
/** Accessible name for the input; the domain caller supplies a per-category label. */
|
||||
label = input($localize`:@@upload.fileInput.label:Bestand kiezen`);
|
||||
|
||||
filesSelected = output<File[]>();
|
||||
|
||||
protected readonly instructionsId = `upload-instructions-${nextId++}`;
|
||||
protected readonly dragging = signal(false);
|
||||
|
||||
private fileInput = viewChild.required<ElementRef<HTMLInputElement>>('fileInput');
|
||||
|
||||
protected buttonText = computed(() =>
|
||||
this.multiple()
|
||||
? $localize`:@@upload.fileInput.addMany:Bestanden toevoegen`
|
||||
: $localize`:@@upload.fileInput.addOne:Bestand toevoegen`,
|
||||
);
|
||||
|
||||
/** Always-visible instruction: allowed file types + max size (CIBG pattern). */
|
||||
protected instructions = computed(() => {
|
||||
const types = this.accept().map((m) => TYPE_LABELS[m] ?? m.split('/').pop()?.toUpperCase() ?? m).join(', ');
|
||||
const size = this.maxSizeMb();
|
||||
if (types && size > 0) return $localize`:@@upload.fileInput.instrBoth:Toegestaan: ${types}:types: · max ${size}:size: MB`;
|
||||
if (types) return $localize`:@@upload.fileInput.instrTypes:Toegestaan: ${types}:types:`;
|
||||
if (size > 0) return $localize`:@@upload.fileInput.instrSize:Max ${size}:size: MB`;
|
||||
return '';
|
||||
});
|
||||
|
||||
onChange(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
this.filesSelected.emit(Array.from(input.files ?? []));
|
||||
this.fileInput().nativeElement.value = '';
|
||||
}
|
||||
|
||||
onDragOver(event: DragEvent) {
|
||||
if (this.disabled()) return;
|
||||
event.preventDefault(); // allow drop
|
||||
this.dragging.set(true);
|
||||
}
|
||||
onDragLeave(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
this.dragging.set(false);
|
||||
}
|
||||
onDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
this.dragging.set(false);
|
||||
if (this.disabled()) return;
|
||||
const files = Array.from(event.dataTransfer?.files ?? []);
|
||||
if (files.length) this.filesSelected.emit(files);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,20 @@ const meta: Meta<FileInputComponent> = {
|
||||
component: FileInputComponent,
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<app-file-input [inputId]="inputId" [accept]="accept" [multiple]="multiple" [disabled]="disabled" />`,
|
||||
template: `<app-file-input [inputId]="inputId" [accept]="accept" [maxSizeMb]="maxSizeMb" [multiple]="multiple" [disabled]="disabled" />`,
|
||||
}),
|
||||
};
|
||||
export default meta;
|
||||
type Story = StoryObj<FileInputComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: { inputId: 'diploma', accept: ['application/pdf'], multiple: false, disabled: false },
|
||||
args: { inputId: 'diploma', accept: ['application/pdf', 'image/jpeg'], maxSizeMb: 10, multiple: false, disabled: false },
|
||||
};
|
||||
|
||||
export const Multiple: Story = {
|
||||
args: { inputId: 'cv', accept: ['application/pdf'], maxSizeMb: 5, multiple: true, disabled: false },
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: { inputId: 'diploma-disabled', accept: ['application/pdf'], multiple: false, disabled: true },
|
||||
args: { inputId: 'diploma-disabled', accept: ['application/pdf'], maxSizeMb: 10, multiple: false, disabled: true },
|
||||
};
|
||||
|
||||
@@ -3,23 +3,36 @@ import type { Upload } from '@shared/upload/upload.machine';
|
||||
import { DocumentChipComponent } from '../document-chip/document-chip.component';
|
||||
import { UploadProgressBarComponent } from '../upload-progress-bar/upload-progress-bar.component';
|
||||
|
||||
/** Molecule: one upload row — its chip plus a progress bar while uploading. Pure UI:
|
||||
forwards `remove`/`retry` from the chip. */
|
||||
/** Molecule: one row in a CIBG Bestand-upload file list — a `.file-container` `<li>`
|
||||
with the `.file` block (via document-chip), the `.actions` (retry/remove), and a
|
||||
progress bar while uploading. Used on an `<li>` so `ul.file-list`'s direct child is
|
||||
a native `<li>`. Pure UI: emits `remove`/`retry`; the container decides what they mean. */
|
||||
@Component({
|
||||
selector: 'app-single-upload',
|
||||
selector: 'li[app-single-upload]',
|
||||
host: { class: 'file-container' },
|
||||
imports: [DocumentChipComponent, UploadProgressBarComponent],
|
||||
styles: [`
|
||||
:host { display: flex; flex-direction: column; gap: var(--rhc-space-max-sm); }
|
||||
:host { flex-wrap: wrap; align-items: center; }
|
||||
.upload-progress { flex: 1 0 100%; margin-block-start: var(--rhc-space-max-sm); }
|
||||
.actions .btn { background: none; border: none; cursor: pointer; padding: 0; }
|
||||
.actions .btn-retry { color: var(--rhc-color-foreground-link); text-decoration: underline; font: inherit; }
|
||||
`],
|
||||
template: `
|
||||
<app-document-chip
|
||||
[fileName]="upload().fileName"
|
||||
[status]="upload().status"
|
||||
[previewUrl]="previewUrl()"
|
||||
(remove)="remove.emit()"
|
||||
(retry)="retry.emit()" />
|
||||
[fileSizeMb]="upload().fileSizeMb"
|
||||
[previewUrl]="previewUrl()" />
|
||||
<div class="actions">
|
||||
@if (upload().status.type === 'failed') {
|
||||
<button type="button" class="btn btn-retry" [attr.aria-label]="retryLabel" (click)="retry.emit()" i18n="@@upload.chip.retry">Opnieuw</button>
|
||||
}
|
||||
@if (upload().status.type !== 'deleting') {
|
||||
<button type="button" class="btn icon-remove" [attr.aria-label]="removeLabel" (click)="remove.emit()"></button>
|
||||
}
|
||||
</div>
|
||||
@if (progressPct() !== null) {
|
||||
<app-upload-progress-bar [progressPct]="progressPct()!" />
|
||||
<app-upload-progress-bar class="upload-progress" [progressPct]="progressPct()!" />
|
||||
}
|
||||
`,
|
||||
})
|
||||
@@ -41,4 +54,11 @@ export class SingleUploadComponent {
|
||||
|
||||
remove = output<void>();
|
||||
retry = output<void>();
|
||||
|
||||
protected get removeLabel(): string {
|
||||
return $localize`:@@upload.chip.removeAria:${this.upload().fileName}:fileName: verwijderen`;
|
||||
}
|
||||
protected get retryLabel(): string {
|
||||
return $localize`:@@upload.chip.retryAria:${this.upload().fileName}:fileName: opnieuw uploaden`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ const meta: Meta<SingleUploadComponent> = {
|
||||
component: SingleUploadComponent,
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<app-single-upload [upload]="upload" />`,
|
||||
template: `<ul class="file-list"><li app-single-upload [upload]="upload"></li></ul>`,
|
||||
}),
|
||||
};
|
||||
export default meta;
|
||||
|
||||
Reference in New Issue
Block a user