diff --git a/CHANGELOG.md b/CHANGELOG.md
index 669b0d11cd..2a464edc70 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,38 @@
# What's new
+## v3.19.2
+
+__Fixes__
+- Added process indicator when archiving, un-archiving, or deleting a case. Issue: [#2974](https://github.com/Tangerine-Community/Tangerine/issues/2974)
+
+__Server upgrade instructions__
+
+Reminder: Consider using the [Tangerine Upgrade Checklist](https://docs.tangerinecentral.org/system-administrator/upgrade-checklist.html) for making sure you test the upgrade safely.
+
+```
+cd tangerine
+# Check the size of the data folder.
+du -sh data
+# Check disk for free space. Ensure there is at least 10GB + size of the data folder amount of free space in order to perform the upgrade.
+df -h
+# Turn off tangerine and database.
+docker stop tangerine couchdb
+# Create a backup of the data folder.
+cp -r data ../data-backup-$(date "+%F-%T")
+# Fetch the updates.
+git fetch origin
+git checkout v3.19.2
+./start.sh v3.19.2
+# Remove Tangerine's previous version Docker Image.
+docker rmi tangerine/tangerine:v3.19.1
+# Perform additional upgrades.
+docker exec -it tangerine bash
+# This will index all database views in all groups. It may take many hours if
+# the project has a lot of data.
+wedge pre-warm-views --target $T_COUCHDB_ENDPOINT
+```
+
+
## v3.19.1
__Fixes__
diff --git a/editor/src/app/app.component.html b/editor/src/app/app.component.html
index 1b59215bf4..e4043fdefe 100644
--- a/editor/src/app/app.component.html
+++ b/editor/src/app/app.component.html
@@ -203,7 +203,8 @@
-
+
+
\ No newline at end of file
diff --git a/editor/src/app/app.component.ts b/editor/src/app/app.component.ts
index d72303b377..e5640d2d1e 100644
--- a/editor/src/app/app.component.ts
+++ b/editor/src/app/app.component.ts
@@ -4,7 +4,7 @@ import { HttpClient } from '@angular/common/http';
import { TangyFormsInfoService } from './tangy-forms/tangy-forms-info-service';
import { TangyFormService } from './tangy-forms/tangy-form.service';
import { MenuService } from './shared/_services/menu.service';
-import { Component, OnInit, ChangeDetectorRef, OnDestroy, ViewChild } from '@angular/core';
+import {Component, OnInit, ChangeDetectorRef, OnDestroy, ViewChild, ElementRef} from '@angular/core';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { AuthenticationService } from './core/auth/_services/authentication.service';
@@ -17,6 +17,8 @@ import { _TRANSLATE } from './shared/_services/translation-marker';
import { NgxPermissionsService } from 'ngx-permissions';
import { CaseService } from './case/services/case.service';
import { Get } from 'tangy-form/helpers.js'
+import {ProcessMonitorService} from "./shared/_services/process-monitor.service";
+import {LoadingUiComponent} from "./core/loading-ui.component";
@Component({
selector: 'app-root',
@@ -38,7 +40,8 @@ export class AppComponent implements OnInit, OnDestroy {
isConfirmDialogActive = false;
@ViewChild('snav', {static: true}) snav: MatSidenav;
-
+ @ViewChild('loadingUi', { static: true }) loadingUi: ElementRef;
+
private _mobileQueryListener: () => void;
constructor(
@@ -57,7 +60,8 @@ export class AppComponent implements OnInit, OnDestroy {
changeDetectorRef: ChangeDetectorRef,
media: MediaMatcher,
private appConfigService: AppConfigService,
- private permissionService: NgxPermissionsService
+ private permissionService: NgxPermissionsService,
+ private processMonitorService:ProcessMonitorService
) {
translate.setDefaultLang('translation');
translate.use('translation');
@@ -82,7 +86,8 @@ export class AppComponent implements OnInit, OnDestroy {
case: caseService,
cases: casesService,
caseDefinition: caseDefinitionsService,
- translate: window['t']
+ translate: window['t'],
+ process:processMonitorService
}
}
@@ -116,6 +121,13 @@ export class AppComponent implements OnInit, OnDestroy {
}
});
this.window.translation = await this.appConfigService.getTranslations();
+
+ this.processMonitorService.busy.subscribe((isBusy) => {
+ this.loadingUi.nativeElement.hidden = false
+ });
+ this.processMonitorService.done.subscribe((isDone) => {
+ this.loadingUi.nativeElement.hidden = true
+ });
}
ngOnDestroy(): void {
diff --git a/editor/src/app/app.module.ts b/editor/src/app/app.module.ts
index 93b705d679..0b4f925526 100644
--- a/editor/src/app/app.module.ts
+++ b/editor/src/app/app.module.ts
@@ -29,6 +29,7 @@ import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
+import './core/loading-ui.component'
import { NgxPermissionsModule } from 'ngx-permissions';
import { RouterModule } from '@angular/router';
export function HttpLoaderFactory(httpClient: HttpClient) {
diff --git a/editor/src/app/case/components/case/case.component.ts b/editor/src/app/case/components/case/case.component.ts
index 61ce264eb3..94ff3be43c 100644
--- a/editor/src/app/case/components/case/case.component.ts
+++ b/editor/src/app/case/components/case/case.component.ts
@@ -8,6 +8,7 @@ import {Issue} from "../../classes/issue.class";
import {GroupIssuesService} from "../../../groups/services/group-issues.service";
import { _TRANSLATE } from 'src/app/shared/_services/translation-marker';
import { AuthenticationService } from 'src/app/core/auth/_services/authentication.service';
+import {ProcessMonitorService} from "../../../shared/_services/process-monitor.service";
class CaseEventInfo {
caseEvents:Array;
@@ -34,6 +35,7 @@ export class CaseComponent implements AfterContentInit, OnDestroy {
issues:Array
moment
groupId:string
+ process: any;
constructor(
private route: ActivatedRoute,
@@ -42,6 +44,7 @@ export class CaseComponent implements AfterContentInit, OnDestroy {
private ref: ChangeDetectorRef,
authenticationService: AuthenticationService,
private groupIssuesService:GroupIssuesService,
+ private processMonitorService: ProcessMonitorService
) {
ref.detach()
this.window = window
@@ -130,7 +133,9 @@ export class CaseComponent implements AfterContentInit, OnDestroy {
async archive() {
const confirmation = confirm(_TRANSLATE('Are you sure you want to archive this case?'))
if (confirmation) {
+ this.process = this.processMonitorService.start('archiving a case', 'Archiving a case.')
await this.caseService.archive()
+ this.processMonitorService.stop(this.process.id)
this.ref.detectChanges()
}
}
@@ -138,7 +143,9 @@ export class CaseComponent implements AfterContentInit, OnDestroy {
async unarchive() {
const confirmation = confirm(_TRANSLATE('Are you sure you want to unarchive this case?'))
if (confirmation) {
+ this.process = this.processMonitorService.start('unarchiving a case', 'Un-archiving a case.')
await this.caseService.unarchive()
+ this.processMonitorService.stop(this.process.id)
this.ref.detectChanges()
}
}
@@ -148,7 +155,9 @@ export class CaseComponent implements AfterContentInit, OnDestroy {
_TRANSLATE('Are you sure you want to delete this case? You will not be able to undo the operation')
);
if (confirmDelete) {
+ this.process = this.processMonitorService.start('deleting a case', 'Deleting a case.')
await this.caseService.delete()
+ this.processMonitorService.stop(this.process.id)
this.router.navigate(['groups', window.location.pathname.split('/')[2], 'data', 'cases'])
}
}
diff --git a/editor/src/app/core/loading-ui.component.ts b/editor/src/app/core/loading-ui.component.ts
new file mode 100644
index 0000000000..b886741d9d
--- /dev/null
+++ b/editor/src/app/core/loading-ui.component.ts
@@ -0,0 +1,48 @@
+import {LitElement, html, css} from 'lit-element';
+import {customElement, property} from "lit-element/lib/decorators";
+import '@polymer/paper-progress/paper-progress.js';
+
+@customElement('loading-ui')
+export class LoadingUiComponent extends LitElement {
+ static styles = css`
+ :host {
+ display: block;
+ }
+ .loading-text {
+ display: flex;
+ width: 50%;
+ height: 100px;
+ margin: auto;
+ margin-top: 200px;
+ border-radius: 10px;
+ border: 3px dashed #1c87c9;
+ align-items: center;
+ justify-content: center;
+ background: var(--primary-color);
+ color:white;
+ opacity: 100%;
+ font-size: x-large;
+ }
+ `;
+
+ render() {
+ return html`
+
+ `;
+ }
+
+ escape() {
+ if (confirm(`${window['T'].translate('Please only leave this dialog if you believe there is an error in the application.')}`)) {
+ window['T'].process.clear()
+ }
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'loading-ui': LoadingUiComponent;
+ }
+}
+
diff --git a/editor/src/app/shared/_guards/process-guard.service.ts b/editor/src/app/shared/_guards/process-guard.service.ts
new file mode 100644
index 0000000000..6a076d9d89
--- /dev/null
+++ b/editor/src/app/shared/_guards/process-guard.service.ts
@@ -0,0 +1,26 @@
+import {Injectable} from "@angular/core";
+import {CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot} from "@angular/router";
+import {Observable} from "rxjs";
+import {ProcessMonitorService} from "../_services/process-monitor.service";
+import {TangyFormsPlayerComponent} from "../../tangy-forms/tangy-forms-player/tangy-forms-player.component";
+
+@Injectable()
+export class ProcessGuard implements CanDeactivate {
+ constructor(
+ private processMonitorService:ProcessMonitorService
+ ) { }
+
+ canDeactivate(component: TangyFormsPlayerComponent,
+ currentRoute: ActivatedRouteSnapshot,
+ currentState: RouterStateSnapshot,
+ nextState?: RouterStateSnapshot): Observable|Promise|boolean {
+ // If no processes being busy
+ if (this.processMonitorService.processes.length > 0) {
+ return false;
+ } else {
+ return true;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/editor/src/app/shared/_services/process-monitor.service.ts b/editor/src/app/shared/_services/process-monitor.service.ts
new file mode 100644
index 0000000000..ce2633738e
--- /dev/null
+++ b/editor/src/app/shared/_services/process-monitor.service.ts
@@ -0,0 +1,58 @@
+import { Subject } from 'rxjs';
+import { v4 as UUID } from 'uuid';
+import { Injectable } from '@angular/core';
+
+interface Process {
+ id:string
+ name:string
+ description:string
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+class ProcessMonitorService {
+
+ // When number of processes go from 0 to 1, this Subject will emit true.
+ busy = new Subject()
+ // When number of processes go to 0, this Subject will emit true.
+ done = new Subject()
+ // A list of the active processes.
+ processes:Array = []
+
+ constructor() {
+ }
+
+ hasNoProcesses = this.processes.length === 0
+ ? true
+ : false
+
+ start(name, description):Process {
+ const process = {
+ id: UUID(),
+ name,
+ description
+ }
+
+ this.processes.push(process)
+ if (this.hasNoProcesses) {
+ this.busy.next(true)
+ }
+ return process
+ }
+
+ stop(pid:string) {
+ this.processes = this.processes.filter(process => process.id !== pid)
+ if (this.processes.length === 0) {
+ this.done.next(true)
+ }
+ }
+
+ clear() {
+ this.processes = []
+ this.done.next(true)
+ }
+
+}
+
+export { ProcessMonitorService };
\ No newline at end of file
diff --git a/editor/src/app/shared/shared.module.ts b/editor/src/app/shared/shared.module.ts
index 29f72d92d8..32a15012b3 100644
--- a/editor/src/app/shared/shared.module.ts
+++ b/editor/src/app/shared/shared.module.ts
@@ -17,6 +17,8 @@ import { HasSomePermissionsDirective } from '../core/auth/_directives/has-some-p
import { HasAllPermissionsDirective } from '../core/auth/_directives/has-all-permissions.directive';
import { DynamicTableComponent } from './_components/dynamic-table/dynamic-table.component';
import { MatMenuModule } from '@angular/material/menu';
+import {ProcessMonitorService} from "./_services/process-monitor.service";
+import {ProcessGuard} from "./_guards/process-guard.service";
@NgModule({
imports: [
@@ -29,7 +31,9 @@ import { MatMenuModule } from '@angular/material/menu';
providers: [
AppConfigService,
ServerConfigService,
- LoginGuard
+ LoginGuard,
+ ProcessMonitorService,
+ ProcessGuard
],
exports: [
TranslateModule,
diff --git a/editor/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts b/editor/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
index 6373fd75ac..4f09bf1d6e 100755
--- a/editor/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
+++ b/editor/src/app/tangy-forms/tangy-forms-player/tangy-forms-player.component.ts
@@ -6,8 +6,9 @@ import { TangyFormsInfoService } from 'src/app/tangy-forms/tangy-forms-info-serv
import { Component, ViewChild, ElementRef, Input } from '@angular/core';
import { _TRANSLATE } from '../../shared/translation-marker';
import { TangyFormService } from '../tangy-form.service';
-const sleep = (milliseconds) => new Promise((res) => setTimeout(() => res(true), milliseconds))
+import {ProcessMonitorService} from "../../shared/_services/process-monitor.service";
+const sleep = (milliseconds) => new Promise((res) => setTimeout(() => res(true), milliseconds))
@Component({
selector: 'app-tangy-forms-player',
@@ -46,9 +47,12 @@ export class TangyFormsPlayerComponent {
window:any;
@ViewChild('container', {static: true}) container: ElementRef;
+ process: any;
+
constructor(
private tangyFormsInfoService:TangyFormsInfoService,
- private tangyFormService: TangyFormService
+ private tangyFormService: TangyFormService,
+ private processMonitorService: ProcessMonitorService,
) {
this.window = window
}
@@ -136,6 +140,11 @@ export class TangyFormsPlayerComponent {
this.throttledSaveResponse(response)
})
}
+ formEl.addEventListener('before-submit', async (event) => {
+ if (this.preventSubmit) event.preventDefault()
+ this.process = this.processMonitorService.start('saving-a-tangy-form', 'Updating a form response.')
+ this.$submit.next(true)
+ })
formEl.addEventListener('submit', async (event) => {
if (this.preventSubmit) event.preventDefault()
while (this.throttledSaveFiring === true) {
@@ -148,6 +157,7 @@ export class TangyFormsPlayerComponent {
while (this.throttledSaveFiring === true) {
await sleep(1000)
}
+ this.processMonitorService.stop(this.process.id)
this.$afterSubmit.next(true)
})
formEl.addEventListener('resubmit', async (event) => {
diff --git a/editor/src/styles/styles.scss b/editor/src/styles/styles.scss
index 0deea7fa70..e382876e1e 100644
--- a/editor/src/styles/styles.scss
+++ b/editor/src/styles/styles.scss
@@ -306,3 +306,14 @@ mat-card-content > * {
.icon-list-item>mwc-icon{
color: var(--primary-color) !important;
}
+
+.loading-ui-container {
+ position: fixed;
+ display: block;
+ height: 100%;
+ width: 100%;
+ opacity: 0.7;
+ background-color: #fff;
+ top: 0;
+ z-index: 100;
+}