Compare commits
No commits in common. "master" and "feat/checkReferenceType" have entirely different histories.
master
...
feat/check
|
@ -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'),
|
||||
],
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -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}%`);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 }),
|
||||
// };
|
||||
// }
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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[])
|
||||
// );
|
|
@ -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';
|
|
@ -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
|
@ -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
|
@ -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",
|
||||
|
|
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 526 B |
Before Width: | Height: | Size: 2.2 KiB |
|
@ -1,3 +0,0 @@
|
|||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
HostUrl=https://sea1.geoinformation.dev/favicon-32x32.png
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
public/favicon.png
Normal file
After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 952 KiB |
|
@ -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"}
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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 = '© <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>
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'}`">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 & 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 & 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(', ') }}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
Before Width: | Height: | Size: 287 KiB After Width: | Height: | Size: 17 KiB |
|
@ -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,
|
||||
|
|
|
@ -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==">
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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']);
|
||||
|
||||
|
|
|
@ -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));
|
||||
});
|
|
@ -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)',
|
||||
|
|