Compare commits
9 commits
537c6fd81a
...
b540547e4c
Author | SHA1 | Date | |
---|---|---|---|
b540547e4c | |||
36cd7a757b | |||
a41b091214 | |||
a3031169ca | |||
4c5a8f5a42 | |||
8d47a58d29 | |||
a5e0a36327 | |||
c0496be51b | |||
2c4f51be68 |
122 changed files with 6281 additions and 10424 deletions
|
@ -17,4 +17,6 @@ REDIS_PORT=6379
|
|||
REDIS_PASSWORD=
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=
|
||||
RESEND_API_KEY=
|
||||
RESEND_API_KEY=
|
||||
OPENSEARCH_HOST=http://localhost
|
||||
OPENSEARCH_CORE=tethys-records
|
78
.gitea/workflows/checkReferenceType.yaml
Normal file
78
.gitea/workflows/checkReferenceType.yaml
Normal file
|
@ -0,0 +1,78 @@
|
|||
# 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"
|
|
@ -4,7 +4,13 @@
|
|||
name: CI Pipeline
|
||||
run-name: ${{ github.actor }} is running CI pipeline
|
||||
# trigger build when pushing, or when creating a pull request
|
||||
on: [push, pull_request]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
# Label of the container job
|
||||
|
@ -12,7 +18,7 @@ jobs:
|
|||
# run build on latest ubuntu
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
container: node:18-bullseye
|
||||
container: node:20-bullseye
|
||||
|
||||
services:
|
||||
mydb:
|
||||
|
@ -70,6 +76,7 @@ jobs:
|
|||
&& 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
|
||||
|
||||
# finally run the tests
|
||||
# - run: npm test
|
||||
|
@ -95,3 +102,4 @@ jobs:
|
|||
# uses: coverallsapp/github-action@master
|
||||
# with:
|
||||
# github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -7,3 +7,4 @@ coverage
|
|||
tmp
|
||||
docker-compose.yml
|
||||
.env.test
|
||||
public/assets
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
################## First Stage - Creating base #########################
|
||||
|
||||
# Created a variable to hold our node base image
|
||||
ARG NODE_IMAGE=node:20-bookworm-slim
|
||||
ARG NODE_IMAGE=node:22-bookworm-slim
|
||||
|
||||
FROM $NODE_IMAGE AS base
|
||||
# Install dumb-init and ClamAV, and perform ClamAV database update
|
||||
|
|
7
ace.js
7
ace.js
|
@ -15,10 +15,11 @@
|
|||
/**
|
||||
* Register hook to process TypeScript files using ts-node
|
||||
*/
|
||||
import { register } from 'node:module'
|
||||
register('ts-node/esm', import.meta.url)
|
||||
// import { register } from 'node:module';
|
||||
// register('ts-node/esm', import.meta.url);
|
||||
import 'ts-node-maintained/register/esm';
|
||||
|
||||
/**
|
||||
* Import ace console entrypoint
|
||||
*/
|
||||
await import('./bin/console.js')
|
||||
await import('./bin/console.js');
|
||||
|
|
168
adonisrc.ts
168
adonisrc.ts
|
@ -1,7 +1,7 @@
|
|||
import { defineConfig } from '@adonisjs/core/app'
|
||||
import { defineConfig } from '@adonisjs/core/app';
|
||||
|
||||
export default defineConfig({
|
||||
/*
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Commands
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -10,12 +10,11 @@ export default defineConfig({
|
|||
| will be scanned automatically from the "./commands" directory.
|
||||
|
||||
*/
|
||||
commands: [
|
||||
() => import('@adonisjs/core/commands'),
|
||||
() => import('@adonisjs/lucid/commands'),
|
||||
() => import('@adonisjs/mail/commands')
|
||||
],
|
||||
/*
|
||||
commands: [
|
||||
() => import('@adonisjs/core/commands'),
|
||||
() => import('@adonisjs/lucid/commands'),
|
||||
() => import('@adonisjs/mail/commands')],
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Preloads
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -23,19 +22,21 @@ export default defineConfig({
|
|||
| List of modules to import before starting the application.
|
||||
|
|
||||
*/
|
||||
preloads: [
|
||||
() => import('./start/routes.js'),
|
||||
() => import('./start/kernel.js'),
|
||||
() => import('#start/validator'),
|
||||
() => import('#start/rules/unique'),
|
||||
() => import('#start/rules/translated_language'),
|
||||
() => import('#start/rules/unique_person'),
|
||||
() => import('#start/rules/file_length'),
|
||||
() => import('#start/rules/file_scan'),
|
||||
() => import('#start/rules/allowed_extensions_mimetypes'),
|
||||
() => import('#start/rules/dependent_array_min_length')
|
||||
],
|
||||
/*
|
||||
preloads: [
|
||||
() => import('./start/routes.js'),
|
||||
() => import('./start/kernel.js'),
|
||||
() => import('#start/validator'),
|
||||
() => import('#start/rules/unique'),
|
||||
() => import('#start/rules/translated_language'),
|
||||
() => import('#start/rules/unique_person'),
|
||||
() => import('#start/rules/file_length'),
|
||||
() => import('#start/rules/file_scan'),
|
||||
() => import('#start/rules/allowed_extensions_mimetypes'),
|
||||
() => import('#start/rules/dependent_array_min_length'),
|
||||
() => import('#start/rules/referenceValidation'),
|
||||
() => import('#start/rules/valid_mimetype'),
|
||||
],
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Service providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -44,48 +45,49 @@ export default defineConfig({
|
|||
| application
|
||||
|
|
||||
*/
|
||||
providers: [
|
||||
// () => import('./providers/AppProvider.js'),
|
||||
() => import('@adonisjs/core/providers/app_provider'),
|
||||
() => import('@adonisjs/core/providers/hash_provider'),
|
||||
{
|
||||
file: () => import('@adonisjs/core/providers/repl_provider'),
|
||||
environment: ['repl', 'test'],
|
||||
},
|
||||
() => import('@adonisjs/session/session_provider'),
|
||||
() => import('@adonisjs/core/providers/edge_provider'),
|
||||
() => import('@adonisjs/shield/shield_provider'),
|
||||
// () => import('@eidellev/inertia-adonisjs'),
|
||||
// () => import('@adonisjs/inertia/inertia_provider'),
|
||||
() => import('#providers/app_provider'),
|
||||
() => import('#providers/inertia_provider'),
|
||||
() => import('@adonisjs/lucid/database_provider'),
|
||||
() => import('@adonisjs/auth/auth_provider'),
|
||||
// () => import('@eidellev/adonis-stardust'),
|
||||
() => import('@adonisjs/redis/redis_provider'),
|
||||
() => import('@adonisjs/encore/encore_provider'),
|
||||
() => import('@adonisjs/static/static_provider'),
|
||||
() => import('#providers/stardust_provider'),
|
||||
() => import('#providers/query_builder_provider'),
|
||||
() => import('#providers/token_worker_provider'),
|
||||
// () => import('#providers/validator_provider'),
|
||||
() => import('#providers/drive/provider/drive_provider'),
|
||||
// () => import('@adonisjs/core/providers/vinejs_provider'),
|
||||
() => import('#providers/vinejs_provider'),
|
||||
() => import('@adonisjs/mail/mail_provider')
|
||||
// () => import('#providers/mail_provider'),
|
||||
],
|
||||
metaFiles: [
|
||||
{
|
||||
pattern: 'public/**',
|
||||
reloadServer: false,
|
||||
},
|
||||
{
|
||||
pattern: 'resources/views/**/*.edge',
|
||||
reloadServer: false,
|
||||
},
|
||||
],
|
||||
/*
|
||||
providers: [
|
||||
// () => import('./providers/AppProvider.js'),
|
||||
() => import('@adonisjs/core/providers/app_provider'),
|
||||
() => import('@adonisjs/core/providers/hash_provider'),
|
||||
{
|
||||
file: () => import('@adonisjs/core/providers/repl_provider'),
|
||||
environment: ['repl', 'test'],
|
||||
},
|
||||
() => import('@adonisjs/session/session_provider'),
|
||||
() => import('@adonisjs/core/providers/edge_provider'),
|
||||
() => import('@adonisjs/shield/shield_provider'),
|
||||
// () => import('@eidellev/inertia-adonisjs'),
|
||||
// () => import('@adonisjs/inertia/inertia_provider'),
|
||||
() => import('#providers/app_provider'),
|
||||
() => import('#providers/inertia_provider'),
|
||||
() => import('@adonisjs/lucid/database_provider'),
|
||||
() => import('@adonisjs/auth/auth_provider'),
|
||||
// () => import('@eidellev/adonis-stardust'),
|
||||
() => import('@adonisjs/redis/redis_provider'),
|
||||
// () => import('@adonisjs/encore/encore_provider'),
|
||||
() => import('@adonisjs/static/static_provider'),
|
||||
() => import('#providers/stardust_provider'),
|
||||
() => import('#providers/query_builder_provider'),
|
||||
() => import('#providers/token_worker_provider'),
|
||||
// () => import('#providers/validator_provider'),
|
||||
// () => import('#providers/drive/provider/drive_provider'),
|
||||
() => import('@adonisjs/drive/drive_provider'),
|
||||
// () => import('@adonisjs/core/providers/vinejs_provider'),
|
||||
() => import('#providers/vinejs_provider'),
|
||||
() => import('@adonisjs/mail/mail_provider'),
|
||||
() => import('@adonisjs/vite/vite_provider'),
|
||||
],
|
||||
metaFiles: [
|
||||
{
|
||||
pattern: 'public/**',
|
||||
reloadServer: false,
|
||||
},
|
||||
{
|
||||
pattern: 'resources/views/**/*.edge',
|
||||
reloadServer: false,
|
||||
},
|
||||
],
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Tests
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -94,22 +96,24 @@ export default defineConfig({
|
|||
| and add additional suites.
|
||||
|
|
||||
*/
|
||||
tests: {
|
||||
suites: [
|
||||
{
|
||||
files: ['tests/unit/**/*.spec(.ts|.js)'],
|
||||
name: 'unit',
|
||||
timeout: 2000,
|
||||
},
|
||||
{
|
||||
files: ['tests/functional/**/*.spec(.ts|.js)'],
|
||||
name: 'functional',
|
||||
timeout: 30000,
|
||||
},
|
||||
],
|
||||
forceExit: false,
|
||||
},
|
||||
|
||||
|
||||
|
||||
})
|
||||
tests: {
|
||||
suites: [
|
||||
{
|
||||
files: ['tests/unit/**/*.spec(.ts|.js)'],
|
||||
name: 'unit',
|
||||
timeout: 2000,
|
||||
},
|
||||
{
|
||||
files: ['tests/functional/**/*.spec(.ts|.js)'],
|
||||
name: 'functional',
|
||||
timeout: 30000,
|
||||
},
|
||||
],
|
||||
forceExit: false,
|
||||
},
|
||||
assetsBundler: false,
|
||||
hooks: {
|
||||
onBuildStarting: [() => import('@adonisjs/vite/build_hook')],
|
||||
},
|
||||
// assetsBundler: false
|
||||
});
|
||||
|
|
|
@ -25,6 +25,7 @@ export default class MimetypeController {
|
|||
const newDatasetSchema = vine.object({
|
||||
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
|
||||
alternate_mimetype: vine.array(vine.string().isValidMimetype()).distinct().optional(), // define alias mimetypes
|
||||
enabled: vine.boolean(),
|
||||
});
|
||||
// await request.validate({ schema: newDatasetSchema, messages: this.messages });
|
||||
|
@ -32,18 +33,22 @@ export default class MimetypeController {
|
|||
// Step 2 - Validate request body against the schema
|
||||
// await request.validate({ schema: newDatasetSchema, messages: this.messages });
|
||||
const validator = vine.compile(newDatasetSchema);
|
||||
validator.messagesProvider = new SimpleMessagesProvider(this.messages);
|
||||
await request.validateUsing(validator);
|
||||
validator.messagesProvider = new SimpleMessagesProvider(this.messages);
|
||||
await request.validateUsing(validator, { messagesProvider: new SimpleMessagesProvider(this.messages) });
|
||||
} catch (error) {
|
||||
// Step 3 - Handle errors
|
||||
// return response.badRequest(error.messages);
|
||||
throw error;
|
||||
}
|
||||
const input = request.only(['name', 'enabled', 'file_extension']);
|
||||
const input = request.only(['name', 'enabled', 'file_extension', 'alternate_mimetype']);
|
||||
// Concatenate the file_extensions array into a string with '|' as the separator
|
||||
if (Array.isArray(input.file_extension)) {
|
||||
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);
|
||||
// if (request.input('roles')) {
|
||||
// const roles: Array<number> = request.input('roles');
|
||||
|
|
|
@ -9,12 +9,14 @@ export default class AuthorsController {
|
|||
// where exists (select * from gba.documents inner join gba.link_documents_persons on "documents"."id" = "link_documents_persons"."document_id"
|
||||
// where ("link_documents_persons"."role" = 'author') and ("persons"."id" = "link_documents_persons"."person_id"));
|
||||
const authors = await Person.query()
|
||||
.where('name_type', 'Personal')
|
||||
.whereHas('datasets', (dQuery) => {
|
||||
dQuery.wherePivot('role', 'author');
|
||||
})
|
||||
.withCount('datasets', (query) => {
|
||||
query.as('datasets_count');
|
||||
});
|
||||
})
|
||||
.orderBy('datasets_count', 'desc');
|
||||
|
||||
return authors;
|
||||
}
|
||||
|
|
|
@ -1,65 +1,135 @@
|
|||
import type { HttpContext } from '@adonisjs/core/http';
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
// import * as fs from 'fs';
|
||||
// import * as path from 'path';
|
||||
import redis from '@adonisjs/redis/services/main';
|
||||
|
||||
const prefixes = ['von', 'van'];
|
||||
const PREFIXES = ['von', 'van'];
|
||||
const DEFAULT_SIZE = 50;
|
||||
const FONT_SIZE_RATIO = 0.4;
|
||||
const COLOR_LIGHTENING_PERCENT = 60;
|
||||
const COLOR_DARKENING_FACTOR = 0.6;
|
||||
|
||||
// node ace make:controller Author
|
||||
export default class AvatarController {
|
||||
public async generateAvatar({ request, response }: HttpContext) {
|
||||
try {
|
||||
const { name, background, textColor, size } = request.only(['name', 'background', 'textColor', 'size']);
|
||||
const { name, size = DEFAULT_SIZE } = request.only(['name', 'size']);
|
||||
if (!name) {
|
||||
return response.status(StatusCodes.BAD_REQUEST).json({ error: 'Name is required' });
|
||||
}
|
||||
|
||||
// Build a unique cache key for the given name and size
|
||||
const cacheKey = `avatar:${name.trim().toLowerCase()}-${size}`;
|
||||
const cachedSvg = await redis.get(cacheKey);
|
||||
if (cachedSvg) {
|
||||
this.setResponseHeaders(response);
|
||||
return response.send(cachedSvg);
|
||||
}
|
||||
|
||||
// Generate initials
|
||||
// const initials = name
|
||||
// .split(' ')
|
||||
// .map((part) => part.charAt(0).toUpperCase())
|
||||
// .join('');
|
||||
const initials = this.getInitials(name);
|
||||
const colors = this.generateColors(name);
|
||||
const svgContent = this.createSvg(size, colors, initials);
|
||||
|
||||
// Define SVG content with dynamic values for initials, background color, text color, and size
|
||||
const svgContent = `
|
||||
<svg width="${size || 50}" height="${size || 50}" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100%" height="100%" fill="#${background || '7F9CF5'}"/>
|
||||
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-weight="bold" font-family="Arial, sans-serif" font-size="${
|
||||
(size / 100) * 40 || 25
|
||||
}" fill="#${textColor || 'ffffff'}">${initials}</text>
|
||||
</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');
|
||||
// // Cache the generated avatar for future use, e.g. 1 hour expiry
|
||||
await redis.setex(cacheKey, 3600, svgContent);
|
||||
|
||||
this.setResponseHeaders(response);
|
||||
return response.send(svgContent);
|
||||
} catch (error) {
|
||||
return response.status(StatusCodes.OK).json({ error: error.message });
|
||||
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
private getInitials(name: string) {
|
||||
const parts = name.split(' ');
|
||||
let initials = '';
|
||||
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) {
|
||||
const firstName = parts[0];
|
||||
const lastName = parts[parts.length - 1];
|
||||
return this.getMultiWordInitials(parts);
|
||||
}
|
||||
return parts[0].substring(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
const firstInitial = firstName.charAt(0).toUpperCase();
|
||||
const lastInitial = lastName.charAt(0).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()) {
|
||||
initials = firstInitial + lastName.charAt(1).toUpperCase();
|
||||
} else {
|
||||
initials = firstInitial + lastInitial;
|
||||
}
|
||||
} else if (parts.length === 1) {
|
||||
initials = parts[0].substring(0, 2).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>
|
||||
`;
|
||||
}
|
||||
|
||||
private setResponseHeaders(response: HttpContext['response']): void {
|
||||
response.header('Content-type', 'image/svg+xml');
|
||||
response.header('Cache-Control', 'no-cache');
|
||||
response.header('Pragma', 'no-cache');
|
||||
response.header('Expires', '0');
|
||||
}
|
||||
|
||||
private getColorFromName(name: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
return initials;
|
||||
const colorParts = [];
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,15 @@ import { StatusCodes } from 'http-status-codes';
|
|||
// node ace make:controller Author
|
||||
export default class DatasetController {
|
||||
public async index({}: HttpContext) {
|
||||
// select * from gba.persons
|
||||
// where exists (select * from gba.documents inner join gba.link_documents_persons on "documents"."id" = "link_documents_persons"."document_id"
|
||||
// where ("link_documents_persons"."role" = 'author') and ("persons"."id" = "link_documents_persons"."person_id"));
|
||||
const datasets = await Dataset.query().where('server_state', 'published').orWhere('server_state', 'deleted');
|
||||
// Select datasets with server_state 'published' or 'deleted' and sort by the last published date
|
||||
const datasets = await Dataset.query()
|
||||
.where(function (query) {
|
||||
query.where('server_state', 'published')
|
||||
.orWhere('server_state', 'deleted');
|
||||
})
|
||||
.preload('titles')
|
||||
.preload('identifier')
|
||||
.orderBy('server_date_published', 'desc');
|
||||
|
||||
return datasets;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ export default class FileController {
|
|||
// where: { id: id },
|
||||
// });
|
||||
if (file) {
|
||||
const filePath = '/storage/app/public/' + file.pathName;
|
||||
const filePath = '/storage/app/data/' + file.pathName;
|
||||
const ext = path.extname(filePath);
|
||||
const fileName = file.label + ext;
|
||||
try {
|
||||
|
|
|
@ -9,6 +9,24 @@ import BackupCode from '#models/backup_code';
|
|||
|
||||
// Here we are generating secret and recovery codes for the user that’s enabling 2FA and storing them to our database.
|
||||
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) {
|
||||
const user = (await User.find(auth.user?.id)) as User;
|
||||
// await user.load('totp_secret');
|
||||
|
|
36
app/Controllers/Http/Api/collections_controller.ts
Normal file
36
app/Controllers/Http/Api/collections_controller.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import type { HttpContext } from '@adonisjs/core/http';
|
||||
import Collection from '#models/collection';
|
||||
|
||||
export default class CollectionsController {
|
||||
public async show({ params, response }: HttpContext) {
|
||||
// Get the collection id from route parameters
|
||||
const collectionId = params.id;
|
||||
|
||||
// Find the selected collection by id
|
||||
const collection = await Collection.find(collectionId);
|
||||
if (!collection) {
|
||||
return response.status(404).json({ message: 'Collection not found' });
|
||||
}
|
||||
|
||||
// Query for narrower concepts: collections whose parent_id equals the selected collection's id
|
||||
const narrowerCollections = await Collection.query().where('parent_id', collection.id) || [];
|
||||
|
||||
// For broader concept, if the selected collection has a parent_id fetch that record (otherwise null)
|
||||
const broaderCollection: Collection[] | never[] | null = await (async () => {
|
||||
if (collection.parent_id) {
|
||||
// Try to fetch the parent...
|
||||
const parent = await Collection.find(collection.parent_id)
|
||||
// If found, return it wrapped in an array; if not found, return null (or empty array if you prefer)
|
||||
return parent ? [parent] : null
|
||||
}
|
||||
return []
|
||||
})()
|
||||
|
||||
// Return the selected collection along with its narrower and broader concepts in JSON format
|
||||
return response.json({
|
||||
selectedCollection: collection,
|
||||
narrowerCollections,
|
||||
broaderCollection,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -6,6 +6,11 @@ import hash from '@adonisjs/core/services/hash';
|
|||
// import { schema, rules } from '@adonisjs/validator';
|
||||
import vine from '@vinejs/vine';
|
||||
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 that’s enabling 2FA and storing them to our database.
|
||||
export default class UserController {
|
||||
|
@ -28,7 +33,7 @@ export default class UserController {
|
|||
user: user,
|
||||
twoFactorEnabled: user.isTwoFactorEnabled,
|
||||
// code: await TwoFactorAuthProvider.generateQrCode(user),
|
||||
backupState: backupState,
|
||||
backupState: backupState,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -40,10 +45,8 @@ export default class UserController {
|
|||
// });
|
||||
const passwordSchema = vine.object({
|
||||
// first step
|
||||
old_password: vine
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^[a-zA-Z0-9]+$/),
|
||||
old_password: vine.string().trim(),
|
||||
// .regex(/^[a-zA-Z0-9]+$/),
|
||||
new_password: vine.string().confirmed({ confirmationField: 'confirm_password' }).trim().minLength(8).maxLength(255),
|
||||
});
|
||||
try {
|
||||
|
@ -54,9 +57,9 @@ export default class UserController {
|
|||
// return response.badRequest(error.messages);
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
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']);
|
||||
|
||||
// if (!(old_password && new_password && confirm_password)) {
|
||||
|
@ -82,6 +85,171 @@ 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> {
|
||||
// const user: User | undefined = auth?.user;
|
||||
const user = (await User.find(auth.user?.id)) as User;
|
||||
|
@ -115,7 +283,7 @@ export default class UserController {
|
|||
} else {
|
||||
session.flash('error', 'User not found.');
|
||||
}
|
||||
|
||||
|
||||
return response.redirect().back();
|
||||
// return inertia.render('Auth/AccountInfo', {
|
||||
// // status: {
|
||||
|
|
|
@ -19,14 +19,13 @@ import XmlModel from '#app/Library/XmlModel';
|
|||
import logger from '@adonisjs/core/services/logger';
|
||||
import ResumptionToken from '#app/Library/Oai/ResumptionToken';
|
||||
// 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/core'
|
||||
import { inject } from '@adonisjs/core';
|
||||
// import { TokenWorkerContract } from "MyApp/Models/TokenWorker";
|
||||
import TokenWorkerContract from '#library/Oai/TokenWorkerContract';
|
||||
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
|
||||
|
||||
|
||||
interface XslTParameter {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
@ -35,12 +34,14 @@ interface Dictionary {
|
|||
[index: string]: string;
|
||||
}
|
||||
|
||||
interface ListParameter {
|
||||
interface PagingParameter {
|
||||
cursor: number;
|
||||
totalIds: number;
|
||||
totalLength: number;
|
||||
start: number;
|
||||
reldocIds: (number | null)[];
|
||||
nextDocIds: number[];
|
||||
activeWorkIds: number[];
|
||||
metadataPrefix: string;
|
||||
queryParams: Object;
|
||||
}
|
||||
|
||||
@inject()
|
||||
|
@ -49,6 +50,7 @@ export default class OaiController {
|
|||
private sampleRegEx = /^[A-Za-zäüÄÜß0-9\-_.!~]+$/;
|
||||
private xsltParameter: XslTParameter;
|
||||
|
||||
private firstPublishedDataset: Dataset | null;
|
||||
/**
|
||||
* Holds xml representation of document information to be processed.
|
||||
*
|
||||
|
@ -57,7 +59,6 @@ export default class OaiController {
|
|||
private xml: XMLBuilder;
|
||||
private proc;
|
||||
|
||||
|
||||
constructor(public tokenWorker: TokenWorkerContract) {
|
||||
// Load the XSLT file
|
||||
this.proc = readFileSync('public/assets2/datasetxml2oai.sef.json');
|
||||
|
@ -85,9 +86,9 @@ export default class OaiController {
|
|||
let earliestDateFromDb;
|
||||
// const oaiRequest: OaiParameter = request.body;
|
||||
try {
|
||||
const firstPublishedDataset: Dataset | null = await Dataset.earliestPublicationDate();
|
||||
firstPublishedDataset != null &&
|
||||
(earliestDateFromDb = firstPublishedDataset.server_date_published.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"));
|
||||
this.firstPublishedDataset = await Dataset.earliestPublicationDate();
|
||||
this.firstPublishedDataset != null &&
|
||||
(earliestDateFromDb = this.firstPublishedDataset.server_date_published.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"));
|
||||
this.xsltParameter['earliestDatestamp'] = earliestDateFromDb;
|
||||
// start the request
|
||||
await this.handleRequest(oaiRequest, request);
|
||||
|
@ -162,22 +163,19 @@ export default class OaiController {
|
|||
} else if (verb == 'GetRecord') {
|
||||
await this.handleGetRecord(oaiRequest);
|
||||
} else if (verb == 'ListRecords') {
|
||||
await this.handleListRecords(oaiRequest);
|
||||
// Get browser fingerprint from the request:
|
||||
const browserFingerprint = this.getBrowserFingerprint(request);
|
||||
await this.handleListRecords(oaiRequest, browserFingerprint);
|
||||
} else if (verb == 'ListIdentifiers') {
|
||||
await this.handleListIdentifiers(oaiRequest);
|
||||
// Get browser fingerprint from the request:
|
||||
const browserFingerprint = this.getBrowserFingerprint(request);
|
||||
await this.handleListIdentifiers(oaiRequest, browserFingerprint);
|
||||
} else if (verb == 'ListSets') {
|
||||
await this.handleListSets();
|
||||
} else {
|
||||
this.handleIllegalVerb();
|
||||
}
|
||||
} 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(
|
||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
'The verb provided in the request is illegal.',
|
||||
|
@ -187,11 +185,11 @@ export default class OaiController {
|
|||
}
|
||||
|
||||
protected handleIdentify() {
|
||||
const email = process.env.OAI_EMAIL || 'repository@geosphere.at';
|
||||
const repositoryName = 'Tethys RDR';
|
||||
const repIdentifier = 'tethys.at';
|
||||
const sampleIdentifier = 'oai:' + repIdentifier + ':1'; //$this->_configuration->getSampleIdentifier();
|
||||
|
||||
// Get configuration values from environment or a dedicated configuration service
|
||||
const email = process.env.OAI_EMAIL ?? 'repository@geosphere.at';
|
||||
const repositoryName = process.env.OAI_REPOSITORY_NAME ?? 'Tethys RDR';
|
||||
const repIdentifier = process.env.OAI_REP_IDENTIFIER ?? 'tethys.at';
|
||||
const sampleIdentifier = `oai:${repIdentifier}:1`;
|
||||
// Dataset::earliestPublicationDate()->server_date_published->format('Y-m-d\TH:i:s\Z') : null;
|
||||
// earliestDateFromDb!= null && (this.xsltParameter['earliestDatestamp'] = earliestDateFromDb?.server_date_published);
|
||||
|
||||
|
@ -216,7 +214,7 @@ export default class OaiController {
|
|||
|
||||
const sets: { [key: string]: string } = {
|
||||
'open_access': 'Set for open access licenses',
|
||||
'openaire_data': "OpenAIRE",
|
||||
'openaire_data': 'OpenAIRE',
|
||||
'doc-type:ResearchData': 'Set for document type ResearchData',
|
||||
...(await this.getSetsForDatasetTypes()),
|
||||
...(await this.getSetsForCollections()),
|
||||
|
@ -234,7 +232,15 @@ export default class OaiController {
|
|||
const repIdentifier = 'tethys.at';
|
||||
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);
|
||||
|
||||
// Retrieve dataset with associated XML cache and collection roles
|
||||
const dataset = await Dataset.query()
|
||||
.where('publish_id', dataId)
|
||||
.preload('xmlCache')
|
||||
|
@ -251,59 +257,61 @@ export default class OaiController {
|
|||
);
|
||||
}
|
||||
|
||||
// Validate and set the metadata prefix parameter
|
||||
const metadataPrefix = this.validateAndGetMetadataPrefix(oaiRequest);
|
||||
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);
|
||||
|
||||
// add xml elements
|
||||
// Build the XML for the dataset record and add it to the root node
|
||||
const datasetNode = this.xml.root().ele('Datasets');
|
||||
await this.createXmlRecord(dataset, datasetNode);
|
||||
}
|
||||
|
||||
protected async handleListIdentifiers(oaiRequest: Dictionary) {
|
||||
!this.tokenWorker.isConnected && (await this.tokenWorker.connect());
|
||||
protected async handleListIdentifiers(oaiRequest: Dictionary, browserFingerprint: string) {
|
||||
if (!this.tokenWorker.isConnected) {
|
||||
await this.tokenWorker.connect();
|
||||
}
|
||||
|
||||
const maxIdentifier: number = config.get('oai.max.listidentifiers', 100);
|
||||
await this.handleLists(oaiRequest, maxIdentifier);
|
||||
await this.handleLists(oaiRequest, maxIdentifier, browserFingerprint);
|
||||
}
|
||||
|
||||
protected async handleListRecords(oaiRequest: Dictionary) {
|
||||
!this.tokenWorker.isConnected && (await this.tokenWorker.connect());
|
||||
protected async handleListRecords(oaiRequest: Dictionary, browserFingerprint: string) {
|
||||
if (!this.tokenWorker.isConnected) {
|
||||
await this.tokenWorker.connect();
|
||||
}
|
||||
|
||||
const maxRecords: number = config.get('oai.max.listrecords', 100);
|
||||
await this.handleLists(oaiRequest, maxRecords);
|
||||
await this.handleLists(oaiRequest, maxRecords, browserFingerprint);
|
||||
}
|
||||
|
||||
private async handleLists(oaiRequest: Dictionary, maxRecords: number) {
|
||||
maxRecords = maxRecords || 100;
|
||||
private async handleLists(oaiRequest: Dictionary, maxRecords: number, browserFingerprint: string) {
|
||||
const repIdentifier = 'tethys.at';
|
||||
this.xsltParameter['repIdentifier'] = repIdentifier;
|
||||
const datasetNode = this.xml.root().ele('Datasets');
|
||||
|
||||
// list initialisation
|
||||
const numWrapper: ListParameter = {
|
||||
const paginationParams: PagingParameter ={
|
||||
cursor: 0,
|
||||
totalIds: 0,
|
||||
totalLength: 0,
|
||||
start: maxRecords + 1,
|
||||
reldocIds: [],
|
||||
nextDocIds: [],
|
||||
activeWorkIds: [],
|
||||
metadataPrefix: '',
|
||||
queryParams: {},
|
||||
};
|
||||
|
||||
// resumptionToken is defined
|
||||
if ('resumptionToken' in oaiRequest) {
|
||||
await this.handleResumptionToken(oaiRequest, maxRecords, numWrapper);
|
||||
await this.handleResumptionToken(oaiRequest, maxRecords, paginationParams);
|
||||
} else {
|
||||
// no resumptionToken is given
|
||||
await this.handleNoResumptionToken(oaiRequest, numWrapper);
|
||||
await this.handleNoResumptionToken(oaiRequest, paginationParams, maxRecords);
|
||||
}
|
||||
|
||||
// handling of document ids
|
||||
const restIds = numWrapper.reldocIds as number[];
|
||||
const workIds = restIds.splice(0, maxRecords) as number[]; // array_splice(restIds, 0, maxRecords);
|
||||
const nextIds: number[] = paginationParams.nextDocIds;
|
||||
const workIds: number[] = paginationParams.activeWorkIds;
|
||||
|
||||
// no records returned
|
||||
if (workIds.length == 0) {
|
||||
if (workIds.length === 0) {
|
||||
throw new OaiModelException(
|
||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
'The combination of the given values results in an empty list.',
|
||||
|
@ -311,169 +319,218 @@ export default class OaiController {
|
|||
);
|
||||
}
|
||||
|
||||
const datasets: Dataset[] = await Dataset.query()
|
||||
const datasets = await Dataset.query()
|
||||
.whereIn('publish_id', workIds)
|
||||
.preload('xmlCache')
|
||||
.preload('collections', (builder) => {
|
||||
builder.preload('collectionRole');
|
||||
})
|
||||
.orderBy('publish_id');
|
||||
|
||||
for (const dataset of datasets) {
|
||||
await this.createXmlRecord(dataset, datasetNode);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
await this.setResumptionToken(nextIds, paginationParams, browserFingerprint);
|
||||
}
|
||||
|
||||
private async handleResumptionToken(oaiRequest: Dictionary, maxRecords: number, numWrapper: ListParameter) {
|
||||
const resParam = oaiRequest['resumptionToken']; //e.g. "158886496600000"
|
||||
private async handleNoResumptionToken(oaiRequest: Dictionary, paginationParams: PagingParameter, maxRecords: number) {
|
||||
this.validateMetadataPrefix(oaiRequest, paginationParams);
|
||||
const finder: ModelQueryBuilderContract<typeof Dataset, Dataset> = Dataset.query().whereIn(
|
||||
'server_state',
|
||||
this.deliveringDocumentStates,
|
||||
);
|
||||
this.applySetFilter(finder, oaiRequest);
|
||||
this.applyDateFilters(finder, oaiRequest);
|
||||
await this.fetchAndSetResults(finder, paginationParams, oaiRequest, maxRecords);
|
||||
}
|
||||
|
||||
private async fetchAndSetResults(
|
||||
finder: ModelQueryBuilderContract<typeof Dataset, Dataset>,
|
||||
paginationParams: PagingParameter,
|
||||
oaiRequest: Dictionary,
|
||||
maxRecords: number
|
||||
) {
|
||||
const totalResult = await finder
|
||||
.clone()
|
||||
.count('* as total')
|
||||
.first()
|
||||
.then((res) => res?.$extras.total);
|
||||
paginationParams.totalLength = Number(totalResult);
|
||||
|
||||
const combinedRecords: Dataset[] = await finder.select('publish_id').orderBy('publish_id').offset(0).limit(maxRecords*2);
|
||||
|
||||
paginationParams.activeWorkIds = combinedRecords.slice(0, 100).map((dat) => Number(dat.publish_id));
|
||||
paginationParams.nextDocIds = combinedRecords.slice(100).map((dat) => Number(dat.publish_id));
|
||||
|
||||
// No resumption token was used – set queryParams from the current oaiRequest
|
||||
paginationParams.queryParams = {
|
||||
...oaiRequest,
|
||||
deliveringStates: this.deliveringDocumentStates,
|
||||
};
|
||||
|
||||
// paginationParams.totalLength = 230;
|
||||
}
|
||||
|
||||
private async handleResumptionToken(oaiRequest: Dictionary, maxRecords: number, paginationParams: PagingParameter) {
|
||||
const resParam = oaiRequest['resumptionToken'];
|
||||
const token = await this.tokenWorker.get(resParam);
|
||||
|
||||
if (!token) {
|
||||
throw new OaiModelException(StatusCodes.INTERNAL_SERVER_ERROR, 'cache is outdated.', OaiErrorCodes.BADRESUMPTIONTOKEN);
|
||||
}
|
||||
|
||||
numWrapper.cursor = token.startPosition - 1; //startet dann bei Index 10
|
||||
numWrapper.start = token.startPosition + maxRecords;
|
||||
numWrapper.totalIds = token.totalIds;
|
||||
numWrapper.reldocIds = token.documentIds;
|
||||
numWrapper.metadataPrefix = token.metadataPrefix;
|
||||
// this.setResumptionParameters(token, maxRecords, paginationParams);
|
||||
paginationParams.cursor = token.startPosition - 1;
|
||||
paginationParams.start = token.startPosition + maxRecords;
|
||||
paginationParams.totalLength = token.totalIds;
|
||||
paginationParams.activeWorkIds = token.documentIds;
|
||||
paginationParams.metadataPrefix = token.metadataPrefix;
|
||||
paginationParams.queryParams = token.queryParams;
|
||||
this.xsltParameter['oai_metadataPrefix'] = token.metadataPrefix;
|
||||
|
||||
this.xsltParameter['oai_metadataPrefix'] = numWrapper.metadataPrefix;
|
||||
const finder = this.buildDatasetQueryViaToken(token);
|
||||
const nextRecords: Dataset[] = await this.fetchNextRecords(finder, token, maxRecords);
|
||||
paginationParams.nextDocIds = nextRecords.map((dat) => Number(dat.publish_id));
|
||||
}
|
||||
|
||||
private async handleNoResumptionToken(oaiRequest: Dictionary, numWrapper: ListParameter) {
|
||||
// no resumptionToken is given
|
||||
if ('metadataPrefix' in oaiRequest) {
|
||||
numWrapper.metadataPrefix = oaiRequest['metadataPrefix'];
|
||||
} else {
|
||||
private async setResumptionToken(nextIds: number[], paginationParams: PagingParameter, browserFingerprint: string) {
|
||||
const countRestIds = nextIds.length;
|
||||
if (countRestIds > 0) {
|
||||
// const token = this.createResumptionToken(paginationParams, nextIds);
|
||||
const token = new ResumptionToken();
|
||||
token.startPosition = paginationParams.start;
|
||||
token.totalIds = paginationParams.totalLength;
|
||||
token.documentIds = nextIds;
|
||||
token.metadataPrefix = paginationParams.metadataPrefix;
|
||||
token.queryParams = paginationParams.queryParams;
|
||||
const res: string = await this.tokenWorker.set(token, browserFingerprint);
|
||||
this.setParamResumption(res, paginationParams.cursor, paginationParams.totalLength);
|
||||
}
|
||||
}
|
||||
|
||||
private buildDatasetQueryViaToken(token: ResumptionToken) {
|
||||
const finder = Dataset.query();
|
||||
const originalQuery = token.queryParams || {};
|
||||
const deliveringStates = originalQuery.deliveringStates || this.deliveringDocumentStates;
|
||||
|
||||
finder.whereIn('server_state', deliveringStates);
|
||||
this.applySetFilter(finder, originalQuery);
|
||||
this.applyDateFilters(finder, originalQuery);
|
||||
|
||||
return finder;
|
||||
}
|
||||
|
||||
private async fetchNextRecords(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, token: ResumptionToken, maxRecords: number) {
|
||||
return finder
|
||||
.select('publish_id')
|
||||
.orderBy('publish_id')
|
||||
.offset(token.startPosition - 1 + maxRecords)
|
||||
.limit(100);
|
||||
}
|
||||
|
||||
private validateMetadataPrefix(oaiRequest: Dictionary, paginationParams: PagingParameter) {
|
||||
if (!('metadataPrefix' in oaiRequest)) {
|
||||
throw new OaiModelException(
|
||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
'The prefix of the metadata argument is unknown.',
|
||||
OaiErrorCodes.BADARGUMENT,
|
||||
);
|
||||
}
|
||||
this.xsltParameter['oai_metadataPrefix'] = numWrapper.metadataPrefix;
|
||||
paginationParams.metadataPrefix = oaiRequest['metadataPrefix'];
|
||||
this.xsltParameter['oai_metadataPrefix'] = paginationParams.metadataPrefix;
|
||||
}
|
||||
|
||||
let finder: ModelQueryBuilderContract<typeof Dataset, Dataset> = Dataset.query();
|
||||
// add server state restrictions
|
||||
finder.whereIn('server_state', this.deliveringDocumentStates);
|
||||
if ('set' in oaiRequest) {
|
||||
const set = oaiRequest['set'] as string;
|
||||
const setArray = set.split(':');
|
||||
private applySetFilter(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, queryParams: any) {
|
||||
if ('set' in queryParams) {
|
||||
const [setType, setValue] = queryParams['set'].split(':');
|
||||
|
||||
if (setArray[0] == 'data-type') {
|
||||
if (setArray.length == 2 && setArray[1]) {
|
||||
finder.where('type', setArray[1]);
|
||||
}
|
||||
} else if (setArray[0] == 'open_access') {
|
||||
const openAccessLicences = ['CC-BY-4.0', 'CC-BY-SA-4.0'];
|
||||
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]);
|
||||
switch (setType) {
|
||||
case 'data-type':
|
||||
setValue && finder.where('type', setValue);
|
||||
break;
|
||||
case 'open_access':
|
||||
finder.andWhereHas('licenses', (query) => {
|
||||
query.whereIn('name', ['CC-BY-4.0', 'CC-BY-SA-4.0']);
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'ddc':
|
||||
setValue &&
|
||||
finder.andWhereHas('collections', (query) => {
|
||||
query.where('number', setValue);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// const timeZone = "Europe/Vienna"; // Canonical time zone name
|
||||
// &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');
|
||||
private applyDateFilters(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, queryParams: any) {
|
||||
const { from, until } = queryParams;
|
||||
|
||||
if (from.length != until.length) {
|
||||
throw new OaiModelException(
|
||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
'The request has different granularities for the from and until parameters.',
|
||||
OaiErrorCodes.BADARGUMENT,
|
||||
);
|
||||
}
|
||||
fromDate.hour() == 0 && (fromDate = fromDate.startOf('day'));
|
||||
untilDate.hour() == 0 && (untilDate = untilDate.endOf('day'));
|
||||
if (from && until) {
|
||||
this.handleFromUntilFilter(finder, from, until);
|
||||
} else if (from) {
|
||||
this.handleFromFilter(finder, from);
|
||||
} else if (until) {
|
||||
this.handleUntilFilter(finder, until);
|
||||
}
|
||||
}
|
||||
|
||||
finder.whereBetween('server_date_published', [fromDate.format('YYYY-MM-DD HH:mm:ss'), untilDate.format('YYYY-MM-DD HH:mm:ss')]);
|
||||
} else if ('from' in oaiRequest && !('until' in oaiRequest)) {
|
||||
const from = oaiRequest['from'] as string;
|
||||
let fromDate = dayjs(from);
|
||||
if (!fromDate.isValid()) {
|
||||
throw new OaiModelException(
|
||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
'From date parameter is not valid.',
|
||||
OaiErrorCodes.BADARGUMENT,
|
||||
);
|
||||
}
|
||||
fromDate = dayjs.tz(from, 'Europe/Vienna');
|
||||
fromDate.hour() == 0 && (fromDate = fromDate.startOf('day'));
|
||||
private handleFromUntilFilter(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, from: string, until: string) {
|
||||
const fromDate = this.parseDateWithValidation(from, 'From');
|
||||
const untilDate = this.parseDateWithValidation(until, 'Until');
|
||||
|
||||
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,
|
||||
);
|
||||
} 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'));
|
||||
}
|
||||
if (from.length !== until.length) {
|
||||
throw new OaiModelException(
|
||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
'The request has different granularities for the from and until parameters.',
|
||||
OaiErrorCodes.BADARGUMENT,
|
||||
);
|
||||
}
|
||||
|
||||
let reldocIdsDocs = await finder.select('publish_id').orderBy('publish_id');
|
||||
numWrapper.reldocIds = reldocIdsDocs.map((dat) => dat.publish_id);
|
||||
numWrapper.totalIds = numWrapper.reldocIds.length; //212
|
||||
finder.whereBetween('server_date_published', [fromDate.format('YYYY-MM-DD HH:mm:ss'), untilDate.format('YYYY-MM-DD HH:mm:ss')]);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -641,4 +698,30 @@ export default class OaiController {
|
|||
this.xsltParameter['oai_error_code'] = 'badVerb';
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -276,7 +276,7 @@ export default class DatasetsController {
|
|||
validateSMTP: false,
|
||||
});
|
||||
const validRecipientEmail: boolean = validationResult.valid;
|
||||
let emailStatusMessage = '';
|
||||
// let emailStatusMessage = '';
|
||||
|
||||
if (sendMail == true) {
|
||||
if (dataset.editor.email && validRecipientEmail) {
|
||||
|
@ -289,7 +289,7 @@ export default class DatasetsController {
|
|||
<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) {
|
||||
logger.error(error);
|
||||
return response
|
||||
|
@ -297,7 +297,7 @@ export default class DatasetsController {
|
|||
.toRoute('reviewer.dataset.list');
|
||||
}
|
||||
} 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.`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import Description from '#models/description';
|
|||
import Language from '#models/language';
|
||||
import Coverage from '#models/coverage';
|
||||
import Collection from '#models/collection';
|
||||
import CollectionRole from '#models/collection_role';
|
||||
import dayjs from 'dayjs';
|
||||
import Person from '#models/person';
|
||||
import db from '@adonisjs/lucid/services/db';
|
||||
|
@ -33,7 +34,9 @@ import File from '#models/file';
|
|||
import ClamScan from 'clamscan';
|
||||
// import { ValidationException } from '@adonisjs/validator';
|
||||
// import Drive from '@ioc:Adonis/Core/Drive';
|
||||
import drive from '#services/drive';
|
||||
// import drive from '#services/drive';
|
||||
import drive from '@adonisjs/drive/services/main';
|
||||
import path from 'path';
|
||||
import { Exception } from '@adonisjs/core/exceptions';
|
||||
import { MultipartFile } from '@adonisjs/core/types/bodyparser';
|
||||
import * as crypto from 'crypto';
|
||||
|
@ -363,7 +366,8 @@ export default class DatasetController {
|
|||
references: vine
|
||||
.array(
|
||||
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)),
|
||||
relation: vine.enum(Object.values(RelationTypes)),
|
||||
label: vine.string().trim().minLength(2).maxLength(255),
|
||||
|
@ -498,7 +502,7 @@ export default class DatasetController {
|
|||
}
|
||||
|
||||
// save collection
|
||||
const collection: Collection | null = await Collection.query().where('id', 21).first();
|
||||
const collection: Collection | null = await Collection.query().where('id', 594).first();
|
||||
collection && (await dataset.useTransaction(trx).related('collections').attach([collection.id]));
|
||||
|
||||
// save coverage
|
||||
|
@ -531,11 +535,18 @@ export default class DatasetController {
|
|||
const fileName = this.generateFilename(file.extname as string);
|
||||
const mimeType = file.headers['content-type'] || 'application/octet-stream'; // Fallback to a default MIME type
|
||||
const datasetFolder = `files/${dataset.id}`;
|
||||
const datasetFullPath = path.join(`${datasetFolder}`, fileName);
|
||||
// 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,
|
||||
overwrite: true, // overwrite in case of conflict
|
||||
disk: 'local',
|
||||
});
|
||||
|
||||
// save file metadata into db
|
||||
const newFile = new File();
|
||||
newFile.pathName = `${datasetFolder}/${fileName}`;
|
||||
|
@ -1030,10 +1041,16 @@ export default class DatasetController {
|
|||
// move to disk:
|
||||
const fileName = `file-${cuid()}.${fileData.extname}`; //'file-ls0jyb8xbzqtrclufu2z2e0c.pdf'
|
||||
const datasetFolder = `files/${dataset.id}`; // 'files/307'
|
||||
const datasetFullPath = path.join(`${datasetFolder}`, fileName);
|
||||
// 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,
|
||||
overwrite: true, // overwrite in case of conflict
|
||||
driver: 'local',
|
||||
});
|
||||
|
||||
//save to db:
|
||||
|
@ -1160,31 +1177,32 @@ export default class DatasetController {
|
|||
if (validStates.includes(dataset.server_state)) {
|
||||
if (dataset.files && dataset.files.length > 0) {
|
||||
for (const file of dataset.files) {
|
||||
// overwritten delete method also delets file on filespace
|
||||
// overwritten delete method also delets file on filespace and db object
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
const datasetFolder = `files/${params.id}`;
|
||||
const folderExists = await drive.exists(datasetFolder);
|
||||
if (folderExists) {
|
||||
const dirListing = drive.list(datasetFolder);
|
||||
const folderContents = await dirListing.toArray();
|
||||
if (folderContents.length === 0) {
|
||||
await drive.delete(datasetFolder);
|
||||
}
|
||||
// delete dataset wirh relation in db
|
||||
await dataset.delete();
|
||||
session.flash({ message: 'You have deleted 1 dataset!' });
|
||||
return response.redirect().toRoute('dataset.list');
|
||||
} else {
|
||||
// session.flash({
|
||||
// warning: `You cannot delete this dataset! Invalid server_state: "${dataset.server_state}"!`,
|
||||
// });
|
||||
return response
|
||||
.flash({ warning: `You cannot delete this dataset! Dataset folder "${datasetFolder}" doesn't exist!` })
|
||||
.redirect()
|
||||
.back();
|
||||
}
|
||||
// const folderExists = await drive.use('local').exists(datasetFolder);
|
||||
// if (folderExists) {
|
||||
// const dirListing = drive.list(datasetFolder);
|
||||
// const folderContents = await dirListing.toArray();
|
||||
// if (folderContents.length === 0) {
|
||||
// await drive.delete(datasetFolder);
|
||||
// }
|
||||
await drive.use('local').deleteAll(datasetFolder);
|
||||
// delete dataset wirh relation in db
|
||||
await dataset.delete();
|
||||
session.flash({ message: 'You have deleted 1 dataset!' });
|
||||
return response.redirect().toRoute('dataset.list');
|
||||
// } else {
|
||||
// // session.flash({
|
||||
// // warning: `You cannot delete this dataset! Invalid server_state: "${dataset.server_state}"!`,
|
||||
// // });
|
||||
// return response
|
||||
// .flash({ warning: `You cannot delete this dataset! Dataset folder "${datasetFolder}" doesn't exist!` })
|
||||
// .redirect()
|
||||
// .back();
|
||||
// }
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof errors.E_VALIDATION_ERROR) {
|
||||
|
@ -1192,11 +1210,42 @@ export default class DatasetController {
|
|||
throw error;
|
||||
} else if (error instanceof Exception) {
|
||||
// General exception handling
|
||||
return response.flash('errors', { error: error.message }).redirect().back();
|
||||
session.flash({ error: error.message });
|
||||
return response.redirect().back();
|
||||
} else {
|
||||
session.flash({ error: 'An error occurred while deleting the dataset.' });
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import DoiClientException from '#app/exceptions/DoiClientException';
|
|||
import { StatusCodes } from 'http-status-codes';
|
||||
import logger from '@adonisjs/core/services/logger';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import axios from 'axios';
|
||||
import { default as axios } from 'axios';
|
||||
|
||||
export class DoiClient implements DoiClientContract {
|
||||
public username: string;
|
||||
|
@ -50,7 +50,7 @@ export class DoiClient implements DoiClientContract {
|
|||
'Content-Type': 'application/xml;charset=UTF-8',
|
||||
};
|
||||
try {
|
||||
const metadataResponse = await axios.default.put(`${this.serviceUrl}/metadata/${doiValue}`, xmlMeta, { auth, headers });
|
||||
const metadataResponse = await axios.put(`${this.serviceUrl}/metadata/${doiValue}`, xmlMeta, { auth, headers });
|
||||
|
||||
// Response Codes
|
||||
// 201 Created: operation successful
|
||||
|
@ -65,7 +65,7 @@ export class DoiClient implements DoiClientContract {
|
|||
throw new DoiClientException(metadataResponse.status, message);
|
||||
}
|
||||
|
||||
const doiResponse = await axios.default.put(`${this.serviceUrl}/doi/${doiValue}`, `doi=${doiValue}\nurl=${landingPageUrl}`, {
|
||||
const doiResponse = await axios.put(`${this.serviceUrl}/doi/${doiValue}`, `doi=${doiValue}\nurl=${landingPageUrl}`, {
|
||||
auth,
|
||||
headers,
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ export default class ResumptionToken {
|
|||
private _resumptionId = '';
|
||||
private _startPosition = 0;
|
||||
private _totalIds = 0;
|
||||
private _queryParams: Record<string, any> = {};
|
||||
|
||||
get key(): string {
|
||||
return this.metadataPrefix + this.startPosition + this.totalIds;
|
||||
|
@ -48,4 +49,12 @@ export default class ResumptionToken {
|
|||
set totalIds(totalIds: number) {
|
||||
this._totalIds = totalIds;
|
||||
}
|
||||
|
||||
get queryParams(): Record<string, any> {
|
||||
return this._queryParams;
|
||||
}
|
||||
|
||||
set queryParams(params: Record<string, any>) {
|
||||
this._queryParams = params;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,6 @@ export default abstract class TokenWorkerContract {
|
|||
abstract connect(): void;
|
||||
abstract close(): void;
|
||||
abstract get(key: string): Promise<ResumptionToken | null>;
|
||||
abstract set(token: ResumptionToken): Promise<string>;
|
||||
abstract set(token: ResumptionToken, browserFingerprint: string): Promise<string>;
|
||||
}
|
||||
|
||||
|
|
|
@ -40,14 +40,64 @@ export default class TokenWorkerService implements TokenWorkerContract {
|
|||
return result !== undefined && result !== null;
|
||||
}
|
||||
|
||||
public async set(token: ResumptionToken): Promise<string> {
|
||||
const uniqueName = await this.generateUniqueName();
|
||||
/**
|
||||
* Simplified set method that stores the token using a browser fingerprint key.
|
||||
* If the token for that fingerprint already exists and its documentIds match the new token,
|
||||
* then the fingerprint key is simply returned.
|
||||
*/
|
||||
public async set(token: ResumptionToken, browserFingerprint: string): Promise<string> {
|
||||
// Generate a 15-digit unique number string based on the fingerprint
|
||||
const uniqueNumberKey = this.createUniqueNumberFromFingerprint(browserFingerprint, token.documentIds, token.totalIds);
|
||||
// Optionally, you could prefix it if desired, e.g. 'rs_' + uniqueNumberKey
|
||||
const fingerprintKey = uniqueNumberKey;
|
||||
|
||||
// const fingerprintKey = `rs_fp_${browserFingerprint}`;
|
||||
const existingTokenString = await this.cache.get(fingerprintKey);
|
||||
|
||||
if (existingTokenString) {
|
||||
const existingToken = this.parseToken(existingTokenString);
|
||||
if (this.arraysAreEqual(existingToken.documentIds, token.documentIds)) {
|
||||
return fingerprintKey;
|
||||
}
|
||||
}
|
||||
|
||||
const serialToken = JSON.stringify(token);
|
||||
await this.cache.setEx(uniqueName, this.ttl, serialToken);
|
||||
return uniqueName;
|
||||
await this.cache.setEx(fingerprintKey, this.ttl, serialToken);
|
||||
return fingerprintKey;
|
||||
}
|
||||
|
||||
// Updated helper method to generate a unique key based on fingerprint and documentIds
|
||||
private createUniqueNumberFromFingerprint(browserFingerprint: string, documentIds: number[], totalIds: number): string {
|
||||
// Combine the fingerprint, document IDs and totalIds to produce the input string
|
||||
const combined = browserFingerprint + ':' + documentIds.join('-') + ':' + totalIds;
|
||||
// Simple hash algorithm
|
||||
let hash = 0;
|
||||
for (let i = 0; i < combined.length; i++) {
|
||||
hash = (hash << 5) - hash + combined.charCodeAt(i);
|
||||
hash |= 0; // Convert to 32-bit integer
|
||||
}
|
||||
// Ensure positive number and limit it to at most 15 digits
|
||||
const positiveHash = Math.abs(hash) % 1000000000000000;
|
||||
// Pad with trailing zeros to ensure a 15-digit string
|
||||
return positiveHash.toString().padEnd(15, '0');
|
||||
}
|
||||
|
||||
// Add a helper function to compare two arrays of numbers with identical order
|
||||
private arraysAreEqual(arr1: number[], arr2: number[]): boolean {
|
||||
if (arr1.length !== arr2.length) {
|
||||
return false;
|
||||
}
|
||||
return arr1.every((num, index) => num === arr2[index]);
|
||||
}
|
||||
|
||||
// public async set(token: ResumptionToken): Promise<string> {
|
||||
// const uniqueName = await this.generateUniqueName();
|
||||
|
||||
// const serialToken = JSON.stringify(token);
|
||||
// await this.cache.setEx(uniqueName, this.ttl, serialToken);
|
||||
// return uniqueName;
|
||||
// }
|
||||
|
||||
private async generateUniqueName(): Promise<string> {
|
||||
let fc = 0;
|
||||
const uniqueId = dayjs().unix().toString();
|
||||
|
|
|
@ -209,6 +209,15 @@ export default class Dataset extends DatasetExtension {
|
|||
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, {
|
||||
pivotForeignKey: 'document_id',
|
||||
pivotRelatedForeignKey: 'person_id',
|
||||
|
|
|
@ -3,12 +3,12 @@ import { column, hasMany, belongsTo, SnakeCaseNamingStrategy, computed } from '@
|
|||
import HashValue from './hash_value.js';
|
||||
import Dataset from './dataset.js';
|
||||
import BaseModel from './base_model.js';
|
||||
// import { Buffer } from 'buffer';
|
||||
import * as fs from 'fs';
|
||||
import crypto from 'crypto';
|
||||
// import Drive from '@ioc:Adonis/Core/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 { BelongsTo } from "@adonisjs/lucid/types/relations";
|
||||
|
@ -88,7 +88,8 @@ export default class File extends BaseModel {
|
|||
serializeAs: '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');
|
||||
// return mainTitle ? mainTitle.value : null;
|
||||
}
|
||||
|
@ -165,7 +166,7 @@ export default class File extends BaseModel {
|
|||
public async delete() {
|
||||
if (this.pathName) {
|
||||
// Delete file from additional storage
|
||||
await drive.delete(this.pathName);
|
||||
await drive.use('local').delete(this.pathName);
|
||||
}
|
||||
|
||||
// Call the original delete method of the BaseModel to remove the record from the database
|
||||
|
|
|
@ -16,9 +16,14 @@ export default class MimeType extends BaseModel {
|
|||
@column({})
|
||||
public name: string;
|
||||
|
||||
// 1 : n file_extensions are separated by '|' in the database
|
||||
@column({})
|
||||
public file_extension: string;
|
||||
|
||||
// 1 : n alternate_mimetype are separated by '|' in the database
|
||||
@column({})
|
||||
public alternate_mimetype: string;
|
||||
|
||||
@column({})
|
||||
public enabled: boolean;
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ export default class Person extends BaseModel {
|
|||
serializeAs: 'name',
|
||||
})
|
||||
public get fullName() {
|
||||
return `${this.firstName} ${this.lastName}`;
|
||||
return [this.firstName, this.lastName].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
// @computed()
|
||||
|
@ -64,10 +64,13 @@ export default class Person extends BaseModel {
|
|||
// return '2023-03-21 08:45:00';
|
||||
// }
|
||||
|
||||
@computed()
|
||||
|
||||
@computed({
|
||||
serializeAs: 'dataset_count',
|
||||
})
|
||||
public get datasetCount() {
|
||||
const stock = this.$extras.datasets_count; //my pivot column name was "stock"
|
||||
return stock;
|
||||
return Number(stock);
|
||||
}
|
||||
|
||||
@computed()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { DateTime } from 'luxon';
|
||||
import { withAuthFinder } from '@adonisjs/auth/mixins/lucid';
|
||||
import { column, manyToMany, hasMany, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm';
|
||||
import { column, manyToMany, hasMany, SnakeCaseNamingStrategy, computed, beforeFetch, beforeFind } from '@adonisjs/lucid/orm';
|
||||
import hash from '@adonisjs/core/services/hash';
|
||||
import Role from './role.js';
|
||||
import db from '@adonisjs/lucid/services/db';
|
||||
|
@ -49,7 +49,6 @@ export default class User extends compose(BaseModel, AuthFinder) {
|
|||
@column()
|
||||
public login: string;
|
||||
|
||||
|
||||
@column()
|
||||
public firstName: string;
|
||||
|
||||
|
@ -87,6 +86,9 @@ export default class User extends compose(BaseModel, AuthFinder) {
|
|||
@column({})
|
||||
public state: number;
|
||||
|
||||
@column({})
|
||||
public avatar: string;
|
||||
|
||||
// @hasOne(() => TotpSecret, {
|
||||
// foreignKey: 'user_id',
|
||||
// })
|
||||
|
@ -104,6 +106,7 @@ export default class User extends compose(BaseModel, AuthFinder) {
|
|||
// return Boolean(this.totp_secret?.twoFactorSecret);
|
||||
}
|
||||
|
||||
|
||||
@manyToMany(() => Role, {
|
||||
pivotForeignKey: 'account_id',
|
||||
pivotRelatedForeignKey: 'role_id',
|
||||
|
@ -121,6 +124,27 @@ export default class User extends compose(BaseModel, AuthFinder) {
|
|||
})
|
||||
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[]> {
|
||||
const test = await this.related('backupcodes').query();
|
||||
// return test.map((role) => role.code);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import vine, { SimpleMessagesProvider } from '@vinejs/vine';
|
||||
import { TitleTypes, DescriptionTypes, ContributorTypes, ReferenceIdentifierTypes, RelationTypes } from '#contracts/enums';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// import MimeType from '#models/mime_type';
|
||||
|
||||
// const enabledExtensions = await MimeType.query().select('file_extension').where('enabled', true).exec();
|
||||
|
@ -125,7 +126,7 @@ export const createDatasetValidator = vine.compile(
|
|||
references: vine
|
||||
.array(
|
||||
vine.object({
|
||||
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)),
|
||||
relation: vine.enum(Object.values(RelationTypes)),
|
||||
label: vine.string().trim().minLength(2).maxLength(255),
|
||||
|
@ -272,7 +273,7 @@ export const updateDatasetValidator = vine.compile(
|
|||
references: vine
|
||||
.array(
|
||||
vine.object({
|
||||
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)),
|
||||
relation: vine.enum(Object.values(RelationTypes)),
|
||||
label: vine.string().trim().minLength(2).maxLength(255),
|
||||
|
|
|
@ -142,7 +142,7 @@ export class VanillaErrorReporter implements ErrorReporterContract {
|
|||
// }
|
||||
this.hasErrors = true;
|
||||
|
||||
var test = field.getFieldPath();
|
||||
// var test = field.getFieldPath();
|
||||
|
||||
// this.errors.push(error);
|
||||
// if (this.errors[error.field]) {
|
||||
|
|
|
@ -88,7 +88,7 @@ export default class ValidateChecksum extends BaseCommand {
|
|||
);
|
||||
|
||||
// Construct the file path
|
||||
const filePath = '/storage/app/public/' + file.pathName;
|
||||
const filePath = '/storage/app/data/' + file.pathName;
|
||||
|
||||
try {
|
||||
// Calculate the MD5 checksum of the file
|
||||
|
|
|
@ -80,7 +80,8 @@ export const http = defineConfig({
|
|||
| headers.
|
||||
|
|
||||
*/
|
||||
trustProxy: proxyAddr.compile('loopback'),
|
||||
// trustProxy: proxyAddr.compile('loopback'),
|
||||
trustProxy: proxyAddr.compile(['127.0.0.1', '::1/128']),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
@ -47,7 +47,7 @@ const databaseConfig = defineConfig({
|
|||
migrations: {
|
||||
naturalSort: true,
|
||||
},
|
||||
healthCheck: false,
|
||||
// healthCheck: false,
|
||||
debug: false,
|
||||
pool: { min: 1, max: 100 },
|
||||
},
|
||||
|
|
190
config/drive.ts
190
config/drive.ts
|
@ -1,151 +1,45 @@
|
|||
/**
|
||||
* 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';
|
||||
// import env from '#start/env'
|
||||
// import app from '@adonisjs/core/services/app'
|
||||
import { defineConfig, services } from '@adonisjs/drive'
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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'),
|
||||
const driveConfig = defineConfig({
|
||||
|
||||
default: 'public',
|
||||
|
||||
|
||||
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/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: '/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,
|
||||
// },
|
||||
services: {
|
||||
|
||||
/**
|
||||
* Persist files on the local filesystem
|
||||
*/
|
||||
public: services.fs({
|
||||
location: '/storage/app/public/',
|
||||
serveFiles: true,
|
||||
routeBasePath: '/public',
|
||||
visibility: 'public',
|
||||
}),
|
||||
local: services.fs({
|
||||
location: '/storage/app/data/',
|
||||
serveFiles: true,
|
||||
routeBasePath: '/data',
|
||||
visibility: 'public',
|
||||
}),
|
||||
|
||||
|
||||
/**
|
||||
* Persist files on Digital Ocean spaces
|
||||
*/
|
||||
// spaces: services.s3({
|
||||
// credentials: {
|
||||
// accessKeyId: env.get('SPACES_KEY'),
|
||||
// secretAccessKey: env.get('SPACES_SECRET'),
|
||||
// },
|
||||
// region: env.get('SPACES_REGION'),
|
||||
// bucket: env.get('SPACES_BUCKET'),
|
||||
// endpoint: env.get('SPACES_ENDPOINT'),
|
||||
// visibility: 'public',
|
||||
// }),
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
export default driveConfig
|
233
config/drive_self.ts
Normal file
233
config/drive_self.ts
Normal file
|
@ -0,0 +1,233 @@
|
|||
/**
|
||||
* 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,
|
||||
// },
|
||||
},
|
||||
});
|
|
@ -1,7 +1,8 @@
|
|||
import { defineConfig } from '@adonisjs/inertia';
|
||||
import type { HttpContext } from '@adonisjs/core/http';
|
||||
import type { InferSharedProps } from '@adonisjs/inertia/types'
|
||||
|
||||
export default defineConfig({
|
||||
const inertiaConfig = defineConfig({
|
||||
/**
|
||||
* Path to the Edge view that will be used as the root view for Inertia responses
|
||||
*/
|
||||
|
@ -52,6 +53,12 @@ export default defineConfig({
|
|||
},
|
||||
});
|
||||
|
||||
export default inertiaConfig
|
||||
|
||||
declare module '@adonisjs/inertia/types' {
|
||||
export interface SharedProps extends InferSharedProps<typeof inertiaConfig> {}
|
||||
}
|
||||
|
||||
// import { InertiaConfig } from '@ioc:EidelLev/Inertia';
|
||||
|
||||
// /*
|
||||
|
|
|
@ -12,7 +12,7 @@ const mailConfig = defineConfig({
|
|||
mailers: {
|
||||
|
||||
smtp: transports.smtp({
|
||||
socketTimeout: 5000,// Overall timeout (5 seconds)
|
||||
// socketTimeout: 5000,// Overall timeout (5 seconds)
|
||||
host: env.get('SMTP_HOST', ''),
|
||||
port: env.get('SMTP_PORT'),
|
||||
secure: false,
|
||||
|
@ -30,10 +30,10 @@ const mailConfig = defineConfig({
|
|||
}, */
|
||||
}),
|
||||
|
||||
resend: transports.resend({
|
||||
key: env.get('RESEND_API_KEY'),
|
||||
baseUrl: 'https://api.resend.com',
|
||||
}),
|
||||
// resend: transports.resend({
|
||||
// key: env.get('RESEND_API_KEY'),
|
||||
// baseUrl: 'https://api.resend.com',
|
||||
// }),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
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';
|
||||
|
||||
const sessionConfig = defineConfig({
|
||||
|
|
32
config/vite.ts
Normal file
32
config/vite.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
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;
|
|
@ -18,6 +18,7 @@ export default class Accounts extends BaseSchema {
|
|||
table.text("two_factor_recovery_codes").nullable();
|
||||
table.smallint('state').nullable();
|
||||
table.bigint('last_counter').nullable();
|
||||
table.string('avatar').nullable();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -43,6 +44,7 @@ export default class Accounts extends BaseSchema {
|
|||
// two_factor_recovery_codes text COLLATE pg_catalog."default",
|
||||
// state smallint,
|
||||
// last_counter bigint,
|
||||
// avatar character varying(255),
|
||||
// )
|
||||
|
||||
// ALTER TABLE gba.accounts
|
||||
|
@ -85,3 +87,6 @@ export default class Accounts extends BaseSchema {
|
|||
// GRANT ALL ON SEQUENCE gba.totp_secrets_id_seq TO tethys_admin;
|
||||
|
||||
// ALTER TABLE gba.totp_secrets ALTER COLUMN id SET DEFAULT nextval('gba.totp_secrets_id_seq');
|
||||
|
||||
|
||||
// ALTER TABLE "accounts" ADD COLUMN "avatar" VARCHAR(255) NULL
|
||||
|
|
|
@ -54,3 +54,8 @@ export default class Collections extends BaseSchema {
|
|||
// ON UPDATE CASCADE
|
||||
// ON DELETE CASCADE
|
||||
// )
|
||||
|
||||
|
||||
// change to normal intzeger:
|
||||
// ALTER TABLE collections ALTER COLUMN id DROP DEFAULT;
|
||||
// DROP SEQUENCE IF EXISTS collections_id_seq;
|
||||
|
|
18
database/migrations/update_1_to_mime_types.ts
Normal file
18
database/migrations/update_1_to_mime_types.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
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
|
10224
package-lock.json
generated
10224
package-lock.json
generated
File diff suppressed because it is too large
Load diff
77
package.json
77
package.json
|
@ -4,7 +4,8 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"type-check": "tsc --noEmit",
|
||||
"dev": "node ace serve --watch",
|
||||
"dev": "node ace serve",
|
||||
"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: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'",
|
||||
|
@ -15,59 +16,58 @@
|
|||
"format-check": "prettier --check ./**/*.{ts,js}",
|
||||
"test": "node ace test"
|
||||
},
|
||||
"eslintIgnore": [
|
||||
"build"
|
||||
],
|
||||
"eslintConfig": {
|
||||
"ignorePatterns": [
|
||||
"build"
|
||||
]
|
||||
},
|
||||
"alias": {
|
||||
"vue": "./node_modules/vue/dist/vue.esm-bundler.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@adonisjs/assembler": "^7.1.1",
|
||||
"@adonisjs/tsconfig": "^1.2.1",
|
||||
"@babel/core": "^7.20.12",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-proposal-decorators": "^7.20.13",
|
||||
"@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",
|
||||
"@adonisjs/tsconfig": "^1.4.0",
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@japa/assert": "^4.0.1",
|
||||
"@japa/plugin-adonisjs": "^4.0.0",
|
||||
"@japa/runner": "^4.2.0",
|
||||
"@mdi/js": "^7.1.96",
|
||||
"@poppinss/utils": "^6.7.2",
|
||||
"@swc/core": "^1.4.2",
|
||||
"@symfony/webpack-encore": "^5.0.1",
|
||||
"@swc/wasm": "^1.10.14",
|
||||
"@tailwindcss/forms": "^0.5.2",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/clamscan": "^2.0.4",
|
||||
"@types/escape-html": "^1.0.4",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/leaflet": "^1.9.3",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.5.5",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/proxy-addr": "^2.0.0",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/source-map-support": "^0.5.6",
|
||||
"@types/sprintf-js": "^1.1.4",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"babel-preset-typescript-vue3": "^2.0.17",
|
||||
"chart.js": "^4.2.0",
|
||||
"dotenv-webpack": "^8.0.1",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-adonis": "^2.1.1",
|
||||
"eslint-plugin-prettier": "^5.0.0-alpha.2",
|
||||
"hot-hook": "^0.4.0",
|
||||
"numeral": "^2.0.6",
|
||||
"pinia": "^2.0.30",
|
||||
"pino-pretty": "^11.2.2",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"prettier": "^3.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"supertest": "^6.3.3",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.1.3",
|
||||
"ts-node-maintained": "^10.9.5",
|
||||
"typescript": "~5.7",
|
||||
"vite": "^6.0.11",
|
||||
"vue": "^3.4.26",
|
||||
"vue-facing-decorator": "^3.0.0",
|
||||
"vue-loader": "^17.0.1",
|
||||
|
@ -75,30 +75,31 @@
|
|||
"xslt3": "^2.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adonisjs/auth": "^9.1.1",
|
||||
"@adonisjs/core": "^6.3.1",
|
||||
"@adonisjs/auth": "^9.2.4",
|
||||
"@adonisjs/core": "^6.17.0",
|
||||
"@adonisjs/cors": "^2.2.1",
|
||||
"@adonisjs/drive": "^2.3.0",
|
||||
"@adonisjs/encore": "^1.0.0",
|
||||
"@adonisjs/inertia": "^1.0.0-7",
|
||||
"@adonisjs/lucid": "^21.1.0",
|
||||
"@adonisjs/drive": "^3.2.0",
|
||||
"@adonisjs/inertia": "^2.1.3",
|
||||
"@adonisjs/lucid": "^21.5.1",
|
||||
"@adonisjs/mail": "^9.2.2",
|
||||
"@adonisjs/redis": "^9.1.0",
|
||||
"@adonisjs/session": "^7.1.1",
|
||||
"@adonisjs/session": "^7.5.0",
|
||||
"@adonisjs/shield": "^8.1.1",
|
||||
"@adonisjs/static": "^1.1.1",
|
||||
"@adonisjs/vite": "^4.0.0",
|
||||
"@eidellev/adonis-stardust": "^3.0.0",
|
||||
"@fontsource/archivo-black": "^5.0.1",
|
||||
"@fontsource/inter": "^5.0.1",
|
||||
"@inertiajs/inertia": "^0.11.1",
|
||||
"@inertiajs/vue3": "^1.0.0",
|
||||
"@opensearch-project/opensearch": "^2.4.0",
|
||||
"@inertiajs/vue3": "^2.0.3",
|
||||
"@opensearch-project/opensearch": "^3.2.0",
|
||||
"@phc/format": "^1.0.0",
|
||||
"@vinejs/vine": "^2.0.0",
|
||||
"@poppinss/manager": "^5.0.2",
|
||||
"@vinejs/vine": "^3.0.0",
|
||||
"axios": "^1.7.9",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"clamscan": "^2.1.2",
|
||||
"crypto": "^1.0.1",
|
||||
"dayjs": "^1.11.7",
|
||||
"deep-email-validator": "^0.1.21",
|
||||
"edge.js": "^6.0.1",
|
||||
|
@ -121,6 +122,12 @@
|
|||
"vuedraggable": "^4.1.0",
|
||||
"xmlbuilder2": "^3.1.1"
|
||||
},
|
||||
"hotHook": {
|
||||
"boundaries": [
|
||||
"./app/Controllers/**/*.ts",
|
||||
"./app/middleware/*.ts"
|
||||
]
|
||||
},
|
||||
"type": "module",
|
||||
"imports": {
|
||||
"#controllers/*": "./app/Controllers/*.js",
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
// 'postcss-import': {},
|
||||
'tailwindcss/nesting': {},
|
||||
// 'postcss-nesting': {},
|
||||
'tailwindcss/nesting': {},
|
||||
// "@tailwindcss/postcss": {},
|
||||
// tailwindcss: {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
|
|
|
@ -74,7 +74,8 @@ export class LocalDriver implements LocalDriverContract {
|
|||
*/
|
||||
public async exists(location: string): Promise<boolean> {
|
||||
try {
|
||||
return await this.adapter.pathExists(this.makePath(location));
|
||||
let path_temp = this.makePath(location); //'/storage/app/files/421'
|
||||
return await this.adapter.pathExists(path_temp);
|
||||
} catch (error) {
|
||||
throw CannotGetMetaDataException.invoke(location, 'exists', error);
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ export default class MailProvider {
|
|||
const mailConfigProvider = this.app.config.get('mail');
|
||||
const config = await configProvider.resolve<any>(this.app, mailConfigProvider);
|
||||
|
||||
const iwas = await config.mailers.smtp();
|
||||
await config.mailers.smtp();
|
||||
// iwas.config.host = 'hhhost';
|
||||
// this.app.config.set('mail.mailers.smtp.host', 'xhost');
|
||||
// const iwas = await config.mailers.smtp();
|
||||
|
|
|
@ -63,6 +63,15 @@ export default class QueryBuilderProvider {
|
|||
|
||||
public register() {
|
||||
// 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() {
|
||||
|
@ -73,15 +82,14 @@ export default class QueryBuilderProvider {
|
|||
// let rolesPluck = {};
|
||||
let rolesPluck: { [key: number]: any } = {};
|
||||
const result = await this.exec();
|
||||
result.forEach((user, index) => {
|
||||
let idc;
|
||||
result.forEach((user: { [key: string]: any }, index: number) => {
|
||||
let idc: number;
|
||||
if (!id) {
|
||||
idc = index;
|
||||
} else {
|
||||
idc = user[id];
|
||||
}
|
||||
const value = user[valueColumn];
|
||||
// rolesPluck[idc] = user.name;
|
||||
const value: any = user[valueColumn];
|
||||
rolesPluck[idc] = value;
|
||||
});
|
||||
return rolesPluck;
|
||||
|
|
|
@ -4,14 +4,15 @@
|
|||
|--------------------------------------------------------------------------
|
||||
|*/
|
||||
import type { ApplicationService } from '@adonisjs/core/types';
|
||||
import vine, { BaseLiteralType, Vine } from '@vinejs/vine';
|
||||
import type { Validation, FieldContext, FieldOptions } from '@vinejs/vine/types';
|
||||
import vine, { symbols, BaseLiteralType, Vine } from '@vinejs/vine';
|
||||
import type { FieldContext, FieldOptions } from '@vinejs/vine/types';
|
||||
// import type { MultipartFile, FileValidationOptions } from '@adonisjs/bodyparser/types';
|
||||
import type { MultipartFile } from '@adonisjs/core/bodyparser';
|
||||
import type { FileValidationOptions } from '@adonisjs/core/types/bodyparser';
|
||||
import { Request, RequestValidator } from '@adonisjs/core/http';
|
||||
import MimeType from '#models/mime_type';
|
||||
|
||||
|
||||
/**
|
||||
* Validation options accepted by the "file" rule
|
||||
*/
|
||||
|
@ -28,8 +29,7 @@ declare module '@vinejs/vine' {
|
|||
* Extend HTTP request class
|
||||
*/
|
||||
declare module '@adonisjs/core/http' {
|
||||
interface Request extends RequestValidator {
|
||||
}
|
||||
interface Request extends RequestValidator {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -48,7 +48,7 @@ export async function getEnabledExtensions() {
|
|||
.flat();
|
||||
|
||||
return extensions;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* VineJS validation rule that validates the file to be an
|
||||
* instance of BodyParser MultipartFile class.
|
||||
|
@ -79,11 +79,13 @@ const isMultipartFile = vine.createRule(async (file: MultipartFile | unknown, op
|
|||
// if (validatedFile.allowedExtensions === undefined && validationOptions.extnames) {
|
||||
// validatedFile.allowedExtensions = validationOptions.extnames;
|
||||
// }
|
||||
if (validatedFile.allowedExtensions === undefined && validationOptions.extnames) {
|
||||
if (validatedFile.allowedExtensions === undefined && validationOptions.extnames !== undefined) {
|
||||
validatedFile.allowedExtensions = validationOptions.extnames; // await getEnabledExtensions();
|
||||
} else if (validatedFile.allowedExtensions === undefined && validationOptions.extnames === undefined) {
|
||||
validatedFile.allowedExtensions = await getEnabledExtensions();
|
||||
}
|
||||
/**
|
||||
* wieder löschen
|
||||
/**
|
||||
* wieder löschen
|
||||
* Set extensions when it's defined in the options and missing
|
||||
* on the file instance
|
||||
*/
|
||||
|
@ -102,7 +104,20 @@ 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> {
|
||||
|
||||
[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;
|
||||
// constructor(validationOptions?: FileRuleValidationOptions, options?: FieldOptions, validations?: Validation<any>[]);
|
||||
// clone(): this;
|
||||
|
@ -111,14 +126,16 @@ 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']
|
||||
// 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, [isMultipartFile(validationOptions || {})]);
|
||||
this.validationOptions = validationOptions;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,10 +169,12 @@ export default class VinejsProvider {
|
|||
* The validate method can be used to validate the request
|
||||
* data for the current request using VineJS validators
|
||||
*/
|
||||
Request.macro('validateUsing', function (...args) {
|
||||
return new RequestValidator(this.ctx).validateUsing(...args);
|
||||
});
|
||||
|
||||
Request.macro('validateUsing', function (this: Request, ...args) {
|
||||
if (!this.ctx) {
|
||||
throw new Error('HttpContext is not available');
|
||||
}
|
||||
return new RequestValidator(this.ctx).validateUsing(...args);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"entrypoints": {
|
||||
"app": {
|
||||
"css": [
|
||||
"http://localhost:8080/assets/app.css"
|
||||
],
|
||||
"js": [
|
||||
"http://localhost:8080/assets/app.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
|
@ -1,19 +1,20 @@
|
|||
/* @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'); */
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import '_checkbox-radio-switch.css';
|
||||
@import '_progress.css';
|
||||
@import '_scrollbars.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/archivo-black/index.css';
|
||||
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
:root {
|
||||
--color-main-background: #ffffff;
|
||||
--color-main-background-rgb: 255,255,255;
|
||||
|
|
BIN
resources/js/Components/.FormField.vue.swo
Normal file
BIN
resources/js/Components/.FormField.vue.swo
Normal file
Binary file not shown.
|
@ -1,162 +1,143 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, ComputedRef } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { Link, usePage } from '@inertiajs/vue3';
|
||||
// import { Link } from '@inertiajs/inertia-vue3';
|
||||
|
||||
import { StyleService } from '@/Stores/style.service';
|
||||
import { mdiMinus, mdiPlus } from '@mdi/js';
|
||||
import { getButtonColor } from '@/colors';
|
||||
import BaseIcon from '@/Components/BaseIcon.vue';
|
||||
// import AsideMenuList from '@/Components/AsideMenuList.vue';
|
||||
import { stardust } from '@eidellev/adonis-stardust/client';
|
||||
import type { User } from '@/Dataset';
|
||||
import { MenuItem } from '@headlessui/vue';
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
parentItem: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
// isDropdownList: Boolean,
|
||||
});
|
||||
interface MenuItem {
|
||||
href?: string;
|
||||
route?: string;
|
||||
icon?: string;
|
||||
label: string;
|
||||
target?: string;
|
||||
color?: string;
|
||||
children?: MenuItem[];
|
||||
isOpen?: boolean;
|
||||
roles?: string[];
|
||||
}
|
||||
|
||||
const user: ComputedRef<User> = computed(() => {
|
||||
return usePage().props.authUser as User;
|
||||
const props = defineProps<{
|
||||
item: MenuItem;
|
||||
parentItem?: MenuItem;
|
||||
// isDropdownList?: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'menu-click', event: Event, item: MenuItem): void;
|
||||
}>();
|
||||
|
||||
// Retrieve authenticated user from page props
|
||||
const user = computed<User>(() => usePage().props.authUser as User);
|
||||
|
||||
// Check if the menu item has children
|
||||
const hasChildren = computed(() => {
|
||||
return Array.isArray(props.item?.children) && props.item.children.length > 0;
|
||||
});
|
||||
|
||||
const itemRoute = computed(() => (props.item && props.item.route ? stardust.route(props.item.route) : ''));
|
||||
// const isCurrentRoute = computed(() => (props.item && props.item.route ? stardust.isCurrent(props.item.route): false));
|
||||
// const itemHref = computed(() => (props.item && props.item.href ? props.item.href : ''));
|
||||
|
||||
const emit = defineEmits(['menu-click']);
|
||||
// Determine which element to render based on 'href' or 'route'
|
||||
const isComponent = computed(() => {
|
||||
if (props.item.href) {
|
||||
return 'a';
|
||||
}
|
||||
if (props.item.route) {
|
||||
return Link;
|
||||
}
|
||||
return 'div';
|
||||
});
|
||||
|
||||
// Check if any child route is active
|
||||
const isChildActive = computed(() => {
|
||||
if (props.item.children && props.item.children.length > 0) {
|
||||
return props.item.children.some(child => child.route && stardust.isCurrent(child.route));
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Automatically use prop item.isOpen if set from the parent,
|
||||
// or if one of its children is active then force open state.
|
||||
const isOpen = computed(() => {
|
||||
return props.item.isOpen || isChildActive.value;
|
||||
});
|
||||
|
||||
|
||||
const styleService = StyleService();
|
||||
|
||||
const hasColor = computed(() => props.item && props.item.color);
|
||||
|
||||
// const isDropdownOpen = ref(false);
|
||||
|
||||
// const isChildSelected = computed(() => {
|
||||
// if (props.item.children && props.item.children.length > 0) {
|
||||
// return children.value.some(childItem => stardust.isCurrent(childItem.route));
|
||||
// }
|
||||
// return false;
|
||||
|
||||
// const children = computed(() => {
|
||||
// return props.item.children || [];
|
||||
// });
|
||||
|
||||
|
||||
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(() => [
|
||||
hasChildren ? 'py-3 px-6 text-sm font-semibold' : 'py-3 px-6',
|
||||
hasColor.value ? getButtonColor(props.item.color, false, true) : styleService.asideMenuItemStyle,
|
||||
]);
|
||||
|
||||
|
||||
|
||||
// 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) => {
|
||||
const menuClick = (event: Event) => {
|
||||
emit('menu-click', event, props.item);
|
||||
|
||||
if (hasChildren.value) {
|
||||
// if (isChildSelected.value == false) {
|
||||
// isDropdownOpen.value = !isDropdownOpen.value;
|
||||
props.item.isOpen = !props.item.isOpen;
|
||||
// }
|
||||
// Toggle open state if the menu has children
|
||||
props.item.isOpen = !props.item.isOpen;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// const handleChildSelected = () => {
|
||||
// isChildSelected.value = true;
|
||||
// };
|
||||
|
||||
|
||||
const activeInactiveStyle = computed(() => {
|
||||
const activeStyle = computed(() => {
|
||||
if (props.item.route && stardust.isCurrent(props.item.route)) {
|
||||
// console.log(props.item.route);
|
||||
return styleService.asideMenuItemActiveStyle;
|
||||
return 'text-sky-600 font-bold';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const is = computed(() => {
|
||||
if (props.item.href) {
|
||||
return 'a';
|
||||
}
|
||||
if (props.item.route) {
|
||||
return Link;
|
||||
}
|
||||
|
||||
return 'div';
|
||||
});
|
||||
|
||||
const hasRoles = computed(() => {
|
||||
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 true
|
||||
});
|
||||
|
||||
// props.routeName && stardust.isCurrent(props.routeName) ? props.activeColor : null
|
||||
</script>
|
||||
|
||||
<!-- :target="props.item.target ?? null" -->
|
||||
<template>
|
||||
<li v-if="hasRoles">
|
||||
<!-- <component :is="itemHref ? 'div' : Link" :href="itemHref ? itemHref : itemRoute" -->
|
||||
<component :is="is" :href="itemRoute ? stardust.route(props.item.route) : props.item.href"
|
||||
class="flex cursor-pointer dark:text-slate-300 dark:hover:text-white menu-item-wrapper" :class="componentClass"
|
||||
@click="menuClick" v-bind:target="props.item.target ?? null">
|
||||
<BaseIcon v-if="item.icon" :path="item.icon" class="flex-none menu-item-icon" :class="activeInactiveStyle"
|
||||
w="w-16" :size="18" />
|
||||
<component :is="isComponent" :href="props.item.href ? props.item.href : itemRoute"
|
||||
class="flex cursor-pointer dark:text-slate-300 dark:hover:text-white menu-item-wrapper"
|
||||
:class="componentClass" @click="menuClick" :target="props.item.target || null">
|
||||
<BaseIcon v-if="props.item.icon" :path="props.item.icon" class="flex-none menu-item-icon"
|
||||
:class="activeStyle" w="w-16" :size="18" />
|
||||
<div class="menu-item-label">
|
||||
<span class="grow text-ellipsis line-clamp-1" :class="activeInactiveStyle">
|
||||
{{ item.label }}
|
||||
<span class="grow text-ellipsis line-clamp-1" :class="[activeStyle]">
|
||||
{{ props.item.label }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- plus icon for expanding sub menu -->
|
||||
<BaseIcon v-if="hasChildren" :path="props.item.isOpen ? mdiMinus : mdiPlus" class="flex-none"
|
||||
:class="[activeInactiveStyle]" w="w-12" />
|
||||
<!-- Display plus or minus icon if there are child items -->
|
||||
<BaseIcon v-if="hasChildren" :path="isOpen ? mdiMinus : mdiPlus" class="flex-none"
|
||||
:class="[activeStyle]" w="w-12" />
|
||||
</component>
|
||||
<!-- Render dropdown -->
|
||||
<div class="menu-item-dropdown"
|
||||
:class="[styleService.asideMenuDropdownStyle, props.item.isOpen ? 'block dark:bg-slate-800/50' : 'hidden']"
|
||||
v-if="hasChildren">
|
||||
:class="[styleService.asideMenuDropdownStyle, isOpen ? 'block dark:bg-slate-800/50' : 'hidden']"
|
||||
v-if="props.item.children && props.item.children.length > 0">
|
||||
<ul>
|
||||
<!-- <li v-for="( child, index ) in children " :key="index">
|
||||
<AsideMenuItem :item="child" :key="index"> </AsideMenuItem>
|
||||
</li> -->
|
||||
<AsideMenuItem v-for="(childItem, index) in children" :key="index" :item="childItem" />
|
||||
|
||||
<AsideMenuItem v-for="(childItem, index) in (props.item.children as any[])" :key="index" :item="childItem"
|
||||
@menu-click="$emit('menu-click', $event, childItem)" />
|
||||
</ul>
|
||||
</div>
|
||||
<!-- <AsideMenuList v-if="hasChildren" :items="item.children"
|
||||
:class="[styleService.asideMenuDropdownStyle, isDropdownOpen ? 'block dark:bg-slate-800/50' : 'hidden']" /> -->
|
||||
|
||||
|
||||
</li>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
@ -167,17 +148,12 @@ const hasRoles = computed(() => {
|
|||
}
|
||||
|
||||
.menu-item-icon {
|
||||
font-size: 2.5rem;
|
||||
/* margin-right: 10px; */
|
||||
font-size: 2.5rem;
|
||||
/* margin-right: 10px; */
|
||||
}
|
||||
|
||||
/* .menu-item-label {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
} */
|
||||
|
||||
.menu-item-dropdown {
|
||||
/* margin-left: 10px; */
|
||||
padding-left: 0.75rem;
|
||||
/* margin-left: 10px; */
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -36,13 +36,24 @@ const logoutItemClick = async () => {
|
|||
await router.post(stardust.route('logout'));
|
||||
};
|
||||
|
||||
const menuClick = (event, item) => {
|
||||
interface MenuItem {
|
||||
name: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
const menuClick = (event: Event, item: MenuItem) => {
|
||||
emit('menu-click', event, item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside id="aside" class="lg:py-2 lg:pl-2 w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden">
|
||||
<aside
|
||||
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.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">
|
||||
|
|
|
@ -12,6 +12,10 @@ const props = defineProps({
|
|||
type: String,
|
||||
default: null,
|
||||
},
|
||||
showHeaderIcon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
headerIcon: {
|
||||
type: String,
|
||||
default: null,
|
||||
|
@ -63,7 +67,7 @@ const submit = (e) => {
|
|||
<BaseIcon v-if="icon" :path="icon" class="mr-3" />
|
||||
{{ title }}
|
||||
</div>
|
||||
<button class="flex items-center py-3 px-4 justify-center ring-blue-700 focus:ring" @click="headerIconClick">
|
||||
<button v-if="showHeaderIcon" class="flex items-center py-3 px-4 justify-center ring-blue-700 focus:ring" @click="headerIconClick">
|
||||
<BaseIcon :path="computedHeaderIcon" />
|
||||
</button>
|
||||
</header>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script setup>
|
||||
<script lang="ts" setup>
|
||||
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 BaseLevel from '@/Components/BaseLevel.vue';
|
||||
import PillTag from '@/Components/PillTag.vue';
|
||||
|
@ -27,6 +27,10 @@ const props = defineProps({
|
|||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
count: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: null,
|
||||
|
@ -42,11 +46,11 @@ const pillType = computed(() => {
|
|||
return props.type;
|
||||
}
|
||||
|
||||
if (props.progress) {
|
||||
if (props.progress >= 60) {
|
||||
if (props.count) {
|
||||
if (props.count >= 20) {
|
||||
return 'success';
|
||||
}
|
||||
if (props.progress >= 40) {
|
||||
if (props.count >= 5) {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
|
@ -56,17 +60,17 @@ const pillType = computed(() => {
|
|||
return 'info';
|
||||
});
|
||||
|
||||
const pillIcon = computed(() => {
|
||||
return {
|
||||
success: mdiTrendingUp,
|
||||
warning: mdiTrendingNeutral,
|
||||
danger: mdiTrendingDown,
|
||||
info: mdiTrendingNeutral,
|
||||
}[pillType.value];
|
||||
});
|
||||
// const pillIcon = computed(() => {
|
||||
// return {
|
||||
// success: mdiTrendingUp,
|
||||
// warning: mdiTrendingNeutral,
|
||||
// danger: mdiTrendingDown,
|
||||
// info: mdiTrendingNeutral,
|
||||
// }[pillType.value];
|
||||
// });
|
||||
|
||||
const pillText = computed(() => props.text ?? `${props.progress}%`);
|
||||
</script>
|
||||
// const pillText = computed(() => props.text ?? `${props.progress}%`);
|
||||
// </script>
|
||||
|
||||
<template>
|
||||
<CardBox class="mb-6 last:mb-0" hoverable>
|
||||
|
@ -83,7 +87,17 @@ const pillText = computed(() => props.text ?? `${props.progress}%`);
|
|||
</p>
|
||||
</div>
|
||||
</BaseLevel>
|
||||
<PillTag :type="pillType" :text="pillText" small :icon="pillIcon" />
|
||||
<!-- <PillTag :type="pillType" :text="text" small :icon="pillIcon" /> -->
|
||||
|
||||
<div class="text-center md:text-right space-y-2">
|
||||
<p class="text-sm text-gray-500">
|
||||
Count
|
||||
</p>
|
||||
<div>
|
||||
<PillTag :type="pillType" :text="String(count)" small />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</BaseLevel>
|
||||
</CardBox>
|
||||
</template>
|
||||
|
|
107
resources/js/Components/CardBoxDataset.vue
Normal file
107
resources/js/Components/CardBoxDataset.vue
Normal file
|
@ -0,0 +1,107 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, PropType } from 'vue';
|
||||
import { mdiChartTimelineVariant, mdiFileDocumentOutline, mdiFileOutline, mdiDatabase } from '@mdi/js';
|
||||
import CardBox from '@/Components/CardBox.vue';
|
||||
import PillTag from '@/Components/PillTag.vue';
|
||||
import IconRounded from '@/Components/IconRounded.vue';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
// Extend dayjs to support relative times
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
interface Dataset {
|
||||
account_id: number;
|
||||
created_at: string;
|
||||
creating_corporation: string;
|
||||
editor_id: number;
|
||||
embargo_date: string | null;
|
||||
id: number;
|
||||
language: string;
|
||||
main_abstract: string | null;
|
||||
main_title: string | null;
|
||||
preferred_reviewer: string | null;
|
||||
preferred_reviewer_email: string | null;
|
||||
project_id: number | null;
|
||||
publish_id: number;
|
||||
publisher_name: string;
|
||||
reject_editor_note: string | null;
|
||||
reject_reviewer_note: string | null;
|
||||
remaining_time: number;
|
||||
reviewer_id: number;
|
||||
server_date_modified: string;
|
||||
server_date_published: string;
|
||||
server_state: string;
|
||||
type: string;
|
||||
doi_identifier: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
dataset: {
|
||||
type: Object as PropType<Dataset>,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const icon = computed(() => {
|
||||
switch (props.dataset.type) {
|
||||
case 'analysisdata':
|
||||
return { icon: mdiChartTimelineVariant, type: 'success' };
|
||||
case 'measurementdata':
|
||||
return { icon: mdiFileDocumentOutline, type: 'warning' };
|
||||
case 'monitoring':
|
||||
return { icon: mdiFileOutline, type: 'info' };
|
||||
case 'remotesensing':
|
||||
return { icon: mdiDatabase, type: 'primary' };
|
||||
case 'gis':
|
||||
return { icon: mdiDatabase, type: 'info' };
|
||||
case 'models':
|
||||
return { icon: mdiChartTimelineVariant, type: 'success' };
|
||||
case 'mixedtype':
|
||||
return { icon: mdiFileDocumentOutline, type: 'warning' };
|
||||
case 'vocabulary':
|
||||
return { icon: mdiFileOutline, type: 'info' };
|
||||
default:
|
||||
return { icon: mdiDatabase, type: 'secondary' };
|
||||
}
|
||||
});
|
||||
|
||||
const displayTitle = computed(() => props.dataset.main_title || 'Untitled Dataset');
|
||||
|
||||
const doiLink = computed(() => {
|
||||
return `https://doi.tethys.at/10.24341/tethys.${props.dataset.publish_id}`;
|
||||
});
|
||||
|
||||
const relativeDate = computed(() => {
|
||||
const publishedDate = dayjs(props.dataset.server_date_published);
|
||||
if (publishedDate.isValid()) {
|
||||
return publishedDate.fromNow();
|
||||
}
|
||||
return props.dataset.server_date_published;
|
||||
});
|
||||
|
||||
// const displayBusiness = computed(() => props.dataset.publisher_name);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardBox class="mb-6 last:mb-0" hoverable>
|
||||
<div class="flex items-start justify-between">
|
||||
<IconRounded :icon="icon.icon" :type="icon.type" class="mr-6" />
|
||||
<div class="flex-grow space-y-1 text-left" style="width: 70%;">
|
||||
<h4 class="text-lg truncate" >
|
||||
{{ displayTitle }}
|
||||
</h4>
|
||||
<p class="text-gray-500 dark:text-slate-400">
|
||||
<b>
|
||||
<a :href="doiLink" target="_blank">View Publication</a>
|
||||
</b>
|
||||
• {{ relativeDate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<p class="text-sm text-gray-500">{{ props.dataset.type }}</p>
|
||||
<PillTag :type="icon.type" :text="props.dataset.type" small class="inline-flex" />
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
</template>
|
|
@ -198,7 +198,7 @@ import DeleteIcon from '@/Components/Icons/Delete.vue';
|
|||
import RefreshIcon from '@/Components/Icons/Refresh.vue';
|
||||
// import { Page, PageProps, Errors, ErrorBag } from '@inertiajs/inertia';
|
||||
import Draggable from 'vuedraggable';
|
||||
import { Buffer } from 'buffer';
|
||||
// import { Buffer } from 'buffer';
|
||||
import { TethysFile } from '@/Dataset';
|
||||
|
||||
// lastModified: 1691759507591
|
||||
|
@ -445,18 +445,19 @@ class FileUploadComponent extends Vue {
|
|||
let localUrl: string = '';
|
||||
if (file instanceof File) {
|
||||
localUrl = URL.createObjectURL(file as Blob);
|
||||
} else if (file.fileData) {
|
||||
// const blob = new Blob([file.fileData]);
|
||||
// localUrl = URL.createObjectURL(blob);
|
||||
const parsed = JSON.parse(file.fileData);
|
||||
file.fileData = '';
|
||||
// retrieve the original buffer of data
|
||||
const buff = Buffer.from(parsed.blob, 'base64');
|
||||
const blob = new Blob([buff], { type: 'application/octet-stream' });
|
||||
// file.blob = blob;
|
||||
localUrl = URL.createObjectURL(blob);
|
||||
file.fileSrc = localUrl;
|
||||
}
|
||||
}
|
||||
// else if (file.fileData) {
|
||||
// // const blob = new Blob([file.fileData]);
|
||||
// // localUrl = URL.createObjectURL(blob);
|
||||
// const parsed = JSON.parse(file.fileData);
|
||||
// file.fileData = '';
|
||||
// // retrieve the original buffer of data
|
||||
// const buff = Buffer.from(parsed.blob, 'base64');
|
||||
// const blob = new Blob([buff], { type: 'application/octet-stream' });
|
||||
// // file.blob = blob;
|
||||
// localUrl = URL.createObjectURL(blob);
|
||||
// file.fileSrc = localUrl;
|
||||
// }
|
||||
|
||||
// setTimeout(() => {
|
||||
// URL.revokeObjectURL(localUrl);
|
||||
|
|
|
@ -1,10 +1,22 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import FormCheckRadio from '@/Components/FormCheckRadio.vue';
|
||||
import BaseButton from '@/Components/BaseButton.vue';
|
||||
import FormControl from '@/Components/FormControl.vue';
|
||||
import { mdiPlusCircle } from '@mdi/js';
|
||||
const props = defineProps({
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
default: () => { },
|
||||
},
|
||||
allowManualAdding: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
manualAddingPlaceholder: {
|
||||
type: String,
|
||||
default: 'Add manually',
|
||||
required: false,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
|
@ -55,6 +67,29 @@ const computedValue = computed({
|
|||
const hasIdAttribute = (obj: any): obj is { id: any } => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
@ -63,15 +98,17 @@ const hasIdAttribute = (obj: any): obj is { id: any } => {
|
|||
<!-- :label="value" -->
|
||||
<!-- :input-value="value.id"
|
||||
:label="value.name" -->
|
||||
<FormCheckRadio
|
||||
v-for="(value, key) in options"
|
||||
:key="key"
|
||||
v-model="computedValue"
|
||||
:type="type"
|
||||
:name="name"
|
||||
:input-value="key"
|
||||
:label="value"
|
||||
:class="componentClass"
|
||||
/>
|
||||
<div v-if="allowManualAdding && type === 'checkbox'" class="flex items-center mt-2 mb-2 relative">
|
||||
<input v-model="newOption" :placeholder="manualAddingPlaceholder" :class="inputElClass"
|
||||
@keydown.prevent.enter="addOption" />
|
||||
<svg v-show="newOption.length >= 2" @click.prevent="addOption" xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-6 h-6 absolute right-2 top-1/2 transform -translate-y-1/2 cursor-pointer text-gray-500"
|
||||
viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-6H5v-2h6V5h2v6h6v2h-6v6z" />
|
||||
</svg>
|
||||
</div>
|
||||
<FormCheckRadio v-for="(value, key) in options" :key="key" v-model="computedValue" :type="type" :name="name"
|
||||
:input-value="key" :label="value" :class="componentClass" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -118,6 +118,9 @@ if (props.ctrlKFocus) {
|
|||
mainService.isFieldFocusRegistered = false;
|
||||
});
|
||||
}
|
||||
const focus = () => {
|
||||
inputEl?.value.focus();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -130,7 +133,7 @@ if (props.ctrlKFocus) {
|
|||
</option>
|
||||
</select>
|
||||
<textarea v-else-if="computedType === 'textarea'" :id="id" v-model="computedValue" :class="inputElClass"
|
||||
:name="name" :placeholder="placeholder" :required="required" />
|
||||
:name="name" :placeholder="placeholder" :required="required" :readonly="isReadOnly"/>
|
||||
<input v-else :id="id" ref="inputEl" v-model="computedValue" :name="name" :inputmode="inputmode"
|
||||
:autocomplete="autocomplete" :required="required" :placeholder="placeholder" :type="computedType"
|
||||
:class="inputElClass" :readonly="isReadOnly" />
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { computed, useSlots } from 'vue';
|
||||
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
default: null,
|
||||
|
@ -15,6 +15,10 @@ defineProps({
|
|||
type: String,
|
||||
default: null,
|
||||
},
|
||||
// class: {
|
||||
// type: Object,
|
||||
// default: {},
|
||||
// },
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
|
@ -36,7 +40,7 @@ const wrapperClass = computed(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-6 last:mb-0">
|
||||
<div :class="['last:mb-0', 'mb-6']">
|
||||
<!-- <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>
|
||||
<div v-bind:class="wrapperClass">
|
||||
|
|
|
@ -268,7 +268,7 @@ export default class DrawControlComponent extends Vue {
|
|||
position: absolute;
|
||||
left: 10px;
|
||||
top: 100px;
|
||||
z-index: 999;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.btn-group-vertical button {
|
||||
|
|
|
@ -372,6 +372,9 @@ export default class MapComponent extends Vue {
|
|||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.leaflet-container .leaflet-pane {
|
||||
z-index: 30!important;
|
||||
}
|
||||
/* .leaflet-pane {
|
||||
z-index: 30;
|
||||
} */
|
||||
|
|
|
@ -100,7 +100,7 @@ export default class ZoomControlComponent extends Vue {
|
|||
position: absolute;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
z-index: 999;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.btn-group-vertical button {
|
||||
|
|
138
resources/js/Components/MimetypeInput.vue
Normal file
138
resources/js/Components/MimetypeInput.vue
Normal file
|
@ -0,0 +1,138 @@
|
|||
<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>
|
|
@ -72,7 +72,7 @@ const menuNavBarToggle = () => {
|
|||
const menuOpenLg = () => {
|
||||
layoutStore.isAsideLgActive = true;
|
||||
};
|
||||
const userHasRoles = (roleNames): boolean => {
|
||||
const userHasRoles = (roleNames: Array<string>): boolean => {
|
||||
return user.value.roles.some(role => roleNames.includes(role.name));
|
||||
};
|
||||
|
||||
|
@ -95,7 +95,7 @@ const showAbout = async () => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<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"
|
||||
<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"
|
||||
:class="{ 'xl:pl-60': props.showBurger == true }">
|
||||
<FirstrunWizard ref="about"></FirstrunWizard>
|
||||
<div class="flex lg:items-stretch" :class="containerMaxW">
|
||||
|
@ -122,10 +122,10 @@ const showAbout = async () => {
|
|||
<BaseIcon :path="isMenuNavBarActive ? mdiClose : mdiDotsVertical" size="24" />
|
||||
</NavBarItem>
|
||||
</div>
|
||||
<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"
|
||||
<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"
|
||||
:class="[isMenuNavBarActive ? 'block' : 'hidden']">
|
||||
<div
|
||||
class="max-h-screen-menu overflow-y-auto lg:overflow-visible lg:flex lg:items-stretch lg:justify-end lg:ml-auto">
|
||||
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">
|
||||
|
||||
<!-- help menu -->
|
||||
<NavBarMenu>
|
||||
|
@ -150,7 +150,7 @@ const showAbout = async () => {
|
|||
<!-- personal menu -->
|
||||
<NavBarMenu>
|
||||
<NavBarItemLabel v-bind:label="`hello ${user.login}`">
|
||||
<UserAvatarCurrentUser class="w-6 h-6 mr-3 inline-flex" />
|
||||
<UserAvatarCurrentUser :user="user" class="w-6 h-6 mr-3 inline-flex" />
|
||||
</NavBarItemLabel>
|
||||
<template #dropdown>
|
||||
<!-- <NavBarItem> -->
|
||||
|
@ -186,7 +186,7 @@ const showAbout = async () => {
|
|||
<NavBarItem is-desktop-icon-only @click.prevent="toggleLightDark">
|
||||
<NavBarItemLabel v-bind:icon="mdiThemeLightDark" label="Light/Dark" is-desktop-icon-only />
|
||||
</NavBarItem>
|
||||
<NavBarItem href="https://gitea.geologie.ac.at/geolba/tethys" target="_blank" is-desktop-icon-only>
|
||||
<NavBarItem href="https://gitea.geosphere.at/geolba/tethys.backend" target="_blank" is-desktop-icon-only>
|
||||
<NavBarItemLabel v-bind:icon="mdiGithub" label="GitHub" is-desktop-icon-only />
|
||||
</NavBarItem>
|
||||
<NavBarItem is-desktop-icon-only @click="showAbout">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<script setup>
|
||||
<script lang="ts" setup>
|
||||
import { StyleService } from '@/Stores/style.service';
|
||||
import { computed, ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js';
|
||||
|
@ -15,10 +15,10 @@ const toggle = () => {
|
|||
isDropdownActive.value = !isDropdownActive.value;
|
||||
};
|
||||
|
||||
const root = ref(null);
|
||||
const root = ref(NavBarItem);
|
||||
|
||||
const forceClose = (event) => {
|
||||
if (!root.value.$el.contains(event.target)) {
|
||||
const forceClose = (event: MouseEvent) => {
|
||||
if (!root.value?.$el.contains(event.target)) {
|
||||
isDropdownActive.value = false;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -5,9 +5,9 @@ import SectionBanner from '@/Components/SectionBanner.vue';
|
|||
</script>
|
||||
<template>
|
||||
<SectionBanner bg="greenBlue">
|
||||
<h1 class="text-3xl text-white mb-6">Like the project? Please star on <b>Gitea</b>!</h1>
|
||||
<h1 class="text-3xl text-white mb-6">Like the project? Please star on <b>GeoSphere Git Repository</b>!</h1>
|
||||
<div>
|
||||
<BaseButton href="https://gitea.geologie.ac.at/geolba/tethys" :icon="mdiGithub" label="Gitea" target="_blank" rounded-full />
|
||||
<BaseButton href="https://gitea.geosphere.at/geolba/tethys.backend" :icon="mdiGithub" label="Forgejo" target="_blank" rounded-full />
|
||||
</div>
|
||||
</SectionBanner>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { checkStrength } from './logic/index';
|
||||
import { mdiFormTextboxPassword } from '@mdi/js';
|
||||
import FormField from '@/Components/FormField.vue';
|
||||
|
@ -7,17 +7,25 @@ import FormControl from '@/Components/FormControl.vue';
|
|||
|
||||
// Define props
|
||||
const props = defineProps<{
|
||||
password: string;
|
||||
modelValue: string;
|
||||
errors: Partial<Record<"new_password" | "old_password" | "confirm_password", string>>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['update:password', 'score']);
|
||||
const emit = defineEmits(['update:modelValue', 'score']);
|
||||
|
||||
// A local reactive variable for password input
|
||||
const localPassword = ref(props.password);
|
||||
// Watch localPassword and emit changes back to the parent
|
||||
watch(localPassword, (newValue) => {
|
||||
emit('update:password', newValue);
|
||||
// // A local reactive variable for password input
|
||||
// const localPassword = ref(props.modelValue);
|
||||
// // Watch localPassword and emit changes back to the parent
|
||||
// watch(localPassword, (newValue) => {
|
||||
// emit('update:modelValue', newValue);
|
||||
// });
|
||||
const localPassword = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value);
|
||||
// const { score } = checkStrength(localPassword.value);
|
||||
// emit('score', score);
|
||||
},
|
||||
});
|
||||
|
||||
type PasswordMetrics = {
|
||||
|
@ -53,7 +61,7 @@ const passwordMetrics = computed<PasswordMetrics>(() => {
|
|||
|
||||
<template>
|
||||
<!-- 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
|
||||
:error="errors.new_password">
|
||||
<!-- Secure Icon -->
|
||||
|
@ -72,6 +80,10 @@ const passwordMetrics = computed<PasswordMetrics>(() => {
|
|||
{{ errors.new_password }}
|
||||
</div>
|
||||
</FormControl>
|
||||
<!-- Score Display -->
|
||||
<div class="text-gray-700 text-sm">
|
||||
{{ passwordMetrics.score }} / 6 points max
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<!-- Password Strength Bar -->
|
||||
|
@ -93,9 +105,9 @@ const passwordMetrics = computed<PasswordMetrics>(() => {
|
|||
</ul>
|
||||
</div>
|
||||
<!-- Score Display -->
|
||||
<div class="text-gray-700 text-sm">
|
||||
<!-- <div class="text-gray-700 text-sm">
|
||||
{{ passwordMetrics.score }} / 6 points max
|
||||
</div>
|
||||
</div> -->
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, Ref } from 'vue';
|
||||
import { MainService } from '@/Stores/main';
|
||||
import { StyleService } from '@/Stores/style.service';
|
||||
import { mdiEye, mdiTrashCan } from '@mdi/js';
|
||||
import { mdiEye } from '@mdi/js';
|
||||
import CardBoxModal from '@/Components/CardBoxModal.vue';
|
||||
import TableCheckboxCell from '@/Components/TableCheckboxCell.vue';
|
||||
import BaseLevel from '@/Components/BaseLevel.vue';
|
||||
import BaseButtons from '@/Components/BaseButtons.vue';
|
||||
import BaseButton from '@/Components/BaseButton.vue';
|
||||
import UserAvatar from '@/Components/UserAvatar.vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { User } from '@/Stores/main';
|
||||
|
||||
defineProps({
|
||||
checkable: Boolean,
|
||||
checkable: Boolean,
|
||||
});
|
||||
|
||||
const styleService = StyleService();
|
||||
|
@ -19,128 +21,124 @@ const mainService = MainService();
|
|||
const items = computed(() => mainService.clients);
|
||||
|
||||
const isModalActive = ref(false);
|
||||
const isModalDangerActive = ref(false);
|
||||
// const isModalDangerActive = ref(false);
|
||||
const perPage = ref(5);
|
||||
const currentPage = ref(0);
|
||||
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 numPages = computed(() => Math.ceil(items.value.length / perPage.value));
|
||||
|
||||
const currentPageHuman = computed(() => currentPage.value + 1);
|
||||
|
||||
const pagesList = computed(() => {
|
||||
const pagesList = [];
|
||||
|
||||
for (let i = 0; i < numPages.value; i++) {
|
||||
pagesList.push(i);
|
||||
}
|
||||
|
||||
return pagesList;
|
||||
const pagesList = [];
|
||||
for (let i = 0; i < numPages.value; i++) {
|
||||
pagesList.push(i);
|
||||
}
|
||||
return pagesList;
|
||||
});
|
||||
|
||||
const remove = (arr, cb) => {
|
||||
const newArr = [];
|
||||
|
||||
arr.forEach((item) => {
|
||||
if (!cb(item)) {
|
||||
newArr.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return newArr;
|
||||
const newArr = [];
|
||||
arr.forEach((item) => {
|
||||
if (!cb(item)) {
|
||||
newArr.push(item);
|
||||
}
|
||||
});
|
||||
return newArr;
|
||||
};
|
||||
|
||||
const checked = (isChecked, client) => {
|
||||
if (isChecked) {
|
||||
checkedRows.value.push(client);
|
||||
} else {
|
||||
checkedRows.value = remove(checkedRows.value, (row) => row.id === client.id);
|
||||
}
|
||||
if (isChecked) {
|
||||
checkedRows.value.push(client);
|
||||
} else {
|
||||
checkedRows.value = remove(checkedRows.value, (row) => row.id === client.id);
|
||||
}
|
||||
};
|
||||
|
||||
const showModal = (client: User) => {
|
||||
currentClient.value = client;
|
||||
isModalActive.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardBoxModal v-model="isModalActive" title="Sample modal">
|
||||
<p>Lorem ipsum dolor sit amet <b>adipiscing elit</b></p>
|
||||
<p>This is sample modal</p>
|
||||
</CardBoxModal>
|
||||
|
||||
<CardBoxModal v-model="isModalDangerActive" large-title="Please confirm" button="danger" has-cancel>
|
||||
<p>Lorem ipsum dolor sit amet <b>adipiscing elit</b></p>
|
||||
<p>This is sample modal</p>
|
||||
</CardBoxModal>
|
||||
|
||||
<div v-if="checkedRows.length" class="p-3 bg-gray-100/50 dark:bg-slate-800">
|
||||
<span
|
||||
v-for="checkedRow in checkedRows"
|
||||
:key="checkedRow.id"
|
||||
class="inline-block px-2 py-1 rounded-sm mr-2 text-sm bg-gray-100 dark:bg-slate-700"
|
||||
>
|
||||
{{ checkedRow.name }}
|
||||
</span>
|
||||
<CardBoxModal v-model="isModalActive" :title="currentClient ? currentClient.login : ''">
|
||||
<div v-if="currentClient">
|
||||
<p>Login: {{ currentClient.login }}</p>
|
||||
<p>Email: {{ currentClient.email }}</p>
|
||||
<p>Created: {{ currentClient?.created_at ? dayjs(currentClient.created_at).format('MMM D, YYYY h:mm A') : 'N/A' }}
|
||||
</p>
|
||||
</div>
|
||||
</CardBoxModal>
|
||||
<!-- <CardBoxModal v-model="isModalDangerActive" large-title="Please confirm" button="danger" has-cancel>
|
||||
<p>Lorem ipsum dolor sit amet <b>adipiscing elit</b></p>
|
||||
<p>This is sample modal</p>
|
||||
</CardBoxModal> -->
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-if="checkable" />
|
||||
<th />
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>City</th>
|
||||
<th>Progress</th>
|
||||
<th>Created</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="client in itemsPaginated" :key="client.id">
|
||||
<TableCheckboxCell v-if="checkable" @checked="checked($event, client)" />
|
||||
<td class="border-b-0 lg:w-6 before:hidden">
|
||||
<UserAvatar :username="client.name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
|
||||
</td>
|
||||
<td data-label="Name">
|
||||
{{ client.name }}
|
||||
</td>
|
||||
<td data-label="Email">
|
||||
{{ client.email }}
|
||||
</td>
|
||||
<td data-label="City">
|
||||
{{ client.city }}
|
||||
</td>
|
||||
<td data-label="Progress" class="lg:w-32">
|
||||
<progress class="flex w-2/5 self-center lg:w-full" max="100" v-bind:value="client.progress">
|
||||
{{ client.progress }}
|
||||
</progress>
|
||||
</td>
|
||||
<td data-label="Created" class="lg:w-1 whitespace-nowrap">
|
||||
<small class="text-gray-500 dark:text-slate-400" :title="client.created">{{ client.created }}</small>
|
||||
</td>
|
||||
<td class="before:hidden lg:w-1 whitespace-nowrap">
|
||||
<BaseButtons type="justify-start lg:justify-end" no-wrap>
|
||||
<BaseButton color="info" :icon="mdiEye" small @click="isModalActive = true" />
|
||||
<BaseButton color="danger" :icon="mdiTrashCan" small @click="isModalDangerActive = true" />
|
||||
</BaseButtons>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="p-3 lg:px-6 border-t border-gray-100 dark:border-slate-800">
|
||||
<BaseLevel>
|
||||
<BaseButtons>
|
||||
<BaseButton
|
||||
v-for="page in pagesList"
|
||||
:key="page"
|
||||
:active="page === currentPage"
|
||||
:label="page + 1"
|
||||
small
|
||||
:outline="styleService.darkMode"
|
||||
@click="currentPage = page"
|
||||
/>
|
||||
</BaseButtons>
|
||||
<small>Page {{ currentPageHuman }} of {{ numPages }}</small>
|
||||
</BaseLevel>
|
||||
</div>
|
||||
</template>
|
||||
<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.login }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-if="checkable" />
|
||||
<th />
|
||||
<th>Login</th>
|
||||
<th>Email</th>
|
||||
<th>Created</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="client in itemsPaginated" :key="client.id">
|
||||
<TableCheckboxCell v-if="checkable" @checked="checked($event, client)" />
|
||||
<td class="border-b-0 lg:w-6 before:hidden">
|
||||
<!-- <UserAvatar :username="client.login" :avatar="client.avatar" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" /> -->
|
||||
<div v-if="client.avatar">
|
||||
<UserAvatar :default-url="client.avatar ? '/public' + client.avatar : ''"
|
||||
:username="client.first_name + ' ' + client.last_name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
|
||||
</div>
|
||||
|
||||
|
||||
<div v-else>
|
||||
<UserAvatar :username="client.first_name + ' ' + client.last_name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="Login">
|
||||
{{ client.login }}
|
||||
</td>
|
||||
<td data-label="Email">
|
||||
{{ client.email }}
|
||||
</td>
|
||||
<td data-label="Created">
|
||||
<small class="text-gray-500 dark:text-slate-400"
|
||||
:title="client.created_at ? dayjs(client.created_at).format('MMM D, YYYY h:mm A') : 'N/A'">
|
||||
{{ client.created_at ? dayjs(client.created_at).format('MMM D, YYYY h:mm A') : 'N/A' }}
|
||||
</small>
|
||||
</td>
|
||||
<td class="before:hidden lg:w-1 whitespace-nowrap">
|
||||
<BaseButtons type="justify-start lg:justify-end" no-wrap>
|
||||
<BaseButton color="info" :icon="mdiEye" small @click="showModal(client)" />
|
||||
<!-- <BaseButton color="danger" :icon="mdiTrashCan" small @click="isModalDangerActive = true" /> -->
|
||||
</BaseButtons>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="p-3 lg:px-6 border-t border-gray-100 dark:border-slate-800">
|
||||
<BaseLevel>
|
||||
<BaseButtons>
|
||||
<BaseButton v-for="page in pagesList" :key="page" :active="page === currentPage" :label="page + 1" small
|
||||
:outline="styleService.darkMode" @click="currentPage = page" />
|
||||
</BaseButtons>
|
||||
<small>Page {{ currentPageHuman }} of {{ numPages }}</small>
|
||||
</BaseLevel>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<script setup>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
|
@ -6,9 +6,9 @@ const props = defineProps({
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
avatar: {
|
||||
defaultUrl: {
|
||||
type: String,
|
||||
default: null,
|
||||
required: false
|
||||
},
|
||||
api: {
|
||||
type: String,
|
||||
|
@ -16,93 +16,63 @@ const props = defineProps({
|
|||
},
|
||||
});
|
||||
|
||||
const avatar = computed(
|
||||
// () => 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 avatar = computed(() => {
|
||||
return props.defaultUrl ?? generateAvatarUrl(props.username);
|
||||
});
|
||||
|
||||
const username = computed(() => props.username);
|
||||
|
||||
const darkenColor = (color) => {
|
||||
// Convert hex to RGB
|
||||
const r = parseInt(color.slice(0, 2), 16);
|
||||
const g = parseInt(color.slice(2, 4), 16);
|
||||
const b = parseInt(color.slice(4, 6), 16);
|
||||
// const darkenColor = (color: string) => {
|
||||
// const r = parseInt(color.slice(0, 2), 16);
|
||||
// const g = parseInt(color.slice(2, 4), 16);
|
||||
// const b = parseInt(color.slice(4, 6), 16);
|
||||
|
||||
// Calculate darker color by reducing 20% of each RGB component
|
||||
const darkerR = Math.round(r * 0.6);
|
||||
const darkerG = Math.round(g * 0.6);
|
||||
const darkerB = Math.round(b * 0.6);
|
||||
// const darkerR = Math.round(r * 0.6);
|
||||
// const darkerG = Math.round(g * 0.6);
|
||||
// const darkerB = Math.round(b * 0.6);
|
||||
|
||||
// Convert back to hex
|
||||
const darkerColor = ((darkerR << 16) + (darkerG << 8) + darkerB).toString(16);
|
||||
// const darkerColor = ((darkerR << 16) + (darkerG << 8) + darkerB).toString(16);
|
||||
|
||||
return darkerColor.padStart(6, '0'); // Ensure it's 6 digits
|
||||
};
|
||||
// return darkerColor.padStart(6, '0');
|
||||
// };
|
||||
|
||||
const getRandomColor = () => {
|
||||
return Math.floor(Math.random() * 16777215).toString(16);
|
||||
};
|
||||
// const getColorFromName = (name: string): string => {
|
||||
// let hash = 0;
|
||||
// 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 adjustOpacity = (hexColor, opacity) => {
|
||||
// Remove # if present
|
||||
hexColor = hexColor.replace('#', '');
|
||||
// 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);
|
||||
// const lightenColor = (hexColor: string, percent: number): string => {
|
||||
// let r = parseInt(hexColor.substring(0, 2), 16);
|
||||
// let g = parseInt(hexColor.substring(2, 4), 16);
|
||||
// let b = parseInt(hexColor.substring(4, 6), 16);
|
||||
|
||||
// const r = parseInt(hexColor.slice(1, 3), 16);
|
||||
// const g = parseInt(hexColor.slice(3, 5), 16);
|
||||
// const b = parseInt(hexColor.slice(5, 7), 16);
|
||||
const [r, g, b] = hexColor.match(/\w\w/g).map(x => parseInt(x, 16));
|
||||
// r = Math.floor(r * (100 + percent) / 100);
|
||||
// g = Math.floor(g * (100 + percent) / 100);
|
||||
// b = Math.floor(b * (100 + percent) / 100);
|
||||
|
||||
return `rgba(${r},${g},${b},${opacity})`;
|
||||
};
|
||||
// r = (r < 255) ? r : 255;
|
||||
// g = (g < 255) ? g : 255;
|
||||
// b = (b < 255) ? b : 255;
|
||||
|
||||
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);
|
||||
// const lighterHex = ((r << 16) | (g << 8) | b).toString(16);
|
||||
|
||||
r = Math.floor(r * (100 + percent) / 100);
|
||||
g = Math.floor(g * (100 + percent) / 100);
|
||||
b = Math.floor(b * (100 + percent) / 100);
|
||||
// return lighterHex.padStart(6, '0');
|
||||
// };
|
||||
|
||||
r = (r < 255) ? r : 255;
|
||||
g = (g < 255) ? g : 255;
|
||||
b = (b < 255) ? b : 255;
|
||||
const generateAvatarUrl = (name: string): string => {
|
||||
// const originalColor = getColorFromName(name);
|
||||
// const backgroundColor = lightenColor(originalColor, 60);
|
||||
// const textColor = darkenColor(originalColor);
|
||||
|
||||
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}`;
|
||||
const avatarUrl = `/api/avatar?name=${name}&size=50`;
|
||||
return avatarUrl;
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,12 +1,29 @@
|
|||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
// import { usePage } from '@inertiajs/vue3'
|
||||
import { usePage } from '@inertiajs/vue3';
|
||||
// import { computed } from 'vue';
|
||||
// import { usePage } from '@inertiajs/vue3';
|
||||
import UserAvatar from '@/Components/UserAvatar.vue';
|
||||
|
||||
const userName = computed(() => usePage().props.auth?.user.name);
|
||||
defineProps({
|
||||
user: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<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 v-else>
|
||||
<UserAvatar :username="user.first_name + ' ' + user.last_name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
|
||||
|
||||
</template> -->
|
||||
|
|
30
resources/js/Components/action-message.vue
Normal file
30
resources/js/Components/action-message.vue
Normal file
|
@ -0,0 +1,30 @@
|
|||
<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>
|
79
resources/js/Components/avatar-input.vue
Normal file
79
resources/js/Components/avatar-input.vue
Normal file
|
@ -0,0 +1,79 @@
|
|||
<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>
|
|
@ -1,43 +1,42 @@
|
|||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { MainService } from '@/Stores/main'
|
||||
import { mdiCheckDecagram } from '@mdi/js'
|
||||
import BaseLevel from '@/Components/BaseLevel.vue'
|
||||
import UserAvatarCurrentUser from '@/Components/UserAvatarCurrentUser.vue'
|
||||
import CardBox from '@/Components/CardBox.vue'
|
||||
import FormCheckRadioGroup from '@/Components/FormCheckRadioGroup.vue'
|
||||
import PillTag from '@/Components/PillTag.vue'
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { MainService } from '@/Stores/main';
|
||||
import { mdiCheckDecagram } from '@mdi/js';
|
||||
import BaseLevel from '@/Components/BaseLevel.vue';
|
||||
import UserAvatarCurrentUser from '@/Components/UserAvatarCurrentUser.vue';
|
||||
import CardBox from '@/Components/CardBox.vue';
|
||||
import FormCheckRadioGroup from '@/Components/FormCheckRadioGroup.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 userSwitchVal = ref([])
|
||||
const user: ComputedRef<User> = computed(() => {
|
||||
return usePage().props.authUser as User;
|
||||
});
|
||||
|
||||
const userSwitchVal = ref([]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardBox>
|
||||
<BaseLevel type="justify-around lg:justify-center">
|
||||
<UserAvatarCurrentUser class="lg:mx-12" />
|
||||
<UserAvatarCurrentUser :user="user" class="lg:mx-12" />
|
||||
<div class="space-y-3 text-center md:text-left lg:mx-12">
|
||||
<div class="flex justify-center md:block">
|
||||
<FormCheckRadioGroup
|
||||
v-model="userSwitchVal"
|
||||
name="sample-switch"
|
||||
type="switch"
|
||||
:options="{ one: 'Notifications' }"
|
||||
/>
|
||||
<FormCheckRadioGroup v-model="userSwitchVal" name="sample-switch" type="switch"
|
||||
:options="{ one: 'Notifications' }" />
|
||||
</div>
|
||||
<h1 class="text-2xl">
|
||||
Howdy, <b>{{ userName }}</b>!
|
||||
</h1>
|
||||
<p>Last login <b>12 mins ago</b> from <b>127.0.0.1</b></p>
|
||||
<div class="flex justify-center md:block">
|
||||
<PillTag
|
||||
text="Verified"
|
||||
type="info"
|
||||
:icon="mdiCheckDecagram"
|
||||
/>
|
||||
<PillTag text="Verified" type="info" :icon="mdiCheckDecagram" />
|
||||
</div>
|
||||
</div>
|
||||
</BaseLevel>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts" setup>
|
||||
import { Head, usePage } from '@inertiajs/vue3';
|
||||
import { usePage } from '@inertiajs/vue3';
|
||||
import { mdiAccountKey, mdiSquareEditOutline, mdiAlertBoxOutline } from '@mdi/js';
|
||||
import { computed, ComputedRef } from 'vue';
|
||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref, watch, computed, Ref, reactive } from 'vue';
|
||||
import { ref, reactive } from 'vue';
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import { mdiAccountKey, mdiArrowLeftBoldOutline } from '@mdi/js';
|
||||
import { mdiAccountKey, mdiArrowLeftBoldOutline, mdiTrashCan, mdiImageText, mdiPlus } from '@mdi/js';
|
||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||
import SectionMain from '@/Components/SectionMain.vue';
|
||||
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
|
||||
|
@ -10,115 +10,97 @@ import BaseDivider from '@/Components/BaseDivider.vue';
|
|||
import BaseButton from '@/Components/BaseButton.vue';
|
||||
import BaseButtons from '@/Components/BaseButtons.vue';
|
||||
import { stardust } from '@eidellev/adonis-stardust/client';
|
||||
import mime from 'mime';
|
||||
import FormField from '@/Components/FormField.vue';
|
||||
import FormControl from '@/Components/FormControl.vue';
|
||||
import standardTypes from 'mime/types/standard.js';
|
||||
import otherTypes from 'mime/types/other.js';
|
||||
import FormCheckRadioGroup from '@/Components/FormCheckRadioGroup.vue';
|
||||
import MimetypeInput from '@/Components/MimetypeInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
permissions: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
defineProps({
|
||||
borderless: Boolean,
|
||||
transparent: 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 standardMimeTypes = Object.keys(standardTypes);
|
||||
const otherMimeTypes = Object.keys(otherTypes);
|
||||
const mimeTypes = [...standardMimeTypes, ...otherMimeTypes];
|
||||
// const standardMimeTypes = Object.keys(standardTypes);
|
||||
// const otherMimeTypes = Object.keys(otherTypes);
|
||||
// const customMimeTypes = Object.keys(customTypes);
|
||||
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>>({});
|
||||
interface FormData {
|
||||
name: string;
|
||||
file_extension: string[];
|
||||
alternate_mimetype: string[];
|
||||
enabled: boolean;
|
||||
[key: string]: string | string[] | boolean;
|
||||
}
|
||||
const form = useForm<FormData>({
|
||||
name: '',
|
||||
file_extension: [],
|
||||
alternate_mimetype: [],
|
||||
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);
|
||||
|
||||
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 addAlternateMimetype = () => {
|
||||
form.alternate_mimetype.push("");
|
||||
};
|
||||
const removeAliasMimetype = (index: number) => {
|
||||
form.alternate_mimetype.splice(index, 1);
|
||||
};
|
||||
|
||||
// Function to reset the object
|
||||
function resetFileExtensions() {
|
||||
// Reset to an empty object
|
||||
Object.keys(file_extensions).forEach(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 => {
|
||||
let extensions = mime.getExtension(mimeType)
|
||||
let extensions = mimeTypesMap.get(mimeType) || null;
|
||||
return extensions !== null;
|
||||
};
|
||||
|
||||
async function handleInputChange(e: Event) {
|
||||
const target = <HTMLInputElement>e.target;
|
||||
newExtension.value = target.value;
|
||||
const clearInput = () => {
|
||||
// newExtension.value = '';
|
||||
// showDropdown.value = false;
|
||||
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) => {
|
||||
form.name = mimeType;
|
||||
// file_extensions.values = [];
|
||||
resetFileExtensions();
|
||||
showDropdown.value = false;
|
||||
newExtension.value = ''; // Reset the input
|
||||
selectedIndex.value = -1;
|
||||
// showDropdown.value = false;
|
||||
// newExtension.value = '';
|
||||
// selectedIndex.value = -1;
|
||||
|
||||
if (form.name && isValidMimeType(form.name)) {
|
||||
const extensions = mime.getAllExtensions(form.name) as Set<string>;
|
||||
// Iterate over each extension and set both key and value to the extension
|
||||
Array.from(extensions).forEach(extension => {
|
||||
// const extensions = mime.getAllExtensions(form.name) as Set<string>;
|
||||
const extensions = mimeTypesMap.get(mimeType);
|
||||
extensions?.forEach(extension => {
|
||||
file_extensions[extension] = extension;
|
||||
});
|
||||
} else {
|
||||
|
@ -126,61 +108,14 @@ 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 () => {
|
||||
if (isValidForm()) {
|
||||
await form.post(stardust.route('settings.mimetype.store'), {
|
||||
preserveScroll: true,
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
// Form validation before submission
|
||||
const isValidForm = (): boolean => {
|
||||
if (!form.name) {
|
||||
form.errors.name = 'Name is required.';
|
||||
|
@ -190,6 +125,7 @@ const isValidForm = (): boolean => {
|
|||
}
|
||||
if (!form.file_extension.length) {
|
||||
form.errors.file_extension = 'At least one file extension is required.';
|
||||
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
@ -205,59 +141,15 @@ const isValidForm = (): boolean => {
|
|||
<BaseButton :route-name="stardust.route('settings.mimetype.index')" :icon="mdiArrowLeftBoldOutline"
|
||||
label="Back" color="white" rounded-full small />
|
||||
</SectionTitleLineWithButton>
|
||||
<!-- <CardBox form @submit.prevent="form.post(stardust.route('role.store'))"> -->
|
||||
<CardBox form @submit.prevent="submit()">
|
||||
|
||||
<!-- 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>
|
||||
<CardBox form>
|
||||
<MimetypeInput @on-select-result="selectResult" @on-clear-input="clearInput" :transparent="transparent"
|
||||
:borderless="borderless" :mimeTypes="mimeTypes" :isValidMimeType="isValidMimeType" />
|
||||
<div v-if="mimetypeError" class="text-red-400 text-sm mt-1">
|
||||
{{ mimetypeError }}
|
||||
</div>
|
||||
|
||||
<BaseDivider v-if="form.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"
|
||||
:is-read-only=isReadOnly>
|
||||
|
@ -266,34 +158,79 @@ const isValidForm = (): boolean => {
|
|||
</div>
|
||||
</FormControl>
|
||||
</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">
|
||||
<label for="rights" class="checkbox mr-6 mb-3 last:mr-0">
|
||||
<input type="checkbox" id="rights" required v-model="form.enabled" />
|
||||
<span class="check" />
|
||||
<a class="pl-2" target="_blank">Enable mimetype immediately </a>
|
||||
|
||||
</label>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Extensions" wrap-body>
|
||||
</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 />
|
||||
is-column allow-manual-adding manual-adding-placeholder="Enter additional file extension manually" />
|
||||
|
||||
</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 class="text-red-400 text-sm" v-if="form.errors.file_extension">
|
||||
{{ form.errors.file_extension }}
|
||||
</div>
|
||||
|
||||
<BaseDivider />
|
||||
<BaseDivider v-if="Object.keys(file_extensions).length > 0" />
|
||||
|
||||
<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>
|
||||
<BaseButtons>
|
||||
<BaseButton type="submit" color="info" label="Create" :class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing" />
|
||||
:disabled="form.processing" @click.prevent="submit()" />
|
||||
</BaseButtons>
|
||||
</template>
|
||||
</CardBox>
|
||||
|
|
|
@ -32,7 +32,7 @@ const form = useForm({
|
|||
// isPreferationRequired: false,
|
||||
});
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
await form.delete(stardust.route('settings.mimetype.deleteStore', [props.mimetype.id]));
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
import { Head, usePage } from '@inertiajs/vue3';
|
||||
import { mdiAccountKey, mdiSquareEditOutline, mdiAlertBoxOutline, mdiPlus, mdiTrashCan } from '@mdi/js';
|
||||
import { mdiAccountKey, mdiSquareEditOutline, mdiAlertBoxOutline, mdiPlus, mdiTrashCan, mdiCheckCircle, mdiCloseCircle } from '@mdi/js';
|
||||
import { computed, ComputedRef } from 'vue';
|
||||
import type { PropType } from "vue";
|
||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||
|
@ -16,6 +16,7 @@ interface MimeType {
|
|||
id: number;
|
||||
name: string;
|
||||
file_extension: string;
|
||||
alternate_mimetype: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
|
@ -54,24 +55,41 @@ const flash: ComputedRef<any> = computed(() => {
|
|||
{{ flash.message }}
|
||||
</NotificationBar>
|
||||
<CardBox class="mb-6" has-table>
|
||||
<table>
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th v-if="can.edit">Actions</th>
|
||||
<th class="px-4 py-2 text-left text-sm font-medium uppercase tracking-wider">Mimetype</th>
|
||||
<th class="px-4 py-2 text-left text-sm font-medium uppercase tracking-wider">Alternate Mime Types</th>
|
||||
<th v-if="can.edit" class="px-4 py-2 text-left text-sm font-medium uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<tr v-for="mimetype in mimetypes" :key="mimetype.id">
|
||||
<td data-label="Name">
|
||||
{{ mimetype.name }} ({{ mimetype.file_extension }})
|
||||
<td class="px-4 py-2 whitespace-nowrap text-left text-sm" data-label="Name">
|
||||
<span class="flex items-center">
|
||||
<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 data-label="Status">
|
||||
<td class="px-4 py-2 whitespace-nowrap text-left text-sm" data-label="Alternate Mime Types">
|
||||
<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-else>Inactive</template>
|
||||
</td>
|
||||
<td v-if="can.edit" class="before:hidden lg:w-1 whitespace-nowrap">
|
||||
</td> -->
|
||||
<td v-if="can.edit" class="before:hidden lg:w-1 whitespace-nowrap px-4 py-2 whitespace-nowrap text-left text-sm">
|
||||
<BaseButtons type="justify-start lg:justify-end" no-wrap>
|
||||
<BaseButton v-if="mimetype.enabled"
|
||||
:route-name="stardust.route('settings.mimetype.down', [mimetype.id])"
|
||||
|
|
|
@ -28,7 +28,7 @@ const form = useForm({
|
|||
});
|
||||
|
||||
const submit = async () => {
|
||||
await form.post(stardust.route('settings.role.store'), form);
|
||||
await form.post(stardust.route('settings.role.store'));
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup>
|
||||
import { Head, Link, useForm } from '@inertiajs/vue3';
|
||||
<script setup lang="ts">
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import { mdiAccountKey, mdiArrowLeftBoldOutline, mdiFormTextarea } from '@mdi/js';
|
||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||
import SectionMain from '@/Components/SectionMain.vue';
|
||||
|
@ -29,15 +29,15 @@ const props = defineProps({
|
|||
});
|
||||
|
||||
const form = useForm({
|
||||
_method: 'put',
|
||||
name: props.role.name,
|
||||
description: props.role.description,
|
||||
permissions: props.roleHasPermissions,
|
||||
});
|
||||
|
||||
const submit = async () => {
|
||||
// await Inertia.post(stardust.route('user.store'), form);
|
||||
await form.put(stardust.route('settings.role.update', [props.role.id]), form);
|
||||
// await Inertia.post(stardust.route('user.store'), form); old
|
||||
await form.put(stardust.route('settings.role.update', [props.role.id]));
|
||||
// await router.put(stardust.route('settings.role.update', [props.role.id]), form);
|
||||
};
|
||||
</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="submit()">
|
||||
<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">
|
||||
{{ form.errors.name }}
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
mdiAsterisk,
|
||||
mdiFormTextboxPassword,
|
||||
mdiArrowLeftBoldOutline,
|
||||
mdiAlertBoxOutline,
|
||||
mdiAlertBoxOutline,
|
||||
} from '@mdi/js';
|
||||
import SectionMain from '@/Components/SectionMain.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 enabled = ref(false);
|
||||
const handleScore = (score: number) => {
|
||||
if (score >= 4){
|
||||
enabled.value = true;
|
||||
} else {
|
||||
enabled.value = false;
|
||||
}
|
||||
// strengthLabel.value = scoreLabel;
|
||||
// score.value = scoreValue;
|
||||
};
|
||||
// const enabled = ref(false);
|
||||
// const handleScore = (score: number) => {
|
||||
// if (score >= 4) {
|
||||
// enabled.value = true;
|
||||
// } else {
|
||||
// enabled.value = false;
|
||||
// }
|
||||
// // strengthLabel.value = scoreLabel;
|
||||
// // score.value = scoreValue;
|
||||
// };
|
||||
|
||||
defineProps({
|
||||
// user will be returned from controller action
|
||||
|
@ -82,20 +82,20 @@ defineProps({
|
|||
// };
|
||||
|
||||
|
||||
const passwordForm = useForm({
|
||||
old_password: '',
|
||||
new_password: '',
|
||||
confirm_password: '',
|
||||
});
|
||||
const passwordSubmit = async () => {
|
||||
await passwordForm.post(stardust.route('account.password.store'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
// console.log(resp);
|
||||
passwordForm.reset();
|
||||
},
|
||||
});
|
||||
};
|
||||
// const passwordForm = useForm({
|
||||
// old_password: '',
|
||||
// new_password: '',
|
||||
// confirm_password: '',
|
||||
// });
|
||||
// const passwordSubmit = async () => {
|
||||
// await passwordForm.post(stardust.route('account.password.store'), {
|
||||
// preserveScroll: true,
|
||||
// onSuccess: () => {
|
||||
// // console.log(resp);
|
||||
// passwordForm.reset();
|
||||
// },
|
||||
// });
|
||||
// };
|
||||
|
||||
const flash: Ref<any> = computed(() => {
|
||||
return usePage().props.flash;
|
||||
|
@ -139,40 +139,10 @@ const flash: Ref<any> = computed(() => {
|
|||
{{ $page.props.flash.message }}
|
||||
</NotificationBar> -->
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- <div class="grid grid-cols-1 lg:grid-cols-1 gap-6"> -->
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
|
||||
<!-- password form -->
|
||||
<!-- <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()">
|
||||
<!-- <CardBox id="passwordForm" title="Change Password" :icon="mdiLock" form
|
||||
@submit.prevent="passwordSubmit()">
|
||||
<FormValidationErrors v-bind:errors="errors" />
|
||||
|
||||
<FormField label="Current password" help="Required. Your current password"
|
||||
|
@ -186,22 +156,15 @@ const flash: Ref<any> = computed(() => {
|
|||
</FormField>
|
||||
<BaseDivider />
|
||||
|
||||
<!-- <FormField label="New password" help="Required. New password"
|
||||
: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" />
|
||||
<PasswordMeter v-model="passwordForm.new_password" :errors="passwordForm.errors"
|
||||
@score="handleScore" />
|
||||
|
||||
|
||||
<FormField label="Confirm password" help="Required. New password one more time"
|
||||
:class="{ 'text-red-400': passwordForm.errors.confirm_password }">
|
||||
<FormControl v-model="passwordForm.confirm_password" :icon="mdiFormTextboxPassword"
|
||||
name="confirm_password" type="password" required :error="passwordForm.errors.confirm_password">
|
||||
name="confirm_password" type="password" required
|
||||
:error="passwordForm.errors.confirm_password">
|
||||
<div class="text-red-400 text-sm" v-if="passwordForm.errors.confirm_password">
|
||||
{{ passwordForm.errors.confirm_password }}
|
||||
</div>
|
||||
|
@ -219,16 +182,17 @@ const flash: Ref<any> = computed(() => {
|
|||
|
||||
<template #footer>
|
||||
<BaseButtons>
|
||||
<BaseButton type="submit" color="info" label="Change password" :disabled="passwordForm.processing == true || enabled == false" />
|
||||
<BaseButton type="submit" color="info" label="Change password"
|
||||
:disabled="passwordForm.processing == true || enabled == false" />
|
||||
</BaseButtons>
|
||||
</template>
|
||||
</CardBox>
|
||||
</CardBox> -->
|
||||
|
||||
|
||||
|
||||
<PersonalTotpSettings :twoFactorEnabled="twoFactorEnabled" :backupState="backupState">
|
||||
</PersonalTotpSettings>
|
||||
<!-- <PersonalSettings :state="backupState"/> -->
|
||||
<PersonalTotpSettings :twoFactorEnabled="twoFactorEnabled" :backupState="backupState">
|
||||
</PersonalTotpSettings>
|
||||
<!-- <PersonalSettings :state="backupState"/> -->
|
||||
|
||||
<!-- <CardBox v-if="!props.twoFactorEnabled" title="Two-Factor Authentication" :icon="mdiInformation" form
|
||||
@submit.prevent="enableTwoFactorAuthentication()">
|
||||
|
@ -248,7 +212,7 @@ const flash: Ref<any> = computed(() => {
|
|||
</template>
|
||||
</CardBox> -->
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</SectionMain>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<!-- <SectionFullScreen v-slot="{ cardClass }" :bg="'greenBlue'"> -->
|
||||
<SectionFullScreen v-slot="{ cardClass }">
|
||||
<a class="text-2xl font-semibold flex justify-center items-center mb-8 lg:mb-10">
|
||||
<img src="/logo.svg" class="h-10 mr-4" alt="Windster Logo" />
|
||||
<img src="../../logo.svg" class="h-10 mr-4" alt="Windster Logo" />
|
||||
<!-- <span class="self-center text-2xl font-bold whitespace-nowrap">Tethys</span> -->
|
||||
</a>
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
import AuthLayout from '@/Layouts/Auth.vue';
|
||||
import { reactive } from 'vue';
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { Inertia } from '@inertiajs/inertia';
|
||||
// import { Inertia } from '@inertiajs/inertia';
|
||||
// import { NButton, NInput } from 'naive-ui';
|
||||
// import { useForm } from '@inertiajs/inertia-vue3'
|
||||
import FormInput from '@/Components/FormInput.vue';
|
||||
|
@ -45,7 +45,7 @@ export default {
|
|||
});
|
||||
|
||||
const submit = async () => {
|
||||
await Inertia.post('/app/register', form);
|
||||
// await Inertia.post('/app/register', form);
|
||||
};
|
||||
|
||||
return { form, submit };
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import { Head } from '@inertiajs/vue3';
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { MainService } from '@/Stores/main';
|
||||
// import { Inertia } from '@inertiajs/inertia';
|
||||
import {
|
||||
mdiAccountMultiple,
|
||||
mdiDatabaseOutline,
|
||||
|
@ -13,20 +12,18 @@ import {
|
|||
mdiGithub,
|
||||
mdiChartPie,
|
||||
} 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 SectionMain from '@/Components/SectionMain.vue';
|
||||
import CardBoxWidget from '@/Components/CardBoxWidget.vue';
|
||||
import CardBox from '@/Components/CardBox.vue';
|
||||
import TableSampleClients from '@/Components/TableSampleClients.vue';
|
||||
import NotificationBar from '@/Components/NotificationBar.vue';
|
||||
// import NotificationBar from '@/Components/NotificationBar.vue';
|
||||
import BaseButton from '@/Components/BaseButton.vue';
|
||||
import CardBoxTransaction from '@/Components/CardBoxTransaction.vue';
|
||||
import CardBoxClient from '@/Components/CardBoxClient.vue';
|
||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
|
||||
import SectionBannerStarOnGitHub from '@/Components/SectionBannerStarOnGitea.vue';
|
||||
import CardBoxDataset from '@/Components/CardBoxDataset.vue';
|
||||
const mainService = MainService()
|
||||
|
||||
// const chartData = ref();
|
||||
|
@ -36,43 +33,39 @@ const fillChartData = async () => {
|
|||
// chartData.value = mainService.graphData;
|
||||
};
|
||||
const chartData = computed(() => mainService.graphData);
|
||||
onMounted(async () => {
|
||||
await mainService.fetchChartData("2022");
|
||||
});
|
||||
;
|
||||
/* Fetch sample data */
|
||||
mainService.fetch('clients');
|
||||
mainService.fetch('history');
|
||||
// onMounted(async () => {
|
||||
// await mainService.fetchChartData("2022");
|
||||
// });
|
||||
|
||||
mainService.fetchApi('authors');
|
||||
mainService.fetchApi('datasets');
|
||||
// mainService.fetch('clients');
|
||||
// mainService.fetch('history');
|
||||
|
||||
// mainService.fetchApi('authors');
|
||||
// mainService.fetchApi('datasets');
|
||||
|
||||
// const clientBarItems = computed(() => mainService.clients.slice(0, 4));
|
||||
const transactionBarItems = computed(() => mainService.history);
|
||||
// const transactionBarItems = computed(() => mainService.history);
|
||||
|
||||
const authorBarItems = computed(() => mainService.authors.slice(0, 4));
|
||||
const authorBarItems = computed(() => mainService.authors.slice(0, 5));
|
||||
const authors = computed(() => mainService.authors);
|
||||
const datasets = computed(() => mainService.datasets);
|
||||
// const props = defineProps({
|
||||
// user: {
|
||||
// type: Object,
|
||||
// default: () => ({}),
|
||||
// }
|
||||
// });
|
||||
const datasetBarItems = computed(() => mainService.datasets.slice(0, 5));
|
||||
// let test = datasets.value;
|
||||
// console.log(test);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<LayoutAuthenticated :showAsideMenu="false">
|
||||
<Head title="Dashboard" />
|
||||
|
||||
<!-- <section class="p-6" v-bind:class="containerMaxW"> -->
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton v-bind:icon="mdiChartTimelineVariant" title="Overview" main>
|
||||
<BaseButton
|
||||
href="https://gitea.geologie.ac.at/geolba/tethys"
|
||||
href="https://gitea.geosphere.at/geolba/tethys.backend"
|
||||
target="_blank"
|
||||
:icon="mdiGithub"
|
||||
label="Star on Gitea"
|
||||
label="Star on GeoSphere Forgejo"
|
||||
color="contrast"
|
||||
rounded-full
|
||||
small
|
||||
|
@ -96,16 +89,13 @@ const datasets = computed(() => mainService.datasets);
|
|||
:number="datasets.length"
|
||||
label="Publications"
|
||||
/>
|
||||
<!-- <CardBoxWidget trend="193" trend-type="info" color="text-blue-500" :icon="mdiCartOutline" :number="datasets.length"
|
||||
prefix="$" label="Publications" /> -->
|
||||
<CardBoxWidget
|
||||
trend="Overflow"
|
||||
trend-type="alert"
|
||||
color="text-red-500"
|
||||
trend="+25%"
|
||||
trend-type="up"
|
||||
color="text-purple-500"
|
||||
:icon="mdiChartTimelineVariant"
|
||||
:number="256"
|
||||
suffix="%"
|
||||
label="Performance"
|
||||
:number="52"
|
||||
label="Citations"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -117,19 +107,15 @@ const datasets = computed(() => mainService.datasets);
|
|||
:name="client.name"
|
||||
:email="client.email"
|
||||
:date="client.created_at"
|
||||
:text="client.datasetCount"
|
||||
:text="client.identifier_orcid"
|
||||
:count="client.dataset_count"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col justify-between">
|
||||
<CardBoxTransaction
|
||||
v-for="(transaction, index) in transactionBarItems"
|
||||
<CardBoxDataset
|
||||
v-for="(dataset, index) in datasetBarItems"
|
||||
:key="index"
|
||||
:amount="transaction.amount"
|
||||
:date="transaction.date"
|
||||
:business="transaction.business"
|
||||
:type="transaction.type"
|
||||
:name="transaction.name"
|
||||
:account="transaction.account"
|
||||
:dataset="dataset"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -143,33 +129,13 @@ const datasets = computed(() => mainService.datasets);
|
|||
</div>
|
||||
</CardBox>
|
||||
|
||||
<SectionTitleLineWithButton :icon="mdiAccountMultiple" title="Submitters (to do)" />
|
||||
<SectionTitleLineWithButton :icon="mdiAccountMultiple" title="Submitters" />
|
||||
|
||||
<NotificationBar color="info" :icon="mdiMonitorCellphone"> <b>Responsive table.</b> Collapses on mobile </NotificationBar>
|
||||
<!-- <NotificationBar color="info" :icon="mdiMonitorCellphone"> <b>Responsive table.</b> Collapses on mobile </NotificationBar> -->
|
||||
|
||||
<CardBox :icon="mdiMonitorCellphone" title="Responsive table" has-table>
|
||||
<TableSampleClients />
|
||||
</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>
|
||||
<!-- </section> -->
|
||||
</LayoutAuthenticated>
|
||||
</template>
|
||||
|
|
|
@ -26,17 +26,6 @@ const errors: Ref<any> = computed(() => {
|
|||
return usePage().props.errors;
|
||||
});
|
||||
|
||||
// const form = useForm({
|
||||
// preferred_reviewer: '',
|
||||
// preferred_reviewer_email: '',
|
||||
// preferation: 'yes_preferation',
|
||||
|
||||
// // preferation: '',
|
||||
// // isPreferationRequired: false,
|
||||
// });
|
||||
|
||||
// const isPreferationRequired = computed(() => form.preferation === 'yes_preferation');
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
// Notification.showInfo(`doi implementation is in developement. Create DOI for dataset ${props.dataset.publish_id} later on`);
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import { ref, Ref } from 'vue';
|
||||
import { mdiChartTimelineVariant } from '@mdi/js';
|
||||
import { mdiChartTimelineVariant, mdiGithub } from '@mdi/js';
|
||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||
import SectionMain from '@/Components/SectionMain.vue';
|
||||
import BaseButton from '@/Components/BaseButton.vue';
|
||||
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
|
||||
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 { OpensearchDocument } from '@/Dataset';
|
||||
|
||||
|
@ -48,14 +48,15 @@ const mapOptions: MapOptions = {
|
|||
|
||||
<template>
|
||||
<LayoutAuthenticated :showAsideMenu="false">
|
||||
|
||||
<Head title="Map" />
|
||||
|
||||
<!-- <section class="p-6" v-bind:class="containerMaxW"> -->
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton v-bind:icon="mdiChartTimelineVariant" title="Tethys Map" main>
|
||||
<!-- <BaseButton href="https://gitea.geologie.ac.at/geolba/tethys" target="_blank" :icon="mdiGithub"
|
||||
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 href="https://gitea.geosphere.at/geolba/tethys" target="_blank" :icon="mdiGithub"
|
||||
label="Star on GeoSPhere Forgejo" color="contrast" rounded-full small />
|
||||
<!-- <BaseButton :route-name="stardust.route('app.login.show')" label="Login" color="white" rounded-full small /> -->
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
<!-- <SectionBannerStarOnGitea /> -->
|
||||
|
@ -80,19 +81,20 @@ const mapOptions: MapOptions = {
|
|||
<div v-for="author in dataset.author" :key="author" class="mb-1">{{ author }}</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<span class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
|
||||
<span
|
||||
class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
|
||||
{{ dataset.year }}
|
||||
</span>
|
||||
<span class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
|
||||
<span
|
||||
class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
|
||||
{{ dataset.language }}
|
||||
</span>
|
||||
</div>
|
||||
<p>
|
||||
<span class="label"><i class="fas fa-file"></i> {{ dataset.doctype }}</span>
|
||||
<!-- <span>Licence: {{ document.licence }}</span> -->
|
||||
<span v-if="openAccessLicences.includes(dataset.licence)" class="label titlecase"
|
||||
><i class="fas fa-lock-open"></i> Open Access</span
|
||||
>
|
||||
<span v-if="openAccessLicences.includes(dataset.licence)" class="label titlecase"><i
|
||||
class="fas fa-lock-open"></i> Open Access</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,91 +1,343 @@
|
|||
<template>
|
||||
<div class="flex flex-col h-screen p-4 bg-gray-100">
|
||||
<header class="flex justify-between items-center mb-4">
|
||||
<h1 class="text-xl font-bold">SKOS Browser</h1>
|
||||
<div class="flex space-x-2">
|
||||
<button @click="updateApp" title="Update the application">
|
||||
<img src="/Resources/Images/refresh.png" alt="Update" class="w-4 h-4" />
|
||||
</button>
|
||||
<button @click="showInfo" title="Info">
|
||||
<img src="/Resources/Images/info.png" alt="Info" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<LayoutAuthenticated>
|
||||
|
||||
<div class="bg-white shadow-md rounded-lg p-4 mb-6">
|
||||
<h2 class="text-lg font-semibold">GBA-Thesaurus</h2>
|
||||
<label class="block text-sm font-medium">Aktueller Endpoint:</label>
|
||||
<!-- <TreeView :items="endpoints" @select="onEndpointSelected" /> -->
|
||||
</div>
|
||||
<Head title="Profile"></Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton :icon="mdiLibraryShelves" title="Library Classification" main>
|
||||
<div class="bg-lime-100 shadow rounded-lg p-6 mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<label for="role-select" class="block text-lg font-medium text-gray-700 mb-1">
|
||||
Select Classification Role <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select id="role-select" v-model="selectedCollectionRole"
|
||||
class="w-full border border-gray-300 rounded-md p-2 text-gray-700 focus:ring-2 focus:ring-indigo-500"
|
||||
required>
|
||||
<!-- <option value="" disabled selected>Please select a role</option> -->
|
||||
<option v-for="collRole in collectionRoles" :key="collRole.id" :value="collRole">
|
||||
{{ collRole.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ml-4 hidden md:block">
|
||||
<span class="text-sm text-gray-600 italic">* required</span>
|
||||
</div>
|
||||
</div>
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
|
||||
<!-- Available TopLevel Collections -->
|
||||
<CardBox class="mb-4 rounded-lg p-4">
|
||||
<h2 class="text-lg font-bold text-gray-800 dark:text-slate-400 mb-2">Available Toplevel-Collections
|
||||
<span v-if="selectedCollectionRole && !selectedToplevelCollection"
|
||||
class="text-sm text-red-500 italic">(click to
|
||||
select)</span>
|
||||
</h2>
|
||||
<ul class="flex flex-wrap gap-2">
|
||||
<li v-for="col in collections" :key="col.id" :class="{
|
||||
'cursor-pointer p-2 border border-gray-200 rounded hover:bg-sky-50 text-sky-700 text-sm': true,
|
||||
'bg-sky-100 border-sky-500': selectedToplevelCollection && selectedToplevelCollection.id === col.id
|
||||
}" @click="onToplevelCollectionSelected(col)">
|
||||
{{ `${col.name} (${col.number})` }}
|
||||
</li>
|
||||
<li v-if="collections.length === 0" class="text-gray-800 dark:text-slate-400">
|
||||
No collections available.
|
||||
</li>
|
||||
</ul>
|
||||
</CardBox>
|
||||
|
||||
<!-- Collections Listing -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||
|
||||
<!-- Broader Collection (Parent) -->
|
||||
<CardBox v-if="selectedCollection" class="rounded-lg p-4" has-form-data>
|
||||
<h2 class="text-lg font-bold text-gray-800 dark:text-slate-400 mb-2">Broader Collection</h2>
|
||||
<ul class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
|
||||
<li v-for="parent in broaderCollections" :key="parent.id"
|
||||
class="cursor-pointer p-2 border border-gray-200 rounded bg-green-50 text-green-700 text-sm hover:bg-green-100 hover:underline"
|
||||
@click="onCollectionSelected(parent)" title="Click to select this collection">
|
||||
{{ `${parent.name} (${parent.number})` }}
|
||||
</li>
|
||||
<li v-if="broaderCollections.length === 0" class="text-gray-500 text-sm">
|
||||
No broader collections available.
|
||||
</li>
|
||||
</ul>
|
||||
</CardBox>
|
||||
|
||||
<!-- Selected Collection Details -->
|
||||
<CardBox v-if="selectedCollection" class="rounded-lg p-4" has-form-data>
|
||||
<h3 class="text-xl font-bold text-gray-800 dark:text-slate-400 mb-2">Selected Collection</h3>
|
||||
<p
|
||||
class="cursor-pointer p-2 border border-gray-200 rounded bg-green-50 text-green-700 text-sm hover:bg-green-100">
|
||||
{{ `${selectedCollection.name} (${selectedCollection.number})` }}
|
||||
</p>
|
||||
</CardBox>
|
||||
|
||||
<!-- Narrower Collections (Children) -->
|
||||
<CardBox v-if="selectedCollection" class="rounded-lg p-4" has-form-data>
|
||||
<h2 class="text-lg font-bold text-gray-800 dark:text-slate-400 mb-2">Narrower Collections</h2>
|
||||
<!-- <ul class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
|
||||
<li v-for="child in narrowerCollections" :key="child.id"
|
||||
class="cursor-pointer p-2 border border-gray-200 rounded bg-green-50 text-green-700 text-sm hover:bg-green-100 hover:underline"
|
||||
@click="onCollectionSelected(child)">
|
||||
{{ `${child.name} (${child.number})` }}
|
||||
</li>
|
||||
<li v-if="narrowerCollections.length === 0" class="text-gray-500 text-sm">
|
||||
No sub-collections available.
|
||||
</li>
|
||||
</ul> -->
|
||||
<draggable v-if="narrowerCollections.length > 0" v-model="narrowerCollections"
|
||||
:group="{ name: 'collections' }" tag="ul" class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
|
||||
<template #item="{ element: child }">
|
||||
<li :key="child.id"
|
||||
class="cursor-pointer p-2 border border-gray-200 rounded bg-green-50 text-green-700 text-sm hover:bg-green-100 hover:underline"
|
||||
@click="onCollectionSelected(child)">
|
||||
{{ `${child.name} (${child.number})` }}
|
||||
</li>
|
||||
</template>
|
||||
</draggable>
|
||||
<ul v-else class="flex flex-wrap gap-2 max-h-60 overflow-y-auto">
|
||||
<li class="text-gray-500 text-sm">
|
||||
No sub-collections available.
|
||||
</li>
|
||||
</ul>
|
||||
</CardBox>
|
||||
|
||||
<div class="bg-white shadow-md rounded-lg p-4">
|
||||
<h2 class="text-lg font-semibold">Konzept-Suche</h2>
|
||||
<!-- <Autocomplete v-model="selectedConcept" :items="concepts" placeholder="Search for a concept" @change="onConceptSelected" /> -->
|
||||
<div class="mt-4">
|
||||
<h3 class="text-md font-medium">Ausgewähltes Konzept</h3>
|
||||
<p>{{ selectedConcept.title }}</p>
|
||||
<a :href="selectedConcept.uri" target="_blank" class="text-blue-500">URI</a>
|
||||
<textarea
|
||||
v-model="selectedConcept.description"
|
||||
class="mt-2 w-full h-24 border rounded"
|
||||
placeholder="Description"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-md font-medium">Untergeordnete Konzepte</h3>
|
||||
<!-- <LinkLabelList :items="narrowerConcepts" /> -->
|
||||
|
||||
<div class="mb-4 rounded-lg">
|
||||
<div v-if="selectedCollection" class="bg-gray-100 shadow rounded-lg p-6 mb-6">
|
||||
<p class="mb-4 text-gray-700">Please drag your collections here to classify your previously created
|
||||
dataset
|
||||
according to library classification standards.</p>
|
||||
<draggable v-model="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"
|
||||
tag="ul">
|
||||
<template #item="{ element }">
|
||||
<div :key="element.id"
|
||||
class="p-2 m-1 bg-sky-200 text-sky-800 rounded flex items-center gap-2 h-7">
|
||||
<span>{{ element.name }} ({{ element.number }})</span>
|
||||
<button
|
||||
@click="dropCollections = dropCollections.filter(item => item.id !== element.id)"
|
||||
class="hover:text-sky-600 flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20"
|
||||
fill="currentColor">
|
||||
<path fill-rule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-md font-medium">Übergeordnete Konzepte</h3>
|
||||
<!-- <LinkLabelList :items="broaderConcepts" /> -->
|
||||
|
||||
<div class="p-6 border-t border-gray-100 dark:border-slate-800">
|
||||
<BaseButtons>
|
||||
<BaseButton @click.stop="syncDatasetCollections" label="Save" color="info" small
|
||||
:disabled="isSaveDisabled" :style="{ opacity: isSaveDisabled ? 0.5 : 1 }">
|
||||
</BaseButton>
|
||||
</BaseButtons>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<h3 class="text-md font-medium">Verwandte Konzepte</h3>
|
||||
<!-- <LinkLabelList :items="relatedConcepts" /> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</SectionMain>
|
||||
</LayoutAuthenticated>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import TreeView from './TreeView.vue'; // Assuming you have a TreeView component
|
||||
// import Autocomplete from './Autocomplete.vue'; // Assuming you have an Autocomplete component
|
||||
// import LinkLabelList from './LinkLabelList.vue'; // Assuming you have a LinkLabelList component
|
||||
<script lang="ts" setup>
|
||||
import { ref, Ref, watch, computed } from 'vue';
|
||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||
import SectionMain from '@/Components/SectionMain.vue';
|
||||
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
|
||||
import axios from 'axios';
|
||||
import { mdiLibraryShelves } from '@mdi/js';
|
||||
import draggable from 'vuedraggable';
|
||||
import BaseButton from '@/Components/BaseButton.vue';
|
||||
import BaseButtons from '@/Components/BaseButtons.vue';
|
||||
import CardBox from '@/Components/CardBox.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
// TreeView,
|
||||
// Autocomplete,
|
||||
// LinkLabelList,
|
||||
interface CollectionRole {
|
||||
id: number;
|
||||
name: string;
|
||||
collections?: any[];
|
||||
}
|
||||
|
||||
interface Collection {
|
||||
id: number;
|
||||
name: string;
|
||||
number: string;
|
||||
parent_id?: number | null;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
collectionRoles: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
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
|
||||
};
|
||||
dataset: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
methods: {
|
||||
updateApp() {
|
||||
// Handle app update logic
|
||||
},
|
||||
showInfo() {
|
||||
// Handle showing information
|
||||
},
|
||||
onEndpointSelected(endpoint) {
|
||||
// Handle endpoint selection
|
||||
},
|
||||
onConceptSelected(concept) {
|
||||
this.selectedConcept = concept;
|
||||
// Handle concept selection logic, e.g., fetching related concepts
|
||||
},
|
||||
relatedCollections: Array<Collection>
|
||||
});
|
||||
|
||||
const collectionRoles: Ref<CollectionRole[]> = ref(props.collectionRoles as CollectionRole[]);
|
||||
const collections: Ref<Collection[]> = ref<Collection[]>([]);
|
||||
const selectedCollectionRole = ref<CollectionRole | null>(null);
|
||||
const selectedToplevelCollection = ref<Collection | null>(null);
|
||||
const selectedCollection = ref<Collection | null>(null);
|
||||
const narrowerCollections = ref<Collection[]>([]);
|
||||
const broaderCollections = ref<Collection[]>([]);
|
||||
|
||||
|
||||
// const onCollectionRoleSelected = (event: Event) => {
|
||||
// const target = event.target as HTMLSelectElement;
|
||||
// const roleId = Number(target.value);
|
||||
// selectedCollectionRole.value =
|
||||
// collectionRoles.value.find((role: CollectionRole) => role.id === roleId) || null;
|
||||
|
||||
// // Clear any previously selected collection or related data
|
||||
// selectedCollection.value = null;
|
||||
// narrowerCollections.value = [];
|
||||
// broaderCollections.value = [];
|
||||
|
||||
// // fetchTopLevelCollections(roleId);
|
||||
// collections.value = selectedCollectionRole.value?.collections || []
|
||||
// };
|
||||
|
||||
// New reactive array to hold dropped collections for the dataset
|
||||
const dropCollections: Ref<Collection[]> = ref([]);
|
||||
|
||||
// If there are related collections passed in, fill dropCollections with these.
|
||||
if (props.relatedCollections && props.relatedCollections.length > 0) {
|
||||
dropCollections.value = props.relatedCollections;
|
||||
}
|
||||
|
||||
// Add a computed property for the disabled state based on dropCollections length
|
||||
const isSaveDisabled = computed(() => dropCollections.value.length === 0);
|
||||
|
||||
// If the collectionRoles prop might load asynchronously (or change), you can watch for it:
|
||||
watch(
|
||||
() => props.collectionRoles as CollectionRole[],
|
||||
(newCollectionRoles: CollectionRole[]) => {
|
||||
collectionRoles.value = newCollectionRoles;
|
||||
// Preselect the role with name "ccs" if it exists
|
||||
const found: CollectionRole | undefined = collectionRoles.value.find(
|
||||
role => role.name.toLowerCase() === 'ccs'
|
||||
);
|
||||
if (found?.name === 'ccs') {
|
||||
selectedCollectionRole.value = found;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
// Watch for changes in selectedCollectionRole and update related collections state
|
||||
watch(
|
||||
() => selectedCollectionRole.value as CollectionRole,
|
||||
(newSelectedCollectionRole: CollectionRole | null) => {
|
||||
if (newSelectedCollectionRole != null) {
|
||||
collections.value = newSelectedCollectionRole.collections || []
|
||||
} else {
|
||||
selectedToplevelCollection.value = null;
|
||||
selectedCollection.value = null;
|
||||
collections.value = []
|
||||
}
|
||||
// Reset dependent variables when the role changes
|
||||
selectedCollection.value = null
|
||||
narrowerCollections.value = []
|
||||
broaderCollections.value = []
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Watch for changes in dropCollections
|
||||
watch(
|
||||
() => dropCollections.value,
|
||||
() => {
|
||||
if (selectedCollection.value) {
|
||||
fetchCollections(selectedCollection.value.id);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const onToplevelCollectionSelected = (collection: Collection) => {
|
||||
selectedToplevelCollection.value = collection;
|
||||
selectedCollection.value = collection;
|
||||
// call the API endpoint to get both.
|
||||
fetchCollections(collection.id)
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
/* Add your styles here */
|
||||
.btn-primary {
|
||||
background-color: #4f46e5;
|
||||
color: white;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #4338ca;
|
||||
}
|
||||
|
||||
.btn-primary:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #4f46e5;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: white;
|
||||
color: #374151;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.btn-secondary:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #6366f1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -45,7 +45,7 @@ import { LayerOptions } from '@/Components/Map/LayerOptions';
|
|||
import TableKeywords from '@/Components/TableKeywords.vue';
|
||||
import NotificationBar from '@/Components/NotificationBar.vue';
|
||||
import FileUploadComponent from '@/Components/FileUpload.vue';
|
||||
import Person from '#models/person';
|
||||
import type Person from '#models/person';
|
||||
|
||||
const props = defineProps({
|
||||
licenses: {
|
||||
|
@ -96,6 +96,27 @@ const flash: ComputedRef<any> = computed(() => {
|
|||
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();
|
||||
|
||||
// let serrors = reactive([]);
|
||||
|
@ -1050,7 +1071,7 @@ Removes a selected keyword
|
|||
<!-- <input name="Reference Value" class="form-control"
|
||||
placeholder="[VALUE]" v-model="item.value" /> -->
|
||||
<FormControl required v-model="item.value" :type="'text'"
|
||||
placeholder="[VALUE]" :errors="form.errors.embargo_date">
|
||||
:placeholder="getPlaceholder(form.references[index].type)" :errors="form.errors.embargo_date">
|
||||
<div class="text-red-400 text-sm"
|
||||
v-if="form.errors[`references.${index}.value`] && Array.isArray(form.errors[`references.${index}.value`])">
|
||||
{{ form.errors[`references.${index}.value`].join(', ') }}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// import { Head, Link, useForm, usePage } from '@inertiajs/inertia-vue3';
|
||||
import { Head, usePage } from '@inertiajs/vue3';
|
||||
import { ComputedRef } from 'vue';
|
||||
import { mdiSquareEditOutline, mdiTrashCan, mdiAlertBoxOutline, mdiLockOpen } from '@mdi/js';
|
||||
import { mdiSquareEditOutline, mdiTrashCan, mdiAlertBoxOutline, mdiLockOpen, mdiLibraryShelves } from '@mdi/js';
|
||||
import { computed } from 'vue';
|
||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||
import SectionMain from '@/Components/SectionMain.vue';
|
||||
|
@ -139,7 +139,10 @@ const formatServerState = (state: string) => {
|
|||
:route-name="stardust.route('dataset.release', [dataset.id])" color="info"
|
||||
:icon="mdiLockOpen" :label="'Release'" small />
|
||||
<BaseButton v-if="can.edit" :route-name="stardust.route('dataset.edit', [dataset.id])"
|
||||
color="info" :icon="mdiSquareEditOutline" :label="'Edit'" small />
|
||||
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"
|
||||
:route-name="stardust.route('dataset.delete', [dataset.id])" :icon="mdiTrashCan"
|
||||
small />
|
||||
|
|
135
resources/js/Pages/profile/partials/update-password-form.vue
Normal file
135
resources/js/Pages/profile/partials/update-password-form.vue
Normal file
|
@ -0,0 +1,135 @@
|
|||
<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>
|
|
@ -0,0 +1,179 @@
|
|||
<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>
|
139
resources/js/Pages/profile/show.vue
Normal file
139
resources/js/Pages/profile/show.vue
Normal file
|
@ -0,0 +1,139 @@
|
|||
<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> -->
|
|
@ -2,6 +2,56 @@ import { defineStore } from 'pinia';
|
|||
import axios from 'axios';
|
||||
import { Dataset } from '@/Dataset';
|
||||
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 {
|
||||
id: number;
|
||||
|
@ -9,10 +59,12 @@ export interface Person {
|
|||
email: string;
|
||||
name_type: string;
|
||||
identifier_orcid: string;
|
||||
datasetCount: string;
|
||||
dataset_count: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface TransactionItem {
|
||||
amount: number;
|
||||
account: string;
|
||||
|
@ -61,7 +113,7 @@ export const MainService = defineStore('main', {
|
|||
isFieldFocusRegistered: false,
|
||||
|
||||
/* Sample data for starting dashboard(commonly used) */
|
||||
clients: [],
|
||||
clients: [] as Array<User>,
|
||||
history: [] as Array<TransactionItem>,
|
||||
|
||||
// api based data
|
||||
|
@ -184,7 +236,7 @@ export const MainService = defineStore('main', {
|
|||
this.totpState = state;
|
||||
},
|
||||
|
||||
async fetchChartData(year: string) {
|
||||
fetchChartData(year: string) {
|
||||
// sampleDataKey= authors or datasets
|
||||
axios
|
||||
.get(`/api/statistic/${year}`)
|
||||
|
|
|
@ -2,15 +2,16 @@ import '../css/app.css';
|
|||
import { createApp, h } from 'vue';
|
||||
import { Inertia } from '@inertiajs/inertia';
|
||||
|
||||
import { createInertiaApp } from '@inertiajs/vue3';
|
||||
import { Head, Link, createInertiaApp } from '@inertiajs/vue3';
|
||||
// import DefaultLayout from '@/Layouts/Default.vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import { StyleService } from '@/Stores/style.service';
|
||||
import { LayoutService } from '@/Stores/layout';
|
||||
import { LocaleStore } from '@/Stores/locale';
|
||||
import { MainService } from './Stores/main';
|
||||
import { darkModeKey, styleKey } from '@/config';
|
||||
// import type { DefineComponent } from 'vue';
|
||||
// import { resolvePageComponent } from '@adonisjs/inertia/helpers';
|
||||
import type { DefineComponent } from 'vue';
|
||||
import { resolvePageComponent } from '@adonisjs/inertia/helpers';
|
||||
const pinia = createPinia();
|
||||
// import i18n from './i18n';
|
||||
import { EmitterPlugin } from '@/EmitterDirective';
|
||||
|
@ -36,33 +37,21 @@ createInertiaApp({
|
|||
progress: {
|
||||
// color: '#4B5563',
|
||||
color: '#22C55E',
|
||||
showSpinner: true,
|
||||
},
|
||||
// Webpack
|
||||
// resolve: (name) => require(`./Pages/${name}`),
|
||||
// resolve: (name) => require(`./Pages/${name}.vue`),
|
||||
// add default layout
|
||||
// resolve: (name) => {
|
||||
// const page = require(`./Pages/${name}.vue`).default;
|
||||
// Webpack
|
||||
// resolve: async (name: string) => {
|
||||
// // 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: async (name: string) => {
|
||||
// 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'));
|
||||
},
|
||||
// resolve: (name) => {
|
||||
// return resolvePageComponent(
|
||||
// `./Pages/${name}.vue`,
|
||||
// import.meta.glob<DefineComponent>('./pages/**/*.vue'),
|
||||
// )
|
||||
// },
|
||||
|
||||
setup({ el, App, props, plugin }) {
|
||||
const app = createApp({ render: () => h(App, props) })
|
||||
|
@ -72,11 +61,19 @@ createInertiaApp({
|
|||
.use(EmitterPlugin);
|
||||
// .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(() => {
|
||||
app.mount(el);
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
const styleService = StyleService(pinia);
|
||||
|
@ -84,7 +81,7 @@ const layoutService = LayoutService(pinia);
|
|||
const localeService = LocaleStore(pinia);
|
||||
|
||||
localeService.initializeLocale();
|
||||
// const mainService = MainService(pinia);
|
||||
const mainService = MainService(pinia);
|
||||
// mainService.setUser(user);
|
||||
|
||||
/* App style */
|
||||
|
@ -94,6 +91,12 @@ styleService.setStyle(localStorage[styleKey] ?? 'basic');
|
|||
if ((!localStorage[darkModeKey] && window.matchMedia('(prefers-color-scheme: dark)').matches) || localStorage[darkModeKey] === '1') {
|
||||
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 */
|
||||
Inertia.on('navigate', () => {
|
||||
|
|
183
resources/js/logo.svg
Normal file
183
resources/js/logo.svg
Normal file
|
@ -0,0 +1,183 @@
|
|||
<!-- <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>
|
|
@ -12,6 +12,7 @@ import {
|
|||
mdiShieldCrownOutline,
|
||||
mdiLicense,
|
||||
mdiFileDocument,
|
||||
mdiLibraryShelves
|
||||
} from '@mdi/js';
|
||||
|
||||
export default [
|
||||
|
@ -27,6 +28,11 @@ export default [
|
|||
icon: mdiLock,
|
||||
label: 'Security',
|
||||
},
|
||||
{
|
||||
route: 'settings.profile.edit',
|
||||
icon: mdiLock,
|
||||
label: 'Profile',
|
||||
},
|
||||
// {
|
||||
// route: 'dataset.create',
|
||||
// icon: mdiPublish,
|
||||
|
@ -106,6 +112,11 @@ export default [
|
|||
icon: mdiPublish,
|
||||
label: 'Create Dataset',
|
||||
},
|
||||
// {
|
||||
// route: 'dataset.categorize',
|
||||
// icon: mdiLibraryShelves,
|
||||
// label: 'Library Classification',
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -152,9 +163,9 @@ export default [
|
|||
// label: 'Create Dataset',
|
||||
// },
|
||||
{
|
||||
href: 'https://gitea.geologie.ac.at/geolba/tethys',
|
||||
href: 'https://gitea.geosphere.at/geolba/tethys.backend',
|
||||
icon: mdiGithub,
|
||||
label: 'Gitea',
|
||||
label: 'Forgejo',
|
||||
target: '_blank',
|
||||
},
|
||||
{
|
||||
|
|
|
@ -15,5 +15,5 @@
|
|||
},
|
||||
},
|
||||
"include": ["./**/*.ts", "./**/*.vue"],
|
||||
"exclude": ["./utils/*.js"],
|
||||
"exclude": ["./utils/*.js", "./utils/Timer.js", "./utils/focusTrap.js"],
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue