@memberjunction/ng-scheduling 5.43.0 → 5.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/panels/scheduled-job-summary/scheduled-job-summary.component.js +3 -2
- package/dist/lib/panels/scheduled-job-summary/scheduled-job-summary.component.js.map +1 -1
- package/dist/lib/scheduling.module.d.ts +1 -1
- package/dist/lib/scheduling.module.d.ts.map +1 -1
- package/dist/lib/scheduling.module.js +6 -3
- package/dist/lib/scheduling.module.js.map +1 -1
- package/package.json +9 -8
- package/dist/__tests__/index.test.d.ts +0 -2
- package/dist/__tests__/index.test.d.ts.map +0 -1
- package/dist/__tests__/index.test.js +0 -7
- package/dist/__tests__/index.test.js.map +0 -1
- package/dist/__tests__/scheduled-job-service.test.d.ts +0 -2
- package/dist/__tests__/scheduled-job-service.test.d.ts.map +0 -1
- package/dist/__tests__/scheduled-job-service.test.js +0 -157
- package/dist/__tests__/scheduled-job-service.test.js.map +0 -1
|
@@ -2,6 +2,7 @@ import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, Change
|
|
|
2
2
|
import { ScheduledJobService } from '../../services/scheduled-job.service';
|
|
3
3
|
import * as i0 from "@angular/core";
|
|
4
4
|
import * as i1 from "@memberjunction/ng-shared-generic";
|
|
5
|
+
import * as i2 from "@memberjunction/ng-ui-components";
|
|
5
6
|
export class ScheduledJobSummaryComponent {
|
|
6
7
|
cdr = inject(ChangeDetectorRef);
|
|
7
8
|
scheduledJobService = inject(ScheduledJobService);
|
|
@@ -68,11 +69,11 @@ export class ScheduledJobSummaryComponent {
|
|
|
68
69
|
this.cdr.markForCheck();
|
|
69
70
|
}
|
|
70
71
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ScheduledJobSummaryComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
71
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: ScheduledJobSummaryComponent, isStandalone: false, selector: "mj-scheduled-job-summary", inputs: { ScheduledJobID: "ScheduledJobID", ShowEditButton: "ShowEditButton" }, outputs: { EditRequested: "EditRequested" }, ngImport: i0, template: "@if (IsLoading) {\n <div class=\"summary-loading\">\n <mj-loading size=\"small\"></mj-loading>\n </div>\n} @else if (Job) {\n <div class=\"summary-card\">\n <div class=\"summary-header\">\n <div class=\"summary-name\">\n <i class=\"fa-solid fa-calendar-check\"></i>\n {{ Job.Name }}\n </div>\n <span class=\"status-chip\" [class]=\"StatusClass\">{{ Job.Status }}</span>\n </div>\n\n <div class=\"summary-meta\">\n <div class=\"meta-item\">\n <i class=\"fa-regular fa-clock\"></i>\n <span>{{ Job.CronExpression }}</span>\n </div>\n @if (Job.Timezone && Job.Timezone !== 'UTC') {\n <div class=\"meta-item\">\n <i class=\"fa-solid fa-globe\"></i>\n <span>{{ Job.Timezone }}</span>\n </div>\n }\n </div>\n\n <div class=\"summary-stats\">\n <div class=\"stat\">\n <span class=\"stat-num\">{{ TotalRuns }}</span>\n <span class=\"stat-lbl\">Runs</span>\n </div>\n <div class=\"stat\">\n <span class=\"stat-num success\">{{ SuccessRuns }}</span>\n <span class=\"stat-lbl\">OK</span>\n </div>\n <div class=\"stat\">\n <span class=\"stat-num error\">{{ FailedRuns }}</span>\n <span class=\"stat-lbl\">Failed</span>\n </div>\n <div class=\"stat\">\n <span class=\"stat-num\">{{ SuccessRate }}</span>\n <span class=\"stat-lbl\">Rate</span>\n </div>\n </div>\n\n @if (ShowEditButton) {\n <div class=\"summary-actions\">\n <button class=\"edit-btn\" (click)=\"OnEditClick()\">\n <i class=\"fa-solid fa-pen-to-square\"></i> Edit Schedule\n </button>\n </div>\n }\n </div>\n} @else {\n <
|
|
72
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: ScheduledJobSummaryComponent, isStandalone: false, selector: "mj-scheduled-job-summary", inputs: { ScheduledJobID: "ScheduledJobID", ShowEditButton: "ShowEditButton" }, outputs: { EditRequested: "EditRequested" }, ngImport: i0, template: "@if (IsLoading) {\n <div class=\"summary-loading\">\n <mj-loading size=\"small\"></mj-loading>\n </div>\n} @else if (Job) {\n <div class=\"summary-card\">\n <div class=\"summary-header\">\n <div class=\"summary-name\">\n <i class=\"fa-solid fa-calendar-check\"></i>\n {{ Job.Name }}\n </div>\n <span class=\"status-chip\" [class]=\"StatusClass\">{{ Job.Status }}</span>\n </div>\n\n <div class=\"summary-meta\">\n <div class=\"meta-item\">\n <i class=\"fa-regular fa-clock\"></i>\n <span>{{ Job.CronExpression }}</span>\n </div>\n @if (Job.Timezone && Job.Timezone !== 'UTC') {\n <div class=\"meta-item\">\n <i class=\"fa-solid fa-globe\"></i>\n <span>{{ Job.Timezone }}</span>\n </div>\n }\n </div>\n\n <div class=\"summary-stats\">\n <div class=\"stat\">\n <span class=\"stat-num\">{{ TotalRuns }}</span>\n <span class=\"stat-lbl\">Runs</span>\n </div>\n <div class=\"stat\">\n <span class=\"stat-num success\">{{ SuccessRuns }}</span>\n <span class=\"stat-lbl\">OK</span>\n </div>\n <div class=\"stat\">\n <span class=\"stat-num error\">{{ FailedRuns }}</span>\n <span class=\"stat-lbl\">Failed</span>\n </div>\n <div class=\"stat\">\n <span class=\"stat-num\">{{ SuccessRate }}</span>\n <span class=\"stat-lbl\">Rate</span>\n </div>\n </div>\n\n @if (ShowEditButton) {\n <div class=\"summary-actions\">\n <button class=\"edit-btn\" (click)=\"OnEditClick()\">\n <i class=\"fa-solid fa-pen-to-square\"></i> Edit Schedule\n </button>\n </div>\n }\n </div>\n} @else {\n <mj-empty-state class=\"summary-empty\" Size=\"compact\"\n Icon=\"fa-regular fa-calendar-xmark\"\n Title=\"No schedule configured\" />\n}\n", styles: [".summary-loading {\n display: flex;\n justify-content: center;\n padding: 16px;\n}\n\n.summary-card {\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n padding: 14px;\n background: var(--mj-bg-surface);\n}\n\n.summary-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 10px;\n}\n\n.summary-name {\n font-size: 14px;\n font-weight: 600;\n color: var(--mj-text-primary);\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.summary-name i {\n color: var(--mj-brand-primary);\n}\n\n.status-chip {\n font-size: 11px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 10px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.status-active {\n background: var(--mj-status-success-bg);\n color: var(--mj-status-success-text);\n}\n\n.status-paused {\n background: var(--mj-status-warning-bg);\n color: var(--mj-status-warning-text);\n}\n\n.status-disabled {\n background: var(--mj-status-error-bg);\n color: var(--mj-status-error-text);\n}\n\n.status-pending {\n background: var(--mj-status-info-bg);\n color: var(--mj-status-info-text);\n}\n\n.summary-meta {\n display: flex;\n flex-wrap: wrap;\n gap: 12px;\n margin-bottom: 10px;\n}\n\n.meta-item {\n display: flex;\n align-items: center;\n gap: 5px;\n font-size: 12px;\n color: var(--mj-text-secondary);\n}\n\n.meta-item i {\n font-size: 11px;\n}\n\n.summary-stats {\n display: flex;\n gap: 16px;\n padding: 8px 0;\n border-top: 1px solid var(--mj-border-subtle);\n}\n\n.stat {\n text-align: center;\n flex: 1;\n}\n\n.stat-num {\n display: block;\n font-size: 16px;\n font-weight: 700;\n color: var(--mj-text-primary);\n}\n\n.stat-num.success {\n color: var(--mj-status-success);\n}\n\n.stat-num.error {\n color: var(--mj-status-error);\n}\n\n.stat-lbl {\n display: block;\n font-size: 10px;\n color: var(--mj-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.summary-actions {\n margin-top: 10px;\n padding-top: 10px;\n border-top: 1px solid var(--mj-border-subtle);\n}\n\n.edit-btn {\n background: none;\n border: 1px solid var(--mj-border-strong);\n border-radius: 6px;\n padding: 6px 12px;\n font-size: 12px;\n color: var(--mj-brand-primary);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 5px;\n transition: background 0.15s;\n}\n\n.edit-btn:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, var(--mj-bg-surface));\n}\n\n.summary-empty {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px;\n color: var(--mj-text-muted);\n font-size: 13px;\n}\n\n.summary-empty i {\n font-size: 16px;\n}\n"], dependencies: [{ kind: "component", type: i1.LoadingComponent, selector: "mj-loading", inputs: ["text", "showText", "animationDuration", "size", "textColor", "logoColor", "logoGradient", "animation"] }, { kind: "component", type: i2.MJEmptyStateComponent, selector: "mj-empty-state", inputs: ["Icon", "Title", "Message", "ActionText", "ActionIcon", "ActionVariant", "Variant", "Size", "Role"], outputs: ["Action"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
72
73
|
}
|
|
73
74
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: ScheduledJobSummaryComponent, decorators: [{
|
|
74
75
|
type: Component,
|
|
75
|
-
args: [{ selector: 'mj-scheduled-job-summary', standalone: false, changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (IsLoading) {\n <div class=\"summary-loading\">\n <mj-loading size=\"small\"></mj-loading>\n </div>\n} @else if (Job) {\n <div class=\"summary-card\">\n <div class=\"summary-header\">\n <div class=\"summary-name\">\n <i class=\"fa-solid fa-calendar-check\"></i>\n {{ Job.Name }}\n </div>\n <span class=\"status-chip\" [class]=\"StatusClass\">{{ Job.Status }}</span>\n </div>\n\n <div class=\"summary-meta\">\n <div class=\"meta-item\">\n <i class=\"fa-regular fa-clock\"></i>\n <span>{{ Job.CronExpression }}</span>\n </div>\n @if (Job.Timezone && Job.Timezone !== 'UTC') {\n <div class=\"meta-item\">\n <i class=\"fa-solid fa-globe\"></i>\n <span>{{ Job.Timezone }}</span>\n </div>\n }\n </div>\n\n <div class=\"summary-stats\">\n <div class=\"stat\">\n <span class=\"stat-num\">{{ TotalRuns }}</span>\n <span class=\"stat-lbl\">Runs</span>\n </div>\n <div class=\"stat\">\n <span class=\"stat-num success\">{{ SuccessRuns }}</span>\n <span class=\"stat-lbl\">OK</span>\n </div>\n <div class=\"stat\">\n <span class=\"stat-num error\">{{ FailedRuns }}</span>\n <span class=\"stat-lbl\">Failed</span>\n </div>\n <div class=\"stat\">\n <span class=\"stat-num\">{{ SuccessRate }}</span>\n <span class=\"stat-lbl\">Rate</span>\n </div>\n </div>\n\n @if (ShowEditButton) {\n <div class=\"summary-actions\">\n <button class=\"edit-btn\" (click)=\"OnEditClick()\">\n <i class=\"fa-solid fa-pen-to-square\"></i> Edit Schedule\n </button>\n </div>\n }\n </div>\n} @else {\n <
|
|
76
|
+
args: [{ selector: 'mj-scheduled-job-summary', standalone: false, changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (IsLoading) {\n <div class=\"summary-loading\">\n <mj-loading size=\"small\"></mj-loading>\n </div>\n} @else if (Job) {\n <div class=\"summary-card\">\n <div class=\"summary-header\">\n <div class=\"summary-name\">\n <i class=\"fa-solid fa-calendar-check\"></i>\n {{ Job.Name }}\n </div>\n <span class=\"status-chip\" [class]=\"StatusClass\">{{ Job.Status }}</span>\n </div>\n\n <div class=\"summary-meta\">\n <div class=\"meta-item\">\n <i class=\"fa-regular fa-clock\"></i>\n <span>{{ Job.CronExpression }}</span>\n </div>\n @if (Job.Timezone && Job.Timezone !== 'UTC') {\n <div class=\"meta-item\">\n <i class=\"fa-solid fa-globe\"></i>\n <span>{{ Job.Timezone }}</span>\n </div>\n }\n </div>\n\n <div class=\"summary-stats\">\n <div class=\"stat\">\n <span class=\"stat-num\">{{ TotalRuns }}</span>\n <span class=\"stat-lbl\">Runs</span>\n </div>\n <div class=\"stat\">\n <span class=\"stat-num success\">{{ SuccessRuns }}</span>\n <span class=\"stat-lbl\">OK</span>\n </div>\n <div class=\"stat\">\n <span class=\"stat-num error\">{{ FailedRuns }}</span>\n <span class=\"stat-lbl\">Failed</span>\n </div>\n <div class=\"stat\">\n <span class=\"stat-num\">{{ SuccessRate }}</span>\n <span class=\"stat-lbl\">Rate</span>\n </div>\n </div>\n\n @if (ShowEditButton) {\n <div class=\"summary-actions\">\n <button class=\"edit-btn\" (click)=\"OnEditClick()\">\n <i class=\"fa-solid fa-pen-to-square\"></i> Edit Schedule\n </button>\n </div>\n }\n </div>\n} @else {\n <mj-empty-state class=\"summary-empty\" Size=\"compact\"\n Icon=\"fa-regular fa-calendar-xmark\"\n Title=\"No schedule configured\" />\n}\n", styles: [".summary-loading {\n display: flex;\n justify-content: center;\n padding: 16px;\n}\n\n.summary-card {\n border: 1px solid var(--mj-border-default);\n border-radius: 8px;\n padding: 14px;\n background: var(--mj-bg-surface);\n}\n\n.summary-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 10px;\n}\n\n.summary-name {\n font-size: 14px;\n font-weight: 600;\n color: var(--mj-text-primary);\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.summary-name i {\n color: var(--mj-brand-primary);\n}\n\n.status-chip {\n font-size: 11px;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 10px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.status-active {\n background: var(--mj-status-success-bg);\n color: var(--mj-status-success-text);\n}\n\n.status-paused {\n background: var(--mj-status-warning-bg);\n color: var(--mj-status-warning-text);\n}\n\n.status-disabled {\n background: var(--mj-status-error-bg);\n color: var(--mj-status-error-text);\n}\n\n.status-pending {\n background: var(--mj-status-info-bg);\n color: var(--mj-status-info-text);\n}\n\n.summary-meta {\n display: flex;\n flex-wrap: wrap;\n gap: 12px;\n margin-bottom: 10px;\n}\n\n.meta-item {\n display: flex;\n align-items: center;\n gap: 5px;\n font-size: 12px;\n color: var(--mj-text-secondary);\n}\n\n.meta-item i {\n font-size: 11px;\n}\n\n.summary-stats {\n display: flex;\n gap: 16px;\n padding: 8px 0;\n border-top: 1px solid var(--mj-border-subtle);\n}\n\n.stat {\n text-align: center;\n flex: 1;\n}\n\n.stat-num {\n display: block;\n font-size: 16px;\n font-weight: 700;\n color: var(--mj-text-primary);\n}\n\n.stat-num.success {\n color: var(--mj-status-success);\n}\n\n.stat-num.error {\n color: var(--mj-status-error);\n}\n\n.stat-lbl {\n display: block;\n font-size: 10px;\n color: var(--mj-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.summary-actions {\n margin-top: 10px;\n padding-top: 10px;\n border-top: 1px solid var(--mj-border-subtle);\n}\n\n.edit-btn {\n background: none;\n border: 1px solid var(--mj-border-strong);\n border-radius: 6px;\n padding: 6px 12px;\n font-size: 12px;\n color: var(--mj-brand-primary);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 5px;\n transition: background 0.15s;\n}\n\n.edit-btn:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, var(--mj-bg-surface));\n}\n\n.summary-empty {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px;\n color: var(--mj-text-muted);\n font-size: 13px;\n}\n\n.summary-empty i {\n font-size: 16px;\n}\n"] }]
|
|
76
77
|
}], propDecorators: { ScheduledJobID: [{
|
|
77
78
|
type: Input
|
|
78
79
|
}], ShowEditButton: [{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scheduled-job-summary.component.js","sourceRoot":"","sources":["../../../../src/lib/panels/scheduled-job-summary/scheduled-job-summary.component.ts","../../../../src/lib/panels/scheduled-job-summary/scheduled-job-summary.component.html"],"names":[],"mappings":"AAAA,OAAO,EACH,SAAS,EACT,KAAK,EACL,MAAM,EACN,YAAY,EACZ,uBAAuB,EACvB,iBAAiB,EACjB,MAAM,GAET,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC
|
|
1
|
+
{"version":3,"file":"scheduled-job-summary.component.js","sourceRoot":"","sources":["../../../../src/lib/panels/scheduled-job-summary/scheduled-job-summary.component.ts","../../../../src/lib/panels/scheduled-job-summary/scheduled-job-summary.component.html"],"names":[],"mappings":"AAAA,OAAO,EACH,SAAS,EACT,KAAK,EACL,MAAM,EACN,YAAY,EACZ,uBAAuB,EACvB,iBAAiB,EACjB,MAAM,GAET,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;;;;AAS3E,MAAM,OAAO,4BAA4B;IAC7B,GAAG,GAAG,MAAM,CAAC,iBAAiB,CAAC,CAAC;IAChC,mBAAmB,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAC;IAElD,eAAe,GAAkB,IAAI,CAAC;IAE9C,IACI,cAAc,CAAC,KAAoB;QACnC,IAAI,KAAK,KAAK,IAAI,CAAC,eAAe,EAAE,CAAC;YACjC,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;YAC7B,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;gBACrB,IAAI,CAAC,OAAO,EAAE,CAAC;YACnB,CAAC;QACL,CAAC;IACL,CAAC;IACD,IAAI,cAAc;QACd,OAAO,IAAI,CAAC,eAAe,CAAC;IAChC,CAAC;IAEQ,cAAc,GAAG,IAAI,CAAC;IAErB,aAAa,GAAG,IAAI,YAAY,EAAU,CAAC;IAE9C,SAAS,GAAG,KAAK,CAAC;IAClB,GAAG,GAAgC,IAAI,CAAC;IACxC,SAAS,GAAG,CAAC,CAAC;IACd,WAAW,GAAG,CAAC,CAAC;IAChB,UAAU,GAAG,CAAC,CAAC;IACf,OAAO,GAAmC,IAAI,CAAC;IAE9C,aAAa,GAAG,KAAK,CAAC;IAE9B,KAAK,CAAC,QAAQ;QACV,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACrB,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;IAC9B,CAAC;IAEM,WAAW;QACd,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACvB,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAClD,CAAC;IACL,CAAC;IAED,IAAW,WAAW;QAClB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;QACrD,IAAI,MAAM,KAAK,QAAQ;YAAE,OAAO,eAAe,CAAC;QAChD,IAAI,MAAM,KAAK,QAAQ;YAAE,OAAO,eAAe,CAAC;QAChD,IAAI,MAAM,KAAK,UAAU;YAAE,OAAO,iBAAiB,CAAC;QACpD,OAAO,gBAAgB,CAAC;IAC5B,CAAC;IAED,IAAW,WAAW;QAClB,IAAI,IAAI,CAAC,SAAS,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QACvC,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC;IACvE,CAAC;IAEO,KAAK,CAAC,OAAO;QACjB,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;YACxB,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC;YAChB,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;YACxB,OAAO;QACX,CAAC;QAED,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;QAExB,IAAI,CAAC,GAAG,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QACxE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,WAAW,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;QAClF,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,MAAM,CAAC;QACrE,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,MAAM,CAAC;QACjE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAEhD,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;QACvB,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;IAC5B,CAAC;uGA3EQ,4BAA4B;2FAA5B,4BAA4B,kNCpBzC,2oEA2DA;;2FDvCa,4BAA4B;kBAPxC,SAAS;+BACI,0BAA0B,cACxB,KAAK,mBAGA,uBAAuB,CAAC,MAAM;;sBAQ9C,KAAK;;sBAaL,KAAK;;sBAEL,MAAM","sourcesContent":["import {\n Component,\n Input,\n Output,\n EventEmitter,\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n inject,\n OnInit,\n} from '@angular/core';\nimport { MJScheduledJobEntity, MJScheduledJobRunEntity } from '@memberjunction/core-entities';\nimport { ScheduledJobService } from '../../services/scheduled-job.service';\n\n@Component({\n selector: 'mj-scheduled-job-summary',\n standalone: false,\n templateUrl: './scheduled-job-summary.component.html',\n styleUrls: ['./scheduled-job-summary.component.css'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class ScheduledJobSummaryComponent implements OnInit {\n private cdr = inject(ChangeDetectorRef);\n private scheduledJobService = inject(ScheduledJobService);\n\n private _scheduledJobID: string | null = null;\n\n @Input()\n set ScheduledJobID(value: string | null) {\n if (value !== this._scheduledJobID) {\n this._scheduledJobID = value;\n if (this.isInitialized) {\n this.loadJob();\n }\n }\n }\n get ScheduledJobID(): string | null {\n return this._scheduledJobID;\n }\n\n @Input() ShowEditButton = true;\n\n @Output() EditRequested = new EventEmitter<string>();\n\n public IsLoading = false;\n public Job: MJScheduledJobEntity | null = null;\n public TotalRuns = 0;\n public SuccessRuns = 0;\n public FailedRuns = 0;\n public LastRun: MJScheduledJobRunEntity | null = null;\n\n private isInitialized = false;\n\n async ngOnInit(): Promise<void> {\n await this.loadJob();\n this.isInitialized = true;\n }\n\n public OnEditClick(): void {\n if (this._scheduledJobID) {\n this.EditRequested.emit(this._scheduledJobID);\n }\n }\n\n public get StatusClass(): string {\n const status = this.Job?.Status?.toLowerCase() ?? '';\n if (status === 'active') return 'status-active';\n if (status === 'paused') return 'status-paused';\n if (status === 'disabled') return 'status-disabled';\n return 'status-pending';\n }\n\n public get SuccessRate(): string {\n if (this.TotalRuns === 0) return 'N/A';\n return `${Math.round((this.SuccessRuns / this.TotalRuns) * 100)}%`;\n }\n\n private async loadJob(): Promise<void> {\n if (!this._scheduledJobID) {\n this.Job = null;\n this.cdr.markForCheck();\n return;\n }\n\n this.IsLoading = true;\n this.cdr.markForCheck();\n\n this.Job = await this.scheduledJobService.LoadJob(this._scheduledJobID);\n const runs = await this.scheduledJobService.LoadJobRuns(this._scheduledJobID, 50);\n this.TotalRuns = runs.length;\n this.SuccessRuns = runs.filter(r => r.Status === 'Completed').length;\n this.FailedRuns = runs.filter(r => r.Status === 'Failed').length;\n this.LastRun = runs.length > 0 ? runs[0] : null;\n\n this.IsLoading = false;\n this.cdr.markForCheck();\n }\n}\n","@if (IsLoading) {\n <div class=\"summary-loading\">\n <mj-loading size=\"small\"></mj-loading>\n </div>\n} @else if (Job) {\n <div class=\"summary-card\">\n <div class=\"summary-header\">\n <div class=\"summary-name\">\n <i class=\"fa-solid fa-calendar-check\"></i>\n {{ Job.Name }}\n </div>\n <span class=\"status-chip\" [class]=\"StatusClass\">{{ Job.Status }}</span>\n </div>\n\n <div class=\"summary-meta\">\n <div class=\"meta-item\">\n <i class=\"fa-regular fa-clock\"></i>\n <span>{{ Job.CronExpression }}</span>\n </div>\n @if (Job.Timezone && Job.Timezone !== 'UTC') {\n <div class=\"meta-item\">\n <i class=\"fa-solid fa-globe\"></i>\n <span>{{ Job.Timezone }}</span>\n </div>\n }\n </div>\n\n <div class=\"summary-stats\">\n <div class=\"stat\">\n <span class=\"stat-num\">{{ TotalRuns }}</span>\n <span class=\"stat-lbl\">Runs</span>\n </div>\n <div class=\"stat\">\n <span class=\"stat-num success\">{{ SuccessRuns }}</span>\n <span class=\"stat-lbl\">OK</span>\n </div>\n <div class=\"stat\">\n <span class=\"stat-num error\">{{ FailedRuns }}</span>\n <span class=\"stat-lbl\">Failed</span>\n </div>\n <div class=\"stat\">\n <span class=\"stat-num\">{{ SuccessRate }}</span>\n <span class=\"stat-lbl\">Rate</span>\n </div>\n </div>\n\n @if (ShowEditButton) {\n <div class=\"summary-actions\">\n <button class=\"edit-btn\" (click)=\"OnEditClick()\">\n <i class=\"fa-solid fa-pen-to-square\"></i> Edit Schedule\n </button>\n </div>\n }\n </div>\n} @else {\n <mj-empty-state class=\"summary-empty\" Size=\"compact\"\n Icon=\"fa-regular fa-calendar-xmark\"\n Title=\"No schedule configured\" />\n}\n"]}
|
|
@@ -9,7 +9,7 @@ import * as i7 from "@memberjunction/ng-shared-generic";
|
|
|
9
9
|
import * as i8 from "@memberjunction/ng-ui-components";
|
|
10
10
|
export declare class SchedulingModule {
|
|
11
11
|
static ɵfac: i0.ɵɵFactoryDeclaration<SchedulingModule, never>;
|
|
12
|
-
static ɵmod: i0.ɵɵNgModuleDeclaration<SchedulingModule, [typeof i1.ScheduledJobEditorComponent, typeof i2.ScheduledJobSummaryComponent, typeof i3.ScheduledJobSlidePanelComponent, typeof i4.ScheduledJobDialogComponent], [typeof i5.CommonModule, typeof i6.FormsModule, typeof i7.SharedGenericModule, typeof i8.MJDialogComponent], [typeof i1.ScheduledJobEditorComponent, typeof i2.ScheduledJobSummaryComponent, typeof i3.ScheduledJobSlidePanelComponent, typeof i4.ScheduledJobDialogComponent]>;
|
|
12
|
+
static ɵmod: i0.ɵɵNgModuleDeclaration<SchedulingModule, [typeof i1.ScheduledJobEditorComponent, typeof i2.ScheduledJobSummaryComponent, typeof i3.ScheduledJobSlidePanelComponent, typeof i4.ScheduledJobDialogComponent], [typeof i5.CommonModule, typeof i6.FormsModule, typeof i7.SharedGenericModule, typeof i8.MJDialogComponent, typeof i8.MJEmptyStateComponent], [typeof i1.ScheduledJobEditorComponent, typeof i2.ScheduledJobSummaryComponent, typeof i3.ScheduledJobSlidePanelComponent, typeof i4.ScheduledJobDialogComponent]>;
|
|
13
13
|
static ɵinj: i0.ɵɵInjectorDeclaration<SchedulingModule>;
|
|
14
14
|
}
|
|
15
15
|
//# sourceMappingURL=scheduling.module.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scheduling.module.d.ts","sourceRoot":"","sources":["../../src/lib/scheduling.module.ts"],"names":[],"mappings":";;;;;;;;;AAWA,
|
|
1
|
+
{"version":3,"file":"scheduling.module.d.ts","sourceRoot":"","sources":["../../src/lib/scheduling.module.ts"],"names":[],"mappings":";;;;;;;;;AAWA,qBAqBa,gBAAgB;yCAAhB,gBAAgB;0CAAhB,gBAAgB;0CAAhB,gBAAgB;CAAG"}
|
|
@@ -2,7 +2,7 @@ import { NgModule } from '@angular/core';
|
|
|
2
2
|
import { CommonModule } from '@angular/common';
|
|
3
3
|
import { FormsModule } from '@angular/forms';
|
|
4
4
|
import { SharedGenericModule } from '@memberjunction/ng-shared-generic';
|
|
5
|
-
import { MJDialogComponent } from '@memberjunction/ng-ui-components';
|
|
5
|
+
import { MJDialogComponent, MJEmptyStateComponent } from '@memberjunction/ng-ui-components';
|
|
6
6
|
import { ScheduledJobEditorComponent } from './panels/scheduled-job-editor/scheduled-job-editor.component';
|
|
7
7
|
import { ScheduledJobSummaryComponent } from './panels/scheduled-job-summary/scheduled-job-summary.component';
|
|
8
8
|
import { ScheduledJobSlidePanelComponent } from './slide-panel/scheduled-job-slide-panel.component';
|
|
@@ -16,14 +16,16 @@ export class SchedulingModule {
|
|
|
16
16
|
ScheduledJobDialogComponent], imports: [CommonModule,
|
|
17
17
|
FormsModule,
|
|
18
18
|
SharedGenericModule,
|
|
19
|
-
MJDialogComponent
|
|
19
|
+
MJDialogComponent,
|
|
20
|
+
MJEmptyStateComponent], exports: [ScheduledJobEditorComponent,
|
|
20
21
|
ScheduledJobSummaryComponent,
|
|
21
22
|
ScheduledJobSlidePanelComponent,
|
|
22
23
|
ScheduledJobDialogComponent] });
|
|
23
24
|
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SchedulingModule, imports: [CommonModule,
|
|
24
25
|
FormsModule,
|
|
25
26
|
SharedGenericModule,
|
|
26
|
-
MJDialogComponent
|
|
27
|
+
MJDialogComponent,
|
|
28
|
+
MJEmptyStateComponent] });
|
|
27
29
|
}
|
|
28
30
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: SchedulingModule, decorators: [{
|
|
29
31
|
type: NgModule,
|
|
@@ -39,6 +41,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
39
41
|
FormsModule,
|
|
40
42
|
SharedGenericModule,
|
|
41
43
|
MJDialogComponent,
|
|
44
|
+
MJEmptyStateComponent,
|
|
42
45
|
],
|
|
43
46
|
exports: [
|
|
44
47
|
ScheduledJobEditorComponent,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scheduling.module.js","sourceRoot":"","sources":["../../src/lib/scheduling.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"scheduling.module.js","sourceRoot":"","sources":["../../src/lib/scheduling.module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;AAE5F,OAAO,EAAE,2BAA2B,EAAE,MAAM,8DAA8D,CAAC;AAC3G,OAAO,EAAE,4BAA4B,EAAE,MAAM,gEAAgE,CAAC;AAC9G,OAAO,EAAE,+BAA+B,EAAE,MAAM,mDAAmD,CAAC;AACpG,OAAO,EAAE,2BAA2B,EAAE,MAAM,0CAA0C,CAAC;;AAuBvF,MAAM,OAAO,gBAAgB;uGAAhB,gBAAgB;wGAAhB,gBAAgB,iBAnBrB,2BAA2B;YAC3B,4BAA4B;YAC5B,+BAA+B;YAC/B,2BAA2B,aAG3B,YAAY;YACZ,WAAW;YACX,mBAAmB;YACnB,iBAAiB;YACjB,qBAAqB,aAGrB,2BAA2B;YAC3B,4BAA4B;YAC5B,+BAA+B;YAC/B,2BAA2B;wGAGtB,gBAAgB,YAbrB,YAAY;YACZ,WAAW;YACX,mBAAmB;YACnB,iBAAiB;YACjB,qBAAqB;;2FAShB,gBAAgB;kBArB5B,QAAQ;mBAAC;oBACN,YAAY,EAAE;wBACV,2BAA2B;wBAC3B,4BAA4B;wBAC5B,+BAA+B;wBAC/B,2BAA2B;qBAC9B;oBACD,OAAO,EAAE;wBACL,YAAY;wBACZ,WAAW;wBACX,mBAAmB;wBACnB,iBAAiB;wBACjB,qBAAqB;qBACxB;oBACD,OAAO,EAAE;wBACL,2BAA2B;wBAC3B,4BAA4B;wBAC5B,+BAA+B;wBAC/B,2BAA2B;qBAC9B;iBACJ","sourcesContent":["import { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { SharedGenericModule } from '@memberjunction/ng-shared-generic';\nimport { MJDialogComponent, MJEmptyStateComponent } from '@memberjunction/ng-ui-components';\n\nimport { ScheduledJobEditorComponent } from './panels/scheduled-job-editor/scheduled-job-editor.component';\nimport { ScheduledJobSummaryComponent } from './panels/scheduled-job-summary/scheduled-job-summary.component';\nimport { ScheduledJobSlidePanelComponent } from './slide-panel/scheduled-job-slide-panel.component';\nimport { ScheduledJobDialogComponent } from './dialogs/scheduled-job-dialog.component';\n\n@NgModule({\n declarations: [\n ScheduledJobEditorComponent,\n ScheduledJobSummaryComponent,\n ScheduledJobSlidePanelComponent,\n ScheduledJobDialogComponent,\n ],\n imports: [\n CommonModule,\n FormsModule,\n SharedGenericModule,\n MJDialogComponent,\n MJEmptyStateComponent,\n ],\n exports: [\n ScheduledJobEditorComponent,\n ScheduledJobSummaryComponent,\n ScheduledJobSlidePanelComponent,\n ScheduledJobDialogComponent,\n ],\n})\nexport class SchedulingModule {}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/ng-scheduling",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.44.0",
|
|
4
4
|
"description": "MemberJunction: Reusable Angular components for viewing and editing Scheduled Jobs - panels, slide-in, dialog, and service",
|
|
5
5
|
"main": "./dist/public-api.js",
|
|
6
6
|
"typings": "./dist/public-api.d.ts",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@angular/compiler": "21.1.3",
|
|
25
25
|
"@angular/compiler-cli": "21.1.3",
|
|
26
|
+
"@memberjunction/ng-test-utils": "5.44.0",
|
|
26
27
|
"vitest": "^4.0.18"
|
|
27
28
|
},
|
|
28
29
|
"peerDependencies": {
|
|
@@ -31,13 +32,13 @@
|
|
|
31
32
|
"@angular/forms": "21.1.3"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
|
-
"@memberjunction/core": "5.
|
|
35
|
-
"@memberjunction/ng-base-types": "5.
|
|
36
|
-
"@memberjunction/core-entities": "5.
|
|
37
|
-
"@memberjunction/global": "5.
|
|
38
|
-
"@memberjunction/ng-notifications": "5.
|
|
39
|
-
"@memberjunction/ng-shared-generic": "5.
|
|
40
|
-
"@memberjunction/ng-ui-components": "5.
|
|
35
|
+
"@memberjunction/core": "5.44.0",
|
|
36
|
+
"@memberjunction/ng-base-types": "5.44.0",
|
|
37
|
+
"@memberjunction/core-entities": "5.44.0",
|
|
38
|
+
"@memberjunction/global": "5.44.0",
|
|
39
|
+
"@memberjunction/ng-notifications": "5.44.0",
|
|
40
|
+
"@memberjunction/ng-shared-generic": "5.44.0",
|
|
41
|
+
"@memberjunction/ng-ui-components": "5.44.0",
|
|
41
42
|
"tslib": "^2.8.1",
|
|
42
43
|
"rxjs": "^7.8.2"
|
|
43
44
|
},
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/index.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../../src/__tests__/index.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C,QAAQ,CAAC,+BAA+B,EAAE,GAAG,EAAE;IAC7C,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, it, expect } from 'vitest';\n\ndescribe('@memberjunction/ng-scheduling', () => {\n it('should have a passing test', () => {\n expect(true).toBe(true);\n });\n});\n"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"scheduled-job-service.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/scheduled-job-service.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
/**
|
|
3
|
-
* Tests for ScheduledJobService caching, loading, and delegation logic.
|
|
4
|
-
*/
|
|
5
|
-
vi.mock('@angular/core', () => ({
|
|
6
|
-
Injectable: () => (target) => target,
|
|
7
|
-
}));
|
|
8
|
-
// Shared mock for RunView.RunView method — we reassign per test
|
|
9
|
-
const mockRunViewMethod = vi.fn();
|
|
10
|
-
// Shared mock for entity Load
|
|
11
|
-
const mockEntityLoad = vi.fn().mockResolvedValue(true);
|
|
12
|
-
vi.mock('@memberjunction/core', () => {
|
|
13
|
-
const MockMetadata = function Metadata() {
|
|
14
|
-
return {
|
|
15
|
-
GetEntityObject: vi.fn().mockResolvedValue({
|
|
16
|
-
Load: mockEntityLoad,
|
|
17
|
-
ID: 'job-001',
|
|
18
|
-
Name: 'Test Job',
|
|
19
|
-
}),
|
|
20
|
-
Entities: [],
|
|
21
|
-
};
|
|
22
|
-
};
|
|
23
|
-
// Add static Provider for the migration's `?? Metadata.Provider` fallback in services.
|
|
24
|
-
// Use a getter for GetEntityObject so we don't reference top-level let/const variables
|
|
25
|
-
// at vi.mock factory hoist time.
|
|
26
|
-
MockMetadata.Provider = {
|
|
27
|
-
Entities: [],
|
|
28
|
-
GetEntityObject(..._args) {
|
|
29
|
-
return Promise.resolve({
|
|
30
|
-
Load: mockEntityLoad,
|
|
31
|
-
ID: 'job-001',
|
|
32
|
-
Name: 'Test Job',
|
|
33
|
-
});
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
const MockRunView = function RunView() { return { RunView: mockRunViewMethod }; };
|
|
37
|
-
// Multi-provider migration: services now use RunView.FromMetadataProvider(...) instead of `new RunView()`.
|
|
38
|
-
MockRunView.FromMetadataProvider = (_p) => ({ RunView: mockRunViewMethod });
|
|
39
|
-
return { Metadata: MockMetadata, RunView: MockRunView };
|
|
40
|
-
});
|
|
41
|
-
vi.mock('@memberjunction/core-entities', () => ({
|
|
42
|
-
MJScheduledJobEntity: class {
|
|
43
|
-
},
|
|
44
|
-
MJScheduledJobTypeEntity: class {
|
|
45
|
-
},
|
|
46
|
-
MJScheduledJobRunEntity: class {
|
|
47
|
-
},
|
|
48
|
-
}));
|
|
49
|
-
import { ScheduledJobService } from '../lib/services/scheduled-job.service';
|
|
50
|
-
describe('ScheduledJobService', () => {
|
|
51
|
-
let service;
|
|
52
|
-
beforeEach(() => {
|
|
53
|
-
vi.clearAllMocks();
|
|
54
|
-
mockEntityLoad.mockResolvedValue(true);
|
|
55
|
-
service = new ScheduledJobService();
|
|
56
|
-
});
|
|
57
|
-
describe('initial state', () => {
|
|
58
|
-
it('should have empty JobTypes initially', () => {
|
|
59
|
-
expect(service.JobTypes).toEqual([]);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
describe('LoadJobTypes', () => {
|
|
63
|
-
it('should load job types from RunView on first call', async () => {
|
|
64
|
-
const mockTypes = [{ ID: 't1', Name: 'Type A' }, { ID: 't2', Name: 'Type B' }];
|
|
65
|
-
mockRunViewMethod.mockResolvedValue({ Success: true, Results: mockTypes });
|
|
66
|
-
const result = await service.LoadJobTypes();
|
|
67
|
-
expect(result).toEqual(mockTypes);
|
|
68
|
-
expect(service.JobTypes).toEqual(mockTypes);
|
|
69
|
-
expect(mockRunViewMethod).toHaveBeenCalledTimes(1);
|
|
70
|
-
});
|
|
71
|
-
it('should return cached data on subsequent calls without re-fetching', async () => {
|
|
72
|
-
const mockTypes = [{ ID: 't1', Name: 'Type A' }];
|
|
73
|
-
mockRunViewMethod.mockResolvedValue({ Success: true, Results: mockTypes });
|
|
74
|
-
await service.LoadJobTypes();
|
|
75
|
-
const result2 = await service.LoadJobTypes();
|
|
76
|
-
expect(result2).toEqual(mockTypes);
|
|
77
|
-
expect(mockRunViewMethod).toHaveBeenCalledTimes(1);
|
|
78
|
-
});
|
|
79
|
-
it('should deduplicate concurrent calls (promise sharing)', async () => {
|
|
80
|
-
const mockTypes = [{ ID: 't1', Name: 'Type A' }];
|
|
81
|
-
mockRunViewMethod.mockResolvedValue({ Success: true, Results: mockTypes });
|
|
82
|
-
const [r1, r2] = await Promise.all([service.LoadJobTypes(), service.LoadJobTypes()]);
|
|
83
|
-
expect(r1).toEqual(mockTypes);
|
|
84
|
-
expect(r2).toEqual(mockTypes);
|
|
85
|
-
expect(mockRunViewMethod).toHaveBeenCalledTimes(1);
|
|
86
|
-
});
|
|
87
|
-
it('should return empty array when RunView fails', async () => {
|
|
88
|
-
mockRunViewMethod.mockResolvedValue({ Success: false, Results: [] });
|
|
89
|
-
const result = await service.LoadJobTypes();
|
|
90
|
-
expect(result).toEqual([]);
|
|
91
|
-
});
|
|
92
|
-
it('should call RunView with correct params', async () => {
|
|
93
|
-
mockRunViewMethod.mockResolvedValue({ Success: true, Results: [] });
|
|
94
|
-
await service.LoadJobTypes();
|
|
95
|
-
expect(mockRunViewMethod).toHaveBeenCalledWith(expect.objectContaining({
|
|
96
|
-
EntityName: 'MJ: Scheduled Job Types',
|
|
97
|
-
OrderBy: 'Name',
|
|
98
|
-
ResultType: 'entity_object',
|
|
99
|
-
}));
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
describe('ClearCache', () => {
|
|
103
|
-
it('should clear cached job types', async () => {
|
|
104
|
-
mockRunViewMethod.mockResolvedValue({ Success: true, Results: [{ ID: 't1' }] });
|
|
105
|
-
await service.LoadJobTypes();
|
|
106
|
-
expect(service.JobTypes).toHaveLength(1);
|
|
107
|
-
service.ClearCache();
|
|
108
|
-
expect(service.JobTypes).toEqual([]);
|
|
109
|
-
});
|
|
110
|
-
it('should force re-fetch after clear', async () => {
|
|
111
|
-
mockRunViewMethod
|
|
112
|
-
.mockResolvedValueOnce({ Success: true, Results: [{ ID: 't1' }] })
|
|
113
|
-
.mockResolvedValueOnce({ Success: true, Results: [{ ID: 't1' }, { ID: 't2' }] });
|
|
114
|
-
await service.LoadJobTypes();
|
|
115
|
-
service.ClearCache();
|
|
116
|
-
await service.LoadJobTypes();
|
|
117
|
-
expect(service.JobTypes).toHaveLength(2);
|
|
118
|
-
expect(mockRunViewMethod).toHaveBeenCalledTimes(2);
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
describe('LoadJob', () => {
|
|
122
|
-
it('should load a single job by ID', async () => {
|
|
123
|
-
const result = await service.LoadJob('job-001');
|
|
124
|
-
expect(result).not.toBeNull();
|
|
125
|
-
});
|
|
126
|
-
it('should return null when job load fails', async () => {
|
|
127
|
-
mockEntityLoad.mockResolvedValueOnce(false);
|
|
128
|
-
const result = await service.LoadJob('nonexistent');
|
|
129
|
-
expect(result).toBeNull();
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
describe('LoadJobRuns', () => {
|
|
133
|
-
it('should load runs with correct filter and ordering', async () => {
|
|
134
|
-
const mockRuns = [{ ID: 'run-1' }, { ID: 'run-2' }];
|
|
135
|
-
mockRunViewMethod.mockResolvedValue({ Success: true, Results: mockRuns });
|
|
136
|
-
const result = await service.LoadJobRuns('job-001');
|
|
137
|
-
expect(result).toEqual(mockRuns);
|
|
138
|
-
expect(mockRunViewMethod).toHaveBeenCalledWith(expect.objectContaining({
|
|
139
|
-
EntityName: 'MJ: Scheduled Job Runs',
|
|
140
|
-
ExtraFilter: "ScheduledJobID='job-001'",
|
|
141
|
-
OrderBy: 'StartedAt DESC',
|
|
142
|
-
MaxRows: 10,
|
|
143
|
-
}));
|
|
144
|
-
});
|
|
145
|
-
it('should respect custom maxRows', async () => {
|
|
146
|
-
mockRunViewMethod.mockResolvedValue({ Success: true, Results: [] });
|
|
147
|
-
await service.LoadJobRuns('job-001', 25);
|
|
148
|
-
expect(mockRunViewMethod).toHaveBeenCalledWith(expect.objectContaining({ MaxRows: 25 }));
|
|
149
|
-
});
|
|
150
|
-
it('should return empty array on failure', async () => {
|
|
151
|
-
mockRunViewMethod.mockResolvedValue({ Success: false, Results: [] });
|
|
152
|
-
const result = await service.LoadJobRuns('job-001');
|
|
153
|
-
expect(result).toEqual([]);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
//# sourceMappingURL=scheduled-job-service.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"scheduled-job-service.test.js","sourceRoot":"","sources":["../../src/__tests__/scheduled-job-service.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect, vi, beforeEach } from 'vitest';\n\n/**\n * Tests for ScheduledJobService caching, loading, and delegation logic.\n */\n\nvi.mock('@angular/core', () => ({\n Injectable: () => (target: Function) => target,\n}));\n\n// Shared mock for RunView.RunView method — we reassign per test\nconst mockRunViewMethod = vi.fn();\n// Shared mock for entity Load\nconst mockEntityLoad = vi.fn().mockResolvedValue(true);\n\nvi.mock('@memberjunction/core', () => {\n const MockMetadata: any = function Metadata() {\n return {\n GetEntityObject: vi.fn().mockResolvedValue({\n Load: mockEntityLoad,\n ID: 'job-001',\n Name: 'Test Job',\n }),\n Entities: [],\n };\n };\n // Add static Provider for the migration's `?? Metadata.Provider` fallback in services.\n // Use a getter for GetEntityObject so we don't reference top-level let/const variables\n // at vi.mock factory hoist time.\n MockMetadata.Provider = {\n Entities: [],\n GetEntityObject(..._args: unknown[]) {\n return Promise.resolve({\n Load: mockEntityLoad,\n ID: 'job-001',\n Name: 'Test Job',\n });\n },\n };\n const MockRunView: any = function RunView() { return { RunView: mockRunViewMethod }; };\n // Multi-provider migration: services now use RunView.FromMetadataProvider(...) instead of `new RunView()`.\n MockRunView.FromMetadataProvider = (_p: unknown) => ({ RunView: mockRunViewMethod });\n return { Metadata: MockMetadata, RunView: MockRunView };\n});\n\nvi.mock('@memberjunction/core-entities', () => ({\n MJScheduledJobEntity: class {},\n MJScheduledJobTypeEntity: class {},\n MJScheduledJobRunEntity: class {},\n}));\n\nimport { ScheduledJobService } from '../lib/services/scheduled-job.service';\n\ndescribe('ScheduledJobService', () => {\n let service: ScheduledJobService;\n\n beforeEach(() => {\n vi.clearAllMocks();\n mockEntityLoad.mockResolvedValue(true);\n service = new ScheduledJobService();\n });\n\n describe('initial state', () => {\n it('should have empty JobTypes initially', () => {\n expect(service.JobTypes).toEqual([]);\n });\n });\n\n describe('LoadJobTypes', () => {\n it('should load job types from RunView on first call', async () => {\n const mockTypes = [{ ID: 't1', Name: 'Type A' }, { ID: 't2', Name: 'Type B' }];\n mockRunViewMethod.mockResolvedValue({ Success: true, Results: mockTypes });\n\n const result = await service.LoadJobTypes();\n expect(result).toEqual(mockTypes);\n expect(service.JobTypes).toEqual(mockTypes);\n expect(mockRunViewMethod).toHaveBeenCalledTimes(1);\n });\n\n it('should return cached data on subsequent calls without re-fetching', async () => {\n const mockTypes = [{ ID: 't1', Name: 'Type A' }];\n mockRunViewMethod.mockResolvedValue({ Success: true, Results: mockTypes });\n\n await service.LoadJobTypes();\n const result2 = await service.LoadJobTypes();\n expect(result2).toEqual(mockTypes);\n expect(mockRunViewMethod).toHaveBeenCalledTimes(1);\n });\n\n it('should deduplicate concurrent calls (promise sharing)', async () => {\n const mockTypes = [{ ID: 't1', Name: 'Type A' }];\n mockRunViewMethod.mockResolvedValue({ Success: true, Results: mockTypes });\n\n const [r1, r2] = await Promise.all([service.LoadJobTypes(), service.LoadJobTypes()]);\n expect(r1).toEqual(mockTypes);\n expect(r2).toEqual(mockTypes);\n expect(mockRunViewMethod).toHaveBeenCalledTimes(1);\n });\n\n it('should return empty array when RunView fails', async () => {\n mockRunViewMethod.mockResolvedValue({ Success: false, Results: [] });\n const result = await service.LoadJobTypes();\n expect(result).toEqual([]);\n });\n\n it('should call RunView with correct params', async () => {\n mockRunViewMethod.mockResolvedValue({ Success: true, Results: [] });\n await service.LoadJobTypes();\n expect(mockRunViewMethod).toHaveBeenCalledWith(expect.objectContaining({\n EntityName: 'MJ: Scheduled Job Types',\n OrderBy: 'Name',\n ResultType: 'entity_object',\n }));\n });\n });\n\n describe('ClearCache', () => {\n it('should clear cached job types', async () => {\n mockRunViewMethod.mockResolvedValue({ Success: true, Results: [{ ID: 't1' }] });\n await service.LoadJobTypes();\n expect(service.JobTypes).toHaveLength(1);\n\n service.ClearCache();\n expect(service.JobTypes).toEqual([]);\n });\n\n it('should force re-fetch after clear', async () => {\n mockRunViewMethod\n .mockResolvedValueOnce({ Success: true, Results: [{ ID: 't1' }] })\n .mockResolvedValueOnce({ Success: true, Results: [{ ID: 't1' }, { ID: 't2' }] });\n\n await service.LoadJobTypes();\n service.ClearCache();\n await service.LoadJobTypes();\n\n expect(service.JobTypes).toHaveLength(2);\n expect(mockRunViewMethod).toHaveBeenCalledTimes(2);\n });\n });\n\n describe('LoadJob', () => {\n it('should load a single job by ID', async () => {\n const result = await service.LoadJob('job-001');\n expect(result).not.toBeNull();\n });\n\n it('should return null when job load fails', async () => {\n mockEntityLoad.mockResolvedValueOnce(false);\n const result = await service.LoadJob('nonexistent');\n expect(result).toBeNull();\n });\n });\n\n describe('LoadJobRuns', () => {\n it('should load runs with correct filter and ordering', async () => {\n const mockRuns = [{ ID: 'run-1' }, { ID: 'run-2' }];\n mockRunViewMethod.mockResolvedValue({ Success: true, Results: mockRuns });\n\n const result = await service.LoadJobRuns('job-001');\n expect(result).toEqual(mockRuns);\n expect(mockRunViewMethod).toHaveBeenCalledWith(expect.objectContaining({\n EntityName: 'MJ: Scheduled Job Runs',\n ExtraFilter: \"ScheduledJobID='job-001'\",\n OrderBy: 'StartedAt DESC',\n MaxRows: 10,\n }));\n });\n\n it('should respect custom maxRows', async () => {\n mockRunViewMethod.mockResolvedValue({ Success: true, Results: [] });\n await service.LoadJobRuns('job-001', 25);\n expect(mockRunViewMethod).toHaveBeenCalledWith(expect.objectContaining({ MaxRows: 25 }));\n });\n\n it('should return empty array on failure', async () => {\n mockRunViewMethod.mockResolvedValue({ Success: false, Results: [] });\n const result = await service.LoadJobRuns('job-001');\n expect(result).toEqual([]);\n });\n });\n});\n"]}
|