From 772db5ad4569704e7cf48a709d4fb712b315e80a Mon Sep 17 00:00:00 2001 From: Luisa Date: Wed, 26 Apr 2023 17:46:33 +0200 Subject: [PATCH 01/16] Implemented backend imports until json upload --- .../topics/topic-repository.service.ts | 6 + .../repositories/topics/topic.action.ts | 2 + .../utils/import/import-utils.ts | 4 + .../base-via-backend-import.service.ts | 732 ++++++++++++++++++ .../base-via-backend-import-list.component.ts | 64 ++ .../topic-import/topic-import.component.html | 8 +- .../topic-import/topic-import.component.ts | 4 +- .../topic-import.service.ts | 45 +- client/src/app/ui/base/import-service.ts | 22 + .../via-backend-import-list.component.html | 335 ++++++++ .../via-backend-import-list.component.scss | 63 ++ .../via-backend-import-list.component.spec.ts | 23 + .../via-backend-import-list.component.ts | 522 +++++++++++++ .../definitions/import-via-backend-preview.ts | 62 ++ .../modules/import-list/import-list.module.ts | 4 +- 15 files changed, 1871 insertions(+), 25 deletions(-) create mode 100644 client/src/app/site/base/base-import.service/base-via-backend-import.service.ts create mode 100644 client/src/app/site/base/base-via-backend-import-list.component.ts create mode 100644 client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html create mode 100644 client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.scss create mode 100644 client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.spec.ts create mode 100644 client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts create mode 100644 client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts diff --git a/client/src/app/gateways/repositories/topics/topic-repository.service.ts b/client/src/app/gateways/repositories/topics/topic-repository.service.ts index cb1c5452ac..9495003e08 100644 --- a/client/src/app/gateways/repositories/topics/topic-repository.service.ts +++ b/client/src/app/gateways/repositories/topics/topic-repository.service.ts @@ -2,7 +2,9 @@ import { Injectable } from '@angular/core'; import { Identifiable } from 'src/app/domain/interfaces'; import { Topic } from 'src/app/domain/models/topics/topic'; import { ViewAgendaItem, ViewTopic } from 'src/app/site/pages/meetings/pages/agenda'; +import { ImportViaBackendJSONUploadResponse } from 'src/app/ui/modules/import-list/definitions/import-via-backend-preview'; +import { Action } from '../../actions'; import { createAgendaItem } from '../agenda'; import { AgendaItemRepositoryService } from '../agenda/agenda-item-repository.service'; import { BaseAgendaItemAndListOfSpeakersContentObjectRepository } from '../base-agenda-item-and-list-of-speakers-content-object-repository'; @@ -41,6 +43,10 @@ export class TopicRepositoryService extends BaseAgendaItemAndListOfSpeakersConte return this.sendBulkActionToBackend(TopicAction.DELETE, payload); } + public jsonUpload(payload: { [key: string]: any }): Action { + return this.createAction(TopicAction.JSON_UPLOAD, payload); + } + public getTitle = (topic: ViewTopic) => topic.title; public override getListTitle = (topic: ViewTopic) => { diff --git a/client/src/app/gateways/repositories/topics/topic.action.ts b/client/src/app/gateways/repositories/topics/topic.action.ts index 46e95ea0a5..76d1314d4a 100644 --- a/client/src/app/gateways/repositories/topics/topic.action.ts +++ b/client/src/app/gateways/repositories/topics/topic.action.ts @@ -2,4 +2,6 @@ export class TopicAction { public static readonly CREATE = `topic.create`; public static readonly UPDATE = `topic.update`; public static readonly DELETE = `topic.delete`; + public static readonly JSON_UPLOAD = `topic.json_upload`; + public static readonly IMPORT = `topic.import`; } diff --git a/client/src/app/infrastructure/utils/import/import-utils.ts b/client/src/app/infrastructure/utils/import/import-utils.ts index 9330506555..7c88a37f1d 100644 --- a/client/src/app/infrastructure/utils/import/import-utils.ts +++ b/client/src/app/infrastructure/utils/import/import-utils.ts @@ -72,6 +72,10 @@ export type ImportConfig = StaticMainImportConfig
{ + modelHeadersAndVerboseNames: K; +} + export const DUPLICATE_IMPORT_ERROR = `Duplicates`; export interface CsvValueParsingConfig { diff --git a/client/src/app/site/base/base-import.service/base-via-backend-import.service.ts b/client/src/app/site/base/base-import.service/base-via-backend-import.service.ts new file mode 100644 index 0000000000..95db53050a --- /dev/null +++ b/client/src/app/site/base/base-import.service/base-via-backend-import.service.ts @@ -0,0 +1,732 @@ +import { Directive } from '@angular/core'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { TranslateService } from '@ngx-translate/core'; +import { Papa, ParseConfig } from 'ngx-papaparse'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { Identifiable } from 'src/app/domain/interfaces'; +import { + FileReaderProgressEvent, + ValueLabelCombination, + ViaBackendImportConfig +} from 'src/app/infrastructure/utils/import/import-utils'; +import { ViaBackendImportService } from 'src/app/ui/base/import-service'; +import { ImportViaBackendPhase } from 'src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component'; +import { + ImportViaBackendJSONUploadResponse, + ImportViaBackendPreview, + ImportViaBackendPreviewRow +} from 'src/app/ui/modules/import-list/definitions/import-via-backend-preview'; + +import { ImportServiceCollectorService } from '../../services/import-service-collector.service'; + +@Directive() +export abstract class BaseViaBackendImportService + implements ViaBackendImportService +{ + public chunkSize = 100; + + /** + * List of possible errors and their verbose explanation + */ + public errorList: { [errorKey: string]: string } = {}; + + /** + * The headers expected in the CSV matching import properties (in order) + */ + public expectedHeaders: string[] = []; + + /** + * The minimimal number of header entries needed to successfully create an entry + */ + public requiredHeaderLength = 2; + + /** + * The used column separator. If left on an empty string (default), + * the papaparse parser will automatically decide on separators. + */ + public columnSeparator = ``; + + /** + * The used text separator. + */ + public textSeparator = `"`; + + /** + * The encoding used by the FileReader object. + */ + public encoding = `utf-8`; + + /** + * List of possible encodings and their label. values should be values accepted + * by the FileReader API + */ + public encodings: ValueLabelCombination[] = [ + { value: `utf-8`, label: `UTF 8 - Unicode` }, + { value: `iso-8859-1`, label: `ISO 8859-1 - West European` }, + { value: `iso-8859-15`, label: `ISO 8859-15 - West European (with €)` } + ]; + + /** + * List of possible column separators to pass on to papaParse + */ + public columnSeparators: ValueLabelCombination[] = [ + { label: `Comma`, value: `,` }, + { label: `Semicolon`, value: `;` }, + { label: `Automatic`, value: `` } + ]; + + /** + * List of possible text separators to pass on to papaParse. Note that + * it cannot automatically detect textseparators (value must not be an empty string) + */ + public textSeparators: ValueLabelCombination[] = [ + { label: `Double quotes (")`, value: `"` }, + { label: `Single quotes (')`, value: `'` }, + { label: `Gravis (\`)`, value: `\`` } + ]; + + public get rawFileObservable(): Observable { + return this._rawFileSubject.asObservable(); + } + + public get previewActionIds(): number[] { + return this._preview.results.map(result => result.id); + } + + public get preview(): ImportViaBackendPreview[] { + return this._preview.results; + } + + /** + * storing the summary preview for the import, to avoid recalculating it + * at each display change. + */ + private _preview: ImportViaBackendJSONUploadResponse | null = null; + + private _modelHeadersAndVerboseNames: { [key: string]: string } = {}; + + protected readonly translate: TranslateService = this.importServiceCollector.translate; + protected readonly matSnackbar: MatSnackBar = this.importServiceCollector.matSnackBar; + + protected readonly isMeetingImport: boolean = false; + + /** + * The last parsed file object (may be reparsed with new encoding, thus kept in memory) + */ + private _rawFile: File | null = null; + + private _rawFileSubject = new BehaviorSubject(null); + + /** + * FileReader object for file import + */ + private _reader = new FileReader(); + + private _currentImportPhaseSubject = new BehaviorSubject( + ImportViaBackendPhase.LOADING_PREVIEW + ); + + /** + * the list of parsed models that have been extracted from the opened file + */ + private _csvLines: { [header: string]: string }[] = []; + private _receivedHeaders: string[] = []; + private _mapReceivedExpectedHeaders: { [expectedHeader: string]: string } = {}; + private readonly _papa: Papa = this.importServiceCollector.papa; + + /** + * Constructor. Creates a fileReader to subscribe to it for incoming parsed + * strings + */ + public constructor(private importServiceCollector: ImportServiceCollectorService) { + this._reader.onload = (event: FileReaderProgressEvent) => { + this.parseInput(event.target?.result as string); + }; + this.init(); + } + + /** + * Parses the data input. Expects a string as returned by via a File.readAsText() operation + * + * @param file + */ + public parseInput(file: string): void { + this.init(); + this.clearPreview(); + const papaConfig: ParseConfig = { + header: true, + skipEmptyLines: `greedy`, + quoteChar: this.textSeparator, + transform: (value, columnOrHeader) => (!value ? undefined : value) + }; + if (this.columnSeparator) { + papaConfig.delimiter = this.columnSeparator; + } + const result = this._papa.parse(file, papaConfig); + this._csvLines = result.data; + console.log(`NEW CSV LINES`, result, this._csvLines); + this.parseCsvLines(); + } + + public clearFile(): void { + this.setParsedEntries({}); + this._rawFile = null; + this._rawFileSubject.next(null); + } + + public addLines(...lines: { [header: string]: any }[]): void { + for (const line of lines) { + this._csvLines.push(line); + } + this.parseCsvLines(); + } + + /** + * counts the amount of duplicates that have no decision on the action to + * be taken + */ + public updatePreview(): void { + // TODO: implement! + // this._preview = summary; + } + + public updateSummary(): void { + // TODO: implement + // this._importingStepsSubject.next(this.getEveryImportHandler()); + } + + /** + * Handler after a file was selected. Basic checking for type, then hand + * over to parsing + * + * @param event type is Event, but has target.files, which typescript doesn't seem to recognize + */ + public onSelectFile(event: any): void { + if (event.target.files && event.target.files.length === 1) { + this._rawFile = event.target.files[0]; + this._rawFileSubject.next(this._rawFile); + this.readFile(); + } + } + + /** + * Rereads the (previously selected) file, if present. Thought to be triggered + * by parameter changes on encoding, column, text separators + */ + public refreshFile(): void { + if (this._rawFile) { + this.readFile(); + } + } + + /** + * Resets the data and preview (triggered upon selecting an invalid file) + */ + public clearPreview(): void { + // this.getEveryImportHandler().forEach(handler => handler.doCleanup()); + this.setNextEntries({}); + // this._lostHeaders = { expected: {}, received: [] }; + this._preview = null; + this._currentImportPhaseSubject.next(ImportViaBackendPhase.LOADING_PREVIEW); + // this._isImportValidSubject.next(false); + } + + /** + * set a list of short names for error, indicating which column failed + */ + private getVerboseError(error: string): string { + return this.errorList[error] ?? error; + } + + /** + * Get an extended error description. + * + * @param error + * @returns the extended error desription for that error + */ + public verbose(error: string): string { + return this.errorList[error] || error; + } + + /** + * Queries if a given error is present in the given entry + * + * @param entry the entry to check for the error. + * @param error The error to check for + * @returns true if the error is present + */ + public hasError(entry: ImportViaBackendPreviewRow, error: string): boolean { + return entry.error.includes(error); // TODO: implement properly! + } + + /** + * Executing the import. Creates all secondary data, maps the newly created + * secondary data to the new entries, then creates all entries without errors + * by submitting them to the server. The entries will receive the status + * 'done' on success. + */ + public async doImport(): Promise { + this._currentImportPhaseSubject.next(ImportViaBackendPhase.IMPORTING); + // await this.doBeforeImport(); + + // for (const handler of this.getEveryMainImportHandler()) { + // handler.startImport(); + // await handler.doImport(); + // handler.finishImport(); + // } + + // await this.doAfterImport(); + + // TODO: implement! + + this._currentImportPhaseSubject.next(ImportViaBackendPhase.FINISHED); + this.updatePreview(); + } + + private setNextEntries(nextEntries: { [importTrackId: number]: ImportViaBackendPreviewRow }): void { + // this._newEntries.next(nextEntries); + // TODO: implement properly! + this.updatePreview(); + } + + private parseCsvLines(): void { + this._receivedHeaders = Object.keys(this._csvLines[0]); + const isValid = this.checkHeaderLength(); + // this.checkReceivedHeaders(); + if (!isValid) { + return; + } + this.propagateNextNewEntries(); + this.updateSummary(); + } + + /** + * parses pre-prepared entries (e.g. from a textarea) instead of a csv structure + * + * @param entries: an array of prepared newEntry objects + */ + private setParsedEntries(entries: { [importTrackId: number]: ImportViaBackendPreviewRow }): void { + this.clearPreview(); + if (!entries) { + return; + } + this.setNextEntries(entries); + } + + private init(): void { + // TODO: implement correctly! + const config = this.getConfig(); + this.expectedHeaders = Object.keys(config.modelHeadersAndVerboseNames); + this._modelHeadersAndVerboseNames = config.modelHeadersAndVerboseNames; + // this._getDuplicatesFn = config.getDuplicatesFn; + // this._requiredFields = config.requiredFields || []; + // this.initializeImportHelpers(); + } + + /** + * reads the _rawFile + */ + private readFile(): void { + if (this._rawFile) { + this._reader.readAsText(this._rawFile, this.encoding); + } + } + + /** + * Checks the first line of the csv (the header) for consistency (length) + * + * @returns true if the line has at least the minimum amount of columns + */ + private checkHeaderLength(): boolean { + // TODO: could probably be shortened! + const snackbarDuration = 3000; + if (this._receivedHeaders.length < this.requiredHeaderLength) { + this.matSnackbar.open(this.translate.instant(`The file has too few columns to be parsed properly.`), ``, { + duration: snackbarDuration + }); + + this.clearPreview(); + return false; + } else if (this._receivedHeaders.length < this.expectedHeaders.length) { + this.matSnackbar.open( + this.translate.instant(`The file seems to have some ommitted columns. They will be considered empty.`), + ``, + { duration: snackbarDuration } + ); + } else if (this._receivedHeaders.length > this.expectedHeaders.length) { + this.matSnackbar.open( + this.translate.instant(`The file seems to have additional columns. They will be ignored.`), + ``, + { duration: snackbarDuration } + ); + } + return true; + } + + private async propagateNextNewEntries(): Promise { + const payload = this.calculateJsonUploadPayload(); + const rawPreview = await this.jsonUpload(payload); + console.log(`UPLOADED JSON =>`, rawPreview); + // TODO: implement rest. I.e. actually save the result! + // const rawEntries = this._csvLines.map((line, i) => this.createRawImportModel(line, i + 1)); + // await this.onBeforeCreatingImportModels(rawEntries); + // for (let entry of rawEntries) { + // const nextEntry = await this.createImportModel(entry); + // this.pushNextNewEntry(nextEntry); + // } + // for (const importHandler of this.getEveryImportHandler()) { + // if (hasBeforeFindAction(importHandler)) { + // await importHandler.onBeforeFind(this.importModels); + // } + // } + // this.importModels.forEach(importModel => this.mapData(importModel)); + // for (const importHandler of this.getEveryImportHandler()) { + // importHandler.pipeModels(this.importModels); + // } + // this.checkImportValidness(); + } + + protected abstract jsonUpload(payload: { + [key: string]: any; + }): Promise; + + protected calculateJsonUploadPayload(): { [key: string]: any } { + return { + data: this._csvLines + }; + } + + // Abstract methods + public abstract downloadCsvExample(): void; + protected abstract getConfig(): ViaBackendImportConfig; + + /** + * Emits an error string to display if a file import cannot be done + */ + // public errorEvent = new EventEmitter(); + + // public get leftReceivedHeaders(): string[] { + // return this._lostHeaders.received; + // } + + // public get leftExpectedHeaders(): { [key: string]: string } { + // return this._lostHeaders.expected; + // } + + // public get headerValues(): { [header: string]: string } { + // return this._mapReceivedExpectedHeaders; + // } + + // private _requiredFields: (keyof MainModel)[] = []; + // private _lostHeaders: { expected: { [header: string]: string }; received: string[] } = { + // expected: {}, + // received: [] + // }; + + // private pushNextNewEntry(nextEntry: ImportViaBackendPreviewRow): void { + // const oldEntries = this._newEntries.value; + // oldEntries[nextEntry.id] = nextEntry; + // this.setNextEntries(oldEntries); + // } + + /** + * a subscribable representation of the new items to be imported + * + * @returns an observable BehaviorSubject + */ + // public getNewEntriesObservable(): Observable { + // return this._newEntries.asObservable().pipe(map(value => Object.values(value))); + // } + + // private async doBeforeImport(): Promise { + // for (const { mainHandler } of Object.values(this._beforeImportHandler)) { + // await mainHandler.doImport(); + // } + // } + + // private async doAfterImport(): Promise { + // for (const { mainHandler, additionalHandlers } of Object.values(this._afterImportHandler)) { + // await mainHandler.doImport(); + // for (const handler of additionalHandlers) { + // handler.pipeImportedSideModels(mainHandler.getModelsToCreate()); + // await handler.doImport(); + // } + // } + // } + + // public setNewHeaderValue(updateMapReceivedExpectedHeaders: { [headerKey: string]: string }): void { + // for (const headerKey of Object.keys(updateMapReceivedExpectedHeaders)) { + // this._mapReceivedExpectedHeaders[headerKey] = updateMapReceivedExpectedHeaders[headerKey]; + // delete this._lostHeaders.expected[headerKey]; + // this.leftReceivedHeaders.splice( + // this.leftReceivedHeaders.findIndex(header => header === updateMapReceivedExpectedHeaders[headerKey]), + // 1 + // ); + // } + // this.checkImportValidness(); + // this.propagateNextNewEntries(); + // } + + /** + * A helper function to specify import-helpers for `ToCreate`. + * Should be overriden to specify the import-helpers. + * + * @returns A map containing import-helpers for specific attributes of `ToCreate`. + */ + // protected getBeforeImportHelpers(): { [key: string]: BeforeImportHandler } { + // return {}; + // } + + // protected pipeParseValue(_value: string, _header: keyof MainModel): any {} + + // protected registerBeforeImportHandler( + // header: string, + // handler: StaticBeforeImportConfig | BaseBeforeImportHandler + // ): void { + // if (handler instanceof BaseBeforeImportHandler) { + // this._beforeImportHandler[header] = { mainHandler: handler }; + // } else { + // this._beforeImportHandler[header] = { + // mainHandler: new StaticBeforeImportHandler(handler, key => this.translate.instant(key)) + // }; + // } + // } + + // protected registerAfterImportHandler( + // header: string, + // handler: StaticAfterImportConfig | BaseAfterImportHandler, + // additionalHandlers?: ( + // | StaticAdditionalImportHandlerConfig + // | BaseAdditionalImportHandler + // )[] + // ): void { + // const getAfterImportHandler = ( + // _handler: + // | StaticAdditionalImportHandlerConfig + // | BaseAdditionalImportHandler + // ) => { + // if (_handler instanceof BaseAdditionalImportHandler) { + // return _handler; + // } else { + // return new StaticAdditionalImportHandler({ + // ..._handler, + // translateFn: key => this.translate.instant(key) + // }); + // } + // }; + // const _additionalHandlers = (additionalHandlers ?? []).map(_handler => getAfterImportHandler(_handler)); + // if (handler instanceof BaseAfterImportHandler) { + // this._afterImportHandler[header] = { mainHandler: handler, additionalHandlers: _additionalHandlers }; + // } else { + // this._afterImportHandler[header] = { + // mainHandler: new StaticAfterImportHandler( + // handler, + // header as keyof MainModel, + // toTranslate => this.translate.instant(toTranslate) + // ), + // additionalHandlers: _additionalHandlers + // }; + // } + // } + + // protected registerMainImportHandler( + // handler: StaticMainImportConfig | BaseMainImportHandler + // ): void { + // if (handler instanceof BaseMainImportHandler) { + // this._otherMainImportHelper.push(handler); + // } else { + // this._otherMainImportHelper.push( + // new StaticMainImportHandler({ + // translateFn: key => this.translate.instant(key), + // resolveEntryFn: importModel => this.resolveEntry(importModel), + // ...handler + // }) + // ); + // } + // } + + // protected async onCreateImportModel(input: RawImportModel): Promise> { + // if (!this._getDuplicatesFn) { + // throw new Error(`No function to check for duplicates defined`); + // } + // const duplicates = await this._getDuplicatesFn(input); + // const hasDuplicates = duplicates.length > 0; + // const entry: ImportViaBackendPreviewRow = new ImportModel({ + // model: input.model as MainModel, + // id: input.id, + // hasDuplicates, + // duplicates + // }); + // return entry; + // } + + /** + * This function pipes received rows from a csv file already mapped to their internal used data structure. + * This is done, before import models are created from those rows. Thus, this function facilitates to decide + * how import models are created depending on the rows in the csv file. + * + * @param _entries + */ + // protected async onBeforeCreatingImportModels(_entries: RawImportModel[]): Promise {} + + // private async createImportModel( + // input: RawImportModel, + // errors: string[] = [] + // ): Promise> { + // const nextEntry = await this.onCreateImportModel(input); + // if (nextEntry.hasDuplicates) { + // errors.push(DUPLICATE_IMPORT_ERROR); + // } + // if (errors.length) { + // nextEntry.errors = errors.map(error => this.getVerboseError(error)); + // nextEntry.status = `error`; + // } + // return nextEntry; + // } + + /** + * Maps the value in one csv line for every header to the header, which is later used for models that will be created or updated. + * These headers are specified in `_mapReceivedExpectedHeader`. + * + * @param line a csv line + * + * @returns an object which has the headers of the models used internal + */ + // private createRawImportModel(line: CsvJsonMapping, index: number): RawImportModel { + // const rawObject = Object.keys(this._mapReceivedExpectedHeaders).mapToObject(expectedHeader => { + // const receivedHeader = this._mapReceivedExpectedHeaders[expectedHeader]; + // return { [expectedHeader]: line[receivedHeader] }; + // }); + // return { + // id: index, + // model: rawObject as MainModel + // }; + // } + + /** + * Maps incoming data of probably manual typed headers and values into headers, used by the rest of an import + * process. + * + * @param line An incoming header <-> value map + * @param importTrackId The number of an import object + * + * @returns A new model which values are linked to any helpers if needed. + */ + // private mapData(importModel: ImportViaBackendPreviewRow): void { + // const rawObject = importModel.data; + // const errors = []; + // for (const expectedHeader of Object.keys(this._mapReceivedExpectedHeaders)) { + // // const handler = this._beforeImportHandler[expectedHeader] || this._afterImportHandler[expectedHeader]; + // const csvValue = rawObject[expectedHeader as keyof MainModel] as any; + // try { + // const value = this.parseCsvValue(csvValue, { + // header: expectedHeader as keyof MainModel, + // importModel: importModel, + // importFindHandler: handler?.mainHandler, + // allImportModels: this.importModels + // }); + // rawObject[expectedHeader as keyof MainModel] = value; + // } catch (e) { + // console.debug(`Error while parsing ${expectedHeader}\n`, e); + // errors.push((e as any).message); + // rawObject[expectedHeader as keyof MainModel] = csvValue; + // } + // } + // // importModel.errors = importModel.errors.concat(errors); + // } + + // private getSelfImportHelper(): MainImportHandler { + // return this._selfImportHelper!; + // } + + // private initializeImportHelpers(): void { + // const { createFn, updateFn, getDuplicatesFn, verboseNameFn, shouldCreateModelFn } = this.getConfig(); + // const handlers = Object.entries(this.getBeforeImportHelpers()).mapToObject(([key, value]) => ({ + // [key]: { mainHandler: value } + // })); + // this._beforeImportHandler = { ...this._beforeImportHandler, ...handlers }; + // this._selfImportHelper = new StaticMainImportHandler({ + // verboseNameFn, + // getDuplicatesFn, + // shouldCreateModelFn, + // createFn, + // updateFn, + // translateFn: key => this.translate.instant(key), + // resolveEntryFn: importModel => this.resolveEntry(importModel) + // }); + // this.updateSummary(); + // } + + // private resolveEntry(entry: ImportViaBackendPreviewRow): MainModel { + // let model = { ...entry.newEntry } as MainModel; + // for (const key of Object.keys(this._beforeImportHandler)) { + // const { mainHandler: handler } = this._beforeImportHandler[key]; + // const result = handler.doResolve(model, key); + // model = result.model; + // if (result.unresolvedModels) { + // entry.errors = (entry.errors ?? []).concat(this.getVerboseError(result.verboseName as string)); + // this.updatePreview(); + // break; + // } + // } + // for (const key of Object.keys(this._afterImportHandler)) { + // delete model[key as keyof MainModel]; + // } + // return model; + // } + + // private parseCsvValue(value: string, config: CsvValueParsingConfig): any { + // if (config.importFindHandler) { + // const { importModel, allImportModels } = config; + // return config.importFindHandler.findByName(value, { importModel, allImportModels }); + // } + // // value = this.pipeParseValue(value, config.header) ?? value; + // return value; + // } + + // private checkReceivedHeaders(): void { + // const leftReceivedHeaders = [...this._receivedHeaders]; + // const expectedHeaders = [...this.expectedHeaders]; + // while (expectedHeaders.length > 0) { + // const toExpected = expectedHeaders.shift() as string; + // const nextHeader = this._modelHeadersAndVerboseNames[toExpected]; + // const nextHeaderTranslated = this.translate.instant(nextHeader); + // let index = leftReceivedHeaders.findIndex(header => header === nextHeaderTranslated); + // if (index > -1) { + // this._mapReceivedExpectedHeaders[toExpected] = nextHeaderTranslated; + // leftReceivedHeaders.splice(index, 1); + // continue; + // } + // index = leftReceivedHeaders.findIndex(header => header === nextHeader); + // if (index > -1) { + // this._mapReceivedExpectedHeaders[toExpected] = nextHeader; + // leftReceivedHeaders.splice(index, 1); + // continue; + // } + // this._mapReceivedExpectedHeaders[toExpected] = toExpected; + // this._lostHeaders.expected[toExpected] = nextHeaderTranslated; + // } + // this._lostHeaders.received = leftReceivedHeaders; + // this.checkImportValidness(); + // } + + // private checkImportValidness(): void { + // const requiredFulfilled = this._requiredFields.every( + // field => !Object.keys(this._lostHeaders.expected).includes(field as string) + // ); + // const validModels = this.importModels.some(importModel => !importModel.errors.length); + // this._isImportValidSubject.next(requiredFulfilled && validModels); + // } + + // private getEveryImportHandler(): ImportHandler[] { + // const beforeImportHandlers = Object.values(this._beforeImportHandler).map(({ mainHandler }) => mainHandler); + // const afterImportHandlers = Object.values(this._afterImportHandler).flatMap( + // ({ mainHandler, additionalHandlers }) => [mainHandler, ...additionalHandlers] + // ); + // return [...beforeImportHandlers, ...this.getEveryMainImportHandler(), ...afterImportHandlers]; + // } + + // private getEveryMainImportHandler(): MainImportHandler[] { + // return [this.getSelfImportHelper(), ...this._otherMainImportHelper]; + // } +} diff --git a/client/src/app/site/base/base-via-backend-import-list.component.ts b/client/src/app/site/base/base-via-backend-import-list.component.ts new file mode 100644 index 0000000000..b9244b7dd1 --- /dev/null +++ b/client/src/app/site/base/base-via-backend-import-list.component.ts @@ -0,0 +1,64 @@ +import { Directive } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { BaseComponent } from 'src/app/site/base/base.component'; + +import { Identifiable } from '../../domain/interfaces'; +import { getLongPreview, getShortPreview } from '../../infrastructure/utils'; +import { ComponentServiceCollectorService } from '../services/component-service-collector.service'; +import { BaseViaBackendImportService } from './base-import.service/base-via-backend-import.service'; + +@Directive() +export abstract class BaseViaBackendImportListComponent extends BaseComponent { + /** + * Helper function for previews + */ + public getLongPreview = getLongPreview; + + /** + * Helper function for previews + */ + public getShortPreview = getShortPreview; + + /** + * Switch that turns true if a file has been selected in the input + */ + public get canImport(): boolean { + return this._hasFile && this._modelsToCreateAmount > 0; + } + + private _hasFile = false; + private _modelsToCreateAmount = 0; + + public constructor( + componentServiceCollector: ComponentServiceCollectorService, + protected override translate: TranslateService, + protected importer: BaseViaBackendImportService + ) { + super(componentServiceCollector, translate); + } + + // public ngOnInit(): void { + // this.initTable(); + // } + + // /** + // * Initializes the table + // */ + // public initTable(): void { + // const entryObservable = this.importer.getNewEntriesObservable(); + // this.subscriptions.push( + // entryObservable.pipe(distinctUntilChanged(), auditTime(100)).subscribe(newEntries => { + // this._hasFile = newEntries.length > 0; + // this._modelsToCreateAmount = newEntries.length; + // }) + // ); + // } + + /** + * Triggers the importer's import + * + */ + public async doImport(): Promise { + await this.importer.doImport(); + } +} diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.html b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.html index 68492666bb..62d954c443 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.html +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.html @@ -1,4 +1,4 @@ - @@ -14,11 +14,9 @@

{{ 'Import topics' | translate }}

- @@ -57,4 +55,4 @@

{{ 'Import topics' | translate }}

{{ 'Internal item' | translate }}
{{ getTypeString(entry.newEntry.agenda_type) | translate }} -
+ diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.ts b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.ts index 0cd20216c9..39852f86fc 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.ts @@ -3,7 +3,7 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { AgendaItemType, ItemTypeChoices } from 'src/app/domain/models/agenda/agenda-item'; import { Topic } from 'src/app/domain/models/topics/topic'; -import { BaseImportListComponent } from 'src/app/site/base/base-import-list.component'; +import { BaseViaBackendImportListComponent } from 'src/app/site/base/base-via-backend-import-list.component'; import { ComponentServiceCollectorService } from 'src/app/site/services/component-service-collector.service'; import { DurationService } from 'src/app/site/services/duration.service'; import { ImportListHeaderDefinition } from 'src/app/ui/modules/import-list'; @@ -18,7 +18,7 @@ const TEXT_IMPORT_TAB_INDEX = 0; templateUrl: `./topic-import.component.html`, styleUrls: [`./topic-import.component.scss`] }) -export class TopicImportComponent extends BaseImportListComponent { +export class TopicImportComponent extends BaseViaBackendImportListComponent { /** * A form for text input */ diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts index 64442963dd..c58cb5a7f3 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts @@ -3,10 +3,12 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { AgendaItemType, ItemTypeChoices } from 'src/app/domain/models/agenda/agenda-item'; import { Topic } from 'src/app/domain/models/topics/topic'; import { TopicRepositoryService } from 'src/app/gateways/repositories/topics/topic-repository.service'; -import { ImportConfig } from 'src/app/infrastructure/utils/import/import-utils'; -import { BaseImportService } from 'src/app/site/base/base-import.service'; +import { ViaBackendImportConfig } from 'src/app/infrastructure/utils/import/import-utils'; +import { BaseViaBackendImportService } from 'src/app/site/base/base-import.service/base-via-backend-import.service'; +import { ActiveMeetingIdService } from 'src/app/site/pages/meetings/services/active-meeting-id.service'; import { DurationService } from 'src/app/site/services/duration.service'; import { ImportServiceCollectorService } from 'src/app/site/services/import-service-collector.service'; +import { ImportViaBackendJSONUploadResponse } from 'src/app/ui/modules/import-list/definitions/import-via-backend-preview'; import { topicHeadersAndVerboseNames } from '../../../../definitions'; import { TopicExportService } from '../topic-export.service'; @@ -15,7 +17,7 @@ import { TopicImportServiceModule } from '../topic-import-service.module'; @Injectable({ providedIn: TopicImportServiceModule }) -export class TopicImportService extends BaseImportService { +export class TopicImportService extends BaseViaBackendImportService { /** * The minimimal number of header entries needed to successfully create an entry */ @@ -29,6 +31,8 @@ export class TopicImportService extends BaseImportService { ParsingErrors: _(`Some csv values could not be read correctly.`) }; + public override readonly isMeetingImport = true; + /** * Constructor. Calls the abstract class and sets the expected header * @@ -39,7 +43,8 @@ export class TopicImportService extends BaseImportService { serviceCollector: ImportServiceCollectorService, private durationService: DurationService, private repo: TopicRepositoryService, - private exporter: TopicExportService + private exporter: TopicExportService, + private activeMeetingId: ActiveMeetingIdService ) { super(serviceCollector); } @@ -48,25 +53,31 @@ export class TopicImportService extends BaseImportService { this.exporter.downloadCsvImportExample(); } - protected getConfig(): ImportConfig { + protected getConfig(): ViaBackendImportConfig { return { - modelHeadersAndVerboseNames: topicHeadersAndVerboseNames, - verboseNameFn: plural => this.repo.getVerboseName(plural), - getDuplicatesFn: (entry: Partial) => - this.repo.getViewModelList().filter(topic => topic.title === entry.title), - createFn: (entries: any[]) => this.repo.create(...entries) + modelHeadersAndVerboseNames: topicHeadersAndVerboseNames }; } - protected override pipeParseValue(value: string, header: any): any { - if (header === `agenda_duration`) { - return this.parseDuration(value); - } - if (header === `agenda_type`) { - return this.parseType(value); - } + protected override calculateJsonUploadPayload(): any { + let payload = super.calculateJsonUploadPayload(); + payload[`meeting_id`] = this.activeMeetingId.meetingId; + return payload; } + protected async jsonUpload(payload: { [key: string]: any }): Promise { + return await this.repo.jsonUpload(payload).resolve(); + } + + // protected override pipeParseValue(value: string, header: any): any { + // if (header === `agenda_duration`) { + // return this.parseDuration(value); + // } + // if (header === `agenda_type`) { + // return this.parseType(value); + // } + // } + /** * Matching the duration string/number to the time model in use * diff --git a/client/src/app/ui/base/import-service.ts b/client/src/app/ui/base/import-service.ts index e99b2736bd..bc0fb9b49c 100644 --- a/client/src/app/ui/base/import-service.ts +++ b/client/src/app/ui/base/import-service.ts @@ -4,6 +4,8 @@ import { ImportModel } from 'src/app/infrastructure/utils/import/import-model'; import { ImportStep } from 'src/app/infrastructure/utils/import/import-step'; import { ValueLabelCombination } from 'src/app/infrastructure/utils/import/import-utils'; +import { ImportViaBackendPreviewRow } from '../modules/import-list/definitions/import-via-backend-preview'; + interface ImportServicePreview { new: number; done: number; @@ -38,3 +40,23 @@ export interface ImportService { setNewHeaderValue(headerDefinition: { [headerKey: string]: string }): void; downloadCsvExample(): void; } + +export interface ViaBackendImportService { + readonly rawFileObservable: Observable; + readonly encodings: ValueLabelCombination[]; + readonly columnSeparators: ValueLabelCombination[]; + readonly textSeparators: ValueLabelCombination[]; + + columnSeparator: string; + textSeparator: string; + encoding: string; + + verbose(error: string): string; + hasError(row: ImportViaBackendPreviewRow, error: string): boolean; + refreshFile(): void; + clearPreview(): void; + clearFile(): void; + onSelectFile(event: any): void; + doImport(): Promise; + downloadCsvExample(): void; +} diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html new file mode 100644 index 0000000000..e793b7f81c --- /dev/null +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html @@ -0,0 +1,335 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

{{ 'Summary' | translate }}

+ +
+
+
+ + +
+ + +   + {{ 0 }} {{ 'of' | translate }} + +   + + {{ point.value }} + +   + {{ point.key | translate }} +
+
+
+ + +
+
+ {{ 'After verifiy the preview click on "import" please (see top right).' | translate }} +
+ + + + +

{{ 'Preview' | translate }}

+
+ +
+
+
+ + +
+ + {{ + 'Required comma or semicolon separated values with these column header names in the first row:' | translate + }} + +
+
+ + {{ entry | translate }} + + +
+
    +
  • + + {{ field | translate }} + + + {{ 'is required' | translate }}. + {{ 'are required' | translate }}. + {{ 'All other fields are optional and may be empty.' | translate }} +
  • +
  • + {{ + 'Additional columns after the required ones may be present and will not affect the import.' | translate + }} +
  • +
+

+ {{ additionalInfo }} +

+ +
+ + {{ 'Encoding of the file' | translate }} + + + {{ option.label | translate }} + + + + + {{ 'Column separator' | translate }} + + + {{ option.label | translate }} + + + + + {{ 'Text separator' | translate }} + + + {{ option.label | translate }} + + + +
+
+
+ + +
+ {{ file.name }} + +
+
+
+
+ + + {{ model.name | translate }} + add + + + +
+ + warning + + +
+
+ + + +
+ + + + {{ getActionIcon(row) }} + + + warning + + + + + {{ getActionIcon(row) }} + + + warning + + + + + {{ getActionIcon(row) }} + + + + + {{ getActionIcon(row) }} + + + + + + +
+
+
#
+ {{ value }} +
+ + +
+
{{ getColumnLabel(column.property) | translate }}
+ + +
+
+
+
+ + +
+

{{ 'Preview' | translate }}

+ +
+
+ +
+
+ + diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.scss b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.scss new file mode 100644 index 0000000000..57534818df --- /dev/null +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.scss @@ -0,0 +1,63 @@ +@import 'src/assets/styles/tables.scss'; + +:host { + --os-row-height: 50px; +} + +mat-card.import-table { + .code { + span { + display: inline-flex; + } + } +} + +pbl-ngrid-header-cell, +pbl-ngrid-cell { + padding-right: 12px; + + &:first-of-type { + padding-left: 12px !important; + } +} + +.import-list-wrapper { + display: grid; + grid-template-columns: auto auto auto; + column-gap: 12px; +} + +.import-list-summary { + .import-list-icon { + animation: rotation 1s infinite linear; + } +} + +.import-list-preview-table { + height: 500px; + + .pbl-ngrid-row { + height: var(--os-row-height); + } + + .ngrid-preview { + div { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + } +} + +.import-list-preview-row { + font-size: 14px; +} + +@keyframes rotation { + from { + transform: rotate(0deg); + } + to { + transform: rotate(359deg); + } +} diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.spec.ts b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.spec.ts new file mode 100644 index 0000000000..7a30889c98 --- /dev/null +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ViaBackendImportListComponent } from './via-backend-import-list.component'; + +describe('ViaBackendImportListComponent', () => { + let component: ViaBackendImportListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ViaBackendImportListComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ViaBackendImportListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts new file mode 100644 index 0000000000..96b9c8da3e --- /dev/null +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts @@ -0,0 +1,522 @@ +import { + Component, + ContentChild, + ContentChildren, + ElementRef, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + QueryList, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { MatSelectChange } from '@angular/material/select'; +import { MatTab, MatTabChangeEvent } from '@angular/material/tabs'; +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; +import { firstValueFrom, Observable, of } from 'rxjs'; +import { Identifiable } from 'src/app/domain/interfaces'; +import { ValueLabelCombination } from 'src/app/infrastructure/utils/import/import-utils'; +import { ViaBackendImportService } from 'src/app/ui/base/import-service'; + +import { END_POSITION, START_POSITION } from '../../../scrolling-table/directives/scrolling-table-cell-position'; +import { ImportListHeaderDefinition } from '../../definitions'; +import { + ImportViaBackendIndexedPreview, + ImportViaBackendPreviewHeader, + ImportViaBackendPreviewIndexedRow, + ImportViaBackendPreviewSummary +} from '../../definitions/import-via-backend-preview'; +import { ImportListFirstTabDirective } from '../../directives/import-list-first-tab.directive'; +import { ImportListLastTabDirective } from '../../directives/import-list-last-tab.directive'; +import { ImportListStatusTemplateDirective } from '../../directives/import-list-status-template.directive'; + +export enum ImportViaBackendPhase { + LOADING_PREVIEW, + AWAITING_CONFIRM, + IMPORTING, + FINISHED, + ERROR +} + +@Component({ + selector: `os-via-backend-import-list`, + templateUrl: `./via-backend-import-list.component.html`, + styleUrls: [`./via-backend-import-list.component.scss`], + encapsulation: ViewEncapsulation.None +}) +export class ViaBackendImportListComponent implements OnInit, OnDestroy { + public readonly END_POSITION = END_POSITION; + public readonly START_POSITION = START_POSITION; + + @ContentChildren(ImportListFirstTabDirective, { read: MatTab }) + public importListFirstTabs!: QueryList; + + @ContentChildren(ImportListLastTabDirective, { read: MatTab }) + public importListLastTabs!: QueryList; + + @ContentChild(ImportListStatusTemplateDirective, { read: TemplateRef }) + public importListStateTemplate: TemplateRef; + + @ViewChild(`fileInput`) + private fileInput!: ElementRef; + + @Input() + public rowHeight = 50; + + @Input() + public modelName = ``; + + @Input() + public additionalInfo = ``; + + @Input() + public set importer(importer: ViaBackendImportService) { + this._importer = importer; + // this.initTable(); + // importer.errorEvent.subscribe(this.raiseError); + } + + public get importer(): ViaBackendImportService { + return this._importer; + } + + private _importer!: ViaBackendImportService; + + /** + * Defines all necessary and optional fields, that a .csv-file has to contain. + */ + @Input() + public possibleFields: string[] = []; + + @Output() + public selectedTabChanged = new EventEmitter(); + + public readonly Phase = ImportViaBackendPhase; + + /** + * Switch that turns true if a file has been selected in the input + */ + public hasFile!: Observable; + public get rawFileObservable(): Observable { + return this._importer?.rawFileObservable || of(null); + } + + public get defaultColumns(): ImportListHeaderDefinition[] { + return this._defaultColumns; + } + + @Input() + public set defaultColumns(cols: ImportListHeaderDefinition[]) { + this._defaultColumns = cols; + } + + public get previewColumns(): ImportViaBackendPreviewHeader[] { + return this._preview[0].headers; + } + + public get summary(): ImportViaBackendPreviewSummary[] { + return this._preview.flatMap(preview => preview.statistics); + } + + public get rows(): ImportViaBackendPreviewIndexedRow[] { + return this._preview.flatMap(preview => preview.rows); + } + + private _preview: ImportViaBackendIndexedPreview[]; + + /** + * Currently selected encoding. Is set and changed by the config's available + * encodings and user mat-select input + */ + public selectedEncoding = `utf-8`; + + /** + * indicator on which elements to display + */ + public shown: 'all' | 'error' | 'noerror' = `all`; + + /** + * @returns the amount of total item successfully parsed + */ + public get totalCount(): number | null { + return null; // TODO: implement! + // return this._importer && this.hasFile ? this._importer.summary.total : null; + } + + /** + * @returns the encodings available and their labels + */ + public get encodings(): ValueLabelCombination[] { + return this._importer.encodings; + } + + /** + * @returns the available column separators and their labels + */ + public get columnSeparators(): ValueLabelCombination[] { + return this._importer.columnSeparators; + } + + /** + * @eturns the available text separators and their labels + */ + public get textSeparators(): ValueLabelCombination[] { + return this._importer.textSeparators; + } + + public get hasRowErrors(): boolean { + return this.rows.some(row => row.error.length); // TODO: dont call some in getter! + } + + public get requiredFields(): string[] { + return this._requiredFields; + } + + public get dataSource(): Observable { + return this._dataSource; + } + + private _dataSource: Observable = of([]); + private _requiredFields: string[] = []; + private _defaultColumns: ImportListHeaderDefinition[] = []; + + public constructor(private dialog: MatDialog) {} + + /** + * Starts with a clean preview (removing any previously existing import previews) + */ + public ngOnInit(): void { + this._importer.clearPreview(); + // this._defaultColumns = this.createColumns(); + this._requiredFields = this.createRequiredFields(); + } + + public ngOnDestroy(): void { + this._importer.clearFile(); + this._importer.clearPreview(); + } + + /** + * Triggers a change in the tab group: Clearing the preview selection + */ + public onTabChange({ index }: MatTabChangeEvent): void { + this._importer.clearPreview(); + this.selectedTabChanged.emit(index); + } + + public hasSeveralTabs(): boolean { + return this.importListFirstTabs.length + this.importListLastTabs.length > 0; + } + + /** + * triggers the importer's onSelectFile after a file has been chosen + */ + public onSelectFile(event: any): void { + this._importer.onSelectFile(event); + } + + /** + * Triggers the importer's import + * + */ + public async doImport(): Promise { + this._importer.doImport(); //.then(() => this.setFilter()); + } + + public removeSelectedFile(): void { + this.fileInput.nativeElement.value = ``; + this._importer.clearFile(); + } + + /** + * Get the icon for the action of the item + * @param entry a newEntry object with a current status + * @eturn the icon for the action of the item + */ + public getActionIcon(entry: ImportViaBackendPreviewIndexedRow): string { + switch (entry.status) { + case `error`: // no import possible + return `block`; + case `warning`: + return `warn`; + case `new`: + return `add`; + case `done`: // item has been imported + return `done`; + case `generated`: + return `arrow`; + default: + return `block`; // fallback: Error + } + } + + public getErrorDescription(entry: ImportViaBackendPreviewIndexedRow): string { + return entry.error.map(error => _(this.getVerboseError(error))).join(`, `); + } + + /** + * A function to trigger the csv example download. + */ + public downloadCsvExample(): void { + this._importer.downloadCsvExample(); + } + + /** + * Trigger for the column separator selection. + * + * @param event + */ + public selectColSep(event: MatSelectChange): void { + this._importer.columnSeparator = event.value; + this._importer.refreshFile(); + } + + /** + * Trigger for the column separator selection + * + * @param event + */ + public selectTextSep(event: MatSelectChange): void { + this._importer.textSeparator = event.value; + this._importer.refreshFile(); + } + + public getColumnWidth(propertyName: string): number { + return this.defaultColumns.find(col => col.property === propertyName)?.width ?? 50; + } + + public getColumnLabel(propertyName: string): string { + return this.defaultColumns.find(col => col.property === propertyName)?.label ?? propertyName; + } + + /** + * Trigger for the encoding selection + * + * @param event + */ + public selectEncoding(event: MatSelectChange): void { + this._importer.encoding = event.value; + this._importer.refreshFile(); + } + + /** + * Returns a descriptive string for an import error + * + * @param error The short string for the error as listed in the {@lilnk errorList} + * @returns a predefined descriptive error string from the importer + */ + public getVerboseError(error: string): string { + return this._importer.verbose(error); + } + + /** + * Checks if an error is present in a new entry + * + * @param row the NewEntry + * @param error An error as defined as key of {@link errorList} + * @returns true if the error is present in the entry described in the row + */ + public hasError(row: ImportViaBackendPreviewIndexedRow, error: string): boolean { + return this._importer.hasError(row, error); + } + + public async enterFullscreen(dialogTemplate: TemplateRef): Promise { + const ref = this.dialog.open(dialogTemplate, { width: `80vw` }); + await firstValueFrom(ref.afterClosed()); + } + + private createRequiredFields(): string[] { + //TODO: implement! + // const definitions: ImportViaBackendPreviewHeader[] = this.columns ?? [this.headerDefinition!]; + // if (Array.isArray(definitions) && definitions.length > 0) { + // return definitions + // .filter((definition: ImportViaBackendPreviewHeader) => definition.isRequired as boolean) + // .map(definition => definition.label as string); + // } else { + return []; + // } + } + + // @Input() + // public columns?: ImportListHeaderDefinition[]; + + // @Input() + // public headerDefinition?: ImportViaBackendPreviewHeader; + + // @Input() + // public showUnknownHeaders = true; + + // public get hasLeftReceivedHeaders(): boolean { + // return this.leftReceivedHeaders.length > 0; + // } + + // public get leftReceivedHeaders(): string[] { + // return this._importer.leftReceivedHeaders; + // } + + // public get leftExpectedHeaders(): { [key: string]: string } { + // return this._importer.leftExpectedHeaders; + // } + + /** + * @returns the amount of import items that will be imported + */ + // public get newCount(): number { + // return 0; // TODO: implement! + // // return this._importer && this.hasFile ? this._importer.summary.new : 0; + // } + + /** + * @returns the number of import items that cannot be imported + */ + // public get nonImportableCount(): number { + // if (this._importer && this.hasFile) { + // return 0; // TODO: Implement! + // // return this._importer.summary.errors + this._importer.summary.duplicates; + // } + // return 0; + // } + + /** + * @returns the number of import items that have been successfully imported + */ + // public get doneCount(): number { + // return 0; // TODO: implement! + // // return this._importer && this.hasFile ? this._importer.summary.done : 0; + // } + + // public get importPreviewLabel(): string { + // return `${this.modelName || `Models`} will be imported.`; + // } + + // public get importDoneLabel(): string { + // return `${this.modelName || `Models`} have been imported.`; + // } + + // public get importingStepsObservable(): Observable { + // return null; // TODO: implement! + // // return this._importer.importingStepsObservable; + // } + + // public headerValueMap: any = {}; + + /** + * Initializes the table + */ + // public initTable(): void { + // const newEntriesObservable = this._importer.getNewEntriesObservable(); + // this.hasFile = newEntriesObservable.pipe( + // distinctUntilChanged(), + // auditTime(100), + // map(entries => entries.length > 0) + // ); + + // this._dataSource = newEntriesObservable; + // } + + /** + * Updates and manually triggers the filter function. + * See {@link hidden} for options + * (changed from default mat-table filter) + */ + // public setFilter(): void { + // if (this.shown === `all`) { + // // this.vScrollDataSource.setFilter(); + // } else if (this.shown === `noerror`) { + // // const noErrorFilter = (data: ImportModel) => data.status === `done` || data.status !== `error`; + // // this.vScrollDataSource.setFilter(noErrorFilter); + // } else if (this.shown === `error`) { + // // const hasErrorFilter = (data: ImportModel) => + // // data.status === `error` || !!data.errors.length || data.hasDuplicates; + // // this.vScrollDataSource.setFilter(hasErrorFilter); + // } + // } + + /** + * Get the appropiate css class for a row according to the import state + * + * @param row a newEntry object with a current status + * @returns a css class name + */ + // public getStateClass(row: ImportViaBackendPreviewRow): string { + // switch (row.status) { + // case `done`: + // return `import-done import-decided`; + // case `error`: + // return `import-error`; + // default: + // return ``; + // } + // } + + // public getTooltip(value: string | CsvMapping[]): string { + // if (Array.isArray(value)) { + // return value.map(entry => entry.name).join(`;\n\r`); + // } + // return value; + // } + // public isUnknown(headerKey: string): boolean { + // return !this._importer.headerValues[headerKey]; + // } + + // public onChangeUnknownHeaderKey(headerKey: string, value: string): void { + // this.headerValueMap[headerKey] = value; + // this._importer.setNewHeaderValue({ [headerKey]: value }); + // } + + // public isArray(data: any): boolean { + // return Array.isArray(data); + // } + + // public isObject(data: any): boolean { + // return typeof data === `object`; + // } + + // public getLabelByStepPhase(phase: ImportViaBackendPhase): string { + // switch (phase) { + // case ImportViaBackendPhase.FINISHED: + // return _(`have been created`); + // case ImportViaBackendPhase.ERROR: + // return _(`could not be created`); + // default: + // return _(`will be created`); + // } + // } + + // public isTrue(value: any) { + // return [`true`, 1, true, `1`].includes(value); + // } + + // private createColumns(): ImportViaBackendPreviewHeader[] { + // const getHeaderProp = (prop: string) => { + // return prop.startsWith(`newEntry.`) ? prop.slice(`newEntry.`.length) : prop; + // }; + // const definitions = this.columns ?? [this.headerDefinition]; + // if (!definitions) { + // throw new Error(`You have to specify the columns to show`); + // } + // if (Array.isArray(definitions) && definitions.length > 0) { + // const computedDefinitions = definitions + // .filter((definition: ImportViaBackendPreviewHeader) => definition.isTableColumn) + // .map(column => ({ + // ...column, + // property: getHeaderProp(column.property), + // type: this.getTypeByProperty(getHeaderProp(column.property)) + // })); + // return computedDefinitions; + // } + // return []; + // } + + // private getTypeByProperty(property: string): 'boolean' | 'string' { + // if (property.startsWith(`is`) || property.startsWith(`has`)) { + // return `boolean`; + // } else { + // return `string`; + // } + // } +} diff --git a/client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts b/client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts new file mode 100644 index 0000000000..6e8a4e3d8f --- /dev/null +++ b/client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts @@ -0,0 +1,62 @@ +import { Identifiable } from 'src/app/domain/interfaces'; + +enum ImportState { + Error = `error`, + Warning = `warning`, + New = `new`, + Done = `done`, + Generated = `generated` + // could be expanded later +} + +export interface ImportViaBackendPreviewHeader { + property: string; // field name/column title + type: `boolean` | `number` | `string` | `date` | `object`; // date must be in format yyyy-mm-dd + is_list: boolean; // optional, if not given defaults to `false` +} + +export type ImportViaBackendPreviewModelData = + | null + | boolean + | number + | string + | { + value: boolean | number | string; + info: ImportState; + type: `boolean` | `number` | `string` | `date`; + }; + +export interface ImportViaBackendPreviewRow { + status: ImportState; + error: string[]; + data: { + // property name and type must match an entry in the given `headers` + [property: string]: ImportViaBackendPreviewModelData[]; // if is_list is set in corresponding header column, we need here also a list + }[]; +} + +export type ImportViaBackendPreviewIndexedRow = ImportViaBackendPreviewRow & Identifiable; + +export interface ImportViaBackendPreviewSummary { + [text: string]: number; // text like "Total Number of Items", "Created", "Updated", depending the action +} + +export interface ImportViaBackendPreview { + id: number; // id of action_worker to import + headers: ImportViaBackendPreviewHeader[]; + rows: ImportViaBackendPreviewRow[]; + statistics: ImportViaBackendPreviewSummary[]; +} + +export interface ImportViaBackendIndexedPreview { + id: number; // id of action_worker to import + headers: ImportViaBackendPreviewHeader[]; + rows: ImportViaBackendPreviewIndexedRow[]; + statistics: ImportViaBackendPreviewSummary[]; +} + +export interface ImportViaBackendJSONUploadResponse { + success: boolean; + message: string; + results: ImportViaBackendPreview[]; +} diff --git a/client/src/app/ui/modules/import-list/import-list.module.ts b/client/src/app/ui/modules/import-list/import-list.module.ts index c75d21dd76..9d567c7fad 100644 --- a/client/src/app/ui/modules/import-list/import-list.module.ts +++ b/client/src/app/ui/modules/import-list/import-list.module.ts @@ -14,6 +14,7 @@ import { ScrollingTableModule } from 'src/app/ui/modules/scrolling-table'; import { OpenSlidesTranslationModule } from '../../../site/modules/translations'; import { ImportListComponent } from './components/import-list/import-list.component'; +import { ViaBackendImportListComponent } from './components/via-backend-import-list/via-backend-import-list.component'; import { ImportListFirstTabDirective } from './directives/import-list-first-tab.directive'; import { ImportListLastTabDirective } from './directives/import-list-last-tab.directive'; import { ImportListStatusTemplateDirective } from './directives/import-list-status-template.directive'; @@ -22,7 +23,8 @@ const DECLARATIONS = [ ImportListComponent, ImportListFirstTabDirective, ImportListLastTabDirective, - ImportListStatusTemplateDirective + ImportListStatusTemplateDirective, + ViaBackendImportListComponent ]; @NgModule({ From 1f153979c32bac7ba724aed449225bcbf8739fb7 Mon Sep 17 00:00:00 2001 From: Luisa Date: Fri, 28 Apr 2023 17:51:45 +0200 Subject: [PATCH 02/16] Reworked exports for template fields and worked on templates --- .../app/domain/models/agenda/agenda-item.ts | 2 +- .../csv-export-for-backend.service.spec.ts | 16 ++ .../csv-export-for-backend.service.ts | 155 ++++++++++++++++++ .../base-via-backend-import.service.ts | 43 ++++- .../topic-import/topic-import.component.html | 1 + .../topic-import/topic-import.component.ts | 2 +- .../topic-export.service.ts | 6 +- .../agenda-item-export.service.ts | 16 +- client/src/app/ui/base/import-service.ts | 6 +- .../via-backend-import-list.component.html | 75 ++++++--- .../via-backend-import-list.component.ts | 70 ++++++-- .../definitions/import-via-backend-preview.ts | 11 +- 12 files changed, 341 insertions(+), 62 deletions(-) create mode 100644 client/src/app/gateways/export/csv-export.service/csv-export-for-backend.service.spec.ts create mode 100644 client/src/app/gateways/export/csv-export.service/csv-export-for-backend.service.ts diff --git a/client/src/app/domain/models/agenda/agenda-item.ts b/client/src/app/domain/models/agenda/agenda-item.ts index c1c5d21233..d808ab2abd 100644 --- a/client/src/app/domain/models/agenda/agenda-item.ts +++ b/client/src/app/domain/models/agenda/agenda-item.ts @@ -36,7 +36,7 @@ export class AgendaItem extends BaseModel { public type!: AgendaItemType; public is_hidden!: boolean; public is_internal!: boolean; - public duration!: number; // in seconds + public duration!: number; // in minutes public weight!: number; /** * Client-calculated field: The level indicates the indentation of an agenda-item. diff --git a/client/src/app/gateways/export/csv-export.service/csv-export-for-backend.service.spec.ts b/client/src/app/gateways/export/csv-export.service/csv-export-for-backend.service.spec.ts new file mode 100644 index 0000000000..e65b54251a --- /dev/null +++ b/client/src/app/gateways/export/csv-export.service/csv-export-for-backend.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CsvExportForBackendService } from './csv-export-for-backend.service'; + +xdescribe(`CsvExportForBackendService`, () => { + let service: CsvExportForBackendService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CsvExportForBackendService); + }); + + it(`should be created`, () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/client/src/app/gateways/export/csv-export.service/csv-export-for-backend.service.ts b/client/src/app/gateways/export/csv-export.service/csv-export-for-backend.service.ts new file mode 100644 index 0000000000..9f9b4df802 --- /dev/null +++ b/client/src/app/gateways/export/csv-export.service/csv-export-for-backend.service.ts @@ -0,0 +1,155 @@ +import { Injectable } from '@angular/core'; +import { BaseViewModel } from 'src/app/site/base/base-view-model'; + +import { ExportServiceModule } from '../export-service.module'; +import { FileExportService } from '../file-export.service'; +import { + CsvColumnsDefinition, + DEFAULT_COLUMN_SEPARATOR, + DEFAULT_ENCODING, + DEFAULT_LINE_SEPARATOR, + isMapDefinition, + ISO_8859_15_ENCODING, + isPropertyDefinition +} from './csv-export-utils'; + +@Injectable({ + providedIn: ExportServiceModule +}) +export class CsvExportForBackendService { + public constructor(private exporter: FileExportService) {} + + /** + * Saves an array of model data to a CSV. + * @param models Array of model instances to be saved + * @param columns Column definitions + * @param filename name of the resulting file + * @param options optional: + * lineSeparator (defaults to \r\n windows style line separator), + * columnseparator defaults to configured option (',' , other values are ';', '\t' (tab), ' 'whitespace) + */ + public export( + models: T[], + columns: CsvColumnsDefinition, + filename: string, + { + lineSeparator = DEFAULT_LINE_SEPARATOR, + columnSeparator = DEFAULT_COLUMN_SEPARATOR, + encoding = DEFAULT_ENCODING + }: { + lineSeparator?: string; + columnSeparator?: string; + encoding?: 'utf-8' | 'iso-8859-15'; + } = {} + ): void { + let csvContent = []; // Holds all lines as arrays with each column-value + // initial array of usable text separators. The first character not used + // in any text data or as column separator will be used as text separator + + if (lineSeparator === columnSeparator) { + throw new Error(`lineseparator and columnseparator must differ from each other`); + } + + // create header data + const header = columns.map(column => { + let label: string = ``; + if (isPropertyDefinition(column)) { + label = column.label ? column.label : (column.property as string); + } else if (isMapDefinition(column)) { + label = column.label; + } + return label; + }); + csvContent.push(header); + + // create lines + csvContent = csvContent.concat( + models.map(model => + columns.map(column => { + let value: string = ``; + + if (isPropertyDefinition(column)) { + const property: any = model[column.property]; + if (typeof property === `number`) { + value = property.toString(10); + } else if (!property) { + value = ``; + } else if (property === true) { + value = `1`; + } else if (property === false) { + value = `0`; + } else if (typeof property === `function`) { + const bindedFn = property.bind(model); // bind model to access 'this' + value = bindedFn()?.toString(); + } else { + value = property.toString(); + } + } else if (isMapDefinition(column)) { + value = column.map(model); + } + return this.checkCsvTextSafety(value); + }) + ) + ); + + const csvContentAsString: string = csvContent + .map((line: string[]) => line.join(columnSeparator)) + .join(lineSeparator); + const filetype = `text/csv;charset=${encoding}`; + if (encoding === ISO_8859_15_ENCODING) { + this.exporter.saveFile(this.convertTo8859_15(csvContentAsString), filename, filetype); + } else { + this.exporter.saveFile(csvContentAsString, filename, filetype); + } + } + + public dummyCSVExport(headerAndVerboseNames: I, rows: I[], filename: string): void { + const separator = DEFAULT_COLUMN_SEPARATOR; + const encoding: `utf-8` | `iso-8859-15` = DEFAULT_ENCODING as any; + const headerRow = [Object.keys(headerAndVerboseNames).join(separator)]; + const content = rows.map(row => + Object.keys(headerAndVerboseNames) + .map(key => { + let value = row[key as keyof I] || ``; + if (typeof value === `number`) { + value = value.toString(10); + } else if (typeof value === `boolean`) { + value = value ? `1` : `0`; + } + return this.checkCsvTextSafety(value as string); + }) + .join(separator) + ); + const csv = headerRow.concat(content).join(`\r\n`); + const toExport = encoding === ISO_8859_15_ENCODING ? this.convertTo8859_15(csv) : csv; + this.exporter.saveFile(toExport, filename, `text/csv`); + } + + /** + * Ensures, that the given string has escaped double quotes + * and no linebreak. The string itself will also be escaped by `double quotes`. + * + * @param {string} input any input to be sent to CSV + * @returns {string} the cleaned string. + */ + public checkCsvTextSafety(input: string): string { + if (!input) { + return ``; + } + return `"` + input.replace(/"/g, `""`).replace(/(\r\n\t|\n|\r\t)/gm, ``) + `"`; + } + + /** + * get an iso-8859-15 - compatible blob part + * + * @param data + * @returns a Blob part + */ + private convertTo8859_15(data: string): BlobPart { + const array = new Uint8Array(new ArrayBuffer(data.length)); + for (let i = 0; i < data.length; i++) { + array[i] = data.charCodeAt(i); + } + return array; + } +} diff --git a/client/src/app/site/base/base-import.service/base-via-backend-import.service.ts b/client/src/app/site/base/base-import.service/base-via-backend-import.service.ts index 95db53050a..27589b61f0 100644 --- a/client/src/app/site/base/base-import.service/base-via-backend-import.service.ts +++ b/client/src/app/site/base/base-import.service/base-via-backend-import.service.ts @@ -12,6 +12,7 @@ import { import { ViaBackendImportService } from 'src/app/ui/base/import-service'; import { ImportViaBackendPhase } from 'src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component'; import { + ImportViaBackendIndexedPreview, ImportViaBackendJSONUploadResponse, ImportViaBackendPreview, ImportViaBackendPreviewRow @@ -90,18 +91,29 @@ export abstract class BaseViaBackendImportService result.id); + return this._previews.map(result => result.id); } - public get preview(): ImportViaBackendPreview[] { - return this._preview.results; + public get previews(): ImportViaBackendIndexedPreview[] { + return this._previews; + } + + private set previews(preview: ImportViaBackendIndexedPreview[] | null) { + this._previews = preview; + this._previewsSubject.next(preview); } /** * storing the summary preview for the import, to avoid recalculating it * at each display change. */ - private _preview: ImportViaBackendJSONUploadResponse | null = null; + private _previews: ImportViaBackendIndexedPreview[] | null = null; + + public get previewsObservable(): Observable { + return this._previewsSubject as Observable; + } + + private _previewsSubject = new BehaviorSubject(null); private _modelHeadersAndVerboseNames: { [key: string]: string } = {}; @@ -226,7 +238,7 @@ export abstract class BaseViaBackendImportService handler.doCleanup()); this.setNextEntries({}); // this._lostHeaders = { expected: {}, received: [] }; - this._preview = null; + this.previews = null; this._currentImportPhaseSubject.next(ImportViaBackendPhase.LOADING_PREVIEW); // this._isImportValidSubject.next(false); } @@ -256,7 +268,7 @@ export abstract class BaseViaBackendImportService { const payload = this.calculateJsonUploadPayload(); - const rawPreview = await this.jsonUpload(payload); - console.log(`UPLOADED JSON =>`, rawPreview); + const response = (await this.jsonUpload(payload)) as ImportViaBackendJSONUploadResponse[]; + if (!response) { + throw new Error(`Didn't receive preview`); + } + console.log(`UPLOADED JSON =>`, response); + const previews: (ImportViaBackendPreview | ImportViaBackendIndexedPreview)[] = response + .filter(response => response.success) + .flatMap(response => response.results); + let index = 0; + for (let preview of previews) { + for (let row of preview.rows) { + row[`id`] = index; + index++; + } + } + + this.previews = previews as ImportViaBackendIndexedPreview[]; // TODO: implement rest. I.e. actually save the result! // const rawEntries = this._csvLines.map((line, i) => this.createRawImportModel(line, i + 1)); // await this.onBeforeCreatingImportModels(rawEntries); diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.html b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.html index 62d954c443..8f820a3611 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.html +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.html @@ -17,6 +17,7 @@

{{ 'Import topics' | translate }}

diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.ts b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.ts index 39852f86fc..d6eb7b2049 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.ts @@ -24,7 +24,7 @@ export class TopicImportComponent extends BaseViaBackendImportListComponent ({ property: header, diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-export.service/topic-export.service.ts b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-export.service/topic-export.service.ts index 616a0a4c9a..c048f71277 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-export.service/topic-export.service.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-export.service/topic-export.service.ts @@ -21,9 +21,9 @@ export class TopicExportService { public downloadCsvImportExample(): void { const rows: TopicExport[] = [ - { title: `Demo 1`, text: `Demo text 1`, agenda_duration: `1:00`, agenda_comment: `Test comment` }, - { title: `Break`, agenda_duration: `0:10`, agenda_type: `internal` }, - { title: `Demo 2`, text: `Demo text 2`, agenda_duration: `1:30`, agenda_type: `hidden` } + { title: `Demo 1`, text: `Demo text 1`, agenda_duration: `60`, agenda_comment: `Test comment` }, + { title: `Break`, agenda_duration: `10`, agenda_type: `internal` }, + { title: `Demo 2`, text: `Demo text 2`, agenda_duration: `90`, agenda_type: `hidden` } ]; this.csvExportService.dummyCSVExport( diff --git a/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-item-list/services/agenda-item-export.service/agenda-item-export.service.ts b/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-item-list/services/agenda-item-export.service/agenda-item-export.service.ts index d7e061dff7..bf1ed55f52 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-item-list/services/agenda-item-export.service/agenda-item-export.service.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-item-list/services/agenda-item-export.service/agenda-item-export.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { CsvExportService } from 'src/app/gateways/export/csv-export.service'; +import { CsvExportForBackendService } from 'src/app/gateways/export/csv-export.service/csv-export-for-backend.service'; import { OSTreeNode } from 'src/app/infrastructure/definitions/tree'; import { ViewAgendaItem } from 'src/app/site/pages/meetings/pages/agenda'; import { MeetingPdfExportService } from 'src/app/site/pages/meetings/services/export'; @@ -22,7 +22,7 @@ interface AgendaTreePdfEntry { export class AgendaItemExportService { constructor( private translate: TranslateService, - private csvExportService: CsvExportService, + private csvExportService: CsvExportForBackendService, private pdfExportService: MeetingPdfExportService, private treeService: TreeService ) {} @@ -31,16 +31,16 @@ export class AgendaItemExportService { this.csvExportService.export( source, [ - { label: `Title`, map: viewItem => viewItem.getTitle() }, + { label: `title`, map: viewItem => viewItem.getTitle() }, { - label: `Text`, + label: `text`, map: viewItem => viewItem.content_object?.getCSVExportText ? viewItem.content_object.getCSVExportText() : `` }, - { label: `Duration`, property: `duration` }, - { label: `Comment`, property: `comment` }, - { label: `Item type`, property: `verboseCsvType` }, - { label: `Tags`, property: `tags` } + { label: `agenda_duration`, property: `duration` }, + { label: `agenda_comment`, property: `comment` }, + { label: `agenda_type`, property: `verboseCsvType` } + // { label: `tags`, property: `tags` } ], this.translate.instant(`Agenda`) + `.csv` ); diff --git a/client/src/app/ui/base/import-service.ts b/client/src/app/ui/base/import-service.ts index bc0fb9b49c..55a45a16a1 100644 --- a/client/src/app/ui/base/import-service.ts +++ b/client/src/app/ui/base/import-service.ts @@ -4,7 +4,10 @@ import { ImportModel } from 'src/app/infrastructure/utils/import/import-model'; import { ImportStep } from 'src/app/infrastructure/utils/import/import-step'; import { ValueLabelCombination } from 'src/app/infrastructure/utils/import/import-utils'; -import { ImportViaBackendPreviewRow } from '../modules/import-list/definitions/import-via-backend-preview'; +import { + ImportViaBackendIndexedPreview, + ImportViaBackendPreviewRow +} from '../modules/import-list/definitions/import-via-backend-preview'; interface ImportServicePreview { new: number; @@ -46,6 +49,7 @@ export interface ViaBackendImportService { readonly encodings: ValueLabelCombination[]; readonly columnSeparators: ValueLabelCombination[]; readonly textSeparators: ValueLabelCombination[]; + readonly previewsObservable: Observable; columnSeparator: string; textSeparator: string; diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html index e793b7f81c..59da8e03b1 100644 --- a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html @@ -118,16 +118,17 @@

{{ 'Preview' | translate }}

}}
-
- - {{ entry | translate }} +
+ + {{ entry }} + info
  • - {{ field | translate }} + {{ field }} {{ 'is required' | translate }}. @@ -195,37 +196,53 @@

    {{ 'Preview' | translate }}

- - {{ model.name | translate }} - add - -
warning - +
+ + + + {{getEntryIcon(entry[def].info)}} + + + + + + {{ entry | translate }} + {{ entry }} + {{ entry }} + +   + + {{ 'Preview' | translate }} + +
+

The fields are defined as follows

+
+
+
    +
  • + {{ column.property }}: {{ column.label | translate }} +
  • +
+
+
+ +
+
+
diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts index c58cb5a7f3..c4fd931700 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts @@ -8,7 +8,7 @@ import { BaseViaBackendImportService } from 'src/app/site/base/base-import.servi import { ActiveMeetingIdService } from 'src/app/site/pages/meetings/services/active-meeting-id.service'; import { DurationService } from 'src/app/site/services/duration.service'; import { ImportServiceCollectorService } from 'src/app/site/services/import-service-collector.service'; -import { ImportViaBackendJSONUploadResponse } from 'src/app/ui/modules/import-list/definitions/import-via-backend-preview'; +import { ImportViaBackendPreview } from 'src/app/ui/modules/import-list/definitions/import-via-backend-preview'; import { topicHeadersAndVerboseNames } from '../../../../definitions'; import { TopicExportService } from '../topic-export.service'; @@ -33,6 +33,14 @@ export class TopicImportService extends BaseViaBackendImportService { public override readonly isMeetingImport = true; + public override readonly verboseSummaryTitleMap: { [title: string]: string } = { + total: _(`Total topics`), + created: _(`Topics created`), + updated: _(`Topics updated`), + omitted: _(`Topics skipped`), + warning: _(`Topics with warnings (will be skipped)`) + }; + /** * Constructor. Calls the abstract class and sets the expected header * @@ -65,7 +73,7 @@ export class TopicImportService extends BaseViaBackendImportService { return payload; } - protected async jsonUpload(payload: { [key: string]: any }): Promise { + protected async jsonUpload(payload: { [key: string]: any }): Promise { return await this.repo.jsonUpload(payload).resolve(); } diff --git a/client/src/app/ui/base/import-service.ts b/client/src/app/ui/base/import-service.ts index 55a45a16a1..8c2c1ed9ad 100644 --- a/client/src/app/ui/base/import-service.ts +++ b/client/src/app/ui/base/import-service.ts @@ -63,4 +63,5 @@ export interface ViaBackendImportService { onSelectFile(event: any): void; doImport(): Promise; downloadCsvExample(): void; + getVerboseSummaryPointTitle(title: string): string; } diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html index 0dc5926723..74cd99f868 100644 --- a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html @@ -27,7 +27,7 @@ - +

{{ 'Summary' | translate }}

@@ -39,41 +39,15 @@

{{ 'Summary' | translate }}

fullscreen
-
-
- - -
- - -   - {{ 0 }} {{ 'of' | translate }} - -   - - {{ point.value }} - -   - {{ point.key | translate }} -
-
-
- - -
-
+ + + + + +
{{ getSummaryPointTitle(point.name) | translate }}: {{ point.value }}
+
{{ 'After verifiy the preview click on "import" please (see top right).' | translate }}
diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.ts b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.ts index d6eb7b2049..4e26c5423c 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.ts @@ -65,7 +65,11 @@ export class TopicImportComponent extends BaseViaBackendImportListComponent { return payload; } - protected async import(actionWorkerIds: number[]): Promise { - return await this.repo.import(actionWorkerIds.map(id => ({ id, import: true }))).resolve(); + protected async import( + actionWorkerIds: number[], + abort: boolean = false + ): Promise { + return await this.repo.import(actionWorkerIds.map(id => ({ id, import: !abort }))).resolve(); } protected async jsonUpload(payload: { [key: string]: any }): Promise { diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/topic-import.module.ts b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/topic-import.module.ts index dc1608161c..e20836b3cb 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/topic-import.module.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/topic-import.module.ts @@ -4,6 +4,7 @@ import { ReactiveFormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; +import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; import { OpenSlidesTranslationModule } from 'src/app/site/modules/translations'; import { HeadBarModule } from 'src/app/ui/modules/head-bar'; @@ -27,6 +28,7 @@ import { TopicImportRoutingModule } from './topic-import-routing.module'; MatFormFieldModule, MatInputModule, MatIconModule, + MatTooltipModule, SpinnerModule, ReactiveFormsModule, RouterModule diff --git a/client/src/app/ui/base/import-service.ts b/client/src/app/ui/base/import-service.ts index 8c2c1ed9ad..5ac53dac11 100644 --- a/client/src/app/ui/base/import-service.ts +++ b/client/src/app/ui/base/import-service.ts @@ -4,6 +4,7 @@ import { ImportModel } from 'src/app/infrastructure/utils/import/import-model'; import { ImportStep } from 'src/app/infrastructure/utils/import/import-step'; import { ValueLabelCombination } from 'src/app/infrastructure/utils/import/import-utils'; +import { ImportViaBackendPhase } from '../modules/import-list/components/via-backend-import-list/via-backend-import-list.component'; import { ImportViaBackendIndexedPreview, ImportViaBackendPreviewRow @@ -50,6 +51,8 @@ export interface ViaBackendImportService { readonly columnSeparators: ValueLabelCombination[]; readonly textSeparators: ValueLabelCombination[]; readonly previewsObservable: Observable; + readonly currentImportPhaseObservable: Observable; + readonly previewHasRowErrors: boolean; columnSeparator: string; textSeparator: string; @@ -64,4 +67,5 @@ export interface ViaBackendImportService { doImport(): Promise; downloadCsvExample(): void; getVerboseSummaryPointTitle(title: string): string; + clearAll(): void; } diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html index c1e130a3b1..a76e0c07b9 100644 --- a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html @@ -27,7 +27,7 @@ - +

{{ 'Summary' | translate }}

@@ -48,34 +48,12 @@

{{ 'Summary' | translate }}

- {{ 'After verifiy the preview click on "import" please (see top right).' | translate }} + {{ 'The import has failed due to recent changes on the server. Please check the updated preview before clicking on "import" again.' | translate }} + {{ 'After verifying the preview click on "import" please (see top right).' | translate }} + {{ 'The import is in progress, please wait...' | translate }} + {{ 'The import can not proceed. There is likely a problem with the import data, please check the preview for details.' | translate }} + {{ 'The import was successful.' | translate }}
- - -

{{ 'Preview' | translate }}

@@ -219,7 +197,7 @@

{{ 'Preview' | translate }}

{{ 'Preview' | translate }} *osScrollingTableCell="'status'; row as row; config: { width: 25, position: START_POSITION }" class="flex-vertical-center" > - + {{ getActionIcon(row) }}
@@ -252,7 +234,6 @@

{{ 'Preview' | translate }}

" >
{{ getColumnLabel(column.property) | translate }}
- @@ -293,20 +274,3 @@

The fields are defined as follows

- - diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts index 2ee80fa13d..1dbb36a9ff 100644 --- a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts @@ -16,8 +16,9 @@ import { import { MatDialog } from '@angular/material/dialog'; import { MatSelectChange } from '@angular/material/select'; import { MatTab, MatTabChangeEvent } from '@angular/material/tabs'; +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { TranslateService } from '@ngx-translate/core'; -import { firstValueFrom, map, Observable, of } from 'rxjs'; +import { delay, firstValueFrom, map, Observable, of } from 'rxjs'; import { Identifiable } from 'src/app/domain/interfaces'; import { infoDialogSettings } from 'src/app/infrastructure/utils/dialog-settings'; import { ValueLabelCombination } from 'src/app/infrastructure/utils/import/import-utils'; @@ -42,7 +43,7 @@ export enum ImportViaBackendPhase { IMPORTING, FINISHED, ERROR, - FINISHED_WITH_ERRORS + TRY_AGAIN } @Component({ @@ -126,6 +127,28 @@ export class ViaBackendImportListComponent implements On private _rows: ImportViaBackendPreviewIndexedRow[]; private _previewColumns: ImportViaBackendPreviewHeader[]; + public get awaitingConfirm(): boolean { + return this._state === ImportViaBackendPhase.AWAITING_CONFIRM || this.tryAgain; + } + + public get finishedSuccessfully(): boolean { + return this._state === ImportViaBackendPhase.FINISHED; + } + + public get tryAgain(): boolean { + return this._state === ImportViaBackendPhase.TRY_AGAIN; + } + + public get isImporting(): boolean { + return this._state === ImportViaBackendPhase.IMPORTING; + } + + public get hasErrors(): boolean { + return this._state === ImportViaBackendPhase.ERROR; + } + + private _state: ImportViaBackendPhase = ImportViaBackendPhase.LOADING_PREVIEW; + /** * Currently selected encoding. Is set and changed by the config's available * encodings and user mat-select input @@ -159,9 +182,11 @@ export class ViaBackendImportListComponent implements On } public get hasRowErrors(): boolean { - return this.rows.some(row => row.status === ImportState.Error); // TODO: dont call some in getter! + return this._hasErrors; } + private _hasErrors: boolean = false; + public get requiredFields(): string[] { return this._requiredFields; } @@ -182,8 +207,16 @@ export class ViaBackendImportListComponent implements On public ngOnInit(): void { this._importer.clearPreview(); this._requiredFields = this.createRequiredFields(); - this._importer.previewsObservable.subscribe(previews => this.fillPreviewData(previews)); - this._dataSource = this.importer.previewsObservable.pipe(map(previews => this.calculateRows(previews))); + this._importer.currentImportPhaseObservable.subscribe(phase => { + this._state = phase; + }); + this._importer.previewsObservable.subscribe(previews => { + this.fillPreviewData(previews); + }); + this._dataSource = this.importer.previewsObservable.pipe( + map(previews => this.calculateRows(previews)), + delay(50) + ); } public ngOnDestroy(): void { @@ -195,7 +228,8 @@ export class ViaBackendImportListComponent implements On * Triggers a change in the tab group: Clearing the preview selection */ public onTabChange({ index }: MatTabChangeEvent): void { - this._importer.clearPreview(); + this.removeSelectedFile(); + this._importer.clearAll(); this.selectedTabChanged.emit(index); } @@ -219,7 +253,9 @@ export class ViaBackendImportListComponent implements On } public removeSelectedFile(): void { - this.fileInput.nativeElement.value = ``; + if (this.fileInput) { + this.fileInput.nativeElement.value = ``; + } this._importer.clearFile(); } @@ -252,9 +288,12 @@ export class ViaBackendImportListComponent implements On public getRowTooltip(row: ImportViaBackendPreviewIndexedRow): string { switch (row.status) { case ImportState.Error: // no import possible - return this.getErrorDescription(row); + return ( + this.getErrorDescription(row) ?? + _(`There is an unspecified error in this line, which prevents the import.`) + ); case ImportState.Warning: - return this.getErrorDescription(row); + return this.getErrorDescription(row) ?? _(`This row will not be imported, due to an unknown reason.`); case ImportState.New: return this.translate.instant(this.modelName) + ` ` + this.translate.instant(`will be imported`); case ImportState.Done: // item has been imported @@ -353,10 +392,12 @@ export class ViaBackendImportListComponent implements On this._previewColumns = undefined; this._summary = undefined; this._rows = undefined; + this._hasErrors = false; } else { this._previewColumns = previews[0].headers; this._summary = previews.flatMap(preview => preview.statistics).filter(point => point.value); this._rows = this.calculateRows(previews); + this._hasErrors = this.importer.previewHasRowErrors; } } @@ -374,183 +415,4 @@ export class ViaBackendImportListComponent implements On return []; } } - - // @Input() - // public columns?: ImportListHeaderDefinition[]; - - // @Input() - // public headerDefinition?: ImportViaBackendPreviewHeader; - - // @Input() - // public showUnknownHeaders = true; - - // public get hasLeftReceivedHeaders(): boolean { - // return this.leftReceivedHeaders.length > 0; - // } - - // public get leftReceivedHeaders(): string[] { - // return this._importer.leftReceivedHeaders; - // } - - // public get leftExpectedHeaders(): { [key: string]: string } { - // return this._importer.leftExpectedHeaders; - // } - - /** - * @returns the amount of import items that will be imported - */ - // public get newCount(): number { - // return 0; // TODO: implement! - // // return this._importer && this.hasFile ? this._importer.summary.new : 0; - // } - - /** - * @returns the number of import items that cannot be imported - */ - // public get nonImportableCount(): number { - // if (this._importer && this.hasFile) { - // return 0; // TODO: Implement! - // // return this._importer.summary.errors + this._importer.summary.duplicates; - // } - // return 0; - // } - - /** - * @returns the number of import items that have been successfully imported - */ - // public get doneCount(): number { - // return 0; // TODO: implement! - // // return this._importer && this.hasFile ? this._importer.summary.done : 0; - // } - - // public get importPreviewLabel(): string { - // return `${this.modelName || `Models`} will be imported.`; - // } - - // public get importDoneLabel(): string { - // return `${this.modelName || `Models`} have been imported.`; - // } - - // public get importingStepsObservable(): Observable { - // return null; // TODO: implement! - // // return this._importer.importingStepsObservable; - // } - - // public headerValueMap: any = {}; - - /** - * Initializes the table - */ - // public initTable(): void { - // const newEntriesObservable = this._importer.getNewEntriesObservable(); - // this.hasFile = newEntriesObservable.pipe( - // distinctUntilChanged(), - // auditTime(100), - // map(entries => entries.length > 0) - // ); - - // this._dataSource = newEntriesObservable; - // } - - /** - * Updates and manually triggers the filter function. - * See {@link hidden} for options - * (changed from default mat-table filter) - */ - // public setFilter(): void { - // if (this.shown === `all`) { - // // this.vScrollDataSource.setFilter(); - // } else if (this.shown === `noerror`) { - // // const noErrorFilter = (data: ImportModel) => data.status === `done` || data.status !== `error`; - // // this.vScrollDataSource.setFilter(noErrorFilter); - // } else if (this.shown === `error`) { - // // const hasErrorFilter = (data: ImportModel) => - // // data.status === `error` || !!data.errors.length || data.hasDuplicates; - // // this.vScrollDataSource.setFilter(hasErrorFilter); - // } - // } - - /** - * Get the appropiate css class for a row according to the import state - * - * @param row a newEntry object with a current status - * @returns a css class name - */ - // public getStateClass(row: ImportViaBackendPreviewRow): string { - // switch (row.status) { - // case `done`: - // return `import-done import-decided`; - // case `error`: - // return `import-error`; - // default: - // return ``; - // } - // } - - // public getTooltip(value: string | CsvMapping[]): string { - // if (Array.isArray(value)) { - // return value.map(entry => entry.name).join(`;\n\r`); - // } - // return value; - // } - // public isUnknown(headerKey: string): boolean { - // return !this._importer.headerValues[headerKey]; - // } - - // public onChangeUnknownHeaderKey(headerKey: string, value: string): void { - // this.headerValueMap[headerKey] = value; - // this._importer.setNewHeaderValue({ [headerKey]: value }); - // } - - // public isArray(data: any): boolean { - // return Array.isArray(data); - // } - - // public isObject(data: any): boolean { - // return typeof data === `object`; - // } - - // public getLabelByStepPhase(phase: ImportViaBackendPhase): string { - // switch (phase) { - // case ImportViaBackendPhase.FINISHED: - // return _(`have been created`); - // case ImportViaBackendPhase.ERROR: - // return _(`could not be created`); - // default: - // return _(`will be created`); - // } - // } - - // public isTrue(value: any) { - // return [`true`, 1, true, `1`].includes(value); - // } - - // private createColumns(): ImportViaBackendPreviewHeader[] { - // const getHeaderProp = (prop: string) => { - // return prop.startsWith(`newEntry.`) ? prop.slice(`newEntry.`.length) : prop; - // }; - // const definitions = this.columns ?? [this.headerDefinition]; - // if (!definitions) { - // throw new Error(`You have to specify the columns to show`); - // } - // if (Array.isArray(definitions) && definitions.length > 0) { - // const computedDefinitions = definitions - // .filter((definition: ImportViaBackendPreviewHeader) => definition.isTableColumn) - // .map(column => ({ - // ...column, - // property: getHeaderProp(column.property), - // type: this.getTypeByProperty(getHeaderProp(column.property)) - // })); - // return computedDefinitions; - // } - // return []; - // } - - // private getTypeByProperty(property: string): 'boolean' | 'string' { - // if (property.startsWith(`is`) || property.startsWith(`has`)) { - // return `boolean`; - // } else { - // return `string`; - // } - // } } diff --git a/client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts b/client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts index 4c014c5647..92d93fbe8e 100644 --- a/client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts +++ b/client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts @@ -56,8 +56,13 @@ export interface ImportViaBackendIndexedPreview { statistics: ImportViaBackendPreviewSummary[]; } -export interface ImportViaBackendJSONUploadResponse { - success: boolean; - message: string; - results: ImportViaBackendPreview[]; +export function isImportViaBackendPreview(obj: any): obj is ImportViaBackendPreview { + return ( + obj && + typeof obj === `object` && + typeof obj.id === `number` && + Array.isArray(obj.headers) && + Array.isArray(obj.rows) && + Array.isArray(obj.statistics) + ); } From 02017b933554eef792d6d4e7169c4fe52f43802f Mon Sep 17 00:00:00 2001 From: Luisa Date: Wed, 3 May 2023 18:18:34 +0200 Subject: [PATCH 08/16] Some comment cleanup --- .../utils/import/import-utils.ts | 4 - .../base-via-backend-import.service.ts | 87 ++++++++++--------- .../topic-import.service.ts | 50 +---------- .../agenda-item-export.service.ts | 1 - .../via-backend-import-list.component.ts | 20 +---- 5 files changed, 51 insertions(+), 111 deletions(-) diff --git a/client/src/app/infrastructure/utils/import/import-utils.ts b/client/src/app/infrastructure/utils/import/import-utils.ts index 7c88a37f1d..9330506555 100644 --- a/client/src/app/infrastructure/utils/import/import-utils.ts +++ b/client/src/app/infrastructure/utils/import/import-utils.ts @@ -72,10 +72,6 @@ export type ImportConfig = StaticMainImportConfig
{ - modelHeadersAndVerboseNames: K; -} - export const DUPLICATE_IMPORT_ERROR = `Duplicates`; export interface CsvValueParsingConfig { diff --git a/client/src/app/site/base/base-import.service/base-via-backend-import.service.ts b/client/src/app/site/base/base-import.service/base-via-backend-import.service.ts index 232d1ff6b2..d0a5bc3f46 100644 --- a/client/src/app/site/base/base-import.service/base-via-backend-import.service.ts +++ b/client/src/app/site/base/base-import.service/base-via-backend-import.service.ts @@ -4,11 +4,7 @@ import { TranslateService } from '@ngx-translate/core'; import { Papa, ParseConfig } from 'ngx-papaparse'; import { BehaviorSubject, Observable } from 'rxjs'; import { Identifiable } from 'src/app/domain/interfaces'; -import { - FileReaderProgressEvent, - ValueLabelCombination, - ViaBackendImportConfig -} from 'src/app/infrastructure/utils/import/import-utils'; +import { FileReaderProgressEvent, ValueLabelCombination } from 'src/app/infrastructure/utils/import/import-utils'; import { ViaBackendImportService } from 'src/app/ui/base/import-service'; import { ImportViaBackendPhase } from 'src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component'; import { @@ -25,18 +21,6 @@ import { ImportServiceCollectorService } from '../../services/import-service-col export abstract class BaseViaBackendImportService implements ViaBackendImportService { - public chunkSize = 100; - - /** - * List of possible errors and their verbose explanation - */ - public errorList: { [errorKey: string]: string } = {}; - - /** - * The headers expected in the CSV matching import properties (in order) - */ - public expectedHeaders: string[] = []; - /** * The minimimal number of header entries needed to successfully create an entry */ @@ -92,33 +76,25 @@ export abstract class BaseViaBackendImportService result.id) ?? []; + return this._previewActionIds; } public get previewHasRowErrors(): boolean { - return this.previews?.some(preview => preview.rows.some(row => row.status === ImportState.Error)) || false; + return this._previewHasRowErrors; } - public get previews(): ImportViaBackendIndexedPreview[] { - return this._previews; + public get previewsObservable(): Observable { + return this._previewsSubject as Observable; } - private set previews(preview: ImportViaBackendIndexedPreview[] | null) { - this._previews = preview; - this._previewsSubject.next(preview); + public get currentImportPhaseObservable(): Observable { + return this._currentImportPhaseSubject as Observable; } /** - * storing the summary preview for the import, to avoid recalculating it - * at each display change. + * List of possible errors and their verbose explanation. */ - private _previews: ImportViaBackendIndexedPreview[] | null = null; - - public get previewsObservable(): Observable { - return this._previewsSubject as Observable; - } - - private _previewsSubject = new BehaviorSubject(null); + protected abstract readonly errorList: { [errorKey: string]: string }; protected readonly translate: TranslateService = this.importServiceCollector.translate; protected readonly matSnackbar: MatSnackBar = this.importServiceCollector.matSnackBar; @@ -130,6 +106,21 @@ export abstract class BaseViaBackendImportService result.id) ?? []; + this._previewHasRowErrors = + this._previews?.some(preview => preview.rows.some(row => row.status === ImportState.Error)) || false; + this._previewsSubject.next(preview); + } + + private _previews: ImportViaBackendIndexedPreview[] | null = null; + + private _previewsSubject = new BehaviorSubject(null); + + private _previewActionIds: number[] = []; + private _previewHasRowErrors: boolean = false; + /** * The last parsed file object (may be reparsed with new encoding, thus kept in memory) */ @@ -142,16 +133,12 @@ export abstract class BaseViaBackendImportService { - return this._currentImportPhaseSubject as Observable; - } - private _currentImportPhaseSubject = new BehaviorSubject( ImportViaBackendPhase.LOADING_PREVIEW ); /** - * the list of parsed models that have been extracted from the opened file + * the list of parsed models that have been extracted from the opened file or inserted manually */ private _csvLines: { [header: string]: string }[] = []; private _receivedHeaders: string[] = []; @@ -165,8 +152,6 @@ export abstract class BaseViaBackendImportService { this.parseInput(event.target?.result as string); }; - const config = this.getConfig(); - this.expectedHeaders = Object.keys(config.modelHeadersAndVerboseNames); } /** @@ -254,6 +239,9 @@ export abstract class BaseViaBackendImportService; + /** + * Calls the relevant import backend action with the payload. + * + * If abort is set to true, the import should be aborted, + * i.e. the data should NOT be imported, but instead the action worker should be deleted. + */ protected abstract import( actionWorkerIds: number[], abort?: boolean ): Promise; - protected abstract getConfig(): ViaBackendImportConfig; } diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts index e0d3bbacc1..bdf0fbc21c 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts @@ -1,16 +1,13 @@ import { Injectable } from '@angular/core'; import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; -import { AgendaItemType, ItemTypeChoices } from 'src/app/domain/models/agenda/agenda-item'; +import { AgendaItemType } from 'src/app/domain/models/agenda/agenda-item'; import { Topic } from 'src/app/domain/models/topics/topic'; import { TopicRepositoryService } from 'src/app/gateways/repositories/topics/topic-repository.service'; -import { ViaBackendImportConfig } from 'src/app/infrastructure/utils/import/import-utils'; import { BaseViaBackendImportService } from 'src/app/site/base/base-import.service/base-via-backend-import.service'; import { ActiveMeetingIdService } from 'src/app/site/pages/meetings/services/active-meeting-id.service'; -import { DurationService } from 'src/app/site/services/duration.service'; import { ImportServiceCollectorService } from 'src/app/site/services/import-service-collector.service'; import { ImportViaBackendPreview } from 'src/app/ui/modules/import-list/definitions/import-via-backend-preview'; -import { topicHeadersAndVerboseNames } from '../../../../definitions'; import { TopicExportService } from '../topic-export.service'; import { TopicImportServiceModule } from '../topic-import-service.module'; @@ -49,7 +46,6 @@ export class TopicImportService extends BaseViaBackendImportService { */ public constructor( serviceCollector: ImportServiceCollectorService, - private durationService: DurationService, private repo: TopicRepositoryService, private exporter: TopicExportService, private activeMeetingId: ActiveMeetingIdService @@ -61,12 +57,6 @@ export class TopicImportService extends BaseViaBackendImportService { this.exporter.downloadCsvImportExample(); } - protected getConfig(): ViaBackendImportConfig { - return { - modelHeadersAndVerboseNames: topicHeadersAndVerboseNames - }; - } - protected override calculateJsonUploadPayload(): any { let payload = super.calculateJsonUploadPayload(); payload[`meeting_id`] = this.activeMeetingId.meetingId; @@ -84,44 +74,6 @@ export class TopicImportService extends BaseViaBackendImportService { return await this.repo.jsonUpload(payload).resolve(); } - // protected override pipeParseValue(value: string, header: any): any { - // if (header === `agenda_duration`) { - // return this.parseDuration(value); - // } - // if (header === `agenda_type`) { - // return this.parseType(value); - // } - // } - - /** - * Matching the duration string/number to the time model in use - * - * @param input - * @returns duration as defined in durationService - */ - public parseDuration(input: string): number { - return this.durationService.stringToDuration(input); - } - - /** - * Converts information from 'item type' to a model-based type number. - * Accepts either old syntax (numbers) or new visibility choice csv names; - * both defined in {@link itemVisibilityChoices} - * Empty values will be interpreted as default 'public' agenda topics - * - * @param input - * @returns a number as defined for the itemVisibilityChoices - */ - public parseType(input: string | number): AgendaItemType { - if (typeof input === `string`) { - const visibility = ItemTypeChoices.find(choice => choice.csvName === input); - if (visibility) { - return visibility.key; - } - } - return AgendaItemType.COMMON; // default, public item - } - /** * parses the data given by the textArea. Expects an agenda title per line * diff --git a/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-item-list/services/agenda-item-export.service/agenda-item-export.service.ts b/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-item-list/services/agenda-item-export.service/agenda-item-export.service.ts index bf1ed55f52..c26e22c7c8 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-item-list/services/agenda-item-export.service/agenda-item-export.service.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/pages/agenda-item-list/services/agenda-item-export.service/agenda-item-export.service.ts @@ -40,7 +40,6 @@ export class AgendaItemExportService { { label: `agenda_duration`, property: `duration` }, { label: `agenda_comment`, property: `comment` }, { label: `agenda_type`, property: `verboseCsvType` } - // { label: `tags`, property: `tags` } ], this.translate.instant(`Agenda`) + `.csv` ); diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts index 1dbb36a9ff..8713598860 100644 --- a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts @@ -89,7 +89,7 @@ export class ViaBackendImportListComponent implements On private _importer!: ViaBackendImportService; /** - * Defines all necessary and optional fields, that a .csv-file has to contain. + * Defines all necessary and optional fields, that a .csv-file can contain. */ @Input() public possibleFields: string[] = []; @@ -155,11 +155,6 @@ export class ViaBackendImportListComponent implements On */ public selectedEncoding = `utf-8`; - /** - * indicator on which elements to display - */ - public shown: 'all' | 'error' | 'noerror' = `all`; - /** * @returns the encodings available and their labels */ @@ -246,7 +241,6 @@ export class ViaBackendImportListComponent implements On /** * Triggers the importer's import - * */ public async doImport(): Promise { this._importer.doImport(); @@ -265,7 +259,7 @@ export class ViaBackendImportListComponent implements On /** * Get the icon for the action of the item - * @param entry a newEntry object with a current status + * @param entry a row or an entry with a current status * @eturn the icon for the action of the item */ public getActionIcon(entry: ImportViaBackendPreviewIndexedRow): string { @@ -312,8 +306,6 @@ export class ViaBackendImportListComponent implements On /** * Trigger for the column separator selection. - * - * @param event */ public selectColSep(event: MatSelectChange): void { this._importer.columnSeparator = event.value; @@ -322,8 +314,6 @@ export class ViaBackendImportListComponent implements On /** * Trigger for the column separator selection - * - * @param event */ public selectTextSep(event: MatSelectChange): void { this._importer.textSeparator = event.value; @@ -340,8 +330,6 @@ export class ViaBackendImportListComponent implements On /** * Trigger for the encoding selection - * - * @param event */ public selectEncoding(event: MatSelectChange): void { this._importer.encoding = event.value; @@ -359,9 +347,9 @@ export class ViaBackendImportListComponent implements On } /** - * Checks if an error is present in a new entry + * Checks if an error is present in a row * - * @param row the NewEntry + * @param row the row * @param error An error as defined as key of {@link errorList} * @returns true if the error is present in the entry described in the row */ From 1842896d25ed48b700f9b7ac9e91541b46cf04f2 Mon Sep 17 00:00:00 2001 From: Luisa Date: Thu, 4 May 2023 14:16:59 +0200 Subject: [PATCH 09/16] cleanup 3 --- .../base-via-backend-import.service.ts | 56 +++--- .../base-via-backend-import-list.component.ts | 15 ++ .../topic-import.service.ts | 4 +- client/src/app/ui/base/import-service.ts | 6 +- .../via-backend-import-list.component.html | 5 +- .../via-backend-import-list.component.ts | 176 ++++++++++++------ .../definitions/import-via-backend-preview.ts | 22 +-- 7 files changed, 184 insertions(+), 100 deletions(-) diff --git a/client/src/app/site/base/base-import.service/base-via-backend-import.service.ts b/client/src/app/site/base/base-import.service/base-via-backend-import.service.ts index d0a5bc3f46..e0e2720312 100644 --- a/client/src/app/site/base/base-import.service/base-via-backend-import.service.ts +++ b/client/src/app/site/base/base-import.service/base-via-backend-import.service.ts @@ -11,7 +11,6 @@ import { ImportState, ImportViaBackendIndexedPreview, ImportViaBackendPreview, - ImportViaBackendPreviewRow, isImportViaBackendPreview } from 'src/app/ui/modules/import-list/definitions/import-via-backend-preview'; @@ -71,22 +70,37 @@ export abstract class BaseViaBackendImportService { return this._rawFileSubject.asObservable(); } + /** + * Action worker ids that will trigger the currently previewed import. + */ public get previewActionIds(): number[] { return this._previewActionIds; } + /** + * If false there is something wrong with the data. + */ public get previewHasRowErrors(): boolean { - return this._previewHasRowErrors; + return this._previews?.some(preview => preview.state === ImportState.Error) ?? false; } + /** + * Observable that passes updates on the current preview. + */ public get previewsObservable(): Observable { return this._previewsSubject as Observable; } + /** + * Observable that allows one to monitor the part of the import that is currently in process. + */ public get currentImportPhaseObservable(): Observable { return this._currentImportPhaseSubject as Observable; } @@ -99,18 +113,14 @@ export abstract class BaseViaBackendImportService result.id) ?? []; - this._previewHasRowErrors = - this._previews?.some(preview => preview.rows.some(row => row.status === ImportState.Error)) || false; this._previewsSubject.next(preview); } @@ -119,7 +129,6 @@ export abstract class BaseViaBackendImportService(null); private _previewActionIds: number[] = []; - private _previewHasRowErrors: boolean = false; /** * The last parsed file object (may be reparsed with new encoding, thus kept in memory) @@ -175,12 +184,20 @@ export abstract class BaseViaBackendImportService */ public getShortPreview = getShortPreview; + /** + * True if the import is in a state in which an import can be conducted + */ public get canImport(): boolean { return this._state === ImportViaBackendPhase.AWAITING_CONFIRM || this.tryAgain; } + /** + * True if the import has successfully finished. + */ public get finishedSuccessfully(): boolean { return this._state === ImportViaBackendPhase.FINISHED; } + /** + * True if, after an attempted import failed, the view is waiting for the user to confirm the import on the new preview. + */ public get tryAgain(): boolean { return this._state === ImportViaBackendPhase.TRY_AGAIN; } + /** + * True while an import is in progress. + */ public get isImporting(): boolean { return this._state === ImportViaBackendPhase.IMPORTING; } + /** + * True if the preview can not be imported. + */ public get hasErrors(): boolean { return this._state === ImportViaBackendPhase.ERROR; } diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts index bdf0fbc21c..f1055d81f4 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts @@ -28,9 +28,7 @@ export class TopicImportService extends BaseViaBackendImportService { ParsingErrors: _(`Some csv values could not be read correctly.`) }; - public override readonly isMeetingImport = true; - - public override readonly verboseSummaryTitleMap: { [title: string]: string } = { + public override readonly verboseSummaryTitles: { [title: string]: string } = { total: _(`Total topics`), created: _(`Topics created`), updated: _(`Topics updated`), diff --git a/client/src/app/ui/base/import-service.ts b/client/src/app/ui/base/import-service.ts index 5ac53dac11..839de98932 100644 --- a/client/src/app/ui/base/import-service.ts +++ b/client/src/app/ui/base/import-service.ts @@ -5,10 +5,7 @@ import { ImportStep } from 'src/app/infrastructure/utils/import/import-step'; import { ValueLabelCombination } from 'src/app/infrastructure/utils/import/import-utils'; import { ImportViaBackendPhase } from '../modules/import-list/components/via-backend-import-list/via-backend-import-list.component'; -import { - ImportViaBackendIndexedPreview, - ImportViaBackendPreviewRow -} from '../modules/import-list/definitions/import-via-backend-preview'; +import { ImportViaBackendIndexedPreview } from '../modules/import-list/definitions/import-via-backend-preview'; interface ImportServicePreview { new: number; @@ -59,7 +56,6 @@ export interface ViaBackendImportService { encoding: string; verbose(error: string): string; - hasError(row: ImportViaBackendPreviewRow, error: string): boolean; refreshFile(): void; clearPreview(): void; clearFile(): void; diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html index a76e0c07b9..3ae35335a8 100644 --- a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html @@ -150,9 +150,6 @@

{{ 'Preview' | translate }}

- - warning - {{ 'Preview' | translate }} class="flex-vertical-center" > diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts index 8713598860..17269a3770 100644 --- a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts @@ -31,6 +31,7 @@ import { ImportViaBackendIndexedPreview, ImportViaBackendPreviewHeader, ImportViaBackendPreviewIndexedRow, + ImportViaBackendPreviewObject, ImportViaBackendPreviewSummary } from '../../definitions/import-via-backend-preview'; import { ImportListFirstTabDirective } from '../../directives/import-list-first-tab.directive'; @@ -99,56 +100,83 @@ export class ViaBackendImportListComponent implements On public readonly Phase = ImportViaBackendPhase; + /** + * Observable that allows one to monitor the currenty selected file. + */ public get rawFileObservable(): Observable { return this._importer?.rawFileObservable || of(null); } - public get defaultColumns(): ImportListHeaderDefinition[] { - return this._defaultColumns; - } - + /** + * Client-side definition of required/accepted columns. + * Ensures that the client can display information about how the import works. + */ @Input() public set defaultColumns(cols: ImportListHeaderDefinition[]) { this._defaultColumns = cols; + this.setHeaders({ default: cols }); + } + public get defaultColumns(): ImportListHeaderDefinition[] { + return this._defaultColumns; } + /** + * The actual headers of the preview, as they were delivered by the backend. + */ public get previewColumns(): ImportViaBackendPreviewHeader[] { return this._previewColumns; } + /** + * The summary of the preview, as it was delivered by the backend. + */ public get summary(): ImportViaBackendPreviewSummary[] { return this._summary; } + /** + * The rows of the preview, which were delivered by the backend. + * Affixed with fake ids for the purpose of displaying them correctly. + */ public get rows(): ImportViaBackendPreviewIndexedRow[] { return this._rows; } - private _summary: ImportViaBackendPreviewSummary[]; - private _rows: ImportViaBackendPreviewIndexedRow[]; - private _previewColumns: ImportViaBackendPreviewHeader[]; + /** + * True if, after the first json-upload, the view is waiting for the user to confirm the import. + */ public get awaitingConfirm(): boolean { - return this._state === ImportViaBackendPhase.AWAITING_CONFIRM || this.tryAgain; + return this._state === ImportViaBackendPhase.AWAITING_CONFIRM; } + /** + * True if the import has successfully finished. + */ public get finishedSuccessfully(): boolean { return this._state === ImportViaBackendPhase.FINISHED; } + /** + * True if, after an attempted import failed, the view is waiting for the user to confirm the import on the new preview. + */ public get tryAgain(): boolean { return this._state === ImportViaBackendPhase.TRY_AGAIN; } + /** + * True while an import is in progress. + */ public get isImporting(): boolean { return this._state === ImportViaBackendPhase.IMPORTING; } + /** + * True if the preview can not be imported. + */ public get hasErrors(): boolean { return this._state === ImportViaBackendPhase.ERROR; } - private _state: ImportViaBackendPhase = ImportViaBackendPhase.LOADING_PREVIEW; - /** * Currently selected encoding. Is set and changed by the config's available * encodings and user mat-select input @@ -176,31 +204,49 @@ export class ViaBackendImportListComponent implements On return this._importer.textSeparators; } + /** + * If false there is something wrong with the data. + */ public get hasRowErrors(): boolean { - return this._hasErrors; + return this._importer.previewHasRowErrors; } - private _hasErrors: boolean = false; - + /** + * Client side information on the required fields of this import. + * Generated from the information in the defaultColumns. + */ public get requiredFields(): string[] { return this._requiredFields; } + /** + * The Observable from which the views table will be calculated + */ public get dataSource(): Observable { return this._dataSource; } + private _state: ImportViaBackendPhase = ImportViaBackendPhase.LOADING_PREVIEW; + + private _summary: ImportViaBackendPreviewSummary[]; + private _rows: ImportViaBackendPreviewIndexedRow[]; + private _previewColumns: ImportViaBackendPreviewHeader[]; + private _dataSource: Observable = of([]); private _requiredFields: string[] = []; private _defaultColumns: ImportListHeaderDefinition[] = []; + private _headers: { + [property: string]: { default?: ImportListHeaderDefinition; preview?: ImportViaBackendPreviewHeader }; + } = {}; + public constructor(private dialog: MatDialog, private translate: TranslateService) {} /** * Starts with a clean preview (removing any previously existing import previews) */ public ngOnInit(): void { - this._importer.clearPreview(); + this._importer.clearAll(); this._requiredFields = this.createRequiredFields(); this._importer.currentImportPhaseObservable.subscribe(phase => { this._state = phase; @@ -214,9 +260,11 @@ export class ViaBackendImportListComponent implements On ); } + /** + * Resets the importer when leaving the view + */ public ngOnDestroy(): void { this._importer.clearFile(); - this._importer.clearPreview(); } /** @@ -228,6 +276,9 @@ export class ViaBackendImportListComponent implements On this.selectedTabChanged.emit(index); } + /** + * True if there are custom tabs. + */ public hasSeveralTabs(): boolean { return this.importListFirstTabs.length + this.importListLastTabs.length > 0; } @@ -246,6 +297,9 @@ export class ViaBackendImportListComponent implements On this._importer.doImport(); } + /** + * Removes the selected file and also empties the preview. + */ public removeSelectedFile(): void { if (this.fileInput) { this.fileInput.nativeElement.value = ``; @@ -253,17 +307,34 @@ export class ViaBackendImportListComponent implements On this._importer.clearFile(); } + /** + * Gets the relevant backend header information for a property. + */ public getHeader(propertyName: string): ImportViaBackendPreviewHeader { - return this._previewColumns.find(header => header.property === propertyName); + return this._headers[propertyName]?.preview; + } + + /** + * Gets the width of the column for the given property. + */ + public getColumnWidth(propertyName: string): number { + return this._headers[propertyName]?.default?.width ?? 50; + } + + /** + * Gets the label of the column for the given property. + */ + public getColumnLabel(propertyName: string): string { + return this._headers[propertyName]?.default?.label ?? propertyName; } /** - * Get the icon for the action of the item - * @param entry a row or an entry with a current status - * @eturn the icon for the action of the item + * Get the icon for the the item + * @param item a row or an entry with a current state + * @eturn the icon for the item */ - public getActionIcon(entry: ImportViaBackendPreviewIndexedRow): string { - switch (entry.status) { + public getActionIcon(item: ImportViaBackendPreviewIndexedRow | ImportViaBackendPreviewObject): string { + switch (item[`state`] ?? item[`info`]) { case ImportState.Error: // no import possible return `block`; case ImportState.Warning: @@ -279,8 +350,13 @@ export class ViaBackendImportListComponent implements On } } + /** + * Get the correct tooltip for the item + * @param entry a row with a current state + * @eturn the tooltip for the item + */ public getRowTooltip(row: ImportViaBackendPreviewIndexedRow): string { - switch (row.status) { + switch (row.state) { case ImportState.Error: // no import possible return ( this.getErrorDescription(row) ?? @@ -320,16 +396,8 @@ export class ViaBackendImportListComponent implements On this._importer.refreshFile(); } - public getColumnWidth(propertyName: string): number { - return this.defaultColumns.find(col => col.property === propertyName)?.width ?? 50; - } - - public getColumnLabel(propertyName: string): string { - return this.defaultColumns.find(col => col.property === propertyName)?.label ?? propertyName; - } - /** - * Trigger for the encoding selection + * Trigger for the encoding selection. */ public selectEncoding(event: MatSelectChange): void { this._importer.encoding = event.value; @@ -337,42 +405,45 @@ export class ViaBackendImportListComponent implements On } /** - * Returns a descriptive string for an import error - * - * @param error The short string for the error as listed in the {@lilnk errorList} - * @returns a predefined descriptive error string from the importer + * Opens a fullscreen dialog with the given template as content. */ - public getVerboseError(error: string): string { - return this._importer.verbose(error); - } - - /** - * Checks if an error is present in a row - * - * @param row the row - * @param error An error as defined as key of {@link errorList} - * @returns true if the error is present in the entry described in the row - */ - public hasError(row: ImportViaBackendPreviewIndexedRow, error: string): boolean { - return this._importer.hasError(row, error); - } - public async enterFullscreen(dialogTemplate: TemplateRef): Promise { const ref = this.dialog.open(dialogTemplate, { width: `80vw` }); await firstValueFrom(ref.afterClosed()); } + /** + * Opens an info dialog with the given template as content. + */ public async openDialog(dialogTemplate: TemplateRef): Promise { const ref = this.dialog.open(dialogTemplate, infoDialogSettings); await firstValueFrom(ref.afterClosed()); } + /** + * Returns the verbose title for a given summary title. + */ public getSummaryPointTitle(title: string): string { return this._importer.getVerboseSummaryPointTitle(title); } + private setHeaders(data: { + default?: ImportListHeaderDefinition[]; + preview?: ImportViaBackendPreviewHeader[]; + }): void { + for (let key of Object.keys(data)) { + for (let header of data[key] ?? []) { + if (!this._headers[header.property]) { + this._headers[header.property] = { [key]: header }; + } else { + this._headers[header.property][key] = header; + } + } + } + } + private getErrorDescription(entry: ImportViaBackendPreviewIndexedRow): string { - return entry.message?.map(error => this.translate.instant(this.getVerboseError(error))).join(`,\n `); + return entry.message?.map(error => this.translate.instant(this._importer.verbose(error))).join(`,\n `); } private fillPreviewData(previews: ImportViaBackendIndexedPreview[]) { @@ -380,12 +451,11 @@ export class ViaBackendImportListComponent implements On this._previewColumns = undefined; this._summary = undefined; this._rows = undefined; - this._hasErrors = false; } else { this._previewColumns = previews[0].headers; this._summary = previews.flatMap(preview => preview.statistics).filter(point => point.value); this._rows = this.calculateRows(previews); - this._hasErrors = this.importer.previewHasRowErrors; + this.setHeaders({ preview: this._previewColumns }); } } diff --git a/client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts b/client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts index 92d93fbe8e..1b451873f9 100644 --- a/client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts +++ b/client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts @@ -15,19 +15,16 @@ export interface ImportViaBackendPreviewHeader { is_list: boolean; // optional, if not given defaults to `false` } -export type ImportViaBackendPreviewModelData = - | null - | boolean - | number - | string - | { - value: boolean | number | string; - info: ImportState; - type: `boolean` | `number` | `string` | `date`; - }; +export type ImportViaBackendPreviewModelData = null | boolean | number | string | ImportViaBackendPreviewObject; + +export type ImportViaBackendPreviewObject = { + value: boolean | number | string; + info: ImportState; + type: `boolean` | `number` | `string` | `date`; +}; export interface ImportViaBackendPreviewRow { - status: ImportState; + state: ImportState; message: string[]; data: { // property name and type must match an entry in the given `headers` @@ -44,6 +41,7 @@ export interface ImportViaBackendPreviewSummary { export interface ImportViaBackendPreview { id: number; // id of action_worker to import + state: ImportState; // May be `error`, `warning` or `done` headers: ImportViaBackendPreviewHeader[]; rows: ImportViaBackendPreviewRow[]; statistics: ImportViaBackendPreviewSummary[]; @@ -51,6 +49,7 @@ export interface ImportViaBackendPreview { export interface ImportViaBackendIndexedPreview { id: number; // id of action_worker to import + state: ImportState; // May be `error`, `warning` or `done` headers: ImportViaBackendPreviewHeader[]; rows: ImportViaBackendPreviewIndexedRow[]; statistics: ImportViaBackendPreviewSummary[]; @@ -61,6 +60,7 @@ export function isImportViaBackendPreview(obj: any): obj is ImportViaBackendPrev obj && typeof obj === `object` && typeof obj.id === `number` && + typeof obj.state === `string` && Array.isArray(obj.headers) && Array.isArray(obj.rows) && Array.isArray(obj.statistics) From f38feef1dbfecd3ce03877a3c17fd2c2fc610d09 Mon Sep 17 00:00:00 2001 From: Luisa Date: Thu, 4 May 2023 14:49:01 +0200 Subject: [PATCH 10/16] cleanup 4 --- .../via-backend-import-list.component.ts | 2 +- .../import-list/definitions/import-via-backend-preview.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts index 17269a3770..6ca29f5e20 100644 --- a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts @@ -443,7 +443,7 @@ export class ViaBackendImportListComponent implements On } private getErrorDescription(entry: ImportViaBackendPreviewIndexedRow): string { - return entry.message?.map(error => this.translate.instant(this._importer.verbose(error))).join(`,\n `); + return entry.messages?.map(error => this.translate.instant(this._importer.verbose(error))).join(`,\n `); } private fillPreviewData(previews: ImportViaBackendIndexedPreview[]) { diff --git a/client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts b/client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts index 1b451873f9..50eda51080 100644 --- a/client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts +++ b/client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts @@ -25,7 +25,7 @@ export type ImportViaBackendPreviewObject = { export interface ImportViaBackendPreviewRow { state: ImportState; - message: string[]; + messages: string[]; data: { // property name and type must match an entry in the given `headers` [property: string]: ImportViaBackendPreviewModelData | ImportViaBackendPreviewModelData[]; // if is_list is set in corresponding header column, we need here also a list From 452ff5209981a583d7a7d474ee7b664c700831c4 Mon Sep 17 00:00:00 2001 From: Luisa Date: Thu, 4 May 2023 17:26:04 +0200 Subject: [PATCH 11/16] Used better file and class names --- .../topics/topic-repository.service.ts | 10 +- ...vice.ts => base-backend-import.service.ts} | 72 +++++++------- .../base-via-backend-import-list.component.ts | 18 ++-- .../topic-import/topic-import.component.html | 4 +- .../topic-import.service.ts | 10 +- client/src/app/ui/base/import-service.ts | 10 +- ...tml => backend-import-list.component.html} | 0 ...css => backend-import-list.component.scss} | 0 ... => backend-import-list.component.spec.ts} | 10 +- ...nt.ts => backend-import-list.component.ts} | 99 +++++++++---------- .../definitions/backend-import-preview.ts | 68 +++++++++++++ .../definitions/import-via-backend-preview.ts | 68 ------------- .../modules/import-list/import-list.module.ts | 4 +- 13 files changed, 184 insertions(+), 189 deletions(-) rename client/src/app/site/base/base-import.service/{base-via-backend-import.service.ts => base-backend-import.service.ts} (80%) rename client/src/app/ui/modules/import-list/components/via-backend-import-list/{via-backend-import-list.component.html => backend-import-list.component.html} (100%) rename client/src/app/ui/modules/import-list/components/via-backend-import-list/{via-backend-import-list.component.scss => backend-import-list.component.scss} (100%) rename client/src/app/ui/modules/import-list/components/via-backend-import-list/{via-backend-import-list.component.spec.ts => backend-import-list.component.spec.ts} (54%) rename client/src/app/ui/modules/import-list/components/via-backend-import-list/{via-backend-import-list.component.ts => backend-import-list.component.ts} (81%) create mode 100644 client/src/app/ui/modules/import-list/definitions/backend-import-preview.ts delete mode 100644 client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts diff --git a/client/src/app/gateways/repositories/topics/topic-repository.service.ts b/client/src/app/gateways/repositories/topics/topic-repository.service.ts index 67ce941f4d..e631635649 100644 --- a/client/src/app/gateways/repositories/topics/topic-repository.service.ts +++ b/client/src/app/gateways/repositories/topics/topic-repository.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { Identifiable } from 'src/app/domain/interfaces'; import { Topic } from 'src/app/domain/models/topics/topic'; import { ViewAgendaItem, ViewTopic } from 'src/app/site/pages/meetings/pages/agenda'; -import { ImportViaBackendPreview } from 'src/app/ui/modules/import-list/definitions/import-via-backend-preview'; +import { BackendImportRawPreview } from 'src/app/ui/modules/import-list/definitions/backend-import-preview'; import { Action } from '../../actions'; import { createAgendaItem } from '../agenda'; @@ -43,12 +43,12 @@ export class TopicRepositoryService extends BaseAgendaItemAndListOfSpeakersConte return this.sendBulkActionToBackend(TopicAction.DELETE, payload); } - public jsonUpload(payload: { [key: string]: any }): Action { - return this.createAction(TopicAction.JSON_UPLOAD, payload); + public jsonUpload(payload: { [key: string]: any }): Action { + return this.createAction(TopicAction.JSON_UPLOAD, payload); } - public import(payload: { id: number; import: boolean }[]): Action { - return this.createAction(TopicAction.IMPORT, payload); + public import(payload: { id: number; import: boolean }[]): Action { + return this.createAction(TopicAction.IMPORT, payload); } public getTitle = (topic: ViewTopic) => topic.title; diff --git a/client/src/app/site/base/base-import.service/base-via-backend-import.service.ts b/client/src/app/site/base/base-import.service/base-backend-import.service.ts similarity index 80% rename from client/src/app/site/base/base-import.service/base-via-backend-import.service.ts rename to client/src/app/site/base/base-import.service/base-backend-import.service.ts index e0e2720312..8a4959aeac 100644 --- a/client/src/app/site/base/base-import.service/base-via-backend-import.service.ts +++ b/client/src/app/site/base/base-import.service/base-backend-import.service.ts @@ -5,20 +5,20 @@ import { Papa, ParseConfig } from 'ngx-papaparse'; import { BehaviorSubject, Observable } from 'rxjs'; import { Identifiable } from 'src/app/domain/interfaces'; import { FileReaderProgressEvent, ValueLabelCombination } from 'src/app/infrastructure/utils/import/import-utils'; -import { ViaBackendImportService } from 'src/app/ui/base/import-service'; -import { ImportViaBackendPhase } from 'src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component'; +import { BackendImportService } from 'src/app/ui/base/import-service'; +import { BackendImportPhase } from 'src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component'; import { - ImportState, - ImportViaBackendIndexedPreview, - ImportViaBackendPreview, - isImportViaBackendPreview -} from 'src/app/ui/modules/import-list/definitions/import-via-backend-preview'; + BackendImportPreview, + BackendImportRawPreview, + BackendImportState, + isBackendImportRawPreview +} from 'src/app/ui/modules/import-list/definitions/backend-import-preview'; import { ImportServiceCollectorService } from '../../services/import-service-collector.service'; @Directive() -export abstract class BaseViaBackendImportService - implements ViaBackendImportService +export abstract class BaseBackendImportService + implements BackendImportService { /** * The minimimal number of header entries needed to successfully create an entry @@ -88,21 +88,21 @@ export abstract class BaseViaBackendImportService preview.state === ImportState.Error) ?? false; + return this._previews?.some(preview => preview.state === BackendImportState.Error) ?? false; } /** * Observable that passes updates on the current preview. */ - public get previewsObservable(): Observable { - return this._previewsSubject as Observable; + public get previewsObservable(): Observable { + return this._previewsSubject as Observable; } /** * Observable that allows one to monitor the part of the import that is currently in process. */ - public get currentImportPhaseObservable(): Observable { - return this._currentImportPhaseSubject as Observable; + public get currentImportPhaseObservable(): Observable { + return this._currentImportPhaseSubject as Observable; } /** @@ -118,15 +118,15 @@ export abstract class BaseViaBackendImportService result.id) ?? []; this._previewsSubject.next(preview); } - private _previews: ImportViaBackendIndexedPreview[] | null = null; + private _previews: BackendImportPreview[] | null = null; - private _previewsSubject = new BehaviorSubject(null); + private _previewsSubject = new BehaviorSubject(null); private _previewActionIds: number[] = []; @@ -142,9 +142,7 @@ export abstract class BaseViaBackendImportService( - ImportViaBackendPhase.LOADING_PREVIEW - ); + private _currentImportPhaseSubject = new BehaviorSubject(BackendImportPhase.LOADING_PREVIEW); /** * the list of parsed models that have been extracted from the opened file or inserted manually @@ -233,10 +231,10 @@ export abstract class BaseViaBackendImportService { - this._currentImportPhaseSubject.next(ImportViaBackendPhase.IMPORTING); + this._currentImportPhaseSubject.next(BackendImportPhase.IMPORTING); const results = await this.import(this.previewActionIds, false); - if (Array.isArray(results) && results.find(result => isImportViaBackendPreview(result))) { + if (Array.isArray(results) && results.find(result => isBackendImportRawPreview(result))) { const updatedPreviews = results.filter(result => - isImportViaBackendPreview(result) - ) as ImportViaBackendPreview[]; + isBackendImportRawPreview(result) + ) as BackendImportRawPreview[]; this.processRawPreviews(updatedPreviews); if (this.previewHasRowErrors) { - this._currentImportPhaseSubject.next(ImportViaBackendPhase.ERROR); + this._currentImportPhaseSubject.next(BackendImportPhase.ERROR); } else { - this._currentImportPhaseSubject.next(ImportViaBackendPhase.TRY_AGAIN); + this._currentImportPhaseSubject.next(BackendImportPhase.TRY_AGAIN); } } else { - this._currentImportPhaseSubject.next(ImportViaBackendPhase.FINISHED); + this._currentImportPhaseSubject.next(BackendImportPhase.FINISHED); this._csvLines = []; } } @@ -340,20 +338,20 @@ export abstract class BaseViaBackendImportService { this.clearPreview(); const payload = this.calculateJsonUploadPayload(); - const response = (await this.jsonUpload(payload)) as ImportViaBackendPreview[]; + const response = (await this.jsonUpload(payload)) as BackendImportRawPreview[]; if (!response) { throw new Error(`Didn't receive preview`); } this.processRawPreviews(response); if (this.previewHasRowErrors) { - this._currentImportPhaseSubject.next(ImportViaBackendPhase.ERROR); + this._currentImportPhaseSubject.next(BackendImportPhase.ERROR); } else { - this._currentImportPhaseSubject.next(ImportViaBackendPhase.AWAITING_CONFIRM); + this._currentImportPhaseSubject.next(BackendImportPhase.AWAITING_CONFIRM); } } - private processRawPreviews(rawPreviews: ImportViaBackendPreview[]): void { - const previews: (ImportViaBackendPreview | ImportViaBackendIndexedPreview)[] = rawPreviews; + private processRawPreviews(rawPreviews: BackendImportRawPreview[]): void { + const previews: (BackendImportRawPreview | BackendImportPreview)[] = rawPreviews; let index = 1; for (let preview of previews) { for (let row of preview.rows) { @@ -362,7 +360,7 @@ export abstract class BaseViaBackendImportService; + protected abstract jsonUpload(payload: { [key: string]: any }): Promise; /** * Calls the relevant import backend action with the payload. * @@ -384,5 +382,5 @@ export abstract class BaseViaBackendImportService; + ): Promise; } diff --git a/client/src/app/site/base/base-via-backend-import-list.component.ts b/client/src/app/site/base/base-via-backend-import-list.component.ts index d126cda798..b37451c7f4 100644 --- a/client/src/app/site/base/base-via-backend-import-list.component.ts +++ b/client/src/app/site/base/base-via-backend-import-list.component.ts @@ -1,12 +1,12 @@ import { Directive, OnInit } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { BaseComponent } from 'src/app/site/base/base.component'; -import { ImportViaBackendPhase } from 'src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component'; +import { BackendImportPhase } from 'src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component'; import { Identifiable } from '../../domain/interfaces'; import { getLongPreview, getShortPreview } from '../../infrastructure/utils'; import { ComponentServiceCollectorService } from '../services/component-service-collector.service'; -import { BaseViaBackendImportService } from './base-import.service/base-via-backend-import.service'; +import { BaseBackendImportService } from './base-import.service/base-backend-import.service'; @Directive() export abstract class BaseViaBackendImportListComponent @@ -27,43 +27,43 @@ export abstract class BaseViaBackendImportListComponent * True if the import is in a state in which an import can be conducted */ public get canImport(): boolean { - return this._state === ImportViaBackendPhase.AWAITING_CONFIRM || this.tryAgain; + return this._state === BackendImportPhase.AWAITING_CONFIRM || this.tryAgain; } /** * True if the import has successfully finished. */ public get finishedSuccessfully(): boolean { - return this._state === ImportViaBackendPhase.FINISHED; + return this._state === BackendImportPhase.FINISHED; } /** * True if, after an attempted import failed, the view is waiting for the user to confirm the import on the new preview. */ public get tryAgain(): boolean { - return this._state === ImportViaBackendPhase.TRY_AGAIN; + return this._state === BackendImportPhase.TRY_AGAIN; } /** * True while an import is in progress. */ public get isImporting(): boolean { - return this._state === ImportViaBackendPhase.IMPORTING; + return this._state === BackendImportPhase.IMPORTING; } /** * True if the preview can not be imported. */ public get hasErrors(): boolean { - return this._state === ImportViaBackendPhase.ERROR; + return this._state === BackendImportPhase.ERROR; } - private _state: ImportViaBackendPhase = ImportViaBackendPhase.LOADING_PREVIEW; + private _state: BackendImportPhase = BackendImportPhase.LOADING_PREVIEW; public constructor( componentServiceCollector: ComponentServiceCollectorService, protected override translate: TranslateService, - protected importer: BaseViaBackendImportService + protected importer: BaseBackendImportService ) { super(componentServiceCollector, translate); } diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.html b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.html index ec2baca689..f6bad344ac 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.html +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.html @@ -23,7 +23,7 @@

{{ 'Import topics' | translate }}

-{{ 'Import topics' | translate }}
- + diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts index f1055d81f4..90e74445e2 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/services/topic-import.service/topic-import.service.ts @@ -3,10 +3,10 @@ import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; import { AgendaItemType } from 'src/app/domain/models/agenda/agenda-item'; import { Topic } from 'src/app/domain/models/topics/topic'; import { TopicRepositoryService } from 'src/app/gateways/repositories/topics/topic-repository.service'; -import { BaseViaBackendImportService } from 'src/app/site/base/base-import.service/base-via-backend-import.service'; +import { BaseBackendImportService } from 'src/app/site/base/base-import.service/base-backend-import.service'; import { ActiveMeetingIdService } from 'src/app/site/pages/meetings/services/active-meeting-id.service'; import { ImportServiceCollectorService } from 'src/app/site/services/import-service-collector.service'; -import { ImportViaBackendPreview } from 'src/app/ui/modules/import-list/definitions/import-via-backend-preview'; +import { BackendImportRawPreview } from 'src/app/ui/modules/import-list/definitions/backend-import-preview'; import { TopicExportService } from '../topic-export.service'; import { TopicImportServiceModule } from '../topic-import-service.module'; @@ -14,7 +14,7 @@ import { TopicImportServiceModule } from '../topic-import-service.module'; @Injectable({ providedIn: TopicImportServiceModule }) -export class TopicImportService extends BaseViaBackendImportService { +export class TopicImportService extends BaseBackendImportService { /** * The minimimal number of header entries needed to successfully create an entry */ @@ -64,11 +64,11 @@ export class TopicImportService extends BaseViaBackendImportService { protected async import( actionWorkerIds: number[], abort: boolean = false - ): Promise { + ): Promise { return await this.repo.import(actionWorkerIds.map(id => ({ id, import: !abort }))).resolve(); } - protected async jsonUpload(payload: { [key: string]: any }): Promise { + protected async jsonUpload(payload: { [key: string]: any }): Promise { return await this.repo.jsonUpload(payload).resolve(); } diff --git a/client/src/app/ui/base/import-service.ts b/client/src/app/ui/base/import-service.ts index 839de98932..9d18a5fc2a 100644 --- a/client/src/app/ui/base/import-service.ts +++ b/client/src/app/ui/base/import-service.ts @@ -4,8 +4,8 @@ import { ImportModel } from 'src/app/infrastructure/utils/import/import-model'; import { ImportStep } from 'src/app/infrastructure/utils/import/import-step'; import { ValueLabelCombination } from 'src/app/infrastructure/utils/import/import-utils'; -import { ImportViaBackendPhase } from '../modules/import-list/components/via-backend-import-list/via-backend-import-list.component'; -import { ImportViaBackendIndexedPreview } from '../modules/import-list/definitions/import-via-backend-preview'; +import { BackendImportPhase } from '../modules/import-list/components/via-backend-import-list/backend-import-list.component'; +import { BackendImportPreview } from '../modules/import-list/definitions/backend-import-preview'; interface ImportServicePreview { new: number; @@ -42,13 +42,13 @@ export interface ImportService { downloadCsvExample(): void; } -export interface ViaBackendImportService { +export interface BackendImportService { readonly rawFileObservable: Observable; readonly encodings: ValueLabelCombination[]; readonly columnSeparators: ValueLabelCombination[]; readonly textSeparators: ValueLabelCombination[]; - readonly previewsObservable: Observable; - readonly currentImportPhaseObservable: Observable; + readonly previewsObservable: Observable; + readonly currentImportPhaseObservable: Observable; readonly previewHasRowErrors: boolean; columnSeparator: string; diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html b/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html similarity index 100% rename from client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.html rename to client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.scss b/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.scss similarity index 100% rename from client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.scss rename to client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.scss diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.spec.ts b/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.spec.ts similarity index 54% rename from client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.spec.ts rename to client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.spec.ts index efd8aaf543..81bd1b0ca0 100644 --- a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.spec.ts +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.spec.ts @@ -1,17 +1,17 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ViaBackendImportListComponent } from './via-backend-import-list.component'; +import { BackendImportListComponent } from './backend-import-list.component'; xdescribe(`ViaBackendImportListComponent`, () => { - let component: ViaBackendImportListComponent; - let fixture: ComponentFixture; + let component: BackendImportListComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ViaBackendImportListComponent] + declarations: [BackendImportListComponent] }).compileComponents(); - fixture = TestBed.createComponent(ViaBackendImportListComponent); + fixture = TestBed.createComponent(BackendImportListComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts b/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.ts similarity index 81% rename from client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts rename to client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.ts index 6ca29f5e20..7623d8a8a5 100644 --- a/client/src/app/ui/modules/import-list/components/via-backend-import-list/via-backend-import-list.component.ts +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.ts @@ -22,23 +22,23 @@ import { delay, firstValueFrom, map, Observable, of } from 'rxjs'; import { Identifiable } from 'src/app/domain/interfaces'; import { infoDialogSettings } from 'src/app/infrastructure/utils/dialog-settings'; import { ValueLabelCombination } from 'src/app/infrastructure/utils/import/import-utils'; -import { ViaBackendImportService } from 'src/app/ui/base/import-service'; +import { BackendImportService } from 'src/app/ui/base/import-service'; import { END_POSITION, START_POSITION } from '../../../scrolling-table/directives/scrolling-table-cell-position'; import { ImportListHeaderDefinition } from '../../definitions'; import { - ImportState, - ImportViaBackendIndexedPreview, - ImportViaBackendPreviewHeader, - ImportViaBackendPreviewIndexedRow, - ImportViaBackendPreviewObject, - ImportViaBackendPreviewSummary -} from '../../definitions/import-via-backend-preview'; + BackendImportEntryObject, + BackendImportHeader, + BackendImportIdentifiedRow, + BackendImportPreview, + BackendImportState, + BackendImportSummary +} from '../../definitions/backend-import-preview'; import { ImportListFirstTabDirective } from '../../directives/import-list-first-tab.directive'; import { ImportListLastTabDirective } from '../../directives/import-list-last-tab.directive'; import { ImportListStatusTemplateDirective } from '../../directives/import-list-status-template.directive'; -export enum ImportViaBackendPhase { +export enum BackendImportPhase { LOADING_PREVIEW, AWAITING_CONFIRM, IMPORTING, @@ -48,12 +48,12 @@ export enum ImportViaBackendPhase { } @Component({ - selector: `os-via-backend-import-list`, - templateUrl: `./via-backend-import-list.component.html`, - styleUrls: [`./via-backend-import-list.component.scss`], + selector: `os-backend-import-list`, + templateUrl: `./backend-import-list.component.html`, + styleUrls: [`./backend-import-list.component.scss`], encapsulation: ViewEncapsulation.None }) -export class ViaBackendImportListComponent implements OnInit, OnDestroy { +export class BackendImportListComponent implements OnInit, OnDestroy { public readonly END_POSITION = END_POSITION; public readonly START_POSITION = START_POSITION; @@ -79,15 +79,15 @@ export class ViaBackendImportListComponent implements On public additionalInfo = ``; @Input() - public set importer(importer: ViaBackendImportService) { + public set importer(importer: BackendImportService) { this._importer = importer; } - public get importer(): ViaBackendImportService { + public get importer(): BackendImportService { return this._importer; } - private _importer!: ViaBackendImportService; + private _importer!: BackendImportService; /** * Defines all necessary and optional fields, that a .csv-file can contain. @@ -98,7 +98,7 @@ export class ViaBackendImportListComponent implements On @Output() public selectedTabChanged = new EventEmitter(); - public readonly Phase = ImportViaBackendPhase; + public readonly Phase = BackendImportPhase; /** * Observable that allows one to monitor the currenty selected file. @@ -123,14 +123,14 @@ export class ViaBackendImportListComponent implements On /** * The actual headers of the preview, as they were delivered by the backend. */ - public get previewColumns(): ImportViaBackendPreviewHeader[] { + public get previewColumns(): BackendImportHeader[] { return this._previewColumns; } /** * The summary of the preview, as it was delivered by the backend. */ - public get summary(): ImportViaBackendPreviewSummary[] { + public get summary(): BackendImportSummary[] { return this._summary; } @@ -138,7 +138,7 @@ export class ViaBackendImportListComponent implements On * The rows of the preview, which were delivered by the backend. * Affixed with fake ids for the purpose of displaying them correctly. */ - public get rows(): ImportViaBackendPreviewIndexedRow[] { + public get rows(): BackendImportIdentifiedRow[] { return this._rows; } @@ -146,35 +146,35 @@ export class ViaBackendImportListComponent implements On * True if, after the first json-upload, the view is waiting for the user to confirm the import. */ public get awaitingConfirm(): boolean { - return this._state === ImportViaBackendPhase.AWAITING_CONFIRM; + return this._state === BackendImportPhase.AWAITING_CONFIRM; } /** * True if the import has successfully finished. */ public get finishedSuccessfully(): boolean { - return this._state === ImportViaBackendPhase.FINISHED; + return this._state === BackendImportPhase.FINISHED; } /** * True if, after an attempted import failed, the view is waiting for the user to confirm the import on the new preview. */ public get tryAgain(): boolean { - return this._state === ImportViaBackendPhase.TRY_AGAIN; + return this._state === BackendImportPhase.TRY_AGAIN; } /** * True while an import is in progress. */ public get isImporting(): boolean { - return this._state === ImportViaBackendPhase.IMPORTING; + return this._state === BackendImportPhase.IMPORTING; } /** * True if the preview can not be imported. */ public get hasErrors(): boolean { - return this._state === ImportViaBackendPhase.ERROR; + return this._state === BackendImportPhase.ERROR; } /** @@ -222,22 +222,22 @@ export class ViaBackendImportListComponent implements On /** * The Observable from which the views table will be calculated */ - public get dataSource(): Observable { + public get dataSource(): Observable { return this._dataSource; } - private _state: ImportViaBackendPhase = ImportViaBackendPhase.LOADING_PREVIEW; + private _state: BackendImportPhase = BackendImportPhase.LOADING_PREVIEW; - private _summary: ImportViaBackendPreviewSummary[]; - private _rows: ImportViaBackendPreviewIndexedRow[]; - private _previewColumns: ImportViaBackendPreviewHeader[]; + private _summary: BackendImportSummary[]; + private _rows: BackendImportIdentifiedRow[]; + private _previewColumns: BackendImportHeader[]; - private _dataSource: Observable = of([]); + private _dataSource: Observable = of([]); private _requiredFields: string[] = []; private _defaultColumns: ImportListHeaderDefinition[] = []; private _headers: { - [property: string]: { default?: ImportListHeaderDefinition; preview?: ImportViaBackendPreviewHeader }; + [property: string]: { default?: ImportListHeaderDefinition; preview?: BackendImportHeader }; } = {}; public constructor(private dialog: MatDialog, private translate: TranslateService) {} @@ -310,7 +310,7 @@ export class ViaBackendImportListComponent implements On /** * Gets the relevant backend header information for a property. */ - public getHeader(propertyName: string): ImportViaBackendPreviewHeader { + public getHeader(propertyName: string): BackendImportHeader { return this._headers[propertyName]?.preview; } @@ -333,17 +333,17 @@ export class ViaBackendImportListComponent implements On * @param item a row or an entry with a current state * @eturn the icon for the item */ - public getActionIcon(item: ImportViaBackendPreviewIndexedRow | ImportViaBackendPreviewObject): string { + public getActionIcon(item: BackendImportIdentifiedRow | BackendImportEntryObject): string { switch (item[`state`] ?? item[`info`]) { - case ImportState.Error: // no import possible + case BackendImportState.Error: // no import possible return `block`; - case ImportState.Warning: + case BackendImportState.Warning: return `warning`; - case ImportState.New: + case BackendImportState.New: return `add`; - case ImportState.Done: // item has been imported + case BackendImportState.Done: // item has been imported return `done`; - case ImportState.Generated: + case BackendImportState.Generated: return `autorenew`; default: return `block`; // fallback: Error @@ -355,18 +355,18 @@ export class ViaBackendImportListComponent implements On * @param entry a row with a current state * @eturn the tooltip for the item */ - public getRowTooltip(row: ImportViaBackendPreviewIndexedRow): string { + public getRowTooltip(row: BackendImportIdentifiedRow): string { switch (row.state) { - case ImportState.Error: // no import possible + case BackendImportState.Error: // no import possible return ( this.getErrorDescription(row) ?? _(`There is an unspecified error in this line, which prevents the import.`) ); - case ImportState.Warning: + case BackendImportState.Warning: return this.getErrorDescription(row) ?? _(`This row will not be imported, due to an unknown reason.`); - case ImportState.New: + case BackendImportState.New: return this.translate.instant(this.modelName) + ` ` + this.translate.instant(`will be imported`); - case ImportState.Done: // item has been imported + case BackendImportState.Done: // item has been imported return this.translate.instant(this.modelName) + ` ` + this.translate.instant(`has been imported`); default: return undefined; @@ -427,10 +427,7 @@ export class ViaBackendImportListComponent implements On return this._importer.getVerboseSummaryPointTitle(title); } - private setHeaders(data: { - default?: ImportListHeaderDefinition[]; - preview?: ImportViaBackendPreviewHeader[]; - }): void { + private setHeaders(data: { default?: ImportListHeaderDefinition[]; preview?: BackendImportHeader[] }): void { for (let key of Object.keys(data)) { for (let header of data[key] ?? []) { if (!this._headers[header.property]) { @@ -442,11 +439,11 @@ export class ViaBackendImportListComponent implements On } } - private getErrorDescription(entry: ImportViaBackendPreviewIndexedRow): string { + private getErrorDescription(entry: BackendImportIdentifiedRow): string { return entry.messages?.map(error => this.translate.instant(this._importer.verbose(error))).join(`,\n `); } - private fillPreviewData(previews: ImportViaBackendIndexedPreview[]) { + private fillPreviewData(previews: BackendImportPreview[]) { if (!previews || !previews.length) { this._previewColumns = undefined; this._summary = undefined; @@ -459,7 +456,7 @@ export class ViaBackendImportListComponent implements On } } - private calculateRows(previews: ImportViaBackendIndexedPreview[]): ImportViaBackendPreviewIndexedRow[] { + private calculateRows(previews: BackendImportPreview[]): BackendImportIdentifiedRow[] { return previews?.flatMap(preview => preview.rows); } diff --git a/client/src/app/ui/modules/import-list/definitions/backend-import-preview.ts b/client/src/app/ui/modules/import-list/definitions/backend-import-preview.ts new file mode 100644 index 0000000000..2741a1ea2f --- /dev/null +++ b/client/src/app/ui/modules/import-list/definitions/backend-import-preview.ts @@ -0,0 +1,68 @@ +import { Identifiable } from 'src/app/domain/interfaces'; + +export enum BackendImportState { + Error = `error`, + Warning = `warning`, + New = `new`, + Done = `done`, + Generated = `generated` + // could be expanded later +} + +export interface BackendImportHeader { + property: string; // field name/column title + type: `boolean` | `number` | `string` | `date` | `object`; // date must be in format yyyy-mm-dd + is_list: boolean; // optional, if not given defaults to `false` +} + +export type BackendImportEntry = null | boolean | number | string | BackendImportEntryObject; + +export type BackendImportEntryObject = { + value: boolean | number | string; + info: BackendImportState; + type: `boolean` | `number` | `string` | `date`; +}; + +export interface BackendImportRow { + state: BackendImportState; + messages: string[]; + data: { + // property name and type must match an entry in the given `headers` + [property: string]: BackendImportEntry | BackendImportEntry[]; // if is_list is set in corresponding header column, we need here also a list + }; +} + +export type BackendImportIdentifiedRow = BackendImportRow & Identifiable; + +export interface BackendImportSummary { + name: string; // text like "Total Number of Items", "Created", "Updated", depending the action + value: number; +} + +export interface BackendImportRawPreview { + id: number; // id of action_worker to import + state: BackendImportState; // May be `error`, `warning` or `done` + headers: BackendImportHeader[]; + rows: BackendImportRow[]; + statistics: BackendImportSummary[]; +} + +export interface BackendImportPreview { + id: number; // id of action_worker to import + state: BackendImportState; // May be `error`, `warning` or `done` + headers: BackendImportHeader[]; + rows: BackendImportIdentifiedRow[]; + statistics: BackendImportSummary[]; +} + +export function isBackendImportRawPreview(obj: any): obj is BackendImportRawPreview { + return ( + obj && + typeof obj === `object` && + typeof obj.id === `number` && + typeof obj.state === `string` && + Array.isArray(obj.headers) && + Array.isArray(obj.rows) && + Array.isArray(obj.statistics) + ); +} diff --git a/client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts b/client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts deleted file mode 100644 index 50eda51080..0000000000 --- a/client/src/app/ui/modules/import-list/definitions/import-via-backend-preview.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Identifiable } from 'src/app/domain/interfaces'; - -export enum ImportState { - Error = `error`, - Warning = `warning`, - New = `new`, - Done = `done`, - Generated = `generated` - // could be expanded later -} - -export interface ImportViaBackendPreviewHeader { - property: string; // field name/column title - type: `boolean` | `number` | `string` | `date` | `object`; // date must be in format yyyy-mm-dd - is_list: boolean; // optional, if not given defaults to `false` -} - -export type ImportViaBackendPreviewModelData = null | boolean | number | string | ImportViaBackendPreviewObject; - -export type ImportViaBackendPreviewObject = { - value: boolean | number | string; - info: ImportState; - type: `boolean` | `number` | `string` | `date`; -}; - -export interface ImportViaBackendPreviewRow { - state: ImportState; - messages: string[]; - data: { - // property name and type must match an entry in the given `headers` - [property: string]: ImportViaBackendPreviewModelData | ImportViaBackendPreviewModelData[]; // if is_list is set in corresponding header column, we need here also a list - }; -} - -export type ImportViaBackendPreviewIndexedRow = ImportViaBackendPreviewRow & Identifiable; - -export interface ImportViaBackendPreviewSummary { - name: string; // text like "Total Number of Items", "Created", "Updated", depending the action - value: number; -} - -export interface ImportViaBackendPreview { - id: number; // id of action_worker to import - state: ImportState; // May be `error`, `warning` or `done` - headers: ImportViaBackendPreviewHeader[]; - rows: ImportViaBackendPreviewRow[]; - statistics: ImportViaBackendPreviewSummary[]; -} - -export interface ImportViaBackendIndexedPreview { - id: number; // id of action_worker to import - state: ImportState; // May be `error`, `warning` or `done` - headers: ImportViaBackendPreviewHeader[]; - rows: ImportViaBackendPreviewIndexedRow[]; - statistics: ImportViaBackendPreviewSummary[]; -} - -export function isImportViaBackendPreview(obj: any): obj is ImportViaBackendPreview { - return ( - obj && - typeof obj === `object` && - typeof obj.id === `number` && - typeof obj.state === `string` && - Array.isArray(obj.headers) && - Array.isArray(obj.rows) && - Array.isArray(obj.statistics) - ); -} diff --git a/client/src/app/ui/modules/import-list/import-list.module.ts b/client/src/app/ui/modules/import-list/import-list.module.ts index 9d567c7fad..4cc8d88a7a 100644 --- a/client/src/app/ui/modules/import-list/import-list.module.ts +++ b/client/src/app/ui/modules/import-list/import-list.module.ts @@ -14,7 +14,7 @@ import { ScrollingTableModule } from 'src/app/ui/modules/scrolling-table'; import { OpenSlidesTranslationModule } from '../../../site/modules/translations'; import { ImportListComponent } from './components/import-list/import-list.component'; -import { ViaBackendImportListComponent } from './components/via-backend-import-list/via-backend-import-list.component'; +import { BackendImportListComponent } from './components/via-backend-import-list/backend-import-list.component'; import { ImportListFirstTabDirective } from './directives/import-list-first-tab.directive'; import { ImportListLastTabDirective } from './directives/import-list-last-tab.directive'; import { ImportListStatusTemplateDirective } from './directives/import-list-status-template.directive'; @@ -24,7 +24,7 @@ const DECLARATIONS = [ ImportListFirstTabDirective, ImportListLastTabDirective, ImportListStatusTemplateDirective, - ViaBackendImportListComponent + BackendImportListComponent ]; @NgModule({ From a78621e582cf7ea91321c6a034a30bcadaf1ee79 Mon Sep 17 00:00:00 2001 From: Luisa Date: Mon, 15 May 2023 16:55:45 +0200 Subject: [PATCH 12/16] Fixed problems with preview list --- .../backend-import-list.component.html | 10 ++++++---- .../backend-import-list.component.ts | 4 ++++ .../scrolling-table/scrolling-table.component.html | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html b/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html index 3ae35335a8..e1dace4f3f 100644 --- a/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html @@ -55,10 +55,12 @@

{{ 'Summary' | translate }}

{{ 'The import was successful.' | translate }} +

{{ 'Preview' | translate }}

-
- -
+
+ +
+
@@ -138,7 +140,7 @@

{{ 'Preview' | translate }}

-
+
{{ file.name }}
Date: Tue, 30 May 2023 17:54:41 +0200 Subject: [PATCH 13/16] Make potentially longer columns flexible in width --- .../topic-import/topic-import.component.ts | 3 ++- .../backend-import-list.component.html | 2 +- .../backend-import-list.component.ts | 13 ++++++++++--- .../definitions/import-list-header-definition.ts | 1 + 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.ts b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.ts index 4e26c5423c..29f995b078 100644 --- a/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.ts +++ b/client/src/app/site/pages/meetings/pages/agenda/modules/topics/pages/topic-import/components/topic-import/topic-import.component.ts @@ -30,7 +30,8 @@ export class TopicImportComponent extends BaseViaBackendImportListComponenttopicHeadersAndVerboseNames)[header], isTableColumn: true, - isRequired: header === `title` + isRequired: header === `title`, + flexible: [`title`, `text`, `agenda_comment`].includes(header) })); public get isTextImportSelected(): boolean { diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html b/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html index e1dace4f3f..a2b5038bb6 100644 --- a/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html @@ -229,7 +229,7 @@

{{ 'Preview' | translate }}

row as row; definition as def; isDefault: true; - config: { width: getColumnWidth(column.property), minWidth: 150 } + config: getColumnConfig(column.property) " >
{{ getColumnLabel(column.property) | translate }}
diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.ts b/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.ts index 7d465f9a5d..793aba3e6e 100644 --- a/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.ts +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.ts @@ -24,6 +24,7 @@ import { infoDialogSettings } from 'src/app/infrastructure/utils/dialog-settings import { ValueLabelCombination } from 'src/app/infrastructure/utils/import/import-utils'; import { BackendImportService } from 'src/app/ui/base/import-service'; +import { ScrollingTableCellDefConfig } from '../../../scrolling-table/directives/scrolling-table-cell-config'; import { END_POSITION, START_POSITION } from '../../../scrolling-table/directives/scrolling-table-cell-position'; import { ImportListHeaderDefinition } from '../../definitions'; import { @@ -317,10 +318,16 @@ export class BackendImportListComponent implements OnIni } /** - * Gets the width of the column for the given property. + * Gets the style of the column for the given property. */ - public getColumnWidth(propertyName: string): number { - return this._headers[propertyName]?.default?.width ?? 50; + public getColumnConfig(propertyName: string): ScrollingTableCellDefConfig { + const defaultHeader = this._headers[propertyName]?.default; + const colWidth = defaultHeader?.width ?? 50; + const def: ScrollingTableCellDefConfig = { minWidth: Math.max(150, colWidth) }; + if (!defaultHeader?.flexible) { + def.width = colWidth; + } + return def; } /** diff --git a/client/src/app/ui/modules/import-list/definitions/import-list-header-definition.ts b/client/src/app/ui/modules/import-list/definitions/import-list-header-definition.ts index 0ca2c85ae1..79db9bb905 100644 --- a/client/src/app/ui/modules/import-list/definitions/import-list-header-definition.ts +++ b/client/src/app/ui/modules/import-list/definitions/import-list-header-definition.ts @@ -7,4 +7,5 @@ interface HeaderDefinition { isRequired?: boolean; isTableColumn?: boolean; width?: number; + flexible?: boolean; } From 422fa7377da0ca86cf7600ba3e202750619393c2 Mon Sep 17 00:00:00 2001 From: Luisa Date: Thu, 6 Jul 2023 16:44:31 +0200 Subject: [PATCH 14/16] Fix import appearance --- .../base-import.service/base-backend-import.service.ts | 9 ++++++++- .../backend-import-list.component.html | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/client/src/app/site/base/base-import.service/base-backend-import.service.ts b/client/src/app/site/base/base-import.service/base-backend-import.service.ts index 953960f2ad..92eb4bce45 100644 --- a/client/src/app/site/base/base-import.service/base-backend-import.service.ts +++ b/client/src/app/site/base/base-import.service/base-backend-import.service.ts @@ -110,6 +110,13 @@ export abstract class BaseBackendImportService */ protected abstract readonly errorList: { [errorKey: string]: string }; + /** + * List of possible errors and their verbose explanation. + */ + private readonly verboseGeneralErrors: { [errorKey: string]: string } = { + [`Duplicate`]: `Is a duplicate` + }; + protected readonly translate: TranslateService = this.importServiceCollector.translate; protected readonly matSnackbar: MatSnackBar = this.importServiceCollector.matSnackBar; @@ -253,7 +260,7 @@ export abstract class BaseBackendImportService * @returns the extended error desription for that error */ public verbose(error: string): string { - return this.errorList[error] || error; + return this.errorList[error] || this.verboseGeneralErrors[error] || error; } /** diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html b/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html index a71dccd0a6..13e8a167f4 100644 --- a/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html @@ -39,7 +39,7 @@

{{ 'Summary' | translate }}

fullscreen
- +
From af9680463e244fe7f8bfc284a0edc2947b3fa47e Mon Sep 17 00:00:00 2001 From: Luisa Date: Fri, 11 Aug 2023 16:23:37 +0200 Subject: [PATCH 15/16] Fix the file upload thing and remove the scrollbar on the tab group --- .../base-backend-import.service.ts | 5 ++++- .../base/base-via-backend-import-list.component.ts | 14 +++++++++++--- client/src/app/ui/base/import-service.ts | 2 +- .../backend-import-list.component.html | 2 +- .../backend-import-list.component.ts | 6 ++++-- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/client/src/app/site/base/base-import.service/base-backend-import.service.ts b/client/src/app/site/base/base-import.service/base-backend-import.service.ts index 92eb4bce45..6d0fed6c34 100644 --- a/client/src/app/site/base/base-import.service/base-backend-import.service.ts +++ b/client/src/app/site/base/base-import.service/base-backend-import.service.ts @@ -273,8 +273,9 @@ export abstract class BaseBackendImportService /** * Executing the import. + * @returns true if the import finished successfully */ - public async doImport(): Promise { + public async doImport(): Promise { this._currentImportPhaseSubject.next(BackendImportPhase.IMPORTING); const results = await this.import(this.previewActionIds, false); @@ -292,7 +293,9 @@ export abstract class BaseBackendImportService } else { this._currentImportPhaseSubject.next(BackendImportPhase.FINISHED); this._csvLines = []; + return true; } + return false; } /** diff --git a/client/src/app/site/base/base-via-backend-import-list.component.ts b/client/src/app/site/base/base-via-backend-import-list.component.ts index b37451c7f4..7a3474e29c 100644 --- a/client/src/app/site/base/base-via-backend-import-list.component.ts +++ b/client/src/app/site/base/base-via-backend-import-list.component.ts @@ -1,7 +1,10 @@ -import { Directive, OnInit } from '@angular/core'; +import { Directive, OnInit, ViewChild } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { BaseComponent } from 'src/app/site/base/base.component'; -import { BackendImportPhase } from 'src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component'; +import { + BackendImportListComponent, + BackendImportPhase +} from 'src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component'; import { Identifiable } from '../../domain/interfaces'; import { getLongPreview, getShortPreview } from '../../infrastructure/utils'; @@ -13,6 +16,9 @@ export abstract class BaseViaBackendImportListComponent extends BaseComponent implements OnInit { + @ViewChild(BackendImportListComponent) + private list: BackendImportListComponent; + /** * Helper function for previews */ @@ -78,6 +84,8 @@ export abstract class BaseViaBackendImportListComponent * Triggers the importer's import */ public async doImport(): Promise { - await this.importer.doImport(); + if (await this.importer.doImport()) { + this.list.removeSelectedFile(false); + } } } diff --git a/client/src/app/ui/base/import-service.ts b/client/src/app/ui/base/import-service.ts index 9d18a5fc2a..a79af78561 100644 --- a/client/src/app/ui/base/import-service.ts +++ b/client/src/app/ui/base/import-service.ts @@ -60,7 +60,7 @@ export interface BackendImportService { clearPreview(): void; clearFile(): void; onSelectFile(event: any): void; - doImport(): Promise; + doImport(): Promise; downloadCsvExample(): void; getVerboseSummaryPointTitle(title: string): string; clearAll(): void; diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html b/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html index 13e8a167f4..eb4a541378 100644 --- a/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html @@ -137,7 +137,7 @@

{{ 'Preview' | translate }}

-
+
implements OnIni /** * Removes the selected file and also empties the preview. */ - public removeSelectedFile(): void { + public removeSelectedFile(clearImporter = true): void { if (this.fileInput) { this.fileInput.nativeElement.value = ``; } - this._importer.clearFile(); + if (clearImporter) { + this._importer.clearFile(); + } } /** From 517565fb584c80adc3f0ca8b5545efa929ac62ed Mon Sep 17 00:00:00 2001 From: Luisa Date: Fri, 11 Aug 2023 16:38:34 +0200 Subject: [PATCH 16/16] Remove inline styles --- .../backend-import-list.component.html | 5 ++--- .../backend-import-list.component.scss | 8 ++++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html b/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html index eb4a541378..d684780a50 100644 --- a/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html +++ b/client/src/app/ui/modules/import-list/components/via-backend-import-list/backend-import-list.component.html @@ -87,7 +87,7 @@

{{ 'Preview' | translate }}

{{ entry }} - info + info
  • @@ -137,7 +137,7 @@

    {{ 'Preview' | translate }}

-
+
{{ 'Preview' | translate }}
{{ getSummaryPointTitle(point.name) | translate }}:  {{ point.value }}