Squashed commit of the following:
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 40s
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 40s
commit 579f0878e5240dc17db69be1e0b0c0f5af7ef9fe
Author: Arno Kaimbacher <arno.kaimbacher@geosphere.at>
Date: Tue Jun 9 09:25:44 2026 +0200
feat: Refactor error handling in Dataset Edit form and improve validation messages
- Updated error handling in the Dataset Edit form to use a centralized formatError function for displaying validation messages.
- Enhanced user feedback by ensuring that error messages are displayed consistently across various fields.
- Modified the validation rule for arrayContainsTypes to provide clearer error messages for missing main and translated titles/abstracts.
- Introduced a new ValidationService to manage manual construction of validation errors.
- Updated Vite configuration to streamline asset loading and improve performance.
- Adjusted Inertia setup to utilize dynamic imports for page-specific assets.
- Cleaned up unnecessary comments and code in various files for better readability.
commit 5efddc2a58c0e164fef585cc7344c06155dbc2c1
Author: Arno Kaimbacher <arno.kaimbacher@geosphere.at>
Date: Mon Jan 12 17:02:47 2026 +0100
feat: add dataset change detection and form submission composables
- Implemented `useDatasetChangeDetection` for tracking unsaved changes in dataset forms, including comparisons for licenses, basic properties, files, coverage, and more.
- Added `useDatasetFormSubmission` for handling dataset form submissions with validation, success/error handling, and auto-save functionality.
This commit is contained in:
parent
0680879e2f
commit
9368a0dd8d
38 changed files with 5588 additions and 6181 deletions
|
|
@ -14,9 +14,7 @@ import Person from '#models/person';
|
|||
import db from '@adonisjs/lucid/services/db';
|
||||
import { TransactionClientContract } from '@adonisjs/lucid/types/database';
|
||||
import Subject from '#models/subject';
|
||||
// import CreateDatasetValidator from '#validators/create_dataset_validator';
|
||||
import { createDatasetValidator, updateDatasetValidator } from '#validators/dataset';
|
||||
// import UpdateDatasetValidator from '#validators/update_dataset_validator';
|
||||
import {
|
||||
TitleTypes,
|
||||
DescriptionTypes,
|
||||
|
|
@ -40,7 +38,8 @@ import { pipeline } from 'node:stream/promises';
|
|||
import { createWriteStream } from 'node:fs';
|
||||
import type { Multipart } from '@adonisjs/bodyparser';
|
||||
import * as fs from 'fs';
|
||||
import { parseBytesSize, getConfigFor, getTmpPath, formatBytes } from '#app/utils/utility-functions';
|
||||
import { parseBytesSize, getConfigFor, getTmpPath, formatBytes, errorMessage } from '#app/utils/utility-functions';
|
||||
import validation from '#services/validation_service';
|
||||
|
||||
interface Dictionary {
|
||||
[index: string]: string;
|
||||
|
|
@ -207,6 +206,7 @@ export default class DatasetController {
|
|||
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
||||
}),
|
||||
)
|
||||
.minLength(1) // Ensure at least the main title exists
|
||||
// .minLength(2)
|
||||
.arrayContainsTypes({ typeA: 'main', typeB: 'translated' }),
|
||||
descriptions: vine
|
||||
|
|
@ -426,37 +426,31 @@ export default class DatasetController {
|
|||
}
|
||||
|
||||
public async store({ auth, request, response, session }: HttpContext) {
|
||||
// At the top of the store() method, declare an array to hold temporary file paths
|
||||
const uploadedTmpFiles: string[] = [];
|
||||
// Aggregated limit example (adjust as needed)
|
||||
const multipartConfig = getConfigFor('multipart');
|
||||
const aggregatedLimit = multipartConfig.limit ? parseBytesSize(multipartConfig.limit) : 100 * 1024 * 1024;
|
||||
// const aggregatedLimit = 200 * 1024 * 1024;
|
||||
let totalUploadedSize = 0;
|
||||
|
||||
// // Helper function to format bytes as human-readable text
|
||||
// function formatBytes(bytes: number): string {
|
||||
// if (bytes === 0) return '0 Bytes';
|
||||
// const k = 1024;
|
||||
// const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
// const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
// return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
// }
|
||||
// const enabledExtensions = await this.getEnabledExtensions();
|
||||
const multipart: Multipart = request.multipart;
|
||||
|
||||
// NULL GUARD: kein multipart-Body → sauberer Validierungsfehler statt Crash
|
||||
if (!multipart) {
|
||||
validation.throw('files', 'Please select at least one file.', 'required');
|
||||
}
|
||||
|
||||
// Configuration for limits
|
||||
const multipartConfig = getConfigFor('multipart');
|
||||
const aggregatedLimit = multipartConfig.limit ? parseBytesSize(multipartConfig.limit) : 100 * 1024 * 1024;
|
||||
|
||||
let totalUploadedSize = 0;
|
||||
let filesCount = 0; // Tracker: ensure at least one file is processed
|
||||
|
||||
multipart.onFile('files', { deferValidations: true }, async (part) => {
|
||||
// Attach an individual file size accumulator if needed
|
||||
filesCount++;
|
||||
let fileUploadedSize = 0;
|
||||
|
||||
// Simply accumulate the size in on('data') without performing the expensive check per chunk
|
||||
// Accumulate the size per chunk (cheap), defer the limit check to 'end'
|
||||
part.on('data', (chunk) => {
|
||||
// reporter(chunk);
|
||||
// Increase counters using the chunk length
|
||||
fileUploadedSize += chunk.length;
|
||||
});
|
||||
|
||||
// After the file is completely read, update the global counter and check aggregated limit
|
||||
// After the file is fully read, update the global counter and check the aggregated limit
|
||||
part.on('end', () => {
|
||||
totalUploadedSize += fileUploadedSize;
|
||||
part.file.size = fileUploadedSize;
|
||||
|
|
@ -465,6 +459,7 @@ export default class DatasetController {
|
|||
uploadedTmpFiles.push(part.file.tmpPath);
|
||||
}
|
||||
|
||||
// AGGREGATED LIMIT CHECK: abort immediately if too big
|
||||
if (totalUploadedSize > aggregatedLimit) {
|
||||
// Clean up all temporary files if aggregate limit is exceeded
|
||||
uploadedTmpFiles.forEach((tmpPath) => {
|
||||
|
|
@ -474,48 +469,44 @@ export default class DatasetController {
|
|||
console.error('Error cleaning up temporary file:', cleanupError);
|
||||
}
|
||||
});
|
||||
const error = new errors.E_VALIDATION_ERROR({
|
||||
'upload error': `Aggregated upload limit of ${formatBytes(aggregatedLimit)} exceeded. The total size of files being uploaded would exceed the limit.`,
|
||||
});
|
||||
request.multipart.abort(error);
|
||||
request.multipart.abort(
|
||||
validation.make('files', `Upload limit of ${formatBytes(aggregatedLimit)} exceeded.`, 'limit')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
part.on('error', (error) => {
|
||||
// fileUploadError = error;
|
||||
request.multipart.abort(error);
|
||||
});
|
||||
|
||||
// await pipeline(part, createWriteStream(filePath));
|
||||
// return { filePath };
|
||||
// Process file with error handling
|
||||
try {
|
||||
// Extract extension from the client file name, e.g. "Tethys 5 - Ampflwang_dataset.zip"
|
||||
const ext = path.extname(part.file.clientName).replace('.', '');
|
||||
// Attach the extracted extension to the file object for later use
|
||||
part.file.extname = ext;
|
||||
|
||||
// part.file.sortOrder = part.file.sortOrder;
|
||||
|
||||
const tmpPath = getTmpPath(multipartConfig);
|
||||
(part.file as any).tmpPath = tmpPath;
|
||||
|
||||
const writeStream = createWriteStream(tmpPath);
|
||||
await pipeline(part, writeStream);
|
||||
} catch (error) {
|
||||
request.multipart.abort(new errors.E_VALIDATION_ERROR({ 'upload error': error.message }));
|
||||
request.multipart.abort(validation.make('files', error.message, 'upload'));
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await multipart.process();
|
||||
// // Instead of letting an error abort the controller, check if any error occurred
|
||||
|
||||
// EMPTY FIELD CHECK: triggered if process finishes but onFile never ran
|
||||
if (filesCount === 0) {
|
||||
validation.throw('files', 'Please select at least one file.', 'required');
|
||||
}
|
||||
} catch (error) {
|
||||
// This is where you'd expect to catch any errors.
|
||||
session.flash('errors', error.messages);
|
||||
return response.redirect().back();
|
||||
// If it's already a validation error, let it bubble up unchanged
|
||||
if (error instanceof errors.E_VALIDATION_ERROR) throw error;
|
||||
|
||||
// Wrap any other stream/size errors
|
||||
validation.throw('files', errorMessage(error) || 'Upload failed.');
|
||||
}
|
||||
|
||||
// Proceed to transaction and createDatasetAndAssociations
|
||||
let trx: TransactionClientContract | null = null;
|
||||
try {
|
||||
await request.validateUsing(createDatasetValidator);
|
||||
|
|
@ -539,13 +530,11 @@ export default class DatasetController {
|
|||
await trx.rollback();
|
||||
}
|
||||
console.error('Failed to create dataset and related models:', error);
|
||||
// throw new ValidationException(true, { 'upload error': `failed to create dataset and related models. ${error}` });
|
||||
throw error;
|
||||
}
|
||||
|
||||
session.flash('message', 'Dataset has been created successfully');
|
||||
return response.redirect().toRoute('dataset.list');
|
||||
// return response.redirect().back();
|
||||
}
|
||||
private async createDatasetAndAssociations(
|
||||
user: User,
|
||||
|
|
@ -1106,7 +1095,7 @@ export default class DatasetController {
|
|||
}
|
||||
});
|
||||
const error = new errors.E_VALIDATION_ERROR({
|
||||
'upload error': `Aggregated upload limit of ${formatBytes(aggregatedLimit)} exceeded. The total size of files being uploaded would exceed the limit.`,
|
||||
'files': `Aggregated upload limit of ${formatBytes(aggregatedLimit)} exceeded. The total size of files being uploaded would exceed the limit.`,
|
||||
});
|
||||
request.multipart.abort(error);
|
||||
}
|
||||
|
|
@ -1132,7 +1121,7 @@ export default class DatasetController {
|
|||
try {
|
||||
await multipart.process();
|
||||
} catch (error) {
|
||||
session.flash('errors', error.messages);
|
||||
// session.flash('errors', error.messages);
|
||||
return response.redirect().back();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue