Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
063b740
feat(publishing-queue) #36040: Angular portlet shell, Queue tab, Asse…
hmoreras Jun 8, 2026
b01ed43
fix(publishing-queue) #36045: enforce publishing-queue portlet gate o…
hmoreras Jun 8, 2026
8c05085
feat(publishing-queue) #36040: History tab, modals (Details, Configur…
hmoreras Jun 8, 2026
4037903
feat(publishing-queue) #36040: UX iteration — empty state, status chi…
hmoreras Jun 13, 2026
dec6884
merge master
hmoreras Jun 15, 2026
1fba21e
refactor(publishing-queue) #36040: reuse project-wide push publish + …
hmoreras Jun 16, 2026
3d708df
refactor(publishing-queue) #36040: History table layout (dot-tags pat…
hmoreras Jun 16, 2026
a33928b
fix(publishing-queue) #36040: align history status labels with legacy…
hmoreras Jun 17, 2026
5f86b2d
feat(publishing-queue) #36040: add Bundle Name column to history table
hmoreras Jun 17, 2026
5e36a82
feat(publishing-queue) #36040: Select Bundles to Delete dialog (4 sco…
hmoreras Jun 17, 2026
9658986
refactor(publishing-queue) #36040: gate Delete Bundles button on sele…
hmoreras Jun 17, 2026
b11b749
refactor(publishing-queue) #36040: bundle details — single endpoints …
hmoreras Jun 18, 2026
8f983b5
feat(publishing-queue) #36040: upload bundle dialog — canonical drag-…
hmoreras Jun 18, 2026
549b63b
feat(publishing-queue) #36040: history row kebab + layout polish + de…
hmoreras Jun 18, 2026
a2efc00
style(publishing-queue) #36040: history table — drop per-cell font si…
hmoreras Jun 18, 2026
874232b
style(publishing-queue) #36040: smaller font for Bundle Id + dates; m…
hmoreras Jun 18, 2026
d796fdf
refactor(publishing-queue) #36040: single unified table + status chip…
hmoreras Jun 19, 2026
6ec96bb
refactor(publishing-queue) #36040: omit status param when filter is e…
hmoreras Jun 22, 2026
36b8937
feat(publishing-queue) #36040: Add Bundle dropdown + Select Bundle di…
hmoreras Jun 22, 2026
eb7bcb9
style(publishing-queue) #36040: shorter status chip labels + tighter …
hmoreras Jun 23, 2026
502ed82
fix(publishing-queue) #36040: Select Bundle asset table — name + type…
hmoreras Jun 23, 2026
12c16ab
Merge branch 'main' into issue-36040-publishing-queue-angular-portlet
hmoreras Jun 24, 2026
2c4774f
feat(publishing-queue): items column, custom dialog header, row conte…
hmoreras Jun 24, 2026
556366a
feat(publishing-queue): gate bundle + manifest download buttons on HE…
hmoreras Jun 24, 2026
c271d15
feat(publishing-queue): red bundle ids on failed rows, dotRelativeDat…
hmoreras Jun 24, 2026
041da18
Merge branch 'main' into issue-36040-publishing-queue-angular-portlet
hmoreras Jun 24, 2026
b379f31
merge manin
hmoreras Jun 25, 2026
fd17f4d
chore(core-web): regenerate pnpm-lock.yaml for jest-util
hmoreras Jun 25, 2026
d1ee134
feat(publishing-queue): surface the synthetic SCHEDULED status
hmoreras Jun 25, 2026
d8ff4db
feat(publishing-queue): inline Configure step in Select Bundle, push …
hmoreras Jun 25, 2026
c05873f
fix(publishing-queue): persist rows-per-page and match content-drive …
hmoreras Jun 25, 2026
ad5ddcc
fix(publishing-queue): silent polling — stop the every-15s skeleton f…
hmoreras Jun 26, 2026
464b911
fix(publishing-queue): status filter — include SCHEDULED, dedupe by l…
hmoreras Jun 26, 2026
7bdb68c
feat(publishing-queue): inline Download menu in Select Bundle dialog
hmoreras Jun 26, 2026
6e9a58b
feat(publishing-queue): make Select Bundle asset rows clickable, drop…
hmoreras Jun 26, 2026
bb080e8
fix(publishing-queue): drop sort UI from the bundles table — BE doesn…
hmoreras Jun 26, 2026
52261e6
feat(publishing-queue): inline warning instead of disabled buttons
hmoreras Jun 26, 2026
8b18c84
feat(publishing-queue): surface scheduledPublishDate in bundle details
hmoreras Jun 30, 2026
79b99bb
feat(publishing-queue): swap Add Bundle ↔ Retry Send on selection
hmoreras Jun 30, 2026
b4a6b70
feat(publishing-queue): replace 4-scope Remove dialog with a simple c…
hmoreras Jul 1, 2026
556c955
feat(publishing-queue): asset-list dialog polish + allow remove on SC…
hmoreras Jul 1, 2026
b3934bd
fix(publishing-queue): breathe some room around the Select Bundle tra…
hmoreras Jul 1, 2026
920f0a1
feat(publishing-queue): primary Close + unified date format on detail…
hmoreras Jul 2, 2026
700a9f7
feat(publishing-queue): table typography, date format, frozen edge co…
hmoreras Jul 2, 2026
ab81fdf
fix(publishing-queue): hide the Select Bundle trash icon until row hover
hmoreras Jul 2, 2026
3119267
feat(publishing-queue): Select Bundle dialog polish (pagination, head…
hmoreras Jul 2, 2026
d9b4e1b
feat(publishing-queue): Upload dialog — always-clickable Upload with …
hmoreras Jul 2, 2026
d6cd9f2
Merge branch 'main' into issue-36040-publishing-queue-angular-portlet
hmoreras Jul 3, 2026
22142bc
fix(publishing-queue): address code-review feedback
hmoreras Jul 3, 2026
f251c09
fix(publishing-queue): URL-encode bundleId in service path interpolat…
hmoreras Jul 3, 2026
a9acf78
fix(publishing-queue): show loading skeleton on first Select Bundle a…
hmoreras Jul 3, 2026
ba45ed1
feat(publishing-queue): Select Bundle dialog uses dot-empty-container…
hmoreras Jul 3, 2026
ca33811
feat(publishing-queue): toast notifications for retry outcomes
hmoreras Jul 3, 2026
c43b53d
feat(publishing-queue): rename 'Sent' status label to 'Success'
hmoreras Jul 3, 2026
5f11fad
chore(publishing-queue): apply nx format:check across the portlet
hmoreras Jul 3, 2026
d227b26
Merge branch 'main' into issue-36040-publishing-queue-angular-portlet
hmoreras Jul 3, 2026
1a0ae6f
test(publishing-queue): update portlet count after adding publishing-…
hmoreras Jul 3, 2026
d2d4e60
Merge branch 'main' into issue-36040-publishing-queue-angular-portlet
hmoreras Jul 3, 2026
460820c
fix(publishing-queue): render retry toasts via DotMessageDisplayService
hmoreras Jul 3, 2026
5d859e7
Merge branch 'issue-36040-publishing-queue-angular-portlet' of https:…
hmoreras Jul 3, 2026
ceb573c
chore(publishing-queue): drop unused jest-util devDependency
hmoreras Jul 3, 2026
9bbd649
fix(publishing-queue): address maintainer review feedback
hmoreras Jul 6, 2026
9737eb3
refactor(publishing-queue): use `#` ECMAScript private for injected f…
hmoreras Jul 6, 2026
6e04fb6
refactor(publishing-queue): apply \$ signal prefix per ANGULAR_STANDARDS
hmoreras Jul 6, 2026
475fe70
style(publishing-queue): apply prettier formatting to service + spec
hmoreras Jul 6, 2026
03fa3f3
Merge branch 'main' into issue-36040-publishing-queue-angular-portlet
hmoreras Jul 6, 2026
95b4f4f
fix(publishing-queue): narrow push-failure union so TS accepts .error…
hmoreras Jul 6, 2026
b97b6dd
Merge branch 'main' into issue-36040-publishing-queue-angular-portlet
wezell Jul 7, 2026
b7c10dd
test(publishing-queue): Spectator migration + error-path + coverage gaps
hmoreras Jul 7, 2026
95128df
refactor(publishing-queue): finish select-bundle-dialog review cleanup
hmoreras Jul 7, 2026
6ff014d
test(publishing-queue): put toolbar data-testid on the inner button (…
hmoreras Jul 7, 2026
5a5072a
refactor(publishing-queue): drop failed-row red bundle id + trim colu…
hmoreras Jul 8, 2026
4dc6e27
Merge branch 'main' into issue-36040-publishing-queue-angular-portlet
hmoreras Jul 8, 2026
79fe093
Merge branch 'issue-36040-publishing-queue-angular-portlet' of https:…
hmoreras Jul 8, 2026
a62cf19
refactor(publishing-queue): replace Download tiered menu with a step
hmoreras Jul 8, 2026
1f5925f
feat(publishing-queue): make items count clickable to open contents d…
hmoreras Jul 8, 2026
ad61b20
Merge branch 'main' into issue-36040-publishing-queue-angular-portlet
hmoreras Jul 8, 2026
2e0f7c5
revert(publishing-queue): drop v1 PublishingResource portlet gate
hmoreras Jul 9, 2026
4a90cbc
Merge branch 'main' into issue-36040-publishing-queue-angular-portlet
hmoreras Jul 9, 2026
a945ff1
refactor(publishing-queue): register Angular portlet as Beta, keep Do…
hmoreras Jul 9, 2026
eddf7fd
Merge of #36413
mergify[bot] Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions core-web/apps/dotcms-ui/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,16 @@ const PORTLETS_ANGULAR: Route[] = [
data: { reuseRoute: false },
loadChildren: () => import('@dotcms/portlets/dot-tags/portlet').then((m) => m.dotTagsRoutes)
},
{
path: 'publishing-queue-beta',
canActivate: [MenuGuardService],
canActivateChild: [MenuGuardService],
data: { reuseRoute: false },
loadChildren: () =>
import('@dotcms/portlets/dot-publishing-queue/portlet').then(
(m) => m.dotPublishingQueueRoutes
)
},
{
path: 'query-tool',
canActivate: [MenuGuardService],
Expand Down
1 change: 1 addition & 0 deletions core-web/libs/data-access/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export * from './lib/dot-page-workflows-actions/dot-page-workflows-actions.servi
export * from './lib/dot-personalize/dot-personalize.service';
export * from './lib/dot-personas/dot-personas.service';
export * from './lib/dot-properties/dot-properties.service';
export * from './lib/dot-publishing-queue/dot-publishing-queue.service';
export * from './lib/dot-push-publish-filters/dot-push-publish-filters.service';
export * from './lib/dot-resource-links/dot-resource-links.service';
export * from './lib/dot-roles/dot-roles.service';
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,368 @@
import { Observable, of } from 'rxjs';

import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';

import { catchError, map } from 'rxjs/operators';

import {
BundleAssetView,
DotCMSResponse,
PublishAuditStatus,
PublishingJobDetailView,
PublishingJobsResponse,
PushBundleForm,
PushBundleResultView,
RemoveAssetResultView,
RetryBundleResultView,
UnsentBundlesResponse
} from '@dotcms/dotcms-models';

export type PublishingSortField = 'bundle_name' | 'status' | 'created' | 'modified';
export type PublishingSortDirection = 'asc' | 'desc';

export interface ListPublishingJobsParams {
/** Empty/omitted = all statuses (BE returns every row in publish_audit). */
statuses?: readonly PublishAuditStatus[];
page?: number;
perPage?: number;
filter?: string;
sort?: PublishingSortField;
sortDirection?: PublishingSortDirection;
}

export type RetryDeliveryStrategy = 'ALL_ENDPOINTS' | 'FAILED_ENDPOINTS';

export interface RetryBundlesPayload {
bundleIds: string[];
forcePush?: boolean;
deliveryStrategy?: RetryDeliveryStrategy;
}

/**
* Backs the Publishing Queue Angular portlet.
*
* v1 endpoints (`com.dotcms.rest.api.v1.publishing.PublishingResource`):
* - `GET /api/v1/publishing` — list bundles by status (sort/filter via params)
* - `GET /api/v1/publishing/{bundleId}` — full bundle detail with endpoints
* - `POST /api/v1/publishing/retry` — retry bundles (bulk)
* - `DELETE /api/v1/publishing/{bundleId}` — delete single bundle (synchronous)
* - `DELETE /api/v1/publishing/purge?status=...` — bulk delete by status (async,
* WebSocket-notified; omit `status` to use safe defaults = ALL terminal/queued)
*
* Legacy endpoints (`com.dotcms.rest.BundleResource`) still used until #36048 lands:
* - `DELETE /api/bundle/ids` — bulk delete by bundle id (async, WebSocket-notified;
* same endpoint the legacy JSP uses for "SELECTED" in the delete-bundles dialog)
* - `GET /api/bundle/{bundleId}/assets` — asset list inside a bundle
* - `POST /api/bundle/sync` — synchronous .tar.gz upload (licensed)
* - `GET /api/bundle/_download/{bundleId}` — bundle download (URL only)
*
* Two push paths exist:
* - Row-level "Configure & Send" delegates to the project-wide push publish
* dialog (`DotPushPublishDialogService.open(...)` from `@dotcms/dotcms-js`),
* which hits the legacy `/DotAjaxDirector/.../cmd/pushBundle` endpoint via
* `PushPublishService`.
* - The Select Bundle dialog calls `pushBundle` (see below), which posts to
* `POST /api/v1/publishing/push/{bundleId}` — the modern v1 REST endpoint.
*
* Bundle generate-and-download is delegated to the project-wide download dialog
* (`DotDownloadBundleDialogService.open(bundleId)` from
* `@services/dot-download-bundle-dialog/...`), which posts to `/api/bundle/_generate`
* with the user-picked filter + operation and triggers a browser download.
* No bespoke generate endpoint lives here either.
*/
@Injectable({
providedIn: 'root'
})
export class DotPublishingQueueService {
readonly #http = inject(HttpClient);

listPublishingJobs(params: ListPublishingJobsParams): Observable<PublishingJobsResponse> {
let httpParams = new HttpParams();

// Only send status when the caller selected one or more — omitting the
// param tells the BE "all statuses" and makes the FE forward-compatible
// with new server-side statuses (e.g. SCHEDULED, see #36267).
if (params.statuses && params.statuses.length > 0) {
httpParams = httpParams.set('status', params.statuses.join(','));
}

if (params.page !== undefined) {
httpParams = httpParams.set('page', params.page);
}

if (params.perPage !== undefined) {
httpParams = httpParams.set('per_page', params.perPage);
}

if (params.filter && params.filter.length > 0) {
httpParams = httpParams.set('filter', params.filter);
}

if (params.sort) {
const order = params.sortDirection === 'desc' ? '-' : '';
httpParams = httpParams.set('sort', `${order}${params.sort}`);
}

return this.#http.get<PublishingJobsResponse>('/api/v1/publishing', {
params: httpParams
});
}

getPublishingJobDetails(bundleId: string): Observable<PublishingJobDetailView> {
return this.#http
.get<
DotCMSResponse<PublishingJobDetailView>
>(`/api/v1/publishing/${encodeURIComponent(bundleId)}`)
.pipe(map((response) => response.entity));
}

retryBundles(payload: RetryBundlesPayload): Observable<RetryBundleResultView[]> {
return this.#http
.post<DotCMSResponse<RetryBundleResultView[]>>('/api/v1/publishing/retry', payload)
.pipe(map((response) => response.entity));
}

/**
* Queues a bundle for publishing via the modern v1 REST endpoint.
* Replaces the legacy `/DotAjaxDirector/.../cmd/pushBundle` AJAX action — same
* effect, proper JSON in/out + proper HTTP status codes.
*
* BE endpoint: `POST /api/v1/publishing/push/{bundleId}` (see
* `com.dotcms.rest.api.v1.publishing.PublishingResource.pushBundle`).
* Single-bundle per call; callers needing to push N bundles must fan out
* (e.g. via `forkJoin`).
*/
pushBundle(bundleId: string, form: PushBundleForm): Observable<PushBundleResultView> {
return this.#http
.post<
DotCMSResponse<PushBundleResultView>
>(`/api/v1/publishing/push/${encodeURIComponent(bundleId)}`, form)
.pipe(map((response) => response.entity));
}

deleteBundle(bundleId: string): Observable<{ message: string }> {
return this.#http.delete<{
message: string;
}>(`/api/v1/publishing/${encodeURIComponent(bundleId)}`);
}

/**
* Bulk delete bundles by id — fire-and-forget. The BE acks immediately with a
* `ResponseEntityView<string>` message; the actual deletion runs on a background
* thread and notifies the user via WebSocket system message when finished.
*
* Uses the legacy `DELETE /api/bundle/ids` (same endpoint as the JSP) until the
* v1 consolidation work (#36048) ships an equivalent under `/api/v1/publishing`.
*/
deleteBundles(bundleIds: string[]): Observable<unknown> {
return this.#http.request<unknown>('DELETE', '/api/bundle/ids', {
body: { identifiers: bundleIds }
});
}

/**
* Bulk-purges bundles by status — fire-and-forget. Mirrors the legacy
* `/api/bundle/all{,/success,/fail}` endpoints behind a single v1 path. Omit
* `statuses` to use the BE's safe defaults (all terminal + queued; in-progress
* statuses are rejected with 400). BE acks immediately; result is delivered via
* WebSocket system message.
*/
purgeBundles(statuses?: readonly PublishAuditStatus[]): Observable<unknown> {
const params =
statuses && statuses.length > 0
? new HttpParams().set('status', statuses.join(','))
: new HttpParams();
return this.#http.delete<unknown>('/api/v1/publishing/purge', { params });
}

uploadBundle(file: File): Observable<{ bundleName: string; status: string }> {
const formData = new FormData();
formData.append('file', file, file.name);
return this.#http.post<{ bundleName: string; status: string }>(
'/api/bundle/sync',
formData
);
}

/** Builds the absolute download URL for a bundle's `.tar.gz`. */
getBundleDownloadUrl(bundleId: string): string {
return `/api/bundle/_download/${encodeURIComponent(bundleId)}`;
}

/**
* Mirrors the legacy `DotDownloadBundleDialogComponent` submit: POSTs to
* `/api/bundle/_generate` and returns the resulting `.tar.gz` blob plus the
* filename parsed from the `content-disposition` header.
*
* Used by the inline Download menu in the Select Bundle dialog — picking a
* filter from the menu must produce the exact same file a customer gets
* from the legacy modal, so the wire payload here is byte-identical:
* `{ bundleId, operation: '0' | '1', filterKey }`
* where `'0'` = publish, `'1'` = unpublish (BE vocabulary, not the v1 REST
* `'publish'` / `'expire'` strings — the `_generate` endpoint predates
* v1).
*
* Uses `HttpClient.post` with `{ observe: 'response', responseType: 'blob' }`
* so the project's auth + error interceptors apply and the headers are
* available for the filename parse — the legacy dialog used raw `fetch`
* only because of how it bootstrapped pre-v1.
*/
generateBundle(
bundleId: string,
operation: '0' | '1',
filterKey: string
): Observable<{ blob: Blob; filename: string }> {
return this.#http
.post(
'/api/bundle/_generate',
{ bundleId, operation, filterKey },
{
observe: 'response',
responseType: 'blob'
}
)
.pipe(
map((response) => {
if (!response.body) {
throw new Error('Empty bundle-generate response body');
}
return {
blob: response.body,
filename: parseFilenameFromContentDisposition(
response.headers.get('content-disposition')
)
};
})
);
}

/** Builds the absolute download URL for a bundle's manifest CSV. */
getBundleManifestUrl(bundleId: string): string {
return `/api/bundle/${encodeURIComponent(bundleId)}/manifest`;
}

/**
* Returns `true` when the bundle's `.tar.gz` is downloadable right now.
*
* Why this probe exists:
* The legacy JSP gates the "Download Bundle" button on two server-side
* conditions:
* 1. the `.tar.gz` file existing on disk (it may have been purged via
* `DELETE /api/bundle/olderthan/{N}` — there is no scheduled cleanup
* built into the core, so this happens only on manual admin action);
* 2. the targeted environments not being static-publish / S3 (those
* protocols don't produce a downloadable archive).
*
* Neither piece of state is exposed on the current detail response
* (`GET /api/v1/publishing/{bundleId}`). Until the BE adds a `hasBundle`
* (or `endpointProtocols`) flag we can read directly, the FE has to ask
* the actual download endpoint. HEAD is the lightweight option — JAX-RS
* auto-handles HEAD by invoking the `@GET` handler and discarding the
* body, so we get the file-existence answer without paying for the
* payload. A 404 (file purged) becomes `false` via `catchError`.
*
* @see `RemotePublishAjaxAction.downloadBundle` and `BundleResource._download`
* for the server-side behavior.
*/
probeBundleDownload(bundleId: string): Observable<boolean> {
return this.#http.head(this.getBundleDownloadUrl(bundleId), { observe: 'response' }).pipe(
map((response) => response.status === 200),
catchError(() => of(false))
);
}

/**
* Returns `true` when the bundle's manifest CSV is downloadable right now.
*
* Same rationale as {@link probeBundleDownload}: the legacy JSP calls
* `ManifestUtil.manifestExists(bundleId)` server-side, which (a) checks
* that the `.tar.gz` is on disk and (b) scans the archive for a manifest
* entry — bundles built before the manifest feature was introduced have
* no entry. Neither check is exposed on `GET /api/v1/publishing/{bundleId}`,
* so the FE probes the actual download endpoint with HEAD.
*
* @see `BundleResource.downloadManifest` and `ManifestUtil.manifestExists`
* for the server-side behavior.
*/
probeBundleManifest(bundleId: string): Observable<boolean> {
return this.#http.head(this.getBundleManifestUrl(bundleId), { observe: 'response' }).pipe(
map((response) => response.status === 200),
catchError(() => of(false))
);
}

/**
* Lists unsent (draft) bundles owned by the given user.
*
* Backed by the legacy endpoint `GET /api/bundle/getunsendbundles/userid/{userId}`
* (`BundleResource#getUnsendBundles`). The newer v1 `/api/v1/publishing` reads
* from `publish_audit` and does NOT include drafts — drafts live in
* `publishing_bundle` only. This is the only endpoint that surfaces them
* until #36048 (legacy → v1 consolidation) lands.
*
* Response shape: `{ identifier, label, items: [{ id, name }, ...], numRows }`.
* Caller is responsible for mapping `items` to whatever row shape the UI needs.
*/
getUnsendBundles(
userId: string,
filter = '*',
start = 0,
count = 50
): Observable<UnsentBundlesResponse> {
const query = new HttpParams()
.set('name', filter || '*')
.set('start', start)
.set('count', count);

return this.#http.get<UnsentBundlesResponse>(
`/api/bundle/getunsendbundles/userid/${encodeURIComponent(userId)}`,
{ params: query }
);
}

getBundleAssets(bundleId: string): Observable<BundleAssetView[]> {
const params = new HttpParams().set('limit', -1);

return this.#http.get<BundleAssetView[]>(
`/api/bundle/${encodeURIComponent(bundleId)}/assets`,
{ params }
);
}

/**
* Removes one or more assets from an unsent bundle.
* Backed by `BundleManagementResource#removeAssetsFromBundle`
* (`DELETE /api/v1/bundles/{bundleId}/assets`).
*
* Backend returns 409 if the bundle is already in progress.
* Per-asset results live inside `entity[]` — some may fail while others succeed.
*/
removeAssetsFromBundle(
bundleId: string,
assetIds: string[]
): Observable<RemoveAssetResultView[]> {
return this.#http
.request<
DotCMSResponse<RemoveAssetResultView[]>
>('DELETE', `/api/v1/bundles/${encodeURIComponent(bundleId)}/assets`, { body: { assetIds } })
.pipe(map((response) => response.entity));
}
}

/** Pulls `filename` out of a `content-disposition` header. Mirrors the parse
* the legacy download dialog does (`contentDisposition.slice(idx + 'filename='.length)`)
* so generated filenames match exactly. Returns an empty string when the
* header is missing or malformed — `getDownloadLink` will still produce a
* usable anchor in that case. */
function parseFilenameFromContentDisposition(header: string | null): string {
if (!header) {
return '';
}
const key = 'filename=';
const idx = header.indexOf(key);
if (idx < 0) {
return '';
}
return header.slice(idx + key.length).replace(/^"|"$/g, '');
}
Loading
Loading