opf/openproject

View on GitHub
frontend/src/app/core/state/file-links/file-links.service.ts

Summary

Maintainability
B
4 hrs
Test Coverage
// -- copyright
// OpenProject is an open source project management software.
// Copyright (C) 2012-2024 the OpenProject GmbH
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//
// See COPYRIGHT and LICENSE files for more details.
//++

import { applyTransaction } from '@datorama/akita';
import { Injectable } from '@angular/core';
import { HttpHeaders } from '@angular/common/http';
import {
  from,
  Observable,
  of,
} from 'rxjs';
import {
  groupBy,
  mergeMap,
  reduce,
  switchMap,
  tap,
} from 'rxjs/operators';

import {
  IFileLink,
  IFileLinkOriginData,
} from 'core-app/core/state/file-links/file-link.model';
import { IHALCollection } from 'core-app/core/apiv3/types/hal-collection.type';
import { FileLinksStore } from 'core-app/core/state/file-links/file-links.store';
import {
  insertCollectionIntoState,
  removeEntityFromCollectionAndState,
} from 'core-app/core/state/resource-store';
import {
  ResourceStore,
  ResourceStoreService,
} from 'core-app/core/state/resource-store.service';
import { IHalResourceLink } from 'core-app/core/state/hal-resource';
import idFromLink from 'core-app/features/hal/helpers/id-from-link';

@Injectable()
export class FileLinksResourceService extends ResourceStoreService<IFileLink> {
  protected createStore():ResourceStore<IFileLink> {
    return new FileLinksStore();
  }

  protected basePath():string {
    return this.apiV3Service.file_links.path;
  }

  updateCollectionsForWorkPackage(fileLinksSelfLink:string):Observable<IFileLink[]> {
    return this.http
      .get<IHALCollection<IFileLink>>(fileLinksSelfLink)
      .pipe(
        tap((collection) => insertCollectionIntoState(this.store, collection, fileLinksSelfLink)),
        switchMap((collection) => from(collection._embedded.elements)),
        groupBy(
          (fileLink) => fileLink._links.storage.href,
          (fileLink) => fileLink,
        ),
        mergeMap((group$) => {
          const seed = { storage: group$.key, fileLinks: [] as IFileLink[] };
          return group$.pipe(reduce((acc, fileLink) => {
            acc.fileLinks = [...acc.fileLinks, fileLink];
            return acc;
          }, seed));
        }),
        tap((fileLinkCollections) => {
          const storageId = idFromLink(fileLinkCollections.storage);
          const collectionKey = `${fileLinksSelfLink}?filters=[{"storage":{"operator":"=","values":["${storageId}"]}}]`;
          const collection = { _embedded: { elements: fileLinkCollections.fileLinks } } as IHALCollection<IFileLink>;
          insertCollectionIntoState(this.store, collection, collectionKey);
        }),
        reduce((acc, group) => acc.concat(group.fileLinks), [] as IFileLink[]),
      );
  }

  updateCollection(href:string):Observable<IHALCollection<IFileLink>> {
    return this.http
      .get<IHALCollection<IFileLink>>(href)
      .pipe(
        tap((collection) => insertCollectionIntoState(this.store, collection, href)),
      );
  }

  remove(collectionKey:string, fileLink:IFileLink):Observable<void> {
    if (!fileLink._links.delete) {
      return of();
    }

    const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
    return this.http
      .delete<void>(fileLink._links.delete.href, { withCredentials: true, headers })
      .pipe(
        tap(() => removeEntityFromCollectionAndState(this.store, fileLink.id, collectionKey)),
      );
  }

  addFileLinks(
    collectionKey:string,
    addFileLinksHref:string,
    storage:IHalResourceLink,
    filesToLink:IFileLinkOriginData[],
  ):Observable<IHALCollection<IFileLink>> {
    const elements = filesToLink.map((file) => ({
      originData: { ...file },
      _links: { storage },
    }));

    return this.http
      .post<IHALCollection<IFileLink>>(addFileLinksHref, { _type: 'Collection', _embedded: { elements } })
      .pipe(
        tap((collection) => {
          applyTransaction(() => {
            const newFileLinks = collection._embedded.elements;
            this.store.add(newFileLinks);
            this.store.update(
              ({ collections }) => (
                {
                  collections: {
                    ...collections,
                    [collectionKey]: {
                      ...collections[collectionKey],
                      ids: (collections[collectionKey]?.ids || []).concat(newFileLinks.map((link) => link.id)),
                    },
                  },
                }
              ),
            );
          });
        }),
      );
  }
}