Compare commits

..

No commits in common. "master" and "feat/checkReferenceType" have entirely different histories.

84 changed files with 2023 additions and 8761 deletions

View file

@ -35,7 +35,6 @@ export default defineConfig({
() => import('#start/rules/dependent_array_min_length'),
() => import('#start/rules/referenceValidation'),
() => import('#start/rules/valid_mimetype'),
() => import('#start/rules/array_contains_types'),
],
/*
|--------------------------------------------------------------------------

View file

@ -85,9 +85,7 @@ export default class AdminuserController {
// return response.badRequest(error.messages);
throw error;
}
const input: Record<string, any> = request.only(['login', 'email','first_name', 'last_name']);
input.password = request.input('new_password');
const input = request.only(['login', 'email', 'password', 'first_name', 'last_name']);
const user = await User.create(input);
if (request.input('roles')) {
const roles: Array<number> = request.input('roles');
@ -97,6 +95,7 @@ export default class AdminuserController {
session.flash('message', 'User has been created successfully');
return response.redirect().toRoute('settings.user.index');
}
public async show({ request, inertia }: HttpContext) {
const id = request.param('id');
const user = await User.query().where('id', id).firstOrFail();
@ -140,11 +139,9 @@ export default class AdminuserController {
});
// password is optional
let input: Record<string, any>;
if (request.input('new_password')) {
input = request.only(['login', 'email', 'first_name', 'last_name']);
input.password = request.input('new_password');
let input;
if (request.input('password')) {
input = request.only(['login', 'email', 'password', 'first_name', 'last_name']);
} else {
input = request.only(['login', 'email', 'first_name', 'last_name']);
}
@ -159,6 +156,7 @@ export default class AdminuserController {
session.flash('message', 'User has been updated successfully');
return response.redirect().toRoute('settings.user.index');
}
public async destroy({ request, response, session }: HttpContext) {
const id = request.param('id');
const user = await User.findOrFail(id);

View file

@ -64,7 +64,7 @@ export default class MimetypeController {
'maxLength': '{{ field }} must be less then {{ max }} characters long',
'isUnique': '{{ field }} must be unique, and this value is already taken',
'required': '{{ field }} is required',
'file_extension.array.minLength': 'at least {{ min }} mimetypes must be defined',
'file_extension.minLength': 'at least {{ min }} mimetypes must be defined',
'file_extension.*.string': 'Each file extension must be a valid string', // Adjusted to match the type
};

View file

@ -9,7 +9,6 @@ export default class AuthorsController {
// where exists (select * from gba.documents inner join gba.link_documents_persons on "documents"."id" = "link_documents_persons"."document_id"
// where ("link_documents_persons"."role" = 'author') and ("persons"."id" = "link_documents_persons"."person_id"));
const authors = await Person.query()
.preload('datasets')
.where('name_type', 'Personal')
.whereHas('datasets', (dQuery) => {
dQuery.wherePivot('role', 'author');
@ -28,10 +27,7 @@ export default class AuthorsController {
if (request.input('filter')) {
// users = users.whereRaw('name like %?%', [request.input('search')])
const searchTerm = request.input('filter');
authors.andWhere((query) => {
query.whereILike('first_name', `%${searchTerm}%`)
.orWhereILike('last_name', `%${searchTerm}%`);
});
authors.whereILike('first_name', `%${searchTerm}%`).orWhereILike('last_name', `%${searchTerm}%`);
// .orWhere('email', 'like', `%${searchTerm}%`);
}

View file

@ -17,8 +17,7 @@ export default class HomeController {
// .preload('authors')
// .orderBy('server_date_published');
const datasets = await db
.from('documents as doc')
const datasets = await db.from('documents as doc')
.select(['publish_id', 'server_date_published', db.raw(`date_part('year', server_date_published) as pub_year`)])
.where('server_state', serverState)
.innerJoin('link_documents_persons as ba', 'doc.id', 'ba.document_id')
@ -60,6 +59,7 @@ export default class HomeController {
// const year = params.year;
// const from = parseInt(year);
try {
// const datasets = await Database.from('documents as doc')
// .select([Database.raw(`date_part('month', server_date_published) as pub_month`), Database.raw('COUNT(*) as count')])
// .where('server_state', serverState)
@ -68,12 +68,9 @@ export default class HomeController {
// .groupBy('pub_month');
// // .orderBy('server_date_published');
// Calculate the last 4 years including the current year
const currentYear = new Date().getFullYear();
const years = Array.from({ length: 4 }, (_, i) => currentYear - (i + 1)).reverse();
const years = [2021, 2022, 2023]; // Add the second year
const result = await db
.from('documents as doc')
const result = await db.from('documents as doc')
.select([
db.raw(`date_part('year', server_date_published) as pub_year`),
db.raw(`date_part('month', server_date_published) as pub_month`),
@ -86,7 +83,7 @@ export default class HomeController {
.groupBy('pub_year', 'pub_month')
.orderBy('pub_year', 'asc')
.orderBy('pub_month', 'asc');
const labels = Array.from({ length: 12 }, (_, i) => i + 1); // Assuming 12 months
const inputDatasets: Map<string, ChartDataset> = result.reduce((acc, item) => {
@ -103,15 +100,15 @@ export default class HomeController {
acc[pub_year].data[pub_month - 1] = parseInt(count);
return acc;
return acc ;
}, {});
const outputDatasets = Object.entries(inputDatasets).map(([year, data]) => ({
data: data.data,
label: year,
borderColor: data.borderColor,
fill: data.fill,
}));
fill: data.fill
}));
const data = {
labels: labels,
@ -129,11 +126,11 @@ export default class HomeController {
private getRandomHexColor() {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
}
@ -142,4 +139,5 @@ interface ChartDataset {
label: string;
borderColor: string;
fill: boolean;
}

View file

@ -5,7 +5,7 @@ import BackupCode from '#models/backup_code';
// import InvalidCredentialException from 'App/Exceptions/InvalidCredentialException';
import { authValidator } from '#validators/auth';
import hash from '@adonisjs/core/services/hash';
import db from '@adonisjs/lucid/services/db';
import TwoFactorAuthProvider from '#app/services/TwoFactorAuthProvider';
// import { Authenticator } from '@adonisjs/auth';
// import { LoginState } from 'Contracts/enums';
@ -29,10 +29,6 @@ export default class AuthController {
const { email, password } = request.only(['email', 'password']);
try {
await db.connection().rawQuery('SELECT 1')
// // attempt to verify credential and login user
// await auth.use('web').attempt(email, plainPassword);
@ -55,9 +51,6 @@ export default class AuthController {
await auth.use('web').login(user);
} catch (error) {
if (error.code === 'ECONNREFUSED') {
throw error
}
// if login fails, return vague form message and redirect back
session.flash('message', 'Your username, email, or password is incorrect');
return response.redirect().back();

View file

@ -18,33 +18,9 @@ import { HttpException } from 'node-exceptions';
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
import vine, { SimpleMessagesProvider } from '@vinejs/vine';
import mail from '@adonisjs/mail/services/main';
// import { resolveMx } from 'dns/promises';
// import * as net from 'net';
import { validate } from 'deep-email-validator';
import {
TitleTypes,
DescriptionTypes,
ContributorTypes,
PersonNameTypes,
ReferenceIdentifierTypes,
RelationTypes,
SubjectTypes,
DatasetTypes,
} from '#contracts/enums';
import { TransactionClientContract } from '@adonisjs/lucid/types/database';
import db from '@adonisjs/lucid/services/db';
import Project from '#models/project';
import License from '#models/license';
import Language from '#models/language';
import File from '#models/file';
import Coverage from '#models/coverage';
import Title from '#models/title';
import Description from '#models/description';
import Subject from '#models/subject';
import DatasetReference from '#models/dataset_reference';
import Collection from '#models/collection';
import CollectionRole from '#models/collection_role';
import { updateEditorDatasetValidator } from '#validators/dataset';
import { savePersons } from '#app/utils/utility-functions';
// Create a new instance of the client
const client = new Client({ node: 'http://localhost:9200' }); // replace with your OpenSearch endpoint
@ -87,15 +63,8 @@ export default class DatasetsController {
}
datasets.orderBy(attribute, sortOrder);
} else {
// datasets.orderBy('id', 'asc');
// Custom ordering to prioritize rejected_editor state
datasets.orderByRaw(`
CASE
WHEN server_state = 'rejected_reviewer' THEN 0
ELSE 1
END ASC,
id ASC
`);
// users.orderBy('created_at', 'desc');
datasets.orderBy('id', 'asc');
}
// const users = await User.query().orderBy('login').paginate(page, limit);
@ -248,10 +217,6 @@ export default class DatasetsController {
if (dataset.reject_reviewer_note != null) {
dataset.reject_reviewer_note = null;
}
if (dataset.reject_editor_note != null) {
dataset.reject_editor_note = null;
}
//save main and additional titles
const reviewer_id = request.input('reviewer_id', null);
@ -290,7 +255,70 @@ export default class DatasetsController {
});
}
// private async checkEmailDomain(email: string): Promise<boolean> {
// const domain = email.split('@')[1];
// try {
// // Step 1: Check MX records for the domain
// const mxRecords = await resolveMx(domain);
// if (mxRecords.length === 0) {
// return false; // No MX records, can't send email
// }
// // Sort MX records by priority
// mxRecords.sort((a, b) => a.priority - b.priority);
// // Step 2: Attempt SMTP connection to the first available mail server
// const smtpServer = mxRecords[0].exchange;
// return await this.checkMailboxExists(smtpServer, email);
// } catch (error) {
// console.error('Error during MX lookup or SMTP validation:', error);
// return false;
// }
// }
//// Helper function to check if the mailbox exists using SMTP
// private async checkMailboxExists(smtpServer: string, email: string): Promise<boolean> {
// return new Promise((resolve, reject) => {
// const socket = net.createConnection(25, smtpServer);
// socket.on('connect', () => {
// socket.write(`HELO ${smtpServer}\r\n`);
// socket.write(`MAIL FROM: <test@example.com>\r\n`);
// socket.write(`RCPT TO: <${email}>\r\n`);
// });
// socket.on('data', (data) => {
// const response = data.toString();
// if (response.includes('250')) {
// // 250 is an SMTP success code
// socket.end();
// resolve(true); // Email exists
// } else if (response.includes('550')) {
// // 550 means the mailbox doesn't exist
// socket.end();
// resolve(false); // Email doesn't exist
// }
// });
// socket.on('error', (error) => {
// console.error('SMTP connection error:', error);
// socket.end();
// resolve(false);
// });
// socket.on('end', () => {
// // SMTP connection closed
// });
// socket.setTimeout(5000, () => {
// // Timeout after 5 seconds
// socket.end();
// resolve(false); // Assume email doesn't exist if no response
// });
// });
// }
public async rejectUpdate({ request, response, auth }: HttpContext) {
const authUser = auth.user!;
@ -325,7 +353,7 @@ export default class DatasetsController {
return response
.flash(
`Invalid server state. Dataset with id ${id} cannot be rejected. Datset has server state ${dataset.server_state}.`,
'warning',
'warning'
)
.redirect()
.toRoute('editor.dataset.list');
@ -360,9 +388,7 @@ export default class DatasetsController {
emailStatusMessage = ` A rejection email was successfully sent to ${dataset.user.email}.`;
} catch (error) {
logger.error(error);
return response
.flash('Dataset has not been rejected due to an email error: ' + error.message, 'error')
.toRoute('editor.dataset.list');
return response.flash('Dataset has not been rejected due to an email error: ' + error.message, 'error').toRoute('editor.dataset.list');
}
} else {
emailStatusMessage = ` However, the email could not be sent because the submitter's email address (${dataset.user.email}) is not valid.`;
@ -378,7 +404,7 @@ export default class DatasetsController {
.toRoute('editor.dataset.list');
}
public async publish({ request, inertia, response, auth }: HttpContext) {
public async publish({ request, inertia, response }: HttpContext) {
const id = request.param('id');
const dataset = await Dataset.query()
@ -402,14 +428,8 @@ export default class DatasetsController {
.back();
}
return inertia.render('Editor/Dataset/Publish', {
dataset,
can: {
reject: await auth.user?.can(['dataset-editor-reject']),
publish: await auth.user?.can(['dataset-publish']),
},
});
}
@ -451,119 +471,6 @@ export default class DatasetsController {
}
}
public async rejectToReviewer({ request, inertia, response }: HttpContext) {
const id = request.param('id');
const dataset = await Dataset.query()
.where('id', id)
.preload('reviewer', (builder) => {
builder.select('id', 'login', 'email');
})
.firstOrFail();
const validStates = ['reviewed'];
if (!validStates.includes(dataset.server_state)) {
// session.flash('errors', 'Invalid server state!');
return response
.flash(
'warning',
`Invalid server state. Dataset with id ${id} cannot be rejected to the reviewer. Datset has server state ${dataset.server_state}.`,
)
.redirect()
.toRoute('editor.dataset.list');
}
return inertia.render('Editor/Dataset/RejectToReviewer', {
dataset,
});
}
public async rejectToReviewerUpdate({ request, response, auth }: HttpContext) {
const authUser = auth.user!;
const id = request.param('id');
const dataset = await Dataset.query()
.where('id', id)
.preload('reviewer', (builder) => {
builder.select('id', 'login', 'email');
})
.firstOrFail();
const newSchema = vine.object({
server_state: vine.string().trim(),
reject_editor_note: vine.string().trim().minLength(10).maxLength(500),
send_mail: vine.boolean().optional(),
});
try {
// await request.validate({ schema: newSchema });
const validator = vine.compile(newSchema);
await request.validateUsing(validator);
} catch (error) {
// return response.badRequest(error.messages);
throw error;
}
const validStates = ['reviewed'];
if (!validStates.includes(dataset.server_state)) {
// throw new Error('Invalid server state!');
// return response.flash('warning', 'Invalid server state. Dataset cannot be released to editor').redirect().back();
return response
.flash(
`Invalid server state. Dataset with id ${id} cannot be rejected to reviewer. Datset has server state ${dataset.server_state}.`,
'warning',
)
.redirect()
.toRoute('editor.dataset.list');
}
dataset.server_state = 'rejected_to_reviewer';
const rejectEditorNote = request.input('reject_editor_note', '');
dataset.reject_editor_note = rejectEditorNote;
// add logic for sending reject message
const sendMail = request.input('send_email', false);
// const validRecipientEmail = await this.checkEmailDomain('arno.kaimbacher@outlook.at');
const validationResult = await validate({
email: dataset.reviewer.email,
validateSMTP: false,
});
const validRecipientEmail: boolean = validationResult.valid;
await dataset.save();
let emailStatusMessage = '';
if (sendMail == true) {
if (dataset.reviewer.email && validRecipientEmail) {
try {
await mail.send((message) => {
message.to(dataset.reviewer.email).subject('Dataset Rejection Notification').html(`
<p>Dear ${dataset.reviewer.login},</p>
<p>Your dataset with ID ${dataset.id} has been rejected.</p>
<p>Reason for rejection: ${rejectEditorNote}</p>
<p>Best regards,<br>Your Tethys editor: ${authUser.login}</p>
`);
});
emailStatusMessage = ` A rejection email was successfully sent to ${dataset.reviewer.email}.`;
} catch (error) {
logger.error(error);
return response
.flash('Dataset has not been rejected due to an email error: ' + error.message, 'error')
.toRoute('editor.dataset.list');
}
} else {
emailStatusMessage = ` However, the email could not be sent because the submitter's email address (${dataset.reviewer.email}) is not valid.`;
}
}
return response
.flash(
`You have successfully rejected dataset ${dataset.id} reviewed by ${dataset.reviewer.login}.${emailStatusMessage}`,
'message',
)
.toRoute('editor.dataset.list');
}
public async doiCreate({ request, inertia }: HttpContext) {
const id = request.param('id');
const dataset = await Dataset.query()
@ -629,376 +536,10 @@ export default class DatasetsController {
public async show({}: HttpContext) {}
public async edit({ request, inertia, response }: HttpContext) {
const id = request.param('id');
const datasetQuery = Dataset.query().where('id', id);
datasetQuery
.preload('titles', (query) => query.orderBy('id', 'asc'))
.preload('descriptions', (query) => query.orderBy('id', 'asc'))
.preload('coverage')
.preload('licenses')
.preload('authors', (query) => query.orderBy('pivot_sort_order', 'asc'))
.preload('contributors', (query) => query.orderBy('pivot_sort_order', 'asc'))
// .preload('subjects')
.preload('subjects', (builder) => {
builder.orderBy('id', 'asc').withCount('datasets');
})
.preload('references')
.preload('files', (query) => {
query.orderBy('sort_order', 'asc'); // Sort by sort_order column
});
const dataset = await datasetQuery.firstOrFail();
const validStates = ['editor_accepted', 'rejected_reviewer'];
if (!validStates.includes(dataset.server_state)) {
// session.flash('errors', 'Invalid server state!');
return response
.flash(
`Invalid server state. Dataset with id ${id} cannot be edited. Datset has server state ${dataset.server_state}.`,
'warning',
)
.toRoute('editor.dataset.list');
}
const titleTypes = Object.entries(TitleTypes)
.filter(([value]) => value !== 'Main')
.map(([key, value]) => ({ value: key, label: value }));
const descriptionTypes = Object.entries(DescriptionTypes)
.filter(([value]) => value !== 'Abstract')
.map(([key, value]) => ({ value: key, label: value }));
const languages = await Language.query().where('active', true).pluck('part1', 'part1');
// const contributorTypes = Config.get('enums.contributor_types');
const contributorTypes = Object.entries(ContributorTypes).map(([key, value]) => ({ value: key, label: value }));
// const nameTypes = Config.get('enums.name_types');
const nameTypes = Object.entries(PersonNameTypes).map(([key, value]) => ({ value: key, label: value }));
// const messages = await Database.table('messages')
// .pluck('help_text', 'metadata_element');
const projects = await Project.query().pluck('label', 'id');
const currentDate = new Date();
const currentYear = currentDate.getFullYear();
const years = Array.from({ length: currentYear - 1990 + 1 }, (_, index) => 1990 + index);
const licenses = await License.query().select('id', 'name_long').where('active', 'true').pluck('name_long', 'id');
// const userHasRoles = user.roles;
// const datasetHasLicenses = await dataset.related('licenses').query().pluck('id');
// const checkeds = dataset.licenses.first().id;
// const doctypes = {
// analysisdata: { label: 'Analysis', value: 'analysisdata' },
// measurementdata: { label: 'Measurements', value: 'measurementdata' },
// monitoring: 'Monitoring',
// remotesensing: 'Remote Sensing',
// gis: 'GIS',
// models: 'Models',
// mixedtype: 'Mixed Type',
// };
return inertia.render('Editor/Dataset/Edit', {
dataset,
titletypes: titleTypes,
descriptiontypes: descriptionTypes,
contributorTypes,
nameTypes,
languages,
// messages,
projects,
licenses,
// datasetHasLicenses: Object.keys(datasetHasLicenses).map((key) => datasetHasLicenses[key]), //convert object to array with license ids
// checkeds,
years,
// languages,
subjectTypes: SubjectTypes,
referenceIdentifierTypes: Object.entries(ReferenceIdentifierTypes).map(([key, value]) => ({ value: key, label: value })),
relationTypes: Object.entries(RelationTypes).map(([key, value]) => ({ value: key, label: value })),
doctypes: DatasetTypes,
});
}
public async update({ request, response, session }: HttpContext) {
// Get the dataset id from the route parameter
const datasetId = request.param('id');
// Retrieve the dataset and load its existing files
const dataset = await Dataset.findOrFail(datasetId);
await dataset.load('files');
let trx: TransactionClientContract | null = null;
try {
await request.validateUsing(updateEditorDatasetValidator);
trx = await db.transaction();
// const user = (await User.find(auth.user?.id)) as User;
// await this.createDatasetAndAssociations(user, request, trx);
const dataset = await Dataset.findOrFail(datasetId);
// 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 savePersons(dataset, request.input('authors', []), 'author', trx);
await 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);
}
}
// Process all subjects/keywords from the request
const subjects = request.input('subjects');
for (const subjectData of subjects) {
// Case 1: Subject already exists in the database (has an ID)
if (subjectData.id) {
// Retrieve the existing subject
const existingSubject = await Subject.findOrFail(subjectData.id);
// Update subject properties from the request data
existingSubject.value = subjectData.value;
existingSubject.type = subjectData.type;
existingSubject.external_key = subjectData.external_key;
// Only save if there are actual changes
if (existingSubject.$isDirty) {
await existingSubject.save();
}
// Note: The relationship between dataset and subject is already established,
// so we don't need to attach it again
}
// Case 2: New subject being added (no ID)
else {
// Check if a subject with the same value and type already exists in the database
const subject = await Subject.firstOrNew({ value: subjectData.value, type: subjectData.type }, subjectData);
if (subject.$isNew === true) {
// If it's a completely new subject, create and associate it with the dataset
await dataset.useTransaction(trx).related('subjects').save(subject);
} else {
// If the subject already exists, just create the relationship
await dataset.useTransaction(trx).related('subjects').attach([subject.id]);
}
}
}
const subjectsToDelete = request.input('subjectsToDelete', []);
for (const subjectData of subjectsToDelete) {
if (subjectData.id) {
// const subject = await Subject.findOrFail(subjectData.id);
const subject = await Subject.query()
.where('id', subjectData.id)
.preload('datasets', (builder) => {
builder.orderBy('id', 'asc');
})
.withCount('datasets')
.firstOrFail();
// Check if the subject is used by multiple datasets
if (subject.$extras.datasets_count > 1) {
// If used by multiple datasets, just detach it from the current dataset
await dataset.useTransaction(trx).related('subjects').detach([subject.id]);
} else {
// If only used by this dataset, delete the subject completely
await dataset.useTransaction(trx).related('subjects').detach([subject.id]);
await subject.useTransaction(trx).delete();
}
}
}
// Process references
const references = request.input('references', []);
// First, get existing references to determine which ones to update vs. create
const existingReferences = await dataset.related('references').query();
const existingReferencesMap: Map<number, DatasetReference> = new Map(existingReferences.map((ref) => [ref.id, ref]));
for (const referenceData of references) {
if (existingReferencesMap.has(referenceData.id) && referenceData.id) {
// Update existing reference
const reference = existingReferencesMap.get(referenceData.id);
if (reference) {
reference.merge(referenceData);
if (reference.$isDirty) {
await reference.useTransaction(trx).save();
}
}
} else {
// Create new reference
const dataReference = new DatasetReference();
dataReference.fill(referenceData);
await dataset.useTransaction(trx).related('references').save(dataReference);
}
}
// Handle references to delete if provided
const referencesToDelete = request.input('referencesToDelete', []);
for (const referenceData of referencesToDelete) {
if (referenceData.id) {
const reference = await DatasetReference.findOrFail(referenceData.id);
await reference.useTransaction(trx).delete();
}
}
// save coverage
const coverageData = request.input('coverage');
if (coverageData) {
if (coverageData.id) {
const coverage = await Coverage.findOrFail(coverageData.id);
coverage.merge(coverageData);
if (coverage.$isDirty) {
await coverage.useTransaction(trx).save();
}
}
}
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 has been updated successfully');
session.flash('message', 'Dataset has been updated successfully');
// return response.redirect().toRoute('user.index');
return response.redirect().toRoute('editor.dataset.edit', [dataset.id]);
} catch (error) {
if (trx !== null) {
await trx.rollback();
}
console.error('Failed to update dataset and related models:', error);
// throw new ValidationException(true, { 'upload error': `failed to create dataset and related models. ${error}` });
throw error;
}
}
public async categorize({ inertia, request, response }: HttpContext) {
const id = request.param('id');
// Preload dataset and its "collections" relation
const dataset = await Dataset.query().where('id', id).preload('collections').firstOrFail();
const validStates = ['editor_accepted', 'rejected_reviewer'];
if (!validStates.includes(dataset.server_state)) {
// session.flash('errors', 'Invalid server state!');
return response
.flash(
'warning',
`Invalid server state. Dataset with id ${id} cannot be edited. Datset has server state ${dataset.server_state}.`,
)
.redirect()
.toRoute('editor.dataset.list');
}
const collectionRoles = await CollectionRole.query()
.whereIn('name', ['ddc', 'ccs'])
.preload('collections', (coll: Collection) => {
// preloa only top level collection with noparent_id
coll.whereNull('parent_id').orderBy('number', 'asc');
})
.exec();
return inertia.render('Editor/Dataset/Category', {
collectionRoles: collectionRoles,
dataset: dataset,
relatedCollections: dataset.collections,
});
}
public async categorizeUpdate({ request, response, session }: HttpContext) {
// Get the dataset id from the route parameter
const id = request.param('id');
const dataset = await Dataset.query().preload('files').where('id', id).firstOrFail();
const validStates = ['editor_accepted', 'rejected_reviewer'];
if (!validStates.includes(dataset.server_state)) {
return response
.flash(
'warning',
`Invalid server state. Dataset with id ${id} cannot be categorized. Dataset has server state ${dataset.server_state}.`,
)
.redirect()
.toRoute('editor.dataset.list');
}
let trx: TransactionClientContract | null = null;
try {
trx = await db.transaction();
// const user = (await User.find(auth.user?.id)) as User;
// await this.createDatasetAndAssociations(user, request, trx);
// Retrieve the selected collections from the request.
// This should be an array of collection ids.
const collections: number[] = request.input('collections', []);
// Synchronize the dataset collections using the transaction.
await dataset.useTransaction(trx).related('collections').sync(collections);
// Commit the transaction.await trx.commit()
await trx.commit();
// Redirect with a success flash message.
// return response.flash('success', 'Dataset collections updated successfully!').redirect().toRoute('dataset.list');
session.flash('message', 'Dataset collections updated successfully!');
return response.redirect().toRoute('editor.dataset.list');
} catch (error) {
if (trx !== null) {
await trx.rollback();
}
console.error('Failed tocatgorize dataset collections:', error);
// throw new ValidationException(true, { 'upload error': `failed to create dataset and related models. ${error}` });
throw error;
}
}
public async edit({}: HttpContext) {}
// public async update({}: HttpContextContract) {}
public async updateOpensearch({ response }: HttpContext) {
public async update({ response }: HttpContext) {
const id = 273; //request.param('id');
const dataset = await Dataset.query().preload('xmlCache').where('id', id).firstOrFail();
// add xml elements
@ -1114,19 +655,6 @@ export default class DatasetsController {
}
}
public async download({ params, response }: HttpContext) {
const id = params.id;
// Find the file by ID
const file = await File.findOrFail(id);
// const filePath = await drive.use('local').getUrl('/'+ file.filePath)
const filePath = file.filePath;
const fileExt = file.filePath.split('.').pop() || '';
// Set the response headers and download the file
response.header('Content-Type', file.mime_type || 'application/octet-stream');
response.attachment(`${file.label}.${fileExt}`);
return response.download(filePath);
}
public async destroy({}: HttpContext) {}
private async createXmlRecord(dataset: Dataset, datasetNode: XMLBuilder) {

View file

@ -9,7 +9,6 @@ import vine from '@vinejs/vine';
import mail from '@adonisjs/mail/services/main';
import logger from '@adonisjs/core/services/logger';
import { validate } from 'deep-email-validator';
import File from '#models/file';
interface Dictionary {
[index: string]: string;
@ -39,21 +38,13 @@ export default class DatasetsController {
}
datasets.orderBy(attribute, sortOrder);
} else {
// datasets.orderBy('id', 'asc');
// Custom ordering to prioritize rejected_editor state
datasets.orderByRaw(`
CASE
WHEN server_state = 'rejected_to_reviewer' THEN 0
ELSE 1
END ASC,
id ASC
`);
// users.orderBy('created_at', 'desc');
datasets.orderBy('id', 'asc');
}
// const users = await User.query().orderBy('login').paginate(page, limit);
const myDatasets = await datasets
// .where('server_state', 'approved')
.whereIn('server_state', ['approved', 'rejected_to_reviewer'])
.where('server_state', 'approved')
.where('reviewer_id', user.id)
.preload('titles')
@ -71,52 +62,7 @@ export default class DatasetsController {
});
}
public async review({ request, inertia, response, auth }: HttpContext) {
const id = request.param('id');
const datasetQuery = Dataset.query().where('id', id);
datasetQuery
.preload('titles', (query) => query.orderBy('id', 'asc'))
.preload('descriptions', (query) => query.orderBy('id', 'asc'))
.preload('coverage')
.preload('licenses')
.preload('authors', (query) => query.orderBy('pivot_sort_order', 'asc'))
.preload('contributors', (query) => query.orderBy('pivot_sort_order', 'asc'))
// .preload('subjects')
.preload('subjects', (builder) => {
builder.orderBy('id', 'asc').withCount('datasets');
})
.preload('references')
.preload('project')
.preload('files', (query) => {
query.orderBy('sort_order', 'asc'); // Sort by sort_order column
});
const dataset = await datasetQuery.firstOrFail();
const validStates = ['approved', 'rejected_to_reviewer'];
if (!validStates.includes(dataset.server_state)) {
// session.flash('errors', 'Invalid server state!');
return response
.flash(
'warning',
`Invalid server state. Dataset with id ${id} cannot be reviewed. Datset has server state ${dataset.server_state}.`,
)
.redirect()
.toRoute('reviewer.dataset.list');
}
return inertia.render('Reviewer/Dataset/Review', {
dataset,
can: {
review: await auth.user?.can(['dataset-review']),
reject: await auth.user?.can(['dataset-review-reject']),
},
});
}
public async review_old({ request, inertia, response, auth }: HttpContext) {
public async review({ request, inertia, response }: HttpContext) {
const id = request.param('id');
const dataset = await Dataset.query()
.where('id', id)
@ -212,10 +158,6 @@ export default class DatasetsController {
return inertia.render('Reviewer/Dataset/Review', {
dataset,
fields: fields,
can: {
review: await auth.user?.can(['dataset-review']),
reject: await auth.user?.can(['dataset-review-reject']),
},
});
}
@ -224,7 +166,7 @@ export default class DatasetsController {
// const { id } = params;
const dataset = await Dataset.findOrFail(id);
const validStates = ['approved', 'rejected_to_reviewer'];
const validStates = ['approved'];
if (!validStates.includes(dataset.server_state)) {
// throw new Error('Invalid server state!');
// return response.flash('warning', 'Invalid server state. Dataset cannot be released to editor').redirect().back();
@ -238,10 +180,6 @@ export default class DatasetsController {
}
dataset.server_state = 'reviewed';
// if editor has rejected to reviewer:
if (dataset.reject_editor_note != null) {
dataset.reject_editor_note = null;
}
try {
// await dataset.related('editor').associate(user); // speichert schon ab
@ -265,7 +203,7 @@ export default class DatasetsController {
})
.firstOrFail();
const validStates = ['approved', 'rejected_to_reviewer'];
const validStates = ['approved'];
if (!validStates.includes(dataset.server_state)) {
// session.flash('errors', 'Invalid server state!');
return response
@ -312,12 +250,12 @@ export default class DatasetsController {
throw error;
}
const validStates = ['approved', 'rejected_to_reviewer'];
const validStates = ['approved'];
if (!validStates.includes(dataset.server_state)) {
// throw new Error('Invalid server state!');
// return response.flash('warning', 'Invalid server state. Dataset cannot be released to editor').redirect().back();
return response
.flash(
.flash(
`Invalid server state. Dataset with id ${id} cannot be rejected. Datset has server state ${dataset.server_state}.`,
'warning',
)
@ -369,17 +307,4 @@ export default class DatasetsController {
.toRoute('reviewer.dataset.list')
.flash(`You have rejected dataset ${dataset.id}! to editor ${dataset.editor.login}`, 'message');
}
public async download({ params, response }: HttpContext) {
const id = params.id;
// Find the file by ID
const file = await File.findOrFail(id);
// const filePath = await drive.use('local').getUrl('/'+ file.filePath)
const filePath = file.filePath;
const fileExt = file.filePath.split('.').pop() || '';
// Set the response headers and download the file
response.header('Content-Type', file.mime_type || 'application/octet-stream');
response.attachment(`${file.label}.${fileExt}`);
return response.download(filePath);
}
}

View file

@ -29,30 +29,23 @@ import {
} from '#contracts/enums';
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
import DatasetReference from '#models/dataset_reference';
import { cuid } from '@adonisjs/core/helpers';
import File from '#models/file';
import ClamScan from 'clamscan';
// import { ValidationException } from '@adonisjs/validator';
// import Drive from '@ioc:Adonis/Core/Drive';
// import drive from '#services/drive';
import drive from '@adonisjs/drive/services/main';
import path from 'path';
import { Exception } from '@adonisjs/core/exceptions';
import { MultipartFile } from '@adonisjs/core/types/bodyparser';
import * as crypto from 'crypto';
import { pipeline } from 'node:stream/promises';
import { createWriteStream } from 'node:fs';
import type { Multipart } from '@adonisjs/bodyparser';
import * as fs from 'fs';
import { parseBytesSize, getConfigFor, getTmpPath, formatBytes } from '#app/utils/utility-functions';
interface Dictionary {
[index: string]: string;
}
import vine, { SimpleMessagesProvider, errors } from '@vinejs/vine';
export default class DatasetController {
/**
* Bodyparser config
*/
// config: BodyParserConfig = config.get('bodyparser');
public async index({ auth, request, inertia }: HttpContext) {
const user = (await User.find(auth.user?.id)) as User;
const page = request.input('page', 1);
@ -76,16 +69,8 @@ export default class DatasetController {
}
datasets.orderBy(attribute, sortOrder);
} else {
// datasets.orderBy('id', 'asc');
// Custom ordering to prioritize rejected_editor state
datasets.orderByRaw(`
CASE
WHEN server_state = 'rejected_editor' THEN 0
WHEN server_state = 'rejected_reviewer' THEN 1
ELSE 2
END ASC,
id ASC
`);
// users.orderBy('created_at', 'desc');
datasets.orderBy('id', 'asc');
}
// const results = await Database
@ -206,8 +191,7 @@ export default class DatasetController {
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}),
)
// .minLength(2)
.arrayContainsTypes({ typeA: 'main', typeB: 'translated' }),
.minLength(1),
descriptions: vine
.array(
vine.object({
@ -221,8 +205,7 @@ export default class DatasetController {
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}),
)
// .minLength(1),
.arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }),
.minLength(1),
authors: vine
.array(
vine.object({
@ -297,8 +280,7 @@ export default class DatasetController {
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}),
)
// .minLength(2)
.arrayContainsTypes({ typeA: 'main', typeB: 'translated' }),
.minLength(1),
descriptions: vine
.array(
vine.object({
@ -312,8 +294,7 @@ export default class DatasetController {
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}),
)
// .minLength(1),
.arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }),
.minLength(1),
authors: vine
.array(
vine.object({
@ -421,99 +402,21 @@ export default class DatasetController {
}
public async store({ auth, request, response, session }: HttpContext) {
// At the top of the store() method, declare an array to hold temporary file paths
const uploadedTmpFiles: string[] = [];
// Aggregated limit example (adjust as needed)
const multipartConfig = getConfigFor('multipart');
const aggregatedLimit = multipartConfig.limit ? parseBytesSize(multipartConfig.limit) : 100 * 1024 * 1024;
// const aggregatedLimit = 200 * 1024 * 1024;
let totalUploadedSize = 0;
// // Helper function to format bytes as human-readable text
// function formatBytes(bytes: number): string {
// if (bytes === 0) return '0 Bytes';
// const k = 1024;
// const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
// const i = Math.floor(Math.log(bytes) / Math.log(k));
// return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
// }
// const enabledExtensions = await this.getEnabledExtensions();
const multipart: Multipart = request.multipart;
multipart.onFile('files', { deferValidations: true }, async (part) => {
// Attach an individual file size accumulator if needed
let fileUploadedSize = 0;
// Simply accumulate the size in on('data') without performing the expensive check per chunk
part.on('data', (chunk) => {
// reporter(chunk);
// Increase counters using the chunk length
fileUploadedSize += chunk.length;
});
// After the file is completely read, update the global counter and check aggregated limit
part.on('end', () => {
totalUploadedSize += fileUploadedSize;
part.file.size = fileUploadedSize;
// Record the temporary file path
if (part.file.tmpPath) {
uploadedTmpFiles.push(part.file.tmpPath);
}
if (totalUploadedSize > aggregatedLimit) {
// Clean up all temporary files if aggregate limit is exceeded
uploadedTmpFiles.forEach((tmpPath) => {
try {
fs.unlinkSync(tmpPath);
} catch (cleanupError) {
console.error('Error cleaning up temporary file:', cleanupError);
}
});
const error = new errors.E_VALIDATION_ERROR({
'upload error': `Aggregated upload limit of ${formatBytes(aggregatedLimit)} exceeded. The total size of files being uploaded would exceed the limit.`,
});
request.multipart.abort(error);
}
});
part.on('error', (error) => {
// fileUploadError = error;
request.multipart.abort(error);
});
// await pipeline(part, createWriteStream(filePath));
// return { filePath };
// Process file with error handling
try {
// Extract extension from the client file name, e.g. "Tethys 5 - Ampflwang_dataset.zip"
const ext = path.extname(part.file.clientName).replace('.', '');
// Attach the extracted extension to the file object for later use
part.file.extname = ext;
// part.file.sortOrder = part.file.sortOrder;
const tmpPath = getTmpPath(multipartConfig);
(part.file as any).tmpPath = tmpPath;
const writeStream = createWriteStream(tmpPath);
await pipeline(part, writeStream);
} catch (error) {
request.multipart.abort(new errors.E_VALIDATION_ERROR({ 'upload error': error.message }));
}
});
// node ace make:validator CreateDataset
try {
await multipart.process();
// // Instead of letting an error abort the controller, check if any error occurred
// Step 2 - Validate request body against the schema
// await request.validate({ schema: newDatasetSchema, messages: this.messages });
// await request.validate(CreateDatasetValidator);
await request.validateUsing(createDatasetValidator);
// console.log({ payload });
} catch (error) {
// This is where you'd expect to catch any errors.
session.flash('errors', error.messages);
return response.redirect().back();
// Step 3 - Handle errors
// return response.badRequest(error.messages);
throw error;
}
let trx: TransactionClientContract | null = null;
try {
await request.validateUsing(createDatasetValidator);
trx = await db.transaction();
const user = (await User.find(auth.user?.id)) as User;
@ -522,14 +425,6 @@ export default class DatasetController {
await trx.commit();
console.log('Dataset and related models created successfully');
} catch (error) {
// Clean up temporary files if validation or later steps fail
uploadedTmpFiles.forEach((tmpPath) => {
try {
fs.unlinkSync(tmpPath);
} catch (cleanupError) {
console.error('Error cleaning up temporary file:', cleanupError);
}
});
if (trx !== null) {
await trx.rollback();
}
@ -542,19 +437,14 @@ export default class DatasetController {
return response.redirect().toRoute('dataset.list');
// return response.redirect().back();
}
private async createDatasetAndAssociations(
user: User,
request: HttpContext['request'],
trx: TransactionClientContract,
// uploadedFiles: Array<MultipartFile>,
) {
private async createDatasetAndAssociations(user: User, request: HttpContext['request'], trx: TransactionClientContract) {
// Create a new instance of the Dataset model:
const dataset = new Dataset();
dataset.type = request.input('type');
dataset.creating_corporation = request.input('creating_corporation');
dataset.language = request.input('language');
dataset.embargo_date = request.input('embargo_date');
dataset.project_id = request.input('project_id');
//await dataset.related('user').associate(user); // speichert schon ab
// Dataset.$getRelation('user').boot();
// Dataset.$getRelation('user').setRelated(dataset, user);
@ -663,7 +553,7 @@ export default class DatasetController {
newFile.fileSize = file.size;
newFile.mimeType = mimeType;
newFile.label = file.clientName;
newFile.sortOrder = index + 1;
newFile.sortOrder = index;
newFile.visibleInFrontdoor = true;
newFile.visibleInOai = true;
// let path = coverImage.filePath;
@ -814,8 +704,6 @@ export default class DatasetController {
'files.array.minLength': 'At least {{ min }} file upload is required.',
'files.*.size': 'file size is to big',
'files.*.extnames': 'file extension is not supported',
'embargo_date.date.afterOrEqual': `Embargo date must be on or after ${dayjs().add(10, 'day').format('DD.MM.YYYY')}`,
};
// public async release({ params, view }) {
@ -926,7 +814,7 @@ export default class DatasetController {
// throw new GeneralException(trans('exceptions.publish.release.update_error'));
}
public async edit({ request, inertia, response, auth }: HttpContext) {
public async edit({ request, inertia, response }: HttpContext) {
const id = request.param('id');
const datasetQuery = Dataset.query().where('id', id);
datasetQuery
@ -934,8 +822,8 @@ export default class DatasetController {
.preload('descriptions', (query) => query.orderBy('id', 'asc'))
.preload('coverage')
.preload('licenses')
.preload('authors', (query) => query.orderBy('pivot_sort_order', 'asc'))
.preload('contributors', (query) => query.orderBy('pivot_sort_order', 'asc'))
.preload('authors')
.preload('contributors')
// .preload('subjects')
.preload('subjects', (builder) => {
builder.orderBy('id', 'asc').withCount('datasets');
@ -951,9 +839,10 @@ export default class DatasetController {
// session.flash('errors', 'Invalid server state!');
return response
.flash(
`Invalid server state. Dataset with id ${id} cannot be edited. Datset has server state ${dataset.server_state}.`,
'warning',
`Invalid server state. Dataset with id ${id} cannot be edited. Datset has server state ${dataset.server_state}.`,
)
.redirect()
.toRoute('dataset.list');
}
@ -987,15 +876,15 @@ export default class DatasetController {
// const datasetHasLicenses = await dataset.related('licenses').query().pluck('id');
// const checkeds = dataset.licenses.first().id;
// const doctypes = {
// analysisdata: { label: 'Analysis', value: 'analysisdata' },
// measurementdata: { label: 'Measurements', value: 'measurementdata' },
// monitoring: 'Monitoring',
// remotesensing: 'Remote Sensing',
// gis: 'GIS',
// models: 'Models',
// mixedtype: 'Mixed Type',
// };
const doctypes = {
analysisdata: { label: 'Analysis', value: 'analysisdata' },
measurementdata: { label: 'Measurements', value: 'measurementdata' },
monitoring: 'Monitoring',
remotesensing: 'Remote Sensing',
gis: 'GIS',
models: 'Models',
mixedtype: 'Mixed Type',
};
return inertia.render('Submitter/Dataset/Edit', {
dataset,
@ -1014,95 +903,25 @@ export default class DatasetController {
subjectTypes: SubjectTypes,
referenceIdentifierTypes: Object.entries(ReferenceIdentifierTypes).map(([key, value]) => ({ value: key, label: value })),
relationTypes: Object.entries(RelationTypes).map(([key, value]) => ({ value: key, label: value })),
doctypes: DatasetTypes,
can: {
edit: await auth.user?.can(['dataset-edit']),
delete: await auth.user?.can(['dataset-delete']),
},
doctypes,
});
}
public async update({ request, response, session }: HttpContext) {
// Get the dataset id from the route parameter
const datasetId = request.param('id');
// Retrieve the dataset and load its existing files
const dataset = await Dataset.findOrFail(datasetId);
await dataset.load('files');
// Accumulate the size of the already related files
// const preExistingFileSize = dataset.files.reduce((acc, file) => acc + file.fileSize, 0);
let preExistingFileSize = 0;
for (const file of dataset.files) {
preExistingFileSize += Number(file.fileSize);
try {
// await request.validate(UpdateDatasetValidator);
await request.validateUsing(updateDatasetValidator);
} catch (error) {
// - Handle errors
// return response.badRequest(error.messages);
throw error;
// return response.badRequest(error.messages);
}
const uploadedTmpFiles: string[] = [];
// Only process multipart if the request has a multipart content type
const contentType = request.request.headers['content-type'] || '';
if (contentType.includes('multipart/form-data')) {
const multipart: Multipart = request.multipart;
// Aggregated limit example (adjust as needed)
const multipartConfig = getConfigFor('multipart');
const aggregatedLimit = multipartConfig.limit ? parseBytesSize(multipartConfig.limit) : 100 * 1024 * 1024;
// Initialize totalUploadedSize with the size of existing files
let totalUploadedSize = preExistingFileSize;
multipart.onFile('files', { deferValidations: true }, async (part) => {
let fileUploadedSize = 0;
part.on('data', (chunk) => {
fileUploadedSize += chunk.length;
});
part.on('end', () => {
totalUploadedSize += fileUploadedSize;
part.file.size = fileUploadedSize;
if (part.file.tmpPath) {
uploadedTmpFiles.push(part.file.tmpPath);
}
if (totalUploadedSize > aggregatedLimit) {
uploadedTmpFiles.forEach((tmpPath) => {
try {
fs.unlinkSync(tmpPath);
} catch (cleanupError) {
console.error('Error cleaning up temporary file:', cleanupError);
}
});
const error = new errors.E_VALIDATION_ERROR({
'upload error': `Aggregated upload limit of ${formatBytes(aggregatedLimit)} exceeded. The total size of files being uploaded would exceed the limit.`,
});
request.multipart.abort(error);
}
});
part.on('error', (error) => {
request.multipart.abort(error);
});
try {
const fileNameWithoutParams = part.file.clientName.split('?')[0];
const ext = path.extname(fileNameWithoutParams).replace('.', '');
part.file.extname = ext;
const tmpPath = getTmpPath(multipartConfig);
(part.file as any).tmpPath = tmpPath;
const writeStream = createWriteStream(tmpPath);
await pipeline(part, writeStream);
} catch (error) {
request.multipart.abort(new errors.E_VALIDATION_ERROR({ 'upload error': error.message }));
}
});
try {
await multipart.process();
} catch (error) {
session.flash('errors', error.messages);
return response.redirect().back();
}
}
// await request.validate(UpdateDatasetValidator);
const id = request.param('id');
let trx: TransactionClientContract | null = null;
try {
await request.validateUsing(updateDatasetValidator);
trx = await db.transaction();
// const user = (await User.find(auth.user?.id)) as User;
// await this.createDatasetAndAssociations(user, request, trx);
@ -1163,97 +982,22 @@ export default class DatasetController {
}
}
// Process all subjects/keywords from the request
const subjects = request.input('subjects');
for (const subjectData of subjects) {
// Case 1: Subject already exists in the database (has an ID)
if (subjectData.id) {
// Retrieve the existing subject
const existingSubject = await Subject.findOrFail(subjectData.id);
// Update subject properties from the request data
existingSubject.value = subjectData.value;
existingSubject.type = subjectData.type;
existingSubject.external_key = subjectData.external_key;
// Only save if there are actual changes
if (existingSubject.$isDirty) {
await existingSubject.save();
}
// Note: The relationship between dataset and subject is already established,
// so we don't need to attach it again
}
// Case 2: New subject being added (no ID)
else {
// Check if a subject with the same value and type already exists in the database
const subject = await Subject.firstOrNew({ value: subjectData.value, type: subjectData.type }, subjectData);
if (subject.$isNew === true) {
// If it's a completely new subject, create and associate it with the dataset
await dataset.useTransaction(trx).related('subjects').save(subject);
} else {
// If the subject already exists, just create the relationship
await dataset.useTransaction(trx).related('subjects').attach([subject.id]);
}
}
}
const subjectsToDelete = request.input('subjectsToDelete', []);
for (const subjectData of subjectsToDelete) {
if (subjectData.id) {
// const subject = await Subject.findOrFail(subjectData.id);
const subject = await Subject.query()
.where('id', subjectData.id)
.preload('datasets', (builder) => {
builder.orderBy('id', 'asc');
})
.withCount('datasets')
.firstOrFail();
// Check if the subject is used by multiple datasets
if (subject.$extras.datasets_count > 1) {
// If used by multiple datasets, just detach it from the current dataset
await dataset.useTransaction(trx).related('subjects').detach([subject.id]);
} else {
// If only used by this dataset, delete the subject completely
await dataset.useTransaction(trx).related('subjects').detach([subject.id]);
await subject.useTransaction(trx).delete();
}
}
}
// Process references
const references = request.input('references', []);
// First, get existing references to determine which ones to update vs. create
const existingReferences = await dataset.related('references').query();
const existingReferencesMap: Map<number, DatasetReference> = new Map(existingReferences.map((ref) => [ref.id, ref]));
for (const referenceData of references) {
if (existingReferencesMap.has(referenceData.id) && referenceData.id) {
// Update existing reference
const reference = existingReferencesMap.get(referenceData.id);
if (reference) {
reference.merge(referenceData);
if (reference.$isDirty) {
await reference.useTransaction(trx).save();
}
// await dataset.useTransaction(trx).related('subjects').sync([]);
const keywords = request.input('subjects');
for (const keywordData of keywords) {
if (keywordData.id) {
const subject = await Subject.findOrFail(keywordData.id);
// await dataset.useTransaction(trx).related('subjects').attach([keywordData.id]);
subject.value = keywordData.value;
subject.type = keywordData.type;
subject.external_key = keywordData.external_key;
if (subject.$isDirty) {
await subject.save();
}
} else {
// Create new reference
const dataReference = new DatasetReference();
dataReference.fill(referenceData);
await dataset.useTransaction(trx).related('references').save(dataReference);
}
}
// Handle references to delete if provided
const referencesToDelete = request.input('referencesToDelete', []);
for (const referenceData of referencesToDelete) {
if (referenceData.id) {
const reference = await DatasetReference.findOrFail(referenceData.id);
await reference.useTransaction(trx).delete();
const keyword = new Subject();
keyword.fill(keywordData);
await dataset.useTransaction(trx).related('subjects').save(keyword, false);
}
}
@ -1285,9 +1029,9 @@ export default class DatasetController {
// handle new uploaded files:
const uploadedFiles: MultipartFile[] = request.files('files');
if (Array.isArray(uploadedFiles) && uploadedFiles.length > 0) {
for (const [index, file] of uploadedFiles.entries()) {
for (const [index, fileData] of uploadedFiles.entries()) {
try {
await this.scanFileForViruses(file.tmpPath); //, 'gitea.lan', 3310);
await this.scanFileForViruses(fileData.tmpPath); //, 'gitea.lan', 3310);
// await this.scanFileForViruses("/tmp/testfile.txt");
} catch (error) {
// If the file is infected or there's an error scanning the file, throw a validation exception
@ -1295,29 +1039,29 @@ export default class DatasetController {
}
// move to disk:
const fileName = this.generateFilename(file.extname as string);
const fileName = `file-${cuid()}.${fileData.extname}`; //'file-ls0jyb8xbzqtrclufu2z2e0c.pdf'
const datasetFolder = `files/${dataset.id}`; // 'files/307'
const datasetFullPath = path.join(`${datasetFolder}`, fileName);
// await file.moveToDisk(datasetFolder, { name: fileName, overwrite: true }, 'local');
// await file.move(drive.makePath(datasetFolder), {
// await fileData.moveToDisk(datasetFolder, { name: fileName, overwrite: true }, 'local');
// await fileData.move(drive.makePath(datasetFolder), {
// name: fileName,
// overwrite: true, // overwrite in case of conflict
// });
await file.moveToDisk(datasetFullPath, 'local', {
await fileData.moveToDisk(datasetFullPath, 'local', {
name: fileName,
overwrite: true, // overwrite in case of conflict
disk: 'local',
});
//save to db:
const { clientFileName, sortOrder } = this.extractVariableNameAndSortOrder(file.clientName);
const mimeType = file.headers['content-type'] || 'application/octet-stream'; // Fallback to a default MIME type
const { clientFileName, sortOrder } = this.extractVariableNameAndSortOrder(fileData.clientName);
const mimeType = fileData.headers['content-type'] || 'application/octet-stream'; // Fallback to a default MIME type
const newFile = await dataset
.useTransaction(trx)
.related('files')
.create({
pathName: `${datasetFolder}/${fileName}`,
fileSize: file.size,
fileSize: fileData.size,
mimeType,
label: clientFileName,
sortOrder: sortOrder || index,
@ -1357,24 +1101,16 @@ export default class DatasetController {
await dataset.useTransaction(trx).save();
await trx.commit();
console.log('Dataset has been updated successfully');
console.log('Dataset and related models created successfully');
session.flash('message', 'Dataset has been updated successfully');
// return response.redirect().toRoute('user.index');
return response.redirect().toRoute('dataset.edit', [dataset.id]);
} catch (error) {
// Clean up temporary files if validation or later steps fail
uploadedTmpFiles.forEach((tmpPath) => {
try {
fs.unlinkSync(tmpPath);
} catch (cleanupError) {
console.error('Error cleaning up temporary file:', cleanupError);
}
});
if (trx !== null) {
await trx.rollback();
}
console.error('Failed to update dataset and related models:', error);
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;
}
@ -1500,7 +1236,6 @@ export default class DatasetController {
}
const collectionRoles = await CollectionRole.query()
.whereIn('name', ['ddc', 'ccs'])
.preload('collections', (coll: Collection) => {
// preloa only top level collection with noparent_id
coll.whereNull('parent_id').orderBy('number', 'asc');
@ -1540,7 +1275,7 @@ export default class DatasetController {
// This should be an array of collection ids.
const collections: number[] = request.input('collections', []);
// Synchronize the dataset collections using the transaction.
// Synchronize the dataset collections using the transaction.
await dataset.useTransaction(trx).related('collections').sync(collections);
// Commit the transaction.await trx.commit()

View file

@ -1,43 +0,0 @@
// import { Exception } from '@adonisjs/core/exceptions'
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http';
export default class DbHandlerException extends ExceptionHandler {
// constructor() {
// super(Logger)
// }
async handle(error: any, ctx: HttpContext) {
// Check for AggregateError type
if (error.type === 'AggregateError' && error.aggregateErrors) {
const dbErrors = error.aggregateErrors.some((err: any) => err.code === 'ECONNREFUSED' && err.port === 5432);
if (dbErrors) {
return ctx.response.status(503).json({
status: 'error',
message: 'PostgreSQL database connection failed. Please ensure the database service is running.',
details: {
code: error.code,
type: error.type,
ports: error.aggregateErrors.map((err: any) => ({
port: err.port,
address: err.address,
})),
},
});
}
}
// Handle simple ECONNREFUSED errors
if (error.code === 'ECONNREFUSED') {
return ctx.response.status(503).json({
status: 'error',
message: 'Database connection failed. Please ensure PostgreSQL is running.',
code: error.code,
});
}
return super.handle(error, ctx);
}
static status = 500;
}

View file

@ -46,7 +46,6 @@ export default class HttpExceptionHandler extends ExceptionHandler {
// return view.render('./errors/server-error', { error });
// },
// };
protected statusPages: Record<StatusPageRange, StatusPageRenderer> = {
'404': (error, { inertia }) => {
return inertia.render('Errors/ServerError', {
@ -59,47 +58,9 @@ export default class HttpExceptionHandler extends ExceptionHandler {
return inertia.render('Errors/ServerError', {
error: error.message,
code: error.status,
});
},
// '500': (error, { inertia }) => {
// return inertia.render('Errors/postgres_error', {
// status: 'error',
// message: 'PostgreSQL database connection failed. Please ensure the database service is running.',
// details: {
// code: error.code,
// type: error.status,
// ports: error.errors.map((err: any) => ({
// port: err.port,
// address: err.address,
// })),
// },
// });
// },
'500..599': (error, { inertia }) => {
if (error.code === 'ECONNREFUSED') {
const dbErrors = error.errors.some((err: any) => err.code === 'ECONNREFUSED' && err.port === 5432);
if (dbErrors) {
return inertia.render('Errors/postgres_error', {
status: 'error',
message: 'PostgreSQL database connection failed. Please ensure the database service is running.',
details: {
code: error.code,
type: error.status,
ports: error.errors.map((err: any) => ({
port: err.port,
address: err.address,
})),
},
});
}
} else {
return inertia.render('Errors/ServerError', {
error: error.message,
code: error.status,
});
}
});
},
'500..599': (error, { inertia }) => inertia.render('Errors/ServerError', { error: error.message, code: error.status }),
};
// constructor() {
@ -107,7 +68,7 @@ export default class HttpExceptionHandler extends ExceptionHandler {
// }
public async handle(error: any, ctx: HttpContext) {
const { response, request, session, inertia } = ctx;
const { response, request, session } = ctx;
/**
* Handle failed authentication attempt
@ -121,47 +82,6 @@ export default class HttpExceptionHandler extends ExceptionHandler {
// return response.redirect('/dashboard');
// }
// Handle Axios errors
if (error.code === 'ECONNREFUSED') {
const dbErrors = error.errors.some((err: any) => err.code === 'ECONNREFUSED' && err.port === 5432);
if (dbErrors) {
// return ctx.response.status(503).json({
// status: 'error',
// message: 'PostgreSQL database connection failed. Please ensure the database service is running.',
// details: {
// code: error.code,
// type: error.status,
// ports: error.errors.map((err: any) => ({
// port: err.port,
// address: err.address,
// })),
// },
// });
// return inertia.render('Errors/postgres_error', {
// status: 'error',
// message: 'PostgreSQL database connection failed. Please ensure the database service is running.',
// details: {
// code: error.code,
// type: error.status,
// ports: error.errors.map((err: any) => ({
// port: err.port,
// address: err.address,
// })),
// },
// });
}
}
// Handle simple ECONNREFUSED errors
// if (error.code === 'ECONNREFUSED') {
// return ctx.response.status(503).json({
// status: 'error',
// message: 'Database connection failed. Please ensure PostgreSQL is running.',
// code: error.code,
// });
// }
// https://github.com/inertiajs/inertia-laravel/issues/56
// let test = response.getStatus(); //200
// let header = request.header('X-Inertia'); // true
@ -178,21 +98,12 @@ export default class HttpExceptionHandler extends ExceptionHandler {
// ->toResponse($request)
// ->setStatusCode($response->status());
}
// Handle simple ECONNREFUSED errors
// if (error.code === 'ECONNREFUSED') {
// return ctx.response.status(503).json({
// status: 'error',
// message: 'Database connection failed. Please ensure PostgreSQL is running.',
// code: error.code,
// });
// }
// Dynamically change the error templates based on the absence of X-Inertia header
// if (!ctx.request.header('X-Inertia')) {
// this.statusPages = {
// '401..403': (error, { inertia }) => inertia.render('Errors/ServerError', { error: error.message, code: error.status }),
// '404': (error, { inertia }) => inertia.render('Errors/ServerError', { error: error.message, code: error.status }),
// '500..599': (error, { inertia }) => inertia.render('Errors/ServerError', { error: error.message, code: error.status }),
// '401..403': (error, { view }) => view.render('./errors/unauthorized', { error }),
// '404': (error, { view }) => view.render('./errors/not-found', { error }),
// '500..599': (error, { view }) => view.render('./errors/server-error', { error }),
// };
// }

View file

@ -3,7 +3,7 @@ import { DateTime } from 'luxon';
import dayjs from 'dayjs';
import Dataset from './dataset.js';
import BaseModel from './base_model.js';
import type { ManyToMany } from '@adonisjs/lucid/types/relations';
import type { ManyToMany } from "@adonisjs/lucid/types/relations";
export default class Person extends BaseModel {
public static namingStrategy = new SnakeCaseNamingStrategy();
@ -64,8 +64,9 @@ export default class Person extends BaseModel {
// return '2023-03-21 08:45:00';
// }
@computed({
serializeAs: 'dataset_count',
serializeAs: 'dataset_count',
})
public get datasetCount() {
const stock = this.$extras.datasets_count; //my pivot column name was "stock"
@ -78,16 +79,6 @@ export default class Person extends BaseModel {
return contributor_type;
}
@computed({ serializeAs: 'allow_email_contact' })
public get allowEmailContact() {
// If the datasets relation is missing or empty, return false instead of null.
if (!this.datasets || this.datasets.length === 0) {
return false;
}
// Otherwise return the pivot attribute from the first related dataset.
return this.datasets[0].$extras?.pivot_allow_email_contact;
}
@manyToMany(() => Dataset, {
pivotForeignKey: 'person_id',
pivotRelatedForeignKey: 'document_id',

View file

@ -1,57 +0,0 @@
/**
* Qs module config
*/
type QueryStringConfig = {
depth?: number
allowPrototypes?: boolean
plainObjects?: boolean
parameterLimit?: number
arrayLimit?: number
ignoreQueryPrefix?: boolean
delimiter?: RegExp | string
allowDots?: boolean
charset?: 'utf-8' | 'iso-8859-1' | undefined
charsetSentinel?: boolean
interpretNumericEntities?: boolean
parseArrays?: boolean
comma?: boolean
}
/**
* Base config used by all types
*/
type BodyParserBaseConfig = {
encoding: string
limit: string | number
types: string[]
}
/**
* Body parser config for parsing JSON requests
*/
export type BodyParserJSONConfig = BodyParserBaseConfig & {
strict: boolean
convertEmptyStringsToNull: boolean
}
/**
* Parser config for parsing form data
*/
export type BodyParserFormConfig = BodyParserBaseConfig & {
queryString: QueryStringConfig
convertEmptyStringsToNull: boolean
}
/**
* Parser config for parsing raw body (untouched)
*/
export type BodyParserRawConfig = BodyParserBaseConfig
/**
* Body parser config for all supported form types
*/
export type BodyParserConfig = {
allowedMethods: string[]
json: BodyParserJSONConfig
form: BodyParserFormConfig
raw: BodyParserRawConfig
multipart: BodyParserMultipartConfig
}

View file

@ -1,16 +1,3 @@
import { join, isAbsolute } from 'node:path';
import type { BodyParserConfig } from '#models/types';
import { createId } from '@paralleldrive/cuid2';
import { tmpdir } from 'node:os';
import config from '@adonisjs/core/services/config';
import Dataset from '#models/dataset';
import { TransactionClientContract } from '@adonisjs/lucid/types/database';
import Person from '#models/person';
interface Dictionary {
[index: string]: string;
}
export function sum(a: number, b: number): number {
return a + b;
}
@ -37,88 +24,3 @@ export function preg_match(regex: RegExp, str: string) {
const result: boolean = regex.test(str);
return result;
}
/**
* Returns the tmp path for storing the files temporarly
*/
export function getTmpPath(config: BodyParserConfig['multipart']): string {
if (typeof config.tmpFileName === 'function') {
const tmpPath = config.tmpFileName();
return isAbsolute(tmpPath) ? tmpPath : join(tmpdir(), tmpPath);
}
return join(tmpdir(), createId());
}
/**
* Returns config for a given type
*/
export function getConfigFor<K extends keyof BodyParserConfig>(type: K): BodyParserConfig[K] {
const bodyParserConfig: BodyParserConfig = config.get('bodyparser');
const configType = bodyParserConfig[type];
return configType;
}
export function parseBytesSize(size: string): number {
const units: Record<string, number> = {
kb: 1024,
mb: 1024 * 1024,
gb: 1024 * 1024 * 1024,
tb: 1024 * 1024 * 1024 * 1024,
};
const match = size.match(/^(\d+)(kb|mb|gb|tb)$/i); // Regex to match size format
if (!match) {
throw new Error('Invalid size format');
}
const [, value, unit] = match;
return parseInt(value) * units[unit.toLowerCase()];
}
// Helper function to format bytes as human-readable text
export function formatBytes(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
export async function savePersons(dataset: Dataset, persons: any[], role: string, trx: TransactionClientContract) {
for (const [key, person] of persons.entries()) {
const pivotData = {
role: role,
sort_order: key + 1,
allow_email_contact: false,
...extractPivotAttributes(person), // Merge pivot attributes here
};
if (person.id !== undefined) {
await dataset
.useTransaction(trx)
.related('persons')
.attach({
[person.id]: pivotData,
});
} else {
const dataPerson = new Person();
dataPerson.fill(person);
await dataset.useTransaction(trx).related('persons').save(dataPerson, false, pivotData);
}
}
}
// Helper function to extract pivot attributes from a person object
function extractPivotAttributes(person: any) {
const pivotAttributes: Dictionary = {};
for (const key in person) {
if (key.startsWith('pivot_')) {
// pivotAttributes[key] = person[key];
const cleanKey = key.replace('pivot_', ''); // Remove 'pivot_' prefix
pivotAttributes[cleanKey] = person[key];
}
}
return pivotAttributes;
}

View file

@ -40,8 +40,7 @@ export const createDatasetValidator = vine.compile(
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}),
)
// .minLength(2)
.arrayContainsTypes({ typeA: 'main', typeB: 'translated' }),
.minLength(1),
descriptions: vine
.array(
vine.object({
@ -55,8 +54,7 @@ export const createDatasetValidator = vine.compile(
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}),
)
// .minLength(1),
.arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }),
.minLength(1),
authors: vine
.array(
vine.object({
@ -158,7 +156,8 @@ export const createDatasetValidator = vine.compile(
.fileScan({ removeInfected: true }),
)
.minLength(1),
}),);
}),
);
/**
* Validates the dataset's update action
@ -188,8 +187,7 @@ export const updateDatasetValidator = vine.compile(
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}),
)
// .minLength(2)
.arrayContainsTypes({ typeA: 'main', typeB: 'translated' }),
.minLength(1),
descriptions: vine
.array(
vine.object({
@ -203,7 +201,7 @@ export const updateDatasetValidator = vine.compile(
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}),
)
.arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }),
.minLength(1),
authors: vine
.array(
vine.object({
@ -314,137 +312,12 @@ export const updateDatasetValidator = vine.compile(
}),
);
export const updateEditorDatasetValidator = vine.compile(
vine.object({
// first step
language: vine
.string()
.trim()
.regex(/^[a-zA-Z0-9]+$/),
licenses: vine.array(vine.number()).minLength(1), // define at least one license for the new dataset
rights: vine.string().in(['true']),
// second step
type: vine.string().trim().minLength(3).maxLength(255),
creating_corporation: vine.string().trim().minLength(3).maxLength(255),
titles: vine
.array(
vine.object({
value: vine.string().trim().minLength(3).maxLength(255),
type: vine.enum(Object.values(TitleTypes)),
language: vine
.string()
.trim()
.minLength(2)
.maxLength(255)
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}),
)
// .minLength(2)
.arrayContainsTypes({ typeA: 'main', typeB: 'translated' }),
descriptions: vine
.array(
vine.object({
value: vine.string().trim().minLength(3).maxLength(2500),
type: vine.enum(Object.values(DescriptionTypes)),
language: vine
.string()
.trim()
.minLength(2)
.maxLength(255)
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
}),
)
.arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }),
authors: vine
.array(
vine.object({
email: vine
.string()
.trim()
.maxLength(255)
.email()
.normalizeEmail()
.isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
first_name: vine.string().trim().minLength(3).maxLength(255),
last_name: vine.string().trim().minLength(3).maxLength(255),
}),
)
.minLength(1)
.distinct('email'),
contributors: vine
.array(
vine.object({
email: vine
.string()
.trim()
.maxLength(255)
.email()
.normalizeEmail()
.isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
first_name: vine.string().trim().minLength(3).maxLength(255),
last_name: vine.string().trim().minLength(3).maxLength(255),
pivot_contributor_type: vine.enum(Object.keys(ContributorTypes)),
}),
)
.distinct('email')
.optional(),
// third step
project_id: vine.number().optional(),
// embargo_date: schema.date.optional({ format: 'yyyy-MM-dd' }, [rules.after(10, 'days')]),
embargo_date: vine
.date({
formats: ['YYYY-MM-DD'],
})
.afterOrEqual((_field) => {
return dayjs().add(10, 'day').format('YYYY-MM-DD');
})
.optional(),
coverage: vine.object({
x_min: vine.number(),
x_max: vine.number(),
y_min: vine.number(),
y_max: vine.number(),
elevation_absolut: vine.number().positive().optional(),
elevation_min: vine.number().positive().optional().requiredIfExists('elevation_max'),
elevation_max: vine.number().positive().optional().requiredIfExists('elevation_min'),
// type: vine.enum(Object.values(DescriptionTypes)),
depth_absolut: vine.number().negative().optional(),
depth_min: vine.number().negative().optional().requiredIfExists('depth_max'),
depth_max: vine.number().negative().optional().requiredIfExists('depth_min'),
time_abolute: vine.date({ formats: { utc: true } }).optional(),
time_min: vine
.date({ formats: { utc: true } })
.beforeField('time_max')
.optional()
.requiredIfExists('time_max'),
time_max: vine
.date({ formats: { utc: true } })
.afterField('time_min')
.optional()
.requiredIfExists('time_min'),
}),
references: vine
.array(
vine.object({
value: vine.string().trim().minLength(3).maxLength(255).validateReference({ typeField: 'type' }),
type: vine.enum(Object.values(ReferenceIdentifierTypes)),
relation: vine.enum(Object.values(RelationTypes)),
label: vine.string().trim().minLength(2).maxLength(255),
}),
)
.optional(),
subjects: vine
.array(
vine.object({
value: vine.string().trim().minLength(3).maxLength(255),
// pivot_contributor_type: vine.enum(Object.keys(ContributorTypes)),
language: vine.string().trim().minLength(2).maxLength(255),
}),
)
.minLength(3)
.distinct('value'),
}),
);
// files: schema.array([rules.minLength(1)]).members(
// schema.file({
// size: '512mb',
// extnames: ['jpg', 'gif', 'png', 'tif', 'pdf', 'zip', 'fgb', 'nc', 'qml', 'ovr', 'gpkg', 'gml', 'gpx', 'kml', 'kmz', 'json'],
// }),
// ),
let messagesProvider = new SimpleMessagesProvider({
'minLength': '{{ field }} must be at least {{ min }} characters long',
@ -496,10 +369,8 @@ let messagesProvider = new SimpleMessagesProvider({
'files.array.minLength': 'At least {{ min }} file upload is required.',
'files.*.size': 'file size is to big',
'files.*.extnames': 'file extension is not supported',
'embargo_date.date.afterOrEqual': `Embargo date must be on or after ${dayjs().add(10, 'day').format('DD.MM.YYYY')}`,
});
createDatasetValidator.messagesProvider = messagesProvider;
updateDatasetValidator.messagesProvider = messagesProvider;
updateEditorDatasetValidator.messagesProvider = messagesProvider;
// export default createDatasetValidator;

View file

@ -16,7 +16,7 @@ export const createUserValidator = vine.compile(
first_name: vine.string().trim().minLength(3).maxLength(255),
last_name: vine.string().trim().minLength(3).maxLength(255),
email: vine.string().maxLength(255).email().normalizeEmail().isUnique({ table: 'accounts', column: 'email' }),
new_password: vine.string().confirmed({ confirmationField: 'password_confirmation' }).trim().minLength(3).maxLength(60),
password: vine.string().confirmed().trim().minLength(3).maxLength(60),
roles: vine.array(vine.number()).minLength(1), // define at least one role for the new user
}),
);
@ -42,7 +42,7 @@ export const updateUserValidator = vine.withMetaData<{ objId: number }>().compil
.email()
.normalizeEmail()
.isUnique({ table: 'accounts', column: 'email', whereNot: (field) => field.meta.objId }),
new_password: vine.string().confirmed({ confirmationField: 'password_confirmation' }).trim().minLength(3).maxLength(60).optional(),
password: vine.string().confirmed().trim().minLength(3).maxLength(60).optional(),
roles: vine.array(vine.number()).minLength(1), // define at least one role for the new user
}),
);

View file

@ -128,7 +128,7 @@ allowedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],
| projects/:id/file
| ```
*/
processManually: ['/submitter/dataset/submit', '/submitter/dataset/:id/update'],
processManually: [],
/*
|--------------------------------------------------------------------------
@ -185,8 +185,8 @@ allowedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],
| and fields data.
|
*/
limit: '513mb',
//limit: env.get('UPLOAD_LIMIT', '513mb'),
// limit: '20mb',
limit: env.get('UPLOAD_LIMIT', '513mb'),
/*
|--------------------------------------------------------------------------

View file

@ -21,7 +21,6 @@ export enum ServerStates {
rejected_reviewer = 'rejected_reviewer',
rejected_editor = 'rejected_editor',
reviewed = 'reviewed',
rejected_to_reviewer = 'rejected_to_reviewer',
}
// for table dataset_titles

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -86,22 +86,3 @@ export default class Documents extends BaseSchema {
// CONSTRAINT documents_server_state_check CHECK (server_state::text = ANY (ARRAY['deleted'::character varying::text, 'inprogress'::character varying::text, 'published'::character varying::text, 'released'::character varying::text, 'editor_accepted'::character varying::text, 'approved'::character varying::text, 'rejected_reviewer'::character varying::text, 'rejected_editor'::character varying::text, 'reviewed'::character varying::text])),
// CONSTRAINT documents_type_check CHECK (type::text = ANY (ARRAY['analysisdata'::character varying::text, 'measurementdata'::character varying::text, 'monitoring'::character varying::text, 'remotesensing'::character varying::text, 'gis'::character varying::text, 'models'::character varying::text, 'mixedtype'::character varying::text]))
// )
// ALTER TABLE documents DROP CONSTRAINT documents_server_state_check;
// ALTER TABLE documents
// ADD CONSTRAINT documents_server_state_check CHECK (
// server_state::text = ANY (ARRAY[
// 'deleted',
// 'inprogress',
// 'published',
// 'released',
// 'editor_accepted',
// 'approved',
// 'rejected_reviewer',
// 'rejected_editor',
// 'reviewed',
// 'rejected_to_reviewer' -- new value added
// ]::text[])
// );

View file

@ -32,21 +32,3 @@ export default class CollectionsRoles extends BaseSchema {
// visible_oai boolean NOT NULL DEFAULT true,
// CONSTRAINT collections_roles_pkey PRIMARY KEY (id)
// )
// change to normal intzeger:
// ALTER TABLE collections_roles ALTER COLUMN id DROP DEFAULT;
// DROP SEQUENCE IF EXISTS collections_roles_id_seq;
// -- Step 1: Temporarily change one ID to a value not currently used
// UPDATE collections_roles SET id = 99 WHERE name = 'ccs';
// -- Step 2: Change 'ddc' ID to 2 (the old 'ccs' ID)
// UPDATE collections_roles SET id = 2 WHERE name = 'ddc';
// -- Step 3: Change the temporary ID (99) to 3 (the old 'ddc' ID)
// UPDATE collections_roles SET id = 3 WHERE name = 'ccs';
// UPDATE collections_roles SET id = 99 WHERE name = 'bk';
// UPDATE collections_roles SET id = 1 WHERE name = 'institutes';
// UPDATE collections_roles SET id = 4 WHERE name = 'pacs';
// UPDATE collections_roles SET id = 7 WHERE name = 'bk';

View file

@ -5,7 +5,7 @@ export default class Collections extends BaseSchema {
public async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id');//.defaultTo("nextval('collections_id_seq')");
table.increments('id').defaultTo("nextval('collections_id_seq')");
table.integer('role_id').unsigned();
table
.foreign('role_id', 'collections_role_id_foreign')
@ -25,8 +25,6 @@ export default class Collections extends BaseSchema {
.onUpdate('CASCADE');
table.boolean('visible').notNullable().defaultTo(true);
table.boolean('visible_publish').notNullable().defaultTo(true);
table.integer('left_id').unsigned();
table.integer('right_id').unsigned();
});
}
@ -61,26 +59,3 @@ export default class Collections extends BaseSchema {
// change to normal intzeger:
// ALTER TABLE collections ALTER COLUMN id DROP DEFAULT;
// DROP SEQUENCE IF EXISTS collections_id_seq;
// ALTER TABLE collections
// ADD COLUMN left_id INTEGER;
// COMMENT ON COLUMN collections.left_id IS 'comment';
// ALTER TABLE collections
// ADD COLUMN right_id INTEGER;
// COMMENT ON COLUMN collections.right_id IS 'comment';
// -- Step 1: Drop the existing default
// ALTER TABLE collections
// ALTER COLUMN visible DROP DEFAULT,
// ALTER COLUMN visible_publish DROP DEFAULT;
// -- Step 2: Change column types with proper casting
// ALTER TABLE collections
// ALTER COLUMN visible TYPE smallint USING CASE WHEN visible THEN 1 ELSE 0 END,
// ALTER COLUMN visible_publish TYPE smallint USING CASE WHEN visible_publish THEN 1 ELSE 0 END;
// -- Step 3: Set new defaults as smallint
// ALTER TABLE collections
// ALTER COLUMN visible SET DEFAULT 1,
// ALTER COLUMN visible_publish SET DEFAULT 1;

6
index.d.ts vendored
View file

@ -183,9 +183,3 @@ declare module 'saxon-js' {
export function transform(options: ITransformOptions): Promise<ITransformOutput> | ITransformOutput;
}
declare global {
interface File {
sort_order?: number;
}
}

2271
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -58,7 +58,7 @@
"eslint-plugin-prettier": "^5.0.0-alpha.2",
"hot-hook": "^0.4.0",
"numeral": "^2.0.6",
"pinia": "^3.0.2",
"pinia": "^2.0.30",
"pino-pretty": "^13.0.0",
"postcss-loader": "^8.1.1",
"prettier": "^3.4.2",
@ -76,7 +76,6 @@
},
"dependencies": {
"@adonisjs/auth": "^9.2.4",
"@adonisjs/bodyparser": "^10.0.1",
"@adonisjs/core": "^6.17.0",
"@adonisjs/cors": "^2.2.1",
"@adonisjs/drive": "^3.2.0",
@ -97,7 +96,6 @@
"@phc/format": "^1.0.0",
"@poppinss/manager": "^5.0.2",
"@vinejs/vine": "^3.0.0",
"argon2": "^0.43.0",
"axios": "^1.7.9",
"bcrypt": "^5.1.1",
"bcryptjs": "^2.4.3",
@ -117,7 +115,7 @@
"notiwind": "^2.0.0",
"pg": "^8.9.0",
"qrcode": "^1.5.3",
"redis": "^5.0.0",
"redis": "^4.6.10",
"reflect-metadata": "^0.2.1",
"saxon-js": "^2.5.0",
"toastify-js": "^1.12.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -1,3 +0,0 @@
[ZoneTransfer]
ZoneId=3
HostUrl=https://sea1.geoinformation.dev/favicon-32x32.png

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 952 KiB

View file

@ -1 +0,0 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View file

@ -1,7 +1,7 @@
/* @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500&display=swap'); */
/* @import url('https://fonts.googleapis.com/css?family=Roboto:400,400i,600,700'); */
/* @import '_checkbox-radio-switch.css'; */
@import '_checkbox-radio-switch.css';
@import '_progress.css';
@import '_scrollbars.css';
@import '_table.css';

View file

@ -39,10 +39,6 @@ const props = defineProps({
type: String,
default: null,
},
allowEmailContact: {
type: Boolean,
default: false,
}
});
const pillType = computed(() => {
@ -85,8 +81,9 @@ const pillType = computed(() => {
<h4 class="text-xl text-ellipsis">
{{ name }}
</h4>
<p class="text-gray-500 dark:text-slate-400">
<div v-if="props.allowEmailContact"> {{ email }}</div>
<p class="text-gray-500 dark:text-slate-400">
<!-- {{ date }} @ {{ login }} -->
{{ email }}
</p>
</div>
</BaseLevel>

View file

@ -61,10 +61,10 @@ const cancel = () => confirmCancel('cancel');
<CardBox
v-show="value"
:title="title"
class="p-4 shadow-lg max-h-modal w-11/12 md:w-3/5 lg:w-2/5 xl:w-4/12 z-50"
class="shadow-lg max-h-modal w-11/12 md:w-3/5 lg:w-2/5 xl:w-4/12 z-50"
:header-icon="mdiClose"
modal
@header-icon-click="cancel"
@header-icon-click="cancel"
>
<div class="space-y-3">
<h1 v-if="largeTitle" class="text-2xl">

View file

@ -1,75 +0,0 @@
<script setup lang="ts">
import { computed, useSlots } from 'vue';
const props = defineProps({
title: {
type: String,
default: null,
},
icon: {
type: String,
default: null,
},
showHeaderIcon: {
type: Boolean,
default: true,
},
headerIcon: {
type: String,
default: null,
},
rounded: {
type: String,
default: 'rounded-xl',
},
hasFormData: Boolean,
empty: Boolean,
form: Boolean,
hoverable: Boolean,
modal: Boolean,
});
const emit = defineEmits(['header-icon-click', 'submit']);
const is = computed(() => (props.form ? 'form' : 'div'));
const slots = useSlots();
// const footer = computed(() => slots.footer && !!slots.footer());
const componentClass = computed(() => {
const base = [props.rounded, props.modal ? 'dark:bg-slate-900' : 'dark:bg-slate-900/70'];
if (props.hoverable) {
base.push('hover:shadow-lg transition-shadow duration-500');
}
return base;
});
// const headerIconClick = () => {
// emit('header-icon-click');
// };
// const submit = (e) => {
// emit('submit', e);
// };
</script>
<template>
<component :is="is" :class="componentClass" class="bg-white flex flex-col border border-gray-100 dark:border-slate-800 mb-4">
<div v-if="empty" class="text-center py-24 text-gray-500 dark:text-slate-400">
<p>Nothing's here</p>
</div>
<div v-else class="flex-1" :class="[!hasFormData && 'p-6']">
<slot />
</div>
</component>
</template>

View file

@ -1,4 +1,4 @@
<script lang="ts" setup>
<script setup>
import { mdiCog } from '@mdi/js';
import CardBox from '@/Components/CardBox.vue';
import NumberDynamic from '@/Components/NumberDynamic.vue';
@ -49,9 +49,6 @@ defineProps({
<PillTagTrend :trend="trend" :trend-type="trendType" small />
<BaseButton :icon="mdiCog" icon-w="w-4" icon-h="h-4" color="white" small />
</BaseLevel>
<BaseLevel v-else class="mb-3" mobile>
<BaseIcon v-if="icon" :path="icon" size="48" w="w-4" h="h-4" :class="color" />
</BaseLevel>
<BaseLevel mobile>
<div>
<h3 class="text-lg leading-tight text-gray-500 dark:text-slate-400">

View file

@ -17,15 +17,6 @@
<p class="text-lg text-blue-700">Drop files to upload</p>
</div>
<!-- Loading Spinner when processing big files -->
<div v-if="isLoading" class="absolute inset-0 z-60 flex items-center justify-center bg-gray-500 bg-opacity-50">
<svg class="animate-spin h-12 w-12 text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M12 2a10 10 0 0110 10h-4a6 6 0 00-6-6V2z"></path>
</svg>
</div>
<!-- scroll area -->
<div class="h-full p-8 w-full h-full flex flex-col">
<header class="flex items-center justify-center w-full">
@ -41,9 +32,9 @@
<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
</p>
<!-- <p class="text-xs text-gray-500 dark:text-gray-400">SVG, PNG, JPG or GIF (MAX. 800x400px)</p> -->
</div>
<input id="dropzone-file" type="file" class="hidden" @click="showSpinner" @change="onChangeFile"
@cancel="cancelSpinner" multiple="true" />
<input id="dropzone-file" type="file" class="hidden" @change="onChangeFile" multiple="true" />
</label>
</header>
@ -191,7 +182,7 @@
<!-- sticky footer -->
<footer class="flex justify-end px-8 pb-8 pt-4">
<button v-if="showClearButton" id="cancel"
<button id="cancel"
class="ml-3 rounded-sm px-3 py-1 hover:bg-gray-300 focus:shadow-outline focus:outline-none"
@click="clearAllFiles">
Clear
@ -250,8 +241,6 @@ class FileUploadComponent extends Vue {
@Ref('overlay') overlay: HTMLDivElement;
public isLoading: boolean = false;
private counter: number = 0;
// @Prop() files: Array<TestFile>;
@ -268,12 +257,6 @@ class FileUploadComponent extends Vue {
})
filesToDelete: Array<TethysFile>;
@Prop({
type: Boolean,
default: true,
})
showClearButton: boolean;
// // deletetFiles: Array<TethysFile> = [];
get deletetFiles(): Array<TethysFile> {
return this.filesToDelete;
@ -281,7 +264,7 @@ class FileUploadComponent extends Vue {
set deletetFiles(values: Array<TethysFile>) {
// this.modelValue = value;
this.filesToDelete.length = 0;
this.filesToDelete.push(...values);
this.filesToDelete.push(...values);
}
get items(): Array<TethysFile | File> {
@ -359,10 +342,10 @@ class FileUploadComponent extends Vue {
}
// reset counter and append file to gallery when file is dropped
public dropHandler(event: DragEvent): void {
event.preventDefault();
const dataTransfer = event.dataTransfer;
// let bigFileFound = false;
if (dataTransfer) {
for (const file of event.dataTransfer?.files) {
// let fileName = String(file.name.replace(/\.[^/.]+$/, ''));
@ -370,73 +353,28 @@ class FileUploadComponent extends Vue {
// if (file.type.match('image.*')) {
// this.generateURL(file);
// }
// if (file.size > 62914560) { // 60 MB in bytes
// bigFileFound = true;
// }
this._addFile(file);
}
this.overlay.classList.remove('draggedover');
this.counter = 0;
}
// if (bigFileFound) {
// this.isLoading = true;
// // Assume file processing delay; adjust timeout as needed or rely on async processing completion.
// setTimeout(() => {
// this.isLoading = false;
// }, 1500);
// }
}
public showSpinner() {
// event.preventDefault();
this.isLoading = true;
}
public cancelSpinner() {
// const target = event.target as HTMLInputElement;
// // If no files were selected, remove spinner
// if (!target.files || target.files.length === 0) {
// this.isLoading = false;
// }
this.isLoading = false;
}
public onChangeFile(event: Event) {
event.preventDefault();
let target = event.target as HTMLInputElement;
// let uploadedFile = event.target.files[0];
// let fileName = String(event.target.files[0].name.replace(/\.[^/.]+$/, ''));
if (target && target.files) {
for (const file of event.target.files) {
// let fileName = String(event.target.files[0].name.replace(/\.[^/.]+$/, ''));
// file.label = fileName;
// if (file.type.match('image.*')) {
// this.generateURL(file);
// }
// Immediately set spinner if any file is large (over 100 MB)
// for (const file of target.files) {
// if (file.size > 62914560) { // 100 MB
// bigFileFound = true;
// break;
// }
// }
// if (bigFileFound) {
// this.isLoading = true;
// }
this._addFile(file);
}
for (const file of event.target.files) {
// let fileName = String(event.target.files[0].name.replace(/\.[^/.]+$/, ''));
// file.label = fileName;
// if (file.type.match('image.*')) {
// this.generateURL(file);
// }
this._addFile(file);
}
// if (bigFileFound) {
// this.isLoading = true;
// setTimeout(() => {
// this.isLoading = false;
// }, 1500);
// }
// this.overlay.classList.remove('draggedover');
this.counter = 0;
this.isLoading = false;
}
get errors(): IDictionary {
@ -458,9 +396,7 @@ class FileUploadComponent extends Vue {
public clearAllFiles(event: Event) {
event.preventDefault();
if (this.showClearButton == true) {
this.items.splice(0);
}
this.items.splice(0);
}
public removeFile(key: number) {
@ -509,7 +445,7 @@ class FileUploadComponent extends Vue {
let localUrl: string = '';
if (file instanceof File) {
localUrl = URL.createObjectURL(file as Blob);
}
}
// else if (file.fileData) {
// // const blob = new Blob([file.fileData]);
// // localUrl = URL.createObjectURL(blob);
@ -529,6 +465,17 @@ class FileUploadComponent extends Vue {
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: File) {
if (file.size > 1024) {
if (file.size > 1048576) {
@ -541,6 +488,17 @@ class FileUploadComponent extends Vue {
}
}
// private _addFile(file) {
// // const isImage = file.type.match('image.*');
// // const objectURL = URL.createObjectURL(file);
// // this.files[objectURL] = file;
// // let test: TethysFile = { upload: file, label: "dfdsfs", sorting: 0 };
// // file.sorting = this.files.length;
// file.sort_order = (this.items.length + 1),
// this.files.push(file);
// }
private _addFile(file: File) {
// const reader = new FileReader();
// reader.onload = (event) => {
@ -572,11 +530,14 @@ class FileUploadComponent extends Vue {
// this.items.push(test);
this.items[this.items.length] = test;
} else {
file.sort_order = this.items.length + 1;
this.items.push(file);
}
}
// use to check if a file is being dragged
// private _hasFiles({ types = [] as Array<string> }) {
// return types.indexOf('Files') > -1;
// }
private _hasFiles(dataTransfer: DataTransfer | null): boolean {
return dataTransfer ? dataTransfer.items.length > 0 : false;
}

View file

@ -15,10 +15,9 @@ const year = computed(() => new Date().getFullYear());
<!-- Get more with <a href="https://tailwind-vue.justboil.me/" target="_blank" class="text-blue-600">Premium
version</a> -->
</div>
<div class="md:py-1">
<div class="md:py-3">
<a href="https://www.tethys.at" target="_blank">
<!-- <JustboilLogo class="w-auto h-8 md:h-6" /> -->
<JustboilLogo class="w-auto h-12 md:h-10 dark:invert" />
<JustboilLogo class="w-auto h-8 md:h-6" />
</a>
</div>
</BaseLevel>

View file

@ -1,59 +1,43 @@
<script setup lang="ts">
<script setup>
import { computed } from 'vue';
interface Props {
name: string;
type?: 'checkbox' | 'radio' | 'switch';
label?: string | null;
modelValue: Array<any> | string | number | boolean | null;
inputValue: string | number | boolean;
}
const props = defineProps<Props>();
const emit = defineEmits<{ (e: 'update:modelValue', value: Props['modelValue']): void }>();
const props = defineProps({
name: {
type: String,
required: true,
},
type: {
type: String,
default: 'checkbox',
validator: (value) => ['checkbox', 'radio', 'switch'].includes(value),
},
label: {
type: String,
default: null,
},
modelValue: {
type: [Array, String, Number, Boolean],
default: null,
},
inputValue: {
type: [String, Number, Boolean],
required: true,
},
});
const emit = defineEmits(['update:modelValue']);
const computedValue = computed({
get: () => props.modelValue,
set: (value) => {
emit('update:modelValue', props.type === 'radio' ? [value] : value);
emit('update:modelValue', value);
},
});
const inputType = computed(() => (props.type === 'radio' ? 'radio' : 'checkbox'));
// Define isChecked for radio inputs: it's true when the current modelValue equals the inputValue
const isChecked = computed(() => {
if (Array.isArray(computedValue.value) && computedValue.value.length > 0) {
return props.type === 'radio'
? computedValue.value[0] === props.inputValue
: computedValue.value.includes(props.inputValue);
}
return computedValue.value === props.inputValue;
});
</script>
<template>
<label v-if="type === 'radio'" :class="[type]"
class="mr-6 mb-3 last:mr-0 inline-flex items-center cursor-pointer relative">
<input v-model="computedValue" :type="inputType" :name="name" :value="inputValue"
class="absolute left-0 opacity-0 -z-1 focus:outline-none focus:ring-0"
:checked="isChecked" />
<span class="check border transition-colors duration-200 dark:bg-slate-800 block w-5 h-5 rounded-full" :class="{
'border-gray-700': !isChecked,
'bg-radio-checked bg-no-repeat bg-center bg-lime-600 border-lime-600 border-4': isChecked
}" />
<span class="pl-2 control-label">{{ label }}</span>
</label>
<label v-else-if="type === 'checkbox'" :class="[type]"
class="mr-6 mb-3 last:mr-0 inline-flex items-center cursor-pointer relative">
<input v-model="computedValue" :type="inputType" :name="name" :value="inputValue"
class="absolute left-0 opacity-0 -z-1 focus:outline-none focus:ring-0" />
<span class="check border transition-colors duration-200 dark:bg-slate-800 block w-5 h-5 rounded" :class="{
'border-gray-700': !isChecked,
'bg-checkbox-checked bg-no-repeat bg-center bg-lime-600 border-lime-600 border-4': isChecked
}" />
<span class="pl-2 control-label">{{ label }}</span>
<label :class="type" class="mr-6 mb-3 last:mr-0">
<input v-model="computedValue" :type="inputType" :name="name" :value="inputValue" />
<span class="check" />
<span class="pl-2">{{ label }}</span>
</label>
</template>

View file

@ -1,9 +1,9 @@
<script setup lang="ts">
import { computed, ref, PropType } from 'vue';
import { computed, ref } from 'vue';
import FormCheckRadio from '@/Components/FormCheckRadio.vue';
// import BaseButton from '@/Components/BaseButton.vue';
// import FormControl from '@/Components/FormControl.vue';
import BaseButton from '@/Components/BaseButton.vue';
import FormControl from '@/Components/FormControl.vue';
import { mdiPlusCircle } from '@mdi/js';
const props = defineProps({
options: {
type: Object,
@ -23,7 +23,7 @@ const props = defineProps({
required: true,
},
type: {
type: String as PropType<'checkbox' | 'radio' | 'switch'>,
type: String,
default: 'checkbox',
validator: (value: string) => ['checkbox', 'radio', 'switch'].includes(value),
},
@ -47,7 +47,7 @@ const computedValue = computed({
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);
const ids = props.modelValue.map((obj) => obj.id.toString());
return ids;
}
return props.modelValue;
@ -78,11 +78,11 @@ const addOption = () => {
const inputElClass = computed(() => {
const base = [
'px-3 py-2 max-w-full border-gray-700 rounded w-full',
'px-3 py-2 max-w-full focus:ring focus:outline-none border-gray-700 rounded w-full',
'dark:placeholder-gray-400',
'h-12',
'border',
'bg-transparent'
'bg-transparent'
// props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
];
// if (props.icon) {
@ -108,9 +108,7 @@ const inputElClass = computed(() => {
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-6H5v-2h6V5h2v6h6v2h-6v6z" />
</svg>
</div>
<!-- <FormCheckRadio v-for="(value, key) in options" :key="key" v-model="computedValue" :type="type"
:name="name" :input-value="key" :label="value" :class="componentClass" /> -->
<FormCheckRadio v-for="(value, key) in options" :key="key" v-model="computedValue" :type="type"
:name="name" :input-value="isNaN(Number(key)) ? key : Number(key)" :label="value" :class="componentClass" />
<FormCheckRadio v-for="(value, key) in options" :key="key" v-model="computedValue" :type="type" :name="name"
:input-value="key" :label="value" :class="componentClass" />
</div>
</template>

View file

@ -67,28 +67,15 @@ const computedValue = computed({
emit('update:modelValue', value);
},
});
// focus:ring focus:outline-none border-gray-700
const inputElClass = computed(() => {
const base = [
'px-3 py-2 max-w-full rounded w-full',
'px-3 py-2 max-w-full focus:ring focus:outline-none border-gray-700 rounded w-full',
'dark:placeholder-gray-400',
props.extraHigh ? 'h-80' : (computedType.value === 'textarea' ? 'h-44' : 'h-12'),
props.borderless ? 'border-0' : 'border',
// // props.transparent && !props.isReadOnly ? 'bg-transparent' : 'bg-white dark:bg-slate-800',
// props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
// props.transparent && !props.isReadOnly ? 'bg-transparent' : 'bg-white dark:bg-slate-800',
props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
];
// Apply styles based on read-only state.
if (props.isReadOnly) {
// Read-only: no focus ring, grayed-out text and border, and disabled cursor.
base.push('bg-gray-50', 'dark:bg-slate-600', 'border', 'border-gray-300', 'dark:border-slate-600', 'text-gray-500', 'cursor-not-allowed', 'focus:outline-none' ,'focus:ring-0', 'focus:border-gray-300');
} else {
// Actionable field: focus ring, white/dark background, and darker border.
base.push('bg-white dark:bg-slate-800', 'focus:ring focus:outline-none', 'border', 'border-gray-700');
}
if (props.icon) {
base.push('pl-10', 'pr-10');
}

View file

@ -1,74 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
import { mdiLicense } from '@mdi/js';
const props = defineProps({
path: {
type: String,
required: true
},
size: {
type: Number,
default: 24
},
viewBox: {
type: String,
default: '0 0 24 24'
},
color: {
type: String,
default: 'currentColor'
},
className: {
type: String,
default: ''
}
});
// Define all the SVG paths we need
const svgPaths = {
// Document/File icons
document: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
documentPlus: 'M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
// Communication icons
email: 'M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z',
// Identity/User icons
idCard: '10 2a1 1 0 00-1 1v1a1 1 0 002 0V3a1 1 0 00-1-1zM4 4h3a3 3 0 006 0h3a2 2 0 012 2v9a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2zm2.5 7a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm2.45 4a2.5 2.5 0 10-4.9 0h4.9zM12 9a1 1 0 100 2h3a1 1 0 100-2h-3zm-1 4a1 1 0 011-1h2a1 1 0 110 2h-2a1 1 0 01-1-1z',
// Language/Translation icons
// language: 'M7 2a1 1 0 011 1v1h3a1 1 0 110 2H9.578a18.87 18.87 0 01-1.724 4.78c.29.354.596.696.914 1.026a1 1 0 11-1.44 1.389c-.188-.196-.373-.396-.554-.6a19.098 19.098 0 01-3.107 3.567 1 1 0 01-1.334-1.49 17.087 17.087 0 003.13-3.733 18.992 18.992 0 01-1.487-2.494 1 1 0 111.79-.89c.234.47.489.928.764 1.372.417-.934.752-1.913.997-2.927H3a1 1 0 110-2h3V3a1 1 0 011-1zm6 6a1 1 0 01.894.553l2.991 5.982a.869.869 0 01.02.037l.99 1.98a1 1 0 11-1.79.895L15.383 16h-4.764l-.724 1.447a1 1 0 11-1.788-.894l.99-1.98.019-.038 2.99-5.982A1 1 0 0113 8zm-1.382 6h2.764L13 11.236 11.618 14z',
language: 'M12 2a10 10 0 1 0 0 20a10 10 0 1 0 0-20zm0 0c2.5 0 4.5 4.5 4.5 10s-2 10-4.5 10-4.5-4.5-4.5-10 2-10 4.5-10zm0 0a10 10 0 0 1 0 20a10 10 0 0 1 0-20z',
// License/Legal icons
// license: 'M10 2a1 1 0 00-1 1v1.323l-3.954 1.582A1 1 0 004 6.32V16a1 1 0 001.555.832l3-1.2a1 1 0 01.8 0l3 1.2a1 1 0 001.555-.832V6.32a1 1 0 00-1.046-.894L9 4.877V3a1 1 0 00-1-1zm0 14.5a.5.5 0 01-.5-.5v-4a.5.5 0 011 0v4a.5.5 0 01-.5.5zm1.5-10.5a.5.5 0 11-1 0 .5.5 0 011 0z',
license: mdiLicense,
// Building/Organization icons
building: 'M4 4a2 2 0 012-2h8a2 2 0 012 2v12a1 1 0 110 2h-3a1 1 0 01-1-1v-2a1 1 0 00-1-1H9a1 1 0 00-1 1v2a1 1 0 01-1 1H4a1 1 0 110-2V4zm3 1h2v2H7V5zm2 4H7v2h2V9zm2-4h2v2h-2V5zm2 4h-2v2h2V9z',
// Book/Publication icons
book: 'M9 4.804A7.968 7.968 0 005.5 4c-1.255 0-2.443.29-3.5.804v10A7.969 7.969 0 015.5 14c1.669 0 3.218.51 4.5 1.385A7.962 7.962 0 0114.5 14c1.255 0 2.443.29 3.5.804v-10A7.968 7.968 0 0014.5 4c-1.255 0-2.443.29-3.5.804V12a1 1 0 11-2 0V4.804z',
// Download icon
download: 'M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4'
};
const pathData = computed(() => {
return svgPaths[props.path] || props.path;
});
const sizeStyle = computed(() => {
return {
width: `${props.size}px`,
height: `${props.size}px`
};
});
</script>
<template>
<svg :style="sizeStyle" :class="className" :viewBox="viewBox" xmlns="http://www.w3.org/2000/svg" fill="none"
:stroke="color" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path :d="pathData" />
</svg>
</template>

File diff suppressed because one or more lines are too long

View file

@ -1,124 +0,0 @@
<script setup>
import { onMounted, ref, watch } from 'vue';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
const DEFAULT_BASE_LAYER_NAME = 'BaseLayer';
const DEFAULT_BASE_LAYER_ATTRIBUTION = '&copy; <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';
const props = defineProps({
coverage: {
type: Object,
required: true
},
height: {
type: String,
default: '250px'
},
mapId: {
type: String,
default: 'view-map'
}
});
const map = ref(null);
const mapContainer = ref(null);
onMounted(() => {
initializeMap();
});
watch(() => props.coverage, (newCoverage) => {
if (map.value && newCoverage) {
updateBounds();
}
}, { deep: true });
const initializeMap = () => {
// Create the map with minimal controls
map.value = L.map(mapContainer.value, {
zoomControl: false,
attributionControl: false,
dragging: false,
scrollWheelZoom: false,
doubleClickZoom: false,
boxZoom: false,
tap: false,
keyboard: false,
touchZoom: false
});
// // Add a simple tile layer (OpenStreetMap)
let osmGgray = new L.TileLayer.WMS('https://ows.terrestris.de/osm-gray/service', {
format: 'image/png',
attribution: DEFAULT_BASE_LAYER_ATTRIBUTION,
layers: 'OSM-WMS',
});
let layerOptions = {
label: DEFAULT_BASE_LAYER_NAME,
visible: true,
layer: osmGgray,
};
layerOptions.layer.addTo(map.value);
// Add a light-colored rectangle for the coverage area
updateBounds();
};
const updateBounds = () => {
if (!props.coverage || !map.value) return;
// Clear any existing layers except the base tile layer
map.value.eachLayer(layer => {
if (layer instanceof L.Rectangle) {
map.value.removeLayer(layer);
}
});
// Create bounds from the coverage coordinates
const bounds = L.latLngBounds(
[props.coverage.y_min, props.coverage.x_min],
[props.coverage.y_max, props.coverage.x_max]
);
// Add a rectangle with emerald styling
L.rectangle(bounds, {
color: '#10b981', // emerald-500
weight: 2,
fillColor: '#d1fae5', // emerald-100
fillOpacity: 0.5
}).addTo(map.value);
// Fit the map to the bounds with some padding
map.value.fitBounds(bounds, {
padding: [20, 20]
});
};
</script>
<template>
<div class="map-container bg-emerald-50 dark:bg-emerald-900/30
rounded-lg shadow-sm overflow-hidden">
<div :id="mapId" ref="mapContainer" :style="{ height: height }" class="w-full"></div>
</div>
</template>
<style scoped>
/* Ensure the Leaflet container has proper styling */
:deep(.leaflet-container) {
background-color: #f0fdf4;
/* emerald-50 */
}
/* Dark mode adjustments */
@media (prefers-color-scheme: dark) {
:deep(.leaflet-container) {
background-color: rgba(6, 78, 59, 0.3);
/* emerald-900/30 */
}
:deep(.leaflet-tile) {
filter: brightness(0.8) contrast(1.2) grayscale(0.3);
}
}
</style>

View file

@ -21,11 +21,12 @@ import {
mdiFormatListGroup,
mdiFormatListNumbered,
mdiLogout,
mdiGithub,
mdiThemeLightDark,
mdiViewDashboard,
mdiMapSearch,
mdiInformationVariant,
mdiGlasses,
mdiXml
} from '@mdi/js';
import NavBarItem from '@/Components/NavBarItem.vue';
import NavBarItemLabel from '@/Components/NavBarItemLabel.vue';
@ -99,8 +100,7 @@ const showAbout = async () => {
<FirstrunWizard ref="about"></FirstrunWizard>
<div class="flex lg:items-stretch" :class="containerMaxW">
<div class="flex-1 items-stretch flex h-14">
<NavBarItem type="flex lg:hidden" @click.prevent="layoutStore.asideMobileToggle()"
v-if="props.showBurger">
<NavBarItem type="flex lg:hidden" @click.prevent="layoutStore.asideMobileToggle()" v-if="props.showBurger">
<BaseIcon :path="layoutStore.isAsideMobileExpanded ? mdiBackburger : mdiForwardburger" size="24" />
</NavBarItem>
<NavBarItem type="hidden lg:flex xl:hidden" @click.prevent="menuOpenLg" v-if="props.showBurger">
@ -110,9 +110,9 @@ const showAbout = async () => {
<NavBarItemLabel :icon="mdiViewDashboard" label="Dashboard" size="22" is-hover-label-only
route-name="apps.dashboard" />
</NavBarItem>
<!-- <NavBarItem route-name="apps.map">
<NavBarItem route-name="apps.map">
<NavBarItemLabel :icon="mdiMapSearch" label="Map" size="22" is-hover-label-only route-name="apps.map" />
</NavBarItem> -->
</NavBarItem>
<!-- <NavBarItem>
<NavBarSearch />
</NavBarItem> -->
@ -169,10 +169,13 @@ const showAbout = async () => {
</NavBarItem>
<NavBarItem v-if="userHasRoles(['reviewer'])" :route-name="'reviewer.dataset.list'">
<NavBarItemLabel :icon="mdiGlasses" label="Reviewer Menu" />
</NavBarItem>
<!-- <NavBarItem @click="showAbout">
<NavBarItemLabel :icon="mdiInformationVariant" label="About" />
</NavBarItem>
<!-- <NavBarItem>
<NavBarItemLabel :icon="mdiEmail" label="Messages" />
</NavBarItem> -->
<NavBarItem @click="showAbout">
<NavBarItemLabel :icon="mdiInformationVariant" label="About" />
</NavBarItem>
<BaseDivider nav-bar />
<NavBarItem @click="logout">
<NavBarItemLabel :icon="mdiLogout" label="Log Out" />
@ -183,15 +186,12 @@ const showAbout = async () => {
<NavBarItem is-desktop-icon-only @click.prevent="toggleLightDark">
<NavBarItemLabel v-bind:icon="mdiThemeLightDark" label="Light/Dark" is-desktop-icon-only />
</NavBarItem>
<!-- <NavBarItem href="" target="_blank" is-desktop-icon-only>
<NavBarItem href="https://gitea.geosphere.at/geolba/tethys.backend" target="_blank" is-desktop-icon-only>
<NavBarItemLabel v-bind:icon="mdiGithub" label="GitHub" is-desktop-icon-only />
</NavBarItem> -->
<NavBarItem href="/oai" target="_blank" is-desktop-icon-only>
<NavBarItemLabel v-bind:icon="mdiXml" label="OAI Interface" is-desktop-icon-only />
</NavBarItem>
<!-- <NavBarItem is-desktop-icon-only @click="showAbout">
<NavBarItem is-desktop-icon-only @click="showAbout">
<NavBarItemLabel v-bind:icon="mdiInformationVariant" label="About" is-desktop-icon-only />
</NavBarItem> -->
</NavBarItem>
<NavBarItem is-desktop-icon-only @click="logout">
<NavBarItemLabel v-bind:icon="mdiLogout" label="Log out" is-desktop-icon-only />
</NavBarItem>

View file

@ -28,7 +28,7 @@
autocomplete="off"
@keydown.down="onArrowDown"
@keydown.up="onArrowUp"
@keydown.enter.prevent="onEnter"
@keydown.enter="onEnter"
/>
<svg
class="w-4 h-4 absolute left-2.5 top-3.5"

View file

@ -5,7 +5,7 @@
<div class="relative" data-te-dropdown-ref>
<button id="states-button" data-dropdown-toggle="dropdown-states"
class="whitespace-nowrap h-12 z-10 inline-flex items-center py-2.5 px-4 text-sm font-medium text-center text-gray-500 bg-gray-100 border border-gray-300 rounded-l-lg hover:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600 dark:focus:ring-gray-700 dark:text-white dark:border-gray-600"
type="button" :disabled="isReadOnly" @click.prevent="showStates">
type="button" @click.prevent="showStates">
<!-- <svg aria-hidden="true" class="h-3 mr-2" viewBox="0 0 15 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" width="14" height="12" rx="2" fill="white" />
<mask id="mask0_12694_49953" style="mask-type: alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="15" height="12">
@ -65,7 +65,7 @@
</svg> -->
<!-- eng -->
{{ language }}
<svg aria-hidden="true" class="w-4 h-4 ml-1" fill="currentColor" viewBox="0 0 20 20" v-if="!isReadOnly"
<svg aria-hidden="true" class="w-4 h-4 ml-1" fill="currentColor" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
@ -93,7 +93,7 @@
<!-- :class="inputElClass" -->
<!-- class="block p-2.5 w-full z-20 text-sm text-gray-900 bg-gray-50 rounded-r-lg border-l-gray-50 border-l-2 border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-l-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500" -->
<input v-model="computedValue" type="text" :name="props.name" autocomplete="off" :class="inputElClass"
placeholder="Search Keywords..." required @input="handleInput" :readonly="isReadOnly" />
placeholder="Search Keywords..." required @input="handleInput" />
<!-- v-model="data.search" -->
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="computedValue.length < 2"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@ -101,12 +101,12 @@
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="computedValue.length >= 2 && !isReadOnly"
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="computedValue.length >= 2"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" @click="() => {
computedValue = '';
data.isOpen = false;
}
">
">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
@ -166,10 +166,6 @@ let props = defineProps({
type: String,
default: '',
},
isReadOnly: {
type: Boolean,
default: false,
},
required: Boolean,
borderless: Boolean,
transparent: Boolean,
@ -194,18 +190,11 @@ const inputElClass = computed(() => {
'dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500',
'h-12',
props.borderless ? 'border-0' : 'border',
props.transparent && 'bg-transparent',
props.transparent ? 'bg-transparent' : 'bg-white dark:bg-slate-800',
// props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
];
// if (props.icon) {
base.push('pl-10');
if (props.isReadOnly) {
// Read-only: no focus ring, grayed-out text and border, and disabled cursor.
base.push('bg-gray-50', 'dark:bg-slate-600', 'border', 'border-gray-300', 'dark:border-slate-600', 'text-gray-500', 'cursor-not-allowed', 'focus:outline-none', 'focus:ring-0', 'focus:border-gray-300');
} else {
// Actionable field: focus ring, white/dark background, and darker border.
base.push('bg-white dark:bg-slate-800', 'focus:ring focus:outline-none', 'border', 'border-gray-700');
}
// }
return base;
});

View file

@ -15,10 +15,6 @@ defineProps({
required: true,
},
main: Boolean,
showCogButton: {
type: Boolean,
default: false,
}
});
const hasSlot = computed(() => useSlots().default);
@ -34,6 +30,6 @@ const hasSlot = computed(() => useSlots().default);
</h1>
</div>
<slot v-if="hasSlot" />
<BaseButton v-else-if="showCogButton" :icon="mdiCog" small />
<BaseButton v-else :icon="mdiCog" small />
</section>
</template>

View file

@ -6,29 +6,10 @@ import FormField from '@/Components/FormField.vue';
import FormControl from '@/Components/FormControl.vue';
// Define props
// const props = defineProps<{
// modelValue: string,
// errors: Partial<Record<"new_password" | "old_password" | "confirm_password", string>>,
// showRequiredMessage: boolean,
// }>();
const props = defineProps({
modelValue: {
type: String,
},
errors: {
type: Object,
default: () => ({} as Partial<Record<"new_password" | "old_password" | "confirm_password", string>>),
},
showRequiredMessage: {
type: Boolean,
default:true,
},
fieldLabel: {
type: String,
default: 'New password',
}
});
const props = defineProps<{
modelValue: string;
errors: Partial<Record<"new_password" | "old_password" | "confirm_password", string>>;
}>();
const emit = defineEmits(['update:modelValue', 'score']);
@ -80,8 +61,8 @@ const passwordMetrics = computed<PasswordMetrics>(() => {
<template>
<!-- Password input Form -->
<FormField :label="fieldLabel" :help="showRequiredMessage ? 'Required. New password' : ''" :class="{'text-red-400': errors.new_password }">
<FormControl v-model="localPassword" :icon="mdiFormTextboxPassword" name="new_password" type="password" :required="showRequiredMessage"
<FormField label="New password" help="Required. New password" :class="{'text-red-400': errors.new_password }">
<FormControl v-model="localPassword" :icon="mdiFormTextboxPassword" name="new_password" type="password" required
:error="errors.new_password">
<!-- Secure Icon -->
<template #right>
@ -103,10 +84,10 @@ const passwordMetrics = computed<PasswordMetrics>(() => {
<div class="text-gray-700 text-sm">
{{ passwordMetrics.score }} / 6 points max
</div>
</FormField>
</FormField>
<!-- Password Strength Bar -->
<div v-if="passwordMetrics.score > 0"class="po-password-strength-bar w-full h-2 rounded transition-all duration-200 mb-4"
<div class="po-password-strength-bar w-full h-2 rounded transition-all duration-200 mb-4"
:class="passwordMetrics.scoreLabel" :style="{ width: `${(passwordMetrics.score / 6) * 100}%` }"
role="progressbar" :aria-valuenow="passwordMetrics.score" aria-valuemin="0" aria-valuemax="6"
:aria-label="`Password strength: ${passwordMetrics.scoreLabel || 'unknown'}`">

View file

@ -12,7 +12,6 @@ import { Subject } from '@/Dataset';
// import FormField from '@/Components/FormField.vue';
import FormControl from '@/Components/FormControl.vue';
import SearchCategoryAutocomplete from '@/Components/SearchCategoryAutocomplete.vue';
import { mdiRefresh } from '@mdi/js';
const props = defineProps({
checkable: Boolean,
@ -28,22 +27,6 @@ const props = defineProps({
type: Object,
default: () => ({}),
},
subjectsToDelete: {
type: Array<Subject>,
default: [],
}
});
const emit = defineEmits(['update:subjectsToDelete']);
// Create a computed property for subjectsToDelete with getter and setter
const deletetSubjects = computed({
get: () => props.subjectsToDelete,
set: (values: Array<Subject>) => {
props.subjectsToDelete.length = 0;
props.subjectsToDelete.push(...values);
emit('update:subjectsToDelete', values);
}
});
const styleService = StyleService();
@ -75,45 +58,21 @@ const pagesList = computed(() => {
});
const removeItem = (key: number) => {
// items.value.splice(key, 1);
const item = items.value[key];
// If the item has an ID, add it to the delete list
if (item.id) {
addToDeleteList(item);
}
// Remove from the visible list
items.value.splice(key, 1);
};
// Helper function to add a subject to the delete list
const addToDeleteList = (subject: Subject) => {
if (subject.id) {
const newList = [...props.subjectsToDelete, subject];
deletetSubjects.value = newList;
}
};
// Helper function to reactivate a subject (remove from delete list)
const reactivateSubject = (index: number) => {
const newList = [...props.subjectsToDelete];
const removedSubject = newList.splice(index, 1)[0];
deletetSubjects.value = newList;
// Add the subject back to the keywords list if it's not already there
if (removedSubject && !props.keywords.some(k => k.id === removedSubject.id)) {
props.keywords.push(removedSubject);
}
};
const isKeywordReadOnly = (item: Subject) => {
return (item.dataset_count ?? 0) > 1 || item.type !== 'uncontrolled';
};
</script>
<template>
<!-- <CardBoxModal v-model="isModalActive" title="Sample modal">
<p>Lorem ipsum dolor sit amet <b>adipiscing elit</b></p>
<p>This is sample modal</p>
</CardBoxModal>
<CardBoxModal v-model="isModalDangerActive" large-title="Please confirm" button="danger" has-cancel>
<p>Lorem ipsum dolor sit amet <b>adipiscing elit</b></p>
<p>This is sample modal</p>
</CardBoxModal> -->
<!-- <div v-if="checkedRows.length" class="p-3 bg-gray-100/50 dark:bg-slate-800">
<span v-for="checkedRow in checkedRows" :key="checkedRow.id"
@ -128,34 +87,17 @@ const isKeywordReadOnly = (item: Subject) => {
<!-- <th v-if="checkable" /> -->
<!-- <th class="hidden lg:table-cell"></th> -->
<th scope="col">Type</th>
<th scope="col" class="relative">
Value
<div class="inline-block relative ml-1 group">
<button
class="w-4 h-4 rounded-full bg-gray-200 text-gray-600 text-xs flex items-center justify-center focus:outline-none hover:bg-gray-300">
i
</button>
<div
class="absolute left-0 top-full mt-1 w-64 bg-white shadow-lg rounded-md p-3 text-xs text-left z-50 transform scale-0 origin-top-left transition-transform duration-100 group-hover:scale-100">
<p class="text-gray-700">
Keywords are only editable if they are used by a single dataset (Usage Count = 1)".
</p>
<div class="absolute -top-1 left-1 w-2 h-2 bg-white transform rotate-45"></div>
</div>
</div>
</th>
<th scope="col">Value</th>
<th scope="col">Language</th>
<th scope="col">Usage Count</th>
<th scope="col" />
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in itemsPaginated" :key="index">
<td data-label="Type" scope="row">
<FormControl required v-model="item.type"
@update:modelValue="() => { item.value = ''; }" :type="'select'"
placeholder="[Enter Language]" :options="props.subjectTypes">
<FormControl required v-model="item.type" @update:modelValue="() => {item.external_key = undefined; item.value= '';}" :type="'select'" placeholder="[Enter Language]" :options="props.subjectTypes">
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.type`]">
{{ errors[`subjects.${index}.type`].join(', ') }}
</div>
@ -163,19 +105,22 @@ const isKeywordReadOnly = (item: Subject) => {
</td>
<td data-label="Value" scope="row">
<SearchCategoryAutocomplete v-if="item.type !== 'uncontrolled'" v-model="item.value" @subject="
(result) => {
item.language = result.language;
item.external_key = result.uri;
}
" :is-read-only="item.dataset_count > 1">
<SearchCategoryAutocomplete
v-if="item.type !== 'uncontrolled'"
v-model="item.value"
@subject="
(result) => {
item.language = result.language;
item.external_key = result.uri;
}
"
>
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.value`]">
{{ errors[`subjects.${index}.value`].join(', ') }}
</div>
</SearchCategoryAutocomplete>
<FormControl v-else required v-model="item.value" type="text" placeholder="[enter keyword value]"
:borderless="true" :is-read-only="item.dataset_count > 1">
<FormControl v-else required v-model="item.value" type="text" placeholder="[enter keyword value]" :borderless="true">
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.value`]">
{{ errors[`subjects.${index}.value`].join(', ') }}
</div>
@ -183,24 +128,23 @@ const isKeywordReadOnly = (item: Subject) => {
</td>
<td data-label="Language" scope="row">
<FormControl required v-model="item.language" :type="'select'" placeholder="[Enter Lang]"
:options="{ de: 'de', en: 'en' }" :is-read-only="isKeywordReadOnly(item)">
<FormControl
required
v-model="item.language"
:type="'select'"
placeholder="[Enter Lang]"
:options="{ de: 'de', en: 'en' }"
:is-read-only="item.type != 'uncontrolled'"
>
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.language`]">
{{ errors[`subjects.${index}.language`].join(', ') }}
</div>
</FormControl>
</td>
<td data-label="Usage Count" scope="row">
<div class="text-center">
{{ item.dataset_count || 1 }}
</div>
</td>
<td class="before:hidden lg:w-1 whitespace-nowrap" scope="row">
<BaseButtons type="justify-start lg:justify-end" no-wrap>
<!-- <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" /> -->
<BaseButton color="danger" :icon="mdiTrashCan" small @click.prevent="removeItem(index)" />
<BaseButton v-if="index > 2" color="danger" :icon="mdiTrashCan" small @click.prevent="removeItem(index)" />
</BaseButtons>
</td>
</tr>
@ -211,8 +155,15 @@ const isKeywordReadOnly = (item: Subject) => {
<div class="p-3 lg:px-6 border-t border-gray-100 dark:border-slate-800">
<BaseLevel>
<BaseButtons>
<BaseButton v-for="page in pagesList" :key="page" :active="page === currentPage" :label="page + 1" small
:outline="styleService.darkMode" @click="currentPage = page" />
<BaseButton
v-for="page in pagesList"
:key="page"
:active="page === currentPage"
:label="page + 1"
small
:outline="styleService.darkMode"
@click="currentPage = page"
/>
</BaseButtons>
<small>Page {{ currentPageHuman }} of {{ numPages }}</small>
</BaseLevel>
@ -221,47 +172,6 @@ const isKeywordReadOnly = (item: Subject) => {
<div class="text-red-400 text-sm" v-if="errors.subjects && Array.isArray(errors.subjects)">
{{ errors.subjects.join(', ') }}
</div>
<!-- Subjects to delete section -->
<div v-if="deletetSubjects.length > 0" class="mt-8">
<h1 class="pt-8 pb-3 font-semibold sm:text-lg text-gray-900">Keywords To Delete</h1>
<ul id="deleteSubjects" tag="ul" class="flex flex-1 flex-wrap -m-1">
<li v-for="(element, index) in deletetSubjects" :key="index"
class="block p-1 w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/6 xl:w-1/8 h-32">
<article tabindex="0"
class="bg-red-100 group w-full h-full rounded-md cursor-pointer relative shadow-sm overflow-hidden">
<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 font-medium text-sm mb-1">{{
element.value }}</h1>
<div class="flex items-center justify-between mt-auto">
<div class="flex flex-col">
<p class="p-1 size text-xs text-gray-700">
<span class="font-semibold">Type:</span> {{ element.type }}
</p>
<p class="p-1 size text-xs text-gray-700" v-if="element.dataset_count">
<span class="font-semibold">Used by:</span>
<span
class="inline-flex items-center justify-center bg-gray-200 text-gray-800 rounded-full w-5 h-5 text-xs">
{{ element.dataset_count }}
</span> datasets
</p>
</div>
<button
class="delete ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md text-gray-800"
@click.prevent="reactivateSubject(index)">
<svg viewBox="0 0 24 24" class="w-5 h-5">
<path fill="currentColor" :d="mdiRefresh"></path>
</svg>
</button>
</div>
</section>
</article>
</li>
</ul>
</div>
</template>
<style scoped>

View file

@ -1,8 +1,7 @@
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { computed, ComputedRef } from 'vue';
import { Head, useForm, usePage } from '@inertiajs/vue3';
import { mdiAccountKey, mdiArrowLeftBoldOutline, mdiTrashCan, mdiImageText, mdiPlus, mdiAlertBoxOutline } from '@mdi/js';
import { Head, useForm } from '@inertiajs/vue3';
import { mdiAccountKey, mdiArrowLeftBoldOutline, mdiTrashCan, mdiImageText, mdiPlus } from '@mdi/js';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
@ -17,7 +16,6 @@ import standardTypes from 'mime/types/standard.js';
import otherTypes from 'mime/types/other.js';
import FormCheckRadioGroup from '@/Components/FormCheckRadioGroup.vue';
import MimetypeInput from '@/Components/MimetypeInput.vue';
import NotificationBar from '@/Components/NotificationBar.vue';
defineProps({
borderless: Boolean,
@ -25,10 +23,6 @@ defineProps({
ctrlKFocus: Boolean,
});
const flash: ComputedRef<any> = computed(() => {
return usePage().props.flash;
});
const customTypes: { [key: string]: string[] } = {
'application/vnd.opengeospatial.geopackage+sqlite3': ['gpkg'],
'text/plain': ['txt', 'asc', 'c', 'cc', 'h', 'srt'],
@ -147,13 +141,6 @@ const isValidForm = (): boolean => {
<BaseButton :route-name="stardust.route('settings.mimetype.index')" :icon="mdiArrowLeftBoldOutline"
label="Back" color="white" rounded-full small />
</SectionTitleLineWithButton>
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
{{ flash.message }}
</NotificationBar>
<!-- <FormValidationErrors v-bind:errors="errors" /> -->
<CardBox form>
<MimetypeInput @on-select-result="selectResult" @on-clear-input="clearInput" :transparent="transparent"
:borderless="borderless" :mimeTypes="mimeTypes" :isValidMimeType="isValidMimeType" />

View file

@ -41,28 +41,13 @@ const form = useForm({
first_name: '',
last_name: '',
email: '',
new_password: '',
password: '',
password_confirmation: '',
roles: [],
});
const submit = async () => {
// await router.post(stardust.route('settings.user.store'), form);
await form.post(stardust.route('settings.user.store'), {
preserveScroll: true,
onSuccess: () => {
form.reset();
},
onError: () => {
if (form.errors.new_password) {
form.reset('new_password');
enabled.value = false;
// newPasswordInput.value.focus();
// newPasswordInput.value?.focus();
}
},
});
await router.post(stardust.route('settings.user.store'), form);
};
</script>
@ -124,7 +109,7 @@ const submit = async () => {
</FormControl>
</FormField>
<password-meter :password="form.password" @score="handleScore" /> -->
<PasswordMeter v-model="form.new_password" :errors="form.errors" @score="handleScore" />
<PasswordMeter v-model:password="form.password" :errors="form.errors" @score="handleScore" />
<FormField label="Password Confirmation" :class="{ 'text-red-400': errors.password_confirmation }">
<FormControl

View file

@ -42,29 +42,14 @@ const form = useForm({
first_name: props.user.first_name,
last_name: props.user.last_name,
email: props.user.email,
new_password: '',
password: '',
password_confirmation: '',
roles: props.userHasRoles, // fill actual user roles from db
});
const submit = async () => {
// await Inertia.post(stardust.route('user.store'), form);
// await router.put(stardust.route('settings.user.update', [props.user.id]), form);
await form.put(stardust.route('settings.user.update', [props.user.id]), {
preserveScroll: true,
onSuccess: () => {
form.reset();
},
onError: () => {
if (form.errors.new_password) {
form.reset('new_password');
enabled.value = false;
// newPasswordInput.value.focus();
// newPasswordInput.value?.focus();
}
},
});
await router.put(stardust.route('settings.user.update', [props.user.id]), form);
};
const handleScore = (score: number) => {
if (score >= 4){
@ -123,16 +108,15 @@ const handleScore = (score: number) => {
</FormControl>
</FormField>
<!-- <FormField label="Password" :class="{ 'text-red-400': errors.password }">
<FormField label="Password" :class="{ 'text-red-400': errors.password }">
<FormControl v-model="form.password" type="password" placeholder="Enter Password" :errors="errors.password">
<div class="text-red-400 text-sm" v-if="errors.password">
{{ errors.password }}
</div>
</FormControl>
</FormField> -->
<PasswordMeter field-label="Reset User Password" :show-required-message="false" ref="newPasswordInput" v-model="form.new_password" :errors="form.errors" @score="handleScore" />
</FormField>
<PasswordMeter v-model:password="form.password" :errors="form.errors" @score="handleScore" />
<FormField label="Password Confirmation" :class="{ 'text-red-400': errors.password_confirmation }">
<FormControl
@ -167,7 +151,7 @@ const handleScore = (score: number) => {
color="info"
label="Submit"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing == true|| (form.new_password != '' && enabled == false)"
:disabled="form.processing == true|| (form.password != '' && enabled == false)"
/>
</BaseButtons>
</template>

View file

@ -1,7 +1,7 @@
<script setup lang="ts">
// import { Head, Link, useForm } from '@inertiajs/inertia-vue3';
import { ref } from 'vue';
import { useForm, Head } from '@inertiajs/vue3';
import { useForm } from '@inertiajs/vue3';
// import { ref } from 'vue';
// import { reactive } from 'vue';
import {
@ -126,7 +126,6 @@ const flash: Ref<any> = computed(() => {
<template>
<LayoutAuthenticated>
<Head title="Profile Security"></Head>
<SectionMain>
<SectionTitleLineWithButton :icon="mdiAccount" title="Profile" main>
<BaseButton :route-name="stardust.route('dashboard')" :icon="mdiArrowLeftBoldOutline" label="Back"

View file

@ -6,7 +6,7 @@
<!-- <SectionFullScreen v-slot="{ cardClass }" :bg="'greenBlue'"> -->
<SectionFullScreen v-slot="{ cardClass }">
<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 dark:invert" alt="Windster Logo" />
<img src="../../logo.svg" class="h-10 mr-4" alt="Windster Logo" />
<!-- <span class="self-center text-2xl font-bold whitespace-nowrap">Tethys</span> -->
</a>

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Head, usePage } from '@inertiajs/vue3';
import { computed } from 'vue';
import { Head } from '@inertiajs/vue3';
import { computed, onMounted } from 'vue';
import { MainService } from '@/Stores/main';
import {
mdiAccountMultiple,
@ -9,6 +9,7 @@ import {
mdiFinance,
mdiMonitorCellphone,
mdiReload,
mdiGithub,
mdiChartPie,
} from '@mdi/js';
import LineChart from '@/Components/Charts/LineChart.vue';
@ -16,16 +17,18 @@ import SectionMain from '@/Components/SectionMain.vue';
import CardBoxWidget from '@/Components/CardBoxWidget.vue';
import CardBox from '@/Components/CardBox.vue';
import TableSampleClients from '@/Components/TableSampleClients.vue';
// import NotificationBar from '@/Components/NotificationBar.vue';
import BaseButton from '@/Components/BaseButton.vue';
import CardBoxClient from '@/Components/CardBoxClient.vue';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
// import SectionBannerStarOnGitHub from '@/Components/SectionBannerStarOnGitea.vue';
import SectionBannerStarOnGitHub from '@/Components/SectionBannerStarOnGitea.vue';
import CardBoxDataset from '@/Components/CardBoxDataset.vue';
import type { User } from '@/Dataset';
const mainService = MainService()
// const chartData = ref();
const fillChartData = async () => {
await mainService.fetchChartData();
await mainService.fetchChartData("2022");
// chartData.value = chartConfig.sampleChartData();
// chartData.value = mainService.graphData;
};
@ -43,23 +46,13 @@ const chartData = computed(() => mainService.graphData);
// const clientBarItems = computed(() => mainService.clients.slice(0, 4));
// const transactionBarItems = computed(() => mainService.history);
mainService.fetchApi('clients');
mainService.fetchApi('authors');
mainService.fetchApi('datasets');
mainService.fetchChartData();
// const authorBarItems = computed(() => mainService.authors.slice(0, 5));
const authorBarItems = computed(() => mainService.authors.slice(0, 5));
const authors = computed(() => mainService.authors);
const datasets = computed(() => mainService.datasets);
const datasetBarItems = computed(() => mainService.datasets.slice(0, 5));
const submitters = computed(() => mainService.clients);
const user = computed(() => {
return usePage().props.authUser as User;
});
// let test = datasets.value;
// console.log(test);
const userHasRoles = (roleNames: Array<string>): boolean => {
return user.value.roles.some(role => roleNames.includes(role.name));
};
</script>
<template>
@ -68,15 +61,15 @@ const userHasRoles = (roleNames: Array<string>): boolean => {
<SectionMain>
<SectionTitleLineWithButton v-bind:icon="mdiChartTimelineVariant" title="Overview" main>
<!-- <BaseButton
href=""
<BaseButton
href="https://gitea.geosphere.at/geolba/tethys.backend"
target="_blank"
:icon="mdiGithub"
label="Star on GeoSphere Forgejo"
color="contrast"
rounded-full
small
/> -->
/>
</SectionTitleLineWithButton>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6">
@ -87,25 +80,27 @@ const userHasRoles = (roleNames: Array<string>): boolean => {
:icon="mdiAccountMultiple"
:number="authors.length"
label="Authors"
/>
<CardBoxWidget
/>
<CardBoxWidget
trend="193"
trend-type="info"
color="text-blue-500"
:icon="mdiDatabaseOutline"
:number="datasets.length"
label="Publications"
/>
<CardBoxWidget
/>
<CardBoxWidget
trend="+25%"
trend-type="up"
color="text-purple-500"
:icon="mdiChartTimelineVariant"
:number="submitters.length"
label="Submitters"
:number="52"
label="Citations"
/>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- <div class="flex flex-col justify-between">
<div class="flex flex-col justify-between">
<CardBoxClient
v-for="client in authorBarItems"
:key="client.id"
@ -114,9 +109,8 @@ const userHasRoles = (roleNames: Array<string>): boolean => {
:date="client.created_at"
:text="client.identifier_orcid"
:count="client.dataset_count"
/>
</div> -->
</div>
<div class="flex flex-col justify-between">
<CardBoxDataset
v-for="(dataset, index) in datasetBarItems"
@ -126,18 +120,20 @@ const userHasRoles = (roleNames: Array<string>): boolean => {
</div>
</div>
<!-- <SectionBannerStarOnGitHub /> -->
<SectionBannerStarOnGitHub />
<SectionTitleLineWithButton :icon="mdiChartPie" title="Trends overview: Publications per month" ></SectionTitleLineWithButton>
<SectionTitleLineWithButton :icon="mdiChartPie" title="Trends overview: Publications per month" />
<CardBox title="Performance" :icon="mdiFinance" :header-icon="mdiReload" class="mb-6" @header-icon-click="fillChartData">
<div v-if="chartData">
<line-chart :data="chartData" class="h-96" />
</div>
</CardBox>
<SectionTitleLineWithButton v-if="userHasRoles(['administrator'])" :icon="mdiAccountMultiple" title="Submitters" />
<SectionTitleLineWithButton :icon="mdiAccountMultiple" title="Submitters" />
<!-- <NotificationBar color="info" :icon="mdiMonitorCellphone"> <b>Responsive table.</b> Collapses on mobile </NotificationBar> -->
<CardBox v-if="userHasRoles(['administrator'])" :icon="mdiMonitorCellphone" title="Responsive table" has-table>
<CardBox :icon="mdiMonitorCellphone" title="Responsive table" has-table>
<TableSampleClients />
</CardBox>
</SectionMain>

View file

@ -1,377 +0,0 @@
<template>
<LayoutAuthenticated>
<Head title="Classify"></Head>
<SectionMain>
<SectionTitleLineWithButton :icon="mdiLibraryShelves" title="Library Classification" main>
<div class="bg-lime-100 shadow rounded-lg p-6 mb-6 flex items-center justify-between">
<div>
<label for="role-select" class="block text-lg font-medium text-gray-700 mb-1">
Select Classification Role <span class="text-red-500">*</span>
</label>
<select id="role-select" v-model="selectedCollectionRole"
class="w-full border border-gray-300 rounded-md p-2 text-gray-700 focus:ring-2 focus:ring-indigo-500"
required>
<!-- <option value="" disabled selected>Please select a role</option> -->
<option v-for="collRole in collectionRoles" :key="collRole.id" :value="collRole">
{{ collRole.name }}
</option>
</select>
</div>
<div class="ml-4 hidden md:block">
<span class="text-sm text-gray-600 italic">* required</span>
</div>
</div>
</SectionTitleLineWithButton>
<!-- Available TopLevel Collections -->
<CardBox class="mb-4 rounded-lg p-4">
<h2 class="text-lg font-bold text-gray-800 dark:text-slate-400 mb-2">Available Toplevel-Collections
<span v-if="selectedCollectionRole && !selectedToplevelCollection"
class="text-sm text-red-500 italic">(click to
select)</span>
</h2>
<ul class="flex flex-wrap gap-2">
<li v-for="col in collections" :key="col.id" :class="{
'cursor-pointer p-2 border border-gray-200 rounded hover:bg-sky-50 text-sky-700 text-sm': true,
'bg-sky-100 border-sky-500': selectedToplevelCollection && selectedToplevelCollection.id === col.id
}" @click="onToplevelCollectionSelected(col)">
<span class="text-sky-700">{{ col.name }}</span>
<span class="ml-2 px-2 py-0.5 bg-gray-200 text-gray-700 text-xs rounded-full">{{ col.number }}</span>
</li>
<li v-if="collections.length === 0" class="text-gray-800 dark:text-slate-400">
No collections available.
</li>
</ul>
</CardBox>
<!-- Collections Listing -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<!-- Broader Collection (Parent) -->
<CardBox v-if="selectedCollection" class="rounded-lg p-4" has-form-data>
<h2 class="text-lg font-bold text-gray-800 dark:text-slate-400 mb-2">Broader Collection</h2>
<draggable v-if="broaderCollections.length > 0" v-model="broaderCollections"
:group="{ name: 'collections' }" tag="ul" class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
<template #item="{ element: parent }">
<li :key="parent.id" :draggable="!parent.inUse" :class="getChildClasses(parent)"
@click="onCollectionSelected(parent)">
<span class="text-sky-700">{{ parent.name }}</span>
<span class="ml-2 px-2 py-0.5 bg-gray-200 text-gray-700 text-xs rounded-full">{{ parent.number }}</span>
</li>
</template>
</draggable>
<ul v-else class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
<li class="text-gray-500 text-sm">
No broader collections available.
</li>
</ul>
</CardBox>
<!-- Selected Collection Details -->
<CardBox v-if="selectedCollection" class="rounded-lg p-4" has-form-data>
<h3 class="text-xl font-bold text-gray-800 dark:text-slate-400 mb-2">Selected Collection</h3>
<!-- <p :class="[
'cursor-pointer p-2 border border-gray-200 rounded text-sm',
selectedCollection.inUse ? 'bg-gray-200 text-gray-500 drag-none' : 'bg-green-50 text-green-700 hover:bg-green-100 hover:underline cursor-move'
]"></p> -->
<draggable v-model="selectedCollectionArray" :group="{ name: 'collections', pull: 'clone', put: false }" tag="ul" class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
<template #item="{ element }">
<li :key="element.id" :class="[
'p-2 border border-gray-200 rounded text-sm',
element.inUse ? 'bg-gray-200 text-gray-500 drag-none' : 'bg-green-50 text-green-700 hover:bg-green-100 hover:underline cursor-move'
]">
<span class="text-sky-700">{{ element.name }}</span>
<span class="ml-2 px-2 py-0.5 bg-gray-200 text-gray-700 text-xs rounded-full">{{ element.number }}</span>
</li>
</template>
</draggable>
</CardBox>
<!-- Narrower Collections (Children) -->
<CardBox v-if="selectedCollection" class="rounded-lg p-4" has-form-data>
<h2 class="text-lg font-bold text-gray-800 dark:text-slate-400 mb-2">Narrower Collections</h2>
<draggable v-if="narrowerCollections.length > 0" v-model="narrowerCollections"
:group="{ name: 'collections' }" tag="ul" class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
<template #item="{ element: child }">
<li :key="child.id" :draggable="!child.inUse" :class="getChildClasses(child)"
@click="onCollectionSelected(child)">
<span class="text-sky-700">{{ child.name }}</span>
<span class="ml-2 px-2 py-0.5 bg-gray-200 text-gray-700 text-xs rounded-full">{{ child.number }}</span>
</li>
</template>
</draggable>
<ul v-else class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
<li class="text-gray-500 text-sm">
No sub-collections available.
</li>
</ul>
</CardBox>
</div>
<div class="mb-4 rounded-lg">
<div v-if="selectedCollection || selectedCollectionList.length > 0" class="bg-gray-100 shadow rounded-lg p-6 mb-6" :class="{ 'opacity-50': selectedCollection && selectedCollectionList.length === 0 }">
<p class="mb-4 text-gray-700">Please drag your collections here to classify your previously created
dataset
according to library classification standards.</p>
<draggable v-model="selectedCollectionList" :group="{ name: 'collections' }"
class="min-h-36 border-dashed border-2 border-gray-400 p-4 text-sm flex flex-wrap gap-2 max-h-60 overflow-y-auto"
tag="ul"
:disabled="selectedCollection === null && selectedCollectionList.length > 0"
:style="{ opacity: (selectedCollection === null && selectedCollectionList.length > 0) ? 0.5 : 1, pointerEvents: (selectedCollection === null && selectedCollectionList.length > 0) ? 'none' : 'auto' }">
<template #item="{ element }">
<div :key="element.id"
class="p-2 m-1 bg-sky-200 text-sky-800 rounded flex items-center gap-2 h-7">
<span class="text-sky-700">{{ element.name }}</span>
<span class="ml-2 px-2 py-0.5 bg-gray-200 text-gray-700 text-xs rounded-full">{{ element.number }}</span>
<button
@click="selectedCollectionList = selectedCollectionList.filter(item => item.id !== element.id)"
class="hover:text-sky-600 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
</button>
</div>
</template>
</draggable>
</div>
</div>
<div class="p-6 border-t border-gray-100 dark:border-slate-800">
<BaseButtons>
<BaseButton @click.stop="syncDatasetCollections" label="Save" color="info" small
:disabled="isSaveDisabled" :style="{ opacity: isSaveDisabled ? 0.5 : 1 }">
</BaseButton>
</BaseButtons>
</div>
</SectionMain>
</LayoutAuthenticated>
</template>
<script setup lang="ts">
import { ref, Ref, watch, computed } from 'vue';
import { useForm } from '@inertiajs/vue3';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
import axios from 'axios';
import { mdiLibraryShelves } from '@mdi/js';
import draggable from 'vuedraggable';
import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import CardBox from '@/Components/CardBox.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
import { CollectionRole, Collection } from '@/types/models';
// import CollectionRoleSelector from '@/Components/Collection/CollectionRoleSelector.vue';
// import ToplevelCollections from '@/Components/Collection/ToplevelCollections.vue';
// import CollectionHierarchy from '@/Components/Collection/CollectionHierarchy.vue';
// import CollectionDropZone from '@/Components/Collection/CollectionDropZone.vue';
/* --------------------------------------------------------------------------
Props & Reactive State
-------------------------------------------------------------------------- */
const props = defineProps({
collectionRoles: {
type: Array,
required: true,
default: () => []
},
dataset: {
type: Object,
default: () => ({}),
},
relatedCollections: {
type: Array as () => Collection[],
default: () => [] as const
}
});
const collectionRoles: Ref<CollectionRole[]> = ref(props.collectionRoles as CollectionRole[]);
const collections: Ref<Collection[]> = ref<Collection[]>([]);
const selectedCollectionRole = ref<CollectionRole | null>(null);
const selectedToplevelCollection = ref<Collection | null>(null);
const selectedCollection = ref<Collection | null>(null);
const narrowerCollections = ref<Collection[]>([]);
const broaderCollections = ref<Collection[]>([]);
// Reactive list that holds collections dropped by the user
const selectedCollectionList: Ref<Collection[]> = ref<Collection[]>([]);
// Wrap selectedCollection in an array for draggable (always expects an array)
const selectedCollectionArray = computed({
get: () => (selectedCollection.value ? [selectedCollection.value] : []),
set: (value: Collection[]) => {
selectedCollection.value = value.length ? value[0] : null
}
})
const form = useForm({
collections: [] as number[],
});
// Watch for changes in dropCollections
watch(
() => selectedCollectionList.value,
() => {
if (selectedCollection.value) {
fetchCollections(selectedCollection.value.id);
}
},
{ deep: true }
);
/* --------------------------------------------------------------------------
Watchers and Initial Setup
-------------------------------------------------------------------------- */
// If the collectionRoles prop might load asynchronously (or change), you can watch for it:
watch(
() => props.collectionRoles as CollectionRole[],
(newCollectionRoles: CollectionRole[]) => {
collectionRoles.value = newCollectionRoles;
// Preselect the role with name "ccs" if it exists
const found: CollectionRole | undefined = collectionRoles.value.find(
role => role.name.toLowerCase() === 'ccs'
);
if (found?.name === 'ccs') {
selectedCollectionRole.value = found;
}
},
{ immediate: true }
);
// When collection role changes, update available collections and clear dependent state.
watch(
() => selectedCollectionRole.value as CollectionRole,
(newSelectedCollectionRole: CollectionRole | null) => {
if (newSelectedCollectionRole != null) {
collections.value = newSelectedCollectionRole.collections || []
} else {
selectedToplevelCollection.value = null;
selectedCollection.value = null;
collections.value = []
}
// Reset dependent variables when the role changes
selectedCollection.value = null
narrowerCollections.value = []
broaderCollections.value = []
},
{ immediate: true }
);
/* --------------------------------------------------------------------------
Methods
-------------------------------------------------------------------------- */
const onToplevelCollectionSelected = (collection: Collection) => {
selectedToplevelCollection.value = collection;
selectedCollection.value = collection;
// call the API endpoint to get both.
fetchCollections(collection.id);
};
const onCollectionSelected = (collection: Collection) => {
selectedCollection.value = collection;
// call the API endpoint to get both.
fetchCollections(collection.id);
};
/**
* fetchCollections: Retrieves broader and narrower collections.
* Marks any narrower collection as inUse if it appears in selectedCollectionList.
*/
const fetchCollections = async (collectionId: number) => {
try {
const response = await axios.get(`/api/collections/${collectionId}`);
const data = response.data;
// Map each returned narrower collection
narrowerCollections.value = data.narrowerCollections.map((collection: Collection) => {
// If found, mark it as inUse.
const alreadyDropped = selectedCollectionList.value.find(dc => dc.id === collection.id);
return alreadyDropped ? { ...collection, inUse: true } : { ...collection, inUse: false };
});
broaderCollections.value = data.broaderCollection.map((collection: Collection) => {
const alreadyDropped = selectedCollectionList.value.find(dc => dc.id === collection.id);
return alreadyDropped ? { ...collection, inUse: true } : { ...collection, inUse: false };
});
// Check if selected collection is in the selected list
if (selectedCollection.value && selectedCollectionList.value.find(dc => dc.id === selectedCollection.value.id)) {
selectedCollection.value = { ...selectedCollection.value, inUse: true };
} else if (selectedCollection.value) {
selectedCollection.value = { ...selectedCollection.value, inUse: false };
}
} catch (error) {
console.error('Error in fetchCollections:', error);
}
};
const syncDatasetCollections = async () => {
// Extract the ids from the dropCollections list
form.collections = selectedCollectionList.value.map((item: Collection) => item.id);
form.put(stardust.route('editor.dataset.categorizeUpdate', [props.dataset.id]), {
preserveState: true,
onSuccess: () => {
console.log('Dataset collections synced successfully');
},
onError: (errors) => {
console.error('Error syncing dataset collections:', errors);
},
});
};
/**
* getChildClasses returns the Tailwind CSS classes to apply to each collection list item.
*/
const getChildClasses = (child: Collection) => {
return child.inUse
? 'p-2 border border-gray-200 rounded bg-gray-200 text-gray-500 cursor-pointer drag-none'
: 'p-2 border border-gray-200 rounded bg-green-50 text-green-700 cursor-move hover:bg-green-100 hover:underline'
}
// If there are related collections passed in, fill dropCollections with these.
if (props.relatedCollections && props.relatedCollections.length > 0) {
selectedCollectionList.value = props.relatedCollections;
}
// Add a computed property for the disabled state based on dropCollections length
const isSaveDisabled = computed(() => selectedCollectionList.value.length === 0);
</script>
<style scoped>
.btn-primary {
background-color: #4f46e5;
color: white;
border-radius: 0.25rem;
}
.btn-primary:hover {
background-color: #4338ca;
}
.btn-primary:focus {
outline: none;
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #4f46e5;
}
.btn-secondary {
background-color: white;
color: #374151;
border: 1px solid #d1d5db;
border-radius: 0.25rem;
}
.btn-secondary:hover {
background-color: #f9fafb;
}
.btn-secondary:focus {
outline: none;
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #6366f1;
}
</style>

View file

@ -1,865 +0,0 @@
<template>
<LayoutAuthenticated>
<Head title="Edit dataset" />
<SectionMain>
<SectionTitleLineWithButton :icon="mdiImageText" title="Update dataset" main>
<BaseButton :route-name="stardust.route('editor.dataset.list')" :icon="mdiArrowLeftBoldOutline"
label="Back" color="white" rounded-full small />
</SectionTitleLineWithButton>
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
{{ flash.message }}
</NotificationBar>
<FormValidationErrors v-bind:errors="errors" />
<CardBox :form="true">
<!-- <FormValidationErrors v-bind:errors="errors" /> -->
<div class="mb-4">
<div class="flex flex-col md:flex-row">
<!-- (1) language field -->
<FormField label="Language *" help="required: select dataset main language"
:class="{ 'text-red-400': form.errors.language }" class="w-full flex-1">
<FormControl required v-model="form.language" :type="'select'"
placeholder="[Enter Language]" :errors="form.errors.language"
:options="{ de: 'de', en: 'en' }">
<div class="text-red-400 text-sm" v-if="form.errors.language">
{{ form.errors.language.join(', ') }}
</div>
</FormControl>
</FormField>
</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 type="radio" 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"
: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 --"
:errors="form.errors.type" :options="doctypes">
<div class="text-red-400 text-sm"
v-if="form.errors.type && Array.isArray(form.errors.type)">
{{ form.errors.type.join(', ') }}
</div>
</FormControl>
</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>
<BaseDivider />
<!-- (5) titles -->
<CardBox class="mb-6 shadow" :has-form-data="false" title="Titles" :icon="mdiFinance"
:header-icon="mdiPlusCircle" v-on:header-icon-click="addTitle()">
<div class="flex flex-col md:flex-row">
<FormField label="Main Title *" help="required: main title"
:class="{ 'text-red-400': form.errors['titles.0.value'] }" class="w-full mr-1 flex-1">
<FormControl required v-model="form.titles[0].value" type="text"
placeholder="[enter main title]" :show-char-count="true" :max-input-length="255">
<div class="text-red-400 text-sm"
v-if="form.errors['titles.0.value'] && Array.isArray(form.errors['titles.0.value'])">
{{ form.errors['titles.0.value'].join(', ') }}
</div>
</FormControl>
</FormField>
<FormField label="Main Title Language*" help="required: main title language"
:class="{ 'text-red-400': form.errors['titles.0.language'] }"
class="w-full ml-1 flex-1">
<FormControl required v-model="form.titles[0].language" type="text"
:is-read-only="true">
<div class="text-red-400 text-sm"
v-if="form.errors['titles.0.language'] && Array.isArray(form.errors['titles.0.language'])">
{{ form.errors['titles.0.language'].join(', ') }}
</div>
</FormControl>
</FormField>
</div>
<label v-if="form.titles.length > 1">additional titles </label>
<!-- <BaseButton :icon="mdiPlusCircle" @click.prevent="addTitle()" color="modern" rounded-full small /> -->
<table>
<thead>
<tr>
<!-- <th v-if="checkable" /> -->
<th>Title Value</th>
<th>Title Type</th>
<th>Title Language</th>
<th />
</tr>
</thead>
<tbody>
<template v-for="(title, index) in form.titles" :key="index">
<tr v-if="title.type != 'Main'">
<!-- <td scope="row">{{ index + 1 }}</td> -->
<td data-label="Title Value">
<FormControl required v-model="form.titles[index].value" type="text"
placeholder="[enter main title]">
<div class="text-red-400 text-sm"
v-if="form.errors[`titles.${index}.value`]">
{{ form.errors[`titles.${index}.value`].join(', ') }}
</div>
</FormControl>
</td>
<td data-label="Title Type">
<FormControl required v-model="form.titles[index].type" type="select"
:options="titletypes" placeholder="[select title type]">
<div class="text-red-400 text-sm"
v-if="Array.isArray(form.errors[`titles.${index}.type`])">
{{ form.errors[`titles.${index}.type`].join(', ') }}
</div>
</FormControl>
</td>
<td data-label="Title Language">
<FormControl required v-model="form.titles[index].language" type="select"
:options="{ de: 'de', en: 'en' }" placeholder="[select title language]">
<div class="text-red-400 text-sm"
v-if="form.errors[`titles.${index}.language`]">
{{ form.errors[`titles.${index}.language`].join(', ') }}
</div>
</FormControl>
</td>
<td class="before:hidden lg:w-1 whitespace-nowrap">
<BaseButtons type="justify-start lg:justify-end" no-wrap>
<!-- <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" /> -->
<BaseButton color="danger" :icon="mdiTrashCan" small
v-if="title.id == undefined" @click.prevent="removeTitle(index)" />
</BaseButtons>
</td>
</tr>
</template>
</tbody>
</table>
</CardBox>
<!-- (6) descriptions -->
<CardBox class="mb-6 shadow" :has-form-data="false" title="Descriptions" :icon="mdiFinance"
:header-icon="mdiPlusCircle" v-on:header-icon-click="addDescription()">
<div class="flex flex-col md:flex-row">
<FormField label="Main Abstract *" help="required: main abstract"
:class="{ 'text-red-400': form.errors['descriptions.0.value'] }"
class="w-full mr-1 flex-1">
<FormControl required v-model="form.descriptions[0].value" type="textarea"
placeholder="[enter main abstract]" :show-char-count="true"
:max-input-length="2500">
<div class="text-red-400 text-sm"
v-if="form.errors['descriptions.0.value'] && Array.isArray(form.errors['descriptions.0.value'])">
{{ form.errors['descriptions.0.value'].join(', ') }}
</div>
</FormControl>
</FormField>
<FormField label="Main Title Language*" help="required: main abstract language"
:class="{ 'text-red-400': form.errors['descriptions.0.language'] }"
class="w-full ml-1 flex-1">
<FormControl required v-model="form.descriptions[0].language" type="text"
:is-read-only="true">
<div class="text-red-400 text-sm" v-if="form.errors['descriptions.0.value'] && Array.isArray(form.errors['descriptions.0.language'])
">
{{ form.errors['descriptions.0.language'].join(', ') }}
</div>
</FormControl>
</FormField>
</div>
<table>
<thead>
<tr>
<!-- <th v-if="checkable" /> -->
<th>Title Value</th>
<th>Title Type</th>
<th>Title Language</th>
<th />
</tr>
</thead>
<tbody>
<template v-for="(item, index) in form.descriptions" :key="index">
<tr v-if="item.type != 'Abstract'">
<!-- <td scope="row">{{ index + 1 }}</td> -->
<td data-label="Description Value">
<FormControl required v-model="form.descriptions[index].value" type="text"
placeholder="[enter main title]">
<div class="text-red-400 text-sm"
v-if="form.errors[`descriptions.${index}.value`]">
{{ form.errors[`descriptions.${index}.value`].join(', ') }}
</div>
</FormControl>
</td>
<td data-label="Description Type">
<FormControl required v-model="form.descriptions[index].type" type="select"
:options="descriptiontypes" placeholder="[select title type]">
<div class="text-red-400 text-sm"
v-if="Array.isArray(form.errors[`descriptions.${index}.type`])">
{{ form.errors[`descriptions.${index}.type`].join(', ') }}
</div>
</FormControl>
</td>
<td data-label="Description Language">
<FormControl required v-model="form.descriptions[index].language"
type="select" :options="{ de: 'de', en: 'en' }"
placeholder="[select title language]">
<div class="text-red-400 text-sm"
v-if="form.errors[`descriptions.${index}.language`]">
{{ form.errors[`descriptions.${index}.language`].join(', ') }}
</div>
</FormControl>
</td>
<td class="before:hidden lg:w-1 whitespace-nowrap">
<BaseButtons type="justify-start lg:justify-end" no-wrap>
<!-- <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" /> -->
<BaseButton color="danger" :icon="mdiTrashCan" small
v-if="item.id == undefined"
@click.prevent="removeDescription(index)" />
</BaseButtons>
</td>
</tr>
</template>
</tbody>
</table>
</CardBox>
<!-- (7) authors -->
<CardBox class="mb-6 shadow" has-table title="Creators" :icon="mdiBookOpenPageVariant"
:header-icon="mdiPlusCircle" v-on:header-icon-click="addNewAuthor()">
<SearchAutocomplete source="/api/persons" :response-property="'first_name'"
placeholder="search in person table...." v-on:person="onAddAuthor"></SearchAutocomplete>
<TablePersons :persons="form.authors" v-if="form.authors.length > 0" :relation="'authors'" />
<div class="text-red-400 text-sm"
v-if="form.errors.authors && Array.isArray(form.errors.authors)">
{{ form.errors.authors.join(', ') }}
</div>
</CardBox>
<!-- (8) contributors -->
<CardBox class="mb-6 shadow" has-table title="Contributors" :icon="mdiBookOpenPageVariant"
:header-icon="mdiPlusCircle" v-on:header-icon-click="addNewContributor()">
<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" :relation="'contributors'" />
<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': form.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': form.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" :icon="mdiEarthPlus"
:header-icon="mdiPlusCircle" v-on:header-icon-click="addReference()">
<!-- Message when no references exist -->
<div v-if="form.references.length === 0" class="text-center py-4">
<p class="text-gray-600">No references added yet.</p>
<p class="text-gray-400">Click the plus icon above to add a new reference.</p>
</div>
<!-- Reference form -->
<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`]">
{{ 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>
<!-- References to delete section -->
<div v-if="form.referencesToDelete && form.referencesToDelete.length > 0" class="mt-8">
<h1 class="pt-8 pb-3 font-semibold sm:text-lg text-gray-900">References To Delete</h1>
<ul class="flex flex-1 flex-wrap -m-1">
<li v-for="(element, index) in form.referencesToDelete" :key="index"
class="block p-1 w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/6 xl:w-1/8 h-40">
<article tabindex="0"
class="bg-red-100 group w-full h-full rounded-md cursor-pointer relative shadow-sm overflow-hidden">
<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 font-medium text-sm mb-1 truncate overflow-hidden whitespace-nowrap">
{{ element.value }}
</h1>
<div class="flex flex-col mt-auto">
<p class="p-1 size text-xs text-gray-700">
<span class="font-semibold">Type:</span> {{ element.type }}
</p>
<p class="p-1 size text-xs text-gray-700">
<span class="font-semibold">Relation:</span> {{ element.relation }}
</p>
<div class="flex justify-end mt-1">
<button
class="restore ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md text-gray-800"
@click.prevent="restoreReference(index)">
<svg viewBox="0 0 24 24" class="w-5 h-5">
<path fill="currentColor" :d="mdiRestore"></path>
</svg>
</button>
</div>
</div>
</section>
</article>
</li>
</ul>
</div>
</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-model:subjects-to-delete="form.subjectsToDelete"
v-if="form.subjects.length > 0" />
</CardBox>
</div>
<!-- download file list -->
<div class="mb-6">
<h3 class="text-lg font-medium text-gray-700 mb-2 flex items-center">
Files
<div class="inline-block relative ml-2 group">
<button
class="w-5 h-5 rounded-full bg-gray-200 text-gray-600 text-xs flex items-center justify-center focus:outline-none hover:bg-gray-300">
i
</button>
<div
class="absolute left-0 top-full mt-1 w-64 bg-white shadow-lg rounded-md p-3 text-xs text-left z-50 transform scale-0 origin-top-left transition-transform duration-100 group-hover:scale-100">
<p class="text-gray-700">
As a research data repository editor, you can only download the submitted files.
Files cannot be
edited or replaced at this stage.
</p>
<div class="absolute -top-1 left-1 w-2 h-2 bg-white transform rotate-45"></div>
</div>
</div>
</h3>
<div v-if="form.files && form.files.length > 0" class="bg-white rounded-lg shadow overflow-hidden">
<ul class="divide-y divide-gray-200">
<li v-for="file in form.files" :key="file.id"
class="px-4 py-3 flex items-center justify-between hover:bg-gray-50">
<div class="flex items-center space-x-3 flex-1">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-gray-900 truncate">
{{ file.label }}
</p>
<p class="text-xs text-gray-500 truncate">
{{ getFileSize(file) }}
</p>
</div>
</div>
<div class="ml-2 flex-shrink-0 flex space-x-2">
<a v-if="file.id != undefined"
:href="stardust.route('editor.file.download', [file.id])"
class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-blue-700 bg-blue-100 hover:bg-blue-200">
Download
</a>
</div>
</li>
</ul>
</div>
</div>
<template #footer>
<BaseButtons>
<BaseButton @click.stop="submit" :disabled="form.processing" label="Save" color="info"
:class="{ 'opacity-25': form.processing }" small>
</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>
</template>
</CardBox>
<!-- Loading Spinner -->
<div v-if="form.processing"
class="fixed inset-0 flex items-center justify-center bg-gray-500 bg-opacity-50 z-50">
<svg class="animate-spin h-12 w-12 text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M12 2a10 10 0 0110 10h-4a6 6 0 00-6-6V2z"></path>
</svg>
</div>
</SectionMain>
</LayoutAuthenticated>
</template>
<script setup lang="ts">
// 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 { useForm, Head, usePage } from '@inertiajs/vue3';
import { computed, ComputedRef } from 'vue';
import { Dataset, Title, Subject, Person, License } 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 { MapOptions } from '@/Components/Map/MapOptions';
import { LatLngBoundsExpression } from 'leaflet';
import { LayerOptions } from '@/Components/Map/LayerOptions';
import {
mdiImageText,
mdiArrowLeftBoldOutline,
mdiPlusCircle,
mdiFinance,
mdiTrashCan,
mdiBookOpenPageVariant,
mdiEarthPlus,
mdiAlertBoxOutline,
mdiRestore
} from '@mdi/js';
import { notify } from '@/notiwind';
import NotificationBar from '@/Components/NotificationBar.vue';
const props = defineProps({
// errors: {
// type: Object,
// default: () => ({}),
// },
licenses: {
type: Object,
default: () => ({}),
},
languages: {
type: Object,
default: () => ({}),
},
doctypes: {
type: Object,
default: () => ({}),
},
titletypes: {
type: Object,
default: () => ({}),
},
projects: {
type: Object,
default: () => ({}),
},
descriptiontypes: {
type: Object,
default: () => ({}),
},
contributorTypes: {
type: Object,
default: () => ({}),
},
subjectTypes: {
type: Object,
default: () => ({}),
},
referenceIdentifierTypes: {
type: Object,
default: () => ({}),
},
relationTypes: {
type: Object,
default: () => ({}),
},
dataset: {
type: Object,
default: () => ({}),
},
});
const flash: ComputedRef<any> = computed(() => {
return usePage().props.flash;
});
const errors: ComputedRef<any> = computed(() => {
return usePage().props.errors;
});
const mapOptions: MapOptions = {
center: [48.208174, 16.373819],
zoom: 3,
zoomControl: false,
attributionControl: false,
};
const baseMaps: Map<string, LayerOptions> = new Map<string, LayerOptions>();
const fitBounds: LatLngBoundsExpression = [
[46.4318173285, 9.47996951665],
[49.0390742051, 16.9796667823],
];
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);
// // });
// }
// props.dataset.filesToDelete = [];
props.dataset.subjectsToDelete = [];
props.dataset.referencesToDelete = [];
let form = useForm<Dataset>(props.dataset as Dataset);
// const mainService = MainService();
// mainService.fetchfiles(props.dataset);
const submit = async (): Promise<void> => {
let route = stardust.route('editor.dataset.update', [props.dataset.id]);
// await Inertia.post('/app/register', this.form);
// await router.post('/app/register', this.form);
let licenses = form.licenses.map((obj) => {
if (hasIdAttribute(obj)) {
return obj.id.toString()
} else {
return obj;
}
});
await form
.transform((data) => ({
...data,
licenses: licenses,
rights: 'true',
}))
// .put(route);
.put(route, {
onSuccess: () => {
// console.log(form.data());
// mainService.setDataset(form.data());
// formStep.value++;
// form.filesToDelete = [];
// Clear the array using splice
form.subjectsToDelete?.splice(0, form.subjectsToDelete.length);
form.referencesToDelete?.splice(0, form.referencesToDelete.length);
},
});
};
const hasIdAttribute = (obj: License | number): obj is License => {
return typeof obj === 'object' && 'id' in obj;
};
const addTitle = () => {
let newTitle: Title = { value: '', language: '', type: '' };
form.titles.push(newTitle);
};
const removeTitle = (key: any) => {
form.titles.splice(key, 1);
};
const addDescription = () => {
let newDescription = { value: '', language: '', type: '' };
form.descriptions.push(newDescription);
};
const removeDescription = (key: any) => {
form.descriptions.splice(key, 1);
};
const addNewAuthor = () => {
let newAuthor = { status: false, first_name: '', last_name: '', email: '', academic_title: '', identifier_orcid: '', name_type: 'Personal' };
form.authors.push(newAuthor);
};
const onAddAuthor = (person: 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 addNewContributor = () => {
let newContributor = { status: false, first_name: '', last_name: '', email: '', academic_title: '', identifier_orcid: '', name_type: 'Personal', pivot: { contributor_type: '' } };
form.contributors.push(newContributor);
};
const onAddContributor = (person: 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: '', 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: any) => {
const reference = form.references[key];
// If the reference has an ID, it exists in the database
// and should be added to referencesToDelete
if (reference.id) {
// Initialize referencesToDelete array if it doesn't exist
if (!form.referencesToDelete) {
form.referencesToDelete = [];
}
// Add to referencesToDelete
form.referencesToDelete.push(reference);
}
// Remove from form.references array
form.references.splice(key, 1);
};
const restoreReference = (index: number) => {
// Get the reference from referencesToDelete
const reference = form.referencesToDelete[index];
// Add it back to form.references
form.references.push(reference);
// Remove it from referencesToDelete
form.referencesToDelete.splice(index, 1);
};
const onMapInitialized = (newItem: any) => {
console.log(newItem);
};
const getFileSize = (file: File) => {
if (file.size > 1024) {
if (file.size > 1048576) {
return Math.round(file.size / 1048576) + 'mb';
} else {
return Math.round(file.size / 1024) + 'kb';
}
} else {
return file.size + 'b';
}
}
</script>
<style scoped>
.max-w-2xl {
max-width: 2xl;
}
.text-2xl {
font-size: 2xl;
}
.font-bold {
font-weight: bold;
}
.mb-4 {
margin-bottom: 1rem;
}
.block {
display: block;
}
.text-gray-700 {
color: #4b5563;
}
.shadow {
box-shadow:
0 0 0 1px rgba(66, 72, 78, 0.05),
0 1px 2px 0 rgba(66, 72, 78, 0.08),
0 2px 4px 0 rgba(66, 72, 78, 0.12),
0 4px 8px 0 rgba(66, 72, 78, 0.16);
}
</style>

View file

@ -2,7 +2,7 @@
// import { Head, Link, useForm, usePage } from '@inertiajs/inertia-vue3';
import { Head, usePage } from '@inertiajs/vue3';
import { ComputedRef } from 'vue';
import { mdiSquareEditOutline, mdiAlertBoxOutline, mdiShareVariant, mdiBookEdit, mdiUndo, mdiLibraryShelves } from '@mdi/js';
import { mdiSquareEditOutline, mdiAlertBoxOutline, mdiShareVariant, mdiBookEdit, mdiUndo } from '@mdi/js';
import { computed } from 'vue';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue';
@ -96,8 +96,8 @@ const formatServerState = (state: string) => {
<Head title="Editor Datasets" />
<SectionMain>
<NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
{{ flash.message }}
</NotificationBar>
@ -108,30 +108,31 @@ const formatServerState = (state: string) => {
{{ flash.error }}
</NotificationBar>
<!-- table -->
<CardBox class="mb-6" has-table>
<div v-if="props.datasets.data.length > 0">
<table>
<thead>
<tr>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider">
<!-- <Sort label="Dataset Title" attribute="title" :search="form.search" /> -->
Title
</th>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider">
Submitter
</th>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider">
<!-- <Sort label="Email" attribute="email" :search="form.search" /> -->
State
</th>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider">
Editor
</th>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider">
Date of last modification
</th>
<th scope="col" class="relative px-6 py-3 dark:text-white" v-if="can.edit || can.delete">
<th scope="col" class="relative px-6 py-3" v-if="can.edit || can.delete">
<span class="sr-only">Actions</span>
</th>
</tr>
@ -140,110 +141,70 @@ const formatServerState = (state: string) => {
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="dataset in props.datasets.data" :key="dataset.id"
:class="[getRowClass(dataset)]">
<td data-label="Login"
class="py-4 whitespace-nowrap text-gray-700 table-title">
<!-- <Link v-bind:href="stardust.route('settings.user.show', [user.id])"
<td data-label="Login" class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
<!-- <Link v-bind:href="stardust.route('user.show', [user.id])"
class="no-underline hover:underline text-cyan-600 dark:text-cyan-400">
{{ user.login }}
</Link> -->
<!-- {{ user.id }} -->
{{ dataset.main_title }}
<div class="text-sm font-medium">{{ dataset.main_title }}</div>
</td>
<td class="py-4 whitespace-nowrap text-gray-700">
<td class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
<div class="text-sm">{{ dataset.user.login }}</div>
</td>
<td class="py-4 whitespace-nowrap text-gray-700">
<td class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
<div class="text-sm"> {{ formatServerState(dataset.server_state) }}</div>
<div v-if="dataset.server_state === 'rejected_reviewer' && dataset.reject_reviewer_note"
class="inline-block relative ml-2 group">
<button
class="w-5 h-5 rounded-full bg-gray-200 text-gray-600 text-xs flex items-center justify-center focus:outline-none hover:bg-gray-300">
i
</button>
<div
class="absolute left-0 top-full mt-1 w-64 bg-white shadow-lg rounded-md p-3 text-xs text-left z-50 transform scale-0 origin-top-left transition-transform duration-100 group-hover:scale-100">
<p class="text-gray-700 max-h-40 overflow-y-auto overflow-x-hidden whitespace-normal break-words">
{{ dataset.reject_reviewer_note }}
</p>
<div class="absolute -top-1 left-1 w-2 h-2 bg-white transform rotate-45">
</div>
</div>
</div>
</td>
<td class="py-4 whitespace-nowrap text-gray-700"
<td class="py-4 whitespace-nowrap text-gray-700 dark:text-white"
v-if="dataset.server_state === 'released'">
<div class="text-sm" :title="dataset.server_date_modified">
Preferred reviewer: {{ dataset.preferred_reviewer }}
</div>
</td>
<td class="py-4 whitespace-nowrap text-gray-700"
<td class="py-4 whitespace-nowrap text-gray-700 dark:text-white"
v-else-if="dataset.server_state === 'editor_accepted' || dataset.server_state === 'rejected_reviewer'">
<div class="text-sm" :title="dataset.server_date_modified">
In approval by: {{ dataset.editor?.login }}
</div>
</td>
<td class="py-4 whitespace-nowrap text-gray-700" v-else>
<td class="py-4 whitespace-nowrap text-gray-700 dark:text-white" v-else>
<div class="text-sm">{{ dataset.editor?.login }}</div>
</td>
<td data-label="modified" class="py-4 whitespace-nowrap text-gray-700">
<td data-label="modified" class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
<div class="text-sm" :title="dataset.server_date_modified">
{{ dataset.server_date_modified }}
</div>
</td>
<td
class="py-4 whitespace-nowrap text-right text-sm font-medium text-gray-700 dark:text-white">
<div type="justify-start lg:justify-end" class="grid grid-cols-2 gap-x-2 gap-y-2"
no-wrap>
<BaseButtons type="justify-start lg:justify-end" no-wrap>
<BaseButton v-if="can.receive && (dataset.server_state == 'released')"
:route-name="stardust.route('editor.dataset.receive', [dataset.id])"
color="info" :icon="mdiSquareEditOutline" :label="'Receive edit task'" small
class="col-span-1" />
color="info" :icon="mdiSquareEditOutline" :label="'Receive edit task'"
small />
<BaseButton
v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
:route-name="stardust.route('editor.dataset.approve', [dataset.id])"
color="info" :icon="mdiShareVariant" :label="'Approve'" small
class="col-span-1" />
color="info" :icon="mdiShareVariant" :label="'Approve'" small />
<BaseButton
v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
:route-name="stardust.route('editor.dataset.reject', [dataset.id])"
color="info" :icon="mdiUndo" label="Reject" small class="col-span-1">
:route-name="stardust.route('editor.dataset.reject', [dataset.id])"
color="info" :icon="mdiUndo" label="Reject" small>
</BaseButton>
<BaseButton
v-if="can.edit && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
:route-name="stardust.route('editor.dataset.edit', [Number(dataset.id)])"
color="info" :icon="mdiSquareEditOutline" :label="'Edit'" small
class="col-span-1">
</BaseButton>
<BaseButton
v-if="can.edit && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
:route-name="stardust.route('editor.dataset.categorize', [dataset.id])"
color="info" :icon="mdiLibraryShelves" :label="'Classify'" small
class="col-span-1">
</BaseButton>
<BaseButton v-if="can.publish && (dataset.server_state == 'reviewed')"
:route-name="stardust.route('editor.dataset.rejectToReviewer', [dataset.id])"
color="info" :icon="mdiUndo" :label="'Reject To Reviewer'" small
class="col-span-1" />
<BaseButton v-if="can.publish && (dataset.server_state == 'reviewed')"
:route-name="stardust.route('editor.dataset.publish', [dataset.id])"
color="info" :icon="mdiBookEdit" :label="'Publish'" small
class="col-span-1" />
color="info" :icon="mdiBookEdit" :label="'Publish'" small />
<BaseButton
v-if="can.publish && (dataset.server_state == 'published' && !dataset.identifier)"
:route-name="stardust.route('editor.dataset.doi', [dataset.id])"
color="info" :icon="mdiBookEdit" :label="'Mint DOI'" small
class="col-span-1 last-in-row" />
color="info" :icon="mdiBookEdit" :label="'Mint DOI'" small />
</div>
</BaseButtons>
</td>
</tr>
</tbody>
@ -270,17 +231,3 @@ const formatServerState = (state: string) => {
</SectionMain>
</LayoutAuthenticated>
</template>
<style scoped lang="css">
.table-title {
max-width: 200px;
/* set a maximum width */
overflow: hidden;
/* hide overflow */
text-overflow: ellipsis;
/* show ellipsis for overflowed text */
white-space: nowrap;
/* prevent wrapping */
}
</style>

View file

@ -10,7 +10,7 @@ import FormControl from '@/Components/FormControl.vue';
import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
import { mdiArrowLeftBoldOutline, mdiReiterate, mdiBookEdit, mdiUndo } from '@mdi/js';
import { mdiArrowLeftBoldOutline, mdiReiterate } from '@mdi/js';
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
const props = defineProps({
@ -18,10 +18,6 @@ const props = defineProps({
type: Object,
default: () => ({}),
},
can: {
type: Object,
default: () => ({}),
},
});
const flash: Ref<any> = computed(() => {
@ -97,11 +93,7 @@ const handleSubmit = async (e) => {
</p>
<BaseButtons>
<BaseButton type="submit" color="info" label="Set published"
:class="{ 'opacity-25': form.processing }" :disabled="form.processing" :icon="mdiBookEdit" small />
<BaseButton v-if="can.reject && (dataset.server_state == 'reviewed')"
:route-name="stardust.route('editor.dataset.rejectToReviewer', [dataset.id])"
color="info" :icon="mdiUndo" :label="'Reject To Reviewer'" small
class="col-span-1" />
:class="{ 'opacity-25': form.processing }" :disabled="form.processing" />
</BaseButtons>
</template>
</CardBox>

View file

@ -1,117 +0,0 @@
<script setup lang="ts">
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import SectionMain from '@/Components/SectionMain.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
import { useForm, Head, usePage } from '@inertiajs/vue3';
import { computed, Ref } from 'vue';
import CardBox from '@/Components/CardBox.vue';
import FormField from '@/Components/FormField.vue';
import FormControl from '@/Components/FormControl.vue';
import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
import { mdiArrowLeftBoldOutline, mdiReiterate } from '@mdi/js';
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
const props = defineProps({
dataset: {
type: Object,
default: () => ({}),
},
});
// Define the computed property for the label
const computedLabel = computed(() => {
return `Reject to reviewer: ${props.dataset.reviewer?.login || 'Unknown User'}`;
});
const computedEmailLabel = computed(() => {
return props.dataset.reviewer?.email || '';
});
const flash: Ref<any> = computed(() => {
return usePage().props.flash;
});
const errors: Ref<any> = computed(() => {
return usePage().props.errors;
});
const form = useForm({
server_state: 'rejected_to_reviewer',
reject_editor_note: '',
send_email: false,
});
const handleSubmit = async (e: SubmitEvent) => {
e.preventDefault();
await form.put(stardust.route('editor.dataset.rejectToReviewerUpdate', [props.dataset.id]));
// await form.put(stardust.route('editor.dataset.update', [props.dataset.id]));
};
</script>
<template>
<LayoutAuthenticated>
<Head title="Reject reviewed dataset" />
<SectionMain>
<SectionTitleLineWithButton :icon="mdiReiterate" title="Reject reviewed dataset to reviewer" main>
<BaseButton :route-name="stardust.route('editor.dataset.list')" :icon="mdiArrowLeftBoldOutline"
label="Back" color="white" rounded-full small />
</SectionTitleLineWithButton>
<CardBox form @submit.prevent="handleSubmit">
<FormValidationErrors v-bind:errors="errors" />
<FormField label="server state" :class="{ 'text-red-400': form.errors.server_state }">
<FormControl v-model="form.server_state" type="text" placeholder="-- server state --"
:is-read-only="true" :error="form.errors.server_state">
<div class="text-red-400 text-sm" v-if="form.errors.server_state">
{{ form.errors.server_state }}
</div>
</FormControl>
</FormField>
<FormField label="reject note" :class="{ 'text-red-400': form.errors.reject_editor_note }">
<FormControl v-model="form.reject_editor_note" type="textarea"
placeholder="-- reject note for reviewer --" :error="form.errors.reject_editor_note">
<div class="text-red-400 text-sm" v-if="form.errors.reject_editor_note">
{{ form.errors.reject_editor_note }}
</div>
</FormControl>
</FormField>
<!-- <FormControl
type="checkbox"
v-model="form.send_email"
:error="form.errors.send_email">
</FormControl> -->
<FormField label="Email Notification">
<label for="send_email" class="flex items-center mr-6 mb-3">
<input type="checkbox" id="send_email" v-model="form.send_email" class="mr-2" />
<span class="check"></span>
<a class="pl-2 " target="_blank">send email to reviewer
<span class="text-blue-600 hover:underline">
{{ computedEmailLabel }}
</span>
</a>
</label>
</FormField>
<div v-if="flash && flash.warning" class="flex flex-col mt-6 animate-fade-in">
<div class="bg-yellow-500 border-l-4 border-orange-400 text-white p-4" role="alert">
<p class="font-bold">Be Warned</p>
<p>{{ flash.warning }}</p>
</div>
</div>
<template #footer>
<BaseButtons>
<BaseButton type="submit" color="info" :label="computedLabel"
:class="{ 'opacity-25': form.processing }" :disabled="form.processing" />
</BaseButtons>
</template>
</CardBox>
</SectionMain>
</LayoutAuthenticated>
</template>

View file

@ -20,13 +20,11 @@ import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.
import BaseButton from '@/Components/BaseButton.vue';
import { mdiLightbulbAlert, mdiArrowLeftBoldOutline } from '@mdi/js';
import { stardust } from '@eidellev/adonis-stardust/client';
import LayoutGuest from '@/Layouts/LayoutGuest.vue';
@Component({
options: {
layout: LayoutGuest,
},
@Component({
// options: {
// layout: DefaultLayout,
// },
name: 'AppComponent',
components: {

View file

@ -1,71 +0,0 @@
<template>
<div class="min-h-screen flex items-center justify-center bg-gray-100">
<div class="max-w-md w-full p-6 bg-white rounded-md shadow-md">
<h1 class="text-2xl font-bold text-red-500 mb-4">{{ status }}</h1>
<p class="text-gray-700 mb-4">{{ message }}</p>
<div class="text-sm text-gray-500 mb-4">
<p>Error Code: {{ details.code }}</p>
<p>Type: {{ details.type }}</p>
<div v-for="(port, index) in details.ports" :key="index">
<p>Connection attempt {{ index + 1 }}: {{ port.address }}:{{ port.port }}</p>
</div>
</div>
<SectionTitleLineWithButton :icon="mdiLightbulbAlert" :title="'Database Error'" :main="true">
<BaseButton @click.prevent="handleAction" :icon="mdiArrowLeftBoldOutline" label="Dashboard"
color="white" rounded-full small />
</SectionTitleLineWithButton>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from 'vue-facing-decorator';
import { Link, router } from '@inertiajs/vue3';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
import BaseButton from '@/Components/BaseButton.vue';
import { mdiLightbulbAlert, mdiArrowLeftBoldOutline } from '@mdi/js';
import { stardust } from '@eidellev/adonis-stardust/client';
import LayoutGuest from '@/Layouts/LayoutGuest.vue';
@Component({
options: {
layout: LayoutGuest,
},
name: 'PostgresError',
components: {
Link,
BaseButton,
SectionTitleLineWithButton,
},
})
export default class AppComponent extends Vue {
@Prop({
type: String,
default: '',
})
status: string;
@Prop({
type: String,
default: '',
})
message: string;
@Prop({
type: Object,
default: () => ({}),
})
details: {
code: string;
type: string;
ports: Array<{ port: number; address: string }>;
};
mdiLightbulbAlert = mdiLightbulbAlert;
mdiArrowLeftBoldOutline = mdiArrowLeftBoldOutline;
public async handleAction() {
await router.get(stardust.route('dashboard'));
}
}
</script>

View file

@ -51,8 +51,6 @@ const getRowClass = (dataset) => {
rowclass = 'bg-released';
} else if (dataset.server_state == 'published') {
rowclass = 'bg-published';
} else if (dataset.server_state == 'rejected_to_reviewer') {
rowclass = 'bg-rejected-reviewer';
} else {
rowclass = '';
}
@ -98,14 +96,14 @@ const formatServerState = (state: string) => {
<table>
<thead>
<tr>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider">
<!-- <Sort label="Dataset Title" attribute="title" :search="form.search" /> -->
Title
</th>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider">
ID
</th>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider">
<!-- <Sort label="Email" attribute="email" :search="form.search" /> -->
State
</th>
@ -113,10 +111,10 @@ const formatServerState = (state: string) => {
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider">
Editor
</th>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider">
Remaining Time
</th>
<th scope="col" class="relative px-6 py-3 dark:text-white" v-if="can.edit || can.delete">
<th scope="col" class="relative px-6 py-3" v-if="can.edit || can.delete">
<span class="sr-only">Actions</span>
</th>
</tr>
@ -124,52 +122,39 @@ const formatServerState = (state: string) => {
<tbody>
<tr v-for="dataset in props.datasets.data" :key="dataset.id" :class="[getRowClass(dataset)]">
<td data-label="Login"
class="py-4 whitespace-nowrap text-gray-700">
<div class="text-sm table-title">{{ dataset.main_title }}</div>
<td data-label="Login" class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
<!-- <Link v-bind:href="stardust.route('user.show', [user.id])"
class="no-underline hover:underline text-cyan-600 dark:text-cyan-400">
{{ user.login }}
</Link> -->
<div class="text-sm font-medium">{{ dataset.main_title }}</div>
</td>
<td class="py-4 whitespace-nowrap text-gray-700">
<td class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
<div class="text-sm">{{ dataset.id }}</div>
</td>
<td class="py-4 whitespace-nowrap text-gray-700">
<td class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
<div class="text-sm">{{ formatServerState(dataset.server_state) }}</div>
<div v-if="dataset.server_state === 'rejected_to_reviewer' && dataset.reject_editor_note"
class="inline-block relative ml-2 group">
<button
class="w-5 h-5 rounded-full bg-gray-200 text-gray-600 text-xs flex items-center justify-center focus:outline-none hover:bg-gray-300">
i
</button>
<div
class="absolute left-0 top-full mt-1 w-64 bg-white shadow-lg rounded-md p-3 text-xs text-left z-50 transform scale-0 origin-top-left transition-transform duration-100 group-hover:scale-100">
<p class="text-gray-700 max-h-40 overflow-y-auto overflow-x-hidden whitespace-normal break-words">
{{ dataset.reject_editor_note }}
</p>
<div class="absolute -top-1 left-1 w-2 h-2 bg-white transform rotate-45">
</div>
</div>
</div>
</td>
<td class="py-4 whitespace-nowrap text-gray-700">
<td class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
<div class="text-sm">{{ dataset.editor?.login }}</div>
</td>
<td data-label="modified" class="py-4 whitespace-nowrap text-gray-700">
<td data-label="modified" class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
<div class="text-sm" :title="dataset.remaining_time">
{{ dataset.remaining_time + ' days' }}
</div>
</td>
<td
class="py-4 whitespace-nowrap text-right text-sm font-medium text-gray-700">
class="py-4 whitespace-nowrap text-right text-sm font-medium text-gray-700 dark:text-white">
<BaseButtons type="justify-start lg:justify-end" no-wrap>
<BaseButton
v-if="can.reject && (dataset.server_state == 'approved' || dataset.server_state == 'rejected_to_reviewer')"
<BaseButton v-if="can.review && (dataset.server_state == 'approved')"
:route-name="stardust.route('reviewer.dataset.review', [dataset.id])"
color="info" :icon="mdiGlasses" :label="'View'" small />
color="info" :icon="mdiGlasses" :label="'Review'" small />
<BaseButton
v-if="can.reject && (dataset.server_state == 'approved' || dataset.server_state == 'rejected_to_reviewer')"
v-if="can.reject && (dataset.server_state == 'approved')"
:route-name="stardust.route('reviewer.dataset.reject', [dataset.id])"
color="info" :icon="mdiReiterate" :label="'Reject'" small />
</BaseButtons>
@ -200,16 +185,3 @@ const formatServerState = (state: string) => {
</LayoutAuthenticated>
</template>
<style scoped lang="css">
.table-title {
max-width: 200px;
/* set a maximum width */
overflow: hidden;
/* hide overflow */
text-overflow: ellipsis;
/* show ellipsis for overflowed text */
white-space: nowrap;
/* prevent wrapping */
}
</style>

View file

@ -10,23 +10,15 @@ import BaseButtons from '@/Components/BaseButtons.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
import { mdiArrowLeftBoldOutline, mdiGlasses } from '@mdi/js';
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
import { mdiReiterate, mdiBookOpenPageVariant, mdiFinance } from '@mdi/js';
import MapComponentView from '@/Components/Map/MapComponentView.vue';
import IconSvg from '@/Components/Icons/IconSvg.vue';
import CardBoxSimple from '@/Components/CardBoxSimple.vue';
const props = defineProps({
dataset: {
type: Object,
default: () => ({}),
},
// fields: {
// type: Object,
// required: true,
// },
can: {
fields: {
type: Object,
default: () => ({}),
required: true,
},
});
@ -37,6 +29,18 @@ const errors: Ref<any> = computed(() => {
return usePage().props.errors;
});
// const form = useForm({
// preferred_reviewer: '',
// preferred_reviewer_email: '',
// preferation: 'yes_preferation',
// // preferation: '',
// // isPreferationRequired: false,
// });
// const isPreferationRequired = computed(() => form.preferation === 'yes_preferation');
const handleSubmit = async (e) => {
e.preventDefault();
@ -44,19 +48,6 @@ const handleSubmit = async (e) => {
// await form.put(stardust.route('dataset.releaseUpdate', [props.dataset.id]));
// // await form.put(stardust.route('editor.dataset.update', [props.dataset.id]));
};
const getFileSize = (file: File) => {
if (file.size > 1024) {
if (file.size > 1048576) {
return Math.round(file.size / 1048576) + 'mb';
} else {
return Math.round(file.size / 1024) + 'kb';
}
} else {
return file.size + 'b';
}
}
</script>
<template>
@ -65,10 +56,10 @@ const getFileSize = (file: File) => {
<Head title="Review dataset" />
<SectionMain>
<SectionTitleLineWithButton :icon="mdiGlasses" title="Review approved dataset" main>
<BaseButton :route-name="stardust.route('reviewer.dataset.list')" :icon="mdiArrowLeftBoldOutline"
label="Back" color="white" rounded-full small />
<BaseButton :route-name="stardust.route('reviewer.dataset.list')" :icon="mdiArrowLeftBoldOutline" label="Back"
color="white" rounded-full small />
</SectionTitleLineWithButton>
<component is="form" form @submit.prevent="handleSubmit">
<CardBox form @submit.prevent="handleSubmit">
<FormValidationErrors v-bind:errors="errors" />
<div v-if="flash && flash.warning" class="flex flex-col mt-6 animate-fade-in">
@ -79,545 +70,26 @@ const getFileSize = (file: File) => {
</div>
<!-- <div class="mb-4"> -->
<div class="flex flex-col">
<div class="flex flex-row items-center justify-between dark:bg-slate-900 bg-gray-200 p-2 mb-2"
v-for="(fieldValue, field) in fields" :key="field">
<CardBoxSimple>
<div class="mb-6">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Language</h4>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm">
<div class="flex items-center">
<IconSvg path="language" :size="20"
className="mr-2 text-emerald-600 dark:text-emerald-400" />
<span class="text-emerald-700 dark:text-emerald-300 font-medium">
{{ dataset.language || 'Not specified' }}
</span>
</div>
</div>
<label :for="field" class="font-bold h-6 mt-3 text-xs leading-8 uppercase">{{ field }}</label>
<span class="text-sm text-gray-600" v-html="fieldValue"></span>
</div>
<!-- Licenses -->
<div class="mb-6">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Licenses</h4>
<div v-if="dataset.licenses && dataset.licenses.length > 0" class="space-y-2">
<div v-for="license in dataset.licenses" :key="license.id" class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm hover:bg-emerald-100 dark:hover:bg-emerald-900/50 transition-colors">
<div class="flex items-center">
<IconSvg path="license" :size="20"
className="mr-2 text-emerald-600 dark:text-emerald-400" />
<span class="text-emerald-700 dark:text-emerald-300">
{{ license.name }}
</span>
</div>
</div>
</div>
<div v-else class="text-center py-4">
<p class="text-gray-500 dark:text-gray-400 italic">No licenses specified</p>
</div>
<div v-if="dataset.licenses && dataset.licenses.length > 0"
class="mt-3 text-xs text-gray-500 dark:text-gray-400">
Total licenses: {{ dataset.licenses.length }}
</div>
</div>
<div class="flex flex-col md:flex-row gap-4 mb-6">
<!-- (3) dataset_type -->
<div class="w-full mx-2 flex-1">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Dataset Type</h4>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm">
<div class="flex items-center">
<IconSvg path="book" :size="20"
className="mr-2 text-emerald-600 dark:text-emerald-400" />
<span class="text-emerald-700 dark:text-emerald-300 font-medium">
{{ dataset.type || 'Not specified' }}
</span>
</div>
</div>
</div>
<!-- (4) creating_corporation -->
<div class="w-full mx-2 flex-1">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Creating
Corporation
</h4>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm">
<div class="flex items-center">
<IconSvg path="building" :size="20"
className="mr-2 text-emerald-600 dark:text-emerald-400" />
<span class="text-emerald-700 dark:text-emerald-300 font-medium">
{{ dataset.creating_corporation || 'Not specified' }}
</span>
</div>
</div>
</div>
</div>
<div class="flex flex-col md:flex-row gap-4 mb-6">
<!-- (9) project_id -->
<div class="w-full mx-2 flex-1">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Project</h4>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm">
<span class="text-emerald-700 dark:text-emerald-300">
{{ dataset.project?.label || 'Not specified' }}
</span>
</div>
</div>
<!-- (10) embargo_date -->
<div class="w-full mx-2 flex-1">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Embargo Date</h4>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm">
<span v-if="dataset.embargo_date" class="text-emerald-700 dark:text-emerald-300">
{{ dataset.embargo_date }}
</span>
<span v-else class="text-gray-500 dark:text-gray-400 italic">
No embargo date set
</span>
</div>
</div>
</div>
</CardBoxSimple>
<!-- (5) titles -->
<CardBox class="mb-6 shadow" :has-form-data="false" title="Titles" :icon="mdiFinance"
:show-header-icon="false">
<div class="p-4">
<!-- Main Title (highlighted) -->
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Main Title</h4>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border-l-4 border border-emerald-300 dark:border-emerald-700
rounded-lg p-4 shadow-sm mb-4">
<div class="flex justify-between items-start mb-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-200
dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200">
{{ dataset.titles[0].language }}
</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-200
dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200">
Main
</span>
</div>
<h3
class="text-lg font-medium text-emerald-800 dark:text-emerald-200 whitespace-pre-line break-words overflow-wrap-anywhere">
{{ dataset.titles[0].value }}
</h3>
</div>
<!-- Additional titles -->
<div v-if="dataset.titles.length > 1">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Additional Titles
</h4>
<div class="grid gap-3">
<template v-for="(title, index) in dataset.titles" :key="index">
<div v-if="title.type != 'Main'"
class="bg-emerald-50/70 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm hover:bg-emerald-50 dark:hover:bg-emerald-900/30 transition-colors">
<div class="flex justify-between items-start mb-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
bg-emerald-100 dark:bg-emerald-800/70 text-emerald-700 dark:text-emerald-300">
{{ title.language }}
</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
bg-emerald-100 dark:bg-emerald-800/70 text-emerald-700 dark:text-emerald-300">
{{ title.type }}
</span>
</div>
<p
class="text-emerald-700 dark:text-emerald-300 font-medium whitespace-pre-line break-words overflow-wrap-anywhere">
{{ title.value }}
</p>
</div>
</template>
</div>
</div>
<div class="mt-3 text-xs text-gray-500 dark:text-gray-400">
Total titles: {{ dataset.titles.length }}
</div>
</div>
</CardBox>
<!-- (6) descriptions -->
<CardBox class="mb-6 shadow" :has-form-data="false" title="Descriptions" :icon="mdiFinance"
:show-header-icon="false">
<!-- Main Abstract (highlighted) -->
<div class="p-4">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Main Abstract</h4>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border-l-4 border border-emerald-300 dark:border-emerald-700
rounded-lg p-4 shadow-sm mb-4">
<div class="flex justify-between items-start mb-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-200
dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200">
{{ dataset.descriptions[0].language }}
</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-200
dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200">
Abstract
</span>
</div>
<div class="prose prose-emerald dark:prose-invert max-w-none">
<p
class="text-emerald-800 dark:text-emerald-200 whitespace-pre-line break-words overflow-wrap-anywhere">
{{ dataset.descriptions[0].value }}
</p>
</div>
</div>
<!-- Additional descriptions -->
<div v-if="dataset.descriptions.length > 1">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Additional
Descriptions</h4>
<div class="grid gap-3">
<div v-for="(item, index) in dataset.descriptions" :key="index">
<div v-if="item.type != 'Abstract'" class="bg-emerald-50/70 dark:bg-emerald-900/20 border border-emerald-200
dark:border-emerald-800
rounded-lg p-3 shadow-sm hover:bg-emerald-50 dark:hover:bg-emerald-900/30
transition-colors">
<div class="flex justify-between items-start mb-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
bg-emerald-100 dark:bg-emerald-800/70 text-emerald-700 dark:text-emerald-300">
{{ item.language }}
</span>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
bg-emerald-100 dark:bg-emerald-800/70 text-emerald-700 dark:text-emerald-300">
{{ item.type }}
</span>
</div>
<p
class="text-emerald-700 dark:text-emerald-300 text-sm whitespace-pre-line break-words overflow-wrap-anywhere">
{{ item.value }}
</p>
</div>
</div>
</div>
</div>
<div class="mt-3 text-xs text-gray-500 dark:text-gray-400">
Total descriptions: {{ dataset.descriptions.length }}
</div>
</div>
</CardBox>
</div>
<!-- (7) authors -->
<CardBox class="mb-6 shadow" has-table title="Creators" :icon="mdiBookOpenPageVariant"
:show-header-icon="false">
<div v-if="dataset.authors.length === 0" class="text-center py-6">
<p class="text-gray-500 dark:text-gray-400 italic">No authors defined</p>
</div>
<div v-else class="p-4">
<!-- <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Dataset Authors:</h4> -->
<div class="grid gap-3">
<div v-for="(author, index) in dataset.authors" :key="index" class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm hover:bg-emerald-100 dark:hover:bg-emerald-900/50 transition-colors">
<div class="flex flex-col md:flex-row md:items-center">
<div class="flex-1">
<p class="font-medium text-emerald-700 dark:text-emerald-300">
{{ author.academic_title }} {{ author.first_name }} {{ author.last_name
}}
</p>
<div class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-2">
<div v-if="author.email" class="flex items-center text-sm">
<IconSvg path="email" :size="16"
className="mr-1 text-emerald-600 dark:text-emerald-400" />
<span class="text-emerald-600 dark:text-emerald-400">{{ author.email
}}</span>
</div>
<div v-if="author.identifier_orcid" class="flex items-center text-sm">
<IconSvg path="idCard" :size="16"
className="mr-1 text-emerald-600 dark:text-emerald-400">
</IconSvg>
<span class="text-emerald-600 dark:text-emerald-400">ORCID: {{
author.identifier_orcid
}}</span>
</div>
</div>
</div>
<div v-if="author.academic_title" class="mt-2 md:mt-0 md:ml-4">
<span class="bg-emerald-200 dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200
text-xs px-2 py-1 rounded-full">
{{ author.academic_title }}
</span>
</div>
</div>
</div>
</div>
<div v-if="dataset.authors.length > 0" class="mt-3 text-xs text-gray-500 dark:text-gray-400">
Total authors: {{ dataset.authors.length }}
</div>
</div>
</CardBox>
<!-- (8) contributors -->
<CardBox class="mb-6 shadow" has-table title="Contributors" :icon="mdiBookOpenPageVariant"
:show-header-icon="false">
<div v-if="dataset.contributors.length === 0" class="text-center py-6">
<p class="text-gray-500 dark:text-gray-400 italic">No contributors defined</p>
</div>
<div v-else class="p-4">
<!-- <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Dataset Contributors:
</h4> -->
<div class="grid gap-3">
<div v-for="(contributor, index) in dataset.contributors" :key="index" class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm hover:bg-emerald-100 dark:hover:bg-emerald-900/50 transition-colors">
<div class="flex flex-col md:flex-row md:items-center">
<div class="flex-1">
<div class="flex flex-wrap items-center gap-2">
<p class="font-medium text-emerald-700 dark:text-emerald-300">
{{ contributor.academic_title }} {{ contributor.first_name }} {{
contributor.last_name
}}
</p>
<span v-if="contributor.pivot_contributor_type" class="bg-emerald-200 dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200
text-xs px-2 py-1 rounded-full">
{{ contributor.pivot_contributor_type }}
</span>
</div>
<div class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-2">
<div v-if="contributor.email" class="flex items-center text-sm">
<IconSvg path="email" :size="16"
className="mr-1 text-emerald-600 dark:text-emerald-400" />
<span class="text-emerald-600 dark:text-emerald-400">{{
contributor.email }}</span>
</div>
<div v-if="contributor.identifier_orcid" class="flex items-center text-sm">
<IconSvg path="idCard" :size="16"
className="mr-1 text-emerald-600 dark:text-emerald-400">
</IconSvg>
<span class="text-emerald-600 dark:text-emerald-400">ORCID: {{
contributor.identifier_orcid }}</span>
</div>
</div>
</div>
<div v-if="contributor.academic_title" class="mt-2 md:mt-0 md:ml-4">
<span class="bg-emerald-200 dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200
text-xs px-2 py-1 rounded-full">
{{ contributor.academic_title }}
</span>
</div>
</div>
</div>
</div>
<div v-if="dataset.contributors.length > 0"
class="mt-3 text-xs text-gray-500 dark:text-gray-400">
Total contributors: {{ dataset.contributors.length }}
</div>
</div>
</CardBox>
<!-- Map component -->
<CardBoxSimple>
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Geographic Coverage</h4>
<!-- Map container with emerald styling -->
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg shadow-sm overflow-hidden">
<!-- The actual map component -->
<div class="h-64 rounded-md overflow-hidden">
<!-- Use the simplified map component -->
<MapComponentView v-if="dataset.coverage" :coverage="dataset.coverage" height="250px"
:mapId="'dataset-review-map'" />
</div>
<!-- Optional: Add a caption or description -->
<div class="mt-2 text-xs text-emerald-600 dark:text-emerald-400 text-center">
Geographic extent of the dataset
</div>
</div>
<!-- Coordinates display below the map -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 mt-3">
<!-- x min -->
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">X Min
(Longitude)</label>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-2.5 shadow-sm">
<span class="text-emerald-700 dark:text-emerald-300">
{{ dataset.coverage.x_min }}
</span>
</div>
</div>
<!-- x max -->
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">X Max
(Longitude)</label>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-2.5 shadow-sm">
<span class="text-emerald-700 dark:text-emerald-300">
{{ dataset.coverage.x_max }}
</span>
</div>
</div>
<!-- y min -->
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Y Min
(Latitude)</label>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-2.5 shadow-sm">
<span class="text-emerald-700 dark:text-emerald-300">
{{ dataset.coverage.y_min }}
</span>
</div>
</div>
<!-- y max -->
<div>
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Y Max
(Latitude)</label>
<div class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-2.5 shadow-sm">
<span class="text-emerald-700 dark:text-emerald-300">
{{ dataset.coverage.y_max }}
</span>
</div>
</div>
</div>
</CardBoxSimple>
<!-- References -->
<CardBoxSimple>
<div v-if="dataset.references.length === 0" class="text-center py-6">
<p class="text-gray-500 dark:text-gray-400 italic">No references added.</p>
</div>
<div v-else class="p-4">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Dataset References:
</h4>
<div class="grid gap-3">
<div v-for="(item, index) in dataset.references" :key="index" class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm hover:bg-emerald-100 dark:hover:bg-emerald-900/50 transition-colors">
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
<div class="flex-1">
<p class="font-medium text-emerald-700 dark:text-emerald-300">{{ item.value
}}</p>
<p class="text-sm text-emerald-600 dark:text-emerald-400 mt-1">{{ item.label
}}</p>
</div>
<div class="flex mt-2 md:mt-0 space-x-2">
<span class="bg-emerald-200 dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200
text-xs px-2 py-1 rounded-full">
{{ item.type }}
</span>
<span class="bg-emerald-200 dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200
text-xs px-2 py-1 rounded-full">
{{ item.relation }}
</span>
</div>
</div>
</div>
</div>
<div v-if="dataset.references.length > 0" class="mt-3 text-xs text-gray-500 dark:text-gray-400">
Total references: {{ dataset.references.length }}
</div>
</div>
</CardBoxSimple>
<!-- Keywords -->
<CardBoxSimple>
<div v-if="dataset.subjects.length === 0" class="text-center py-6">
<p class="text-gray-500 dark:text-gray-400 italic">No keywords added.</p>
</div>
<div v-else class="p-4">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Keywords/Subjects:
</h4>
<div class="flex flex-wrap gap-2">
<div v-for="(subject, index) in dataset.subjects" :key="index" class="bg-emerald-50 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300
px-3 py-1.5 rounded-full text-sm font-medium border border-emerald-200
dark:border-emerald-800 shadow-sm hover:bg-emerald-100
dark:hover:bg-emerald-900/50 transition-colors">
<span>{{ subject.value }}</span>
<span class="ml-1 text-xs text-emerald-600 dark:text-emerald-400">({{ subject.type
}})</span>
</div>
</div>
<div v-if="dataset.subjects.length > 0" class="mt-3 text-xs text-gray-500 dark:text-gray-400">
Total keywords: {{ dataset.subjects.length }}
</div>
</div>
</CardBoxSimple>
<!-- download file list -->
<CardBoxSimple>
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Dataset Files</h4>
<div v-if="dataset.files && dataset.files.length > 0" class="space-y-2">
<div v-for="file in dataset.files" :key="file.id" class="bg-emerald-50 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800
rounded-lg p-3 shadow-sm hover:bg-emerald-100 dark:hover:bg-emerald-900/50 transition-colors">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3 flex-1">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-emerald-600 dark:text-emerald-400"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-emerald-700 dark:text-emerald-300 truncate">
{{ file.label }}
</p>
<p class="text-xs text-emerald-600/70 dark:text-emerald-400/70 truncate">
{{ getFileSize(file) }}
</p>
</div>
</div>
<div class="ml-2 flex-shrink-0">
<a v-if="file.id != undefined"
:href="stardust.route('reviewer.file.download', [file.id])" class="inline-flex items-center px-3 py-1.5 border border-emerald-300 dark:border-emerald-700
text-xs font-medium rounded-full text-emerald-700 bg-emerald-100
dark:text-emerald-200 dark:bg-emerald-800/70 hover:bg-emerald-200
dark:hover:bg-emerald-800 transition-colors">
<IconSvg path="download" :size="20" className="mr-1" />
Download
</a>
</div>
</div>
</div>
</div>
<div v-else
class="text-center py-6 bg-emerald-50/50 dark:bg-emerald-900/10 rounded-lg border border-emerald-100 dark:border-emerald-900/30">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-12 w-12 mx-auto text-emerald-300 dark:text-emerald-700" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p class="mt-2 text-emerald-600 dark:text-emerald-400 italic">No files attached to this dataset
</p>
</div>
<div v-if="dataset.files && dataset.files.length > 0"
class="mt-3 text-xs text-gray-500 dark:text-gray-400">
Total files: {{ dataset.files.length }}
</div>
</CardBoxSimple>
<BaseButtons>
<!-- <BaseButton type="submit" color="info" label="Receive"
<template #footer>
<BaseButtons>
<!-- <BaseButton type="submit" color="info" label="Receive"
:class="{ 'opacity-25': router.processing }" :disabled="form.processing" /> -->
<BaseButton v-if="can.reject && (dataset.server_state == 'approved' || dataset.server_state == 'rejected_to_reviewer')" type="submit" color="info" label="Accept" small />
<BaseButton
v-if="can.reject && (dataset.server_state == 'approved' || dataset.server_state == 'rejected_to_reviewer')"
:route-name="stardust.route('reviewer.dataset.reject', [dataset.id])" color="info"
:icon="mdiReiterate" :label="'Reject'" small />
</BaseButtons>
</component>
<BaseButton type="submit" color="info" label="Review" />
</BaseButtons>
</template>
</CardBox>
</SectionMain>
</LayoutAuthenticated>
</template>
<style scoped>
.break-words {
word-break: break-word;
}
.overflow-wrap-anywhere {
overflow-wrap: anywhere;
}
</style>

View file

@ -1,7 +1,6 @@
<template>
<LayoutAuthenticated>
<Head title="Classify"></Head>
<Head title="Collections"></Head>
<SectionMain>
<SectionTitleLineWithButton :icon="mdiLibraryShelves" title="Library Classification" main>
<div class="bg-lime-100 shadow rounded-lg p-6 mb-6 flex items-center justify-between">
@ -34,30 +33,29 @@
</h2>
<ul class="flex flex-wrap gap-2">
<li v-for="col in collections" :key="col.id" :class="{
'cursor-pointer p-2 border border-gray-200 rounded hover:bg-sky-50 flex items-center': true,
'cursor-pointer p-2 border border-gray-200 rounded hover:bg-sky-50 text-sky-700 text-sm': true,
'bg-sky-100 border-sky-500': selectedToplevelCollection && selectedToplevelCollection.id === col.id
}" @click="onToplevelCollectionSelected(col)">
<span class="text-sky-700">{{ col.name }}</span>
<span class="ml-2 px-2 py-0.5 bg-gray-200 text-gray-700 text-xs rounded-full">{{ col.number }}</span>
{{ `${col.name} (${col.number})` }}
</li>
<li v-if="collections.length === 0" class="text-gray-800 dark:text-slate-400">
No collections available.
</li>
</ul>
</CardBox>
<!-- Collections Listing -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<!-- Broader Collection (Parent) -->
<CardBox v-if="selectedCollection" class="rounded-lg p-4" has-form-data>
<h2 class="text-lg font-bold text-gray-800 dark:text-slate-400 mb-2">Broader Collection</h2>
<h2 class="text-lg font-bold text-gray-800 dark:text-slate-400 mb-2">Broader Collection</h2>
<draggable v-if="broaderCollections.length > 0" v-model="broaderCollections"
:group="{ name: 'collections' }" tag="ul" class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
<template #item="{ element: parent }">
<li :key="parent.id" :draggable="!parent.inUse" :class="getChildClasses(parent)"
@click="onCollectionSelected(parent)">
<span class="text-sky-700">{{ parent.name }}</span>
<span class="ml-2 px-2 py-0.5 bg-gray-200 text-gray-700 text-xs rounded-full">{{ parent.number }}</span>
{{ `${parent.name} (${parent.number})` }}
</li>
</template>
</draggable>
@ -71,35 +69,23 @@
<!-- Selected Collection Details -->
<CardBox v-if="selectedCollection" class="rounded-lg p-4" has-form-data>
<h3 class="text-xl font-bold text-gray-800 dark:text-slate-400 mb-2">Selected Collection</h3>
<!-- <p :class="[
<p :class="[
'cursor-pointer p-2 border border-gray-200 rounded text-sm',
selectedCollection.inUse ? 'bg-gray-200 text-gray-500 drag-none' : 'bg-green-50 text-green-700 hover:bg-green-100 hover:underline cursor-move'
]"></p> -->
<draggable v-model="selectedCollectionArray"
:group="{ name: 'collections', pull: 'clone', put: false }" tag="ul"
class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
<template #item="{ element }">
<li :key="element.id" :class="[
'p-2 border border-gray-200 rounded text-sm',
element.inUse ? 'bg-gray-200 text-gray-500 drag-none' : 'bg-green-50 text-green-700 hover:bg-green-100 hover:underline cursor-move'
]">
<span class="text-sky-700">{{ element.name }}</span>
<span class="ml-2 px-2 py-0.5 bg-gray-200 text-gray-700 text-xs rounded-full">{{ element.number }}</span>
</li>
</template>
</draggable>
]">
{{ `${selectedCollection.name} (${selectedCollection.number})` }}
</p>
</CardBox>
<!-- Narrower Collections (Children) -->
<CardBox v-if="selectedCollection" class="rounded-lg p-4" has-form-data>
<h2 class="text-lg font-bold text-gray-800 dark:text-slate-400 mb-2">Narrower Collections</h2>
<h2 class="text-lg font-bold text-gray-800 dark:text-slate-400 mb-2">Narrower Collections</h2>
<draggable v-if="narrowerCollections.length > 0" v-model="narrowerCollections"
:group="{ name: 'collections' }" tag="ul" class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
<template #item="{ element: child }">
<li :key="child.id" :draggable="!child.inUse" :class="getChildClasses(child)"
@click="onCollectionSelected(child)">
<span class="text-sky-700">{{ child.name }}</span>
<span class="ml-2 px-2 py-0.5 bg-gray-200 text-gray-700 text-xs rounded-full">{{ child.number }}</span>
{{ `${child.name} (${child.number})` }}
</li>
</template>
</draggable>
@ -113,21 +99,19 @@
</div>
<div class="mb-4 rounded-lg">
<div v-if="selectedCollection || selectedCollectionList.length > 0"
class="bg-gray-100 shadow rounded-lg p-6 mb-6"
:class="{ 'opacity-50': selectedCollection && selectedCollectionList.length === 0 }">
<div v-if="selectedCollection || selectedCollectionList.length > 0" class="bg-gray-100 shadow rounded-lg p-6 mb-6" :class="{ 'opacity-50': selectedCollection && selectedCollectionList.length === 0 }">
<p class="mb-4 text-gray-700">Please drag your collections here to classify your previously created
dataset
according to library classification standards.</p>
<draggable v-model="selectedCollectionList" :group="{ name: 'collections' }"
class="min-h-36 border-dashed border-2 border-gray-400 p-4 text-sm flex flex-wrap gap-2 max-h-60 overflow-y-auto"
tag="ul" :disabled="selectedCollection === null && selectedCollectionList.length > 0"
tag="ul"
:disabled="selectedCollection === null && selectedCollectionList.length > 0"
:style="{ opacity: (selectedCollection === null && selectedCollectionList.length > 0) ? 0.5 : 1, pointerEvents: (selectedCollection === null && selectedCollectionList.length > 0) ? 'none' : 'auto' }">
<template #item="{ element }">
<div :key="element.id"
class="p-2 m-1 bg-sky-200 text-sky-800 rounded flex items-center gap-2 h-7">
<span class="text-sky-700">{{ element.name }}</span>
<span class="ml-2 px-2 py-0.5 bg-gray-200 text-gray-700 text-xs rounded-full">{{ element.number }}</span>
<span>{{ element.name }} ({{ element.number }})</span>
<button
@click="selectedCollectionList = selectedCollectionList.filter(item => item.id !== element.id)"
class="hover:text-sky-600 flex items-center">
@ -140,7 +124,7 @@
</button>
</div>
</template>
</draggable>
</draggable>
</div>
</div>
<div class="p-6 border-t border-gray-100 dark:border-slate-800">
@ -205,16 +189,6 @@ const broaderCollections = ref<Collection[]>([]);
// Reactive list that holds collections dropped by the user
const selectedCollectionList: Ref<Collection[]> = ref<Collection[]>([]);
// Wrap selectedCollection in an array for draggable (always expects an array)
const selectedCollectionArray = computed({
get: () => (selectedCollection.value ? [selectedCollection.value] : []),
set: (value: Collection[]) => {
selectedCollection.value = value.length ? value[0] : null
}
})
const form = useForm({
collections: [] as number[],
});
@ -303,12 +277,6 @@ const fetchCollections = async (collectionId: number) => {
const alreadyDropped = selectedCollectionList.value.find(dc => dc.id === collection.id);
return alreadyDropped ? { ...collection, inUse: true } : { ...collection, inUse: false };
});
// Check if selected collection is in the selected list
if (selectedCollection.value && selectedCollectionList.value.find(dc => dc.id === selectedCollection.value?.id)) {
selectedCollection.value = { ...selectedCollection.value, inUse: true };
} else if (selectedCollection.value) {
selectedCollection.value = { ...selectedCollection.value, inUse: false };
}
} catch (error) {
console.error('Error in fetchCollections:', error);
}

View file

@ -24,6 +24,7 @@ import FormControl from '@/Components/FormControl.vue';
import FormCheckRadioGroup from '@/Components/FormCheckRadioGroup.vue';
import BaseButton from '@/Components/BaseButton.vue';
import { stardust } from '@eidellev/adonis-stardust/client';
// import { Inertia } from '@inertiajs/inertia';
import CardBoxModal from '@/Components/CardBoxModal.vue';
import BaseIcon from '@/Components/BaseIcon.vue';
@ -97,23 +98,23 @@ const flash: ComputedRef<any> = computed(() => {
// Computed property to determine the placeholder based on the selected option
const getPlaceholder = computed(() => (type: string) => {
switch (type) {
case 'DOI':
return 'https://doi.org/10.24341/tethys.236';
case 'Handle':
return '20.500.12345/67890';
case 'ISBN':
return '978-3-85316-076-3';
case 'ISSN':
return '1234-5678';
case 'URL':
return 'https://example.com';
case 'URN':
return 'urn:nbn:de:1234-5678';
default:
return '[VALUE]';
}
case 'DOI':
return 'https://doi.org/10.24341/tethys.236';
case 'Handle':
return '20.500.12345/67890';
case 'ISBN':
return '978-3-85316-076-3';
case 'ISSN':
return '1234-5678';
case 'URL':
return 'https://example.com';
case 'URN':
return 'urn:nbn:de:1234-5678';
default:
return '[VALUE]';
}
});
const mainService = MainService();
@ -220,6 +221,15 @@ if (Object.keys(mainService.dataset).length == 0) {
// descriptions: [{ value: '', type: 'Abstract', language: language }],
// });
let form = useForm<Dataset>(dataset as Dataset);
// form.defaults();
// const emit = defineEmits(['update:modelValue', 'setRef']);
// computed({
// get: () => form.rights,
// set: (value) => {
// emit('update:modelValue', value);
// },
// });
watch(language, (currentValue) => {
if (currentValue != "") {
@ -310,17 +320,12 @@ const nextStep = async () => {
} else if (formStep.value == 3) {
route = stardust.route('dataset.third.step');
}
// When posting in steps 1-3, remove any file uploads from the data.
// formStep.value++;
await form
.transform((data: Dataset) => {
// Create payload and set rights (transforming to a string if needed)
const payload: any = { ...data, rights: data.rights ? 'true' : 'false' };
// Remove the files property so that the partial update is done without files
if (payload.files) {
delete payload.files;
}
return payload;
})
.transform((data) => ({
...data,
rights: form.rights && form.rights == true ? 'true' : 'false',
}))
.post(route, {
onSuccess: () => {
// console.log(form.data());
@ -329,6 +334,7 @@ const nextStep = async () => {
},
});
};
const prevStep = () => {
formStep.value--;
};
@ -337,7 +343,7 @@ const submit = async () => {
let route = stardust.route('dataset.submit');
const files = form.files.map((obj) => {
return new File([obj.blob], obj.label, { type: obj.type, lastModified: obj.lastModified, sort_order: obj.sort_order });
return new File([obj.blob], obj.label, { type: obj.type, lastModified: obj.lastModified });
});
// formStep.value++;
@ -434,12 +440,6 @@ const onAddAuthor = (person: Person) => {
notify({ type: 'info', text: 'person has been successfully added as author' });
}
};
const addNewContributor = () => {
let newContributor = { status: false, first_name: '', last_name: '', email: '', academic_title: '', identifier_orcid: '', name_type: 'Personal', pivot: { contributor_type: '' } };
form.contributors.push(newContributor);
};
const onAddContributor = (person: 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);
@ -462,7 +462,7 @@ const onMapInitialized = (newItem: any) => {
adds a new Keyword
*/
const addKeyword = () => {
let newSubject: Subject = { value: '', language: '', type: 'uncontrolled' };
let newSubject: Subject = { value: 'test', language: '', type: 'uncontrolled' };
//this.dataset.files.push(uploadedFiles[i]);
form.subjects.push(newSubject);
};
@ -499,25 +499,16 @@ Removes a selected keyword
<template>
<CardBoxModal v-model="isModalActive" title="Einverständniserklärung *">
<p class="mb-4 text-gray-700">
Mit dem Setzen des Hakens bestätige ich hiermit folgende Punkte:
</p>
<ul class="list-decimal pl-6 space-y-2 text-sm text-gray-600">
Mit dem Setzen des Hakens bestätige ich hiermit
<ul class="list-decimal">
<li>
die Data Policy von Tethys RDR sowie die
<a href="/docs/HandbuchTethys.pdf" target="_blank"
class="font-medium text-blue-600 hover:text-blue-800 transition-colors underline">
Terms &amp; Conditions
</a>
von Tethys gelesen und verstanden zu haben.
die Data Policy von Tethys RDR sowie die Terms & Conditions von Tethys gelesen und verstanden zu haben
(<a href="/docs/HandbuchTethys.pdf" target="_blank">siehe hier</a>)
</li>
<li>
das Einverständnis aller Co-Autoren über die bevorstehende Datenpublikation schriftlich eingeholt zu
haben.
</li>
<li>
sowohl mit der Data Policy als auch mit den Terms &amp; Conditions einverstanden zu sein.
<li>das Einverständnis aller Co-Autoren über die bevorstehende Datenpublikation schriftlich eingeholt zu
haben
</li>
<li>sowohl mit der Data Policy als auch mit den Terms & Conditions einverstanden zu sein</li>
</ul>
</CardBoxModal>
@ -540,15 +531,15 @@ Removes a selected keyword
<div class="flex items-center">
<!-- <label>{{ form.titles[0].language }}</label>
<label>{{ form.language }}</label> -->
<icon-wizard :is-current="formStep == 1" :is-checked="formStep > 1" :label="'Step 1'">
<icon-wizard :is-current="formStep == 1" :is-checked="formStep > 1" :label="'Language'">
<icon-language></icon-language>
</icon-wizard>
<icon-wizard :is-current="formStep == 2" :is-checked="formStep > 2" :label="'Step 2'">
<icon-wizard :is-current="formStep == 2" :is-checked="formStep > 2" :label="'Mandatory'">
<icon-mandatory></icon-mandatory>
</icon-wizard>
<icon-wizard :is-current="formStep == 3" :is-checked="formStep > 3" :label="'Step 3'">
<icon-wizard :is-current="formStep == 3" :is-checked="formStep > 3" :label="'Recommended'">
<icon-recommendet></icon-recommendet>
</icon-wizard>
@ -576,7 +567,7 @@ Removes a selected keyword
<FormField label="Licenses" wrap-body :class="{ 'text-red-400': form.errors.licenses }"
class="mt-8 w-full mx-2 flex-1">
<FormCheckRadioGroup type="radio" v-model="form.licenses" name="licenses" is-column
<FormCheckRadioGroup v-model="form.licenses" name="roles" is-column
:options="props.licenses" />
</FormField>
@ -584,10 +575,8 @@ Removes a selected keyword
<input class="form-checkbox" name="rights" id="rights" type="checkbox" v-model="dataset.rights" />
terms and conditions
</label> -->
<FormField label="Rights"
help="You must agree that you have read the Terms and Conditions. Please click on the 'i' icon to find a read the policy"
wrap-body :class="{ 'text-red-400': form.errors.rights }"
class="mt-8 w-full mx-2 flex-1 flex-col">
<FormField label="Rights" help="You must agree to continue" wrap-body
:class="{ 'text-red-400': form.errors.rights }" class="mt-8 w-full mx-2 flex-1 flex-col">
<label for="rights" class="checkbox mr-6 mb-3 last:mr-0">
<input type="checkbox" id="rights" required v-model="form.rights" />
<span class="check" />
@ -673,7 +662,7 @@ Removes a selected keyword
<FormField label="Title Value *"
:class="{ 'text-red-400': form.errors[`titles.${index}.value`] }"
class="w-full mx-2 flex-1">
<FormControl required v-model="form.titles[index].value" type="textarea"
<FormControl required v-model="form.titles[index].value" type="text"
placeholder="[enter main title]">
<div class="text-red-400 text-sm"
v-if="form.errors[`titles.${index}.value`]">
@ -749,7 +738,7 @@ Removes a selected keyword
<FormField label="Description Value *"
:class="{ 'text-red-400': form.errors[`descriptions.${index}.value`] }"
class="w-full mx-2 flex-1">
<FormControl required v-model="form.descriptions[index].value" type="textarea"
<FormControl required v-model="form.descriptions[index].value" type="text"
placeholder="[enter additional description]" :show-char-count="true"
:max-input-length="2500">
<div class="text-red-400 text-sm" v-if="form.errors[`descriptions.${index}.value`] &&
@ -789,14 +778,14 @@ Removes a selected keyword
</CardBox>
<!-- authors -->
<CardBox class="mb-6 shadow" has-table title="Creators" :icon="mdiBookOpenPageVariant" :show-header-icon="false">
<CardBox class="mb-6 shadow" has-table title="Creators" :icon="mdiBookOpenPageVariant">
<SearchAutocomplete source="/api/persons" :response-property="'first_name'"
placeholder="search in person table...." v-on:person="onAddAuthor"></SearchAutocomplete>
<TablePersons :errors="form.errors" :persons="form.authors" :relation="'authors'"
v-if="form.authors.length > 0" />
<div class="text-red-400 text-sm" v-if="form.errors.authors && Array.isArray(form.errors.authors)">
{{ form.errors.authors.join(', ') }}
<div class="text-red-400 text-sm" v-if="errors.authors && Array.isArray(errors.authors)">
{{ errors.authors.join(', ') }}
</div>
<div class="w-full md:w-1/2">
<label class="block" for="additionalCreators">Add additional creator(s) if creator is
@ -807,7 +796,7 @@ Removes a selected keyword
</CardBox>
<!-- contributors -->
<CardBox class="mb-6 shadow" has-table title="Contributors" :icon="mdiBookOpenPageVariant" :show-header-icon="false">
<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>
@ -819,12 +808,6 @@ Removes a selected keyword
v-if="form.errors.contributors && Array.isArray(form.errors.contributors)">
{{ form.errors.contributors.join(', ') }}
</div>
<div class="w-full md:w-1/2">
<label class="block" for="additionalCreators">Add additional contributor(s) if
contributor is not in database</label>
<button class="bg-blue-500 text-white py-2 px-4 rounded-sm"
@click.prevent="addNewContributor()">+</button>
</div>
</CardBox>
</div>
@ -851,7 +834,7 @@ Removes a selected keyword
</FormControl>
</FormField>
</div>
<CardBox class="mb-6 shadow" has-table title="Geo Location" :icon="mdiEarthPlus" :show-header-icon="false">
<CardBox class="mb-6 shadow" has-table title="Geo Location" :icon="mdiEarthPlus">
<!-- @onMapInitialized="onMapInitialized" -->
<!-- v-bind-event="{ mapId, name: mapId }" -->
<MapComponent :mapOptions="mapOptions" :baseMaps="baseMaps" :fitBounds="fitBounds"
@ -909,9 +892,9 @@ Removes a selected keyword
</div>
</CardBox>
<CardBox class="mb-6 shadow" has-table title="Coverage Information" :icon="mdiEarthPlus" :show-header-icon="false">
<CardBox class="mb-6 shadow" has-table title="Coverage Information" :icon="mdiEarthPlus">
<!-- elevation menu -->
<div class="flex flex-col md:flex-row mb-3 space-y-2 md:space-y-0 md:space-x-4">
<div class="flex flex-col md:flex-row mb-3 space-y-2 md:space-y-0 md:space-x-4">
<label for="elevation-option-one" class="pure-radio mb-2 md:mb-0">
<input id="elevation-option-one" type="radio" v-model="elevation" value="absolut" />
absolut elevation (m)
@ -1088,8 +1071,7 @@ Removes a selected keyword
<!-- <input name="Reference Value" class="form-control"
placeholder="[VALUE]" v-model="item.value" /> -->
<FormControl required v-model="item.value" :type="'text'"
:placeholder="getPlaceholder(form.references[index].type)"
:errors="form.errors.embargo_date">
:placeholder="getPlaceholder(form.references[index].type)" :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(', ') }}

View file

@ -42,8 +42,7 @@
<!-- (2) licenses -->
<FormField label="Licenses" wrap-body :class="{ 'text-red-400': form.errors.licenses }"
class="mt-8 w-full mx-2 flex-1">
<FormCheckRadioGroup type="radio" v-model="form.licenses" name="licenses" is-column
:options="licenses" />
<FormCheckRadioGroup v-model="form.licenses" name="licenses" is-column :options="licenses" />
</FormField>
<div class="flex flex-col md:flex-row">
@ -79,7 +78,7 @@
<div class="flex flex-col md:flex-row">
<FormField label="Main Title *" help="required: main title"
:class="{ 'text-red-400': form.errors['titles.0.value'] }" class="w-full mr-1 flex-1">
<FormControl required v-model="form.titles[0].value" type="textarea"
<FormControl required v-model="form.titles[0].value" type="text"
placeholder="[enter main title]" :show-char-count="true" :max-input-length="255">
<div class="text-red-400 text-sm"
v-if="form.errors['titles.0.value'] && Array.isArray(form.errors['titles.0.value'])">
@ -117,7 +116,7 @@
<tr v-if="title.type != 'Main'">
<!-- <td scope="row">{{ index + 1 }}</td> -->
<td data-label="Title Value">
<FormControl required v-model="form.titles[index].value" type="textarea"
<FormControl required v-model="form.titles[index].value" type="text"
placeholder="[enter main title]">
<div class="text-red-400 text-sm"
v-if="form.errors[`titles.${index}.value`]">
@ -164,8 +163,7 @@
:class="{ 'text-red-400': form.errors['descriptions.0.value'] }"
class="w-full mr-1 flex-1">
<FormControl required v-model="form.descriptions[0].value" type="textarea"
placeholder="[enter main abstract]" :show-char-count="true"
:max-input-length="2500">
placeholder="[enter main abstract]" :show-char-count="true" :max-input-length="2500">
<div class="text-red-400 text-sm"
v-if="form.errors['descriptions.0.value'] && Array.isArray(form.errors['descriptions.0.value'])">
{{ form.errors['descriptions.0.value'].join(', ') }}
@ -178,7 +176,7 @@
<FormControl required v-model="form.descriptions[0].language" type="text"
:is-read-only="true">
<div class="text-red-400 text-sm" v-if="form.errors['descriptions.0.value'] && Array.isArray(form.errors['descriptions.0.language'])
">
">
{{ form.errors['descriptions.0.language'].join(', ') }}
</div>
</FormControl>
@ -199,7 +197,7 @@
<tr v-if="item.type != 'Abstract'">
<!-- <td scope="row">{{ index + 1 }}</td> -->
<td data-label="Description Value">
<FormControl required v-model="form.descriptions[index].value" type="textarea"
<FormControl required v-model="form.descriptions[index].value" type="text"
placeholder="[enter main title]">
<div class="text-red-400 text-sm"
v-if="form.errors[`descriptions.${index}.value`]">
@ -241,23 +239,19 @@
</CardBox>
<!-- (7) authors -->
<CardBox class="mb-6 shadow" has-table title="Creators" :icon="mdiBookOpenPageVariant"
:header-icon="mdiPlusCircle" v-on:header-icon-click="addNewAuthor()">
<CardBox class="mb-6 shadow" has-table title="Creators" :icon="mdiBookOpenPageVariant">
<SearchAutocomplete source="/api/persons" :response-property="'first_name'"
placeholder="search in person table...." v-on:person="onAddAuthor"></SearchAutocomplete>
<TablePersons :persons="form.authors" v-if="form.authors.length > 0" :errors="form.errors"
:relation="'authors'" />
<div class="text-red-400 text-sm"
v-if="form.errors.authors && Array.isArray(form.errors.authors)">
<TablePersons :persons="form.authors" v-if="form.authors.length > 0" :relation="'authors'"/>
<div class="text-red-400 text-sm" v-if="form.errors.authors && Array.isArray(form.errors.authors)">
{{ form.errors.authors.join(', ') }}
</div>
</CardBox>
<!-- (8) contributors -->
<CardBox class="mb-6 shadow" has-table title="Contributors" :icon="mdiBookOpenPageVariant"
:header-icon="mdiPlusCircle" v-on:header-icon-click="addNewContributor()">
<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>
@ -340,8 +334,8 @@
</FormField>
</div>
<CardBox class="mb-6 shadow" has-table title="Dataset References" :icon="mdiEarthPlus"
:header-icon="mdiPlusCircle" v-on:header-icon-click="addReference">
<CardBox class="mb-6 shadow" has-table title="Dataset References" :icon="mdiEarthPlus" :header-icon="mdiPlusCircle"
v-on:header-icon-click="addReference">
<!-- Message when no references exist -->
<div v-if="form.references.length === 0" class="text-center py-4">
<p class="text-gray-600">No references added yet.</p>
@ -414,43 +408,6 @@
</tr>
</tbody>
</table>
<!-- References to delete section -->
<div v-if="form.referencesToDelete && form.referencesToDelete.length > 0" class="mt-8">
<h1 class="pt-8 pb-3 font-semibold sm:text-lg text-gray-900">References To Delete</h1>
<ul class="flex flex-1 flex-wrap -m-1">
<li v-for="(element, index) in form.referencesToDelete" :key="index"
class="block p-1 w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/6 xl:w-1/8 h-40">
<article tabindex="0"
class="bg-red-100 group w-full h-full rounded-md cursor-pointer relative shadow-sm overflow-hidden">
<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 font-medium text-sm mb-1 truncate overflow-hidden whitespace-nowrap">
{{ element.value }}
</h1>
<div class="flex flex-col mt-auto">
<p class="p-1 size text-xs text-gray-700">
<span class="font-semibold">Type:</span> {{ element.type }}
</p>
<p class="p-1 size text-xs text-gray-700">
<span class="font-semibold">Relation:</span> {{ element.relation }}
</p>
<div class="flex justify-end mt-1">
<button
class="restore ml-auto focus:outline-none hover:bg-gray-300 p-1 rounded-md text-gray-800"
@click.prevent="restoreReference(index)">
<svg viewBox="0 0 24 24" class="w-5 h-5">
<path fill="currentColor" :d="mdiRestore"></path>
</svg>
</button>
</div>
</div>
</section>
</article>
</li>
</ul>
</div>
</CardBox>
<BaseDivider />
@ -463,11 +420,20 @@
</li>
</ul> -->
<TableKeywords :keywords="form.subjects" :errors="form.errors" :subjectTypes="subjectTypes"
v-model:subjects-to-delete="form.subjectsToDelete" v-if="form.subjects.length > 0" />
v-if="form.subjects.length > 0" />
</CardBox>
</div>
<!-- <div class="mb-4">
<label for="description" class="block text-gray-700 font-bold mb-2">Description:</label>
<textarea id="description"
class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
v-model="form.type"></textarea>
</div> -->
<div class="mb-4">
<!-- <label for="project" class="block text-gray-700 font-bold mb-2">Project:</label>
<select
@ -481,28 +447,36 @@
</select> -->
</div>
<FileUploadComponent v-model:files="form.files" v-model:filesToDelete="form.filesToDelete"
:showClearButton="false">
</FileUploadComponent>
<FileUploadComponent v-model:files="form.files" v-model:filesToDelete="form.filesToDelete"></FileUploadComponent>
<div class="text-red-400 text-sm" v-if="form.errors['file'] && Array.isArray(form.errors['files'])">
{{ form.errors['files'].join(', ') }}
</div>
<!-- Add more input fields for the other properties of the dataset -->
<!-- <button
type="submit"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
>
Save
</button> -->
<template #footer>
<BaseButtons>
<BaseButton v-if="can.edit" @click.stop="submit" :disabled="form.processing" label="Save"
color="info" :icon="mdiDisc" :class="{ 'opacity-25': form.processing }" small>
<BaseButton @click.stop="submit" :disabled="form.processing" label="Save" color="info"
:class="{ 'opacity-25': form.processing }" small>
</BaseButton>
<BaseButton v-if="can.edit" :route-name="stardust.route('dataset.release', [dataset.id])"
color="info" :icon="mdiLockOpen" :label="'Release'" small
:disabled="form.processing"
:class="{ 'opacity-25': form.processing }" />
<!-- <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>
</template>
</CardBox>
<!-- Loading Spinner -->
<div v-if="form.processing"
<!-- Loading Spinner -->
<div v-if="form.processing"
class="fixed inset-0 flex items-center justify-center bg-gray-500 bg-opacity-50 z-50">
<svg class="animate-spin h-12 w-12 text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
@ -518,6 +492,8 @@
// 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 { useForm, Head, usePage } from '@inertiajs/vue3';
import { computed, ComputedRef } from 'vue';
@ -551,9 +527,6 @@ import {
mdiBookOpenPageVariant,
mdiEarthPlus,
mdiAlertBoxOutline,
mdiRestore,
mdiLockOpen,
mdiDisc
} from '@mdi/js';
import { notify } from '@/notiwind';
import NotificationBar from '@/Components/NotificationBar.vue';
@ -607,10 +580,8 @@ const props = defineProps({
type: Object,
default: () => ({}),
},
can: {
type: Object,
default: () => ({}),
},
});
const flash: ComputedRef<any> = computed(() => {
@ -621,6 +592,12 @@ const flash: ComputedRef<any> = computed(() => {
const errors: ComputedRef<any> = computed(() => {
return usePage().props.errors;
});
// const errors: ComputedRef<any> = computed(() => {
// return usePage().props.errors;
// });
// const projects = reactive([]);
// const licenses = reactive([]);
const mapOptions: MapOptions = {
center: [48.208174, 16.373819],
@ -635,72 +612,67 @@ const fitBounds: LatLngBoundsExpression = [
];
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);
// // });
// }
props.dataset.filesToDelete = [];
props.dataset.subjectsToDelete = [];
props.dataset.referencesToDelete = [];
let form = useForm<Dataset>(props.dataset as Dataset);
// Add this computed property to the script section
const hasUnsavedChanges = computed(() => {
// Check if form is processing
if (form.processing) return true;
// const mainService = MainService();
// mainService.fetchfiles(props.dataset);
// Compare current form state with original dataset
// Check basic properties
if (form.language !== props.dataset.language) return true;
if (form.type !== props.dataset.type) return true;
if (form.project_id !== props.dataset.project_id) return true;
if (form.embargo_date !== props.dataset.embargo_date) return true;
// Check if licenses have changed
const originalLicenses = Array.isArray(props.dataset.licenses)
? props.dataset.licenses.map(l => typeof l === 'object' ? l.id.toString() : l)
: [];
const currentLicenses = Array.isArray(form.licenses)
? form.licenses.map(l => typeof l === 'object' ? l.id.toString() : l)
: [];
if (JSON.stringify(currentLicenses) !== JSON.stringify(originalLicenses)) return true;
// Check if titles have changed
if (JSON.stringify(form.titles) !== JSON.stringify(props.dataset.titles)) return true;
// Check if descriptions have changed
if (JSON.stringify(form.descriptions) !== JSON.stringify(props.dataset.descriptions)) return true;
// Check if authors have changed
if (JSON.stringify(form.authors) !== JSON.stringify(props.dataset.authors)) return true;
// Check if contributors have changed
if (JSON.stringify(form.contributors) !== JSON.stringify(props.dataset.contributors)) return true;
// const files = computed(() => props.dataset.file);
// Check if subjects have changed
// if (JSON.stringify(form.subjects) !== JSON.stringify(props.dataset.subjects)) return true;
let test = JSON.stringify(form.subjects);
let test2 = JSON.stringify(props.dataset.subjects);
if (test !== test2) {
return true;
}
// Check if references have changed
if (JSON.stringify(form.references) !== JSON.stringify(props.dataset.references)) return true;
// Check if coverage has changed
if (JSON.stringify(form.coverage) !== JSON.stringify(props.dataset.coverage)) return true;
// let form = useForm<Dataset>(props.dataset as Dataset);
// Check if files have changed
if (form.files?.length !== props.dataset.files?.length) return true;
if (form.filesToDelete?.length > 0) return true;
// const form = useForm({
// _method: 'put',
// login: props.user.login,
// email: props.user.email,
// password: '',
// password_confirmation: '',
// roles: props.userHasRoles, // fill actual user roles from db
// });
// async created() {
// // Fetch the list of projects and licenses from the server
// const response = await fetch('/api/datasets/edit/' + this.dataset.id);
// const data = await response.json();
// this.projects = data.projects;
// this.licenses = data.licenses;
// }
// Check if there are new files to upload
if (form.files?.some(file => !file.id)) return true;
// No changes detected
return false;
});
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);
let licenses = form.licenses.map((obj) => {
if (hasIdAttribute(obj)) {
@ -710,6 +682,12 @@ const submit = async (): Promise<void> => {
}
});
// const files = form.files.map((obj) => {
// return new File([obj.blob], obj.label, { type: obj.type, lastModified: obj.lastModified });
// });
const [fileUploads, fileInputs] = form.files?.reduce(
([fileUploads, fileInputs], obj) => {
if (!obj.id) {
@ -722,11 +700,11 @@ const submit = async (): Promise<void> => {
// const file = new File([obj.blob], `${obj.label}?sortOrder=${obj.sort_order}`, options);
// const metadata = JSON.stringify({ sort_order: obj.sort_order });
// const metadataBlob = new Blob([metadata + '\n'], { type: 'application/json' });
const file = new File([obj.blob], `${obj.label}?sortorder=${obj.sort_order}`, options,);
const file = new File([obj.blob], `${obj.label}`, options,);
// const file = new File([obj.blob], `${obj.label}`, options);
// fileUploads[obj.sort_order] = file;
fileUploads.push(file);
} else {
@ -766,9 +744,7 @@ const submit = async (): Promise<void> => {
// formStep.value++;
// form.filesToDelete = [];
// Clear the array using splice
form.filesToDelete?.splice(0, form.filesToDelete.length);
form.subjectsToDelete?.splice(0, form.subjectsToDelete.length);
form.referencesToDelete?.splice(0, form.referencesToDelete.length);
form.filesToDelete?.splice(0, form.filesToDelete.length);
},
});
};
@ -793,11 +769,6 @@ const removeDescription = (key: any) => {
form.descriptions.splice(key, 1);
};
const addNewAuthor = () => {
let newAuthor = { status: false, first_name: '', last_name: '', email: '', academic_title: '', identifier_orcid: '', name_type: 'Personal' };
form.authors.push(newAuthor);
};
const onAddAuthor = (person: 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);
@ -809,11 +780,6 @@ const onAddAuthor = (person: Person) => {
}
};
const addNewContributor = () => {
let newContributor = { status: false, first_name: '', last_name: '', email: '', academic_title: '', identifier_orcid: '', name_type: 'Personal', pivot: { contributor_type: '' } };
form.contributors.push(newContributor);
};
const onAddContributor = (person: 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);
@ -828,7 +794,7 @@ const onAddContributor = (person: Person) => {
};
const addKeyword = () => {
let newSubject: Subject = { value: '', language: '', type: 'uncontrolled' };
let newSubject: Subject = { value: 'test', language: '', type: 'uncontrolled', dataset_count: 0 };
//this.dataset.files.push(uploadedFiles[i]);
form.subjects.push(newSubject);
};
@ -840,35 +806,9 @@ const addReference = () => {
};
const removeReference = (key: any) => {
const reference = form.references[key];
// If the reference has an ID, it exists in the database
// and should be added to referencesToDelete
if (reference.id) {
// Initialize referencesToDelete array if it doesn't exist
if (!form.referencesToDelete) {
form.referencesToDelete = [];
}
// Add to referencesToDelete
form.referencesToDelete.push(reference);
}
// Remove from form.references array
form.references.splice(key, 1);
};
const restoreReference = (index: number) => {
// Get the reference from referencesToDelete
const reference = form.referencesToDelete[index];
// Add it back to form.references
form.references.push(reference);
// Remove it from referencesToDelete
form.referencesToDelete.splice(index, 1);
};
const onMapInitialized = (newItem: any) => {
console.log(newItem);
};

View file

@ -95,18 +95,18 @@ const formatServerState = (state: string) => {
<table class="w-full table-fixed">
<thead>
<tr>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider">
<!-- <Sort label="Dataset Title" attribute="title" :search="form.search" /> -->
Dataset Title
</th>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
</th>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider">
<!-- <Sort label="Email" attribute="email" :search="form.search" /> -->
Server State
</th>
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider dark:text-white">
<th scope="col" class="py-3 text-left text-xs font-medium uppercase tracking-wider">
Date of last modification
</th>
<th scope="col" class="relative px-6 py-3 dark:text-white" v-if="can.edit || can.delete">
<th scope="col" class="relative px-6 py-3" v-if="can.edit || can.delete">
<span class="sr-only">Actions</span>
</th>
</tr>
@ -114,54 +114,35 @@ const formatServerState = (state: string) => {
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="dataset in props.datasets.data" :key="dataset.id" :class="getRowClass(dataset)">
<td data-label="Login"
class="py-4 whitespace-nowrap text-gray-700 table-title">
<td data-label="Login" class="py-4 whitespace-nowrap text-gray-700 dark:text-white table-title">
<!-- <Link v-bind:href="stardust.route('settings.user.show', [user.id])"
class="no-underline hover:underline text-cyan-600 dark:text-cyan-400">
{{ user.login }}
</Link> -->
<!-- {{ user.id }} -->
{{ dataset.main_title }}
{{ dataset.main_title }}
</td>
<td class="py-4 whitespace-nowrap text-gray-700">
<td class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
{{ formatServerState(dataset.server_state) }}
<div v-if="dataset.server_state === 'rejected_editor' && dataset.reject_editor_note"
class="inline-block relative ml-2 group">
<button
class="w-5 h-5 rounded-full bg-gray-200 text-gray-600 text-xs flex items-center justify-center focus:outline-none hover:bg-gray-300">
i
</button>
<div
class="absolute left-0 top-full mt-1 w-64 bg-white shadow-lg rounded-md p-3 text-xs text-left z-50 transform scale-0 origin-top-left transition-transform duration-100 group-hover:scale-100">
<p
class="text-gray-700 max-h-40 overflow-y-auto overflow-x-hidden whitespace-normal break-words">
{{ dataset.reject_editor_note }}
</p>
<div class="absolute -top-1 left-1 w-2 h-2 bg-white transform rotate-45">
</div>
</div>
</div>
</td>
<td data-label="modified" class="py-4 whitespace-nowrap text-gray-700">
<td data-label="modified" class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
<div class="text-sm" :title="dataset.server_date_modified">
{{ dataset.server_date_modified }}
</div>
</td>
<td
class="py-4 whitespace-nowrap text-right text-sm font-medium text-gray-700">
<td class="py-4 whitespace-nowrap text-right text-sm font-medium text-gray-700 dark:text-white">
<BaseButtons v-if="validStates.includes(dataset.server_state)"
type="justify-start lg:justify-end" no-wrap>
<!-- release created dataset -->
<BaseButton v-if="can.edit"
:route-name="stardust.route('dataset.release', [dataset.id])" color="info"
:icon="mdiLockOpen" :label="'Release'" small />
<BaseButton v-if="can.edit"
:route-name="stardust.route('dataset.edit', [dataset.id])" color="info"
:icon="mdiSquareEditOutline" :label="'Edit'" small />
<BaseButton v-if="can.edit" :route-name="stardust.route('dataset.edit', [dataset.id])"
color="info" :icon="mdiSquareEditOutline" :label="'Edit'" small />
<BaseButton v-if="can.edit"
:route-name="stardust.route('dataset.categorize', [dataset.id])" color="info"
:icon="mdiLibraryShelves" :label="'Classify'" small />
:icon="mdiLibraryShelves" :label="'Library'" small />
<BaseButton v-if="can.delete" color="danger"
:route-name="stardust.route('dataset.delete', [dataset.id])" :icon="mdiTrashCan"
small />
@ -171,7 +152,7 @@ const formatServerState = (state: string) => {
</tbody>
</table>
<div class="py-4">
<Pagination v-bind:data="datasets.meta" />
<Pagination v-bind:data="datasets.meta" />
</div>
</CardBox>
</SectionMain>
@ -179,17 +160,13 @@ const formatServerState = (state: string) => {
</template>
<style scoped lang="css">
.table-title {
max-width: 200px;
/* set a maximum width */
overflow: hidden;
/* hide overflow */
text-overflow: ellipsis;
/* show ellipsis for overflowed text */
white-space: nowrap;
/* prevent wrapping */
}
.table-title {
max-width: 200px; /* set a maximum width */
overflow: hidden; /* hide overflow */
text-overflow: ellipsis; /* show ellipsis for overflowed text */
white-space: nowrap; /* prevent wrapping */
}
.table-fixed {
table-layout: fixed;
}
@ -231,4 +208,6 @@ const formatServerState = (state: string) => {
color: whitesmoke;
}*/
</style>
</style>

View file

@ -105,7 +105,7 @@ const updateProfileInformation = () => {
<FormField label="Username" :class="{ 'text-red-400': form.errors.login }">
<FormControl id="username" label="Username" v-model="form.login" class="w-full"
:is-read-only="true">
:is-read-only="!user.is_admin">
<div class="text-red-400 text-sm" v-if="errors.login && Array.isArray(errors.login)">
{{ errors.login.join(', ') }}
</div>
@ -115,7 +115,7 @@ const updateProfileInformation = () => {
<FormField label="Enter Email">
<FormControl v-model="form.email" type="text" placeholder="Email" :errors="form.errors.email"
:is-read-only="true">
:is-read-only="!user.is_admin">
<div class="text-red-400 text-sm" v-if="errors.email && Array.isArray(errors.email)">
{{ errors.email.join(', ') }}
</div>

View file

@ -198,8 +198,7 @@ export const MainService = defineStore('main', {
}
})
.catch((error) => {
// alert(error.message);
throw error;
alert(error.message);
});
},
@ -237,18 +236,17 @@ export const MainService = defineStore('main', {
this.totpState = state;
},
fetchChartData() {
fetchChartData(year: string) {
// sampleDataKey= authors or datasets
axios
.get(`/api/statistic`)
.get(`/api/statistic/${year}`)
.then((r) => {
if (r.data) {
this.graphData = r.data;
}
})
.catch((error) => {
// alert(error.message);
throw error;
alert(error.message);
});
},

View file

@ -81,7 +81,7 @@ const layoutService = LayoutService(pinia);
const localeService = LocaleStore(pinia);
localeService.initializeLocale();
// const mainService = MainService(pinia);
const mainService = MainService(pinia);
// mainService.setUser(user);
/* App style */
@ -91,11 +91,12 @@ styleService.setStyle(localStorage[styleKey] ?? 'basic');
if ((!localStorage[darkModeKey] && window.matchMedia('(prefers-color-scheme: dark)').matches) || localStorage[darkModeKey] === '1') {
styleService.setDarkMode(true);
}
// mainService.fetchApi('clients');
// mainService.fetchApi('authors');
// mainService.fetchApi('datasets');
// mainService.fetchChartData();
// mainService.fetch('clients');
// mainService.fetch('history');
mainService.fetchApi('clients');
mainService.fetchApi('authors');
mainService.fetchApi('datasets');
mainService.fetchChartData("2022");
/* Collapse mobile aside menu on route change */
Inertia.on('navigate', () => {

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 287 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

View file

@ -156,13 +156,18 @@ export default [
// label: 'Create Dataset',
// },
],
},
},
// {
// href: '',
// icon: mdiGithub,
// label: 'Forgejo',
// target: '_blank',
// route: 'dataset.create',
// icon: mdiDatabasePlus,
// label: 'Create Dataset',
// },
{
href: 'https://gitea.geosphere.at/geolba/tethys.backend',
icon: mdiGithub,
label: 'Forgejo',
target: '_blank',
},
{
href: '/oai',
icon: mdiAccountEye,

View file

@ -7,12 +7,6 @@
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<!-- <link rel="icon" href="/apps/theming/favicon/settings?v=ad28c447"> -->
<input type="hidden" id="initial-state-firstrunwizard-desktop"
value="Imh0dHBzOi8vZ2l0ZWEuZ2VvbG9naWUuYWMuYXQvZ2VvbGJhL3RldGh5cy5iYWNrZW5kIg==">

View file

@ -106,14 +106,7 @@ router
// Auth routes
router
.get('/app/login', async ({ inertia }: HttpContext) => {
try {
await db.connection().rawQuery('SELECT 1');
} catch (error) {
if (error.code === 'ECONNREFUSED') {
throw error;
}
}
.get('/app/login', ({ inertia }: HttpContext) => {
return inertia.render('Auth/Login');
})
.as('app.login.show');
@ -253,11 +246,18 @@ router.get('/settings/user/security', [UserController, 'accountInfo']).as('setti
router.post('/settings/user/store', [UserController, 'accountInfoStore']).as('account.password.store').use(middleware.auth());
router.get('/settings/profile/edit', [UserController, 'profile']).as('settings.profile.edit').use(middleware.auth());
router
.put('/settings/profile/:id/update', [UserController, 'profileUpdate'])
.as('settings.profile.update')
.where('id', router.matchers.number())
.use(middleware.auth());
router.put('/settings/password/update', [UserController, 'passwordUpdate']).as('settings.password.update').use(middleware.auth());
.put('/settings/profile/:id/update', [UserController, 'profileUpdate'])
.as('settings.profile.update')
.where('id', router.matchers.number())
.use(middleware.auth());
router
.put('/settings/password/update', [UserController, 'passwordUpdate'])
.as('settings.password.update')
.use(middleware.auth());
// Submitter routes
router
@ -364,44 +364,6 @@ router
.as('editor.dataset.rejectUpdate')
.where('id', router.matchers.number())
.use([middleware.auth(), middleware.can(['dataset-editor-reject'])]);
router
.get('/dataset/:id/edit', [EditorDatasetController, 'edit'])
.as('editor.dataset.edit')
// .where('id', router.matchers.number())
.use([middleware.auth(), middleware.can(['dataset-editor-update'])]);
router
.put('/dataset/:id/update', [EditorDatasetController, 'update'])
.as('editor.dataset.update')
.where('id', router.matchers.number())
.use([middleware.auth(), middleware.can(['dataset-editor-update'])]);
router
.get('/dataset/:id/categorize', [EditorDatasetController, 'categorize'])
.as('editor.dataset.categorize')
.where('id', router.matchers.number())
.use([middleware.auth(), middleware.can(['dataset-editor-update'])]);
router
.put('/dataset/:id/categorizeUpdate', [EditorDatasetController, 'categorizeUpdate'])
.as('editor.dataset.categorizeUpdate')
.where('id', router.matchers.number())
.use([middleware.auth(), middleware.can(['dataset-editor-update'])]);
router
.get('/file/download/:id', [EditorDatasetController, 'download'])
.as('editor.file.download')
.where('id', router.matchers.number())
.use([middleware.auth(), middleware.can(['dataset-editor-update'])]);
router
.get('dataset/:id/rejectToReviewer', [EditorDatasetController, 'rejectToReviewer'])
.as('editor.dataset.rejectToReviewer')
.where('id', router.matchers.number())
.use([middleware.auth(), middleware.can(['dataset-editor-reject'])]);
router
.put('dataset/:id/rejectToReviewer', [EditorDatasetController, 'rejectToReviewerUpdate'])
.as('editor.dataset.rejectToReviewerUpdate')
.where('id', router.matchers.number())
.use([middleware.auth(), middleware.can(['dataset-editor-reject'])]);
router
.get('dataset/:id/publish', [EditorDatasetController, 'publish'])
.as('editor.dataset.publish')
@ -422,10 +384,10 @@ router
.as('editor.dataset.doiStore')
.where('id', router.matchers.number())
.use([middleware.auth(), middleware.can(['dataset-publish'])]);
// router
// .put('/dataset/:id/update', [EditorDatasetController, 'update'])
// .as('editor.dataset.update')
// .use([middleware.auth(), middleware.can(['dataset-editor-edit'])]);
router
.put('/dataset/:id/update', [EditorDatasetController, 'update'])
.as('editor.dataset.update')
.use([middleware.auth(), middleware.can(['dataset-editor-edit'])]);
})
.prefix('editor');
@ -446,11 +408,6 @@ router
.as('reviewer.dataset.reviewUpdate')
.where('id', router.matchers.number())
.use([middleware.auth(), middleware.can(['dataset-review'])]);
router
.get('/file/download/:id', [ReviewerDatasetController, 'download'])
.as('reviewer.file.download')
.where('id', router.matchers.number())
.use([middleware.auth(), middleware.can(['dataset-review'])]);
router
.get('dataset/:id/reject', [ReviewerDatasetController, 'reject'])
.as('reviewer.dataset.reject')

View file

@ -11,8 +11,8 @@ import { middleware } from '../kernel.js';
// API
router
.group(() => {
router.get('clients', [UserController, 'getSubmitters']).as('client.index').use(middleware.auth());;
router.get('authors', [AuthorsController, 'index']).as('author.index').use(middleware.auth());;
router.get('clients', [UserController, 'getSubmitters']).as('client.index');
router.get('authors', [AuthorsController, 'index']).as('author.index');
router.get('datasets', [DatasetController, 'index']).as('dataset.index');
router.get('persons', [AuthorsController, 'persons']).as('author.persons');
@ -20,9 +20,9 @@ router
router.get('/dataset/:publish_id', [DatasetController, 'findOne']).as('dataset.findOne');
router.get('/sitelinks/:year', [HomeController, 'findDocumentsPerYear']);
router.get('/years', [HomeController, 'findYears']);
router.get('/statistic', [HomeController, 'findPublicationsPerMonth']);
router.get('/statistic/:year', [HomeController, 'findPublicationsPerMonth']);
router.get('/file/download/:id', [FileController, 'findOne']).as('file.findOne');
router.get('/download/:id', [FileController, 'findOne']).as('file.findOne');
router.get('/avatar/:name/:background?/:textColor?/:size?', [AvatarController, 'generateAvatar']);

View file

@ -1,80 +0,0 @@
import { FieldContext } from '@vinejs/vine/types';
import vine, { VineArray } from '@vinejs/vine';
import { SchemaTypes } from '@vinejs/vine/types';
type Options = {
typeA: string;
typeB: string;
};
/**
* Custom rule to validate an array of titles contains at least one title
* with type 'main' and one with type 'translated'.
*
* This rule expects the validated value to be an array of objects,
* where each object has a "type" property.
*/
async function arrayContainsTypes(value: unknown, options: Options, field: FieldContext) {
if (!Array.isArray(value)) {
field.report(`The {{field}} must be an array of titles.`, 'array.titlesContainsMainAndTranslated', field);
return false;
}
const typeAExpected = options.typeA.toLowerCase();
const typeBExpected = options.typeB.toLowerCase();
// const hasMain = value.some((title: any) => {
// return typeof title === 'object' && title !== null && String(title.type).toLowerCase() === 'main';
// });
// const hasTranslated = value.some((title: any) => {
// return typeof title === 'object' && title !== null && String(title.type).toLowerCase() === 'translated';
// });
const hasTypeA = value.some((item: any) => {
return typeof item === 'object' && item !== null && String(item.type).toLowerCase() === typeAExpected;
});
const hasTypeB = value.some((item: any) => {
return typeof item === 'object' && item !== null && String(item.type).toLowerCase() === typeBExpected;
});
if (!hasTypeA || !hasTypeB) {
let errorMessage = `The ${field.getFieldPath()} array must have at least one '${options.typeA}' item and one '${options.typeB}' item.`;
// Check for specific field names to produce a more readable message.
if (field.getFieldPath() === 'titles') {
// For titles we expect one main and minimum one translated title.
if (!hasTypeA && !hasTypeB) {
errorMessage = 'For titles, define at least one main title and at least one Translated title as MAIN TITLE.';
} else if (!hasTypeA) {
errorMessage = 'For titles, define at least one main title.';
} else if (!hasTypeB) {
errorMessage = 'For Titles, define at least one Translated title as MAIN TITLE.';
}
} else if (field.getFieldPath() === 'descriptions') {
// For descriptions we expect one abstracts description and minimum one translated description.
if (!hasTypeA && !hasTypeB) {
errorMessage = 'For descriptions, define at least one abstract and at least one Translated description as MAIN ABSTRACT.';
} else if (!hasTypeA) {
errorMessage = 'For descriptions, define at least one abstract.';
} else if (!hasTypeB) {
errorMessage = 'For Descriptions, define at least one Translated description as MAIN ABSTRACT.';
}
}
field.report(errorMessage, 'array.containsTypes', field, options);
return false;
}
return true;
}
export const arrayContainsMainAndTranslatedRule = vine.createRule(arrayContainsTypes);
declare module '@vinejs/vine' {
interface VineArray<Schema extends SchemaTypes> {
arrayContainsTypes(options: Options): this;
}
}
VineArray.macro('arrayContainsTypes', function <Schema extends SchemaTypes>(this: VineArray<Schema>, options: Options) {
return this.use(arrayContainsMainAndTranslatedRule(options));
});

View file

@ -12,10 +12,6 @@ module.exports = {
gray: 'gray',
},
extend: {
backgroundImage: {
'radio-checked': "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z' /%3E%3C/svg%3E\")",
'checkbox-checked': "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1 1'%3E%3Cpath style='fill:%23fff' d='M 0.04038059,0.6267767 0.14644661,0.52071068 0.42928932,0.80355339 0.3232233,0.90961941 z M 0.21715729,0.80355339 0.85355339,0.16715729 0.95961941,0.2732233 0.3232233,0.90961941 z'%3E%3C/path%3E%3C/svg%3E\")",
},
colors: {
'primary': '#22C55E',
'inprogress': 'rgb(94 234 212)',