Skip to content

Commit 04bf07d

Browse files
Port #1411 to dtq-dev-9-base: UoE/Bitstream name in upload notifications (#1506)
Upload success and failure toasts in the submission form now name the file they refer to ("File "report.pdf" uploaded successfully") instead of the anonymous "Upload successful" / "Upload failed". UploaderComponent gains an additive `onCompleteItemWithFile` output carrying the parsed response plus the client-side file name; the legacy `onCompleteItem` is retained and still emits the bare parsed body first, so the four other `(onCompleteItem)` consumers are untouched. `onUploadError` is retyped from `any` to the existing `UploaderError`. `SubmissionUploadFilesComponent` builds every upload notification through one `getNotificationContent` helper, whose `default` interpolate param makes an untranslated locale render the generic message rather than a raw dotted key. v9 adaptations against the 9-base pre-image (which is byte-identical with dspace-9.3 for all six pre-existing files): * `submission-upload-files.component.html` needed two changed lines, not one: 9-base binds `(onUploadError)="onUploadError()"` without `$event`. * `submission-upload-files.component.ts` `onUploadError` had to be written out rather than patched: the 9-base pre-image is `public onUploadError()` with no parameter and no size-limit comparison, so there is no 7.x hunk to apply. * Both specs are the 9-base standalone TestBed harnesses; the 7.x `ScrollToService` / `ConfigurationDataService` / `SharedModule` providers are deliberately not reintroduced, and `of` replaces `of as observableOf`. * Two of the source's eleven new uploader spec cases are NOT ported: they drive `uploader.onWhenAddingFileFailed`, which only exists on dtq-dev because of the CLARIN client-side size-limit branch (#424) that is absent from 9-base. They belong with the #424 port (backlog B-5 / card X-04). The uploader spec is therefore 1 -> 10, not 1 -> 12; `submission-upload-files.component.spec.ts` is 4 -> 17 as designed, and it still covers the size-limit short-circuit because that lives in the component, not in the uploader. The `isFileSizeLimitError` comparison is ported verbatim even though #424 is absent: the key `submission.sections.upload.upload-failed.size-limit-exceeded` does exist on 9-base (en.json5:7733), so the branch compiles and is provably never taken - dead but harmless - and the later #424 port stays purely additive. Source: 6487711 (dtq-dev PR #1411) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent bba4d90 commit 04bf07d

8 files changed

Lines changed: 496 additions & 17 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* An interface that represents a completed single-file upload, carrying both the
3+
* parsed response body and the client-side file name of the file that completed.
4+
*/
5+
export interface UploaderCompleteEvent {
6+
/**
7+
* The parsed response body (e.g. the submission object returned by REST)
8+
*/
9+
response: any;
10+
11+
/**
12+
* The client-side name of the file that completed uploading. Present only when a
13+
* non-empty file name is known — an empty file name is never emitted, so the presence
14+
* of this key means a usable name is available. Whitespace-only names are not trimmed.
15+
*/
16+
fileName?: string;
17+
}

src/app/shared/upload/uploader/uploader.component.spec.ts

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,191 @@ describe('Uploader component', () => {
6868
expect(app).toBeDefined();
6969
}));
7070

71+
it('should emit both onCompleteItem and onCompleteItemWithFile on a completed upload', inject([UploaderComponent], (app: UploaderComponent) => {
72+
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
73+
url: 'http://test',
74+
authToken: null,
75+
disableMultipart: false,
76+
itemAlias: null,
77+
});
78+
app.ngOnInit();
79+
app.ngAfterViewInit();
80+
81+
spyOn(app.onCompleteItem, 'emit');
82+
spyOn(app.onCompleteItemWithFile, 'emit');
83+
84+
const parsed = { foo: 'bar' };
85+
app.uploader.onCompleteItem({ file: { name: 'test.pdf' } } as any, JSON.stringify(parsed), 200, {});
86+
87+
expect(app.onCompleteItem.emit).toHaveBeenCalledWith(parsed);
88+
expect(app.onCompleteItem.emit).toHaveBeenCalledTimes(1);
89+
expect(app.onCompleteItemWithFile.emit).toHaveBeenCalledWith({ response: parsed, fileName: 'test.pdf' });
90+
}));
91+
92+
it('should not emit either completion output when the response body is empty', inject([UploaderComponent], (app: UploaderComponent) => {
93+
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
94+
url: 'http://test',
95+
authToken: null,
96+
disableMultipart: false,
97+
itemAlias: null,
98+
});
99+
app.ngOnInit();
100+
app.ngAfterViewInit();
101+
102+
spyOn(app.onCompleteItem, 'emit');
103+
spyOn(app.onCompleteItemWithFile, 'emit');
104+
105+
app.uploader.onCompleteItem({ file: { name: 'test.pdf' } } as any, '', 204, {});
106+
107+
expect(app.onCompleteItem.emit).not.toHaveBeenCalled();
108+
expect(app.onCompleteItemWithFile.emit).not.toHaveBeenCalled();
109+
}));
110+
111+
it('should omit fileName from the completion event when the item is undefined', inject([UploaderComponent], (app: UploaderComponent) => {
112+
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
113+
url: 'http://test',
114+
authToken: null,
115+
disableMultipart: false,
116+
itemAlias: null,
117+
});
118+
app.ngOnInit();
119+
app.ngAfterViewInit();
120+
121+
spyOn(app.onCompleteItemWithFile, 'emit');
122+
123+
const parsed = { foo: 'bar' };
124+
app.uploader.onCompleteItem(undefined, JSON.stringify(parsed), 200, {});
125+
126+
const arg = (app.onCompleteItemWithFile.emit as jasmine.Spy).calls.mostRecent().args[0];
127+
expect(app.onCompleteItemWithFile.emit).toHaveBeenCalledWith({ response: parsed });
128+
expect(Object.keys(arg)).toEqual(['response']);
129+
expect('fileName' in arg).toBeFalse();
130+
}));
131+
132+
it('should omit fileName from the completion event when the item has no file', inject([UploaderComponent], (app: UploaderComponent) => {
133+
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
134+
url: 'http://test',
135+
authToken: null,
136+
disableMultipart: false,
137+
itemAlias: null,
138+
});
139+
app.ngOnInit();
140+
app.ngAfterViewInit();
141+
142+
spyOn(app.onCompleteItemWithFile, 'emit');
143+
144+
const parsed = { foo: 'bar' };
145+
app.uploader.onCompleteItem({} as any, JSON.stringify(parsed), 200, {});
146+
147+
const arg = (app.onCompleteItemWithFile.emit as jasmine.Spy).calls.mostRecent().args[0];
148+
expect(app.onCompleteItemWithFile.emit).toHaveBeenCalledWith({ response: parsed });
149+
expect(Object.keys(arg)).toEqual(['response']);
150+
expect('fileName' in arg).toBeFalse();
151+
}));
152+
153+
it('should omit fileName from the completion event when the file has no name', inject([UploaderComponent], (app: UploaderComponent) => {
154+
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
155+
url: 'http://test',
156+
authToken: null,
157+
disableMultipart: false,
158+
itemAlias: null,
159+
});
160+
app.ngOnInit();
161+
app.ngAfterViewInit();
162+
163+
spyOn(app.onCompleteItemWithFile, 'emit');
164+
165+
const parsed = { foo: 'bar' };
166+
app.uploader.onCompleteItem({ file: {} } as any, JSON.stringify(parsed), 200, {});
167+
168+
const arg = (app.onCompleteItemWithFile.emit as jasmine.Spy).calls.mostRecent().args[0];
169+
expect(app.onCompleteItemWithFile.emit).toHaveBeenCalledWith({ response: parsed });
170+
expect(Object.keys(arg)).toEqual(['response']);
171+
expect('fileName' in arg).toBeFalse();
172+
}));
173+
174+
it('should omit fileName from the completion event when the file name is an empty string', inject([UploaderComponent], (app: UploaderComponent) => {
175+
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
176+
url: 'http://test',
177+
authToken: null,
178+
disableMultipart: false,
179+
itemAlias: null,
180+
});
181+
app.ngOnInit();
182+
app.ngAfterViewInit();
183+
184+
spyOn(app.onCompleteItemWithFile, 'emit');
185+
186+
const parsed = { foo: 'bar' };
187+
app.uploader.onCompleteItem({ file: { name: '' } } as any, JSON.stringify(parsed), 200, {});
188+
189+
const arg = (app.onCompleteItemWithFile.emit as jasmine.Spy).calls.mostRecent().args[0];
190+
expect(app.onCompleteItemWithFile.emit).toHaveBeenCalledWith({ response: parsed });
191+
expect(Object.keys(arg)).toEqual(['response']);
192+
expect('fileName' in arg).toBeFalse();
193+
}));
194+
195+
it('should keep a whitespace-only file name on the completion event', inject([UploaderComponent], (app: UploaderComponent) => {
196+
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
197+
url: 'http://test',
198+
authToken: null,
199+
disableMultipart: false,
200+
itemAlias: null,
201+
});
202+
app.ngOnInit();
203+
app.ngAfterViewInit();
204+
205+
spyOn(app.onCompleteItemWithFile, 'emit');
206+
207+
const parsed = { foo: 'bar' };
208+
app.uploader.onCompleteItem({ file: { name: ' ' } } as any, JSON.stringify(parsed), 200, {});
209+
210+
expect(app.onCompleteItemWithFile.emit).toHaveBeenCalledWith({ response: parsed, fileName: ' ' });
211+
}));
212+
213+
it('should emit a distinct file name for each of two sequential completed uploads', inject([UploaderComponent], (app: UploaderComponent) => {
214+
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
215+
url: 'http://test',
216+
authToken: null,
217+
disableMultipart: false,
218+
itemAlias: null,
219+
});
220+
app.ngOnInit();
221+
app.ngAfterViewInit();
222+
223+
spyOn(app.onCompleteItemWithFile, 'emit');
224+
225+
app.uploader.onCompleteItem({ file: { name: 'first.pdf' } } as any, JSON.stringify({ n: 1 }), 200, {});
226+
app.uploader.onCompleteItem({ file: { name: 'second.pdf' } } as any, JSON.stringify({ n: 2 }), 200, {});
227+
228+
const emitSpy = app.onCompleteItemWithFile.emit as jasmine.Spy;
229+
expect(emitSpy).toHaveBeenCalledTimes(2);
230+
expect(emitSpy.calls.argsFor(0)[0]).toEqual({ response: { n: 1 }, fileName: 'first.pdf' });
231+
expect(emitSpy.calls.argsFor(1)[0]).toEqual({ response: { n: 2 }, fileName: 'second.pdf' });
232+
}));
233+
234+
it('should emit onUploadError with the item, response, status and headers of the failed upload', inject([UploaderComponent], (app: UploaderComponent) => {
235+
app.uploadFilesOptions = Object.assign(new UploaderOptions(), {
236+
url: 'http://test',
237+
authToken: null,
238+
disableMultipart: false,
239+
itemAlias: null,
240+
});
241+
app.ngOnInit();
242+
app.ngAfterViewInit();
243+
244+
spyOn(app.onUploadError, 'emit');
245+
246+
app.uploader.onErrorItem({ file: { name: 'broken.zip' } } as any, 'boom', 500, {});
247+
248+
expect(app.onUploadError.emit).toHaveBeenCalledWith({
249+
item: { file: { name: 'broken.zip' } },
250+
response: 'boom',
251+
status: 500,
252+
headers: {},
253+
});
254+
}));
255+
71256
});
72257

73258
// declare a test component

src/app/shared/upload/uploader/uploader.component.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import {
3535
isUndefined,
3636
} from '../../empty.util';
3737
import { LiveRegionService } from '../../live-region/live-region.service';
38+
import { UploaderCompleteEvent } from './uploader-complete-event.model';
39+
import { UploaderError } from './uploader-error.model';
3840
import { UploaderOptions } from './uploader-options.model';
3941
import { UploaderProperties } from './uploader-properties.model';
4042

@@ -98,10 +100,16 @@ export class UploaderComponent implements OnInit, AfterViewInit {
98100
*/
99101
@Output() onCompleteItem: EventEmitter<any> = new EventEmitter<any>();
100102

103+
/**
104+
* The function to call when upload is completed, carrying the parsed response together with the
105+
* client-side file name. Emitted alongside {@link onCompleteItem} so existing consumers are unaffected.
106+
*/
107+
@Output() onCompleteItemWithFile: EventEmitter<UploaderCompleteEvent> = new EventEmitter<UploaderCompleteEvent>();
108+
101109
/**
102110
* The function to call on error occurred
103111
*/
104-
@Output() onUploadError: EventEmitter<any> = new EventEmitter<any>();
112+
@Output() onUploadError: EventEmitter<UploaderError> = new EventEmitter<UploaderError>();
105113

106114
/**
107115
* The function to call when a file is selected
@@ -218,6 +226,8 @@ export class UploaderComponent implements OnInit, AfterViewInit {
218226
if (isNotEmpty(response)) {
219227
const responsePath = JSON.parse(response);
220228
this.onCompleteItem.emit(responsePath);
229+
const fileName = item?.file?.name;
230+
this.onCompleteItemWithFile.emit(isNotEmpty(fileName) ? { response: responsePath, fileName } : { response: responsePath });
221231
}
222232
};
223233
this.uploader.onErrorItem = (item: any, response: any, status: any, headers: any) => {

src/app/submission/form/submission-upload-files/submission-upload-files.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
[enableDragOverDocument]="enableDragOverDocument"
66
[onBeforeUpload]="onBeforeUpload"
77
[uploadFilesOptions]="uploadFilesOptions"
8-
(onCompleteItem)="onCompleteItem($event)"
9-
(onUploadError)="onUploadError()"></ds-uploader>
8+
(onCompleteItemWithFile)="onCompleteItem($event)"
9+
(onUploadError)="onUploadError($event)"></ds-uploader>
1010
}

0 commit comments

Comments
 (0)