Skip to content
Merged
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
17 changes: 13 additions & 4 deletions apps/admin-x-framework/src/hooks/use-koenig-file-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,16 @@ export const useKoenigFileUpload = (type: KoenigFileUploadType = 'image'): FileU
} catch (error) {
console.error(error); // eslint-disable-line

// TODO: check for or expose known error types?
// The API wraps some errors (e.g. HostLimitError) with a generic
// message and puts the specific, user-actionable text in `context`.
// Prefer `context` so the user sees the useful message, falling back
// to `message` for errors that don't set a context.
const context = getStringAtPath(error, ['data', 'errors', 0, 'context']) || '';
const message = getStringAtPath(error, ['data', 'errors', 0, 'message']) || getStringAtPath(error, ['message']) || '';

const errorResult = {
message: getStringAtPath(error, ['data', 'errors', 0, 'message']) || getStringAtPath(error, ['message']) || '',
context: getStringAtPath(error, ['data', 'errors', 0, 'context']) || '',
message: context || message,
context,
fileName: file.name
};

Expand All @@ -194,6 +200,9 @@ export const useKoenigFileUpload = (type: KoenigFileUploadType = 'image'): FileU
const upload = async (files: FileList | ReadonlyArray<File> = [], options: UploadOptions = {}) => {
setFilesNumber(files.length);
setLoading(true);
// Each upload attempt starts fresh so retries don't accumulate
// duplicate error messages from previous failed attempts.
setErrors([]);

const validationResult = validate(files);

Expand Down Expand Up @@ -225,7 +234,7 @@ export const useKoenigFileUpload = (type: KoenigFileUploadType = 'image'): FileU
} catch (error) {
console.error(error); // eslint-disable-line no-console

setErrors(prev => [...prev, error as UploadError]);
setErrors([error as UploadError]);
setLoading(false);
setProgress(100);
progressTracker.current.clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ const serverErrorUploadResponse: UploadResponse = {
status: 413
};

// HostLimitError responses use a generic `message` and put the specific,
// user-actionable text in `context` (see @tryghost/mw-error-handler).
const hostLimitUploadResponse: UploadResponse = {
body: {
errors: [{
type: 'HostLimitError',
message: 'Host Limit error, cannot upload image.',
context: 'Your plan supports uploads up to 5.24288MB. Please upgrade to upload larger files.'
}]
},
status: 403
};

interface RequestLog {
method?: string;
url?: string;
Expand Down Expand Up @@ -231,6 +244,47 @@ describe('useKoenigFileUpload', () => {
);
});

it('prefers the actionable context message over the generic message', async () => {
uploadResponse = hostLimitUploadResponse;

const {result} = renderHook(() => useKoenigFileUpload('image'));

const file = makeFile('photo.jpg');
await act(async () => {
await result.current.upload([file]);
});

expect(result.current.errors).toHaveLength(1);
expect(result.current.errors[0].fileName).toBe('photo.jpg');
// The user should see the specific limit text from `context`, not the
// generic "Host Limit error, cannot upload image." wrapper message.
expect(result.current.errors[0].message).toBe(
'Your plan supports uploads up to 5.24288MB. Please upgrade to upload larger files.'
);
expect(result.current.errors[0].context).toBe(
'Your plan supports uploads up to 5.24288MB. Please upgrade to upload larger files.'
);
});

it('does not accumulate errors across repeated failed uploads', async () => {
uploadResponse = hostLimitUploadResponse;

const {result} = renderHook(() => useKoenigFileUpload('image'));

const file = makeFile('photo.jpg');

await act(async () => {
await result.current.upload([file]);
});
expect(result.current.errors).toHaveLength(1);

// Retrying the same failing upload should replace, not append.
await act(async () => {
await result.current.upload([file]);
});
expect(result.current.errors).toHaveLength(1);
});

it('rejects files with no extension for image type', async () => {
const {result} = renderHook(() => useKoenigFileUpload('image'));

Expand Down
18 changes: 9 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ catalog:
'@tryghost/helpers': 1.1.106
'@tryghost/kg-clean-basic-html': 4.3.2
'@tryghost/kg-converters': 1.2.2
'@tryghost/koenig-lexical': 1.8.2
'@tryghost/koenig-lexical': 1.8.3
'@tryghost/limit-service': 1.5.6
'@tryghost/logging': 4.2.1
'@tryghost/nql': 0.13.1
Expand Down
Loading