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; +}