Skip to content

Commit 9234b8b

Browse files
milanmajchrakclaude
andcommitted
Port #1411 to dtq-dev-9-base: UoE/Bitstream name in upload notifications
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 9234b8b

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)