--- /dev/null
+<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>
--- /dev/null
+describe('ShareBundleDialogComponent', () => {
+ it('is pending implementation', () => {
+ pending(
+ 'ShareBundleDialogComponent tests will be implemented once the dialog logic is finalized.'
+ )
+ })
+})
--- /dev/null
+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 })
+ }
+ }
+}
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'
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`
} 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'
}
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() {
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