Consistent naming to Share Link Bundle

This commit is contained in:
shamoon
2025-11-05 17:24:23 -08:00
parent c5ec073a5b
commit 931e2321b3
24 changed files with 283 additions and 271 deletions

View File

@@ -1,7 +0,0 @@
describe('ShareBundleDialogComponent', () => {
it('is pending implementation', () => {
pending(
'ShareBundleDialogComponent tests will be implemented once the dialog logic is finalized.'
)
})
})

View File

@@ -1,7 +0,0 @@
describe('ShareBundleManageDialogComponent', () => {
it('is pending implementation', () => {
pending(
'ShareBundleManageDialogComponent tests will be implemented once the dialog logic is finalized.'
)
})
})

View File

@@ -50,9 +50,9 @@
} @else {
<div class="d-flex flex-column gap-3">
<div class="alert alert-success mb-0" role="status">
<h6 class="alert-heading mb-1" i18n>Share link requested</h6>
<h6 class="alert-heading mb-1" i18n>Share link bundle requested</h6>
<p class="mb-0 small" i18n>
You can copy the link below or open the management screen to monitor its progress. The link will start working once it is ready.
You can copy the share link below or open the manager to monitor progress. The link will start working once the bundle is ready.
</p>
</div>
<dl class="row mb-0 small">
@@ -105,11 +105,11 @@
<div class="modal-footer">
<div class="d-flex align-items-center gap-2 w-100">
<div class="text-light fst-italic small">
<ng-container i18n>A zip file containing the selected documents will be created. This process happens in the background and may take some time, especially for large bundles.</ng-container>
<ng-container i18n>A zip file containing the selected documents will be created for this share link bundle. This process happens in the background and may take some time, especially for large bundles.</ng-container>
</div>
<button type="button" class="btn btn-outline-secondary btn-sm ms-auto" (click)="cancel()">{{ cancelBtnCaption }}</button>
@if (createdBundle) {
<button type="button" class="btn btn-outline-secondary btn-sm" (click)="openManage()" i18n>Open manage links</button>
<button type="button" class="btn btn-outline-secondary btn-sm" (click)="openManage()" i18n>Manage share link bundles</button>
}
@if (!createdBundle) {

View File

@@ -0,0 +1,7 @@
describe('ShareLinkBundleDialogComponent', () => {
it('is pending implementation', () => {
pending(
'ShareLinkBundleDialogComponent tests will be implemented once the dialog logic is finalized.'
)
})
})

View File

@@ -4,17 +4,17 @@ import { Component, Input, inject } from '@angular/core'
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Document } from 'src/app/data/document'
import {
SHARE_BUNDLE_FILE_VERSION_LABELS,
SHARE_BUNDLE_STATUS_LABELS,
ShareBundleCreatePayload,
ShareBundleStatus,
ShareBundleSummary,
} from 'src/app/data/share-bundle'
import {
FileVersion,
SHARE_LINK_EXPIRATION_OPTIONS,
} from 'src/app/data/share-link'
import {
SHARE_LINK_BUNDLE_FILE_VERSION_LABELS,
SHARE_LINK_BUNDLE_STATUS_LABELS,
ShareLinkBundleCreatePayload,
ShareLinkBundleStatus,
ShareLinkBundleSummary,
} from 'src/app/data/share-link-bundle'
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
import { ToastService } from 'src/app/services/toast.service'
@@ -22,8 +22,8 @@ import { environment } from 'src/environments/environment'
import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.component'
@Component({
selector: 'pngx-share-bundle-dialog',
templateUrl: './share-bundle-dialog.component.html',
selector: 'pngx-share-link-bundle-dialog',
templateUrl: './share-link-bundle-dialog.component.html',
imports: [
CommonModule,
ReactiveFormsModule,
@@ -33,7 +33,7 @@ import { ConfirmDialogComponent } from '../confirm-dialog/confirm-dialog.compone
],
providers: [],
})
export class ShareBundleDialogComponent extends ConfirmDialogComponent {
export class ShareLinkBundleDialogComponent extends ConfirmDialogComponent {
private formBuilder = inject(FormBuilder)
private clipboard = inject(Clipboard)
private toastService = inject(ToastService)
@@ -46,20 +46,20 @@ export class ShareBundleDialogComponent extends ConfirmDialogComponent {
shareArchiveVersion: [true],
expirationDays: [7],
})
payload: ShareBundleCreatePayload | null = null
payload: ShareLinkBundleCreatePayload | null = null
readonly expirationOptions = SHARE_LINK_EXPIRATION_OPTIONS
createdBundle: ShareBundleSummary | null = null
createdBundle: ShareLinkBundleSummary | null = null
copied = false
onOpenManage?: () => void
readonly statuses = ShareBundleStatus
readonly statuses = ShareLinkBundleStatus
constructor() {
super()
this.loading = false
this.title = $localize`Share Selected Documents`
this.btnCaption = $localize`Create`
this.title = $localize`Create share link bundle`
this.btnCaption = $localize`Create link`
}
@Input()
@@ -82,14 +82,14 @@ export class ShareBundleDialogComponent extends ConfirmDialogComponent {
super.confirm()
}
getShareUrl(bundle: ShareBundleSummary): string {
getShareUrl(bundle: ShareLinkBundleSummary): string {
const apiURL = new URL(environment.apiBaseUrl)
return `${apiURL.origin}${apiURL.pathname.replace(/\/api\/$/, '/share/')}${
bundle.slug
}`
}
copy(bundle: ShareBundleSummary): void {
copy(bundle: ShareLinkBundleSummary): void {
const success = this.clipboard.copy(this.getShareUrl(bundle))
if (success) {
this.copied = true
@@ -108,11 +108,11 @@ export class ShareBundleDialogComponent extends ConfirmDialogComponent {
}
}
statusLabel(status: ShareBundleSummary['status']): string {
return SHARE_BUNDLE_STATUS_LABELS[status] ?? status
statusLabel(status: ShareLinkBundleSummary['status']): string {
return SHARE_LINK_BUNDLE_STATUS_LABELS[status] ?? status
}
fileVersionLabel(version: FileVersion): string {
return SHARE_BUNDLE_FILE_VERSION_LABELS[version] ?? version
return SHARE_LINK_BUNDLE_FILE_VERSION_LABELS[version] ?? version
}
}

View File

@@ -7,7 +7,7 @@
@if (loading) {
<div class="d-flex align-items-center gap-2">
<div class="spinner-border spinner-border-sm" role="status"></div>
<span i18n>Loading bulk share links…</span>
<span i18n>Loading share link bundles…</span>
</div>
}
@if (!loading && error) {
@@ -22,7 +22,7 @@
</p>
</div>
@if (bundles.length === 0) {
<p class="mb-0 text-muted fst-italic" i18n>No bulk share links currently exist.</p>
<p class="mb-0 text-muted fst-italic" i18n>No share link bundles currently exist.</p>
}
@if (bundles.length > 0) {
<div class="table-responsive">
@@ -92,7 +92,7 @@
@if (copiedSlug !== bundle.slug) {
<i-bs name="clipboard"></i-bs>
}
<span class="visually-hidden" i18n>Copy link</span>
<span class="visually-hidden" i18n>Copy share link</span>
</button>
@if (bundle.status === statuses.Failed) {
<button
@@ -112,7 +112,7 @@
(click)="delete(bundle)"
>
<i-bs name="trash"></i-bs>
<span class="visually-hidden" i18n>Delete link</span>
<span class="visually-hidden" i18n>Delete share link bundle</span>
</button>
</div>
</td>

View File

@@ -0,0 +1,7 @@
describe('ShareLinkBundleManageDialogComponent', () => {
it('is pending implementation', () => {
pending(
'ShareLinkBundleManageDialogComponent tests will be implemented once the dialog logic is finalized.'
)
})
})

View File

@@ -4,40 +4,40 @@ import { Component, OnDestroy, OnInit, inject } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject, catchError, of, switchMap, takeUntil, timer } from 'rxjs'
import {
SHARE_BUNDLE_FILE_VERSION_LABELS,
SHARE_BUNDLE_STATUS_LABELS,
ShareBundleStatus,
ShareBundleSummary,
} from 'src/app/data/share-bundle'
import { FileVersion } from 'src/app/data/share-link'
import {
SHARE_LINK_BUNDLE_FILE_VERSION_LABELS,
SHARE_LINK_BUNDLE_STATUS_LABELS,
ShareLinkBundleStatus,
ShareLinkBundleSummary,
} from 'src/app/data/share-link-bundle'
import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
import { ShareBundleService } from 'src/app/services/rest/share-bundle.service'
import { ShareLinkBundleService } from 'src/app/services/rest/share-link-bundle.service'
import { ToastService } from 'src/app/services/toast.service'
import { environment } from 'src/environments/environment'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
@Component({
selector: 'pngx-share-bundle-manage-dialog',
templateUrl: './share-bundle-manage-dialog.component.html',
selector: 'pngx-share-link-bundle-manage-dialog',
templateUrl: './share-link-bundle-manage-dialog.component.html',
imports: [CommonModule, NgxBootstrapIconsModule, FileSizePipe],
})
export class ShareBundleManageDialogComponent
export class ShareLinkBundleManageDialogComponent
extends LoadingComponentWithPermissions
implements OnInit, OnDestroy
{
private activeModal = inject(NgbActiveModal)
private shareBundleService = inject(ShareBundleService)
private shareLinkBundleService = inject(ShareLinkBundleService)
private toastService = inject(ToastService)
private clipboard = inject(Clipboard)
title = $localize`Bulk Share Links`
title = $localize`Share link bundles`
bundles: ShareBundleSummary[] = []
bundles: ShareLinkBundleSummary[] = []
error: string | null = null
copiedSlug: string | null = null
readonly statuses = ShareBundleStatus
readonly statuses = ShareLinkBundleStatus
readonly fileVersions = FileVersion
private readonly refresh$ = new Subject<boolean>()
@@ -50,14 +50,14 @@ export class ShareBundleManageDialogComponent
this.loading = true
}
this.error = null
return this.shareBundleService.listAllBundles().pipe(
return this.shareLinkBundleService.listAllBundles().pipe(
catchError((error) => {
if (!silent) {
this.loading = false
}
this.error = $localize`Failed to load bulk share links.`
this.error = $localize`Failed to load share link bundles.`
this.toastService.showError(
$localize`Error retrieving bulk share links.`,
$localize`Error retrieving share link bundles.`,
error
)
return of(null)
@@ -84,15 +84,15 @@ export class ShareBundleManageDialogComponent
super.ngOnDestroy()
}
getShareUrl(bundle: ShareBundleSummary): string {
getShareUrl(bundle: ShareLinkBundleSummary): string {
const apiURL = new URL(environment.apiBaseUrl)
return `${apiURL.origin}${apiURL.pathname.replace(/\/api\/$/, '/share/')}${
bundle.slug
}`
}
copy(bundle: ShareBundleSummary): void {
if (bundle.status !== ShareBundleStatus.Ready) {
copy(bundle: ShareLinkBundleSummary): void {
if (bundle.status !== ShareLinkBundleStatus.Ready) {
return
}
const success = this.clipboard.copy(this.getShareUrl(bundle))
@@ -105,30 +105,30 @@ export class ShareBundleManageDialogComponent
}
}
delete(bundle: ShareBundleSummary): void {
delete(bundle: ShareLinkBundleSummary): void {
this.error = null
this.loading = true
this.shareBundleService.delete(bundle).subscribe({
this.shareLinkBundleService.delete(bundle).subscribe({
next: () => {
this.toastService.showInfo($localize`Bulk share link deleted.`)
this.toastService.showInfo($localize`Share link bundle deleted.`)
this.triggerRefresh(false)
},
error: (e) => {
this.loading = false
this.toastService.showError(
$localize`Error deleting bulk share link.`,
$localize`Error deleting share link bundle.`,
e
)
},
})
}
retry(bundle: ShareBundleSummary): void {
retry(bundle: ShareLinkBundleSummary): void {
this.error = null
this.shareBundleService.rebuildBundle(bundle.id).subscribe({
this.shareLinkBundleService.rebuildBundle(bundle.id).subscribe({
next: (updated) => {
this.toastService.showInfo(
$localize`Bulk share link rebuild requested.`
$localize`Share link bundle rebuild requested.`
)
this.replaceBundle(updated)
},
@@ -138,19 +138,19 @@ export class ShareBundleManageDialogComponent
})
}
statusLabel(status: ShareBundleStatus): string {
return SHARE_BUNDLE_STATUS_LABELS[status] ?? status
statusLabel(status: ShareLinkBundleStatus): string {
return SHARE_LINK_BUNDLE_STATUS_LABELS[status] ?? status
}
fileVersionLabel(version: FileVersion): string {
return SHARE_BUNDLE_FILE_VERSION_LABELS[version] ?? version
return SHARE_LINK_BUNDLE_FILE_VERSION_LABELS[version] ?? version
}
close(): void {
this.activeModal.close()
}
private replaceBundle(updated: ShareBundleSummary): void {
private replaceBundle(updated: ShareLinkBundleSummary): void {
const index = this.bundles.findIndex((bundle) => bundle.id === updated.id)
if (index >= 0) {
this.bundles = [

View File

@@ -112,11 +112,11 @@
</div>
</button>
<div ngbDropdownMenu aria-labelledby="dropdownSend" class="shadow">
<button ngbDropdownItem (click)="shareSelected()">
<i-bs name="link"></i-bs>&nbsp;<ng-container i18n>Create a share link</ng-container>
<button ngbDropdownItem (click)="createShareLinkBundle()">
<i-bs name="link"></i-bs>&nbsp;<ng-container i18n>Create a share link bundle</ng-container>
</button>
<button ngbDropdownItem (click)="manageShareLinks()">
<i-bs name="list-ul"></i-bs>&nbsp;<ng-container i18n>Manage existing links</ng-container>
<button ngbDropdownItem (click)="manageShareLinkBundles()">
<i-bs name="list-ul"></i-bs>&nbsp;<ng-container i18n>Manage share link bundles</ng-container>
</button>
<div class="dropdown-divider"></div>
@if (emailEnabled) {

View File

@@ -33,7 +33,7 @@ import {
SelectionDataItem,
} from 'src/app/services/rest/document.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { ShareBundleService } from 'src/app/services/rest/share-bundle.service'
import { ShareLinkBundleService } from 'src/app/services/rest/share-link-bundle.service'
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { TagService } from 'src/app/services/rest/tag.service'
import { SettingsService } from 'src/app/services/settings.service'
@@ -55,8 +55,8 @@ import {
} from '../../common/filterable-dropdown/filterable-dropdown.component'
import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
import { ShareBundleDialogComponent } from '../../common/share-bundle-dialog/share-bundle-dialog.component'
import { ShareBundleManageDialogComponent } from '../../common/share-bundle-manage-dialog/share-bundle-manage-dialog.component'
import { ShareLinkBundleDialogComponent } from '../../common/share-link-bundle-dialog/share-link-bundle-dialog.component'
import { ShareLinkBundleManageDialogComponent } from '../../common/share-link-bundle-manage-dialog/share-link-bundle-manage-dialog.component'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
import { CustomFieldsBulkEditDialogComponent } from './custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component'
@@ -90,7 +90,7 @@ export class BulkEditorComponent
private customFieldService = inject(CustomFieldsService)
private permissionService = inject(PermissionsService)
private savedViewService = inject(SavedViewService)
private shareBundleService = inject(ShareBundleService)
private shareLinkBundleService = inject(ShareLinkBundleService)
tagSelectionModel = new FilterableDropdownSelectionModel(true)
correspondentSelectionModel = new FilterableDropdownSelectionModel()
@@ -912,12 +912,12 @@ export class BulkEditorComponent
return this.settings.get(SETTINGS_KEYS.EMAIL_ENABLED)
}
shareSelected() {
const modal = this.modalService.open(ShareBundleDialogComponent, {
createShareLinkBundle() {
const modal = this.modalService.open(ShareLinkBundleDialogComponent, {
backdrop: 'static',
size: 'lg',
})
const dialog = modal.componentInstance as ShareBundleDialogComponent
const dialog = modal.componentInstance as ShareLinkBundleDialogComponent
const selectedDocuments = this.list.documents.filter((d) =>
this.list.selected.has(d.id)
)
@@ -931,7 +931,7 @@ export class BulkEditorComponent
}
dialog.loading = true
dialog.buttonsEnabled = false
this.shareBundleService
this.shareLinkBundleService
.createBundle(payload)
.pipe(first())
.subscribe({
@@ -943,17 +943,17 @@ export class BulkEditorComponent
dialog.payload = null
dialog.onOpenManage = () => {
modal.close()
this.manageShareLinks()
this.manageShareLinkBundles()
}
this.toastService.showInfo(
$localize`Bulk share link creation requested.`
$localize`Share link bundle creation requested.`
)
},
error: (error) => {
dialog.loading = false
dialog.buttonsEnabled = true
this.toastService.showError(
$localize`Bulk share link creation is not available yet.`,
$localize`Share link bundle creation is not available yet.`,
error
)
},
@@ -961,8 +961,8 @@ export class BulkEditorComponent
})
}
manageShareLinks() {
const modal = this.modalService.open(ShareBundleManageDialogComponent, {
manageShareLinkBundles() {
const modal = this.modalService.open(ShareLinkBundleManageDialogComponent, {
backdrop: 'static',
size: 'lg',
})

View File

@@ -1,40 +0,0 @@
import { FileVersion } from './share-link'
export enum ShareBundleStatus {
Pending = 'pending',
Processing = 'processing',
Ready = 'ready',
Failed = 'failed',
}
export interface ShareBundleSummary {
id: number
slug: string
created: string // Date
expiration?: string // Date
documents: number[]
document_count: number
file_version: FileVersion
status: ShareBundleStatus
built_at?: string
size_bytes?: number
last_error?: string
}
export interface ShareBundleCreatePayload {
document_ids: number[]
file_version: FileVersion
expiration_days: number | null
}
export const SHARE_BUNDLE_STATUS_LABELS: Record<ShareBundleStatus, string> = {
[ShareBundleStatus.Pending]: $localize`Pending`,
[ShareBundleStatus.Processing]: $localize`Processing`,
[ShareBundleStatus.Ready]: $localize`Ready`,
[ShareBundleStatus.Failed]: $localize`Failed`,
}
export const SHARE_BUNDLE_FILE_VERSION_LABELS: Record<FileVersion, string> = {
[FileVersion.Archive]: $localize`Archive`,
[FileVersion.Original]: $localize`Original`,
}

View File

@@ -0,0 +1,46 @@
import { FileVersion } from './share-link'
export enum ShareLinkBundleStatus {
Pending = 'pending',
Processing = 'processing',
Ready = 'ready',
Failed = 'failed',
}
export interface ShareLinkBundleSummary {
id: number
slug: string
created: string // Date
expiration?: string // Date
documents: number[]
document_count: number
file_version: FileVersion
status: ShareLinkBundleStatus
built_at?: string
size_bytes?: number
last_error?: string
}
export interface ShareLinkBundleCreatePayload {
document_ids: number[]
file_version: FileVersion
expiration_days: number | null
}
export const SHARE_LINK_BUNDLE_STATUS_LABELS: Record<
ShareLinkBundleStatus,
string
> = {
[ShareLinkBundleStatus.Pending]: $localize`Pending`,
[ShareLinkBundleStatus.Processing]: $localize`Processing`,
[ShareLinkBundleStatus.Ready]: $localize`Ready`,
[ShareLinkBundleStatus.Failed]: $localize`Failed`,
}
export const SHARE_LINK_BUNDLE_FILE_VERSION_LABELS: Record<
FileVersion,
string
> = {
[FileVersion.Archive]: $localize`Archive`,
[FileVersion.Original]: $localize`Original`,
}

View File

@@ -1,38 +0,0 @@
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import {
ShareBundleCreatePayload,
ShareBundleSummary,
} from 'src/app/data/share-bundle'
import { AbstractNameFilterService } from './abstract-name-filter-service'
@Injectable({
providedIn: 'root',
})
export class ShareBundleService extends AbstractNameFilterService<ShareBundleSummary> {
constructor() {
super()
this.resourceName = 'share_bundles'
}
createBundle(
payload: ShareBundleCreatePayload
): Observable<ShareBundleSummary> {
this.clearCache()
return this.http.post<ShareBundleSummary>(this.getResourceUrl(), payload)
}
rebuildBundle(bundleId: number): Observable<ShareBundleSummary> {
this.clearCache()
return this.http.post<ShareBundleSummary>(
this.getResourceUrl(bundleId, 'rebuild'),
{}
)
}
listAllBundles(): Observable<ShareBundleSummary[]> {
return this.list(1, 1000, 'created', true).pipe(
map((response) => response.results)
)
}
}

View File

@@ -0,0 +1,41 @@
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import {
ShareLinkBundleCreatePayload,
ShareLinkBundleSummary,
} from 'src/app/data/share-link-bundle'
import { AbstractNameFilterService } from './abstract-name-filter-service'
@Injectable({
providedIn: 'root',
})
export class ShareLinkBundleService extends AbstractNameFilterService<ShareLinkBundleSummary> {
constructor() {
super()
this.resourceName = 'share_link_bundles'
}
createBundle(
payload: ShareLinkBundleCreatePayload
): Observable<ShareLinkBundleSummary> {
this.clearCache()
return this.http.post<ShareLinkBundleSummary>(
this.getResourceUrl(),
payload
)
}
rebuildBundle(bundleId: number): Observable<ShareLinkBundleSummary> {
this.clearCache()
return this.http.post<ShareLinkBundleSummary>(
this.getResourceUrl(bundleId, 'rebuild'),
{}
)
}
listAllBundles(): Observable<ShareLinkBundleSummary[]> {
return this.list(1, 1000, 'created', true).pipe(
map((response) => response.results)
)
}
}