From 763e982d2d275c6e4e82377c678c97020255c888 Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Sun, 30 Jul 2023 12:21:11 +0200 Subject: [PATCH] Creating E-mail messenger job and e-mail messaging config #683 --- package-lock.json | 19 +++ package.json | 3 +- src/backend/middlewares/RenderingMWs.ts | 2 + .../model/diagnostics/ConfigDiagnostics.ts | 61 ++++++++++ src/backend/model/jobs/jobs/Job.ts | 1 + src/backend/model/jobs/jobs/TopPickSendJob.ts | 46 +++++++- .../mediamessengers/EmailMediaMessenger.ts | 43 +++++++ src/common/BackendTexts.ts | 16 ++- src/common/config/private/MessagingConfig.ts | 109 ++++++++++++++++++ src/common/config/private/PrivateConfig.ts | 19 ++- src/common/entities/job/JobDTO.ts | 2 +- src/common/entities/job/JobProgressDTO.ts | 1 + src/frontend/app/model/backendtext.service.ts | 30 ++++- .../settings/template/template.component.html | 4 + .../job-progress.settings.component.ts | 2 + .../settings/workflow/workflow.component.css | 3 + .../settings/workflow/workflow.component.html | 59 +++++++++- .../settings/workflow/workflow.component.ts | 18 +++ 18 files changed, 424 insertions(+), 14 deletions(-) create mode 100644 src/backend/model/mediamessengers/EmailMediaMessenger.ts create mode 100644 src/common/config/private/MessagingConfig.ts diff --git a/package-lock.json b/package-lock.json index 9c2bdedb..897f635a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,6 +76,7 @@ "@types/leaflet.markercluster": "1.5.1", "@types/node": "18.15.0", "@types/node-geocoder": "4.2.0", + "@types/nodemailer": "^6.4.9", "@types/sharp": "0.31.1", "@types/xml2js": "0.4.11", "@typescript-eslint/eslint-plugin": "5.54.1", @@ -4892,6 +4893,15 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.9.tgz", + "integrity": "sha512-XYG8Gv+sHjaOtUpiuytahMy2mM3rectgroNbs6R3djZEKmPNiIJwe9KqOJBGzKKnNZNKvnuvmugBgpq3w/S0ig==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -26718,6 +26728,15 @@ } } }, + "@types/nodemailer": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.9.tgz", + "integrity": "sha512-XYG8Gv+sHjaOtUpiuytahMy2mM3rectgroNbs6R3djZEKmPNiIJwe9KqOJBGzKKnNZNKvnuvmugBgpq3w/S0ig==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", diff --git a/package.json b/package.json index ec080e78..f9079d42 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "image-size": "1.0.2", "locale": "0.1.0", "node-geocoder": "4.2.0", - "nodemailer": "^6.9.4", + "nodemailer": "6.9.4", "reflect-metadata": "0.1.13", "sharp": "0.31.3", "ts-exif-parser": "0.2.2", @@ -96,6 +96,7 @@ "@types/leaflet.markercluster": "1.5.1", "@types/node": "18.15.0", "@types/node-geocoder": "4.2.0", + "@types/nodemailer": "6.4.9", "@types/sharp": "0.31.1", "@types/xml2js": "0.4.11", "@typescript-eslint/eslint-plugin": "5.54.1", diff --git a/src/backend/middlewares/RenderingMWs.ts b/src/backend/middlewares/RenderingMWs.ts index b1edfea0..926d5194 100644 --- a/src/backend/middlewares/RenderingMWs.ts +++ b/src/backend/middlewares/RenderingMWs.ts @@ -9,6 +9,7 @@ import {SharingDTO} from '../../common/entities/SharingDTO'; import {Utils} from '../../common/Utils'; import {LoggerRouter} from '../routes/LoggerRouter'; import {TAGS} from '../../common/config/public/ClientConfig'; +import {ConfigDiagnostics} from '../model/diagnostics/ConfigDiagnostics'; const forcedDebug = process.env['NODE_ENV'] === 'debug'; @@ -108,6 +109,7 @@ export class RenderingMWs { res: Response ): Promise { const originalConf = await Config.original(); + await ConfigDiagnostics.checkEnvironment(originalConf); // These are sensitive information, do not send to the client side originalConf.Server.sessionSecret = null; const message = new Message( diff --git a/src/backend/model/diagnostics/ConfigDiagnostics.ts b/src/backend/model/diagnostics/ConfigDiagnostics.ts index be3079c7..334cd7f3 100644 --- a/src/backend/model/diagnostics/ConfigDiagnostics.ts +++ b/src/backend/model/diagnostics/ConfigDiagnostics.ts @@ -26,6 +26,8 @@ import { import {SearchQueryParser} from '../../../common/SearchQueryParser'; import {SearchQueryTypes, TextSearch,} from '../../../common/entities/SearchQueryDTO'; import {Utils} from '../../../common/Utils'; +import {createTransport} from 'nodemailer'; +import {EmailMessagingType, MessagingConfig} from '../../../common/config/private/MessagingConfig'; const LOG_TAG = '[ConfigDiagnostics]'; @@ -79,6 +81,14 @@ export class ConfigDiagnostics { } } + private static async testEmailMessagingConfig(Messaging: MessagingConfig, config: PrivateConfigClass): Promise { + Logger.debug(LOG_TAG, 'Testing EmailMessaging config'); + + if(Messaging.Email.type === EmailMessagingType.sendmail && !Config.Environment.sendMailAvailable){ + throw new Error('sendmail e-mail sending method is not supported as the sendmail application cannot be found in the OS.') + } + } + static testVideoConfig(videoConfig: ServerVideoConfig, config: PrivateConfigClass): Promise { Logger.debug(LOG_TAG, 'Testing video config with ffmpeg test'); @@ -285,15 +295,48 @@ export class ConfigDiagnostics { await ConfigDiagnostics.testSharingConfig(config.Sharing, config); await ConfigDiagnostics.testRandomPhotoConfig(config.Sharing, config); await ConfigDiagnostics.testMapConfig(config.Map); + await ConfigDiagnostics.testEmailMessagingConfig(config.Messaging, config); } + static async checkEnvironment(Config:PrivateConfigClass): Promise { + Logger.debug(LOG_TAG, 'Checking sendmail availability'); + const transporter = createTransport({ + sendmail: true, + }); + try { + Config.Environment.sendMailAvailable = await transporter.verify(); + } catch (e) { + Config.Environment.sendMailAvailable = false; + } + if (!Config.Environment.sendMailAvailable) { + Config.Messaging.Email.type = EmailMessagingType.SMTP; + Logger.info(LOG_TAG, 'Sendmail is not available on the OS. You will need to use an SMTP server if you wish the app to send mails.'); + } + } + static async runDiagnostics(): Promise { if (process.env['NODE_ENV'] === 'debug') { NotificationManager.warning('You are running the application with NODE_ENV=debug. This exposes a lot of debug information that can be a security vulnerability. Set NODE_ENV=production, when you finished debugging.'); } + try { + await ConfigDiagnostics.checkEnvironment(Config); + } catch (ex) { + const err: Error = ex; + NotificationManager.error( + 'Error during checking environment', + err.toString() + ); + Logger.error( + LOG_TAG, + 'Error during checking environment', + err.toString() + ); + process.exit(1); + } + try { await ConfigDiagnostics.testDatabase(Config.Database); } catch (ex) { @@ -525,5 +568,23 @@ export class ConfigDiagnostics { ); Config.Map.mapProvider = MapProviders.OpenStreetMap; } + try { + await ConfigDiagnostics.testEmailMessagingConfig(Config.Messaging, Config); + } catch (ex) { + const err: Error = ex; + NotificationManager.warning( + 'Setting to SMTP method.', + err.toString() + ); + Logger.warn( + LOG_TAG, + 'Setting to SMTP method.', + err.toString() + ); + Config.Messaging.Email.type = EmailMessagingType.SMTP; + } + + } + } diff --git a/src/backend/model/jobs/jobs/Job.ts b/src/backend/model/jobs/jobs/Job.ts index adf77084..3ed5e745 100644 --- a/src/backend/model/jobs/jobs/Job.ts +++ b/src/backend/model/jobs/jobs/Job.ts @@ -154,6 +154,7 @@ export abstract class Job = Record> i this.run(); } catch (e) { Logger.error(LOG_TAG, e); + this.Progress.State = JobProgressStates.failed; } }); } diff --git a/src/backend/model/jobs/jobs/TopPickSendJob.ts b/src/backend/model/jobs/jobs/TopPickSendJob.ts index 1ed05111..094927b5 100644 --- a/src/backend/model/jobs/jobs/TopPickSendJob.ts +++ b/src/backend/model/jobs/jobs/TopPickSendJob.ts @@ -5,9 +5,18 @@ import {SortingMethods} from '../../../../common/entities/SortingMethods'; import {DatePatternFrequency, DatePatternSearch, SearchQueryDTO, SearchQueryTypes} from '../../../../common/entities/SearchQueryDTO'; import {ObjectManagers} from '../../ObjectManagers'; import {PhotoEntity} from '../../database/enitites/PhotoEntity'; +import {EmailMediaMessenger} from '../../mediamessengers/EmailMediaMessenger'; -export class TopPickSendJob extends Job<{ searchQuery: SearchQueryDTO, sortBy: SortingMethods[], pickAmount: number }> { +export class TopPickSendJob extends Job<{ + searchQuery: SearchQueryDTO, + sortBy: SortingMethods[], + pickAmount: number, + emailTo: string, + emailFrom: string, + emailSubject: string, + emailText: string, +}> { public readonly Name = DefaultsJobs[DefaultsJobs['Top Pick Sending']]; public readonly Supported: boolean = true; public readonly ConfigTemplate: ConfigTemplateEntry[] = [ @@ -33,6 +42,30 @@ export class TopPickSendJob extends Job<{ searchQuery: SearchQueryDTO, sortBy: S name: backendTexts.pickAmount.name, description: backendTexts.pickAmount.description, defaultValue: 5, + }, { + id: 'emailTo', + type: 'email', + name: backendTexts.emailTo.name, + description: backendTexts.emailTo.description, + defaultValue: '', + }, { + id: 'emailFrom', + type: 'email', + name: backendTexts.emailFrom.name, + description: backendTexts.emailFrom.description, + defaultValue: 'norelpy@pigallery2.com', + }, { + id: 'emailSubject', + type: 'string', + name: backendTexts.emailSubject.name, + description: backendTexts.emailSubject.description, + defaultValue: 'Latest photos for you', + }, { + id: 'emailText', + type: 'string', + name: backendTexts.emailText.name, + description: backendTexts.emailText.description, + defaultValue: 'I hand picked these photos just for you:', }, ]; private status: 'Listing' | 'Sending' = 'Listing'; @@ -64,11 +97,20 @@ export class TopPickSendJob extends Job<{ searchQuery: SearchQueryDTO, sortBy: S this.Progress.log('Collecting Photos and videos to Send'); this.Progress.Processed++; this.mediaList = await ObjectManagers.getInstance().SearchManager.getNMedia(this.config.searchQuery, this.config.sortBy, this.config.pickAmount); - // console.log(this.mediaList); + // console.log(this.mediaList); return false; } private async stepSending(): Promise { + this.Progress.log('Sending emails'); + const messenger = new EmailMediaMessenger(); + await messenger.sendMedia({ + from: this.config.emailFrom, + to: this.config.emailTo, + subject: this.config.emailSubject, + text: this.config.emailText + }, this.mediaList); + this.Progress.Processed++; return false; } } diff --git a/src/backend/model/mediamessengers/EmailMediaMessenger.ts b/src/backend/model/mediamessengers/EmailMediaMessenger.ts new file mode 100644 index 00000000..7aae180b --- /dev/null +++ b/src/backend/model/mediamessengers/EmailMediaMessenger.ts @@ -0,0 +1,43 @@ +import {createTransport, Transporter} from 'nodemailer'; +import {MediaDTO} from '../../../common/entities/MediaDTO'; +import {Config} from '../../../common/config/private/Config'; +import {EmailMessagingType} from '../../../common/config/private/MessagingConfig'; + +export class EmailMediaMessenger { + transporter: Transporter; + + constructor() { + if (Config.Messaging.Email.type === EmailMessagingType.sendmail) { + this.transporter = createTransport({ + sendmail: true + }); + } else { + this.transporter = createTransport({ + host: Config.Messaging.Email.smtp.host, + port: Config.Messaging.Email.smtp.port, + secure: Config.Messaging.Email.smtp.secure, + requireTLS: Config.Messaging.Email.smtp.requireTLS, + auth: { + user: Config.Messaging.Email.smtp.user, + pass: Config.Messaging.Email.smtp.password + } + }); + } + + } + + public async sendMedia(mailSettings: { + from: string, + to: string, + subject: string, + text: string + }, media: MediaDTO[]) { + + return await this.transporter.sendMail({ + from: mailSettings.from, + to: mailSettings.to, + subject: mailSettings.subject, + text: mailSettings.text + media.map(m => m.name).join(', ') + }); + } +} diff --git a/src/common/BackendTexts.ts b/src/common/BackendTexts.ts index 1503548c..a2ce401e 100644 --- a/src/common/BackendTexts.ts +++ b/src/common/BackendTexts.ts @@ -1,10 +1,14 @@ export type backendText = number; export const backendTexts = { - indexedFilesOnly: { name: 10, description: 12 }, - sizeToGenerate: { name: 20, description: 22 }, - indexChangesOnly: { name: 30, description: 32 }, - searchQuery: { name: 40, description: 42 }, - sortBy: { name: 40, description: 42 }, - pickAmount: { name: 40, description: 42 }, + indexedFilesOnly: {name: 10, description: 12}, + sizeToGenerate: {name: 20, description: 22}, + indexChangesOnly: {name: 30, description: 32}, + searchQuery: {name: 40, description: 42}, + sortBy: {name: 100, description: 102}, + pickAmount: {name: 50, description: 52}, + emailTo: {name: 60, description: 62}, + emailFrom: {name: 70, description: 72}, + emailSubject: {name: 80, description: 82}, + emailText: {name: 90, description: 92} }; diff --git a/src/common/config/private/MessagingConfig.ts b/src/common/config/private/MessagingConfig.ts new file mode 100644 index 00000000..bf23c68d --- /dev/null +++ b/src/common/config/private/MessagingConfig.ts @@ -0,0 +1,109 @@ +/* eslint-disable @typescript-eslint/no-inferrable-types */ +import {SubConfigClass} from '../../../../node_modules/typeconfig/src/decorators/class/SubConfigClass'; +import {ConfigPriority, TAGS} from '../public/ClientConfig'; +import {ConfigProperty} from '../../../../node_modules/typeconfig/src/decorators/property/ConfigPropoerty'; +import {ServerConfig} from './PrivateConfig'; + +export enum EmailMessagingType { + sendmail = 1, + SMTP = 2, +} + +@SubConfigClass({softReadonly: true}) +export class EmailSMTPMessagingConfig { + @ConfigProperty({ + tags: { + name: $localize`Host`, + priority: ConfigPriority.advanced, + hint: 'smtp.example.com' + }, + description: $localize`SMTP host server` + }) + host: string = ''; + + @ConfigProperty({ + tags: { + name: $localize`Port`, + priority: ConfigPriority.advanced, + }, + description: $localize`SMTP server's port` + }) + port: number = 587; + + @ConfigProperty({ + tags: { + name: $localize`isSecure`, + priority: ConfigPriority.advanced, + }, + description: $localize`Is the connection secure. See https://nodemailer.com/smtp/#tls-options for more details` + }) + secure: boolean = false; + + @ConfigProperty({ + tags: { + name: $localize`TLS required`, + priority: ConfigPriority.advanced, + }, + description: $localize`if this is true and secure is false then Nodemailer (used library in the background) tries to use STARTTLS. See https://nodemailer.com/smtp/#tls-options for more details` + }) + requireTLS: boolean = true; + + + @ConfigProperty({ + tags: { + name: $localize`User`, + priority: ConfigPriority.advanced, + }, + description: $localize`User to connect to the SMTP server.` + }) + user: string = ''; + + + @ConfigProperty({ + tags: { + name: $localize`Password`, + priority: ConfigPriority.advanced, + }, + type: 'password', + description: $localize`Password to connect to the SMTP server.` + }) + password: string = ''; + +} + +@SubConfigClass({softReadonly: true}) +export class EmailMessagingConfig { + @ConfigProperty({ + type: EmailMessagingType, + tags: + { + name: $localize`Sending method`, + priority: ConfigPriority.advanced, + uiDisabled: (sc: EmailMessagingConfig, c: ServerConfig) => !c.Environment.sendMailAvailable + } as TAGS, + description: $localize`Sendmail uses the built in unix binary if available. STMP connects to any STMP server of your choice.` + }) + type: EmailMessagingType = EmailMessagingType.sendmail; + + @ConfigProperty({ + tags: + { + name: $localize`SMTP`, + relevant: (c: any) => c.type === EmailMessagingType.SMTP, + } + }) + smtp?: EmailSMTPMessagingConfig = new EmailSMTPMessagingConfig(); + +} + +@SubConfigClass({softReadonly: true}) +export class MessagingConfig { + @ConfigProperty({ + tags: + { + name: $localize`Email`, + }, + description: $localize`The app uses Nodemailer in the background for sending e-mails. Refer to https://nodemailer.com/usage/ if some options are not clear.` + }) + Email: EmailMessagingConfig = new EmailMessagingConfig(); +} diff --git a/src/common/config/private/PrivateConfig.ts b/src/common/config/private/PrivateConfig.ts index 8b924325..08ca3a71 100644 --- a/src/common/config/private/PrivateConfig.ts +++ b/src/common/config/private/PrivateConfig.ts @@ -30,6 +30,7 @@ import {DefaultsJobs} from '../../entities/job/JobDTO'; import {SearchQueryDTO, SearchQueryTypes, TextSearch,} from '../../entities/SearchQueryDTO'; import {SortingMethods} from '../../entities/SortingMethods'; import {UserRoles} from '../../entities/UserDTO'; +import {MessagingConfig} from './MessagingConfig'; declare let $localize: (s: TemplateStringsArray) => string; @@ -395,7 +396,7 @@ export class ServerMetaFileConfig extends ClientMetaFileConfig { uiJob: [{ job: DefaultsJobs[DefaultsJobs['GPX Compression']], relevant: (c) => c.MetaFile.GPXCompressing.enabled - },{ + }, { job: DefaultsJobs[DefaultsJobs['Delete Compressed GPX']], relevant: (c) => c.MetaFile.GPXCompressing.enabled }] @@ -1049,8 +1050,14 @@ export class ServerEnvironmentConfig { buildCommitHash: string | undefined; @ConfigProperty({volatile: true}) isDocker: boolean | undefined; + @ConfigProperty({ + volatile: true, + description: 'App updates on start-up if sendmail binary is available' + }) + sendMailAvailable: boolean | undefined; } + @SubConfigClass({softReadonly: true}) export class ServerConfig extends ClientConfig { @@ -1144,6 +1151,16 @@ export class ServerConfig extends ClientConfig { }) Duplicates: ServerDuplicatesConfig = new ServerDuplicatesConfig(); + @ConfigProperty({ + tags: { + name: $localize`Messaging`, + uiIcon: 'chat', + githubIssue: 683 + } as TAGS, + description: $localize`The App can send messages (like photos on the same day a year ago. aka: "Top Pick"). Here you can configure the delivery method.` + }) + Messaging: MessagingConfig = new MessagingConfig(); + @ConfigProperty({ tags: { name: $localize`Jobs`, diff --git a/src/common/entities/job/JobDTO.ts b/src/common/entities/job/JobDTO.ts index 4d843ee7..f06fe435 100644 --- a/src/common/entities/job/JobDTO.ts +++ b/src/common/entities/job/JobDTO.ts @@ -1,6 +1,6 @@ import {backendText} from '../../BackendTexts'; -export type fieldType = 'string' | 'number' | 'boolean' | 'number-array' | 'SearchQuery' | 'sort-array'; +export type fieldType = 'string' | 'email' | 'number' | 'boolean' | 'number-array' | 'SearchQuery' | 'sort-array'; export enum DefaultsJobs { Indexing = 1, diff --git a/src/common/entities/job/JobProgressDTO.ts b/src/common/entities/job/JobProgressDTO.ts index 7aecb71b..a0a1ed71 100644 --- a/src/common/entities/job/JobProgressDTO.ts +++ b/src/common/entities/job/JobProgressDTO.ts @@ -4,6 +4,7 @@ export enum JobProgressStates { interrupted = 3, canceled = 4, finished = 5, + failed = 6, } export interface JobProgressLogDTO { diff --git a/src/frontend/app/model/backendtext.service.ts b/src/frontend/app/model/backendtext.service.ts index 738d6e6f..c5af99f8 100644 --- a/src/frontend/app/model/backendtext.service.ts +++ b/src/frontend/app/model/backendtext.service.ts @@ -17,8 +17,34 @@ export class BackendtextService { return $localize`Only checks indexed files.`; case backendTexts.indexChangesOnly.name: return $localize`Index changes only`; - case backendTexts.indexChangesOnly.description: - return $localize`Only indexes a folder if it got changed.`; + case backendTexts.searchQuery.name: + return $localize`Search query`; + case backendTexts.searchQuery.description: + return $localize`Search query to list photos and videos.`; + case backendTexts.sortBy.name: + return $localize`Sorting`; + case backendTexts.sortBy.description: + return $localize`Sorts the photos and videos by this.`; + case backendTexts.pickAmount.name: + return $localize`Pick`; + case backendTexts.pickAmount.description: + return $localize`Number of photos and videos to pick.`; + case backendTexts.emailTo.name: + return $localize`E-mail to`; + case backendTexts.emailTo.description: + return $localize`E-mail address of the recipient.`; + case backendTexts.emailFrom.name: + return $localize`E-mail From`; + case backendTexts.emailFrom.description: + return $localize`E-mail sender address.`; + case backendTexts.emailSubject.name: + return $localize`Subject`; + case backendTexts.emailSubject.description: + return $localize`E-mail subject.`; + case backendTexts.emailText.name: + return $localize`Message`; + case backendTexts.emailText.description: + return $localize`E-mail text.`; default: return null; } diff --git a/src/frontend/app/ui/settings/template/template.component.html b/src/frontend/app/ui/settings/template/template.component.html index e35b8a50..08e88fa5 100644 --- a/src/frontend/app/ui/settings/template/template.component.html +++ b/src/frontend/app/ui/settings/template/template.component.html @@ -71,6 +71,10 @@ let-confPath="confPath"> diff --git a/src/frontend/app/ui/settings/workflow/progress/job-progress.settings.component.ts b/src/frontend/app/ui/settings/workflow/progress/job-progress.settings.component.ts index e8a1a52b..40f62d3d 100644 --- a/src/frontend/app/ui/settings/workflow/progress/job-progress.settings.component.ts +++ b/src/frontend/app/ui/settings/workflow/progress/job-progress.settings.component.ts @@ -115,6 +115,8 @@ export class JobProgressComponent implements OnDestroy, OnChanges { return $localize`interrupted`; case JobProgressStates.finished: return $localize`finished`; + case JobProgressStates.failed: + return $localize`failed`; default: return 'unknown state'; } diff --git a/src/frontend/app/ui/settings/workflow/workflow.component.css b/src/frontend/app/ui/settings/workflow/workflow.component.css index e69de29b..26e62ba9 100644 --- a/src/frontend/app/ui/settings/workflow/workflow.component.css +++ b/src/frontend/app/ui/settings/workflow/workflow.component.css @@ -0,0 +1,3 @@ +app-gallery-search-field { + width: 100%; +} diff --git a/src/frontend/app/ui/settings/workflow/workflow.component.html b/src/frontend/app/ui/settings/workflow/workflow.component.html index be043e5b..7983f4a9 100644 --- a/src/frontend/app/ui/settings/workflow/workflow.component.html +++ b/src/frontend/app/ui/settings/workflow/workflow.component.html @@ -1,6 +1,7 @@
-
+
@@ -175,6 +176,14 @@ [(ngModel)]="schedule.config[configEntry.id]" required> + + + + + + + + + + + +
+
+ + + +
+ +
+ +
+
+
+
+ +
+ +
+
+
diff --git a/src/frontend/app/ui/settings/workflow/workflow.component.ts b/src/frontend/app/ui/settings/workflow/workflow.component.ts index 0e5ed79f..51ae5dc5 100644 --- a/src/frontend/app/ui/settings/workflow/workflow.component.ts +++ b/src/frontend/app/ui/settings/workflow/workflow.component.ts @@ -19,6 +19,8 @@ import { ScheduledJobTriggerConfig } from '../../../../../common/config/private/PrivateConfig'; import {ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator} from '@angular/forms'; +import {enumToTranslatedArray} from '../../EnumTranslations'; +import {SortingMethods} from '../../../../../common/entities/SortingMethods'; @Component({ selector: 'app-settings-workflow', @@ -61,6 +63,9 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni allowParallelRun: false, }; + SortingMethods = enumToTranslatedArray(SortingMethods); + + error: string; constructor( @@ -278,4 +283,17 @@ export class WorkflowComponent implements ControlValueAccessor, Validator, OnIni this.onTouched = fn; } + + + AsSortArray(configElement: string | number | string[] | number[]): SortingMethods[] { + return configElement as SortingMethods[]; + } + + removeSorting(configElement: string | number | string[] | number[], j: number): void { + (configElement as SortingMethods[]).splice(j); + } + + AddNewSorting(configElement: string | number | string[] | number[]): void { + (configElement as SortingMethods[]).push(SortingMethods.ascDate) + } }