]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Skeleton share bundle component
authorshamoon <4887959+shamoon@users.noreply.github.com>
Tue, 4 Nov 2025 17:33:19 +0000 (09:33 -0800)
committershamoon <4887959+shamoon@users.noreply.github.com>
Tue, 4 Nov 2025 17:33:19 +0000 (09:33 -0800)
src-ui/src/app/components/common/share-bundle-dialog/share-bundle-dialog.component.html [new file with mode: 0644]
src-ui/src/app/components/common/share-bundle-dialog/share-bundle-dialog.component.scss [new file with mode: 0644]
src-ui/src/app/components/common/share-bundle-dialog/share-bundle-dialog.component.spec.ts [new file with mode: 0644]
src-ui/src/app/components/common/share-bundle-dialog/share-bundle-dialog.component.ts [new file with mode: 0644]
src-ui/src/app/components/common/share-links-dialog/share-links-dialog.component.ts
src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts
src-ui/src/app/data/share-link.ts

diff --git a/src-ui/src/app/components/common/share-bundle-dialog/share-bundle-dialog.component.html b/src-ui/src/app/components/common/share-bundle-dialog/share-bundle-dialog.component.html
new file mode 100644 (file)
index 0000000..4635f3b
--- /dev/null
@@ -0,0 +1,67 @@
+<div class="modal-header">
+  <h4 class="modal-title" i18n>Share Selected Documents</h4>
+  <button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
+</div>
+<div class="modal-body">
+  <form [formGroup]="form" class="d-flex flex-column gap-3">
+    <div>
+      <p class="mb-1">
+        <ng-container i18n>This dialog gathers the options for sending a single link to multiple documents.</ng-container>
+      </p>
+      <p class="mb-1">
+        <ng-container i18n>Selected documents:</ng-container>
+        {{ selectionCount }}
+      </p>
+      @if (documentPreview.length > 0) {
+        <ul class="list-unstyled small mb-0">
+          @for (docId of documentPreview; track docId) {
+            <li>
+              <code>{{ docId }}</code>
+            </li>
+          }
+          @if (selectionCount > documentPreview.length) {
+            <li>
+              <ng-container i18n>+ {{ selectionCount - documentPreview.length }} more…</ng-container>
+            </li>
+          }
+        </ul>
+      }
+    </div>
+
+    <div class="d-flex align-items-center justify-content-between">
+      <div class="form-check form-switch">
+        <input
+          class="form-check-input"
+          type="checkbox"
+          role="switch"
+          id="shareArchiveSwitch"
+          formControlName="shareArchiveVersion"
+          [disabled]="archiveOptionDisabled"
+        />
+        <label class="form-check-label" for="shareArchiveSwitch" i18n>Share archive version</label>
+      </div>
+    </div>
+    @if (archiveOptionDisabled && selectionCount > 0) {
+      <p class="small text-muted mb-0">
+        <ng-container i18n>Archive versions are available only when every selected document has one. Missing archive versions: {{ missingArchiveCount }}.</ng-container>
+      </p>
+    }
+
+    <div class="input-group">
+      <label class="input-group-text" for="expirationDays"><ng-container i18n>Expires</ng-container>:</label>
+      <select class="form-select" id="expirationDays" formControlName="expirationDays">
+        @for (option of expirationOptions; track option.value) {
+          <option [ngValue]="option.value">{{ option.label }}</option>
+        }
+      </select>
+    </div>
+
+    <div class="alert alert-info mb-0" role="alert">
+      <ng-container i18n>Bulk share link creation is still being prototyped. Saving will close this dialog and return the selected options only.</ng-container>
+    </div>
+  </form>
+</div>
+<div class="modal-footer">
+  <button type="button" class="btn btn-outline-secondary btn-sm" (click)="close()" i18n>Close</button>
+  <button type="button" class="btn btn-primary btn-sm" (click)="submit()" i18n>Save options</button>
+</div>
diff --git a/src-ui/src/app/components/common/share-bundle-dialog/share-bundle-dialog.component.scss b/src-ui/src/app/components/common/share-bundle-dialog/share-bundle-dialog.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src-ui/src/app/components/common/share-bundle-dialog/share-bundle-dialog.component.spec.ts b/src-ui/src/app/components/common/share-bundle-dialog/share-bundle-dialog.component.spec.ts
new file mode 100644 (file)
index 0000000..baae428
--- /dev/null
@@ -0,0 +1,7 @@
+describe('ShareBundleDialogComponent', () => {
+  it('is pending implementation', () => {
+    pending(
+      'ShareBundleDialogComponent tests will be implemented once the dialog logic is finalized.'
+    )
+  })
+})
diff --git a/src-ui/src/app/components/common/share-bundle-dialog/share-bundle-dialog.component.ts b/src-ui/src/app/components/common/share-bundle-dialog/share-bundle-dialog.component.ts
new file mode 100644 (file)
index 0000000..148e550
--- /dev/null
@@ -0,0 +1,98 @@
+import { CommonModule } from '@angular/common'
+import { Component, Input, inject } from '@angular/core'
+import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
+import {
+  FileVersion,
+  SHARE_LINK_EXPIRATION_OPTIONS,
+} from 'src/app/data/share-link'
+
+@Component({
+  selector: 'pngx-share-bundle-dialog',
+  templateUrl: './share-bundle-dialog.component.html',
+  standalone: true,
+  imports: [CommonModule, ReactiveFormsModule],
+})
+export class ShareBundleDialogComponent {
+  private activeModal = inject(NgbActiveModal)
+  private formBuilder = inject(FormBuilder)
+
+  private _documentIds: number[] = []
+  private _documentsWithArchive = 0
+
+  selectionCount = 0
+  documentPreview: number[] = []
+  form: FormGroup = this.formBuilder.group({
+    shareArchiveVersion: [true],
+    expirationDays: [7],
+  })
+
+  readonly expirationOptions = SHARE_LINK_EXPIRATION_OPTIONS
+
+  @Input()
+  set documentIds(ids: number[]) {
+    this._documentIds = ids ?? []
+    this.selectionCount = this._documentIds.length
+    this.documentPreview = this._documentIds.slice(0, 10)
+    this.syncArchiveOption()
+  }
+
+  get documentIds(): number[] {
+    return this._documentIds
+  }
+
+  @Input()
+  set documentsWithArchive(count: number) {
+    this._documentsWithArchive = count ?? 0
+    this.syncArchiveOption()
+  }
+
+  get documentsWithArchive(): number {
+    return this._documentsWithArchive
+  }
+
+  get archiveOptionDisabled(): boolean {
+    return (
+      this.selectionCount === 0 ||
+      this._documentsWithArchive !== this.selectionCount
+    )
+  }
+
+  get missingArchiveCount(): number {
+    return Math.max(this.selectionCount - this._documentsWithArchive, 0)
+  }
+
+  close() {
+    this.activeModal.close()
+  }
+
+  submit() {
+    // Placeholder until the backend workflow is wired up.
+    this.activeModal.close({
+      documentIds: this.documentIds,
+      options: {
+        fileVersion: this.form.value.shareArchiveVersion
+          ? FileVersion.Archive
+          : FileVersion.Original,
+        expirationDays: this.form.value.expirationDays,
+      },
+    })
+  }
+
+  private syncArchiveOption() {
+    const control = this.form.get('shareArchiveVersion')
+    if (!control) return
+
+    const canUseArchive =
+      this.selectionCount > 0 &&
+      this._documentsWithArchive === this.selectionCount
+
+    if (canUseArchive) {
+      control.enable({ emitEvent: false })
+      control.patchValue(true, { emitEvent: false })
+    } else {
+      control.disable({ emitEvent: false })
+      control.patchValue(false, { emitEvent: false })
+    }
+  }
+}
index ffe11808c7c7dbed8ce39cb44a29b5fcd147c3f3..813dbd2cf0104d9b95edf0444c2b9cb749446522 100644 (file)
@@ -4,7 +4,11 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
 import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
 import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
 import { first } from 'rxjs'
-import { FileVersion, ShareLink } from 'src/app/data/share-link'
+import {
+  FileVersion,
+  SHARE_LINK_EXPIRATION_OPTIONS,
+  ShareLink,
+} from 'src/app/data/share-link'
 import { ShareLinkService } from 'src/app/services/rest/share-link.service'
 import { ToastService } from 'src/app/services/toast.service'
 import { environment } from 'src/environments/environment'
@@ -21,12 +25,7 @@ export class ShareLinksDialogComponent implements OnInit {
   private toastService = inject(ToastService)
   private clipboard = inject(Clipboard)
 
-  EXPIRATION_OPTIONS = [
-    { label: $localize`1 day`, value: 1 },
-    { label: $localize`7 days`, value: 7 },
-    { label: $localize`30 days`, value: 30 },
-    { label: $localize`Never`, value: null },
-  ]
+  EXPIRATION_OPTIONS = SHARE_LINK_EXPIRATION_OPTIONS
 
   @Input()
   title = $localize`Share Links`
index f3b3f9b0e9a720813403887e9bb65601c25e51b0..0364ef006b9bce512667fca6cc724e4590a977f3 100644 (file)
@@ -54,6 +54,7 @@ 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 { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
 import { CustomFieldsBulkEditDialogComponent } from './custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component'
 
@@ -909,9 +910,19 @@ export class BulkEditorComponent
   }
 
   shareSelected() {
-    this.toastService.showInfo(
-      $localize`Bulk share link creation is coming soon.`
+    const selectedDocuments = this.list.documents.filter((d) =>
+      this.list.selected.has(d.id)
     )
+    const documentsWithArchive = selectedDocuments.filter(
+      (doc) => !!doc.archived_file_name
+    ).length
+
+    const modal = this.modalService.open(ShareBundleDialogComponent, {
+      backdrop: 'static',
+      size: 'lg',
+    })
+    modal.componentInstance.documentIds = Array.from(this.list.selected)
+    modal.componentInstance.documentsWithArchive = documentsWithArchive
   }
 
   manageShareLinks() {
index debc8c111255208a889e46fe4bccaefca96ad65e..d9710bd4736ccea0638d73c9f4b0f90857a2764d 100644 (file)
@@ -5,6 +5,18 @@ export enum FileVersion {
   Original = 'original',
 }
 
+export interface ShareLinkExpirationOption {
+  label: string
+  value: number | null
+}
+
+export const SHARE_LINK_EXPIRATION_OPTIONS: ShareLinkExpirationOption[] = [
+  { label: $localize`1 day`, value: 1 },
+  { label: $localize`7 days`, value: 7 },
+  { label: $localize`30 days`, value: 30 },
+  { label: $localize`Never`, value: null },
+]
+
 export interface ShareLink extends ObjectWithPermissions {
   created: string // Date