From db27995a04b82bc968a677ec0d6204a050aea8d0 Mon Sep 17 00:00:00 2001 From: Jareth Whitney Date: Thu, 9 Jan 2025 16:17:02 -0800 Subject: [PATCH 1/3] feature/deseng751: Added ability to use an external link as if it is an internal file. --- CHANGELOG.md | 7 + package.json | 2 +- src/app/app.module.ts | 8 +- src/app/models/externalLink.ts | 29 +++ .../comment-period-details-tab.component.ts | 10 +- .../add-documents-resolver.services.ts | 49 ++-- .../add-documents.component.html | 2 +- .../add-documents/add-documents.component.ts | 133 ++++++---- .../add-edit-comment-period.component.html | 2 +- .../add-edit-comment-period.component.ts | 22 +- .../detail/detail.component.html | 14 +- .../detail/detail.component.ts | 65 +++-- .../document-detail-resolver.service.ts | 11 +- .../document-edit.component.html | 12 +- .../document-edit.component.scss | 11 + .../external-link.component.html | 102 ++++++++ .../external-link.component.scss | 28 +++ .../external-link.component.spec.ts | 25 ++ .../external-link/external-link.component.ts | 235 ++++++++++++++++++ .../external-link/link-resolver.services.ts | 23 ++ .../project-document-resolver.services.ts | 54 ++-- .../project-documents.component.ts | 168 ++++++++----- .../upload/upload.component.html | 12 +- .../upload/upload.component.scss | 11 + src/app/project/project-routing.module.ts | 17 ++ src/app/project/project.module.ts | 4 +- src/app/services/api.ts | 130 ++++++++++ src/app/services/link.service.spec.ts | 16 ++ src/app/services/link.service.ts | 123 +++++++++ 29 files changed, 1120 insertions(+), 205 deletions(-) create mode 100644 src/app/models/externalLink.ts create mode 100644 src/app/project/project-documents/external-link/external-link.component.html create mode 100644 src/app/project/project-documents/external-link/external-link.component.scss create mode 100644 src/app/project/project-documents/external-link/external-link.component.spec.ts create mode 100644 src/app/project/project-documents/external-link/external-link.component.ts create mode 100644 src/app/project/project-documents/external-link/link-resolver.services.ts create mode 100644 src/app/services/link.service.spec.ts create mode 100644 src/app/services/link.service.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d28111e..e6115858 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +### 1.12.0 Jan 9, 2025 +* Added the ability to add an external link as if it is an internal file. [DESENG-751](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-751) +* Functionality works in all file views, including file list, file details, file add/edit, comment period view, comment period edit. +* All file list functionality should work, including open/download, publish, unpublish, edit, and delete. +* All upper-right context menu items are working from file list, file details, etc. +* No multi-edit for external link files yet. + ### 1.11.0 Nov 26, 2024 * Made agreements optional when creating or editing projects, adjusted project overview display. [DESENG-742](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-742) * Fixed broken repository link for XLSX (Git has been disabled due to download traffic). diff --git a/package.json b/package.json index 497f6cb7..ae5012d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "landuseplanning-admin", - "version": "1.11.0", + "version": "1.12.0", "license": "Apache-2.0", "scripts": { "ng": "ng", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 291d7654..e9e740c4 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -66,11 +66,9 @@ import { AddEditContactComponent } from './contacts/add-edit-contact/add-edit-co import { OrganizationsComponent } from './organizations/organizations.component'; import { OrganizationsTableRowsComponent } from './organizations/organizations-table-rows/organizations-table-rows.component'; import { AddEditOrganizationComponent } from './organizations/add-edit-organization/add-edit-organization.component'; -import { LinkOrganizationComponent } from './shared/components/link-organization/link-organization.component'; -import { LinkOrganizationTableRowsComponent } from './shared/components/link-organization/link-organization-table-rows/link-organization-table-rows.component'; -import { ContactSelectComponent } from './shared/components/contact-select/contact-select.component'; -import { ContactSelectTableRowsComponent } from './shared/components/contact-select/contact-select-table-rows/contact-select-table-rows.component'; import { FileUploadModalComponent } from './file-upload-modal/file-upload-modal.component'; +import { LinkService } from './services/link.service'; +import { ExternalLinkComponent } from './project/project-documents/external-link/external-link.component'; export function kcFactory(keycloakService: KeycloakService) { return () => keycloakService.init(); @@ -106,6 +104,7 @@ export function kcFactory(keycloakService: KeycloakService) { UserTableRowsComponent, EnvBannerComponent, FileUploadModalComponent, + ExternalLinkComponent, ], imports: [ BrowserAnimationsModule, @@ -157,6 +156,7 @@ export function kcFactory(keycloakService: KeycloakService) { SurveyResponseService, SurveyBuilderService, UserService, + LinkService, ], bootstrap: [AppComponent] }) diff --git a/src/app/models/externalLink.ts b/src/app/models/externalLink.ts new file mode 100644 index 00000000..c98dc533 --- /dev/null +++ b/src/app/models/externalLink.ts @@ -0,0 +1,29 @@ +import * as _ from 'lodash'; + +export class ExternalLink { + _id: string; + project: string; + displayName: string; + externalLink: string; + section: string; + dateAdded: Date; + dateUpdated: Date; + description: string; + projectPhase: string; + checkbox: boolean; + isPublished: boolean; + + constructor(obj?: any) { + this._id = obj && obj._id || null; + this.project = obj && obj.project || null; + this.displayName = obj && obj.displayName || null; + this.externalLink = obj && obj.externalLink || null; + this.section = obj && obj.section || null; + this.dateAdded = obj && obj.dateAdded || null; + this.dateUpdated = obj && obj.dateUpdated || null; + this.description = obj && obj.description || null; + this.projectPhase = obj && obj.projectPhase || null; + this.checkbox = false || null; + this.isPublished = false || null; + } +} diff --git a/src/app/project/comment-period/comment-period-details-tabs/comment-period-details-tab.component.ts b/src/app/project/comment-period/comment-period-details-tabs/comment-period-details-tab.component.ts index 18362118..c96f01c3 100644 --- a/src/app/project/comment-period/comment-period-details-tabs/comment-period-details-tab.component.ts +++ b/src/app/project/comment-period/comment-period-details-tabs/comment-period-details-tab.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnInit, OnChanges, OnDestroy } from '@angular/core'; -import { Subject } from 'rxjs'; +import { forkJoin, Subject } from 'rxjs'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Router, ActivatedRoute } from '@angular/router'; import { NgxSmartModalService } from 'ngx-smart-modal'; @@ -13,6 +13,7 @@ import { ApiService } from 'app/services/api'; import { CommentPeriodService } from 'app/services/commentperiod.service'; import { StorageService } from 'app/services/storage.service'; import { DocumentService } from 'app/services/document.service'; +import { LinkService } from 'app/services/link.service'; @Component({ selector: 'app-comment-period-details-tab', @@ -41,6 +42,7 @@ export class CommentPeriodDetailsTabComponent implements OnInit, OnChanges, OnDe constructor( private api: ApiService, private surveyService: SurveyService, + private externalLinkService: LinkService, private commentPeriodService: CommentPeriodService, private documentService: DocumentService, private route: ActivatedRoute, @@ -63,11 +65,11 @@ export class CommentPeriodDetailsTabComponent implements OnInit, OnChanges, OnDe this.projectId = this.storageService.state.currentProject.data._id; if (this.commentPeriod.relatedDocuments.length > 0) { - this.documentService.getByMultiId(this.commentPeriod.relatedDocuments) + forkJoin([this.documentService.getByMultiId(this.commentPeriod.relatedDocuments), this.externalLinkService.getByMultiId(this.commentPeriod.relatedDocuments)]) .takeUntil(this.ngUnsubscribe) .subscribe( data => { - this.commentPeriodDocs = data; + this.commentPeriodDocs = [...data[0] || [], ...data[1] || []]; } ); } @@ -272,7 +274,7 @@ export class CommentPeriodDetailsTabComponent implements OnInit, OnChanges, OnDe * @returns {Promise} */ public downloadDocument(document) { - return this.api.downloadDocument(document); + return document.externalLink ? window.open(document.externalLink) : this.api.downloadDocument(document); } /** diff --git a/src/app/project/comment-periods/add-edit-comment-period/add-documents/add-documents-resolver.services.ts b/src/app/project/comment-periods/add-edit-comment-period/add-documents/add-documents-resolver.services.ts index 70f940d5..72fa9370 100644 --- a/src/app/project/comment-periods/add-edit-comment-period/add-documents/add-documents-resolver.services.ts +++ b/src/app/project/comment-periods/add-edit-comment-period/add-documents/add-documents-resolver.services.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { Resolve, ActivatedRouteSnapshot } from '@angular/router'; -import { Observable } from 'rxjs'; +import { forkJoin, Observable } from 'rxjs'; import { SearchService } from 'app/services/search.service'; import { StorageService } from 'app/services/storage.service'; @@ -12,6 +12,26 @@ export class AddDocumentsResolver implements Resolve> { private storageService: StorageService ) { } + /** + * Retrieves documents or external links + * + * @param {ActivatedRouteSnapshot} route The route to get params from. + * @param {string} schema The schema type to use, either 'Document' or 'ExternalLink' + * @param {string} projectId The project ID of the documents you wish to retrieve + * @returns {Observable} + */ + getFiles = (route: ActivatedRouteSnapshot, schema: string, projectId: string): Observable => { + const keys = route.params.keywords || ''; + const dataset = schema; + const fields = [{ 'name': 'project', 'value': projectId }]; + const pageNum = 1; + const pageSize = 1000; + const sortBy = route.queryParams['sortBy'] || null; + const queryModifier = {}; + const populate = true; + return this.searchService.getSearchResults(keys, dataset, fields, pageNum, pageSize, sortBy, queryModifier, populate); + } + /** * Get route params and make a request to the API to get a set of * documents(files) that match the request params. @@ -20,24 +40,13 @@ export class AddDocumentsResolver implements Resolve> { * @returns {Observable} */ resolve(route: ActivatedRouteSnapshot): Observable { - let projectId; - if (this.storageService.state.currentProject) { - projectId = this.storageService.state.currentProject.data._id; - } else { - projectId = route.parent.parent.params.projId; - } - - const pageNum = Number(route.queryParams['pageNum'] ? route.queryParams['pageNum'] : 1); - const pageSize = Number(route.queryParams['pageSize'] ? route.queryParams['pageSize'] : 10); - const sortBy = route.queryParams['sortBy'] ? route.queryParams['sortBy'] : null; - const keywords = route.params.keywords; - return this.searchService.getSearchResults(keywords, - 'Document', - [{ 'name': 'project', 'value': projectId }], - pageNum, - pageSize, - sortBy, - { documentSource: 'PROJECT' }, - ); + const projectId = this.storageService.state.currentProject?.data?._id || route.parent.parent.params.projId; + const documents = this.getFiles(route, 'Document', projectId); + const externalLinks = this.getFiles(route, 'ExternalLink', projectId); + return forkJoin({ + documents: documents, + externalLinks: externalLinks, + }); } } + diff --git a/src/app/project/comment-periods/add-edit-comment-period/add-documents/add-documents.component.html b/src/app/project/comment-periods/add-edit-comment-period/add-documents/add-documents.component.html index 445693d4..b8d18ce5 100644 --- a/src/app/project/comment-periods/add-edit-comment-period/add-documents/add-documents.component.html +++ b/src/app/project/comment-periods/add-edit-comment-period/add-documents/add-documents.component.html @@ -70,7 +70,7 @@ insert_drive_file - {{doc.documentFileName}} + {{doc.documentFileName || doc.externalLink}} @@ -45,7 +45,7 @@

Section:

Document Date:

-

{{document.datePosted | date}}

+

{{document.datePosted || document.dateAdded || date}}

Display Name:

@@ -59,16 +59,16 @@

Description:

Filename:

-

{{document.documentFileName || '-'}}

+

{{document.documentFileName || document.externalLink || '-'}}

Filesize:

-

{{ (document && document.internalSize) ? humanReadableSize : '0' }}

+

{{ (document?.internalSize) ? humanReadableSize : 'N/A' }}

diff --git a/src/app/project/project-documents/detail/detail.component.ts b/src/app/project/project-documents/detail/detail.component.ts index d63bea5e..649caa5e 100644 --- a/src/app/project/project-documents/detail/detail.component.ts +++ b/src/app/project/project-documents/detail/detail.component.ts @@ -11,6 +11,7 @@ import { NgxSmartModalService } from 'ngx-smart-modal'; import { Utils } from 'app/shared/utils/utils'; import { isEmpty } from 'lodash'; import { DocumentSection } from 'app/models/documentSection'; +import { ExternalLink } from 'app/models/externalLink'; @Component({ selector: 'app-detail', @@ -19,7 +20,7 @@ import { DocumentSection } from 'app/models/documentSection'; }) export class DocumentDetailComponent implements OnInit, OnDestroy { private ngUnsubscribe: Subject = new Subject(); - public document: Document = null; + public document = null; // Document or ExternalLink public currentProject: Project = null; public publishText: string; public humanReadableSize: string; @@ -60,18 +61,14 @@ export class DocumentDetailComponent implements OnInit, OnDestroy { .subscribe((res: any) => { this.document = res.document.document; this.sections = res.document.sections; - this.selectedSection = this.sections.find((section) => section._id === this.document.section) - - const safeName = this.document.documentFileName.replace(/ /g, '_'); - this.documentUrl = `${this.pathAPI}/document/${this.document._id}/fetch/${safeName}`; - if (this.document.read.includes('public')) { - this.publishText = 'Unpublish'; - } else { - this.publishText = 'Publish'; - } + this.selectedSection = this.sections.find((section) => section._id === this.document.section); + const safeName = this.document?.documentFileName ? this.document.documentFileName.replace(/ /g, '_') : this.document.externalLink; + this.documentUrl = this.document?.externalLink ? safeName : `${this.pathAPI}/document/${this.document._id}/fetch/${safeName}`; + this.publishText = !this.document.read?.includes('public') ? 'Publish' : 'Unpublish'; + this._changeDetectionRef.detectChanges(); }); - this.humanReadableSize = this.utils.formatBytes(this.document.internalSize); + this.humanReadableSize = this.utils.formatBytes(this.document?.internalSize) || ''; this.ngxSmartModalService.getModal('confirmation-modal').onAnyCloseEventFinished .takeUntil(this.ngUnsubscribe) @@ -107,6 +104,30 @@ export class DocumentDetailComponent implements OnInit, OnDestroy { }); } + /** + * Maps row data to a format that is familiar to the edit form. + * + * @param {any} file The file to be mapped, Document or ExternalLink + * @returns {object} + * + */ + mapRowData(file) { + return { + displayName: file.displayName, + documentFileName: file.documentFileName || file.externalLink || '', + internalSize: file.internalSize || null, + internalExt: file.internalExt || 'external', + datePosted: file.datePosted || file.dateAdded, + status: file.read.includes('public') ? 'Published' : 'Not Published', + _id: file._id, + project: file.project, + read: file.read, + projectPhase: file.projectPhase, + description: file.description, + section: file.section, + } + } + /** * When a project edit is initiated, get the associated documents * from local storage. Also add the router destination for the project @@ -115,12 +136,28 @@ export class DocumentDetailComponent implements OnInit, OnDestroy { * @return {void} */ onEdit() { - this.storageService.state.selectedDocs = [this.document]; - this.storageService.state.labels = this.document.labels; + this.storageService.state.selectedDocs = [this.mapRowData(this.document)]; + this.storageService.state.labels = this.document.labels || []; this.storageService.state.back = { url: ['/p', this.document.project, 'project-files', 'detail', this.document._id], label: 'View File' }; - this.router.navigate(['p', this.document.project, 'project-files', 'edit']); + this.router.navigate(['p', this.document.project, 'project-files', (this.document.externalLink ? 'edit-link' : 'edit')]); } + onDownload() { + if (this.document.externalLink) { + window.open(this.document.externalLink, "_blank"); + } else { + this.api.downloadDocument(this.document); + } + } + + onOpen() { + if (this.document.externalLink) { + window.open(this.document.externalLink, "_blank"); + } else { + this.api.openDocument(this.document); + } + } + /** * Checks if a document is published. * diff --git a/src/app/project/project-documents/detail/document-detail-resolver.service.ts b/src/app/project/project-documents/detail/document-detail-resolver.service.ts index 4474b9cf..b36b0cce 100644 --- a/src/app/project/project-documents/detail/document-detail-resolver.service.ts +++ b/src/app/project/project-documents/detail/document-detail-resolver.service.ts @@ -4,12 +4,14 @@ import { Observable, forkJoin, from } from 'rxjs'; import { DocumentSectionService } from 'app/services/documentSection.service'; import { DocumentService } from 'app/services/document.service'; +import { LinkService } from 'app/services/link.service'; @Injectable() export class DocumentDetailResolver implements Resolve> { constructor( private documentService: DocumentService, - private documentSectionService: DocumentSectionService + private documentSectionService: DocumentSectionService, + private externalLinkService: LinkService ) { } /** @@ -25,9 +27,10 @@ export class DocumentDetailResolver implements Resolve> { return forkJoin( from(this.documentService.getById(docId)), - from(this.documentSectionService.getAll(projectId)) - ).map(([document, sections]) => { - return { document: document, sections: sections }; + from(this.externalLinkService.getById(docId)), + from(this.documentSectionService.getAll(projectId)), + ).map(([document, link, sections]) => { + return { document: {...document, ...link}, sections: sections }; }) } } diff --git a/src/app/project/project-documents/document-edit/document-edit.component.html b/src/app/project/project-documents/document-edit/document-edit.component.html index da8d97aa..1c0a84fc 100644 --- a/src/app/project/project-documents/document-edit/document-edit.component.html +++ b/src/app/project/project-documents/document-edit/document-edit.component.html @@ -17,7 +17,7 @@
-
    +
    • {{document.displayName || '-'}} ({{document.documentFileName}})
    @@ -37,7 +37,7 @@
    -
    +
    @@ -55,7 +55,7 @@
    -
    +
    @@ -70,7 +70,7 @@
    -
    +
    -
    +
    File description is required
    diff --git a/src/app/project/project-documents/document-edit/document-edit.component.scss b/src/app/project/project-documents/document-edit/document-edit.component.scss index e990f2e9..ca842e59 100644 --- a/src/app/project/project-documents/document-edit/document-edit.component.scss +++ b/src/app/project/project-documents/document-edit/document-edit.component.scss @@ -46,3 +46,14 @@ h1 { margin-top: 0.5rem; color: red; } + +.form-container-65 { + width: 65%; +} + +@media (max-width: 1200px) { + .form-container-65 { + width: 100%; + flex-basis: 100%; + } +} diff --git a/src/app/project/project-documents/external-link/external-link.component.html b/src/app/project/project-documents/external-link/external-link.component.html new file mode 100644 index 00000000..0ed2b11c --- /dev/null +++ b/src/app/project/project-documents/external-link/external-link.component.html @@ -0,0 +1,102 @@ + +
    +
    +
    +
    +
    +
    +
    +
    + + +
    Project Phase is required
    +
    +
    +
    +
    + + +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    File date is required
    +
    +
    +
    +
    + + +
    + + +
    +
    File link is required
    +
    +
    +
    +
    + + +
    + + +
    +
    File name is required
    +
    +
    +
    +
    + + +
    File description is required
    +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    diff --git a/src/app/project/project-documents/external-link/external-link.component.scss b/src/app/project/project-documents/external-link/external-link.component.scss new file mode 100644 index 00000000..55eb29ab --- /dev/null +++ b/src/app/project/project-documents/external-link/external-link.component.scss @@ -0,0 +1,28 @@ +@import "assets/styles/components/add-edit.scss"; + + +.ng-touched.ng-invalid { + border-color: red; +} + +.ng-touched.ng-invalid:focus { + border-color: red; + box-shadow: 0 0 0 0.2rem rgba(255, 17, 0, 0.25); +} + +.invalid-notice { + font-size: 0.9rem; + margin-top: 0.5rem; + color: red; +} + +.form-container-65 { + width: 65%; +} + +@media (max-width: 1200px) { + .form-container-65 { + width: 100%; + flex-basis: 100%; + } +} \ No newline at end of file diff --git a/src/app/project/project-documents/external-link/external-link.component.spec.ts b/src/app/project/project-documents/external-link/external-link.component.spec.ts new file mode 100644 index 00000000..a6c3948d --- /dev/null +++ b/src/app/project/project-documents/external-link/external-link.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ExternalLinkComponent } from './external-link.component'; + +describe('ExternalLinkComponent', () => { + let component: ExternalLinkComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ExternalLinkComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ExternalLinkComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/project/project-documents/external-link/external-link.component.ts b/src/app/project/project-documents/external-link/external-link.component.ts new file mode 100644 index 00000000..b068ac26 --- /dev/null +++ b/src/app/project/project-documents/external-link/external-link.component.ts @@ -0,0 +1,235 @@ +import { Component, OnInit, ChangeDetectorRef, OnDestroy } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Subject } from 'rxjs'; +import * as moment from 'moment-timezone'; + +import { ConfigService } from 'app/services/config.service'; +import { StorageService } from 'app/services/storage.service'; +import { LinkService } from 'app/services/link.service'; + +import { Utils } from 'app/shared/utils/utils'; +import { DocumentSection } from 'app/models/documentSection'; + +@Component({ + selector: 'app-external-link', + templateUrl: './external-link.component.html', + styleUrls: ['./external-link.component.scss'] +}) +export class ExternalLinkComponent implements OnInit, OnDestroy { + private ngUnsubscribe: Subject = new Subject(); + + public currentProject; + public projectFiles: Array = []; + public externalLink = []; + public documents = null; + public documentSections: DocumentSection[] = []; + public dateAdded = null; + public dateUpdated = null; + public labels: any[] = []; + public myForm: FormGroup; + public loading = true; + public docNameInvalid = false; + public externalLinkInvalid = false; + public PROJECT_PHASES: Array = [ + 'Pre-Planning', + 'Plan Initiation', + 'Plan Development', + 'Plan Evaluation and Approval', + 'Plan Implementation and Monitoring' + ]; + + constructor( + private router: Router, + private _changeDetectionRef: ChangeDetectorRef, + private storageService: StorageService, + private linkService: LinkService, + private utils: Utils, + private config: ConfigService, + private route: ActivatedRoute + ) { } + + /** + * Get the current project from local storage. Set up the form for adding + * external links (files). + * + * @return {void} + */ + ngOnInit() { + this.documents = this.storageService.state.selectedDocs; + this.currentProject = this.storageService.state.currentProject.data; + this.route.data + .takeUntil(this.ngUnsubscribe) + .subscribe((res: any) => { + if (res?.sections) { + this.documentSections = res.sections; + } else { + alert('Uh oh, couldn\'t load document sections.'); + console.error('Couldn\'t load document sections.', res) + } + }); + + this.config.lists.forEach(item => { + switch (item.type) { + case 'projectPhase': + break; + } + }); + + const today = new Date(); + const todayObj = { + year: today.getFullYear(), + month: today.getMonth() + 1, + day: today.getDate() + }; + + if (this.documents?.length > 1) { + // If multiple documents were selected, navigate back to the file list. Not supported yet. + this.goBack(); + } else if (this.storageService.state?.form?.values?.length > 0) { + // If there is an existing form in the storage service, populate our form with that data. + this.myForm = this.storageService.state.form; + } else if (this.documents?.length === 1) { + // If we are being passed a single document then we are editing. Populate with document data. + this.dateAdded = this.documents[0].datePosted || this.documents[0].dateAdded; + this.externalLink = this.documents[0].documentFileName || this.documents[0].externalLink; + this.myForm = new FormGroup({ + 'dateAdded': new FormControl(this.utils.convertJSDateToNGBDate(new Date(this.dateAdded)), Validators.required), + 'dateUpdated': new FormControl(), + 'externalLink': new FormControl(this.externalLink, Validators.required), + 'displayName': new FormControl(this.documents[0].displayName, Validators.required), + 'description': new FormControl(this.documents[0].description), + 'projectPhase': new FormControl(this.documents[0].projectPhase, Validators.required), + 'section': new FormControl(this.documents[0].section || ''), + 'read': new FormControl(this.documents[0].read || '') + }); + } else { + // Create a new form. + this.myForm = new FormGroup({ + 'dateAdded': new FormControl('', [Validators.required]), + 'dateUpdated': new FormControl(), + 'externalLink': new FormControl('', [Validators.required]), + 'displayName': new FormControl('', [Validators.required]), + 'description': new FormControl(''), + 'projectPhase': new FormControl('', [Validators.required]), + 'section': new FormControl(''), + }); + this.myForm.controls.dateAdded.setValue(todayObj); + } + this.myForm.controls.dateUpdated.setValue(todayObj); + this.loading = false; + this._changeDetectionRef.detectChanges(); + } + + /** + * Handle a file link. Prepare the external link data then contact the external link API to save it. + * Once saved, navigate the user away from the "link file" view. + * + * @return {void} + */ + public linkFile() { + this.loading = true; + + // Update the form data. + const formData = this.updateFormState(); + if (this.documents[0]?._id) { + this.linkService.update(formData, this.documents[0]._id) + .takeUntil(this.ngUnsubscribe) + .subscribe( + exl => {this.storageService.state.selectedDocs = exl}, + error => { + console.error(error); + alert('Uh-oh, couldn\'t update the external link'); + }, + () => { // onCompleted + this.router.navigate(['p', this.currentProject._id, 'project-files']); + this.loading = false; + } + ) + } else { + this.linkService.add(formData) + .takeUntil(this.ngUnsubscribe) + .subscribe( + exl => {this.storageService.state.selectedDocs = exl}, + error => { + console.error(error); + alert('Uh-oh, couldn\'t create the external link'); + }, + () => { // onCompleted + this.storageService.state = { type: 'documents', data: this.storageService.state.selectedDocs }; + this.router.navigate(['p', this.currentProject._id, 'project-files']); + this.loading = false; + } + ); + } + } + + /** + * Update storage service from current form values. + * + */ + public updateFormState = () => { + const formData = new FormData(); + formData.append('project', this.currentProject._id); + formData.append('externalLink', this.myForm.value.externalLink) + formData.append('displayName', this.myForm.value.displayName); + formData.append('dateAdded', new Date(Number(moment(this.utils.convertFormGroupNGBDateToJSDate(this.myForm.get('dateAdded').value)))).toISOString()); + formData.append('dateUpdated', new Date(Number(moment(this.utils.convertFormGroupNGBDateToJSDate(this.myForm.get('dateUpdated').value)))).toISOString()); + formData.append('description', this.myForm.value.description); + formData.append('projectPhase', this.myForm.value.projectPhase); + formData.append('section', this.myForm.value.section); + formData.append('checkbox', 'false'); + this.storageService.state = { type: 'form', data: null }; + return formData; + } + + /** + * Make sure the external link name doesn't include any invalid characters. + * + * @return {void} + */ + public validateChars() { + this.docNameInvalid = this.myForm.value.displayName.match(/[\/|\\:*?"<>]/g) ? true : false; + } + + /** + * Make sure that the external link is a valid URL to a file. + * + * @return {void} + */ + public validateLink() { + const link = this.myForm.value.externalLink.toLowerCase(); + try { + const url = new URL(link); + this.externalLinkInvalid = false; + } catch { + this.externalLinkInvalid = true; + } + } + + /** + * Terminate subscriptions when component is unmounted. + * + * @return {void} + */ + ngOnDestroy() { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } + + /** + * If local storage has a previous router location, navigate the user to it, + * otherwise, navigate the user to the "project files" view. + * + * @return {void} + */ + goBack() { + if (this.storageService.state.back?.url) { + this.router.navigate(this.storageService.state.back.url); + } else { + this.router.navigate(['/p', this.currentProject._id, 'project-files']); + } + } +} + + diff --git a/src/app/project/project-documents/external-link/link-resolver.services.ts b/src/app/project/project-documents/external-link/link-resolver.services.ts new file mode 100644 index 00000000..82945b99 --- /dev/null +++ b/src/app/project/project-documents/external-link/link-resolver.services.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { Resolve, ActivatedRouteSnapshot } from '@angular/router'; +import { Observable } from 'rxjs'; +import { DocumentSectionService } from 'app/services/documentSection.service'; + +@Injectable() +export class ExternalLinkResolver implements Resolve> { + constructor( + private documentSectionService: DocumentSectionService + ) { } + + /** + * Get route params and make a request to the API to get a set of + * document (file) sections for the current project. + * + * @param {ActivatedRouteSnapshot} route The route to get params from. + * @returns {Observable} + */ + resolve(route: ActivatedRouteSnapshot): Observable { + const projectId = route.parent.paramMap.get('projId'); + return this.documentSectionService.getAll(projectId);; + } +} diff --git a/src/app/project/project-documents/project-document-resolver.services.ts b/src/app/project/project-documents/project-document-resolver.services.ts index 90645d87..5370a285 100644 --- a/src/app/project/project-documents/project-document-resolver.services.ts +++ b/src/app/project/project-documents/project-document-resolver.services.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { Resolve, ActivatedRouteSnapshot } from '@angular/router'; -import { Observable } from 'rxjs'; +import { forkJoin, Observable } from 'rxjs'; import { SearchService } from 'app/services/search.service'; import { StorageService } from 'app/services/storage.service'; @@ -12,6 +12,27 @@ export class DocumentsResolver implements Resolve> { private storageService: StorageService ) { } + /** + * Retrieves documents or external links + * + * @param {ActivatedRouteSnapshot} route The route to get params from. + * @param {string} schema The schema type to use, either 'Document' or 'ExternalLink' + * @param {string} projectId The project ID of the documents you wish to retrieve + * @returns {Observable} + */ + getFiles = (route: ActivatedRouteSnapshot, schema: string, projectId: string): Observable => { + const tableParams = this.storageService.state.projectDocumentTableParams || null; + const keys = tableParams?.keywords || route.params.keywords || ''; + const dataset = schema; + const fields = [{ 'name': 'project', 'value': projectId }]; + const pageNum = 1; + const pageSize = 1000; + const sortBy = tableParams?.sortBy || route.params.sortBy || '-datePosted'; + const queryModifier = {}; + const populate = true; + return this.searchService.getSearchResults(keys, dataset, fields, pageNum, pageSize, sortBy, queryModifier, populate); + } + /** * Get route params and make a request to the API to get a set of * documents(files) that match the request params. @@ -21,30 +42,11 @@ export class DocumentsResolver implements Resolve> { */ resolve(route: ActivatedRouteSnapshot): Observable { const projectId = route.parent.paramMap.get('projId'); - if (this.storageService.state.projectDocumentTableParams == null) { - const pageNum = route.params.pageNum ? route.params.pageNum : 1; - const pageSize = route.params.pageSize ? route.params.pageSize : 10; - const sortBy = route.params.sortBy ? route.params.sortBy : '-datePosted'; - const keywords = route.params.keywords || ''; - return this.searchService.getSearchResults( - keywords, - 'Document', - [{ 'name': 'project', 'value': projectId }], - pageNum, - pageSize, - sortBy, - {}, - true); - } else { - return this.searchService.getSearchResults( - this.storageService.state.projectDocumentTableParams.keywords, - 'Document', - [{ 'name': 'project', 'value': projectId }], - this.storageService.state.projectDocumentTableParams.pageNum, - this.storageService.state.projectDocumentTableParams.pageSize, - this.storageService.state.projectDocumentTableParams.sortBy, - {}, - true); - } + const documents = this.getFiles(route, 'Document', projectId); + const externalLinks = this.getFiles(route, 'ExternalLink', projectId); + return forkJoin({ + documents: documents, + externalLinks: externalLinks, + }); } } diff --git a/src/app/project/project-documents/project-documents.component.ts b/src/app/project/project-documents/project-documents.component.ts index 9fd5896f..daba60d7 100644 --- a/src/app/project/project-documents/project-documents.component.ts +++ b/src/app/project/project-documents/project-documents.component.ts @@ -3,7 +3,6 @@ import { Router, ActivatedRoute } from '@angular/router'; import { Subject, forkJoin } from 'rxjs'; import { MatSnackBar } from '@angular/material/snack-bar'; import { NavBarButton, PageBreadcrumb } from 'app/shared/components/navbar/types'; -import { Document } from 'app/models/document'; import { SearchTerms } from 'app/models/search'; import { isEmpty } from 'lodash' @@ -26,7 +25,8 @@ import { NgxSmartModalService } from 'ngx-smart-modal'; export class ProjectDocumentsComponent implements OnInit, OnDestroy { public terms = new SearchTerms(); private ngUnsubscribe: Subject = new Subject(); - public documents: Document[] = null; + public documents = null; + public documentVault = null; public loading = true; public navBarButtons: NavBarButton[]; public pageBreadcrumbs: PageBreadcrumb[]; @@ -102,7 +102,7 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { .takeUntil(this.ngUnsubscribe) .subscribe(params => { this.tableParams = this.tableTemplateUtils.getParamsFromUrl(params); - if (this.tableParams.sortBy === '') { + if (!this.tableParams.sortBy) { this.tableParams.sortBy = '-datePosted'; } if (params.keywords !== undefined) { @@ -125,13 +125,12 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { .takeUntil(this.ngUnsubscribe) .subscribe((res: any) => { if (res) { - if (res.documents[0].data.meta && res.documents[0].data.meta.length > 0) { - this.tableParams.totalListItems = res.documents[0].data.meta[0].searchResultsTotal; - this.documents = res.documents[0].data.searchResults; - } else { - this.tableParams.totalListItems = 0; - this.documents = []; - } + const documents = res.documents?.documents[0] || []; + const links = res.documents?.externalLinks[0] || []; + const combinedResults = [...documents?.data?.searchResults, ...links?.data?.searchResults]; + this.tableParams.totalListItems = combinedResults?.length || 0; + const sortedResults = this.sortDocuments(combinedResults); + this.documents = this.documentVault = sortedResults; this.setRowData(); this.loading = false; this._changeDetectionRef.detectChanges(); @@ -154,6 +153,13 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { { label: 'Upload File(s)', action: () => this.router.navigate(['p', this.currentProject._id, 'project-files', 'upload']) + }, + { + label: 'Link External File', + action: () => { + this.storageService.state.selectedDocs = []; + this.router.navigate(['p', this.currentProject._id, 'project-files', 'link']); + } }, { label: 'File Sections', @@ -163,6 +169,35 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { } + /** + * Sorts documents based on the current sort selection. + * + * @param {any} documents The combined documents, including actual documents and external links. + * @return {any} + * + */ + public sortDocuments = (documents: any[]) => { + const sortData = this.tableParams.sortBy || '-datePosted'; + const sortDir = '-' === Array.from(this.tableParams.sortBy)[0] ? -1 : 1; + const sortBy = sortData.substring(1); + const mappedResults = documents.map(doc => this.mapRowData(doc)); + if ('displayName' === sortBy || 'internalExt' === sortBy) { + // If sorting strings then convert to lower case. + mappedResults.sort((a, b) => { + if (a[sortBy].toLowerCase() < b[sortBy].toLowerCase()) return -1 * sortDir; + if (a[sortBy].toLowerCase() > b[sortBy].toLowerCase()) return 1 * sortDir; + return 0; + }); + } else { + mappedResults.sort((a, b) => { + if (a[sortBy] < b[sortBy]) return -1 * sortDir; + if (a[sortBy] > b[sortBy]) return 1 * sortDir; + return 0; + }); + } + return mappedResults || []; + } + /** * Display the snackbar UI component which shows a message to the user. * @@ -193,8 +228,13 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { selBox.style.left = '0'; selBox.style.top = '0'; selBox.style.opacity = '0'; - const safeName = item.documentFileName.replace(/ /g, '_'); - selBox.value = `${this.pathAPI}/document/${item._id}/fetch/${safeName}`; + // Create a fetch link if it's an internal file, provide the link if it's an external file. + if (!item.documentFileName.includes('http')) { + const safeName = item.documentFileName.replace(/ /g, '_'); + selBox.value = `${this.pathAPI}/document/${item._id}/fetch/${safeName}`; + } else { + selBox.value = item.documentFileName; + } document.body.appendChild(selBox); selBox.focus(); selBox.select(); @@ -234,7 +274,7 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { if (selectedDocs.length === 1) { this.storageService.state.labels = selectedDocs[0].labels; } - this.router.navigate(['p', this.currentProject._id, 'project-files', 'edit']); + this.router.navigate(['p', this.currentProject._id, 'project-files', ('external' === selectedDocs[0].internalExt ? 'edit-link' : 'edit')]); break; case 'delete': this.onDeleteDocument(); @@ -242,7 +282,11 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { case 'download': this.documentTableData.data.map((item) => { if (item.checkbox === true) { - promises.push(this.api.downloadDocument(this.documents.filter(d => d._id === item._id)[0])); + if ('external' === item.internalExt) { + window.open(item.documentFileName, "_blank"); + } else { + promises.push(this.api.downloadDocument(this.documents.filter(d => d._id === item._id)[0])); + } } }); Promise.all(promises).then(() => { @@ -484,30 +528,46 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { */ setRowData(): void { let documentList = []; - if (this.documents && this.documents.length > 0) { + // Process stored files/documents + if (this.documents?.length > 0) { this.documents.forEach(document => { - documentList.push( - { - displayName: document.displayName, - documentFileName: document.documentFileName, - internalSize: document.internalSize, - internalExt: document.internalExt, - datePosted: document.datePosted, - status: document.read.includes('public') ? 'Published' : 'Not Published', - _id: document._id, - project: document.project, - read: document.read - } - ); + const mappedDoc = this.mapRowData(document); + documentList.push(mappedDoc); }); - this.documentTableData = new TableObject( - DocumentTableRowsComponent, - documentList, - this.tableParams - ); - } + } + if (documentList.length > 0) { + this.documentTableData = new TableObject( + DocumentTableRowsComponent, + documentList, + this.tableParams + ); + } } + /** + * Maps row data to a format that is familiar for the table. + * + * @param {any} file The file to be mapped + * @returns {object} + * + */ + mapRowData(file) { + return { + displayName: file.displayName, + documentFileName: file.documentFileName || file.externalLink || '', + internalSize: file.internalSize || null, + internalExt: file.internalExt || 'external', + datePosted: file.datePosted || file.dateAdded, + status: file.read.includes('public') ? 'Published' : 'Not Published', + _id: file._id, + project: file.project, + read: file.read, + projectPhase: file.projectPhase, + description: file.description, + section: file.section, + } + } + /** * Sort existing results by column(name, date, size, type, etc.) * @@ -519,6 +579,10 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { } else { this.tableParams.sortBy = '+' + column; } + window.scrollTo(0, 0); + this.loading = true; + this.documentVault = this.sortDocuments(this.documentVault); + this.tableTemplateUtils.updateUrl(this.tableParams.sortBy, this.tableParams.currentPage, this.tableParams.pageSize, this.tableParams.filter, this.tableParams.keywords || ''); this.getPaginatedDocs(this.tableParams.currentPage); } @@ -624,30 +688,18 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { * @return {void} */ public getPaginatedDocs(pageNumber: number): void { - // Go to top of page after clicking to a different page. - window.scrollTo(0, 0); - this.loading = true; - - this.tableParams = this.tableTemplateUtils.updateTableParams(this.tableParams, pageNumber, this.tableParams.sortBy); - - this.searchService.getSearchResults( - this.tableParams.keywords || '', - 'Document', - [{ 'name': 'project', 'value': this.currentProject._id }], - pageNumber, - this.tableParams.pageSize, - this.tableParams.sortBy, - this.tableParams.filter, - true) - .takeUntil(this.ngUnsubscribe) - .subscribe((res: any) => { - this.tableParams.totalListItems = res[0].data.meta[0].searchResultsTotal; - this.documents = res[0].data.searchResults; - this.tableTemplateUtils.updateUrl(this.tableParams.sortBy, this.tableParams.currentPage, this.tableParams.pageSize, this.tableParams.filter, this.tableParams.keywords || ''); - this.setRowData(); - this.loading = false; - this._changeDetectionRef.detectChanges(); - }); + window.scrollTo(0, 0); + this.loading = true; + this.tableParams = this.tableTemplateUtils.updateTableParams(this.tableParams, pageNumber, this.tableParams.sortBy); + const startIndex = (pageNumber - 1) * this.tableParams.pageSize; + const endIndex = startIndex + this.tableParams.pageSize; + if (endIndex && 0 < this.documentVault.length) { + this.documents = this.documentVault.slice(startIndex, endIndex); + this.tableTemplateUtils.updateUrl(this.tableParams.sortBy, this.tableParams.currentPage, this.tableParams.pageSize, this.tableParams.filter, this.tableParams.keywords || ''); + this.setRowData(); + this.loading = false; + this._changeDetectionRef.detectChanges(); + } } /** diff --git a/src/app/project/project-documents/upload/upload.component.html b/src/app/project/project-documents/upload/upload.component.html index febaca4b..0784882c 100644 --- a/src/app/project/project-documents/upload/upload.component.html +++ b/src/app/project/project-documents/upload/upload.component.html @@ -20,7 +20,7 @@
    -
    +
    @@ -38,7 +38,7 @@
    -
    +
    -
    +
    -
    +
    @@ -75,7 +75,7 @@
    -
    +

    diff --git a/src/app/project/project-documents/upload/upload.component.scss b/src/app/project/project-documents/upload/upload.component.scss index 84729515..55eb29ab 100644 --- a/src/app/project/project-documents/upload/upload.component.scss +++ b/src/app/project/project-documents/upload/upload.component.scss @@ -15,3 +15,14 @@ margin-top: 0.5rem; color: red; } + +.form-container-65 { + width: 65%; +} + +@media (max-width: 1200px) { + .form-container-65 { + width: 100%; + flex-basis: 100%; + } +} \ No newline at end of file diff --git a/src/app/project/project-routing.module.ts b/src/app/project/project-routing.module.ts index 326e6d25..e1b714ae 100644 --- a/src/app/project/project-routing.module.ts +++ b/src/app/project/project-routing.module.ts @@ -47,6 +47,8 @@ import { AddEditProjectUpdateComponent } from './project-updates/add-edit-projec import { AddEditProjectUpdateResolver } from './project-updates/add-edit-project-update/add-edit-project-update-resolver'; import { DocumentEditResolver } from './project-documents/document-edit/document-edit-resolver.services'; import { UploadResolver } from './project-documents/upload/upload-resolver.services'; +import { ExternalLinkComponent } from './project-documents/external-link/external-link.component'; +import { ExternalLinkResolver } from './project-documents/external-link/link-resolver.services'; const routes: Routes = [ { @@ -100,6 +102,13 @@ const routes: Routes = [ resolve: { sections: UploadResolver } + }, + { + path: 'project-files/link', + component: ExternalLinkComponent, + resolve: { + sections: ExternalLinkResolver + } }, { path: 'project-files/edit', @@ -107,6 +116,13 @@ const routes: Routes = [ resolve: { sections: DocumentEditResolver } + }, + { + path: 'project-files/edit-link', + component: ExternalLinkComponent, + resolve: { + sections: ExternalLinkResolver + } }, { path: 'project-files/edit/add-label', @@ -301,6 +317,7 @@ const routes: Routes = [ CommentPeriodsResolver, DocumentDetailResolver, DocumentsResolver, + ExternalLinkResolver, ProjectUpdatesResolver, DocumentDetailResolver, ProjectResolver, diff --git a/src/app/project/project.module.ts b/src/app/project/project.module.ts index 70518b6b..b9b48eba 100644 --- a/src/app/project/project.module.ts +++ b/src/app/project/project.module.ts @@ -68,6 +68,7 @@ import { PermissionsTableRowsComponent } from './project-permissions/permissions import { FileSectionsResolver } from './project-documents/section/project-files-section-resolver.services'; import { DocumentSectionService } from 'app/services/documentSection.service'; import { UploadResolver } from './project-documents/upload/upload-resolver.services'; +import { LinkService } from 'app/services/link.service'; @NgModule({ imports: [ @@ -145,7 +146,8 @@ import { UploadResolver } from './project-documents/upload/upload-resolver.servi AddEditProjectUpdateResolver, FileSectionsResolver, DocumentEditResolver, - UploadResolver + UploadResolver, + LinkService, ] }) diff --git a/src/app/services/api.ts b/src/app/services/api.ts index 9743a8de..e37fd94e 100644 --- a/src/app/services/api.ts +++ b/src/app/services/api.ts @@ -22,6 +22,7 @@ import { Org } from 'app/models/org'; import { RecentActivity } from 'app/models/recentActivity'; import { CommentPeriodSummary } from 'app/models/commentPeriodSummary'; import { EmailSubscribe } from 'app/models/emailSubscribe'; +import { ExternalLink } from 'app/models/externalLink'; interface LocalLoginResponse { _id: string; @@ -1067,6 +1068,135 @@ export class ApiService { return this.http.post(`${this.pathAPI}/${queryString}`, formData, {}); } + /** + * Get mutliple individual external links by their respective IDs. + * + * @param {Array} ids The external link IDs to retrieve by. + * @returns {Observable} + */ + getExternalLinksByMultiId(ids: Array): Observable { + const fields = [ + '_id', + 'documentFileName', + 'displayName', + 'section', + 'externalLink', + 'dateAdded', + 'dateUpdated', + 'checkbox', + 'project', + 'projectPhase', + 'description', + 'read' + ]; + const queryString = `link?exLinkIds=${this.buildValues(ids)}&fields=${this.buildValues(fields)}`; + return this.http.get(`${this.pathAPI}/${queryString}`, {}); + } + + /** + * Get external link by ID. Return a select set of fields. + * + * @param {string} exLinkId The link ID to get by. + * @returns {Observable} + */ + getLink(exLinkId: string): Observable { + const fields = [ + '_addedBy', + 'displayName', + 'dateAdded', + 'dateUpdated', + 'section', + 'checkbox', + 'project', + 'projectPhase', + 'externalLink', + 'description', + 'read' + ]; + const queryString = `link/${exLinkId}?fields=${this.buildValues(fields)}`; + return this.http.get(`${this.pathAPI}/${queryString}`, {}); + } + + /** + * Get all external links by project. + * + * @param {string} projectId The project ID to get external links for. + * @returns {Observable} + */ + getLinks(projectId: string): Observable { + const fields = [ + '_addedBy', + 'displayName', + 'dateAdded', + 'dateUpdated', + 'section', + 'checkbox', + 'project', + 'projectPhase', + 'externalLink', + 'description', + 'read' + ]; + const queryString = `link/${projectId}?fields=${this.buildValues(fields)}`; + return this.http.get(`${this.pathAPI}/${queryString}`, {}); + } + + /** + * Update a link with edited form data. + * + * @param {FormData} formData The form data to update the link with. + * @param {string} _id The link to udpate. + * @returns {Observable} + */ + updateLink(formData: FormData, exLinkId: any): Observable { + const queryString = `link/${exLinkId}`; + return this.http.put(`${this.pathAPI}/${queryString}`, formData, {}); + } + + /** + * Delete an external link. + * + * @param {ExternalLink} link The link to delete. + * @returns {Observable} + */ + deleteLink(link: ExternalLink): Observable { + const queryString = `link/${link._id}`; + return this.http.delete(`${this.pathAPI}/${queryString}`, {}); + } + + /** + * Publish a link by toggling its visibility to "public" app users. + * + * @param {string} exLinkId The link ID to publish. + * @returns {Observable} + */ + publishLink(exLinkId: String): Observable { + const queryString = `link/${exLinkId}/publish`; + return this.http.put(`${this.pathAPI}/${queryString}`, {}, {}); + } + + /** + * Unpublish a link by toggling its visibility to "public" app users. + * + * @param {string} exLinkId The link ID to unpublish. + * @returns {Observable} + */ + unPublishLink(exLinkId: String): Observable { + const queryString = `link/${exLinkId}/unpublish`; + return this.http.put(`${this.pathAPI}/${queryString}`, {}, {}); + } + + /** + * Add a new link. + * + * @param {FormData} formData The form data to upload the link with. + * @returns {Observable} + */ + addLink(formData: FormData): Observable { + const queryString = `link/`; + return this.http.post(`${this.pathAPI}/${queryString}`, formData, {}); + } + /** * Get a blob of data. Possibly a document(file), or CSV. * diff --git a/src/app/services/link.service.spec.ts b/src/app/services/link.service.spec.ts new file mode 100644 index 00000000..226f1a05 --- /dev/null +++ b/src/app/services/link.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { LinkService } from './link.service'; +import { ApiService } from 'app/services/api'; + +describe('LinkService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [LinkService, { provide: ApiService }] + }); + }); + + it('should be created', inject([LinkService], (service: LinkService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/src/app/services/link.service.ts b/src/app/services/link.service.ts new file mode 100644 index 00000000..4ebb6701 --- /dev/null +++ b/src/app/services/link.service.ts @@ -0,0 +1,123 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map, catchError } from 'rxjs/operators'; + +import { ApiService } from './api'; +import { ExternalLink } from 'app/models/externalLink'; + +@Injectable() +export class LinkService { + constructor(private api: ApiService) { } + + /** + * Get multiple external links by their ids. + * + * @param {Array} ids The document IDs to get with. + * @returns {Observable} + */ + getByMultiId(ids: Array): Observable> { + return this.api.getExternalLinksByMultiId(ids) + .map(res => { + if (res && res.length > 0) { + let exLinks = []; + res.forEach(exl => { + exLinks.push(new ExternalLink(exl)); + }); + return exLinks; + } + return null; + }) + .catch(error => this.api.handleError(error)); + } + + /** + * Get a specific link by its id. Return only the first link if multiple somehow come back. + * + * @param {string} exLinkId The link ID to get with. + * @returns {Observable} + */ + getById(exLinkId: string): Observable { + return this.api.getLink(exLinkId).pipe( + map(res => { + if (res) { + // return the first (only) link + return res[0]; + } + return null; + }), + catchError(error => this.api.handleError(error)) + ); + } + + /** + * Get all links from a single project. + * + * @param {string} currentProjectId The project to get links for. + * @returns {Observable} + */ + public getAll(currentProjectId: string): Observable { + return this.api.getLinks(currentProjectId) + .map((res: any) => { + if (!res || res.length === 0) { + return []; + } else { + return res.map(link => new ExternalLink(link)); + } + }) + .catch(error => this.api.handleError(error)); + } + + /** + * Add a new link. + * + * @param {FormData} formData The form data to add an external link with. + * @returns {Observable} + */ + add(formData: FormData): Observable { + return this.api.addLink(formData).pipe(catchError(error => this.api.handleError(error))); + } + + /** + * Update a link. + * + * @param {FormData} formData The form data to update a link with. + * @param {any} _id The link ID to update. + * @returns {Observable} + */ + update(formData: FormData, _id: any): Observable { + return this.api.updateLink(formData, _id) + .catch(error => this.api.handleError(error)); + } + + /** + * Delete a link. + * + * @param {ExternalLink} link The link to delete. + * @returns {Observable} + */ + delete(link: ExternalLink): Observable { + return this.api.deleteLink(link).pipe(catchError(error => this.api.handleError(error))); + } + + /** + * Publish a link by toggling its visibility for "public" app users. + * + * @param {string} exLinkId The document ID to publish with. + * @returns {Observable} + */ + publish(exLinkId: string): Observable { + return this.api.publishLink(exLinkId) + .catch(error => this.api.handleError(error)); + } + + /** + * Unpublish a link by toggling its visibility for "public" app users. + * + * @param {string} exLinkId The link ID to unpublish with. + * @returns {Observable} + */ + unPublish(exLinkId: string): Observable { + return this.api.unPublishLink(exLinkId) + .catch(error => this.api.handleError(error)); + } +} From f07548a2b387171bbc0ec69508f7a0ea5ec59640 Mon Sep 17 00:00:00 2001 From: Jareth Whitney Date: Tue, 14 Jan 2025 16:31:27 -0800 Subject: [PATCH 2/3] feature/deseng751: Removed unneeded lodash import, fixed indenting, modified window.open calls. --- src/app/models/externalLink.ts | 8 +- .../comment-period-details-tab.component.ts | 5 +- .../add-documents/add-documents.component.ts | 161 +++++++------ .../add-edit-comment-period.component.ts | 16 +- .../detail/detail.component.html | 2 +- .../detail/detail.component.ts | 80 +++---- .../document-edit.component.scss | 2 +- .../external-link.component.html | 34 ++- .../external-link.component.scss | 2 +- .../external-link/external-link.component.ts | 218 +++++++++--------- .../external-link/link-resolver.services.ts | 6 +- .../project-document-resolver.services.ts | 52 ++--- .../project-documents.component.scss | 14 +- .../project-documents.component.ts | 198 ++++++++-------- .../upload/upload.component.scss | 2 +- src/app/project/project-routing.module.ts | 6 +- src/app/project/project.module.ts | 2 +- .../add-edit-project.component.scss | 62 ++--- src/app/services/api.ts | 166 ++++++------- src/app/services/link.service.ts | 52 ++--- 20 files changed, 539 insertions(+), 549 deletions(-) diff --git a/src/app/models/externalLink.ts b/src/app/models/externalLink.ts index c98dc533..9ff73d11 100644 --- a/src/app/models/externalLink.ts +++ b/src/app/models/externalLink.ts @@ -1,17 +1,14 @@ -import * as _ from 'lodash'; - export class ExternalLink { _id: string; project: string; displayName: string; - externalLink: string; + externalLink: string; section: string; dateAdded: Date; dateUpdated: Date; description: string; projectPhase: string; checkbox: boolean; - isPublished: boolean; constructor(obj?: any) { this._id = obj && obj._id || null; @@ -23,7 +20,6 @@ export class ExternalLink { this.dateUpdated = obj && obj.dateUpdated || null; this.description = obj && obj.description || null; this.projectPhase = obj && obj.projectPhase || null; - this.checkbox = false || null; - this.isPublished = false || null; + this.checkbox = false || null; } } diff --git a/src/app/project/comment-period/comment-period-details-tabs/comment-period-details-tab.component.ts b/src/app/project/comment-period/comment-period-details-tabs/comment-period-details-tab.component.ts index c96f01c3..8c871b8e 100644 --- a/src/app/project/comment-period/comment-period-details-tabs/comment-period-details-tab.component.ts +++ b/src/app/project/comment-period/comment-period-details-tabs/comment-period-details-tab.component.ts @@ -42,7 +42,7 @@ export class CommentPeriodDetailsTabComponent implements OnInit, OnChanges, OnDe constructor( private api: ApiService, private surveyService: SurveyService, - private externalLinkService: LinkService, + private externalLinkService: LinkService, private commentPeriodService: CommentPeriodService, private documentService: DocumentService, private route: ActivatedRoute, @@ -65,7 +65,7 @@ export class CommentPeriodDetailsTabComponent implements OnInit, OnChanges, OnDe this.projectId = this.storageService.state.currentProject.data._id; if (this.commentPeriod.relatedDocuments.length > 0) { - forkJoin([this.documentService.getByMultiId(this.commentPeriod.relatedDocuments), this.externalLinkService.getByMultiId(this.commentPeriod.relatedDocuments)]) + forkJoin([this.documentService.getByMultiId(this.commentPeriod.relatedDocuments), this.externalLinkService.getByMultiId(this.commentPeriod.relatedDocuments)]) .takeUntil(this.ngUnsubscribe) .subscribe( data => { @@ -274,6 +274,7 @@ export class CommentPeriodDetailsTabComponent implements OnInit, OnChanges, OnDe * @returns {Promise} */ public downloadDocument(document) { + console.log('ran download document code'); return document.externalLink ? window.open(document.externalLink) : this.api.downloadDocument(document); } diff --git a/src/app/project/comment-periods/add-edit-comment-period/add-documents/add-documents.component.ts b/src/app/project/comment-periods/add-edit-comment-period/add-documents/add-documents.component.ts index f150b019..46ba8990 100644 --- a/src/app/project/comment-periods/add-edit-comment-period/add-documents/add-documents.component.ts +++ b/src/app/project/comment-periods/add-edit-comment-period/add-documents/add-documents.component.ts @@ -2,20 +2,15 @@ import { Component, OnInit, ChangeDetectorRef, OnDestroy, ChangeDetectionStrateg import { PlatformLocation } from '@angular/common'; import { Router, ActivatedRoute } from '@angular/router'; import { Subject } from 'rxjs'; - -import { Document } from 'app/models/document'; import { SearchTerms } from 'app/models/search'; - import { ApiService } from 'app/services/api'; import { SearchService } from 'app/services/search.service'; import { StorageService } from 'app/services/storage.service'; - import { AddDocumentTableRowsComponent } from './add-document-table-rows/add-document-table-rows.component'; import { TableObject } from 'app/shared/components/table-template/table-object'; import { TableParamsObject } from 'app/shared/components/table-template/table-params-object'; import { TableTemplateUtils } from 'app/shared/utils/table-template-utils'; import { encode } from 'punycode'; -import { ExternalLink } from 'app/models/externalLink'; @Component({ selector: 'app-add-documents', @@ -27,7 +22,7 @@ export class AddDocumentComponent implements OnInit, OnDestroy { public terms = new SearchTerms(); private ngUnsubscribe: Subject = new Subject(); public documents = null; - public documentVault = null; + public documentVault = null; public loading = true; public isEditing = false; @@ -124,14 +119,14 @@ export class AddDocumentComponent implements OnInit, OnDestroy { .subscribe((res: any) => { if (res) { const documents = res.documents?.documents?.at(0) || []; - const links = res.documents?.externalLinks?.at(0) || []; - const combinedResults = [...documents?.data?.searchResults, ...links?.data?.searchResults]; - this.tableParams.totalListItems = combinedResults?.length || 0; - const sortedResults = this.sortDocuments(combinedResults); - this.documents = this.documentVault = sortedResults; - this.setDocumentRowData(); - this.loading = false; - this._changeDetectionRef.detectChanges(); + const links = res.documents?.externalLinks?.at(0) || []; + const combinedResults = [...documents?.data?.searchResults, ...links?.data?.searchResults]; + this.tableParams.totalListItems = combinedResults?.length || 0; + const sortedResults = this.sortDocuments(combinedResults); + this.documents = this.documentVault = sortedResults; + this.setDocumentRowData(); + this.loading = false; + this._changeDetectionRef.detectChanges(); } else { alert('Uh-oh, couldn\'t load valued components'); // project not found --> navigate back to search @@ -141,58 +136,58 @@ export class AddDocumentComponent implements OnInit, OnDestroy { } } - /** - * Sorts documents based on the current sort selection. - * - * @param {any} documents The combined documents, including actual documents and external links. - * @return {any} - * - */ - public sortDocuments = (documents: any[]) => { - const sortData = this.tableParams.sortBy || '-datePosted'; - const sortDir = '-' === this.tableParams.sortBy.charAt(0) ? -1 : 1; - const sortBy = sortData.substring(1); - const mappedResults = documents.map(doc => this.mapRowData(doc)); - if ('displayName' === sortBy || 'internalExt' === sortBy) { - // If sorting strings then convert to lower case. - mappedResults.sort((a, b) => { - if (a[sortBy].toLowerCase() < b[sortBy].toLowerCase()) return -1 * sortDir; - if (a[sortBy].toLowerCase() > b[sortBy].toLowerCase()) return 1 * sortDir; - return 0; - }); - } else { - mappedResults.sort((a, b) => { - if (a[sortBy] < b[sortBy]) return -1 * sortDir; - if (a[sortBy] > b[sortBy]) return 1 * sortDir; - return 0; - }); - } - return mappedResults || []; - } + /** + * Sorts documents based on the current sort selection. + * + * @param {any} documents The combined documents, including actual documents and external links. + * @return {any} + * + */ + public sortDocuments = (documents: any[]) => { + const sortData = this.tableParams.sortBy || '-datePosted'; + const sortDir = '-' === this.tableParams.sortBy.charAt(0) ? -1 : 1; + const sortBy = sortData.substring(1); + const mappedResults = documents.map(doc => this.mapRowData(doc)); + if ('displayName' === sortBy || 'internalExt' === sortBy) { + // If sorting strings then convert to lower case. + mappedResults.sort((a, b) => { + if (a[sortBy].toLowerCase() < b[sortBy].toLowerCase()) return -1 * sortDir; + if (a[sortBy].toLowerCase() > b[sortBy].toLowerCase()) return 1 * sortDir; + return 0; + }); + } else { + mappedResults.sort((a, b) => { + if (a[sortBy] < b[sortBy]) return -1 * sortDir; + if (a[sortBy] > b[sortBy]) return 1 * sortDir; + return 0; + }); + } + return mappedResults || []; + } - /** - * Maps row data to a format that is familiar for the table. - * - * @param {any} file The file to be mapped. "Any" because it could be a Document or ExternalLink. - * @returns {object} - * - */ - mapRowData(file) { - return { - displayName: file.displayName, - documentFileName: file.documentFileName || file.externalLink || '', - internalSize: file.internalSize || null, - internalExt: file.internalExt || 'external', - datePosted: file.datePosted || file.dateAdded, - status: file.read.includes('public') ? 'Published' : 'Not Published', - _id: file._id, - project: file.project, - read: file.read, - projectPhase: file.projectPhase, - description: file.description, - section: file.section, - } - } + /** + * Maps row data to a format that is familiar for the table. + * + * @param {any} file The file to be mapped. "Any" because it could be a Document or ExternalLink. + * @returns {object} + * + */ + mapRowData(file) { + return { + displayName: file.displayName, + documentFileName: file.documentFileName || file.externalLink || '', + internalSize: file.internalSize || null, + internalExt: file.internalExt || 'external', + datePosted: file.datePosted || file.dateAdded, + status: file.read.includes('public') ? 'Published' : 'Not Published', + _id: file._id, + project: file.project, + read: file.read, + projectPhase: file.projectPhase, + description: file.description, + section: file.section, + } + } /** * Handle various actions for modifying/working with documents(files). @@ -331,13 +326,13 @@ export class AddDocumentComponent implements OnInit, OnDestroy { } else { this.tableParams.sortBy = '+' + column; } - window.scrollTo(0, 0); + window.scrollTo(0, 0); this.loading = true; - this.documentVault = this.sortDocuments(this.documentVault); - this.tableTemplateUtils.updateUrl(this.tableParams.sortBy, this.tableParams.currentPage, this.tableParams.pageSize, this.tableParams.filter, this.tableParams.keywords || ''); - this.setDocumentRowData(); - this.loading = false; - this._changeDetectionRef.detectChanges(); + this.documentVault = this.sortDocuments(this.documentVault); + this.tableTemplateUtils.updateUrl(this.tableParams.sortBy, this.tableParams.currentPage, this.tableParams.pageSize, this.tableParams.filter, this.tableParams.keywords || ''); + this.setDocumentRowData(); + this.loading = false; + this._changeDetectionRef.detectChanges(); this.getPaginatedDocs(this.tableParams.currentPage); } @@ -375,18 +370,18 @@ export class AddDocumentComponent implements OnInit, OnDestroy { * @return {void} */ public getPaginatedDocs(pageNumber: number): void { - window.scrollTo(0, 0); - this.loading = true; - this.tableParams = this.tableTemplateUtils.updateTableParams(this.tableParams, pageNumber, this.tableParams.sortBy); - const startIndex = (pageNumber - 1) * this.tableParams.pageSize; - const endIndex = startIndex + this.tableParams.pageSize; - if (endIndex && 0 < this.documentVault.length) { - this.documents = this.documentVault.slice(startIndex, endIndex); - this.tableTemplateUtils.updateUrl(this.tableParams.sortBy, this.tableParams.currentPage, this.tableParams.pageSize, this.tableParams.filter, this.tableParams.keywords || ''); - this.setDocumentRowData(); - this.loading = false; - this._changeDetectionRef.detectChanges(); - } + window.scrollTo(0, 0); + this.loading = true; + this.tableParams = this.tableTemplateUtils.updateTableParams(this.tableParams, pageNumber, this.tableParams.sortBy); + const startIndex = (pageNumber - 1) * this.tableParams.pageSize; + const endIndex = startIndex + this.tableParams.pageSize; + if (endIndex && 0 < this.documentVault.length) { + this.documents = this.documentVault.slice(startIndex, endIndex); + this.tableTemplateUtils.updateUrl(this.tableParams.sortBy, this.tableParams.currentPage, this.tableParams.pageSize, this.tableParams.filter, this.tableParams.keywords || ''); + this.setDocumentRowData(); + this.loading = false; + this._changeDetectionRef.detectChanges(); + } } /** diff --git a/src/app/project/comment-periods/add-edit-comment-period/add-edit-comment-period.component.ts b/src/app/project/comment-periods/add-edit-comment-period/add-edit-comment-period.component.ts index 016adadf..82547282 100644 --- a/src/app/project/comment-periods/add-edit-comment-period/add-edit-comment-period.component.ts +++ b/src/app/project/comment-periods/add-edit-comment-period/add-edit-comment-period.component.ts @@ -51,7 +51,7 @@ export class AddEditCommentPeriodComponent implements OnInit, OnDestroy { private commentPeriodService: CommentPeriodService, private config: ConfigService, private surveyService: SurveyService, - private externalLinkService: LinkService, + private externalLinkService: LinkService, private documentService: DocumentService, private formBuilder: FormBuilder, private router: Router, @@ -238,19 +238,17 @@ export class AddEditCommentPeriodComponent implements OnInit, OnDestroy { if (this.storageService.state.selectedDocumentsForCP == null) { if (this.commentPeriod.relatedDocuments.length > 0) { forkJoin([this.documentService.getByMultiId(this.commentPeriod.relatedDocuments), this.externalLinkService.getByMultiId(this.commentPeriod.relatedDocuments)]) - .takeUntil(this.ngUnsubscribe) - .subscribe( - data => { - console.log('made it here'); - this.storageService.state.selectedDocumentsForCP = { type: 'selectedDocumentsForCP', data: [...data[0] || [], ...data[1] || []] }; - } - ); + .takeUntil(this.ngUnsubscribe) + .subscribe( + data => { + this.storageService.state.selectedDocumentsForCP = { type: 'selectedDocumentsForCP', data: [...data[0] || [], ...data[1] || []] }; + } + ); } else { this.storageService.state.selectedDocumentsForCP = { type: 'selectedDocumentsForCP', data: this.commentPeriod.relatedDocuments }; } } } - /** * On comment period form submit, prepare the CP data and submit to the diff --git a/src/app/project/project-documents/detail/detail.component.html b/src/app/project/project-documents/detail/detail.component.html index 64c4a53e..594ce642 100644 --- a/src/app/project/project-documents/detail/detail.component.html +++ b/src/app/project/project-documents/detail/detail.component.html @@ -68,7 +68,7 @@

    Filesize:

    diff --git a/src/app/project/project-documents/detail/detail.component.ts b/src/app/project/project-documents/detail/detail.component.ts index 649caa5e..fd6c37a0 100644 --- a/src/app/project/project-documents/detail/detail.component.ts +++ b/src/app/project/project-documents/detail/detail.component.ts @@ -64,8 +64,8 @@ export class DocumentDetailComponent implements OnInit, OnDestroy { this.selectedSection = this.sections.find((section) => section._id === this.document.section); const safeName = this.document?.documentFileName ? this.document.documentFileName.replace(/ /g, '_') : this.document.externalLink; this.documentUrl = this.document?.externalLink ? safeName : `${this.pathAPI}/document/${this.document._id}/fetch/${safeName}`; - this.publishText = !this.document.read?.includes('public') ? 'Publish' : 'Unpublish'; - + this.publishText = !this.document.read?.includes('public') ? 'Publish' : 'Unpublish'; + this._changeDetectionRef.detectChanges(); }); this.humanReadableSize = this.utils.formatBytes(this.document?.internalSize) || ''; @@ -104,29 +104,29 @@ export class DocumentDetailComponent implements OnInit, OnDestroy { }); } - /** - * Maps row data to a format that is familiar to the edit form. - * - * @param {any} file The file to be mapped, Document or ExternalLink - * @returns {object} - * - */ - mapRowData(file) { - return { - displayName: file.displayName, - documentFileName: file.documentFileName || file.externalLink || '', - internalSize: file.internalSize || null, - internalExt: file.internalExt || 'external', - datePosted: file.datePosted || file.dateAdded, - status: file.read.includes('public') ? 'Published' : 'Not Published', - _id: file._id, - project: file.project, - read: file.read, - projectPhase: file.projectPhase, - description: file.description, - section: file.section, - } - } + /** + * Maps row data to a format that is familiar to the edit form. + * + * @param {any} file The file to be mapped, Document or ExternalLink + * @returns {object} + * + */ + mapRowData(file) { + return { + displayName: file.displayName, + documentFileName: file.documentFileName || file.externalLink || '', + internalSize: file.internalSize || null, + internalExt: file.internalExt || 'external', + datePosted: file.datePosted || file.dateAdded, + status: file.read.includes('public') ? 'Published' : 'Not Published', + _id: file._id, + project: file.project, + read: file.read, + projectPhase: file.projectPhase, + description: file.description, + section: file.section, + } + } /** * When a project edit is initiated, get the associated documents @@ -139,24 +139,24 @@ export class DocumentDetailComponent implements OnInit, OnDestroy { this.storageService.state.selectedDocs = [this.mapRowData(this.document)]; this.storageService.state.labels = this.document.labels || []; this.storageService.state.back = { url: ['/p', this.document.project, 'project-files', 'detail', this.document._id], label: 'View File' }; - this.router.navigate(['p', this.document.project, 'project-files', (this.document.externalLink ? 'edit-link' : 'edit')]); + this.router.navigate(['p', this.document.project, 'project-files', (this.document.externalLink ? 'edit-link' : 'edit')]); } - onDownload() { - if (this.document.externalLink) { - window.open(this.document.externalLink, "_blank"); - } else { - this.api.downloadDocument(this.document); - } - } + onDownload() { + if (this.document.externalLink) { + window.open(this.document.externalLink); + } else { + this.api.downloadDocument(this.document); + } + } - onOpen() { - if (this.document.externalLink) { - window.open(this.document.externalLink, "_blank"); - } else { - this.api.openDocument(this.document); - } - } + onOpen() { + if (this.document.externalLink) { + window.open(this.document.externalLink); + } else { + this.api.openDocument(this.document); + } + } /** * Checks if a document is published. diff --git a/src/app/project/project-documents/document-edit/document-edit.component.scss b/src/app/project/project-documents/document-edit/document-edit.component.scss index ca842e59..3dba60c3 100644 --- a/src/app/project/project-documents/document-edit/document-edit.component.scss +++ b/src/app/project/project-documents/document-edit/document-edit.component.scss @@ -54,6 +54,6 @@ h1 { @media (max-width: 1200px) { .form-container-65 { width: 100%; - flex-basis: 100%; + flex-basis: 100%; } } diff --git a/src/app/project/project-documents/external-link/external-link.component.html b/src/app/project/project-documents/external-link/external-link.component.html index 0ed2b11c..2794f830 100644 --- a/src/app/project/project-documents/external-link/external-link.component.html +++ b/src/app/project/project-documents/external-link/external-link.component.html @@ -10,7 +10,7 @@ Files - +
    @@ -51,15 +51,14 @@
    File date is required
    -
    +
    - - +
    File link is required
    @@ -71,8 +70,7 @@ \ / | : * ? " < > are not accepted file name characters
    - - +
    File name is required
    @@ -85,18 +83,18 @@
    File description is required
    -
    -
    - -
    -
    - - -
    -
    +
    +
    + +
    +
    + + +
    +
    diff --git a/src/app/project/project-documents/external-link/external-link.component.scss b/src/app/project/project-documents/external-link/external-link.component.scss index 55eb29ab..d35549ee 100644 --- a/src/app/project/project-documents/external-link/external-link.component.scss +++ b/src/app/project/project-documents/external-link/external-link.component.scss @@ -23,6 +23,6 @@ @media (max-width: 1200px) { .form-container-65 { width: 100%; - flex-basis: 100%; + flex-basis: 100%; } } \ No newline at end of file diff --git a/src/app/project/project-documents/external-link/external-link.component.ts b/src/app/project/project-documents/external-link/external-link.component.ts index b068ac26..710275c7 100644 --- a/src/app/project/project-documents/external-link/external-link.component.ts +++ b/src/app/project/project-documents/external-link/external-link.component.ts @@ -10,6 +10,8 @@ import { LinkService } from 'app/services/link.service'; import { Utils } from 'app/shared/utils/utils'; import { DocumentSection } from 'app/models/documentSection'; +import { Document } from 'app/models/document'; +import { ExternalLink } from 'app/models/externalLink'; @Component({ selector: 'app-external-link', @@ -22,7 +24,7 @@ export class ExternalLinkComponent implements OnInit, OnDestroy { public currentProject; public projectFiles: Array = []; public externalLink = []; - public documents = null; + public documents: any[] = null; public documentSections: DocumentSection[] = []; public dateAdded = null; public dateUpdated = null; @@ -30,7 +32,7 @@ export class ExternalLinkComponent implements OnInit, OnDestroy { public myForm: FormGroup; public loading = true; public docNameInvalid = false; - public externalLinkInvalid = false; + public externalLinkInvalid = false; public PROJECT_PHASES: Array = [ 'Pre-Planning', 'Plan Initiation', @@ -43,7 +45,7 @@ export class ExternalLinkComponent implements OnInit, OnDestroy { private router: Router, private _changeDetectionRef: ChangeDetectorRef, private storageService: StorageService, - private linkService: LinkService, + private linkService: LinkService, private utils: Utils, private config: ConfigService, private route: ActivatedRoute @@ -51,7 +53,7 @@ export class ExternalLinkComponent implements OnInit, OnDestroy { /** * Get the current project from local storage. Set up the form for adding - * external links (files). + * external links (files). * * @return {void} */ @@ -76,47 +78,47 @@ export class ExternalLinkComponent implements OnInit, OnDestroy { } }); - const today = new Date(); - const todayObj = { - year: today.getFullYear(), - month: today.getMonth() + 1, - day: today.getDate() - }; - - if (this.documents?.length > 1) { - // If multiple documents were selected, navigate back to the file list. Not supported yet. - this.goBack(); - } else if (this.storageService.state?.form?.values?.length > 0) { - // If there is an existing form in the storage service, populate our form with that data. + const today = new Date(); + const todayObj = { + year: today.getFullYear(), + month: today.getMonth() + 1, + day: today.getDate() + }; + + if (this.documents?.length > 1) { + // If multiple documents were selected, navigate back to the file list. Not supported yet. + this.router.navigate(['p', this.currentProject._id, 'project-files']); + } else if (this.storageService.state?.form?.values?.length > 0) { + // If there is an existing form in the storage service, populate our form with that data. this.myForm = this.storageService.state.form; } else if (this.documents?.length === 1) { - // If we are being passed a single document then we are editing. Populate with document data. - this.dateAdded = this.documents[0].datePosted || this.documents[0].dateAdded; - this.externalLink = this.documents[0].documentFileName || this.documents[0].externalLink; - this.myForm = new FormGroup({ - 'dateAdded': new FormControl(this.utils.convertJSDateToNGBDate(new Date(this.dateAdded)), Validators.required), - 'dateUpdated': new FormControl(), - 'externalLink': new FormControl(this.externalLink, Validators.required), - 'displayName': new FormControl(this.documents[0].displayName, Validators.required), - 'description': new FormControl(this.documents[0].description), - 'projectPhase': new FormControl(this.documents[0].projectPhase, Validators.required), - 'section': new FormControl(this.documents[0].section || ''), - 'read': new FormControl(this.documents[0].read || '') - }); - } else { - // Create a new form. - this.myForm = new FormGroup({ - 'dateAdded': new FormControl('', [Validators.required]), - 'dateUpdated': new FormControl(), - 'externalLink': new FormControl('', [Validators.required]), - 'displayName': new FormControl('', [Validators.required]), - 'description': new FormControl(''), - 'projectPhase': new FormControl('', [Validators.required]), - 'section': new FormControl(''), - }); - this.myForm.controls.dateAdded.setValue(todayObj); - } - this.myForm.controls.dateUpdated.setValue(todayObj); + // If we are being passed a single document then we are editing. Populate with document data. + this.dateAdded = this.documents[0].datePosted || this.documents[0].dateAdded; + this.externalLink = this.documents[0].documentFileName || this.documents[0].externalLink; + this.myForm = new FormGroup({ + 'dateAdded': new FormControl(this.utils.convertJSDateToNGBDate(new Date(this.dateAdded)), Validators.required), + 'dateUpdated': new FormControl(), + 'externalLink': new FormControl(this.externalLink, Validators.required), + 'displayName': new FormControl(this.documents[0].displayName, Validators.required), + 'description': new FormControl(this.documents[0].description), + 'projectPhase': new FormControl(this.documents[0].projectPhase, Validators.required), + 'section': new FormControl(this.documents[0].section || ''), + 'read': new FormControl(this.documents[0].read || '') + }); + } else { + // Create a new form. + this.myForm = new FormGroup({ + 'dateAdded': new FormControl('', [Validators.required]), + 'dateUpdated': new FormControl(), + 'externalLink': new FormControl('', [Validators.required]), + 'displayName': new FormControl('', [Validators.required]), + 'description': new FormControl(''), + 'projectPhase': new FormControl('', [Validators.required]), + 'section': new FormControl(''), + }); + this.myForm.controls.dateAdded.setValue(todayObj); + } + this.myForm.controls.dateUpdated.setValue(todayObj); this.loading = false; this._changeDetectionRef.detectChanges(); } @@ -131,57 +133,57 @@ export class ExternalLinkComponent implements OnInit, OnDestroy { this.loading = true; // Update the form data. - const formData = this.updateFormState(); - if (this.documents[0]?._id) { - this.linkService.update(formData, this.documents[0]._id) - .takeUntil(this.ngUnsubscribe) - .subscribe( - exl => {this.storageService.state.selectedDocs = exl}, - error => { - console.error(error); - alert('Uh-oh, couldn\'t update the external link'); - }, - () => { // onCompleted - this.router.navigate(['p', this.currentProject._id, 'project-files']); - this.loading = false; - } - ) - } else { - this.linkService.add(formData) - .takeUntil(this.ngUnsubscribe) - .subscribe( - exl => {this.storageService.state.selectedDocs = exl}, - error => { - console.error(error); - alert('Uh-oh, couldn\'t create the external link'); - }, - () => { // onCompleted - this.storageService.state = { type: 'documents', data: this.storageService.state.selectedDocs }; - this.router.navigate(['p', this.currentProject._id, 'project-files']); - this.loading = false; - } - ); - } + const formData = this.updateFormState(); + if (this.documents[0]?._id) { + this.linkService.update(formData, this.documents[0]._id) + .takeUntil(this.ngUnsubscribe) + .subscribe( + exl => {this.storageService.state.selectedDocs = exl}, + error => { + console.error(error); + alert('Uh-oh, couldn\'t update the external link'); + }, + () => { // onCompleted + this.router.navigate(['p', this.currentProject._id, 'project-files']); + this.loading = false; + } + ) + } else { + this.linkService.add(formData) + .takeUntil(this.ngUnsubscribe) + .subscribe( + exl => {this.storageService.state.selectedDocs = exl}, + error => { + console.error(error); + alert('Uh-oh, couldn\'t create the external link'); + }, + () => { // onCompleted + this.storageService.state = { type: 'documents', data: this.storageService.state.selectedDocs }; + this.router.navigate(['p', this.currentProject._id, 'project-files']); + this.loading = false; + } + ); + } } - /** - * Update storage service from current form values. - * - */ - public updateFormState = () => { - const formData = new FormData(); - formData.append('project', this.currentProject._id); - formData.append('externalLink', this.myForm.value.externalLink) - formData.append('displayName', this.myForm.value.displayName); - formData.append('dateAdded', new Date(Number(moment(this.utils.convertFormGroupNGBDateToJSDate(this.myForm.get('dateAdded').value)))).toISOString()); - formData.append('dateUpdated', new Date(Number(moment(this.utils.convertFormGroupNGBDateToJSDate(this.myForm.get('dateUpdated').value)))).toISOString()); - formData.append('description', this.myForm.value.description); - formData.append('projectPhase', this.myForm.value.projectPhase); - formData.append('section', this.myForm.value.section); - formData.append('checkbox', 'false'); + /** + * Update storage service from current form values. + * + */ + public updateFormState = () => { + const formData = new FormData(); + formData.append('project', this.currentProject._id); + formData.append('externalLink', this.myForm.value.externalLink) + formData.append('displayName', this.myForm.value.displayName); + formData.append('dateAdded', new Date(Number(moment(this.utils.convertFormGroupNGBDateToJSDate(this.myForm.get('dateAdded').value)))).toISOString()); + formData.append('dateUpdated', new Date(Number(moment(this.utils.convertFormGroupNGBDateToJSDate(this.myForm.get('dateUpdated').value)))).toISOString()); + formData.append('description', this.myForm.value.description); + formData.append('projectPhase', this.myForm.value.projectPhase); + formData.append('section', this.myForm.value.section); + formData.append('checkbox', 'false'); this.storageService.state = { type: 'form', data: null }; - return formData; - } + return formData; + } /** * Make sure the external link name doesn't include any invalid characters. @@ -189,22 +191,22 @@ export class ExternalLinkComponent implements OnInit, OnDestroy { * @return {void} */ public validateChars() { - this.docNameInvalid = this.myForm.value.displayName.match(/[\/|\\:*?"<>]/g) ? true : false; + this.docNameInvalid = this.myForm.value.displayName.match(/[\/|\\:*?"<>]/g) ? true : false; } - /** + /** * Make sure that the external link is a valid URL to a file. * * @return {void} */ public validateLink() { - const link = this.myForm.value.externalLink.toLowerCase(); - try { - const url = new URL(link); - this.externalLinkInvalid = false; - } catch { - this.externalLinkInvalid = true; - } + const link = this.myForm.value.externalLink; + try { + const url = new URL(link); + this.externalLinkInvalid = false; + } catch { + this.externalLinkInvalid = true; + } } /** @@ -217,19 +219,19 @@ export class ExternalLinkComponent implements OnInit, OnDestroy { this.ngUnsubscribe.complete(); } - /** + /** * If local storage has a previous router location, navigate the user to it, * otherwise, navigate the user to the "project files" view. * * @return {void} */ - goBack() { - if (this.storageService.state.back?.url) { - this.router.navigate(this.storageService.state.back.url); - } else { - this.router.navigate(['/p', this.currentProject._id, 'project-files']); - } - } + goBack() { + if (this.storageService.state.back?.url) { + this.router.navigate(this.storageService.state.back.url); + } else { + this.router.navigate(['/p', this.currentProject._id, 'project-files']); + } + } } diff --git a/src/app/project/project-documents/external-link/link-resolver.services.ts b/src/app/project/project-documents/external-link/link-resolver.services.ts index 82945b99..f88d5c15 100644 --- a/src/app/project/project-documents/external-link/link-resolver.services.ts +++ b/src/app/project/project-documents/external-link/link-resolver.services.ts @@ -6,7 +6,7 @@ import { DocumentSectionService } from 'app/services/documentSection.service'; @Injectable() export class ExternalLinkResolver implements Resolve> { constructor( - private documentSectionService: DocumentSectionService + private documentSectionService: DocumentSectionService ) { } /** @@ -17,7 +17,7 @@ export class ExternalLinkResolver implements Resolve> { * @returns {Observable} */ resolve(route: ActivatedRouteSnapshot): Observable { - const projectId = route.parent.paramMap.get('projId'); - return this.documentSectionService.getAll(projectId);; + const projectId = route.parent.paramMap.get('projId'); + return this.documentSectionService.getAll(projectId);; } } diff --git a/src/app/project/project-documents/project-document-resolver.services.ts b/src/app/project/project-documents/project-document-resolver.services.ts index 5370a285..aa0bb669 100644 --- a/src/app/project/project-documents/project-document-resolver.services.ts +++ b/src/app/project/project-documents/project-document-resolver.services.ts @@ -12,26 +12,26 @@ export class DocumentsResolver implements Resolve> { private storageService: StorageService ) { } - /** - * Retrieves documents or external links - * - * @param {ActivatedRouteSnapshot} route The route to get params from. - * @param {string} schema The schema type to use, either 'Document' or 'ExternalLink' - * @param {string} projectId The project ID of the documents you wish to retrieve - * @returns {Observable} - */ - getFiles = (route: ActivatedRouteSnapshot, schema: string, projectId: string): Observable => { - const tableParams = this.storageService.state.projectDocumentTableParams || null; - const keys = tableParams?.keywords || route.params.keywords || ''; - const dataset = schema; - const fields = [{ 'name': 'project', 'value': projectId }]; - const pageNum = 1; - const pageSize = 1000; - const sortBy = tableParams?.sortBy || route.params.sortBy || '-datePosted'; - const queryModifier = {}; - const populate = true; - return this.searchService.getSearchResults(keys, dataset, fields, pageNum, pageSize, sortBy, queryModifier, populate); - } + /** + * Retrieves documents or external links + * + * @param {ActivatedRouteSnapshot} route The route to get params from. + * @param {string} schema The schema type to use, either 'Document' or 'ExternalLink' + * @param {string} projectId The project ID of the documents you wish to retrieve + * @returns {Observable} + */ + getFiles = (route: ActivatedRouteSnapshot, schema: string, projectId: string): Observable => { + const tableParams = this.storageService.state.projectDocumentTableParams || null; + const keys = tableParams?.keywords || route.params.keywords || ''; + const dataset = schema; + const fields = [{ 'name': 'project', 'value': projectId }]; + const pageNum = 1; + const pageSize = 1000; + const sortBy = tableParams?.sortBy || route.params.sortBy || '-datePosted'; + const queryModifier = {}; + const populate = true; + return this.searchService.getSearchResults(keys, dataset, fields, pageNum, pageSize, sortBy, queryModifier, populate); + } /** * Get route params and make a request to the API to get a set of @@ -42,11 +42,11 @@ export class DocumentsResolver implements Resolve> { */ resolve(route: ActivatedRouteSnapshot): Observable { const projectId = route.parent.paramMap.get('projId'); - const documents = this.getFiles(route, 'Document', projectId); - const externalLinks = this.getFiles(route, 'ExternalLink', projectId); - return forkJoin({ - documents: documents, - externalLinks: externalLinks, - }); + const documents = this.getFiles(route, 'Document', projectId); + const externalLinks = this.getFiles(route, 'ExternalLink', projectId); + return forkJoin({ + documents: documents, + externalLinks: externalLinks, + }); } } diff --git a/src/app/project/project-documents/project-documents.component.scss b/src/app/project/project-documents/project-documents.component.scss index 65e03176..bd1482f6 100644 --- a/src/app/project/project-documents/project-documents.component.scss +++ b/src/app/project/project-documents/project-documents.component.scss @@ -1,13 +1,13 @@ .input-group-prepend { - flex-wrap: wrap; + flex-wrap: wrap; - button { - margin: 0px 10px 10px 0; - } + button { + margin: 0px 10px 10px 0; + } } .main-search-field { - margin-bottom: 12px; + margin-bottom: 12px; } textarea, @@ -44,8 +44,8 @@ input[type="text"] { color: #007bff; border-color: #007bff; i { - margin-right: 4px; - font-size: 20px; + margin-right: 4px; + font-size: 20px; } &:disabled { border-color: #6c757d; diff --git a/src/app/project/project-documents/project-documents.component.ts b/src/app/project/project-documents/project-documents.component.ts index daba60d7..e9d6ae85 100644 --- a/src/app/project/project-documents/project-documents.component.ts +++ b/src/app/project/project-documents/project-documents.component.ts @@ -26,7 +26,7 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { public terms = new SearchTerms(); private ngUnsubscribe: Subject = new Subject(); public documents = null; - public documentVault = null; + public documentVault = null; public loading = true; public navBarButtons: NavBarButton[]; public pageBreadcrumbs: PageBreadcrumb[]; @@ -125,12 +125,12 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { .takeUntil(this.ngUnsubscribe) .subscribe((res: any) => { if (res) { - const documents = res.documents?.documents[0] || []; - const links = res.documents?.externalLinks[0] || []; - const combinedResults = [...documents?.data?.searchResults, ...links?.data?.searchResults]; - this.tableParams.totalListItems = combinedResults?.length || 0; - const sortedResults = this.sortDocuments(combinedResults); - this.documents = this.documentVault = sortedResults; + const documents = res.documents?.documents[0] || []; + const links = res.documents?.externalLinks[0] || []; + const combinedResults = [...documents?.data?.searchResults, ...links?.data?.searchResults]; + this.tableParams.totalListItems = combinedResults?.length || 0; + const sortedResults = this.sortDocuments(combinedResults); + this.documents = this.documentVault = sortedResults; this.setRowData(); this.loading = false; this._changeDetectionRef.detectChanges(); @@ -154,12 +154,12 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { label: 'Upload File(s)', action: () => this.router.navigate(['p', this.currentProject._id, 'project-files', 'upload']) }, - { + { label: 'Link External File', action: () => { - this.storageService.state.selectedDocs = []; - this.router.navigate(['p', this.currentProject._id, 'project-files', 'link']); - } + this.storageService.state.selectedDocs = []; + this.router.navigate(['p', this.currentProject._id, 'project-files', 'link']); + } }, { label: 'File Sections', @@ -169,34 +169,34 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { } - /** - * Sorts documents based on the current sort selection. - * - * @param {any} documents The combined documents, including actual documents and external links. - * @return {any} - * - */ - public sortDocuments = (documents: any[]) => { - const sortData = this.tableParams.sortBy || '-datePosted'; - const sortDir = '-' === Array.from(this.tableParams.sortBy)[0] ? -1 : 1; - const sortBy = sortData.substring(1); - const mappedResults = documents.map(doc => this.mapRowData(doc)); - if ('displayName' === sortBy || 'internalExt' === sortBy) { - // If sorting strings then convert to lower case. - mappedResults.sort((a, b) => { - if (a[sortBy].toLowerCase() < b[sortBy].toLowerCase()) return -1 * sortDir; - if (a[sortBy].toLowerCase() > b[sortBy].toLowerCase()) return 1 * sortDir; - return 0; - }); - } else { - mappedResults.sort((a, b) => { - if (a[sortBy] < b[sortBy]) return -1 * sortDir; - if (a[sortBy] > b[sortBy]) return 1 * sortDir; - return 0; - }); - } - return mappedResults || []; - } + /** + * Sorts documents based on the current sort selection. + * + * @param {any} documents The combined documents, including actual documents and external links. + * @return {any} + * + */ + public sortDocuments = (documents: any[]) => { + const sortData = this.tableParams.sortBy || '-datePosted'; + const sortDir = '-' === Array.from(this.tableParams.sortBy)[0] ? -1 : 1; + const sortBy = sortData.substring(1); + const mappedResults = documents.map(doc => this.mapRowData(doc)); + if ('displayName' === sortBy || 'internalExt' === sortBy) { + // If sorting strings then convert to lower case. + mappedResults.sort((a, b) => { + if (a[sortBy].toLowerCase() < b[sortBy].toLowerCase()) return -1 * sortDir; + if (a[sortBy].toLowerCase() > b[sortBy].toLowerCase()) return 1 * sortDir; + return 0; + }); + } else { + mappedResults.sort((a, b) => { + if (a[sortBy] < b[sortBy]) return -1 * sortDir; + if (a[sortBy] > b[sortBy]) return 1 * sortDir; + return 0; + }); + } + return mappedResults || []; + } /** * Display the snackbar UI component which shows a message to the user. @@ -228,13 +228,13 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { selBox.style.left = '0'; selBox.style.top = '0'; selBox.style.opacity = '0'; - // Create a fetch link if it's an internal file, provide the link if it's an external file. - if (!item.documentFileName.includes('http')) { - const safeName = item.documentFileName.replace(/ /g, '_'); - selBox.value = `${this.pathAPI}/document/${item._id}/fetch/${safeName}`; - } else { - selBox.value = item.documentFileName; - } + // Create a fetch link if it's an internal file, provide the link if it's an external file. + if (!item.documentFileName.includes('http')) { + const safeName = item.documentFileName.replace(/ /g, '_'); + selBox.value = `${this.pathAPI}/document/${item._id}/fetch/${safeName}`; + } else { + selBox.value = item.documentFileName; + } document.body.appendChild(selBox); selBox.focus(); selBox.select(); @@ -282,11 +282,11 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { case 'download': this.documentTableData.data.map((item) => { if (item.checkbox === true) { - if ('external' === item.internalExt) { - window.open(item.documentFileName, "_blank"); - } else { - promises.push(this.api.downloadDocument(this.documents.filter(d => d._id === item._id)[0])); - } + if ('external' === item.internalExt) { + window.open(item.documentFileName); + } else { + promises.push(this.api.downloadDocument(this.documents.filter(d => d._id === item._id)[0])); + } } }); Promise.all(promises).then(() => { @@ -528,45 +528,45 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { */ setRowData(): void { let documentList = []; - // Process stored files/documents + // Process stored files/documents if (this.documents?.length > 0) { this.documents.forEach(document => { - const mappedDoc = this.mapRowData(document); + const mappedDoc = this.mapRowData(document); documentList.push(mappedDoc); }); - } - if (documentList.length > 0) { - this.documentTableData = new TableObject( - DocumentTableRowsComponent, - documentList, - this.tableParams - ); - } + } + if (documentList.length > 0) { + this.documentTableData = new TableObject( + DocumentTableRowsComponent, + documentList, + this.tableParams + ); + } } - /** - * Maps row data to a format that is familiar for the table. - * - * @param {any} file The file to be mapped - * @returns {object} - * - */ - mapRowData(file) { - return { - displayName: file.displayName, - documentFileName: file.documentFileName || file.externalLink || '', - internalSize: file.internalSize || null, - internalExt: file.internalExt || 'external', - datePosted: file.datePosted || file.dateAdded, - status: file.read.includes('public') ? 'Published' : 'Not Published', - _id: file._id, - project: file.project, - read: file.read, - projectPhase: file.projectPhase, - description: file.description, - section: file.section, - } - } + /** + * Maps row data to a format that is familiar for the table. + * + * @param {any} file The file to be mapped + * @returns {object} + * + */ + mapRowData(file) { + return { + displayName: file.displayName, + documentFileName: file.documentFileName || file.externalLink || '', + internalSize: file.internalSize || null, + internalExt: file.internalExt || 'external', + datePosted: file.datePosted || file.dateAdded, + status: file.read.includes('public') ? 'Published' : 'Not Published', + _id: file._id, + project: file.project, + read: file.read, + projectPhase: file.projectPhase, + description: file.description, + section: file.section, + } + } /** * Sort existing results by column(name, date, size, type, etc.) @@ -579,10 +579,10 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { } else { this.tableParams.sortBy = '+' + column; } - window.scrollTo(0, 0); + window.scrollTo(0, 0); this.loading = true; - this.documentVault = this.sortDocuments(this.documentVault); - this.tableTemplateUtils.updateUrl(this.tableParams.sortBy, this.tableParams.currentPage, this.tableParams.pageSize, this.tableParams.filter, this.tableParams.keywords || ''); + this.documentVault = this.sortDocuments(this.documentVault); + this.tableTemplateUtils.updateUrl(this.tableParams.sortBy, this.tableParams.currentPage, this.tableParams.pageSize, this.tableParams.filter, this.tableParams.keywords || ''); this.getPaginatedDocs(this.tableParams.currentPage); } @@ -688,18 +688,18 @@ export class ProjectDocumentsComponent implements OnInit, OnDestroy { * @return {void} */ public getPaginatedDocs(pageNumber: number): void { - window.scrollTo(0, 0); - this.loading = true; - this.tableParams = this.tableTemplateUtils.updateTableParams(this.tableParams, pageNumber, this.tableParams.sortBy); - const startIndex = (pageNumber - 1) * this.tableParams.pageSize; - const endIndex = startIndex + this.tableParams.pageSize; - if (endIndex && 0 < this.documentVault.length) { - this.documents = this.documentVault.slice(startIndex, endIndex); - this.tableTemplateUtils.updateUrl(this.tableParams.sortBy, this.tableParams.currentPage, this.tableParams.pageSize, this.tableParams.filter, this.tableParams.keywords || ''); - this.setRowData(); - this.loading = false; - this._changeDetectionRef.detectChanges(); - } + window.scrollTo(0, 0); + this.loading = true; + this.tableParams = this.tableTemplateUtils.updateTableParams(this.tableParams, pageNumber, this.tableParams.sortBy); + const startIndex = (pageNumber - 1) * this.tableParams.pageSize; + const endIndex = startIndex + this.tableParams.pageSize; + if (endIndex && 0 < this.documentVault.length) { + this.documents = this.documentVault.slice(startIndex, endIndex); + this.tableTemplateUtils.updateUrl(this.tableParams.sortBy, this.tableParams.currentPage, this.tableParams.pageSize, this.tableParams.filter, this.tableParams.keywords || ''); + this.setRowData(); + this.loading = false; + this._changeDetectionRef.detectChanges(); + } } /** diff --git a/src/app/project/project-documents/upload/upload.component.scss b/src/app/project/project-documents/upload/upload.component.scss index 55eb29ab..d35549ee 100644 --- a/src/app/project/project-documents/upload/upload.component.scss +++ b/src/app/project/project-documents/upload/upload.component.scss @@ -23,6 +23,6 @@ @media (max-width: 1200px) { .form-container-65 { width: 100%; - flex-basis: 100%; + flex-basis: 100%; } } \ No newline at end of file diff --git a/src/app/project/project-routing.module.ts b/src/app/project/project-routing.module.ts index e1b714ae..d5ef6906 100644 --- a/src/app/project/project-routing.module.ts +++ b/src/app/project/project-routing.module.ts @@ -103,7 +103,7 @@ const routes: Routes = [ sections: UploadResolver } }, - { + { path: 'project-files/link', component: ExternalLinkComponent, resolve: { @@ -117,7 +117,7 @@ const routes: Routes = [ sections: DocumentEditResolver } }, - { + { path: 'project-files/edit-link', component: ExternalLinkComponent, resolve: { @@ -317,7 +317,7 @@ const routes: Routes = [ CommentPeriodsResolver, DocumentDetailResolver, DocumentsResolver, - ExternalLinkResolver, + ExternalLinkResolver, ProjectUpdatesResolver, DocumentDetailResolver, ProjectResolver, diff --git a/src/app/project/project.module.ts b/src/app/project/project.module.ts index b9b48eba..48a43dd3 100644 --- a/src/app/project/project.module.ts +++ b/src/app/project/project.module.ts @@ -147,7 +147,7 @@ import { LinkService } from 'app/services/link.service'; FileSectionsResolver, DocumentEditResolver, UploadResolver, - LinkService, + LinkService, ] }) diff --git a/src/app/projects/add-edit-project/add-edit-project.component.scss b/src/app/projects/add-edit-project/add-edit-project.component.scss index a7d16823..852b4995 100644 --- a/src/app/projects/add-edit-project/add-edit-project.component.scss +++ b/src/app/projects/add-edit-project/add-edit-project.component.scss @@ -125,13 +125,13 @@ section .form-container-65 { } .flex-column { - flex-direction: column; + flex-direction: column; } @media (max-width: 1200px) { section .form-container-65 { width: 100%; - flex-basis: 100%; + flex-basis: 100%; } } @@ -148,17 +148,17 @@ section .form-container-65 { } .no-margin-right { - margin-right: 0; + margin-right: 0; } @media (max-width: 799px) { - .small-margin-right, .medium-margin-right { - margin-right: 0; - } + .small-margin-right, .medium-margin-right { + margin-right: 0; + } } .medium-margin-bottom { - margin-bottom: 2rem; + margin-bottom: 2rem; } .small-margin-bottom { @@ -166,11 +166,11 @@ section .form-container-65 { } .no-margin-bottom { - margin-bottom: 0; + margin-bottom: 0; } .full-wide { - width: 100%; + width: 100%; } .half-wide { @@ -182,18 +182,18 @@ section .form-container-65 { } .mobile-wrap { - display: flex; - flex-wrap: nowrap; + display: flex; + flex-wrap: nowrap; } @media (max-width: 799px) { - .half-wide, .quarter-wide { - flex-basis: 100%; - } - .mobile-wrap { - flex-wrap: wrap; - flex-basis: 100%; - } + .half-wide, .quarter-wide { + flex-basis: 100%; + } + .mobile-wrap { + flex-wrap: wrap; + flex-basis: 100%; + } } .shape-file-colour-input { @@ -209,23 +209,23 @@ section .form-container-65 { } .save-cancel-buttons { - position: fixed; - z-index: 2000; - right: 1rem; - margin-top: -1.75rem; - background-color: - rgba(255, 255, 255, 0.65); - padding: 1rem; - border-bottom-left-radius: 4px; + position: fixed; + z-index: 2000; + right: 1rem; + margin-top: -1.75rem; + background-color: + rgba(255, 255, 255, 0.65); + padding: 1rem; + border-bottom-left-radius: 4px; } .logos-button { - width: 100%; - margin-right: 0; - margin-top: 10px; - height: 60px; + width: 100%; + margin-right: 0; + margin-top: 10px; + height: 60px; } .z2001 { - z-index: 2001; + z-index: 2001; } diff --git a/src/app/services/api.ts b/src/app/services/api.ts index e37fd94e..462e79f8 100644 --- a/src/app/services/api.ts +++ b/src/app/services/api.ts @@ -1068,7 +1068,7 @@ export class ApiService { return this.http.post(`${this.pathAPI}/${queryString}`, formData, {}); } - /** + /** * Get mutliple individual external links by their respective IDs. * * @param {Array} ids The external link IDs to retrieve by. @@ -1080,7 +1080,7 @@ export class ApiService { 'documentFileName', 'displayName', 'section', - 'externalLink', + 'externalLink', 'dateAdded', 'dateUpdated', 'checkbox', @@ -1093,31 +1093,31 @@ export class ApiService { return this.http.get(`${this.pathAPI}/${queryString}`, {}); } - /** - * Get external link by ID. Return a select set of fields. - * - * @param {string} exLinkId The link ID to get by. - * @returns {Observable} - */ - getLink(exLinkId: string): Observable { - const fields = [ - '_addedBy', - 'displayName', - 'dateAdded', - 'dateUpdated', - 'section', - 'checkbox', - 'project', - 'projectPhase', - 'externalLink', - 'description', - 'read' - ]; - const queryString = `link/${exLinkId}?fields=${this.buildValues(fields)}`; - return this.http.get(`${this.pathAPI}/${queryString}`, {}); - } - - /** + /** + * Get external link by ID. Return a select set of fields. + * + * @param {string} exLinkId The link ID to get by. + * @returns {Observable} + */ + getLink(exLinkId: string): Observable { + const fields = [ + '_addedBy', + 'displayName', + 'dateAdded', + 'dateUpdated', + 'section', + 'checkbox', + 'project', + 'projectPhase', + 'externalLink', + 'description', + 'read' + ]; + const queryString = `link/${exLinkId}?fields=${this.buildValues(fields)}`; + return this.http.get(`${this.pathAPI}/${queryString}`, {}); + } + + /** * Get all external links by project. * * @param {string} projectId The project ID to get external links for. @@ -1126,67 +1126,67 @@ export class ApiService { getLinks(projectId: string): Observable { const fields = [ '_addedBy', - 'displayName', - 'dateAdded', - 'dateUpdated', - 'section', - 'checkbox', - 'project', - 'projectPhase', - 'externalLink', - 'description', - 'read' + 'displayName', + 'dateAdded', + 'dateUpdated', + 'section', + 'checkbox', + 'project', + 'projectPhase', + 'externalLink', + 'description', + 'read' ]; const queryString = `link/${projectId}?fields=${this.buildValues(fields)}`; return this.http.get(`${this.pathAPI}/${queryString}`, {}); } - /** - * Update a link with edited form data. - * - * @param {FormData} formData The form data to update the link with. - * @param {string} _id The link to udpate. - * @returns {Observable} - */ - updateLink(formData: FormData, exLinkId: any): Observable { - const queryString = `link/${exLinkId}`; - return this.http.put(`${this.pathAPI}/${queryString}`, formData, {}); - } - - /** - * Delete an external link. - * - * @param {ExternalLink} link The link to delete. - * @returns {Observable} - */ - deleteLink(link: ExternalLink): Observable { - const queryString = `link/${link._id}`; - return this.http.delete(`${this.pathAPI}/${queryString}`, {}); - } - - /** - * Publish a link by toggling its visibility to "public" app users. - * - * @param {string} exLinkId The link ID to publish. - * @returns {Observable} - */ - publishLink(exLinkId: String): Observable { - const queryString = `link/${exLinkId}/publish`; - return this.http.put(`${this.pathAPI}/${queryString}`, {}, {}); - } - - /** - * Unpublish a link by toggling its visibility to "public" app users. - * - * @param {string} exLinkId The link ID to unpublish. - * @returns {Observable} - */ - unPublishLink(exLinkId: String): Observable { - const queryString = `link/${exLinkId}/unpublish`; - return this.http.put(`${this.pathAPI}/${queryString}`, {}, {}); - } - - /** + /** + * Update a link with edited form data. + * + * @param {FormData} formData The form data to update the link with. + * @param {string} _id The link to udpate. + * @returns {Observable} + */ + updateLink(formData: FormData, exLinkId: any): Observable { + const queryString = `link/${exLinkId}`; + return this.http.put(`${this.pathAPI}/${queryString}`, formData, {}); + } + + /** + * Delete an external link. + * + * @param {ExternalLink} link The link to delete. + * @returns {Observable} + */ + deleteLink(link: ExternalLink): Observable { + const queryString = `link/${link._id}`; + return this.http.delete(`${this.pathAPI}/${queryString}`, {}); + } + + /** + * Publish a link by toggling its visibility to "public" app users. + * + * @param {string} exLinkId The link ID to publish. + * @returns {Observable} + */ + publishLink(exLinkId: String): Observable { + const queryString = `link/${exLinkId}/publish`; + return this.http.put(`${this.pathAPI}/${queryString}`, {}, {}); + } + + /** + * Unpublish a link by toggling its visibility to "public" app users. + * + * @param {string} exLinkId The link ID to unpublish. + * @returns {Observable} + */ + unPublishLink(exLinkId: String): Observable { + const queryString = `link/${exLinkId}/unpublish`; + return this.http.put(`${this.pathAPI}/${queryString}`, {}, {}); + } + + /** * Add a new link. * * @param {FormData} formData The form data to upload the link with. diff --git a/src/app/services/link.service.ts b/src/app/services/link.service.ts index 4ebb6701..a960f243 100644 --- a/src/app/services/link.service.ts +++ b/src/app/services/link.service.ts @@ -9,26 +9,26 @@ import { ExternalLink } from 'app/models/externalLink'; export class LinkService { constructor(private api: ApiService) { } - /** - * Get multiple external links by their ids. - * - * @param {Array} ids The document IDs to get with. - * @returns {Observable} - */ - getByMultiId(ids: Array): Observable> { - return this.api.getExternalLinksByMultiId(ids) - .map(res => { - if (res && res.length > 0) { - let exLinks = []; - res.forEach(exl => { - exLinks.push(new ExternalLink(exl)); - }); - return exLinks; - } - return null; - }) - .catch(error => this.api.handleError(error)); - } + /** + * Get multiple external links by their ids. + * + * @param {Array} ids The document IDs to get with. + * @returns {Observable} + */ + getByMultiId(ids: Array): Observable> { + return this.api.getExternalLinksByMultiId(ids) + .map(res => { + if (res && res.length > 0) { + let exLinks = []; + res.forEach(exl => { + exLinks.push(new ExternalLink(exl)); + }); + return exLinks; + } + return null; + }) + .catch(error => this.api.handleError(error)); + } /** * Get a specific link by its id. Return only the first link if multiple somehow come back. @@ -49,7 +49,7 @@ export class LinkService { ); } - /** + /** * Get all links from a single project. * * @param {string} currentProjectId The project to get links for. @@ -58,12 +58,12 @@ export class LinkService { public getAll(currentProjectId: string): Observable { return this.api.getLinks(currentProjectId) .map((res: any) => { - if (!res || res.length === 0) { - return []; - } else { - return res.map(link => new ExternalLink(link)); + if (!res || res.length === 0) { + return []; + } else { + return res.map(link => new ExternalLink(link)); } - }) + }) .catch(error => this.api.handleError(error)); } From 607675c581d538a16d2e3c2c3b4c2c505e4f7f5d Mon Sep 17 00:00:00 2001 From: Jareth Whitney Date: Tue, 14 Jan 2025 16:35:21 -0800 Subject: [PATCH 3/3] feature/deseng751: Removed console.log. --- .../comment-period-details-tab.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/project/comment-period/comment-period-details-tabs/comment-period-details-tab.component.ts b/src/app/project/comment-period/comment-period-details-tabs/comment-period-details-tab.component.ts index 8c871b8e..f1efe4eb 100644 --- a/src/app/project/comment-period/comment-period-details-tabs/comment-period-details-tab.component.ts +++ b/src/app/project/comment-period/comment-period-details-tabs/comment-period-details-tab.component.ts @@ -274,7 +274,6 @@ export class CommentPeriodDetailsTabComponent implements OnInit, OnChanges, OnDe * @returns {Promise} */ public downloadDocument(document) { - console.log('ran download document code'); return document.externalLink ? window.open(document.externalLink) : this.api.downloadDocument(document); }