diff --git a/src/app/common/modals/csv-result-modal/csv-modals.component.scss b/src/app/common/modals/csv-result-modal/csv-modals.component.scss new file mode 100644 index 0000000000..00552cd306 --- /dev/null +++ b/src/app/common/modals/csv-result-modal/csv-modals.component.scss @@ -0,0 +1,265 @@ +/* Shared container */ +.csv-modal { + display: flex; + flex-direction: column; + gap: .6rem; /* slightly tighter overall */ + + .modal-header { + display: flex; + align-items: flex-start; + gap: .5rem; + border-bottom: 1px solid #e6e6e6; + padding-bottom: .5rem; + + .icon { margin-top: .15rem; i { font-size: 1.4rem; color: #444; } } + + .titles { + .modal-title { margin: 0; font-size: 1.18rem; font-weight: 600; color: #222; } /* a bit bigger */ + .subtitle { margin-top: .15rem; font-size: .8rem; color: #888; } + } + } + + .modal-footer { + display: flex; + justify-content: flex-end; + padding-top: .25rem; + } + + .btn { + border: 1px solid #cdd2d6; + background: #fff; + color: #111; + padding: .25rem .5rem; + line-height: 1.1; + border-radius: 4px; + cursor: pointer; + &.primary { + background: #2b6cb0; + border-color: #2b6cb0; + color: #fff; + } + &.link { + background: #fff; + color: #3b7ddd; + border-color: #cdd2d6; + } + &:disabled { opacity: .5; cursor: not-allowed; } + } +} + +/* Segmented controls (Success / Errors / Ignored) */ +.segmented { + display: inline-flex; + gap: .5rem; + + .seg { + position: relative; + font-weight: 600; + + &.is-active { + box-shadow: inset 0 0 0 2px #111; + } + + .badge { + margin-left: .35rem; + font-weight: 700; + border-radius: 9px; + padding: .05rem .35rem; + background: #e9ecef; + color: #000; + border: 1px solid #d5d9dd; + font-size: .72rem; + } + + &.success.is-active { + background: #28a745; + border-color: #218838; + color: #fff; + .badge { background: #2ecc71; color: #fff; border-color: #27ae60; } + } + &.error.is-active { + background: #dc3545; + border-color: #c82333; + color: #fff; + .badge { background: #e74c3c; color: #fff; border-color: #c0392b; } + } + &.warn.is-active { + background: #f0ad4e; + border-color: #ec9a2c; + color: #fff; + .badge { background: #f6c36d; color: #fff; border-color: #ec9a2c; } + } + } +} + +/* Empty state */ +.callout { + padding: .6rem; + background: #f7fbff; + border: 1px solid #d6e9ff; + border-radius: 6px; + color: #2b4d7a; +} + +/* Table */ +.table-wrap { + overflow: auto; + max-height: 60vh; + border: 1px solid #eee; + border-radius: 6px; + + .table { + width: 100%; + border-collapse: collapse; + + thead th { + position: sticky; + top: 0; + background: #f7f7f7; + border-bottom: 1px solid #e5e5e5; + font-weight: 700; /* bold headers */ + padding: .5rem .6rem; + white-space: nowrap; + } + tbody td { + padding: .5rem .6rem; + border-top: 1px solid #f0f0f0; + vertical-align: top; + } + + .message-cell pre { + margin: 0; + white-space: pre-wrap; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: .78rem; + color: #333; + } + + .csv-cell { + white-space: nowrap; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: .85rem; + } + } +} + +/* Pager */ +.pager { + display: flex; + align-items: center; + gap: .35rem; + + .page { min-width: 1.8rem; text-align: center; color: #2b6cb0; } + .page.current { background: #2b6cb0; border-color: #2b6cb0; color: #fff; } + .dots { padding: 0 .35rem; color: #7d8aa0; } +} + +/* Upload modal specifics */ +.csv-upload-modal { + .modal-body { display: grid; gap: .5rem; } /* tighter vertical spacing */ + .helper-text { color: #3b3b3b; margin: 0; font-size: .95rem; } /* a bit bigger */ + + .dropzone { + border: 2px dashed #c7d2e3; + border-radius: 8px; + padding: 1rem; /* slightly less padding */ + text-align: center; + background: #f8fbff; + outline: none; + + &:focus-visible { box-shadow: 0 0 0 3px rgba(43,108,176,.35); } + + .dz-content { + display: grid; + justify-items: center; + gap: .2rem; + } + .dz-icon { font-size: 1.6rem; color: #2b6cb0; } + .dz-title { font-weight: 600; color: #274c77; } + .dz-sub { color: #6b7280; font-size: .85rem; } + } + + .hidden-file { display: none; } + + .file-row { + display: inline-flex; + align-items: center; + gap: .5rem; + margin-top: .25rem; + .name { font-weight: 600; } + .start { + background: #2b6cb0; color: #fff; border-color: #2b6cb0; + padding: .25rem .5rem; border-radius: 4px; + } + } + + .uploading { + display: grid; + gap: .5rem; + + .icons { + display: flex; + justify-content: center; + align-items: center; + gap: .5rem; + color: #2b6cb0; + .arrow { font-weight: 700; } + } + + .progress { + height: 12px; /* thicker like legacy */ + background: #e5eefc; + border-radius: 6px; + overflow: hidden; + border: 1px solid #c6d7f7; + + .bar { + height: 100%; + background-image: repeating-linear-gradient( + 45deg, + #3967c9 0, + #3967c9 10px, + #5a82d9 10px, + #5a82d9 20px + ); + transition: width .2s ease; + } + } + } + + .error { color: #b00020; } +} + +/* Upload label + minor spacing */ +.csv-upload-modal { + .modal-body { gap: .5rem; } + .field-label { + font-size: .95rem; + font-weight: 600; + color: #444; + margin-bottom: .25rem; + } + + .dropzone .dz-sub { + text-decoration: underline; + cursor: pointer; + } + + .file-row { + gap: .5rem; + .remove { + border-color: #cdd2d6; + width: 1.6rem; + height: 1.6rem; + line-height: 1.1; + padding: 0; + font-weight: 700; + color: #555; + } + } +} + +.csv-result-modal .table .message-cell { + width: 260px; + min-width: 240px; +} diff --git a/src/app/common/modals/csv-result-modal/csv-modals.component.ts b/src/app/common/modals/csv-result-modal/csv-modals.component.ts new file mode 100644 index 0000000000..105c7aaa08 --- /dev/null +++ b/src/app/common/modals/csv-result-modal/csv-modals.component.ts @@ -0,0 +1,424 @@ +import { Injectable, Component, Inject, HostListener } from '@angular/core'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; + +/* ============================================================================ + Normalisation helpers +============================================================================ */ + +type RowLike = unknown; +type RowObject = Record; + +/** Canonical task-definition columns in required order (API / CSV spec). */ +const CANON_KEYS = [ + 'name', + 'abbreviation', + 'description', + 'weighting', + 'target_grade', + 'restrict_status_updates', + 'max_quality_pts', + 'is_graded', + 'plagiarism_warn_pct', + 'group_set', + 'upload_requirements', + 'start_week', + 'start_day', + 'target_week', + 'target_day', + 'due_week', + 'due_day', + 'tutorial_stream', +] as const; +export type CanonKey = (typeof CANON_KEYS)[number]; + +/** Labels for headers */ +const PRETTY_LABEL: Record = Object.fromEntries( + (CANON_KEYS as readonly string[]).map(k => [ + k, + k.split('_').map(s => (s ? s[0].toUpperCase() + s.slice(1) : s)).join(' '), + ]), +); +// also for ZIP +PRETTY_LABEL['data'] = 'Data'; + +/** Compactly stringify any non-scalar values. */ +function compact(value: unknown): string { + if (value == null) return ''; + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return String(value); + } + try { return JSON.stringify(value); } catch { return String(value); } +} + +/** Try to parse a JSON string safely. */ +function tryParseJSON(s: string): unknown | undefined { + try { return JSON.parse(s); } catch { return undefined; } +} + +/** If value is an array of [key,value] pairs, turn into object. */ +function pairsToObject(arr: unknown): RowObject | undefined { + if (Array.isArray(arr) && arr.every(x => Array.isArray(x) && x.length === 2 && typeof x[0] === 'string')) { + return Object.fromEntries(arr as [string, unknown][]); + } + return undefined; +} + +/** Regex fallback for strings like: ["name","Task"], ["abbreviation","1.1P"], ... (no outer [ ... ]) */ +function loosePairsToObject(s: string): RowObject | undefined { + const re = /\[\s*"([^"]+)"\s*,\s*"((?:\\"|[^"])*)"\s*\]/g; + let m: RegExpExecArray | null; + const out: RowObject = {}; + let found = false; + while ((m = re.exec(s)) !== null) { + out[m[1]] = m[2].replace(/\\"/g, '"'); + found = true; + } + return found ? out : undefined; +} + +/** Convert CSVish row → object, or null if nothing usable. */ +function parseRowToObject(row: RowLike): RowObject | null { + if (row == null) return null; + if (typeof row === 'object' && !Array.isArray(row)) return row as RowObject; + + const fromPairs = pairsToObject(row); + if (fromPairs) return fromPairs; + + if (typeof row === 'string') { + const s = row.trim(); + const j1 = tryParseJSON(s); + if (j1) { + const p = pairsToObject(j1); + if (p) return p; + if (typeof j1 === 'object' && !Array.isArray(j1)) return j1 as RowObject; + } + if (s.includes('["')) { + const wrapped = `[${s.replace(/,\s*$/, '')}]`; + const j2 = tryParseJSON(wrapped); + const p2 = pairsToObject(j2); + if (p2) return p2; + } + const loose = loosePairsToObject(s); + if (loose) return loose; + } + return null; +} + +/** Compute displayed keys: canonical first (if present), extras alphabetically. */ +function computeDisplayedKeys(rows: RowObject[]): string[] { + const present = new Set(); + rows.forEach(r => Object.keys(r).forEach(k => present.add(k))); + const canon = CANON_KEYS.filter(k => present.has(k)); + const extra = Array.from(present).filter(k => !(canon as readonly string[]).includes(k)).sort(); + return [...canon, ...extra]; +} + +/* ============================================================================ + Shared types +============================================================================ */ + +export type TabKey = 'success' | 'errors' | 'ignored'; + +export interface CsvResponseItem { + message: string; + row?: RowLike; // CSV path + data?: unknown; // ZIP path (sometimes a string) +} + +export interface CsvResultResponse { + success: CsvResponseItem[]; + errors: CsvResponseItem[]; + ignored: CsvResponseItem[]; +} + +export interface CsvResultModalData { + title: string; + response: CsvResultResponse; +} + +export interface CsvUploadModalData { + title: string; + message?: string; + url: string; + accept?: string; + onSuccess?: (payload: unknown) => void; // legacy hook +} + +/* ============================================================================ + Dialog launcher +============================================================================ */ +@Injectable({ providedIn: 'root' }) +export class CsvDialogsService { + constructor(private dialog: MatDialog) {} + private resultRef?: MatDialogRef; + + openResult(data: CsvResultModalData) { + if (this.resultRef) { this.resultRef.close(); this.resultRef = undefined; } + this.resultRef = this.dialog.open(CsvResultModalComponent, { + width: '960px', + data, + autoFocus: false, + restoreFocus: false, + }); + this.resultRef.afterClosed().subscribe(() => (this.resultRef = undefined)); + return this.resultRef; + } + + openUpload(data: CsvUploadModalData) { + return this.dialog.open(CsvUploadModalComponent, { + width: '720px', + data, + autoFocus: false, + restoreFocus: false, + }); + } +} + +/* ============================================================================ + RESULT MODAL +============================================================================ */ + +@Component({ + selector: 'f-csv-result-modal', + templateUrl: './csv-result-modal.component.html', + styleUrls: ['./csv-modals.component.scss'], +}) +export class CsvResultModalComponent { + title = ''; + activeTab: TabKey = 'success'; + + /** ZIP vs CSV mode (affects the columns). */ + isZip = false; + + // table + displayedKeys: string[] = []; // 'data' for ZIP, canonical set for CSV + pretty: Record = PRETTY_LABEL; + + // data buckets ready to display + private buckets: Record }[]> = { + success: [], + errors: [], + ignored: [], + }; + + // pager + pageSize = 5; + currentPage = 1; + totalPages = 1; + + constructor( + private ref: MatDialogRef, + @Inject(MAT_DIALOG_DATA) data: CsvResultModalData, + ) { + this.title = data.title; + + // Decide ZIP vs CSV: by title OR by row shape (string-like) + const any = [...(data.response.success || []), ...(data.response.errors || []), ...(data.response.ignored || [])]; + const looksLikeZip = + /zip|sheet|resource/i.test(this.title) || + any.some(it => typeof it.data === 'string') || + any.every(it => typeof (it as any).row === 'string'); + + this.isZip = !!looksLikeZip; + + // Normalise + const normalised: Record = { + success: (data.response.success || []).map(i => ({ message: i.message, values: this.zipify(i) })), + errors: (data.response.errors || []).map(i => ({ message: i.message, values: this.zipify(i) })), + ignored: (data.response.ignored || []).map(i => ({ message: i.message, values: this.zipify(i) })), + }; + + // choose initial tab + if (normalised.errors.length) this.activeTab = 'errors'; + else if (normalised.success.length) this.activeTab = 'success'; + else this.activeTab = 'ignored'; + + // columns + this.displayedKeys = this.isZip + ? ['data'] + : computeDisplayedKeys(normalised[this.activeTab].map(x => x.values)); + + // convert values → strings for display + (['success', 'errors', 'ignored'] as TabKey[]).forEach(tab => { + this.buckets[tab] = normalised[tab].map(it => ({ + message: it.message, + values: Object.fromEntries( + this.displayedKeys.map(k => [k, compact((it.values as RowObject)[k])]), + ) as Record, + })); + }); + + this.recalcPager(); + } + + /** Turn any API item into a display object depending on mode. */ + private zipify(it: CsvResponseItem): RowObject { + if (!this.isZip) return parseRowToObject(it.row as RowLike) || {}; + // ZIP: make best-effort to extract a filename/string + const cand: any = (it as any).data ?? (it as any).row ?? ''; + if (typeof cand === 'string') return { data: cand }; + if (cand && typeof cand === 'object') { + const guess = cand.filename || cand.file || cand.name || cand.path || cand.url || ''; + return { data: guess ? String(guess) : JSON.stringify(cand) }; + } + return { data: '' }; + } + + /* ---- Stop bubbling that re-opens a second dialog ---- */ + @HostListener('click', ['$event']) onHostClick(e: MouseEvent) { e.stopPropagation(); } + @HostListener('mousedown', ['$event'])onHostDown(e: MouseEvent) { e.stopPropagation(); } + @HostListener('pointerdown', ['$event']) onHostPtr(e: PointerEvent) { e.stopPropagation(); } + + /* ---- table helpers ---- */ + count(tab: TabKey): number { return this.buckets[tab]?.length || 0; } + + setTab(tab: TabKey): void { + if (this.activeTab === tab) return; + this.activeTab = tab; + + // adjust columns for CSV only; ZIP is fixed 'data' + if (!this.isZip) { + const keys = computeDisplayedKeys(this.buckets[this.activeTab].map(x => x.values as RowObject)); + this.displayedKeys = keys; + const list = this.buckets[this.activeTab]; + this.buckets[this.activeTab] = list.map(it => ({ + message: it.message, + values: Object.fromEntries(this.displayedKeys.map(k => [k, compact((it.values as RowObject)[k])])) as Record, + })); + } + + this.currentPage = 1; + this.recalcPager(); + } + + pageRows(): { message: string; values: Record }[] { + const all = this.buckets[this.activeTab] || []; + const start = (this.currentPage - 1) * this.pageSize; + return all.slice(start, start + this.pageSize); + } + + /* ---- pager ---- */ + private recalcPager(): void { + const total = this.count(this.activeTab); + this.totalPages = Math.max(1, Math.ceil(total / this.pageSize)); + this.currentPage = Math.min(this.currentPage, this.totalPages); + } + + pageNumbers(): (number | '…')[] { + const pages: (number | '…')[] = []; + const total = this.totalPages; + if (total <= 7) { for (let i = 1; i <= total; i++) pages.push(i); return pages; } + const cur = this.currentPage; + pages.push(1); + const add = (n: number | '…') => pages.push(n); + if (cur > 3) add('…'); + for (let n = Math.max(2, cur - 1); n <= Math.min(total - 1, cur + 1); n++) add(n); + if (cur < total - 2) add('…'); + add(total); + return pages; + } + + goFirst(): void { if (this.currentPage > 1) this.currentPage = 1; } + goPrev(): void { if (this.currentPage > 1) this.currentPage--; } + goNext(): void { if (this.currentPage < this.totalPages) this.currentPage++; } + goLast(): void { if (this.currentPage < this.totalPages) this.currentPage = this.totalPages; } + goTo(p: number | '…'): void { + if (p === '…') return; + const n = Number(p); + if (!Number.isFinite(n)) return; + this.currentPage = Math.min(Math.max(1, n), this.totalPages); + } + + close(): void { this.ref.close(); } +} + +/* ============================================================================ + UPLOAD MODAL (unchanged CSV behaviour; wording/icon auto-switch) +============================================================================ */ + +type UploadStage = 'ready' | 'uploading'; + +@Component({ + selector: 'f-csv-upload-modal', + templateUrl: './csv-upload-modal.component.html', + styleUrls: ['./csv-modals.component.scss'], +}) +export class CsvUploadModalComponent { + stage: UploadStage = 'ready'; + file: File | null = null; + errorText = ''; + progress = 0; + + /** Infer intent from the dialog title (CSV vs ZIP). */ + get isZip(): boolean { return /zip|sheet|resource/i.test(this.data.title); } + get fileKind(): 'CSV' | 'ZIP' { return this.isZip ? 'ZIP' : 'CSV'; } + + constructor( + private http: HttpClient, + private dialog: MatDialog, + private ref: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: CsvUploadModalData, + ) {} + + // --- dropzone / pick --- + onFileChange(e: Event): void { + const input = e.target as HTMLInputElement; + this.file = input.files && input.files.length ? input.files[0] : null; + this.errorText = ''; + } + onDragOver(e: DragEvent): void { e.preventDefault(); } + onFileDrop(e: DragEvent): void { + e.preventDefault(); + const f = e.dataTransfer?.files?.[0]; + if (f) { this.file = f; this.errorText = ''; } + } + clearFile(): void { this.file = null; this.errorText = ''; } + + startUpload(): void { + if (!this.file) { this.errorText = `Please choose a ${this.fileKind} file.`; return; } + this.stage = 'uploading'; + this.progress = 10; + + const form = new FormData(); + form.append('file', this.file); + + this.http.post(this.data.url, form).subscribe({ + next: (payload: unknown) => { + // give legacy callers a chance to handle results themselves + let handledByCaller = false; + try { this.data.onSuccess?.(payload); handledByCaller = !!this.data.onSuccess; } catch { /* ignore */ } + + this.ref.close(); + + // if caller didn't handle, open our results + if (!handledByCaller) { + const p: any = payload as any; + if (p && (p.success || p.errors || p.ignored)) { + const modalData: CsvResultModalData = { + title: this.data.title.replace(/^Upload/i, 'Import Results'), + response: { + success: p.success ?? [], + errors: p.errors ?? [], + ignored: p.ignored ?? [], + }, + }; + this.dialog.open(CsvResultModalComponent, { + width: '960px', + data: modalData, + autoFocus: false, + restoreFocus: false, + }); + } + } + }, + error: (err: HttpErrorResponse) => { + this.stage = 'ready'; + this.errorText = err.message || 'Upload failed.'; + }, + complete: () => { this.progress = 100; }, + }); + } + + close(): void { this.ref.close(); } +} diff --git a/src/app/common/modals/csv-result-modal/csv-result-modal.coffee b/src/app/common/modals/csv-result-modal/csv-result-modal.coffee deleted file mode 100644 index 0536d68003..0000000000 --- a/src/app/common/modals/csv-result-modal/csv-result-modal.coffee +++ /dev/null @@ -1,105 +0,0 @@ -angular.module("doubtfire.common.modals.csv-result-modal", []) - -# -# Services for making new modals -# -.factory("CsvResultModal", ($modal, alertService) -> - CsvResultModal = {} - - # - # Show the results from a CSV upload with the provided title. - # - # Response should be a hash with the following data: - # - # { - # success: [ {row: ..., message:""}, ... ], - # errors: [ {row: ..., message:""}, ... ], - # ignored: [ {row: ..., message:""}, ... ] - # } - # - CsvResultModal.show = (title, response) -> - if response.errors.length == 0 - alertService.success( "Data uploaded. Success with #{response.success.length} items.", 2000) - else if response.success.length > 0 - alertService.message( "Data uploaded, success with #{response.success.length} items, but #{response.errors.length} errors.", 6000) - else - alertService.error( "Data uploaded but #{response.errors.length} errors", 6000) - - $modal.open - templateUrl: 'common/modals/csv-result-modal/csv-result-modal.tpl.html' - controller: 'CsvResultModalCtrl' - resolve: - title: -> title - response: -> response - - CsvResultModal -) - -# -# Controller for CSV result modal -# -.controller('CsvResultModalCtrl', ($scope, $modalInstance, title, response) -> - $scope.title = title - $scope.response = response - - # Pagination details - $scope.currentPage = 1 - $scope.maxSize = 5 - $scope.pageSize = 5 - - if response.errors.length > 0 - $scope.activeCsvResponseSelection = 'errors' - else if response.success.length > 0 - $scope.activeCsvResponseSelection = 'success' - else - $scope.activeCsvResponseSelection = 'ignored' - - $scope.itemData = (selector) -> - $scope.response[selector] - - $scope.close = -> - $modalInstance.dismiss() -) - - -.factory("CsvUploadModal", ($modal, alertService) -> - CsvUploadModal = {} - - # - # Shows a dialog to allow people to upload a CSV. - # - CsvUploadModal.show = (title, message, batchFiles, url, onSuccess) -> - $modal.open - templateUrl: 'common/modals/csv-result-modal/csv-upload-modal.tpl.html' - controller: 'CsvUploadModalCtrl' - resolve: - title: -> title - message: -> message - batchFiles: -> batchFiles - url: -> url - onSuccess: -> onSuccess - - CsvUploadModal -) - -# -# Controller for CSV result modal -# -.controller('CsvUploadModalCtrl', ($scope, $modalInstance, title, message, batchFiles, url, onSuccess) -> - - wrapSuccess = (response) -> - $modalInstance.dismiss() - onSuccess(response) - - $scope.title = title - $scope.message = message - $scope.batchFiles = batchFiles - $scope.url = url - $scope.onSuccess = wrapSuccess - - - - $scope.close = -> - $modalInstance.dismiss() -) - diff --git a/src/app/common/modals/csv-result-modal/csv-result-modal.component.html b/src/app/common/modals/csv-result-modal/csv-result-modal.component.html new file mode 100644 index 0000000000..340ba038f9 --- /dev/null +++ b/src/app/common/modals/csv-result-modal/csv-result-modal.component.html @@ -0,0 +1,134 @@ + diff --git a/src/app/common/modals/csv-result-modal/csv-result-modal.scss b/src/app/common/modals/csv-result-modal/csv-result-modal.scss deleted file mode 100644 index 9f56205b97..0000000000 --- a/src/app/common/modals/csv-result-modal/csv-result-modal.scss +++ /dev/null @@ -1,21 +0,0 @@ -.csv-result-modal { - - .modal-header small { - display: block; - } - - table { - table-layout: fixed; - } - - table th.message { width: 20%; } - table th.data { width: 80%; } - - td.csv-row { - display: block; - overflow-x: scroll; - font-family: monospace; - white-space: nowrap; - } - -} diff --git a/src/app/common/modals/csv-result-modal/csv-result-modal.tpl.html b/src/app/common/modals/csv-result-modal/csv-result-modal.tpl.html deleted file mode 100644 index 0e0b38f0d3..0000000000 --- a/src/app/common/modals/csv-result-modal/csv-result-modal.tpl.html +++ /dev/null @@ -1,51 +0,0 @@ -
- - - -
\ No newline at end of file diff --git a/src/app/common/modals/csv-result-modal/csv-upload-modal.component.html b/src/app/common/modals/csv-result-modal/csv-upload-modal.component.html new file mode 100644 index 0000000000..8152d392f8 --- /dev/null +++ b/src/app/common/modals/csv-result-modal/csv-upload-modal.component.html @@ -0,0 +1,77 @@ + diff --git a/src/app/common/modals/csv-result-modal/csv-upload-modal.tpl.html b/src/app/common/modals/csv-result-modal/csv-upload-modal.tpl.html deleted file mode 100644 index 6112f50c7e..0000000000 --- a/src/app/common/modals/csv-result-modal/csv-upload-modal.tpl.html +++ /dev/null @@ -1,15 +0,0 @@ -
- - - -
diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index c714ff0e5a..1c8d32813d 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -96,6 +96,11 @@ import {ExtensionCommentComponent} from './tasks/task-comments-viewer/extension- import {CampusListComponent} from './admin/institution-settings/campuses/campus-list/campus-list.component'; import {ExtensionModalComponent} from './common/modals/extension-modal/extension-modal.component'; import {CalendarModalComponent} from './common/modals/calendar-modal/calendar-modal.component'; +import { + CsvDialogsService, + CsvResultModalComponent, + CsvUploadModalComponent +} from './common/modals/csv-result-modal/csv-modals.component'; import {MatRadioModule} from '@angular/material/radio'; import {MatButtonToggleModule} from '@angular/material/button-toggle'; import {DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatOptionModule} from '@angular/material/core'; @@ -389,6 +394,8 @@ import { UnitStudentEnrolmentModalComponent } from './units/modals/unit-student- TaskScormCardComponent, ScormExtensionCommentComponent, ScormExtensionModalComponent, + CsvResultModalComponent, + CsvUploadModalComponent, ], // Services we provide providers: [ @@ -421,6 +428,7 @@ import { UnitStudentEnrolmentModalComponent } from './units/modals/unit-student- FileDownloaderService, CheckForUpdateService, TaskOutcomeAlignmentService, + CsvDialogsService, visualisationsProvider, commentsModalProvider, rootScopeProvider, diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index d9245086aa..ba74e74d15 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -7,6 +7,7 @@ import * as angular from 'angular'; import {downgradeInjectable, downgradeComponent} from '@angular/upgrade/static'; +import { CsvDialogsService } from './common/modals/csv-result-modal/csv-modals.component'; // Here are the old angular node modules, previously loaded via grunt //#region @@ -27,6 +28,36 @@ import 'angulartics/dist/angulartics.min.js'; import 'angulartics-google-analytics/lib/angulartics-google-analytics.js'; import 'angular-md5/angular-md5.js'; +// Legacy bridge: provides 'doubtfire.common.modals.csv-result-modal' for ng1. +// Keep this BEFORE any build/* imports that require it +const csvModalsNg1 = angular.module('doubtfire.common.modals.csv-result-modal', []); + +csvModalsNg1.factory('CsvDialogsService', downgradeInjectable(CsvDialogsService)); + +csvModalsNg1.factory('CsvResultModal', ['CsvDialogsService', (dlg: CsvDialogsService) => ({ + show: (title: string, response: any) => dlg.openResult({ title, response }), +})]); + +csvModalsNg1.factory('CsvUploadModal', ['CsvDialogsService', (dlg: CsvDialogsService) => ({ + // keep legacy signature (incl. batchFiles) for callers + show: ( + title: string, + message: string, + batchFiles: any, + url: string, + onSuccess?: (payload: unknown) => void + ) => + dlg.openUpload({ + title, + message, + url, + onSuccess: (payload: unknown) => { + if (typeof onSuccess === 'function') onSuccess(payload); + }, + }), +})]); + + // Ok... here is what we need to convert! import 'build/templates-app.js'; @@ -112,7 +143,6 @@ import 'build/src/app/common/filters/filters.js'; import 'build/src/app/common/content-editable/content-editable.js'; import 'build/src/app/common/modals/confirmation-modal/confirmation-modal.js'; import 'build/src/app/common/modals/comments-modal/comments-modal.js'; -import 'build/src/app/common/modals/csv-result-modal/csv-result-modal.js'; import 'build/src/app/common/modals/modals.js'; import 'build/src/app/common/file-uploader/file-uploader.js'; import 'build/src/app/common/common.js';