Feature: copy workflows and mail rules, improve layout (#7727)

This commit is contained in:
shamoon
2024-09-16 22:02:51 -07:00
committed by GitHub
parent 8aa35540b5
commit 6192c15c4d
9 changed files with 227 additions and 70 deletions

View File

@@ -19,7 +19,7 @@
<div class="row">
<div class="col" i18n>Name</div>
<div class="col" i18n>Server</div>
<div class="col" i18n>Username</div>
<div class="col d-none d-sm-block" i18n>Username</div>
<div class="col" i18n>Actions</div>
</div>
</li>
@@ -29,9 +29,21 @@
<div class="row">
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount)">{{account.name}}</button></div>
<div class="col d-flex align-items-center">{{account.imap_server}}</div>
<div class="col d-flex align-items-center">{{account.username}}</div>
<div class="col d-flex align-items-center d-none d-sm-block">{{account.username}}</div>
<div class="col">
<div class="btn-group">
<div class="btn-group d-block d-sm-none">
<div ngbDropdown container="body" class="d-inline-block">
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle>
<i-bs name="three-dots-vertical"></i-bs>
</button>
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
<button (click)="editMailAccount(account)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" ngbDropdownItem i18n>Edit</button>
<button (click)="editPermissions(account)" *pngxIfOwner="account" ngbDropdownItem i18n>Permissions</button>
<button (click)="deleteMailAccount(account)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailAccount }" ngbDropdownItem i18n>Delete</button>
</div>
</div>
</div>
<div class="btn-group d-none d-sm-block">
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" [disabled]="!userCanEdit(account)" class="btn btn-sm btn-outline-secondary" type="button" (click)="editMailAccount(account)">
<i-bs width="1em" height="1em" name="pencil"></i-bs>&nbsp;<ng-container i18n>Edit</ng-container>
</button>
@@ -64,7 +76,7 @@
<li class="list-group-item">
<div class="row">
<div class="col" i18n>Name</div>
<div class="col" i18n>Sort Order</div>
<div class="col d-none d-sm-block" i18n>Sort Order</div>
<div class="col" i18n>Account</div>
<div class="col" i18n>Actions</div>
</div>
@@ -74,19 +86,39 @@
<li class="list-group-item">
<div class="row">
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule)">{{rule.name}}</button></div>
<div class="col d-flex align-items-center">{{rule.order}}</div>
<div class="col d-flex align-items-center d-none d-sm-block">{{rule.order}}</div>
<div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
<div class="col">
<div class="btn-group">
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailRule }" [disabled]="!userCanEdit(rule)" class="btn btn-sm btn-outline-secondary" type="button" (click)="editMailRule(rule)">
<i-bs width="1em" height="1em" name="pencil"></i-bs>&nbsp;<ng-container i18n>Edit</ng-container>
</button>
<button *pngxIfOwner="rule" class="btn btn-sm btn-outline-secondary" type="button" (click)="editPermissions(rule)">
<i-bs width="1em" height="1em" name="person-lock"></i-bs>&nbsp;<ng-container i18n>Permissions</ng-container>
</button>
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailRule }" [disabled]="!userIsOwner(rule)" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailRule(rule)">
<i-bs width="1em" height="1em" name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
<div class="btn-group d-block d-sm-none">
<div ngbDropdown container="body" class="d-inline-block">
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle>
<i-bs name="three-dots-vertical"></i-bs>
</button>
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
<button (click)="editMailRule(rule)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailRule }" ngbDropdownItem i18n>Edit</button>
<button (click)="editPermissions(rule)" *pngxIfOwner="rule" ngbDropdownItem i18n>Permissions</button>
<button (click)="deleteMailRule(rule)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailRule }" ngbDropdownItem i18n>Delete</button>
<button (click)="copyMailRule(rule)" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.MailRule }" ngbDropdownItem i18n>Copy</button>
</div>
</div>
</div>
<div class="btn-toolbar d-none d-sm-flex gap-2" role="toolbar">
<div class="btn-group">
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailRule }" [disabled]="!userCanEdit(rule)" class="btn btn-sm btn-outline-secondary" type="button" (click)="editMailRule(rule)">
<i-bs width="1em" height="1em" name="pencil"></i-bs>&nbsp;<ng-container i18n>Edit</ng-container>
</button>
<button *pngxIfOwner="rule" class="btn btn-sm btn-outline-secondary" type="button" (click)="editPermissions(rule)">
<i-bs width="1em" height="1em" name="person-lock"></i-bs>&nbsp;<ng-container i18n>Permissions</ng-container>
</button>
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailRule }" [disabled]="!userIsOwner(rule)" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailRule(rule)">
<i-bs width="1em" height="1em" name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
<div class="btn-group">
<button *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.MailRule }" class="btn btn-sm btn-outline-secondary" type="button" (click)="copyMailRule(rule)">
<i-bs width="1em" height="1em" name="files"></i-bs>&nbsp;<ng-container i18n>Copy</ng-container>
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,4 @@
// hide caret on mobile dropdown
.d-block.d-sm-none .dropdown-toggle::after {
display: none;
}

View File

@@ -226,6 +226,17 @@ describe('MailComponent', () => {
component.editMailRule()
})
it('should support copy mail rule', () => {
completeSetup()
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.copyMailRule(mailRules[0] as MailRule)
const editDialog = modal.componentInstance as MailRuleEditDialogComponent
expect(editDialog.object.id).toBeNull()
expect(editDialog.object.name).toEqual(`${mailRules[0].name} (copy)`)
expect(editDialog.dialogMode).toEqual(EditDialogMode.CREATE)
})
it('should support delete mail rule, show error if needed', () => {
completeSetup()
let modal: NgbModalRef

View File

@@ -137,14 +137,13 @@ export class MailComponent
})
}
editMailRule(rule: MailRule = null) {
editMailRule(rule: MailRule = null, forceCreate = false) {
const modal = this.modalService.open(MailRuleEditDialogComponent, {
backdrop: 'static',
size: 'xl',
})
modal.componentInstance.dialogMode = rule
? EditDialogMode.EDIT
: EditDialogMode.CREATE
modal.componentInstance.dialogMode =
rule && !forceCreate ? EditDialogMode.EDIT : EditDialogMode.CREATE
modal.componentInstance.object = rule
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
@@ -164,6 +163,13 @@ export class MailComponent
})
}
copyMailRule(rule: MailRule) {
const clone = { ...rule }
clone.id = null
clone.name = `${rule.name} (copy)`
this.editMailRule(clone, true)
}
deleteMailRule(rule: MailRule) {
const modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static',

View File

@@ -15,9 +15,9 @@
<li class="list-group-item">
<div class="row">
<div class="col" i18n>Name</div>
<div class="col" i18n>Sort order</div>
<div class="col d-none d-sm-block" i18n>Sort order</div>
<div class="col" i18n>Status</div>
<div class="col" i18n>Triggers</div>
<div class="col d-none d-sm-block" i18n>Triggers</div>
<div class="col" i18n>Actions</div>
</div>
</li>
@@ -26,17 +26,37 @@
<li class="list-group-item">
<div class="row">
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editWorkflow(workflow)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Workflow)">{{workflow.name}}</button></div>
<div class="col d-flex align-items-center"><code>{{workflow.order}}</code></div>
<div class="col d-flex align-items-center d-none d-sm-block"><code>{{workflow.order}}</code></div>
<div class="col d-flex align-items-center"><code> @if(workflow.enabled) { <ng-container i18n>Enabled</ng-container> } @else { <span i18n class="text-muted">Disabled</span> }</code></div>
<div class="col d-flex align-items-center">{{getTypesList(workflow)}}</div>
<div class="col d-flex align-items-center d-none d-sm-block">{{getTypesList(workflow)}}</div>
<div class="col">
<div class="btn-group">
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Workflow }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editWorkflow(workflow)">
<i-bs width="1em" height="1em" name="pencil"></i-bs>&nbsp;<ng-container i18n>Edit</ng-container>
</button>
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Workflow }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteWorkflow(workflow)">
<i-bs width="1em" height="1em" name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
<div class="btn-group d-block d-sm-none">
<div ngbDropdown container="body" class="d-inline-block">
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle>
<i-bs name="three-dots-vertical"></i-bs>
</button>
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
<button (click)="editWorkflow(workflow)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Workflow }" ngbDropdownItem i18n>Edit</button>
<button (click)="deleteWorkflow(workflow)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Workflow }" ngbDropdownItem i18n>Delete</button>
<button (click)="copyWorkflow(workflow)" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Workflow }" ngbDropdownItem i18n>Copy</button>
</div>
</div>
</div>
<div class="btn-toolbar d-none d-sm-flex gap-2" role="toolbar">
<div class="btn-group">
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Workflow }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editWorkflow(workflow)">
<i-bs width="1em" height="1em" name="pencil"></i-bs>&nbsp;<ng-container i18n>Edit</ng-container>
</button>
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Workflow }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteWorkflow(workflow)">
<i-bs width="1em" height="1em" name="trash"></i-bs>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
<div class="btn-group">
<button *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Workflow }" class="btn btn-sm btn-outline-secondary" type="button" (click)="copyWorkflow(workflow)">
<i-bs width="1em" height="1em" name="files"></i-bs>&nbsp;<ng-container i18n>Copy</ng-container>
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,4 @@
// hide caret on mobile dropdown
.d-block.d-sm-none .dropdown-toggle::after {
display: none;
}

View File

@@ -26,6 +26,7 @@ import {
import { WorkflowActionType } from 'src/app/data/workflow-action'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
const workflows: Workflow[] = [
{
@@ -173,6 +174,19 @@ describe('WorkflowsComponent', () => {
expect(reloadSpy).toHaveBeenCalled()
})
it('should support copy', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const copyButton = fixture.debugElement.queryAll(By.css('button'))[6]
copyButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined()
const editDialog = modal.componentInstance as WorkflowEditDialogComponent
expect(editDialog.object.name).toEqual(workflows[0].name + ' (copy)')
expect(editDialog.dialogMode).toEqual(EditDialogMode.CREATE)
})
it('should support delete, show notification on error / success', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
@@ -180,7 +194,7 @@ describe('WorkflowsComponent', () => {
const deleteSpy = jest.spyOn(workflowService, 'delete')
const reloadSpy = jest.spyOn(component, 'reload')
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[4]
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[5]
deleteButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined()

View File

@@ -57,14 +57,13 @@ export class WorkflowsComponent
.join(', ')
}
editWorkflow(workflow: Workflow) {
editWorkflow(workflow: Workflow, forceCreate: boolean = false) {
const modal = this.modalService.open(WorkflowEditDialogComponent, {
backdrop: 'static',
size: 'xl',
})
modal.componentInstance.dialogMode = workflow
? EditDialogMode.EDIT
: EditDialogMode.CREATE
modal.componentInstance.dialogMode =
workflow && !forceCreate ? EditDialogMode.EDIT : EditDialogMode.CREATE
if (workflow) {
// quick "deep" clone so original doesn't get modified
const clone = Object.assign({}, workflow)
@@ -88,6 +87,25 @@ export class WorkflowsComponent
})
}
copyWorkflow(workflow: Workflow) {
const clone = Object.assign({}, workflow)
clone.id = null
clone.name = `${workflow.name} (copy)`
clone.actions = [
...workflow.actions.map((a) => {
a.id = null
return a
}),
]
clone.triggers = [
...workflow.triggers.map((t) => {
t.id = null
return t
}),
]
this.editWorkflow(clone, true)
}
deleteWorkflow(workflow: Workflow) {
const modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static',