feat: update API controllers, validations, and Vue components
All checks were successful
CI / container-job (push) Successful in 49s
All checks were successful
CI / container-job (push) Successful in 49s
- Modified Api/Authors.Controller.ts to use only personal types and sort by dataset_count. - Completely rewritten AvatarController.ts. - Added new Api/CollectionsController.ts for querying collections and collection_roles. - Modified Api/DatasetController.ts to preload titles, identifier and order by server_date_published. - Modified FileController.ts to serve files from /storage/app/data/ instead of /storage/app/public. - Added new Api/UserController for requesting submitters (getSubmitters). - Improved OaiController.ts with performant DB queries for better ResumptionToken handling. - Modified Submitter/DatasetController.ts by adding a categorize method for library classification. - Rewritten ResumptionToken.ts. - Improved TokenWorkerService.ts to utilize browser fingerprint. - Edited dataset.ts by adding the doiIdentifier property. - Enhanced person.ts to improve the fullName property. - Completely rewritten AsideMenuItem.vue component. - Updated CarBoxClient.vue to use TypeScript. - Added new CardBoxDataset.vue for displaying recent datasets on the dashboard. - Completely rewritten TableSampleClients.vue for the dashboard. - Completely rewritten UserAvatar.vue. - Made small layout changes in Dashboard.vue. - Added new Category.vue for browsing scientific collections. - Adapted the pinia store in main.ts. - Added additional routes in start/routes.ts and start/api/routes.ts. - Improved referenceValidation.ts for better ISBN existence checking. - NPM dependency updates.
This commit is contained in:
parent
36cd7a757b
commit
b540547e4c
34 changed files with 1757 additions and 1018 deletions
|
@ -9,12 +9,14 @@ export default class AuthorsController {
|
||||||
// where exists (select * from gba.documents inner join gba.link_documents_persons on "documents"."id" = "link_documents_persons"."document_id"
|
// where exists (select * from gba.documents inner join gba.link_documents_persons on "documents"."id" = "link_documents_persons"."document_id"
|
||||||
// where ("link_documents_persons"."role" = 'author') and ("persons"."id" = "link_documents_persons"."person_id"));
|
// where ("link_documents_persons"."role" = 'author') and ("persons"."id" = "link_documents_persons"."person_id"));
|
||||||
const authors = await Person.query()
|
const authors = await Person.query()
|
||||||
|
.where('name_type', 'Personal')
|
||||||
.whereHas('datasets', (dQuery) => {
|
.whereHas('datasets', (dQuery) => {
|
||||||
dQuery.wherePivot('role', 'author');
|
dQuery.wherePivot('role', 'author');
|
||||||
})
|
})
|
||||||
.withCount('datasets', (query) => {
|
.withCount('datasets', (query) => {
|
||||||
query.as('datasets_count');
|
query.as('datasets_count');
|
||||||
});
|
})
|
||||||
|
.orderBy('datasets_count', 'desc');
|
||||||
|
|
||||||
return authors;
|
return authors;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,104 +1,135 @@
|
||||||
import type { HttpContext } from '@adonisjs/core/http';
|
import type { HttpContext } from '@adonisjs/core/http';
|
||||||
import { StatusCodes } from 'http-status-codes';
|
import { StatusCodes } from 'http-status-codes';
|
||||||
|
import redis from '@adonisjs/redis/services/main';
|
||||||
|
|
||||||
const prefixes = ['von', 'van'];
|
const PREFIXES = ['von', 'van'];
|
||||||
|
const DEFAULT_SIZE = 50;
|
||||||
|
const FONT_SIZE_RATIO = 0.4;
|
||||||
|
const COLOR_LIGHTENING_PERCENT = 60;
|
||||||
|
const COLOR_DARKENING_FACTOR = 0.6;
|
||||||
|
|
||||||
export default class AvatarController {
|
export default class AvatarController {
|
||||||
public async generateAvatar({ request, response }: HttpContext) {
|
public async generateAvatar({ request, response }: HttpContext) {
|
||||||
try {
|
try {
|
||||||
const { name, size } = request.only(['name', 'size']);
|
const { name, size = DEFAULT_SIZE } = request.only(['name', 'size']);
|
||||||
|
if (!name) {
|
||||||
|
return response.status(StatusCodes.BAD_REQUEST).json({ error: 'Name is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a unique cache key for the given name and size
|
||||||
|
const cacheKey = `avatar:${name.trim().toLowerCase()}-${size}`;
|
||||||
|
const cachedSvg = await redis.get(cacheKey);
|
||||||
|
if (cachedSvg) {
|
||||||
|
this.setResponseHeaders(response);
|
||||||
|
return response.send(cachedSvg);
|
||||||
|
}
|
||||||
|
|
||||||
const initials = this.getInitials(name);
|
const initials = this.getInitials(name);
|
||||||
|
const colors = this.generateColors(name);
|
||||||
|
const svgContent = this.createSvg(size, colors, initials);
|
||||||
|
|
||||||
const originalColor = this.getColorFromName(name);
|
// // Cache the generated avatar for future use, e.g. 1 hour expiry
|
||||||
const backgroundColor = this.lightenColor(originalColor, 60);
|
await redis.setex(cacheKey, 3600, svgContent);
|
||||||
const textColor = this.darkenColor(originalColor);
|
|
||||||
|
|
||||||
const svgContent = `
|
this.setResponseHeaders(response);
|
||||||
<svg width="${size || 50}" height="${size || 50}" xmlns="http://www.w3.org/2000/svg">
|
return response.send(svgContent);
|
||||||
<rect width="100%" height="100%" fill="#${backgroundColor}"/>
|
} catch (error) {
|
||||||
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-weight="bold" font-family="Arial, sans-serif" font-size="${
|
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: error.message });
|
||||||
(size / 100) * 40 || 25
|
}
|
||||||
}" fill="#${textColor}">${initials}</text>
|
}
|
||||||
|
|
||||||
|
private getInitials(name: string): string {
|
||||||
|
const parts = name
|
||||||
|
.trim()
|
||||||
|
.split(' ')
|
||||||
|
.filter((part) => part.length > 0);
|
||||||
|
|
||||||
|
if (parts.length === 0) {
|
||||||
|
return 'NA';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
return this.getMultiWordInitials(parts);
|
||||||
|
}
|
||||||
|
return parts[0].substring(0, 2).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMultiWordInitials(parts: string[]): string {
|
||||||
|
const firstName = parts[0];
|
||||||
|
const lastName = parts[parts.length - 1];
|
||||||
|
const firstInitial = firstName.charAt(0).toUpperCase();
|
||||||
|
const lastInitial = lastName.charAt(0).toUpperCase();
|
||||||
|
|
||||||
|
if (PREFIXES.includes(lastName.toLowerCase()) && lastName === lastName.toUpperCase()) {
|
||||||
|
return firstInitial + lastName.charAt(1).toUpperCase();
|
||||||
|
}
|
||||||
|
return firstInitial + lastInitial;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateColors(name: string): { background: string; text: string } {
|
||||||
|
const baseColor = this.getColorFromName(name);
|
||||||
|
return {
|
||||||
|
background: this.lightenColor(baseColor, COLOR_LIGHTENING_PERCENT),
|
||||||
|
text: this.darkenColor(baseColor),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private createSvg(size: number, colors: { background: string; text: string }, initials: string): string {
|
||||||
|
const fontSize = size * FONT_SIZE_RATIO;
|
||||||
|
return `
|
||||||
|
<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="100%" height="100%" fill="#${colors.background}"/>
|
||||||
|
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-weight="bold" font-family="Arial, sans-serif" font-size="${fontSize}" fill="#${colors.text}">${initials}</text>
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setResponseHeaders(response: HttpContext['response']): void {
|
||||||
response.header('Content-type', 'image/svg+xml');
|
response.header('Content-type', 'image/svg+xml');
|
||||||
response.header('Cache-Control', 'no-cache');
|
response.header('Cache-Control', 'no-cache');
|
||||||
response.header('Pragma', 'no-cache');
|
response.header('Pragma', 'no-cache');
|
||||||
response.header('Expires', '0');
|
response.header('Expires', '0');
|
||||||
|
|
||||||
return response.send(svgContent);
|
|
||||||
} catch (error) {
|
|
||||||
return response.status(StatusCodes.OK).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getInitials(name: string) {
|
private getColorFromName(name: string): string {
|
||||||
const parts = name.split(' ');
|
|
||||||
let initials = '';
|
|
||||||
|
|
||||||
if (parts.length >= 2) {
|
|
||||||
const firstName = parts[0];
|
|
||||||
const lastName = parts[parts.length - 1];
|
|
||||||
|
|
||||||
const firstInitial = firstName.charAt(0).toUpperCase();
|
|
||||||
const lastInitial = lastName.charAt(0).toUpperCase();
|
|
||||||
|
|
||||||
if (prefixes.includes(lastName.toLowerCase()) && lastName === lastName.toUpperCase()) {
|
|
||||||
initials = firstInitial + lastName.charAt(1).toUpperCase();
|
|
||||||
} else {
|
|
||||||
initials = firstInitial + lastInitial;
|
|
||||||
}
|
|
||||||
} else if (parts.length === 1) {
|
|
||||||
initials = parts[0].substring(0, 2).toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
return initials;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getColorFromName(name: string) {
|
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
for (let i = 0; i < name.length; i++) {
|
for (let i = 0; i < name.length; i++) {
|
||||||
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
}
|
}
|
||||||
let color = '#';
|
|
||||||
|
const colorParts = [];
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
const value = (hash >> (i * 8)) & 0xff;
|
const value = (hash >> (i * 8)) & 0xff;
|
||||||
color += ('00' + value.toString(16)).substr(-2);
|
colorParts.push(value.toString(16).padStart(2, '0'));
|
||||||
}
|
}
|
||||||
return color.replace('#', '');
|
return colorParts.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
private lightenColor(hexColor: string, percent: number) {
|
private lightenColor(hexColor: string, percent: number): string {
|
||||||
let r = parseInt(hexColor.substring(0, 2), 16);
|
const r = parseInt(hexColor.substring(0, 2), 16);
|
||||||
let g = parseInt(hexColor.substring(2, 4), 16);
|
const g = parseInt(hexColor.substring(2, 4), 16);
|
||||||
let b = parseInt(hexColor.substring(4, 6), 16);
|
const b = parseInt(hexColor.substring(4, 6), 16);
|
||||||
|
|
||||||
r = Math.floor((r * (100 + percent)) / 100);
|
const lightenValue = (value: number) => Math.min(255, Math.floor((value * (100 + percent)) / 100));
|
||||||
g = Math.floor((g * (100 + percent)) / 100);
|
|
||||||
b = Math.floor((b * (100 + percent)) / 100);
|
|
||||||
|
|
||||||
r = r < 255 ? r : 255;
|
const newR = lightenValue(r);
|
||||||
g = g < 255 ? g : 255;
|
const newG = lightenValue(g);
|
||||||
b = b < 255 ? b : 255;
|
const newB = lightenValue(b);
|
||||||
|
|
||||||
const lighterHex = ((r << 16) | (g << 8) | b).toString(16);
|
return ((newR << 16) | (newG << 8) | newB).toString(16).padStart(6, '0');
|
||||||
|
|
||||||
return lighterHex.padStart(6, '0');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private darkenColor(hexColor: string) {
|
private darkenColor(hexColor: string): string {
|
||||||
const r = parseInt(hexColor.slice(0, 2), 16);
|
const r = parseInt(hexColor.slice(0, 2), 16);
|
||||||
const g = parseInt(hexColor.slice(2, 4), 16);
|
const g = parseInt(hexColor.slice(2, 4), 16);
|
||||||
const b = parseInt(hexColor.slice(4, 6), 16);
|
const b = parseInt(hexColor.slice(4, 6), 16);
|
||||||
|
|
||||||
const darkerR = Math.round(r * 0.6);
|
const darkenValue = (value: number) => Math.round(value * COLOR_DARKENING_FACTOR);
|
||||||
const darkerG = Math.round(g * 0.6);
|
|
||||||
const darkerB = Math.round(b * 0.6);
|
|
||||||
|
|
||||||
const darkerColor = ((darkerR << 16) + (darkerG << 8) + darkerB).toString(16);
|
const darkerR = darkenValue(r);
|
||||||
|
const darkerG = darkenValue(g);
|
||||||
|
const darkerB = darkenValue(b);
|
||||||
|
|
||||||
return darkerColor.padStart(6, '0');
|
return ((darkerR << 16) + (darkerG << 8) + darkerB).toString(16).padStart(6, '0');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,15 @@ import { StatusCodes } from 'http-status-codes';
|
||||||
// node ace make:controller Author
|
// node ace make:controller Author
|
||||||
export default class DatasetController {
|
export default class DatasetController {
|
||||||
public async index({}: HttpContext) {
|
public async index({}: HttpContext) {
|
||||||
// select * from gba.persons
|
// Select datasets with server_state 'published' or 'deleted' and sort by the last published date
|
||||||
// where exists (select * from gba.documents inner join gba.link_documents_persons on "documents"."id" = "link_documents_persons"."document_id"
|
const datasets = await Dataset.query()
|
||||||
// where ("link_documents_persons"."role" = 'author') and ("persons"."id" = "link_documents_persons"."person_id"));
|
.where(function (query) {
|
||||||
const datasets = await Dataset.query().where('server_state', 'published').orWhere('server_state', 'deleted');
|
query.where('server_state', 'published')
|
||||||
|
.orWhere('server_state', 'deleted');
|
||||||
|
})
|
||||||
|
.preload('titles')
|
||||||
|
.preload('identifier')
|
||||||
|
.orderBy('server_date_published', 'desc');
|
||||||
|
|
||||||
return datasets;
|
return datasets;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default class FileController {
|
||||||
// where: { id: id },
|
// where: { id: id },
|
||||||
// });
|
// });
|
||||||
if (file) {
|
if (file) {
|
||||||
const filePath = '/storage/app/public/' + file.pathName;
|
const filePath = '/storage/app/data/' + file.pathName;
|
||||||
const ext = path.extname(filePath);
|
const ext = path.extname(filePath);
|
||||||
const fileName = file.label + ext;
|
const fileName = file.label + ext;
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -9,6 +9,24 @@ import BackupCode from '#models/backup_code';
|
||||||
|
|
||||||
// Here we are generating secret and recovery codes for the user that’s enabling 2FA and storing them to our database.
|
// Here we are generating secret and recovery codes for the user that’s enabling 2FA and storing them to our database.
|
||||||
export default class UserController {
|
export default class UserController {
|
||||||
|
public async getSubmitters({ response }: HttpContext) {
|
||||||
|
try {
|
||||||
|
const submitters = await User.query()
|
||||||
|
.preload('roles', (query) => {
|
||||||
|
query.where('name', 'submitter')
|
||||||
|
})
|
||||||
|
.whereHas('roles', (query) => {
|
||||||
|
query.where('name', 'submitter')
|
||||||
|
})
|
||||||
|
.exec();
|
||||||
|
return submitters;
|
||||||
|
} catch (error) {
|
||||||
|
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
|
||||||
|
message: 'Invalid TOTP state',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async enable({ auth, response, request }: HttpContext) {
|
public async enable({ auth, response, request }: HttpContext) {
|
||||||
const user = (await User.find(auth.user?.id)) as User;
|
const user = (await User.find(auth.user?.id)) as User;
|
||||||
// await user.load('totp_secret');
|
// await user.load('totp_secret');
|
||||||
|
|
36
app/Controllers/Http/Api/collections_controller.ts
Normal file
36
app/Controllers/Http/Api/collections_controller.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import type { HttpContext } from '@adonisjs/core/http';
|
||||||
|
import Collection from '#models/collection';
|
||||||
|
|
||||||
|
export default class CollectionsController {
|
||||||
|
public async show({ params, response }: HttpContext) {
|
||||||
|
// Get the collection id from route parameters
|
||||||
|
const collectionId = params.id;
|
||||||
|
|
||||||
|
// Find the selected collection by id
|
||||||
|
const collection = await Collection.find(collectionId);
|
||||||
|
if (!collection) {
|
||||||
|
return response.status(404).json({ message: 'Collection not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query for narrower concepts: collections whose parent_id equals the selected collection's id
|
||||||
|
const narrowerCollections = await Collection.query().where('parent_id', collection.id) || [];
|
||||||
|
|
||||||
|
// For broader concept, if the selected collection has a parent_id fetch that record (otherwise null)
|
||||||
|
const broaderCollection: Collection[] | never[] | null = await (async () => {
|
||||||
|
if (collection.parent_id) {
|
||||||
|
// Try to fetch the parent...
|
||||||
|
const parent = await Collection.find(collection.parent_id)
|
||||||
|
// If found, return it wrapped in an array; if not found, return null (or empty array if you prefer)
|
||||||
|
return parent ? [parent] : null
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
})()
|
||||||
|
|
||||||
|
// Return the selected collection along with its narrower and broader concepts in JSON format
|
||||||
|
return response.json({
|
||||||
|
selectedCollection: collection,
|
||||||
|
narrowerCollections,
|
||||||
|
broaderCollection,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,14 +19,13 @@ import XmlModel from '#app/Library/XmlModel';
|
||||||
import logger from '@adonisjs/core/services/logger';
|
import logger from '@adonisjs/core/services/logger';
|
||||||
import ResumptionToken from '#app/Library/Oai/ResumptionToken';
|
import ResumptionToken from '#app/Library/Oai/ResumptionToken';
|
||||||
// import Config from '@ioc:Adonis/Core/Config';
|
// import Config from '@ioc:Adonis/Core/Config';
|
||||||
import config from '@adonisjs/core/services/config'
|
import config from '@adonisjs/core/services/config';
|
||||||
// import { inject } from '@adonisjs/fold';
|
// import { inject } from '@adonisjs/fold';
|
||||||
import { inject } from '@adonisjs/core'
|
import { inject } from '@adonisjs/core';
|
||||||
// import { TokenWorkerContract } from "MyApp/Models/TokenWorker";
|
// import { TokenWorkerContract } from "MyApp/Models/TokenWorker";
|
||||||
import TokenWorkerContract from '#library/Oai/TokenWorkerContract';
|
import TokenWorkerContract from '#library/Oai/TokenWorkerContract';
|
||||||
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
|
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
|
||||||
|
|
||||||
|
|
||||||
interface XslTParameter {
|
interface XslTParameter {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
@ -35,12 +34,14 @@ interface Dictionary {
|
||||||
[index: string]: string;
|
[index: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListParameter {
|
interface PagingParameter {
|
||||||
cursor: number;
|
cursor: number;
|
||||||
totalIds: number;
|
totalLength: number;
|
||||||
start: number;
|
start: number;
|
||||||
reldocIds: (number | null)[];
|
nextDocIds: number[];
|
||||||
|
activeWorkIds: number[];
|
||||||
metadataPrefix: string;
|
metadataPrefix: string;
|
||||||
|
queryParams: Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
@inject()
|
@inject()
|
||||||
|
@ -49,6 +50,7 @@ export default class OaiController {
|
||||||
private sampleRegEx = /^[A-Za-zäüÄÜß0-9\-_.!~]+$/;
|
private sampleRegEx = /^[A-Za-zäüÄÜß0-9\-_.!~]+$/;
|
||||||
private xsltParameter: XslTParameter;
|
private xsltParameter: XslTParameter;
|
||||||
|
|
||||||
|
private firstPublishedDataset: Dataset | null;
|
||||||
/**
|
/**
|
||||||
* Holds xml representation of document information to be processed.
|
* Holds xml representation of document information to be processed.
|
||||||
*
|
*
|
||||||
|
@ -57,7 +59,6 @@ export default class OaiController {
|
||||||
private xml: XMLBuilder;
|
private xml: XMLBuilder;
|
||||||
private proc;
|
private proc;
|
||||||
|
|
||||||
|
|
||||||
constructor(public tokenWorker: TokenWorkerContract) {
|
constructor(public tokenWorker: TokenWorkerContract) {
|
||||||
// Load the XSLT file
|
// Load the XSLT file
|
||||||
this.proc = readFileSync('public/assets2/datasetxml2oai.sef.json');
|
this.proc = readFileSync('public/assets2/datasetxml2oai.sef.json');
|
||||||
|
@ -85,9 +86,9 @@ export default class OaiController {
|
||||||
let earliestDateFromDb;
|
let earliestDateFromDb;
|
||||||
// const oaiRequest: OaiParameter = request.body;
|
// const oaiRequest: OaiParameter = request.body;
|
||||||
try {
|
try {
|
||||||
const firstPublishedDataset: Dataset | null = await Dataset.earliestPublicationDate();
|
this.firstPublishedDataset = await Dataset.earliestPublicationDate();
|
||||||
firstPublishedDataset != null &&
|
this.firstPublishedDataset != null &&
|
||||||
(earliestDateFromDb = firstPublishedDataset.server_date_published.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"));
|
(earliestDateFromDb = this.firstPublishedDataset.server_date_published.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"));
|
||||||
this.xsltParameter['earliestDatestamp'] = earliestDateFromDb;
|
this.xsltParameter['earliestDatestamp'] = earliestDateFromDb;
|
||||||
// start the request
|
// start the request
|
||||||
await this.handleRequest(oaiRequest, request);
|
await this.handleRequest(oaiRequest, request);
|
||||||
|
@ -162,22 +163,19 @@ export default class OaiController {
|
||||||
} else if (verb == 'GetRecord') {
|
} else if (verb == 'GetRecord') {
|
||||||
await this.handleGetRecord(oaiRequest);
|
await this.handleGetRecord(oaiRequest);
|
||||||
} else if (verb == 'ListRecords') {
|
} else if (verb == 'ListRecords') {
|
||||||
await this.handleListRecords(oaiRequest);
|
// Get browser fingerprint from the request:
|
||||||
|
const browserFingerprint = this.getBrowserFingerprint(request);
|
||||||
|
await this.handleListRecords(oaiRequest, browserFingerprint);
|
||||||
} else if (verb == 'ListIdentifiers') {
|
} else if (verb == 'ListIdentifiers') {
|
||||||
await this.handleListIdentifiers(oaiRequest);
|
// Get browser fingerprint from the request:
|
||||||
|
const browserFingerprint = this.getBrowserFingerprint(request);
|
||||||
|
await this.handleListIdentifiers(oaiRequest, browserFingerprint);
|
||||||
} else if (verb == 'ListSets') {
|
} else if (verb == 'ListSets') {
|
||||||
await this.handleListSets();
|
await this.handleListSets();
|
||||||
} else {
|
} else {
|
||||||
this.handleIllegalVerb();
|
this.handleIllegalVerb();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// // try {
|
|
||||||
// // console.log("Async code example.")
|
|
||||||
// const err = new PageNotFoundException("verb not found");
|
|
||||||
// throw err;
|
|
||||||
// // } catch (error) { // manually catching
|
|
||||||
// // next(error); // passing to default middleware error handler
|
|
||||||
// // }
|
|
||||||
throw new OaiModelException(
|
throw new OaiModelException(
|
||||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
'The verb provided in the request is illegal.',
|
'The verb provided in the request is illegal.',
|
||||||
|
@ -187,11 +185,11 @@ export default class OaiController {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected handleIdentify() {
|
protected handleIdentify() {
|
||||||
const email = process.env.OAI_EMAIL || 'repository@geosphere.at';
|
// Get configuration values from environment or a dedicated configuration service
|
||||||
const repositoryName = 'Tethys RDR';
|
const email = process.env.OAI_EMAIL ?? 'repository@geosphere.at';
|
||||||
const repIdentifier = 'tethys.at';
|
const repositoryName = process.env.OAI_REPOSITORY_NAME ?? 'Tethys RDR';
|
||||||
const sampleIdentifier = 'oai:' + repIdentifier + ':1'; //$this->_configuration->getSampleIdentifier();
|
const repIdentifier = process.env.OAI_REP_IDENTIFIER ?? 'tethys.at';
|
||||||
|
const sampleIdentifier = `oai:${repIdentifier}:1`;
|
||||||
// Dataset::earliestPublicationDate()->server_date_published->format('Y-m-d\TH:i:s\Z') : null;
|
// Dataset::earliestPublicationDate()->server_date_published->format('Y-m-d\TH:i:s\Z') : null;
|
||||||
// earliestDateFromDb!= null && (this.xsltParameter['earliestDatestamp'] = earliestDateFromDb?.server_date_published);
|
// earliestDateFromDb!= null && (this.xsltParameter['earliestDatestamp'] = earliestDateFromDb?.server_date_published);
|
||||||
|
|
||||||
|
@ -216,7 +214,7 @@ export default class OaiController {
|
||||||
|
|
||||||
const sets: { [key: string]: string } = {
|
const sets: { [key: string]: string } = {
|
||||||
'open_access': 'Set for open access licenses',
|
'open_access': 'Set for open access licenses',
|
||||||
'openaire_data': "OpenAIRE",
|
'openaire_data': 'OpenAIRE',
|
||||||
'doc-type:ResearchData': 'Set for document type ResearchData',
|
'doc-type:ResearchData': 'Set for document type ResearchData',
|
||||||
...(await this.getSetsForDatasetTypes()),
|
...(await this.getSetsForDatasetTypes()),
|
||||||
...(await this.getSetsForCollections()),
|
...(await this.getSetsForCollections()),
|
||||||
|
@ -234,7 +232,15 @@ export default class OaiController {
|
||||||
const repIdentifier = 'tethys.at';
|
const repIdentifier = 'tethys.at';
|
||||||
this.xsltParameter['repIdentifier'] = repIdentifier;
|
this.xsltParameter['repIdentifier'] = repIdentifier;
|
||||||
|
|
||||||
|
// Validate that required parameter exists early
|
||||||
|
if (!('identifier' in oaiRequest)) {
|
||||||
|
throw new BadOaiModelException('The prefix of the identifier argument is unknown.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and extract the dataset identifier from the request
|
||||||
const dataId = this.validateAndGetIdentifier(oaiRequest);
|
const dataId = this.validateAndGetIdentifier(oaiRequest);
|
||||||
|
|
||||||
|
// Retrieve dataset with associated XML cache and collection roles
|
||||||
const dataset = await Dataset.query()
|
const dataset = await Dataset.query()
|
||||||
.where('publish_id', dataId)
|
.where('publish_id', dataId)
|
||||||
.preload('xmlCache')
|
.preload('xmlCache')
|
||||||
|
@ -251,59 +257,61 @@ export default class OaiController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate and set the metadata prefix parameter
|
||||||
const metadataPrefix = this.validateAndGetMetadataPrefix(oaiRequest);
|
const metadataPrefix = this.validateAndGetMetadataPrefix(oaiRequest);
|
||||||
this.xsltParameter['oai_metadataPrefix'] = metadataPrefix;
|
this.xsltParameter['oai_metadataPrefix'] = metadataPrefix;
|
||||||
// do not deliver datasets which are restricted by document state defined in deliveringStates
|
|
||||||
|
// Ensure that the dataset is in an exportable state
|
||||||
this.validateDatasetState(dataset);
|
this.validateDatasetState(dataset);
|
||||||
|
|
||||||
// add xml elements
|
// Build the XML for the dataset record and add it to the root node
|
||||||
const datasetNode = this.xml.root().ele('Datasets');
|
const datasetNode = this.xml.root().ele('Datasets');
|
||||||
await this.createXmlRecord(dataset, datasetNode);
|
await this.createXmlRecord(dataset, datasetNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async handleListIdentifiers(oaiRequest: Dictionary) {
|
protected async handleListIdentifiers(oaiRequest: Dictionary, browserFingerprint: string) {
|
||||||
!this.tokenWorker.isConnected && (await this.tokenWorker.connect());
|
if (!this.tokenWorker.isConnected) {
|
||||||
|
await this.tokenWorker.connect();
|
||||||
|
}
|
||||||
|
|
||||||
const maxIdentifier: number = config.get('oai.max.listidentifiers', 100);
|
const maxIdentifier: number = config.get('oai.max.listidentifiers', 100);
|
||||||
await this.handleLists(oaiRequest, maxIdentifier);
|
await this.handleLists(oaiRequest, maxIdentifier, browserFingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async handleListRecords(oaiRequest: Dictionary) {
|
protected async handleListRecords(oaiRequest: Dictionary, browserFingerprint: string) {
|
||||||
!this.tokenWorker.isConnected && (await this.tokenWorker.connect());
|
if (!this.tokenWorker.isConnected) {
|
||||||
|
await this.tokenWorker.connect();
|
||||||
|
}
|
||||||
|
|
||||||
const maxRecords: number = config.get('oai.max.listrecords', 100);
|
const maxRecords: number = config.get('oai.max.listrecords', 100);
|
||||||
await this.handleLists(oaiRequest, maxRecords);
|
await this.handleLists(oaiRequest, maxRecords, browserFingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleLists(oaiRequest: Dictionary, maxRecords: number) {
|
private async handleLists(oaiRequest: Dictionary, maxRecords: number, browserFingerprint: string) {
|
||||||
maxRecords = maxRecords || 100;
|
|
||||||
const repIdentifier = 'tethys.at';
|
const repIdentifier = 'tethys.at';
|
||||||
this.xsltParameter['repIdentifier'] = repIdentifier;
|
this.xsltParameter['repIdentifier'] = repIdentifier;
|
||||||
const datasetNode = this.xml.root().ele('Datasets');
|
const datasetNode = this.xml.root().ele('Datasets');
|
||||||
|
|
||||||
// list initialisation
|
const paginationParams: PagingParameter ={
|
||||||
const numWrapper: ListParameter = {
|
|
||||||
cursor: 0,
|
cursor: 0,
|
||||||
totalIds: 0,
|
totalLength: 0,
|
||||||
start: maxRecords + 1,
|
start: maxRecords + 1,
|
||||||
reldocIds: [],
|
nextDocIds: [],
|
||||||
|
activeWorkIds: [],
|
||||||
metadataPrefix: '',
|
metadataPrefix: '',
|
||||||
|
queryParams: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
// resumptionToken is defined
|
|
||||||
if ('resumptionToken' in oaiRequest) {
|
if ('resumptionToken' in oaiRequest) {
|
||||||
await this.handleResumptionToken(oaiRequest, maxRecords, numWrapper);
|
await this.handleResumptionToken(oaiRequest, maxRecords, paginationParams);
|
||||||
} else {
|
} else {
|
||||||
// no resumptionToken is given
|
await this.handleNoResumptionToken(oaiRequest, paginationParams, maxRecords);
|
||||||
await this.handleNoResumptionToken(oaiRequest, numWrapper);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handling of document ids
|
const nextIds: number[] = paginationParams.nextDocIds;
|
||||||
const restIds = numWrapper.reldocIds as number[];
|
const workIds: number[] = paginationParams.activeWorkIds;
|
||||||
const workIds = restIds.splice(0, maxRecords) as number[]; // array_splice(restIds, 0, maxRecords);
|
|
||||||
|
|
||||||
// no records returned
|
if (workIds.length === 0) {
|
||||||
if (workIds.length == 0) {
|
|
||||||
throw new OaiModelException(
|
throw new OaiModelException(
|
||||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
'The combination of the given values results in an empty list.',
|
'The combination of the given values results in an empty list.',
|
||||||
|
@ -311,169 +319,218 @@ export default class OaiController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const datasets: Dataset[] = await Dataset.query()
|
const datasets = await Dataset.query()
|
||||||
.whereIn('publish_id', workIds)
|
.whereIn('publish_id', workIds)
|
||||||
.preload('xmlCache')
|
.preload('xmlCache')
|
||||||
.preload('collections', (builder) => {
|
.preload('collections', (builder) => {
|
||||||
builder.preload('collectionRole');
|
builder.preload('collectionRole');
|
||||||
})
|
})
|
||||||
.orderBy('publish_id');
|
.orderBy('publish_id');
|
||||||
|
|
||||||
for (const dataset of datasets) {
|
for (const dataset of datasets) {
|
||||||
await this.createXmlRecord(dataset, datasetNode);
|
await this.createXmlRecord(dataset, datasetNode);
|
||||||
}
|
}
|
||||||
|
await this.setResumptionToken(nextIds, paginationParams, browserFingerprint);
|
||||||
// store the further Ids in a resumption-file
|
|
||||||
const countRestIds = restIds.length; //84
|
|
||||||
if (countRestIds > 0) {
|
|
||||||
const token = new ResumptionToken();
|
|
||||||
token.startPosition = numWrapper.start; //101
|
|
||||||
token.totalIds = numWrapper.totalIds; //184
|
|
||||||
token.documentIds = restIds; //101 -184
|
|
||||||
token.metadataPrefix = numWrapper.metadataPrefix;
|
|
||||||
|
|
||||||
// $tokenWorker->storeResumptionToken($token);
|
|
||||||
const res: string = await this.tokenWorker.set(token);
|
|
||||||
|
|
||||||
// set parameters for the resumptionToken-node
|
|
||||||
// const res = token.ResumptionId;
|
|
||||||
this.setParamResumption(res, numWrapper.cursor, numWrapper.totalIds);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleResumptionToken(oaiRequest: Dictionary, maxRecords: number, numWrapper: ListParameter) {
|
private async handleNoResumptionToken(oaiRequest: Dictionary, paginationParams: PagingParameter, maxRecords: number) {
|
||||||
const resParam = oaiRequest['resumptionToken']; //e.g. "158886496600000"
|
this.validateMetadataPrefix(oaiRequest, paginationParams);
|
||||||
|
const finder: ModelQueryBuilderContract<typeof Dataset, Dataset> = Dataset.query().whereIn(
|
||||||
|
'server_state',
|
||||||
|
this.deliveringDocumentStates,
|
||||||
|
);
|
||||||
|
this.applySetFilter(finder, oaiRequest);
|
||||||
|
this.applyDateFilters(finder, oaiRequest);
|
||||||
|
await this.fetchAndSetResults(finder, paginationParams, oaiRequest, maxRecords);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchAndSetResults(
|
||||||
|
finder: ModelQueryBuilderContract<typeof Dataset, Dataset>,
|
||||||
|
paginationParams: PagingParameter,
|
||||||
|
oaiRequest: Dictionary,
|
||||||
|
maxRecords: number
|
||||||
|
) {
|
||||||
|
const totalResult = await finder
|
||||||
|
.clone()
|
||||||
|
.count('* as total')
|
||||||
|
.first()
|
||||||
|
.then((res) => res?.$extras.total);
|
||||||
|
paginationParams.totalLength = Number(totalResult);
|
||||||
|
|
||||||
|
const combinedRecords: Dataset[] = await finder.select('publish_id').orderBy('publish_id').offset(0).limit(maxRecords*2);
|
||||||
|
|
||||||
|
paginationParams.activeWorkIds = combinedRecords.slice(0, 100).map((dat) => Number(dat.publish_id));
|
||||||
|
paginationParams.nextDocIds = combinedRecords.slice(100).map((dat) => Number(dat.publish_id));
|
||||||
|
|
||||||
|
// No resumption token was used – set queryParams from the current oaiRequest
|
||||||
|
paginationParams.queryParams = {
|
||||||
|
...oaiRequest,
|
||||||
|
deliveringStates: this.deliveringDocumentStates,
|
||||||
|
};
|
||||||
|
|
||||||
|
// paginationParams.totalLength = 230;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleResumptionToken(oaiRequest: Dictionary, maxRecords: number, paginationParams: PagingParameter) {
|
||||||
|
const resParam = oaiRequest['resumptionToken'];
|
||||||
const token = await this.tokenWorker.get(resParam);
|
const token = await this.tokenWorker.get(resParam);
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new OaiModelException(StatusCodes.INTERNAL_SERVER_ERROR, 'cache is outdated.', OaiErrorCodes.BADRESUMPTIONTOKEN);
|
throw new OaiModelException(StatusCodes.INTERNAL_SERVER_ERROR, 'cache is outdated.', OaiErrorCodes.BADRESUMPTIONTOKEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
numWrapper.cursor = token.startPosition - 1; //startet dann bei Index 10
|
// this.setResumptionParameters(token, maxRecords, paginationParams);
|
||||||
numWrapper.start = token.startPosition + maxRecords;
|
paginationParams.cursor = token.startPosition - 1;
|
||||||
numWrapper.totalIds = token.totalIds;
|
paginationParams.start = token.startPosition + maxRecords;
|
||||||
numWrapper.reldocIds = token.documentIds;
|
paginationParams.totalLength = token.totalIds;
|
||||||
numWrapper.metadataPrefix = token.metadataPrefix;
|
paginationParams.activeWorkIds = token.documentIds;
|
||||||
|
paginationParams.metadataPrefix = token.metadataPrefix;
|
||||||
|
paginationParams.queryParams = token.queryParams;
|
||||||
|
this.xsltParameter['oai_metadataPrefix'] = token.metadataPrefix;
|
||||||
|
|
||||||
this.xsltParameter['oai_metadataPrefix'] = numWrapper.metadataPrefix;
|
const finder = this.buildDatasetQueryViaToken(token);
|
||||||
|
const nextRecords: Dataset[] = await this.fetchNextRecords(finder, token, maxRecords);
|
||||||
|
paginationParams.nextDocIds = nextRecords.map((dat) => Number(dat.publish_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleNoResumptionToken(oaiRequest: Dictionary, numWrapper: ListParameter) {
|
private async setResumptionToken(nextIds: number[], paginationParams: PagingParameter, browserFingerprint: string) {
|
||||||
// no resumptionToken is given
|
const countRestIds = nextIds.length;
|
||||||
if ('metadataPrefix' in oaiRequest) {
|
if (countRestIds > 0) {
|
||||||
numWrapper.metadataPrefix = oaiRequest['metadataPrefix'];
|
// const token = this.createResumptionToken(paginationParams, nextIds);
|
||||||
} else {
|
const token = new ResumptionToken();
|
||||||
|
token.startPosition = paginationParams.start;
|
||||||
|
token.totalIds = paginationParams.totalLength;
|
||||||
|
token.documentIds = nextIds;
|
||||||
|
token.metadataPrefix = paginationParams.metadataPrefix;
|
||||||
|
token.queryParams = paginationParams.queryParams;
|
||||||
|
const res: string = await this.tokenWorker.set(token, browserFingerprint);
|
||||||
|
this.setParamResumption(res, paginationParams.cursor, paginationParams.totalLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildDatasetQueryViaToken(token: ResumptionToken) {
|
||||||
|
const finder = Dataset.query();
|
||||||
|
const originalQuery = token.queryParams || {};
|
||||||
|
const deliveringStates = originalQuery.deliveringStates || this.deliveringDocumentStates;
|
||||||
|
|
||||||
|
finder.whereIn('server_state', deliveringStates);
|
||||||
|
this.applySetFilter(finder, originalQuery);
|
||||||
|
this.applyDateFilters(finder, originalQuery);
|
||||||
|
|
||||||
|
return finder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchNextRecords(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, token: ResumptionToken, maxRecords: number) {
|
||||||
|
return finder
|
||||||
|
.select('publish_id')
|
||||||
|
.orderBy('publish_id')
|
||||||
|
.offset(token.startPosition - 1 + maxRecords)
|
||||||
|
.limit(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateMetadataPrefix(oaiRequest: Dictionary, paginationParams: PagingParameter) {
|
||||||
|
if (!('metadataPrefix' in oaiRequest)) {
|
||||||
throw new OaiModelException(
|
throw new OaiModelException(
|
||||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
'The prefix of the metadata argument is unknown.',
|
'The prefix of the metadata argument is unknown.',
|
||||||
OaiErrorCodes.BADARGUMENT,
|
OaiErrorCodes.BADARGUMENT,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.xsltParameter['oai_metadataPrefix'] = numWrapper.metadataPrefix;
|
paginationParams.metadataPrefix = oaiRequest['metadataPrefix'];
|
||||||
|
this.xsltParameter['oai_metadataPrefix'] = paginationParams.metadataPrefix;
|
||||||
let finder: ModelQueryBuilderContract<typeof Dataset, Dataset> = Dataset.query();
|
|
||||||
// add server state restrictions
|
|
||||||
finder.whereIn('server_state', this.deliveringDocumentStates);
|
|
||||||
if ('set' in oaiRequest) {
|
|
||||||
const set = oaiRequest['set'] as string;
|
|
||||||
const setArray = set.split(':');
|
|
||||||
|
|
||||||
if (setArray[0] == 'data-type') {
|
|
||||||
if (setArray.length == 2 && setArray[1]) {
|
|
||||||
finder.where('type', setArray[1]);
|
|
||||||
}
|
}
|
||||||
} else if (setArray[0] == 'open_access') {
|
|
||||||
const openAccessLicences = ['CC-BY-4.0', 'CC-BY-SA-4.0'];
|
private applySetFilter(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, queryParams: any) {
|
||||||
|
if ('set' in queryParams) {
|
||||||
|
const [setType, setValue] = queryParams['set'].split(':');
|
||||||
|
|
||||||
|
switch (setType) {
|
||||||
|
case 'data-type':
|
||||||
|
setValue && finder.where('type', setValue);
|
||||||
|
break;
|
||||||
|
case 'open_access':
|
||||||
finder.andWhereHas('licenses', (query) => {
|
finder.andWhereHas('licenses', (query) => {
|
||||||
query.whereIn('name', openAccessLicences);
|
query.whereIn('name', ['CC-BY-4.0', 'CC-BY-SA-4.0']);
|
||||||
});
|
});
|
||||||
} else if (setArray[0] == 'ddc') {
|
break;
|
||||||
if (setArray.length == 2 && setArray[1] != '') {
|
case 'ddc':
|
||||||
|
setValue &&
|
||||||
finder.andWhereHas('collections', (query) => {
|
finder.andWhereHas('collections', (query) => {
|
||||||
query.where('number', setArray[1]);
|
query.where('number', setValue);
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// const timeZone = "Europe/Vienna"; // Canonical time zone name
|
private applyDateFilters(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, queryParams: any) {
|
||||||
// &from=2020-09-03&until2020-09-03
|
const { from, until } = queryParams;
|
||||||
// &from=2020-09-11&until=2021-05-11
|
|
||||||
if ('from' in oaiRequest && 'until' in oaiRequest) {
|
|
||||||
const from = oaiRequest['from'] as string;
|
|
||||||
let fromDate = dayjs(from); //.tz(timeZone);
|
|
||||||
const until = oaiRequest['until'] as string;
|
|
||||||
let untilDate = dayjs(until); //.tz(timeZone);
|
|
||||||
if (!fromDate.isValid() || !untilDate.isValid()) {
|
|
||||||
throw new OaiModelException(StatusCodes.INTERNAL_SERVER_ERROR, 'Date Parameter is not valid.', OaiErrorCodes.BADARGUMENT);
|
|
||||||
}
|
|
||||||
fromDate = dayjs.tz(from, 'Europe/Vienna');
|
|
||||||
untilDate = dayjs.tz(until, 'Europe/Vienna');
|
|
||||||
|
|
||||||
if (from.length != until.length) {
|
if (from && until) {
|
||||||
|
this.handleFromUntilFilter(finder, from, until);
|
||||||
|
} else if (from) {
|
||||||
|
this.handleFromFilter(finder, from);
|
||||||
|
} else if (until) {
|
||||||
|
this.handleUntilFilter(finder, until);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleFromUntilFilter(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, from: string, until: string) {
|
||||||
|
const fromDate = this.parseDateWithValidation(from, 'From');
|
||||||
|
const untilDate = this.parseDateWithValidation(until, 'Until');
|
||||||
|
|
||||||
|
if (from.length !== until.length) {
|
||||||
throw new OaiModelException(
|
throw new OaiModelException(
|
||||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
'The request has different granularities for the from and until parameters.',
|
'The request has different granularities for the from and until parameters.',
|
||||||
OaiErrorCodes.BADARGUMENT,
|
OaiErrorCodes.BADARGUMENT,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
fromDate.hour() == 0 && (fromDate = fromDate.startOf('day'));
|
|
||||||
untilDate.hour() == 0 && (untilDate = untilDate.endOf('day'));
|
|
||||||
|
|
||||||
finder.whereBetween('server_date_published', [fromDate.format('YYYY-MM-DD HH:mm:ss'), untilDate.format('YYYY-MM-DD HH:mm:ss')]);
|
finder.whereBetween('server_date_published', [fromDate.format('YYYY-MM-DD HH:mm:ss'), untilDate.format('YYYY-MM-DD HH:mm:ss')]);
|
||||||
} else if ('from' in oaiRequest && !('until' in oaiRequest)) {
|
|
||||||
const from = oaiRequest['from'] as string;
|
|
||||||
let fromDate = dayjs(from);
|
|
||||||
if (!fromDate.isValid()) {
|
|
||||||
throw new OaiModelException(
|
|
||||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
|
||||||
'From date parameter is not valid.',
|
|
||||||
OaiErrorCodes.BADARGUMENT,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
fromDate = dayjs.tz(from, 'Europe/Vienna');
|
|
||||||
fromDate.hour() == 0 && (fromDate = fromDate.startOf('day'));
|
|
||||||
|
|
||||||
|
private handleFromFilter(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, from: string) {
|
||||||
|
const fromDate = this.parseDateWithValidation(from, 'From');
|
||||||
const now = dayjs();
|
const now = dayjs();
|
||||||
|
|
||||||
if (fromDate.isAfter(now)) {
|
if (fromDate.isAfter(now)) {
|
||||||
throw new OaiModelException(
|
throw new OaiModelException(
|
||||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
'Given from date is greater than now. The given values results in an empty list.',
|
'Given from date is greater than now. The given values results in an empty list.',
|
||||||
OaiErrorCodes.NORECORDSMATCH,
|
OaiErrorCodes.NORECORDSMATCH,
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
|
|
||||||
finder.andWhere('server_date_published', '>=', fromDate.format('YYYY-MM-DD HH:mm:ss'));
|
finder.andWhere('server_date_published', '>=', fromDate.format('YYYY-MM-DD HH:mm:ss'));
|
||||||
}
|
}
|
||||||
} else if (!('from' in oaiRequest) && 'until' in oaiRequest) {
|
|
||||||
const until = oaiRequest['until'] as string;
|
|
||||||
let untilDate = dayjs(until);
|
|
||||||
if (!untilDate.isValid()) {
|
|
||||||
throw new OaiModelException(
|
|
||||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
|
||||||
'Until date parameter is not valid.',
|
|
||||||
OaiErrorCodes.BADARGUMENT,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
untilDate = dayjs.tz(until, 'Europe/Vienna');
|
|
||||||
untilDate.hour() == 0 && (untilDate = untilDate.endOf('day'));
|
|
||||||
|
|
||||||
const firstPublishedDataset: Dataset = (await Dataset.earliestPublicationDate()) as Dataset;
|
private handleUntilFilter(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, until: string) {
|
||||||
const earliestPublicationDate = dayjs(firstPublishedDataset.server_date_published.toISO()); //format("YYYY-MM-DDThh:mm:ss[Z]"));
|
const untilDate = this.parseDateWithValidation(until, 'Until');
|
||||||
|
|
||||||
|
const earliestPublicationDate = dayjs(this.firstPublishedDataset?.server_date_published.toISO());
|
||||||
|
|
||||||
if (earliestPublicationDate.isAfter(untilDate)) {
|
if (earliestPublicationDate.isAfter(untilDate)) {
|
||||||
throw new OaiModelException(
|
throw new OaiModelException(
|
||||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
`earliestDatestamp is greater than given until date.
|
'earliestDatestamp is greater than given until date. The given values results in an empty list.',
|
||||||
The given values results in an empty list.`,
|
|
||||||
OaiErrorCodes.NORECORDSMATCH,
|
OaiErrorCodes.NORECORDSMATCH,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
finder.andWhere('server_date_published', '<=', untilDate.format('YYYY-MM-DD HH:mm:ss'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let reldocIdsDocs = await finder.select('publish_id').orderBy('publish_id');
|
finder.andWhere('server_date_published', '<=', untilDate.format('YYYY-MM-DD HH:mm:ss'));
|
||||||
numWrapper.reldocIds = reldocIdsDocs.map((dat) => dat.publish_id);
|
}
|
||||||
numWrapper.totalIds = numWrapper.reldocIds.length; //212
|
|
||||||
|
private parseDateWithValidation(dateStr: string, label: string) {
|
||||||
|
let date = dayjs(dateStr);
|
||||||
|
if (!date.isValid()) {
|
||||||
|
throw new OaiModelException(
|
||||||
|
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
|
`${label} date parameter is not valid.`,
|
||||||
|
OaiErrorCodes.BADARGUMENT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
date = dayjs.tz(dateStr, 'Europe/Vienna');
|
||||||
|
return date.hour() === 0 ? (label === 'From' ? date.startOf('day') : date.endOf('day')) : date;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setParamResumption(res: string, cursor: number, totalIds: number) {
|
private setParamResumption(res: string, cursor: number, totalIds: number) {
|
||||||
|
@ -641,4 +698,30 @@ export default class OaiController {
|
||||||
this.xsltParameter['oai_error_code'] = 'badVerb';
|
this.xsltParameter['oai_error_code'] = 'badVerb';
|
||||||
this.xsltParameter['oai_error_message'] = 'The verb provided in the request is illegal.';
|
this.xsltParameter['oai_error_message'] = 'The verb provided in the request is illegal.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to build a browser fingerprint by combining:
|
||||||
|
* - User-Agent header,
|
||||||
|
* - the IP address,
|
||||||
|
* - Accept-Language header,
|
||||||
|
* - current timestamp rounded to the hour.
|
||||||
|
*
|
||||||
|
* Every new hour, this will return a different fingerprint.
|
||||||
|
*/
|
||||||
|
private getBrowserFingerprint(request: Request): string {
|
||||||
|
const userAgent = request.header('user-agent') || 'unknown';
|
||||||
|
// Check for X-Forwarded-For header to use the client IP from the proxy if available.
|
||||||
|
const xForwardedFor = request.header('x-forwarded-for');
|
||||||
|
let ip = request.ip();
|
||||||
|
// console.log(ip);
|
||||||
|
if (xForwardedFor) {
|
||||||
|
// X-Forwarded-For may contain a comma-separated list of IPs; the first one is the client IP.
|
||||||
|
ip = xForwardedFor.split(',')[0].trim();
|
||||||
|
// console.log('xforwardedfor ip' + ip);
|
||||||
|
}
|
||||||
|
const locale = request.header('accept-language') || 'default';
|
||||||
|
// Round the current time to the start of the hour.
|
||||||
|
const timestampHour = dayjs().startOf('hour').format('YYYY-MM-DDTHH');
|
||||||
|
return `${userAgent}-${ip}-${locale}-${timestampHour}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import Description from '#models/description';
|
||||||
import Language from '#models/language';
|
import Language from '#models/language';
|
||||||
import Coverage from '#models/coverage';
|
import Coverage from '#models/coverage';
|
||||||
import Collection from '#models/collection';
|
import Collection from '#models/collection';
|
||||||
|
import CollectionRole from '#models/collection_role';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import Person from '#models/person';
|
import Person from '#models/person';
|
||||||
import db from '@adonisjs/lucid/services/db';
|
import db from '@adonisjs/lucid/services/db';
|
||||||
|
@ -501,7 +502,7 @@ export default class DatasetController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// save collection
|
// save collection
|
||||||
const collection: Collection | null = await Collection.query().where('id', 21).first();
|
const collection: Collection | null = await Collection.query().where('id', 594).first();
|
||||||
collection && (await dataset.useTransaction(trx).related('collections').attach([collection.id]));
|
collection && (await dataset.useTransaction(trx).related('collections').attach([collection.id]));
|
||||||
|
|
||||||
// save coverage
|
// save coverage
|
||||||
|
@ -1217,4 +1218,34 @@ export default class DatasetController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = ['inprogress', 'rejected_editor'];
|
||||||
|
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('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('Submitter/Dataset/Category', {
|
||||||
|
collectionRoles: collectionRoles,
|
||||||
|
dataset: dataset,
|
||||||
|
relatedCollections: dataset.collections,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ export default class ResumptionToken {
|
||||||
private _resumptionId = '';
|
private _resumptionId = '';
|
||||||
private _startPosition = 0;
|
private _startPosition = 0;
|
||||||
private _totalIds = 0;
|
private _totalIds = 0;
|
||||||
|
private _queryParams: Record<string, any> = {};
|
||||||
|
|
||||||
get key(): string {
|
get key(): string {
|
||||||
return this.metadataPrefix + this.startPosition + this.totalIds;
|
return this.metadataPrefix + this.startPosition + this.totalIds;
|
||||||
|
@ -48,4 +49,12 @@ export default class ResumptionToken {
|
||||||
set totalIds(totalIds: number) {
|
set totalIds(totalIds: number) {
|
||||||
this._totalIds = totalIds;
|
this._totalIds = totalIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get queryParams(): Record<string, any> {
|
||||||
|
return this._queryParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
set queryParams(params: Record<string, any>) {
|
||||||
|
this._queryParams = params;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,6 @@ export default abstract class TokenWorkerContract {
|
||||||
abstract connect(): void;
|
abstract connect(): void;
|
||||||
abstract close(): void;
|
abstract close(): void;
|
||||||
abstract get(key: string): Promise<ResumptionToken | null>;
|
abstract get(key: string): Promise<ResumptionToken | null>;
|
||||||
abstract set(token: ResumptionToken): Promise<string>;
|
abstract set(token: ResumptionToken, browserFingerprint: string): Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,14 +40,64 @@ export default class TokenWorkerService implements TokenWorkerContract {
|
||||||
return result !== undefined && result !== null;
|
return result !== undefined && result !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async set(token: ResumptionToken): Promise<string> {
|
/**
|
||||||
const uniqueName = await this.generateUniqueName();
|
* Simplified set method that stores the token using a browser fingerprint key.
|
||||||
|
* If the token for that fingerprint already exists and its documentIds match the new token,
|
||||||
|
* then the fingerprint key is simply returned.
|
||||||
|
*/
|
||||||
|
public async set(token: ResumptionToken, browserFingerprint: string): Promise<string> {
|
||||||
|
// Generate a 15-digit unique number string based on the fingerprint
|
||||||
|
const uniqueNumberKey = this.createUniqueNumberFromFingerprint(browserFingerprint, token.documentIds, token.totalIds);
|
||||||
|
// Optionally, you could prefix it if desired, e.g. 'rs_' + uniqueNumberKey
|
||||||
|
const fingerprintKey = uniqueNumberKey;
|
||||||
|
|
||||||
|
// const fingerprintKey = `rs_fp_${browserFingerprint}`;
|
||||||
|
const existingTokenString = await this.cache.get(fingerprintKey);
|
||||||
|
|
||||||
|
if (existingTokenString) {
|
||||||
|
const existingToken = this.parseToken(existingTokenString);
|
||||||
|
if (this.arraysAreEqual(existingToken.documentIds, token.documentIds)) {
|
||||||
|
return fingerprintKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const serialToken = JSON.stringify(token);
|
const serialToken = JSON.stringify(token);
|
||||||
await this.cache.setEx(uniqueName, this.ttl, serialToken);
|
await this.cache.setEx(fingerprintKey, this.ttl, serialToken);
|
||||||
return uniqueName;
|
return fingerprintKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updated helper method to generate a unique key based on fingerprint and documentIds
|
||||||
|
private createUniqueNumberFromFingerprint(browserFingerprint: string, documentIds: number[], totalIds: number): string {
|
||||||
|
// Combine the fingerprint, document IDs and totalIds to produce the input string
|
||||||
|
const combined = browserFingerprint + ':' + documentIds.join('-') + ':' + totalIds;
|
||||||
|
// Simple hash algorithm
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < combined.length; i++) {
|
||||||
|
hash = (hash << 5) - hash + combined.charCodeAt(i);
|
||||||
|
hash |= 0; // Convert to 32-bit integer
|
||||||
|
}
|
||||||
|
// Ensure positive number and limit it to at most 15 digits
|
||||||
|
const positiveHash = Math.abs(hash) % 1000000000000000;
|
||||||
|
// Pad with trailing zeros to ensure a 15-digit string
|
||||||
|
return positiveHash.toString().padEnd(15, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a helper function to compare two arrays of numbers with identical order
|
||||||
|
private arraysAreEqual(arr1: number[], arr2: number[]): boolean {
|
||||||
|
if (arr1.length !== arr2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return arr1.every((num, index) => num === arr2[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// public async set(token: ResumptionToken): Promise<string> {
|
||||||
|
// const uniqueName = await this.generateUniqueName();
|
||||||
|
|
||||||
|
// const serialToken = JSON.stringify(token);
|
||||||
|
// await this.cache.setEx(uniqueName, this.ttl, serialToken);
|
||||||
|
// return uniqueName;
|
||||||
|
// }
|
||||||
|
|
||||||
private async generateUniqueName(): Promise<string> {
|
private async generateUniqueName(): Promise<string> {
|
||||||
let fc = 0;
|
let fc = 0;
|
||||||
const uniqueId = dayjs().unix().toString();
|
const uniqueId = dayjs().unix().toString();
|
||||||
|
|
|
@ -209,6 +209,15 @@ export default class Dataset extends DatasetExtension {
|
||||||
return mainTitle ? mainTitle.value : null;
|
return mainTitle ? mainTitle.value : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed({
|
||||||
|
serializeAs: 'doi_identifier',
|
||||||
|
})
|
||||||
|
public get doiIdentifier() {
|
||||||
|
// return `${this.firstName} ${this.lastName}`;
|
||||||
|
const identifier: DatasetIdentifier = this.identifier;
|
||||||
|
return identifier ? identifier.value : null;
|
||||||
|
}
|
||||||
|
|
||||||
@manyToMany(() => Person, {
|
@manyToMany(() => Person, {
|
||||||
pivotForeignKey: 'document_id',
|
pivotForeignKey: 'document_id',
|
||||||
pivotRelatedForeignKey: 'person_id',
|
pivotRelatedForeignKey: 'person_id',
|
||||||
|
|
|
@ -51,7 +51,7 @@ export default class Person extends BaseModel {
|
||||||
serializeAs: 'name',
|
serializeAs: 'name',
|
||||||
})
|
})
|
||||||
public get fullName() {
|
public get fullName() {
|
||||||
return `${this.firstName} ${this.lastName}`;
|
return [this.firstName, this.lastName].filter(Boolean).join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
// @computed()
|
// @computed()
|
||||||
|
@ -64,10 +64,13 @@ export default class Person extends BaseModel {
|
||||||
// return '2023-03-21 08:45:00';
|
// return '2023-03-21 08:45:00';
|
||||||
// }
|
// }
|
||||||
|
|
||||||
@computed()
|
|
||||||
|
@computed({
|
||||||
|
serializeAs: 'dataset_count',
|
||||||
|
})
|
||||||
public get datasetCount() {
|
public get datasetCount() {
|
||||||
const stock = this.$extras.datasets_count; //my pivot column name was "stock"
|
const stock = this.$extras.datasets_count; //my pivot column name was "stock"
|
||||||
return stock;
|
return Number(stock);
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed()
|
@computed()
|
||||||
|
|
|
@ -88,7 +88,7 @@ export default class ValidateChecksum extends BaseCommand {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Construct the file path
|
// Construct the file path
|
||||||
const filePath = '/storage/app/public/' + file.pathName;
|
const filePath = '/storage/app/data/' + file.pathName;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Calculate the MD5 checksum of the file
|
// Calculate the MD5 checksum of the file
|
||||||
|
|
|
@ -80,7 +80,8 @@ export const http = defineConfig({
|
||||||
| headers.
|
| headers.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
trustProxy: proxyAddr.compile('loopback'),
|
// trustProxy: proxyAddr.compile('loopback'),
|
||||||
|
trustProxy: proxyAddr.compile(['127.0.0.1', '::1/128']),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
@ -18,6 +18,7 @@ export default class Accounts extends BaseSchema {
|
||||||
table.text("two_factor_recovery_codes").nullable();
|
table.text("two_factor_recovery_codes").nullable();
|
||||||
table.smallint('state').nullable();
|
table.smallint('state').nullable();
|
||||||
table.bigint('last_counter').nullable();
|
table.bigint('last_counter').nullable();
|
||||||
|
table.string('avatar').nullable();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +44,7 @@ export default class Accounts extends BaseSchema {
|
||||||
// two_factor_recovery_codes text COLLATE pg_catalog."default",
|
// two_factor_recovery_codes text COLLATE pg_catalog."default",
|
||||||
// state smallint,
|
// state smallint,
|
||||||
// last_counter bigint,
|
// last_counter bigint,
|
||||||
|
// avatar character varying(255),
|
||||||
// )
|
// )
|
||||||
|
|
||||||
// ALTER TABLE gba.accounts
|
// ALTER TABLE gba.accounts
|
||||||
|
@ -85,3 +87,6 @@ export default class Accounts extends BaseSchema {
|
||||||
// GRANT ALL ON SEQUENCE gba.totp_secrets_id_seq TO tethys_admin;
|
// GRANT ALL ON SEQUENCE gba.totp_secrets_id_seq TO tethys_admin;
|
||||||
|
|
||||||
// ALTER TABLE gba.totp_secrets ALTER COLUMN id SET DEFAULT nextval('gba.totp_secrets_id_seq');
|
// ALTER TABLE gba.totp_secrets ALTER COLUMN id SET DEFAULT nextval('gba.totp_secrets_id_seq');
|
||||||
|
|
||||||
|
|
||||||
|
// ALTER TABLE "accounts" ADD COLUMN "avatar" VARCHAR(255) NULL
|
||||||
|
|
|
@ -54,3 +54,8 @@ export default class Collections extends BaseSchema {
|
||||||
// ON UPDATE CASCADE
|
// ON UPDATE CASCADE
|
||||||
// ON DELETE CASCADE
|
// ON DELETE CASCADE
|
||||||
// )
|
// )
|
||||||
|
|
||||||
|
|
||||||
|
// change to normal intzeger:
|
||||||
|
// ALTER TABLE collections ALTER COLUMN id DROP DEFAULT;
|
||||||
|
// DROP SEQUENCE IF EXISTS collections_id_seq;
|
||||||
|
|
643
package-lock.json
generated
643
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,161 +1,142 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ComputedRef } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { Link, usePage } from '@inertiajs/vue3';
|
import { Link, usePage } from '@inertiajs/vue3';
|
||||||
// import { Link } from '@inertiajs/inertia-vue3';
|
|
||||||
|
|
||||||
import { StyleService } from '@/Stores/style.service';
|
import { StyleService } from '@/Stores/style.service';
|
||||||
import { mdiMinus, mdiPlus } from '@mdi/js';
|
import { mdiMinus, mdiPlus } from '@mdi/js';
|
||||||
import { getButtonColor } from '@/colors';
|
import { getButtonColor } from '@/colors';
|
||||||
import BaseIcon from '@/Components/BaseIcon.vue';
|
import BaseIcon from '@/Components/BaseIcon.vue';
|
||||||
// import AsideMenuList from '@/Components/AsideMenuList.vue';
|
|
||||||
import { stardust } from '@eidellev/adonis-stardust/client';
|
import { stardust } from '@eidellev/adonis-stardust/client';
|
||||||
import type { User } from '@/Dataset';
|
import type { User } from '@/Dataset';
|
||||||
|
import { MenuItem } from '@headlessui/vue';
|
||||||
|
|
||||||
const props = defineProps({
|
interface MenuItem {
|
||||||
item: {
|
href?: string;
|
||||||
type: Object,
|
route?: string;
|
||||||
required: true,
|
icon?: string;
|
||||||
},
|
label: string;
|
||||||
parentItem: {
|
target?: string;
|
||||||
type: Object,
|
color?: string;
|
||||||
required: false,
|
children?: MenuItem[];
|
||||||
},
|
isOpen?: boolean;
|
||||||
// isDropdownList: Boolean,
|
roles?: string[];
|
||||||
});
|
}
|
||||||
|
|
||||||
const user: ComputedRef<User> = computed(() => {
|
const props = defineProps<{
|
||||||
return usePage().props.authUser as User;
|
item: MenuItem;
|
||||||
|
parentItem?: MenuItem;
|
||||||
|
// isDropdownList?: boolean;
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'menu-click', event: Event, item: MenuItem): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// Retrieve authenticated user from page props
|
||||||
|
const user = computed<User>(() => usePage().props.authUser as User);
|
||||||
|
|
||||||
|
// Check if the menu item has children
|
||||||
|
const hasChildren = computed(() => {
|
||||||
|
return Array.isArray(props.item?.children) && props.item.children.length > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const itemRoute = computed(() => (props.item && props.item.route ? stardust.route(props.item.route) : ''));
|
const itemRoute = computed(() => (props.item && props.item.route ? stardust.route(props.item.route) : ''));
|
||||||
// const isCurrentRoute = computed(() => (props.item && props.item.route ? stardust.isCurrent(props.item.route): false));
|
// const isCurrentRoute = computed(() => (props.item && props.item.route ? stardust.isCurrent(props.item.route): false));
|
||||||
// const itemHref = computed(() => (props.item && props.item.href ? props.item.href : ''));
|
|
||||||
|
|
||||||
const emit = defineEmits(['menu-click']);
|
// Determine which element to render based on 'href' or 'route'
|
||||||
|
const isComponent = computed(() => {
|
||||||
|
if (props.item.href) {
|
||||||
|
return 'a';
|
||||||
|
}
|
||||||
|
if (props.item.route) {
|
||||||
|
return Link;
|
||||||
|
}
|
||||||
|
return 'div';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if any child route is active
|
||||||
|
const isChildActive = computed(() => {
|
||||||
|
if (props.item.children && props.item.children.length > 0) {
|
||||||
|
return props.item.children.some(child => child.route && stardust.isCurrent(child.route));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Automatically use prop item.isOpen if set from the parent,
|
||||||
|
// or if one of its children is active then force open state.
|
||||||
|
const isOpen = computed(() => {
|
||||||
|
return props.item.isOpen || isChildActive.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const styleService = StyleService();
|
const styleService = StyleService();
|
||||||
|
|
||||||
const hasColor = computed(() => props.item && props.item.color);
|
const hasColor = computed(() => props.item && props.item.color);
|
||||||
|
|
||||||
// const isDropdownOpen = ref(false);
|
|
||||||
|
|
||||||
// const isChildSelected = computed(() => {
|
|
||||||
// if (props.item.children && props.item.children.length > 0) {
|
// const children = computed(() => {
|
||||||
// return children.value.some(childItem => stardust.isCurrent(childItem.route));
|
// return props.item.children || [];
|
||||||
// }
|
|
||||||
// return false;
|
|
||||||
// });
|
// });
|
||||||
|
|
||||||
|
|
||||||
const hasChildren = computed(() => {
|
|
||||||
// props.item.children?.length > 0
|
|
||||||
if (props.item.children && props.item.children.length > 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
const children = computed(() => {
|
|
||||||
return props.item.children || [];
|
|
||||||
});
|
|
||||||
|
|
||||||
const componentClass = computed(() => [
|
const componentClass = computed(() => [
|
||||||
hasChildren ? 'py-3 px-6 text-sm font-semibold' : 'py-3 px-6',
|
hasChildren ? 'py-3 px-6 text-sm font-semibold' : 'py-3 px-6',
|
||||||
hasColor.value ? getButtonColor(props.item.color, false, true) : styleService.asideMenuItemStyle,
|
hasColor.value ? getButtonColor(props.item.color, false, true) : styleService.asideMenuItemStyle,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
const menuClick = (event: Event) => {
|
||||||
// const toggleDropdown = () => {
|
|
||||||
// // emit('menu-click', event, props.item);
|
|
||||||
// // console.log(props.item);
|
|
||||||
// if (hasChildren.value) {
|
|
||||||
// isDropdownOpen.value = !isDropdownOpen.value;
|
|
||||||
// }
|
|
||||||
// // if (props.parentItem?.hasDropdown.value) {
|
|
||||||
// // props.parentItem.isDropdownActive.value = true;
|
|
||||||
// // }
|
|
||||||
// };
|
|
||||||
|
|
||||||
const menuClick = (event) => {
|
|
||||||
emit('menu-click', event, props.item);
|
emit('menu-click', event, props.item);
|
||||||
|
|
||||||
if (hasChildren.value) {
|
if (hasChildren.value) {
|
||||||
// if (isChildSelected.value == false) {
|
// Toggle open state if the menu has children
|
||||||
// isDropdownOpen.value = !isDropdownOpen.value;
|
|
||||||
props.item.isOpen = !props.item.isOpen;
|
props.item.isOpen = !props.item.isOpen;
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// const handleChildSelected = () => {
|
const activeStyle = computed(() => {
|
||||||
// isChildSelected.value = true;
|
|
||||||
// };
|
|
||||||
|
|
||||||
|
|
||||||
const activeInactiveStyle = computed(() => {
|
|
||||||
if (props.item.route && stardust.isCurrent(props.item.route)) {
|
if (props.item.route && stardust.isCurrent(props.item.route)) {
|
||||||
// console.log(props.item.route);
|
// console.log(props.item.route);
|
||||||
return styleService.asideMenuItemActiveStyle;
|
return 'text-sky-600 font-bold';
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const is = computed(() => {
|
|
||||||
if (props.item.href) {
|
|
||||||
return 'a';
|
|
||||||
}
|
|
||||||
if (props.item.route) {
|
|
||||||
return Link;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'div';
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasRoles = computed(() => {
|
const hasRoles = computed(() => {
|
||||||
if (props.item.roles) {
|
if (props.item.roles) {
|
||||||
return user.value.roles.some(role => props.item.roles.includes(role.name));
|
return user.value.roles.some(role => props.item.roles?.includes(role.name));
|
||||||
// return test;
|
// return test;
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
});
|
});
|
||||||
|
|
||||||
// props.routeName && stardust.isCurrent(props.routeName) ? props.activeColor : null
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- :target="props.item.target ?? null" -->
|
<!-- :target="props.item.target ?? null" -->
|
||||||
<template>
|
<template>
|
||||||
<li v-if="hasRoles">
|
<li v-if="hasRoles">
|
||||||
<!-- <component :is="itemHref ? 'div' : Link" :href="itemHref ? itemHref : itemRoute" -->
|
<component :is="isComponent" :href="props.item.href ? props.item.href : itemRoute"
|
||||||
<component :is="is" :href="itemRoute ? stardust.route(props.item.route) : props.item.href"
|
class="flex cursor-pointer dark:text-slate-300 dark:hover:text-white menu-item-wrapper"
|
||||||
class="flex cursor-pointer dark:text-slate-300 dark:hover:text-white menu-item-wrapper" :class="componentClass"
|
:class="componentClass" @click="menuClick" :target="props.item.target || null">
|
||||||
@click="menuClick" v-bind:target="props.item.target ?? null">
|
<BaseIcon v-if="props.item.icon" :path="props.item.icon" class="flex-none menu-item-icon"
|
||||||
<BaseIcon v-if="item.icon" :path="item.icon" class="flex-none menu-item-icon" :class="activeInactiveStyle"
|
:class="activeStyle" w="w-16" :size="18" />
|
||||||
w="w-16" :size="18" />
|
|
||||||
<div class="menu-item-label">
|
<div class="menu-item-label">
|
||||||
<span class="grow text-ellipsis line-clamp-1" :class="activeInactiveStyle">
|
<span class="grow text-ellipsis line-clamp-1" :class="[activeStyle]">
|
||||||
{{ item.label }}
|
{{ props.item.label }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- plus icon for expanding sub menu -->
|
<!-- Display plus or minus icon if there are child items -->
|
||||||
<BaseIcon v-if="hasChildren" :path="props.item.isOpen ? mdiMinus : mdiPlus" class="flex-none"
|
<BaseIcon v-if="hasChildren" :path="isOpen ? mdiMinus : mdiPlus" class="flex-none"
|
||||||
:class="[activeInactiveStyle]" w="w-12" />
|
:class="[activeStyle]" w="w-12" />
|
||||||
</component>
|
</component>
|
||||||
|
<!-- Render dropdown -->
|
||||||
<div class="menu-item-dropdown"
|
<div class="menu-item-dropdown"
|
||||||
:class="[styleService.asideMenuDropdownStyle, props.item.isOpen ? 'block dark:bg-slate-800/50' : 'hidden']"
|
:class="[styleService.asideMenuDropdownStyle, isOpen ? 'block dark:bg-slate-800/50' : 'hidden']"
|
||||||
v-if="hasChildren">
|
v-if="props.item.children && props.item.children.length > 0">
|
||||||
<ul>
|
<ul>
|
||||||
<!-- <li v-for="( child, index ) in children " :key="index">
|
|
||||||
<AsideMenuItem :item="child" :key="index"> </AsideMenuItem>
|
<AsideMenuItem v-for="(childItem, index) in (props.item.children as any[])" :key="index" :item="childItem"
|
||||||
</li> -->
|
@menu-click="$emit('menu-click', $event, childItem)" />
|
||||||
<AsideMenuItem v-for="(childItem, index) in children" :key="index" :item="childItem" />
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<!-- <AsideMenuList v-if="hasChildren" :items="item.children"
|
|
||||||
:class="[styleService.asideMenuDropdownStyle, isDropdownOpen ? 'block dark:bg-slate-800/50' : 'hidden']" /> -->
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -171,11 +152,6 @@ const hasRoles = computed(() => {
|
||||||
/* margin-right: 10px; */
|
/* margin-right: 10px; */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .menu-item-label {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
} */
|
|
||||||
|
|
||||||
.menu-item-dropdown {
|
.menu-item-dropdown {
|
||||||
/* margin-left: 10px; */
|
/* margin-left: 10px; */
|
||||||
padding-left: 0.75rem;
|
padding-left: 0.75rem;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { mdiTrendingDown, mdiTrendingUp, mdiTrendingNeutral } from '@mdi/js';
|
// import { mdiTrendingDown, mdiTrendingUp, mdiTrendingNeutral } from '@mdi/js';
|
||||||
import CardBox from '@/Components/CardBox.vue';
|
import CardBox from '@/Components/CardBox.vue';
|
||||||
import BaseLevel from '@/Components/BaseLevel.vue';
|
import BaseLevel from '@/Components/BaseLevel.vue';
|
||||||
import PillTag from '@/Components/PillTag.vue';
|
import PillTag from '@/Components/PillTag.vue';
|
||||||
|
@ -27,6 +27,10 @@ const props = defineProps({
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
|
count: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
text: {
|
text: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
|
@ -42,11 +46,11 @@ const pillType = computed(() => {
|
||||||
return props.type;
|
return props.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.progress) {
|
if (props.count) {
|
||||||
if (props.progress >= 60) {
|
if (props.count >= 20) {
|
||||||
return 'success';
|
return 'success';
|
||||||
}
|
}
|
||||||
if (props.progress >= 40) {
|
if (props.count >= 5) {
|
||||||
return 'warning';
|
return 'warning';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,17 +60,17 @@ const pillType = computed(() => {
|
||||||
return 'info';
|
return 'info';
|
||||||
});
|
});
|
||||||
|
|
||||||
const pillIcon = computed(() => {
|
// const pillIcon = computed(() => {
|
||||||
return {
|
// return {
|
||||||
success: mdiTrendingUp,
|
// success: mdiTrendingUp,
|
||||||
warning: mdiTrendingNeutral,
|
// warning: mdiTrendingNeutral,
|
||||||
danger: mdiTrendingDown,
|
// danger: mdiTrendingDown,
|
||||||
info: mdiTrendingNeutral,
|
// info: mdiTrendingNeutral,
|
||||||
}[pillType.value];
|
// }[pillType.value];
|
||||||
});
|
// });
|
||||||
|
|
||||||
const pillText = computed(() => props.text ?? `${props.progress}%`);
|
// const pillText = computed(() => props.text ?? `${props.progress}%`);
|
||||||
</script>
|
// </script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CardBox class="mb-6 last:mb-0" hoverable>
|
<CardBox class="mb-6 last:mb-0" hoverable>
|
||||||
|
@ -83,7 +87,17 @@ const pillText = computed(() => props.text ?? `${props.progress}%`);
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</BaseLevel>
|
</BaseLevel>
|
||||||
<PillTag :type="pillType" :text="pillText" small :icon="pillIcon" />
|
<!-- <PillTag :type="pillType" :text="text" small :icon="pillIcon" /> -->
|
||||||
|
|
||||||
|
<div class="text-center md:text-right space-y-2">
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Count
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<PillTag :type="pillType" :text="String(count)" small />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</BaseLevel>
|
</BaseLevel>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</template>
|
</template>
|
||||||
|
|
107
resources/js/Components/CardBoxDataset.vue
Normal file
107
resources/js/Components/CardBoxDataset.vue
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, PropType } from 'vue';
|
||||||
|
import { mdiChartTimelineVariant, mdiFileDocumentOutline, mdiFileOutline, mdiDatabase } from '@mdi/js';
|
||||||
|
import CardBox from '@/Components/CardBox.vue';
|
||||||
|
import PillTag from '@/Components/PillTag.vue';
|
||||||
|
import IconRounded from '@/Components/IconRounded.vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
|
||||||
|
// Extend dayjs to support relative times
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
interface Dataset {
|
||||||
|
account_id: number;
|
||||||
|
created_at: string;
|
||||||
|
creating_corporation: string;
|
||||||
|
editor_id: number;
|
||||||
|
embargo_date: string | null;
|
||||||
|
id: number;
|
||||||
|
language: string;
|
||||||
|
main_abstract: string | null;
|
||||||
|
main_title: string | null;
|
||||||
|
preferred_reviewer: string | null;
|
||||||
|
preferred_reviewer_email: string | null;
|
||||||
|
project_id: number | null;
|
||||||
|
publish_id: number;
|
||||||
|
publisher_name: string;
|
||||||
|
reject_editor_note: string | null;
|
||||||
|
reject_reviewer_note: string | null;
|
||||||
|
remaining_time: number;
|
||||||
|
reviewer_id: number;
|
||||||
|
server_date_modified: string;
|
||||||
|
server_date_published: string;
|
||||||
|
server_state: string;
|
||||||
|
type: string;
|
||||||
|
doi_identifier: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
dataset: {
|
||||||
|
type: Object as PropType<Dataset>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const icon = computed(() => {
|
||||||
|
switch (props.dataset.type) {
|
||||||
|
case 'analysisdata':
|
||||||
|
return { icon: mdiChartTimelineVariant, type: 'success' };
|
||||||
|
case 'measurementdata':
|
||||||
|
return { icon: mdiFileDocumentOutline, type: 'warning' };
|
||||||
|
case 'monitoring':
|
||||||
|
return { icon: mdiFileOutline, type: 'info' };
|
||||||
|
case 'remotesensing':
|
||||||
|
return { icon: mdiDatabase, type: 'primary' };
|
||||||
|
case 'gis':
|
||||||
|
return { icon: mdiDatabase, type: 'info' };
|
||||||
|
case 'models':
|
||||||
|
return { icon: mdiChartTimelineVariant, type: 'success' };
|
||||||
|
case 'mixedtype':
|
||||||
|
return { icon: mdiFileDocumentOutline, type: 'warning' };
|
||||||
|
case 'vocabulary':
|
||||||
|
return { icon: mdiFileOutline, type: 'info' };
|
||||||
|
default:
|
||||||
|
return { icon: mdiDatabase, type: 'secondary' };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayTitle = computed(() => props.dataset.main_title || 'Untitled Dataset');
|
||||||
|
|
||||||
|
const doiLink = computed(() => {
|
||||||
|
return `https://doi.tethys.at/10.24341/tethys.${props.dataset.publish_id}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const relativeDate = computed(() => {
|
||||||
|
const publishedDate = dayjs(props.dataset.server_date_published);
|
||||||
|
if (publishedDate.isValid()) {
|
||||||
|
return publishedDate.fromNow();
|
||||||
|
}
|
||||||
|
return props.dataset.server_date_published;
|
||||||
|
});
|
||||||
|
|
||||||
|
// const displayBusiness = computed(() => props.dataset.publisher_name);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CardBox class="mb-6 last:mb-0" hoverable>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<IconRounded :icon="icon.icon" :type="icon.type" class="mr-6" />
|
||||||
|
<div class="flex-grow space-y-1 text-left" style="width: 70%;">
|
||||||
|
<h4 class="text-lg truncate" >
|
||||||
|
{{ displayTitle }}
|
||||||
|
</h4>
|
||||||
|
<p class="text-gray-500 dark:text-slate-400">
|
||||||
|
<b>
|
||||||
|
<a :href="doiLink" target="_blank">View Publication</a>
|
||||||
|
</b>
|
||||||
|
• {{ relativeDate }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-end gap-2">
|
||||||
|
<p class="text-sm text-gray-500">{{ props.dataset.type }}</p>
|
||||||
|
<PillTag :type="icon.type" :text="props.dataset.type" small class="inline-flex" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</template>
|
|
@ -1,14 +1,16 @@
|
||||||
<script setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, Ref } from 'vue';
|
||||||
import { MainService } from '@/Stores/main';
|
import { MainService } from '@/Stores/main';
|
||||||
import { StyleService } from '@/Stores/style.service';
|
import { StyleService } from '@/Stores/style.service';
|
||||||
import { mdiEye, mdiTrashCan } from '@mdi/js';
|
import { mdiEye } from '@mdi/js';
|
||||||
import CardBoxModal from '@/Components/CardBoxModal.vue';
|
import CardBoxModal from '@/Components/CardBoxModal.vue';
|
||||||
import TableCheckboxCell from '@/Components/TableCheckboxCell.vue';
|
import TableCheckboxCell from '@/Components/TableCheckboxCell.vue';
|
||||||
import BaseLevel from '@/Components/BaseLevel.vue';
|
import BaseLevel from '@/Components/BaseLevel.vue';
|
||||||
import BaseButtons from '@/Components/BaseButtons.vue';
|
import BaseButtons from '@/Components/BaseButtons.vue';
|
||||||
import BaseButton from '@/Components/BaseButton.vue';
|
import BaseButton from '@/Components/BaseButton.vue';
|
||||||
import UserAvatar from '@/Components/UserAvatar.vue';
|
import UserAvatar from '@/Components/UserAvatar.vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { User } from '@/Stores/main';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
checkable: Boolean,
|
checkable: Boolean,
|
||||||
|
@ -19,36 +21,32 @@ const mainService = MainService();
|
||||||
const items = computed(() => mainService.clients);
|
const items = computed(() => mainService.clients);
|
||||||
|
|
||||||
const isModalActive = ref(false);
|
const isModalActive = ref(false);
|
||||||
const isModalDangerActive = ref(false);
|
// const isModalDangerActive = ref(false);
|
||||||
const perPage = ref(5);
|
const perPage = ref(5);
|
||||||
const currentPage = ref(0);
|
const currentPage = ref(0);
|
||||||
const checkedRows = ref([]);
|
const checkedRows = ref([]);
|
||||||
|
const currentClient: Ref<User | null> = ref(null);
|
||||||
|
|
||||||
const itemsPaginated = computed(() => items.value.slice(perPage.value * currentPage.value, perPage.value * (currentPage.value + 1)));
|
const itemsPaginated = computed(() => items.value.slice(perPage.value * currentPage.value, perPage.value * (currentPage.value + 1)));
|
||||||
|
|
||||||
const numPages = computed(() => Math.ceil(items.value.length / perPage.value));
|
const numPages = computed(() => Math.ceil(items.value.length / perPage.value));
|
||||||
|
|
||||||
const currentPageHuman = computed(() => currentPage.value + 1);
|
const currentPageHuman = computed(() => currentPage.value + 1);
|
||||||
|
|
||||||
const pagesList = computed(() => {
|
const pagesList = computed(() => {
|
||||||
const pagesList = [];
|
const pagesList = [];
|
||||||
|
|
||||||
for (let i = 0; i < numPages.value; i++) {
|
for (let i = 0; i < numPages.value; i++) {
|
||||||
pagesList.push(i);
|
pagesList.push(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
return pagesList;
|
return pagesList;
|
||||||
});
|
});
|
||||||
|
|
||||||
const remove = (arr, cb) => {
|
const remove = (arr, cb) => {
|
||||||
const newArr = [];
|
const newArr = [];
|
||||||
|
|
||||||
arr.forEach((item) => {
|
arr.forEach((item) => {
|
||||||
if (!cb(item)) {
|
if (!cb(item)) {
|
||||||
newArr.push(item);
|
newArr.push(item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newArr;
|
return newArr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -59,26 +57,31 @@ const checked = (isChecked, client) => {
|
||||||
checkedRows.value = remove(checkedRows.value, (row) => row.id === client.id);
|
checkedRows.value = remove(checkedRows.value, (row) => row.id === client.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showModal = (client: User) => {
|
||||||
|
currentClient.value = client;
|
||||||
|
isModalActive.value = true;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CardBoxModal v-model="isModalActive" title="Sample modal">
|
<CardBoxModal v-model="isModalActive" :title="currentClient ? currentClient.login : ''">
|
||||||
|
<div v-if="currentClient">
|
||||||
|
<p>Login: {{ currentClient.login }}</p>
|
||||||
|
<p>Email: {{ currentClient.email }}</p>
|
||||||
|
<p>Created: {{ currentClient?.created_at ? dayjs(currentClient.created_at).format('MMM D, YYYY h:mm A') : 'N/A' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</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>Lorem ipsum dolor sit amet <b>adipiscing elit</b></p>
|
||||||
<p>This is sample modal</p>
|
<p>This is sample modal</p>
|
||||||
</CardBoxModal>
|
</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
|
<span v-for="checkedRow in checkedRows" :key="checkedRow.id"
|
||||||
v-for="checkedRow in checkedRows"
|
class="inline-block px-2 py-1 rounded-sm mr-2 text-sm bg-gray-100 dark:bg-slate-700">
|
||||||
:key="checkedRow.id"
|
{{ checkedRow.login }}
|
||||||
class="inline-block px-2 py-1 rounded-sm mr-2 text-sm bg-gray-100 dark:bg-slate-700"
|
|
||||||
>
|
|
||||||
{{ checkedRow.name }}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -87,10 +90,8 @@ const checked = (isChecked, client) => {
|
||||||
<tr>
|
<tr>
|
||||||
<th v-if="checkable" />
|
<th v-if="checkable" />
|
||||||
<th />
|
<th />
|
||||||
<th>Name</th>
|
<th>Login</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>City</th>
|
|
||||||
<th>Progress</th>
|
|
||||||
<th>Created</th>
|
<th>Created</th>
|
||||||
<th />
|
<th />
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -99,29 +100,33 @@ const checked = (isChecked, client) => {
|
||||||
<tr v-for="client in itemsPaginated" :key="client.id">
|
<tr v-for="client in itemsPaginated" :key="client.id">
|
||||||
<TableCheckboxCell v-if="checkable" @checked="checked($event, client)" />
|
<TableCheckboxCell v-if="checkable" @checked="checked($event, client)" />
|
||||||
<td class="border-b-0 lg:w-6 before:hidden">
|
<td class="border-b-0 lg:w-6 before:hidden">
|
||||||
<UserAvatar :username="client.name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
|
<!-- <UserAvatar :username="client.login" :avatar="client.avatar" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" /> -->
|
||||||
|
<div v-if="client.avatar">
|
||||||
|
<UserAvatar :default-url="client.avatar ? '/public' + client.avatar : ''"
|
||||||
|
:username="client.first_name + ' ' + client.last_name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<UserAvatar :username="client.first_name + ' ' + client.last_name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Name">
|
<td data-label="Login">
|
||||||
{{ client.name }}
|
{{ client.login }}
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Email">
|
<td data-label="Email">
|
||||||
{{ client.email }}
|
{{ client.email }}
|
||||||
</td>
|
</td>
|
||||||
<td data-label="City">
|
<td data-label="Created">
|
||||||
{{ client.city }}
|
<small class="text-gray-500 dark:text-slate-400"
|
||||||
</td>
|
:title="client.created_at ? dayjs(client.created_at).format('MMM D, YYYY h:mm A') : 'N/A'">
|
||||||
<td data-label="Progress" class="lg:w-32">
|
{{ client.created_at ? dayjs(client.created_at).format('MMM D, YYYY h:mm A') : 'N/A' }}
|
||||||
<progress class="flex w-2/5 self-center lg:w-full" max="100" v-bind:value="client.progress">
|
</small>
|
||||||
{{ client.progress }}
|
|
||||||
</progress>
|
|
||||||
</td>
|
|
||||||
<td data-label="Created" class="lg:w-1 whitespace-nowrap">
|
|
||||||
<small class="text-gray-500 dark:text-slate-400" :title="client.created">{{ client.created }}</small>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="before:hidden lg:w-1 whitespace-nowrap">
|
<td class="before:hidden lg:w-1 whitespace-nowrap">
|
||||||
<BaseButtons type="justify-start lg:justify-end" no-wrap>
|
<BaseButtons type="justify-start lg:justify-end" no-wrap>
|
||||||
<BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" />
|
<BaseButton color="info" :icon="mdiEye" small @click="showModal(client)" />
|
||||||
<BaseButton color="danger" :icon="mdiTrashCan" small @click="isModalDangerActive = true" />
|
<!-- <BaseButton color="danger" :icon="mdiTrashCan" small @click="isModalDangerActive = true" /> -->
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -130,15 +135,8 @@ const checked = (isChecked, client) => {
|
||||||
<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>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<script setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
@ -22,55 +22,55 @@ const avatar = computed(() => {
|
||||||
|
|
||||||
const username = computed(() => props.username);
|
const username = computed(() => props.username);
|
||||||
|
|
||||||
const darkenColor = (color) => {
|
// const darkenColor = (color: string) => {
|
||||||
const r = parseInt(color.slice(0, 2), 16);
|
// const r = parseInt(color.slice(0, 2), 16);
|
||||||
const g = parseInt(color.slice(2, 4), 16);
|
// const g = parseInt(color.slice(2, 4), 16);
|
||||||
const b = parseInt(color.slice(4, 6), 16);
|
// const b = parseInt(color.slice(4, 6), 16);
|
||||||
|
|
||||||
const darkerR = Math.round(r * 0.6);
|
// const darkerR = Math.round(r * 0.6);
|
||||||
const darkerG = Math.round(g * 0.6);
|
// const darkerG = Math.round(g * 0.6);
|
||||||
const darkerB = Math.round(b * 0.6);
|
// const darkerB = Math.round(b * 0.6);
|
||||||
|
|
||||||
const darkerColor = ((darkerR << 16) + (darkerG << 8) + darkerB).toString(16);
|
// const darkerColor = ((darkerR << 16) + (darkerG << 8) + darkerB).toString(16);
|
||||||
|
|
||||||
return darkerColor.padStart(6, '0');
|
// return darkerColor.padStart(6, '0');
|
||||||
};
|
// };
|
||||||
|
|
||||||
const getColorFromName = (name) => {
|
// const getColorFromName = (name: string): string => {
|
||||||
let hash = 0;
|
// let hash = 0;
|
||||||
for (let i = 0; i < name.length; i++) {
|
// for (let i = 0; i < name.length; i++) {
|
||||||
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
// hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
}
|
// }
|
||||||
let color = '#';
|
// let color = '#';
|
||||||
for (let i = 0; i < 3; i++) {
|
// for (let i = 0; i < 3; i++) {
|
||||||
const value = (hash >> (i * 8)) & 0xff;
|
// const value = (hash >> (i * 8)) & 0xff;
|
||||||
color += ('00' + value.toString(16)).substr(-2);
|
// color += ('00' + value.toString(16)).substr(-2);
|
||||||
}
|
// }
|
||||||
return color.replace('#', '');
|
// return color.replace('#', '');
|
||||||
};
|
// };
|
||||||
|
|
||||||
const lightenColor = (hexColor, percent) => {
|
// const lightenColor = (hexColor: string, percent: number): string => {
|
||||||
let r = parseInt(hexColor.substring(0, 2), 16);
|
// let r = parseInt(hexColor.substring(0, 2), 16);
|
||||||
let g = parseInt(hexColor.substring(2, 4), 16);
|
// let g = parseInt(hexColor.substring(2, 4), 16);
|
||||||
let b = parseInt(hexColor.substring(4, 6), 16);
|
// let b = parseInt(hexColor.substring(4, 6), 16);
|
||||||
|
|
||||||
r = Math.floor(r * (100 + percent) / 100);
|
// r = Math.floor(r * (100 + percent) / 100);
|
||||||
g = Math.floor(g * (100 + percent) / 100);
|
// g = Math.floor(g * (100 + percent) / 100);
|
||||||
b = Math.floor(b * (100 + percent) / 100);
|
// b = Math.floor(b * (100 + percent) / 100);
|
||||||
|
|
||||||
r = (r < 255) ? r : 255;
|
// r = (r < 255) ? r : 255;
|
||||||
g = (g < 255) ? g : 255;
|
// g = (g < 255) ? g : 255;
|
||||||
b = (b < 255) ? b : 255;
|
// b = (b < 255) ? b : 255;
|
||||||
|
|
||||||
const lighterHex = ((r << 16) | (g << 8) | b).toString(16);
|
// const lighterHex = ((r << 16) | (g << 8) | b).toString(16);
|
||||||
|
|
||||||
return lighterHex.padStart(6, '0');
|
// return lighterHex.padStart(6, '0');
|
||||||
};
|
// };
|
||||||
|
|
||||||
const generateAvatarUrl = (name) => {
|
const generateAvatarUrl = (name: string): string => {
|
||||||
const originalColor = getColorFromName(name);
|
// const originalColor = getColorFromName(name);
|
||||||
const backgroundColor = lightenColor(originalColor, 60);
|
// const backgroundColor = lightenColor(originalColor, 60);
|
||||||
const textColor = darkenColor(originalColor);
|
// const textColor = darkenColor(originalColor);
|
||||||
|
|
||||||
const avatarUrl = `/api/avatar?name=${name}&size=50`;
|
const avatarUrl = `/api/avatar?name=${name}&size=50`;
|
||||||
return avatarUrl;
|
return avatarUrl;
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import { Head } from '@inertiajs/vue3';
|
import { Head } from '@inertiajs/vue3';
|
||||||
import { computed, onMounted } from 'vue';
|
import { computed, onMounted } from 'vue';
|
||||||
import { MainService } from '@/Stores/main';
|
import { MainService } from '@/Stores/main';
|
||||||
// import { Inertia } from '@inertiajs/inertia';
|
|
||||||
import {
|
import {
|
||||||
mdiAccountMultiple,
|
mdiAccountMultiple,
|
||||||
mdiDatabaseOutline,
|
mdiDatabaseOutline,
|
||||||
|
@ -13,21 +12,18 @@ import {
|
||||||
mdiGithub,
|
mdiGithub,
|
||||||
mdiChartPie,
|
mdiChartPie,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
// import { containerMaxW } from '@/config.js'; // "xl:max-w-6xl xl:mx-auto"
|
|
||||||
// import * as chartConfig from '@/Components/Charts/chart.config.js';
|
|
||||||
import LineChart from '@/Components/Charts/LineChart.vue';
|
import LineChart from '@/Components/Charts/LineChart.vue';
|
||||||
import UserCard from '@/Components/unused/UserCard.vue';
|
|
||||||
import SectionMain from '@/Components/SectionMain.vue';
|
import SectionMain from '@/Components/SectionMain.vue';
|
||||||
import CardBoxWidget from '@/Components/CardBoxWidget.vue';
|
import CardBoxWidget from '@/Components/CardBoxWidget.vue';
|
||||||
import CardBox from '@/Components/CardBox.vue';
|
import CardBox from '@/Components/CardBox.vue';
|
||||||
import TableSampleClients from '@/Components/TableSampleClients.vue';
|
import TableSampleClients from '@/Components/TableSampleClients.vue';
|
||||||
import NotificationBar from '@/Components/NotificationBar.vue';
|
// import NotificationBar from '@/Components/NotificationBar.vue';
|
||||||
import BaseButton from '@/Components/BaseButton.vue';
|
import BaseButton from '@/Components/BaseButton.vue';
|
||||||
import CardBoxTransaction from '@/Components/CardBoxTransaction.vue';
|
|
||||||
import CardBoxClient from '@/Components/CardBoxClient.vue';
|
import CardBoxClient from '@/Components/CardBoxClient.vue';
|
||||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||||
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
|
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
|
||||||
import SectionBannerStarOnGitHub from '@/Components/SectionBannerStarOnGitea.vue';
|
import SectionBannerStarOnGitHub from '@/Components/SectionBannerStarOnGitea.vue';
|
||||||
|
import CardBoxDataset from '@/Components/CardBoxDataset.vue';
|
||||||
const mainService = MainService()
|
const mainService = MainService()
|
||||||
|
|
||||||
// const chartData = ref();
|
// const chartData = ref();
|
||||||
|
@ -37,36 +33,32 @@ const fillChartData = async () => {
|
||||||
// chartData.value = mainService.graphData;
|
// chartData.value = mainService.graphData;
|
||||||
};
|
};
|
||||||
const chartData = computed(() => mainService.graphData);
|
const chartData = computed(() => mainService.graphData);
|
||||||
onMounted(async () => {
|
// onMounted(async () => {
|
||||||
await mainService.fetchChartData("2022");
|
// await mainService.fetchChartData("2022");
|
||||||
});
|
// });
|
||||||
;
|
|
||||||
/* Fetch sample data */
|
|
||||||
mainService.fetch('clients');
|
|
||||||
mainService.fetch('history');
|
|
||||||
|
|
||||||
mainService.fetchApi('authors');
|
// mainService.fetch('clients');
|
||||||
mainService.fetchApi('datasets');
|
// mainService.fetch('history');
|
||||||
|
|
||||||
|
// mainService.fetchApi('authors');
|
||||||
|
// mainService.fetchApi('datasets');
|
||||||
|
|
||||||
// const clientBarItems = computed(() => mainService.clients.slice(0, 4));
|
// const clientBarItems = computed(() => mainService.clients.slice(0, 4));
|
||||||
const transactionBarItems = computed(() => mainService.history);
|
// const transactionBarItems = computed(() => mainService.history);
|
||||||
|
|
||||||
const authorBarItems = computed(() => mainService.authors.slice(0, 4));
|
const authorBarItems = computed(() => mainService.authors.slice(0, 5));
|
||||||
const authors = computed(() => mainService.authors);
|
const authors = computed(() => mainService.authors);
|
||||||
const datasets = computed(() => mainService.datasets);
|
const datasets = computed(() => mainService.datasets);
|
||||||
// const props = defineProps({
|
const datasetBarItems = computed(() => mainService.datasets.slice(0, 5));
|
||||||
// user: {
|
// let test = datasets.value;
|
||||||
// type: Object,
|
// console.log(test);
|
||||||
// default: () => ({}),
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutAuthenticated :showAsideMenu="false">
|
<LayoutAuthenticated :showAsideMenu="false">
|
||||||
<Head title="Dashboard" />
|
<Head title="Dashboard" />
|
||||||
|
|
||||||
<!-- <section class="p-6" v-bind:class="containerMaxW"> -->
|
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton v-bind:icon="mdiChartTimelineVariant" title="Overview" main>
|
<SectionTitleLineWithButton v-bind:icon="mdiChartTimelineVariant" title="Overview" main>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
|
@ -97,16 +89,13 @@ const datasets = computed(() => mainService.datasets);
|
||||||
:number="datasets.length"
|
:number="datasets.length"
|
||||||
label="Publications"
|
label="Publications"
|
||||||
/>
|
/>
|
||||||
<!-- <CardBoxWidget trend="193" trend-type="info" color="text-blue-500" :icon="mdiCartOutline" :number="datasets.length"
|
|
||||||
prefix="$" label="Publications" /> -->
|
|
||||||
<CardBoxWidget
|
<CardBoxWidget
|
||||||
trend="Overflow"
|
trend="+25%"
|
||||||
trend-type="alert"
|
trend-type="up"
|
||||||
color="text-red-500"
|
color="text-purple-500"
|
||||||
:icon="mdiChartTimelineVariant"
|
:icon="mdiChartTimelineVariant"
|
||||||
:number="256"
|
:number="52"
|
||||||
suffix="%"
|
label="Citations"
|
||||||
label="Performance"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -118,25 +107,19 @@ const datasets = computed(() => mainService.datasets);
|
||||||
:name="client.name"
|
:name="client.name"
|
||||||
:email="client.email"
|
:email="client.email"
|
||||||
:date="client.created_at"
|
:date="client.created_at"
|
||||||
:text="client.datasetCount"
|
:text="client.identifier_orcid"
|
||||||
|
:count="client.dataset_count"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col justify-between">
|
<div class="flex flex-col justify-between">
|
||||||
<CardBoxTransaction
|
<CardBoxDataset
|
||||||
v-for="(transaction, index) in transactionBarItems"
|
v-for="(dataset, index) in datasetBarItems"
|
||||||
:key="index"
|
:key="index"
|
||||||
:amount="transaction.amount"
|
:dataset="dataset"
|
||||||
:date="transaction.date"
|
|
||||||
:business="transaction.business"
|
|
||||||
:type="transaction.type"
|
|
||||||
:name="transaction.name"
|
|
||||||
:account="transaction.account"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UserCard />
|
|
||||||
|
|
||||||
<SectionBannerStarOnGitHub />
|
<SectionBannerStarOnGitHub />
|
||||||
|
|
||||||
<SectionTitleLineWithButton :icon="mdiChartPie" title="Trends overview: Publications per month" />
|
<SectionTitleLineWithButton :icon="mdiChartPie" title="Trends overview: Publications per month" />
|
||||||
|
@ -146,33 +129,13 @@ const datasets = computed(() => mainService.datasets);
|
||||||
</div>
|
</div>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
|
|
||||||
<SectionTitleLineWithButton :icon="mdiAccountMultiple" title="Submitters (to do)" />
|
<SectionTitleLineWithButton :icon="mdiAccountMultiple" title="Submitters" />
|
||||||
|
|
||||||
<NotificationBar color="info" :icon="mdiMonitorCellphone"> <b>Responsive table.</b> Collapses on mobile </NotificationBar>
|
<!-- <NotificationBar color="info" :icon="mdiMonitorCellphone"> <b>Responsive table.</b> Collapses on mobile </NotificationBar> -->
|
||||||
|
|
||||||
<CardBox :icon="mdiMonitorCellphone" title="Responsive table" has-table>
|
<CardBox :icon="mdiMonitorCellphone" title="Responsive table" has-table>
|
||||||
<TableSampleClients />
|
<TableSampleClients />
|
||||||
</CardBox>
|
</CardBox>
|
||||||
|
|
||||||
<!-- <CardBox>
|
|
||||||
<p class="mb-3 text-gray-500 dark:text-gray-400">
|
|
||||||
Discover the power of Tethys, the cutting-edge web backend solution that revolutionizes the way you handle research
|
|
||||||
data. At the heart of Tethys lies our meticulously developed research data repository, which leverages state-of-the-art
|
|
||||||
CI/CD techniques to deliver a seamless and efficient experience.
|
|
||||||
</p>
|
|
||||||
<p class="mb-3 text-gray-500 dark:text-gray-400">
|
|
||||||
CI/CD, or Continuous Integration and Continuous Deployment, is a modern software development approach that ensures your
|
|
||||||
code undergoes automated testing, continuous integration, and frequent deployment. By embracing CI/CD techniques, we
|
|
||||||
ensure that every code change in our research data repository is thoroughly validated, enhancing reliability and
|
|
||||||
accelerating development cycles.
|
|
||||||
</p>
|
|
||||||
<p class="mb-3 text-gray-500 dark:text-gray-400">
|
|
||||||
With Tethys, you can say goodbye to the complexities of manual deployments and embrace a streamlined process that
|
|
||||||
eliminates errors and minimizes downtime. Our CI/CD pipeline automatically verifies each code commit, runs comprehensive
|
|
||||||
tests, and deploys the repository seamlessly, ensuring that your research data is always up-to-date and accessible.
|
|
||||||
</p>
|
|
||||||
</CardBox> -->
|
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
<!-- </section> -->
|
|
||||||
</LayoutAuthenticated>
|
</LayoutAuthenticated>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -26,17 +26,6 @@ const errors: Ref<any> = computed(() => {
|
||||||
return usePage().props.errors;
|
return usePage().props.errors;
|
||||||
});
|
});
|
||||||
|
|
||||||
// const form = useForm({
|
|
||||||
// preferred_reviewer: '',
|
|
||||||
// preferred_reviewer_email: '',
|
|
||||||
// preferation: 'yes_preferation',
|
|
||||||
|
|
||||||
// // preferation: '',
|
|
||||||
// // isPreferationRequired: false,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const isPreferationRequired = computed(() => form.preferation === 'yes_preferation');
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Notification.showInfo(`doi implementation is in developement. Create DOI for dataset ${props.dataset.publish_id} later on`);
|
// Notification.showInfo(`doi implementation is in developement. Create DOI for dataset ${props.dataset.publish_id} later on`);
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head } from '@inertiajs/vue3';
|
import { Head } from '@inertiajs/vue3';
|
||||||
import { ref, Ref } from 'vue';
|
import { ref, Ref } from 'vue';
|
||||||
import { mdiChartTimelineVariant } from '@mdi/js';
|
import { mdiChartTimelineVariant, mdiGithub } from '@mdi/js';
|
||||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||||
import SectionMain from '@/Components/SectionMain.vue';
|
import SectionMain from '@/Components/SectionMain.vue';
|
||||||
import BaseButton from '@/Components/BaseButton.vue';
|
import BaseButton from '@/Components/BaseButton.vue';
|
||||||
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
|
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
|
||||||
import { MapOptions } from '@/Components/Map/MapOptions';
|
import { MapOptions } from '@/Components/Map/MapOptions';
|
||||||
import { stardust } from '@eidellev/adonis-stardust/client';
|
// import { stardust } from '@eidellev/adonis-stardust/client';
|
||||||
import SearchMap from '@/Components/Map/SearchMap.vue';
|
import SearchMap from '@/Components/Map/SearchMap.vue';
|
||||||
import { OpensearchDocument } from '@/Dataset';
|
import { OpensearchDocument } from '@/Dataset';
|
||||||
|
|
||||||
|
@ -48,14 +48,15 @@ const mapOptions: MapOptions = {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutAuthenticated :showAsideMenu="false">
|
<LayoutAuthenticated :showAsideMenu="false">
|
||||||
|
|
||||||
<Head title="Map" />
|
<Head title="Map" />
|
||||||
|
|
||||||
<!-- <section class="p-6" v-bind:class="containerMaxW"> -->
|
<!-- <section class="p-6" v-bind:class="containerMaxW"> -->
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton v-bind:icon="mdiChartTimelineVariant" title="Tethys Map" main>
|
<SectionTitleLineWithButton v-bind:icon="mdiChartTimelineVariant" title="Tethys Map" main>
|
||||||
<!-- <BaseButton href="https://gitea.geologie.ac.at/geolba/tethys" target="_blank" :icon="mdiGithub"
|
<BaseButton href="https://gitea.geosphere.at/geolba/tethys" target="_blank" :icon="mdiGithub"
|
||||||
label="Star on Gitea" color="contrast" rounded-full small /> -->
|
label="Star on GeoSPhere Forgejo" color="contrast" rounded-full small />
|
||||||
<BaseButton :route-name="stardust.route('app.login.show')" label="Login" color="white" rounded-full small />
|
<!-- <BaseButton :route-name="stardust.route('app.login.show')" label="Login" color="white" rounded-full small /> -->
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
<!-- <SectionBannerStarOnGitea /> -->
|
<!-- <SectionBannerStarOnGitea /> -->
|
||||||
|
@ -80,19 +81,20 @@ const mapOptions: MapOptions = {
|
||||||
<div v-for="author in dataset.author" :key="author" class="mb-1">{{ author }}</div>
|
<div v-for="author in dataset.author" :key="author" class="mb-1">{{ author }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<span class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
|
<span
|
||||||
|
class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
|
||||||
{{ dataset.year }}
|
{{ dataset.year }}
|
||||||
</span>
|
</span>
|
||||||
<span class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
|
<span
|
||||||
|
class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
|
||||||
{{ dataset.language }}
|
{{ dataset.language }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<span class="label"><i class="fas fa-file"></i> {{ dataset.doctype }}</span>
|
<span class="label"><i class="fas fa-file"></i> {{ dataset.doctype }}</span>
|
||||||
<!-- <span>Licence: {{ document.licence }}</span> -->
|
<!-- <span>Licence: {{ document.licence }}</span> -->
|
||||||
<span v-if="openAccessLicences.includes(dataset.licence)" class="label titlecase"
|
<span v-if="openAccessLicences.includes(dataset.licence)" class="label titlecase"><i
|
||||||
><i class="fas fa-lock-open"></i> Open Access</span
|
class="fas fa-lock-open"></i> Open Access</span>
|
||||||
>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,91 +1,343 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-screen p-4 bg-gray-100">
|
<LayoutAuthenticated>
|
||||||
<header class="flex justify-between items-center mb-4">
|
|
||||||
<h1 class="text-xl font-bold">SKOS Browser</h1>
|
<Head title="Profile"></Head>
|
||||||
<div class="flex space-x-2">
|
<SectionMain>
|
||||||
<button @click="updateApp" title="Update the application">
|
<SectionTitleLineWithButton :icon="mdiLibraryShelves" title="Library Classification" main>
|
||||||
<!-- <img src="/Resources/Images/refresh.png" alt="Update" class="w-4 h-4" /> -->
|
<div class="bg-lime-100 shadow rounded-lg p-6 mb-6 flex items-center justify-between">
|
||||||
</button>
|
<div>
|
||||||
<button @click="showInfo" title="Info">
|
<label for="role-select" class="block text-lg font-medium text-gray-700 mb-1">
|
||||||
<!-- <img src="/Resources/Images/info.png" alt="Info" class="w-4 h-4" /> -->
|
Select Classification Role <span class="text-red-500">*</span>
|
||||||
</button>
|
</label>
|
||||||
</div>
|
<select id="role-select" v-model="selectedCollectionRole"
|
||||||
</header>
|
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>
|
||||||
|
<ul class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
|
||||||
|
<li v-for="parent in broaderCollections" :key="parent.id"
|
||||||
|
class="cursor-pointer p-2 border border-gray-200 rounded bg-green-50 text-green-700 text-sm hover:bg-green-100 hover:underline"
|
||||||
|
@click="onCollectionSelected(parent)" title="Click to select this collection">
|
||||||
|
{{ `${parent.name} (${parent.number})` }}
|
||||||
|
</li>
|
||||||
|
<li v-if="broaderCollections.length === 0" 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 bg-green-50 text-green-700 text-sm hover:bg-green-100">
|
||||||
|
{{ `${selectedCollection.name} (${selectedCollection.number})` }}
|
||||||
|
</p>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<!-- Narrower Collections (Children) -->
|
||||||
|
<CardBox v-if="selectedCollection" class="rounded-lg p-4" has-form-data>
|
||||||
|
<h2 class="text-lg font-bold text-gray-800 dark:text-slate-400 mb-2">Narrower Collections</h2>
|
||||||
|
<!-- <ul class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
|
||||||
|
<li v-for="child in narrowerCollections" :key="child.id"
|
||||||
|
class="cursor-pointer p-2 border border-gray-200 rounded bg-green-50 text-green-700 text-sm hover:bg-green-100 hover:underline"
|
||||||
|
@click="onCollectionSelected(child)">
|
||||||
|
{{ `${child.name} (${child.number})` }}
|
||||||
|
</li>
|
||||||
|
<li v-if="narrowerCollections.length === 0" class="text-gray-500 text-sm">
|
||||||
|
No sub-collections available.
|
||||||
|
</li>
|
||||||
|
</ul> -->
|
||||||
|
<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"
|
||||||
|
class="cursor-pointer p-2 border border-gray-200 rounded bg-green-50 text-green-700 text-sm hover:bg-green-100 hover:underline"
|
||||||
|
@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 class="bg-white shadow-md rounded-lg p-4 mb-6">
|
|
||||||
<h2 class="text-lg font-semibold">GBA-Thesaurus</h2>
|
|
||||||
<label class="block text-sm font-medium">Aktueller Endpoint:</label>
|
|
||||||
<!-- <TreeView :items="endpoints" @select="onEndpointSelected" /> -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white shadow-md rounded-lg p-4">
|
<div class="mb-4 rounded-lg">
|
||||||
<h2 class="text-lg font-semibold">Konzept-Suche</h2>
|
<div v-if="selectedCollection" class="bg-gray-100 shadow rounded-lg p-6 mb-6">
|
||||||
<!-- <Autocomplete v-model="selectedConcept" :items="concepts" placeholder="Search for a concept" @change="onConceptSelected" /> -->
|
<p class="mb-4 text-gray-700">Please drag your collections here to classify your previously created
|
||||||
<div class="mt-4">
|
dataset
|
||||||
<h3 class="text-md font-medium">Ausgewähltes Konzept</h3>
|
according to library classification standards.</p>
|
||||||
<p>{{ selectedConcept.title }}</p>
|
<draggable v-model="dropCollections" :group="{ name: 'collections' }"
|
||||||
<a :href="selectedConcept.uri" target="_blank" class="text-blue-500">URI</a>
|
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"
|
||||||
<textarea
|
tag="ul">
|
||||||
v-model="selectedConcept.description"
|
<template #item="{ element }">
|
||||||
class="mt-2 w-full h-24 border rounded"
|
<div :key="element.id"
|
||||||
placeholder="Description"
|
class="p-2 m-1 bg-sky-200 text-sky-800 rounded flex items-center gap-2 h-7">
|
||||||
></textarea>
|
<span>{{ element.name }} ({{ element.number }})</span>
|
||||||
</div>
|
<button
|
||||||
<div class="mt-4">
|
@click="dropCollections = dropCollections.filter(item => item.id !== element.id)"
|
||||||
<h3 class="text-md font-medium">Untergeordnete Konzepte</h3>
|
class="hover:text-sky-600 flex items-center">
|
||||||
<!-- <LinkLabelList :items="narrowerConcepts" /> -->
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20"
|
||||||
</div>
|
fill="currentColor">
|
||||||
<div class="mt-4">
|
<path fill-rule="evenodd"
|
||||||
<h3 class="text-md font-medium">Übergeordnete Konzepte</h3>
|
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"
|
||||||
<!-- <LinkLabelList :items="broaderConcepts" /> -->
|
clip-rule="evenodd" />
|
||||||
</div>
|
</svg>
|
||||||
<div class="mt-4">
|
</button>
|
||||||
<h3 class="text-md font-medium">Verwandte Konzepte</h3>
|
|
||||||
<!-- <LinkLabelList :items="relatedConcepts" /> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<div class="p-6 border-t border-gray-100 dark:border-slate-800">
|
||||||
// import TreeView from './TreeView.vue'; // Assuming you have a TreeView component
|
<BaseButtons>
|
||||||
// import Autocomplete from './Autocomplete.vue'; // Assuming you have an Autocomplete component
|
<BaseButton @click.stop="syncDatasetCollections" label="Save" color="info" small
|
||||||
// import LinkLabelList from './LinkLabelList.vue'; // Assuming you have a LinkLabelList component
|
:disabled="isSaveDisabled" :style="{ opacity: isSaveDisabled ? 0.5 : 1 }">
|
||||||
|
</BaseButton>
|
||||||
|
</BaseButtons>
|
||||||
|
</div>
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
// TreeView,
|
|
||||||
// Autocomplete,
|
|
||||||
// LinkLabelList,
|
</SectionMain>
|
||||||
|
</LayoutAuthenticated>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, Ref, watch, computed } from 'vue';
|
||||||
|
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';
|
||||||
|
|
||||||
|
interface CollectionRole {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
collections?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Collection {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
number: string;
|
||||||
|
parent_id?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
collectionRoles: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
default: () => []
|
||||||
},
|
},
|
||||||
data() {
|
dataset: {
|
||||||
return {
|
type: Object,
|
||||||
endpoints: [], // This should be populated with your data
|
default: () => ({}),
|
||||||
concepts: [], // This should be populated with your data
|
},
|
||||||
selectedConcept: {},
|
relatedCollections: Array<Collection>
|
||||||
narrowerConcepts: [], // Populate with data
|
});
|
||||||
broaderConcepts: [], // Populate with data
|
|
||||||
relatedConcepts: [], // Populate with data
|
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[]>([]);
|
||||||
|
|
||||||
|
|
||||||
|
// const onCollectionRoleSelected = (event: Event) => {
|
||||||
|
// const target = event.target as HTMLSelectElement;
|
||||||
|
// const roleId = Number(target.value);
|
||||||
|
// selectedCollectionRole.value =
|
||||||
|
// collectionRoles.value.find((role: CollectionRole) => role.id === roleId) || null;
|
||||||
|
|
||||||
|
// // Clear any previously selected collection or related data
|
||||||
|
// selectedCollection.value = null;
|
||||||
|
// narrowerCollections.value = [];
|
||||||
|
// broaderCollections.value = [];
|
||||||
|
|
||||||
|
// // fetchTopLevelCollections(roleId);
|
||||||
|
// collections.value = selectedCollectionRole.value?.collections || []
|
||||||
|
// };
|
||||||
|
|
||||||
|
// New reactive array to hold dropped collections for the dataset
|
||||||
|
const dropCollections: Ref<Collection[]> = ref([]);
|
||||||
|
|
||||||
|
// If there are related collections passed in, fill dropCollections with these.
|
||||||
|
if (props.relatedCollections && props.relatedCollections.length > 0) {
|
||||||
|
dropCollections.value = props.relatedCollections;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a computed property for the disabled state based on dropCollections length
|
||||||
|
const isSaveDisabled = computed(() => dropCollections.value.length === 0);
|
||||||
|
|
||||||
|
// 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 }
|
||||||
|
);
|
||||||
|
// Watch for changes in selectedCollectionRole and update related collections 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 }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Watch for changes in dropCollections
|
||||||
|
watch(
|
||||||
|
() => dropCollections.value,
|
||||||
|
() => {
|
||||||
|
if (selectedCollection.value) {
|
||||||
|
fetchCollections(selectedCollection.value.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const onToplevelCollectionSelected = (collection: Collection) => {
|
||||||
|
selectedToplevelCollection.value = collection;
|
||||||
|
selectedCollection.value = collection;
|
||||||
|
// call the API endpoint to get both.
|
||||||
|
fetchCollections(collection.id)
|
||||||
};
|
};
|
||||||
},
|
|
||||||
methods: {
|
const onCollectionSelected = (collection: Collection) => {
|
||||||
updateApp() {
|
selectedCollection.value = collection;
|
||||||
// Handle app update logic
|
// call the API endpoint to get both.
|
||||||
},
|
fetchCollections(collection.id)
|
||||||
showInfo() {
|
|
||||||
// Handle showing information
|
|
||||||
},
|
|
||||||
onEndpointSelected(endpoint) {
|
|
||||||
// Handle endpoint selection
|
|
||||||
},
|
|
||||||
onConceptSelected(concept) {
|
|
||||||
this.selectedConcept = concept;
|
|
||||||
// Handle concept selection logic, e.g., fetching related concepts
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// New function to load both narrower and broader concepts using the real API route.
|
||||||
|
const fetchCollections = async (collectionId: number) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/api/collections/${collectionId}`);
|
||||||
|
const data = response.data;
|
||||||
|
|
||||||
|
// Set narrower concepts with filtered collections
|
||||||
|
narrowerCollections.value = data.narrowerCollections.filter(
|
||||||
|
collection => !dropCollections.value.some(dc => dc.id === collection.id)
|
||||||
|
);
|
||||||
|
// For broader concepts, if present, wrap it in an array (or change your template accordingly)
|
||||||
|
broaderCollections.value = data.broaderCollection;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in fetchConcepts:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncDatasetCollections = async () => {
|
||||||
|
try {
|
||||||
|
// Extract the ids from the dropCollections list
|
||||||
|
const collectionIds = dropCollections.value.map(item => item.id);
|
||||||
|
await axios.post('/api/dataset/collections/sync', { collections: collectionIds });
|
||||||
|
// Optionally show a success message or refresh dataset info
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error syncing dataset collections:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Add your styles here */
|
.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>
|
</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, mdiTrashCan, mdiAlertBoxOutline, mdiLockOpen } from '@mdi/js';
|
import { mdiSquareEditOutline, mdiTrashCan, mdiAlertBoxOutline, mdiLockOpen, 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';
|
||||||
|
@ -140,6 +140,9 @@ const formatServerState = (state: string) => {
|
||||||
:icon="mdiLockOpen" :label="'Release'" small />
|
:icon="mdiLockOpen" :label="'Release'" small />
|
||||||
<BaseButton v-if="can.edit" :route-name="stardust.route('dataset.edit', [dataset.id])"
|
<BaseButton v-if="can.edit" :route-name="stardust.route('dataset.edit', [dataset.id])"
|
||||||
color="info" :icon="mdiSquareEditOutline" :label="'Edit'" small />
|
color="info" :icon="mdiSquareEditOutline" :label="'Edit'" small />
|
||||||
|
<BaseButton v-if="can.edit"
|
||||||
|
:route-name="stardust.route('dataset.categorize', [dataset.id])" color="info"
|
||||||
|
:icon="mdiLibraryShelves" :label="'Library'" small />
|
||||||
<BaseButton v-if="can.delete" color="danger"
|
<BaseButton v-if="can.delete" color="danger"
|
||||||
:route-name="stardust.route('dataset.delete', [dataset.id])" :icon="mdiTrashCan"
|
:route-name="stardust.route('dataset.delete', [dataset.id])" :icon="mdiTrashCan"
|
||||||
small />
|
small />
|
||||||
|
|
|
@ -2,6 +2,56 @@ import { defineStore } from 'pinia';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Dataset } from '@/Dataset';
|
import { Dataset } from '@/Dataset';
|
||||||
import menu from '@/menu';
|
import menu from '@/menu';
|
||||||
|
// import type Person from '#models/person';
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
login: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
created_at: DateTime;
|
||||||
|
updatedAt: DateTime;
|
||||||
|
lastLoginAt: DateTime;
|
||||||
|
isActive: boolean;
|
||||||
|
isVerified: boolean;
|
||||||
|
roles: string[];
|
||||||
|
permissions: string[];
|
||||||
|
settings: Record<string, any>;
|
||||||
|
profile: {
|
||||||
|
avatar: string;
|
||||||
|
bio: string;
|
||||||
|
location: string;
|
||||||
|
website: string;
|
||||||
|
social: {
|
||||||
|
twitter: string;
|
||||||
|
facebook: string;
|
||||||
|
linkedin: string;
|
||||||
|
github: string;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
verifyPassword: (plainPassword: string) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DateTime {
|
||||||
|
get: (unit: keyof DateTime) => number;
|
||||||
|
getPossibleOffsets: () => DateTime[];
|
||||||
|
toRelativeCalendar: (options?: ToRelativeCalendarOptions) => string | null;
|
||||||
|
toFormat: (format: string) => string;
|
||||||
|
toISO: () => string;
|
||||||
|
toJSON: () => string;
|
||||||
|
toString: () => string;
|
||||||
|
toLocaleString: (options?: Intl.DateTimeFormatOptions) => string;
|
||||||
|
toUTC: () => DateTime;
|
||||||
|
toLocal: () => DateTime;
|
||||||
|
valueOf: () => number;
|
||||||
|
toMillis: () => number;
|
||||||
|
toSeconds: () => number;
|
||||||
|
toUnixInteger: () => number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface Person {
|
export interface Person {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -9,10 +59,12 @@ export interface Person {
|
||||||
email: string;
|
email: string;
|
||||||
name_type: string;
|
name_type: string;
|
||||||
identifier_orcid: string;
|
identifier_orcid: string;
|
||||||
datasetCount: string;
|
dataset_count: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface TransactionItem {
|
interface TransactionItem {
|
||||||
amount: number;
|
amount: number;
|
||||||
account: string;
|
account: string;
|
||||||
|
@ -61,7 +113,7 @@ export const MainService = defineStore('main', {
|
||||||
isFieldFocusRegistered: false,
|
isFieldFocusRegistered: false,
|
||||||
|
|
||||||
/* Sample data for starting dashboard(commonly used) */
|
/* Sample data for starting dashboard(commonly used) */
|
||||||
clients: [],
|
clients: [] as Array<User>,
|
||||||
history: [] as Array<TransactionItem>,
|
history: [] as Array<TransactionItem>,
|
||||||
|
|
||||||
// api based data
|
// api based data
|
||||||
|
@ -184,7 +236,7 @@ export const MainService = defineStore('main', {
|
||||||
this.totpState = state;
|
this.totpState = state;
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetchChartData(year: string) {
|
fetchChartData(year: string) {
|
||||||
// sampleDataKey= authors or datasets
|
// sampleDataKey= authors or datasets
|
||||||
axios
|
axios
|
||||||
.get(`/api/statistic/${year}`)
|
.get(`/api/statistic/${year}`)
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { createPinia } from 'pinia';
|
||||||
import { StyleService } from '@/Stores/style.service';
|
import { StyleService } from '@/Stores/style.service';
|
||||||
import { LayoutService } from '@/Stores/layout';
|
import { LayoutService } from '@/Stores/layout';
|
||||||
import { LocaleStore } from '@/Stores/locale';
|
import { LocaleStore } from '@/Stores/locale';
|
||||||
|
import { MainService } from './Stores/main';
|
||||||
import { darkModeKey, styleKey } from '@/config';
|
import { darkModeKey, styleKey } from '@/config';
|
||||||
import type { DefineComponent } from 'vue';
|
import type { DefineComponent } from 'vue';
|
||||||
import { resolvePageComponent } from '@adonisjs/inertia/helpers';
|
import { resolvePageComponent } from '@adonisjs/inertia/helpers';
|
||||||
|
@ -80,7 +81,7 @@ const layoutService = LayoutService(pinia);
|
||||||
const localeService = LocaleStore(pinia);
|
const localeService = LocaleStore(pinia);
|
||||||
|
|
||||||
localeService.initializeLocale();
|
localeService.initializeLocale();
|
||||||
// const mainService = MainService(pinia);
|
const mainService = MainService(pinia);
|
||||||
// mainService.setUser(user);
|
// mainService.setUser(user);
|
||||||
|
|
||||||
/* App style */
|
/* App style */
|
||||||
|
@ -90,6 +91,12 @@ styleService.setStyle(localStorage[styleKey] ?? 'basic');
|
||||||
if ((!localStorage[darkModeKey] && window.matchMedia('(prefers-color-scheme: dark)').matches) || localStorage[darkModeKey] === '1') {
|
if ((!localStorage[darkModeKey] && window.matchMedia('(prefers-color-scheme: dark)').matches) || localStorage[darkModeKey] === '1') {
|
||||||
styleService.setDarkMode(true);
|
styleService.setDarkMode(true);
|
||||||
}
|
}
|
||||||
|
// mainService.fetch('clients');
|
||||||
|
// mainService.fetch('history');
|
||||||
|
mainService.fetchApi('clients');
|
||||||
|
mainService.fetchApi('authors');
|
||||||
|
mainService.fetchApi('datasets');
|
||||||
|
mainService.fetchChartData("2022");
|
||||||
|
|
||||||
/* Collapse mobile aside menu on route change */
|
/* Collapse mobile aside menu on route change */
|
||||||
Inertia.on('navigate', () => {
|
Inertia.on('navigate', () => {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
mdiShieldCrownOutline,
|
mdiShieldCrownOutline,
|
||||||
mdiLicense,
|
mdiLicense,
|
||||||
mdiFileDocument,
|
mdiFileDocument,
|
||||||
|
mdiLibraryShelves
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
|
@ -111,6 +112,11 @@ export default [
|
||||||
icon: mdiPublish,
|
icon: mdiPublish,
|
||||||
label: 'Create Dataset',
|
label: 'Create Dataset',
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// route: 'dataset.categorize',
|
||||||
|
// icon: mdiLibraryShelves,
|
||||||
|
// label: 'Library Classification',
|
||||||
|
// },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -314,9 +314,11 @@ router
|
||||||
.as('dataset.deleteUpdate')
|
.as('dataset.deleteUpdate')
|
||||||
.use([middleware.auth(), middleware.can(['dataset-delete'])]);
|
.use([middleware.auth(), middleware.can(['dataset-delete'])]);
|
||||||
router.get('/person', [PersonController, 'index']).as('person.index').use([middleware.auth()]);
|
router.get('/person', [PersonController, 'index']).as('person.index').use([middleware.auth()]);
|
||||||
router.get('/dataset/categorize', ({ inertia }: HttpContext) => {
|
router
|
||||||
return inertia.render('Submitter/Dataset/Category');
|
.get('/dataset/:id/categorize', [DatasetController, 'categorize'])
|
||||||
});
|
.as('dataset.categorize')
|
||||||
|
.where('id', router.matchers.number())
|
||||||
|
.use([middleware.auth(), middleware.can(['dataset-edit'])]);
|
||||||
})
|
})
|
||||||
.prefix('submitter');
|
.prefix('submitter');
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,12 @@ import HomeController from '#controllers/Http/Api/HomeController';
|
||||||
import FileController from '#controllers/Http/Api/FileController';
|
import FileController from '#controllers/Http/Api/FileController';
|
||||||
import AvatarController from '#controllers/Http/Api/AvatarController';
|
import AvatarController from '#controllers/Http/Api/AvatarController';
|
||||||
import UserController from '#controllers/Http/Api/UserController';
|
import UserController from '#controllers/Http/Api/UserController';
|
||||||
|
import CollectionsController from '#controllers/Http/Api/collections_controller';
|
||||||
import { middleware } from '../kernel.js';
|
import { middleware } from '../kernel.js';
|
||||||
// API
|
// API
|
||||||
router
|
router
|
||||||
.group(() => {
|
.group(() => {
|
||||||
|
router.get('clients', [UserController, 'getSubmitters']).as('client.index');
|
||||||
router.get('authors', [AuthorsController, 'index']).as('author.index');
|
router.get('authors', [AuthorsController, 'index']).as('author.index');
|
||||||
router.get('datasets', [DatasetController, 'index']).as('dataset.index');
|
router.get('datasets', [DatasetController, 'index']).as('dataset.index');
|
||||||
router.get('persons', [AuthorsController, 'persons']).as('author.persons');
|
router.get('persons', [AuthorsController, 'persons']).as('author.persons');
|
||||||
|
@ -32,6 +34,8 @@ router
|
||||||
.post('/twofactor_backupcodes/settings/create', [UserController, 'createCodes'])
|
.post('/twofactor_backupcodes/settings/create', [UserController, 'createCodes'])
|
||||||
.as('apps.twofactor_backupcodes.create')
|
.as('apps.twofactor_backupcodes.create')
|
||||||
.use(middleware.auth());
|
.use(middleware.auth());
|
||||||
|
|
||||||
|
router.get('collections/:id', [CollectionsController, 'show']).as('collection.show')
|
||||||
})
|
})
|
||||||
// .namespace('App/Controllers/Http/Api')
|
// .namespace('App/Controllers/Http/Api')
|
||||||
.prefix('api');
|
.prefix('api');
|
||||||
|
|
|
@ -19,12 +19,63 @@ async function checkDoiExists(doi: string): Promise<boolean> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to check if ISBN exists using the Open Library API
|
// Function to check if ISBN exists using the Open Library API
|
||||||
|
// async function checkIsbnExists(isbn: string): Promise<boolean> {
|
||||||
|
// try {
|
||||||
|
// const response = await axios.get(`https://isbnsearch.org/isbn/${isbn}`);
|
||||||
|
// return response.status === 200 && response.data.includes('ISBN'); // Check if response contains ISBN information
|
||||||
|
// } catch (error) {
|
||||||
|
// return false; // If request fails, ISBN does not exist
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
async function checkIsbnExists(isbn: string): Promise<boolean> {
|
async function checkIsbnExists(isbn: string): Promise<boolean> {
|
||||||
|
// Try Open Library first
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`https://isbnsearch.org/isbn/${isbn}`);
|
const response = await axios.get(`https://openlibrary.org/api/books?bibkeys=ISBN:${isbn}&format=json&jscmd=data`);
|
||||||
return response.status === 200 && response.data.includes('ISBN'); // Check if response contains ISBN information
|
const data = response.data;
|
||||||
|
if (Object.keys(data).length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false; // If request fails, ISBN does not exist
|
// If an error occurs, continue to the next API
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to Google Books API
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`https://www.googleapis.com/books/v1/volumes?q=isbn:${isbn}`);
|
||||||
|
const data = response.data;
|
||||||
|
if (data.totalItems > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If an error occurs, continue to the next API
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lastly use the Koha library by scraping HTML
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`https://bibliothek.geosphere.at/cgi-bin/koha/opac-search.pl?idx=nb&q=${isbn}`);
|
||||||
|
const html = response.data;
|
||||||
|
// Check if zero results are explicitly indicated (German or English)
|
||||||
|
if (html.includes('Keine Treffer gefunden!') || html.includes('Your search returned 0 results')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Try to extract the count from German message
|
||||||
|
let match = html.match(/Ihre Suche erzielte\s*(\d+)\s*Treffer/);
|
||||||
|
|
||||||
|
// If not found, try the English equivalent
|
||||||
|
if (!match) {
|
||||||
|
match = html.match(/Your search returned\s*(\d+)\s*results/);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match && match[1]) {
|
||||||
|
const count = parseInt(match[1], 10);
|
||||||
|
return count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: if no match is found, return false
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,10 +93,10 @@ async function validateReference(value: unknown, options: Options, field: FieldC
|
||||||
try {
|
try {
|
||||||
const exists = await checkDoiExists(value);
|
const exists = await checkDoiExists(value);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
field.report('The {{ field }} must be an existing DOI', 'validateReference', field);
|
field.report('The {{ field }} must be an existing URL', 'validateReference', field);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
field.report('Error checking DOI existence: ' + error.message, 'validateReference', field);
|
field.report('Error checking URL existence: ' + error.message, 'validateReference', field);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue