Skip to content
Closed
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
1 change: 1 addition & 0 deletions packages/base/avif-image-def.gts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const AVIF_MAX_HEADER_BYTES = 65_536;

export class AvifDef extends ImageDef {
static displayName = 'AVIF Image';
static acceptTypes = '.avif,image/avif';

static async extractAttributes(
url: string,
Expand Down
1 change: 1 addition & 0 deletions packages/base/gif-image-def.gts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { extractGifDimensions } from './gif-meta-extractor';

export class GifDef extends ImageDef {
static displayName = 'GIF Image';
static acceptTypes = '.gif,image/gif';

static async extractAttributes(
url: string,
Expand Down
1 change: 1 addition & 0 deletions packages/base/image-file-def.gts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ class Fitted extends Component<typeof ImageDef> {

export class ImageDef extends FileDef {
static displayName = 'Image';
static acceptTypes = 'image/*';

@field width = contains(NumberField);
@field height = contains(NumberField);
Expand Down
1 change: 1 addition & 0 deletions packages/base/jpg-image-def.gts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const JPEG_MAX_HEADER_BYTES = 65_536;

export class JpgDef extends ImageDef {
static displayName = 'JPEG Image';
static acceptTypes = '.jpg,.jpeg,image/jpeg';

static async extractAttributes(
url: string,
Expand Down
1 change: 1 addition & 0 deletions packages/base/markdown-file-def.gts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ function extractExcerpt(markdown: string): string {

export class MarkdownDef extends FileDef {
static displayName = 'Markdown';
static acceptTypes = '.md,.markdown';

@field title = contains(StringField);
@field excerpt = contains(StringField);
Expand Down
1 change: 1 addition & 0 deletions packages/base/png-image-def.gts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { extractPngDimensions } from './png-meta-extractor';

export class PngDef extends ImageDef {
static displayName = 'PNG Image';
static acceptTypes = '.png,image/png';

static async extractAttributes(
url: string,
Expand Down
1 change: 1 addition & 0 deletions packages/base/svg-image-def.gts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { extractSvgDimensions } from './svg-meta-extractor';

export class SvgDef extends ImageDef {
static displayName = 'SVG Image';
static acceptTypes = '.svg,image/svg+xml';

static async extractAttributes(
url: string,
Expand Down
1 change: 1 addition & 0 deletions packages/base/webp-image-def.gts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { extractWebpDimensions } from './webp-meta-extractor';

export class WebpDef extends ImageDef {
static displayName = 'WebP Image';
static acceptTypes = '.webp,image/webp';

static async extractAttributes(
url: string,
Expand Down
188 changes: 160 additions & 28 deletions packages/host/app/components/operator-mode/choose-file-modal.gts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,25 @@ import {
BoxelButton,
FieldContainer,
BoxelSelect,
LoadingIndicator,
} from '@cardstack/boxel-ui/components';

import { eq } from '@cardstack/boxel-ui/helpers';

import {
Deferred,
RealmPaths,
isCardErrorJSONAPI,
loadCardDef,
type CodeRef,
type LocalPath,
} from '@cardstack/runtime-common';

import ModalContainer from '@cardstack/host/components/modal-container';

import type FileUploadService from '@cardstack/host/services/file-upload';
import type { FileUploadTask } from '@cardstack/host/services/file-upload';
import type LoaderService from '@cardstack/host/services/loader-service';
import type OperatorModeStateService from '@cardstack/host/services/operator-mode-state-service';
import type RealmService from '@cardstack/host/services/realm';
import type StoreService from '@cardstack/host/services/store';
Expand All @@ -44,10 +51,14 @@ export default class ChooseFileModal extends Component<Signature> {
@tracked selectedFile?: LocalPath;
@tracked fileTypeFilter?: CodeRef;
@tracked fileTypeName?: string;
@tracked acceptTypes?: string;
@tracked currentUpload?: FileUploadTask;

@service declare private operatorModeStateService: OperatorModeStateService;
@service declare private realm: RealmService;
@service declare private store: StoreService;
@service('file-upload') declare private fileUpload: FileUploadService;
@service('loader-service') declare private loaderService: LoaderService;

constructor(owner: Owner, args: Signature['Args']) {
super(owner, args);
Expand All @@ -64,6 +75,11 @@ export default class ChooseFileModal extends Component<Signature> {
return 'Choose a File';
}

private get isUploadBusy(): boolean {
let state = this.currentUpload?.state;
return state === 'picking' || state === 'uploading';
}

// public API
async chooseFile<T extends FileDef>(opts?: {
fileType?: CodeRef;
Expand All @@ -72,12 +88,25 @@ export default class ChooseFileModal extends Component<Signature> {
this.deferred = new Deferred();
this.fileTypeFilter = opts?.fileType;
this.fileTypeName = opts?.fileTypeName;
this.acceptTypes = undefined;
this.currentUpload = undefined;
let defaultRealm = this.knownRealms.find(
(r) =>
r.url.toString() === this.operatorModeStateService.realmURL?.toString(),
);
this.selectedRealm = defaultRealm ?? this.selectedRealm;

if (opts?.fileType) {
try {
let cardDef = await loadCardDef(opts.fileType, {
loader: this.loaderService.loader,
});
this.acceptTypes = (cardDef as any).acceptTypes;
} catch {
// If we can't load the def, acceptTypes stays undefined (allow all)
}
}

let file = await this.deferred.promise;
if (file) {
return file as T;
Expand Down Expand Up @@ -105,14 +134,36 @@ export default class ChooseFileModal extends Component<Signature> {
this.deferred.fulfill(file);
}
} finally {
this.selectedRealm = this.knownRealms[0];
this.selectedFile = undefined;
this.fileTypeFilter = undefined;
this.fileTypeName = undefined;
this.deferred = undefined;
this.resetState();
}
}

@action
private triggerUpload() {
let task = this.fileUpload.uploadFile({
realmURL: this.selectedRealm.url,
acceptTypes: this.acceptTypes,
});
this.currentUpload = task;
task.result.then((fileDef) => {
if (fileDef && this.deferred) {
this.deferred.fulfill(fileDef);
this.resetState();
}
this.currentUpload = undefined;
});
}

private resetState() {
this.selectedRealm = this.knownRealms[0];
this.selectedFile = undefined;
this.fileTypeFilter = undefined;
this.fileTypeName = undefined;
this.acceptTypes = undefined;
this.currentUpload = undefined;
this.deferred = undefined;
}

private get knownRealms() {
return Object.entries(this.realm.allRealmsInfo).map((entry) => ({
url: new URL(entry[0]),
Expand Down Expand Up @@ -166,11 +217,19 @@ export default class ChooseFileModal extends Component<Signature> {
max-width: 100%;
min-width: 13rem;
}
.footer {
display: flex;
justify-content: space-between;
max-width: 100%;
min-width: 13rem;
}
.footer-left {
}
.footer-buttons {
display: flex;
margin-left: auto;
gap: var(--horizontal-gap);
align-self: center;
align-items: center;
margin-left: auto;
}
fieldset.field {
border: none;
Expand Down Expand Up @@ -208,6 +267,33 @@ export default class ChooseFileModal extends Component<Signature> {
border: none;
padding: 0 var(--boxel-sp) 40px var(--boxel-sp);
}
.upload-progress {
display: flex;
align-items: center;
gap: var(--boxel-sp-xs);
flex: 1;
}
.upload-spinner {
--boxel-loading-indicator-size: 1.25em;
}
.upload-file-name {
font: var(--boxel-font-xs);
color: var(--boxel-600);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 120px;
}
.upload-error-row {
display: flex;
align-items: center;
gap: var(--boxel-sp-xs);
flex: 1;
}
.upload-error {
color: var(--boxel-error-200);
font: var(--boxel-font-xs);
}
</style>
{{#if this.deferred}}
<ModalContainer
Expand Down Expand Up @@ -253,28 +339,74 @@ export default class ChooseFileModal extends Component<Signature> {
/>
{{/each}}
</FieldContainer>
<FieldContainer class='field buttons' @label='' @tag='div'>
<div class='footer'>
<div class='footer-left'>
{{#if (eq this.currentUpload.state 'picking')}}
<BoxelButton
@size='tall'
@disabled={{true}}
data-test-choose-file-modal-upload-button
>
Choose a file&hellip;
</BoxelButton>
{{else if (eq this.currentUpload.state 'uploading')}}
<div
class='upload-progress'
data-test-choose-file-modal-upload-progress
>
<span
class='upload-file-name'
>{{this.currentUpload.fileName}}</span>
<LoadingIndicator class='upload-spinner' />
</div>
{{else if (eq this.currentUpload.state 'error')}}
<div class='upload-error-row'>
<div
class='upload-error'
data-test-choose-file-modal-upload-error
>{{this.currentUpload.error}}</div>
<BoxelButton
@size='tall'
{{on 'click' this.triggerUpload}}
data-test-choose-file-modal-upload-button
>
Retry&hellip;
</BoxelButton>
</div>
{{else}}
<BoxelButton
@size='tall'
{{on 'click' this.triggerUpload}}
data-test-choose-file-modal-upload-button
>
Upload&hellip;
</BoxelButton>
{{/if}}
</div>
<div class='footer-buttons'>
<BoxelButton
@size='tall'
{{on 'click' (fn this.pick undefined)}}
{{onKeyMod 'Escape'}}
data-test-choose-file-modal-cancel-button
>
Cancel
</BoxelButton>
<BoxelButton
@kind='primary'
@size='tall'
@disabled={{this.isUploadBusy}}
{{on 'click' (fn this.pick this.selectedFile)}}
{{onKeyMod 'Enter'}}
data-test-choose-file-modal-add-button
>
Add
</BoxelButton>
</div>
</div>
</FieldContainer>
</:content>
<:footer>
<div class='footer-buttons'>
<BoxelButton
@size='tall'
{{on 'click' (fn this.pick undefined)}}
{{onKeyMod 'Escape'}}
data-test-choose-file-modal-cancel-button
>
Cancel
</BoxelButton>
<BoxelButton
@kind='primary'
@size='tall'
{{on 'click' (fn this.pick this.selectedFile)}}
{{onKeyMod 'Enter'}}
data-test-choose-file-modal-add-button
>
Add
</BoxelButton>
</div>
</:footer>
</ModalContainer>
{{/if}}
</template>
Expand Down
Loading
Loading