- added npm package dotenv-webpack for using env variables on clientside

- added API File Controller for downloading files e.g. /api/download/1022
- also create has codes by submitting new dataset
- added edit dataset functionalities for role submitter
- added the following route for role submitter: /dataset/:id/update', 'DatasetController.update'
- created extra UpdateDatasetValidator.ts for validating updated dataset
- npm updates
This commit is contained in:
Kaimbacher 2023-11-22 17:06:55 +01:00
parent a7142f694f
commit d8bdce1369
23 changed files with 2181 additions and 853 deletions

View File

@ -0,0 +1,54 @@
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import File from 'App/Models/File';
import { StatusCodes } from 'http-status-codes';
import * as fs from 'fs';
import * as path from 'path';
// node ace make:controller Author
export default class FileController {
// @Get("download/:id")
public async findOne({ response, params }: HttpContextContract) {
const id = params.id;
const file = await File.findOrFail(id);
// const file = await File.findOne({
// where: { id: id },
// });
if (file) {
const filePath = '/storage/app/public/' + file.pathName;
const ext = path.extname(filePath);
const fileName = file.label + ext;
try {
fs.accessSync(filePath, fs.constants.R_OK); //| fs.constants.W_OK);
// console.log("can read/write:", path);
response
.header('Cache-Control', 'no-cache private')
.header('Content-Description', 'File Transfer')
.header('Content-Type', file.mimeType)
.header('Content-Disposition', 'inline; filename=' + fileName)
.header('Content-Transfer-Encoding', 'binary')
.header('Access-Control-Allow-Origin', '*')
.header('Access-Control-Allow-Methods', 'GET,POST');
response.status(StatusCodes.OK).download(filePath);
} catch (err) {
// console.log("no access:", path);
response.status(StatusCodes.NOT_FOUND).send({
message: `File with id ${id} doesn't exist on file server`,
});
}
// res.status(StatusCodes.OK).sendFile(filePath, (err) => {
// // res.setHeader("Content-Type", "application/json");
// // res.removeHeader("Content-Disposition");
// res.status(StatusCodes.NOT_FOUND).send({
// message: `File with id ${id} doesn't exist on file server`,
// });
// });
} else {
response.status(StatusCodes.NOT_FOUND).send({
message: `Cannot find File with id=${id}.`,
});
}
}
}

View File

@ -15,6 +15,7 @@ import Database from '@ioc:Adonis/Lucid/Database';
import { TransactionClientContract } from '@ioc:Adonis/Lucid/Database'; import { TransactionClientContract } from '@ioc:Adonis/Lucid/Database';
import Subject from 'App/Models/Subject'; import Subject from 'App/Models/Subject';
import CreateDatasetValidator from 'App/Validators/CreateDatasetValidator'; import CreateDatasetValidator from 'App/Validators/CreateDatasetValidator';
import UpdateDatasetValidator from 'App/Validators/UpdateDatasetValidator';
import { import {
TitleTypes, TitleTypes,
DescriptionTypes, DescriptionTypes,
@ -33,8 +34,6 @@ import ClamScan from 'clamscan';
import { ValidationException } from '@ioc:Adonis/Core/Validator'; import { ValidationException } from '@ioc:Adonis/Core/Validator';
import Drive from '@ioc:Adonis/Core/Drive'; import Drive from '@ioc:Adonis/Core/Drive';
import { Exception } from '@adonisjs/core/build/standalone'; import { Exception } from '@adonisjs/core/build/standalone';
// import XmlModel from 'App/Library/XmlModel';
// import { XMLBuilder } from 'xmlbuilder2/lib/interfaces';
export default class DatasetController { export default class DatasetController {
public async index({ auth, request, inertia }: HttpContextContract) { public async index({ auth, request, inertia }: HttpContextContract) {
@ -355,7 +354,7 @@ export default class DatasetController {
//store licenses: //store licenses:
const licenses: number[] = request.input('licenses', []); const licenses: number[] = request.input('licenses', []);
dataset.useTransaction(trx).related('licenses').sync(licenses); await dataset.useTransaction(trx).related('licenses').sync(licenses);
// save authors and contributors // save authors and contributors
await this.savePersons(dataset, request.input('authors', []), 'author', trx); await this.savePersons(dataset, request.input('authors', []), 'author', trx);
@ -456,7 +455,7 @@ export default class DatasetController {
newFile.visibleInOai = true; newFile.visibleInOai = true;
// let path = coverImage.filePath; // let path = coverImage.filePath;
await dataset.useTransaction(trx).related('files').save(newFile); await dataset.useTransaction(trx).related('files').save(newFile);
// await newFile.createHashValues(); await newFile.createHashValues(trx);
} }
} }
@ -682,29 +681,39 @@ export default class DatasetController {
// throw new GeneralException(trans('exceptions.publish.release.update_error')); // throw new GeneralException(trans('exceptions.publish.release.update_error'));
} }
public async edit({ params, inertia }) { public async edit({ request, inertia, response }) {
const datasetQuery = Dataset.query().where('id', params.id); const id = request.param('id');
datasetQuery.preload('titles').preload('descriptions').preload('coverage'); const datasetQuery = Dataset.query().where('id', id);
const dataset = await datasetQuery.firstOrFail(); datasetQuery
.preload('titles', (query) => query.orderBy('id', 'asc'))
.preload('descriptions', (query) => query.orderBy('id', 'asc'))
.preload('coverage')
.preload('licenses')
.preload('authors')
.preload('contributors')
.preload('subjects')
.preload('references')
.preload('files');
// await dataset.loadMany([ const dataset = await datasetQuery.firstOrFail();
// 'licenses', const validStates = ['inprogress', 'rejected_editor'];
// 'authors', if (!validStates.includes(dataset.server_state)) {
// 'contributors', // session.flash('errors', 'Invalid server state!');
// 'titles', return response
// 'abstracts', .flash(
// 'files', 'warning',
// 'coverage', `Invalid server state. Dataset with id ${id} cannot be edited. Datset has server state ${dataset.server_state}.`,
// 'subjects', )
// 'references', .redirect()
// ]); .toRoute('dataset.list');
}
const titleTypes = Object.entries(TitleTypes) const titleTypes = Object.entries(TitleTypes)
// .filter(([value]) => value !== 'Main') .filter(([value]) => value !== 'Main')
.map(([key, value]) => ({ value: key, label: value })); .map(([key, value]) => ({ value: key, label: value }));
const descriptionTypes = Object.entries(DescriptionTypes) const descriptionTypes = Object.entries(DescriptionTypes)
// .filter(([value]) => value !== 'Abstract') .filter(([value]) => value !== 'Abstract')
.map(([key, value]) => ({ value: key, label: value })); .map(([key, value]) => ({ value: key, label: value }));
const languages = await Language.query().where('active', true).pluck('part1', 'part1'); const languages = await Language.query().where('active', true).pluck('part1', 'part1');
@ -724,33 +733,11 @@ export default class DatasetController {
const currentYear = currentDate.getFullYear(); const currentYear = currentDate.getFullYear();
const years = Array.from({ length: currentYear - 1990 + 1 }, (_, index) => 1990 + index); const years = Array.from({ length: currentYear - 1990 + 1 }, (_, index) => 1990 + index);
// const licenses = await License.query() const licenses = await License.query().select('id', 'name_long').where('active', 'true').pluck('name_long', 'id');
// .select('id', 'name_long', 'link_licence') // const userHasRoles = user.roles;
// .orderBy('sort_order') // const datasetHasLicenses = await dataset.related('licenses').query().pluck('id');
// .fetch();
const licenses = await License.query().select('id', 'name_long', 'link_licence').orderBy('sort_order');
// const checkeds = dataset.licenses.first().id; // const checkeds = dataset.licenses.first().id;
const keywordTypes = {
uncontrolled: 'uncontrolled',
swd: 'swd',
};
const referenceTypes = ['DOI', 'Handle', 'ISBN', 'ISSN', 'URL', 'URN'];
const relationTypes = [
'IsSupplementTo',
'IsSupplementedBy',
'IsContinuedBy',
'Continues',
'IsNewVersionOf',
'IsPartOf',
'HasPart',
'Compiles',
'IsVariantFormOf',
];
const doctypes = { const doctypes = {
analysisdata: { label: 'Analysis', value: 'analysisdata' }, analysisdata: { label: 'Analysis', value: 'analysisdata' },
measurementdata: { label: 'Measurements', value: 'measurementdata' }, measurementdata: { label: 'Measurements', value: 'measurementdata' },
@ -771,16 +758,164 @@ export default class DatasetController {
// messages, // messages,
projects, projects,
licenses, licenses,
// datasetHasLicenses: Object.keys(datasetHasLicenses).map((key) => datasetHasLicenses[key]), //convert object to array with license ids
// checkeds, // checkeds,
years, years,
// languages, // languages,
keywordTypes, subjectTypes: SubjectTypes,
referenceTypes, referenceIdentifierTypes: Object.entries(ReferenceIdentifierTypes).map(([key, value]) => ({ value: key, label: value })),
relationTypes, relationTypes: Object.entries(RelationTypes).map(([key, value]) => ({ value: key, label: value })),
doctypes, doctypes,
}); });
} }
public async update({ request, response, session }: HttpContextContract) {
try {
// await request.validate({ schema: newDatasetSchema, messages: this.messages });
await request.validate(UpdateDatasetValidator);
} catch (error) {
// - Handle errors
// return response.badRequest(error.messages);
throw error;
// return response.badRequest(error.messages);
}
const id = request.param('id');
let trx: TransactionClientContract | null = null;
try {
trx = await Database.transaction();
// const user = (await User.find(auth.user?.id)) as User;
// await this.createDatasetAndAssociations(user, request, trx);
const dataset = await Dataset.findOrFail(id);
// save the licenses
const licenses: number[] = request.input('licenses', []);
// await dataset.useTransaction(trx).related('licenses').sync(licenses);
await dataset.useTransaction(trx).related('licenses').sync(licenses);
// save authors and contributors
await dataset.useTransaction(trx).related('authors').sync([]);
await dataset.useTransaction(trx).related('contributors').sync([]);
await this.savePersons(dataset, request.input('authors', []), 'author', trx);
await this.savePersons(dataset, request.input('contributors', []), 'contributor', trx);
//save the titles:
const titles = request.input('titles', []);
// const savedTitles:Array<Title> = [];
for (const titleData of titles) {
if (titleData.id) {
const title = await Title.findOrFail(titleData.id);
title.value = titleData.value;
title.language = titleData.language;
title.type = titleData.type;
if (title.$isDirty) {
await title.useTransaction(trx).save();
// await dataset.useTransaction(trx).related('titles').save(title);
// savedTitles.push(title);
}
} else {
const title = new Title();
title.fill(titleData);
// savedTitles.push(title);
await dataset.useTransaction(trx).related('titles').save(title);
}
}
// save the abstracts
const descriptions = request.input('descriptions', []);
// const savedTitles:Array<Title> = [];
for (const descriptionData of descriptions) {
if (descriptionData.id) {
const description = await Description.findOrFail(descriptionData.id);
description.value = descriptionData.value;
description.language = descriptionData.language;
description.type = descriptionData.type;
if (description.$isDirty) {
await description.useTransaction(trx).save();
// await dataset.useTransaction(trx).related('titles').save(title);
// savedTitles.push(title);
}
} else {
const description = new Description();
description.fill(descriptionData);
// savedTitles.push(title);
await dataset.useTransaction(trx).related('descriptions').save(description);
}
}
// Save already existing files
const files = request.input('fileInputs', []);
for (const fileData of files) {
if (fileData.id) {
const file = await File.findOrFail(fileData.id);
file.label = fileData.label;
file.sortOrder = fileData.sort_order;
if (file.$isDirty) {
await file.useTransaction(trx).save();
}
}
}
// handle new uploaded files:
const uploadedFiles = request.files('files');
if (Array.isArray(uploadedFiles) && uploadedFiles.length > 0) {
// let index = 1;
// for (const key in files) {
// const formFile = files[key]
// for (const fileData of files) {
for (const [index, fileData] of uploadedFiles.entries()) {
// const uploads = request.file('uploads');
// const fileIndex = formFile.file;
// const file = uploads[fileIndex];
const fileName = `file-${cuid()}.${fileData.extname}`;
const mimeType = fileData.headers['content-type'] || 'application/octet-stream'; // Fallback to a default MIME type
const datasetFolder = `files/${dataset.id}`;
await fileData.moveToDisk(
datasetFolder,
{
name: fileName,
overwrite: true, // overwrite in case of conflict
},
'local',
);
// save file metadata into db
const newFile = new File();
newFile.pathName = `${datasetFolder}/${fileName}`;
newFile.fileSize = fileData.size;
newFile.mimeType = mimeType;
newFile.label = fileData.clientName;
newFile.sortOrder = index;
newFile.visibleInFrontdoor = true;
newFile.visibleInOai = true;
// let path = coverImage.filePath;
await dataset.useTransaction(trx).related('files').save(newFile);
await newFile.createHashValues();
}
}
const input = request.only(['project_id', 'embargo_date', 'language', 'type', 'creating_corporation']);
// dataset.type = request.input('type');
dataset.merge(input);
// let test: boolean = dataset.$isDirty;
await dataset.useTransaction(trx).save();
await trx.commit();
console.log('Dataset and related models created successfully');
} catch (error) {
if (trx !== null) {
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('user.index');
return response.redirect().back();
}
public async delete({ request, inertia, response, session }) { public async delete({ request, inertia, response, session }) {
const id = request.param('id'); const id = request.param('id');
try { try {

View File

@ -9,6 +9,11 @@ export default class Description extends BaseModel {
public static timestamps = false; public static timestamps = false;
public static fillable: string[] = ['value', 'type', 'language']; public static fillable: string[] = ['value', 'type', 'language'];
@column({
isPrimary: true,
})
public id: number;
@column({}) @column({})
public document_id: number; public document_id: number;

View File

@ -1,19 +1,18 @@
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { import { column, hasMany, HasMany, belongsTo, BelongsTo, SnakeCaseNamingStrategy, computed } from '@ioc:Adonis/Lucid/Orm';
column,
hasMany,
HasMany,
belongsTo,
BelongsTo,
// manyToMany,
// ManyToMany,
SnakeCaseNamingStrategy,
} from '@ioc:Adonis/Lucid/Orm';
import HashValue from './HashValue'; import HashValue from './HashValue';
import Dataset from './Dataset'; import Dataset from './Dataset';
import BaseModel from './BaseModel'; import BaseModel from './BaseModel';
// import { Buffer } from 'buffer';
import * as fs from 'fs';
import crypto from 'crypto';
import { TransactionClientContract } from '@ioc:Adonis/Lucid/Database';
export default class File extends BaseModel { export default class File extends BaseModel {
// private readonly _data: Uint8Array;
// private readonly _type: string;
// private readonly _size: number;
public static namingStrategy = new SnakeCaseNamingStrategy(); public static namingStrategy = new SnakeCaseNamingStrategy();
public static primaryKey = 'id'; public static primaryKey = 'id';
public static table = 'document_files'; public static table = 'document_files';
@ -73,4 +72,93 @@ export default class File extends BaseModel {
foreignKey: 'file_id', foreignKey: 'file_id',
}) })
public hashvalues: HasMany<typeof HashValue>; public hashvalues: HasMany<typeof HashValue>;
@computed({
serializeAs: 'filePath',
})
public get filePath() {
return `/storage/app/public/${this.pathName}`;
// const mainTitle = this.titles?.find((title) => title.type === 'Main');
// return mainTitle ? mainTitle.value : null;
}
@computed({
serializeAs: 'size',
})
public get size() {
return this.fileSize;
}
@computed({
serializeAs: 'type',
})
public get type() {
return this.mimeType;
}
@computed({
serializeAs: 'name',
})
get name(): string {
return this.label;
}
@computed({
serializeAs: 'lastModified',
})
get lastModified(): number {
return this.updatedAt.toUnixInteger(); //.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
}
readonly webkitRelativePath: string = '';
@computed({
serializeAs: 'fileData',
})
public get fileData(): string {
// return this.fileData;
// const fileData = fs.readFileSync(path.resolve(__dirname, this.filePath));
// const fileData = fs.readFileSync(this.filePath);
const fileContent: Buffer = fs.readFileSync(this.filePath);
// Create a Blob from the file content
// const blob = new Blob([fileContent], { type: this.type }); // Adjust
// let fileSrc = URL.createObjectURL(blob);
// return fileSrc;
// return Buffer.from(fileContent);
// get the buffer from somewhere
// const buff = fs.readFileSync('./test.bin');
// create a JSON string that contains the data in the property "blob"
const json = JSON.stringify({ blob: fileContent.toString('base64') });
return json;
}
public async createHashValues(trx?: TransactionClientContract) {
const hashtypes: string[] = ['md5', 'sha512'];
for (const type of hashtypes) {
const hash = new HashValue();
hash.type = type;
const hashString = await this.checksumFile(this.filePath, type); // Assuming getRealHash is a method in the same model
hash.value = hashString;
// https://github.com/adonisjs/core/discussions/1872#discussioncomment-132289
const file: File = this;
if (trx) {
await file.useTransaction(trx).related('hashvalues').save(hash); // Save the hash value to the database
} else {
await file.related('hashvalues').save(hash); // Save the hash value to the database
}
}
}
private async checksumFile(path, hashName = 'md5'): Promise<string> {
return new Promise((resolve, reject) => {
const hash = crypto.createHash(hashName);
const stream = fs.createReadStream(path);
stream.on('error', (err) => reject(err));
stream.on('data', (chunk) => hash.update(chunk));
stream.on('end', () => resolve(hash.digest('hex')));
});
}
} }

View File

@ -3,7 +3,7 @@ import File from './File';
export default class HashValue extends BaseModel { export default class HashValue extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy(); public static namingStrategy = new SnakeCaseNamingStrategy();
public static primaryKey = 'file_id, type'; // public static primaryKey = 'file_id,type';
public static table = 'file_hashvalues'; public static table = 'file_hashvalues';
// static get primaryKey () { // static get primaryKey () {
@ -20,10 +20,10 @@ export default class HashValue extends BaseModel {
// public id: number; // public id: number;
// Foreign key is still on the same model // Foreign key is still on the same model
@column({}) @column({ isPrimary: true })
public file_id: number; public file_id: number;
@column({}) @column({ isPrimary: true })
public type: string; public type: string;
@column() @column()

View File

@ -10,6 +10,11 @@ export default class Title extends BaseModel {
public static timestamps = false; public static timestamps = false;
public static fillable: string[] = ['value', 'type', 'language']; public static fillable: string[] = ['value', 'type', 'language'];
@column({
isPrimary: true,
})
public id: number;
@column({}) @column({})
public document_id: number; public document_id: number;

View File

@ -136,7 +136,7 @@ export default class CreateDatasetValidator {
'unique': '{{ field }} must be unique, and this value is already taken', 'unique': '{{ field }} must be unique, and this value is already taken',
// 'confirmed': '{{ field }} is not correct', // 'confirmed': '{{ field }} is not correct',
'licenses.minLength': 'at least {{ options.minLength }} permission must be defined', 'licenses.minLength': 'at least {{ options.minLength }} permission must be defined',
'licenses.*.number': 'Define roles as valid numbers', 'licenses.*.number': 'Define licences as valid numbers',
'rights.equalTo': 'you must agree to continue', 'rights.equalTo': 'you must agree to continue',
'titles.0.value.minLength': 'Main Title must be at least {{ options.minLength }} characters long', 'titles.0.value.minLength': 'Main Title must be at least {{ options.minLength }} characters long',

View File

@ -0,0 +1,179 @@
import { schema, CustomMessages, rules } from '@ioc:Adonis/Core/Validator';
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import dayjs from 'dayjs';
import { TitleTypes, DescriptionTypes, RelationTypes, ReferenceIdentifierTypes, ContributorTypes } from 'Contracts/enums';
export default class UpdateDatasetValidator {
constructor(protected ctx: HttpContextContract) {}
/*
* Define schema to validate the "shape", "type", "formatting" and "integrity" of data.
*
* For example:
* 1. The username must be of data type string. But then also, it should
* not contain special characters or numbers.
* ```
* schema.string({}, [ rules.alpha() ])
* ```
*
* 2. The email must be of data type string, formatted as a valid
* email. But also, not used by any other user.
* ```
* schema.string({}, [
* rules.email(),
* rules.unique({ table: 'users', column: 'email' }),
* ])
* ```
*/
public schema = schema.create({
// first step
language: schema.string({ trim: true }, [
rules.regex(/^[a-zA-Z0-9-_]+$/), //Must be alphanumeric with hyphens or underscores
]),
licenses: schema.array([rules.minLength(1)]).members(schema.number()), // define at least one license for the new dataset
rights: schema.string([rules.equalTo('true')]),
// second step
type: schema.string({ trim: true }, [rules.minLength(3), rules.maxLength(255)]),
creating_corporation: schema.string({ trim: true }, [rules.minLength(3), rules.maxLength(255)]),
titles: schema.array([rules.minLength(1)]).members(
schema.object().members({
value: schema.string({ trim: true }, [rules.minLength(3), rules.maxLength(255)]),
type: schema.enum(Object.values(TitleTypes)),
language: schema.string({ trim: true }, [
rules.minLength(2),
rules.maxLength(255),
rules.translatedLanguage('/language', 'type'),
]),
}),
),
descriptions: schema.array([rules.minLength(1)]).members(
schema.object().members({
value: schema.string({ trim: true }, [rules.minLength(3), rules.maxLength(255)]),
type: schema.enum(Object.values(DescriptionTypes)),
language: schema.string({ trim: true }, [
rules.minLength(2),
rules.maxLength(255),
rules.translatedLanguage('/language', 'type'),
]),
}),
),
authors: schema.array([rules.minLength(1)]).members(schema.object().members({ email: schema.string({ trim: true }) })),
contributors: schema.array.optional().members(
schema.object().members({
email: schema.string({ trim: true }),
pivot_contributor_type: schema.enum(Object.keys(ContributorTypes)),
}),
),
// third step
project_id: schema.number.optional(),
embargo_date: schema.date.optional({ format: 'yyyy-MM-dd' }, [rules.after(10, 'days')]),
coverage: schema.object().members({
x_min: schema.number(),
x_max: schema.number(),
y_min: schema.number(),
y_max: schema.number(),
elevation_absolut: schema.number.optional(),
elevation_min: schema.number.optional([rules.requiredIfExists('elevation_max')]),
elevation_max: schema.number.optional([rules.requiredIfExists('elevation_min')]),
depth_absolut: schema.number.optional(),
depth_min: schema.number.optional([rules.requiredIfExists('depth_max')]),
depth_max: schema.number.optional([rules.requiredIfExists('depth_min')]),
}),
references: schema.array.optional([rules.uniqueArray('value')]).members(
schema.object().members({
value: schema.string({ trim: true }, [rules.minLength(3), rules.maxLength(255)]),
type: schema.enum(Object.values(ReferenceIdentifierTypes)),
relation: schema.enum(Object.values(RelationTypes)),
label: schema.string({ trim: true }, [rules.minLength(2), rules.maxLength(255)]),
}),
),
subjects: schema.array([rules.minLength(3), rules.uniqueArray('value')]).members(
schema.object().members({
value: schema.string({ trim: true }, [
rules.minLength(3),
rules.maxLength(255),
// rules.unique({ table: 'dataset_subjects', column: 'value' }),
]),
// type: schema.enum(Object.values(TitleTypes)),
language: schema.string({ trim: true }, [rules.minLength(2), rules.maxLength(255)]),
}),
),
// file: schema.file({
// size: '100mb',
// extnames: ['jpg', 'gif', 'png'],
// }),
files: schema.array.optional().members(
schema.file({
size: '100mb',
extnames: ['jpg', 'gif', 'png', 'tif', 'pdf'],
}),
)
// upload: schema.object().members({
// label: schema.string({ trim: true }, [rules.maxLength(255)]),
// // label: schema.string({ trim: true }, [
// // // rules.minLength(3),
// // // rules.maxLength(255),
// // ]),
// }),
});
/**
* Custom messages for validation failures. You can make use of dot notation `(.)`
* for targeting nested fields and array expressions `(*)` for targeting all
* children of an array. For example:
*
* {
* 'profile.username.required': 'Username is required',
* 'scores.*.number': 'Define scores as valid numbers'
* }
*
*/
public messages: CustomMessages = {
'minLength': '{{ field }} must be at least {{ options.minLength }} characters long',
'maxLength': '{{ field }} must be less then {{ options.maxLength }} characters long',
'required': '{{ field }} is required',
'unique': '{{ field }} must be unique, and this value is already taken',
// 'confirmed': '{{ field }} is not correct',
'licenses.minLength': 'at least {{ options.minLength }} permission must be defined',
'licenses.*.number': 'Define licences as valid numbers',
'rights.equalTo': 'you must agree to continue',
'titles.0.value.minLength': 'Main Title must be at least {{ options.minLength }} characters long',
'titles.0.value.required': 'Main Title is required',
'titles.*.value.required': 'Additional title is required, if defined',
'titles.*.type.required': 'Additional title type is required',
'titles.*.language.required': 'Additional title language is required',
'titles.*.language.translatedLanguage': 'The language of the translated title must be different from the language of the dataset',
'descriptions.0.value.minLength': 'Main Abstract must be at least {{ options.minLength }} characters long',
'descriptions.0.value.required': 'Main Abstract is required',
'descriptions.*.value.required': 'Additional description is required, if defined',
'descriptions.*.type.required': 'Additional description type is required',
'descriptions.*.language.required': 'Additional description language is required',
'descriptions.*.language.translatedLanguage':
'The language of the translated description must be different from the language of the dataset',
'authors.minLength': 'at least {{ options.minLength }} author must be defined',
'contributors.*.pivot_contributor_type.required': 'contributor type is required, if defined',
'after': `{{ field }} must be older than ${dayjs().add(10, 'day')}`,
'subjects.minLength': 'at least {{ options.minLength }} keywords must be defined',
'subjects.uniqueArray': 'The {{ options.array }} array must have unique values based on the {{ options.field }} attribute.',
'subjects.*.value.required': 'keyword value is required',
'subjects.*.value.minLength': 'keyword value must be at least {{ options.minLength }} characters long',
'subjects.*.type.required': 'keyword type is required',
'subjects.*.language.required': 'language of keyword is required',
'references.*.value.required': 'Additional reference value is required, if defined',
'references.*.type.required': 'Additional reference identifier type is required',
'references.*.relation.required': 'Additional reference relation type is required',
'references.*.label.required': 'Additional reference label is required',
'files.minLength': 'At least {{ options.minLength }} file upload is required.',
'files.*.size': 'file size is to big',
'files.extnames': 'file extension is not supported',
};
}

1279
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -43,6 +43,7 @@
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"babel-preset-typescript-vue3": "^2.0.17", "babel-preset-typescript-vue3": "^2.0.17",
"chart.js": "^4.2.0", "chart.js": "^4.2.0",
"dotenv-webpack": "^8.0.1",
"eslint": "^8.32.0", "eslint": "^8.32.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-adonis": "^2.1.1", "eslint-plugin-adonis": "^2.1.1",

View File

@ -1,34 +1,21 @@
<template> <template>
<section <section aria-label="File Upload Modal"
aria-label="File Upload Modal"
class="relative h-full flex flex-col bg-white dark:bg-slate-900/70 shadow-xl rounded-md" class="relative h-full flex flex-col bg-white dark:bg-slate-900/70 shadow-xl rounded-md"
v-on:dragenter="dragEnterHandler" v-on:dragenter="dragEnterHandler" v-on:dragleave="dragLeaveHandler" v-on:dragover="dragOverHandler"
v-on:dragleave="dragLeaveHandler" v-on:drop="dropHandler">
v-on:dragover="dragOverHandler"
v-on:drop="dropHandler"
>
<!-- ondrop="dropHandler(event);" <!-- ondrop="dropHandler(event);"
ondragover="dragOverHandler(event);" ondragover="dragOverHandler(event);"
ondragleave="dragLeaveHandler(event);" ondragleave="dragLeaveHandler(event);"
ondragenter="dragEnterHandler(event);" --> ondragenter="dragEnterHandler(event);" -->
<!-- overlay --> <!-- overlay -->
<div <div id="overlay" ref="overlay"
id="overlay" class="w-full h-full absolute top-0 left-0 pointer-events-none z-50 flex flex-col items-center justify-center rounded-md">
ref="overlay"
class="w-full h-full absolute top-0 left-0 pointer-events-none z-50 flex flex-col items-center justify-center rounded-md"
>
<i> <i>
<svg <svg class="fill-current w-12 h-12 mb-3 text-blue-700" xmlns="http://www.w3.org/2000/svg" width="24"
class="fill-current w-12 h-12 mb-3 text-blue-700" height="24" viewBox="0 0 24 24">
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path <path
d="M19.479 10.092c-.212-3.951-3.473-7.092-7.479-7.092-4.005 0-7.267 3.141-7.479 7.092-2.57.463-4.521 2.706-4.521 5.408 0 3.037 2.463 5.5 5.5 5.5h13c3.037 0 5.5-2.463 5.5-5.5 0-2.702-1.951-4.945-4.521-5.408zm-7.479-1.092l4 4h-3v4h-2v-4h-3l4-4z" d="M19.479 10.092c-.212-3.951-3.473-7.092-7.479-7.092-4.005 0-7.267 3.141-7.479 7.092-2.57.463-4.521 2.706-4.521 5.408 0 3.037 2.463 5.5 5.5 5.5h13c3.037 0 5.5-2.463 5.5-5.5 0-2.702-1.951-4.945-4.521-5.408zm-7.479-1.092l4 4h-3v4h-2v-4h-3l4-4z" />
/>
</svg> </svg>
</i> </i>
<p class="text-lg text-blue-700">Drop files to upload</p> <p class="text-lg text-blue-700">Drop files to upload</p>
@ -46,25 +33,14 @@
</button> </button>
</header> --> </header> -->
<header class="flex items-center justify-center w-full"> <header class="flex items-center justify-center w-full">
<label <label for="dropzone-file"
for="dropzone-file" class="flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600">
class="flex flex-col items-center justify-center w-full h-64 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600"
>
<div class="flex flex-col items-center justify-center pt-5 pb-6"> <div class="flex flex-col items-center justify-center pt-5 pb-6">
<svg <svg aria-hidden="true" class="w-10 h-10 mb-3 text-gray-400" fill="none" stroke="currentColor"
aria-hidden="true" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
class="w-10 h-10 mb-3 text-gray-400" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
fill="none" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12">
stroke="currentColor" </path>
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
></path>
</svg> </svg>
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400"> <p class="mb-2 text-sm text-gray-500 dark:text-gray-400">
<span class="font-semibold">Click to upload</span> or drag and drop <span class="font-semibold">Click to upload</span> or drag and drop
@ -78,7 +54,8 @@
<h1 class="pt-8 pb-3 font-semibold sm:text-lg text-gray-900">To Upload</h1> <h1 class="pt-8 pb-3 font-semibold sm:text-lg text-gray-900">To Upload</h1>
<!-- <ul id="gallery" class="flex flex-1 flex-wrap -m-1"> --> <!-- <ul id="gallery" class="flex flex-1 flex-wrap -m-1"> -->
<draggable id="gallery" tag="ul" class="flex flex-1 flex-wrap -m-1" v-model="files" item-key="sorting">
<draggable id="galleryxy" tag="ul" class="flex flex-1 flex-wrap -m-1" v-model="items" item-key="sort_order">
<!-- <li <!-- <li
v-if="files.length == 0" v-if="files.length == 0"
id="empty" id="empty"
@ -108,17 +85,12 @@
<span class="text-small text-gray-500">No files selected</span> <span class="text-small text-gray-500">No files selected</span>
</li> --> </li> -->
<template #item="{ index, element }"> <template #item="{ index, element }">
<li class="block p-1 w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/6 xl:w-1/8 h-24"> <li class="block p-1 w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/6 xl:w-1/8 h-24" :key="index">
<article <article v-if="element.type.match('image.*')" tabindex="0"
v-if="element.type.match('image.*')" class="bg-gray-50 group hasImage w-full h-full rounded-md cursor-pointer relative text-transparent hover:text-white shadow-sm">
tabindex="0" <!-- :src="element.fileSrc" :src="generateURL(element)" -->
class="bg-gray-50 group hasImage w-full h-full rounded-md cursor-pointer relative text-transparent hover:text-white shadow-sm" <img :alt="element.name" :src="element.fileSrc"
> class="img-preview w-full h-full sticky object-cover rounded-md bg-fixed opacity-75" />
<img
:alt="element.name"
:src="generateURL(element)"
class="img-preview w-full h-full sticky object-cover rounded-md bg-fixed opacity-75"
/>
<!-- <section <!-- <section
class="hasError text-red-500 shadow-sm font-semibold flex flex-row rounded-md text-xs break-words w-full h-full z-21 absolute top-0 py-2 px-3" class="hasError text-red-500 shadow-sm font-semibold flex flex-row rounded-md text-xs break-words w-full h-full z-21 absolute top-0 py-2 px-3"
> >
@ -132,15 +104,14 @@
<DeleteIcon></DeleteIcon> <DeleteIcon></DeleteIcon>
</button> </button>
</section> --> </section> -->
<section class="flex flex-col rounded-md text-xs break-words w-full h-full z-20 absolute top-0 py-2 px-3"> <section
class="flex flex-col rounded-md text-xs break-words w-full h-full z-20 absolute top-0 py-2 px-3">
<h1 class="flex-1">{{ element.name }}</h1> <h1 class="flex-1">{{ element.name }}</h1>
<div class="flex"> <div class="flex">
<p class="p-1 size text-xs">{{ getFileSize(element) }}</p> <p class="p-1 size text-xs">{{ getFileSize(element) }}</p>
<p class="p-1 size text-xs text-gray-700">{{ index }}</p> <p class="p-1 size text-xs text-gray-700">{{ element.sort_order }}</p>
<button <button class="delete ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md"
class="delete ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md" @click="removeFile(index)">
@click="removeFile(index)"
>
<DeleteIcon></DeleteIcon> <DeleteIcon></DeleteIcon>
</button> </button>
</div> </div>
@ -150,16 +121,17 @@
</div> --> </div> -->
</article> </article>
<!-- :class="errors && errors[`files.${index}`] ? 'bg-red-400' : 'bg-gray-100'" --> <!-- :class="errors && errors[`files.${index}`] ? 'bg-red-400' : 'bg-gray-100'" -->
<article v-else tabindex="0" class="bg-gray-100 group w-full h-full rounded-md cursor-pointer relative shadow-sm"> <article v-else tabindex="0"
<section class="flex flex-col rounded-md text-xs break-words w-full h-full z-20 absolute top-0 py-2 px-3"> class="bg-gray-100 group w-full h-full rounded-md cursor-pointer relative shadow-sm">
<section
class="flex flex-col rounded-md text-xs break-words w-full h-full z-20 absolute top-0 py-2 px-3">
<h1 class="flex-1 text-gray-700 group-hover:text-blue-800">{{ element.name }}</h1> <h1 class="flex-1 text-gray-700 group-hover:text-blue-800">{{ element.name }}</h1>
<div class="flex"> <div class="flex">
<p class="p-1 size text-xs text-gray-700">{{ getFileSize(element) }}</p> <p class="p-1 size text-xs text-gray-700">{{ getFileSize(element) }}</p>
<p class="p-1 size text-xs text-gray-700">{{ index }}</p> <p class="p-1 size text-xs text-gray-700">{{ element.sort_order }}</p>
<button <button
class="delete ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md text-gray-800" class="delete ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md text-gray-800"
@click="removeFile(index)" @click="removeFile(index)">
>
<DeleteIcon></DeleteIcon> <DeleteIcon></DeleteIcon>
</button> </button>
</div> </div>
@ -191,11 +163,8 @@
<!-- sticky footer --> <!-- sticky footer -->
<footer class="flex justify-end px-8 pb-8 pt-4"> <footer class="flex justify-end px-8 pb-8 pt-4">
<button <button id="cancel" class="ml-3 rounded-sm px-3 py-1 hover:bg-gray-300 focus:shadow-outline focus:outline-none"
id="cancel" @click="clearAllFiles">
class="ml-3 rounded-sm px-3 py-1 hover:bg-gray-300 focus:shadow-outline focus:outline-none"
@click="clearAllFiles"
>
Clear Clear
</button> </button>
</footer> </footer>
@ -203,13 +172,20 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue, Prop, Ref } from 'vue-facing-decorator'; import { Component, Vue, Prop, Ref, Watch } from 'vue-facing-decorator';
// import BaseButton from './BaseButton.vue';
import { usePage } from '@inertiajs/vue3'; import { usePage } from '@inertiajs/vue3';
import DeleteIcon from '@/Components/Icons/Delete.vue'; import DeleteIcon from '@/Components/Icons/Delete.vue';
// import { Page, PageProps, Errors, ErrorBag } from '@inertiajs/inertia'; // import { Page, PageProps, Errors, ErrorBag } from '@inertiajs/inertia';
import Draggable from 'vuedraggable'; import Draggable from 'vuedraggable';
import { TestFile } from '@/Dataset'; import { Buffer } from 'buffer';
import { TethysFile } from '@/Dataset';
// lastModified: 1691759507591
// lastModifiedDate: Fri Aug 11 2023 15:11:47 GMT+0200 (Mitteleuropäische Sommerzeit)
// name: 'freieIP.png'
// size: 112237
// type: 'image/png'
// webkitRelativePath: ''
interface IDictionary { interface IDictionary {
[index: string]: Array<string>; [index: string]: Array<string>;
@ -239,7 +215,7 @@ interface InteriaPage {
Draggable, Draggable,
}, },
}) })
export default class FileUploadComponent extends Vue { class FileUploadComponent extends Vue {
/** /**
* Connect map id. * Connect map id.
*/ */
@ -255,22 +231,45 @@ export default class FileUploadComponent extends Vue {
// @Prop() files: Array<TestFile>; // @Prop() files: Array<TestFile>;
@Prop({ @Prop({
type: Array<TestFile>, type: Array<File>,
default: [], default: [],
}) })
modelValue: Array<TestFile>; files: Array<TethysFile | File>;
// mdiTrashCan = mdiTrashCan;
get files() { get items(): Array<TethysFile | File> {
return this.modelValue; return this.files;
} }
set files(value: Array<TestFile>) { set items(values: Array<TethysFile | File>) {
// this.modelValue = value; // this.modelValue = value;
this.modelValue.length = 0; this.files.length = 0;
this.modelValue.push(...value); this.files.push(...values);
// values.forEach((item, index) => {
// item.sort_order = index + 1; // Assuming sort_order starts from 1
// this.files.push(item);
// });
} }
dragEnterHandler(e) { @Watch("files", {
deep: true
})
public propertyWatcher(newItems: Array<TethysFile>) {
// Update sort_order based on the new index when the list is changed
newItems.forEach((item, index) => {
item.sort_order = index + 1; // Assuming sort_order starts from 1
});
}
public created() {
for (const file of this.files) {
if (!(file instanceof File)) {
// console.log(`${file.name} path is ${file.filePath} here.`);
this.generateURL(file);
// console.log(`${file.fileSrc} path.`);
}
}
}
public dragEnterHandler(e) {
e.preventDefault(); e.preventDefault();
if (!this._hasFiles(e.dataTransfer)) { if (!this._hasFiles(e.dataTransfer)) {
return; return;
@ -278,17 +277,17 @@ export default class FileUploadComponent extends Vue {
++this.counter && this.overlay.classList.add('draggedover'); ++this.counter && this.overlay.classList.add('draggedover');
} }
dragLeaveHandler() { public dragLeaveHandler() {
1 > --this.counter && this.overlay.classList.remove('draggedover'); 1 > --this.counter && this.overlay.classList.remove('draggedover');
} }
dragOverHandler(e) { public dragOverHandler(e) {
if (this._hasFiles(e.dataTransfer)) { if (this._hasFiles(e.dataTransfer)) {
e.preventDefault(); e.preventDefault();
} }
} }
startDrag(evt, item) { public startDrag(evt, item) {
evt.dataTransfer.dropEffect = 'move'; evt.dataTransfer.dropEffect = 'move';
evt.dataTransfer.effectAllowed = 'move'; evt.dataTransfer.effectAllowed = 'move';
evt.dataTransfer.setData('itemID', item.id); evt.dataTransfer.setData('itemID', item.id);
@ -296,28 +295,31 @@ export default class FileUploadComponent extends Vue {
// reset counter and append file to gallery when file is dropped // reset counter and append file to gallery when file is dropped
dropHandler(event) { public dropHandler(event) {
event.preventDefault(); event.preventDefault();
for (const file of event.dataTransfer.files) { for (const file of event.dataTransfer.files) {
// let fileName = String(file.name.replace(/\.[^/.]+$/, '')); // let fileName = String(file.name.replace(/\.[^/.]+$/, ''));
// file.label = fileName; // file.label = fileName;
// if (file.type.match('image.*')) {
// this.generateURL(file);
// }
this._addFile(file); this._addFile(file);
} }
this.overlay.classList.remove('draggedover'); this.overlay.classList.remove('draggedover');
this.counter = 0; this.counter = 0;
} }
onChangeFile(event) { public onChangeFile(event) {
event.preventDefault(); event.preventDefault();
// let uploadedFile = event.target.files[0]; // let uploadedFile = event.target.files[0];
// let fileName = String(event.target.files[0].name.replace(/\.[^/.]+$/, '')); // let fileName = String(event.target.files[0].name.replace(/\.[^/.]+$/, ''));
// form.file = event.target.files[0];
// form.upload.label = fileName;
// console.log(file.file);
for (const file of event.target.files) { for (const file of event.target.files) {
// let fileName = String(event.target.files[0].name.replace(/\.[^/.]+$/, '')); // let fileName = String(event.target.files[0].name.replace(/\.[^/.]+$/, ''));
// file.label = fileName; // file.label = fileName;
// if (file.type.match('image.*')) {
// this.generateURL(file);
// }
this._addFile(file); this._addFile(file);
} }
// this.overlay.classList.remove('draggedover'); // this.overlay.classList.remove('draggedover');
@ -341,24 +343,58 @@ export default class FileUploadComponent extends Vue {
return Object.fromEntries(Object.entries(this.errors).filter(([key]) => key.startsWith('file'))); return Object.fromEntries(Object.entries(this.errors).filter(([key]) => key.startsWith('file')));
} }
clearAllFiles(event) { public clearAllFiles(event) {
event.preventDefault(); event.preventDefault();
this.files.splice(0); this.items.splice(0);
} }
removeFile(key) { public removeFile(key) {
this.files.splice(key, 1); this.items.splice(key, 1);
} }
generateURL(file) { public generateURL(file: TethysFile | File): string {
let fileSrc = URL.createObjectURL(file); // const arrayBuffer = Buffer.from(file.fileData.data);
setTimeout(() => { // const blob = new Blob([file.fileData.data], { type: 'application/octet-stream' });
URL.revokeObjectURL(fileSrc); // const blob = new Blob([file.fileData], { type: 'image/png'});
}, 1000); // let fileSrc = file.fileData;
return fileSrc;
let localUrl: string = "";
if (file instanceof File) {
localUrl = URL.createObjectURL(file as Blob);
} else if (file.filePath) {
// const blob = new Blob([file.fileData]);
// localUrl = URL.createObjectURL(blob);
const parsed = JSON.parse(file.fileData);
// retrieve the original buffer of data
const buff = Buffer.from(parsed.blob, "base64");
const blob = new Blob([buff], { type: 'application/octet-stream' });
// file.blob = blob;
localUrl = URL.createObjectURL(blob);
file.fileSrc = localUrl;
} }
getFileSize(file) { // setTimeout(() => {
// URL.revokeObjectURL(localUrl);
// }, 1000);
return localUrl;
}
// private async downloadFile(id: number): Promise<string> {
// const response = await axios.get<Blob>(`/api/download/${id}`, {
// responseType: 'blob',
// });
// const url = URL.createObjectURL(response.data);
// setTimeout(() => {
// URL.revokeObjectURL(url);
// }, 1000);
// return url;
// }
public getFileSize(file) {
if (file.size > 1024) { if (file.size > 1024) {
if (file.size > 1048576) { if (file.size > 1048576) {
return Math.round(file.size / 1048576) + 'mb'; return Math.round(file.size / 1048576) + 'mb';
@ -370,29 +406,75 @@ export default class FileUploadComponent extends Vue {
} }
} }
// check if file is of type image and prepend the initialied // private _addFile(file) {
// template to the target element // // const isImage = file.type.match('image.*');
private _addFile(file: TestFile) { // // const objectURL = URL.createObjectURL(file);
// const isImage = file.type.match('image.*');
// const objectURL = URL.createObjectURL(file);
// this.files[objectURL] = file; // // this.files[objectURL] = file;
// let test: TethysFile = { upload: file, label: "dfdsfs", sorting: 0 }; // // let test: TethysFile = { upload: file, label: "dfdsfs", sorting: 0 };
// file.sorting = this.files.length; // // file.sorting = this.files.length;
this.files.push(file); // file.sort_order = (this.items.length + 1),
// this.files.push(file);
// }
private _addFile(file: File) {
// const reader = new FileReader();
// reader.onload = (event) => {
// const base64Data = (event.target as FileReader).result as string;
// this.items.push(test);
// };
// reader.readAsDataURL(file);
if (file instanceof File) {
// const base64Data = await this.readBase64(file);
let test: TethysFile = {
label: file.name,
name: file.name,
size: file.size,
file_size: file.size,
// fileData: JSON.stringify({ blob: base64Data }),
// filePath: file.mozFullPath,
// path_name: file.mozFullPath,
webkitRelativePath: '',
lastModified: file.lastModified,
type: file.type,
mime_type: file.type,
visible_in_frontdoor: false,
visible_in_oai: false,
fileSrc: file.type.match('image.*')? this.generateURL(file) : "",
blob: file as Blob,
sort_order: (this.items.length + 1),
};
// this.items.push(test);
this.items[this.items.length] = test;
} }
else {
this.items.push(file);
}
}
// private async readBase64(blob: Blob): Promise<string> {
// return new Promise<string>((resolve, reject) => {
// const reader = new FileReader();
// reader.onload = (event) => resolve((event.target as FileReader).result as string);
// reader.onerror = reject;
// reader.readAsDataURL(blob);
// });
// }
// use to check if a file is being dragged // use to check if a file is being dragged
private _hasFiles({ types = [] as Array<string> }) { private _hasFiles({ types = [] as Array<string> }) {
return types.indexOf('Files') > -1; return types.indexOf('Files') > -1;
} }
} }
export default FileUploadComponent;
</script> </script>
<style lang="css"> <style lang="css">
.hasImage:hover section { .hasImage:hover section {
background-color: rgba(5, 5, 5, 0.4); background-color: rgba(5, 5, 5, 0.4);
} }
.hasImage:hover button:hover { .hasImage:hover button:hover {
background: rgba(5, 5, 5, 0.45); background: rgba(5, 5, 5, 0.45);
} }
@ -409,6 +491,7 @@ i {
#overlay.draggedover { #overlay.draggedover {
background-color: rgba(255, 255, 255, 0.7); background-color: rgba(255, 255, 255, 0.7);
} }
#overlay.draggedover p, #overlay.draggedover p,
#overlay.draggedover i { #overlay.draggedover i {
opacity: 1; opacity: 1;

View File

@ -1,4 +1,4 @@
<script setup> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import FormCheckRadio from '@/Components/FormCheckRadio.vue'; import FormCheckRadio from '@/Components/FormCheckRadio.vue';
const props = defineProps({ const props = defineProps({
@ -13,7 +13,7 @@ const props = defineProps({
type: { type: {
type: String, type: String,
default: 'checkbox', default: 'checkbox',
validator: (value) => ['checkbox', 'radio', 'switch'].includes(value), validator: (value: string) => ['checkbox', 'radio', 'switch'].includes(value),
}, },
componentClass: { componentClass: {
type: String, type: String,
@ -27,11 +27,34 @@ const props = defineProps({
}); });
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
const computedValue = computed({ const computedValue = computed({
get: () => props.modelValue, // get: () => props.modelValue,
get: () => {
// const ids = props.modelValue.map((obj) => obj.id);
// return ids;
if (Array.isArray(props.modelValue)) {
if (props.modelValue.every((item) => typeof item === 'number')) {
return props.modelValue;
} else if (props.modelValue.every((item) => hasIdAttribute(item))) {
const ids = props.modelValue.map((obj) => obj.id.toString());
return ids;
}
return props.modelValue;
}
// return props.modelValue;
},
set: (value) => { set: (value) => {
emit('update:modelValue', value); emit('update:modelValue', value);
}, },
}); });
// Define a type guard to check if an object has an 'id' attribute
// function hasIdAttribute(obj: any): obj is { id: any } {
// return typeof obj === 'object' && 'id' in obj;
// }
const hasIdAttribute = (obj: any): obj is { id: any } => {
return typeof obj === 'object' && 'id' in obj;
};
</script> </script>
<template> <template>

View File

@ -59,8 +59,9 @@ Map.include({
}); });
const DEFAULT_BASE_LAYER_NAME = 'BaseLayer'; const DEFAULT_BASE_LAYER_NAME = 'BaseLayer';
const DEFAULT_BASE_LAYER_ATTRIBUTION = '&copy; <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors'; const DEFAULT_BASE_LAYER_ATTRIBUTION = '&copy; <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';
// const OPEN_SEARCH_HOST = 'http://localhost:9200'; // const OPENSEARCH_HOST = 'http://localhost:9200';
const OPEN_SEARCH_HOST = 'http://192.168.21.18'; const OPENSEARCH_HOST = 'http://192.168.21.18';
// const OPENSEARCH_HOST = `http://${process.env.OPENSEARCH_PUBLIC_HOST}`;
let map: Map; let map: Map;
const props = defineProps({ const props = defineProps({
@ -226,7 +227,7 @@ const handleDrawEventCreated = async (event) => {
try { try {
let response = await axios({ let response = await axios({
method: 'POST', method: 'POST',
url: OPEN_SEARCH_HOST + '/tethys-records/_search', url: OPENSEARCH_HOST + '/tethys-records/_search',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
data: { data: {
size: 1000, size: 1000,

View File

@ -6,7 +6,7 @@ import { gradientBgPurplePink, gradientBgDark, gradientBgPinkRed, gradientBgGree
const props = defineProps({ const props = defineProps({
bg: { bg: {
type: String, type: String,
required: true, required: false,
validator: (value) => ['purplePink', 'pinkRed', 'greenBlue'].includes(value), validator: (value) => ['purplePink', 'pinkRed', 'greenBlue'].includes(value),
}, },
}); });
@ -23,9 +23,11 @@ const colorClass = computed(() => {
return gradientBgPinkRed; return gradientBgPinkRed;
case 'greenBlue': case 'greenBlue':
return gradientBgGreenBlue; return gradientBgGreenBlue;
default:
return 'bg-white text-black dark:bg-slate-900/70 dark:text-white';
} }
return 'bg-white'; // return 'bg-white';
}); });
</script> </script>

View File

@ -12,9 +12,10 @@ export interface Dataset {
| (IErrorMessage | undefined) | (IErrorMessage | undefined)
| Coverage | Coverage
| Array<DatasetReference> | Array<DatasetReference>
| Array<File>; | Array<File>
| (Array<number> | Array<Object>);
language: Ref<string>; language: Ref<string>;
// licenses: Array<number>; licenses: Array<number> | Array<Object>;
rights: boolean; rights: boolean;
type: string; type: string;
creating_corporation: string; creating_corporation: string;
@ -29,24 +30,49 @@ export interface Dataset {
// async (user): Promise<void>; // async (user): Promise<void>;
subjects: Array<Subject>; subjects: Array<Subject>;
references: Array<DatasetReference>; references: Array<DatasetReference>;
files: Array<TestFile> | undefined; files: Array<TethysFile>;
// upload: TethysFile // upload: TethysFile
} }
/** Provides information about files and allows JavaScript in a web page to access their content. */ /** Provides information about files and allows JavaScript in a web page to access their content. */
export interface TestFile extends Blob { // export interface TethysFile {
// readonly lastModified: number;
// readonly name: string;
// readonly webkitRelativePath: string;
// id: number;
// label: string;
// sorting: number;
// filePath: string;
// fileSrc: string;
// }
export interface TethysFile {
readonly lastModified: number; readonly lastModified: number;
readonly name: string; readonly name: string;
readonly webkitRelativePath: string; readonly webkitRelativePath: string;
id?: number;
label: string; label: string;
sorting: number; // sorting: number;
} // path_name?: string; //only db path_name
filePath?: string;
fileSrc?: string;
blob: Blob;
fileData?: any;
// export interface TethysFile { //additional:
// label: string, comment?: string;
// sorting: number, document_id?: number;
// upload: File, file_size: number;
// } language?: string;
mime_type: string;
type?: string;
size: number;
sort_order: number;
visible_in_frontdoor: boolean;
visible_in_oai: boolean;
}
export interface Subject { export interface Subject {
// id: number; // id: number;
@ -64,12 +90,14 @@ export interface DatasetReference {
} }
export interface Title { export interface Title {
id?: number;
value: string; value: string;
type: string; type: string;
language: string | Ref<string>; language: string | Ref<string>;
} }
export interface Description { export interface Description {
id?: number;
value: string; value: string;
type: string; type: string;
language: string | Ref<string>; language: string | Ref<string>;

View File

@ -34,6 +34,7 @@ import FormControl from '@/Components/FormControl.vue';
<LayoutGuest> <LayoutGuest>
<Head title="Login" /> <Head title="Login" />
<!-- <SectionFullScreen v-slot="{ cardClass }" :bg="'greenBlue'"> -->
<SectionFullScreen v-slot="{ cardClass }"> <SectionFullScreen v-slot="{ cardClass }">
<a class="text-2xl font-semibold flex justify-center items-center mb-8 lg:mb-10"> <a class="text-2xl font-semibold flex justify-center items-center mb-8 lg:mb-10">
<img src="/logo.svg" class="h-10 mr-4" alt="Windster Logo" /> <img src="/logo.svg" class="h-10 mr-4" alt="Windster Logo" />

View File

@ -22,9 +22,7 @@ import CardBox from '@/Components/CardBox.vue';
import FormField from '@/Components/FormField.vue'; import FormField from '@/Components/FormField.vue';
import FormControl from '@/Components/FormControl.vue'; import FormControl from '@/Components/FormControl.vue';
import FormCheckRadioGroup from '@/Components/FormCheckRadioGroup.vue'; import FormCheckRadioGroup from '@/Components/FormCheckRadioGroup.vue';
// import BaseDivider from '@/Components/BaseDivider.vue';
import BaseButton from '@/Components/BaseButton.vue'; import BaseButton from '@/Components/BaseButton.vue';
// import BaseButtons from '@/Components/BaseButtons.vue';
import { stardust } from '@eidellev/adonis-stardust/client'; import { stardust } from '@eidellev/adonis-stardust/client';
// import { Inertia } from '@inertiajs/inertia'; // import { Inertia } from '@inertiajs/inertia';
import CardBoxModal from '@/Components/CardBoxModal.vue'; import CardBoxModal from '@/Components/CardBoxModal.vue';
@ -196,7 +194,7 @@ if (Object.keys(mainService.dataset).length == 0) {
// titles: [{ value: '', type: 'Main', language: language }], // titles: [{ value: '', type: 'Main', language: language }],
// descriptions: [{ value: '', type: 'Abstract', language: language }], // descriptions: [{ value: '', type: 'Abstract', language: language }],
// }); // });
let form = useForm<Dataset>(dataset); let form = useForm<Dataset>(dataset as Dataset);
// form.defaults(); // form.defaults();
// const emit = defineEmits(['update:modelValue', 'setRef']); // const emit = defineEmits(['update:modelValue', 'setRef']);
@ -292,6 +290,9 @@ const submit = async () => {
// this.currentStatus = STATUS_SAVING; // this.currentStatus = STATUS_SAVING;
// serrors = []; // serrors = [];
// const files = form.files.map((obj) => {
// return new File([obj.blob], obj.label, { type: obj.type, lastModified: obj.lastModified });
// });
// formStep.value++; // formStep.value++;
await form await form
@ -730,7 +731,8 @@ Removes a selected keyword
<TablePersons :persons="form.contributors" v-if="form.contributors.length > 0" <TablePersons :persons="form.contributors" v-if="form.contributors.length > 0"
:contributortypes="contributorTypes" :errors="form.errors" /> :contributortypes="contributorTypes" :errors="form.errors" />
<div class="text-red-400 text-sm" v-if="form.errors.contributors && Array.isArray(form.errors.contributors)"> <div class="text-red-400 text-sm"
v-if="form.errors.contributors && Array.isArray(form.errors.contributors)">
{{ form.errors.contributors.join(', ') }} {{ form.errors.contributors.join(', ') }}
</div> </div>
</CardBox> </CardBox>
@ -1013,7 +1015,7 @@ Removes a selected keyword
</p> </p>
</div> --> </div> -->
<FileUploadComponent v-model="form.files"></FileUploadComponent> <FileUploadComponent :files="form.files"></FileUploadComponent>
<div class="text-red-400 text-sm" v-if="form.errors['file'] && Array.isArray(form.errors['file'])"> <div class="text-red-400 text-sm" v-if="form.errors['file'] && Array.isArray(form.errors['file'])">
{{ form.errors['file'].join(', ') }} {{ form.errors['file'].join(', ') }}
@ -1039,7 +1041,8 @@ Removes a selected keyword
Next Next
</button> </button>
<button v-if="formStep == 4" :disabled="form.processing" :class="{ 'opacity-25': form.processing }" <button v-if="formStep == 4" :disabled="form.processing"
:class="{ 'opacity-25': form.processing }"
class="text-base hover:scale-110 focus:outline-none flex justify-center px-4 py-2 rounded font-bold cursor-pointer hover:bg-teal-200 bg-teal-100 text-teal-700 border duration-200 ease-in-out border-teal-600 transition" class="text-base hover:scale-110 focus:outline-none flex justify-center px-4 py-2 rounded font-bold cursor-pointer hover:bg-teal-200 bg-teal-100 text-teal-700 border duration-200 ease-in-out border-teal-600 transition"
@click.stop="submit"> @click.stop="submit">
Save Save

View File

@ -12,6 +12,7 @@
<!-- <div class="max-w-2xl mx-auto"> --> <!-- <div class="max-w-2xl mx-auto"> -->
<CardBox :form="true"> <CardBox :form="true">
<FormValidationErrors v-bind:errors="errors" />
<div class="mb-4"> <div class="mb-4">
<!-- <label for="title" class="block text-gray-700 font-bold mb-2">Title:</label> <!-- <label for="title" class="block text-gray-700 font-bold mb-2">Title:</label>
<input <input
@ -21,6 +22,7 @@
v-model="form.language" v-model="form.language"
/> --> /> -->
<div class="flex flex-col md:flex-row"> <div class="flex flex-col md:flex-row">
<!-- (1) language field -->
<FormField label="Language *" help="required: select dataset main language" <FormField label="Language *" help="required: select dataset main language"
:class="{ 'text-red-400': errors.language }" class="w-full flex-1"> :class="{ 'text-red-400': errors.language }" class="w-full flex-1">
<FormControl required v-model="form.language" :type="'select'" placeholder="[Enter Language]" <FormControl required v-model="form.language" :type="'select'" placeholder="[Enter Language]"
@ -31,17 +33,41 @@
</FormControl> </FormControl>
</FormField> </FormField>
</div> </div>
<!-- (2) licenses -->
<FormField label="Licenses" wrap-body :class="{ 'text-red-400': form.errors.licenses }"
class="mt-8 w-full mx-2 flex-1">
<FormCheckRadioGroup v-model="form.licenses" name="licenses" is-column :options="licenses" />
</FormField>
<div class="flex flex-col md:flex-row">
<!-- (3) dataset_type -->
<FormField label="Dataset Type *" help="required: dataset type" <FormField label="Dataset Type *" help="required: dataset type"
:class="{ 'text-red-400': form.errors.type }"> :class="{ 'text-red-400': form.errors.type }" class="w-full mx-2 flex-1">
<FormControl required v-model="form.type" :type="'select'" placeholder="-- select type --" <FormControl required v-model="form.type" :type="'select'" placeholder="-- select type --"
:errors="errors.type" :options="doctypes"> :errors="errors.type" :options="doctypes">
<div class="text-red-400 text-sm" v-if="form.errors.type && Array.isArray(form.errors.type)"> <div class="text-red-400 text-sm"
v-if="form.errors.type && Array.isArray(form.errors.type)">
{{ form.errors.type.join(', ') }} {{ form.errors.type.join(', ') }}
</div> </div>
</FormControl> </FormControl>
</FormField> </FormField>
<!-- (4) creating_corporation -->
<FormField label="Creating Corporation *"
:class="{ 'text-red-400': form.errors.creating_corporation }" class="w-full mx-2 flex-1">
<FormControl required v-model="form.creating_corporation" type="text"
placeholder="[enter creating corporation]" :is-read-only="true">
<div class="text-red-400 text-sm"
v-if="form.errors.creating_corporation && Array.isArray(form.errors.creating_corporation)">
{{ form.errors.creating_corporation.join(', ') }}
</div>
</FormControl>
</FormField>
</div>
<!-- titles --> <BaseDivider />
<!-- (5) titles -->
<CardBox class="mb-6 shadow" :has-form-data="false" title="Titles" :icon="mdiFinance" <CardBox class="mb-6 shadow" :has-form-data="false" title="Titles" :icon="mdiFinance"
:header-icon="mdiPlusCircle" v-on:header-icon-click="addTitle()"> :header-icon="mdiPlusCircle" v-on:header-icon-click="addTitle()">
<div class="flex flex-col md:flex-row"> <div class="flex flex-col md:flex-row">
@ -79,8 +105,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template v-for="(item, index) in form.titles" :key="index"> <template v-for="(title, index) in form.titles" :key="index">
<tr v-if="item.type != 'Main'"> <tr v-if="title.type != 'Main'">
<!-- <td scope="row">{{ index + 1 }}</td> --> <!-- <td scope="row">{{ index + 1 }}</td> -->
<td data-label="Title Value"> <td data-label="Title Value">
<FormControl required v-model="form.titles[index].value" type="text" <FormControl required v-model="form.titles[index].value" type="text"
@ -113,7 +139,7 @@
<BaseButtons type="justify-start lg:justify-end" no-wrap> <BaseButtons type="justify-start lg:justify-end" no-wrap>
<!-- <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" /> --> <!-- <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" /> -->
<BaseButton color="danger" :icon="mdiTrashCan" small <BaseButton color="danger" :icon="mdiTrashCan" small
@click.prevent="removeTitle(index)" /> v-if="title.id == undefined" @click.prevent="removeTitle(index)" />
</BaseButtons> </BaseButtons>
</td> </td>
</tr> </tr>
@ -122,6 +148,7 @@
</table> </table>
</CardBox> </CardBox>
<!-- (6) descriptions -->
<CardBox class="mb-6 shadow" :has-form-data="false" title="Descriptions" :icon="mdiFinance" <CardBox class="mb-6 shadow" :has-form-data="false" title="Descriptions" :icon="mdiFinance"
:header-icon="mdiPlusCircle" v-on:header-icon-click="addDescription()"> :header-icon="mdiPlusCircle" v-on:header-icon-click="addDescription()">
<div class="flex flex-col md:flex-row"> <div class="flex flex-col md:flex-row">
@ -172,7 +199,7 @@
</td> </td>
<td data-label="Description Type"> <td data-label="Description Type">
<FormControl required v-model="form.descriptions[index].type" type="select" <FormControl required v-model="form.descriptions[index].type" type="select"
:options="props.descriptiontypes" placeholder="[select title type]"> :options="descriptiontypes" placeholder="[select title type]">
<div class="text-red-400 text-sm" <div class="text-red-400 text-sm"
v-if="Array.isArray(form.errors[`descriptions.${index}.type`])"> v-if="Array.isArray(form.errors[`descriptions.${index}.type`])">
{{ form.errors[`descriptions.${index}.type`].join(', ') }} {{ form.errors[`descriptions.${index}.type`].join(', ') }}
@ -191,7 +218,7 @@
<td class="before:hidden lg:w-1 whitespace-nowrap"> <td class="before:hidden lg:w-1 whitespace-nowrap">
<BaseButtons type="justify-start lg:justify-end" no-wrap> <BaseButtons type="justify-start lg:justify-end" no-wrap>
<!-- <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" /> --> <!-- <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" /> -->
<BaseButton color="danger" :icon="mdiTrashCan" small <BaseButton color="danger" :icon="mdiTrashCan" small v-if="item.id == undefined"
@click.prevent="removeDescription(index)" /> @click.prevent="removeDescription(index)" />
</BaseButtons> </BaseButtons>
</td> </td>
@ -201,14 +228,182 @@
</table> </table>
</CardBox> </CardBox>
<MapComponent v-if="form.coverage" <!-- (7) authors -->
:mapOptions="mapOptions" <CardBox class="mb-6 shadow" has-table title="Authors" :icon="mdiBookOpenPageVariant">
:baseMaps="baseMaps" <SearchAutocomplete source="/api/persons" :response-property="'first_name'"
:fitBounds="fitBounds" placeholder="search in person table...." v-on:person="onAddAuthor"></SearchAutocomplete>
:coverage="form.coverage"
:mapId="mapId" <TablePersons :persons="form.authors" v-if="form.authors.length > 0" />
v-bind-event:onMapInitializedEvent="onMapInitialized" <div class="text-red-400 text-sm" v-if="errors.authors && Array.isArray(errors.authors)">
></MapComponent> {{ errors.authors.join(', ') }}
</div>
</CardBox>
<!-- (8) contributors -->
<CardBox class="mb-6 shadow" has-table title="Contributors" :icon="mdiBookOpenPageVariant">
<SearchAutocomplete source="/api/persons" :response-property="'first_name'"
placeholder="search in person table...." v-on:person="onAddContributor">
</SearchAutocomplete>
<TablePersons :persons="form.contributors" v-if="form.contributors.length > 0"
:contributortypes="contributorTypes" :errors="form.errors" />
<div class="text-red-400 text-sm"
v-if="form.errors.contributors && Array.isArray(form.errors.contributors)">
{{ form.errors.contributors.join(', ') }}
</div>
</CardBox>
<div class="flex flex-col md:flex-row">
<!-- (9) project_id -->
<FormField label="Project.." help="project is optional"
:class="{ 'text-red-400': errors.project_id }" class="w-full mx-2 flex-1">
<FormControl required v-model="form.project_id" :type="'select'" placeholder="[Select Project]"
:errors="form.errors.project_id" :options="projects">
<div class="text-red-400 text-sm" v-if="form.errors.project_id">
{{ form.errors.project_id.join(', ') }}
</div>
</FormControl>
</FormField>
<!-- (10) embargo_date -->
<FormField label="Embargo Date.." help="embargo date is optional"
:class="{ 'text-red-400': errors.embargo_date }" class="w-full mx-2 flex-1">
<FormControl v-model="form.embargo_date" :type="'date'" placeholder="date('y-m-d')"
:errors="form.errors.embargo_date">
<div class="text-red-400 text-sm" v-if="form.errors.embargo_date">
{{ form.errors.embargo_date.join(', ') }}
</div>
</FormControl>
</FormField>
</div>
<BaseDivider />
<MapComponent v-if="form.coverage" :mapOptions="mapOptions" :baseMaps="baseMaps" :fitBounds="fitBounds"
:coverage="form.coverage" :mapId="mapId" v-bind-event:onMapInitializedEvent="onMapInitialized">
</MapComponent>
<div class="flex flex-col md:flex-row">
<!-- x min and max -->
<FormField label="Coverage X Min" :class="{ 'text-red-400': form.errors['coverage.x_min'] }"
class="w-full mx-2 flex-1">
<FormControl required v-model="form.coverage.x_min" type="text" placeholder="[enter x_min]">
<div class="text-red-400 text-sm"
v-if="form.errors['coverage.x_min'] && Array.isArray(form.errors['coverage.x_min'])">
{{ form.errors['coverage.x_min'].join(', ') }}
</div>
</FormControl>
</FormField>
<FormField label="Coverage X Max" :class="{ 'text-red-400': form.errors['coverage.x_max'] }"
class="w-full mx-2 flex-1">
<FormControl required v-model="form.coverage.x_max" type="text" placeholder="[enter x_max]">
<div class="text-red-400 text-sm"
v-if="form.errors['coverage.x_max'] && Array.isArray(form.errors['coverage.x_max'])">
{{ form.errors['coverage.x_max'].join(', ') }}
</div>
</FormControl>
</FormField>
<!-- y min and max -->
<FormField label="Coverage Y Min" :class="{ 'text-red-400': form.errors['coverage.y_min'] }"
class="w-full mx-2 flex-1">
<FormControl required v-model="form.coverage.y_min" type="text" placeholder="[enter y_min]">
<div class="text-red-400 text-sm"
v-if="form.errors['coverage.y_min'] && Array.isArray(form.errors['coverage.y_min'])">
{{ form.errors['coverage.y_min'].join(', ') }}
</div>
</FormControl>
</FormField>
<FormField label="Coverage Y Max" :class="{ 'text-red-400': form.errors['coverage.y_max'] }"
class="w-full mx-2 flex-1">
<FormControl required v-model="form.coverage.y_max" type="text" placeholder="[enter y_max]">
<div class="text-red-400 text-sm"
v-if="form.errors['coverage.y_max'] && Array.isArray(form.errors['coverage.y_max'])">
{{ form.errors['coverage.y_max'].join(', ') }}
</div>
</FormControl>
</FormField>
</div>
<CardBox class="mb-6 shadow" has-table title="Dataset References" :header-icon="mdiPlusCircle"
v-on:header-icon-click="addReference">
<table class="table-fixed border-green-900" v-if="form.references.length">
<thead>
<tr>
<th class="w-4/12">Value</th>
<th class="w-2/12">Type</th>
<th class="w-3/12">Relation</th>
<th class="w-2/12">Label</th>
<th class="w-1/12"></th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in form.references">
<td data-label="Reference Value">
<!-- <input name="Reference Value" class="form-control"
placeholder="[VALUE]" v-model="item.value" /> -->
<FormControl required v-model="item.value" :type="'text'" placeholder="[VALUE]"
:errors="form.errors.embargo_date">
<div class="text-red-400 text-sm"
v-if="form.errors[`references.${index}.value`] && Array.isArray(form.errors[`references.${index}.value`])">
{{ form.errors[`references.${index}.value`].join(', ') }}
</div>
</FormControl>
</td>
<td>
<FormControl required v-model="form.references[index].type" type="select"
:options="referenceIdentifierTypes" placeholder="[type]">
<div class="text-red-400 text-sm"
v-if="Array.isArray(form.errors[`references.${index}.type`])">
{{ form.errors[`references.${index}.type`].join(', ') }}
</div>
</FormControl>
</td>
<td>
<!-- {!! Form::select('Reference[Relation]', $relationTypes, null,
['placeholder' => '[relationType]', 'v-model' => 'item.relation',
'data-vv-scope' => 'step-2'])
!!} -->
<FormControl required v-model="form.references[index].relation" type="select"
:options="relationTypes" placeholder="[relation type]">
<div class="text-red-400 text-sm"
v-if="Array.isArray(form.errors[`references.${index}.relation`])">
{{ form.errors[`references.${index}.relation`].join(', ') }}
</div>
</FormControl>
</td>
<td data-label="Reference Label">
<!-- <input name="Reference Label" class="form-control" v-model="item.label" /> -->
<FormControl required v-model="form.references[index].label" type="text"
placeholder="[reference label]">
<div class="text-red-400 text-sm"
v-if="form.errors[`references.${index}.label`] && Array.isArray(form.errors[`references.${index}.label`])">
{{ form.errors[`references.${index}.label`].join(', ') }}
</div>
</FormControl>
</td>
<td class="before:hidden lg:w-1 whitespace-nowrap">
<!-- <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" /> -->
<BaseButton color="danger" :icon="mdiTrashCan" small
@click.prevent="removeReference(index)" />
</td>
</tr>
</tbody>
</table>
</CardBox>
<BaseDivider />
<CardBox class="mb-6 shadow" has-table title="Dataset Keywords" :icon="mdiEarthPlus"
:header-icon="mdiPlusCircle" v-on:header-icon-click="addKeyword">
<!-- <ul>
<li v-for="(subject, index) in form.subjects" :key="index">
{{ subject.value }} <BaseButton color="danger" :icon="mdiTrashCan" small @click.prevent="removeKeyword(index)" />
</li>
</ul> -->
<TableKeywords :keywords="form.subjects" :errors="form.errors" :subjectTypes="subjectTypes"
v-if="form.subjects.length > 0" />
</CardBox>
</div> </div>
@ -233,18 +428,14 @@
</option> </option>
</select> --> </select> -->
</div> </div>
<div class="mb-4">
<label for="license" class="block text-gray-700 font-bold mb-2">License:</label> <FileUploadComponent :files="form.files"></FileUploadComponent>
<!-- <select
id="license" <div class="text-red-400 text-sm" v-if="form.errors['file'] && Array.isArray(form.errors['files'])">
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" {{ form.errors['files'].join(', ') }}
v-model="dataset.license_id"
>
<option v-for="license in licenses" :key="license.id" :value="license.id" class="block px-4 py-2 text-gray-700">
{{ license.name_long }}
</option>
</select> -->
</div> </div>
<!-- Add more input fields for the other properties of the dataset --> <!-- Add more input fields for the other properties of the dataset -->
<!-- <button <!-- <button
type="submit" type="submit"
@ -252,11 +443,17 @@
> >
Save Save
</button> --> </button> -->
<template #footer> <template #footer>
<BaseButtons> <BaseButtons>
<BaseButton type="submit" label="Submit" color="info" :class="{ 'opacity-25': form.processing }" <BaseButton @click.stop="submit" :disabled="form.processing" label="Save" color="info"
:disabled="form.processing"> :class="{ 'opacity-25': form.processing }" small>
</BaseButton> </BaseButton>
<!-- <button :disabled="form.processing" :class="{ 'opacity-25': form.processing }"
class="text-base hover:scale-110 focus:outline-none flex justify-center px-4 py-2 rounded font-bold cursor-pointer hover:bg-teal-200 bg-teal-100 text-teal-700 border duration-200 ease-in-out border-teal-600 transition"
@click.stop="submit">
Save
</button> -->
</BaseButtons> </BaseButtons>
</template> </template>
</CardBox> </CardBox>
@ -266,31 +463,61 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Head, useForm } from '@inertiajs/vue3'; // import EditComponent from "./../EditComponent";
// export default EditComponent;
// import { Component, Vue, Prop, Setup, toNative } from 'vue-facing-decorator';
// import AuthLayout from '@/Layouts/Auth.vue';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue'; import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import { Dataset, Title } from '@/Dataset'; import { useForm, Head } from '@inertiajs/vue3';
// import { ref } from 'vue';
// import { MainService } from '@/Stores/main';
// import FormInput from '@/Components/FormInput.vue'; // @/Components/FormInput.vue'
import { Dataset, Title, Subject, TethysFile } from '@/Dataset';
import { stardust } from '@eidellev/adonis-stardust/client';
import FormField from '@/Components/FormField.vue'; import FormField from '@/Components/FormField.vue';
import FormControl from '@/Components/FormControl.vue'; import FormControl from '@/Components/FormControl.vue';
import SectionMain from '@/Components/SectionMain.vue'; import SectionMain from '@/Components/SectionMain.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue'; import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
import { mdiImageText, mdiArrowLeftBoldOutline, mdiPlusCircle, mdiFinance, mdiTrashCan } from '@mdi/js'; import FormCheckRadioGroup from '@/Components/FormCheckRadioGroup.vue';
// import BaseDivider from '@/Components/BaseDivider.vue';
import BaseButton from '@/Components/BaseButton.vue'; import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue'; import BaseButtons from '@/Components/BaseButtons.vue';
import BaseDivider from '@/Components/BaseDivider.vue';
import CardBox from '@/Components/CardBox.vue'; import CardBox from '@/Components/CardBox.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
import MapComponent from '@/Components/Map/map.component.vue'; import MapComponent from '@/Components/Map/map.component.vue';
import SearchAutocomplete from '@/Components/SearchAutocomplete.vue';
import TablePersons from '@/Components/TablePersons.vue';
import TableKeywords from '@/Components/TableKeywords.vue';
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
import FileUploadComponent from '@/Components/FileUpload.vue';
import { MapOptions } from '@/Components/Map/MapOptions'; import { MapOptions } from '@/Components/Map/MapOptions';
import { LatLngBoundsExpression } from 'leaflet/src/geo/LatLngBounds'; import { LatLngBoundsExpression } from 'leaflet/src/geo/LatLngBounds';
import { LayerOptions } from '@/Components/Map/LayerOptions'; import { LayerOptions } from '@/Components/Map/LayerOptions';
import {
mdiImageText,
mdiArrowLeftBoldOutline,
mdiPlusCircle,
mdiFinance,
mdiTrashCan,
mdiBookOpenPageVariant,
mdiEarthPlus,
} from '@mdi/js';
import { notify } from '@/notiwind';
const props = defineProps({ const props = defineProps({
dataset: { errors: {
type: Object,
default: () => ({}),
},
licenses: {
type: Object,
default: () => ({}),
},
languages: {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
}, },
languages: {},
doctypes: { doctypes: {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
@ -299,14 +526,37 @@ const props = defineProps({
type: Object, type: Object,
default: () => ({}), default: () => ({}),
}, },
projects: {
type: Object,
default: () => ({}),
},
descriptiontypes: { descriptiontypes: {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
}, },
errors: { contributorTypes: {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
}, },
subjectTypes: {
type: Object,
default: () => ({}),
},
referenceIdentifierTypes: {
type: Object,
default: () => ({}),
},
relationTypes: {
type: Object,
default: () => ({}),
},
dataset: {
type: Object,
default: () => ({}),
},
}); });
// const projects = reactive([]); // const projects = reactive([]);
@ -325,7 +575,42 @@ const fitBounds: LatLngBoundsExpression = [
]; ];
const mapId = 'test'; const mapId = 'test';
// const downloadFile = async (id: string): Promise<string> => {
// const response = await axios.get<Blob>(`/api/download/${id}`, {
// responseType: 'blob',
// });
// const url = URL.createObjectURL(response.data);
// setTimeout(() => {
// URL.revokeObjectURL(url);
// }, 1000);
// return url;
// };
// for (const file of props.dataset.files) {
// // console.log(`${file.name} path is ${file.filePath} here.`);
// file.fileSrc = ref("");
// // downloadFile(file.id).then((value: string) => {
// // file.fileSrc = ref(value);
// // form = useForm<Dataset>(props.dataset as Dataset);
// // });
// }
let form = useForm<Dataset>(props.dataset as Dataset); let form = useForm<Dataset>(props.dataset as Dataset);
// const mainService = MainService();
// mainService.fetchfiles(props.dataset);
// const files = computed(() => props.dataset.file);
// let form = useForm<Dataset>(props.dataset as Dataset);
// const form = useForm({ // const form = useForm({
// _method: 'put', // _method: 'put',
// login: props.user.login, // login: props.user.login,
@ -342,9 +627,63 @@ let form = useForm<Dataset>(props.dataset as Dataset);
// this.projects = data.projects; // this.projects = data.projects;
// this.licenses = data.licenses; // this.licenses = data.licenses;
// } // }
const submit = async (): Promise<void> => {
let route = stardust.route('dataset.update', [props.dataset.id]);
// await Inertia.post('/app/register', this.form);
// await router.post('/app/register', this.form);
if (form.licenses.every((item) => hasIdAttribute(item))) {
form.licenses = form.licenses.map((obj) => obj.id.toString());
}
const [fileUploads, fileInputs] = form.files?.reduce(
([fileUploads, fileInputs], obj) => {
if (!obj.id) {
// return MultipartFile for file upload
fileUploads[obj.sort_order] = new File([obj.blob], obj.label, { type: obj.type, lastModified: obj.lastModified });
} else {
// return normal request input
fileInputs.push(obj);
}
return [fileUploads, fileInputs];
},
[[], []] as [Array<File>, Array<TethysFile>]
) as [Array<File>, Array<TethysFile>];
await form
.transform((data) => ({
...data,
licenses: form.licenses.every((item) => hasIdAttribute(item))
? form.licenses.map((obj) => obj.id.toString())
: form.licenses,
files: fileUploads,
fileInputs: fileInputs,
// files: form.files.map((obj) => {
// let file;
// if (!obj.id) {
// // return MultipartFile for file upload
// file = new File([obj.blob], obj.label, { type: obj.type, lastModified: obj.lastModified });
// } else {
// // return normal request input
// file = obj;
// }
// return file;
// }),
rights: 'true',
}))
.put(route);
};
const hasIdAttribute = (obj: any): obj is { id: any } => {
return typeof obj === 'object' && 'id' in obj;
};
const addTitle = () => { const addTitle = () => {
let newTitle: Title = { value: '', language: '', type: '' }; let newTitle: Title = { value: '', language: '', type: '' };
//this.dataset.files.push(uploadedFiles[i]);
form.titles.push(newTitle); form.titles.push(newTitle);
}; };
const removeTitle = (key) => { const removeTitle = (key) => {
@ -353,20 +692,58 @@ const removeTitle = (key) => {
const addDescription = () => { const addDescription = () => {
let newDescription = { value: '', language: '', type: '' }; let newDescription = { value: '', language: '', type: '' };
//this.dataset.files.push(uploadedFiles[i]);
form.descriptions.push(newDescription); form.descriptions.push(newDescription);
}; };
const removeDescription = (key) => { const removeDescription = (key) => {
form.descriptions.splice(key, 1); form.descriptions.splice(key, 1);
}; };
const onAddAuthor = (person) => {
if (form.authors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as author' }, 4000);
} else if (form.contributors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as contributor' });
} else {
form.authors.push(person);
notify({ type: 'info', text: 'person has been successfully added as author' });
}
};
const onAddContributor = (person) => {
if (form.contributors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as contributor' }, 4000);
} else if (form.authors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as author' }, 4000);
} else {
// person.pivot = { contributor_type: '' };
// // person.pivot = { name_type: '', contributor_type: '' };
form.contributors.push(person);
notify({ type: 'info', text: 'person has been successfully added as contributor' }, 4000);
}
};
const addKeyword = () => {
let newSubject: Subject = { value: 'test', language: '', type: 'uncontrolled' };
//this.dataset.files.push(uploadedFiles[i]);
form.subjects.push(newSubject);
};
const addReference = () => {
let newReference = { value: '', label: '', relation: '', type: '' };
//this.dataset.files.push(uploadedFiles[i]);
form.references.push(newReference);
};
const removeReference = (key) => {
form.references.splice(key, 1);
};
const onMapInitialized = (newItem) => { const onMapInitialized = (newItem) => {
// notify({ type: 'info', text: message });
console.log(newItem); console.log(newItem);
}; };
</script> </script>
<!-- <style> <style>
.max-w-2xl { .max-w-2xl {
max-width: 2xl; max-width: 2xl;
} }
@ -398,4 +775,4 @@ const onMapInitialized = (newItem) => {
0 2px 4px 0 rgba(66, 72, 78, 0.12), 0 2px 4px 0 rgba(66, 72, 78, 0.12),
0 4px 8px 0 rgba(66, 72, 78, 0.16); 0 4px 8px 0 rgba(66, 72, 78, 0.16);
} }
</style> --> </style>

View File

@ -0,0 +1,240 @@
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator';
// import AuthLayout from '@/Layouts/Auth.vue';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import { useForm, InertiaForm, Head } from '@inertiajs/vue3';
import FormInput from '@/Components/FormInput.vue'; // @/Components/FormInput.vue'
import { Dataset, Title, Subject } from '@/Dataset';
import { stardust } from '@eidellev/adonis-stardust/client';
import FormField from '@/Components/FormField.vue';
import FormControl from '@/Components/FormControl.vue';
import SectionMain from '@/Components/SectionMain.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
import FormCheckRadioGroup from '@/Components/FormCheckRadioGroup.vue';
import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import BaseDivider from '@/Components/BaseDivider.vue';
import CardBox from '@/Components/CardBox.vue';
import MapComponent from '@/Components/Map/map.component.vue';
import SearchAutocomplete from '@/Components/SearchAutocomplete.vue';
import TablePersons from '@/Components/TablePersons.vue';
import TableKeywords from '@/Components/TableKeywords.vue';
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
import FileUploadComponent from '@/Components/FileUpload.vue';
import { MapOptions } from '@/Components/Map/MapOptions';
import { LatLngBoundsExpression } from 'leaflet/src/geo/LatLngBounds';
import { LayerOptions } from '@/Components/Map/LayerOptions';
import {
mdiImageText,
mdiArrowLeftBoldOutline,
mdiPlusCircle,
mdiFinance,
mdiTrashCan,
mdiBookOpenPageVariant,
mdiEarthPlus,
} from '@mdi/js';
import { notify } from '@/notiwind';
export interface IErrorMessage {
[key: string]: Array<string>;
}
@Component({
name: 'EditComponent',
components: {
LayoutAuthenticated,
FormInput,
Head,
FormField,
FormControl,
SectionMain,
SectionTitleLineWithButton,
FormCheckRadioGroup,
BaseButton,
BaseButtons,
BaseDivider,
CardBox,
MapComponent,
SearchAutocomplete,
TablePersons,
TableKeywords,
FormValidationErrors,
FileUploadComponent,
},
})
class EditComponent extends Vue {
// Component Property
@Prop({ type: Object, default: () => ({}) })
public errors: IErrorMessage;
@Prop({ type: Object, default: () => ({}) })
public licenses;
@Prop({ type: Object, default: () => ({}) })
public languages;
@Prop({ type: Object, default: () => ({}) })
public doctypes;
@Prop({ type: Object, default: () => ({}) })
public titletypes;
@Prop({ type: Object, default: () => ({}) })
public projects;
@Prop({ type: Object, default: () => ({}) })
public descriptiontypes;
@Prop({ type: Object, default: () => {} })
public contributorTypes;
@Prop({ type: Object, default: () => ({}) })
public subjectTypes;
@Prop({ type: Object, default: () => ({}) })
public referenceIdentifierTypes;
@Prop({ type: Object, default: () => ({}) })
public relationTypes;
@Prop({ type: Object, default: () => ({}) })
public dataset: Dataset;
// @Prop({
// type: Object,
// default: () => ({}),
// })
// public datasetHasLicenses;
// Data Property
// public form: InertiaForm<Dataset>; // = useForm<Dataset>(this.dataset as Dataset);
// public form : InertiaForm<Dataset>= useForm<Dataset>([]);
// @Setup(() => useForm<Dataset>(this.dataset as Dataset))
public form: InertiaForm<Dataset>;
// @Hook
created() {
this.form = useForm<Dataset>(this.dataset as Dataset);
// this.form.licenses = this.datasetHasLicenses;
this.form.uploads = [];
}
public mapOptions: MapOptions = {
center: [48.208174, 16.373819],
zoom: 3,
zoomControl: false,
attributionControl: false,
};
public baseMaps: Map<string, LayerOptions> = new Map<string, LayerOptions>();
public fitBounds: LatLngBoundsExpression = [
[46.4318173285, 9.47996951665],
[49.0390742051, 16.9796667823],
];
public mapId = 'test';
mdiImageText = mdiImageText;
mdiArrowLeftBoldOutline = mdiArrowLeftBoldOutline;
mdiPlusCircle = mdiPlusCircle;
mdiFinance = mdiFinance;
mdiTrashCan = mdiTrashCan;
mdiBookOpenPageVariant = mdiBookOpenPageVariant;
mdiEarthPlus = mdiEarthPlus;
stardust = stardust;
// mounted() {
// this.form = useForm<Dataset>(this.dataset as Dataset);// Initialize myData with the value of propValue
// }
// public results: Array<any> = [];
// Component method
public async submit(): Promise<void> {
let route = this.stardust.route('dataset.update', [this.dataset.id]);
// await Inertia.post('/app/register', this.form);
// await router.post('/app/register', this.form);
if (this.form.licenses.every((item) => this.hasIdAttribute(item))) {
this.form.licenses = this.form.licenses.map((obj) => obj.id.toString());
}
await this.form
.transform((data) => ({
...data,
licenses: this.form.licenses.every((item) => this.hasIdAttribute(item))
? this.form.licenses.map((obj) => obj.id.toString())
: this.form.licenses,
rights: 'true',
}))
.put(route);
}
private hasIdAttribute(obj: any): obj is { id: any } {
return typeof obj === 'object' && 'id' in obj;
}
public addTitle(): void {
const newTitle: Title = { value: '', language: '', type: '' };
this.form.titles.push(newTitle);
}
public removeTitle(key: number): void {
this.form.titles.splice(key, 1);
}
public addDescription(): void {
const newDescription = { value: '', language: '', type: '' };
this.form.descriptions.push(newDescription);
}
public removeDescription(key: number): void {
this.form.descriptions.splice(key, 1);
}
public onAddAuthor(person) {
if (this.form.authors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as author' }, 4000);
} else if (this.form.contributors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as contributor' });
} else {
this.form.authors.push(person);
notify({ type: 'info', text: 'person has been successfully added as author' });
}
}
public onAddContributor(person) {
if (this.form.contributors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as contributor' }, 4000);
} else if (this.form.authors.filter((e) => e.id === person.id).length > 0) {
notify({ type: 'warning', title: 'Warning', text: 'person is already defined as author' }, 4000);
} else {
// person.pivot = { contributor_type: '' };
// // person.pivot = { name_type: '', contributor_type: '' };
this.form.contributors.push(person);
notify({ type: 'info', text: 'person has been successfully added as contributor' }, 4000);
}
}
public addKeyword() {
let newSubject: Subject = { value: 'test', language: '', type: 'uncontrolled' };
//this.dataset.files.push(uploadedFiles[i]);
this.form.subjects.push(newSubject);
}
public addReference() {
let newReference = { value: '', label: '', relation: '', type: '' };
//this.dataset.files.push(uploadedFiles[i]);
this.form.references.push(newReference);
}
public removeReference(key) {
this.form.references.splice(key, 1);
}
public onMapInitialized(newItem: any): void {
console.log(newItem);
}
}
export default toNative(EditComponent);
// export default toNative(EditComponent);

View File

@ -39,6 +39,7 @@ export const MainService = defineStore('main', {
authors: [] as Array<Person>, authors: [] as Array<Person>,
// persons: [] as Array<Person>, // persons: [] as Array<Person>,
datasets: [], datasets: [],
files:[],
dataset: {} as Dataset, dataset: {} as Dataset,
}), }),
@ -108,5 +109,19 @@ export const MainService = defineStore('main', {
alert(error.message); alert(error.message);
}); });
}, },
// fetchfiles(id) {
// // sampleDataKey= authors or datasets
// axios
// .get(`api/files/${id}`)
// .then((r) => {
// if (r.data) {
// this[sampleDataKey] = r.data;
// }
// })
// .catch((error) => {
// alert(error.message);
// });
// },
}, },
}); });

View File

@ -172,6 +172,12 @@ Route.group(() => {
.as('dataset.edit') .as('dataset.edit')
.where('id', Route.matchers.number()) .where('id', Route.matchers.number())
.middleware(['auth', 'can:dataset-submit']); .middleware(['auth', 'can:dataset-submit']);
Route.put('/dataset/:id/update', 'DatasetController.update')
.as('dataset.update')
.where('id', Route.matchers.number())
.middleware(['auth', 'can:dataset-submit']);
Route.get('/dataset/:id/delete', 'DatasetController.delete').as('dataset.delete').middleware(['auth', 'can:dataset-delete']); Route.get('/dataset/:id/delete', 'DatasetController.delete').as('dataset.delete').middleware(['auth', 'can:dataset-delete']);
Route.put('/dataset/:id/deleteupdate', 'DatasetController.deleteUpdate') Route.put('/dataset/:id/deleteupdate', 'DatasetController.deleteUpdate')
.as('dataset.deleteUpdate') .as('dataset.deleteUpdate')

View File

@ -16,6 +16,8 @@ Route.group(() => {
Route.get('/dataset/:publish_id', 'DatasetController.findOne').as('dataset.findOne'); Route.get('/dataset/:publish_id', 'DatasetController.findOne').as('dataset.findOne');
Route.get('/sitelinks/:year', 'HomeController.findDocumentsPerYear'); Route.get('/sitelinks/:year', 'HomeController.findDocumentsPerYear');
Route.get('/years', 'HomeController.findYears'); Route.get('/years', 'HomeController.findYears');
Route.get('/download/:id', 'FileController.findOne').as('file.findOne');
}); });
// .middleware("auth:api"); // .middleware("auth:api");
}) })

View File

@ -1,6 +1,17 @@
const { join, resolve, dirname } = require('path'); const { join, resolve, dirname } = require('path');
const Encore = require('@symfony/webpack-encore'); const Encore = require('@symfony/webpack-encore');
const { VueLoaderPlugin } = require('vue-loader'); const { VueLoaderPlugin } = require('vue-loader');
const dotenv = require('dotenv-webpack');
// Load the environment variables from the.env file
Encore.addPlugin(
new dotenv({
path: ".env",
defaults: ".env",
systemvars: true,
allowEmptyValues: true,
})
)
const babelLoader = { const babelLoader = {
// test: /\.js$/, // test: /\.js$/,