hotfix: enhance editor dataset management and UI improvements
- Implemented dataset editing functionality for editor roles, including fetching, updating, and categorizing datasets. - Added routes and controller actions for editing, updating, and categorizing datasets within the editor interface. - Integrated UI components for managing dataset metadata, subjects, references, and files. - Enhanced keyword management with features for adding, editing, and deleting keywords, including handling keywords used by multiple datasets. - Improved reference management with features for adding, editing, and deleting dataset references. - Added validation for dataset updates using the `updateEditorDatasetValidator`. - Updated the dataset edit form to include components for managing titles, descriptions, authors, contributors, licenses, coverage, subjects, references, and files. - Implemented transaction management for dataset updates to ensure data consistency. - Added a download route for files associated with datasets. - Improved the UI for displaying and interacting with datasets in the editor index view, including adding edit and categorize buttons. - Fixed an issue where the file size was not correctly calculated. - Added a tooltip to the keyword value column in the TableKeywords component to explain the editability of keywords. - Added a section to display keywords that are marked for deletion. - Added a section to display references that are marked for deletion. - Added a restore button to the references to delete section to restore references. - Updated the SearchCategoryAutocomplete component to support read-only mode. - Updated the FormControl component to support read-only mode. - Added icons and styling improvements to various components. - Added a default value for subjectsToDelete and referencesToDelete in the dataset model. - Updated the FooterBar component to use the JustboilLogo component. - Updated the app.ts file to fetch chart data without a year parameter. - Updated the Login.vue file to invert the logo in dark mode. - Updated the AccountInfo.vue file to add a Head component.
|
@ -18,9 +18,32 @@ import { HttpException } from 'node-exceptions';
|
||||||
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
|
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
|
||||||
import vine, { SimpleMessagesProvider } from '@vinejs/vine';
|
import vine, { SimpleMessagesProvider } from '@vinejs/vine';
|
||||||
import mail from '@adonisjs/mail/services/main';
|
import mail from '@adonisjs/mail/services/main';
|
||||||
// import { resolveMx } from 'dns/promises';
|
|
||||||
// import * as net from 'net';
|
|
||||||
import { validate } from 'deep-email-validator';
|
import { validate } from 'deep-email-validator';
|
||||||
|
import {
|
||||||
|
TitleTypes,
|
||||||
|
DescriptionTypes,
|
||||||
|
ContributorTypes,
|
||||||
|
PersonNameTypes,
|
||||||
|
ReferenceIdentifierTypes,
|
||||||
|
RelationTypes,
|
||||||
|
SubjectTypes,
|
||||||
|
} 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
|
// Create a new instance of the client
|
||||||
const client = new Client({ node: 'http://localhost:9200' }); // replace with your OpenSearch endpoint
|
const client = new Client({ node: 'http://localhost:9200' }); // replace with your OpenSearch endpoint
|
||||||
|
|
||||||
|
@ -255,71 +278,6 @@ 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) {
|
public async rejectUpdate({ request, response, auth }: HttpContext) {
|
||||||
const authUser = auth.user!;
|
const authUser = auth.user!;
|
||||||
|
|
||||||
|
@ -353,7 +311,7 @@ export default class DatasetsController {
|
||||||
return response
|
return response
|
||||||
.flash(
|
.flash(
|
||||||
`Invalid server state. Dataset with id ${id} cannot be rejected. Datset has server state ${dataset.server_state}.`,
|
`Invalid server state. Dataset with id ${id} cannot be rejected. Datset has server state ${dataset.server_state}.`,
|
||||||
'warning'
|
'warning',
|
||||||
)
|
)
|
||||||
.redirect()
|
.redirect()
|
||||||
.toRoute('editor.dataset.list');
|
.toRoute('editor.dataset.list');
|
||||||
|
@ -388,7 +346,9 @@ export default class DatasetsController {
|
||||||
emailStatusMessage = ` A rejection email was successfully sent to ${dataset.user.email}.`;
|
emailStatusMessage = ` A rejection email was successfully sent to ${dataset.user.email}.`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(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 {
|
} else {
|
||||||
emailStatusMessage = ` However, the email could not be sent because the submitter's email address (${dataset.user.email}) is not valid.`;
|
emailStatusMessage = ` However, the email could not be sent because the submitter's email address (${dataset.user.email}) is not valid.`;
|
||||||
|
@ -536,10 +496,375 @@ export default class DatasetsController {
|
||||||
|
|
||||||
public async show({}: HttpContext) {}
|
public async show({}: HttpContext) {}
|
||||||
|
|
||||||
public async edit({}: 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')
|
||||||
|
.preload('contributors')
|
||||||
|
// .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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
.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 update({}: HttpContextContract) {}
|
// public async update({}: HttpContextContract) {}
|
||||||
public async update({ response }: HttpContext) {
|
public async updateOpensearch({ response }: HttpContext) {
|
||||||
const id = 273; //request.param('id');
|
const id = 273; //request.param('id');
|
||||||
const dataset = await Dataset.query().preload('xmlCache').where('id', id).firstOrFail();
|
const dataset = await Dataset.query().preload('xmlCache').where('id', id).firstOrFail();
|
||||||
// add xml elements
|
// add xml elements
|
||||||
|
@ -655,6 +980,19 @@ 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) {}
|
public async destroy({}: HttpContext) {}
|
||||||
|
|
||||||
private async createXmlRecord(dataset: Dataset, datasetNode: XMLBuilder) {
|
private async createXmlRecord(dataset: Dataset, datasetNode: XMLBuilder) {
|
||||||
|
|
|
@ -29,12 +29,8 @@ import {
|
||||||
} from '#contracts/enums';
|
} from '#contracts/enums';
|
||||||
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
|
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
|
||||||
import DatasetReference from '#models/dataset_reference';
|
import DatasetReference from '#models/dataset_reference';
|
||||||
import { cuid } from '@adonisjs/core/helpers';
|
|
||||||
import File from '#models/file';
|
import File from '#models/file';
|
||||||
import ClamScan from 'clamscan';
|
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 drive from '@adonisjs/drive/services/main';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { Exception } from '@adonisjs/core/exceptions';
|
import { Exception } from '@adonisjs/core/exceptions';
|
||||||
|
@ -945,10 +941,9 @@ export default class DatasetController {
|
||||||
// session.flash('errors', 'Invalid server state!');
|
// session.flash('errors', 'Invalid server state!');
|
||||||
return response
|
return response
|
||||||
.flash(
|
.flash(
|
||||||
'warning',
|
|
||||||
`Invalid server state. Dataset with id ${id} cannot be edited. Datset has server state ${dataset.server_state}.`,
|
`Invalid server state. Dataset with id ${id} cannot be edited. Datset has server state ${dataset.server_state}.`,
|
||||||
|
'warning',
|
||||||
)
|
)
|
||||||
.redirect()
|
|
||||||
.toRoute('dataset.list');
|
.toRoute('dataset.list');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1020,7 +1015,11 @@ export default class DatasetController {
|
||||||
const dataset = await Dataset.findOrFail(datasetId);
|
const dataset = await Dataset.findOrFail(datasetId);
|
||||||
await dataset.load('files');
|
await dataset.load('files');
|
||||||
// Accumulate the size of the already related files
|
// Accumulate the size of the already related files
|
||||||
const preExistingFileSize = dataset.files.reduce((acc, file) => acc + file.fileSize, 0);
|
// const preExistingFileSize = dataset.files.reduce((acc, file) => acc + file.fileSize, 0);
|
||||||
|
let preExistingFileSize = 0;
|
||||||
|
for (const file of dataset.files) {
|
||||||
|
preExistingFileSize += Number(file.fileSize);
|
||||||
|
}
|
||||||
|
|
||||||
const uploadedTmpFiles: string[] = [];
|
const uploadedTmpFiles: string[] = [];
|
||||||
// Only process multipart if the request has a multipart content type
|
// Only process multipart if the request has a multipart content type
|
||||||
|
@ -1150,22 +1149,97 @@ export default class DatasetController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// await dataset.useTransaction(trx).related('subjects').sync([]);
|
// Process all subjects/keywords from the request
|
||||||
const keywords = request.input('subjects');
|
const subjects = request.input('subjects');
|
||||||
for (const keywordData of keywords) {
|
for (const subjectData of subjects) {
|
||||||
if (keywordData.id) {
|
// Case 1: Subject already exists in the database (has an ID)
|
||||||
const subject = await Subject.findOrFail(keywordData.id);
|
if (subjectData.id) {
|
||||||
// await dataset.useTransaction(trx).related('subjects').attach([keywordData.id]);
|
// Retrieve the existing subject
|
||||||
subject.value = keywordData.value;
|
const existingSubject = await Subject.findOrFail(subjectData.id);
|
||||||
subject.type = keywordData.type;
|
|
||||||
subject.external_key = keywordData.external_key;
|
// Update subject properties from the request data
|
||||||
if (subject.$isDirty) {
|
existingSubject.value = subjectData.value;
|
||||||
await subject.save();
|
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 {
|
} else {
|
||||||
const keyword = new Subject();
|
// Create new reference
|
||||||
keyword.fill(keywordData);
|
const dataReference = new DatasetReference();
|
||||||
await dataset.useTransaction(trx).related('subjects').save(keyword, false);
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1269,7 +1343,7 @@ export default class DatasetController {
|
||||||
await dataset.useTransaction(trx).save();
|
await dataset.useTransaction(trx).save();
|
||||||
|
|
||||||
await trx.commit();
|
await trx.commit();
|
||||||
console.log('Dataset and related models created successfully');
|
console.log('Dataset has been updated successfully');
|
||||||
|
|
||||||
session.flash('message', 'Dataset has been updated successfully');
|
session.flash('message', 'Dataset has been updated successfully');
|
||||||
// return response.redirect().toRoute('user.index');
|
// return response.redirect().toRoute('user.index');
|
||||||
|
|
|
@ -3,6 +3,13 @@ import type { BodyParserConfig } from '#models/types';
|
||||||
import { createId } from '@paralleldrive/cuid2';
|
import { createId } from '@paralleldrive/cuid2';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import config from '@adonisjs/core/services/config';
|
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 {
|
export function sum(a: number, b: number): number {
|
||||||
return a + b;
|
return a + b;
|
||||||
|
@ -78,3 +85,40 @@ export function formatBytes(bytes: number): string {
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -314,12 +314,137 @@ export const updateDatasetValidator = vine.compile(
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// files: schema.array([rules.minLength(1)]).members(
|
export const updateEditorDatasetValidator = vine.compile(
|
||||||
// schema.file({
|
vine.object({
|
||||||
// size: '512mb',
|
// first step
|
||||||
// extnames: ['jpg', 'gif', 'png', 'tif', 'pdf', 'zip', 'fgb', 'nc', 'qml', 'ovr', 'gpkg', 'gml', 'gpx', 'kml', 'kmz', 'json'],
|
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'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
let messagesProvider = new SimpleMessagesProvider({
|
let messagesProvider = new SimpleMessagesProvider({
|
||||||
'minLength': '{{ field }} must be at least {{ min }} characters long',
|
'minLength': '{{ field }} must be at least {{ min }} characters long',
|
||||||
|
|
BIN
public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 526 B |
BIN
public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 18 KiB |
9
public/favicon.svg
Normal file
After Width: | Height: | Size: 952 KiB |
1
public/site.webmanifest
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"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"}
|
|
@ -15,9 +15,10 @@ const year = computed(() => new Date().getFullYear());
|
||||||
<!-- Get more with <a href="https://tailwind-vue.justboil.me/" target="_blank" class="text-blue-600">Premium
|
<!-- Get more with <a href="https://tailwind-vue.justboil.me/" target="_blank" class="text-blue-600">Premium
|
||||||
version</a> -->
|
version</a> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="md:py-3">
|
<div class="md:py-1">
|
||||||
<a href="https://www.tethys.at" target="_blank">
|
<a href="https://www.tethys.at" target="_blank">
|
||||||
<JustboilLogo class="w-auto h-8 md:h-6" />
|
<!-- <JustboilLogo class="w-auto h-8 md:h-6" /> -->
|
||||||
|
<JustboilLogo class="w-auto h-12 md:h-10 dark:invert" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</BaseLevel>
|
</BaseLevel>
|
||||||
|
|
|
@ -170,12 +170,9 @@ const showAbout = async () => {
|
||||||
<NavBarItem v-if="userHasRoles(['reviewer'])" :route-name="'reviewer.dataset.list'">
|
<NavBarItem v-if="userHasRoles(['reviewer'])" :route-name="'reviewer.dataset.list'">
|
||||||
<NavBarItemLabel :icon="mdiGlasses" label="Reviewer Menu" />
|
<NavBarItemLabel :icon="mdiGlasses" label="Reviewer Menu" />
|
||||||
</NavBarItem>
|
</NavBarItem>
|
||||||
<!-- <NavBarItem>
|
<!-- <NavBarItem @click="showAbout">
|
||||||
<NavBarItemLabel :icon="mdiEmail" label="Messages" />
|
|
||||||
</NavBarItem> -->
|
|
||||||
<NavBarItem @click="showAbout">
|
|
||||||
<NavBarItemLabel :icon="mdiInformationVariant" label="About" />
|
<NavBarItemLabel :icon="mdiInformationVariant" label="About" />
|
||||||
</NavBarItem>
|
</NavBarItem> -->
|
||||||
<BaseDivider nav-bar />
|
<BaseDivider nav-bar />
|
||||||
<NavBarItem @click="logout">
|
<NavBarItem @click="logout">
|
||||||
<NavBarItemLabel :icon="mdiLogout" label="Log Out" />
|
<NavBarItemLabel :icon="mdiLogout" label="Log Out" />
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<div class="relative" data-te-dropdown-ref>
|
<div class="relative" data-te-dropdown-ref>
|
||||||
<button id="states-button" data-dropdown-toggle="dropdown-states"
|
<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"
|
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" @click.prevent="showStates">
|
type="button" :disabled="isReadOnly" @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">
|
<!-- <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" />
|
<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">
|
<mask id="mask0_12694_49953" style="mask-type: alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="15" height="12">
|
||||||
|
@ -65,7 +65,7 @@
|
||||||
</svg> -->
|
</svg> -->
|
||||||
<!-- eng -->
|
<!-- eng -->
|
||||||
{{ language }}
|
{{ language }}
|
||||||
<svg aria-hidden="true" class="w-4 h-4 ml-1" fill="currentColor" viewBox="0 0 20 20"
|
<svg aria-hidden="true" class="w-4 h-4 ml-1" fill="currentColor" viewBox="0 0 20 20" v-if="!isReadOnly"
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd"
|
<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"
|
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="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" -->
|
<!-- 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"
|
<input v-model="computedValue" type="text" :name="props.name" autocomplete="off" :class="inputElClass"
|
||||||
placeholder="Search Keywords..." required @input="handleInput" />
|
placeholder="Search Keywords..." required @input="handleInput" :readonly="isReadOnly" />
|
||||||
<!-- v-model="data.search" -->
|
<!-- v-model="data.search" -->
|
||||||
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="computedValue.length < 2"
|
<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">
|
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
@ -101,7 +101,7 @@
|
||||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="computedValue.length >= 2"
|
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="computedValue.length >= 2 && !isReadOnly"
|
||||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" @click="() => {
|
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" @click="() => {
|
||||||
computedValue = '';
|
computedValue = '';
|
||||||
data.isOpen = false;
|
data.isOpen = false;
|
||||||
|
@ -166,6 +166,10 @@ let props = defineProps({
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
isReadOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
required: Boolean,
|
required: Boolean,
|
||||||
borderless: Boolean,
|
borderless: Boolean,
|
||||||
transparent: Boolean,
|
transparent: Boolean,
|
||||||
|
@ -190,11 +194,18 @@ const inputElClass = computed(() => {
|
||||||
'dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500',
|
'dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500',
|
||||||
'h-12',
|
'h-12',
|
||||||
props.borderless ? 'border-0' : 'border',
|
props.borderless ? 'border-0' : 'border',
|
||||||
props.transparent ? 'bg-transparent' : 'bg-white dark:bg-slate-800',
|
props.transparent && 'bg-transparent',
|
||||||
// props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
|
// props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
|
||||||
];
|
];
|
||||||
// if (props.icon) {
|
// if (props.icon) {
|
||||||
base.push('pl-10');
|
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;
|
return base;
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { Subject } from '@/Dataset';
|
||||||
// import FormField from '@/Components/FormField.vue';
|
// import FormField from '@/Components/FormField.vue';
|
||||||
import FormControl from '@/Components/FormControl.vue';
|
import FormControl from '@/Components/FormControl.vue';
|
||||||
import SearchCategoryAutocomplete from '@/Components/SearchCategoryAutocomplete.vue';
|
import SearchCategoryAutocomplete from '@/Components/SearchCategoryAutocomplete.vue';
|
||||||
|
import { mdiRefresh } from '@mdi/js';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
checkable: Boolean,
|
checkable: Boolean,
|
||||||
|
@ -27,6 +28,22 @@ const props = defineProps({
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
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();
|
const styleService = StyleService();
|
||||||
|
@ -58,21 +75,45 @@ const pagesList = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const removeItem = (key: number) => {
|
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);
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<!-- <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"
|
<span v-for="checkedRow in checkedRows" :key="checkedRow.id"
|
||||||
|
@ -87,9 +128,24 @@ const removeItem = (key: number) => {
|
||||||
<!-- <th v-if="checkable" /> -->
|
<!-- <th v-if="checkable" /> -->
|
||||||
<!-- <th class="hidden lg:table-cell"></th> -->
|
<!-- <th class="hidden lg:table-cell"></th> -->
|
||||||
<th scope="col">Type</th>
|
<th scope="col">Type</th>
|
||||||
<th scope="col">Value</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">Language</th>
|
<th scope="col">Language</th>
|
||||||
|
<th scope="col">Usage Count</th>
|
||||||
<th scope="col" />
|
<th scope="col" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -97,7 +153,9 @@ const removeItem = (key: number) => {
|
||||||
<tr v-for="(item, index) in itemsPaginated" :key="index">
|
<tr v-for="(item, index) in itemsPaginated" :key="index">
|
||||||
|
|
||||||
<td data-label="Type" scope="row">
|
<td data-label="Type" scope="row">
|
||||||
<FormControl required v-model="item.type" @update:modelValue="() => {item.external_key = undefined; 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`]">
|
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.type`]">
|
||||||
{{ errors[`subjects.${index}.type`].join(', ') }}
|
{{ errors[`subjects.${index}.type`].join(', ') }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -105,22 +163,19 @@ const removeItem = (key: number) => {
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td data-label="Value" scope="row">
|
<td data-label="Value" scope="row">
|
||||||
<SearchCategoryAutocomplete
|
<SearchCategoryAutocomplete v-if="item.type !== 'uncontrolled'" v-model="item.value" @subject="
|
||||||
v-if="item.type !== 'uncontrolled'"
|
|
||||||
v-model="item.value"
|
|
||||||
@subject="
|
|
||||||
(result) => {
|
(result) => {
|
||||||
item.language = result.language;
|
item.language = result.language;
|
||||||
item.external_key = result.uri;
|
item.external_key = result.uri;
|
||||||
}
|
}
|
||||||
"
|
" :is-read-only="item.dataset_count > 1">
|
||||||
>
|
|
||||||
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.value`]">
|
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.value`]">
|
||||||
{{ errors[`subjects.${index}.value`].join(', ') }}
|
{{ errors[`subjects.${index}.value`].join(', ') }}
|
||||||
</div>
|
</div>
|
||||||
</SearchCategoryAutocomplete>
|
</SearchCategoryAutocomplete>
|
||||||
|
|
||||||
<FormControl v-else required v-model="item.value" type="text" placeholder="[enter keyword value]" :borderless="true">
|
<FormControl v-else required v-model="item.value" type="text" placeholder="[enter keyword value]"
|
||||||
|
:borderless="true" :is-read-only="item.dataset_count > 1">
|
||||||
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.value`]">
|
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.value`]">
|
||||||
{{ errors[`subjects.${index}.value`].join(', ') }}
|
{{ errors[`subjects.${index}.value`].join(', ') }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -128,23 +183,24 @@ const removeItem = (key: number) => {
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td data-label="Language" scope="row">
|
<td data-label="Language" scope="row">
|
||||||
<FormControl
|
<FormControl required v-model="item.language" :type="'select'" placeholder="[Enter Lang]"
|
||||||
required
|
:options="{ de: 'de', en: 'en' }" :is-read-only="isKeywordReadOnly(item)">
|
||||||
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`]">
|
<div class="text-red-400 text-sm" v-if="errors[`subjects.${index}.language`]">
|
||||||
{{ errors[`subjects.${index}.language`].join(', ') }}
|
{{ errors[`subjects.${index}.language`].join(', ') }}
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</td>
|
</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">
|
<td class="before:hidden lg:w-1 whitespace-nowrap" scope="row">
|
||||||
<BaseButtons type="justify-start lg:justify-end" no-wrap>
|
<BaseButtons type="justify-start lg:justify-end" no-wrap>
|
||||||
<!-- <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" /> -->
|
<!-- <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" /> -->
|
||||||
<BaseButton v-if="index > 2" color="danger" :icon="mdiTrashCan" small @click.prevent="removeItem(index)" />
|
<BaseButton color="danger" :icon="mdiTrashCan" small @click.prevent="removeItem(index)" />
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -155,15 +211,8 @@ const removeItem = (key: number) => {
|
||||||
<div class="p-3 lg:px-6 border-t border-gray-100 dark:border-slate-800">
|
<div class="p-3 lg:px-6 border-t border-gray-100 dark:border-slate-800">
|
||||||
<BaseLevel>
|
<BaseLevel>
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton
|
<BaseButton v-for="page in pagesList" :key="page" :active="page === currentPage" :label="page + 1" small
|
||||||
v-for="page in pagesList"
|
:outline="styleService.darkMode" @click="currentPage = page" />
|
||||||
:key="page"
|
|
||||||
:active="page === currentPage"
|
|
||||||
:label="page + 1"
|
|
||||||
small
|
|
||||||
:outline="styleService.darkMode"
|
|
||||||
@click="currentPage = page"
|
|
||||||
/>
|
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
<small>Page {{ currentPageHuman }} of {{ numPages }}</small>
|
<small>Page {{ currentPageHuman }} of {{ numPages }}</small>
|
||||||
</BaseLevel>
|
</BaseLevel>
|
||||||
|
@ -172,6 +221,47 @@ const removeItem = (key: number) => {
|
||||||
<div class="text-red-400 text-sm" v-if="errors.subjects && Array.isArray(errors.subjects)">
|
<div class="text-red-400 text-sm" v-if="errors.subjects && Array.isArray(errors.subjects)">
|
||||||
{{ errors.subjects.join(', ') }}
|
{{ errors.subjects.join(', ') }}
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// import { Head, Link, useForm } from '@inertiajs/inertia-vue3';
|
// import { Head, Link, useForm } from '@inertiajs/inertia-vue3';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useForm } from '@inertiajs/vue3';
|
import { useForm, Head } from '@inertiajs/vue3';
|
||||||
// import { ref } from 'vue';
|
// import { ref } from 'vue';
|
||||||
// import { reactive } from 'vue';
|
// import { reactive } from 'vue';
|
||||||
import {
|
import {
|
||||||
|
@ -126,6 +126,7 @@ const flash: Ref<any> = computed(() => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutAuthenticated>
|
<LayoutAuthenticated>
|
||||||
|
<Head title="Profile Security"></Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton :icon="mdiAccount" title="Profile" main>
|
<SectionTitleLineWithButton :icon="mdiAccount" title="Profile" main>
|
||||||
<BaseButton :route-name="stardust.route('dashboard')" :icon="mdiArrowLeftBoldOutline" label="Back"
|
<BaseButton :route-name="stardust.route('dashboard')" :icon="mdiArrowLeftBoldOutline" label="Back"
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<!-- <SectionFullScreen v-slot="{ cardClass }" :bg="'greenBlue'"> -->
|
<!-- <SectionFullScreen v-slot="{ cardClass }" :bg="'greenBlue'"> -->
|
||||||
<SectionFullScreen v-slot="{ cardClass }">
|
<SectionFullScreen v-slot="{ cardClass }">
|
||||||
<a class="text-2xl font-semibold flex justify-center items-center mb-8 lg:mb-10">
|
<a class="text-2xl font-semibold flex justify-center items-center mb-8 lg:mb-10">
|
||||||
<img src="../../logo.svg" class="h-10 mr-4" alt="Windster Logo" />
|
<img src="../../logo.svg" class="h-10 mr-4 dark:invert" alt="Windster Logo" />
|
||||||
<!-- <span class="self-center text-2xl font-bold whitespace-nowrap">Tethys</span> -->
|
<!-- <span class="self-center text-2xl font-bold whitespace-nowrap">Tethys</span> -->
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
371
resources/js/Pages/Editor/Dataset/Category.vue
Normal file
|
@ -0,0 +1,371 @@
|
||||||
|
<template>
|
||||||
|
<LayoutAuthenticated>
|
||||||
|
<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">
|
||||||
|
<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)">
|
||||||
|
{{ `${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>
|
||||||
|
<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)">
|
||||||
|
{{ `${parent.name} (${parent.number})` }}
|
||||||
|
</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'
|
||||||
|
]">
|
||||||
|
{{ `${element.name} (${element.number})` }}
|
||||||
|
</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)">
|
||||||
|
{{ `${child.name} (${child.number})` }}
|
||||||
|
</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>{{ element.name }} ({{ 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>
|
853
resources/js/Pages/Editor/Dataset/Edit.vue
Normal file
|
@ -0,0 +1,853 @@
|
||||||
|
<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">
|
||||||
|
<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">
|
||||||
|
<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 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 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, Link, useForm, usePage } from '@inertiajs/inertia-vue3';
|
||||||
import { Head, usePage } from '@inertiajs/vue3';
|
import { Head, usePage } from '@inertiajs/vue3';
|
||||||
import { ComputedRef } from 'vue';
|
import { ComputedRef } from 'vue';
|
||||||
import { mdiSquareEditOutline, mdiAlertBoxOutline, mdiShareVariant, mdiBookEdit, mdiUndo } from '@mdi/js';
|
import { mdiSquareEditOutline, mdiAlertBoxOutline, mdiShareVariant, mdiBookEdit, mdiUndo, mdiLibraryShelves } from '@mdi/js';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||||
import SectionMain from '@/Components/SectionMain.vue';
|
import SectionMain from '@/Components/SectionMain.vue';
|
||||||
|
@ -108,6 +108,7 @@ const formatServerState = (state: string) => {
|
||||||
{{ flash.error }}
|
{{ flash.error }}
|
||||||
</NotificationBar>
|
</NotificationBar>
|
||||||
|
|
||||||
|
|
||||||
<!-- table -->
|
<!-- table -->
|
||||||
<CardBox class="mb-6" has-table>
|
<CardBox class="mb-6" has-table>
|
||||||
<div v-if="props.datasets.data.length > 0">
|
<div v-if="props.datasets.data.length > 0">
|
||||||
|
@ -141,12 +142,14 @@ const formatServerState = (state: string) => {
|
||||||
<tbody class="bg-white divide-y divide-gray-200">
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
<tr v-for="dataset in props.datasets.data" :key="dataset.id"
|
<tr v-for="dataset in props.datasets.data" :key="dataset.id"
|
||||||
:class="[getRowClass(dataset)]">
|
:class="[getRowClass(dataset)]">
|
||||||
<td data-label="Login" class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
|
<td data-label="Login"
|
||||||
<!-- <Link v-bind:href="stardust.route('user.show', [user.id])"
|
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">
|
class="no-underline hover:underline text-cyan-600 dark:text-cyan-400">
|
||||||
{{ user.login }}
|
{{ user.login }}
|
||||||
</Link> -->
|
</Link> -->
|
||||||
<div class="text-sm font-medium">{{ dataset.main_title }}</div>
|
<!-- {{ user.id }} -->
|
||||||
|
{{ dataset.main_title }}
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
|
<td class="py-4 whitespace-nowrap text-gray-700 dark:text-white">
|
||||||
<div class="text-sm">{{ dataset.user.login }}</div>
|
<div class="text-sm">{{ dataset.user.login }}</div>
|
||||||
|
@ -178,33 +181,46 @@ const formatServerState = (state: string) => {
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
class="py-4 whitespace-nowrap text-right text-sm font-medium text-gray-700 dark:text-white">
|
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>
|
<div type="justify-start lg:justify-end" class="grid grid-cols-2 gap-x-2 gap-y-2" no-wrap>
|
||||||
|
|
||||||
<BaseButton v-if="can.receive && (dataset.server_state == 'released')"
|
<BaseButton v-if="can.receive && (dataset.server_state == 'released')"
|
||||||
:route-name="stardust.route('editor.dataset.receive', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.receive', [dataset.id])"
|
||||||
color="info" :icon="mdiSquareEditOutline" :label="'Receive edit task'"
|
color="info" :icon="mdiSquareEditOutline" :label="'Receive edit task'"
|
||||||
small />
|
small class="col-span-1"/>
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
||||||
:route-name="stardust.route('editor.dataset.approve', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.approve', [dataset.id])"
|
||||||
color="info" :icon="mdiShareVariant" :label="'Approve'" small />
|
color="info" :icon="mdiShareVariant" :label="'Approve'" small class="col-span-1"/>
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
v-if="can.approve && (dataset.server_state == 'editor_accepted' || dataset.server_state == 'rejected_reviewer')"
|
||||||
:route-name="stardust.route('editor.dataset.reject', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.reject', [dataset.id])"
|
||||||
color="info" :icon="mdiUndo" label="Reject" small>
|
color="info" :icon="mdiUndo" label="'Reject'" 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.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="'Sets'" small class="col-span-1">
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
|
||||||
<BaseButton v-if="can.publish && (dataset.server_state == 'reviewed')"
|
<BaseButton v-if="can.publish && (dataset.server_state == 'reviewed')"
|
||||||
:route-name="stardust.route('editor.dataset.publish', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.publish', [dataset.id])"
|
||||||
color="info" :icon="mdiBookEdit" :label="'Publish'" small />
|
color="info" :icon="mdiBookEdit" :label="'Publish'" small class="col-span-1"/>
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
v-if="can.publish && (dataset.server_state == 'published' && !dataset.identifier)"
|
v-if="can.publish && (dataset.server_state == 'published' && !dataset.identifier)"
|
||||||
:route-name="stardust.route('editor.dataset.doi', [dataset.id])"
|
:route-name="stardust.route('editor.dataset.doi', [dataset.id])"
|
||||||
color="info" :icon="mdiBookEdit" :label="'Mint DOI'" small />
|
color="info" :icon="mdiBookEdit" :label="'Mint DOI'" small class="col-span-1 last-in-row"/>
|
||||||
|
|
||||||
</BaseButtons>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -231,3 +247,18 @@ const formatServerState = (state: string) => {
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
</LayoutAuthenticated>
|
</LayoutAuthenticated>
|
||||||
</template>
|
</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>
|
|
@ -295,7 +295,7 @@ const fetchCollections = async (collectionId: number) => {
|
||||||
return alreadyDropped ? { ...collection, inUse: true } : { ...collection, inUse: false };
|
return alreadyDropped ? { ...collection, inUse: true } : { ...collection, inUse: false };
|
||||||
});
|
});
|
||||||
// Check if selected collection is in the selected list
|
// Check if selected collection is in the selected list
|
||||||
if (selectedCollection.value && selectedCollectionList.value.find(dc => dc.id === selectedCollection.value.id)) {
|
if (selectedCollection.value && selectedCollectionList.value.find(dc => dc.id === selectedCollection.value?.id)) {
|
||||||
selectedCollection.value = { ...selectedCollection.value, inUse: true };
|
selectedCollection.value = { ...selectedCollection.value, inUse: true };
|
||||||
} else if (selectedCollection.value) {
|
} else if (selectedCollection.value) {
|
||||||
selectedCollection.value = { ...selectedCollection.value, inUse: false };
|
selectedCollection.value = { ...selectedCollection.value, inUse: false };
|
||||||
|
|
|
@ -544,15 +544,15 @@ Removes a selected keyword
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<!-- <label>{{ form.titles[0].language }}</label>
|
<!-- <label>{{ form.titles[0].language }}</label>
|
||||||
<label>{{ form.language }}</label> -->
|
<label>{{ form.language }}</label> -->
|
||||||
<icon-wizard :is-current="formStep == 1" :is-checked="formStep > 1" :label="'Language'">
|
<icon-wizard :is-current="formStep == 1" :is-checked="formStep > 1" :label="'Step 1'">
|
||||||
<icon-language></icon-language>
|
<icon-language></icon-language>
|
||||||
</icon-wizard>
|
</icon-wizard>
|
||||||
|
|
||||||
<icon-wizard :is-current="formStep == 2" :is-checked="formStep > 2" :label="'Mandatory'">
|
<icon-wizard :is-current="formStep == 2" :is-checked="formStep > 2" :label="'Step 2'">
|
||||||
<icon-mandatory></icon-mandatory>
|
<icon-mandatory></icon-mandatory>
|
||||||
</icon-wizard>
|
</icon-wizard>
|
||||||
|
|
||||||
<icon-wizard :is-current="formStep == 3" :is-checked="formStep > 3" :label="'Recommended'">
|
<icon-wizard :is-current="formStep == 3" :is-checked="formStep > 3" :label="'Step 3'">
|
||||||
<icon-recommendet></icon-recommendet>
|
<icon-recommendet></icon-recommendet>
|
||||||
</icon-wizard>
|
</icon-wizard>
|
||||||
|
|
||||||
|
@ -588,7 +588,7 @@ Removes a selected keyword
|
||||||
<input class="form-checkbox" name="rights" id="rights" type="checkbox" v-model="dataset.rights" />
|
<input class="form-checkbox" name="rights" id="rights" type="checkbox" v-model="dataset.rights" />
|
||||||
terms and conditions
|
terms and conditions
|
||||||
</label> -->
|
</label> -->
|
||||||
<FormField label="Rights" help="You must agree to continue" wrap-body
|
<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">
|
: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">
|
<label for="rights" class="checkbox mr-6 mb-3 last:mr-0">
|
||||||
<input type="checkbox" id="rights" required v-model="form.rights" />
|
<input type="checkbox" id="rights" required v-model="form.rights" />
|
||||||
|
|
|
@ -42,7 +42,8 @@
|
||||||
<!-- (2) licenses -->
|
<!-- (2) licenses -->
|
||||||
<FormField label="Licenses" wrap-body :class="{ 'text-red-400': form.errors.licenses }"
|
<FormField label="Licenses" wrap-body :class="{ 'text-red-400': form.errors.licenses }"
|
||||||
class="mt-8 w-full mx-2 flex-1">
|
class="mt-8 w-full mx-2 flex-1">
|
||||||
<FormCheckRadioGroup type="radio" v-model="form.licenses" name="licenses" is-column :options="licenses" />
|
<FormCheckRadioGroup type="radio" v-model="form.licenses" name="licenses" is-column
|
||||||
|
:options="licenses" />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row">
|
<div class="flex flex-col md:flex-row">
|
||||||
|
@ -163,7 +164,8 @@
|
||||||
:class="{ 'text-red-400': form.errors['descriptions.0.value'] }"
|
:class="{ 'text-red-400': form.errors['descriptions.0.value'] }"
|
||||||
class="w-full mr-1 flex-1">
|
class="w-full mr-1 flex-1">
|
||||||
<FormControl required v-model="form.descriptions[0].value" type="textarea"
|
<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"
|
<div class="text-red-400 text-sm"
|
||||||
v-if="form.errors['descriptions.0.value'] && Array.isArray(form.errors['descriptions.0.value'])">
|
v-if="form.errors['descriptions.0.value'] && Array.isArray(form.errors['descriptions.0.value'])">
|
||||||
{{ form.errors['descriptions.0.value'].join(', ') }}
|
{{ form.errors['descriptions.0.value'].join(', ') }}
|
||||||
|
@ -244,7 +246,8 @@
|
||||||
placeholder="search in person table...." v-on:person="onAddAuthor"></SearchAutocomplete>
|
placeholder="search in person table...." v-on:person="onAddAuthor"></SearchAutocomplete>
|
||||||
|
|
||||||
<TablePersons :persons="form.authors" v-if="form.authors.length > 0" :relation="'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)">
|
<div class="text-red-400 text-sm"
|
||||||
|
v-if="form.errors.authors && Array.isArray(form.errors.authors)">
|
||||||
{{ form.errors.authors.join(', ') }}
|
{{ form.errors.authors.join(', ') }}
|
||||||
</div>
|
</div>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
|
@ -334,8 +337,8 @@
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CardBox class="mb-6 shadow" has-table title="Dataset References" :icon="mdiEarthPlus" :header-icon="mdiPlusCircle"
|
<CardBox class="mb-6 shadow" has-table title="Dataset References" :icon="mdiEarthPlus"
|
||||||
v-on:header-icon-click="addReference">
|
:header-icon="mdiPlusCircle" v-on:header-icon-click="addReference">
|
||||||
<!-- Message when no references exist -->
|
<!-- Message when no references exist -->
|
||||||
<div v-if="form.references.length === 0" class="text-center py-4">
|
<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-600">No references added yet.</p>
|
||||||
|
@ -408,6 +411,42 @@
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
</CardBox>
|
||||||
|
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
|
@ -420,7 +459,7 @@
|
||||||
</li>
|
</li>
|
||||||
</ul> -->
|
</ul> -->
|
||||||
<TableKeywords :keywords="form.subjects" :errors="form.errors" :subjectTypes="subjectTypes"
|
<TableKeywords :keywords="form.subjects" :errors="form.errors" :subjectTypes="subjectTypes"
|
||||||
v-if="form.subjects.length > 0" />
|
v-model:subjects-to-delete="form.subjectsToDelete" v-if="form.subjects.length > 0" />
|
||||||
</CardBox>
|
</CardBox>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -447,7 +486,9 @@
|
||||||
</select> -->
|
</select> -->
|
||||||
</div>
|
</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"
|
||||||
|
:showClearButton="false">
|
||||||
|
</FileUploadComponent>
|
||||||
|
|
||||||
<div class="text-red-400 text-sm" v-if="form.errors['file'] && Array.isArray(form.errors['files'])">
|
<div class="text-red-400 text-sm" v-if="form.errors['file'] && Array.isArray(form.errors['files'])">
|
||||||
{{ form.errors['files'].join(', ') }}
|
{{ form.errors['files'].join(', ') }}
|
||||||
|
@ -527,6 +568,7 @@ import {
|
||||||
mdiBookOpenPageVariant,
|
mdiBookOpenPageVariant,
|
||||||
mdiEarthPlus,
|
mdiEarthPlus,
|
||||||
mdiAlertBoxOutline,
|
mdiAlertBoxOutline,
|
||||||
|
mdiRestore
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import { notify } from '@/notiwind';
|
import { notify } from '@/notiwind';
|
||||||
import NotificationBar from '@/Components/NotificationBar.vue';
|
import NotificationBar from '@/Components/NotificationBar.vue';
|
||||||
|
@ -633,6 +675,8 @@ const mapId = 'test';
|
||||||
// }
|
// }
|
||||||
|
|
||||||
props.dataset.filesToDelete = [];
|
props.dataset.filesToDelete = [];
|
||||||
|
props.dataset.subjectsToDelete = [];
|
||||||
|
props.dataset.referencesToDelete = [];
|
||||||
let form = useForm<Dataset>(props.dataset as Dataset);
|
let form = useForm<Dataset>(props.dataset as Dataset);
|
||||||
|
|
||||||
// const mainService = MainService();
|
// const mainService = MainService();
|
||||||
|
@ -745,6 +789,8 @@ const submit = async (): Promise<void> => {
|
||||||
// form.filesToDelete = [];
|
// form.filesToDelete = [];
|
||||||
// Clear the array using splice
|
// Clear the array using splice
|
||||||
form.filesToDelete?.splice(0, form.filesToDelete.length);
|
form.filesToDelete?.splice(0, form.filesToDelete.length);
|
||||||
|
form.subjectsToDelete?.splice(0, form.subjectsToDelete.length);
|
||||||
|
form.referencesToDelete?.splice(0, form.referencesToDelete.length);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -794,7 +840,7 @@ const onAddContributor = (person: Person) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const addKeyword = () => {
|
const addKeyword = () => {
|
||||||
let newSubject: Subject = { value: 'test', language: '', type: 'uncontrolled', dataset_count: 0 };
|
let newSubject: Subject = { value: '', language: '', type: 'uncontrolled' };
|
||||||
//this.dataset.files.push(uploadedFiles[i]);
|
//this.dataset.files.push(uploadedFiles[i]);
|
||||||
form.subjects.push(newSubject);
|
form.subjects.push(newSubject);
|
||||||
};
|
};
|
||||||
|
@ -806,9 +852,35 @@ const addReference = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeReference = (key: any) => {
|
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);
|
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) => {
|
const onMapInitialized = (newItem: any) => {
|
||||||
console.log(newItem);
|
console.log(newItem);
|
||||||
};
|
};
|
||||||
|
|
|
@ -96,7 +96,7 @@ if ((!localStorage[darkModeKey] && window.matchMedia('(prefers-color-scheme: dar
|
||||||
mainService.fetchApi('clients');
|
mainService.fetchApi('clients');
|
||||||
mainService.fetchApi('authors');
|
mainService.fetchApi('authors');
|
||||||
mainService.fetchApi('datasets');
|
mainService.fetchApi('datasets');
|
||||||
mainService.fetchChartData("2022");
|
mainService.fetchChartData();
|
||||||
|
|
||||||
/* Collapse mobile aside menu on route change */
|
/* Collapse mobile aside menu on route change */
|
||||||
Inertia.on('navigate', () => {
|
Inertia.on('navigate', () => {
|
||||||
|
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 287 KiB |
|
@ -7,6 +7,12 @@
|
||||||
<meta name="msapplication-TileColor" content="#da532c">
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<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"> -->
|
<!-- <link rel="icon" href="/apps/theming/favicon/settings?v=ad28c447"> -->
|
||||||
<input type="hidden" id="initial-state-firstrunwizard-desktop"
|
<input type="hidden" id="initial-state-firstrunwizard-desktop"
|
||||||
value="Imh0dHBzOi8vZ2l0ZWEuZ2VvbG9naWUuYWMuYXQvZ2VvbGJhL3RldGh5cy5iYWNrZW5kIg==">
|
value="Imh0dHBzOi8vZ2l0ZWEuZ2VvbG9naWUuYWMuYXQvZ2VvbGJhL3RldGh5cy5iYWNrZW5kIg==">
|
||||||
|
|
|
@ -364,6 +364,35 @@ router
|
||||||
.as('editor.dataset.rejectUpdate')
|
.as('editor.dataset.rejectUpdate')
|
||||||
.where('id', router.matchers.number())
|
.where('id', router.matchers.number())
|
||||||
.use([middleware.auth(), middleware.can(['dataset-editor-reject'])]);
|
.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
|
router
|
||||||
.get('dataset/:id/publish', [EditorDatasetController, 'publish'])
|
.get('dataset/:id/publish', [EditorDatasetController, 'publish'])
|
||||||
.as('editor.dataset.publish')
|
.as('editor.dataset.publish')
|
||||||
|
@ -384,10 +413,10 @@ router
|
||||||
.as('editor.dataset.doiStore')
|
.as('editor.dataset.doiStore')
|
||||||
.where('id', router.matchers.number())
|
.where('id', router.matchers.number())
|
||||||
.use([middleware.auth(), middleware.can(['dataset-publish'])]);
|
.use([middleware.auth(), middleware.can(['dataset-publish'])]);
|
||||||
router
|
// router
|
||||||
.put('/dataset/:id/update', [EditorDatasetController, 'update'])
|
// .put('/dataset/:id/update', [EditorDatasetController, 'update'])
|
||||||
.as('editor.dataset.update')
|
// .as('editor.dataset.update')
|
||||||
.use([middleware.auth(), middleware.can(['dataset-editor-edit'])]);
|
// .use([middleware.auth(), middleware.can(['dataset-editor-edit'])]);
|
||||||
})
|
})
|
||||||
.prefix('editor');
|
.prefix('editor');
|
||||||
|
|
||||||
|
|