Compare commits

..

No commits in common. "b540547e4cd98ce8296f58689dbf40141a75d954" and "537c6fd81aa7031c7805c59620eceb72b502da4c" have entirely different histories.

122 changed files with 10438 additions and 6295 deletions

View file

@ -17,6 +17,4 @@ REDIS_PORT=6379
REDIS_PASSWORD= REDIS_PASSWORD=
SMTP_HOST= SMTP_HOST=
SMTP_PORT= SMTP_PORT=
RESEND_API_KEY= RESEND_API_KEY=
OPENSEARCH_HOST=http://localhost
OPENSEARCH_CORE=tethys-records

View file

@ -1,78 +0,0 @@
# This is a Gitea Actions workflow configuration file for running CI tests on the `feat/checkReferenceType` branch.
# The workflow is named "CI" and runs on the latest Ubuntu environment using a Node.js 20 Docker container.
# It sets up a PostgreSQL service with specified environment variables and health checks.
# The workflow includes the following steps:
# 1. Checkout the repository using the actions/checkout@v3 action.
# 2. Install Node.js dependencies using `npm ci`.
# 3. Create a `.env.test` file by copying from `.env.example`.
# 4. Set up environment variables in the `.env.test` file, including database connection details and other app-specific settings.
# 5. Run functional tests using the `node ace test functional --groups "ReferenceValidation"` command.
name: CI
run-name: Running tests for checkReferenceType branch
on:
push:
branches:
- feat/checkReferenceType
jobs:
container-job:
runs-on: ubuntu-latest
# Docker Hub image that `container-job` executes in
container: node:20-bullseye
services:
# Label used to access the service container
postgres:
image: postgres:latest
env:
POSTGRES_USER: alice
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
POSTGRES_DB: tethys_dev
# ports:
# - 5432:5432
options: |
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout repository
uses: actions/checkout@v3
# - name: Set up Node.js
# uses: actions/setup-node@v2
# with:
# node-version: '20'
- name: Install dependencies
run: npm ci
- name: Create .env.test file
run: cp .env.example .env.test
- name: Set up environment variables
run: |
echo "DB_CONNECTION=pg" >> .env.test
echo "PG_HOST=postgres" >> .env.test
echo "PG_PORT=5432" >> .env.test
echo "PG_USER=alice" >> .env.test
echo "PG_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}" >> .env.test
echo "PG_DB_NAME=tethys_dev" >> .env.test
echo "NODE_ENV=test" >> .env.test
echo "ASSETS_DRIVER=fake" >> .env.test
echo "SESSION_DRIVER=memory" >> .env.test
echo "HASH_DRIVER=bcrypt" >> .env.test
echo "HOST=127.0.0.1" >> .env.test
echo "PORT=3333" >> .env.test
echo "APP_NAME=TethysCloud" >> .env.test
echo "APP_URL=http://${HOST}:${PORT}" >> .env.test
echo "CACHE_VIEWS=false" >> .env.test
echo "APP_KEY=pfi5N2ACN4tMJ5d8d8BPHfh3FEuvleej" >> .env.test
echo "DRIVE_DISK=local" >> .env.test
echo "OAI_LIST_SIZE=200" >> .env.test
echo "OPENSEARCH_HOST=${{ secrets.OPENSEARCH_HOST }}" >> .env.test
echo "OPENSEARCH_CORE=tethys-records" >> .env.test
- name: Run tests
run: node ace test functional --groups "ReferenceValidation"

View file

@ -4,13 +4,7 @@
name: CI Pipeline name: CI Pipeline
run-name: ${{ github.actor }} is running CI pipeline run-name: ${{ github.actor }} is running CI pipeline
# trigger build when pushing, or when creating a pull request # trigger build when pushing, or when creating a pull request
on: on: [push, pull_request]
push:
branches:
- master
pull_request:
branches:
- master
jobs: jobs:
# Label of the container job # Label of the container job
@ -18,7 +12,7 @@ jobs:
# run build on latest ubuntu # run build on latest ubuntu
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: node:20-bullseye container: node:18-bullseye
services: services:
mydb: mydb:
@ -76,7 +70,6 @@ jobs:
&& echo "CACHE_VIEWS=false" >> .env.test && echo "CACHE_VIEWS=false" >> .env.test
&& echo "APP_KEY=pfi5N2ACN4tMJ5d8d8BPHfh3FEuvleej" >> .env.test && echo "APP_KEY=pfi5N2ACN4tMJ5d8d8BPHfh3FEuvleej" >> .env.test
&& echo "DRIVE_DISK=local" >> .env.test && echo "DRIVE_DISK=local" >> .env.test
&& echo "OAI_LIST_SIZE=200" >> .env.test
# finally run the tests # finally run the tests
# - run: npm test # - run: npm test
@ -102,4 +95,3 @@ jobs:
# uses: coverallsapp/github-action@master # uses: coverallsapp/github-action@master
# with: # with:
# github-token: ${{ secrets.GITHUB_TOKEN }} # github-token: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View file

@ -7,4 +7,3 @@ coverage
tmp tmp
docker-compose.yml docker-compose.yml
.env.test .env.test
public/assets

View file

@ -1,7 +1,7 @@
################## First Stage - Creating base ######################### ################## First Stage - Creating base #########################
# Created a variable to hold our node base image # Created a variable to hold our node base image
ARG NODE_IMAGE=node:22-bookworm-slim ARG NODE_IMAGE=node:20-bookworm-slim
FROM $NODE_IMAGE AS base FROM $NODE_IMAGE AS base
# Install dumb-init and ClamAV, and perform ClamAV database update # Install dumb-init and ClamAV, and perform ClamAV database update

7
ace.js
View file

@ -15,11 +15,10 @@
/** /**
* Register hook to process TypeScript files using ts-node * Register hook to process TypeScript files using ts-node
*/ */
// import { register } from 'node:module'; import { register } from 'node:module'
// register('ts-node/esm', import.meta.url); register('ts-node/esm', import.meta.url)
import 'ts-node-maintained/register/esm';
/** /**
* Import ace console entrypoint * Import ace console entrypoint
*/ */
await import('./bin/console.js'); await import('./bin/console.js')

View file

@ -1,7 +1,7 @@
import { defineConfig } from '@adonisjs/core/app'; import { defineConfig } from '@adonisjs/core/app'
export default defineConfig({ export default defineConfig({
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Commands | Commands
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -10,11 +10,12 @@ export default defineConfig({
| will be scanned automatically from the "./commands" directory. | will be scanned automatically from the "./commands" directory.
*/ */
commands: [ commands: [
() => import('@adonisjs/core/commands'), () => import('@adonisjs/core/commands'),
() => import('@adonisjs/lucid/commands'), () => import('@adonisjs/lucid/commands'),
() => import('@adonisjs/mail/commands')], () => import('@adonisjs/mail/commands')
/* ],
/*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Preloads | Preloads
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -22,21 +23,19 @@ export default defineConfig({
| List of modules to import before starting the application. | List of modules to import before starting the application.
| |
*/ */
preloads: [ preloads: [
() => import('./start/routes.js'), () => import('./start/routes.js'),
() => import('./start/kernel.js'), () => import('./start/kernel.js'),
() => import('#start/validator'), () => import('#start/validator'),
() => import('#start/rules/unique'), () => import('#start/rules/unique'),
() => import('#start/rules/translated_language'), () => import('#start/rules/translated_language'),
() => import('#start/rules/unique_person'), () => import('#start/rules/unique_person'),
() => import('#start/rules/file_length'), () => import('#start/rules/file_length'),
() => import('#start/rules/file_scan'), () => import('#start/rules/file_scan'),
() => import('#start/rules/allowed_extensions_mimetypes'), () => import('#start/rules/allowed_extensions_mimetypes'),
() => import('#start/rules/dependent_array_min_length'), () => import('#start/rules/dependent_array_min_length')
() => import('#start/rules/referenceValidation'), ],
() => import('#start/rules/valid_mimetype'), /*
],
/*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Service providers | Service providers
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -45,49 +44,48 @@ export default defineConfig({
| application | application
| |
*/ */
providers: [ providers: [
// () => import('./providers/AppProvider.js'), // () => import('./providers/AppProvider.js'),
() => import('@adonisjs/core/providers/app_provider'), () => import('@adonisjs/core/providers/app_provider'),
() => import('@adonisjs/core/providers/hash_provider'), () => import('@adonisjs/core/providers/hash_provider'),
{ {
file: () => import('@adonisjs/core/providers/repl_provider'), file: () => import('@adonisjs/core/providers/repl_provider'),
environment: ['repl', 'test'], environment: ['repl', 'test'],
}, },
() => import('@adonisjs/session/session_provider'), () => import('@adonisjs/session/session_provider'),
() => import('@adonisjs/core/providers/edge_provider'), () => import('@adonisjs/core/providers/edge_provider'),
() => import('@adonisjs/shield/shield_provider'), () => import('@adonisjs/shield/shield_provider'),
// () => import('@eidellev/inertia-adonisjs'), // () => import('@eidellev/inertia-adonisjs'),
// () => import('@adonisjs/inertia/inertia_provider'), // () => import('@adonisjs/inertia/inertia_provider'),
() => import('#providers/app_provider'), () => import('#providers/app_provider'),
() => import('#providers/inertia_provider'), () => import('#providers/inertia_provider'),
() => import('@adonisjs/lucid/database_provider'), () => import('@adonisjs/lucid/database_provider'),
() => import('@adonisjs/auth/auth_provider'), () => import('@adonisjs/auth/auth_provider'),
// () => import('@eidellev/adonis-stardust'), // () => import('@eidellev/adonis-stardust'),
() => import('@adonisjs/redis/redis_provider'), () => import('@adonisjs/redis/redis_provider'),
// () => import('@adonisjs/encore/encore_provider'), () => import('@adonisjs/encore/encore_provider'),
() => import('@adonisjs/static/static_provider'), () => import('@adonisjs/static/static_provider'),
() => import('#providers/stardust_provider'), () => import('#providers/stardust_provider'),
() => import('#providers/query_builder_provider'), () => import('#providers/query_builder_provider'),
() => import('#providers/token_worker_provider'), () => import('#providers/token_worker_provider'),
// () => import('#providers/validator_provider'), // () => import('#providers/validator_provider'),
// () => import('#providers/drive/provider/drive_provider'), () => import('#providers/drive/provider/drive_provider'),
() => import('@adonisjs/drive/drive_provider'), // () => import('@adonisjs/core/providers/vinejs_provider'),
// () => import('@adonisjs/core/providers/vinejs_provider'), () => import('#providers/vinejs_provider'),
() => import('#providers/vinejs_provider'), () => import('@adonisjs/mail/mail_provider')
() => import('@adonisjs/mail/mail_provider'), // () => import('#providers/mail_provider'),
() => import('@adonisjs/vite/vite_provider'), ],
], metaFiles: [
metaFiles: [ {
{ pattern: 'public/**',
pattern: 'public/**', reloadServer: false,
reloadServer: false, },
}, {
{ pattern: 'resources/views/**/*.edge',
pattern: 'resources/views/**/*.edge', reloadServer: false,
reloadServer: false, },
}, ],
], /*
/*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Tests | Tests
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -96,24 +94,22 @@ export default defineConfig({
| and add additional suites. | and add additional suites.
| |
*/ */
tests: { tests: {
suites: [ suites: [
{ {
files: ['tests/unit/**/*.spec(.ts|.js)'], files: ['tests/unit/**/*.spec(.ts|.js)'],
name: 'unit', name: 'unit',
timeout: 2000, timeout: 2000,
}, },
{ {
files: ['tests/functional/**/*.spec(.ts|.js)'], files: ['tests/functional/**/*.spec(.ts|.js)'],
name: 'functional', name: 'functional',
timeout: 30000, timeout: 30000,
}, },
], ],
forceExit: false, forceExit: false,
}, },
assetsBundler: false,
hooks: {
onBuildStarting: [() => import('@adonisjs/vite/build_hook')],
}, })
// assetsBundler: false
});

View file

@ -25,7 +25,6 @@ export default class MimetypeController {
const newDatasetSchema = vine.object({ const newDatasetSchema = vine.object({
name: vine.string().trim().isUnique({ table: 'mime_types', column: 'name' }), name: vine.string().trim().isUnique({ table: 'mime_types', column: 'name' }),
file_extension: vine.array(vine.string()).minLength(1), // define at least one extension for the new mimetype file_extension: vine.array(vine.string()).minLength(1), // define at least one extension for the new mimetype
alternate_mimetype: vine.array(vine.string().isValidMimetype()).distinct().optional(), // define alias mimetypes
enabled: vine.boolean(), enabled: vine.boolean(),
}); });
// await request.validate({ schema: newDatasetSchema, messages: this.messages }); // await request.validate({ schema: newDatasetSchema, messages: this.messages });
@ -33,22 +32,18 @@ export default class MimetypeController {
// Step 2 - Validate request body against the schema // Step 2 - Validate request body against the schema
// await request.validate({ schema: newDatasetSchema, messages: this.messages }); // await request.validate({ schema: newDatasetSchema, messages: this.messages });
const validator = vine.compile(newDatasetSchema); const validator = vine.compile(newDatasetSchema);
validator.messagesProvider = new SimpleMessagesProvider(this.messages); validator.messagesProvider = new SimpleMessagesProvider(this.messages);
await request.validateUsing(validator, { messagesProvider: new SimpleMessagesProvider(this.messages) }); await request.validateUsing(validator);
} catch (error) { } catch (error) {
// Step 3 - Handle errors // Step 3 - Handle errors
// return response.badRequest(error.messages); // return response.badRequest(error.messages);
throw error; throw error;
} }
const input = request.only(['name', 'enabled', 'file_extension', 'alternate_mimetype']); const input = request.only(['name', 'enabled', 'file_extension']);
// Concatenate the file_extensions array into a string with '|' as the separator // Concatenate the file_extensions array into a string with '|' as the separator
if (Array.isArray(input.file_extension)) { if (Array.isArray(input.file_extension)) {
input.file_extension = input.file_extension.join('|'); input.file_extension = input.file_extension.join('|');
} }
// Concatenate the alias_mimetype array into a string with '|' as the separator
if (Array.isArray(input.alternate_mimetype)) {
input.alternate_mimetype = input.alternate_mimetype.join('|');
}
await MimeType.create(input); await MimeType.create(input);
// if (request.input('roles')) { // if (request.input('roles')) {
// const roles: Array<number> = request.input('roles'); // const roles: Array<number> = request.input('roles');

View file

@ -9,14 +9,12 @@ 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;
} }

View file

@ -1,135 +1,65 @@
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'; // import * as fs from 'fs';
// import * as path from 'path';
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;
// node ace make:controller Author
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 = DEFAULT_SIZE } = request.only(['name', 'size']); const { name, background, textColor, size } = request.only(['name', 'background', 'textColor', '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);
}
// Generate initials
// const initials = name
// .split(' ')
// .map((part) => part.charAt(0).toUpperCase())
// .join('');
const initials = this.getInitials(name); const initials = this.getInitials(name);
const colors = this.generateColors(name);
const svgContent = this.createSvg(size, colors, initials);
// // Cache the generated avatar for future use, e.g. 1 hour expiry // Define SVG content with dynamic values for initials, background color, text color, and size
await redis.setex(cacheKey, 3600, svgContent); const svgContent = `
<svg width="${size || 50}" height="${size || 50}" xmlns="http://www.w3.org/2000/svg">
this.setResponseHeaders(response); <rect width="100%" height="100%" fill="#${background || '7F9CF5'}"/>
return response.send(svgContent); <text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-weight="bold" font-family="Arial, sans-serif" font-size="${
} catch (error) { (size / 100) * 40 || 25
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: error.message }); }" fill="#${textColor || 'ffffff'}">${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>
`; `;
// Set response headers for SVG content
response.header('Content-type', 'image/svg+xml');
response.header('Cache-Control', 'no-cache');
response.header('Pragma', 'no-cache');
response.header('Expires', '0');
return response.send(svgContent);
} catch (error) {
return response.status(StatusCodes.OK).json({ error: error.message });
}
} }
private setResponseHeaders(response: HttpContext['response']): void { private getInitials(name: string) {
response.header('Content-type', 'image/svg+xml'); const parts = name.split(' ');
response.header('Cache-Control', 'no-cache'); let initials = '';
response.header('Pragma', 'no-cache');
response.header('Expires', '0');
}
private getColorFromName(name: string): string { if (parts.length >= 2) {
let hash = 0; const firstName = parts[0];
for (let i = 0; i < name.length; i++) { const lastName = parts[parts.length - 1];
hash = name.charCodeAt(i) + ((hash << 5) - hash);
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();
} }
const colorParts = []; return initials;
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff;
colorParts.push(value.toString(16).padStart(2, '0'));
}
return colorParts.join('');
}
private lightenColor(hexColor: string, percent: number): string {
const r = parseInt(hexColor.substring(0, 2), 16);
const g = parseInt(hexColor.substring(2, 4), 16);
const b = parseInt(hexColor.substring(4, 6), 16);
const lightenValue = (value: number) => Math.min(255, Math.floor((value * (100 + percent)) / 100));
const newR = lightenValue(r);
const newG = lightenValue(g);
const newB = lightenValue(b);
return ((newR << 16) | (newG << 8) | newB).toString(16).padStart(6, '0');
}
private darkenColor(hexColor: string): string {
const r = parseInt(hexColor.slice(0, 2), 16);
const g = parseInt(hexColor.slice(2, 4), 16);
const b = parseInt(hexColor.slice(4, 6), 16);
const darkenValue = (value: number) => Math.round(value * COLOR_DARKENING_FACTOR);
const darkerR = darkenValue(r);
const darkerG = darkenValue(g);
const darkerB = darkenValue(b);
return ((darkerR << 16) + (darkerG << 8) + darkerB).toString(16).padStart(6, '0');
} }
} }

View file

@ -6,15 +6,10 @@ 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 datasets with server_state 'published' or 'deleted' and sort by the last published date // select * from gba.persons
const datasets = await Dataset.query() // where exists (select * from gba.documents inner join gba.link_documents_persons on "documents"."id" = "link_documents_persons"."document_id"
.where(function (query) { // where ("link_documents_persons"."role" = 'author') and ("persons"."id" = "link_documents_persons"."person_id"));
query.where('server_state', 'published') const datasets = await Dataset.query().where('server_state', 'published').orWhere('server_state', 'deleted');
.orWhere('server_state', 'deleted');
})
.preload('titles')
.preload('identifier')
.orderBy('server_date_published', 'desc');
return datasets; return datasets;
} }

View file

@ -14,7 +14,7 @@ export default class FileController {
// where: { id: id }, // where: { id: id },
// }); // });
if (file) { if (file) {
const filePath = '/storage/app/data/' + file.pathName; const filePath = '/storage/app/public/' + file.pathName;
const ext = path.extname(filePath); const ext = path.extname(filePath);
const fileName = file.label + ext; const fileName = file.label + ext;
try { try {

View file

@ -9,24 +9,6 @@ import BackupCode from '#models/backup_code';
// Here we are generating secret and recovery codes for the user thats enabling 2FA and storing them to our database. // Here we are generating secret and recovery codes for the user thats 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');

View file

@ -1,36 +0,0 @@
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,
});
}
}

View file

@ -6,11 +6,6 @@ import hash from '@adonisjs/core/services/hash';
// import { schema, rules } from '@adonisjs/validator'; // import { schema, rules } from '@adonisjs/validator';
import vine from '@vinejs/vine'; import vine from '@vinejs/vine';
import BackupCodeStorage, { SecureRandom } from '#services/backup_code_storage'; import BackupCodeStorage, { SecureRandom } from '#services/backup_code_storage';
import path from 'path';
import crypto from 'crypto';
// import drive from '#services/drive';
import drive from '@adonisjs/drive/services/main';
import logger from '@adonisjs/core/services/logger';
// Here we are generating secret and recovery codes for the user thats enabling 2FA and storing them to our database. // Here we are generating secret and recovery codes for the user thats enabling 2FA and storing them to our database.
export default class UserController { export default class UserController {
@ -33,7 +28,7 @@ export default class UserController {
user: user, user: user,
twoFactorEnabled: user.isTwoFactorEnabled, twoFactorEnabled: user.isTwoFactorEnabled,
// code: await TwoFactorAuthProvider.generateQrCode(user), // code: await TwoFactorAuthProvider.generateQrCode(user),
backupState: backupState, backupState: backupState,
}); });
} }
@ -45,8 +40,10 @@ export default class UserController {
// }); // });
const passwordSchema = vine.object({ const passwordSchema = vine.object({
// first step // first step
old_password: vine.string().trim(), old_password: vine
// .regex(/^[a-zA-Z0-9]+$/), .string()
.trim()
.regex(/^[a-zA-Z0-9]+$/),
new_password: vine.string().confirmed({ confirmationField: 'confirm_password' }).trim().minLength(8).maxLength(255), new_password: vine.string().confirmed({ confirmationField: 'confirm_password' }).trim().minLength(8).maxLength(255),
}); });
try { try {
@ -57,9 +54,9 @@ export default class UserController {
// return response.badRequest(error.messages); // return response.badRequest(error.messages);
throw error; throw error;
} }
try { try {
const user = (await auth.user) as User; const user = await auth.user as User;
const { old_password, new_password } = request.only(['old_password', 'new_password']); const { old_password, new_password } = request.only(['old_password', 'new_password']);
// if (!(old_password && new_password && confirm_password)) { // if (!(old_password && new_password && confirm_password)) {
@ -85,171 +82,6 @@ export default class UserController {
} }
} }
public async profile({ inertia, auth }: HttpContext) {
const user = await User.find(auth.user?.id);
// let test = await drive.use().getUrl(user?.avatar);
// user?.preload('roles');
const avatarFullPathUrl = user?.avatar ? await drive.use('public').getUrl(user.avatar) : null;
return inertia.render('profile/show', {
user: user,
defaultUrl: avatarFullPathUrl,
});
}
/**
* Update the user's profile information.
*
* @param {HttpContext} ctx - The HTTP context object.
* @returns {Promise<void>}
*/
public async profileUpdate({ auth, request, response, session }: HttpContext) {
if (!auth.user) {
session.flash('error', 'You must be logged in to update your profile.');
return response.redirect().toRoute('login');
}
const updateProfileValidator = vine.withMetaData<{ userId: number }>().compile(
vine.object({
first_name: vine.string().trim().minLength(4).maxLength(255),
last_name: vine.string().trim().minLength(4).maxLength(255),
login: vine.string().trim().minLength(4).maxLength(255),
email: vine
.string()
.trim()
.maxLength(255)
.email()
.normalizeEmail()
.isUnique({ table: 'accounts', column: 'email', whereNot: (field) => field.meta.userId }),
avatar: vine
.myfile({
size: '2mb',
extnames: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'],
})
// .allowedMimetypeExtensions({
// allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'],
// })
.optional(),
}),
);
const user = await User.find(auth.user.id);
if (!user) {
session.flash('error', 'User not found.');
return response.redirect().toRoute('login');
}
try {
// validate update form
await request.validateUsing(updateProfileValidator, {
meta: {
userId: user.id,
},
});
const { login, email, first_name, last_name } = request.only(['login', 'email', 'first_name', 'last_name']);
const sanitizedData: { [key: string]: any } = {
login: login?.trim(),
email: email?.toLowerCase().trim(),
first_name: first_name?.trim(),
last_name: last_name?.trim(),
// avatar: "",
};
const toCamelCase = (str: string) => str.replace(/_([a-z])/g, (g) => g[1].toUpperCase());
const hasInputChanges = Object.keys(sanitizedData).some((key) => {
const camelKey = toCamelCase(key);
return sanitizedData[key] !== (user.$attributes as { [key: string]: any })[camelKey];
});
let hasAvatarChanged = false;
const avatar = request.file('avatar');
if (avatar) {
const fileHash = crypto
.createHash('sha256')
.update(avatar.clientName + avatar.size)
.digest('hex');
const fileName = `avatar-${fileHash}.${avatar.extname}`;
const avatarFullPath = path.join('/uploads', `${user.login}`, fileName);
if (user.avatar != avatarFullPath) {
if (user.avatar) {
await drive.use('public').delete(user.avatar);
}
hasAvatarChanged = user.avatar !== avatarFullPath;
await avatar.moveToDisk(avatarFullPath, 'public', {
name: fileName,
overwrite: true, // overwrite in case of conflict
disk: 'public',
});
sanitizedData.avatar = avatarFullPath;
}
}
if (!hasInputChanges && !hasAvatarChanged) {
session.flash('message', 'No changes were made.');
return response.redirect().back();
}
await user.merge(sanitizedData).save();
session.flash('message', 'User has been updated successfully');
return response.redirect().toRoute('settings.profile.edit');
} catch (error) {
logger.error('Profile update failed:', error);
// session.flash('errors', 'Profile update failed. Please try again.');
// return response.redirect().back();
throw error;
}
}
public async passwordUpdate({ auth, request, response, session }: HttpContext) {
// const passwordSchema = schema.create({
// old_password: schema.string({ trim: true }, [rules.required()]),
// new_password: schema.string({ trim: true }, [rules.minLength(8), rules.maxLength(255), rules.confirmed('confirm_password')]),
// confirm_password: schema.string({ trim: true }, [rules.required()]),
// });
const passwordSchema = vine.object({
// first step
old_password: vine.string().trim(),
// .regex(/^[a-zA-Z0-9]+$/),
new_password: vine.string().confirmed({ confirmationField: 'confirm_password' }).trim().minLength(8).maxLength(255),
});
try {
// await request.validate({ schema: passwordSchema });
const validator = vine.compile(passwordSchema);
await request.validateUsing(validator);
} catch (error) {
// return response.badRequest(error.messages);
throw error;
}
try {
const user = (await auth.user) as User;
const { old_password, new_password } = request.only(['old_password', 'new_password']);
// if (!(old_password && new_password && confirm_password)) {
// return response.status(400).send({ warning: 'Old password and new password are required.' });
// }
// Verify if the provided old password matches the user's current password
const isSame = await hash.verify(user.password, old_password);
if (!isSame) {
session.flash('warning', 'Old password is incorrect.');
return response.redirect().back();
// return response.flash('warning', 'Old password is incorrect.').redirect().back();
}
// Hash the new password before updating the user's password
user.password = new_password;
await user.save();
// return response.status(200).send({ message: 'Password updated successfully.' });
session.flash({ message: 'Password updated successfully.' });
return response.redirect().toRoute('settings.profile.edit');
} catch (error) {
// return response.status(500).send({ message: 'Internal server error.' });
return response.flash('warning', `Invalid server state. Internal server error.`).redirect().back();
}
}
public async enableTwoFactorAuthentication({ auth, response, session }: HttpContext): Promise<void> { public async enableTwoFactorAuthentication({ auth, response, session }: HttpContext): Promise<void> {
// const user: User | undefined = auth?.user; // const user: User | undefined = auth?.user;
const user = (await User.find(auth.user?.id)) as User; const user = (await User.find(auth.user?.id)) as User;
@ -283,7 +115,7 @@ export default class UserController {
} else { } else {
session.flash('error', 'User not found.'); session.flash('error', 'User not found.');
} }
return response.redirect().back(); return response.redirect().back();
// return inertia.render('Auth/AccountInfo', { // return inertia.render('Auth/AccountInfo', {
// // status: { // // status: {

View file

@ -19,13 +19,14 @@ 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;
} }
@ -34,14 +35,12 @@ interface Dictionary {
[index: string]: string; [index: string]: string;
} }
interface PagingParameter { interface ListParameter {
cursor: number; cursor: number;
totalLength: number; totalIds: number;
start: number; start: number;
nextDocIds: number[]; reldocIds: (number | null)[];
activeWorkIds: number[];
metadataPrefix: string; metadataPrefix: string;
queryParams: Object;
} }
@inject() @inject()
@ -50,7 +49,6 @@ 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.
* *
@ -59,6 +57,7 @@ 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');
@ -86,9 +85,9 @@ export default class OaiController {
let earliestDateFromDb; let earliestDateFromDb;
// const oaiRequest: OaiParameter = request.body; // const oaiRequest: OaiParameter = request.body;
try { try {
this.firstPublishedDataset = await Dataset.earliestPublicationDate(); const firstPublishedDataset: Dataset | null = await Dataset.earliestPublicationDate();
this.firstPublishedDataset != null && firstPublishedDataset != null &&
(earliestDateFromDb = this.firstPublishedDataset.server_date_published.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")); (earliestDateFromDb = 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);
@ -163,19 +162,22 @@ 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') {
// Get browser fingerprint from the request: await this.handleListRecords(oaiRequest);
const browserFingerprint = this.getBrowserFingerprint(request);
await this.handleListRecords(oaiRequest, browserFingerprint);
} else if (verb == 'ListIdentifiers') { } else if (verb == 'ListIdentifiers') {
// Get browser fingerprint from the request: await this.handleListIdentifiers(oaiRequest);
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.',
@ -185,11 +187,11 @@ export default class OaiController {
} }
protected handleIdentify() { protected handleIdentify() {
// Get configuration values from environment or a dedicated configuration service const email = process.env.OAI_EMAIL || 'repository@geosphere.at';
const email = process.env.OAI_EMAIL ?? 'repository@geosphere.at'; const repositoryName = 'Tethys RDR';
const repositoryName = process.env.OAI_REPOSITORY_NAME ?? 'Tethys RDR'; const repIdentifier = 'tethys.at';
const repIdentifier = process.env.OAI_REP_IDENTIFIER ?? 'tethys.at'; const sampleIdentifier = 'oai:' + repIdentifier + ':1'; //$this->_configuration->getSampleIdentifier();
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);
@ -214,7 +216,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()),
@ -232,15 +234,7 @@ 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')
@ -257,61 +251,59 @@ 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);
// Build the XML for the dataset record and add it to the root node // add xml elements
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, browserFingerprint: string) { protected async handleListIdentifiers(oaiRequest: Dictionary) {
if (!this.tokenWorker.isConnected) { !this.tokenWorker.isConnected && (await this.tokenWorker.connect());
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, browserFingerprint); await this.handleLists(oaiRequest, maxIdentifier);
} }
protected async handleListRecords(oaiRequest: Dictionary, browserFingerprint: string) { protected async handleListRecords(oaiRequest: Dictionary) {
if (!this.tokenWorker.isConnected) { !this.tokenWorker.isConnected && (await this.tokenWorker.connect());
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, browserFingerprint); await this.handleLists(oaiRequest, maxRecords);
} }
private async handleLists(oaiRequest: Dictionary, maxRecords: number, browserFingerprint: string) { private async handleLists(oaiRequest: Dictionary, maxRecords: number) {
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');
const paginationParams: PagingParameter ={ // list initialisation
const numWrapper: ListParameter = {
cursor: 0, cursor: 0,
totalLength: 0, totalIds: 0,
start: maxRecords + 1, start: maxRecords + 1,
nextDocIds: [], reldocIds: [],
activeWorkIds: [],
metadataPrefix: '', metadataPrefix: '',
queryParams: {},
}; };
// resumptionToken is defined
if ('resumptionToken' in oaiRequest) { if ('resumptionToken' in oaiRequest) {
await this.handleResumptionToken(oaiRequest, maxRecords, paginationParams); await this.handleResumptionToken(oaiRequest, maxRecords, numWrapper);
} else { } else {
await this.handleNoResumptionToken(oaiRequest, paginationParams, maxRecords); // no resumptionToken is given
await this.handleNoResumptionToken(oaiRequest, numWrapper);
} }
const nextIds: number[] = paginationParams.nextDocIds; // handling of document ids
const workIds: number[] = paginationParams.activeWorkIds; const restIds = numWrapper.reldocIds as number[];
const workIds = restIds.splice(0, maxRecords) as number[]; // array_splice(restIds, 0, maxRecords);
if (workIds.length === 0) { // no records returned
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.',
@ -319,218 +311,169 @@ export default class OaiController {
); );
} }
const datasets = await Dataset.query() const datasets: Dataset[] = 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 handleNoResumptionToken(oaiRequest: Dictionary, paginationParams: PagingParameter, maxRecords: number) { private async handleResumptionToken(oaiRequest: Dictionary, maxRecords: number, numWrapper: ListParameter) {
this.validateMetadataPrefix(oaiRequest, paginationParams); const resParam = oaiRequest['resumptionToken']; //e.g. "158886496600000"
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);
} }
// this.setResumptionParameters(token, maxRecords, paginationParams); numWrapper.cursor = token.startPosition - 1; //startet dann bei Index 10
paginationParams.cursor = token.startPosition - 1; numWrapper.start = token.startPosition + maxRecords;
paginationParams.start = token.startPosition + maxRecords; numWrapper.totalIds = token.totalIds;
paginationParams.totalLength = token.totalIds; numWrapper.reldocIds = token.documentIds;
paginationParams.activeWorkIds = token.documentIds; numWrapper.metadataPrefix = token.metadataPrefix;
paginationParams.metadataPrefix = token.metadataPrefix;
paginationParams.queryParams = token.queryParams;
this.xsltParameter['oai_metadataPrefix'] = token.metadataPrefix;
const finder = this.buildDatasetQueryViaToken(token); this.xsltParameter['oai_metadataPrefix'] = numWrapper.metadataPrefix;
const nextRecords: Dataset[] = await this.fetchNextRecords(finder, token, maxRecords);
paginationParams.nextDocIds = nextRecords.map((dat) => Number(dat.publish_id));
} }
private async setResumptionToken(nextIds: number[], paginationParams: PagingParameter, browserFingerprint: string) { private async handleNoResumptionToken(oaiRequest: Dictionary, numWrapper: ListParameter) {
const countRestIds = nextIds.length; // no resumptionToken is given
if (countRestIds > 0) { if ('metadataPrefix' in oaiRequest) {
// const token = this.createResumptionToken(paginationParams, nextIds); numWrapper.metadataPrefix = oaiRequest['metadataPrefix'];
const token = new ResumptionToken(); } else {
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,
); );
} }
paginationParams.metadataPrefix = oaiRequest['metadataPrefix']; this.xsltParameter['oai_metadataPrefix'] = numWrapper.metadataPrefix;
this.xsltParameter['oai_metadataPrefix'] = paginationParams.metadataPrefix;
}
private applySetFilter(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, queryParams: any) { let finder: ModelQueryBuilderContract<typeof Dataset, Dataset> = Dataset.query();
if ('set' in queryParams) { // add server state restrictions
const [setType, setValue] = queryParams['set'].split(':'); finder.whereIn('server_state', this.deliveringDocumentStates);
if ('set' in oaiRequest) {
const set = oaiRequest['set'] as string;
const setArray = set.split(':');
switch (setType) { if (setArray[0] == 'data-type') {
case 'data-type': if (setArray.length == 2 && setArray[1]) {
setValue && finder.where('type', setValue); finder.where('type', setArray[1]);
break; }
case 'open_access': } else if (setArray[0] == 'open_access') {
finder.andWhereHas('licenses', (query) => { const openAccessLicences = ['CC-BY-4.0', 'CC-BY-SA-4.0'];
query.whereIn('name', ['CC-BY-4.0', 'CC-BY-SA-4.0']); finder.andWhereHas('licenses', (query) => {
query.whereIn('name', openAccessLicences);
});
} else if (setArray[0] == 'ddc') {
if (setArray.length == 2 && setArray[1] != '') {
finder.andWhereHas('collections', (query) => {
query.where('number', setArray[1]);
}); });
break; }
case 'ddc':
setValue &&
finder.andWhereHas('collections', (query) => {
query.where('number', setValue);
});
break;
} }
} }
}
private applyDateFilters(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, queryParams: any) { // const timeZone = "Europe/Vienna"; // Canonical time zone name
const { from, until } = queryParams; // &from=2020-09-03&until2020-09-03
// &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 && until) { if (from.length != until.length) {
this.handleFromUntilFilter(finder, from, until); throw new OaiModelException(
} else if (from) { StatusCodes.INTERNAL_SERVER_ERROR,
this.handleFromFilter(finder, from); 'The request has different granularities for the from and until parameters.',
} else if (until) { OaiErrorCodes.BADARGUMENT,
this.handleUntilFilter(finder, until); );
} }
} fromDate.hour() == 0 && (fromDate = fromDate.startOf('day'));
untilDate.hour() == 0 && (untilDate = untilDate.endOf('day'));
private handleFromUntilFilter(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, from: string, until: string) { finder.whereBetween('server_date_published', [fromDate.format('YYYY-MM-DD HH:mm:ss'), untilDate.format('YYYY-MM-DD HH:mm:ss')]);
const fromDate = this.parseDateWithValidation(from, 'From'); } else if ('from' in oaiRequest && !('until' in oaiRequest)) {
const untilDate = this.parseDateWithValidation(until, 'Until'); 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'));
if (from.length !== until.length) { const now = dayjs();
throw new OaiModelException( if (fromDate.isAfter(now)) {
StatusCodes.INTERNAL_SERVER_ERROR, throw new OaiModelException(
'The request has different granularities for the from and until parameters.', StatusCodes.INTERNAL_SERVER_ERROR,
OaiErrorCodes.BADARGUMENT, 'Given from date is greater than now. The given values results in an empty list.',
); OaiErrorCodes.NORECORDSMATCH,
);
} else {
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;
const earliestPublicationDate = dayjs(firstPublishedDataset.server_date_published.toISO()); //format("YYYY-MM-DDThh:mm:ss[Z]"));
if (earliestPublicationDate.isAfter(untilDate)) {
throw new OaiModelException(
StatusCodes.INTERNAL_SERVER_ERROR,
`earliestDatestamp is greater than given until date.
The given values results in an empty list.`,
OaiErrorCodes.NORECORDSMATCH,
);
} else {
finder.andWhere('server_date_published', '<=', 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')]); let reldocIdsDocs = await finder.select('publish_id').orderBy('publish_id');
} numWrapper.reldocIds = reldocIdsDocs.map((dat) => dat.publish_id);
numWrapper.totalIds = numWrapper.reldocIds.length; //212
private handleFromFilter(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, from: string) {
const fromDate = this.parseDateWithValidation(from, 'From');
const now = dayjs();
if (fromDate.isAfter(now)) {
throw new OaiModelException(
StatusCodes.INTERNAL_SERVER_ERROR,
'Given from date is greater than now. The given values results in an empty list.',
OaiErrorCodes.NORECORDSMATCH,
);
}
finder.andWhere('server_date_published', '>=', fromDate.format('YYYY-MM-DD HH:mm:ss'));
}
private handleUntilFilter(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, until: string) {
const untilDate = this.parseDateWithValidation(until, 'Until');
const earliestPublicationDate = dayjs(this.firstPublishedDataset?.server_date_published.toISO());
if (earliestPublicationDate.isAfter(untilDate)) {
throw new OaiModelException(
StatusCodes.INTERNAL_SERVER_ERROR,
'earliestDatestamp is greater than given until date. The given values results in an empty list.',
OaiErrorCodes.NORECORDSMATCH,
);
}
finder.andWhere('server_date_published', '<=', untilDate.format('YYYY-MM-DD HH:mm:ss'));
}
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) {
@ -698,30 +641,4 @@ 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}`;
}
} }

View file

@ -276,7 +276,7 @@ export default class DatasetsController {
validateSMTP: false, validateSMTP: false,
}); });
const validRecipientEmail: boolean = validationResult.valid; const validRecipientEmail: boolean = validationResult.valid;
// let emailStatusMessage = ''; let emailStatusMessage = '';
if (sendMail == true) { if (sendMail == true) {
if (dataset.editor.email && validRecipientEmail) { if (dataset.editor.email && validRecipientEmail) {
@ -289,7 +289,7 @@ export default class DatasetsController {
<p>Best regards,<br>Your Tethys reviewer: ${authUser.login}</p> <p>Best regards,<br>Your Tethys reviewer: ${authUser.login}</p>
`); `);
}); });
// emailStatusMessage = ` A rejection email was successfully sent to ${dataset.editor.email}.`; emailStatusMessage = ` A rejection email was successfully sent to ${dataset.editor.email}.`;
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
return response return response
@ -297,7 +297,7 @@ export default class DatasetsController {
.toRoute('reviewer.dataset.list'); .toRoute('reviewer.dataset.list');
} }
} else { } else {
// emailStatusMessage = ` However, the email could not be sent because the editor's email address (${dataset.editor.email}) is not valid.`; emailStatusMessage = ` However, the email could not be sent because the editor's email address (${dataset.editor.email}) is not valid.`;
} }
} }

View file

@ -8,7 +8,6 @@ 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';
@ -34,9 +33,7 @@ import File from '#models/file';
import ClamScan from 'clamscan'; import ClamScan from 'clamscan';
// import { ValidationException } from '@adonisjs/validator'; // import { ValidationException } from '@adonisjs/validator';
// import Drive from '@ioc:Adonis/Core/Drive'; // import Drive from '@ioc:Adonis/Core/Drive';
// import drive from '#services/drive'; import drive from '#services/drive';
import drive from '@adonisjs/drive/services/main';
import path from 'path';
import { Exception } from '@adonisjs/core/exceptions'; import { Exception } from '@adonisjs/core/exceptions';
import { MultipartFile } from '@adonisjs/core/types/bodyparser'; import { MultipartFile } from '@adonisjs/core/types/bodyparser';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
@ -366,8 +363,7 @@ export default class DatasetController {
references: vine references: vine
.array( .array(
vine.object({ vine.object({
// value: vine.string().trim().minLength(3).maxLength(255), value: vine.string().trim().minLength(3).maxLength(255),
value: vine.string().trim().minLength(3).maxLength(255).validateReference({ typeField: 'type' }),
type: vine.enum(Object.values(ReferenceIdentifierTypes)), type: vine.enum(Object.values(ReferenceIdentifierTypes)),
relation: vine.enum(Object.values(RelationTypes)), relation: vine.enum(Object.values(RelationTypes)),
label: vine.string().trim().minLength(2).maxLength(255), label: vine.string().trim().minLength(2).maxLength(255),
@ -502,7 +498,7 @@ export default class DatasetController {
} }
// save collection // save collection
const collection: Collection | null = await Collection.query().where('id', 594).first(); const collection: Collection | null = await Collection.query().where('id', 21).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
@ -535,18 +531,11 @@ export default class DatasetController {
const fileName = this.generateFilename(file.extname as string); const fileName = this.generateFilename(file.extname as string);
const mimeType = file.headers['content-type'] || 'application/octet-stream'; // Fallback to a default MIME type const mimeType = file.headers['content-type'] || 'application/octet-stream'; // Fallback to a default MIME type
const datasetFolder = `files/${dataset.id}`; const datasetFolder = `files/${dataset.id}`;
const datasetFullPath = path.join(`${datasetFolder}`, fileName);
// const size = file.size; // const size = file.size;
// await file.move(drive.makePath(datasetFolder), { await file.move(drive.makePath(datasetFolder), {
// name: fileName,
// overwrite: true, // overwrite in case of conflict
// });
await file.moveToDisk(datasetFullPath, 'local', {
name: fileName, name: fileName,
overwrite: true, // overwrite in case of conflict overwrite: true, // overwrite in case of conflict
disk: 'local',
}); });
// save file metadata into db // save file metadata into db
const newFile = new File(); const newFile = new File();
newFile.pathName = `${datasetFolder}/${fileName}`; newFile.pathName = `${datasetFolder}/${fileName}`;
@ -1041,16 +1030,10 @@ export default class DatasetController {
// move to disk: // move to disk:
const fileName = `file-${cuid()}.${fileData.extname}`; //'file-ls0jyb8xbzqtrclufu2z2e0c.pdf' const fileName = `file-${cuid()}.${fileData.extname}`; //'file-ls0jyb8xbzqtrclufu2z2e0c.pdf'
const datasetFolder = `files/${dataset.id}`; // 'files/307' const datasetFolder = `files/${dataset.id}`; // 'files/307'
const datasetFullPath = path.join(`${datasetFolder}`, fileName);
// await fileData.moveToDisk(datasetFolder, { name: fileName, overwrite: true }, 'local'); // await fileData.moveToDisk(datasetFolder, { name: fileName, overwrite: true }, 'local');
// await fileData.move(drive.makePath(datasetFolder), { await fileData.move(drive.makePath(datasetFolder), {
// name: fileName,
// overwrite: true, // overwrite in case of conflict
// });
await fileData.moveToDisk(datasetFullPath, {
name: fileName, name: fileName,
overwrite: true, // overwrite in case of conflict overwrite: true, // overwrite in case of conflict
driver: 'local',
}); });
//save to db: //save to db:
@ -1177,32 +1160,31 @@ export default class DatasetController {
if (validStates.includes(dataset.server_state)) { if (validStates.includes(dataset.server_state)) {
if (dataset.files && dataset.files.length > 0) { if (dataset.files && dataset.files.length > 0) {
for (const file of dataset.files) { for (const file of dataset.files) {
// overwritten delete method also delets file on filespace and db object // overwritten delete method also delets file on filespace
await file.delete(); await file.delete();
} }
} }
const datasetFolder = `files/${params.id}`; const datasetFolder = `files/${params.id}`;
// const folderExists = await drive.use('local').exists(datasetFolder); const folderExists = await drive.exists(datasetFolder);
// if (folderExists) { if (folderExists) {
// const dirListing = drive.list(datasetFolder); const dirListing = drive.list(datasetFolder);
// const folderContents = await dirListing.toArray(); const folderContents = await dirListing.toArray();
// if (folderContents.length === 0) { if (folderContents.length === 0) {
// await drive.delete(datasetFolder); await drive.delete(datasetFolder);
// } }
await drive.use('local').deleteAll(datasetFolder); // delete dataset wirh relation in db
// delete dataset wirh relation in db await dataset.delete();
await dataset.delete(); session.flash({ message: 'You have deleted 1 dataset!' });
session.flash({ message: 'You have deleted 1 dataset!' }); return response.redirect().toRoute('dataset.list');
return response.redirect().toRoute('dataset.list'); } else {
// } else { // session.flash({
// // session.flash({ // warning: `You cannot delete this dataset! Invalid server_state: "${dataset.server_state}"!`,
// // warning: `You cannot delete this dataset! Invalid server_state: "${dataset.server_state}"!`, // });
// // }); return response
// return response .flash({ warning: `You cannot delete this dataset! Dataset folder "${datasetFolder}" doesn't exist!` })
// .flash({ warning: `You cannot delete this dataset! Dataset folder "${datasetFolder}" doesn't exist!` }) .redirect()
// .redirect() .back();
// .back(); }
// }
} }
} catch (error) { } catch (error) {
if (error instanceof errors.E_VALIDATION_ERROR) { if (error instanceof errors.E_VALIDATION_ERROR) {
@ -1210,42 +1192,11 @@ export default class DatasetController {
throw error; throw error;
} else if (error instanceof Exception) { } else if (error instanceof Exception) {
// General exception handling // General exception handling
session.flash({ error: error.message }); return response.flash('errors', { error: error.message }).redirect().back();
return response.redirect().back();
} else { } else {
session.flash({ error: 'An error occurred while deleting the dataset.' }); session.flash({ error: 'An error occurred while deleting the dataset.' });
return response.redirect().back(); return response.redirect().back();
} }
} }
} }
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,
});
}
} }

View file

@ -6,7 +6,7 @@ import DoiClientException from '#app/exceptions/DoiClientException';
import { StatusCodes } from 'http-status-codes'; import { StatusCodes } from 'http-status-codes';
import logger from '@adonisjs/core/services/logger'; import logger from '@adonisjs/core/services/logger';
import { AxiosResponse } from 'axios'; import { AxiosResponse } from 'axios';
import { default as axios } from 'axios'; import axios from 'axios';
export class DoiClient implements DoiClientContract { export class DoiClient implements DoiClientContract {
public username: string; public username: string;
@ -50,7 +50,7 @@ export class DoiClient implements DoiClientContract {
'Content-Type': 'application/xml;charset=UTF-8', 'Content-Type': 'application/xml;charset=UTF-8',
}; };
try { try {
const metadataResponse = await axios.put(`${this.serviceUrl}/metadata/${doiValue}`, xmlMeta, { auth, headers }); const metadataResponse = await axios.default.put(`${this.serviceUrl}/metadata/${doiValue}`, xmlMeta, { auth, headers });
// Response Codes // Response Codes
// 201 Created: operation successful // 201 Created: operation successful
@ -65,7 +65,7 @@ export class DoiClient implements DoiClientContract {
throw new DoiClientException(metadataResponse.status, message); throw new DoiClientException(metadataResponse.status, message);
} }
const doiResponse = await axios.put(`${this.serviceUrl}/doi/${doiValue}`, `doi=${doiValue}\nurl=${landingPageUrl}`, { const doiResponse = await axios.default.put(`${this.serviceUrl}/doi/${doiValue}`, `doi=${doiValue}\nurl=${landingPageUrl}`, {
auth, auth,
headers, headers,
}); });

View file

@ -4,7 +4,6 @@ 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;
@ -49,12 +48,4 @@ 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;
}
} }

View file

@ -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, browserFingerprint: string): Promise<string>; abstract set(token: ResumptionToken): Promise<string>;
} }

View file

@ -40,64 +40,14 @@ export default class TokenWorkerService implements TokenWorkerContract {
return result !== undefined && result !== null; return result !== undefined && result !== null;
} }
/** public async set(token: ResumptionToken): Promise<string> {
* Simplified set method that stores the token using a browser fingerprint key. const uniqueName = await this.generateUniqueName();
* 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(fingerprintKey, this.ttl, serialToken); await this.cache.setEx(uniqueName, this.ttl, serialToken);
return fingerprintKey; return uniqueName;
} }
// 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();

View file

@ -209,15 +209,6 @@ 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',

View file

@ -3,12 +3,12 @@ import { column, hasMany, belongsTo, SnakeCaseNamingStrategy, computed } from '@
import HashValue from './hash_value.js'; import HashValue from './hash_value.js';
import Dataset from './dataset.js'; import Dataset from './dataset.js';
import BaseModel from './base_model.js'; import BaseModel from './base_model.js';
// import { Buffer } from 'buffer';
import * as fs from 'fs'; import * as fs from 'fs';
import crypto from 'crypto'; import crypto from 'crypto';
// import Drive from '@ioc:Adonis/Core/Drive'; // import Drive from '@ioc:Adonis/Core/Drive';
// import Drive from '@adonisjs/drive'; // import Drive from '@adonisjs/drive';
// import drive from '#services/drive'; import drive from '#services/drive';
import drive from '@adonisjs/drive/services/main';
import type { HasMany } from "@adonisjs/lucid/types/relations"; import type { HasMany } from "@adonisjs/lucid/types/relations";
import type { BelongsTo } from "@adonisjs/lucid/types/relations"; import type { BelongsTo } from "@adonisjs/lucid/types/relations";
@ -88,8 +88,7 @@ export default class File extends BaseModel {
serializeAs: 'filePath', serializeAs: 'filePath',
}) })
public get filePath() { public get filePath() {
// return `/storage/app/public/${this.pathName}`; return `/storage/app/public/${this.pathName}`;
return `/storage/app/data/${this.pathName}`;
// const mainTitle = this.titles?.find((title) => title.type === 'Main'); // const mainTitle = this.titles?.find((title) => title.type === 'Main');
// return mainTitle ? mainTitle.value : null; // return mainTitle ? mainTitle.value : null;
} }
@ -166,7 +165,7 @@ export default class File extends BaseModel {
public async delete() { public async delete() {
if (this.pathName) { if (this.pathName) {
// Delete file from additional storage // Delete file from additional storage
await drive.use('local').delete(this.pathName); await drive.delete(this.pathName);
} }
// Call the original delete method of the BaseModel to remove the record from the database // Call the original delete method of the BaseModel to remove the record from the database

View file

@ -16,14 +16,9 @@ export default class MimeType extends BaseModel {
@column({}) @column({})
public name: string; public name: string;
// 1 : n file_extensions are separated by '|' in the database
@column({}) @column({})
public file_extension: string; public file_extension: string;
// 1 : n alternate_mimetype are separated by '|' in the database
@column({})
public alternate_mimetype: string;
@column({}) @column({})
public enabled: boolean; public enabled: boolean;

View file

@ -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].filter(Boolean).join(' '); return `${this.firstName} ${this.lastName}`;
} }
// @computed() // @computed()
@ -64,13 +64,10 @@ 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 Number(stock); return stock;
} }
@computed() @computed()

View file

@ -1,6 +1,6 @@
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { withAuthFinder } from '@adonisjs/auth/mixins/lucid'; import { withAuthFinder } from '@adonisjs/auth/mixins/lucid';
import { column, manyToMany, hasMany, SnakeCaseNamingStrategy, computed, beforeFetch, beforeFind } from '@adonisjs/lucid/orm'; import { column, manyToMany, hasMany, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm';
import hash from '@adonisjs/core/services/hash'; import hash from '@adonisjs/core/services/hash';
import Role from './role.js'; import Role from './role.js';
import db from '@adonisjs/lucid/services/db'; import db from '@adonisjs/lucid/services/db';
@ -49,6 +49,7 @@ export default class User extends compose(BaseModel, AuthFinder) {
@column() @column()
public login: string; public login: string;
@column() @column()
public firstName: string; public firstName: string;
@ -86,9 +87,6 @@ export default class User extends compose(BaseModel, AuthFinder) {
@column({}) @column({})
public state: number; public state: number;
@column({})
public avatar: string;
// @hasOne(() => TotpSecret, { // @hasOne(() => TotpSecret, {
// foreignKey: 'user_id', // foreignKey: 'user_id',
// }) // })
@ -106,7 +104,6 @@ export default class User extends compose(BaseModel, AuthFinder) {
// return Boolean(this.totp_secret?.twoFactorSecret); // return Boolean(this.totp_secret?.twoFactorSecret);
} }
@manyToMany(() => Role, { @manyToMany(() => Role, {
pivotForeignKey: 'account_id', pivotForeignKey: 'account_id',
pivotRelatedForeignKey: 'role_id', pivotRelatedForeignKey: 'role_id',
@ -124,27 +121,6 @@ export default class User extends compose(BaseModel, AuthFinder) {
}) })
public backupcodes: HasMany<typeof BackupCode>; public backupcodes: HasMany<typeof BackupCode>;
@computed({
serializeAs: 'is_admin',
})
public get isAdmin(): boolean {
const roles = this.roles;
const isAdmin = roles?.map((role: Role) => role.name).includes('administrator');
return isAdmin;
}
// public toJSON() {
// return {
// ...super.toJSON(),
// roles: []
// };
// }
@beforeFind()
@beforeFetch()
public static preloadRoles(user: User) {
user.preload('roles')
}
public async getBackupCodes(this: User): Promise<BackupCode[]> { public async getBackupCodes(this: User): Promise<BackupCode[]> {
const test = await this.related('backupcodes').query(); const test = await this.related('backupcodes').query();
// return test.map((role) => role.code); // return test.map((role) => role.code);

View file

@ -1,7 +1,6 @@
import vine, { SimpleMessagesProvider } from '@vinejs/vine'; import vine, { SimpleMessagesProvider } from '@vinejs/vine';
import { TitleTypes, DescriptionTypes, ContributorTypes, ReferenceIdentifierTypes, RelationTypes } from '#contracts/enums'; import { TitleTypes, DescriptionTypes, ContributorTypes, ReferenceIdentifierTypes, RelationTypes } from '#contracts/enums';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
// import MimeType from '#models/mime_type'; // import MimeType from '#models/mime_type';
// const enabledExtensions = await MimeType.query().select('file_extension').where('enabled', true).exec(); // const enabledExtensions = await MimeType.query().select('file_extension').where('enabled', true).exec();
@ -126,7 +125,7 @@ export const createDatasetValidator = vine.compile(
references: vine references: vine
.array( .array(
vine.object({ vine.object({
value: vine.string().trim().minLength(3).maxLength(255).validateReference({ typeField: 'type' }), value: vine.string().trim().minLength(3).maxLength(255),
type: vine.enum(Object.values(ReferenceIdentifierTypes)), type: vine.enum(Object.values(ReferenceIdentifierTypes)),
relation: vine.enum(Object.values(RelationTypes)), relation: vine.enum(Object.values(RelationTypes)),
label: vine.string().trim().minLength(2).maxLength(255), label: vine.string().trim().minLength(2).maxLength(255),
@ -273,7 +272,7 @@ export const updateDatasetValidator = vine.compile(
references: vine references: vine
.array( .array(
vine.object({ vine.object({
value: vine.string().trim().minLength(3).maxLength(255).validateReference({ typeField: 'type' }), value: vine.string().trim().minLength(3).maxLength(255),
type: vine.enum(Object.values(ReferenceIdentifierTypes)), type: vine.enum(Object.values(ReferenceIdentifierTypes)),
relation: vine.enum(Object.values(RelationTypes)), relation: vine.enum(Object.values(RelationTypes)),
label: vine.string().trim().minLength(2).maxLength(255), label: vine.string().trim().minLength(2).maxLength(255),

View file

@ -142,7 +142,7 @@ export class VanillaErrorReporter implements ErrorReporterContract {
// } // }
this.hasErrors = true; this.hasErrors = true;
// var test = field.getFieldPath(); var test = field.getFieldPath();
// this.errors.push(error); // this.errors.push(error);
// if (this.errors[error.field]) { // if (this.errors[error.field]) {

View file

@ -88,7 +88,7 @@ export default class ValidateChecksum extends BaseCommand {
); );
// Construct the file path // Construct the file path
const filePath = '/storage/app/data/' + file.pathName; const filePath = '/storage/app/public/' + file.pathName;
try { try {
// Calculate the MD5 checksum of the file // Calculate the MD5 checksum of the file

View file

@ -80,8 +80,7 @@ export const http = defineConfig({
| headers. | headers.
| |
*/ */
// trustProxy: proxyAddr.compile('loopback'), trustProxy: proxyAddr.compile('loopback'),
trustProxy: proxyAddr.compile(['127.0.0.1', '::1/128']),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View file

@ -47,7 +47,7 @@ const databaseConfig = defineConfig({
migrations: { migrations: {
naturalSort: true, naturalSort: true,
}, },
// healthCheck: false, healthCheck: false,
debug: false, debug: false,
pool: { min: 1, max: 100 }, pool: { min: 1, max: 100 },
}, },

View file

@ -1,45 +1,151 @@
// import env from '#start/env' /**
// import app from '@adonisjs/core/services/app' * Config source: https://git.io/JBt3o
import { defineConfig, services } from '@adonisjs/drive' *
* Feel free to let us know via PR, if you find something broken in this config
* file.
*/
import { defineConfig } from '#providers/drive/src/types/define_config';
import env from '#start/env';
// import { driveConfig } from '@adonisjs/core/build/config';
// import { driveConfig } from "@adonisjs/drive/build/config.js";
// import Application from '@ioc:Adonis/Core/Application';
const driveConfig = defineConfig({ /*
|--------------------------------------------------------------------------
default: 'public', | Drive Config
|--------------------------------------------------------------------------
|
| The `DriveConfig` relies on the `DisksList` interface which is
| defined inside the `contracts` directory.
|
*/
export default defineConfig({
/*
|--------------------------------------------------------------------------
| Default disk
|--------------------------------------------------------------------------
|
| The default disk to use for managing file uploads. The value is driven by
| the `DRIVE_DISK` environment variable.
|
*/
disk: env.get('DRIVE_DISK', 'local'),
services: { disks: {
/*
/** |--------------------------------------------------------------------------
* Persist files on the local filesystem | Local
*/ |--------------------------------------------------------------------------
public: services.fs({ |
location: '/storage/app/public/', | Uses the local file system to manage files. Make sure to turn off serving
serveFiles: true, | files when not using this disk.
routeBasePath: '/public', |
visibility: 'public', */
}), local: {
local: services.fs({ driver: 'local',
location: '/storage/app/data/', visibility: 'public',
serveFiles: true,
routeBasePath: '/data', /*
visibility: 'public', |--------------------------------------------------------------------------
}), | Storage root - Local driver only
|--------------------------------------------------------------------------
|
/** | Define an absolute path to the storage directory from where to read the
* Persist files on Digital Ocean spaces | files.
*/ |
// spaces: services.s3({ */
// credentials: { // root: Application.tmpPath('uploads'),
// accessKeyId: env.get('SPACES_KEY'), root: '/storage/app/public',
// secretAccessKey: env.get('SPACES_SECRET'),
// }, /*
// region: env.get('SPACES_REGION'), |--------------------------------------------------------------------------
// bucket: env.get('SPACES_BUCKET'), | Serve files - Local driver only
// endpoint: env.get('SPACES_ENDPOINT'), |--------------------------------------------------------------------------
// visibility: 'public', |
// }), | When this is set to true, AdonisJS will configure a files server to serve
| files from the disk root. This is done to mimic the behavior of cloud
| storage services that has inbuilt capabilities to serve files.
|
*/
serveFiles: true,
/*
|--------------------------------------------------------------------------
| Base path - Local driver only
|--------------------------------------------------------------------------
|
| Base path is always required when "serveFiles = true". Also make sure
| the `basePath` is unique across all the disks using "local" driver and
| you are not registering routes with this prefix.
|
*/
basePath: '/uploads',
},
/*
|--------------------------------------------------------------------------
| S3 Driver
|--------------------------------------------------------------------------
|
| Uses the S3 cloud storage to manage files. Make sure to install the s3
| drive separately when using it.
|
|**************************************************************************
| npm i @adonisjs/drive-s3
|**************************************************************************
|
*/
// s3: {
// driver: 's3',
// visibility: 'public',
// key: Env.get('S3_KEY'),
// secret: Env.get('S3_SECRET'),
// region: Env.get('S3_REGION'),
// bucket: Env.get('S3_BUCKET'),
// endpoint: Env.get('S3_ENDPOINT'),
//
// // For minio to work
// // forcePathStyle: true,
// },
/*
|--------------------------------------------------------------------------
| GCS Driver
|--------------------------------------------------------------------------
|
| Uses the Google cloud storage to manage files. Make sure to install the GCS
| drive separately when using it.
|
|**************************************************************************
| npm i @adonisjs/drive-gcs
|**************************************************************************
|
*/
// gcs: {
// driver: 'gcs',
// visibility: 'public',
// keyFilename: Env.get('GCS_KEY_FILENAME'),
// bucket: Env.get('GCS_BUCKET'),
/*
|--------------------------------------------------------------------------
| Uniform ACL - Google cloud storage only
|--------------------------------------------------------------------------
|
| When using the Uniform ACL on the bucket, the "visibility" option is
| ignored. Since, the files ACL is managed by the google bucket policies
| directly.
|
|**************************************************************************
| Learn more: https://cloud.google.com/storage/docs/uniform-bucket-level-access
|**************************************************************************
|
| The following option just informs drive whether your bucket is using uniform
| ACL or not. The actual setting needs to be toggled within the Google cloud
| console.
|
*/
// usingUniformAcl: false,
// },
}, },
}) });
export default driveConfig

View file

@ -1,233 +0,0 @@
/**
* Config source: https://git.io/JBt3o
*
* Feel free to let us know via PR, if you find something broken in this config
* file.
*/
import { defineConfig } from '#providers/drive/src/types/define_config';
import env from '#start/env';
// import { driveConfig } from '@adonisjs/core/build/config';
// import { driveConfig } from "@adonisjs/drive/build/config.js";
// import Application from '@ioc:Adonis/Core/Application';
/*
|--------------------------------------------------------------------------
| Drive Config
|--------------------------------------------------------------------------
|
| The `DriveConfig` relies on the `DisksList` interface which is
| defined inside the `contracts` directory.
|
*/
export default defineConfig({
/*
|--------------------------------------------------------------------------
| Default disk
|--------------------------------------------------------------------------
|
| The default disk to use for managing file uploads. The value is driven by
| the `DRIVE_DISK` environment variable.
|
*/
disk: env.get('DRIVE_DISK', 'local'),
disks: {
/*
|--------------------------------------------------------------------------
| Local
|--------------------------------------------------------------------------
|
| Uses the local file system to manage files. Make sure to turn off serving
| files when not using this disk.
|
*/
local: {
driver: 'local',
visibility: 'public',
/*
|--------------------------------------------------------------------------
| Storage root - Local driver only
|--------------------------------------------------------------------------
|
| Define an absolute path to the storage directory from where to read the
| files.
|
*/
// root: Application.tmpPath('uploads'),
root: '/storage/app/data',
/*
|--------------------------------------------------------------------------
| Serve files - Local driver only
|--------------------------------------------------------------------------
|
| When this is set to true, AdonisJS will configure a files server to serve
| files from the disk root. This is done to mimic the behavior of cloud
| storage services that has inbuilt capabilities to serve files.
|
*/
serveFiles: true,
/*
|--------------------------------------------------------------------------
| Base path - Local driver only
|--------------------------------------------------------------------------
|
| Base path is always required when "serveFiles = true". Also make sure
| the `basePath` is unique across all the disks using "local" driver and
| you are not registering routes with this prefix.
|
*/
basePath: '/files',
},
local: {
driver: 'local',
visibility: 'public',
/*
|--------------------------------------------------------------------------
| Storage root - Local driver only
|--------------------------------------------------------------------------
|
| Define an absolute path to the storage directory from where to read the
| files.
|
*/
// root: Application.tmpPath('uploads'),
root: '/storage/app/data',
/*
|--------------------------------------------------------------------------
| Serve files - Local driver only
|--------------------------------------------------------------------------
|
| When this is set to true, AdonisJS will configure a files server to serve
| files from the disk root. This is done to mimic the behavior of cloud
| storage services that has inbuilt capabilities to serve files.
|
*/
serveFiles: true,
/*
|--------------------------------------------------------------------------
| Base path - Local driver only
|--------------------------------------------------------------------------
|
| Base path is always required when "serveFiles = true". Also make sure
| the `basePath` is unique across all the disks using "local" driver and
| you are not registering routes with this prefix.
|
*/
basePath: '/files',
},
fs: {
driver: 'local',
visibility: 'public',
/*
|--------------------------------------------------------------------------
| Storage root - Local driver only
|--------------------------------------------------------------------------
|
| Define an absolute path to the storage directory from where to read the
| files.
|
*/
// root: Application.tmpPath('uploads'),
root: '/storage/app/public',
/*
|--------------------------------------------------------------------------
| Serve files - Local driver only
|--------------------------------------------------------------------------
|
| When this is set to true, AdonisJS will configure a files server to serve
| files from the disk root. This is done to mimic the behavior of cloud
| storage services that has inbuilt capabilities to serve files.
|
*/
serveFiles: true,
/*
|--------------------------------------------------------------------------
| Base path - Local driver only
|--------------------------------------------------------------------------
|
| Base path is always required when "serveFiles = true". Also make sure
| the `basePath` is unique across all the disks using "local" driver and
| you are not registering routes with this prefix.
|
*/
basePath: '/public',
},
/*
|--------------------------------------------------------------------------
| S3 Driver
|--------------------------------------------------------------------------
|
| Uses the S3 cloud storage to manage files. Make sure to install the s3
| drive separately when using it.
|
|**************************************************************************
| npm i @adonisjs/drive-s3
|**************************************************************************
|
*/
// s3: {
// driver: 's3',
// visibility: 'public',
// key: Env.get('S3_KEY'),
// secret: Env.get('S3_SECRET'),
// region: Env.get('S3_REGION'),
// bucket: Env.get('S3_BUCKET'),
// endpoint: Env.get('S3_ENDPOINT'),
//
// // For minio to work
// // forcePathStyle: true,
// },
/*
|--------------------------------------------------------------------------
| GCS Driver
|--------------------------------------------------------------------------
|
| Uses the Google cloud storage to manage files. Make sure to install the GCS
| drive separately when using it.
|
|**************************************************************************
| npm i @adonisjs/drive-gcs
|**************************************************************************
|
*/
// gcs: {
// driver: 'gcs',
// visibility: 'public',
// keyFilename: Env.get('GCS_KEY_FILENAME'),
// bucket: Env.get('GCS_BUCKET'),
/*
|--------------------------------------------------------------------------
| Uniform ACL - Google cloud storage only
|--------------------------------------------------------------------------
|
| When using the Uniform ACL on the bucket, the "visibility" option is
| ignored. Since, the files ACL is managed by the google bucket policies
| directly.
|
|**************************************************************************
| Learn more: https://cloud.google.com/storage/docs/uniform-bucket-level-access
|**************************************************************************
|
| The following option just informs drive whether your bucket is using uniform
| ACL or not. The actual setting needs to be toggled within the Google cloud
| console.
|
*/
// usingUniformAcl: false,
// },
},
});

View file

@ -1,8 +1,7 @@
import { defineConfig } from '@adonisjs/inertia'; import { defineConfig } from '@adonisjs/inertia';
import type { HttpContext } from '@adonisjs/core/http'; import type { HttpContext } from '@adonisjs/core/http';
import type { InferSharedProps } from '@adonisjs/inertia/types'
const inertiaConfig = defineConfig({ export default defineConfig({
/** /**
* Path to the Edge view that will be used as the root view for Inertia responses * Path to the Edge view that will be used as the root view for Inertia responses
*/ */
@ -53,12 +52,6 @@ const inertiaConfig = defineConfig({
}, },
}); });
export default inertiaConfig
declare module '@adonisjs/inertia/types' {
export interface SharedProps extends InferSharedProps<typeof inertiaConfig> {}
}
// import { InertiaConfig } from '@ioc:EidelLev/Inertia'; // import { InertiaConfig } from '@ioc:EidelLev/Inertia';
// /* // /*

View file

@ -12,7 +12,7 @@ const mailConfig = defineConfig({
mailers: { mailers: {
smtp: transports.smtp({ smtp: transports.smtp({
// socketTimeout: 5000,// Overall timeout (5 seconds) socketTimeout: 5000,// Overall timeout (5 seconds)
host: env.get('SMTP_HOST', ''), host: env.get('SMTP_HOST', ''),
port: env.get('SMTP_PORT'), port: env.get('SMTP_PORT'),
secure: false, secure: false,
@ -30,10 +30,10 @@ const mailConfig = defineConfig({
}, */ }, */
}), }),
// resend: transports.resend({ resend: transports.resend({
// key: env.get('RESEND_API_KEY'), key: env.get('RESEND_API_KEY'),
// baseUrl: 'https://api.resend.com', baseUrl: 'https://api.resend.com',
// }), }),
}, },
}); });

View file

@ -6,7 +6,7 @@
*/ */
import env from '#start/env'; import env from '#start/env';
// import app from '@adonisjs/core/services/app'; import app from '@adonisjs/core/services/app';
import { defineConfig, stores } from '@adonisjs/session'; import { defineConfig, stores } from '@adonisjs/session';
const sessionConfig = defineConfig({ const sessionConfig = defineConfig({

View file

@ -1,32 +0,0 @@
import { defineConfig } from '@adonisjs/vite';
const viteBackendConfig = defineConfig({
/**
* The output of vite will be written inside this
* directory. The path should be relative from
* the application root.
*/
buildDirectory: 'public/assets',
/**
* The path to the manifest file generated by the
* "vite build" command.
*/
manifestFile: 'public/assets/.vite/manifest.json',
/**
* Feel free to change the value of the "assetsUrl" to
* point to a CDN in production.
*/
assetsUrl: '/assets',
/**
* Add defer attribute to scripts for better performance.
*/
scriptAttributes: {
defer: true,
},
});
export default viteBackendConfig;

View file

@ -18,7 +18,6 @@ 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();
}); });
} }
@ -44,7 +43,6 @@ 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
@ -87,6 +85,3 @@ 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

View file

@ -54,8 +54,3 @@ 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;

View file

@ -1,18 +0,0 @@
import { BaseSchema } from "@adonisjs/lucid/schema";
export default class AddAlternateMimetypeToMimeTypes extends BaseSchema {
protected tableName = 'mime_types';
public async up () {
this.schema.alterTable(this.tableName, (table) => {
table.string('alternate_mimetype').nullable();
});
}
public async down () {
this.schema.alterTable(this.tableName, (table) => {
table.dropColumn('alternate_mimetype');
});
}
}
// ALTER TABLE "mime_types" ADD COLUMN "alternate_mimetype" VARCHAR(255) NULL

10238
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,8 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"dev": "node ace serve", "dev": "node ace serve --watch",
"devInspect": "node ace serve --watch --node-args='--inspect'",
"compress:xslt": "./node_modules/xslt3/xslt3.js -xsl:public/assets2/datasetxml2oai-pmh.xslt -export:public/assets2/datasetxml2oai.sef.json -t -nogo '-ns:##html5'", "compress:xslt": "./node_modules/xslt3/xslt3.js -xsl:public/assets2/datasetxml2oai-pmh.xslt -export:public/assets2/datasetxml2oai.sef.json -t -nogo '-ns:##html5'",
"compress:solr": "./node_modules/xslt3/xslt3.js -xsl:public/assets2/solr.xslt -export:public/assets2/solr.sef.json -t -nogo '-ns:##html5'", "compress:solr": "./node_modules/xslt3/xslt3.js -xsl:public/assets2/solr.xslt -export:public/assets2/solr.sef.json -t -nogo '-ns:##html5'",
"compress:doi": "./node_modules/xslt3/xslt3.js -xsl:public/assets2/doi_datacite.xslt -export:public/assets2/doi_datacite.sef.json -t -nogo '-ns:##html5'", "compress:doi": "./node_modules/xslt3/xslt3.js -xsl:public/assets2/doi_datacite.xslt -export:public/assets2/doi_datacite.sef.json -t -nogo '-ns:##html5'",
@ -16,58 +15,59 @@
"format-check": "prettier --check ./**/*.{ts,js}", "format-check": "prettier --check ./**/*.{ts,js}",
"test": "node ace test" "test": "node ace test"
}, },
"eslintConfig": { "eslintIgnore": [
"ignorePatterns": [ "build"
"build" ],
]
},
"alias": { "alias": {
"vue": "./node_modules/vue/dist/vue.esm-bundler.js" "vue": "./node_modules/vue/dist/vue.esm-bundler.js"
}, },
"devDependencies": { "devDependencies": {
"@adonisjs/assembler": "^7.1.1", "@adonisjs/assembler": "^7.1.1",
"@adonisjs/tsconfig": "^1.4.0", "@adonisjs/tsconfig": "^1.2.1",
"@headlessui/vue": "^1.7.23", "@babel/core": "^7.20.12",
"@japa/assert": "^4.0.1", "@babel/plugin-proposal-class-properties": "^7.18.6",
"@japa/plugin-adonisjs": "^4.0.0", "@babel/plugin-proposal-decorators": "^7.20.13",
"@japa/runner": "^4.2.0", "@babel/plugin-transform-runtime": "^7.19.6",
"@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.18.6",
"@japa/api-client": "^2.0.3",
"@japa/assert": "^3.0.0",
"@japa/plugin-adonisjs": "^3.0.0",
"@japa/runner": "^3.1.1",
"@mdi/js": "^7.1.96", "@mdi/js": "^7.1.96",
"@poppinss/utils": "^6.7.2", "@poppinss/utils": "^6.7.2",
"@swc/wasm": "^1.10.14", "@swc/core": "^1.4.2",
"@symfony/webpack-encore": "^5.0.1",
"@tailwindcss/forms": "^0.5.2", "@tailwindcss/forms": "^0.5.2",
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/clamscan": "^2.0.4", "@types/clamscan": "^2.0.4",
"@types/escape-html": "^1.0.4", "@types/escape-html": "^1.0.4",
"@types/fs-extra": "^11.0.4",
"@types/leaflet": "^1.9.3", "@types/leaflet": "^1.9.3",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^22.10.2", "@types/node": "^22.5.5",
"@types/proxy-addr": "^2.0.0", "@types/proxy-addr": "^2.0.0",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@types/source-map-support": "^0.5.6", "@types/source-map-support": "^0.5.6",
"@types/sprintf-js": "^1.1.4", "@types/sprintf-js": "^1.1.4",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@vitejs/plugin-vue": "^5.2.1",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"babel-preset-typescript-vue3": "^2.0.17", "babel-preset-typescript-vue3": "^2.0.17",
"chart.js": "^4.2.0", "chart.js": "^4.2.0",
"dotenv-webpack": "^8.0.1", "dotenv-webpack": "^8.0.1",
"eslint": "^8.57.1", "eslint": "^8.57.1",
"eslint-config-prettier": "^10.0.1", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-adonis": "^2.1.1", "eslint-plugin-adonis": "^2.1.1",
"eslint-plugin-prettier": "^5.0.0-alpha.2", "eslint-plugin-prettier": "^5.0.0-alpha.2",
"hot-hook": "^0.4.0",
"numeral": "^2.0.6", "numeral": "^2.0.6",
"pinia": "^2.0.30", "pinia": "^2.0.30",
"pino-pretty": "^13.0.0", "pino-pretty": "^11.2.2",
"postcss-loader": "^8.1.1", "postcss-loader": "^8.1.1",
"prettier": "^3.4.2", "prettier": "^3.0.0",
"supertest": "^6.3.3", "supertest": "^6.3.3",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.2.4",
"ts-loader": "^9.4.2", "ts-loader": "^9.4.2",
"ts-node-maintained": "^10.9.5", "ts-node": "^10.9.2",
"typescript": "~5.7", "typescript": "^5.1.3",
"vite": "^6.0.11",
"vue": "^3.4.26", "vue": "^3.4.26",
"vue-facing-decorator": "^3.0.0", "vue-facing-decorator": "^3.0.0",
"vue-loader": "^17.0.1", "vue-loader": "^17.0.1",
@ -75,31 +75,30 @@
"xslt3": "^2.5.0" "xslt3": "^2.5.0"
}, },
"dependencies": { "dependencies": {
"@adonisjs/auth": "^9.2.4", "@adonisjs/auth": "^9.1.1",
"@adonisjs/core": "^6.17.0", "@adonisjs/core": "^6.3.1",
"@adonisjs/cors": "^2.2.1", "@adonisjs/cors": "^2.2.1",
"@adonisjs/drive": "^3.2.0", "@adonisjs/drive": "^2.3.0",
"@adonisjs/inertia": "^2.1.3", "@adonisjs/encore": "^1.0.0",
"@adonisjs/lucid": "^21.5.1", "@adonisjs/inertia": "^1.0.0-7",
"@adonisjs/lucid": "^21.1.0",
"@adonisjs/mail": "^9.2.2", "@adonisjs/mail": "^9.2.2",
"@adonisjs/redis": "^9.1.0", "@adonisjs/redis": "^9.1.0",
"@adonisjs/session": "^7.5.0", "@adonisjs/session": "^7.1.1",
"@adonisjs/shield": "^8.1.1", "@adonisjs/shield": "^8.1.1",
"@adonisjs/static": "^1.1.1", "@adonisjs/static": "^1.1.1",
"@adonisjs/vite": "^4.0.0",
"@eidellev/adonis-stardust": "^3.0.0", "@eidellev/adonis-stardust": "^3.0.0",
"@fontsource/archivo-black": "^5.0.1", "@fontsource/archivo-black": "^5.0.1",
"@fontsource/inter": "^5.0.1", "@fontsource/inter": "^5.0.1",
"@inertiajs/inertia": "^0.11.1", "@inertiajs/inertia": "^0.11.1",
"@inertiajs/vue3": "^2.0.3", "@inertiajs/vue3": "^1.0.0",
"@opensearch-project/opensearch": "^3.2.0", "@opensearch-project/opensearch": "^2.4.0",
"@phc/format": "^1.0.0", "@phc/format": "^1.0.0",
"@poppinss/manager": "^5.0.2", "@vinejs/vine": "^2.0.0",
"@vinejs/vine": "^3.0.0",
"axios": "^1.7.9",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"clamscan": "^2.1.2", "clamscan": "^2.1.2",
"crypto": "^1.0.1",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"deep-email-validator": "^0.1.21", "deep-email-validator": "^0.1.21",
"edge.js": "^6.0.1", "edge.js": "^6.0.1",
@ -122,12 +121,6 @@
"vuedraggable": "^4.1.0", "vuedraggable": "^4.1.0",
"xmlbuilder2": "^3.1.1" "xmlbuilder2": "^3.1.1"
}, },
"hotHook": {
"boundaries": [
"./app/Controllers/**/*.ts",
"./app/middleware/*.ts"
]
},
"type": "module", "type": "module",
"imports": { "imports": {
"#controllers/*": "./app/Controllers/*.js", "#controllers/*": "./app/Controllers/*.js",

View file

@ -1,10 +1,7 @@
module.exports = { module.exports = {
plugins: { plugins: {
// 'postcss-import': {}, // 'postcss-import': {},
// 'postcss-nesting': {}, 'tailwindcss/nesting': {},
'tailwindcss/nesting': {},
// "@tailwindcss/postcss": {},
// tailwindcss: {},
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },

View file

@ -74,8 +74,7 @@ export class LocalDriver implements LocalDriverContract {
*/ */
public async exists(location: string): Promise<boolean> { public async exists(location: string): Promise<boolean> {
try { try {
let path_temp = this.makePath(location); //'/storage/app/files/421' return await this.adapter.pathExists(this.makePath(location));
return await this.adapter.pathExists(path_temp);
} catch (error) { } catch (error) {
throw CannotGetMetaDataException.invoke(location, 'exists', error); throw CannotGetMetaDataException.invoke(location, 'exists', error);
} }

View file

@ -69,7 +69,7 @@ export default class MailProvider {
const mailConfigProvider = this.app.config.get('mail'); const mailConfigProvider = this.app.config.get('mail');
const config = await configProvider.resolve<any>(this.app, mailConfigProvider); const config = await configProvider.resolve<any>(this.app, mailConfigProvider);
await config.mailers.smtp(); const iwas = await config.mailers.smtp();
// iwas.config.host = 'hhhost'; // iwas.config.host = 'hhhost';
// this.app.config.set('mail.mailers.smtp.host', 'xhost'); // this.app.config.set('mail.mailers.smtp.host', 'xhost');
// const iwas = await config.mailers.smtp(); // const iwas = await config.mailers.smtp();

View file

@ -63,15 +63,6 @@ export default class QueryBuilderProvider {
public register() { public register() {
// Register your own bindings // Register your own bindings
// const ModelQueryBuilder = this.app.container.bind('@adonisjs/lucid/orm/ModelQueryBuilder');
// ModelQueryBuilder.macro('whereTrue', function (columnName: string) {
// return this.where(columnName, true);
// });
// ModelQueryBuilder.macro('whereFalse', function (columnName: string) {
// return this.where(columnName, false);
// });
} }
public async boot() { public async boot() {
@ -82,14 +73,15 @@ export default class QueryBuilderProvider {
// let rolesPluck = {}; // let rolesPluck = {};
let rolesPluck: { [key: number]: any } = {}; let rolesPluck: { [key: number]: any } = {};
const result = await this.exec(); const result = await this.exec();
result.forEach((user: { [key: string]: any }, index: number) => { result.forEach((user, index) => {
let idc: number; let idc;
if (!id) { if (!id) {
idc = index; idc = index;
} else { } else {
idc = user[id]; idc = user[id];
} }
const value: any = user[valueColumn]; const value = user[valueColumn];
// rolesPluck[idc] = user.name;
rolesPluck[idc] = value; rolesPluck[idc] = value;
}); });
return rolesPluck; return rolesPluck;

View file

@ -4,15 +4,14 @@
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
|*/ |*/
import type { ApplicationService } from '@adonisjs/core/types'; import type { ApplicationService } from '@adonisjs/core/types';
import vine, { symbols, BaseLiteralType, Vine } from '@vinejs/vine'; import vine, { BaseLiteralType, Vine } from '@vinejs/vine';
import type { FieldContext, FieldOptions } from '@vinejs/vine/types'; import type { Validation, FieldContext, FieldOptions } from '@vinejs/vine/types';
// import type { MultipartFile, FileValidationOptions } from '@adonisjs/bodyparser/types'; // import type { MultipartFile, FileValidationOptions } from '@adonisjs/bodyparser/types';
import type { MultipartFile } from '@adonisjs/core/bodyparser'; import type { MultipartFile } from '@adonisjs/core/bodyparser';
import type { FileValidationOptions } from '@adonisjs/core/types/bodyparser'; import type { FileValidationOptions } from '@adonisjs/core/types/bodyparser';
import { Request, RequestValidator } from '@adonisjs/core/http'; import { Request, RequestValidator } from '@adonisjs/core/http';
import MimeType from '#models/mime_type'; import MimeType from '#models/mime_type';
/** /**
* Validation options accepted by the "file" rule * Validation options accepted by the "file" rule
*/ */
@ -29,7 +28,8 @@ declare module '@vinejs/vine' {
* Extend HTTP request class * Extend HTTP request class
*/ */
declare module '@adonisjs/core/http' { declare module '@adonisjs/core/http' {
interface Request extends RequestValidator {} interface Request extends RequestValidator {
}
} }
/** /**
@ -48,7 +48,7 @@ export async function getEnabledExtensions() {
.flat(); .flat();
return extensions; return extensions;
} };
/** /**
* VineJS validation rule that validates the file to be an * VineJS validation rule that validates the file to be an
* instance of BodyParser MultipartFile class. * instance of BodyParser MultipartFile class.
@ -79,13 +79,11 @@ const isMultipartFile = vine.createRule(async (file: MultipartFile | unknown, op
// if (validatedFile.allowedExtensions === undefined && validationOptions.extnames) { // if (validatedFile.allowedExtensions === undefined && validationOptions.extnames) {
// validatedFile.allowedExtensions = validationOptions.extnames; // validatedFile.allowedExtensions = validationOptions.extnames;
// } // }
if (validatedFile.allowedExtensions === undefined && validationOptions.extnames !== undefined) { if (validatedFile.allowedExtensions === undefined && validationOptions.extnames) {
validatedFile.allowedExtensions = validationOptions.extnames; // await getEnabledExtensions();
} else if (validatedFile.allowedExtensions === undefined && validationOptions.extnames === undefined) {
validatedFile.allowedExtensions = await getEnabledExtensions(); validatedFile.allowedExtensions = await getEnabledExtensions();
} }
/** /**
* wieder löschen * wieder löschen
* Set extensions when it's defined in the options and missing * Set extensions when it's defined in the options and missing
* on the file instance * on the file instance
*/ */
@ -104,20 +102,7 @@ const isMultipartFile = vine.createRule(async (file: MultipartFile | unknown, op
}); });
}); });
const MULTIPART_FILE: typeof symbols.SUBTYPE = symbols.SUBTYPE;
export class VineMultipartFile extends BaseLiteralType<MultipartFile, MultipartFile, MultipartFile> { export class VineMultipartFile extends BaseLiteralType<MultipartFile, MultipartFile, MultipartFile> {
[MULTIPART_FILE]: string;
// constructor(validationOptions?: FileRuleValidationOptions, options?: FieldOptions) {
// super(options, [isMultipartFile(validationOptions || {})]);
// this.validationOptions = validationOptions;
// this.#private = true;
// }
// clone(): this {
// return new VineMultipartFile(this.validationOptions, this.cloneOptions()) as this;
// }
// #private; // #private;
// constructor(validationOptions?: FileRuleValidationOptions, options?: FieldOptions, validations?: Validation<any>[]); // constructor(validationOptions?: FileRuleValidationOptions, options?: FieldOptions, validations?: Validation<any>[]);
// clone(): this; // clone(): this;
@ -126,16 +111,14 @@ export class VineMultipartFile extends BaseLiteralType<MultipartFile, MultipartF
// extnames: (18) ['gpkg', 'htm', 'html', 'csv', 'txt', 'asc', 'c', 'cc', 'h', 'srt', 'tiff', 'pdf', 'png', 'zip', 'jpg', 'jpeg', 'jpe', 'xlsx'] // extnames: (18) ['gpkg', 'htm', 'html', 'csv', 'txt', 'asc', 'c', 'cc', 'h', 'srt', 'tiff', 'pdf', 'png', 'zip', 'jpg', 'jpeg', 'jpe', 'xlsx']
// size: '512mb' // size: '512mb'
// public constructor(validationOptions?: FileRuleValidationOptions, options?: FieldOptions, validations?: Validation<any>[]) { public constructor(validationOptions?: FileRuleValidationOptions, options?: FieldOptions, validations?: Validation<any>[]) {
public constructor(validationOptions?: FileRuleValidationOptions, options?: FieldOptions) {
// super(options, validations); // super(options, validations);
super(options, [isMultipartFile(validationOptions || {})]); super(options, [isMultipartFile(validationOptions || {})]);
this.validationOptions = validationOptions; this.validationOptions = validationOptions;
} }
public clone(): any { public clone(): any {
// return new VineMultipartFile(this.validationOptions, this.cloneOptions(), this.cloneValidations()); return new VineMultipartFile(this.validationOptions, this.cloneOptions(), this.cloneValidations());
return new VineMultipartFile(this.validationOptions, this.cloneOptions());
} }
} }
@ -169,12 +152,10 @@ export default class VinejsProvider {
* The validate method can be used to validate the request * The validate method can be used to validate the request
* data for the current request using VineJS validators * data for the current request using VineJS validators
*/ */
Request.macro('validateUsing', function (this: Request, ...args) { Request.macro('validateUsing', function (...args) {
if (!this.ctx) { return new RequestValidator(this.ctx).validateUsing(...args);
throw new Error('HttpContext is not available'); });
}
return new RequestValidator(this.ctx).validateUsing(...args);
});
} }
/** /**

View file

@ -0,0 +1,12 @@
{
"entrypoints": {
"app": {
"css": [
"http://localhost:8080/assets/app.css"
],
"js": [
"http://localhost:8080/assets/app.js"
]
}
}
}

103
public/assets/manifest.json Normal file
View file

@ -0,0 +1,103 @@
{
"assets/app.css": "http://localhost:8080/assets/app.css",
"assets/app.js": "http://localhost:8080/assets/app.js",
"assets/resources_js_apps_settings_l18n_de_js.js": "http://localhost:8080/assets/resources_js_apps_settings_l18n_de_js.js",
"assets/resources_js_apps_settings_l18n_en_js.js": "http://localhost:8080/assets/resources_js_apps_settings_l18n_en_js.js",
"assets/resources_js_Pages_Admin_License_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_License_Index_vue.js",
"assets/resources_js_Pages_Admin_Mimetype_Create_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Mimetype_Create_vue.js",
"assets/resources_js_Pages_Admin_Mimetype_Delete_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Mimetype_Delete_vue.js",
"assets/resources_js_Pages_Admin_Mimetype_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Mimetype_Index_vue.js",
"assets/resources_js_Pages_Admin_Permission_Create_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Permission_Create_vue.js",
"assets/resources_js_Pages_Admin_Permission_Edit_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Permission_Edit_vue.js",
"assets/resources_js_Pages_Admin_Permission_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Permission_Index_vue.js",
"assets/resources_js_Pages_Admin_Permission_Show_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Permission_Show_vue.js",
"assets/resources_js_Pages_Admin_Role_Create_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Role_Create_vue.js",
"assets/resources_js_Pages_Admin_Role_Edit_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Role_Edit_vue.js",
"assets/resources_js_Pages_Admin_Role_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Role_Index_vue.js",
"assets/resources_js_Pages_Admin_Role_Show_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Role_Show_vue.js",
"assets/resources_js_Pages_Admin_Settings_vue-resources_js_utils_toast_css.css": "http://localhost:8080/assets/resources_js_Pages_Admin_Settings_vue-resources_js_utils_toast_css.css",
"assets/resources_js_Pages_Admin_Settings_vue-resources_js_utils_toast_css.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Settings_vue-resources_js_utils_toast_css.js",
"assets/resources_js_Pages_Admin_User_Create_vue-resources_js_Components_SimplePasswordMeter_password-f3312a.css": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Create_vue-resources_js_Components_SimplePasswordMeter_password-f3312a.css",
"assets/resources_js_Pages_Admin_User_Create_vue-resources_js_Components_SimplePasswordMeter_password-f3312a.js": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Create_vue-resources_js_Components_SimplePasswordMeter_password-f3312a.js",
"assets/resources_js_Pages_Admin_User_Edit_vue-resources_js_Components_SimplePasswordMeter_password-m-6dc207.css": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Edit_vue-resources_js_Components_SimplePasswordMeter_password-m-6dc207.css",
"assets/resources_js_Pages_Admin_User_Edit_vue-resources_js_Components_SimplePasswordMeter_password-m-6dc207.js": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Edit_vue-resources_js_Components_SimplePasswordMeter_password-m-6dc207.js",
"assets/resources_js_Pages_Admin_User_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Index_vue.js",
"assets/resources_js_Pages_Admin_User_Show_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Show_vue.js",
"assets/resources_js_Pages_App_vue.js": "http://localhost:8080/assets/resources_js_Pages_App_vue.js",
"assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css-resources_js_Components_-06c7b5.css": "http://localhost:8080/assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css-resources_js_Components_-06c7b5.css",
"assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css-resources_js_Components_-06c7b5.js": "http://localhost:8080/assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css-resources_js_Components_-06c7b5.js",
"assets/resources_js_Pages_Auth_Login_vue.js": "http://localhost:8080/assets/resources_js_Pages_Auth_Login_vue.js",
"assets/resources_js_Pages_Auth_Register_vue.js": "http://localhost:8080/assets/resources_js_Pages_Auth_Register_vue.js",
"assets/resources_js_Pages_Dashboard_vue.js": "http://localhost:8080/assets/resources_js_Pages_Dashboard_vue.js",
"assets/resources_js_Pages_Editor_Dataset_Approve_vue.js": "http://localhost:8080/assets/resources_js_Pages_Editor_Dataset_Approve_vue.js",
"assets/resources_js_Pages_Editor_Dataset_Doi_vue.js": "http://localhost:8080/assets/resources_js_Pages_Editor_Dataset_Doi_vue.js",
"assets/resources_js_Pages_Editor_Dataset_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Editor_Dataset_Index_vue.js",
"assets/resources_js_Pages_Editor_Dataset_Publish_vue.js": "http://localhost:8080/assets/resources_js_Pages_Editor_Dataset_Publish_vue.js",
"assets/resources_js_Pages_Editor_Dataset_Receive_vue.js": "http://localhost:8080/assets/resources_js_Pages_Editor_Dataset_Receive_vue.js",
"assets/resources_js_Pages_Editor_Dataset_Reject_vue.js": "http://localhost:8080/assets/resources_js_Pages_Editor_Dataset_Reject_vue.js",
"assets/resources_js_Pages_Error_vue.js": "http://localhost:8080/assets/resources_js_Pages_Error_vue.js",
"assets/resources_js_Pages_Errors_ServerError_vue.js": "http://localhost:8080/assets/resources_js_Pages_Errors_ServerError_vue.js",
"assets/resources_js_Pages_Errors_not_found_vue.js": "http://localhost:8080/assets/resources_js_Pages_Errors_not_found_vue.js",
"assets/resources_js_Pages_Map_vue-resources_js_Components_Map_draw_component_vue-resources_js_Compon-b0925c.css": "http://localhost:8080/assets/resources_js_Pages_Map_vue-resources_js_Components_Map_draw_component_vue-resources_js_Compon-b0925c.css",
"assets/resources_js_Pages_Map_vue-resources_js_Components_Map_draw_component_vue-resources_js_Compon-b0925c.js": "http://localhost:8080/assets/resources_js_Pages_Map_vue-resources_js_Components_Map_draw_component_vue-resources_js_Compon-b0925c.js",
"assets/resources_js_Pages_ProfileView_vue.js": "http://localhost:8080/assets/resources_js_Pages_ProfileView_vue.js",
"assets/resources_js_Pages_Reviewer_Dataset_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Reviewer_Dataset_Index_vue.js",
"assets/resources_js_Pages_Reviewer_Dataset_Reject_vue.js": "http://localhost:8080/assets/resources_js_Pages_Reviewer_Dataset_Reject_vue.js",
"assets/resources_js_Pages_Reviewer_Dataset_Review_vue.js": "http://localhost:8080/assets/resources_js_Pages_Reviewer_Dataset_Review_vue.js",
"assets/resources_js_Pages_Submitter_Dataset_Category_vue.css": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Category_vue.css",
"assets/resources_js_Pages_Submitter_Dataset_Category_vue.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Category_vue.js",
"assets/resources_js_Pages_Submitter_Dataset_Create_vue-resources_js_utils_toast_css-resources_js_Com-03a898.css": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Create_vue-resources_js_utils_toast_css-resources_js_Com-03a898.css",
"assets/resources_js_Pages_Submitter_Dataset_Create_vue-resources_js_utils_toast_css-resources_js_Com-03a898.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Create_vue-resources_js_utils_toast_css-resources_js_Com-03a898.js",
"assets/resources_js_Pages_Submitter_Dataset_Delete_vue.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Delete_vue.js",
"assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_utils_toast_css-resources_js_Compo-a37b65.css": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_utils_toast_css-resources_js_Compo-a37b65.css",
"assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_utils_toast_css-resources_js_Compo-a37b65.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_utils_toast_css-resources_js_Compo-a37b65.js",
"assets/resources_js_Pages_Submitter_Dataset_Index_vue.css": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Index_vue.css",
"assets/resources_js_Pages_Submitter_Dataset_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Index_vue.js",
"assets/resources_js_Pages_Submitter_Dataset_Release_vue.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Release_vue.js",
"assets/resources_js_Pages_Submitter_Person_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Person_Index_vue.js",
"assets/resources_js_Pages_register-view_register-view-component_vue.js": "http://localhost:8080/assets/resources_js_Pages_register-view_register-view-component_vue.js",
"assets/vendors-node_modules_mdi_js_mdi_js-node_modules_vue-loader_dist_exportHelper_js.js": "http://localhost:8080/assets/vendors-node_modules_mdi_js_mdi_js-node_modules_vue-loader_dist_exportHelper_js.js",
"assets/vendors-node_modules_focus-trap_dist_focus-trap_esm_js-node_modules_notiwind_dist_index_esm_js.js": "http://localhost:8080/assets/vendors-node_modules_focus-trap_dist_focus-trap_esm_js-node_modules_notiwind_dist_index_esm_js.js",
"assets/vendors-node_modules_vue-facing-decorator_dist_esm_utils_js.js": "http://localhost:8080/assets/vendors-node_modules_vue-facing-decorator_dist_esm_utils_js.js",
"assets/vendors-node_modules_toastify-js_src_toastify_js.js": "http://localhost:8080/assets/vendors-node_modules_toastify-js_src_toastify_js.js",
"assets/vendors-node_modules_leaflet_dist_leaflet-src_js-node_modules_leaflet_src_control_Control_Att-adabdc.js": "http://localhost:8080/assets/vendors-node_modules_leaflet_dist_leaflet-src_js-node_modules_leaflet_src_control_Control_Att-adabdc.js",
"assets/vendors-node_modules_buffer_index_js-node_modules_vuedraggable_dist_vuedraggable_umd_js.js": "http://localhost:8080/assets/vendors-node_modules_buffer_index_js-node_modules_vuedraggable_dist_vuedraggable_umd_js.js",
"assets/vendors-node_modules_mime_dist_src_index_js.js": "http://localhost:8080/assets/vendors-node_modules_mime_dist_src_index_js.js",
"assets/vendors-node_modules_numeral_numeral_js-node_modules_chart_js_dist_chart_js.js": "http://localhost:8080/assets/vendors-node_modules_numeral_numeral_js-node_modules_chart_js_dist_chart_js.js",
"assets/resources_js_Components_BaseButton_vue.js": "http://localhost:8080/assets/resources_js_Components_BaseButton_vue.js",
"assets/resources_js_Stores_main_ts-resources_js_Components_BaseDivider_vue-resources_js_Components_C-b45805.js": "http://localhost:8080/assets/resources_js_Stores_main_ts-resources_js_Components_BaseDivider_vue-resources_js_Components_C-b45805.js",
"assets/resources_js_Layouts_LayoutAuthenticated_vue.css": "http://localhost:8080/assets/resources_js_Layouts_LayoutAuthenticated_vue.css",
"assets/resources_js_Layouts_LayoutAuthenticated_vue.js": "http://localhost:8080/assets/resources_js_Layouts_LayoutAuthenticated_vue.js",
"assets/resources_js_Components_BaseButtons_vue-resources_js_Components_FormControl_vue-resources_js_-d830d6.js": "http://localhost:8080/assets/resources_js_Components_BaseButtons_vue-resources_js_Components_FormControl_vue-resources_js_-d830d6.js",
"assets/resources_js_Components_Admin_Pagination_vue-resources_js_Components_BaseButtons_vue-resource-6f3a70.js": "http://localhost:8080/assets/resources_js_Components_Admin_Pagination_vue-resources_js_Components_BaseButtons_vue-resource-6f3a70.js",
"assets/resources_js_utils_toast_ts-resources_js_Components_NotificationBar_vue.js": "http://localhost:8080/assets/resources_js_utils_toast_ts-resources_js_Components_NotificationBar_vue.js",
"assets/resources_js_Components_Map_draw_component_vue-resources_js_Components_Map_zoom_component_vue-058bcc.js": "http://localhost:8080/assets/resources_js_Components_Map_draw_component_vue-resources_js_Components_Map_zoom_component_vue-058bcc.js",
"assets/resources_js_Components_SectionMain_vue-resources_js_Components_SectionTitleLineWithButton_vu-764dfe.js": "http://localhost:8080/assets/resources_js_Components_SectionMain_vue-resources_js_Components_SectionTitleLineWithButton_vu-764dfe.js",
"assets/resources_js_Components_BaseButtons_vue-resources_js_Components_NotificationBar_vue-resources-7e06d8.js": "http://localhost:8080/assets/resources_js_Components_BaseButtons_vue-resources_js_Components_NotificationBar_vue-resources-7e06d8.js",
"assets/resources_js_Components_Admin_Sort_vue-resources_js_Components_SectionTitleLineWithButton_vue.js": "http://localhost:8080/assets/resources_js_Components_Admin_Sort_vue-resources_js_Components_SectionTitleLineWithButton_vue.js",
"assets/resources_js_Components_CardBoxModal_vue.js": "http://localhost:8080/assets/resources_js_Components_CardBoxModal_vue.js",
"assets/resources_js_Components_FileUpload_vue-resources_js_Components_FormCheckRadioGroup_vue-resour-25e686.js": "http://localhost:8080/assets/resources_js_Components_FileUpload_vue-resources_js_Components_FormCheckRadioGroup_vue-resour-25e686.js",
"assets/fonts/inter-latin-ext-400-normal.woff": "http://localhost:8080/assets/fonts/inter-latin-ext-400-normal.1c20f7dc.woff",
"assets/fonts/inter-latin-400-normal.woff": "http://localhost:8080/assets/fonts/inter-latin-400-normal.b0c8fe9d.woff",
"assets/fonts/inter-latin-ext-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-latin-ext-400-normal.3d10c85f.woff2",
"assets/fonts/inter-latin-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-latin-400-normal.9698cc7d.woff2",
"assets/fonts/archivo-black-latin-400-normal.woff2": "http://localhost:8080/assets/fonts/archivo-black-latin-400-normal.fc847a1f.woff2",
"assets/fonts/archivo-black-latin-ext-400-normal.woff2": "http://localhost:8080/assets/fonts/archivo-black-latin-ext-400-normal.21761451.woff2",
"assets/fonts/inter-cyrillic-ext-400-normal.woff": "http://localhost:8080/assets/fonts/inter-cyrillic-ext-400-normal.e8945162.woff",
"assets/fonts/archivo-black-latin-400-normal.woff": "http://localhost:8080/assets/fonts/archivo-black-latin-400-normal.58a301a6.woff",
"assets/fonts/inter-cyrillic-ext-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-cyrillic-ext-400-normal.fd1478dc.woff2",
"assets/fonts/inter-cyrillic-400-normal.woff": "http://localhost:8080/assets/fonts/inter-cyrillic-400-normal.e2841352.woff",
"assets/fonts/inter-greek-400-normal.woff": "http://localhost:8080/assets/fonts/inter-greek-400-normal.a42da273.woff",
"assets/fonts/archivo-black-latin-ext-400-normal.woff": "http://localhost:8080/assets/fonts/archivo-black-latin-ext-400-normal.5ab5ba92.woff",
"assets/fonts/inter-greek-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-greek-400-normal.a8de720a.woff2",
"assets/fonts/inter-cyrillic-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-cyrillic-400-normal.cb04b2ee.woff2",
"assets/fonts/inter-greek-ext-400-normal.woff": "http://localhost:8080/assets/fonts/inter-greek-ext-400-normal.b9e1e894.woff",
"assets/fonts/inter-vietnamese-400-normal.woff": "http://localhost:8080/assets/fonts/inter-vietnamese-400-normal.96f8adc7.woff",
"assets/fonts/inter-greek-ext-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-greek-ext-400-normal.f2fa0d9e.woff2",
"assets/fonts/inter-vietnamese-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-vietnamese-400-normal.44c9df13.woff2",
"assets/images/marker-icon.png": "http://localhost:8080/assets/images/marker-icon.2b3e1faf.png",
"assets/images/layers-2x.png": "http://localhost:8080/assets/images/layers-2x.8f2c4d11.png",
"assets/images/layers.png": "http://localhost:8080/assets/images/layers.416d9136.png",
"assets/images/Close.svg": "http://localhost:8080/assets/images/Close.e4887675.svg",
"assets/vendors-node_modules_vue-facing-decorator_dist_esm_index_js-node_modules_vue-facing-decorator-818045.js": "http://localhost:8080/assets/vendors-node_modules_vue-facing-decorator_dist_esm_index_js-node_modules_vue-facing-decorator-818045.js"
}

View file

@ -1,20 +1,19 @@
/* @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500&display=swap'); */ /* @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500&display=swap'); */
/* @import url('https://fonts.googleapis.com/css?family=Roboto:400,400i,600,700'); */ /* @import url('https://fonts.googleapis.com/css?family=Roboto:400,400i,600,700'); */
@tailwind base;
@tailwind components;
@tailwind utilities;
@import '_checkbox-radio-switch.css'; @import '_checkbox-radio-switch.css';
@import '_progress.css'; @import '_progress.css';
@import '_scrollbars.css'; @import '_scrollbars.css';
@import '_table.css'; @import '_table.css';
/* @import '~leaflet/dist/leaflet.css'; */ @import '~leaflet/dist/leaflet.css';
@import '~/leaflet/dist/leaflet.css';
@import '@fontsource/inter/index.css'; @import '@fontsource/inter/index.css';
@import '@fontsource/archivo-black/index.css'; @import '@fontsource/archivo-black/index.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
:root { :root {
--color-main-background: #ffffff; --color-main-background: #ffffff;
--color-main-background-rgb: 255,255,255; --color-main-background-rgb: 255,255,255;

View file

@ -1,143 +1,162 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed, ComputedRef } 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';
interface MenuItem { const props = defineProps({
href?: string; item: {
route?: string; type: Object,
icon?: string; required: true,
label: string; },
target?: string; parentItem: {
color?: string; type: Object,
children?: MenuItem[]; required: false,
isOpen?: boolean; },
roles?: string[]; // isDropdownList: Boolean,
} });
const props = defineProps<{ const user: ComputedRef<User> = computed(() => {
item: MenuItem; return usePage().props.authUser as User;
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 : ''));
// Determine which element to render based on 'href' or 'route' const emit = defineEmits(['menu-click']);
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(() => {
// const children = computed(() => { // if (props.item.children && props.item.children.length > 0) {
// return props.item.children || []; // return children.value.some(childItem => stardust.isCurrent(childItem.route));
// }
// 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) {
// Toggle open state if the menu has children // if (isChildSelected.value == false) {
props.item.isOpen = !props.item.isOpen; // isDropdownOpen.value = !isDropdownOpen.value;
props.item.isOpen = !props.item.isOpen;
// }
} }
}; };
const activeStyle = computed(() => { // const handleChildSelected = () => {
// 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 'text-sky-600 font-bold'; return styleService.asideMenuItemActiveStyle;
} 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="isComponent" :href="props.item.href ? props.item.href : itemRoute" <!-- <component :is="itemHref ? 'div' : Link" :href="itemHref ? itemHref : itemRoute" -->
class="flex cursor-pointer dark:text-slate-300 dark:hover:text-white menu-item-wrapper" <component :is="is" :href="itemRoute ? stardust.route(props.item.route) : props.item.href"
:class="componentClass" @click="menuClick" :target="props.item.target || null"> class="flex cursor-pointer dark:text-slate-300 dark:hover:text-white menu-item-wrapper" :class="componentClass"
<BaseIcon v-if="props.item.icon" :path="props.item.icon" class="flex-none menu-item-icon" @click="menuClick" v-bind:target="props.item.target ?? null">
:class="activeStyle" w="w-16" :size="18" /> <BaseIcon v-if="item.icon" :path="item.icon" class="flex-none menu-item-icon" :class="activeInactiveStyle"
w="w-16" :size="18" />
<div class="menu-item-label"> <div class="menu-item-label">
<span class="grow text-ellipsis line-clamp-1" :class="[activeStyle]"> <span class="grow text-ellipsis line-clamp-1" :class="activeInactiveStyle">
{{ props.item.label }} {{ item.label }}
</span> </span>
</div> </div>
<!-- Display plus or minus icon if there are child items --> <!-- plus icon for expanding sub menu -->
<BaseIcon v-if="hasChildren" :path="isOpen ? mdiMinus : mdiPlus" class="flex-none" <BaseIcon v-if="hasChildren" :path="props.item.isOpen ? mdiMinus : mdiPlus" class="flex-none"
:class="[activeStyle]" w="w-12" /> :class="[activeInactiveStyle]" w="w-12" />
</component> </component>
<!-- Render dropdown -->
<div class="menu-item-dropdown" <div class="menu-item-dropdown"
:class="[styleService.asideMenuDropdownStyle, isOpen ? 'block dark:bg-slate-800/50' : 'hidden']" :class="[styleService.asideMenuDropdownStyle, props.item.isOpen ? 'block dark:bg-slate-800/50' : 'hidden']"
v-if="props.item.children && props.item.children.length > 0"> v-if="hasChildren">
<ul> <ul>
<!-- <li v-for="( child, index ) in children " :key="index">
<AsideMenuItem v-for="(childItem, index) in (props.item.children as any[])" :key="index" :item="childItem" <AsideMenuItem :item="child" :key="index"> </AsideMenuItem>
@menu-click="$emit('menu-click', $event, childItem)" /> </li> -->
<AsideMenuItem v-for="(childItem, index) in children" :key="index" :item="childItem" />
</ul> </ul>
</div> </div>
</li> <!-- <AsideMenuList v-if="hasChildren" :items="item.children"
:class="[styleService.asideMenuDropdownStyle, isDropdownOpen ? 'block dark:bg-slate-800/50' : 'hidden']" /> -->
</li>
</template> </template>
<style> <style>
@ -148,12 +167,17 @@ const hasRoles = computed(() => {
} }
.menu-item-icon { .menu-item-icon {
font-size: 2.5rem; font-size: 2.5rem;
/* 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;
} }
</style> </style>

View file

@ -36,24 +36,13 @@ const logoutItemClick = async () => {
await router.post(stardust.route('logout')); await router.post(stardust.route('logout'));
}; };
interface MenuItem { const menuClick = (event, item) => {
name: string;
label: string;
icon: string;
color: string;
link: string;
}
const menuClick = (event: Event, item: MenuItem) => {
emit('menu-click', event, item); emit('menu-click', event, item);
}; };
</script> </script>
<template> <template>
<aside <aside id="aside" class="lg:py-2 lg:pl-2 w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden">
id="aside"
class="lg:pb-2 lg:pl-2 w-60 fixed flex z-40 top-0 lg:top-16 h-screen lg:h-[calc(100vh-64px)] transition-position overflow-hidden"
>
<div :class="styleStore.asideStyle" class="lg:rounded-xl flex-1 flex flex-col overflow-hidden dark:bg-slate-900"> <div :class="styleStore.asideStyle" class="lg:rounded-xl flex-1 flex flex-col overflow-hidden dark:bg-slate-900">
<div :class="styleStore.asideBrandStyle" class="flex flex-row h-14 items-center justify-between dark:bg-slate-900"> <div :class="styleStore.asideBrandStyle" class="flex flex-row h-14 items-center justify-between dark:bg-slate-900">
<div class="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0"> <div class="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">

View file

@ -12,10 +12,6 @@ const props = defineProps({
type: String, type: String,
default: null, default: null,
}, },
showHeaderIcon: {
type: Boolean,
default: true,
},
headerIcon: { headerIcon: {
type: String, type: String,
default: null, default: null,
@ -67,7 +63,7 @@ const submit = (e) => {
<BaseIcon v-if="icon" :path="icon" class="mr-3" /> <BaseIcon v-if="icon" :path="icon" class="mr-3" />
{{ title }} {{ title }}
</div> </div>
<button v-if="showHeaderIcon" class="flex items-center py-3 px-4 justify-center ring-blue-700 focus:ring" @click="headerIconClick"> <button class="flex items-center py-3 px-4 justify-center ring-blue-700 focus:ring" @click="headerIconClick">
<BaseIcon :path="computedHeaderIcon" /> <BaseIcon :path="computedHeaderIcon" />
</button> </button>
</header> </header>

View file

@ -1,6 +1,6 @@
<script lang="ts" setup> <script 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,10 +27,6 @@ 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,
@ -46,11 +42,11 @@ const pillType = computed(() => {
return props.type; return props.type;
} }
if (props.count) { if (props.progress) {
if (props.count >= 20) { if (props.progress >= 60) {
return 'success'; return 'success';
} }
if (props.count >= 5) { if (props.progress >= 40) {
return 'warning'; return 'warning';
} }
@ -60,17 +56,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>
@ -87,17 +83,7 @@ const pillType = computed(() => {
</p> </p>
</div> </div>
</BaseLevel> </BaseLevel>
<!-- <PillTag :type="pillType" :text="text" small :icon="pillIcon" /> --> <PillTag :type="pillType" :text="pillText" 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>

View file

@ -1,107 +0,0 @@
<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>

View file

@ -198,7 +198,7 @@ import DeleteIcon from '@/Components/Icons/Delete.vue';
import RefreshIcon from '@/Components/Icons/Refresh.vue'; import RefreshIcon from '@/Components/Icons/Refresh.vue';
// import { Page, PageProps, Errors, ErrorBag } from '@inertiajs/inertia'; // import { Page, PageProps, Errors, ErrorBag } from '@inertiajs/inertia';
import Draggable from 'vuedraggable'; import Draggable from 'vuedraggable';
// import { Buffer } from 'buffer'; import { Buffer } from 'buffer';
import { TethysFile } from '@/Dataset'; import { TethysFile } from '@/Dataset';
// lastModified: 1691759507591 // lastModified: 1691759507591
@ -445,19 +445,18 @@ class FileUploadComponent extends Vue {
let localUrl: string = ''; let localUrl: string = '';
if (file instanceof File) { if (file instanceof File) {
localUrl = URL.createObjectURL(file as Blob); localUrl = URL.createObjectURL(file as Blob);
} } else if (file.fileData) {
// else if (file.fileData) { // const blob = new Blob([file.fileData]);
// // const blob = new Blob([file.fileData]); // localUrl = URL.createObjectURL(blob);
// // localUrl = URL.createObjectURL(blob); const parsed = JSON.parse(file.fileData);
// const parsed = JSON.parse(file.fileData); file.fileData = '';
// file.fileData = ''; // retrieve the original buffer of data
// // retrieve the original buffer of data const buff = Buffer.from(parsed.blob, 'base64');
// const buff = Buffer.from(parsed.blob, 'base64'); const blob = new Blob([buff], { type: 'application/octet-stream' });
// const blob = new Blob([buff], { type: 'application/octet-stream' }); // file.blob = blob;
// // file.blob = blob; localUrl = URL.createObjectURL(blob);
// localUrl = URL.createObjectURL(blob); file.fileSrc = localUrl;
// file.fileSrc = localUrl; }
// }
// setTimeout(() => { // setTimeout(() => {
// URL.revokeObjectURL(localUrl); // URL.revokeObjectURL(localUrl);

View file

@ -1,22 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { computed } from 'vue';
import FormCheckRadio from '@/Components/FormCheckRadio.vue'; import FormCheckRadio from '@/Components/FormCheckRadio.vue';
import BaseButton from '@/Components/BaseButton.vue';
import FormControl from '@/Components/FormControl.vue';
import { mdiPlusCircle } from '@mdi/js';
const props = defineProps({ const props = defineProps({
options: { options: {
type: Object, type: Object,
default: () => { }, default: () => {},
},
allowManualAdding: {
type: Boolean,
default: false,
},
manualAddingPlaceholder: {
type: String,
default: 'Add manually',
required: false,
}, },
name: { name: {
type: String, type: String,
@ -67,29 +55,6 @@ const computedValue = computed({
const hasIdAttribute = (obj: any): obj is { id: any } => { const hasIdAttribute = (obj: any): obj is { id: any } => {
return typeof obj === 'object' && 'id' in obj; return typeof obj === 'object' && 'id' in obj;
}; };
const newOption = ref<string>('');
const addOption = () => {
if (newOption.value && !props.options[newOption.value]) {
props.options[newOption.value] = newOption.value;
newOption.value = '';
}
};
const inputElClass = computed(() => {
const base = [
'px-3 py-2 max-w-full focus:ring focus:outline-none border-gray-700 rounded w-full',
'dark:placeholder-gray-400',
'h-12',
'border',
'bg-transparent'
// props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
];
// if (props.icon) {
// base.push('pl-10');
// }
return base;
});
</script> </script>
<template> <template>
@ -98,17 +63,15 @@ const inputElClass = computed(() => {
<!-- :label="value" --> <!-- :label="value" -->
<!-- :input-value="value.id" <!-- :input-value="value.id"
:label="value.name" --> :label="value.name" -->
<div v-if="allowManualAdding && type === 'checkbox'" class="flex items-center mt-2 mb-2 relative"> <FormCheckRadio
<input v-model="newOption" :placeholder="manualAddingPlaceholder" :class="inputElClass" v-for="(value, key) in options"
@keydown.prevent.enter="addOption" /> :key="key"
<svg v-show="newOption.length >= 2" @click.prevent="addOption" xmlns="http://www.w3.org/2000/svg" v-model="computedValue"
class="w-6 h-6 absolute right-2 top-1/2 transform -translate-y-1/2 cursor-pointer text-gray-500" :type="type"
viewBox="0 0 24 24" fill="currentColor"> :name="name"
<path :input-value="key"
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-6H5v-2h6V5h2v6h6v2h-6v6z" /> :label="value"
</svg> :class="componentClass"
</div> />
<FormCheckRadio v-for="(value, key) in options" :key="key" v-model="computedValue" :type="type" :name="name"
:input-value="key" :label="value" :class="componentClass" />
</div> </div>
</template> </template>

View file

@ -118,9 +118,6 @@ if (props.ctrlKFocus) {
mainService.isFieldFocusRegistered = false; mainService.isFieldFocusRegistered = false;
}); });
} }
const focus = () => {
inputEl?.value.focus();
};
</script> </script>
<template> <template>
@ -133,7 +130,7 @@ const focus = () => {
</option> </option>
</select> </select>
<textarea v-else-if="computedType === 'textarea'" :id="id" v-model="computedValue" :class="inputElClass" <textarea v-else-if="computedType === 'textarea'" :id="id" v-model="computedValue" :class="inputElClass"
:name="name" :placeholder="placeholder" :required="required" :readonly="isReadOnly"/> :name="name" :placeholder="placeholder" :required="required" />
<input v-else :id="id" ref="inputEl" v-model="computedValue" :name="name" :inputmode="inputmode" <input v-else :id="id" ref="inputEl" v-model="computedValue" :name="name" :inputmode="inputmode"
:autocomplete="autocomplete" :required="required" :placeholder="placeholder" :type="computedType" :autocomplete="autocomplete" :required="required" :placeholder="placeholder" :type="computedType"
:class="inputElClass" :readonly="isReadOnly" /> :class="inputElClass" :readonly="isReadOnly" />

View file

@ -2,7 +2,7 @@
import { computed, useSlots } from 'vue'; import { computed, useSlots } from 'vue';
const props = defineProps({ defineProps({
label: { label: {
type: String, type: String,
default: null, default: null,
@ -15,10 +15,6 @@ const props = defineProps({
type: String, type: String,
default: null, default: null,
}, },
// class: {
// type: Object,
// default: {},
// },
}); });
const slots = useSlots(); const slots = useSlots();
@ -40,7 +36,7 @@ const wrapperClass = computed(() => {
</script> </script>
<template> <template>
<div :class="['last:mb-0', 'mb-6']"> <div class="mb-6 last:mb-0">
<!-- <label v-if="label" :for="labelFor" class="block font-bold mb-2">{{ label }}</label> --> <!-- <label v-if="label" :for="labelFor" class="block font-bold mb-2">{{ label }}</label> -->
<label v-if="label" :for="labelFor" class="font-bold h-6 mt-3 text-xs leading-8 uppercase">{{ label }}</label> <label v-if="label" :for="labelFor" class="font-bold h-6 mt-3 text-xs leading-8 uppercase">{{ label }}</label>
<div v-bind:class="wrapperClass"> <div v-bind:class="wrapperClass">

View file

@ -268,7 +268,7 @@ export default class DrawControlComponent extends Vue {
position: absolute; position: absolute;
left: 10px; left: 10px;
top: 100px; top: 100px;
z-index: 40; z-index: 999;
} }
.btn-group-vertical button { .btn-group-vertical button {

View file

@ -372,9 +372,6 @@ export default class MapComponent extends Vue {
margin-top: 0.5em; margin-top: 0.5em;
} }
.leaflet-container .leaflet-pane {
z-index: 30!important;
}
/* .leaflet-pane { /* .leaflet-pane {
z-index: 30; z-index: 30;
} */ } */

View file

@ -100,7 +100,7 @@ export default class ZoomControlComponent extends Vue {
position: absolute; position: absolute;
left: 10px; left: 10px;
top: 10px; top: 10px;
z-index: 40; z-index: 999;
} }
.btn-group-vertical button { .btn-group-vertical button {

View file

@ -1,138 +0,0 @@
<template>
<div class="relative mb-4">
<!-- <label for="mimetype-input" class="block text-sm font-medium text-gray-700">Search for Mimetypes</label> -->
<input id="mimetype-input" v-model="newExtension" type="text" placeholder="Enter Mimetype Name"
class="block w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-blue-500 focus:border-blue-500"
:class="inputElClass" @input="handleInputChange" @keydown.down="onArrowDown" @keydown.up="onArrowUp"
@keydown.prevent.enter="onEnter">
</input>
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="newExtension.length < 2"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="newExtension.length >= 2"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
@click="clearInput">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
<ul v-if="showDropdown && filteredMimetypes.length"
class="bg-white dark:bg-slate-800 w-full mt-2 max-h-28 overflow-y-auto scroll-smooth">
<li v-for="(mimeType, index) in filteredMimetypes" :key="index" @click="selectResult(mimeType)"
class="pl-8 pr-2 py-1 border-b-2 border-gray-100 relative cursor-pointer hover:bg-blue-500 hover:text-white"
:class="{
'bg-blue-500 text-white': selectedIndex === index,
'bg-white text-gray-900': selectedIndex !== index
}" :ref="(el) => {
if (ul) {
ul[index] = el as HTMLLIElement;
}
}">
<svg class="absolute w-4 h-4 left-2 top-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
<span>{{ mimeType }}</span>
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { ref, Ref, watch, computed } from 'vue';
// import mime from 'mime';
const emit = defineEmits(['onSelectResult', 'onClearInput'])
const props = defineProps({
borderless: Boolean,
transparent: Boolean,
mimeTypes: {
type: Array as () => string[],
required: true
},
// form: Object,
// isValidMimeType: Function,
});
const newExtension = ref('');
const showDropdown = ref(false);
const filteredMimetypes = ref<string[]>([]);
const selectedIndex: Ref<number> = ref(0);
const ul: Ref<HTMLLIElement[] | null> = ref<HTMLLIElement[]>([]);
watch(selectedIndex, (selectedIndex: number) => {
if (selectedIndex != null && ul.value != null) {
const currentElement: HTMLLIElement = ul.value[selectedIndex];
currentElement &&
currentElement?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'start',
});
}
});
const inputElClass = computed(() => {
const base = [
'block p-2.5 w-full z-20 text-sm text-gray-900 bg-gray-50 rounded-r-lg',
'dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500',
'h-12',
props.borderless ? 'border-0' : 'border',
props.transparent ? 'bg-transparent' : 'bg-white dark:bg-slate-800',
];
base.push('pl-10');
return base;
});
const handleInputChange = (e: Event) => {
const target = <HTMLInputElement>e.target;
newExtension.value = target.value;
if (newExtension.value.length >= 2) {
showDropdown.value = true;
filteredMimetypes.value = props.mimeTypes.filter(mimeType =>
mimeType.toLowerCase().includes(newExtension.value.toLowerCase())
);
} else {
showDropdown.value = false;
}
};
const selectResult = (mimeType: string) => {
showDropdown.value = false;
newExtension.value = '';
selectedIndex.value = -1;
emit('onSelectResult', mimeType);
};
const clearInput = () => {
newExtension.value = '';
showDropdown.value = false;
// props.form.name = '';
// props.resetFileExtensions();
emit('onClearInput');
};
const onArrowDown = () => {
if (filteredMimetypes.value.length > 0) {
selectedIndex.value = selectedIndex.value === filteredMimetypes.value.length - 1 ? 0 : selectedIndex.value + 1;
}
};
const onArrowUp = () => {
if (filteredMimetypes.value.length > 0) {
selectedIndex.value = selectedIndex.value == 0 || selectedIndex.value == -1 ? filteredMimetypes.value.length - 1 : selectedIndex.value - 1;
}
};
const onEnter = () => {
if (Array.isArray(filteredMimetypes.value) && filteredMimetypes.value.length && selectedIndex.value !== -1 && selectedIndex.value < filteredMimetypes.value.length) {
const mimeType = filteredMimetypes.value[selectedIndex.value];
selectResult(mimeType);
}
};
</script>

View file

@ -72,7 +72,7 @@ const menuNavBarToggle = () => {
const menuOpenLg = () => { const menuOpenLg = () => {
layoutStore.isAsideLgActive = true; layoutStore.isAsideLgActive = true;
}; };
const userHasRoles = (roleNames: Array<string>): boolean => { const userHasRoles = (roleNames): boolean => {
return user.value.roles.some(role => roleNames.includes(role.name)); return user.value.roles.some(role => roleNames.includes(role.name));
}; };
@ -95,7 +95,7 @@ const showAbout = async () => {
</script> </script>
<template> <template>
<nav class="text-base top-0 left-0 right-0 fixed bg-lime h-14 z-50 w-screen transition-position lg:w-auto dark:bg-slate-800" <nav class="text-base top-0 left-0 right-0 fixed bg-gray-50 h-14 z-40 w-screen transition-position lg:w-auto dark:bg-slate-800"
:class="{ 'xl:pl-60': props.showBurger == true }"> :class="{ 'xl:pl-60': props.showBurger == true }">
<FirstrunWizard ref="about"></FirstrunWizard> <FirstrunWizard ref="about"></FirstrunWizard>
<div class="flex lg:items-stretch" :class="containerMaxW"> <div class="flex lg:items-stretch" :class="containerMaxW">
@ -122,10 +122,10 @@ const showAbout = async () => {
<BaseIcon :path="isMenuNavBarActive ? mdiClose : mdiDotsVertical" size="24" /> <BaseIcon :path="isMenuNavBarActive ? mdiClose : mdiDotsVertical" size="24" />
</NavBarItem> </NavBarItem>
</div> </div>
<div class="fixed w-screen top-14 left-0 shadow lg:w-auto lg:items-stretch lg:flex lg:grow lg:static lg:border-b-0 lg:overflow-visible lg:shadow-none dark:bg-slate-800" <div class="absolute w-screen top-14 left-0 bg-gray-50 shadow lg:w-auto lg:items-stretch lg:flex lg:grow lg:static lg:border-b-0 lg:overflow-visible lg:shadow-none dark:bg-slate-800"
:class="[isMenuNavBarActive ? 'block' : 'hidden']"> :class="[isMenuNavBarActive ? 'block' : 'hidden']">
<div <div
class="bg-white lg:bg-lime dark:bg-transparent max-h-screen-menu overflow-y-auto lg:overflow-visible lg:flex lg:items-stretch lg:justify-end lg:ml-auto"> class="max-h-screen-menu overflow-y-auto lg:overflow-visible lg:flex lg:items-stretch lg:justify-end lg:ml-auto">
<!-- help menu --> <!-- help menu -->
<NavBarMenu> <NavBarMenu>
@ -150,7 +150,7 @@ const showAbout = async () => {
<!-- personal menu --> <!-- personal menu -->
<NavBarMenu> <NavBarMenu>
<NavBarItemLabel v-bind:label="`hello ${user.login}`"> <NavBarItemLabel v-bind:label="`hello ${user.login}`">
<UserAvatarCurrentUser :user="user" class="w-6 h-6 mr-3 inline-flex" /> <UserAvatarCurrentUser class="w-6 h-6 mr-3 inline-flex" />
</NavBarItemLabel> </NavBarItemLabel>
<template #dropdown> <template #dropdown>
<!-- <NavBarItem> --> <!-- <NavBarItem> -->
@ -186,7 +186,7 @@ const showAbout = async () => {
<NavBarItem is-desktop-icon-only @click.prevent="toggleLightDark"> <NavBarItem is-desktop-icon-only @click.prevent="toggleLightDark">
<NavBarItemLabel v-bind:icon="mdiThemeLightDark" label="Light/Dark" is-desktop-icon-only /> <NavBarItemLabel v-bind:icon="mdiThemeLightDark" label="Light/Dark" is-desktop-icon-only />
</NavBarItem> </NavBarItem>
<NavBarItem href="https://gitea.geosphere.at/geolba/tethys.backend" target="_blank" is-desktop-icon-only> <NavBarItem href="https://gitea.geologie.ac.at/geolba/tethys" target="_blank" is-desktop-icon-only>
<NavBarItemLabel v-bind:icon="mdiGithub" label="GitHub" is-desktop-icon-only /> <NavBarItemLabel v-bind:icon="mdiGithub" label="GitHub" is-desktop-icon-only />
</NavBarItem> </NavBarItem>
<NavBarItem is-desktop-icon-only @click="showAbout"> <NavBarItem is-desktop-icon-only @click="showAbout">

View file

@ -1,4 +1,4 @@
<script lang="ts" setup> <script setup>
import { StyleService } from '@/Stores/style.service'; import { StyleService } from '@/Stores/style.service';
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'; import { computed, ref, onMounted, onBeforeUnmount } from 'vue';
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'; import { mdiChevronUp, mdiChevronDown } from '@mdi/js';
@ -15,10 +15,10 @@ const toggle = () => {
isDropdownActive.value = !isDropdownActive.value; isDropdownActive.value = !isDropdownActive.value;
}; };
const root = ref(NavBarItem); const root = ref(null);
const forceClose = (event: MouseEvent) => { const forceClose = (event) => {
if (!root.value?.$el.contains(event.target)) { if (!root.value.$el.contains(event.target)) {
isDropdownActive.value = false; isDropdownActive.value = false;
} }
}; };

View file

@ -5,9 +5,9 @@ import SectionBanner from '@/Components/SectionBanner.vue';
</script> </script>
<template> <template>
<SectionBanner bg="greenBlue"> <SectionBanner bg="greenBlue">
<h1 class="text-3xl text-white mb-6">Like the project? Please star on <b>GeoSphere Git Repository</b>!</h1> <h1 class="text-3xl text-white mb-6">Like the project? Please star on <b>Gitea</b>!</h1>
<div> <div>
<BaseButton href="https://gitea.geosphere.at/geolba/tethys.backend" :icon="mdiGithub" label="Forgejo" target="_blank" rounded-full /> <BaseButton href="https://gitea.geologie.ac.at/geolba/tethys" :icon="mdiGithub" label="Gitea" target="_blank" rounded-full />
</div> </div>
</SectionBanner> </SectionBanner>
</template> </template>

View file

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed, ref, watch } from 'vue';
import { checkStrength } from './logic/index'; import { checkStrength } from './logic/index';
import { mdiFormTextboxPassword } from '@mdi/js'; import { mdiFormTextboxPassword } from '@mdi/js';
import FormField from '@/Components/FormField.vue'; import FormField from '@/Components/FormField.vue';
@ -7,25 +7,17 @@ import FormControl from '@/Components/FormControl.vue';
// Define props // Define props
const props = defineProps<{ const props = defineProps<{
modelValue: string; password: string;
errors: Partial<Record<"new_password" | "old_password" | "confirm_password", string>>; errors: Partial<Record<"new_password" | "old_password" | "confirm_password", string>>;
}>(); }>();
const emit = defineEmits(['update:modelValue', 'score']); const emit = defineEmits(['update:password', 'score']);
// // A local reactive variable for password input // A local reactive variable for password input
// const localPassword = ref(props.modelValue); const localPassword = ref(props.password);
// // Watch localPassword and emit changes back to the parent // Watch localPassword and emit changes back to the parent
// watch(localPassword, (newValue) => { watch(localPassword, (newValue) => {
// emit('update:modelValue', newValue); emit('update:password', newValue);
// });
const localPassword = computed({
get: () => props.modelValue,
set: (value) => {
emit('update:modelValue', value);
// const { score } = checkStrength(localPassword.value);
// emit('score', score);
},
}); });
type PasswordMetrics = { type PasswordMetrics = {
@ -61,7 +53,7 @@ const passwordMetrics = computed<PasswordMetrics>(() => {
<template> <template>
<!-- Password input Form --> <!-- Password input Form -->
<FormField label="New password" help="Required. New password" :class="{'text-red-400': errors.new_password }"> <FormField label="New password" help="Required. New password" :class="{ 'text-red-400': errors.new_password }">
<FormControl v-model="localPassword" :icon="mdiFormTextboxPassword" name="new_password" type="password" required <FormControl v-model="localPassword" :icon="mdiFormTextboxPassword" name="new_password" type="password" required
:error="errors.new_password"> :error="errors.new_password">
<!-- Secure Icon --> <!-- Secure Icon -->
@ -80,10 +72,6 @@ const passwordMetrics = computed<PasswordMetrics>(() => {
{{ errors.new_password }} {{ errors.new_password }}
</div> </div>
</FormControl> </FormControl>
<!-- Score Display -->
<div class="text-gray-700 text-sm">
{{ passwordMetrics.score }} / 6 points max
</div>
</FormField> </FormField>
<!-- Password Strength Bar --> <!-- Password Strength Bar -->
@ -105,9 +93,9 @@ const passwordMetrics = computed<PasswordMetrics>(() => {
</ul> </ul>
</div> </div>
<!-- Score Display --> <!-- Score Display -->
<!-- <div class="text-gray-700 text-sm"> <div class="text-gray-700 text-sm">
{{ passwordMetrics.score }} / 6 points max {{ passwordMetrics.score }} / 6 points max
</div> --> </div>
</template> </template>
<style lang="css" scoped> <style lang="css" scoped>

View file

@ -1,19 +1,17 @@
<script lang="ts" setup> <script setup>
import { computed, ref, Ref } from 'vue'; import { computed, 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 } from '@mdi/js'; import { mdiEye, mdiTrashCan } 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,
}); });
const styleService = StyleService(); const styleService = StyleService();
@ -21,124 +19,128 @@ 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++) {
pagesList.push(i); for (let i = 0; i < numPages.value; i++) {
} pagesList.push(i);
return pagesList; }
return pagesList;
}); });
const remove = (arr, cb) => { const remove = (arr, cb) => {
const newArr = []; const newArr = [];
arr.forEach((item) => {
if (!cb(item)) { arr.forEach((item) => {
newArr.push(item); if (!cb(item)) {
} newArr.push(item);
}); }
return newArr; });
return newArr;
}; };
const checked = (isChecked, client) => { const checked = (isChecked, client) => {
if (isChecked) { if (isChecked) {
checkedRows.value.push(client); checkedRows.value.push(client);
} else { } else {
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="currentClient ? currentClient.login : ''"> <CardBoxModal v-model="isModalActive" title="Sample modal">
<div v-if="currentClient"> <p>Lorem ipsum dolor sit amet <b>adipiscing elit</b></p>
<p>Login: {{ currentClient.login }}</p> <p>This is sample modal</p>
<p>Email: {{ currentClient.email }}</p> </CardBoxModal>
<p>Created: {{ currentClient?.created_at ? dayjs(currentClient.created_at).format('MMM D, YYYY h:mm A') : 'N/A' }}
</p> <CardBoxModal v-model="isModalDangerActive" large-title="Please confirm" button="danger" has-cancel>
<p>Lorem ipsum dolor sit amet <b>adipiscing elit</b></p>
<p>This is sample modal</p>
</CardBoxModal>
<div v-if="checkedRows.length" class="p-3 bg-gray-100/50 dark:bg-slate-800">
<span
v-for="checkedRow in checkedRows"
:key="checkedRow.id"
class="inline-block px-2 py-1 rounded-sm mr-2 text-sm bg-gray-100 dark:bg-slate-700"
>
{{ checkedRow.name }}
</span>
</div> </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>This is sample modal</p>
</CardBoxModal> -->
<div v-if="checkedRows.length" class="p-3 bg-gray-100/50 dark:bg-slate-800"> <table>
<span v-for="checkedRow in checkedRows" :key="checkedRow.id" <thead>
class="inline-block px-2 py-1 rounded-sm mr-2 text-sm bg-gray-100 dark:bg-slate-700"> <tr>
{{ checkedRow.login }} <th v-if="checkable" />
</span> <th />
</div> <th>Name</th>
<th>Email</th>
<table> <th>City</th>
<thead> <th>Progress</th>
<tr> <th>Created</th>
<th v-if="checkable" /> <th />
<th /> </tr>
<th>Login</th> </thead>
<th>Email</th> <tbody>
<th>Created</th> <tr v-for="client in itemsPaginated" :key="client.id">
<th /> <TableCheckboxCell v-if="checkable" @checked="checked($event, client)" />
</tr> <td class="border-b-0 lg:w-6 before:hidden">
</thead> <UserAvatar :username="client.name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
<tbody> </td>
<tr v-for="client in itemsPaginated" :key="client.id"> <td data-label="Name">
<TableCheckboxCell v-if="checkable" @checked="checked($event, client)" /> {{ client.name }}
<td class="border-b-0 lg:w-6 before:hidden"> </td>
<!-- <UserAvatar :username="client.login" :avatar="client.avatar" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" /> --> <td data-label="Email">
<div v-if="client.avatar"> {{ client.email }}
<UserAvatar :default-url="client.avatar ? '/public' + client.avatar : ''" </td>
:username="client.first_name + ' ' + client.last_name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" /> <td data-label="City">
</div> {{ client.city }}
</td>
<td data-label="Progress" class="lg:w-32">
<div v-else> <progress class="flex w-2/5 self-center lg:w-full" max="100" v-bind:value="client.progress">
<UserAvatar :username="client.first_name + ' ' + client.last_name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" /> {{ client.progress }}
</div> </progress>
</td> </td>
<td data-label="Login"> <td data-label="Created" class="lg:w-1 whitespace-nowrap">
{{ client.login }} <small class="text-gray-500 dark:text-slate-400" :title="client.created">{{ client.created }}</small>
</td> </td>
<td data-label="Email"> <td class="before:hidden lg:w-1 whitespace-nowrap">
{{ client.email }} <BaseButtons type="justify-start lg:justify-end" no-wrap>
</td> <BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" />
<td data-label="Created"> <BaseButton color="danger" :icon="mdiTrashCan" small @click="isModalDangerActive = true" />
<small class="text-gray-500 dark:text-slate-400" </BaseButtons>
:title="client.created_at ? dayjs(client.created_at).format('MMM D, YYYY h:mm A') : 'N/A'"> </td>
{{ client.created_at ? dayjs(client.created_at).format('MMM D, YYYY h:mm A') : 'N/A' }} </tr>
</small> </tbody>
</td> </table>
<td class="before:hidden lg:w-1 whitespace-nowrap"> <div class="p-3 lg:px-6 border-t border-gray-100 dark:border-slate-800">
<BaseButtons type="justify-start lg:justify-end" no-wrap> <BaseLevel>
<BaseButton color="info" :icon="mdiEye" small @click="showModal(client)" /> <BaseButtons>
<!-- <BaseButton color="danger" :icon="mdiTrashCan" small @click="isModalDangerActive = true" /> --> <BaseButton
</BaseButtons> v-for="page in pagesList"
</td> :key="page"
</tr> :active="page === currentPage"
</tbody> :label="page + 1"
</table> small
<div class="p-3 lg:px-6 border-t border-gray-100 dark:border-slate-800"> :outline="styleService.darkMode"
<BaseLevel> @click="currentPage = page"
<BaseButtons> />
<BaseButton v-for="page in pagesList" :key="page" :active="page === currentPage" :label="page + 1" small </BaseButtons>
:outline="styleService.darkMode" @click="currentPage = page" /> <small>Page {{ currentPageHuman }} of {{ numPages }}</small>
</BaseButtons> </BaseLevel>
<small>Page {{ currentPageHuman }} of {{ numPages }}</small> </div>
</BaseLevel> </template>
</div>
</template>

View file

@ -1,4 +1,4 @@
<script lang="ts" setup> <script setup>
import { computed } from 'vue'; import { computed } from 'vue';
const props = defineProps({ const props = defineProps({
@ -6,9 +6,9 @@ const props = defineProps({
type: String, type: String,
required: true, required: true,
}, },
defaultUrl: { avatar: {
type: String, type: String,
required: false default: null,
}, },
api: { api: {
type: String, type: String,
@ -16,63 +16,93 @@ const props = defineProps({
}, },
}); });
const avatar = computed(() => { const avatar = computed(
return props.defaultUrl ?? generateAvatarUrl(props.username); // () => props.avatar ?? `https://avatars.dicebear.com/api/${props.api}/${props.username?.replace(/[^a-z0-9]+/i, '-')}.svg`
});
// () => props.avatar ?? `https://avatars.dicebear.com/api/initials/${props.username}.svg`,
() => {
const initials = props.username
.split(' ')
.map((part) => part.charAt(0).toUpperCase())
.join('');
return props.avatar ?? generateAvatarUrl(props.username);
},
);
const username = computed(() => props.username); const username = computed(() => props.username);
// const darkenColor = (color: string) => { const darkenColor = (color) => {
// const r = parseInt(color.slice(0, 2), 16); // Convert hex to RGB
// const g = parseInt(color.slice(2, 4), 16); const r = parseInt(color.slice(0, 2), 16);
// const b = parseInt(color.slice(4, 6), 16); const g = parseInt(color.slice(2, 4), 16);
const b = parseInt(color.slice(4, 6), 16);
// const darkerR = Math.round(r * 0.6); // Calculate darker color by reducing 20% of each RGB component
// const darkerG = Math.round(g * 0.6); const darkerR = Math.round(r * 0.6);
// const darkerB = Math.round(b * 0.6); const darkerG = Math.round(g * 0.6);
const darkerB = Math.round(b * 0.6);
// const darkerColor = ((darkerR << 16) + (darkerG << 8) + darkerB).toString(16); // Convert back to hex
const darkerColor = ((darkerR << 16) + (darkerG << 8) + darkerB).toString(16);
// return darkerColor.padStart(6, '0'); return darkerColor.padStart(6, '0'); // Ensure it's 6 digits
// }; };
// const getColorFromName = (name: string): string => { const getRandomColor = () => {
// let hash = 0; return Math.floor(Math.random() * 16777215).toString(16);
// for (let i = 0; i < name.length; i++) { };
// hash = name.charCodeAt(i) + ((hash << 5) - hash);
// }
// let color = '#';
// for (let i = 0; i < 3; i++) {
// const value = (hash >> (i * 8)) & 0xff;
// color += ('00' + value.toString(16)).substr(-2);
// }
// return color.replace('#', '');
// };
// const lightenColor = (hexColor: string, percent: number): string => { const adjustOpacity = (hexColor, opacity) => {
// let r = parseInt(hexColor.substring(0, 2), 16); // Remove # if present
// let g = parseInt(hexColor.substring(2, 4), 16); hexColor = hexColor.replace('#', '');
// let b = parseInt(hexColor.substring(4, 6), 16); // Convert hex to RGB
// const r = parseInt(hexColor.slice(0, 2), 16);
// const g = parseInt(hexColor.slice(2, 4), 16);
// const b = parseInt(hexColor.slice(4, 6), 16);
// r = Math.floor(r * (100 + percent) / 100); // const r = parseInt(hexColor.slice(1, 3), 16);
// g = Math.floor(g * (100 + percent) / 100); // const g = parseInt(hexColor.slice(3, 5), 16);
// b = Math.floor(b * (100 + percent) / 100); // const b = parseInt(hexColor.slice(5, 7), 16);
const [r, g, b] = hexColor.match(/\w\w/g).map(x => parseInt(x, 16));
// r = (r < 255) ? r : 255; return `rgba(${r},${g},${b},${opacity})`;
// g = (g < 255) ? g : 255; };
// b = (b < 255) ? b : 255;
// const lighterHex = ((r << 16) | (g << 8) | b).toString(16); const lightenColor = (hexColor, percent) => {
let r = parseInt(hexColor.substring(0, 2), 16);
let g = parseInt(hexColor.substring(2, 4), 16);
let b = parseInt(hexColor.substring(4, 6), 16);
// return lighterHex.padStart(6, '0'); r = Math.floor(r * (100 + percent) / 100);
// }; g = Math.floor(g * (100 + percent) / 100);
b = Math.floor(b * (100 + percent) / 100);
const generateAvatarUrl = (name: string): string => { r = (r < 255) ? r : 255;
// const originalColor = getColorFromName(name); g = (g < 255) ? g : 255;
// const backgroundColor = lightenColor(originalColor, 60); b = (b < 255) ? b : 255;
// const textColor = darkenColor(originalColor);
const avatarUrl = `/api/avatar?name=${name}&size=50`; const lighterHex = ((r << 16) | (g << 8) | b).toString(16);
return lighterHex.padStart(6, '0');
};
// backgroundColor = '7F9CF5',
const generateAvatarUrl = (name) => {
const initials = name
.split(' ')
.map((part) => part.charAt(0).toUpperCase())
.join('');
const originalColor = getRandomColor();
const backgroundColor = lightenColor(originalColor, 60); // Lighten by 20%
const textColor = darkenColor(originalColor);
// const avatarUrl = `https://ui-avatars.com/api/?name=${initials}&size=50&background=${backgroundColor}&color=${textColor}`;
const avatarUrl = `/api/avatar?name=${name}&size=50&background=${backgroundColor}&textColor=${textColor}`;
return avatarUrl; return avatarUrl;
}; };
</script> </script>

View file

@ -1,29 +1,12 @@
<script setup> <script setup>
// import { computed } from 'vue'; import { computed } from 'vue';
// import { usePage } from '@inertiajs/vue3'; // import { usePage } from '@inertiajs/vue3'
import { usePage } from '@inertiajs/vue3';
import UserAvatar from '@/Components/UserAvatar.vue'; import UserAvatar from '@/Components/UserAvatar.vue';
defineProps({ const userName = computed(() => usePage().props.auth?.user.name);
user: {
type: Object,
required: true,
},
});
</script> </script>
<template> <template>
<UserAvatar v-bind:username="'userName'" api="initials" />
<div v-if="user.avatar">
<UserAvatar :default-url="user.avatar ? '/public' + user.avatar : ''"
:username="user.first_name + ' ' + user.last_name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
</div>
<div v-else>
<UserAvatar :username="user.first_name + ' ' + user.last_name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
</div>
</template> </template>
<!-- <template v-else>
<UserAvatar :username="user.first_name + ' ' + user.last_name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
</template> -->

View file

@ -1,30 +0,0 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { colorsBgLight, colorsOutline } from '@/colors';
const props = defineProps({
on: Boolean,
icon: {
type: String,
default: null,
},
outline: Boolean,
color: {
type: String,
required: false,
default: 'info',
},
});
const componentClass = computed(() => (props.outline ? colorsOutline[props.color] : colorsBgLight[props.color]));
</script>
<template>
<div >
<transition leave-active-class="transition ease-in duration-1000" leave-from-class="opacity-100"
leave-to-class="opacity-0" :class="componentClass" class="px-3 py-2 last:mb-0 border rounded transition-colors duration-150 text-sm text-gray-600">
<div v-show="on">
<slot />
</div>
</transition>
</div>
</template>

View file

@ -1,79 +0,0 @@
<template>
<div class="relative inline-block overflow-hidden rounded-full">
<input type="file" ref="avatarInput" @change="onChangeFile" class="hidden" accept="image/*">
<img :src="avatarUrl" alt="Avatar" class="h-full w-full object-cover">
<div class="absolute top-0 h-full w-full bg-black bg-opacity-25 flex items-center justify-center">
<button @click.prevent="browse"
class="rounded-full hover:bg-white hover:bg-opacity-25 p-2 focus:outline-none text-white transition-colors duration-300">
<IconRounded :icon="mdiCameraEnhanceOutline" class="bg-transparent h-6 w-6" />
</button>
<button v-if="file" @click.prevent="reset"
class="rounded-full hover:bg-white hover:bg-opacity-25 p-2 focus:outline-none text-white transition-colors duration-300">
<IconRounded :icon="mdiAlphaXCircleOutline " class="bg-transparent h-6 w-6" />
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, defineProps, defineEmits } from 'vue';
import { mdiCameraEnhanceOutline, mdiAlphaXCircleOutline } from '@mdi/js';
import IconRounded from './IconRounded.vue';
const props = defineProps({
modelValue: File,
defaultSrc: {
type: String,
required: true,
},
});
// vue data properties
const file = ref<File | null>(null);
const avatarUrl = ref<string>(props.defaultSrc);
const avatarInput = ref<HTMLInputElement | null>(null);
// const avatarUrl = computed({
// get: () => props.modelValue ? props.modelValue : props.defaultSrc,
// set: (value: string) => {
// emit('update:modelValue', value);
// },
// });
const emit = defineEmits<{ (e: 'update:modelValue', file: File | null): void; (e: 'input', file: File | null): void }>();
// const avatarInput = ref<HTMLInputElement | null>(null);
const browse = () => {
avatarInput.value?.click();
};
const reset = () => {
file.value = null;
avatarUrl.value = props.defaultSrc;
emit('input', file.value);
};
const onChangeFile = (e: Event) => {
// const target = (<HTMLInputElement>e.target)
const target = e.target as HTMLInputElement;
if (target.files && target.files[0]) {
file.value = target.files[0];
}
if (file.value) {
emit('input', file.value);
emit('update:modelValue', file.value);
let reader = new FileReader();
reader.readAsDataURL(file.value);
reader.onload = (e) => {
avatarUrl.value = e.target?.result as string;
};
}
};
</script>

View file

@ -1,42 +1,43 @@
<script lang="ts" setup> <script setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue'
import { MainService } from '@/Stores/main'; import { MainService } from '@/Stores/main'
import { mdiCheckDecagram } from '@mdi/js'; import { mdiCheckDecagram } from '@mdi/js'
import BaseLevel from '@/Components/BaseLevel.vue'; import BaseLevel from '@/Components/BaseLevel.vue'
import UserAvatarCurrentUser from '@/Components/UserAvatarCurrentUser.vue'; import UserAvatarCurrentUser from '@/Components/UserAvatarCurrentUser.vue'
import CardBox from '@/Components/CardBox.vue'; import CardBox from '@/Components/CardBox.vue'
import FormCheckRadioGroup from '@/Components/FormCheckRadioGroup.vue'; import FormCheckRadioGroup from '@/Components/FormCheckRadioGroup.vue'
import PillTag from '@/Components/PillTag.vue'; import PillTag from '@/Components/PillTag.vue'
import { usePage } from '@inertiajs/vue3';
import type { User } from '@/Dataset';
import type { ComputedRef } from 'vue';
const mainService = MainService(); const mainService = MainService()
const userName = computed(() => mainService.userName); const userName = computed(() => mainService.userName)
const user: ComputedRef<User> = computed(() => { const userSwitchVal = ref([])
return usePage().props.authUser as User;
});
const userSwitchVal = ref([]);
</script> </script>
<template> <template>
<CardBox> <CardBox>
<BaseLevel type="justify-around lg:justify-center"> <BaseLevel type="justify-around lg:justify-center">
<UserAvatarCurrentUser :user="user" class="lg:mx-12" /> <UserAvatarCurrentUser class="lg:mx-12" />
<div class="space-y-3 text-center md:text-left lg:mx-12"> <div class="space-y-3 text-center md:text-left lg:mx-12">
<div class="flex justify-center md:block"> <div class="flex justify-center md:block">
<FormCheckRadioGroup v-model="userSwitchVal" name="sample-switch" type="switch" <FormCheckRadioGroup
:options="{ one: 'Notifications' }" /> v-model="userSwitchVal"
name="sample-switch"
type="switch"
:options="{ one: 'Notifications' }"
/>
</div> </div>
<h1 class="text-2xl"> <h1 class="text-2xl">
Howdy, <b>{{ userName }}</b>! Howdy, <b>{{ userName }}</b>!
</h1> </h1>
<p>Last login <b>12 mins ago</b> from <b>127.0.0.1</b></p> <p>Last login <b>12 mins ago</b> from <b>127.0.0.1</b></p>
<div class="flex justify-center md:block"> <div class="flex justify-center md:block">
<PillTag text="Verified" type="info" :icon="mdiCheckDecagram" /> <PillTag
text="Verified"
type="info"
:icon="mdiCheckDecagram"
/>
</div> </div>
</div> </div>
</BaseLevel> </BaseLevel>

View file

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { usePage } from '@inertiajs/vue3'; import { Head, usePage } from '@inertiajs/vue3';
import { mdiAccountKey, mdiSquareEditOutline, mdiAlertBoxOutline } from '@mdi/js'; import { mdiAccountKey, mdiSquareEditOutline, mdiAlertBoxOutline } from '@mdi/js';
import { computed, ComputedRef } from 'vue'; import { computed, ComputedRef } from 'vue';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue'; import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';

View file

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive } from 'vue'; import { ref, watch, computed, Ref, reactive } from 'vue';
import { Head, useForm } from '@inertiajs/vue3'; import { Head, useForm } from '@inertiajs/vue3';
import { mdiAccountKey, mdiArrowLeftBoldOutline, mdiTrashCan, mdiImageText, mdiPlus } from '@mdi/js'; import { mdiAccountKey, mdiArrowLeftBoldOutline } 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 SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue'; import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
@ -10,97 +10,115 @@ import BaseDivider from '@/Components/BaseDivider.vue';
import BaseButton from '@/Components/BaseButton.vue'; import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue'; import BaseButtons from '@/Components/BaseButtons.vue';
import { stardust } from '@eidellev/adonis-stardust/client'; import { stardust } from '@eidellev/adonis-stardust/client';
import mime from 'mime';
import FormField from '@/Components/FormField.vue'; import FormField from '@/Components/FormField.vue';
import FormControl from '@/Components/FormControl.vue'; import FormControl from '@/Components/FormControl.vue';
import standardTypes from 'mime/types/standard.js'; import standardTypes from 'mime/types/standard.js';
import otherTypes from 'mime/types/other.js'; import otherTypes from 'mime/types/other.js';
import FormCheckRadioGroup from '@/Components/FormCheckRadioGroup.vue'; import FormCheckRadioGroup from '@/Components/FormCheckRadioGroup.vue';
import MimetypeInput from '@/Components/MimetypeInput.vue';
defineProps({ const props = defineProps({
permissions: {
type: Object,
default: () => ({}),
},
borderless: Boolean, borderless: Boolean,
transparent: Boolean, transparent: Boolean,
ctrlKFocus: Boolean, ctrlKFocus: Boolean,
}); });
const customTypes: { [key: string]: string[] } = {
'application/vnd.opengeospatial.geopackage+sqlite3': ['gpkg'],
'text/plain': ['txt', 'asc', 'c', 'cc', 'h', 'srt'],
};
const isReadOnly: boolean = true; const isReadOnly: boolean = true;
// const standardMimeTypes = Object.keys(standardTypes); const standardMimeTypes = Object.keys(standardTypes);
// const otherMimeTypes = Object.keys(otherTypes); const otherMimeTypes = Object.keys(otherTypes);
// const customMimeTypes = Object.keys(customTypes); const mimeTypes = [...standardMimeTypes, ...otherMimeTypes];
const mimeTypesMap = new Map<string, string[]>();
Object.entries(standardTypes).forEach(([mimeType, extensions]) => {
mimeTypesMap.set(mimeType, extensions);
});
Object.entries(otherTypes).forEach(([mimeType, extensions]) => {
mimeTypesMap.set(mimeType, extensions);
});
Object.entries(customTypes).forEach(([mimeType, extensions]) => {
mimeTypesMap.set(mimeType, extensions);
});
const mimeTypes = Array.from(mimeTypesMap.keys());
const file_extensions = reactive<Record<string, string>>({}); const file_extensions = reactive<Record<string, string>>({});
interface FormData { interface FormData {
name: string; name: string;
file_extension: string[]; file_extension: string[];
alternate_mimetype: string[];
enabled: boolean; enabled: boolean;
[key: string]: string | string[] | boolean;
} }
const form = useForm<FormData>({ const form = useForm<FormData>({
name: '', name: '',
file_extension: [], file_extension: [],
alternate_mimetype: [],
enabled: true, enabled: true,
}); });
const filteredMimetypes = ref<string[]>([]); // Stores the filtered MIME types for the dropdown
const showDropdown = ref(false); // Controls the visibility of the autocomplete dropdown
const selectedIndex: Ref<number> = ref(0); // Track selected MIME type in the dropdown
const ul: Ref<HTMLLIElement[] | null> = ref<HTMLLIElement[]>([]);
const newExtension: Ref = ref(''); //reactive([] as Array<string>);
const mimetypeError = ref<string | null>(null); const mimetypeError = ref<string | null>(null);
const addAlternateMimetype = () => { watch(selectedIndex, (selectedIndex: number) => {
form.alternate_mimetype.push(""); if (selectedIndex != null && ul.value != null) {
}; const currentElement: HTMLLIElement = ul.value[selectedIndex];
const removeAliasMimetype = (index: number) => { currentElement &&
form.alternate_mimetype.splice(index, 1); currentElement?.scrollIntoView({
}; behavior: 'smooth',
block: 'nearest',
inline: 'start',
});
}
});
// Function to reset the object
function resetFileExtensions() { function resetFileExtensions() {
// Reset to an empty object
Object.keys(file_extensions).forEach(key => { Object.keys(file_extensions).forEach(key => {
delete file_extensions[key]; delete file_extensions[key];
}); });
} }
const inputElClass = computed(() => {
const base = [
'block p-2.5 w-full z-20 text-sm text-gray-900 bg-gray-50 rounded-r-lg',
'dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500',
'h-12',
props.borderless ? 'border-0' : 'border',
props.transparent ? 'bg-transparent' : 'bg-white dark:bg-slate-800',
];
// if (props.icon) {
base.push('pl-10');
// }
return base;
});
// Check if the MIME type is valid
const isValidMimeType = (mimeType: string): boolean => { const isValidMimeType = (mimeType: string): boolean => {
let extensions = mimeTypesMap.get(mimeType) || null; let extensions = mime.getExtension(mimeType)
return extensions !== null; return extensions !== null;
}; };
const clearInput = () => { async function handleInputChange(e: Event) {
// newExtension.value = ''; const target = <HTMLInputElement>e.target;
// showDropdown.value = false; newExtension.value = target.value;
form.name = '';
resetFileExtensions();
};
if (newExtension.value.length >= 2) {
showDropdown.value = true;
filteredMimetypes.value = mimeTypes.filter(mimeType =>
mimeType.toLowerCase().includes(newExtension.value.toLowerCase())
);
} else {
// data.results = [];
showDropdown.value = false;
}
}
// Handle MIME type selection from the dropdown
const selectResult = (mimeType: string) => { const selectResult = (mimeType: string) => {
form.name = mimeType; form.name = mimeType;
// file_extensions.values = [];
resetFileExtensions(); resetFileExtensions();
// showDropdown.value = false; showDropdown.value = false;
// newExtension.value = ''; newExtension.value = ''; // Reset the input
// selectedIndex.value = -1; selectedIndex.value = -1;
if (form.name && isValidMimeType(form.name)) { if (form.name && isValidMimeType(form.name)) {
// const extensions = mime.getAllExtensions(form.name) as Set<string>; const extensions = mime.getAllExtensions(form.name) as Set<string>;
const extensions = mimeTypesMap.get(mimeType); // Iterate over each extension and set both key and value to the extension
extensions?.forEach(extension => { Array.from(extensions).forEach(extension => {
file_extensions[extension] = extension; file_extensions[extension] = extension;
}); });
} else { } else {
@ -108,14 +126,61 @@ const selectResult = (mimeType: string) => {
} }
}; };
function onArrowDown() {
if (filteredMimetypes.value.length > 0) {
selectedIndex.value = selectedIndex.value === filteredMimetypes.value.length - 1 ? 0 : selectedIndex.value + 1;
// const currentElement: HTMLLIElement = ul.value[selectedIndex.value];
}
}
function onArrowUp() {
if (filteredMimetypes.value.length > 0) {
selectedIndex.value = selectedIndex.value == 0 || selectedIndex.value == -1 ? filteredMimetypes.value.length - 1 : selectedIndex.value - 1;
}
}
function onEnter() {
if (Array.isArray(filteredMimetypes.value) && filteredMimetypes.value.length && selectedIndex.value !== -1 && selectedIndex.value < filteredMimetypes.value.length) {
const mimeType = filteredMimetypes.value[selectedIndex.value];
// this.$emit('person', person);
form.name = mimeType;
// reset form file extensions
// file_extensions.values = [];
resetFileExtensions();
showDropdown.value = false;
newExtension.value = ''; // Reset the input
selectedIndex.value = -1;
if (form.name) {
// clear all loaded file extensions
// file_extensions.values = [];
resetFileExtensions();
if (isValidMimeType(form.name)) {
let extensions = mime.getAllExtensions(form.name) as Set<string>;
// Convert the Set to an array of objects
// Convert the Set to an object
Array.from(extensions).forEach(extension => {
file_extensions[extension] = extension;
});
// file_extensions.push(...formattedExtensions);
} else {
mimetypeError.value = 'Invalid MIME type.';
}
}
}
}
// Handle form submission
const submit = async () => { const submit = async () => {
if (isValidForm()) { if (isValidForm()) {
await form.post(stardust.route('settings.mimetype.store'), { await form.post(stardust.route('settings.mimetype.store'), {
preserveScroll: true, preserveScroll: true,
}); });
} }
}; };
// Form validation before submission
const isValidForm = (): boolean => { const isValidForm = (): boolean => {
if (!form.name) { if (!form.name) {
form.errors.name = 'Name is required.'; form.errors.name = 'Name is required.';
@ -125,7 +190,6 @@ const isValidForm = (): boolean => {
} }
if (!form.file_extension.length) { if (!form.file_extension.length) {
form.errors.file_extension = 'At least one file extension is required.'; form.errors.file_extension = 'At least one file extension is required.';
return false; return false;
} }
return true; return true;
@ -141,15 +205,59 @@ const isValidForm = (): boolean => {
<BaseButton :route-name="stardust.route('settings.mimetype.index')" :icon="mdiArrowLeftBoldOutline" <BaseButton :route-name="stardust.route('settings.mimetype.index')" :icon="mdiArrowLeftBoldOutline"
label="Back" color="white" rounded-full small /> label="Back" color="white" rounded-full small />
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<CardBox form> <!-- <CardBox form @submit.prevent="form.post(stardust.route('role.store'))"> -->
<MimetypeInput @on-select-result="selectResult" @on-clear-input="clearInput" :transparent="transparent" <CardBox form @submit.prevent="submit()">
:borderless="borderless" :mimeTypes="mimeTypes" :isValidMimeType="isValidMimeType" />
<!-- MIME Type Input Field with Autocomplete -->
<div class="relative mb-4">
<input v-model="newExtension" type="text" placeholder="Enter Mimetype Name"
class="block w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-blue-500 focus:border-blue-500"
:class="inputElClass" @input="handleInputChange" @keydown.down="onArrowDown"
@keydown.up="onArrowUp" @keydown.prevent.enter="onEnter">
</input>
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="newExtension.length < 2"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="newExtension.length >= 2"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" @click="() => {
newExtension = '';
showDropdown = false;
form.name = '';
resetFileExtensions();
}
">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
<ul v-if="showDropdown && filteredMimetypes.length"
class="bg-white dark:bg-slate-800 w-full mt-2 max-h-28 overflow-y-auto scroll-smooth">
<li v-for="(mimeType, index) in filteredMimetypes" :key="index" @click="selectResult(mimeType)"
class="pl-8 pr-2 py-1 border-b-2 border-gray-100 relative cursor-pointer hover:bg-blue-500 hover:text-white"
:class="{
'bg-blue-500 text-white': selectedIndex === index,
'bg-white text-gray-900': selectedIndex !== index
}" :ref="(el) => {
if (ul) {
ul[index] = el as HTMLLIElement;
}
}">
<svg class="absolute w-4 h-4 left-2 top-2" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
<span>{{ mimeType }}</span>
</li>
</ul>
</div>
<div v-if="mimetypeError" class="text-red-400 text-sm mt-1"> <div v-if="mimetypeError" class="text-red-400 text-sm mt-1">
{{ mimetypeError }} {{ mimetypeError }}
</div> </div>
<BaseDivider v-if="form.name" />
<FormField v-if="form.name" label="Mimetype Name" :class="{ 'text-red-400': form.errors.name }"> <FormField v-if="form.name" label="Mimetype Name" :class="{ 'text-red-400': form.errors.name }">
<FormControl v-model="form.name" name="display_name" :error="form.errors.name" <FormControl v-model="form.name" name="display_name" :error="form.errors.name"
:is-read-only=isReadOnly> :is-read-only=isReadOnly>
@ -158,79 +266,34 @@ const isValidForm = (): boolean => {
</div> </div>
</FormControl> </FormControl>
</FormField> </FormField>
<!-- <FormField v-if="form.name" help="Activate mimetype immediately?" wrap-body
<FormField v-if="form.name" help="Activate mimetype immediately?" wrap-body
class="mt-8 w-full mx-2 flex-1 flex-col"> class="mt-8 w-full mx-2 flex-1 flex-col">
<label for="rights" class="checkbox mr-6 mb-3 last:mr-0"> <label for="rights" class="checkbox mr-6 mb-3 last:mr-0">
<input type="checkbox" id="rights" required v-model="form.enabled" /> <input type="checkbox" id="rights" required v-model="form.enabled" />
<span class="check" /> <span class="check" />
<a class="pl-2" target="_blank">Enable mimetype immediately </a> <a class="pl-2" target="_blank">Enable mimetype immediately </a>
</label>
</FormField> -->
<FormField v-if="Object.keys(file_extensions).length > 0" label="File Extensions" wrap-body>
<!-- <div class="flex items-center mt-2">
<FormControl v-model="newExtension" placeholder="Enter file extension" class="mr-2" />
<BaseButton color="info" @click="addFileExtension" label="Add" />
</div> -->
<FormCheckRadioGroup v-model="form.file_extension" :options="file_extensions" name="file_extensions"
is-column allow-manual-adding manual-adding-placeholder="Enter additional file extension manually" />
</label>
</FormField> </FormField>
<div class="text-red-400 text-sm" v-if="form.errors.file_extension">
{{ form.errors.file_extension }} <FormField label="Extensions" wrap-body>
<FormCheckRadioGroup v-model="form.file_extension" :options="file_extensions" name="file_extensions"
is-column />
</FormField>
<div class="text-red-400 text-sm"
v-if="form.errors.file_extension && Array.isArray(form.errors.file_extension)">
{{ form.errors.file_extension.join(', ') }}
</div> </div>
<BaseDivider v-if="Object.keys(file_extensions).length > 0" /> <BaseDivider />
<CardBox v-if="form.name" class="mb-6 shadow" has-table :icon="mdiImageText" title="Alternate Mimetypes"
:header-icon="mdiPlus" @header-icon-click="addAlternateMimetype">
<div v-if="form.alternate_mimetype.length === 0" class="text-center py-4">
<p class="text-gray-600">No alternate mimetypes added yet.</p>
<p class="text-gray-400">
Click the plus icon above to add a new alternate mimetype.
<br>
An alternate mimetype is needed to ensure compatibility across different systems and
software.
For example, the GeoPackage standard mimetype is
'application/vnd.opengeospatial.geopackage+sqlite3', but most software stores it as
'application/x-sqlite3'. Therefore, 'application/x-sqlite3' must be added as an alternate
mimetype.
</p>
</div>
<table class="table-fixed border-green-900" v-if="form.alternate_mimetype.length > 0">
<thead>
<tr>
<th class="w-10/12">Alias Mimetype</th>
<th class="w-2/12"></th>
</tr>
</thead>
<tbody>
<tr v-for="(alias, index) in form.alternate_mimetype" :key="index">
<td>
<FormControl required v-model="form.alternate_mimetype[index]"
placeholder="[alternate mimetype]">
<div class="text-red-400 text-sm"
v-if="Array.isArray(form.errors[`alternate_mimetype.${index}`])">
{{ form.errors[`alternate_mimetype.${index}`]?.join(', ') }}
</div>
</FormControl>
</td>
<td class="before:hidden lg:w-1 whitespace-nowrap">
<BaseButton color="danger" :icon="mdiTrashCan" small
@click.prevent="removeAliasMimetype(index)" />
</td>
</tr>
</tbody>
</table>
<div class="text-red-400 text-sm"
v-if="form.errors.alternate_mimetype && Array.isArray(form.errors.alternate_mimetype)">
{{ form.errors.alternate_mimetype.join(', ') }}
</div>
</CardBox>
<template #footer> <template #footer>
<BaseButtons> <BaseButtons>
<BaseButton type="submit" color="info" label="Create" :class="{ 'opacity-25': form.processing }" <BaseButton type="submit" color="info" label="Create" :class="{ 'opacity-25': form.processing }"
:disabled="form.processing" @click.prevent="submit()" /> :disabled="form.processing" />
</BaseButtons> </BaseButtons>
</template> </template>
</CardBox> </CardBox>

View file

@ -32,7 +32,7 @@ const form = useForm({
// isPreferationRequired: false, // isPreferationRequired: false,
}); });
const handleSubmit = async (e: Event) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
await form.delete(stardust.route('settings.mimetype.deleteStore', [props.mimetype.id])); await form.delete(stardust.route('settings.mimetype.deleteStore', [props.mimetype.id]));
}; };

View file

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Head, usePage } from '@inertiajs/vue3'; import { Head, usePage } from '@inertiajs/vue3';
import { mdiAccountKey, mdiSquareEditOutline, mdiAlertBoxOutline, mdiPlus, mdiTrashCan, mdiCheckCircle, mdiCloseCircle } from '@mdi/js'; import { mdiAccountKey, mdiSquareEditOutline, mdiAlertBoxOutline, mdiPlus, mdiTrashCan } from '@mdi/js';
import { computed, ComputedRef } from 'vue'; import { computed, ComputedRef } from 'vue';
import type { PropType } from "vue"; import type { PropType } from "vue";
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue'; import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
@ -16,7 +16,6 @@ interface MimeType {
id: number; id: number;
name: string; name: string;
file_extension: string; file_extension: string;
alternate_mimetype: string;
enabled: boolean; enabled: boolean;
} }
@ -55,41 +54,24 @@ const flash: ComputedRef<any> = computed(() => {
{{ flash.message }} {{ flash.message }}
</NotificationBar> </NotificationBar>
<CardBox class="mb-6" has-table> <CardBox class="mb-6" has-table>
<table class="min-w-full divide-y divide-gray-200"> <table>
<thead> <thead>
<tr> <tr>
<th class="px-4 py-2 text-left text-sm font-medium uppercase tracking-wider">Mimetype</th> <th>Name</th>
<th class="px-4 py-2 text-left text-sm font-medium uppercase tracking-wider">Alternate Mime Types</th> <th>Status</th>
<th v-if="can.edit" class="px-4 py-2 text-left text-sm font-medium uppercase tracking-wider">Actions</th> <th v-if="can.edit">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200"> <tbody>
<tr v-for="mimetype in mimetypes" :key="mimetype.id"> <tr v-for="mimetype in mimetypes" :key="mimetype.id">
<td class="px-4 py-2 whitespace-nowrap text-left text-sm" data-label="Name"> <td data-label="Name">
<span class="flex items-center"> {{ mimetype.name }} ({{ mimetype.file_extension }})
<svg viewBox="0 0 24 24" v-if="mimetype.enabled" :class="{'text-green-500': mimetype.enabled}" class="w-4 h-4 mr-2">
<path fill="currentColor" :d="mdiCheckCircle" />
</svg>
<svg v-else viewBox="0 0 24 24" :class="{'text-red-500': !mimetype.enabled}" class="w-4 h-4 mr-2">
<path fill="currentColor" :d="mdiCloseCircle" />
</svg>
<br>
<span class="truncate block max-w-xs">{{ mimetype.name }}</span>
</span>
<ul class="list-none pl-0">
<li v-for="ext in mimetype.file_extension?.split('|')" :key="ext" class="flex items-center truncate block max-w-xs">- .{{ ext }}</li>
</ul>
</td> </td>
<td class="px-4 py-2 whitespace-nowrap text-left text-sm" data-label="Alternate Mime Types"> <td data-label="Status">
<ul class="list-none pl-0">
<li v-for="alt in mimetype.alternate_mimetype?.split('|')" :key="alt" class="flex items-center truncate block max-w-xs">- {{ alt }}</li>
</ul>
</td>
<!-- <td class="px-4 py-2 whitespace-nowrap" data-label="Status">
<template v-if="mimetype.enabled">Active</template> <template v-if="mimetype.enabled">Active</template>
<template v-else>Inactive</template> <template v-else>Inactive</template>
</td> --> </td>
<td v-if="can.edit" class="before:hidden lg:w-1 whitespace-nowrap px-4 py-2 whitespace-nowrap text-left text-sm"> <td v-if="can.edit" 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 v-if="mimetype.enabled" <BaseButton v-if="mimetype.enabled"
:route-name="stardust.route('settings.mimetype.down', [mimetype.id])" :route-name="stardust.route('settings.mimetype.down', [mimetype.id])"

View file

@ -28,7 +28,7 @@ const form = useForm({
}); });
const submit = async () => { const submit = async () => {
await form.post(stardust.route('settings.role.store')); await form.post(stardust.route('settings.role.store'), form);
}; };
</script> </script>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup>
import { Head, useForm } from '@inertiajs/vue3'; import { Head, Link, useForm } from '@inertiajs/vue3';
import { mdiAccountKey, mdiArrowLeftBoldOutline, mdiFormTextarea } from '@mdi/js'; import { mdiAccountKey, mdiArrowLeftBoldOutline, mdiFormTextarea } 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';
@ -29,15 +29,15 @@ const props = defineProps({
}); });
const form = useForm({ const form = useForm({
_method: 'put',
name: props.role.name, name: props.role.name,
description: props.role.description, description: props.role.description,
permissions: props.roleHasPermissions, permissions: props.roleHasPermissions,
}); });
const submit = async () => { const submit = async () => {
// await Inertia.post(stardust.route('user.store'), form); old // await Inertia.post(stardust.route('user.store'), form);
await form.put(stardust.route('settings.role.update', [props.role.id])); await form.put(stardust.route('settings.role.update', [props.role.id]), form);
// await router.put(stardust.route('settings.role.update', [props.role.id]), form);
}; };
</script> </script>
@ -58,7 +58,7 @@ const submit = async () => {
<!-- <CardBox form @submit.prevent="form.put(stardust.route('role.update', [props.role.id]))"> --> <!-- <CardBox form @submit.prevent="form.put(stardust.route('role.update', [props.role.id]))"> -->
<CardBox form @submit.prevent="submit()"> <CardBox form @submit.prevent="submit()">
<FormField label="Name" :class="{ 'text-red-400': form.errors.name }"> <FormField label="Name" :class="{ 'text-red-400': form.errors.name }">
<FormControl v-model="form.name" type="text" placeholder="Enter Name" required :error="form.errors.name" :is-read-only=true> <FormControl v-model="form.name" type="text" placeholder="Enter Name" required :error="form.errors.name" is-read-only="true">
<div class="text-red-400 text-sm" v-if="form.errors.name"> <div class="text-red-400 text-sm" v-if="form.errors.name">
{{ form.errors.name }} {{ form.errors.name }}
</div> </div>

View file

@ -12,7 +12,7 @@ import {
mdiAsterisk, mdiAsterisk,
mdiFormTextboxPassword, mdiFormTextboxPassword,
mdiArrowLeftBoldOutline, mdiArrowLeftBoldOutline,
mdiAlertBoxOutline, mdiAlertBoxOutline,
} from '@mdi/js'; } from '@mdi/js';
import SectionMain from '@/Components/SectionMain.vue'; import SectionMain from '@/Components/SectionMain.vue';
import CardBox from '@/Components/CardBox.vue'; import CardBox from '@/Components/CardBox.vue';
@ -36,16 +36,16 @@ import PasswordMeter from '@/Components/SimplePasswordMeter/password-meter.vue';
const emit = defineEmits(['confirm', 'update:confirmation']) const emit = defineEmits(['confirm', 'update:confirmation'])
// const enabled = ref(false); const enabled = ref(false);
// const handleScore = (score: number) => { const handleScore = (score: number) => {
// if (score >= 4) { if (score >= 4){
// enabled.value = true; enabled.value = true;
// } else { } else {
// enabled.value = false; enabled.value = false;
// } }
// // strengthLabel.value = scoreLabel; // strengthLabel.value = scoreLabel;
// // score.value = scoreValue; // score.value = scoreValue;
// }; };
defineProps({ defineProps({
// user will be returned from controller action // user will be returned from controller action
@ -82,20 +82,20 @@ defineProps({
// }; // };
// const passwordForm = useForm({ const passwordForm = useForm({
// old_password: '', old_password: '',
// new_password: '', new_password: '',
// confirm_password: '', confirm_password: '',
// }); });
// const passwordSubmit = async () => { const passwordSubmit = async () => {
// await passwordForm.post(stardust.route('account.password.store'), { await passwordForm.post(stardust.route('account.password.store'), {
// preserveScroll: true, preserveScroll: true,
// onSuccess: () => { onSuccess: () => {
// // console.log(resp); // console.log(resp);
// passwordForm.reset(); passwordForm.reset();
// }, },
// }); });
// }; };
const flash: Ref<any> = computed(() => { const flash: Ref<any> = computed(() => {
return usePage().props.flash; return usePage().props.flash;
@ -139,10 +139,40 @@ const flash: Ref<any> = computed(() => {
{{ $page.props.flash.message }} {{ $page.props.flash.message }}
</NotificationBar> --> </NotificationBar> -->
<div class="grid grid-cols-1 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- <div class="grid grid-cols-1 lg:grid-cols-1 gap-6"> -->
<!-- <CardBox id="passwordForm" title="Change Password" :icon="mdiLock" form <!-- password form -->
@submit.prevent="passwordSubmit()"> <!-- <CardBox title="Edit Profile" :icon="mdiAccountCircle" form @submit.prevent="profileForm.post(route('admin.account.info.store'))"> -->
<!-- <CardBox title="Edit Profile" :icon="mdiAccountCircle" form @submit.prevent="profileSubmit()">
<FormField label="Login" help="Required. Your login name" :class="{ 'text-red-400': errors.login }">
<FormControl v-model="profileForm.login" v-bind:icon="mdiAccount" name="login" required :error="errors.login">
<div class="text-red-400 text-sm" v-if="errors.login">
{{ errors.login }}
</div>
</FormControl>
</FormField>
<FormField label="Email" help="Required. Your e-mail" :class="{ 'text-red-400': errors.email }">
<FormControl v-model="profileForm.email" :icon="mdiMail" type="email" name="email" required :error="errors.email">
<div class="text-red-400 text-sm" v-if="errors.email">
{{ errors.email }}
</div>
</FormControl>
</FormField>
<template #footer>
<BaseButtons>
<BaseButton color="info" type="submit" label="Submit" />
</BaseButtons>
</template>
</CardBox> -->
<!-- password form -->
<!-- <CardBox title="Change Password" :icon="mdiLock" form @submit.prevent="passwordForm.post(route('admin.account.password.store'), {
preserveScroll: true,
onSuccess: () => passwordForm.reset(),
}) "> -->
<CardBox id="passwordForm" title="Change Password" :icon="mdiLock" form @submit.prevent="passwordSubmit()">
<FormValidationErrors v-bind:errors="errors" /> <FormValidationErrors v-bind:errors="errors" />
<FormField label="Current password" help="Required. Your current password" <FormField label="Current password" help="Required. Your current password"
@ -156,15 +186,22 @@ const flash: Ref<any> = computed(() => {
</FormField> </FormField>
<BaseDivider /> <BaseDivider />
<PasswordMeter v-model="passwordForm.new_password" :errors="passwordForm.errors" <!-- <FormField label="New password" help="Required. New password"
@score="handleScore" /> :class="{ 'text-red-400': passwordForm.errors.new_password }">
<FormControl v-model="passwordForm.new_password" :icon="mdiFormTextboxPassword" name="new_password"
type="password" required :error="passwordForm.errors.new_password">
<div class="text-red-400 text-sm" v-if="passwordForm.errors.new_password">
{{ passwordForm.errors.new_password }}
</div>
</FormControl>
</FormField> -->
<PasswordMeter v-model:password="passwordForm.new_password" :errors="passwordForm.errors" @score="handleScore" />
<FormField label="Confirm password" help="Required. New password one more time" <FormField label="Confirm password" help="Required. New password one more time"
:class="{ 'text-red-400': passwordForm.errors.confirm_password }"> :class="{ 'text-red-400': passwordForm.errors.confirm_password }">
<FormControl v-model="passwordForm.confirm_password" :icon="mdiFormTextboxPassword" <FormControl v-model="passwordForm.confirm_password" :icon="mdiFormTextboxPassword"
name="confirm_password" type="password" required name="confirm_password" type="password" required :error="passwordForm.errors.confirm_password">
:error="passwordForm.errors.confirm_password">
<div class="text-red-400 text-sm" v-if="passwordForm.errors.confirm_password"> <div class="text-red-400 text-sm" v-if="passwordForm.errors.confirm_password">
{{ passwordForm.errors.confirm_password }} {{ passwordForm.errors.confirm_password }}
</div> </div>
@ -182,17 +219,16 @@ const flash: Ref<any> = computed(() => {
<template #footer> <template #footer>
<BaseButtons> <BaseButtons>
<BaseButton type="submit" color="info" label="Change password" <BaseButton type="submit" color="info" label="Change password" :disabled="passwordForm.processing == true || enabled == false" />
:disabled="passwordForm.processing == true || enabled == false" />
</BaseButtons> </BaseButtons>
</template> </template>
</CardBox> --> </CardBox>
<PersonalTotpSettings :twoFactorEnabled="twoFactorEnabled" :backupState="backupState"> <PersonalTotpSettings :twoFactorEnabled="twoFactorEnabled" :backupState="backupState">
</PersonalTotpSettings> </PersonalTotpSettings>
<!-- <PersonalSettings :state="backupState"/> --> <!-- <PersonalSettings :state="backupState"/> -->
<!-- <CardBox v-if="!props.twoFactorEnabled" title="Two-Factor Authentication" :icon="mdiInformation" form <!-- <CardBox v-if="!props.twoFactorEnabled" title="Two-Factor Authentication" :icon="mdiInformation" form
@submit.prevent="enableTwoFactorAuthentication()"> @submit.prevent="enableTwoFactorAuthentication()">
@ -212,7 +248,7 @@ const flash: Ref<any> = computed(() => {
</template> </template>
</CardBox> --> </CardBox> -->
</div> </div>
</SectionMain> </SectionMain>

View file

@ -6,7 +6,7 @@
<!-- <SectionFullScreen v-slot="{ cardClass }" :bg="'greenBlue'"> --> <!-- <SectionFullScreen v-slot="{ cardClass }" :bg="'greenBlue'"> -->
<SectionFullScreen v-slot="{ cardClass }"> <SectionFullScreen v-slot="{ cardClass }">
<a class="text-2xl font-semibold flex justify-center items-center mb-8 lg:mb-10"> <a class="text-2xl font-semibold flex justify-center items-center mb-8 lg:mb-10">
<img src="../../logo.svg" class="h-10 mr-4" alt="Windster Logo" /> <img src="/logo.svg" class="h-10 mr-4" alt="Windster Logo" />
<!-- <span class="self-center text-2xl font-bold whitespace-nowrap">Tethys</span> --> <!-- <span class="self-center text-2xl font-bold whitespace-nowrap">Tethys</span> -->
</a> </a>

View file

@ -20,7 +20,7 @@
import AuthLayout from '@/Layouts/Auth.vue'; import AuthLayout from '@/Layouts/Auth.vue';
import { reactive } from 'vue'; import { reactive } from 'vue';
import { useForm } from '@inertiajs/vue3'; import { useForm } from '@inertiajs/vue3';
// import { Inertia } from '@inertiajs/inertia'; import { Inertia } from '@inertiajs/inertia';
// import { NButton, NInput } from 'naive-ui'; // import { NButton, NInput } from 'naive-ui';
// import { useForm } from '@inertiajs/inertia-vue3' // import { useForm } from '@inertiajs/inertia-vue3'
import FormInput from '@/Components/FormInput.vue'; import FormInput from '@/Components/FormInput.vue';
@ -45,7 +45,7 @@ export default {
}); });
const submit = async () => { const submit = async () => {
// await Inertia.post('/app/register', form); await Inertia.post('/app/register', form);
}; };
return { form, submit }; return { form, submit };

View file

@ -2,6 +2,7 @@
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,
@ -12,18 +13,20 @@ 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 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();
@ -33,39 +36,43 @@ 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.fetch('clients'); mainService.fetchApi('authors');
// mainService.fetch('history'); mainService.fetchApi('datasets');
// 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, 5)); const authorBarItems = computed(() => mainService.authors.slice(0, 4));
const authors = computed(() => mainService.authors); const authors = computed(() => mainService.authors);
const datasets = computed(() => mainService.datasets); const datasets = computed(() => mainService.datasets);
const datasetBarItems = computed(() => mainService.datasets.slice(0, 5)); // const props = defineProps({
// let test = datasets.value; // user: {
// console.log(test); // type: Object,
// 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
href="https://gitea.geosphere.at/geolba/tethys.backend" href="https://gitea.geologie.ac.at/geolba/tethys"
target="_blank" target="_blank"
:icon="mdiGithub" :icon="mdiGithub"
label="Star on GeoSphere Forgejo" label="Star on Gitea"
color="contrast" color="contrast"
rounded-full rounded-full
small small
@ -89,13 +96,16 @@ const datasetBarItems = computed(() => mainService.datasets.slice(0, 5));
: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="+25%" trend="Overflow"
trend-type="up" trend-type="alert"
color="text-purple-500" color="text-red-500"
:icon="mdiChartTimelineVariant" :icon="mdiChartTimelineVariant"
:number="52" :number="256"
label="Citations" suffix="%"
label="Performance"
/> />
</div> </div>
@ -107,15 +117,19 @@ const datasetBarItems = computed(() => mainService.datasets.slice(0, 5));
:name="client.name" :name="client.name"
:email="client.email" :email="client.email"
:date="client.created_at" :date="client.created_at"
:text="client.identifier_orcid" :text="client.datasetCount"
:count="client.dataset_count"
/> />
</div> </div>
<div class="flex flex-col justify-between"> <div class="flex flex-col justify-between">
<CardBoxDataset <CardBoxTransaction
v-for="(dataset, index) in datasetBarItems" v-for="(transaction, index) in transactionBarItems"
:key="index" :key="index"
:dataset="dataset" :amount="transaction.amount"
:date="transaction.date"
:business="transaction.business"
:type="transaction.type"
:name="transaction.name"
:account="transaction.account"
/> />
</div> </div>
</div> </div>
@ -129,13 +143,33 @@ const datasetBarItems = computed(() => mainService.datasets.slice(0, 5));
</div> </div>
</CardBox> </CardBox>
<SectionTitleLineWithButton :icon="mdiAccountMultiple" title="Submitters" /> <SectionTitleLineWithButton :icon="mdiAccountMultiple" title="Submitters (to do)" />
<!-- <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>

View file

@ -26,6 +26,17 @@ 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`);

View file

@ -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, mdiGithub } from '@mdi/js'; import { mdiChartTimelineVariant } 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,15 +48,14 @@ 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.geosphere.at/geolba/tethys" target="_blank" :icon="mdiGithub" <!-- <BaseButton href="https://gitea.geologie.ac.at/geolba/tethys" target="_blank" :icon="mdiGithub"
label="Star on GeoSPhere Forgejo" color="contrast" rounded-full small /> label="Star on Gitea" 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 /> -->
@ -81,20 +80,19 @@ 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 <span class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
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 <span class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
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"><i <span v-if="openAccessLicences.includes(dataset.licence)" class="label titlecase"
class="fas fa-lock-open"></i> Open Access</span> ><i class="fas fa-lock-open"></i> Open Access</span
>
</p> </p>
</div> </div>
</div> </div>

View file

@ -1,343 +1,91 @@
<template> <template>
<LayoutAuthenticated> <div class="flex flex-col h-screen p-4 bg-gray-100">
<header class="flex justify-between items-center mb-4">
<Head title="Profile"></Head> <h1 class="text-xl font-bold">SKOS Browser</h1>
<SectionMain> <div class="flex space-x-2">
<SectionTitleLineWithButton :icon="mdiLibraryShelves" title="Library Classification" main> <button @click="updateApp" title="Update the application">
<div class="bg-lime-100 shadow rounded-lg p-6 mb-6 flex items-center justify-between"> <img src="/Resources/Images/refresh.png" alt="Update" class="w-4 h-4" />
<div> </button>
<label for="role-select" class="block text-lg font-medium text-gray-700 mb-1"> <button @click="showInfo" title="Info">
Select Classification Role <span class="text-red-500">*</span> <img src="/Resources/Images/info.png" alt="Info" class="w-4 h-4" />
</label> </button>
<select id="role-select" v-model="selectedCollectionRole"
class="w-full border border-gray-300 rounded-md p-2 text-gray-700 focus:ring-2 focus:ring-indigo-500"
required>
<!-- <option value="" disabled selected>Please select a role</option> -->
<option v-for="collRole in collectionRoles" :key="collRole.id" :value="collRole">
{{ collRole.name }}
</option>
</select>
</div>
<div class="ml-4 hidden md:block">
<span class="text-sm text-gray-600 italic">* required</span>
</div>
</div>
</SectionTitleLineWithButton>
<!-- Available TopLevel Collections -->
<CardBox class="mb-4 rounded-lg p-4">
<h2 class="text-lg font-bold text-gray-800 dark:text-slate-400 mb-2">Available Toplevel-Collections
<span v-if="selectedCollectionRole && !selectedToplevelCollection"
class="text-sm text-red-500 italic">(click to
select)</span>
</h2>
<ul class="flex flex-wrap gap-2">
<li v-for="col in collections" :key="col.id" :class="{
'cursor-pointer p-2 border border-gray-200 rounded hover:bg-sky-50 text-sky-700 text-sm': true,
'bg-sky-100 border-sky-500': selectedToplevelCollection && selectedToplevelCollection.id === col.id
}" @click="onToplevelCollectionSelected(col)">
{{ `${col.name} (${col.number})` }}
</li>
<li v-if="collections.length === 0" class="text-gray-800 dark:text-slate-400">
No collections available.
</li>
</ul>
</CardBox>
<!-- Collections Listing -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<!-- Broader Collection (Parent) -->
<CardBox v-if="selectedCollection" class="rounded-lg p-4" has-form-data>
<h2 class="text-lg font-bold text-gray-800 dark:text-slate-400 mb-2">Broader Collection</h2>
<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> </div>
</header>
<div class="mb-4 rounded-lg"> <div class="bg-white shadow-md rounded-lg p-4 mb-6">
<div v-if="selectedCollection" class="bg-gray-100 shadow rounded-lg p-6 mb-6"> <h2 class="text-lg font-semibold">GBA-Thesaurus</h2>
<p class="mb-4 text-gray-700">Please drag your collections here to classify your previously created <label class="block text-sm font-medium">Aktueller Endpoint:</label>
dataset <!-- <TreeView :items="endpoints" @select="onEndpointSelected" /> -->
according to library classification standards.</p> </div>
<draggable v-model="dropCollections" :group="{ name: 'collections' }"
class="min-h-36 border-dashed border-2 border-gray-400 p-4 text-sm flex flex-wrap gap-2 max-h-60 overflow-y-auto" <div class="bg-white shadow-md rounded-lg p-4">
tag="ul"> <h2 class="text-lg font-semibold">Konzept-Suche</h2>
<template #item="{ element }"> <!-- <Autocomplete v-model="selectedConcept" :items="concepts" placeholder="Search for a concept" @change="onConceptSelected" /> -->
<div :key="element.id" <div class="mt-4">
class="p-2 m-1 bg-sky-200 text-sky-800 rounded flex items-center gap-2 h-7"> <h3 class="text-md font-medium">Ausgewähltes Konzept</h3>
<span>{{ element.name }} ({{ element.number }})</span> <p>{{ selectedConcept.title }}</p>
<button <a :href="selectedConcept.uri" target="_blank" class="text-blue-500">URI</a>
@click="dropCollections = dropCollections.filter(item => item.id !== element.id)" <textarea
class="hover:text-sky-600 flex items-center"> v-model="selectedConcept.description"
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" class="mt-2 w-full h-24 border rounded"
fill="currentColor"> placeholder="Description"
<path fill-rule="evenodd" ></textarea>
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
</button>
</div>
</template>
</draggable>
</div>
</div> </div>
<div class="mt-4">
<div class="p-6 border-t border-gray-100 dark:border-slate-800"> <h3 class="text-md font-medium">Untergeordnete Konzepte</h3>
<BaseButtons> <!-- <LinkLabelList :items="narrowerConcepts" /> -->
<BaseButton @click.stop="syncDatasetCollections" label="Save" color="info" small
:disabled="isSaveDisabled" :style="{ opacity: isSaveDisabled ? 0.5 : 1 }">
</BaseButton>
</BaseButtons>
</div> </div>
<div class="mt-4">
<h3 class="text-md font-medium">Übergeordnete Konzepte</h3>
<!-- <LinkLabelList :items="broaderConcepts" /> -->
</div>
<div class="mt-4">
</SectionMain> <h3 class="text-md font-medium">Verwandte Konzepte</h3>
</LayoutAuthenticated> <!-- <LinkLabelList :items="relatedConcepts" /> -->
</div>
</div>
</div>
</template> </template>
<script lang="ts" setup> <script>
import { ref, Ref, watch, computed } from 'vue'; // import TreeView from './TreeView.vue'; // Assuming you have a TreeView component
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue'; // import Autocomplete from './Autocomplete.vue'; // Assuming you have an Autocomplete component
import SectionMain from '@/Components/SectionMain.vue'; // import LinkLabelList from './LinkLabelList.vue'; // Assuming you have a LinkLabelList component
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 { export default {
id: number; components: {
name: string; // TreeView,
collections?: any[]; // Autocomplete,
} // LinkLabelList,
interface Collection {
id: number;
name: string;
number: string;
parent_id?: number | null;
}
const props = defineProps({
collectionRoles: {
type: Array,
required: true,
default: () => []
}, },
dataset: { data() {
type: Object, return {
default: () => ({}), endpoints: [], // This should be populated with your data
concepts: [], // This should be populated with your data
selectedConcept: {},
narrowerConcepts: [], // Populate with data
broaderConcepts: [], // Populate with data
relatedConcepts: [], // Populate with data
};
}, },
relatedCollections: Array<Collection> methods: {
}); updateApp() {
// Handle app update logic
const collectionRoles: Ref<CollectionRole[]> = ref(props.collectionRoles as CollectionRole[]); },
const collections: Ref<Collection[]> = ref<Collection[]>([]); showInfo() {
const selectedCollectionRole = ref<CollectionRole | null>(null); // Handle showing information
const selectedToplevelCollection = ref<Collection | null>(null); },
const selectedCollection = ref<Collection | null>(null); onEndpointSelected(endpoint) {
const narrowerCollections = ref<Collection[]>([]); // Handle endpoint selection
const broaderCollections = ref<Collection[]>([]); },
onConceptSelected(concept) {
this.selectedConcept = concept;
// const onCollectionRoleSelected = (event: Event) => { // Handle concept selection logic, e.g., fetching related concepts
// 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)
}; };
const onCollectionSelected = (collection: Collection) => {
selectedCollection.value = collection;
// call the API endpoint to get both.
fetchCollections(collection.id)
};
// 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>
.btn-primary { /* Add your styles here */
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>

View file

@ -45,7 +45,7 @@ import { LayerOptions } from '@/Components/Map/LayerOptions';
import TableKeywords from '@/Components/TableKeywords.vue'; import TableKeywords from '@/Components/TableKeywords.vue';
import NotificationBar from '@/Components/NotificationBar.vue'; import NotificationBar from '@/Components/NotificationBar.vue';
import FileUploadComponent from '@/Components/FileUpload.vue'; import FileUploadComponent from '@/Components/FileUpload.vue';
import type Person from '#models/person'; import Person from '#models/person';
const props = defineProps({ const props = defineProps({
licenses: { licenses: {
@ -96,27 +96,6 @@ const flash: ComputedRef<any> = computed(() => {
return usePage().props.flash; return usePage().props.flash;
}); });
// Computed property to determine the placeholder based on the selected option
const getPlaceholder = computed(() => (type: string) => {
switch (type) {
case 'DOI':
return 'https://doi.org/10.24341/tethys.236';
case 'Handle':
return '20.500.12345/67890';
case 'ISBN':
return '978-3-85316-076-3';
case 'ISSN':
return '1234-5678';
case 'URL':
return 'https://example.com';
case 'URN':
return 'urn:nbn:de:1234-5678';
default:
return '[VALUE]';
}
});
const mainService = MainService(); const mainService = MainService();
// let serrors = reactive([]); // let serrors = reactive([]);
@ -1071,7 +1050,7 @@ Removes a selected keyword
<!-- <input name="Reference Value" class="form-control" <!-- <input name="Reference Value" class="form-control"
placeholder="[VALUE]" v-model="item.value" /> --> placeholder="[VALUE]" v-model="item.value" /> -->
<FormControl required v-model="item.value" :type="'text'" <FormControl required v-model="item.value" :type="'text'"
:placeholder="getPlaceholder(form.references[index].type)" :errors="form.errors.embargo_date"> placeholder="[VALUE]" :errors="form.errors.embargo_date">
<div class="text-red-400 text-sm" <div class="text-red-400 text-sm"
v-if="form.errors[`references.${index}.value`] && Array.isArray(form.errors[`references.${index}.value`])"> v-if="form.errors[`references.${index}.value`] && Array.isArray(form.errors[`references.${index}.value`])">
{{ form.errors[`references.${index}.value`].join(', ') }} {{ form.errors[`references.${index}.value`].join(', ') }}

View file

@ -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, mdiLibraryShelves } from '@mdi/js'; import { mdiSquareEditOutline, mdiTrashCan, mdiAlertBoxOutline, mdiLockOpen } 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';
@ -139,10 +139,7 @@ const formatServerState = (state: string) => {
:route-name="stardust.route('dataset.release', [dataset.id])" color="info" :route-name="stardust.route('dataset.release', [dataset.id])" color="info"
: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 />

View file

@ -1,135 +0,0 @@
<script lang="ts" setup>
import FormControl from '@/Components/FormControl.vue';
import FormField from '@/Components/FormField.vue';
import ActionMessage from '@/Components/action-message.vue'
import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue';
import { useForm, usePage } from '@inertiajs/vue3';
import { ref, Ref, computed } from 'vue';
import { stardust } from '@eidellev/adonis-stardust/client';
import CardBox from '@/Components/CardBox.vue';
import { mdiLock } from '@mdi/js';
import PasswordMeter from '@/Components/SimplePasswordMeter/password-meter.vue';
// import BaseDivider from '@/Components/BaseDivider.vue';
// const errors: Ref<any> = computed(() => {
// return usePage().props.errors;
// });
const flash: Ref<any> = computed(() => {
return usePage().props.flash;
});
const newPasswordInput: Ref<typeof FormControl | null> = ref(null);
const oldPasswordInput: Ref<typeof FormControl | null> = ref(null);
const enabled = ref(false);
const handleScore = (score: number) => {
if (score >= 4) {
enabled.value = true;
} else {
enabled.value = false;
}
// strengthLabel.value = scoreLabel;
// score.value = scoreValue;
};
const form = useForm({
old_password: '',
new_password: '',
confirm_password: '',
});
const updatePassword = async () => {
await form.put(stardust.route('settings.password.update'), {
preserveScroll: true,
onSuccess: () => {
form.reset();
},
onError: () => {
if (form.errors.new_password) {
form.reset('new_password', 'confirm_password');
enabled.value = false;
// newPasswordInput.value.focus();
// newPasswordInput.value?.focus();
}
if (form.errors.old_password) {
form.reset('old_password');
// oldPasswordInput.value?.focus();
}
},
});
};
</script>
<template>
<!-- <div class="p-7 text-gray-900 bg-white rounded-lg border border-gray-100 shadow dark:border-gray-600 dark:bg-secondary-dark dark:text-white"> -->
<!-- <div class="mb-4">
<h3 class="text-dark text-md">{{ 'Update Password' }}</h3>
</div> -->
<CardBox id="passwordForm" title="Change Password" :icon="mdiLock" form :show-header-icon="false">
<FormField label="Current password" help="Required. Your current password"
:class="{ 'text-red-400': form.errors.old_password }">
<FormControl label="Current Password" id="current_password" ref="oldPasswordInput"
:placeholder="'Please Enter Current Password'" v-model="form.old_password"
:error="form.errors.old_password" type="password" class="block w-full" autocomplete="current-password">
<div class="text-red-400 text-sm" v-if="form.errors.old_password">
{{ form.errors.old_password }}
</div>
</FormControl>
</FormField>
<!-- <div class="col-span-6 sm:col-span-4"> -->
<!-- <FormControl label="New Password" id="password" :placeholder="'Please Enter New Password'"
ref="newPasswordInput" v-model="form.new_password" type="password" class="block w-full"
autocomplete="new-password" :error="form.errors.new_password">
<div class="text-red-400 text-sm" v-if="form.errors.new_password">
{{ form.errors.new_password }}
</div>
</FormControl> -->
<PasswordMeter ref="newPasswordInput" v-model="form.new_password" :errors="form.errors"
@score="handleScore" />
<!-- </div> -->
<FormField label="Confirm password" help="Required. New password one more time"
:class="{ 'text-red-400': form.errors.confirm_password }">
<FormControl label="Confirm Password" :placeholder="'Please Enter Confirm Password'" id="confirm_password"
v-model="form.confirm_password" type="password" class="block w-full" autocomplete="new-password"
:error="form.errors.confirm_password">
<div class="text-red-400 text-sm" v-if="form.errors.confirm_password">
{{ form.errors.confirm_password }}
</div>
</FormControl>
</FormField>
<!-- <BaseDivider /> -->
<template #footer>
<div class="flex items-center justify-end gap-3 mt-5">
<ActionMessage v-if="flash.message" :on="form.recentlySuccessful" color="success">
{{ flash.message }}
</ActionMessage>
<ActionMessage v-if="flash.warning" :on="form.recentlySuccessful" color="warning">
{{ flash.warning }}
</ActionMessage>
<BaseButtons>
<BaseButton type="submit" color="info" label="Change password" @click.prevent="updatePassword()"
:disabled="form.processing == true || enabled == false" />
</BaseButtons>
</div>
</template>
</CardBox>
</template>

View file

@ -1,179 +0,0 @@
<script lang="ts" setup>
import { computed, Ref, ref } from 'vue';
import { useForm, usePage } from '@inertiajs/vue3';
import ActionMessage from '@/Components/action-message.vue'
import FormControl from '@/Components/FormControl.vue';
import FormField from '@/Components/FormField.vue';
import CardBox from '@/Components/CardBox.vue';
import { mdiAccount } from '@mdi/js';
import BaseButton from '@/Components/BaseButton.vue';
import BaseButtons from '@/Components/BaseButtons.vue'
import { stardust } from '@eidellev/adonis-stardust/client';
import AvatarInput from '@/Components/avatar-input.vue';
const props = defineProps({
user: {
type: Object,
required: true,
},
defaultUrl: {
type: String,
required: false,
},
});
const errors: Ref<any> = computed(() => {
return usePage().props.errors || {}
});
const flash: Ref<any> = computed(() => {
return usePage().props.flash;
});
const fullName = computed(() => `${props.user.first_name} ${props.user.last_name}`);
const recentlyHasError = ref(false);
const form = useForm({
first_name: props.user.first_name,
last_name: props.user.last_name,
login: props.user.login,
email: props.user.email,
// mobile: `${props.user.mobile}`,
avatar: undefined as File | undefined,
});
// const verificationLinkSent = ref(null);
const avatarInput: Ref<HTMLInputElement | null> = ref(null);
const updateProfileInformation = () => {
// if (avatarInput.value) {
// form.avatar = avatarInput.value?.files ? avatarInput.value.files[0] : undefined;
// }
form.put(stardust.route('settings.profile.update', [props.user.id]), {
errorBag: 'updateProfileInformation',
preserveScroll: true,
onSuccess: () => {
// clearPhotoFileInput();
},
onError: () => {
if (form.errors.avatar) {
if (avatarInput.value) {
avatarInput.value.value = '';
}
}
recentlyHasError.value = true
setTimeout(() => {
recentlyHasError.value = false
}, 5000)
},
});
};
// const sendEmailVerification = () => {
// verificationLinkSent.value = true;
// };
</script>
<template>
<CardBox id="passwordForm" title="Basic Info" :icon="mdiAccount" form :show-header-icon="false">
<!-- <FormValidationErrors v-bind:errors="errors" /> -->
<AvatarInput class="h-24 w-24 rounded-full" v-model="form.avatar" ref="avatarInput"
:default-src="defaultUrl ? defaultUrl : '/api/avatar?name=' + fullName + '&size=50'">
</AvatarInput>
<div class="text-red-400 text-sm" v-if="errors.avatar && Array.isArray(errors.avatar)">
{{ errors.avatar.join(', ') }}
</div>
<FormField label="First Name" :class="{ 'text-red-400': form.errors.first_name }">
<FormControl id="first_name" label="First Name" v-model="form.first_name" :error="form.errors.first_name"
type="text" :placeholder="'First Name'" class="w-full" autocomplete="first_name">
<div class="text-red-400 text-sm" v-if="errors.first_name && Array.isArray(errors.first_name)">
{{ errors.first_name.join(', ') }}
</div>
</FormControl>
</FormField>
<FormField label="Last Name" :class="{ 'text-red-400': form.errors.last_name }">
<FormControl id="last_name" label="Last Name" v-model="form.last_name" :error="form.errors.last_name"
type="text" :placeholder="'Last Name'" class="w-full" autocomplete="last_name">
<div class="text-red-400 text-sm" v-if="errors.last_name && Array.isArray(errors.last_name)">
{{ errors.last_name.join(', ') }}
</div>
</FormControl>
</FormField>
<FormField label="Username" :class="{ 'text-red-400': form.errors.login }">
<FormControl id="username" label="Username" v-model="form.login" class="w-full"
:is-read-only="!user.is_admin">
<div class="text-red-400 text-sm" v-if="errors.login && Array.isArray(errors.login)">
{{ errors.login.join(', ') }}
</div>
</FormControl>
</FormField>
<FormField label="Enter Email">
<FormControl v-model="form.email" type="text" placeholder="Email" :errors="form.errors.email"
:is-read-only="!user.is_admin">
<div class="text-red-400 text-sm" v-if="errors.email && Array.isArray(errors.email)">
{{ errors.email.join(', ') }}
</div>
</FormControl>
</FormField>
<!-- Email -->
<!-- <div>
<FormControl label="Email" id="email" v-model="form.email" :readonly="!user.is_super_admin"
:disabled="!user.is_super_admin" class="w-full" />
<div v-if="user.email_verified_at === null">
<p class="text-sm mt-2">
{{ 'Your email address is unverified.' }}
<Link :href="route('verification.send')" method="post" as="button"
class="underline text-gray-600 hover:text-gray-900" @click.prevent="sendEmailVerification">
{{ 'Click here to re-send the verification email.' }}
</Link>
</p>
<div v-show="verificationLinkSent" class="mt-2 font-medium text-sm text-green-600">
{{ 'A new verification link has been sent to your email address.' }}
</div>
</div>
</div> -->
<!-- <div class="relative">
<FormControl label="Mobile" id="mobile" class="w-full" input-class="w-full pl-5" v-model="form.mobile"
:readonly="!user.is_super_admin" :disabled="!user.is_super_admin" />
<span class="absolute top-9 left-0 inline-flex items-center ml-2 font-bold text-secondary-light">+</span>
</div> -->
<!-- <div class="col-span-2 flex justify-end items-center mt-5"> -->
<template #footer>
<div class="flex items-center justify-end gap-3 ">
<ActionMessage :on="recentlyHasError" color="warning">
<ul class="list-disc list-inside space-y-2 text-sm">
<li v-for="(messages, field) in errors" :key="field" class="flex flex-col">
<span class="font-semibold capitalize text-gray-700 dark:text-gray-300">{{ field }}:</span>
<span class="text-red-600 dark:text-red-400 ml-4">{{ messages.join(', ') }}</span>
</li>
</ul>
</ActionMessage>
<ActionMessage v-if="flash.message" :on="form.recentlySuccessful" color="success">
{{ flash.message }}
</ActionMessage>
<ActionMessage v-if="flash.warning" :on="form.recentlySuccessful" color="warning">
{{ flash.warning }}
</ActionMessage>
<BaseButtons>
<BaseButton type="submit" color="info" label="Save Changes"
@click.prevent="updateProfileInformation" :disabled="form.processing" />
</BaseButtons>
</div>
</template>
</CardBox>
<!-- </div>
</div> -->
</template>

View file

@ -1,139 +0,0 @@
<script lang="ts" setup>
/*=========================================================================================
File Name: show profile
----------------------------------------------------------------------------------------
Author: Arno Kaimbacher
Author URL: https://jakint.at/
==========================================================================================*/
// import { useUrlSearchParams } from '@vueuse/core';
// import { usePage } from '@inertiajs/vue3';
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue';
import { ref, computed } from 'vue';
// import IconRounded from '@/Components/IconRounded.vue';
// import AdminLayout from '@/Layouts/AdminLayout.vue';
// import AppLayout from '@/Layouts/AppLayout.vue';
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
import UpdatePasswordForm from '@/Pages/profile/partials/update-password-form.vue';
import UpdateProfileInformationForm from '@/Pages/profile/partials/update-profile-information-form.vue';
import { usePage } from '@inertiajs/vue3';
import SectionMain from '@/Components/SectionMain.vue';
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
import { mdiAccount, mdiArrowLeftBoldOutline, mdiFormTextboxPassword } from '@mdi/js';
import { stardust } from '@eidellev/adonis-stardust/client';
import BaseButton from '@/Components/BaseButton.vue';
import BaseIcon from '@/Components/BaseIcon.vue';
defineProps({
user: {
type: Object,
required: true,
},
defaultUrl: {
type: String,
required: true,
},
});
const tabs = [
{ id: 1, title: 'My Profile', icon: mdiAccount },
{ id: 2, title: 'Change Password', icon: mdiFormTextboxPassword },
];
// const params = useUrlSearchParams('history');
const selectedTab = ref(0);
function changeTab(index: number) {
selectedTab.value = index;
}
const user = computed(() => usePage().props.user);
// const sessions = computed(() => usePage().props.sessions);
// const Layout = computed(() => (user.value.is_super_admin ? AdminLayout : AppLayout));
</script>
<template>
<Component :is="LayoutAuthenticated">
<Head title="Profile"></Head>
<SectionMain>
<SectionTitleLineWithButton :icon="mdiAccount" title="Profile" main>
<BaseButton :route-name="stardust.route('dashboard')" :icon="mdiArrowLeftBoldOutline" label="Back"
color="white" rounded-full small />
</SectionTitleLineWithButton>
<!-- <NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
{{ flash.message }}
</NotificationBar> -->
<div class="">
<TabGroup :selectedIndex="selectedTab" @change="changeTab">
<TabList class="flex space-x-7 p-1">
<Tab v-for="(tab, index) in tabs" as="template" :key="index" v-slot="{ selected }">
<button :class="[
'inline-flex items-center justify-center font-semibold focus:outline-none disabled:opacity-25 pb-2',
selected
? 'text-dark border-b-2 border-primary transition-all duration-500 inline-block'
: 'text-light',
]">
<!-- <IconRounded :name="tab.icon" class="h-5 mr-3" /> -->
<BaseIcon :path="tab.icon" class="flex-none" w="w-12" />
{{ tab.title }}
</button>
</Tab>
</TabList>
<transition v-show="selectedTab >= 0" enter-active-class="transition duration-100 ease-out"
enter-from-class="transform scale-95 opacity-0" enter-to-class="transform scale-100 opacity-100"
leave-active-class="transition duration-75 ease-in"
leave-from-class="transform scale-100 opacity-100"
leave-to-class="transform scale-95 opacity-0">
<TabPanels class="mt-2">
<TabPanel :key="Date.now().toString() + 1" :class="[]">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<UpdateProfileInformationForm class="p-5" :user="user" :default-url="defaultUrl" />
</div>
</TabPanel>
<TabPanel :key="Date.now().toString() + 3" :class="[]">
<!-- <div class="grid grid-cols-1 md:grid-cols-2 gap-x-5 gap-y-7 items-start"> -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<UpdatePasswordForm class="p-5" />
</div>
</TabPanel>
</TabPanels>
</transition>
</TabGroup>
</div>
</SectionMain>
</Component>
</template>
<style>
.cool-link::after {
content: '';
display: block;
width: 0;
height: 2px;
background: #fda92d;
transition: width 0.3s;
}
.cool-link:hover::after {
width: 100%;
}
</style>
<!-- <script>
import { defineComponent } from 'vue';
export default defineComponent({
layout: false,
});
</script> -->

View file

@ -2,56 +2,6 @@ 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;
@ -59,12 +9,10 @@ export interface Person {
email: string; email: string;
name_type: string; name_type: string;
identifier_orcid: string; identifier_orcid: string;
dataset_count: number; datasetCount: string;
created_at: string; created_at: string;
} }
interface TransactionItem { interface TransactionItem {
amount: number; amount: number;
account: string; account: string;
@ -113,7 +61,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: [] as Array<User>, clients: [],
history: [] as Array<TransactionItem>, history: [] as Array<TransactionItem>,
// api based data // api based data
@ -236,7 +184,7 @@ export const MainService = defineStore('main', {
this.totpState = state; this.totpState = state;
}, },
fetchChartData(year: string) { async fetchChartData(year: string) {
// sampleDataKey= authors or datasets // sampleDataKey= authors or datasets
axios axios
.get(`/api/statistic/${year}`) .get(`/api/statistic/${year}`)

View file

@ -2,16 +2,15 @@ import '../css/app.css';
import { createApp, h } from 'vue'; import { createApp, h } from 'vue';
import { Inertia } from '@inertiajs/inertia'; import { Inertia } from '@inertiajs/inertia';
import { Head, Link, createInertiaApp } from '@inertiajs/vue3'; import { createInertiaApp } from '@inertiajs/vue3';
// import DefaultLayout from '@/Layouts/Default.vue'; // import DefaultLayout from '@/Layouts/Default.vue';
import { createPinia } from 'pinia'; 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';
const pinia = createPinia(); const pinia = createPinia();
// import i18n from './i18n'; // import i18n from './i18n';
import { EmitterPlugin } from '@/EmitterDirective'; import { EmitterPlugin } from '@/EmitterDirective';
@ -37,21 +36,33 @@ createInertiaApp({
progress: { progress: {
// color: '#4B5563', // color: '#4B5563',
color: '#22C55E', color: '#22C55E',
showSpinner: true,
}, },
// Webpack // Webpack
// resolve: async (name: string) => { // resolve: (name) => require(`./Pages/${name}`),
// // Dynamically import the Vue component using import // resolve: (name) => require(`./Pages/${name}.vue`),
// const { default: page } = await import(`./Pages/${name}.vue`); // add default layout
// // const page = require(`./Pages/${name}.vue`).default; // resolve: (name) => {
// const page = require(`./Pages/${name}.vue`).default;
// // if (!page.layout) { // // if (!page.layout) {
// // page.layout = DefaultLayout; // // page.layout = DefaultLayout;
// // } // // }
// return page; // return page;
// }, // },
resolve: (name) => { resolve: async (name: string) => {
return resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob<DefineComponent>('./Pages/**/*.vue')); // Dynamically import the Vue component using import
const { default: page } = await import(`./Pages/${name}.vue`);
// const page = require(`./Pages/${name}.vue`).default;
// if (!page.layout) {
// page.layout = DefaultLayout;
// }
return page;
}, },
// resolve: (name) => {
// return resolvePageComponent(
// `./Pages/${name}.vue`,
// import.meta.glob<DefineComponent>('./pages/**/*.vue'),
// )
// },
setup({ el, App, props, plugin }) { setup({ el, App, props, plugin }) {
const app = createApp({ render: () => h(App, props) }) const app = createApp({ render: () => h(App, props) })
@ -61,19 +72,11 @@ createInertiaApp({
.use(EmitterPlugin); .use(EmitterPlugin);
// .component('inertia-link', Link) // .component('inertia-link', Link)
app.component('Head', Head);
app.component('Link', Link);
// Listen for navigation event to handle layout changes
// window.addEventListener('inertia:navigate', () => {
// layoutService.isAsideMobileExpanded = false;
// layoutService.isAsideLgActive = false;
// });
asyncPlugin.install('settings').then(() => { asyncPlugin.install('settings').then(() => {
app.mount(el); app.mount(el);
}); });
}, },
}); });
const styleService = StyleService(pinia); const styleService = StyleService(pinia);
@ -81,7 +84,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 */
@ -91,12 +94,6 @@ 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', () => {

View file

@ -1,183 +0,0 @@
<!-- <svg width="60" height="59" viewBox="0 0 60 59" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M22.0604 27.0103C29.7333 23.0893 35.8474 16.6142 39.6254 8.51905C40.1123 7.47585 39.871 6.25371 39.3648 5.21976C38.1966 2.83369 37.9273 0.35731 31.5792 1.07481L29.0769 1.19405C30.0128 1.19405 29.5449 1.19405 29.0769 1.19405C26.2213 9.83921 20.8705 16.9667 13.0175 20.9078C7.54419 23.7048 3.49869 28.7902 1.59493 34.8927L1.08063 36.5412C1.02718 36.7125 0.953783 36.7439 1.03271 36.905V36.905C1.69551 38.2585 2.41237 37.8338 1.24774 36.8773C-0.0162787 35.8391 4.54649 41.9692 8.20235 42.3859C8.98846 42.4755 9.62624 41.8869 9.99718 41.188C12.8897 35.7384 16.5983 29.7497 22.0604 27.0103Z"
fill="#3798A6" />
<path
d="M54.293 18.2545C52.9177 18.0829 51.6326 19.0064 50.8501 20.1503C46.5712 26.4051 40.9276 31.4619 34.0992 34.9362C28.0309 37.9875 23.3905 43.7085 21.2488 50.5738V50.5738C20.8823 51.9051 21.321 53.3053 22.0817 54.4577C22.8541 55.6279 23.2013 56.5055 25.0387 57.2056C25.2331 57.2797 25.4363 57.33 25.6412 57.3659L29.6561 58.0696C30.1014 58.1477 30.5803 57.9029 30.6385 57.4546V57.4546C30.6522 57.3488 30.6656 57.26 30.6995 57.1589C33.5732 48.5843 39.3809 41.6511 47.1875 37.7332C52.0449 35.251 56.5674 29.4173 58.7712 23.8703C59.2774 22.5963 58.9189 21.1675 57.9822 20.1666V20.1666C57.743 19.9111 57.4687 19.6866 57.1678 19.5079C55.698 18.635 54.964 18.3382 54.293 18.2545Z"
fill="#3798A6" />
<path
d="M17.8462 46.7817C18.4399 48.0437 20.2086 47.5179 20.8231 46.2658C23.9715 39.8511 29.07 34.8495 35.2979 31.6915C41.4799 28.5111 46.6705 23.683 50.3399 17.7735C50.6236 17.3166 50.7683 16.7849 50.7594 16.2472C50.658 10.1604 49.3958 9.67645 47.3183 8.9339C45.5442 8.29976 43.3135 8.84337 42.332 10.4516C37.9465 17.6372 31.7406 23.4608 24.3513 27.2417C19.6664 29.5615 15.8593 33.5228 13.4571 38.3374C13.2609 38.7306 13.1639 39.1681 13.1742 39.6074C13.3408 46.715 16.2215 43.2109 17.84 46.768C17.8439 46.7766 17.8422 46.773 17.8462 46.7817V46.7817Z"
fill="#3798A6" />
</svg> -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 626.4 224.7" style="enable-background:new 0 0 626.4 224.7;" xml:space="preserve">
<style type="text/css">
.st0 {
fill: #336699;
}
.st1 {
fill: #00FFFF;
}
.st2 {
fill: #30D5C8;
}
.st3 {
opacity: 0.8;
fill: #336699;
}
.st4 {
opacity: 0.5;
fill: #336699;
}
.st5 {
fill: #393939;
}
</style>
<g>
<g>
<path class="st0"
d="M215.3,69.1l-4.4,19.2h-47.1l-4.2,18h43.7l-4.5,19.2h-43.6l-5.9,25.4h47L192,170h-73.1l23.3-100.9H215.3z" />
<path class="st0" d="M311.6,88.6h-31.4L261.4,170h-26.1L254,88.6h-31.5l4.5-19.5h89L311.6,88.6z" />
<path class="st0" d="M422.1,69.1L398.9,170h-26.1l10.3-44.6h-38.3L334.4,170h-26.1l23.3-100.9h26.1l-8.6,37.1h38.3l8.6-37.1H422.1
z" />
<path class="st0"
d="M536.7,69.1l-51.4,61.9l-9.1,39h-26.2l8.8-37.8l-23.2-63.1h27.9l13,39.6l30.2-39.6H536.7z" />
<path class="st0" d="M563.8,171.8c-8.6,0-16.3-0.7-23.1-2.2c-6.8-1.5-12.6-3.3-17.5-5.6l5.2-24.2h2.8c4.7,4.2,10.1,7.5,16.4,9.9
c6.3,2.3,13,3.5,20.3,3.5c7.4,0,12.9-1,16.3-3c3.5-2,5.2-4.9,5.2-8.6c0-1.4-0.3-2.6-0.8-3.6c-0.6-1-1.7-2-3.3-3
c-1.6-1-3.9-2-6.7-2.9c-2.8-1-6.5-2.1-10.9-3.4c-4.9-1.4-9.3-2.8-13.2-4.4c-3.9-1.6-7.2-3.5-10-5.6c-2.8-2.2-4.8-4.8-6.3-7.7
c-1.4-2.9-2.1-6.3-2.1-10.4c0-10.1,4.5-18.2,13.4-24.2c8.9-6,21.1-9,36.6-9c7.5,0,14.5,0.6,21.1,1.8c6.6,1.2,12.2,2.9,16.8,4.9
l-4.9,23.2h-2.8c-3.5-3.2-8.2-5.9-13.9-8.1c-5.8-2.2-12.1-3.3-18.9-3.3c-6.7,0-11.9,1-15.5,2.9c-3.7,1.9-5.5,4.6-5.5,8
c0,1.6,0.3,2.9,0.8,3.9c0.5,1,1.6,2.1,3.3,3c1.4,0.9,3.7,1.9,6.8,2.9c3.1,1.1,6.8,2.2,11,3.3c11.3,3,19.4,6.6,24.2,10.6
s7.2,9.6,7.2,16.5c0,5.8-1.3,11-3.9,15.4c-2.6,4.4-6.2,8-10.8,10.8c-4.8,2.9-10.3,5.1-16.5,6.4
C578.5,171.1,571.6,171.8,563.8,171.8z" />
</g>
<path class="st1" d="M539.2,37.1" />
<path class="st1" d="M539.2,37.1" />
<polygon class="st2" points="24,213.8 57.7,213.8 98.2,44.6 12.5,45.1 3.7,81 56.7,80.6 " />
<polygon class="st0" points="72.1,170.5 112.4,2.8 199.8,2.8 192.4,39.2 137.1,39.2 105.8,170.1 " />
<polygon class="st0" points="207,2.6 225.9,2.6 218.6,39 199.9,39 " />
<polygon class="st3" points="233.2,2.9 252.1,2.9 244.7,39.4 226.1,39.4 " />
<polygon class="st4" points="259.2,2.9 278.1,2.9 270.8,39.4 252.1,39.4 " />
<g>
<path class="st5" d="M97.4,213.7h-4.7l-8-12.1h-5.8l-2.8,12.1H72l7-30.2h8.3c1.9,0,3.4,0.1,4.5,0.4c1.2,0.2,2.2,0.7,3,1.3
c0.8,0.6,1.4,1.2,1.8,2c0.4,0.8,0.7,1.8,0.7,2.9c0,2.5-0.8,4.7-2.4,6.6c-1.6,1.9-3.7,3.2-6.2,4L97.4,213.7z M92.9,191
c0-0.7-0.1-1.3-0.3-1.8c-0.2-0.5-0.6-0.9-1-1.3c-0.5-0.4-1.2-0.7-1.9-0.9c-0.7-0.2-1.7-0.2-2.8-0.2h-4.6l-2.7,11.6h4.3
c1.3,0,2.5-0.1,3.5-0.4c1-0.2,1.9-0.7,2.7-1.3c0.9-0.7,1.6-1.5,2.1-2.5C92.6,193.2,92.9,192.1,92.9,191z" />
<path class="st5" d="M111.5,214.2c-3.2,0-5.6-0.7-7.4-2.2c-1.8-1.5-2.7-3.6-2.7-6.5c0-4.2,1.3-7.7,4-10.7c2.6-3,5.9-4.5,9.8-4.5
c2.6,0,4.6,0.6,5.9,1.9c1.4,1.3,2.1,3.1,2.1,5.4c0,0.4-0.1,1-0.2,1.9c-0.1,0.9-0.3,1.9-0.6,3.1h-16.8c-0.1,0.4-0.1,0.8-0.2,1.2
c0,0.4-0.1,0.7-0.1,1.1c0,1.9,0.6,3.4,1.8,4.5c1.2,1.1,2.8,1.6,5,1.6c1.5,0,3-0.3,4.6-0.9s2.9-1.2,4-2h0.2l-0.8,4.1
c-0.7,0.2-1.3,0.5-1.8,0.7c-0.5,0.2-1.2,0.4-2.1,0.6c-0.8,0.2-1.6,0.4-2.2,0.5C113.3,214.2,112.5,214.2,111.5,214.2z M119.2,199.9
c0.1-0.4,0.1-0.7,0.1-1c0-0.3,0-0.6,0-0.9c0-1.4-0.4-2.6-1.2-3.4c-0.8-0.8-2.1-1.2-3.8-1.2c-1.9,0-3.6,0.6-5.1,1.8
c-1.5,1.2-2.5,2.8-3,4.7H119.2z" />
<path class="st5" d="M132.8,214.3c-1.7,0-3.2-0.2-4.6-0.6s-2.5-0.9-3.4-1.4l0.9-4.1h0.2c0.3,0.2,0.7,0.5,1.1,0.9
c0.5,0.3,1,0.7,1.7,1c0.6,0.3,1.4,0.6,2.2,0.8c0.8,0.2,1.7,0.3,2.6,0.3c1.9,0,3.4-0.4,4.5-1.1c1.1-0.7,1.6-1.7,1.6-3.1
c0-0.7-0.3-1.3-0.8-1.7c-0.6-0.4-1.4-0.7-2.4-0.9c-0.5-0.1-1.2-0.3-1.9-0.4c-0.7-0.1-1.5-0.3-2.3-0.5c-1.6-0.4-2.8-1.1-3.5-1.9
c-0.8-0.8-1.1-1.9-1.1-3.1c0-1.1,0.2-2,0.7-3c0.5-0.9,1.1-1.8,2.1-2.5c0.9-0.7,2-1.3,3.3-1.8c1.3-0.5,2.8-0.7,4.5-0.7
c1.4,0,2.7,0.2,4.1,0.5c1.4,0.3,2.5,0.8,3.3,1.3l-0.8,3.9h-0.2c-0.2-0.2-0.5-0.4-1-0.7c-0.4-0.3-1-0.6-1.7-0.9
c-0.6-0.3-1.3-0.5-2.1-0.7c-0.8-0.2-1.6-0.3-2.4-0.3c-1.7,0-3.1,0.4-4.1,1.1s-1.6,1.7-1.6,2.9c0,0.7,0.3,1.2,0.8,1.7
c0.5,0.5,1.3,0.8,2.4,1.1c0.7,0.2,1.4,0.3,2.1,0.5c0.7,0.1,1.4,0.3,2.1,0.5c1.6,0.4,2.8,1,3.6,1.8s1.2,1.9,1.2,3.1
c0,1-0.2,2.1-0.7,3.1c-0.5,1-1.2,1.9-2.2,2.6c-1,0.8-2.1,1.4-3.5,1.8C136,214,134.5,214.3,132.8,214.3z" />
<path class="st5" d="M158.2,214.2c-3.2,0-5.6-0.7-7.4-2.2c-1.8-1.5-2.7-3.6-2.7-6.5c0-4.2,1.3-7.7,4-10.7c2.6-3,5.9-4.5,9.8-4.5
c2.6,0,4.6,0.6,5.9,1.9c1.4,1.3,2.1,3.1,2.1,5.4c0,0.4-0.1,1-0.2,1.9c-0.1,0.9-0.3,1.9-0.6,3.1h-16.8c-0.1,0.4-0.1,0.8-0.2,1.2
c0,0.4-0.1,0.7-0.1,1.1c0,1.9,0.6,3.4,1.8,4.5c1.2,1.1,2.8,1.6,5,1.6c1.5,0,3-0.3,4.6-0.9c1.6-0.6,2.9-1.2,4-2h0.2l-0.8,4.1
c-0.7,0.2-1.3,0.5-1.8,0.7c-0.5,0.2-1.2,0.4-2.1,0.6c-0.8,0.2-1.6,0.4-2.2,0.5C160,214.2,159.2,214.2,158.2,214.2z M165.9,199.9
c0.1-0.4,0.1-0.7,0.1-1c0-0.3,0-0.6,0-0.9c0-1.4-0.4-2.6-1.2-3.4c-0.8-0.8-2.1-1.2-3.8-1.2c-1.9,0-3.6,0.6-5.1,1.8
c-1.5,1.2-2.5,2.8-3,4.7H165.9z" />
<path class="st5"
d="M186.8,211.3c-0.4,0.2-0.9,0.5-1.5,0.9c-0.6,0.4-1.3,0.7-1.9,1c-0.7,0.3-1.5,0.6-2.3,0.8
c-0.9,0.2-1.8,0.3-3,0.3c-1.8,0-3.3-0.5-4.4-1.6c-1.1-1-1.7-2.4-1.7-4.1c0-1.8,0.4-3.3,1.2-4.6c0.8-1.2,2-2.2,3.5-3
c1.5-0.8,3.4-1.3,5.6-1.7c2.2-0.3,4.6-0.6,7.4-0.6c0.1-0.4,0.2-0.7,0.2-1c0.1-0.3,0.1-0.6,0.1-0.9c0-0.6-0.1-1.2-0.4-1.6
c-0.3-0.4-0.6-0.7-1.1-1c-0.5-0.2-1-0.4-1.7-0.5c-0.6-0.1-1.3-0.1-2.1-0.1c-1.2,0-2.5,0.2-4,0.6c-1.5,0.4-2.7,0.7-3.6,1.1H177
l0.8-3.8c0.8-0.2,1.9-0.4,3.4-0.7c1.5-0.3,2.9-0.4,4.3-0.4c2.8,0,5,0.4,6.4,1.3c1.4,0.9,2.1,2.3,2.1,4.2c0,0.4,0,0.8-0.1,1.2
s-0.1,0.8-0.2,1.2l-3.6,15.4h-3.8L186.8,211.3z M189.1,201.8c-2.1,0.1-4,0.2-5.6,0.5c-1.6,0.2-3,0.6-4,1c-1.1,0.4-1.9,1.1-2.5,1.8
c-0.6,0.8-0.9,1.7-0.9,2.9c0,1,0.3,1.7,1,2.2c0.7,0.5,1.7,0.8,3.1,0.8c1.2,0,2.5-0.3,3.8-0.8c1.3-0.5,2.5-1.2,3.6-2L189.1,201.8z" />
<path class="st5" d="M215.9,195.1h-0.2c-0.5-0.1-1-0.2-1.5-0.3c-0.5-0.1-1-0.1-1.7-0.1c-1.3,0-2.5,0.3-3.8,0.9
c-1.3,0.6-2.5,1.3-3.6,2.1l-3.7,16.1h-3.9l5.2-22.7h3.9l-0.8,3.3c1.8-1.3,3.3-2.1,4.6-2.6c1.3-0.5,2.5-0.7,3.6-0.7
c0.7,0,1.2,0,1.4,0.1c0.3,0,0.7,0.1,1.3,0.2L215.9,195.1z" />
<path class="st5" d="M225.2,214.2c-1.4,0-2.7-0.2-3.8-0.5c-1.1-0.3-2.1-0.9-2.9-1.6c-0.8-0.7-1.4-1.6-1.9-2.7
c-0.4-1.1-0.7-2.3-0.7-3.7c0-2.1,0.3-4.1,1-5.9c0.7-1.8,1.6-3.5,2.8-4.9c1.2-1.4,2.6-2.4,4.4-3.2c1.7-0.8,3.6-1.2,5.6-1.2
c1.3,0,2.6,0.2,3.8,0.5c1.2,0.4,2.2,0.8,3.1,1.3l-0.8,4.1h-0.2c-0.3-0.2-0.6-0.5-1-0.8c-0.4-0.3-0.9-0.6-1.4-0.9
c-0.6-0.3-1.2-0.5-1.9-0.7c-0.7-0.2-1.5-0.3-2.3-0.3c-2.6,0-4.8,1.1-6.5,3.3c-1.7,2.2-2.5,4.9-2.5,8.1c0,1.9,0.5,3.4,1.5,4.4
c1,1,2.4,1.5,4.2,1.5c0.9,0,1.7-0.1,2.6-0.4c0.9-0.2,1.6-0.5,2.2-0.8c0.7-0.3,1.3-0.6,1.9-1c0.6-0.3,1-0.6,1.2-0.8h0.2l-0.8,4.2
c-1.2,0.5-2.4,1-3.8,1.4C227.9,214,226.5,214.2,225.2,214.2z" />
<path class="st5" d="M259.9,196.2c0,0.3,0,0.8-0.1,1.3c-0.1,0.6-0.1,1-0.3,1.5l-3.4,14.7h-3.8l3-12.9c0.2-0.7,0.3-1.3,0.4-1.9
c0.1-0.5,0.1-1.1,0.1-1.6c0-1.1-0.3-2-0.9-2.6c-0.6-0.6-1.6-0.9-3.1-0.9c-1,0-2.2,0.3-3.4,0.9c-1.2,0.6-2.5,1.2-3.7,2l-3.9,16.9
h-3.8l7.3-31.6h3.8l-2.7,11.4c1.5-1,2.8-1.8,4.1-2.3c1.3-0.5,2.6-0.8,3.9-0.8c2,0,3.5,0.5,4.6,1.5
C259.4,192.9,259.9,194.3,259.9,196.2z" />
<path class="st5" d="M308.1,195.1c0,3.3-0.8,6.4-2.5,9.4c-1.7,3-4,5.2-6.9,6.8c-1.8,1-3.6,1.6-5.5,1.9c-1.9,0.3-4,0.5-6.4,0.5
h-8.2l7-30.2h7.1c2.2,0,4.2,0.2,6,0.5c1.8,0.3,3.5,1,5,2c1.4,1,2.5,2.2,3.3,3.8C307.7,191.2,308.1,193,308.1,195.1z M303.8,195.4
c0-1.5-0.3-2.9-0.8-4c-0.6-1.1-1.4-2-2.5-2.8c-1.1-0.7-2.3-1.2-3.5-1.5c-1.3-0.2-2.8-0.4-4.8-0.4h-3.4l-5.5,23.6h4.2
c1.9,0,3.7-0.2,5.2-0.5c1.5-0.3,3-0.9,4.3-1.6c2.2-1.2,3.9-3,5-5.4C303.3,200.6,303.8,198.1,303.8,195.4z" />
<path class="st5"
d="M324.7,211.3c-0.4,0.2-0.9,0.5-1.5,0.9c-0.6,0.4-1.3,0.7-1.9,1c-0.7,0.3-1.5,0.6-2.3,0.8
c-0.9,0.2-1.8,0.3-3,0.3c-1.8,0-3.3-0.5-4.4-1.6c-1.1-1-1.7-2.4-1.7-4.1c0-1.8,0.4-3.3,1.2-4.6c0.8-1.2,2-2.2,3.5-3
c1.5-0.8,3.4-1.3,5.6-1.7c2.2-0.3,4.6-0.6,7.4-0.6c0.1-0.4,0.2-0.7,0.2-1c0.1-0.3,0.1-0.6,0.1-0.9c0-0.6-0.1-1.2-0.4-1.6
c-0.3-0.4-0.6-0.7-1.1-1c-0.5-0.2-1-0.4-1.7-0.5c-0.6-0.1-1.3-0.1-2.1-0.1c-1.2,0-2.5,0.2-4,0.6c-1.5,0.4-2.7,0.7-3.6,1.1h-0.2
l0.8-3.8c0.8-0.2,1.9-0.4,3.4-0.7c1.5-0.3,2.9-0.4,4.3-0.4c2.8,0,5,0.4,6.4,1.3c1.4,0.9,2.1,2.3,2.1,4.2c0,0.4,0,0.8-0.1,1.2
s-0.1,0.8-0.2,1.2l-3.6,15.4h-3.8L324.7,211.3z M327,201.8c-2.1,0.1-4,0.2-5.6,0.5c-1.6,0.2-3,0.6-4,1c-1.1,0.4-1.9,1.1-2.5,1.8
c-0.6,0.8-0.9,1.7-0.9,2.9c0,1,0.3,1.7,1,2.2c0.7,0.5,1.7,0.8,3.1,0.8c1.2,0,2.5-0.3,3.8-0.8c1.3-0.5,2.5-1.2,3.6-2L327,201.8z" />
<path class="st5" d="M352.5,191l-0.7,3.1h-7.9l-2.4,10.5c-0.1,0.5-0.3,1.1-0.4,1.8c-0.1,0.7-0.2,1.2-0.2,1.6c0,1,0.3,1.7,0.8,2.2
c0.5,0.5,1.4,0.7,2.8,0.7c0.6,0,1.2-0.1,2-0.3c0.8-0.2,1.3-0.3,1.6-0.4h0.2l-0.7,3.3c-0.8,0.2-1.6,0.3-2.4,0.5
c-0.9,0.1-1.6,0.2-2.3,0.2c-1.9,0-3.3-0.4-4.4-1.2c-1-0.8-1.5-2.1-1.5-3.9c0-0.4,0-0.9,0.1-1.3c0.1-0.4,0.1-0.9,0.3-1.5l2.8-12.2
h-2.6l0.7-3.1h2.6l1.5-6.5h3.9l-1.5,6.5H352.5z" />
<path class="st5"
d="M366.3,211.3c-0.4,0.2-0.9,0.5-1.5,0.9c-0.6,0.4-1.3,0.7-1.9,1c-0.7,0.3-1.5,0.6-2.3,0.8
c-0.9,0.2-1.8,0.3-3,0.3c-1.8,0-3.3-0.5-4.4-1.6c-1.1-1-1.7-2.4-1.7-4.1c0-1.8,0.4-3.3,1.2-4.6c0.8-1.2,2-2.2,3.5-3
c1.5-0.8,3.4-1.3,5.6-1.7c2.2-0.3,4.6-0.6,7.4-0.6c0.1-0.4,0.2-0.7,0.2-1c0.1-0.3,0.1-0.6,0.1-0.9c0-0.6-0.1-1.2-0.4-1.6
c-0.3-0.4-0.6-0.7-1.1-1c-0.5-0.2-1-0.4-1.7-0.5c-0.6-0.1-1.3-0.1-2.1-0.1c-1.2,0-2.5,0.2-4,0.6c-1.5,0.4-2.7,0.7-3.6,1.1h-0.2
l0.8-3.8c0.8-0.2,1.9-0.4,3.4-0.7c1.5-0.3,2.9-0.4,4.3-0.4c2.8,0,5,0.4,6.4,1.3c1.4,0.9,2.1,2.3,2.1,4.2c0,0.4,0,0.8-0.1,1.2
s-0.1,0.8-0.2,1.2l-3.6,15.4h-3.8L366.3,211.3z M368.5,201.8c-2.1,0.1-4,0.2-5.6,0.5c-1.6,0.2-3,0.6-4,1c-1.1,0.4-1.9,1.1-2.5,1.8
c-0.6,0.8-0.9,1.7-0.9,2.9c0,1,0.3,1.7,1,2.2c0.7,0.5,1.7,0.8,3.1,0.8c1.2,0,2.5-0.3,3.8-0.8c1.3-0.5,2.5-1.2,3.6-2L368.5,201.8z" />
<path class="st5" d="M417.4,213.7h-4.7l-8-12.1h-5.8l-2.8,12.1H392l7-30.2h8.3c1.9,0,3.4,0.1,4.5,0.4c1.2,0.2,2.2,0.7,3,1.3
c0.8,0.6,1.4,1.2,1.8,2c0.4,0.8,0.7,1.8,0.7,2.9c0,2.5-0.8,4.7-2.4,6.6c-1.6,1.9-3.7,3.2-6.2,4L417.4,213.7z M412.8,191
c0-0.7-0.1-1.3-0.3-1.8c-0.2-0.5-0.6-0.9-1-1.3c-0.5-0.4-1.2-0.7-1.9-0.9c-0.7-0.2-1.7-0.2-2.8-0.2h-4.6l-2.7,11.6h4.3
c1.3,0,2.5-0.1,3.5-0.4c1-0.2,1.9-0.7,2.7-1.3c0.9-0.7,1.6-1.5,2.1-2.5C412.6,193.2,412.8,192.1,412.8,191z" />
<path class="st5" d="M431.5,214.2c-3.2,0-5.6-0.7-7.4-2.2c-1.8-1.5-2.7-3.6-2.7-6.5c0-4.2,1.3-7.7,4-10.7s5.9-4.5,9.8-4.5
c2.6,0,4.6,0.6,5.9,1.9c1.4,1.3,2.1,3.1,2.1,5.4c0,0.4-0.1,1-0.2,1.9c-0.1,0.9-0.3,1.9-0.6,3.1h-16.8c-0.1,0.4-0.1,0.8-0.2,1.2
c0,0.4-0.1,0.7-0.1,1.1c0,1.9,0.6,3.4,1.8,4.5c1.2,1.1,2.8,1.6,5,1.6c1.5,0,3-0.3,4.6-0.9c1.6-0.6,2.9-1.2,4-2h0.2l-0.8,4.1
c-0.7,0.2-1.3,0.5-1.8,0.7s-1.2,0.4-2.1,0.6c-0.8,0.2-1.6,0.4-2.2,0.5C433.3,214.2,432.5,214.2,431.5,214.2z M439.2,199.9
c0.1-0.4,0.1-0.7,0.1-1c0-0.3,0-0.6,0-0.9c0-1.4-0.4-2.6-1.2-3.4c-0.8-0.8-2.1-1.2-3.8-1.2c-1.9,0-3.6,0.6-5.1,1.8
c-1.5,1.2-2.5,2.8-3,4.7H439.2z" />
<path class="st5" d="M468.8,198.2c0,2.2-0.4,4.4-1.1,6.3c-0.7,2-1.6,3.7-2.8,5c-1.2,1.4-2.5,2.5-4.1,3.4c-1.6,0.8-3.2,1.2-5,1.2
c-1.2,0-2.4-0.1-3.4-0.4s-2-0.7-2.8-1.2l-2.2,9.5h-3.8l7.2-31h3.8l-0.6,2.4c1.3-0.9,2.5-1.6,3.7-2.2c1.2-0.6,2.6-0.8,4.1-0.8
c2.2,0,3.9,0.7,5.1,2.1C468.2,193.8,468.8,195.7,468.8,198.2z M464.8,198.9c0-1.6-0.4-2.9-1.1-3.7c-0.7-0.9-1.8-1.3-3.4-1.3
c-1.1,0-2.3,0.3-3.5,0.8c-1.2,0.6-2.3,1.2-3.4,1.9l-3,12.9c0.9,0.5,1.7,0.8,2.5,1.1c0.8,0.2,1.8,0.3,2.9,0.3c1.4,0,2.7-0.3,3.8-1
s2.1-1.6,2.8-2.6c0.8-1.1,1.4-2.4,1.7-3.8C464.6,202,464.8,200.5,464.8,198.9z" />
<path class="st5" d="M494.1,199.1c0,2-0.3,4-0.9,5.8c-0.6,1.9-1.5,3.5-2.7,4.9c-1.2,1.4-2.6,2.6-4.1,3.4c-1.6,0.8-3.4,1.2-5.4,1.2
c-2.7,0-4.8-0.8-6.3-2.3c-1.5-1.5-2.3-3.7-2.3-6.4c0-2,0.3-4,0.9-5.8c0.6-1.8,1.5-3.5,2.7-4.9c1.1-1.4,2.5-2.5,4.2-3.3
c1.6-0.8,3.4-1.2,5.4-1.2c2.6,0,4.7,0.7,6.3,2.2S494.1,196.3,494.1,199.1z M487.8,207.6c0.7-1.1,1.3-2.3,1.7-3.8s0.6-2.9,0.6-4.5
c0-1.9-0.5-3.3-1.4-4.3c-0.9-1-2.2-1.5-3.9-1.5c-1.3,0-2.5,0.3-3.6,0.9c-1,0.6-2,1.5-2.7,2.6c-0.7,1.1-1.3,2.3-1.7,3.8
c-0.4,1.4-0.6,2.9-0.6,4.5c0,1.9,0.5,3.3,1.4,4.3c0.9,1,2.2,1.5,3.9,1.5c1.3,0,2.5-0.3,3.6-0.9
C486.2,209.6,487.1,208.7,487.8,207.6z" />
<path class="st5" d="M504.2,214.3c-1.7,0-3.2-0.2-4.6-0.6c-1.3-0.4-2.5-0.9-3.4-1.4l0.9-4.1h0.2c0.3,0.2,0.7,0.5,1.1,0.9
c0.5,0.3,1,0.7,1.7,1c0.6,0.3,1.4,0.6,2.2,0.8c0.8,0.2,1.7,0.3,2.6,0.3c1.9,0,3.4-0.4,4.5-1.1s1.6-1.7,1.6-3.1
c0-0.7-0.3-1.3-0.8-1.7c-0.6-0.4-1.4-0.7-2.4-0.9c-0.5-0.1-1.2-0.3-1.9-0.4c-0.7-0.1-1.5-0.3-2.3-0.5c-1.6-0.4-2.8-1.1-3.5-1.9
c-0.8-0.8-1.1-1.9-1.1-3.1c0-1.1,0.2-2,0.7-3c0.5-0.9,1.1-1.8,2.1-2.5c0.9-0.7,2-1.3,3.3-1.8c1.3-0.5,2.8-0.7,4.5-0.7
c1.4,0,2.7,0.2,4.1,0.5c1.4,0.3,2.5,0.8,3.3,1.3l-0.8,3.9H516c-0.2-0.2-0.5-0.4-1-0.7c-0.4-0.3-1-0.6-1.7-0.9
c-0.6-0.3-1.3-0.5-2.1-0.7c-0.8-0.2-1.6-0.3-2.4-0.3c-1.7,0-3.1,0.4-4.1,1.1c-1.1,0.7-1.6,1.7-1.6,2.9c0,0.7,0.3,1.2,0.8,1.7
c0.5,0.5,1.3,0.8,2.4,1.1c0.7,0.2,1.4,0.3,2.1,0.5c0.7,0.1,1.4,0.3,2.1,0.5c1.6,0.4,2.8,1,3.6,1.8s1.2,1.9,1.2,3.1
c0,1-0.2,2.1-0.7,3.1c-0.5,1-1.2,1.9-2.2,2.6c-1,0.8-2.1,1.4-3.5,1.8C507.4,214,505.9,214.3,504.2,214.3z" />
<path class="st5" d="M528,191l-5.3,22.7h-3.8l5.3-22.7H528z M530.1,183.3l-0.9,4h-4.3l0.9-4H530.1z" />
<path class="st5" d="M547.4,191l-0.7,3.1h-7.9l-2.4,10.5c-0.1,0.5-0.3,1.1-0.4,1.8c-0.1,0.7-0.2,1.2-0.2,1.6c0,1,0.3,1.7,0.8,2.2
c0.5,0.5,1.4,0.7,2.8,0.7c0.6,0,1.2-0.1,2-0.3c0.8-0.2,1.3-0.3,1.6-0.4h0.2l-0.7,3.3c-0.8,0.2-1.6,0.3-2.4,0.5
c-0.9,0.1-1.6,0.2-2.3,0.2c-1.9,0-3.3-0.4-4.4-1.2c-1-0.8-1.5-2.1-1.5-3.9c0-0.4,0-0.9,0.1-1.3c0.1-0.4,0.1-0.9,0.3-1.5l2.8-12.2
h-2.6l0.7-3.1h2.6l1.5-6.5h3.9l-1.5,6.5H547.4z" />
<path class="st5" d="M569.2,199.1c0,2-0.3,4-0.9,5.8c-0.6,1.9-1.5,3.5-2.7,4.9c-1.2,1.4-2.6,2.6-4.1,3.4c-1.5,0.8-3.4,1.2-5.4,1.2
c-2.7,0-4.8-0.8-6.3-2.3c-1.5-1.5-2.3-3.7-2.3-6.4c0-2,0.3-4,0.9-5.8c0.6-1.8,1.5-3.5,2.7-4.9c1.1-1.4,2.5-2.5,4.2-3.3
c1.6-0.8,3.4-1.2,5.4-1.2c2.6,0,4.7,0.7,6.3,2.2C568.4,194.1,569.2,196.3,569.2,199.1z M563,207.6c0.7-1.1,1.3-2.3,1.7-3.8
c0.4-1.4,0.6-2.9,0.6-4.5c0-1.9-0.5-3.3-1.4-4.3c-0.9-1-2.2-1.5-3.9-1.5c-1.3,0-2.5,0.3-3.6,0.9c-1,0.6-2,1.5-2.7,2.6
c-0.7,1.1-1.3,2.3-1.7,3.8c-0.4,1.4-0.6,2.9-0.6,4.5c0,1.9,0.5,3.3,1.4,4.3c0.9,1,2.2,1.5,3.9,1.5c1.3,0,2.5-0.3,3.6-0.9
C561.3,209.6,562.2,208.7,563,207.6z" />
<path class="st5" d="M590.6,195.1h-0.2c-0.5-0.1-1.1-0.2-1.5-0.3c-0.5-0.1-1-0.1-1.7-0.1c-1.3,0-2.5,0.3-3.8,0.9
c-1.3,0.6-2.5,1.3-3.6,2.1l-3.7,16.1h-3.9l5.2-22.7h3.9l-0.8,3.3c1.8-1.3,3.3-2.1,4.6-2.6c1.3-0.5,2.5-0.7,3.6-0.7
c0.7,0,1.2,0,1.4,0.1c0.3,0,0.7,0.1,1.3,0.2L590.6,195.1z" />
<path class="st5" d="M594.1,222.1h-4.2l6.6-9.7l-4.1-21.3h4l3.2,16.9l10.9-16.9h4.2L594.1,222.1z" />
</g>
</g>
</svg>

View file

@ -12,7 +12,6 @@ import {
mdiShieldCrownOutline, mdiShieldCrownOutline,
mdiLicense, mdiLicense,
mdiFileDocument, mdiFileDocument,
mdiLibraryShelves
} from '@mdi/js'; } from '@mdi/js';
export default [ export default [
@ -28,11 +27,6 @@ export default [
icon: mdiLock, icon: mdiLock,
label: 'Security', label: 'Security',
}, },
{
route: 'settings.profile.edit',
icon: mdiLock,
label: 'Profile',
},
// { // {
// route: 'dataset.create', // route: 'dataset.create',
// icon: mdiPublish, // icon: mdiPublish,
@ -112,11 +106,6 @@ export default [
icon: mdiPublish, icon: mdiPublish,
label: 'Create Dataset', label: 'Create Dataset',
}, },
// {
// route: 'dataset.categorize',
// icon: mdiLibraryShelves,
// label: 'Library Classification',
// },
], ],
}, },
{ {
@ -163,9 +152,9 @@ export default [
// label: 'Create Dataset', // label: 'Create Dataset',
// }, // },
{ {
href: 'https://gitea.geosphere.at/geolba/tethys.backend', href: 'https://gitea.geologie.ac.at/geolba/tethys',
icon: mdiGithub, icon: mdiGithub,
label: 'Forgejo', label: 'Gitea',
target: '_blank', target: '_blank',
}, },
{ {

View file

@ -15,5 +15,5 @@
}, },
}, },
"include": ["./**/*.ts", "./**/*.vue"], "include": ["./**/*.ts", "./**/*.vue"],
"exclude": ["./utils/*.js", "./utils/Timer.js", "./utils/focusTrap.js"], "exclude": ["./utils/*.js"],
} }

Some files were not shown because too many files have changed in this diff Show more