]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Basic wiring of existing bundles
authorshamoon <4887959+shamoon@users.noreply.github.com>
Wed, 5 Nov 2025 02:57:33 +0000 (18:57 -0800)
committershamoon <4887959+shamoon@users.noreply.github.com>
Wed, 5 Nov 2025 02:57:33 +0000 (18:57 -0800)
src-ui/src/app/components/common/share-bundle-manage-dialog/share-bundle-manage-dialog.component.html [new file with mode: 0644]
src-ui/src/app/components/common/share-bundle-manage-dialog/share-bundle-manage-dialog.component.spec.ts [new file with mode: 0644]
src-ui/src/app/components/common/share-bundle-manage-dialog/share-bundle-manage-dialog.component.ts [new file with mode: 0644]
src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts
src-ui/src/app/services/rest/share-bundle.service.ts

diff --git a/src-ui/src/app/components/common/share-bundle-manage-dialog/share-bundle-manage-dialog.component.html b/src-ui/src/app/components/common/share-bundle-manage-dialog/share-bundle-manage-dialog.component.html
new file mode 100644 (file)
index 0000000..297d4d0
--- /dev/null
@@ -0,0 +1,86 @@
+<div class="modal-header">
+  <h4 class="modal-title">{{ title }}</h4>
+  <button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
+</div>
+
+<div class="modal-body">
+  @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>
+    </div>
+  }
+  @if (!loading && error) {
+    <div class="alert alert-danger mb-0" role="alert">
+      {{ error }}
+    </div>
+  }
+  @if (!loading && !error) {
+    @if (bundles.length === 0) {
+      <p class="mb-0 text-muted fst-italic" i18n>No bulk share links currently exist.</p>
+    }
+    @if (bundles.length > 0) {
+      <div class="table-responsive">
+        <table class="table table-sm align-middle mb-0">
+          <thead>
+            <tr>
+              <th scope="col" i18n>Created</th>
+              <th scope="col" i18n>Status</th>
+              <th scope="col" i18n>Expires</th>
+              <th scope="col" i18n>Documents</th>
+              <th scope="col" i18n>Actions</th>
+            </tr>
+          </thead>
+          <tbody>
+            @for (bundle of bundles; track bundle.id) {
+              <tr>
+                <td>{{ bundle.created | date: 'short' }}</td>
+                <td>
+                  <span class="badge text-bg-secondary text-uppercase">{{ bundle.status }}</span>
+                </td>
+                <td>
+                  @if (bundle.expiration) {
+                    {{ bundle.expiration | date: 'short' }}
+                  }
+                  @if (!bundle.expiration) {
+                    <span i18n>Never</span>
+                  }
+                </td>
+                <td>{{ bundle.document_count }}</td>
+                <td>
+                  <div class="btn-group btn-group-sm">
+                    <button
+                      type="button"
+                      class="btn btn-outline-primary"
+                      (click)="copy(bundle)"
+                    >
+                      @if (copiedSlug === bundle.slug) {
+                        <i-bs name="clipboard-check"></i-bs>
+                      }
+                      @if (copiedSlug !== bundle.slug) {
+                        <i-bs name="clipboard"></i-bs>
+                      }
+                      <span class="visually-hidden" i18n>Copy link</span>
+                    </button>
+                    <button
+                      type="button"
+                      class="btn btn-outline-danger"
+                      (click)="delete(bundle)"
+                    >
+                      <i-bs name="trash"></i-bs>
+                      <span class="visually-hidden" i18n>Delete link</span>
+                    </button>
+                  </div>
+                </td>
+              </tr>
+            }
+          </tbody>
+        </table>
+      </div>
+    }
+  }
+</div>
+
+<div class="modal-footer">
+  <button type="button" class="btn btn-outline-secondary btn-sm" (click)="close()" i18n>Close</button>
+</div>
diff --git a/src-ui/src/app/components/common/share-bundle-manage-dialog/share-bundle-manage-dialog.component.spec.ts b/src-ui/src/app/components/common/share-bundle-manage-dialog/share-bundle-manage-dialog.component.spec.ts
new file mode 100644 (file)
index 0000000..3f39da1
--- /dev/null
@@ -0,0 +1,7 @@
+describe('ShareBundleManageDialogComponent', () => {
+  it('is pending implementation', () => {
+    pending(
+      'ShareBundleManageDialogComponent tests will be implemented once the dialog logic is finalized.'
+    )
+  })
+})
diff --git a/src-ui/src/app/components/common/share-bundle-manage-dialog/share-bundle-manage-dialog.component.ts b/src-ui/src/app/components/common/share-bundle-manage-dialog/share-bundle-manage-dialog.component.ts
new file mode 100644 (file)
index 0000000..e552ffe
--- /dev/null
@@ -0,0 +1,95 @@
+import { Clipboard } from '@angular/cdk/clipboard'
+import { CommonModule } from '@angular/common'
+import { Component, OnInit, inject } from '@angular/core'
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
+import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
+import { first } from 'rxjs'
+import { ShareBundleSummary } from 'src/app/data/share-bundle'
+import { ShareBundleService } from 'src/app/services/rest/share-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',
+  standalone: true,
+  imports: [CommonModule, NgxBootstrapIconsModule],
+})
+export class ShareBundleManageDialogComponent
+  extends LoadingComponentWithPermissions
+  implements OnInit
+{
+  private activeModal = inject(NgbActiveModal)
+  private shareBundleService = inject(ShareBundleService)
+  private toastService = inject(ToastService)
+  private clipboard = inject(Clipboard)
+
+  title = $localize`Bulk Share Links`
+
+  bundles: ShareBundleSummary[] = []
+  error: string
+  copiedSlug: string
+
+  ngOnInit(): void {
+    this.fetchBundles()
+  }
+
+  fetchBundles(): void {
+    this.loading = true
+    this.error = null
+    this.shareBundleService
+      .listAllBundles()
+      .pipe(first())
+      .subscribe({
+        next: (results) => {
+          this.bundles = results
+          this.loading = false
+        },
+        error: (e) => {
+          this.loading = false
+          this.error = $localize`Failed to load bulk share links.`
+          this.toastService.showError(
+            $localize`Error retrieving bulk share links.`,
+            e
+          )
+        },
+      })
+  }
+
+  getShareUrl(bundle: ShareBundleSummary): string {
+    const apiURL = new URL(environment.apiBaseUrl)
+    return `${apiURL.origin}${apiURL.pathname.replace(/\/api\/$/, '/share/')}${
+      bundle.slug
+    }`
+  }
+
+  copy(bundle: ShareBundleSummary): void {
+    const success = this.clipboard.copy(this.getShareUrl(bundle))
+    if (success) {
+      this.copiedSlug = bundle.slug
+      setTimeout(() => {
+        this.copiedSlug = null
+      }, 3000)
+    }
+  }
+
+  delete(bundle: ShareBundleSummary): void {
+    this.shareBundleService.delete(bundle).subscribe({
+      next: () => {
+        this.toastService.showInfo($localize`Bulk share link deleted.`)
+        this.fetchBundles()
+      },
+      error: (e) => {
+        this.toastService.showError(
+          $localize`Error deleting bulk share link.`,
+          e
+        )
+      },
+    })
+  }
+
+  close(): void {
+    this.activeModal.close()
+  }
+}
index 7748b76764b9d08d0a9d705cc8e7da07f4b37703..8f47ac020888f1a01fed0e9985a0b2013dca1e23 100644 (file)
@@ -56,6 +56,7 @@ import {
 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 { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
 import { CustomFieldsBulkEditDialogComponent } from './custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component'
 
@@ -955,9 +956,10 @@ export class BulkEditorComponent
   }
 
   manageShareLinks() {
-    this.toastService.showInfo(
-      $localize`Bulk share link management is coming soon.`
-    )
+    const modal = this.modalService.open(ShareBundleManageDialogComponent, {
+      backdrop: 'static',
+      size: 'lg',
+    })
   }
 
   emailSelected() {
index 0b66da77fc40fdb76b7f693ab1784134fdad3117..7fcb9fc4e0390be9f468b282c22159eb7a359506 100644 (file)
@@ -1,5 +1,6 @@
 import { Injectable } from '@angular/core'
 import { Observable } from 'rxjs'
+import { map } from 'rxjs/operators'
 import {
   ShareBundleCreatePayload,
   ShareBundleSummary,
@@ -22,12 +23,9 @@ export class ShareBundleService extends AbstractNameFilterService<ShareBundleSum
     return this.http.post<ShareBundleSummary>(this.getResourceUrl(), payload)
   }
 
-  listBundlesForDocuments(
-    documentIds: number[]
-  ): Observable<ShareBundleSummary[]> {
-    const params = { documents: documentIds.join(',') }
-    return this.http.get<ShareBundleSummary[]>(this.getResourceUrl(), {
-      params,
-    })
+  listAllBundles(): Observable<ShareBundleSummary[]> {
+    return this.list(1, 1000, 'created', true).pipe(
+      map((response) => response.results)
+    )
   }
 }