Feature: separate save / save & close buttons (#3575)

* Add setting to decide whether the edit dialog should automatically close on save

* Add the actual button to the ui

* Revert "Add the actual button to the ui"

This reverts commit e1f5a8bde0e8a2f6bafa2896d6a7e57e70d0c670.

* Revert "Add setting to decide whether the edit dialog should automatically close on save"

This reverts commit feef3c909b055b4bc4d2884324a4678348e1e536.

* Add button for save without exit

* Correct save button ordering, ensure perms, update translation strings

* fix e2e tests

* Add unit testing for save / save & close button

---------

Update messages.xlf

Update document-detail.component.spec.ts

Co-Authored-By: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
Daniel Dietzler 2023-06-18 06:47:52 +02:00 committed by shamoon
parent 4c4b571a88
commit 4693632c7d
5 changed files with 236 additions and 172 deletions

View File

@ -12,9 +12,13 @@ test('should activate / deactivate save button when changes are saved', async ({
await expect(page.getByTitle('Storage path', { exact: true })).toHaveText( await expect(page.getByTitle('Storage path', { exact: true })).toHaveText(
/\w+/ /\w+/
) )
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled() await expect(
page.getByRole('button', { name: 'Save', exact: true })
).toBeDisabled()
await page.getByTitle('Storage path').getByTitle('Clear all').click() await page.getByTitle('Storage path').getByTitle('Clear all').click()
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled() await expect(
page.getByRole('button', { name: 'Save', exact: true })
).toBeEnabled()
}) })
test('should warn on unsaved changes', async ({ page }) => { test('should warn on unsaved changes', async ({ page }) => {
@ -23,13 +27,17 @@ test('should warn on unsaved changes', async ({ page }) => {
await expect(page.getByTitle('Correspondent', { exact: true })).toHaveText( await expect(page.getByTitle('Correspondent', { exact: true })).toHaveText(
/\w+/ /\w+/
) )
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled() await expect(
page.getByRole('button', { name: 'Save', exact: true })
).toBeDisabled()
await page await page
.getByTitle('Storage path', { exact: true }) .getByTitle('Storage path', { exact: true })
.getByTitle('Clear all') .getByTitle('Clear all')
.click() .click()
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled() await expect(
await page.getByRole('button', { name: 'Close' }).click() page.getByRole('button', { name: 'Save', exact: true })
).toBeEnabled()
await page.getByRole('button', { name: 'Close', exact: true }).click()
await expect(page.getByRole('dialog')).toHaveText(/unsaved changes/) await expect(page.getByRole('dialog')).toHaveText(/unsaved changes/)
await page.getByRole('button', { name: 'Cancel' }).click() await page.getByRole('button', { name: 'Cancel' }).click()
await page.getByRole('link', { name: 'Close all' }).click() await page.getByRole('link', { name: 'Close all' }).click()

File diff suppressed because it is too large Load Diff

View File

@ -191,9 +191,12 @@
<div [ngbNavOutlet]="nav" class="mt-2"></div> <div [ngbNavOutlet]="nav" class="mt-2"></div>
<ng-container> <ng-container>
<button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Discard</button>&nbsp; <button type="button" class="btn btn-outline-secondary me-2" (click)="discard()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Discard</button>
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save & next</button>&nbsp; <ng-container *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
<button type="submit" class="btn btn-primary" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save</button>&nbsp; <button *ngIf="hasNext()" type="button" class="btn btn-outline-primary me-2" (click)="saveEditNext()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save &amp; next</button>
<button *ngIf="!hasNext()" type="button" class="btn btn-outline-primary me-2" (click)="save(true)" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save &amp; close</button>
<button type="submit" class="btn btn-primary" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save</button>
</ng-container>
</ng-container> </ng-container>
</form> </form>
</div> </div>

View File

@ -372,12 +372,25 @@ describe('DocumentDetailComponent', () => {
const updateSpy = jest.spyOn(documentService, 'update') const updateSpy = jest.spyOn(documentService, 'update')
const toastSpy = jest.spyOn(toastService, 'showInfo') const toastSpy = jest.spyOn(toastService, 'showInfo')
updateSpy.mockImplementation((o) => of(doc)) updateSpy.mockImplementation((o) => of(doc))
component.save() component.save(true)
expect(updateSpy).toHaveBeenCalled() expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).toHaveBeenCalled() expect(closeSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.') expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.')
}) })
it('should support save without close and show success toast', () => {
initNormally()
component.title = 'Foo Bar'
const closeSpy = jest.spyOn(component, 'close')
const updateSpy = jest.spyOn(documentService, 'update')
const toastSpy = jest.spyOn(toastService, 'showInfo')
updateSpy.mockImplementation((o) => of(doc))
component.save()
expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).not.toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.')
})
it('should show toast error on save if error occurs', () => { it('should show toast error on save if error occurs', () => {
currentUserHasObjectPermissions = true currentUserHasObjectPermissions = true
initNormally() initNormally()
@ -406,7 +419,7 @@ describe('DocumentDetailComponent', () => {
updateSpy.mockImplementation(() => updateSpy.mockImplementation(() =>
throwError(() => new Error('failed to save')) throwError(() => new Error('failed to save'))
) )
component.save() component.save(true)
expect(updateSpy).toHaveBeenCalled() expect(updateSpy).toHaveBeenCalled()
expect(closeSpy).toHaveBeenCalled() expect(closeSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.') expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.')
@ -430,7 +443,7 @@ describe('DocumentDetailComponent', () => {
expect expect
}) })
it('should show toast error on saveAll if error occurs', () => { it('should show toast error on save & next if error occurs', () => {
currentUserHasObjectPermissions = true currentUserHasObjectPermissions = true
initNormally() initNormally()
component.title = 'Foo Bar' component.title = 'Foo Bar'
@ -448,6 +461,39 @@ describe('DocumentDetailComponent', () => {
) )
}) })
it('should show save button and save & close or save & next', () => {
const nextSpy = jest.spyOn(component, 'hasNext')
nextSpy.mockReturnValueOnce(false)
fixture.detectChanges()
expect(
fixture.debugElement
.queryAll(By.css('button'))
.find((b) => b.nativeElement.textContent === 'Save')
).not.toBeUndefined()
expect(
fixture.debugElement
.queryAll(By.css('button'))
.find((b) => b.nativeElement.textContent === 'Save & close')
).not.toBeUndefined()
expect(
fixture.debugElement
.queryAll(By.css('button'))
.find((b) => b.nativeElement.textContent === 'Save & next')
).toBeUndefined()
nextSpy.mockReturnValue(true)
fixture.detectChanges()
expect(
fixture.debugElement
.queryAll(By.css('button'))
.find((b) => b.nativeElement.textContent === 'Save & close')
).toBeUndefined()
expect(
fixture.debugElement
.queryAll(By.css('button'))
.find((b) => b.nativeElement.textContent === 'Save & next')
).not.toBeUndefined()
})
it('should allow close and navigate to documents by default', () => { it('should allow close and navigate to documents by default', () => {
initNormally() initNormally()
const navigateSpy = jest.spyOn(router, 'navigate') const navigateSpy = jest.spyOn(router, 'navigate')

View File

@ -518,7 +518,7 @@ export class DocumentDetailComponent
}) })
} }
save() { save(close: boolean = false) {
this.networkActive = true this.networkActive = true
this.documentsService this.documentsService
.update(this.document) .update(this.document)
@ -527,7 +527,7 @@ export class DocumentDetailComponent
next: () => { next: () => {
this.store.next(this.documentForm.value) this.store.next(this.documentForm.value)
this.toastService.showInfo($localize`Document saved successfully.`) this.toastService.showInfo($localize`Document saved successfully.`)
this.close() close && this.close()
this.networkActive = false this.networkActive = false
this.error = null this.error = null
}, },
@ -535,7 +535,7 @@ export class DocumentDetailComponent
this.networkActive = false this.networkActive = false
if (!this.userCanEdit) { if (!this.userCanEdit) {
this.toastService.showInfo($localize`Document saved successfully.`) this.toastService.showInfo($localize`Document saved successfully.`)
this.close() close && this.close()
} else { } else {
this.error = error.error this.error = error.error
this.toastService.showError( this.toastService.showError(