Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,37 @@
</div>
}
@if (!item.isFolder && !isZip) {
<div prBgImage [bgSrc]="recordThumbnailUrl" [cover]="inGridView"></div>
<div
prBgImage
[bgSrc]="recordThumbnailUrl"
[cover]="inGridView"
[class.replaced-by-thumbnail-placeholder]="!!thumbnailPlaceholder"
></div>
@if (thumbnailPlaceholder; as placeholder) {
<div
class="thumbnail-placeholder"
[attr.data-placeholder]="placeholder"
>
@switch (placeholder) {
@case (RecordThumbnailPlaceholder.Preparing) {
<img src="assets/svg/access-copy/row-preparing.svg" alt="" />
}
@case (RecordThumbnailPlaceholder.Failed) {
<img src="assets/svg/access-copy/row-failed.svg" alt="" />
}
@case (RecordThumbnailPlaceholder.FileType) {
<span class="thumbnail-placeholder-file-type">
<img src="assets/svg/access-copy/row-file-outline.svg" alt="" />
@if (originalFileExtension) {
<span class="thumbnail-placeholder-file-extension">{{
originalFileExtension
}}</span>
}
</span>
}
}
</div>
}
}
</ng-container>
@if (inGridView && item.isFolder ? item.view : true) {
Expand Down Expand Up @@ -87,6 +117,13 @@
>
}
</div>
@if (previewStatusLabel; as statusLabel) {
<div class="preview-status" [attr.data-preview-status]="statusLabel.tone">
<span class="preview-status-label">{{ statusLabel.text }}</span>
<span class="preview-status-stored">•</span>
<span class="preview-status-stored">Stored</span>
</div>
}
<div class="second-row">
@if (!showAccess || !item.ShareArchiveVO) {
<span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,18 @@ $bg-transition-length: 0.075s;
color: $text-muted;
}

.preview-status {
display: none;
gap: 4px;
margin-top: 4px;
font-size: 12px;
line-height: 16px;

@include after($tablet-horizontal) {
display: flex;
}
}

@include after($tablet-horizontal) {
.second-row {
display: none;
Expand Down Expand Up @@ -267,7 +279,8 @@ $bg-transition-length: 0.075s;
}

[prBgImage],
.file-list-item-folder-icon {
.file-list-item-folder-icon,
.thumbnail-placeholder {
height: $row-height - ($grid-unit * 0.5);
width: $row-height - ($grid-unit * 0.5);
flex: 0 0 $row-height - ($grid-unit * 0.5);
Expand Down Expand Up @@ -435,3 +448,71 @@ $multi-select-radius: 30px;
.public-archive {
margin-bottom: 5rem;
}

.preview-status {
&[data-preview-status='preparing'] .preview-status-label {
color: $PR-yellow-500;
}

&[data-preview-status='failed'] .preview-status-label {
color: $PR-red-500;
}

.preview-status-stored {
color: $PR-blue-400;
}
}

[prBgImage].replaced-by-thumbnail-placeholder {
@include after($tablet-horizontal) {
display: none;
}
}

.thumbnail-placeholder {
display: none;
align-items: center;
justify-content: center;
overflow: hidden;

@include after($tablet-horizontal) {
display: flex;
}

&[data-placeholder='preparing'] img {
width: 100%;
height: 100%;
}

&[data-placeholder='failed'],
&[data-placeholder='file-type'] {
background-color: $PR-blue-25;
border: 1px solid $PR-blue-50;
}
}

.thumbnail-placeholder-file-type {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
}

.thumbnail-placeholder-file-extension {
position: absolute;
left: 8px;
bottom: 5px;
height: 10px;
padding: 0 3px;
display: flex;
align-items: center;
border-radius: 2px;
background-image: linear-gradient(117deg, $PR-purple 0%, $PR-orange 100%);
color: $PR-brand-white;
font-size: 7px;
line-height: 1;
letter-spacing: 1.12px;
text-transform: uppercase;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { EditService } from '@core/services/edit/edit.service';
import { DeviceService } from '@shared/services/device/device.service';
import { provideNoopAnimations } from '@angular/platform-browser/animations';
import { GetThumbnailPipe } from '@shared/pipes/get-thumbnail.pipe';
import { RecordVO } from '@models/record-vo';
import { FileFormat } from '@models/file-vo';
import { GeneratedFileStatus } from '@models/generated-file-status';
import { RecordThumbnailPlaceholder } from '@models/record-thumbnail-placeholder';
import { FileListItemComponent } from './file-list-item.component';

@Pipe({ name: 'itemTypeIcon' })
Expand Down Expand Up @@ -607,4 +611,138 @@ describe('FileListItemComponent', () => {
expect(component.startDisplayTime).toBe('2020-06-10');
});
});

describe('access copy status', () => {
const makeRecord = (fields: Partial<RecordVO>) =>
new RecordVO({
displayName: "Grandma Ruth's Recipes",
archiveNbr: '0001-0001',
folder_linkId: 1,
type: 'type.record.document',
FileVOs: [
{
fileId: 1,
size: 1024,
format: FileFormat.Original,
fileURL: 'https://example.com/original',
downloadURL: 'https://example.com/download',
type: 'type.file.document.docx',
},
],
...fields,
});

const showItem = async (item: RecordVO) => {
component.item = item;
await component.ngOnInit();
fixture.detectChanges();
};

const statusLine = (): HTMLElement | null =>
fixture.nativeElement.querySelector('.preview-status');

const placeholderElement = (): HTMLElement | null =>
fixture.nativeElement.querySelector('.thumbnail-placeholder');

it('shows that the viewable copy is being prepared', async () => {
await showItem(
makeRecord({
accessCopyStatus: GeneratedFileStatus.Processing,
thumbnail256Status: GeneratedFileStatus.Processing,
}),
);

expect(component.thumbnailPlaceholder).toBe(
RecordThumbnailPlaceholder.Preparing,
);

expect(
placeholderElement().querySelector('img').getAttribute('src'),
).toBe('assets/svg/access-copy/row-preparing.svg');

expect(statusLine().getAttribute('data-preview-status')).toBe(
'preparing',
);

expect(statusLine().textContent).toContain('Preparing to view...');
expect(statusLine().textContent).toContain('Stored');
});

it('shows that the preview is unavailable when the copy failed', async () => {
await showItem(
makeRecord({
accessCopyStatus: GeneratedFileStatus.Failed,
thumbnail256Status: GeneratedFileStatus.Failed,
}),
);

expect(component.thumbnailPlaceholder).toBe(
RecordThumbnailPlaceholder.Failed,
);

expect(statusLine().getAttribute('data-preview-status')).toBe('failed');
expect(statusLine().textContent).toContain('Preview unavailable');
});

it('shows the file type and no status line when no copy will be made', async () => {
await showItem(
makeRecord({
type: 'type.record.image',
uploadFileName: 'family-history-graph.svg',
FileVOs: [],
accessCopyStatus: null,
thumbnail256Status: null,
}),
);

expect(component.thumbnailPlaceholder).toBe(
RecordThumbnailPlaceholder.FileType,
);

expect(
placeholderElement().querySelector(
'.thumbnail-placeholder-file-extension',
).textContent,
).toBe('svg');

expect(statusLine()).toBeNull();
});

it('keeps an existing thumbnail and shows no placeholder', async () => {
await showItem(
makeRecord({
thumbURL200: 'https://example.com/thumb200.jpg',
accessCopyStatus: GeneratedFileStatus.Failed,
thumbnail256Status: GeneratedFileStatus.Failed,
}),
);

expect(component.thumbnailPlaceholder).toBeUndefined();
expect(placeholderElement()).toBeNull();
});

it('shows nothing new in grid view', async () => {
component.inGridView = true;
component.item = makeRecord({
accessCopyStatus: GeneratedFileStatus.Processing,
});

expect(component.thumbnailPlaceholder).toBeUndefined();
expect(component.previewStatusLabel).toBeUndefined();
});

it('shows nothing new behind the stock image of a listed share preview', async () => {
const router = TestBed.inject(Router);
(router.routerState.snapshot as any).url = '/share/test';

await showItem(
makeRecord({ accessCopyStatus: GeneratedFileStatus.Processing }),
);

expect(component.thumbnailPlaceholder).toBeUndefined();
expect(component.previewStatusLabel).toBeUndefined();

(router.routerState.snapshot as any).url = '/';
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ import { ngIfFadeInAnimation } from '@shared/animations';
import { RouteData } from '@root/app/app.routes';
import { ThumbnailCache } from '@shared/utilities/thumbnail-cache/thumbnail-cache';
import { GetThumbnail } from '@models/get-thumbnail';
import {
getOriginalFileExtension,
getRecordPreviewState,
RecordPreviewState,
} from '@models/record-preview-state';
import {
getRecordThumbnailPlaceholder,
RecordThumbnailPlaceholder,
} from '@models/record-thumbnail-placeholder';
import { ShareLinksService } from '@root/app/share-links/services/share-links.service';
import { ItemClickEvent } from '../file-list/file-list.component';
import { SharingComponent } from '../sharing/sharing.component';
Expand Down Expand Up @@ -155,6 +164,24 @@ const DOUBLE_CLICK_TIMEOUT = 350;
const MOUSE_DOWN_DRAG_TIMEOUT = 500;
const DRAG_MIN_Y = 1;

interface PreviewStatusLabel {
text: string;
tone: 'preparing' | 'failed';
}

const PREVIEW_STATUS_LABELS: Partial<
Record<RecordPreviewState, PreviewStatusLabel>
> = {
[RecordPreviewState.Preparing]: {
text: 'Preparing to view...',
tone: 'preparing',
},
[RecordPreviewState.Failed]: {
text: 'Preview unavailable',
tone: 'failed',
},
};

@Component({
selector: 'pr-file-list-item',
templateUrl: './file-list-item.component.html',
Expand Down Expand Up @@ -209,6 +236,8 @@ export class FileListItemComponent
public isUnlistedShare = false;
public recordThumbnailUrl: string | undefined;

public readonly RecordThumbnailPlaceholder = RecordThumbnailPlaceholder;

private folderThumb: string;
private folderContentsType: FolderContentsType = FolderContentsType.NORMAL;

Expand Down Expand Up @@ -269,6 +298,35 @@ export class FileListItemComponent
return edtfStartDate || this.item.displayDT;
}

get thumbnailPlaceholder(): RecordThumbnailPlaceholder | undefined {
if (!this.showsRecordPreviewStatus || this.recordThumbnailUrl) {
return undefined;
}
return getRecordThumbnailPlaceholder(this.item as RecordVO);
}

get previewStatusLabel(): PreviewStatusLabel | undefined {
if (!this.showsRecordPreviewStatus) {
return undefined;
}
return PREVIEW_STATUS_LABELS[getRecordPreviewState(this.item as RecordVO)];
}

get originalFileExtension(): string | undefined {
return getOriginalFileExtension(this.item as RecordVO);
}

private get showsRecordPreviewStatus(): boolean {
const showsStockPreviewImage =
this.isInSharePreview && !this.isUnlistedShare;
return (
this.item.isRecord &&
!this.isZip &&
!this.inGridView &&
!showsStockPreviewImage
);
}

async ngOnInit() {
this.isInSharePreview =
this.router.routerState.snapshot.url.includes('/share/');
Expand Down
Loading
Loading