mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-08-04 15:41:41 +00:00
Merge tag 'v2.12.0' into order-details
# Conflicts: # src/Entity/Parts/Part.php # src/Entity/Parts/PartTraits/AdvancedPropertyTrait.php # src/Services/LabelSystem/SandboxedTwigFactory.php
This commit is contained in:
commit
a1c26de9ae
727 changed files with 78440 additions and 57852 deletions
|
|
@ -12,7 +12,7 @@ opcache.max_accelerated_files = 20000
|
|||
opcache.memory_consumption = 256
|
||||
opcache.enable_file_override = 1
|
||||
|
||||
memory_limit = 256M
|
||||
memory_limit = 512M
|
||||
|
||||
upload_max_filesize=256M
|
||||
post_max_size=300M
|
||||
post_max_size=300M
|
||||
|
|
|
|||
|
|
@ -26,6 +26,28 @@ if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then
|
|||
composer install --prefer-dist --no-progress --no-interaction
|
||||
fi
|
||||
|
||||
# Install additional composer packages if COMPOSER_EXTRA_PACKAGES is set
|
||||
if [ -n "$COMPOSER_EXTRA_PACKAGES" ]; then
|
||||
echo "Installing additional composer packages: $COMPOSER_EXTRA_PACKAGES"
|
||||
# Note: COMPOSER_EXTRA_PACKAGES is intentionally not quoted to allow word splitting
|
||||
# This enables passing multiple package names separated by spaces
|
||||
# shellcheck disable=SC2086
|
||||
composer require $COMPOSER_EXTRA_PACKAGES --no-install --no-interaction --no-progress
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Running composer install to install packages without dev dependencies..."
|
||||
composer install --no-dev --no-interaction --no-progress --optimize-autoloader
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Successfully installed additional composer packages"
|
||||
else
|
||||
echo "Failed to install composer dependencies"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Failed to add additional composer packages to composer.json"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if grep -q ^DATABASE_URL= .env; then
|
||||
echo "Waiting for database to be ready..."
|
||||
ATTEMPTS_LEFT_TO_REACH_DATABASE=60
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
worker {
|
||||
file ./public/index.php
|
||||
env APP_RUNTIME Runtime\FrankenPhpSymfony\Runtime
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,28 @@ if [ -d /var/www/html/var/db ]; then
|
|||
fi
|
||||
fi
|
||||
|
||||
# Install additional composer packages if COMPOSER_EXTRA_PACKAGES is set
|
||||
if [ -n "$COMPOSER_EXTRA_PACKAGES" ]; then
|
||||
echo "Installing additional composer packages: $COMPOSER_EXTRA_PACKAGES"
|
||||
# Note: COMPOSER_EXTRA_PACKAGES is intentionally not quoted to allow word splitting
|
||||
# This enables passing multiple package names separated by spaces
|
||||
# shellcheck disable=SC2086
|
||||
sudo -E -u www-data composer require $COMPOSER_EXTRA_PACKAGES --no-install --no-interaction --no-progress
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Running composer install to install packages without dev dependencies..."
|
||||
sudo -E -u www-data composer install --no-dev --no-interaction --no-progress --optimize-autoloader
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Successfully installed additional composer packages"
|
||||
else
|
||||
echo "Failed to install composer dependencies"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Failed to add additional composer packages to composer.json"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Start PHP-FPM (the PHP_VERSION is replaced by the configured version in the Dockerfile)
|
||||
php-fpmPHP_VERSION -F &
|
||||
|
||||
|
|
|
|||
38
.env
38
.env
|
|
@ -31,8 +31,7 @@ DATABASE_EMULATE_NATURAL_SORT=0
|
|||
# General settings
|
||||
###################################################################################
|
||||
|
||||
# The public reachable URL of this Part-DB installation. This is used for generating links in SAML and email templates
|
||||
# This must end with a slash!
|
||||
# The public reachable URL of this Part-DB installation. This is used for generating links in SAML and email templates or when no request context is available.
|
||||
DEFAULT_URI="https://partdb.changeme.invalid/"
|
||||
|
||||
###################################################################################
|
||||
|
|
@ -60,6 +59,28 @@ ERROR_PAGE_ADMIN_EMAIL=''
|
|||
# If this is set to true, solutions to common problems are shown on error pages. Disable this, if you do not want your users to see them...
|
||||
ERROR_PAGE_SHOW_HELP=1
|
||||
|
||||
###################################################################################
|
||||
# Update Manager settings
|
||||
###################################################################################
|
||||
|
||||
# Disable web-based updates from the Update Manager UI (0=enabled, 1=disabled).
|
||||
# When disabled, use the CLI command "php bin/console partdb:update" instead.
|
||||
DISABLE_WEB_UPDATES=1
|
||||
|
||||
# Disable backup restore from the Update Manager UI (0=enabled, 1=disabled).
|
||||
# Restoring backups is a destructive operation that could overwrite your database.
|
||||
DISABLE_BACKUP_RESTORE=1
|
||||
|
||||
# Disable backup download from the Update Manager UI (0=enabled, 1=disabled).
|
||||
# Backups contain sensitive data including password hashes and secrets.
|
||||
# When enabled, users must confirm their password before downloading.
|
||||
DISABLE_BACKUP_DOWNLOAD=1
|
||||
|
||||
# Watchtower integration for Docker-based updates.
|
||||
# Set these to enable one-click updates via the Update Manager UI.
|
||||
# See https://containrrr.dev/watchtower/ for Watchtower setup.
|
||||
WATCHTOWER_API_URL=
|
||||
WATCHTOWER_API_TOKEN=
|
||||
|
||||
###################################################################################
|
||||
# SAML Single sign on-settings
|
||||
|
|
@ -106,6 +127,10 @@ SAML_SP_PRIVATE_KEY="MIIE..."
|
|||
# In demo mode things it is not possible for a user to change his password and his settings.
|
||||
DEMO_MODE=0
|
||||
|
||||
# When this is set to 1, users can make Part-DB directly download a file specified as a URL from the local network and create it as a local file.
|
||||
# This allows users access to all resources available in the local network, which could be a security risk, so use this only if you trust your users and have a secure local network.
|
||||
ALLOW_ATTACHMENT_DOWNLOADS_FROM_LOCALNETWORK=0
|
||||
|
||||
# Change this to true, if no url rewriting (like mod_rewrite for Apache) is available
|
||||
# In that case all URL contains the index.php front controller in URL
|
||||
NO_URL_REWRITE_AVAILABLE=0
|
||||
|
|
@ -134,4 +159,13 @@ CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
|||
###> symfony/framework-bundle ###
|
||||
APP_ENV=prod
|
||||
APP_SECRET=a03498528f5a5fc089273ec9ae5b2849
|
||||
APP_SHARE_DIR=var/share
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> symfony/ai-generic-platform ###
|
||||
# GENERIC_BASE_URL=https://api.example.com/v1
|
||||
###< symfony/ai-generic-platform ###
|
||||
|
||||
###> symfony/ai-open-router-platform ###
|
||||
OPENROUTER_API_KEY=
|
||||
###< symfony/ai-open-router-platform ###
|
||||
|
|
|
|||
186
.github/copilot-instructions.md
vendored
Normal file
186
.github/copilot-instructions.md
vendored
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
# Copilot Instructions for Part-DB
|
||||
|
||||
Part-DB is an Open-Source inventory management system for electronic components built with Symfony 7.4 and modern web technologies.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Backend**: PHP 8.2+, Symfony 7.4, Doctrine ORM
|
||||
- **Frontend**: Bootstrap 5, Hotwire Stimulus/Turbo, TypeScript, Webpack Encore
|
||||
- **Database**: MySQL 5.7+/MariaDB 10.4+/PostgreSQL 10+/SQLite
|
||||
- **Testing**: PHPUnit with DAMA Doctrine Test Bundle
|
||||
- **Code Quality**: Easy Coding Standard (ECS), PHPStan (level 5)
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `src/`: PHP application code organized by purpose (Controller, Entity, Service, Form, etc.)
|
||||
- `assets/`: Frontend TypeScript/JavaScript and CSS files
|
||||
- `templates/`: Twig templates for views
|
||||
- `tests/`: PHPUnit tests mirroring the `src/` structure
|
||||
- `config/`: Symfony configuration files
|
||||
- `public/`: Web-accessible files
|
||||
- `translations/`: Translation files for multi-language support
|
||||
|
||||
## Coding Standards
|
||||
|
||||
### PHP Code
|
||||
|
||||
- Follow [PSR-12](https://www.php-fig.org/psr/psr-12/) and [Symfony coding standards](https://symfony.com/doc/current/contributing/code/standards.html)
|
||||
- Use type hints for all parameters and return types
|
||||
- Always declare strict types: `declare(strict_types=1);` at the top of PHP files
|
||||
- Use PHPDoc blocks for complex logic or when type information is needed
|
||||
|
||||
### TypeScript/JavaScript
|
||||
|
||||
- Use TypeScript for new frontend code
|
||||
- Follow existing Stimulus controller patterns in `assets/controllers/`
|
||||
- Use Bootstrap 5 components and utilities
|
||||
- Leverage Hotwire Turbo for dynamic page updates
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- Entities: Use descriptive names that reflect database models (e.g., `Part`, `StorageLocation`)
|
||||
- Controllers: Suffix with `Controller` (e.g., `PartController`)
|
||||
- Services: Descriptive names reflecting their purpose (e.g., `PartService`, `LabelGenerator`)
|
||||
- Tests: Match the class being tested with `Test` suffix (e.g., `PartTest`, `PartControllerTest`)
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Install PHP dependencies: `composer install`
|
||||
- Install JS dependencies: `yarn install`
|
||||
- Build frontend assets: `yarn build` (production) or `yarn watch` (development)
|
||||
|
||||
### Database
|
||||
|
||||
- Create database: `php bin/console doctrine:database:create --env=dev`
|
||||
- Run migrations: `php bin/console doctrine:migrations:migrate --env=dev`
|
||||
- Load fixtures: `php bin/console partdb:fixtures:load -n --env=dev`
|
||||
|
||||
Or use Makefile shortcuts:
|
||||
- `make dev-setup`: Complete development environment setup
|
||||
- `make dev-reset`: Reset development environment (cache clear + migrate)
|
||||
|
||||
### Testing
|
||||
|
||||
- Set up test environment: `make test-setup`
|
||||
- Run all tests: `php bin/phpunit`
|
||||
- Run specific test: `php bin/phpunit tests/Path/To/SpecificTest.php`
|
||||
- Run tests with coverage: `php bin/phpunit --coverage-html var/coverage`
|
||||
- Test environment uses SQLite by default for speed
|
||||
|
||||
### Static Analysis
|
||||
|
||||
- Run PHPStan: `composer phpstan` or `COMPOSER_MEMORY_LIMIT=-1 php -d memory_limit=1G vendor/bin/phpstan analyse src --level 5`
|
||||
- PHPStan configuration is in `phpstan.dist.neon`
|
||||
|
||||
### Running the Application
|
||||
|
||||
- Development server: `symfony serve` (requires Symfony CLI)
|
||||
- Or configure Apache/nginx to serve from `public/` directory
|
||||
- Set `APP_ENV=dev` in `.env.local` for development mode
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Security
|
||||
|
||||
- Always sanitize user input
|
||||
- Use Symfony's security component for authentication/authorization
|
||||
- Check permissions using the permission system before allowing actions
|
||||
- Never expose sensitive data in logs or error messages
|
||||
- Use parameterized queries (Doctrine handles this automatically)
|
||||
|
||||
### Performance
|
||||
|
||||
- Use Doctrine query builder for complex queries instead of DQL when possible
|
||||
- Lazy load relationships to avoid N+1 queries
|
||||
- Cache results when appropriate using Symfony's cache component
|
||||
- Use pagination for large result sets (DataTables integration available)
|
||||
|
||||
### Database
|
||||
|
||||
- Always create migrations for schema changes: `php bin/console make:migration`
|
||||
- Review migration files before running them
|
||||
- Use Doctrine annotations or attributes for entity mapping
|
||||
- Follow existing entity patterns for relationships and lifecycle callbacks
|
||||
|
||||
### Frontend
|
||||
|
||||
- Use Stimulus controllers for interactive components
|
||||
- Leverage Turbo for dynamic page updates without full page reloads
|
||||
- Use Bootstrap 5 classes for styling
|
||||
- Keep JavaScript modular and organized in controllers
|
||||
- Use the translation system for user-facing strings
|
||||
|
||||
### Translations
|
||||
|
||||
- Use translation keys, not hardcoded strings: `{{ 'part.info.title'|trans }}`
|
||||
- Add new translation keys to `translations/` files
|
||||
- Primary language is English (en)
|
||||
- Translations are managed via Crowdin, but can be edited locally if needed
|
||||
|
||||
### Testing
|
||||
|
||||
- Write unit tests for services and helpers
|
||||
- Write functional tests for controllers
|
||||
- Use fixtures for test data
|
||||
- Tests should be isolated and not depend on execution order
|
||||
- Mock external dependencies when appropriate
|
||||
- Follow existing test patterns in the repository
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Creating an Entity
|
||||
|
||||
1. Create entity class in `src/Entity/` with Doctrine attributes
|
||||
2. Generate migration: `php bin/console make:migration`
|
||||
3. Review and run migration: `php bin/console doctrine:migrations:migrate`
|
||||
4. Create repository if needed in `src/Repository/`
|
||||
5. Add fixtures in `src/DataFixtures/` for testing
|
||||
|
||||
### Adding a Form
|
||||
|
||||
1. Create form type in `src/Form/`
|
||||
2. Extend `AbstractType` and implement `buildForm()` and `configureOptions()`
|
||||
3. Use in controller and render in Twig template
|
||||
4. Follow existing form patterns for consistency
|
||||
|
||||
### Creating a Controller Action
|
||||
|
||||
1. Add method to appropriate controller in `src/Controller/`
|
||||
2. Use route attributes for routing
|
||||
3. Check permissions using security voters
|
||||
4. Return Response or render Twig template
|
||||
5. Add corresponding template in `templates/`
|
||||
|
||||
### Adding a Service
|
||||
|
||||
1. Create service class in `src/Services/`
|
||||
2. Use dependency injection via constructor
|
||||
3. Tag service in `config/services.yaml` if needed
|
||||
4. Services are autowired by default
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Part-DB uses fine-grained permissions - always check user permissions before actions
|
||||
- Multi-language support is critical - use translation keys everywhere
|
||||
- The application supports multiple database backends - write portable code
|
||||
- Responsive design is important - test on mobile/tablet viewports
|
||||
- Event system is used for logging changes - emit events when appropriate
|
||||
- API Platform is integrated for REST API endpoints
|
||||
|
||||
## Multi-tenancy Considerations
|
||||
|
||||
- Part-DB is designed as a single-tenant application with multiple users
|
||||
- User groups have different permission levels
|
||||
- Always scope queries to respect user permissions
|
||||
- Use the security context to get current user information
|
||||
|
||||
## Resources
|
||||
|
||||
- [Documentation](https://docs.part-db.de/)
|
||||
- [Contributing Guide](CONTRIBUTING.md)
|
||||
- [Symfony Documentation](https://symfony.com/doc/current/index.html)
|
||||
- [Doctrine Documentation](https://www.doctrine-project.org/projects/doctrine-orm/en/current/)
|
||||
- [Bootstrap 5 Documentation](https://getbootstrap.com/docs/5.1/)
|
||||
- [Hotwire Documentation](https://hotwired.dev/)
|
||||
26
.github/workflows/assets_artifact_build.yml
vendored
26
.github/workflows/assets_artifact_build.yml
vendored
|
|
@ -8,6 +8,9 @@ on:
|
|||
branches:
|
||||
- '*'
|
||||
- "!l10n_*" # Dont test localization branches
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
- 'v*.*.*-**'
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
|
@ -17,12 +20,14 @@ jobs:
|
|||
assets_artifact_build:
|
||||
name: Build assets artifact
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
APP_ENV: prod
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
|
|
@ -37,7 +42,7 @@ jobs:
|
|||
run: |
|
||||
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
|
|
@ -51,7 +56,7 @@ jobs:
|
|||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
|
@ -60,9 +65,9 @@ jobs:
|
|||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '22'
|
||||
|
||||
- name: Install yarn dependencies
|
||||
run: yarn install
|
||||
|
|
@ -80,13 +85,20 @@ jobs:
|
|||
run: zip -r /tmp/partdb_assets.zip public/build/ vendor/
|
||||
|
||||
- name: Upload assets artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: Only dependencies and built assets
|
||||
path: /tmp/partdb_assets.zip
|
||||
|
||||
- name: Upload full artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: Full Part-DB including dependencies and built assets
|
||||
path: /tmp/partdb_with_assets.zip
|
||||
|
||||
- name: Upload assets as release attachment
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
gh release upload "${{ github.ref_name }}" /tmp/partdb_assets.zip /tmp/partdb_with_assets.zip --clobber
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
|
|
|||
123
.github/workflows/docker_build.yml
vendored
123
.github/workflows/docker_build.yml
vendored
|
|
@ -15,16 +15,28 @@ on:
|
|||
- 'v*.*.*-**'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
platform-slug: amd64
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
platform-slug: arm64
|
||||
- platform: linux/arm/v7
|
||||
runner: ubuntu-24.04-arm
|
||||
platform-slug: armv7
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Docker meta
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
# list of Docker images to use as base name for tags
|
||||
images: |
|
||||
|
|
@ -32,13 +44,12 @@ jobs:
|
|||
# Mark the image build from master as latest (as we dont have really releases yet)
|
||||
tags: |
|
||||
type=edge,branch=master
|
||||
type=ref,event=branch,
|
||||
type=ref,event=tag,
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=schedule
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
labels: |
|
||||
org.opencontainers.image.source=${{ github.event.repository.clone_url }}
|
||||
|
|
@ -49,31 +60,101 @@ jobs:
|
|||
org.opencontainers.image.source=https://github.com/Part-DB/Part-DB-symfony
|
||||
org.opencontainers.image.authors=Jan Böhmer
|
||||
org.opencontainers.licenses=AGPL-3.0-or-later
|
||||
# Disable automatic 'latest' tag in build jobs - it will be created in merge job
|
||||
flavor: |
|
||||
latest=false
|
||||
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
outputs: type=image,name=jbtronics/part-db1,push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
||||
cache-from: type=gha,scope=build-${{ matrix.platform }}
|
||||
cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }}
|
||||
|
||||
-
|
||||
name: Export digest
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
-
|
||||
name: Upload digest
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-${{ matrix.platform-slug }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
-
|
||||
name: Download digests
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
-
|
||||
name: Docker meta
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
jbtronics/part-db1
|
||||
tags: |
|
||||
type=edge,branch=master
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=schedule
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=ref,event=pr
|
||||
|
||||
-
|
||||
name: Login to DockerHub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
-
|
||||
name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf 'jbtronics/part-db1@sha256:%s ' *)
|
||||
|
||||
-
|
||||
name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect jbtronics/part-db1:${{ steps.docker_meta.outputs.version }}
|
||||
|
|
|
|||
123
.github/workflows/docker_frankenphp.yml
vendored
123
.github/workflows/docker_frankenphp.yml
vendored
|
|
@ -15,16 +15,28 @@ on:
|
|||
- 'v*.*.*-**'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
platform-slug: amd64
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
platform-slug: arm64
|
||||
- platform: linux/arm/v7
|
||||
runner: ubuntu-24.04-arm
|
||||
platform-slug: armv7
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Docker meta
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
# list of Docker images to use as base name for tags
|
||||
images: |
|
||||
|
|
@ -32,13 +44,12 @@ jobs:
|
|||
# Mark the image build from master as latest (as we dont have really releases yet)
|
||||
tags: |
|
||||
type=edge,branch=master
|
||||
type=ref,event=branch,
|
||||
type=ref,event=tag,
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=schedule
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
labels: |
|
||||
org.opencontainers.image.source=${{ github.event.repository.clone_url }}
|
||||
|
|
@ -49,32 +60,102 @@ jobs:
|
|||
org.opencontainers.image.source=https://github.com/Part-DB/Part-DB-server
|
||||
org.opencontainers.image.authors=Jan Böhmer
|
||||
org.opencontainers.licenses=AGPL-3.0-or-later
|
||||
# Disable automatic 'latest' tag in build jobs - it will be created in merge job
|
||||
flavor: |
|
||||
latest=false
|
||||
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: 'arm64,arm'
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile-frankenphp
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
outputs: type=image,name=partdborg/part-db,push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
|
||||
cache-from: type=gha,scope=build-${{ matrix.platform }}
|
||||
cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }}
|
||||
|
||||
-
|
||||
name: Export digest
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
-
|
||||
name: Upload digest
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-${{ matrix.platform-slug }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
-
|
||||
name: Download digests
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: /tmp/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
-
|
||||
name: Docker meta
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
partdborg/part-db
|
||||
tags: |
|
||||
type=edge,branch=master
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=schedule
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=ref,event=pr
|
||||
|
||||
-
|
||||
name: Login to DockerHub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
-
|
||||
name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf 'partdborg/part-db@sha256:%s ' *)
|
||||
|
||||
-
|
||||
name: Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect partdborg/part-db:${{ steps.docker_meta.outputs.version }}
|
||||
|
|
|
|||
4
.github/workflows/static_analysis.yml
vendored
4
.github/workflows/static_analysis.yml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
|
|
@ -34,7 +34,7 @@ jobs:
|
|||
run: |
|
||||
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
|
|
|
|||
12
.github/workflows/tests.yml
vendored
12
.github/workflows/tests.yml
vendored
|
|
@ -46,7 +46,7 @@ jobs:
|
|||
if: matrix.db-type == 'postgres'
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
|
|
@ -81,7 +81,7 @@ jobs:
|
|||
id: composer-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
|
|
@ -92,7 +92,7 @@ jobs:
|
|||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
|
|
@ -104,9 +104,9 @@ jobs:
|
|||
run: composer install --prefer-dist --no-progress
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '22'
|
||||
|
||||
- name: Install yarn dependencies
|
||||
run: yarn install
|
||||
|
|
@ -129,7 +129,7 @@ jobs:
|
|||
run: ./bin/phpunit --coverage-clover=coverage.xml
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
env_vars: PHP_VERSION,DB_TYPE
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
|
|||
13
.gitignore
vendored
13
.gitignore
vendored
|
|
@ -25,6 +25,10 @@
|
|||
uploads/*
|
||||
!uploads/.keep
|
||||
|
||||
# Some people use Certbot or similar tools to make SSL certificates.
|
||||
# Also see https://www.rfc-editor.org/rfc/rfc5785
|
||||
public/.well-known/
|
||||
|
||||
# Do not keep cache files
|
||||
.php_cs.cache
|
||||
.phpcs-cache
|
||||
|
|
@ -50,4 +54,11 @@ phpstan.neon
|
|||
###< phpstan/phpstan ###
|
||||
|
||||
.claude/
|
||||
CLAUDE.md
|
||||
CLAUDE.md
|
||||
|
||||
.codex
|
||||
migrations/.codex
|
||||
docker-data/
|
||||
scripts/
|
||||
db/
|
||||
docker-compose.yaml
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# How to contribute
|
||||
|
||||
Thank you for consider to contribute to Part-DB!
|
||||
Please read the text below, so your contributed content can be contributed easily to Part-DB.
|
||||
Thank you for considering contributing to Part-DB!
|
||||
Please read the text below, so your contributed content can be incorporated into Part-DB easily.
|
||||
|
||||
You can contribute to Part-DB in various ways:
|
||||
* Report bugs and request new features via [issues](https://github.com/Part-DB/Part-DB-server/issues)
|
||||
|
|
@ -18,38 +18,38 @@ Part-DB uses translation keys (e.g. part.info.title) that are sorted by their us
|
|||
was translated in other languages (this is possible via the "Other languages" dropdown in the translation editor).
|
||||
|
||||
## Project structure
|
||||
Part-DB uses symfony's recommended [project structure](https://symfony.com/doc/current/best_practices.html).
|
||||
Part-DB uses Symfony's recommended [project structure](https://symfony.com/doc/current/best_practices.html).
|
||||
Interesting folders are:
|
||||
* `public`: Everything in this directory will be publicy accessible via web. Use this folder to serve static images.
|
||||
* `assets`: The frontend assets are saved here. You can find the javascript and CSS code here.
|
||||
* `src`: Part-DB's PHP code is saved here. Note that the sub directories are structured by the classes purposes (so use `Controller` Controllers, `Entities` for Database models, etc.)
|
||||
* `translations`: The translations used in Part-DB are saved here
|
||||
* `public`: Everything in this directory will be publicly accessible via web. Use this folder to serve static images.
|
||||
* `assets`: The frontend assets are saved here. You can find the JavaScript and CSS code here.
|
||||
* `src`: Part-DB's PHP code is saved here. Note that the subdirectories are structured by the classes' purposes (so use `Controller` for Controllers, `Entity` for Database models, etc.)
|
||||
* `translations`: The translations used in Part-DB are saved here.
|
||||
* `templates`: The templates (HTML) that are used by Twig to render the different pages. Email templates are also saved here.
|
||||
* `tests/`: Tests that can be run by PHPunit.
|
||||
* `tests/`: Tests that can be run by PHPUnit.
|
||||
|
||||
## Development environment
|
||||
For setting up an development you will need to install PHP, composer, a database server (MySQL or MariaDB) and yarn (which needs an nodejs environment).
|
||||
* Copy `.env` to `.env.local` and change `APP_ENV` to `APP_ENV=dev`. That way you will get development tools (symfony profiler) and other features that
|
||||
For setting up a development environment, you will need to install PHP, Composer, a database server (MySQL or MariaDB) and yarn (which needs a Node.js environment).
|
||||
* Copy `.env` to `.env.local` and change `APP_ENV` to `APP_ENV=dev`. That way you will get development tools (Symfony profiler) and other features that
|
||||
will simplify development.
|
||||
* Run `composer install` (without -o) to install PHP dependencies and `yarn install` to install frontend dependencies
|
||||
* Run `yarn watch`. The program will run in the background and compile the frontend files whenever you change something in the CSS or TypeScript files
|
||||
* For running Part-DB it is recommended to use [Symfony CLI](https://symfony.com/download).
|
||||
That way you can run a correct configured webserver with `symfony serve`
|
||||
* Run `composer install` (without -o) to install PHP dependencies and `yarn install` to install frontend dependencies.
|
||||
* Run `yarn watch`. The program will run in the background and compile the frontend files whenever you change something in the CSS or TypeScript files.
|
||||
* For running Part-DB, it is recommended to use [Symfony CLI](https://symfony.com/download).
|
||||
That way you can run a correctly configured webserver with `symfony serve`.
|
||||
|
||||
## Coding style
|
||||
Code should follow the [PSR12-Standard](https://www.php-fig.org/psr/psr-12/) and symfony's [coding standards](https://symfony.com/doc/current/contributing/code/standards.html).
|
||||
Code should follow the [PSR-12 Standard](https://www.php-fig.org/psr/psr-12/) and Symfony's [coding standards](https://symfony.com/doc/current/contributing/code/standards.html).
|
||||
|
||||
Part-DB uses [Easy Coding Standard](https://github.com/symplify/easy-coding-standard) to check and fix coding style violations:
|
||||
* To check your code for valid code style run `vendor/bin/ecs check src/`
|
||||
* To fix violations run `vendor/bin/ecs check src/` (please checks afterwards if the code is valid afterwards)
|
||||
* To check your code for valid code style, run `vendor/bin/ecs check src/`
|
||||
* To fix violations, run `vendor/bin/ecs check src/ --fix` (please check afterwards if the code is still valid)
|
||||
|
||||
## GitHub actions
|
||||
Part-DB uses GitHub actions to run various tests and checks on the code:
|
||||
Part-DB uses GitHub Actions to run various tests and checks on the code:
|
||||
* Yarn dependencies can compile
|
||||
* PHPunit tests run successful
|
||||
* Config files, translations and templates has valid syntax
|
||||
* Doctrine schema valid
|
||||
* No known vulnerable dependecies are used
|
||||
* Static analysis successful (phpstan with `--level=2`)
|
||||
* PHPUnit tests run successfully
|
||||
* Config files, translations, and templates have valid syntax
|
||||
* Doctrine schema is valid
|
||||
* No known vulnerable dependencies are used
|
||||
* Static analysis is successful (phpstan with `--level=2`)
|
||||
|
||||
Further the code coverage of the PHPunit tests is determined and uploaded to [CodeCov](https://codecov.io/gh/Part-DB/Part-DB-server).
|
||||
Further, the code coverage of the PHPUnit tests is determined and uploaded to [CodeCov](https://codecov.io/gh/Part-DB/Part-DB-server).
|
||||
|
|
|
|||
117
Dockerfile
117
Dockerfile
|
|
@ -1,15 +1,75 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
ARG BASE_IMAGE=debian:bookworm-slim
|
||||
ARG PHP_VERSION=8.4
|
||||
ARG NODE_VERSION=22
|
||||
# Node.js build stage for building frontend assets
|
||||
# Use native platform for build stage as it's platform-independent
|
||||
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-bookworm-slim AS node-builder
|
||||
ARG TARGETARCH
|
||||
WORKDIR /app
|
||||
|
||||
# Install composer and minimal PHP for running Symfony commands
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Use BuildKit cache mounts for apt in builder stage
|
||||
RUN --mount=type=cache,id=apt-cache-node-$TARGETARCH,target=/var/cache/apt \
|
||||
--mount=type=cache,id=apt-lists-node-$TARGETARCH,target=/var/lib/apt/lists \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
php-cli \
|
||||
php-xml \
|
||||
php-mbstring \
|
||||
unzip \
|
||||
git \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy composer files and install dependencies (needed for Symfony UX assets)
|
||||
COPY composer.json composer.lock symfony.lock ./
|
||||
|
||||
# Use BuildKit cache for Composer downloads
|
||||
RUN --mount=type=cache,id=composer-cache,target=/root/.cache/composer \
|
||||
composer install --no-scripts --no-autoloader --no-dev --prefer-dist --ignore-platform-reqs
|
||||
|
||||
# Copy all application files needed for cache warmup and webpack build
|
||||
COPY .env* ./
|
||||
COPY bin ./bin
|
||||
COPY config ./config
|
||||
COPY src ./src
|
||||
COPY translations ./translations
|
||||
COPY public ./public
|
||||
COPY assets ./assets
|
||||
COPY webpack.config.js ./
|
||||
|
||||
# Generate autoloader
|
||||
RUN composer dump-autoload
|
||||
|
||||
# Create required directories for cache warmup
|
||||
RUN mkdir -p var/cache var/log uploads public/media
|
||||
|
||||
# Dump translations, which we need for cache warmup
|
||||
RUN php bin/console cache:warmup -n --env=prod 2>&1
|
||||
|
||||
# Copy package files and install node dependencies
|
||||
COPY package.json yarn.lock ./
|
||||
# Use BuildKit cache for yarn/npm
|
||||
RUN --mount=type=cache,id=yarn-cache,target=/root/.cache/yarn \
|
||||
--mount=type=cache,id=npm-cache,target=/root/.npm \
|
||||
yarn install --network-timeout 600000
|
||||
|
||||
# Build the assets
|
||||
RUN yarn build
|
||||
|
||||
# Clean up
|
||||
RUN yarn cache clean && rm -rf node_modules/
|
||||
|
||||
# Base stage for PHP
|
||||
FROM ${BASE_IMAGE} AS base
|
||||
ARG PHP_VERSION
|
||||
ARG TARGETARCH
|
||||
|
||||
# Install needed dependencies for PHP build
|
||||
#RUN apt-get update && apt-get install -y pkg-config curl libcurl4-openssl-dev libicu-dev \
|
||||
# libpng-dev libjpeg-dev libfreetype6-dev gnupg zip libzip-dev libjpeg62-turbo-dev libonig-dev libxslt-dev libwebp-dev vim \
|
||||
# && apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN apt-get update && apt-get -y install \
|
||||
# Use BuildKit cache mounts for apt in base stage
|
||||
RUN --mount=type=cache,id=apt-cache-$TARGETARCH,target=/var/cache/apt \
|
||||
--mount=type=cache,id=apt-lists-$TARGETARCH,target=/var/lib/apt/lists \
|
||||
apt-get update && apt-get -y install \
|
||||
apt-transport-https \
|
||||
lsb-release \
|
||||
ca-certificates \
|
||||
|
|
@ -39,21 +99,10 @@ RUN apt-get update && apt-get -y install \
|
|||
gpg \
|
||||
sudo \
|
||||
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/* \
|
||||
# Create workdir and set permissions if directory does not exists
|
||||
&& mkdir -p /var/www/html \
|
||||
&& chown -R www-data:www-data /var/www/html \
|
||||
# delete the "index.html" that installing Apache drops in here
|
||||
&& rm -rvf /var/www/html/*
|
||||
|
||||
# Install node and yarn
|
||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
|
||||
curl -sL https://deb.nodesource.com/setup_22.x | bash - && \
|
||||
apt-get update && apt-get install -y \
|
||||
nodejs \
|
||||
yarn \
|
||||
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
|
|
@ -67,14 +116,12 @@ ENV APACHE_ENVVARS=$APACHE_CONFDIR/envvars
|
|||
# : ${APACHE_RUN_USER:=www-data}
|
||||
# export APACHE_RUN_USER
|
||||
# so that they can be overridden at runtime ("-e APACHE_RUN_USER=...")
|
||||
RUN sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS"; \
|
||||
set -eux; . "$APACHE_ENVVARS"; \
|
||||
\
|
||||
# logs should go to stdout / stderr
|
||||
ln -sfT /dev/stderr "$APACHE_LOG_DIR/error.log"; \
|
||||
ln -sfT /dev/stdout "$APACHE_LOG_DIR/access.log"; \
|
||||
ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log"; \
|
||||
chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR";
|
||||
RUN sed -ri 's/^export ([^=]+)=(.*)$/: ${\1:=\2}\nexport \1/' "$APACHE_ENVVARS" && \
|
||||
set -eux; . "$APACHE_ENVVARS" && \
|
||||
ln -sfT /dev/stderr "$APACHE_LOG_DIR/error.log" && \
|
||||
ln -sfT /dev/stdout "$APACHE_LOG_DIR/access.log" && \
|
||||
ln -sfT /dev/stdout "$APACHE_LOG_DIR/other_vhosts_access.log" && \
|
||||
chown -R --no-dereference "$APACHE_RUN_USER:$APACHE_RUN_GROUP" "$APACHE_LOG_DIR"
|
||||
|
||||
# ---
|
||||
|
||||
|
|
@ -143,7 +190,6 @@ COPY --chown=www-data:www-data . .
|
|||
# Setup apache2
|
||||
RUN a2dissite 000-default.conf && \
|
||||
a2ensite symfony.conf && \
|
||||
# Enable php-fpm
|
||||
a2enmod proxy_fcgi setenvif && \
|
||||
a2enconf php${PHP_VERSION}-fpm && \
|
||||
a2enconf docker-php && \
|
||||
|
|
@ -151,12 +197,13 @@ RUN a2dissite 000-default.conf && \
|
|||
|
||||
# Install composer and yarn dependencies for Part-DB
|
||||
USER www-data
|
||||
RUN composer install -a --no-dev && \
|
||||
# Use BuildKit cache for Composer when running as www-data by setting COMPOSER_CACHE_DIR
|
||||
RUN --mount=type=cache,id=composer-cache,target=/tmp/.composer-cache \
|
||||
COMPOSER_CACHE_DIR=/tmp/.composer-cache composer install -a --no-dev && \
|
||||
composer clear-cache
|
||||
RUN yarn install --network-timeout 600000 && \
|
||||
yarn build && \
|
||||
yarn cache clean && \
|
||||
rm -rf node_modules/
|
||||
|
||||
# Copy built frontend assets from node-builder stage
|
||||
COPY --from=node-builder --chown=www-data:www-data /app/public/build ./public/build
|
||||
|
||||
# Use docker env to output logs to stdout
|
||||
ENV APP_ENV=docker
|
||||
|
|
@ -168,10 +215,12 @@ USER root
|
|||
RUN sed -i "s/PHP_VERSION/${PHP_VERSION}/g" ./.docker/partdb-entrypoint.sh
|
||||
|
||||
# Copy entrypoint and apache2-foreground to /usr/local/bin and make it executable
|
||||
RUN install ./.docker/partdb-entrypoint.sh /usr/local/bin && \
|
||||
install ./.docker/apache2-foreground /usr/local/bin
|
||||
# Convert CRLF -> LF and install entrypoint scripts with executable mode
|
||||
RUN sed -i 's/\r$//' ./.docker/partdb-entrypoint.sh ./.docker/apache2-foreground && \
|
||||
install -m 0755 ./.docker/partdb-entrypoint.sh /usr/local/bin/ && \
|
||||
install -m 0755 ./.docker/apache2-foreground /usr/local/bin/
|
||||
ENTRYPOINT ["partdb-entrypoint.sh"]
|
||||
CMD ["apache2-foreground"]
|
||||
CMD ["/usr/local/bin/apache2-foreground"]
|
||||
|
||||
# https://httpd.apache.org/docs/2.4/stopping.html#gracefulstop
|
||||
STOPSIGNAL SIGWINCH
|
||||
|
|
|
|||
|
|
@ -1,6 +1,72 @@
|
|||
FROM dunglas/frankenphp:1-php8.4 AS frankenphp_upstream
|
||||
ARG NODE_VERSION=22
|
||||
|
||||
RUN apt-get update && apt-get -y install \
|
||||
# Node.js build stage for building frontend assets
|
||||
# Use native platform for build stage as it's platform-independent
|
||||
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-bookworm-slim AS node-builder
|
||||
ARG TARGETARCH
|
||||
WORKDIR /app
|
||||
|
||||
# Install composer and minimal PHP for running Symfony commands
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Use BuildKit cache mounts for apt in builder stage
|
||||
RUN --mount=type=cache,id=apt-cache-node-$TARGETARCH,target=/var/cache/apt \
|
||||
--mount=type=cache,id=apt-lists-node-$TARGETARCH,target=/var/lib/apt/lists \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
php-cli \
|
||||
php-xml \
|
||||
php-mbstring \
|
||||
unzip \
|
||||
git \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy composer files and install dependencies (needed for Symfony UX assets)
|
||||
COPY composer.json composer.lock symfony.lock ./
|
||||
|
||||
# Use BuildKit cache for Composer downloads
|
||||
RUN --mount=type=cache,id=composer-cache,target=/root/.cache/composer \
|
||||
composer install --no-scripts --no-autoloader --no-dev --prefer-dist --ignore-platform-reqs
|
||||
|
||||
# Copy all application files needed for cache warmup and webpack build
|
||||
COPY .env* ./
|
||||
COPY bin ./bin
|
||||
COPY config ./config
|
||||
COPY src ./src
|
||||
COPY translations ./translations
|
||||
COPY public ./public
|
||||
COPY assets ./assets
|
||||
COPY webpack.config.js ./
|
||||
|
||||
# Generate autoloader
|
||||
RUN composer dump-autoload
|
||||
|
||||
# Create required directories for cache warmup
|
||||
RUN mkdir -p var/cache var/log uploads public/media
|
||||
|
||||
# Dump translations, which we need for cache warmup
|
||||
RUN php bin/console cache:warmup -n --env=prod 2>&1
|
||||
|
||||
# Copy package files and install node dependencies
|
||||
COPY package.json yarn.lock ./
|
||||
|
||||
# Use BuildKit cache for yarn/npm
|
||||
RUN --mount=type=cache,id=yarn-cache,target=/root/.cache/yarn \
|
||||
--mount=type=cache,id=npm-cache,target=/root/.npm \
|
||||
yarn install --network-timeout 600000
|
||||
|
||||
|
||||
# Build the assets
|
||||
RUN yarn build
|
||||
|
||||
# Clean up
|
||||
RUN yarn cache clean && rm -rf node_modules/
|
||||
|
||||
# FrankenPHP base stage
|
||||
FROM dunglas/frankenphp:1-php8.4-bookworm AS frankenphp_upstream
|
||||
ARG TARGETARCH
|
||||
RUN --mount=type=cache,id=apt-cache-$TARGETARCH,target=/var/cache/apt \
|
||||
--mount=type=cache,id=apt-lists-$TARGETARCH,target=/var/lib/apt/lists \
|
||||
apt-get update && apt-get -y install \
|
||||
curl \
|
||||
ca-certificates \
|
||||
mariadb-client \
|
||||
|
|
@ -13,34 +79,6 @@ RUN apt-get update && apt-get -y install \
|
|||
zip \
|
||||
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
RUN set -eux; \
|
||||
# Prepare keyrings directory
|
||||
mkdir -p /etc/apt/keyrings; \
|
||||
\
|
||||
# Import Yarn GPG key
|
||||
curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg \
|
||||
| tee /etc/apt/keyrings/yarn.gpg >/dev/null; \
|
||||
chmod 644 /etc/apt/keyrings/yarn.gpg; \
|
||||
\
|
||||
# Add Yarn repo with signed-by
|
||||
echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian stable main" \
|
||||
| tee /etc/apt/sources.list.d/yarn.list; \
|
||||
\
|
||||
# Run NodeSource setup script (unchanged)
|
||||
curl -sL https://deb.nodesource.com/setup_22.x | bash -; \
|
||||
\
|
||||
# Install Node.js + Yarn
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
nodejs \
|
||||
yarn; \
|
||||
\
|
||||
# Cleanup
|
||||
apt-get -y autoremove; \
|
||||
apt-get clean autoclean; \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
# Install PHP
|
||||
RUN set -eux; \
|
||||
install-php-extensions \
|
||||
|
|
@ -86,14 +124,11 @@ COPY --link . ./
|
|||
RUN set -eux; \
|
||||
mkdir -p var/cache var/log; \
|
||||
composer dump-autoload --classmap-authoritative --no-dev; \
|
||||
composer dump-env prod; \
|
||||
composer run-script --no-dev post-install-cmd; \
|
||||
chmod +x bin/console; sync;
|
||||
|
||||
RUN yarn install --network-timeout 600000 && \
|
||||
yarn build && \
|
||||
yarn cache clean && \
|
||||
rm -rf node_modules/
|
||||
# Copy built frontend assets from node-builder stage
|
||||
COPY --from=node-builder /app/public/build ./public/build
|
||||
|
||||
# Use docker env to output logs to stdout
|
||||
ENV APP_ENV=docker
|
||||
|
|
@ -112,8 +147,8 @@ VOLUME ["/var/www/html/uploads", "/var/www/html/public/media"]
|
|||
HEALTHCHECK --start-period=60s CMD curl -f http://localhost:2019/metrics || exit 1
|
||||
|
||||
# See https://caddyserver.com/docs/conventions#file-locations for details
|
||||
ENV XDG_CONFIG_HOME /config
|
||||
ENV XDG_DATA_HOME /data
|
||||
ENV XDG_CONFIG_HOME=/config
|
||||
ENV XDG_DATA_HOME=/data
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -1,4 +1,3 @@
|
|||
[](https://scrutinizer-ci.com/g/Part-DB/Part-DB-symfony/?branch=master)
|
||||

|
||||

|
||||
[](https://codecov.io/gh/Part-DB/Part-DB-server)
|
||||
|
|
@ -29,8 +28,8 @@ If you want to test Part-DB without installing it, you can use [this](https://de
|
|||
|
||||
You can log in with username: *user* and password: *user*.
|
||||
|
||||
Every change to the master branch gets automatically deployed, so it represents the current development progress and is
|
||||
may not completely stable. Please mind, that the free Heroku instance is used, so it can take some time when loading
|
||||
Every change to the master branch gets automatically deployed, so it represents the current development progress and
|
||||
may not be completely stable. Please mind, that the free Heroku instance is used, so it can take some time when loading
|
||||
the page
|
||||
for the first time.
|
||||
|
||||
|
|
@ -62,6 +61,8 @@ for the first time.
|
|||
* Automatic thumbnail generation for pictures
|
||||
* Use cloud providers (like Octopart, Digikey, Farnell, LCSC or TME) to automatically get part information, datasheets, and
|
||||
prices for parts
|
||||
* Retrieve part information from arbitrary shop websites, using either conventional data extraction from structured metadata, or AI based data extraction.
|
||||
A browser plugin allows to quickly submit parts from any website to your Part-DB instance, and even allows to circumvent anti-bot measures on shop websites.
|
||||
* API to access Part-DB from other applications/scripts
|
||||
* [Integration with KiCad](https://docs.part-db.de/usage/eda_integration.html): Use Part-DB as the central datasource for your
|
||||
KiCad and see available parts from Part-DB directly inside KiCad.
|
||||
|
|
@ -74,11 +75,11 @@ Part-DB is also used by small companies and universities for managing their inve
|
|||
## Requirements
|
||||
|
||||
* A **web server** (like Apache2 or nginx) that is capable of
|
||||
running [Symfony 6](https://symfony.com/doc/current/reference/requirements.html),
|
||||
running [Symfony 7](https://symfony.com/doc/current/reference/requirements.html),
|
||||
this includes a minimum PHP version of **PHP 8.2**
|
||||
* A **MySQL** (at least 5.7) /**MariaDB** (at least 10.4) database server, or **PostgreSQL** 10+ if you do not want to use SQLite.
|
||||
* Shell access to your server is highly recommended!
|
||||
* For building the client-side assets **yarn** and **nodejs** (>= 20.0) is needed.
|
||||
* For building the client-side assets **yarn** and **nodejs** (>= 22.0) is needed.
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -142,7 +143,7 @@ There you will find various methods to support development on a monthly or a one
|
|||
|
||||
## Built with
|
||||
|
||||
* [Symfony 5](https://symfony.com/): The main framework used for the serverside PHP
|
||||
* [Symfony 6](https://symfony.com/): The main framework used for the serverside PHP
|
||||
* [Bootstrap 5](https://getbootstrap.com/) and [Bootswatch](https://bootswatch.com/): Used as website theme
|
||||
* [Fontawesome](https://fontawesome.com/): Used as icon set
|
||||
* [Hotwire Stimulus](https://stimulus.hotwired.dev/) and [Hotwire Turbo](https://turbo.hotwired.dev/): Frontend
|
||||
|
|
|
|||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
2.2.1
|
||||
2.12.0
|
||||
|
|
|
|||
206
assets/commands/kicad_populate_default_mappings.json
Normal file
206
assets/commands/kicad_populate_default_mappings.json
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
{
|
||||
"_comment": "Default KiCad footprint/symbol mappings for partdb:kicad:populate command. Based on KiCad 9.x standard libraries. Use --mapping-file to override or extend these mappings.",
|
||||
"footprints": {
|
||||
"SOT-23": "Package_TO_SOT_SMD:SOT-23",
|
||||
"SOT-23-3": "Package_TO_SOT_SMD:SOT-23",
|
||||
"SOT-23-5": "Package_TO_SOT_SMD:SOT-23-5",
|
||||
"SOT-23-6": "Package_TO_SOT_SMD:SOT-23-6",
|
||||
"SOT-223": "Package_TO_SOT_SMD:SOT-223-3_TabPin2",
|
||||
"SOT-223-3": "Package_TO_SOT_SMD:SOT-223-3_TabPin2",
|
||||
"SOT-89": "Package_TO_SOT_SMD:SOT-89-3",
|
||||
"SOT-89-3": "Package_TO_SOT_SMD:SOT-89-3",
|
||||
"SOT-323": "Package_TO_SOT_SMD:SOT-323_SC-70",
|
||||
"SOT-363": "Package_TO_SOT_SMD:SOT-363_SC-70-6",
|
||||
"TSOT-25": "Package_TO_SOT_SMD:SOT-23-5",
|
||||
"SC-70-5": "Package_TO_SOT_SMD:SOT-353_SC-70-5",
|
||||
"SC-70-6": "Package_TO_SOT_SMD:SOT-363_SC-70-6",
|
||||
"TO-220": "Package_TO_SOT_THT:TO-220-3_Vertical",
|
||||
"TO-220AB": "Package_TO_SOT_THT:TO-220-3_Vertical",
|
||||
"TO-220AB-3": "Package_TO_SOT_THT:TO-220-3_Vertical",
|
||||
"TO-220FP": "Package_TO_SOT_THT:TO-220F-3_Vertical",
|
||||
"TO-247-3": "Package_TO_SOT_THT:TO-247-3_Vertical",
|
||||
"TO-92": "Package_TO_SOT_THT:TO-92_Inline",
|
||||
"TO-92-3": "Package_TO_SOT_THT:TO-92_Inline",
|
||||
"TO-252": "Package_TO_SOT_SMD:TO-252-2",
|
||||
"TO-252-2L": "Package_TO_SOT_SMD:TO-252-2",
|
||||
"TO-252-3L": "Package_TO_SOT_SMD:TO-252-3",
|
||||
"TO-263": "Package_TO_SOT_SMD:TO-263-2",
|
||||
"TO-263-2": "Package_TO_SOT_SMD:TO-263-2",
|
||||
"D2PAK": "Package_TO_SOT_SMD:TO-252-2",
|
||||
"DPAK": "Package_TO_SOT_SMD:TO-252-2",
|
||||
"SOIC-8": "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm",
|
||||
"ESOP-8": "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm",
|
||||
"SOIC-14": "Package_SO:SOIC-14_3.9x8.7mm_P1.27mm",
|
||||
"SOIC-16": "Package_SO:SOIC-16_3.9x9.9mm_P1.27mm",
|
||||
"TSSOP-8": "Package_SO:TSSOP-8_3x3mm_P0.65mm",
|
||||
"TSSOP-14": "Package_SO:TSSOP-14_4.4x5mm_P0.65mm",
|
||||
"TSSOP-16": "Package_SO:TSSOP-16_4.4x5mm_P0.65mm",
|
||||
"TSSOP-16L": "Package_SO:TSSOP-16_4.4x5mm_P0.65mm",
|
||||
"TSSOP-20": "Package_SO:TSSOP-20_4.4x6.5mm_P0.65mm",
|
||||
"MSOP-8": "Package_SO:MSOP-8_3x3mm_P0.65mm",
|
||||
"MSOP-10": "Package_SO:MSOP-10_3x3mm_P0.5mm",
|
||||
"MSOP-16": "Package_SO:MSOP-16_3x4mm_P0.5mm",
|
||||
"SO-5": "Package_TO_SOT_SMD:SOT-23-5",
|
||||
"DIP-4": "Package_DIP:DIP-4_W7.62mm",
|
||||
"DIP-6": "Package_DIP:DIP-6_W7.62mm",
|
||||
"DIP-8": "Package_DIP:DIP-8_W7.62mm",
|
||||
"DIP-14": "Package_DIP:DIP-14_W7.62mm",
|
||||
"DIP-16": "Package_DIP:DIP-16_W7.62mm",
|
||||
"DIP-18": "Package_DIP:DIP-18_W7.62mm",
|
||||
"DIP-20": "Package_DIP:DIP-20_W7.62mm",
|
||||
"DIP-24": "Package_DIP:DIP-24_W7.62mm",
|
||||
"DIP-28": "Package_DIP:DIP-28_W7.62mm",
|
||||
"DIP-40": "Package_DIP:DIP-40_W15.24mm",
|
||||
"QFN-8": "Package_DFN_QFN:QFN-8-1EP_3x3mm_P0.65mm_EP1.55x1.55mm",
|
||||
"QFN-12(3x3)": "Package_DFN_QFN:QFN-12-1EP_3x3mm_P0.5mm_EP1.65x1.65mm",
|
||||
"QFN-16": "Package_DFN_QFN:QFN-16-1EP_3x3mm_P0.5mm_EP1.45x1.45mm",
|
||||
"QFN-20": "Package_DFN_QFN:QFN-20-1EP_4x4mm_P0.5mm_EP2.5x2.5mm",
|
||||
"QFN-24": "Package_DFN_QFN:QFN-24-1EP_4x4mm_P0.5mm_EP2.45x2.45mm",
|
||||
"QFN-32": "Package_DFN_QFN:QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm",
|
||||
"QFN-48": "Package_DFN_QFN:QFN-48-1EP_7x7mm_P0.5mm_EP5.3x5.3mm",
|
||||
"TQFP-32": "Package_QFP:TQFP-32_7x7mm_P0.8mm",
|
||||
"TQFP-44": "Package_QFP:TQFP-44_10x10mm_P0.8mm",
|
||||
"TQFP-48": "Package_QFP:TQFP-48_7x7mm_P0.5mm",
|
||||
"TQFP-48(7x7)": "Package_QFP:TQFP-48_7x7mm_P0.5mm",
|
||||
"TQFP-64": "Package_QFP:TQFP-64_10x10mm_P0.5mm",
|
||||
"TQFP-100": "Package_QFP:TQFP-100_14x14mm_P0.5mm",
|
||||
"LQFP-32": "Package_QFP:LQFP-32_7x7mm_P0.8mm",
|
||||
"LQFP-48": "Package_QFP:LQFP-48_7x7mm_P0.5mm",
|
||||
"LQFP-64": "Package_QFP:LQFP-64_10x10mm_P0.5mm",
|
||||
"LQFP-100": "Package_QFP:LQFP-100_14x14mm_P0.5mm",
|
||||
|
||||
"SOD-123": "Diode_SMD:D_SOD-123",
|
||||
"SOD-123F": "Diode_SMD:D_SOD-123F",
|
||||
"SOD-123FL": "Diode_SMD:D_SOD-123F",
|
||||
"SOD-323": "Diode_SMD:D_SOD-323",
|
||||
"SOD-523": "Diode_SMD:D_SOD-523",
|
||||
"SOD-882": "Diode_SMD:D_SOD-882",
|
||||
"SOD-882D": "Diode_SMD:D_SOD-882",
|
||||
"SMA(DO-214AC)": "Diode_SMD:D_SMA",
|
||||
"SMA": "Diode_SMD:D_SMA",
|
||||
"SMB": "Diode_SMD:D_SMB",
|
||||
"SMC": "Diode_SMD:D_SMC",
|
||||
|
||||
"DO-35": "Diode_THT:D_DO-35_SOD27_P7.62mm_Horizontal",
|
||||
"DO-35(DO-204AH)": "Diode_THT:D_DO-35_SOD27_P7.62mm_Horizontal",
|
||||
"DO-41": "Diode_THT:D_DO-41_SOD81_P10.16mm_Horizontal",
|
||||
"DO-201": "Diode_THT:D_DO-201_P15.24mm_Horizontal",
|
||||
|
||||
"DFN-2(0.6x1)": "Package_DFN_QFN:DFN-2-1EP_0.6x1.0mm_P0.65mm_EP0.2x0.55mm",
|
||||
"DFN1006-2": "Package_DFN_QFN:DFN-2_1.0x0.6mm",
|
||||
"DFN-6": "Package_DFN_QFN:DFN-6-1EP_2x2mm_P0.65mm_EP1x1.6mm",
|
||||
"DFN-8": "Package_DFN_QFN:DFN-8-1EP_3x2mm_P0.5mm_EP1.3x1.5mm",
|
||||
|
||||
"0201": "Resistor_SMD:R_0201_0603Metric",
|
||||
"0402": "Resistor_SMD:R_0402_1005Metric",
|
||||
"0603": "Resistor_SMD:R_0603_1608Metric",
|
||||
"0805": "Resistor_SMD:R_0805_2012Metric",
|
||||
"1206": "Resistor_SMD:R_1206_3216Metric",
|
||||
"1210": "Resistor_SMD:R_1210_3225Metric",
|
||||
"1812": "Resistor_SMD:R_1812_4532Metric",
|
||||
"2010": "Resistor_SMD:R_2010_5025Metric",
|
||||
"2512": "Resistor_SMD:R_2512_6332Metric",
|
||||
"2917": "Resistor_SMD:R_2917_7343Metric",
|
||||
"2920": "Resistor_SMD:R_2920_7350Metric",
|
||||
|
||||
"CASE-A-3216-18(mm)": "Capacitor_Tantalum_SMD:CP_EIA-3216-18_Kemet-A",
|
||||
"CASE-B-3528-21(mm)": "Capacitor_Tantalum_SMD:CP_EIA-3528-21_Kemet-B",
|
||||
"CASE-C-6032-28(mm)": "Capacitor_Tantalum_SMD:CP_EIA-6032-28_Kemet-C",
|
||||
"CASE-D-7343-31(mm)": "Capacitor_Tantalum_SMD:CP_EIA-7343-31_Kemet-D",
|
||||
"CASE-E-7343-43(mm)": "Capacitor_Tantalum_SMD:CP_EIA-7343-43_Kemet-E",
|
||||
|
||||
"SMD,D4xL5.4mm": "Capacitor_SMD:CP_Elec_4x5.4",
|
||||
"SMD,D5xL5.4mm": "Capacitor_SMD:CP_Elec_5x5.4",
|
||||
"SMD,D6.3xL5.4mm": "Capacitor_SMD:CP_Elec_6.3x5.4",
|
||||
"SMD,D6.3xL7.7mm": "Capacitor_SMD:CP_Elec_6.3x7.7",
|
||||
"SMD,D8xL6.5mm": "Capacitor_SMD:CP_Elec_8x6.5",
|
||||
"SMD,D8xL10mm": "Capacitor_SMD:CP_Elec_8x10",
|
||||
"SMD,D10xL10mm": "Capacitor_SMD:CP_Elec_10x10",
|
||||
"SMD,D10xL10.5mm": "Capacitor_SMD:CP_Elec_10x10.5",
|
||||
|
||||
"Through Hole,D5xL11mm": "Capacitor_THT:CP_Radial_D5.0mm_P2.00mm",
|
||||
"Through Hole,D6.3xL11mm": "Capacitor_THT:CP_Radial_D6.3mm_P2.50mm",
|
||||
"Through Hole,D8xL11mm": "Capacitor_THT:CP_Radial_D8.0mm_P3.50mm",
|
||||
"Through Hole,D10xL16mm": "Capacitor_THT:CP_Radial_D10.0mm_P5.00mm",
|
||||
"Through Hole,D10xL20mm": "Capacitor_THT:CP_Radial_D10.0mm_P5.00mm",
|
||||
"Through Hole,D12.5xL20mm": "Capacitor_THT:CP_Radial_D12.5mm_P5.00mm",
|
||||
|
||||
"LED 3mm": "LED_THT:LED_D3.0mm",
|
||||
"LED 5mm": "LED_THT:LED_D5.0mm",
|
||||
"LED 0603": "LED_SMD:LED_0603_1608Metric",
|
||||
"LED 0805": "LED_SMD:LED_0805_2012Metric",
|
||||
"SMD5050-4P": "LED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mm",
|
||||
"SMD5050-6P": "LED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mm",
|
||||
|
||||
"HC-49": "Crystal:Crystal_HC49-4H_Vertical",
|
||||
"HC-49/U": "Crystal:Crystal_HC49-4H_Vertical",
|
||||
"HC-49/S": "Crystal:Crystal_HC49-U_Vertical",
|
||||
"HC-49/US": "Crystal:Crystal_HC49-U_Vertical",
|
||||
|
||||
"USB-A": "Connector_USB:USB_A_Stewart_SS-52100-001_Horizontal",
|
||||
"USB-B": "Connector_USB:USB_B_OST_USB-B1HSxx_Horizontal",
|
||||
"USB-Mini-B": "Connector_USB:USB_Mini-B_Lumberg_2486_01_Horizontal",
|
||||
"USB-Micro-B": "Connector_USB:USB_Micro-B_Molex-105017-0001",
|
||||
"USB-C": "Connector_USB:USB_C_Receptacle_GCT_USB4085",
|
||||
|
||||
"1x2 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical",
|
||||
"1x3 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical",
|
||||
"1x4 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical",
|
||||
"1x5 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_Vertical",
|
||||
"1x6 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_Vertical",
|
||||
"1x8 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x08_P2.54mm_Vertical",
|
||||
"1x10 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_1x10_P2.54mm_Vertical",
|
||||
"2x2 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x02_P2.54mm_Vertical",
|
||||
"2x3 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x03_P2.54mm_Vertical",
|
||||
"2x4 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x04_P2.54mm_Vertical",
|
||||
"2x5 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x05_P2.54mm_Vertical",
|
||||
"2x10 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x10_P2.54mm_Vertical",
|
||||
"2x20 P2.54mm": "Connector_PinHeader_2.54mm:PinHeader_2x20_P2.54mm_Vertical",
|
||||
"SIP-3-2.54mm": "Package_SIP:SIP-3_P2.54mm",
|
||||
"SIP-4-2.54mm": "Package_SIP:SIP-4_P2.54mm",
|
||||
"SIP-5-2.54mm": "Package_SIP:SIP-5_P2.54mm"
|
||||
},
|
||||
"categories": {
|
||||
"Electrolytic": "Device:C_Polarized",
|
||||
"Polarized": "Device:C_Polarized",
|
||||
"Tantalum": "Device:C_Polarized",
|
||||
"Zener": "Device:D_Zener",
|
||||
"Schottky": "Device:D_Schottky",
|
||||
"TVS": "Device:D_TVS",
|
||||
"LED": "Device:LED",
|
||||
"NPN": "Device:Q_NPN_BCE",
|
||||
"PNP": "Device:Q_PNP_BCE",
|
||||
"N-MOSFET": "Device:Q_NMOS_GDS",
|
||||
"NMOS": "Device:Q_NMOS_GDS",
|
||||
"N-MOS": "Device:Q_NMOS_GDS",
|
||||
"P-MOSFET": "Device:Q_PMOS_GDS",
|
||||
"PMOS": "Device:Q_PMOS_GDS",
|
||||
"P-MOS": "Device:Q_PMOS_GDS",
|
||||
"MOSFET": "Device:Q_NMOS_GDS",
|
||||
"JFET": "Device:Q_NJFET_DSG",
|
||||
"Ferrite": "Device:Ferrite_Bead",
|
||||
"Crystal": "Device:Crystal",
|
||||
"Oscillator": "Oscillator:Oscillator_Crystal",
|
||||
"Fuse": "Device:Fuse",
|
||||
"Transformer": "Device:Transformer_1P_1S",
|
||||
"Resistor": "Device:R",
|
||||
"Capacitor": "Device:C",
|
||||
"Inductor": "Device:L",
|
||||
"Diode": "Device:D",
|
||||
"Transistor": "Device:Q_NPN_BCE",
|
||||
"Voltage Regulator": "Regulator_Linear:LM317_TO-220",
|
||||
"LDO": "Regulator_Linear:AMS1117-3.3",
|
||||
"Op-Amp": "Amplifier_Operational:LM358",
|
||||
"Comparator": "Comparator:LM393",
|
||||
"Optocoupler": "Isolator:PC817",
|
||||
"Relay": "Relay:Relay_DPDT",
|
||||
"Connector": "Connector:Conn_01x02",
|
||||
"Switch": "Switch:SW_Push",
|
||||
"Button": "Switch:SW_Push",
|
||||
"Potentiometer": "Device:R_POT",
|
||||
"Trimpot": "Device:R_POT",
|
||||
"Thermistor": "Device:Thermistor",
|
||||
"Varistor": "Device:Varistor",
|
||||
"Photo": "Device:LED"
|
||||
}
|
||||
}
|
||||
55
assets/controllers/backup_restore_controller.js
Normal file
55
assets/controllers/backup_restore_controller.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
/**
|
||||
* Stimulus controller for backup restore confirmation dialogs.
|
||||
* Shows a confirmation dialog with backup details before allowing restore.
|
||||
*/
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
filename: { type: String, default: '' },
|
||||
date: { type: String, default: '' },
|
||||
confirmTitle: { type: String, default: 'Restore Backup' },
|
||||
confirmMessage: { type: String, default: 'Are you sure you want to restore from this backup?' },
|
||||
confirmWarning: { type: String, default: 'This will overwrite your current database. This action cannot be undone!' },
|
||||
};
|
||||
|
||||
connect() {
|
||||
this.element.addEventListener('submit', this.handleSubmit.bind(this));
|
||||
}
|
||||
|
||||
handleSubmit(event) {
|
||||
// Always prevent default first
|
||||
event.preventDefault();
|
||||
|
||||
// Build confirmation message
|
||||
const message = this.confirmTitleValue + '\n\n' +
|
||||
'Backup: ' + this.filenameValue + '\n' +
|
||||
'Date: ' + this.dateValue + '\n\n' +
|
||||
this.confirmMessageValue + '\n\n' +
|
||||
'⚠️ ' + this.confirmWarningValue;
|
||||
|
||||
// Only submit if user confirms
|
||||
if (confirm(message)) {
|
||||
this.element.submit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,14 +3,16 @@ import { generateCsrfHeaders } from "./csrf_protection_controller"
|
|||
|
||||
export default class extends Controller {
|
||||
static targets = ["progressBar", "progressText"]
|
||||
static values = {
|
||||
static values = {
|
||||
jobId: Number,
|
||||
partId: Number,
|
||||
researchUrl: String,
|
||||
researchAllUrl: String,
|
||||
markCompletedUrl: String,
|
||||
markSkippedUrl: String,
|
||||
markPendingUrl: String
|
||||
markPendingUrl: String,
|
||||
quickApplyUrl: String,
|
||||
quickApplyAllUrl: String
|
||||
}
|
||||
|
||||
connect() {
|
||||
|
|
@ -119,13 +121,11 @@ export default class extends Controller {
|
|||
|
||||
async markSkipped(event) {
|
||||
const partId = event.currentTarget.dataset.partId
|
||||
const reason = prompt('Reason for skipping (optional):') || ''
|
||||
|
||||
|
||||
try {
|
||||
const url = this.markSkippedUrlValue.replace('__PART_ID__', partId)
|
||||
const data = await this.fetchWithErrorHandling(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ reason })
|
||||
method: 'POST'
|
||||
})
|
||||
|
||||
if (data.success) {
|
||||
|
|
@ -321,6 +321,94 @@ export default class extends Controller {
|
|||
}
|
||||
}
|
||||
|
||||
async quickApply(event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const partId = event.currentTarget.dataset.partId
|
||||
const providerKey = event.currentTarget.dataset.providerKey
|
||||
const providerId = event.currentTarget.dataset.providerId
|
||||
const button = event.currentTarget
|
||||
const originalHtml = button.innerHTML
|
||||
|
||||
button.disabled = true
|
||||
button.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Applying...'
|
||||
|
||||
try {
|
||||
const url = this.quickApplyUrlValue.replace('__PART_ID__', partId)
|
||||
const data = await this.fetchWithErrorHandling(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ providerKey, providerId })
|
||||
}, 60000)
|
||||
|
||||
if (data.success) {
|
||||
this.updateProgressDisplay(data)
|
||||
this.showSuccessMessage(data.message || 'Part updated successfully')
|
||||
sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
|
||||
window.location.reload()
|
||||
} else {
|
||||
this.showErrorMessage(data.error || 'Quick apply failed')
|
||||
button.innerHTML = originalHtml
|
||||
button.disabled = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in quick apply:', error)
|
||||
this.showErrorMessage(error.message || 'Quick apply failed')
|
||||
button.innerHTML = originalHtml
|
||||
button.disabled = false
|
||||
}
|
||||
}
|
||||
|
||||
async quickApplyAll(event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
if (!confirm('This will apply the top search result to all pending parts without individual review. Continue?')) {
|
||||
return
|
||||
}
|
||||
|
||||
const button = event.currentTarget
|
||||
const spinner = document.getElementById('quick-apply-all-spinner')
|
||||
const originalHtml = button.innerHTML
|
||||
|
||||
button.disabled = true
|
||||
if (spinner) {
|
||||
spinner.style.display = 'inline-block'
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await this.fetchWithErrorHandling(this.quickApplyAllUrlValue, {
|
||||
method: 'POST'
|
||||
}, 300000)
|
||||
|
||||
if (data.success) {
|
||||
this.updateProgressDisplay(data)
|
||||
|
||||
let message = data.message || 'Bulk apply completed'
|
||||
if (data.errors && data.errors.length > 0) {
|
||||
message += '\nErrors:\n' + data.errors.join('\n')
|
||||
}
|
||||
|
||||
this.showSuccessMessage(message)
|
||||
sessionStorage.setItem('bulkImportScrollPosition', window.scrollY.toString())
|
||||
window.location.reload()
|
||||
} else {
|
||||
this.showErrorMessage(data.error || 'Bulk apply failed')
|
||||
button.innerHTML = originalHtml
|
||||
button.disabled = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in quick apply all:', error)
|
||||
this.showErrorMessage(error.message || 'Bulk apply failed')
|
||||
button.innerHTML = originalHtml
|
||||
button.disabled = false
|
||||
} finally {
|
||||
if (spinner) {
|
||||
spinner.style.display = 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showSuccessMessage(message) {
|
||||
this.showToast('success', message)
|
||||
}
|
||||
|
|
|
|||
274
assets/controllers/common/dirty_form_controller.js
Normal file
274
assets/controllers/common/dirty_form_controller.js
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Controller} from "@hotwired/stimulus";
|
||||
import {visit} from "@hotwired/turbo";
|
||||
import * as bootbox from "bootbox";
|
||||
import "../../css/components/bootbox_extensions.css";
|
||||
import "../../css/components/dirty_form.css";
|
||||
|
||||
/**
|
||||
* Attach to a <form> element (or a wrapper containing a <form>) to prevent accidental navigation
|
||||
* away when the form has unsaved changes.
|
||||
*
|
||||
* Dirty detection is event-driven: `change` and `input` events bubble up to the form and trigger
|
||||
* a check of whether any element's current value differs from the DOM default recorded in the HTML
|
||||
* (`defaultValue` / `defaultChecked` / `option.defaultSelected`). Using both events covers both
|
||||
* native widgets (which fire `change`) and rich-text editors like CKEditor (which fire `input`
|
||||
* when they sync their underlying textarea).
|
||||
*
|
||||
* Validation failures (server returns 200 with `.is-invalid` fields) are always treated as dirty:
|
||||
* the submitted data was never saved, so navigating away would lose it. This removes the need for
|
||||
* any snapshot mechanism — the `.is-invalid` classes in the re-rendered HTML are the signal.
|
||||
*
|
||||
* Intercepts three navigation paths:
|
||||
* 1. Any <a href> link click (capture phase)
|
||||
* 2. window beforeunload
|
||||
* 3. turbo:before-visit
|
||||
*
|
||||
* Values:
|
||||
* - confirmTitle (String) – dialog title
|
||||
* - confirmMessage (String) – dialog body text
|
||||
*/
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
confirmTitle: {type: String, default: 'Unsaved Changes'},
|
||||
confirmMessage: {type: String, default: 'You have unsaved changes. Are you sure you want to leave this page?'},
|
||||
};
|
||||
|
||||
connect() {
|
||||
this._form = (this.element.tagName === 'FORM') ? this.element : this.element.querySelector('form');
|
||||
this._isDirty = false;
|
||||
this._submitting = false;
|
||||
this._navigating = false;
|
||||
|
||||
this._changeHandler = this._handleChange.bind(this);
|
||||
this._linkClickHandler = this._handleLinkClick.bind(this);
|
||||
this._beforeUnloadHandler = this._handleBeforeUnload.bind(this);
|
||||
this._turboBeforeVisitHandler = this._handleTurboBeforeVisit.bind(this);
|
||||
this._turboSubmitEndHandler = this._handleTurboSubmitEnd.bind(this);
|
||||
|
||||
if (this._form) {
|
||||
this._form.addEventListener('change', this._changeHandler);
|
||||
// CKEditor (and other rich-text widgets) dispatch `input` rather than `change`
|
||||
// when their underlying textarea value is updated.
|
||||
this._form.addEventListener('input', this._changeHandler);
|
||||
}
|
||||
document.addEventListener('click', this._linkClickHandler, true);
|
||||
window.addEventListener('beforeunload', this._beforeUnloadHandler);
|
||||
document.addEventListener('turbo:before-visit', this._turboBeforeVisitHandler);
|
||||
document.addEventListener('turbo:submit-end', this._turboSubmitEndHandler);
|
||||
|
||||
const modal = this.element.closest('.modal');
|
||||
if (modal) {
|
||||
this._modal = modal;
|
||||
this._modalHideHandler = this._handleModalHide.bind(this);
|
||||
modal.addEventListener('hide.bs.modal', this._modalHideHandler);
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this._form) {
|
||||
this._form.removeEventListener('change', this._changeHandler);
|
||||
this._form.removeEventListener('input', this._changeHandler);
|
||||
}
|
||||
document.removeEventListener('click', this._linkClickHandler, true);
|
||||
window.removeEventListener('beforeunload', this._beforeUnloadHandler);
|
||||
document.removeEventListener('turbo:before-visit', this._turboBeforeVisitHandler);
|
||||
document.removeEventListener('turbo:submit-end', this._turboSubmitEndHandler);
|
||||
|
||||
if (this._modal && this._modalHideHandler) {
|
||||
this._modal.removeEventListener('hide.bs.modal', this._modalHideHandler);
|
||||
}
|
||||
}
|
||||
|
||||
/** data-action="submit->common--dirty-form#submit" — suppresses the guard while saving. */
|
||||
submit() {
|
||||
this._submitting = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* data-action="reset->common--dirty-form#resetDirtyState" — marks the form as clean after
|
||||
* a programmatic reset. Native change events are not fired by form.reset(), so we set the
|
||||
* flag directly. Turbo also calls form.reset() internally before the post-submit redirect;
|
||||
* the _submitting guard prevents that from incorrectly clearing the flag.
|
||||
*/
|
||||
resetDirtyState() {
|
||||
if (this._submitting) return;
|
||||
|
||||
// Wait for a frame to allow the form's DOM state to update after the reset() call, then refresh markers and update the dirty flag.
|
||||
requestAnimationFrame(() => {
|
||||
this._isDirty = false;
|
||||
this._clearDirtyMarkers();
|
||||
});
|
||||
}
|
||||
|
||||
_handleChange(event) {
|
||||
const target = event?.target;
|
||||
if (target?.name) {
|
||||
this._updateDirtyMarker(target);
|
||||
} else {
|
||||
this._refreshDirtyMarkers();
|
||||
}
|
||||
this._isDirty = this._form?.querySelector('[data-dirty]') !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk every named form element and update its `data-dirty` attribute.
|
||||
* Un-named elements (e.g. the visible TristateCheckbox whose name was removed) are
|
||||
* skipped — they are not submitted and are not the source of truth for form data.
|
||||
*/
|
||||
_refreshDirtyMarkers() {
|
||||
if (!this._form) return;
|
||||
for (const el of this._form.elements) {
|
||||
if (!el.name) continue;
|
||||
this._updateDirtyMarker(el);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or clear `data-dirty` on a single named form element.
|
||||
* Hidden inputs are not visually rendered, so special handling applies:
|
||||
* - TristateCheckbox: the hidden backing input is preceded by a nameless visual checkbox —
|
||||
* mark that instead.
|
||||
* - Other hidden inputs (e.g. CSRF tokens): ignored.
|
||||
* TomSelect hides the <select> before .ts-wrapper (sibling); CSS targets .ts-control via the
|
||||
* adjacent-sibling combinator on the select's data-dirty attribute.
|
||||
*/
|
||||
_updateDirtyMarker(el) {
|
||||
if (el.type === 'hidden') {
|
||||
const visual = el.previousElementSibling;
|
||||
if (visual instanceof HTMLInputElement && !visual.name) {
|
||||
visual.toggleAttribute('data-dirty', el.value !== el.defaultValue);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const dirty = this._isElementDirty(el);
|
||||
el.toggleAttribute('data-dirty', dirty);
|
||||
}
|
||||
|
||||
_clearDirtyMarkers() {
|
||||
this._form?.querySelectorAll('[data-dirty]').forEach(el => el.removeAttribute('data-dirty'));
|
||||
}
|
||||
|
||||
_isElementDirty(el) {
|
||||
//Disabled elements are not editable, so ignore them even if their value differs from the default.
|
||||
if (el.disabled) return false;
|
||||
|
||||
if (el.type === 'file') return false;
|
||||
if (el.type === 'checkbox' || el.type === 'radio') {
|
||||
return el.checked !== el.defaultChecked;
|
||||
}
|
||||
if (el.tagName === 'SELECT') {
|
||||
// TomSelect sets data-default-value to the value at init time.
|
||||
// The native option.defaultSelected approach is unreliable when no option
|
||||
// carries the `selected` attribute — the browser auto-selects option[0]
|
||||
// (selected=true) while defaultSelected stays false, causing a false positive.
|
||||
if (el.dataset.defaultValue !== undefined) {
|
||||
return el.value !== el.dataset.defaultValue;
|
||||
}
|
||||
for (const option of el.options) {
|
||||
if (option.selected !== option.defaultSelected) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let defaultValue = el.defaultValue;
|
||||
|
||||
//If an element has an data-default-value, use that for dirty checking instead of the DOM default Value. Set for example by the ckeditor-controller
|
||||
if (el.dataset.defaultValue !== undefined) {
|
||||
defaultValue = el.dataset.defaultValue;
|
||||
}
|
||||
return el.value !== defaultValue;
|
||||
}
|
||||
|
||||
_isFormDirty() {
|
||||
if (this._submitting) return false;
|
||||
// A form with validation errors was submitted but never saved — always treat as dirty.
|
||||
if (this._form?.querySelector('.is-invalid')) return true;
|
||||
return this._isDirty;
|
||||
}
|
||||
|
||||
_confirmNavigation(onConfirm) {
|
||||
bootbox.confirm({
|
||||
title: this.confirmTitleValue,
|
||||
message: this.confirmMessageValue,
|
||||
callback: (result) => { if (result) onConfirm(); }
|
||||
});
|
||||
}
|
||||
|
||||
_handleLinkClick(event) {
|
||||
if (this._navigating) return;
|
||||
|
||||
const link = event.target.closest('a[href]');
|
||||
if (!link) return;
|
||||
|
||||
const href = link.getAttribute('href');
|
||||
if (!href || href.startsWith('#')) return;
|
||||
if (link.target === '_blank' || link.target === '_top' || link.target === '_parent') return;
|
||||
if (link.hasAttribute('data-dirty-form-ignore')) return;
|
||||
|
||||
if (!this._isFormDirty()) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this._confirmNavigation(() => { this._navigating = true; link.click(); });
|
||||
}
|
||||
|
||||
_handleBeforeUnload(event) {
|
||||
if (this._navigating || !this._isFormDirty()) return;
|
||||
event.preventDefault();
|
||||
event.returnValue = '';
|
||||
}
|
||||
|
||||
_handleTurboBeforeVisit(event) {
|
||||
if (this._navigating || !this._isFormDirty()) return;
|
||||
|
||||
event.preventDefault();
|
||||
const url = event.detail.url;
|
||||
const frame = event.detail.frame;
|
||||
this._confirmNavigation(() => {
|
||||
this._navigating = true;
|
||||
if (frame) { window.Turbo.visit(url, { frame }); } else { visit(url); }
|
||||
});
|
||||
}
|
||||
|
||||
_handleTurboSubmitEnd(event) {
|
||||
const submittedForm = event.detail?.formSubmission?.formElement;
|
||||
if (submittedForm !== this._form) return;
|
||||
|
||||
// For a successful save (redirect), the controller will disconnect with the Turbo
|
||||
// navigation; reset is only needed for validation errors where the form stays in the DOM.
|
||||
const savedSuccessfully = event.detail.success && event.detail.fetchResponse?.redirected;
|
||||
if (!savedSuccessfully) {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
_handleModalHide(event) {
|
||||
if (this._navigating || !this._isFormDirty()) return;
|
||||
|
||||
event.preventDefault();
|
||||
this._confirmNavigation(() => {
|
||||
this._navigating = true;
|
||||
window.bootstrap?.Modal?.getInstance(this._modal)?.hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,10 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import { Toast } from 'bootstrap';
|
||||
|
||||
/**
|
||||
* The purpose of this controller, is to show all containers.
|
||||
* They should already be added via turbo-streams, but have to be called for to show them.
|
||||
*/
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
//Move all toasts from the page into our toast container and show them
|
||||
|
|
@ -33,4 +37,4 @@ export default class extends Controller {
|
|||
const toast = new Toast(this.element);
|
||||
toast.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
|
|||
const tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/;
|
||||
|
||||
// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
|
||||
// Use `form.requestSubmit()` to ensure that the submit event is triggered. Using `form.submit()` will not trigger the event
|
||||
// and thus this event-listener will not be executed.
|
||||
document.addEventListener('submit', function (event) {
|
||||
generateCsrfToken(event.target);
|
||||
}, true);
|
||||
|
|
@ -33,8 +35,8 @@ export function generateCsrfToken (formElement) {
|
|||
if (!csrfCookie && nameCheck.test(csrfToken)) {
|
||||
csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
|
||||
csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
|
||||
csrfField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
csrfField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
if (csrfCookie && tokenCheck.test(csrfToken)) {
|
||||
const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
|
||||
|
|
|
|||
377
assets/controllers/docker_update_progress_controller.js
Normal file
377
assets/controllers/docker_update_progress_controller.js
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
/**
|
||||
* Stimulus controller for Docker update progress tracking.
|
||||
*
|
||||
* Polls the health check endpoint to detect when the container restarts
|
||||
* after a Watchtower-triggered update. Drives the step timeline UI
|
||||
* with timestamps, matching the git update progress style.
|
||||
*/
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
healthUrl: String,
|
||||
previousVersion: { type: String, default: 'unknown' },
|
||||
pollInterval: { type: Number, default: 5000 },
|
||||
maxWaitTime: { type: Number, default: 600000 }, // 10 minutes
|
||||
// Translated UI strings (passed from Twig template)
|
||||
textPulling: { type: String, default: 'Waiting for Watchtower to pull the new image...' },
|
||||
textPullingDetail: { type: String, default: 'Watchtower is checking for and downloading the latest Docker image...' },
|
||||
textRestarting: { type: String, default: 'Container is restarting with the new image...' },
|
||||
textRestartingDetail: { type: String, default: 'The container is being recreated with the updated image. This may take a moment...' },
|
||||
textSuccess: { type: String, default: 'Update Complete!' },
|
||||
textSuccessDetail: { type: String, default: 'Part-DB has been updated successfully via Docker.' },
|
||||
textTimeout: { type: String, default: 'Update Taking Longer Than Expected' },
|
||||
textTimeoutDetail: { type: String, default: 'The update may still be in progress. Check your Docker logs for details.' },
|
||||
textStepPull: { type: String, default: 'Pull Image' },
|
||||
textStepRestart: { type: String, default: 'Restart Container' },
|
||||
};
|
||||
|
||||
static targets = [
|
||||
// Header
|
||||
'headerWhale', 'titleIcon',
|
||||
'statusText', 'statusSubtext',
|
||||
'progressBar', 'elapsedTime',
|
||||
// Alerts
|
||||
'stepAlert', 'stepName', 'stepMessage',
|
||||
'successAlert', 'timeoutAlert', 'errorAlert', 'errorMessage', 'warningAlert',
|
||||
// Step timeline (multi-target arrays)
|
||||
'stepRow', 'stepIcon', 'stepDetail', 'stepTime',
|
||||
// Version display
|
||||
'newVersion', 'previousVersion',
|
||||
// Actions
|
||||
'actions',
|
||||
];
|
||||
|
||||
// Step definitions: name -> { index, progress% }
|
||||
static STEPS = {
|
||||
trigger: { index: 0, progress: 15 },
|
||||
pull: { index: 1, progress: 30 },
|
||||
stop: { index: 2, progress: 50 },
|
||||
restart: { index: 3, progress: 65 },
|
||||
health: { index: 4, progress: 80 },
|
||||
verify: { index: 5, progress: 100 },
|
||||
};
|
||||
|
||||
connect() {
|
||||
this.serverWentDown = false;
|
||||
this.serverCameBack = false;
|
||||
this.startTime = Date.now();
|
||||
this.timer = null;
|
||||
this.currentStep = 'pull'; // trigger is already done
|
||||
this.stepTimestamps = { trigger: this.formatTime(new Date()) };
|
||||
this.consecutiveSuccessCount = 0;
|
||||
|
||||
// Set the trigger step timestamp
|
||||
this.setStepTimestamp(0, this.stepTimestamps.trigger);
|
||||
|
||||
this.poll();
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
}
|
||||
|
||||
createTimeoutSignal(ms) {
|
||||
if (typeof AbortSignal.timeout === 'function') {
|
||||
return AbortSignal.timeout(ms);
|
||||
}
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), ms);
|
||||
return controller.signal;
|
||||
}
|
||||
|
||||
async poll() {
|
||||
const elapsed = Date.now() - this.startTime;
|
||||
this.updateElapsedTime(elapsed);
|
||||
|
||||
if (elapsed > this.maxWaitTimeValue) {
|
||||
this.showTimeout();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(this.healthUrlValue, {
|
||||
cache: 'no-store',
|
||||
signal: this.createTimeoutSignal(4000),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let data;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch (parseError) {
|
||||
this.schedulePoll();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.serverWentDown) {
|
||||
// Server came back! Move through health check -> verify
|
||||
if (!this.serverCameBack) {
|
||||
this.serverCameBack = true;
|
||||
this.advanceToStep('health');
|
||||
}
|
||||
|
||||
this.consecutiveSuccessCount++;
|
||||
|
||||
// Wait for 2 consecutive successes to confirm stability
|
||||
if (this.consecutiveSuccessCount >= 2) {
|
||||
this.showSuccess(data.version);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Server still up - Watchtower pulling image
|
||||
this.showPulling();
|
||||
}
|
||||
} else if (response.status === 503) {
|
||||
// Maintenance mode or shutting down
|
||||
this.serverWentDown = true;
|
||||
this.consecutiveSuccessCount = 0;
|
||||
this.advanceToStep('stop');
|
||||
} else {
|
||||
if (this.serverWentDown) {
|
||||
this.showRestarting();
|
||||
} else {
|
||||
this.showPulling();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Connection refused = container is down
|
||||
if (!this.serverWentDown) {
|
||||
this.serverWentDown = true;
|
||||
this.advanceToStep('stop');
|
||||
}
|
||||
this.consecutiveSuccessCount = 0;
|
||||
this.showRestarting();
|
||||
}
|
||||
|
||||
this.schedulePoll();
|
||||
}
|
||||
|
||||
schedulePoll() {
|
||||
this.timer = setTimeout(() => this.poll(), this.pollIntervalValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance the step timeline to a specific step.
|
||||
* Marks all previous steps as complete with timestamps.
|
||||
*/
|
||||
advanceToStep(stepName) {
|
||||
const steps = this.constructor.STEPS;
|
||||
const targetIndex = steps[stepName]?.index;
|
||||
if (targetIndex === undefined) return;
|
||||
|
||||
const stepNames = Object.keys(steps);
|
||||
const now = this.formatTime(new Date());
|
||||
|
||||
for (let i = 0; i < stepNames.length; i++) {
|
||||
const name = stepNames[i];
|
||||
|
||||
if (i < targetIndex) {
|
||||
// Completed step
|
||||
this.markStepComplete(i, this.stepTimestamps[name] || now);
|
||||
if (!this.stepTimestamps[name]) {
|
||||
this.stepTimestamps[name] = now;
|
||||
}
|
||||
} else if (i === targetIndex) {
|
||||
// Current active step
|
||||
this.markStepActive(i);
|
||||
this.stepTimestamps[name] = now;
|
||||
this.setStepTimestamp(i, now);
|
||||
this.currentStep = name;
|
||||
}
|
||||
// Steps after targetIndex remain pending (no change needed)
|
||||
}
|
||||
|
||||
// Update progress bar
|
||||
this.updateProgressBar(steps[stepName].progress);
|
||||
}
|
||||
|
||||
showPulling() {
|
||||
if (this.hasStatusTextTarget) {
|
||||
this.statusTextTarget.textContent = this.textPullingValue;
|
||||
}
|
||||
if (this.hasStepNameTarget) {
|
||||
this.stepNameTarget.textContent = this.textStepPullValue;
|
||||
}
|
||||
if (this.hasStepMessageTarget) {
|
||||
this.stepMessageTarget.textContent = this.textPullingDetailValue;
|
||||
}
|
||||
this.updateProgressBar(30);
|
||||
}
|
||||
|
||||
showRestarting() {
|
||||
// Advance to restart step if we haven't already
|
||||
if (this.currentStep !== 'restart' && this.currentStep !== 'health' && this.currentStep !== 'verify') {
|
||||
this.advanceToStep('restart');
|
||||
}
|
||||
|
||||
if (this.hasStatusTextTarget) {
|
||||
this.statusTextTarget.textContent = this.textRestartingValue;
|
||||
}
|
||||
if (this.hasStepNameTarget) {
|
||||
this.stepNameTarget.textContent = this.textStepRestartValue;
|
||||
}
|
||||
if (this.hasStepMessageTarget) {
|
||||
this.stepMessageTarget.textContent = this.textRestartingDetailValue;
|
||||
}
|
||||
}
|
||||
|
||||
showSuccess(newVersion) {
|
||||
// Advance all steps to complete
|
||||
const steps = this.constructor.STEPS;
|
||||
const stepNames = Object.keys(steps);
|
||||
const now = this.formatTime(new Date());
|
||||
|
||||
for (let i = 0; i < stepNames.length; i++) {
|
||||
const name = stepNames[i];
|
||||
this.markStepComplete(i, this.stepTimestamps[name] || now);
|
||||
}
|
||||
|
||||
this.updateProgressBar(100);
|
||||
|
||||
// Update whale animation
|
||||
if (this.hasHeaderWhaleTarget) {
|
||||
this.headerWhaleTarget.classList.add('success');
|
||||
}
|
||||
if (this.hasTitleIconTarget) {
|
||||
this.titleIconTarget.className = 'fas fa-check-circle text-success';
|
||||
}
|
||||
|
||||
if (this.hasStatusTextTarget) {
|
||||
this.statusTextTarget.textContent = this.textSuccessValue;
|
||||
}
|
||||
if (this.hasStatusSubtextTarget) {
|
||||
this.statusSubtextTarget.textContent = this.textSuccessDetailValue;
|
||||
}
|
||||
|
||||
// Hide step alert, show success alert
|
||||
this.toggleTarget('stepAlert', false);
|
||||
this.toggleTarget('successAlert', true);
|
||||
this.toggleTarget('warningAlert', false);
|
||||
this.toggleTarget('actions', true);
|
||||
|
||||
if (this.hasNewVersionTarget) {
|
||||
this.newVersionTarget.textContent = newVersion || 'latest';
|
||||
}
|
||||
if (this.hasPreviousVersionTarget) {
|
||||
this.previousVersionTarget.textContent = this.previousVersionValue;
|
||||
}
|
||||
}
|
||||
|
||||
showTimeout() {
|
||||
this.updateProgressBar(0);
|
||||
|
||||
if (this.hasHeaderWhaleTarget) {
|
||||
this.headerWhaleTarget.classList.add('timeout');
|
||||
}
|
||||
if (this.hasTitleIconTarget) {
|
||||
this.titleIconTarget.className = 'fas fa-exclamation-triangle text-warning';
|
||||
}
|
||||
|
||||
if (this.hasStatusTextTarget) {
|
||||
this.statusTextTarget.textContent = this.textTimeoutValue;
|
||||
}
|
||||
if (this.hasStatusSubtextTarget) {
|
||||
this.statusSubtextTarget.textContent = this.textTimeoutDetailValue;
|
||||
}
|
||||
|
||||
this.toggleTarget('stepAlert', false);
|
||||
this.toggleTarget('timeoutAlert', true);
|
||||
this.toggleTarget('warningAlert', false);
|
||||
this.toggleTarget('actions', true);
|
||||
}
|
||||
|
||||
// --- Step timeline helpers ---
|
||||
|
||||
markStepComplete(index, timestamp) {
|
||||
if (this.stepIconTargets[index]) {
|
||||
this.stepIconTargets[index].className = 'fas fa-check-circle text-success me-3';
|
||||
}
|
||||
if (this.stepRowTargets[index]) {
|
||||
this.stepRowTargets[index].classList.remove('text-muted');
|
||||
}
|
||||
if (timestamp) {
|
||||
this.setStepTimestamp(index, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
markStepActive(index) {
|
||||
if (this.stepIconTargets[index]) {
|
||||
this.stepIconTargets[index].className = 'fas fa-spinner fa-spin text-primary me-3';
|
||||
}
|
||||
if (this.stepRowTargets[index]) {
|
||||
this.stepRowTargets[index].classList.remove('text-muted');
|
||||
}
|
||||
}
|
||||
|
||||
setStepTimestamp(index, time) {
|
||||
if (this.stepTimeTargets[index]) {
|
||||
this.stepTimeTargets[index].textContent = time;
|
||||
}
|
||||
}
|
||||
|
||||
// --- UI helpers ---
|
||||
|
||||
toggleTarget(name, show) {
|
||||
const hasMethod = 'has' + name.charAt(0).toUpperCase() + name.slice(1) + 'Target';
|
||||
if (this[hasMethod]) {
|
||||
this[name + 'Target'].classList.toggle('d-none', !show);
|
||||
}
|
||||
}
|
||||
|
||||
updateProgressBar(percent) {
|
||||
if (this.hasProgressBarTarget) {
|
||||
const bar = this.progressBarTarget;
|
||||
// Remove all width classes
|
||||
bar.classList.remove('progress-w-0', 'progress-w-15', 'progress-w-30', 'progress-w-50', 'progress-w-65', 'progress-w-80', 'progress-w-100');
|
||||
bar.classList.add('progress-w-' + percent);
|
||||
bar.textContent = percent + '%';
|
||||
bar.setAttribute('aria-valuenow', percent);
|
||||
|
||||
bar.classList.remove('bg-success', 'bg-danger', 'progress-bar-striped', 'progress-bar-animated');
|
||||
if (percent === 100) {
|
||||
bar.classList.add('bg-success');
|
||||
} else if (percent === 0) {
|
||||
bar.classList.add('bg-danger');
|
||||
} else {
|
||||
bar.classList.add('progress-bar-striped', 'progress-bar-animated');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateElapsedTime(elapsed) {
|
||||
if (this.hasElapsedTimeTarget) {
|
||||
const seconds = Math.floor(elapsed / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
this.elapsedTimeTarget.textContent = minutes > 0
|
||||
? `${minutes}m ${remainingSeconds}s`
|
||||
: `${remainingSeconds}s`;
|
||||
}
|
||||
}
|
||||
|
||||
formatTime(date) {
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
}
|
||||
}
|
||||
155
assets/controllers/elements/ai_model_autocomplete_controller.js
Normal file
155
assets/controllers/elements/ai_model_autocomplete_controller.js
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Controller} from "@hotwired/stimulus";
|
||||
|
||||
import "tom-select/dist/css/tom-select.bootstrap5.css";
|
||||
import '../../css/components/tom-select_extensions.css';
|
||||
import TomSelect from "tom-select";
|
||||
|
||||
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
|
||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
||||
import TomSelect_form_reset_handler from '../../tomselect/form_reset_handler/form_reset_handler'
|
||||
|
||||
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
|
||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
||||
TomSelect.define('form_reset_handler', TomSelect_form_reset_handler)
|
||||
|
||||
export default class extends Controller {
|
||||
_tomSelect;
|
||||
|
||||
_platformSelector;
|
||||
|
||||
connect() {
|
||||
|
||||
let dropdownParent = "body";
|
||||
if (this.element.closest('.modal')) {
|
||||
dropdownParent = null
|
||||
}
|
||||
|
||||
//Try to find the platform selector
|
||||
const platformSelector = document.querySelector("select[data-platform-selector-label='" + this.element.dataset.platformSelector + "']");
|
||||
//Clear tomselect options, if the platform selector changes
|
||||
if (platformSelector) {
|
||||
this.platformSelector = platformSelector;
|
||||
platformSelector.addEventListener('change', () => {
|
||||
//Force reload of options by clearing the cache and options of TomSelect and triggering a search with an empty string
|
||||
this._tomSelect.clearOptions();
|
||||
this._tomSelect.clearCache();
|
||||
this._tomSelect.load('');
|
||||
});
|
||||
}
|
||||
|
||||
let settings = {
|
||||
persistent: false,
|
||||
create: true,
|
||||
maxItems: 1,
|
||||
preload: 'focus',
|
||||
createOnBlur: true,
|
||||
selectOnTab: true,
|
||||
clearAfterSelect: true,
|
||||
shouldLoad: ((query) => true),
|
||||
maxOptions: null,
|
||||
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin
|
||||
delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING',
|
||||
dropdownParent: dropdownParent,
|
||||
render: {
|
||||
item: (data, escape) => {
|
||||
return '<span>' + escape(data.label) + '</span>';
|
||||
},
|
||||
option: (data, escape) => {
|
||||
if (data.image) {
|
||||
return "<div class='row m-0'><div class='col-2 pl-0 pr-1'><img class='typeahead-image' src='" + data.image + "'/></div><div class='col-10'>" + data.label + "</div></div>"
|
||||
}
|
||||
return '<div>' + escape(data.label) + '</div>';
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
'autoselect_typed': {},
|
||||
'click_to_edit': {},
|
||||
'clear_button': {},
|
||||
'restore_on_backspace': {},
|
||||
'form_reset_handler': {}
|
||||
}
|
||||
};
|
||||
|
||||
if(this.element.dataset.urlTemplate) {
|
||||
const base_url = this.element.dataset.urlTemplate;
|
||||
settings.searchField = "label";
|
||||
settings.sortField = "label";
|
||||
settings.valueField = "label";
|
||||
settings.load = (query, callback) => {
|
||||
|
||||
|
||||
if (!this.platformSelector) {
|
||||
console.error("Platform selector not found for AI model autocomplete");
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
//Platform is the selected option
|
||||
const platform = this.platformSelector.value;
|
||||
if (!platform) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
||||
//Only fetch each platform once
|
||||
if(self.platformLoaded === platform) {
|
||||
callback();
|
||||
}
|
||||
|
||||
|
||||
const url = base_url.replace('__PLATFORM__', encodeURIComponent(platform));
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
|
||||
self.platformLoaded = platform;
|
||||
|
||||
var data = [];
|
||||
|
||||
for (const name in json) {
|
||||
data.push({
|
||||
"label": name,
|
||||
"capabilities": json[name].capabilities,
|
||||
});
|
||||
}
|
||||
|
||||
callback(data);
|
||||
}).catch(()=>{
|
||||
callback();
|
||||
});
|
||||
};
|
||||
}
|
||||
this._tomSelect = new TomSelect(this.element, settings);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
super.disconnect();
|
||||
//Destroy the TomSelect instance
|
||||
this._tomSelect.destroy();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -25,9 +25,11 @@ import TomSelect from "tom-select";
|
|||
|
||||
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
|
||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
||||
import TomSelect_form_reset_handler from '../../tomselect/form_reset_handler/form_reset_handler'
|
||||
|
||||
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
|
||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
||||
TomSelect.define('form_reset_handler', TomSelect_form_reset_handler)
|
||||
|
||||
export default class extends Controller {
|
||||
_tomSelect;
|
||||
|
|
@ -45,6 +47,7 @@ export default class extends Controller {
|
|||
maxItems: 1,
|
||||
createOnBlur: true,
|
||||
selectOnTab: true,
|
||||
clearAfterSelect: true,
|
||||
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin
|
||||
delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING',
|
||||
dropdownParent: dropdownParent,
|
||||
|
|
@ -63,7 +66,8 @@ export default class extends Controller {
|
|||
'autoselect_typed': {},
|
||||
'click_to_edit': {},
|
||||
'clear_button': {},
|
||||
"restore_on_backspace": {}
|
||||
'restore_on_backspace': {},
|
||||
'form_reset_handler': {}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import "ckeditor5/ckeditor5.css";;
|
|||
import "../../css/components/ckeditor.css";
|
||||
|
||||
const translationContext = require.context(
|
||||
'ckeditor5/translations',
|
||||
'ckeditor5-translations', //Alias defined in webpack.config.js
|
||||
false,
|
||||
//Only load the translation files we will really need
|
||||
/(de|it|fr|ru|ja|cs|da|zh|pl|hu)\.js$/
|
||||
|
|
@ -91,6 +91,9 @@ export default class extends Controller {
|
|||
config.translations = [window.CKEDITOR_TRANSLATIONS, translations];
|
||||
}
|
||||
|
||||
//Apply the default value of the source element as data attribute, so that dirty-form-controller can detect changes
|
||||
this.element.dataset.defaultValue = this.element.defaultValue;
|
||||
|
||||
const watchdog = new EditorWatchdog();
|
||||
watchdog.setCreator((elementOrData, editorConfig) => {
|
||||
return EDITOR_TYPE.create(elementOrData, editorConfig)
|
||||
|
|
@ -106,6 +109,26 @@ export default class extends Controller {
|
|||
editor_div.classList.add(...new_classes.split(","));
|
||||
}
|
||||
|
||||
// Automatic synchronization of source input
|
||||
editor.model.document.on("change:data", () => {
|
||||
editor.updateSourceElement();
|
||||
|
||||
// Dispatch the input event for further treatment
|
||||
this.element.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
|
||||
//Set an reset listener to update the editor if the source element is reset (e.g. by a reset button)
|
||||
if (this.element.form && this.element.name) {
|
||||
this.element.form.addEventListener("reset", () => {
|
||||
if (editor.isReadOnly) {
|
||||
return;
|
||||
}
|
||||
if (this.element.dataset.defaultValue !== undefined) {
|
||||
editor.setData(this.element.dataset.defaultValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//This return is important! Otherwise we get mysterious errors in the console
|
||||
//See: https://github.com/ckeditor/ckeditor5/issues/5897#issuecomment-628471302
|
||||
return editor;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {Controller} from "@hotwired/stimulus";
|
|||
|
||||
import * as bootbox from "bootbox";
|
||||
import "../../css/components/bootbox_extensions.css";
|
||||
import accept from "attr-accept";
|
||||
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
|
|
@ -73,15 +74,33 @@ export default class extends Controller {
|
|||
const newElementStr = this.htmlDecode(prototype.replace(regex, this.generateUID()));
|
||||
|
||||
|
||||
let ret = null;
|
||||
|
||||
//Insert new html after the last child element
|
||||
//If the table has a tbody, insert it there
|
||||
//Afterwards return the newly created row
|
||||
if(targetTable.tBodies[0]) {
|
||||
targetTable.tBodies[0].insertAdjacentHTML('beforeend', newElementStr);
|
||||
return targetTable.tBodies[0].lastElementChild;
|
||||
ret = targetTable.tBodies[0].lastElementChild;
|
||||
} else { //Otherwise just insert it
|
||||
targetTable.insertAdjacentHTML('beforeend', newElementStr);
|
||||
return targetTable.lastElementChild;
|
||||
ret = targetTable.lastElementChild;
|
||||
}
|
||||
|
||||
//Trigger an event to notify other components that a new element has been created, so they can for example initialize select2 on it
|
||||
targetTable.dispatchEvent(new CustomEvent("collection:elementAdded", {bubbles: true}));
|
||||
|
||||
this.focusNumberInput(ret);
|
||||
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
focusNumberInput(element) {
|
||||
const fields = element.querySelectorAll("input[type=number]");
|
||||
//Focus the first available number input field to open the numeric keyboard on mobile devices
|
||||
if(fields.length > 0) {
|
||||
fields[0].focus();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +131,33 @@ export default class extends Controller {
|
|||
dataTransfer.items.add(file);
|
||||
|
||||
rowInput.files = dataTransfer.files;
|
||||
|
||||
//Check the file extension and find the corresponding attachment type based on the data-filetype_filter attribute
|
||||
const attachmentTypeSelect = newElement.querySelector("select");
|
||||
if (attachmentTypeSelect) {
|
||||
let foundMatch = false;
|
||||
for (let j = 0; j < attachmentTypeSelect.options.length; j++) {
|
||||
const option = attachmentTypeSelect.options[j];
|
||||
//skip disabled options
|
||||
if (option.disabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const filter = option.getAttribute('data-filetype_filter');
|
||||
if (filter) {
|
||||
if (accept({name: file.name, type: file.type}, filter)) {
|
||||
attachmentTypeSelect.value = option.value;
|
||||
foundMatch = true;
|
||||
break;
|
||||
}
|
||||
} else { //If no filter is set, chose this option until we find a better match
|
||||
if (!foundMatch) {
|
||||
attachmentTypeSelect.value = option.value;
|
||||
foundMatch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -189,4 +235,4 @@ export default class extends Controller {
|
|||
del();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,8 +83,6 @@ export default class extends Controller {
|
|||
if (data) {
|
||||
//Do not save the start value (current page), as we want to always start at the first page on a page reload
|
||||
delete data.start;
|
||||
//Reset the data length to the default value by deleting the length property
|
||||
delete data.length;
|
||||
}
|
||||
|
||||
return data;
|
||||
|
|
@ -108,11 +106,27 @@ export default class extends Controller {
|
|||
const raw_order = saved_state.order;
|
||||
|
||||
settings.initial_order = raw_order.map((order) => {
|
||||
//Skip if direction is empty, as this is the default, otherwise datatables server is confused when the order is sent in the request, but the initial order is set to an empty direction
|
||||
if (order[1] === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
//The saved order index is visual (post-reorder). If colReorder state
|
||||
//exists, map it back to the original column index so the server sorts
|
||||
//the correct column. colReorder[visualIndex] == originalIndex.
|
||||
let columnIndex = order[0];
|
||||
if (saved_state.colReorder) {
|
||||
columnIndex = saved_state.colReorder[columnIndex];
|
||||
}
|
||||
|
||||
return {
|
||||
column: order[0],
|
||||
column: columnIndex,
|
||||
dir: order[1]
|
||||
}
|
||||
});
|
||||
|
||||
//Remove null values from the initial_order array
|
||||
settings.initial_order = settings.initial_order.filter(order => order !== null);
|
||||
}
|
||||
|
||||
let options = {
|
||||
|
|
|
|||
250
assets/controllers/elements/ipn_suggestion_controller.js
Normal file
250
assets/controllers/elements/ipn_suggestion_controller.js
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
import { Controller } from "@hotwired/stimulus";
|
||||
import "../../css/components/autocomplete_bootstrap_theme.css";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["input"];
|
||||
static values = {
|
||||
partId: Number,
|
||||
partCategoryId: Number,
|
||||
partDescription: String,
|
||||
suggestions: Object,
|
||||
commonSectionHeader: String, // Dynamic header for common Prefixes
|
||||
partIncrementHeader: String, // Dynamic header for new possible part increment
|
||||
suggestUrl: String,
|
||||
};
|
||||
|
||||
connect() {
|
||||
this.configureAutocomplete();
|
||||
this.watchCategoryChanges();
|
||||
this.watchDescriptionChanges();
|
||||
}
|
||||
|
||||
templates = {
|
||||
commonSectionHeader({ title, html }) {
|
||||
return html`
|
||||
<section class="aa-Source">
|
||||
<div class="aa-SourceHeader">
|
||||
<span class="aa-SourceHeaderTitle">${title}</span>
|
||||
<div class="aa-SourceHeaderLine"></div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
},
|
||||
partIncrementHeader({ title, html }) {
|
||||
return html`
|
||||
<section class="aa-Source">
|
||||
<div class="aa-SourceHeader">
|
||||
<span class="aa-SourceHeaderTitle">${title}</span>
|
||||
<div class="aa-SourceHeaderLine"></div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
},
|
||||
list({ html }) {
|
||||
return html`
|
||||
<ul class="aa-List" role="listbox"></ul>
|
||||
`;
|
||||
},
|
||||
item({ suggestion, description, html }) {
|
||||
return html`
|
||||
<li class="aa-Item" role="option" data-suggestion="${suggestion}" aria-selected="false">
|
||||
<div class="aa-ItemWrapper">
|
||||
<div class="aa-ItemContent">
|
||||
<div class="aa-ItemIcon aa-ItemIcon--noBorder">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 21c4.971 0 9-4.029 9-9s-4.029-9-9-9-9 4.029-9 9 4.029 9 9 9z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="aa-ItemContentBody">
|
||||
<div class="aa-ItemContentTitle">${suggestion}</div>
|
||||
<div class="aa-ItemContentDescription">${description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
},
|
||||
};
|
||||
|
||||
configureAutocomplete() {
|
||||
const inputField = this.inputTarget;
|
||||
const commonPrefixes = this.suggestionsValue.commonPrefixes || [];
|
||||
const prefixesPartIncrement = this.suggestionsValue.prefixesPartIncrement || [];
|
||||
const commonHeader = this.commonSectionHeaderValue;
|
||||
const partIncrementHeader = this.partIncrementHeaderValue;
|
||||
|
||||
if (!inputField || (!commonPrefixes.length && !prefixesPartIncrement.length)) return;
|
||||
|
||||
// Check whether the panel should be created at the update
|
||||
if (this.isPanelInitialized) {
|
||||
const existingPanel = inputField.parentNode.querySelector(".aa-Panel");
|
||||
if (existingPanel) {
|
||||
// Only remove the panel in the update phase
|
||||
|
||||
existingPanel.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Create panel
|
||||
const panel = document.createElement("div");
|
||||
panel.classList.add("aa-Panel");
|
||||
panel.style.display = "none";
|
||||
|
||||
// Create panel layout
|
||||
const panelLayout = document.createElement("div");
|
||||
panelLayout.classList.add("aa-PanelLayout", "aa-Panel--scrollable");
|
||||
|
||||
// Section for prefixes part increment
|
||||
if (prefixesPartIncrement.length) {
|
||||
const partIncrementSection = document.createElement("section");
|
||||
partIncrementSection.classList.add("aa-Source");
|
||||
|
||||
const partIncrementHeaderHtml = this.templates.partIncrementHeader({
|
||||
title: partIncrementHeader,
|
||||
html: String.raw,
|
||||
});
|
||||
partIncrementSection.innerHTML += partIncrementHeaderHtml;
|
||||
|
||||
const partIncrementList = document.createElement("ul");
|
||||
partIncrementList.classList.add("aa-List");
|
||||
partIncrementList.setAttribute("role", "listbox");
|
||||
|
||||
prefixesPartIncrement.forEach((prefix) => {
|
||||
const itemHTML = this.templates.item({
|
||||
suggestion: prefix.title,
|
||||
description: prefix.description,
|
||||
html: String.raw,
|
||||
});
|
||||
partIncrementList.innerHTML += itemHTML;
|
||||
});
|
||||
|
||||
partIncrementSection.appendChild(partIncrementList);
|
||||
panelLayout.appendChild(partIncrementSection);
|
||||
}
|
||||
|
||||
// Section for common prefixes
|
||||
if (commonPrefixes.length) {
|
||||
const commonSection = document.createElement("section");
|
||||
commonSection.classList.add("aa-Source");
|
||||
|
||||
const commonSectionHeader = this.templates.commonSectionHeader({
|
||||
title: commonHeader,
|
||||
html: String.raw,
|
||||
});
|
||||
commonSection.innerHTML += commonSectionHeader;
|
||||
|
||||
const commonList = document.createElement("ul");
|
||||
commonList.classList.add("aa-List");
|
||||
commonList.setAttribute("role", "listbox");
|
||||
|
||||
commonPrefixes.forEach((prefix) => {
|
||||
const itemHTML = this.templates.item({
|
||||
suggestion: prefix.title,
|
||||
description: prefix.description,
|
||||
html: String.raw,
|
||||
});
|
||||
commonList.innerHTML += itemHTML;
|
||||
});
|
||||
|
||||
commonSection.appendChild(commonList);
|
||||
panelLayout.appendChild(commonSection);
|
||||
}
|
||||
|
||||
panel.appendChild(panelLayout);
|
||||
inputField.parentNode.appendChild(panel);
|
||||
|
||||
inputField.addEventListener("focus", () => {
|
||||
panel.style.display = "block";
|
||||
});
|
||||
|
||||
inputField.addEventListener("blur", () => {
|
||||
setTimeout(() => {
|
||||
panel.style.display = "none";
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Selection of an item
|
||||
panelLayout.addEventListener("mousedown", (event) => {
|
||||
const target = event.target.closest("li");
|
||||
|
||||
if (target) {
|
||||
inputField.value = target.dataset.suggestion;
|
||||
panel.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
this.isPanelInitialized = true;
|
||||
};
|
||||
|
||||
watchCategoryChanges() {
|
||||
const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]');
|
||||
const descriptionField = document.querySelector('[data-ipn-suggestion="descriptionField"]');
|
||||
this.previousCategoryId = Number(this.partCategoryIdValue);
|
||||
|
||||
if (categoryField) {
|
||||
categoryField.addEventListener("change", () => {
|
||||
const categoryId = Number(categoryField.value);
|
||||
const description = String(descriptionField?.value ?? '');
|
||||
|
||||
// Check whether the category has changed compared to the previous ID
|
||||
if (categoryId !== this.previousCategoryId) {
|
||||
this.fetchNewSuggestions(categoryId, description);
|
||||
this.previousCategoryId = categoryId;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
watchDescriptionChanges() {
|
||||
const categoryField = document.querySelector('[data-ipn-suggestion="categoryField"]');
|
||||
const descriptionField = document.querySelector('[data-ipn-suggestion="descriptionField"]');
|
||||
this.previousDescription = String(this.partDescriptionValue);
|
||||
|
||||
if (descriptionField) {
|
||||
descriptionField.addEventListener("input", () => {
|
||||
const categoryId = Number(categoryField.value);
|
||||
const description = String(descriptionField?.value ?? '');
|
||||
|
||||
// Check whether the description has changed compared to the previous one
|
||||
if (description !== this.previousDescription) {
|
||||
this.fetchNewSuggestions(categoryId, description);
|
||||
this.previousDescription = description;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fetchNewSuggestions(categoryId, description) {
|
||||
const baseUrl = this.suggestUrlValue;
|
||||
const partId = this.partIdValue;
|
||||
const truncatedDescription = description.length > 150 ? description.substring(0, 150) : description;
|
||||
const encodedDescription = this.base64EncodeUtf8(truncatedDescription);
|
||||
const url = `${baseUrl}?partId=${partId}&categoryId=${categoryId}` + (description !== '' ? `&description=${encodedDescription}` : '');
|
||||
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error when calling up the IPN-suggestions: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
this.suggestionsValue = data;
|
||||
this.configureAutocomplete();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Errors when loading the new IPN-suggestions:", error);
|
||||
});
|
||||
};
|
||||
|
||||
base64EncodeUtf8(text) {
|
||||
const utf8Bytes = new TextEncoder().encode(text);
|
||||
return btoa(String.fromCharCode(...utf8Bytes));
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Controller} from "@hotwired/stimulus";
|
||||
|
||||
/**
|
||||
* Purpose of this controller is to allow users to input non-printable characters like EOT, FS, etc. in a form field and submit them correctly with the form.
|
||||
* The visible input field encodes non-printable characters via their Unicode Control picture representation, e.g. \n becomes ␊ and \t becomes ␉, so that they can be displayed in the input field without breaking the form submission.
|
||||
* The actual value of the field, which is submitted with the form, is stored in a hidden input and contains the non-printable characters in their original form.
|
||||
*/
|
||||
export default class extends Controller {
|
||||
|
||||
_hiddenInput;
|
||||
|
||||
connect() {
|
||||
this.element.addEventListener("input", this._update.bind(this));
|
||||
|
||||
// We use a hidden input to store the actual value of the field, which is submitted with the form.
|
||||
// The visible input is just for user interaction and can contain non-printable characters, which are not allowed in the hidden input.
|
||||
this._hiddenInput = document.createElement("input");
|
||||
this._hiddenInput.type = "hidden";
|
||||
this._hiddenInput.name = this.element.name;
|
||||
this.element.removeAttribute("name");
|
||||
this.element.parentNode.insertBefore(this._hiddenInput, this.element.nextSibling);
|
||||
|
||||
this.element.addEventListener("keypress", this._onKeyPress.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that non-printable characters like EOT, FS, etc. gets added to the input value when the user types them
|
||||
* @param event
|
||||
* @private
|
||||
*/
|
||||
_onKeyPress(event) {
|
||||
const ALLOWED_INPUT_CODES = [4, 28, 29, 30, 31]; //EOT, FS, GS, RS, US
|
||||
|
||||
if (!ALLOWED_INPUT_CODES.includes(event.keyCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
const char = String.fromCharCode(event.keyCode);
|
||||
this.element.value += char;
|
||||
|
||||
this._update();
|
||||
|
||||
|
||||
}
|
||||
|
||||
_update() {
|
||||
//Chrome workaround: Remove a leading ∠ character (U+2220) that appears when the user types a non-printable character at the beginning of the input field.
|
||||
if (this.element.value.startsWith("∠")) {
|
||||
this.element.value = this.element.value.substring(1);
|
||||
}
|
||||
|
||||
// Remove non-printable characters from the input value and store them in the hidden input
|
||||
const normalizedValue = this.decodeNonPrintableChars(this.element.value);
|
||||
this._hiddenInput.value = normalizedValue;
|
||||
|
||||
// Encode non-printable characters in the visible input to their Unicode Control picture representation
|
||||
const encodedValue = this.encodeNonPrintableChars(normalizedValue);
|
||||
if (encodedValue !== this.element.value) {
|
||||
this.element.value = encodedValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes non-printable characters in the given string via their Unicode Control picture representation, e.g. \n becomes ␊ and \t becomes ␉.
|
||||
* This allows us to display non-printable characters in the input field without breaking the form submission.
|
||||
* @param str
|
||||
*/
|
||||
encodeNonPrintableChars(str) {
|
||||
return str.replace(/[\x00-\x1F\x7F]/g, (char) => {
|
||||
const code = char.charCodeAt(0);
|
||||
return String.fromCharCode(0x2400 + code);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the Unicode Control picture representation of non-printable characters back to their original form, e.g. ␊ becomes \n and ␉ becomes \t.
|
||||
* @param str
|
||||
*/
|
||||
decodeNonPrintableChars(str) {
|
||||
return str.replace(/[\u2400-\u241F\u2421]/g, (char) => {
|
||||
const code = char.charCodeAt(0) - 0x2400;
|
||||
return String.fromCharCode(code);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -26,9 +26,6 @@ import {marked} from "marked";
|
|||
|
||||
import {
|
||||
trans,
|
||||
SEARCH_PLACEHOLDER,
|
||||
SEARCH_SUBMIT,
|
||||
STATISTICS_PARTS
|
||||
} from '../../translator';
|
||||
|
||||
|
||||
|
|
@ -82,9 +79,9 @@ export default class extends Controller {
|
|||
panelPlacement: this.element.dataset.panelPlacement,
|
||||
plugins: [recentSearchesPlugin],
|
||||
openOnFocus: true,
|
||||
placeholder: trans(SEARCH_PLACEHOLDER),
|
||||
placeholder: trans("search.placeholder"),
|
||||
translations: {
|
||||
submitButtonTitle: trans(SEARCH_SUBMIT)
|
||||
submitButtonTitle: trans("search.submit")
|
||||
},
|
||||
|
||||
// Use a navigator compatible with turbo:
|
||||
|
|
@ -153,7 +150,7 @@ export default class extends Controller {
|
|||
},
|
||||
templates: {
|
||||
header({ html }) {
|
||||
return html`<span class="aa-SourceHeaderTitle">${trans(STATISTICS_PARTS)}</span>
|
||||
return html`<span class="aa-SourceHeaderTitle">${trans("part.labelp")}</span>
|
||||
<div class="aa-SourceHeaderLine" />`;
|
||||
},
|
||||
item({item, components, html}) {
|
||||
|
|
@ -197,4 +194,4 @@ export default class extends Controller {
|
|||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import "tom-select/dist/css/tom-select.bootstrap5.css";
|
|||
import '../../css/components/tom-select_extensions.css';
|
||||
import TomSelect from "tom-select";
|
||||
import {marked} from "marked";
|
||||
import TomSelect_form_reset_handler from '../../tomselect/form_reset_handler/form_reset_handler'
|
||||
|
||||
TomSelect.define('form_reset_handler', TomSelect_form_reset_handler)
|
||||
|
||||
export default class extends Controller {
|
||||
_tomSelect;
|
||||
|
|
@ -18,11 +21,13 @@ export default class extends Controller {
|
|||
|
||||
let settings = {
|
||||
allowEmptyOption: true,
|
||||
plugins: ['dropdown_input'],
|
||||
plugins: ['dropdown_input', this.element.required ? null : 'clear_button', 'form_reset_handler'],
|
||||
searchField: ["name", "description", "category", "footprint"],
|
||||
valueField: "id",
|
||||
labelField: "name",
|
||||
dropdownParent: dropdownParent,
|
||||
selectOnTab: true,
|
||||
clearAfterSelect: true,
|
||||
preload: "focus",
|
||||
render: {
|
||||
item: (data, escape) => {
|
||||
|
|
|
|||
|
|
@ -25,8 +25,7 @@ import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en';
|
|||
import * as zxcvbnDePackage from '@zxcvbn-ts/language-de';
|
||||
import * as zxcvbnFrPackage from '@zxcvbn-ts/language-fr';
|
||||
import * as zxcvbnJaPackage from '@zxcvbn-ts/language-ja';
|
||||
import {trans, USER_PASSWORD_STRENGTH_VERY_WEAK, USER_PASSWORD_STRENGTH_WEAK, USER_PASSWORD_STRENGTH_MEDIUM,
|
||||
USER_PASSWORD_STRENGTH_STRONG, USER_PASSWORD_STRENGTH_VERY_STRONG} from '../../translator.js';
|
||||
import {trans} from '../../translator.js';
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller {
|
||||
|
|
@ -89,23 +88,23 @@ export default class extends Controller {
|
|||
|
||||
switch (level) {
|
||||
case 0:
|
||||
text = trans(USER_PASSWORD_STRENGTH_VERY_WEAK);
|
||||
text = trans("user.password_strength.very_weak");
|
||||
classes = "bg-danger badge-danger";
|
||||
break;
|
||||
case 1:
|
||||
text = trans(USER_PASSWORD_STRENGTH_WEAK);
|
||||
text = trans("user.password_strength.weak");
|
||||
classes = "bg-warning badge-warning";
|
||||
break;
|
||||
case 2:
|
||||
text = trans(USER_PASSWORD_STRENGTH_MEDIUM)
|
||||
text = trans("user.password_strength.medium");
|
||||
classes = "bg-info badge-info";
|
||||
break;
|
||||
case 3:
|
||||
text = trans(USER_PASSWORD_STRENGTH_STRONG);
|
||||
text = trans("user.password_strength.strong");
|
||||
classes = "bg-primary badge-primary";
|
||||
break;
|
||||
case 4:
|
||||
text = trans(USER_PASSWORD_STRENGTH_VERY_STRONG);
|
||||
text = trans("user.password_strength.very_strong");
|
||||
classes = "bg-success badge-success";
|
||||
break;
|
||||
default:
|
||||
|
|
@ -120,4 +119,4 @@ export default class extends Controller {
|
|||
this.badgeTarget.classList.add("badge");
|
||||
this.badgeTarget.classList.add(...classes.split(" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ import {Controller} from "@hotwired/stimulus";
|
|||
import "tom-select/dist/css/tom-select.bootstrap5.css";
|
||||
import '../../css/components/tom-select_extensions.css';
|
||||
import TomSelect from "tom-select";
|
||||
import TomSelect_form_reset_handler from '../../tomselect/form_reset_handler/form_reset_handler'
|
||||
|
||||
TomSelect.define('form_reset_handler', TomSelect_form_reset_handler)
|
||||
|
||||
export default class extends Controller {
|
||||
|
||||
|
|
@ -44,11 +47,12 @@ export default class extends Controller {
|
|||
}
|
||||
|
||||
let settings = {
|
||||
plugins: ["clear_button"],
|
||||
plugins: ["clear_button", "form_reset_handler"],
|
||||
allowEmptyOption: true,
|
||||
selectOnTab: true,
|
||||
maxOptions: null,
|
||||
dropdownParent: dropdownParent,
|
||||
clearAfterSelect: true,
|
||||
|
||||
render: {
|
||||
item: this.renderItem.bind(this),
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ export default class extends Controller {
|
|||
maxItems: 1000,
|
||||
allowEmptyOption: true,
|
||||
dropdownParent: dropdownParent,
|
||||
selectOnTab: true,
|
||||
clearAfterSelect: true,
|
||||
plugins: ['remove_button'],
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import {Controller} from "@hotwired/stimulus";
|
||||
import {default as TreeController} from "./tree_controller";
|
||||
import {EVENT_INITIALIZED} from "@jbtronics/bs-treeview";
|
||||
|
||||
export default class extends TreeController {
|
||||
static targets = [ "tree", 'sourceText' ];
|
||||
|
|
@ -40,6 +41,8 @@ export default class extends TreeController {
|
|||
//Check if we have a saved mode
|
||||
const stored_mode = localStorage.getItem(this._storage_key);
|
||||
|
||||
this._frame = this.element.dataset.frame || "content"; //By default, navigate in the content frame, if a frame is defined
|
||||
|
||||
//Use stored mode if possible, otherwise use default
|
||||
if(stored_mode) {
|
||||
try {
|
||||
|
|
@ -55,6 +58,39 @@ export default class extends TreeController {
|
|||
|
||||
//Register an event listener which checks if the tree needs to be updated
|
||||
document.addEventListener('turbo:render', this.doUpdateIfNeeded.bind(this));
|
||||
|
||||
//Register an event listener, to check if we end up on a page we can highlight in the tree, if so then higlight it
|
||||
document.addEventListener('turbo:load', this._onTurboLoad.bind(this));
|
||||
//On initial page load the tree is not available yet, so do another check after the tree is initialized
|
||||
this.treeTarget.addEventListener(EVENT_INITIALIZED, (event) => {
|
||||
this.selectNodeWithURL(document.location)
|
||||
});
|
||||
}
|
||||
|
||||
_onTurboLoad(event) {
|
||||
this.selectNodeWithURL(event.detail.url);
|
||||
}
|
||||
|
||||
selectNodeWithURL(url) {
|
||||
//Get path from url
|
||||
const path = new URL(url).pathname;
|
||||
|
||||
if (!this._tree) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Unselect all nodes
|
||||
this._tree.unselectAll({silent: true, ignorePreventUnselect: true});
|
||||
|
||||
//Try to find a node with this path as data-path
|
||||
const nodes = this._tree.findNodes(path, "href");
|
||||
if (nodes.length !== 1) {
|
||||
return; //We can only work with exactly one node, if there are multiple nodes with the same path, we cannot know which one to select, so we do nothing
|
||||
}
|
||||
const node = nodes[0];
|
||||
|
||||
node.setSelected(true, {ignorePreventUnselect: true, silent: true});
|
||||
this._tree.revealNode(node);
|
||||
}
|
||||
|
||||
doUpdateIfNeeded()
|
||||
|
|
|
|||
|
|
@ -25,9 +25,11 @@ import TomSelect from "tom-select";
|
|||
|
||||
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
|
||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
||||
import TomSelect_form_reset_handler from '../../tomselect/form_reset_handler/form_reset_handler'
|
||||
|
||||
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
|
||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
||||
TomSelect.define('form_reset_handler', TomSelect_form_reset_handler)
|
||||
|
||||
/**
|
||||
* This is the frontend controller for StaticFileAutocompleteType form element.
|
||||
|
|
@ -56,6 +58,7 @@ export default class extends Controller {
|
|||
searchField: 'text',
|
||||
orderField: 'text',
|
||||
dropdownParent: dropdownParent,
|
||||
clearAfterSelect: true,
|
||||
|
||||
//This a an ugly solution to disable the delimiter parsing of the TomSelect plugin
|
||||
delimiter: 'VERY_L0NG_D€LIMITER_WHICH_WILL_NEVER_BE_ENCOUNTERED_IN_A_STRING',
|
||||
|
|
@ -63,7 +66,8 @@ export default class extends Controller {
|
|||
'autoselect_typed': {},
|
||||
'click_to_edit': {},
|
||||
'clear_button': {},
|
||||
'restore_on_backspace': {}
|
||||
'restore_on_backspace': {},
|
||||
'form_reset_handler': {}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -22,10 +22,12 @@ import '../../css/components/tom-select_extensions.css';
|
|||
import TomSelect from "tom-select";
|
||||
import {Controller} from "@hotwired/stimulus";
|
||||
|
||||
import {trans, ENTITY_SELECT_GROUP_NEW_NOT_ADDED_TO_DB} from '../../translator.js'
|
||||
|
||||
import {trans} from '../../translator.js'
|
||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
||||
import TomSelect_form_reset_handler from '../../tomselect/form_reset_handler/form_reset_handler'
|
||||
|
||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
||||
TomSelect.define('form_reset_handler', TomSelect_form_reset_handler)
|
||||
|
||||
export default class extends Controller {
|
||||
_tomSelect;
|
||||
|
|
@ -58,6 +60,7 @@ export default class extends Controller {
|
|||
delimiter: "$$VERY_LONG_DELIMITER_THAT_SHOULD_NEVER_APPEAR$$",
|
||||
splitOn: null,
|
||||
dropdownParent: dropdownParent,
|
||||
clearAfterSelect: true,
|
||||
|
||||
searchField: [
|
||||
{field: "text", weight : 2},
|
||||
|
|
@ -95,6 +98,7 @@ export default class extends Controller {
|
|||
|
||||
plugins: {
|
||||
"autoselect_typed": {},
|
||||
"form_reset_handler": {},
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -104,6 +108,7 @@ export default class extends Controller {
|
|||
}
|
||||
|
||||
this._tomSelect = new TomSelect(this.element, settings);
|
||||
|
||||
//Do not do a sync here as this breaks the initial rendering of the empty option
|
||||
//this._tomSelect.sync();
|
||||
}
|
||||
|
|
@ -204,7 +209,7 @@ export default class extends Controller {
|
|||
|
||||
if (data.not_in_db_yet) {
|
||||
//Not yet added items are shown italic and with a badge
|
||||
name += "<i><b>" + escape(data.text) + "</b></i>" + "<span class='ms-3 badge bg-info badge-info'>" + trans(ENTITY_SELECT_GROUP_NEW_NOT_ADDED_TO_DB) + "</span>";
|
||||
name += "<i><b>" + escape(data.text) + "</b></i>" + "<span class='ms-3 badge bg-info badge-info'>" + trans("entity.select.group.new_not_added_to_DB") + "</span>";
|
||||
} else {
|
||||
name += "<b>" + escape(data.text) + "</b>";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,11 @@ import TomSelect from "tom-select";
|
|||
|
||||
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
|
||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
||||
import TomSelect_form_reset_handler from '../../tomselect/form_reset_handler/form_reset_handler'
|
||||
|
||||
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
|
||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
||||
TomSelect.define('form_reset_handler', TomSelect_form_reset_handler)
|
||||
|
||||
export default class extends Controller {
|
||||
_tomSelect;
|
||||
|
|
@ -43,12 +45,14 @@ export default class extends Controller {
|
|||
remove_button:{},
|
||||
'autoselect_typed': {},
|
||||
'click_to_edit': {},
|
||||
'form_reset_handler': {},
|
||||
},
|
||||
persistent: false,
|
||||
selectOnTab: true,
|
||||
createOnBlur: true,
|
||||
create: true,
|
||||
dropdownParent: dropdownParent,
|
||||
clearAfterSelect: true,
|
||||
};
|
||||
|
||||
if(this.element.dataset.autocomplete) {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ export default class extends Controller {
|
|||
*/
|
||||
_tree = null;
|
||||
|
||||
_frame = "frame";
|
||||
|
||||
connect() {
|
||||
const treeElement = this.treeTarget;
|
||||
if (!treeElement) {
|
||||
|
|
@ -48,6 +50,7 @@ export default class extends Controller {
|
|||
|
||||
this._url = this.element.dataset.treeUrl;
|
||||
this._data = this.element.dataset.treeData;
|
||||
this._frame = this.element.dataset.frame || "content"; //By default, navigate in the content frame, if a frame is defined
|
||||
|
||||
if(this.element.dataset.treeShowTags === "true") {
|
||||
this._showTags = true;
|
||||
|
|
@ -99,8 +102,18 @@ export default class extends Controller {
|
|||
onNodeSelected: (event) => {
|
||||
const node = event.detail.node;
|
||||
if (node.href) {
|
||||
window.Turbo.visit(node.href, {action: "advance"});
|
||||
this._registerURLWatcher(node);
|
||||
const url = node.href;
|
||||
// Turbo.visit with a frame target bypasses turbo:before-visit, so dispatch it
|
||||
// manually so that dirty-form guards can intercept it.
|
||||
const beforeVisitEvent = new CustomEvent('turbo:before-visit', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
detail: { url, frame: this._frame },
|
||||
});
|
||||
document.dispatchEvent(beforeVisitEvent);
|
||||
if (!beforeVisitEvent.defaultPrevented) {
|
||||
window.Turbo.visit(url, {action: "advance", frame: this._frame});
|
||||
}
|
||||
}
|
||||
},
|
||||
}, [BS5Theme, BS53Theme, FAIconTheme]);
|
||||
|
|
@ -110,41 +123,12 @@ export default class extends Controller {
|
|||
const treeView = event.detail.treeView;
|
||||
treeView.revealNode(treeView.getSelected());
|
||||
|
||||
//Add the url watcher to all selected nodes
|
||||
for (const node of treeView.getSelected()) {
|
||||
this._registerURLWatcher(node);
|
||||
}
|
||||
|
||||
//Add contextmenu event listener to the tree, which allows us to open the links in a new tab with a right click
|
||||
treeView.getTreeElement().addEventListener("contextmenu", this._onContextMenu.bind(this));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
_registerURLWatcher(node)
|
||||
{
|
||||
//Register a watcher for a location change, which will unselect the node, if the location changes
|
||||
const desired_url = node.href;
|
||||
|
||||
//Ensure that the node is unselected, if the location changes
|
||||
const unselectNode = () => {
|
||||
//Parse url so we can properly compare them
|
||||
const desired = new URL(node.href, window.location.origin);
|
||||
|
||||
//We only compare the pathname, because the hash and parameters should not matter
|
||||
if(window.location.pathname !== desired.pathname) {
|
||||
//The ignore parameter is important here, otherwise the node will not be unselected
|
||||
node.setSelected(false, {silent: true, ignorePreventUnselect: true});
|
||||
|
||||
//Unregister the watcher
|
||||
document.removeEventListener('turbo:load', unselectNode);
|
||||
}
|
||||
};
|
||||
|
||||
//Register the watcher via hotwire turbo
|
||||
//We must just load to have the new url in window.location
|
||||
document.addEventListener('turbo:load', unselectNode);
|
||||
}
|
||||
|
||||
_onContextMenu(event)
|
||||
{
|
||||
|
|
@ -198,4 +182,4 @@ export default class extends Controller {
|
|||
return myResolve(this._data);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,13 @@ export default class extends Controller {
|
|||
newFieldSelect.addEventListener('change', this.updateFieldOptions.bind(this))
|
||||
}
|
||||
|
||||
// Auto-increment priority based on existing mappings
|
||||
const nextPriority = this.getNextPriority()
|
||||
const priorityInput = newRow.querySelector('input[name*="[priority]"]')
|
||||
if (priorityInput) {
|
||||
priorityInput.value = nextPriority
|
||||
}
|
||||
|
||||
this.updateFieldOptions()
|
||||
this.updateAddButtonState()
|
||||
}
|
||||
|
|
@ -119,6 +126,18 @@ export default class extends Controller {
|
|||
}
|
||||
}
|
||||
|
||||
getNextPriority() {
|
||||
const priorityInputs = this.tbodyTarget.querySelectorAll('input[name*="[priority]"]')
|
||||
let maxPriority = 0
|
||||
priorityInputs.forEach(input => {
|
||||
const val = parseInt(input.value, 10)
|
||||
if (!isNaN(val) && val > maxPriority) {
|
||||
maxPriority = val
|
||||
}
|
||||
})
|
||||
return Math.min(maxPriority + 1, 10)
|
||||
}
|
||||
|
||||
handleFormSubmit(event) {
|
||||
if (this.hasSubmitButtonTarget) {
|
||||
this.submitButtonTarget.disabled = true
|
||||
|
|
|
|||
|
|
@ -62,6 +62,6 @@ export default class extends Controller {
|
|||
element.disabled = true;
|
||||
}
|
||||
|
||||
form.submit();
|
||||
form.requestSubmit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
136
assets/controllers/helpers/scan_special_char_controller.js
Normal file
136
assets/controllers/helpers/scan_special_char_controller.js
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
/**
|
||||
* This controller listens for a special non-printable character (SOH / ASCII 1) to be entered anywhere on the page,
|
||||
* which is then used as a trigger to submit the following characters as a barcode / scan input.
|
||||
*/
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
// Optional: Log to confirm global attachment
|
||||
console.log("Scanner listener active")
|
||||
|
||||
this.isCapturing = false
|
||||
this.buffer = ""
|
||||
|
||||
window.addEventListener("keypress", this.handleKeydown.bind(this))
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.isCapturing = false
|
||||
this.buffer = ""
|
||||
this.timeoutId = null
|
||||
}
|
||||
|
||||
handleKeydown(event) {
|
||||
|
||||
// Ignore if the user is typing in a form field
|
||||
const isInput = ["INPUT", "TEXTAREA", "SELECT"].includes(event.target.tagName) ||
|
||||
event.target.isContentEditable;
|
||||
if (isInput) return
|
||||
|
||||
// 1. Detect Start of Header (SOH / Ctrl+A)
|
||||
if (event.key === "\x01" || event.keyCode === 1) {
|
||||
this.startCapturing(event)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Process characters if in capture mode
|
||||
if (this.isCapturing) {
|
||||
this.resetTimeout() // Push the expiration back with every keypress
|
||||
|
||||
if (event.key === "Enter" || event.keyCode === 13) {
|
||||
|
||||
this.finishCapturing(event)
|
||||
} else if (event.key.length === 1) {
|
||||
this.buffer += event.key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startCapturing(event) {
|
||||
this.isCapturing = true
|
||||
this.buffer = ""
|
||||
this.resetTimeout()
|
||||
event.preventDefault()
|
||||
console.debug("Scan character detected. Capture started...")
|
||||
}
|
||||
|
||||
finishCapturing(event) {
|
||||
event.preventDefault()
|
||||
const data = this.buffer;
|
||||
this.stopCapturing()
|
||||
this.processCapture(data)
|
||||
}
|
||||
|
||||
stopCapturing() {
|
||||
this.isCapturing = false
|
||||
this.buffer = ""
|
||||
if (this.timeoutId) clearTimeout(this.timeoutId)
|
||||
console.debug("Capture cleared/finished.")
|
||||
}
|
||||
|
||||
resetTimeout() {
|
||||
if (this.timeoutId) clearTimeout(this.timeoutId)
|
||||
|
||||
this.timeoutId = setTimeout(() => {
|
||||
if (this.isCapturing) {
|
||||
console.warn("Capture timed out. Resetting buffer.")
|
||||
this.stopCapturing()
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
processCapture(data) {
|
||||
if (!data) return
|
||||
|
||||
console.debug("Captured scan data: " + data)
|
||||
|
||||
const scanInput = document.getElementById("scan_dialog_input");
|
||||
if (scanInput) { //When we are on the scan dialog page, submit the form there
|
||||
this._submitScanForm(data);
|
||||
} else { //Otherwise use our own form (e.g. on the part list page)
|
||||
this.element.querySelector("input[name='input']").value = data;
|
||||
this.element.requestSubmit();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
_submitScanForm(data) {
|
||||
const scanInput = document.getElementById("scan_dialog_input");
|
||||
if (!scanInput) {
|
||||
console.error("Scan input field not found!")
|
||||
return;
|
||||
}
|
||||
|
||||
scanInput.value = data;
|
||||
scanInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
||||
const form = document.getElementById("scan_dialog_form");
|
||||
if (!form) {
|
||||
console.error("Scan form not found!")
|
||||
return;
|
||||
}
|
||||
|
||||
form.requestSubmit();
|
||||
}
|
||||
}
|
||||
|
|
@ -21,17 +21,31 @@ import {Controller} from "@hotwired/stimulus";
|
|||
//import * as ZXing from "@zxing/library";
|
||||
|
||||
import {Html5QrcodeScanner, Html5Qrcode} from "@part-db/html5-qrcode";
|
||||
import { generateCsrfToken, generateCsrfHeaders } from "../csrf_protection_controller";
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
|
||||
export default class extends Controller {
|
||||
|
||||
//codeReader = null;
|
||||
|
||||
_scanner = null;
|
||||
|
||||
_submitting = false;
|
||||
_lastDecodedText = "";
|
||||
_onInfoChange = null;
|
||||
|
||||
connect() {
|
||||
console.log('Init Scanner');
|
||||
|
||||
// Prevent double init if connect fires twice
|
||||
if (this._scanner) return;
|
||||
|
||||
// clear last decoded barcode when state changes on info box
|
||||
const info = document.getElementById("scan_dialog_info_mode");
|
||||
if (info) {
|
||||
this._onInfoChange = () => {
|
||||
this._lastDecodedText = "";
|
||||
};
|
||||
info.addEventListener("change", this._onInfoChange);
|
||||
}
|
||||
|
||||
const isMobile = window.matchMedia("(max-width: 768px)").matches;
|
||||
|
||||
//This function ensures, that the qrbox is 70% of the total viewport
|
||||
let qrboxFunction = function(viewfinderWidth, viewfinderHeight) {
|
||||
|
|
@ -45,31 +59,67 @@ export default class extends Controller {
|
|||
}
|
||||
|
||||
//Try to get the number of cameras. If the number is 0, then the promise will fail, and we show the warning dialog
|
||||
Html5Qrcode.getCameras().catch((devices) => {
|
||||
document.getElementById('scanner-warning').classList.remove('d-none');
|
||||
Html5Qrcode.getCameras().catch(() => {
|
||||
document.getElementById("scanner-warning")?.classList.remove("d-none");
|
||||
});
|
||||
|
||||
this._scanner = new Html5QrcodeScanner(this.element.id, {
|
||||
fps: 10,
|
||||
qrbox: qrboxFunction,
|
||||
// Key change: shrink preview height on mobile
|
||||
...(isMobile ? { aspectRatio: 1.0 } : {}),
|
||||
experimentalFeatures: {
|
||||
//This option improves reading quality on android chrome
|
||||
useBarCodeDetectorIfSupported: true
|
||||
}
|
||||
useBarCodeDetectorIfSupported: true,
|
||||
},
|
||||
}, false);
|
||||
|
||||
this._scanner.render(this.onScanSuccess.bind(this));
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this._scanner.pause();
|
||||
this._scanner.clear();
|
||||
|
||||
// If we already stopped/cleared before submit, nothing to do.
|
||||
const scanner = this._scanner;
|
||||
this._scanner = null;
|
||||
this._lastDecodedText = "";
|
||||
|
||||
// Unbind info-mode change handler (always do this, even if scanner is null)
|
||||
const info = document.getElementById("scan_dialog_info_mode");
|
||||
if (info && this._onInfoChange) {
|
||||
info.removeEventListener("change", this._onInfoChange);
|
||||
}
|
||||
this._onInfoChange = null;
|
||||
|
||||
if (!scanner) return;
|
||||
|
||||
try {
|
||||
const p = scanner.clear?.();
|
||||
if (p && typeof p.then === "function") p.catch(() => {});
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
onScanSuccess(decodedText, decodedResult) {
|
||||
//Put our decoded Text into the input box
|
||||
document.getElementById('scan_dialog_input').value = decodedText;
|
||||
|
||||
onScanSuccess(decodedText) {
|
||||
if (!decodedText) return;
|
||||
|
||||
const normalized = String(decodedText).trim();
|
||||
if (!normalized) return;
|
||||
|
||||
// scan once per barcode
|
||||
if (normalized === this._lastDecodedText) return;
|
||||
|
||||
// Mark as handled immediately (prevents spam even if callback fires repeatedly)
|
||||
this._lastDecodedText = normalized;
|
||||
|
||||
const input = document.getElementById('scan_dialog_input');
|
||||
input.value = decodedText;
|
||||
//Trigger nonprintable char input controller to update the hidden input value
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
|
||||
//Submit form
|
||||
document.getElementById('scan_dialog_form').submit();
|
||||
document.getElementById('scan_dialog_form').requestSubmit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,11 @@ import "katex/dist/katex.css";
|
|||
|
||||
import TomSelect_click_to_edit from '../../tomselect/click_to_edit/click_to_edit'
|
||||
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
|
||||
import TomSelect_form_reset_handler from '../../tomselect/form_reset_handler/form_reset_handler'
|
||||
|
||||
TomSelect.define('click_to_edit', TomSelect_click_to_edit)
|
||||
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
|
||||
TomSelect.define('form_reset_handler', TomSelect_form_reset_handler)
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default class extends Controller
|
||||
|
|
@ -63,7 +65,8 @@ export default class extends Controller
|
|||
'autoselect_typed': {},
|
||||
'click_to_edit': {},
|
||||
'clear_button': {},
|
||||
'restore_on_backspace': {}
|
||||
'restore_on_backspace': {},
|
||||
'form_reset_handler': {}
|
||||
},
|
||||
persistent: false,
|
||||
maxItems: 1,
|
||||
|
|
@ -75,6 +78,7 @@ export default class extends Controller
|
|||
searchField: "name",
|
||||
//labelField: "name",
|
||||
valueField: "name",
|
||||
clearAfterSelect: true,
|
||||
onItemAdd: this.onItemAdd.bind(this),
|
||||
render: {
|
||||
option: (data, escape) => {
|
||||
|
|
@ -136,4 +140,4 @@ export default class extends Controller
|
|||
//Destroy the TomSelect instance
|
||||
this._tomSelect.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
assets/controllers/pages/part_stocktake_modal_controller.js
Normal file
27
assets/controllers/pages/part_stocktake_modal_controller.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import {Controller} from "@hotwired/stimulus";
|
||||
import {Modal} from "bootstrap";
|
||||
|
||||
export default class extends Controller
|
||||
{
|
||||
connect() {
|
||||
this.element.addEventListener('show.bs.modal', event => this._handleModalOpen(event));
|
||||
}
|
||||
|
||||
_handleModalOpen(event) {
|
||||
// Button that triggered the modal
|
||||
const button = event.relatedTarget;
|
||||
|
||||
const amountInput = this.element.querySelector('input[name="amount"]');
|
||||
|
||||
// Extract info from button attributes
|
||||
const lotID = button.getAttribute('data-lot-id');
|
||||
const lotAmount = button.getAttribute('data-lot-amount');
|
||||
|
||||
//Find the expected amount field and set the value to the lot amount
|
||||
const expectedAmountInput = this.element.querySelector('#stocktake-modal-expected-amount');
|
||||
expectedAmountInput.textContent = lotAmount;
|
||||
|
||||
//Set the action and lotID inputs in the form
|
||||
this.element.querySelector('input[name="lot_id"]').setAttribute('value', lotID);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ export default class extends Controller
|
|||
{
|
||||
connect() {
|
||||
this.element.addEventListener('show.bs.modal', event => this._handleModalOpen(event));
|
||||
this.element.addEventListener('shown.bs.modal', event => this._handleModalShown(event));
|
||||
}
|
||||
|
||||
_handleModalOpen(event) {
|
||||
|
|
@ -61,4 +62,8 @@ export default class extends Controller
|
|||
amountInput.setAttribute('max', lotAmount);
|
||||
}
|
||||
}
|
||||
|
||||
_handleModalShown(event) {
|
||||
this.element.querySelector('input[name="amount"]').focus();
|
||||
}
|
||||
}
|
||||
68
assets/controllers/pages/synonyms_collection_controller.js
Normal file
68
assets/controllers/pages/synonyms_collection_controller.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ['items'];
|
||||
static values = {
|
||||
prototype: String,
|
||||
prototypeName: { type: String, default: '__name__' },
|
||||
index: { type: Number, default: 0 },
|
||||
};
|
||||
|
||||
connect() {
|
||||
if (!this.hasIndexValue || Number.isNaN(this.indexValue)) {
|
||||
this.indexValue = this.itemsTarget?.children.length || 0;
|
||||
}
|
||||
}
|
||||
|
||||
add(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const encodedProto = this.prototypeValue || '';
|
||||
const placeholder = this.prototypeNameValue || '__name__';
|
||||
if (!encodedProto || !this.itemsTarget) return;
|
||||
|
||||
const protoHtml = this._decodeHtmlAttribute(encodedProto);
|
||||
|
||||
const idx = this.indexValue;
|
||||
const html = protoHtml.replace(new RegExp(placeholder, 'g'), String(idx));
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = html;
|
||||
const newItem = wrapper.firstElementChild;
|
||||
if (newItem) {
|
||||
this.itemsTarget.appendChild(newItem);
|
||||
this.indexValue = idx + 1;
|
||||
}
|
||||
}
|
||||
|
||||
remove(event) {
|
||||
event.preventDefault();
|
||||
const row = event.currentTarget.closest('.tc-item');
|
||||
if (row) row.remove();
|
||||
}
|
||||
|
||||
_decodeHtmlAttribute(str) {
|
||||
const tmp = document.createElement('textarea');
|
||||
tmp.innerHTML = str;
|
||||
return tmp.value || tmp.textContent || '';
|
||||
}
|
||||
}
|
||||
81
assets/controllers/update_confirm_controller.js
Normal file
81
assets/controllers/update_confirm_controller.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
/**
|
||||
* Stimulus controller for update/downgrade confirmation dialogs.
|
||||
* Intercepts form submission and shows a confirmation dialog before proceeding.
|
||||
*/
|
||||
export default class extends Controller {
|
||||
static values = {
|
||||
isDowngrade: { type: Boolean, default: false },
|
||||
targetVersion: { type: String, default: '' },
|
||||
confirmUpdate: { type: String, default: 'Are you sure you want to update Part-DB?' },
|
||||
confirmDowngrade: { type: String, default: 'Are you sure you want to downgrade Part-DB?' },
|
||||
downgradeWarning: { type: String, default: 'WARNING: This version does not include the Update Manager.' },
|
||||
minUpdateManagerVersion: { type: String, default: '2.6.0' },
|
||||
};
|
||||
|
||||
connect() {
|
||||
this.element.addEventListener('submit', this.handleSubmit.bind(this));
|
||||
}
|
||||
|
||||
handleSubmit(event) {
|
||||
// Always prevent default first
|
||||
event.preventDefault();
|
||||
|
||||
const targetClean = this.targetVersionValue.replace(/^v/, '');
|
||||
let message;
|
||||
|
||||
if (this.isDowngradeValue) {
|
||||
// Check if downgrading to a version without Update Manager
|
||||
if (this.compareVersions(targetClean, this.minUpdateManagerVersionValue) < 0) {
|
||||
message = this.confirmDowngradeValue + '\n\n⚠️ ' + this.downgradeWarningValue;
|
||||
} else {
|
||||
message = this.confirmDowngradeValue;
|
||||
}
|
||||
} else {
|
||||
message = this.confirmUpdateValue;
|
||||
}
|
||||
|
||||
// Only submit if user confirms
|
||||
if (confirm(message)) {
|
||||
// Remove the event listener to prevent infinite loop, then submit
|
||||
this.element.removeEventListener('submit', this.handleSubmit.bind(this));
|
||||
this.element.submit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two version strings (e.g., "2.5.0" vs "2.6.0")
|
||||
* Returns -1 if v1 < v2, 0 if equal, 1 if v1 > v2
|
||||
*/
|
||||
compareVersions(v1, v2) {
|
||||
const parts1 = v1.split('.').map(Number);
|
||||
const parts2 = v2.split('.').map(Number);
|
||||
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
||||
const p1 = parts1[i] || 0;
|
||||
const p2 = parts2[i] || 0;
|
||||
if (p1 < p2) return -1;
|
||||
if (p1 > p2) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -58,6 +58,12 @@
|
|||
object-fit: contain;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.part-info-image {
|
||||
max-height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.object-fit-cover {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ showing the sidebar (on devices with md or higher)
|
|||
*/
|
||||
#sidebar-toggle-button {
|
||||
position: fixed;
|
||||
left: 3px;
|
||||
left: 2px;
|
||||
bottom: 50%;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -125,3 +125,25 @@ Classes for Datatables export
|
|||
.export-helper{
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**********************************************************
|
||||
* Table row highlighting tools
|
||||
***********************************************************/
|
||||
|
||||
.row-highlight {
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.20); /* Adds depth */
|
||||
position: relative;
|
||||
z-index: 1; /* Ensures the shadow overlaps other rows */
|
||||
border-left: 5px solid var(--bs-primary); /* Adds a vertical accent bar */
|
||||
}
|
||||
|
||||
@keyframes pulse-highlight {
|
||||
0% { outline: 2px solid transparent; }
|
||||
50% { outline: 2px solid var(--bs-primary); }
|
||||
100% { outline: 2px solid transparent; }
|
||||
}
|
||||
|
||||
.row-pulse {
|
||||
animation: pulse-highlight 1s ease-in-out;
|
||||
animation-iteration-count: 3;
|
||||
}
|
||||
|
|
|
|||
61
assets/css/components/dirty_form.css
Normal file
61
assets/css/components/dirty_form.css
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* Standard Bootstrap form controls */
|
||||
.form-control[data-dirty],
|
||||
.form-select[data-dirty],
|
||||
input[data-dirty] + .form-control,
|
||||
select[data-dirty] + .form-select
|
||||
{
|
||||
border-color: var(--bs-info);
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.form-control[data-dirty]:focus,
|
||||
.form-select[data-dirty]:focus,
|
||||
input[data-dirty] + .form-control:focus,
|
||||
select[data-dirty] + .form-select:focus
|
||||
{
|
||||
border-color: var(--bs-info);
|
||||
border-width: 2px;
|
||||
box-shadow: 0 0 0 0.25rem rgba(var(--bs-info-rgb), 0.25);
|
||||
}
|
||||
|
||||
/* Checkboxes and radios */
|
||||
.form-check-input[data-dirty] {
|
||||
border-color: var(--bs-info);
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.form-check-input[data-dirty]:checked {
|
||||
background-color: var(--bs-info);
|
||||
border-color: var(--bs-info);
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
/* CKEditor: the editable sits after the hidden source textarea in the DOM */
|
||||
textarea[data-dirty] ~ .ck.ck-editor .ck-editor__editable:not(.ck-focused) {
|
||||
border-color: var(--bs-info) !important;
|
||||
border-width: 2px !important;
|
||||
}
|
||||
|
||||
textarea[data-dirty] ~ .ck.ck-editor .ck-editor__editable.ck-focused {
|
||||
border-width: 2px !important;
|
||||
box-shadow: 0 0 0 0.25rem rgba(var(--bs-info-rgb), 0.25) !important;
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ import '../css/app/treeview.css';
|
|||
import '../css/app/images.css';
|
||||
|
||||
// start the Stimulus application
|
||||
import '../bootstrap';
|
||||
import '../stimulus_bootstrap';
|
||||
|
||||
// Need jQuery? Install it with "yarn add jquery", then uncomment to require it.
|
||||
const $ = require('jquery');
|
||||
|
|
@ -44,7 +44,7 @@ import "./register_events";
|
|||
import "./tristate_checkboxes";
|
||||
|
||||
//Define jquery globally
|
||||
window.$ = window.jQuery = require("jquery");
|
||||
global.$ = global.jQuery = require("jquery");
|
||||
|
||||
//Use the local WASM file for the ZXing library
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ export default class TristateCheckbox {
|
|||
|
||||
//Do a refresh to set the correct styling of the checkbox
|
||||
this._refresh();
|
||||
// Anchor the hidden input's default so dirty-form comparisons have a clean baseline.
|
||||
this._hiddenInput.defaultValue = this._hiddenInput.value;
|
||||
|
||||
this._element.addEventListener('click', this.click.bind(this));
|
||||
}
|
||||
|
|
@ -202,6 +204,9 @@ export default class TristateCheckbox {
|
|||
}
|
||||
|
||||
this._refresh();
|
||||
// Notify change listeners (e.g. dirty-form controller) since programmatic
|
||||
// value changes don't fire native change events.
|
||||
this._hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -27,7 +27,12 @@ class RegisterEventHelper {
|
|||
constructor() {
|
||||
this.registerTooltips();
|
||||
this.configureDropdowns();
|
||||
this.registerSpecialCharInput();
|
||||
|
||||
// Only register special character input if enabled in configuration
|
||||
const keybindingsEnabled = document.body.dataset.keybindingsSpecialCharacters !== 'false';
|
||||
if (keybindingsEnabled) {
|
||||
this.registerSpecialCharInput();
|
||||
}
|
||||
|
||||
//Initialize ClipboardJS
|
||||
this.registerLoadHandler(() => {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,9 @@ class TristateHelper {
|
|||
|
||||
document.addEventListener("turbo:load", listener);
|
||||
document.addEventListener("turbo:render", listener);
|
||||
document.addEventListener("turbo:frame-load", listener);
|
||||
document.addEventListener("collection:elementAdded", listener);
|
||||
}
|
||||
}
|
||||
|
||||
export default new TristateHelper();
|
||||
export default new TristateHelper();
|
||||
|
|
|
|||
|
|
@ -198,6 +198,7 @@ class WebauthnTFA {
|
|||
{
|
||||
const resultField = document.getElementById('_auth_code');
|
||||
resultField.value = JSON.stringify(data)
|
||||
//requestSubmit() do not work here, probably because the submit is considered invalid. But as we do not use CSFR tokens, it should be fine.
|
||||
form.submit();
|
||||
}
|
||||
|
||||
|
|
@ -232,4 +233,4 @@ class WebauthnTFA {
|
|||
}
|
||||
}
|
||||
|
||||
window.webauthnTFA = new WebauthnTFA();
|
||||
window.webauthnTFA = new WebauthnTFA();
|
||||
|
|
|
|||
46
assets/tomselect/form_reset_handler/form_reset_handler.js
Normal file
46
assets/tomselect/form_reset_handler/form_reset_handler.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* TomSelect plugin for dirty-check integration and form reset support.
|
||||
*
|
||||
* Sets data-default-value on the underlying <select> so the dirty-check
|
||||
* controller can detect changes, and restores that value when the form is reset.
|
||||
*/
|
||||
export default function form_reset_handler() {
|
||||
const self = this;
|
||||
const input = this.input;
|
||||
|
||||
// Multiple selects not yet supported
|
||||
if (input.multiple) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Always capture the initial value, even empty string.
|
||||
// Empty string is falsy, so the old `|| null` guard would silently skip it,
|
||||
// leaving data-default-value unset and breaking the dirty check for blank defaults.
|
||||
input.dataset.defaultValue = input.value;
|
||||
|
||||
if (input.form) {
|
||||
input.form.addEventListener('reset', () => {
|
||||
input.value = input.dataset.defaultValue ?? '';
|
||||
self.sync();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { localeFallbacks } from '../var/translations/configuration';
|
||||
import { trans, getLocale, setLocale, setLocaleFallbacks } from '@symfony/ux-translator';
|
||||
import { createTranslator } from '@symfony/ux-translator';
|
||||
import { messages, localeFallbacks } from '../var/translations/index.js';
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony UX Translator package.
|
||||
*
|
||||
|
|
@ -9,8 +10,12 @@ import { trans, getLocale, setLocale, setLocaleFallbacks } from '@symfony/ux-tra
|
|||
* If you use TypeScript, you can rename this file to "translator.ts" to take advantage of types checking.
|
||||
*/
|
||||
|
||||
setLocaleFallbacks(localeFallbacks);
|
||||
const translator = createTranslator({
|
||||
messages,
|
||||
localeFallbacks,
|
||||
});
|
||||
|
||||
export { trans };
|
||||
|
||||
export * from '../var/translations';
|
||||
// Wrapper function with default domain set to 'frontend'
|
||||
export const trans = (id, parameters = {}, domain = 'frontend', locale = null) => {
|
||||
return translator.trans(id, parameters, domain, locale);
|
||||
};
|
||||
|
|
|
|||
21
bin/phpunit
21
bin/phpunit
|
|
@ -1,23 +1,4 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
if (!ini_get('date.timezone')) {
|
||||
ini_set('date.timezone', 'UTC');
|
||||
}
|
||||
|
||||
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
|
||||
if (PHP_VERSION_ID >= 80000) {
|
||||
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
|
||||
} else {
|
||||
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
|
||||
require PHPUNIT_COMPOSER_INSTALL;
|
||||
PHPUnit\TextUI\Command::main();
|
||||
}
|
||||
} else {
|
||||
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
|
||||
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';
|
||||
}
|
||||
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
|
||||
|
|
|
|||
|
|
@ -11,12 +11,14 @@
|
|||
"ext-intl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-zip": "*",
|
||||
"amphp/http-client": "^5.1",
|
||||
"api-platform/doctrine-orm": "^4.1",
|
||||
"api-platform/json-api": "^4.0.0",
|
||||
"api-platform/symfony": "^4.0.0",
|
||||
"beberlei/doctrineextensions": "^1.2",
|
||||
"brick/math": "^0.13.1",
|
||||
"brick/math": "^0.17.0",
|
||||
"brick/schema": "^0.2.0",
|
||||
"composer/ca-bundle": "^1.5",
|
||||
"composer/package-versions-deprecated": "^1.11.99.5",
|
||||
"doctrine/data-fixtures": "^2.0.0",
|
||||
|
|
@ -26,11 +28,12 @@
|
|||
"doctrine/orm": "^3.2.0",
|
||||
"dompdf/dompdf": "^3.1.2",
|
||||
"gregwar/captcha-bundle": "^2.1.0",
|
||||
"hshn/base64-encoded-file": "^5.0",
|
||||
"hshn/base64-encoded-file": "^6.0",
|
||||
"jbtronics/2fa-webauthn": "^3.0.0",
|
||||
"jbtronics/dompdf-font-loader-bundle": "^1.0.0",
|
||||
"jbtronics/settings-bundle": "^3.0.0",
|
||||
"jfcherng/php-diff": "^6.14",
|
||||
"jkphl/micrometa": "^v3.4.0",
|
||||
"knpuniversity/oauth2-client-bundle": "^2.15",
|
||||
"league/commonmark": "^2.7",
|
||||
"league/csv": "^9.8.0",
|
||||
|
|
@ -43,12 +46,10 @@
|
|||
"nelmio/security-bundle": "^3.0",
|
||||
"nyholm/psr7": "^1.1",
|
||||
"omines/datatables-bundle": "^0.10.0",
|
||||
"paragonie/sodium_compat": "^1.21",
|
||||
"part-db/label-fonts": "^1.0",
|
||||
"part-db/swap-bundle": "^6.0.0",
|
||||
"phpoffice/phpspreadsheet": "^5.0.0",
|
||||
"rhukster/dom-sanitizer": "^1.0",
|
||||
"runtime/frankenphp-symfony": "^0.2.0",
|
||||
"s9e/text-formatter": "^2.1",
|
||||
"scheb/2fa-backup-code": "^v7.11.0",
|
||||
"scheb/2fa-bundle": "^v7.11.0",
|
||||
|
|
@ -56,39 +57,43 @@
|
|||
"scheb/2fa-trusted-device": "^v7.11.0",
|
||||
"shivas/versioning-bundle": "^4.0",
|
||||
"spatie/db-dumper": "^3.3.1",
|
||||
"symfony/ai-bundle": "^0.9.0",
|
||||
"symfony/ai-lm-studio-platform": "^0.9.0",
|
||||
"symfony/ai-open-router-platform": "^0.9.0",
|
||||
"symfony/apache-pack": "^1.0",
|
||||
"symfony/asset": "7.3.*",
|
||||
"symfony/console": "7.3.*",
|
||||
"symfony/css-selector": "7.3.*",
|
||||
"symfony/dom-crawler": "7.3.*",
|
||||
"symfony/dotenv": "7.3.*",
|
||||
"symfony/expression-language": "7.3.*",
|
||||
"symfony/asset": "7.4.*",
|
||||
"symfony/console": "7.4.*",
|
||||
"symfony/css-selector": "7.4.*",
|
||||
"symfony/dom-crawler": "7.4.*",
|
||||
"symfony/dotenv": "7.4.*",
|
||||
"symfony/expression-language": "7.4.*",
|
||||
"symfony/flex": "^v2.3.1",
|
||||
"symfony/form": "7.3.*",
|
||||
"symfony/framework-bundle": "7.3.*",
|
||||
"symfony/http-client": "7.3.*",
|
||||
"symfony/http-kernel": "7.3.*",
|
||||
"symfony/mailer": "7.3.*",
|
||||
"symfony/monolog-bundle": "^3.1",
|
||||
"symfony/polyfill-php82": "^1.28",
|
||||
"symfony/process": "7.3.*",
|
||||
"symfony/property-access": "7.3.*",
|
||||
"symfony/property-info": "7.3.*",
|
||||
"symfony/rate-limiter": "7.3.*",
|
||||
"symfony/runtime": "7.3.*",
|
||||
"symfony/security-bundle": "7.3.*",
|
||||
"symfony/serializer": "7.3.*",
|
||||
"symfony/string": "7.3.*",
|
||||
"symfony/translation": "7.3.*",
|
||||
"symfony/twig-bundle": "7.3.*",
|
||||
"symfony/ux-translator": "^2.10",
|
||||
"symfony/form": "7.4.*",
|
||||
"symfony/framework-bundle": "7.4.*",
|
||||
"symfony/http-client": "7.4.*",
|
||||
"symfony/http-kernel": "7.4.*",
|
||||
"symfony/mailer": "7.4.*",
|
||||
"symfony/monolog-bundle": "^4.0",
|
||||
"symfony/process": "7.4.*",
|
||||
"symfony/property-access": "7.4.*",
|
||||
"symfony/property-info": "7.4.*",
|
||||
"symfony/rate-limiter": "7.4.*",
|
||||
"symfony/runtime": "7.4.*",
|
||||
"symfony/security-bundle": "7.4.*",
|
||||
"symfony/serializer": "7.4.*",
|
||||
"symfony/string": "7.4.*",
|
||||
"symfony/translation": "7.4.*",
|
||||
"symfony/twig-bundle": "7.4.*",
|
||||
"symfony/type-info": "7.4.*",
|
||||
"symfony/ux-translator": "^2.32.0",
|
||||
"symfony/ux-turbo": "^2.0",
|
||||
"symfony/validator": "7.3.*",
|
||||
"symfony/web-link": "7.3.*",
|
||||
"symfony/validator": "7.4.*",
|
||||
"symfony/web-link": "7.4.*",
|
||||
"symfony/webpack-encore-bundle": "^v2.0.1",
|
||||
"symfony/yaml": "7.3.*",
|
||||
"symplify/easy-coding-standard": "^12.5.20",
|
||||
"symfony/yaml": "7.4.*",
|
||||
"symplify/easy-coding-standard": "^13.0",
|
||||
"tecnickcom/tc-lib-barcode": "^2.1.4",
|
||||
"tiendanube/gtinvalidation": "^1.0",
|
||||
"twig/cssinliner-extra": "^3.0",
|
||||
"twig/extra-bundle": "^3.8",
|
||||
"twig/html-extra": "^3.8",
|
||||
|
|
@ -111,16 +116,23 @@
|
|||
"phpunit/phpunit": "^11.5.0",
|
||||
"rector/rector": "^2.0.4",
|
||||
"roave/security-advisories": "dev-latest",
|
||||
"symfony/browser-kit": "7.3.*",
|
||||
"symfony/debug-bundle": "7.3.*",
|
||||
"symfony/browser-kit": "7.4.*",
|
||||
"symfony/debug-bundle": "7.4.*",
|
||||
"symfony/maker-bundle": "^1.13",
|
||||
"symfony/phpunit-bridge": "7.3.*",
|
||||
"symfony/stopwatch": "7.3.*",
|
||||
"symfony/web-profiler-bundle": "7.3.*"
|
||||
"symfony/phpunit-bridge": "7.4.*",
|
||||
"symfony/stopwatch": "7.4.*",
|
||||
"symfony/web-profiler-bundle": "7.4.*"
|
||||
},
|
||||
"replace": {
|
||||
"symfony/polyfill-mbstring": "*",
|
||||
"symfony/polyfill-php74": "*",
|
||||
"symfony/polyfill-php80": "*",
|
||||
"symfony/polyfill-php81": "*",
|
||||
"symfony/polyfill-php82": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-bcmath": "Used to improve price calculation performance",
|
||||
"ext-gmp": "Used to improve price calculation performanice"
|
||||
"ext-gmp": "Used to improve price calculation performance"
|
||||
},
|
||||
"config": {
|
||||
"preferred-install": {
|
||||
|
|
@ -167,8 +179,13 @@
|
|||
"extra": {
|
||||
"symfony": {
|
||||
"allow-contrib": false,
|
||||
"require": "7.3.*",
|
||||
"require": "7.4.*",
|
||||
"docker": true
|
||||
},
|
||||
"phpstan/extension-installer": {
|
||||
"ignore" : [
|
||||
"ekino/phpstan-banned-code"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6641
composer.lock
generated
6641
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -33,4 +33,5 @@ return [
|
|||
Jbtronics\SettingsBundle\JbtronicsSettingsBundle::class => ['all' => true],
|
||||
Jbtronics\TranslationEditorBundle\JbtronicsTranslationEditorBundle::class => ['dev' => true],
|
||||
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||
Symfony\AI\AiBundle\AiBundle::class => ['all' => true],
|
||||
];
|
||||
|
|
|
|||
27
config/packages/ai.yaml
Normal file
27
config/packages/ai.yaml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
ai:
|
||||
platform:
|
||||
# Inference Platform configuration
|
||||
# see https://github.com/symfony/ai/tree/main/src/platform#platform-bridges
|
||||
|
||||
# openai:
|
||||
# api_key: '%env(OPENAI_API_KEY)%'
|
||||
|
||||
agent:
|
||||
# Agent configuration
|
||||
# see https://symfony.com/doc/current/ai/bundles/ai-bundle.html
|
||||
|
||||
# default:
|
||||
# platform: 'ai.platform.openai'
|
||||
# model: 'gpt-5-mini'
|
||||
# prompt: |
|
||||
# You are a pirate and you write funny.
|
||||
# tools:
|
||||
# - 'Symfony\AI\Agent\Bridge\Clock\Clock'
|
||||
|
||||
store:
|
||||
# Store configuration
|
||||
|
||||
# chromadb:
|
||||
# default:
|
||||
# client: 'client.service.id'
|
||||
# collection: 'my_collection'
|
||||
5
config/packages/ai_generic_platform.yaml
Normal file
5
config/packages/ai_generic_platform.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
ai:
|
||||
platform:
|
||||
generic:
|
||||
default:
|
||||
base_url: '%env(GENERIC_BASE_URL)%'
|
||||
4
config/packages/ai_lm_studio_platform.yaml
Normal file
4
config/packages/ai_lm_studio_platform.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
ai:
|
||||
platform:
|
||||
lmstudio:
|
||||
host_url: '%env(string:settings:ai_lmstudio:hostURL)%'
|
||||
4
config/packages/ai_open_router_platform.yaml
Normal file
4
config/packages/ai_open_router_platform.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
ai:
|
||||
platform:
|
||||
openrouter:
|
||||
api_key: '%env(string:settings:ai_openrouter:apiKey)%'
|
||||
|
|
@ -23,3 +23,7 @@ framework:
|
|||
|
||||
info_provider.cache:
|
||||
adapter: cache.app
|
||||
|
||||
cache.settings:
|
||||
adapter: cache.system
|
||||
tags: true
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ datatables:
|
|||
|
||||
# Set options, as documented at https://datatables.net/reference/option/
|
||||
options:
|
||||
lengthMenu : [[10, 25, 50, 100], [10, 25, 50, 100]] # We add the "All" option, when part tables are generated
|
||||
lengthMenu : [[10, 25, 50, 100, 250, 500], [10, 25, 50, 100, 250, 500]] # We add the "All" option, when part tables are generated
|
||||
#pageLength: '%partdb.table.default_page_size%' # Set to -1 to disable pagination (i.e. show all rows) by default
|
||||
pageLength: 50 #TODO
|
||||
dom: " <'row' <'col mb-2 input-group flex-nowrap' B l > <'col-auto mb-2' < p >>>
|
||||
|
|
|
|||
|
|
@ -20,12 +20,14 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Symfony\Config\DoctrineConfig;
|
||||
|
||||
/**
|
||||
* This class extends the default doctrine ORM configuration to enable native lazy objects on PHP 8.4+.
|
||||
* We have to do this in a PHP file, because the yaml file does not support conditionals on PHP version.
|
||||
*/
|
||||
|
||||
return static function(\Symfony\Config\DoctrineConfig $doctrine) {
|
||||
return static function(DoctrineConfig $doctrine) {
|
||||
//On PHP 8.4+ we can use native lazy objects, which are much more efficient than proxies.
|
||||
if (PHP_VERSION_ID >= 80400) {
|
||||
$doctrine->orm()->enableNativeLazyObjects(true);
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ doctrine:
|
|||
natsort: App\Doctrine\Functions\Natsort
|
||||
array_position: App\Doctrine\Functions\ArrayPosition
|
||||
ilike: App\Doctrine\Functions\ILike
|
||||
si_value_sort: App\Doctrine\Functions\SiValueSort
|
||||
|
||||
when@test:
|
||||
doctrine:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# yaml-language-server: $schema=../../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
|
||||
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||
framework:
|
||||
secret: '%env(APP_SECRET)%'
|
||||
|
|
@ -8,6 +9,7 @@ framework:
|
|||
# Must be set to true, to enable the change of HTTP method via _method parameter, otherwise our delete routines does not work anymore
|
||||
# TODO: Rework delete routines to work without _method parameter as it is not recommended anymore (see https://github.com/symfony/symfony/issues/45278)
|
||||
http_method_override: true
|
||||
allowed_http_method_override: ['DELETE']
|
||||
|
||||
# Allow users to configure trusted hosts via .env variables
|
||||
# see https://symfony.com/doc/current/reference/configuration/framework.html#trusted-hosts
|
||||
|
|
|
|||
|
|
@ -35,4 +35,4 @@ knpu_oauth2_client:
|
|||
provider_options:
|
||||
urlAuthorize: 'https://identity.nexar.com/connect/authorize'
|
||||
urlAccessToken: 'https://identity.nexar.com/connect/token'
|
||||
urlResourceOwnerDetails: ''
|
||||
urlResourceOwnerDetails: ''
|
||||
|
|
|
|||
|
|
@ -10,14 +10,6 @@ when@dev:
|
|||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!event"]
|
||||
# uncomment to get logging in your browser
|
||||
# you may have to allow bigger header sizes in your Web server configuration
|
||||
#firephp:
|
||||
# type: firephp
|
||||
# level: info
|
||||
#chromephp:
|
||||
# type: chromephp
|
||||
# level: info
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
|
|
@ -45,6 +37,7 @@ when@prod:
|
|||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
channels: ["!deprecation"]
|
||||
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||
nested:
|
||||
type: stream
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ jbtronics_settings:
|
|||
|
||||
cache:
|
||||
default_cacheable: true
|
||||
service: 'cache.settings'
|
||||
|
||||
orm_storage:
|
||||
default_entity_class: App\Entity\SettingsEntry
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
twig:
|
||||
default_path: '%kernel.project_dir%/templates'
|
||||
form_themes: ['bootstrap_5_horizontal_layout.html.twig', 'form/extended_bootstrap_layout.html.twig', 'form/permission_layout.html.twig', 'form/filter_types_layout.html.twig']
|
||||
form_themes: ['bootstrap_5_horizontal_layout.html.twig', 'form/extended_bootstrap_layout.html.twig', 'form/permission_layout.html.twig', 'form/filter_types_layout.html.twig', 'form/synonyms_collection.html.twig']
|
||||
|
||||
paths:
|
||||
'%kernel.project_dir%/assets/css': css
|
||||
|
|
@ -18,6 +18,11 @@ twig:
|
|||
saml_enabled: '%partdb.saml.enabled%'
|
||||
part_preview_generator: '@App\Services\Attachments\PartPreviewGenerator'
|
||||
|
||||
# Bootstrap grid classes used for horizontal form layouts
|
||||
col_label: 'col-sm-3 col-lg-2' # The column classes for form labels
|
||||
col_input: 'col-sm-9 col-lg-10' # The column classes for form input fields
|
||||
offset_label: 'offset-sm-3 offset-lg-2' # Offset classes for elements that should be aligned with the input fields (e.g., submit buttons)
|
||||
|
||||
when@test:
|
||||
twig:
|
||||
strict_variables: true
|
||||
strict_variables: true
|
||||
|
|
|
|||
|
|
@ -1,3 +1,12 @@
|
|||
ux_translator:
|
||||
# The directory where the JavaScript translations are dumped
|
||||
dump_directory: '%kernel.project_dir%/var/translations'
|
||||
# Only include the frontend translation domain in the JavaScript bundle
|
||||
domains:
|
||||
- 'frontend'
|
||||
|
||||
when@prod:
|
||||
ux_translator:
|
||||
# Control whether TypeScript types are dumped alongside translations.
|
||||
# Disable this if you do not use TypeScript (e.g. in production when using AssetMapper), to speed up cache warmup.
|
||||
# dump_typescript: false
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ parameters:
|
|||
partdb.title: '%env(string:settings:customization:instanceName)%' # The title shown inside of Part-DB (e.g. in the navbar and on homepage)
|
||||
partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl', 'hu'] # The languages that are shown in user drop down menu
|
||||
|
||||
partdb.default_uri: '%env(string:DEFAULT_URI)%' # The default URI to use for the Part-DB instance (e.g. https://part-db.example.com/). This is used for generating links in emails
|
||||
partdb.default_uri: '%env(addSlash:string:DEFAULT_URI)%' # The default URI to use for the Part-DB instance (e.g. https://part-db.example.com/). This is used for generating links in emails
|
||||
|
||||
partdb.db.emulate_natural_sort: '%env(bool:DATABASE_EMULATE_NATURAL_SORT)%' # If this is set to true, natural sorting is emulated on platforms that do not support it natively. This can be slow on large datasets.
|
||||
|
||||
|
|
@ -105,6 +105,8 @@ parameters:
|
|||
|
||||
env(DATABASE_EMULATE_NATURAL_SORT): 0
|
||||
|
||||
env(ALLOW_ATTACHMENT_DOWNLOADS_FROM_LOCALNETWORK): 0
|
||||
|
||||
######################################################################################################################
|
||||
# Bulk Info Provider Import Configuration
|
||||
######################################################################################################################
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
|
|||
|
||||
parts: # e.g. this maps to perms_parts in User/Group database
|
||||
group: "data"
|
||||
label: "perm.parts"
|
||||
label: "[[Part]]"
|
||||
operations: # Here are all possible operations are listed => the op name is mapped to bit value
|
||||
read:
|
||||
label: "perm.read"
|
||||
# If a part can be read by a user, he can also see all the datastructures (except devices)
|
||||
alsoSet: ['storelocations.read', 'footprints.read', 'categories.read', 'suppliers.read', 'manufacturers.read',
|
||||
'currencies.read', 'attachment_types.read', 'measurement_units.read']
|
||||
'currencies.read', 'attachment_types.read', 'measurement_units.read', 'part_custom_states.read']
|
||||
apiTokenRole: ROLE_API_READ_ONLY
|
||||
edit:
|
||||
label: "perm.edit"
|
||||
|
|
@ -68,10 +68,13 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
|
|||
move:
|
||||
label: "perm.parts_stock.move"
|
||||
apiTokenRole: ROLE_API_EDIT
|
||||
stocktake:
|
||||
label: "perm.parts_stock.stocktake"
|
||||
apiTokenRole: ROLE_API_EDIT
|
||||
|
||||
|
||||
storelocations: &PART_CONTAINING
|
||||
label: "perm.storelocations"
|
||||
label: "[[Storage_location]]"
|
||||
group: "data"
|
||||
operations:
|
||||
read:
|
||||
|
|
@ -103,35 +106,39 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
|
|||
|
||||
footprints:
|
||||
<<: *PART_CONTAINING
|
||||
label: "perm.part.footprints"
|
||||
label: "[[Footprint]]"
|
||||
|
||||
categories:
|
||||
<<: *PART_CONTAINING
|
||||
label: "perm.part.categories"
|
||||
label: "[[Category]]"
|
||||
|
||||
suppliers:
|
||||
<<: *PART_CONTAINING
|
||||
label: "perm.part.supplier"
|
||||
label: "[[Supplier]]"
|
||||
|
||||
manufacturers:
|
||||
<<: *PART_CONTAINING
|
||||
label: "perm.part.manufacturers"
|
||||
label: "[[Manufacturer]]"
|
||||
|
||||
projects:
|
||||
<<: *PART_CONTAINING
|
||||
label: "perm.projects"
|
||||
label: "[[Project]]"
|
||||
|
||||
attachment_types:
|
||||
<<: *PART_CONTAINING
|
||||
label: "perm.part.attachment_types"
|
||||
label: "[[Attachment_type]]"
|
||||
|
||||
currencies:
|
||||
<<: *PART_CONTAINING
|
||||
label: "perm.currencies"
|
||||
label: "[[Currency]]"
|
||||
|
||||
measurement_units:
|
||||
<<: *PART_CONTAINING
|
||||
label: "perm.measurement_units"
|
||||
label: "[[Measurement_unit]]"
|
||||
|
||||
part_custom_states:
|
||||
<<: *PART_CONTAINING
|
||||
label: "[[Part_custom_state]]"
|
||||
|
||||
tools:
|
||||
label: "perm.part.tools"
|
||||
|
|
@ -293,6 +300,10 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
|
|||
show_updates:
|
||||
label: "perm.system.show_available_updates"
|
||||
apiTokenRole: ROLE_API_ADMIN
|
||||
manage_updates:
|
||||
label: "perm.system.manage_updates"
|
||||
alsoSet: ['show_updates', 'server_infos']
|
||||
apiTokenRole: ROLE_API_ADMIN
|
||||
|
||||
|
||||
attachments:
|
||||
|
|
@ -373,4 +384,4 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co
|
|||
manage_tokens:
|
||||
label: "perm.api.manage_tokens"
|
||||
alsoSet: ['access_api']
|
||||
apiTokenRole: ROLE_API_FULL
|
||||
apiTokenRole: ROLE_API_FULL
|
||||
|
|
|
|||
3448
config/reference.php
Normal file
3448
config/reference.php
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,3 +1,12 @@
|
|||
# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json
|
||||
|
||||
# This file is the entry point to configure the routes of your app.
|
||||
# Methods with the #[Route] attribute are automatically imported.
|
||||
# See also https://symfony.com/doc/current/routing.html
|
||||
|
||||
# To list all registered routes, run the following command:
|
||||
# bin/console debug:router
|
||||
|
||||
# Redirect every url without an locale to the locale of the user/the global base locale
|
||||
|
||||
scan_qr:
|
||||
|
|
@ -16,4 +25,4 @@ redirector:
|
|||
url: ".*"
|
||||
controller: App\Controller\RedirectController::addLocalePart
|
||||
# Dont match localized routes (no redirection loop, if no root with that name exists) or API prefixed routes
|
||||
condition: "not (request.getPathInfo() matches '/^\\\\/([a-z]{2}(_[A-Z]{2})?|api)\\\\//')"
|
||||
condition: "not (request.getPathInfo() matches '/^\\\\/([a-z]{2}(_[A-Z]{2})?|api)\\\\//')"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
|
||||
|
||||
# This file is the entry point to configure your own services.
|
||||
# Files in the packages/ subdirectory configure your dependencies.
|
||||
# See also https://symfony.com/doc/current/service_container/import.html
|
||||
|
||||
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
|
||||
|
|
@ -29,6 +32,9 @@ services:
|
|||
# this creates a service per class whose id is the fully-qualified class name
|
||||
App\:
|
||||
resource: '../src/'
|
||||
exclude:
|
||||
- '../src/Entity/'
|
||||
- '../src/Helpers/'
|
||||
|
||||
# controllers are imported separately to make sure services can be injected
|
||||
# as action arguments even if you don't extend any base controller class
|
||||
|
|
@ -227,9 +233,15 @@ services:
|
|||
arguments:
|
||||
$enforce_index_php: '%env(bool:NO_URL_REWRITE_AVAILABLE)%'
|
||||
|
||||
App\Doctrine\Purger\ResetAutoIncrementPurgerFactory:
|
||||
App\Repository\PartRepository:
|
||||
arguments:
|
||||
$translator: '@translator'
|
||||
tags: ['doctrine.repository_service']
|
||||
|
||||
App\EventSubscriber\UserSystem\PartUniqueIpnSubscriber:
|
||||
tags:
|
||||
- { name: 'doctrine.fixtures.purger_factory', alias: 'reset_autoincrement_purger' }
|
||||
- { name: doctrine.event_listener, event: onFlush, connection: default }
|
||||
|
||||
|
||||
# We are needing this service inside a migration, where only the container is injected. So we need to define it as public, to access it from the container.
|
||||
App\Services\UserSystem\PermissionPresetsHelper:
|
||||
|
|
@ -262,7 +274,11 @@ services:
|
|||
tags:
|
||||
- { name: monolog.processor }
|
||||
|
||||
when@test:
|
||||
App\Doctrine\Purger\ResetAutoIncrementPurgerFactory:
|
||||
tags:
|
||||
- { name: 'doctrine.fixtures.purger_factory', alias: 'reset_autoincrement_purger' }
|
||||
|
||||
when@test: &test
|
||||
services:
|
||||
# Decorate the doctrine fixtures load command to use our custom purger by default
|
||||
doctrine.fixtures_load_command.custom:
|
||||
|
|
|
|||
|
|
@ -5,3 +5,5 @@ files:
|
|||
translation: /translations/validators.%two_letters_code%.xlf
|
||||
- source: /translations/security.en.xlf
|
||||
translation: /translations/security.%two_letters_code%.xlf
|
||||
- source: /translations/frontend.en.xlf
|
||||
translation: /translations/frontend.%two_letters_code%.xlf
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ This allows external applications to interact with Part-DB, extend it or integra
|
|||
> Some features might be missing or not working yet.
|
||||
> Also be aware, that there might be security issues in the API, which could allow attackers to access or edit data via
|
||||
> the API, which
|
||||
> they normally should be able to access. So currently you should only use the API with trusted users and trusted
|
||||
> they normally should not be able to access. So currently you should only use the API with trusted users and trusted
|
||||
> applications.
|
||||
|
||||
Part-DB uses [API Platform](https://api-platform.com/) to provide the API, which allows for easy creation of REST APIs
|
||||
|
|
@ -46,7 +46,7 @@ See [Authentication chapter]({% link api/authentication.md %}) for more details.
|
|||
|
||||
The API is split into different endpoints, which are reachable under the `/api/` path of your Part-DB instance (
|
||||
e.g. `https://your-part-db.local/api/`).
|
||||
There are various endpoints for each entity type (like `part`, `manufacturer`, etc.), which allow you to read and write data, and some special endpoints like `search` or `statistics`.
|
||||
There are various endpoints for each entity type (like `parts`, `manufacturers`, etc.), which allow you to read and write data, and some special endpoints like `search` or `statistics`.
|
||||
|
||||
For example, all API endpoints for managing categories are available under `/api/categories/`. Depending on the exact
|
||||
path and the HTTP method used, you can read, create, update or delete categories.
|
||||
|
|
@ -56,7 +56,7 @@ For most entities, there are endpoints like this:
|
|||
* **POST**: `/api/categories/` - Create a new category
|
||||
* **GET**: `/api/categories/{id}` - Get a specific category by its ID
|
||||
* **DELETE**: `/api/categories/{id}` - Delete a specific category by its ID
|
||||
* **UPDATE**: `/api/categories/{id}` - Update a specific category by its ID. Only the fields which are sent in the
|
||||
* **PATCH**: `/api/categories/{id}` - Update a specific category by its ID. Only the fields which are sent in the
|
||||
request are updated, all other fields are left unchanged.
|
||||
Be aware that you have to set the [JSON Merge Patch](https://datatracker.ietf.org/doc/html/rfc7386) content type
|
||||
header (`Content-Type: application/merge-patch+json`) for this to work.
|
||||
|
|
@ -106,11 +106,11 @@ This is a great way to test the API and see how it works, without having to writ
|
|||
|
||||
By default, all list endpoints are paginated, which means only a certain number of results is returned per request.
|
||||
To get another page of the results, you have to use the `page` query parameter, which contains the page number you want
|
||||
to get (e.g. `/api/categoues/?page=2`).
|
||||
to get (e.g. `/api/categories/?page=2`).
|
||||
When using JSONLD, the links to the next page are also included in the `hydra:view` property of the response.
|
||||
|
||||
To change the size of the pages (the number of items in a single page) use the `itemsPerPage` query parameter (
|
||||
e.g. `/api/categoues/?itemsPerPage=50`).
|
||||
e.g. `/api/categories/?itemsPerPage=50`).
|
||||
|
||||
See [API Platform docs](https://api-platform.com/docs/core/pagination) for more infos.
|
||||
|
||||
|
|
|
|||
|
|
@ -23,14 +23,14 @@ each other so that it does not matter which one of your 1000 things of Part you
|
|||
A part entity has many fields, which can be used to describe it better. Most of the fields are optional:
|
||||
|
||||
* **Name** (Required): The name of the part or how you want to call it. This could be a manufacturer-provided name, or a
|
||||
name you thought of yourself. Each name needs to be unique and must exist in a single category.
|
||||
name you thought of yourself. Each name needs to be unique and must exist in a single category only.
|
||||
* **Description**: A short (single-line) description of what this part is/does. For longer information, you should use
|
||||
the comment field or the specifications
|
||||
* **Category** (Required): The category (see there) to which this part belongs to.
|
||||
* **Tags**: The list of tags this part belongs to. Tags can be used to group parts logically (similar to the category),
|
||||
but tags are much less strict and formal (they don't have to be defined forehands) and you can assign multiple tags to
|
||||
but tags are much less strict and formal (they don't have to be defined beforehand) and you can assign multiple tags to
|
||||
a part. When clicking on a tag, a list with all parts which have the same tag, is shown.
|
||||
* **Min Instock**: *Not really implemented yet*. Parts where the total instock is below this value, will show up for
|
||||
* **Min Instock**: *Not fully implemented yet*. Parts where the total instock is below this value will show up for
|
||||
ordering.
|
||||
* **Footprint**: See there. Useful especially for electronic parts, which have one of the common electronic footprints (
|
||||
like DIP8, SMD0805 or similar). If a part has no explicitly defined preview picture, the preview picture of its
|
||||
|
|
@ -48,9 +48,9 @@ A part entity has many fields, which can be used to describe it better. Most of
|
|||
completely trustworthy.
|
||||
* **Favorite**: Parts with this flag are highlighted in parts lists
|
||||
* **Mass**: The mass of a single piece of this part (so of a single transistor). Given in grams.
|
||||
* **Internal Part number** (IPN): Each part is automatically assigned a numerical ID that identifies a part in the
|
||||
database. This ID depends on when a part was created and can not be changed. If you want to assign your own unique
|
||||
identifiers, or sync parts identifiers with the identifiers of another database you can use this field.
|
||||
* **Internal Part Number** (IPN): Each part is automatically assigned a numerical ID that identifies a part in the
|
||||
database. This ID depends on when a part was created and cannot be changed. If you want to assign your own unique
|
||||
identifiers, or sync parts identifiers with the identifiers of another database, you can use this field.
|
||||
|
||||
### Stock / Part lot
|
||||
|
||||
|
|
@ -99,12 +99,12 @@ possible category tree could look like this:
|
|||
|
||||
### Supplier
|
||||
|
||||
A Supplier is a vendor/distributor where you can buy/order parts. Price information of parts is associated with a
|
||||
A supplier is a vendor/distributor where you can buy/order parts. Price information of parts is associated with a
|
||||
supplier.
|
||||
|
||||
### Manufacturer
|
||||
|
||||
A manufacturer represents the company that manufacturers/builds various parts (not necessarily sell them). If the
|
||||
A manufacturer represents the company that manufactures/builds various parts (not necessarily sells them). If the
|
||||
manufacturer also sells the parts, you have to create a supplier for that.
|
||||
|
||||
### Storage location
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ nav_order: 5
|
|||
|
||||
# Configuration
|
||||
|
||||
Part-DBs behavior can be configured to your needs. There are different kinds of configuration options: Options, which are
|
||||
Part-DB's behavior can be configured to your needs. There are different kinds of configuration options: Options that are
|
||||
user-changeable (changeable dynamically via frontend), options that can be configured by environment variables, and
|
||||
options that are only configurable via Symfony config files.
|
||||
|
||||
## User configruation
|
||||
## User configuration
|
||||
|
||||
The following things can be changed for every user and a user can change it for himself (if he has the correct permission
|
||||
for it). Configuration is either possible via the user's own settings page (where you can also change the password) or via
|
||||
|
|
@ -40,10 +40,10 @@ The following configuration options can only be changed by the server administra
|
|||
variables, changing the `.env.local` file or setting env for your docker container. Here are just the most important
|
||||
options listed, see `.env` file for the full list of possible env variables.
|
||||
|
||||
Environment variables allow to overwrite settings in the web interface. This is useful, if you want to enforce certain
|
||||
settings to be unchangable by users, or if you want to configure settings in a central place in a deployed environment.
|
||||
Environment variables allow you to overwrite settings in the web interface. This is useful if you want to enforce certain
|
||||
settings to be unchangeable by users, or if you want to configure settings in a central place in a deployed environment.
|
||||
On the settings page, you can hover over a setting to see, which environment variable can be used to overwrite it, it
|
||||
is shown as tooltip. API keys or similar sensitve data which is overwritten by env variables, are redacted on the web
|
||||
is shown as tooltip. API keys or similar sensitive data which is overwritten by env variables, are redacted on the web
|
||||
interface, so that even administrators cannot see them (only the last 2 characters and the length).
|
||||
|
||||
For technical and security reasons some settings can only be configured via environment variables and not via the web
|
||||
|
|
@ -86,6 +86,10 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
|
|||
* `ATTACHMENT_DOWNLOAD_BY_DEFAULT`: When this is set to 1, the "download external file" checkbox is checked by default
|
||||
when adding a new attachment. Otherwise, it is unchecked by default. Use this if you wanna download all attachments
|
||||
locally by default. Attachment download is only possible, when `ALLOW_ATTACHMENT_DOWNLOADS` is set to 1.
|
||||
* `ALLOW_ATTACHMENT_DOWNLOADS_FROM_LOCALNETWORK` (default `0`): When this is set to 1, users can make Part-DB directly download a file specified as a URL from the local network and create it as a local file. This allows users access to all resources available in the local network, which could be a security risk, so use this only if you trust your users and have a secure local network.
|
||||
* `ATTACHMENT_SHOW_HTML_FILES`: When enabled, user uploaded HTML attachments can be viewed directly in the browser.
|
||||
Many potential malicious functions are restricted, still this is a potential security risk and should only be enabled,
|
||||
if you trust the users who can upload files. When set to 0, HTML files are rendered as plain text.
|
||||
* `USE_GRAVATAR`: Set to `1` to use [gravatar.com](https://gravatar.com/) images for user avatars (as long as they have
|
||||
not set their own picture). The users browsers have to download the pictures from a third-party (gravatar) server, so
|
||||
this might be a privacy risk.
|
||||
|
|
@ -105,17 +109,29 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
|
|||
* `part_delete`: Delete operation of an existing part
|
||||
* `part_create`: Creation of a new part
|
||||
* `part_stock_operation`: Stock operation on a part (therefore withdraw, add or move stock)
|
||||
* `datastructure_edit`: Edit operation of an existing datastructure (e.g. category, manufacturer, ...)
|
||||
* `datastructure_delete`: Delete operation of a existing datastructure (e.g. category, manufacturer, ...)
|
||||
* `datastructure_create`: Creation of a new datastructure (e.g. category, manufacturer, ...)
|
||||
* `CHECK_FOR_UPDATES` (default `1`): Set this to 0, if you do not want Part-DB to connect to GitHub to check for new
|
||||
versions, or if your server can not connect to the internet.
|
||||
* `datastructure_edit`: Edit operation of an existing data structure (e.g. category, manufacturer, ...)
|
||||
* `datastructure_delete`: Delete operation of an existing data structure (e.g. category, manufacturer, ...)
|
||||
* `datastructure_create`: Creation of a new data structure (e.g. category, manufacturer, ...)
|
||||
* `CHECK_FOR_UPDATES` (default `1`): Set this to 0 if you do not want Part-DB to connect to GitHub to check for new
|
||||
versions, or if your server cannot connect to the internet.
|
||||
* `APP_SECRET` (env only): This variable is a configuration parameter used for various security-related purposes,
|
||||
particularly for securing and protecting various aspects of your application. It's a secret key that is used for
|
||||
cryptographic operations and security measures (session management, CSRF protection, etc..). Therefore this
|
||||
value should be handled as confidential data and not shared publicly.
|
||||
* `SHOW_PART_IMAGE_OVERLAY`: Set to 0 to disable the part image overlay, which appears if you hover over an image in the
|
||||
part image gallery
|
||||
* `IPN_SUGGEST_REGEX`: A global regular expression, that part IPNs have to fulfill. Enforce your own format for your users.
|
||||
* `IPN_SUGGEST_REGEX_HELP`: Define your own user help text for the Regex format specification.
|
||||
* `IPN_AUTO_APPEND_SUFFIX`: When enabled, an incremental suffix will be added to the user input when entering an existing
|
||||
* IPN again upon saving.
|
||||
* `IPN_SUGGEST_PART_DIGITS`: Defines the fixed number of digits used as the increment at the end of an IPN (Internal Part Number).
|
||||
IPN prefixes, maintained within part categories and their hierarchy, form the foundation for suggesting complete IPNs.
|
||||
These suggestions become accessible during IPN input of a part. The constant specifies the digits used to calculate and assign
|
||||
unique increments for parts within a category hierarchy, ensuring consistency and uniqueness in IPN generation.
|
||||
* `IPN_USE_DUPLICATE_DESCRIPTION`: When enabled, the part’s description is used to find existing parts with the same
|
||||
description and to determine the next available IPN by incrementing their numeric suffix for the suggestion list.
|
||||
* `KEYBINDINGS_SPECIAL_CHARS_ENABLED`: Set this to 0 to disable the special character keybindings (Alt + key) for inserting special characters. This can be useful if
|
||||
they conflict with your keyboard layout or system shortcuts.
|
||||
|
||||
### E-Mail settings (all env only)
|
||||
|
||||
|
|
@ -129,6 +145,18 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
|
|||
* `ALLOW_EMAIL_PW_RESET`: Set this value to true, if you want to allow users to reset their password via an email
|
||||
notification. You have to configure the mail provider first before via the MAILER_DSN setting.
|
||||
|
||||
### Update manager settings
|
||||
* `DISABLE_WEB_UPDATES` (default `1`): Set this to 0 to enable web-based updates. When enabled, you can perform updates
|
||||
via the web interface in the update manager. This is disabled by default for security reasons, as it can be a risk if
|
||||
not used carefully. You can still use the CLI commands to perform updates, even when web updates are disabled.
|
||||
* `DISABLE_BACKUP_RESTORE` (default `1`): Set this to 0 to enable backup restore via the web interface. When enabled, you can
|
||||
restore backups via the web interface in the update manager. This is disabled by default for security reasons, as it can
|
||||
be a risk if not used carefully. You can still use the CLI commands to perform backup restores, even when web-based
|
||||
backup restore is disabled.
|
||||
* `DISABLE_BACKUP_DOWNLOAD` (default `1`): Set this to 0 to enable backup download via the web interface. When enabled, you can download backups via the web interface
|
||||
in the update manager. This is disabled by default for security reasons, as it can be a risk if not used carefully, as
|
||||
the downloads contain sensitive data like password hashes or secrets.
|
||||
|
||||
### Table related settings
|
||||
|
||||
* `TABLE_DEFAULT_PAGE_SIZE`: The default page size for tables. This is the number of rows which are shown per page. Set
|
||||
|
|
@ -136,7 +164,7 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
|
|||
* `TABLE_PARTS_DEFAULT_COLUMNS`: The columns in parts tables, which are visible by default (when loading table for first
|
||||
time).
|
||||
Also specify the default order of the columns. This is a comma separated list of column names. Available columns
|
||||
are: `name`, `id`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `storage_location`, `amount`, `minamount`, `partUnit`, `addedDate`, `lastModified`, `needs_review`, `favorite`, `manufacturing_status`, `manufacturer_product_number`, `mass`, `tags`, `attachments`, `edit`.
|
||||
are: `name`, `id`, `ipn`, `description`, `category`, `footprint`, `manufacturer`, `storage_location`, `amount`, `minamount`, `partUnit`, `partCustomState`, `addedDate`, `lastModified`, `needs_review`, `favorite`, `manufacturing_status`, `manufacturer_product_number`, `mass`, `tags`, `attachments`, `edit`.
|
||||
|
||||
### History/Eventlog-related settings
|
||||
|
||||
|
|
@ -252,10 +280,10 @@ markdown (and even some subset of HTML) syntax to format the text.
|
|||
|
||||
## parameters.yaml
|
||||
|
||||
You can also configure some options via the `config/parameters.yaml` file. This should normally not need,
|
||||
and you should know what you are doing, when you change something here. You should expect, that you will have to do some
|
||||
manual merge, when you have changed something here and update to a newer version of Part-DB. It is possible that
|
||||
configuration options here will change or be completely removed in future versions of Part-DB.
|
||||
You can also configure some options via the `config/parameters.yaml` file. This should normally not be needed,
|
||||
and you should know what you are doing when you change something here. You should expect that you will have to do some
|
||||
manual merges when you have changed something here and update to a newer version of Part-DB. It is possible that
|
||||
configuration options here will change or be completely removed in future versions of Part-DB.
|
||||
|
||||
If you change something here, you have to clear the cache, before the changes will take effect with the
|
||||
command `bin/console cache:clear`.
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ It is installed on a web server and so can be accessed with any browser without
|
|||
> You can log in with username: **user** and password: **user**, to change/create data.
|
||||
>
|
||||
> Every change to the master branch gets automatically deployed, so it represents the current development progress and
|
||||
> is
|
||||
> maybe not completely stable. Please mind, that the free Heroku instance is used, so it can take some time when loading
|
||||
> may not be completely stable. Please mind, that the free Heroku instance is used, so it can take some time when loading
|
||||
> the page
|
||||
> for the first time.
|
||||
|
||||
|
|
@ -28,32 +27,33 @@ It is installed on a web server and so can be accessed with any browser without
|
|||
* Inventory management of your electronic parts. Each part can be assigned to a category, footprint, manufacturer,
|
||||
and multiple store locations and price information. Parts can be grouped using tags. You can associate various files
|
||||
like datasheets or pictures with the parts.
|
||||
* Multi-language support (currently German, English, Russian, Japanese and French (experimental))
|
||||
* Barcodes/Labels generator for parts and storage locations, scan barcodes via webcam using the builtin barcode scanner
|
||||
* User system with groups and detailed (fine granular) permissions.
|
||||
* Multi-language support (currently German, English, Russian, Japanese, French, Czech, Danish, and Chinese)
|
||||
* Barcodes/Labels generator for parts and storage locations, scan barcodes via webcam using the built-in barcode scanner
|
||||
* User system with groups and detailed (fine-grained) permissions.
|
||||
Two-factor authentication is supported (Google Authenticator and Webauthn/U2F keys) and can be enforced for groups.
|
||||
Password reset via email can be setup.
|
||||
Password reset via email can be set up.
|
||||
* Optional support for single sign-on (SSO) via SAML (using an intermediate service
|
||||
like [Keycloak](https://www.keycloak.org/) you can connect Part-DB to an existing LDAP or Active Directory server)
|
||||
* Import/Export system
|
||||
* Project management: Create projects and assign parts to the bill of material (BOM), to show how often you could build
|
||||
this project and directly withdraw all components needed from DB
|
||||
* Event log: Track what changes happens to your inventory, track which user does what. Revert your parts to older
|
||||
* Event log: Track what changes happen to your inventory, track which user does what. Revert your parts to older
|
||||
versions.
|
||||
* Responsive design: You can use Part-DB on your PC, your tablet and your smartphone using the same interface.
|
||||
* Responsive design: You can use Part-DB on your PC, your tablet, and your smartphone using the same interface.
|
||||
* MySQL, SQLite and PostgreSQL are supported as database backends
|
||||
* Support for rich text descriptions and comments in parts
|
||||
* Support for multiple currencies and automatic update of exchange rates supported
|
||||
* Powerful search and filter function, including parametric search (search for parts according to some specifications)
|
||||
* Easy migration from an existing PartKeepr instance (see [here]({%link partkeepr_migration.md %}))
|
||||
* Use cloud providers (like Octopart, Digikey, Farnell or TME) to automatically get part information, datasheets and
|
||||
* Use cloud providers (like Octopart, Digikey, Farnell, Mouser, or TME) to automatically get part information, datasheets, and
|
||||
prices for parts (see [here]({% link usage/information_provider_system.md %}))
|
||||
* Retrieve part information from arbitrary shop websites, using either conventional data extraction from structured metadata, or AI based data extraction
|
||||
* API to access Part-DB from other applications/scripts
|
||||
* [Integration with KiCad]({%link usage/eda_integration.md %}): Use Part-DB as central datasource for your
|
||||
KiCad and see available parts from Part-DB directly inside KiCad.
|
||||
* [Integration with KiCad]({%link usage/eda_integration.md %}): Use Part-DB as the central datasource for your
|
||||
KiCad and see available parts from Part-DB directly inside KiCad.
|
||||
|
||||
With these features Part-DB is useful to hobbyists, who want to keep track of their private electronic parts inventory,
|
||||
or makerspaces, where many users have should have (controlled) access to the shared inventory.
|
||||
With these features, Part-DB is useful to hobbyists, who want to keep track of their private electronic parts inventory,
|
||||
or makerspaces, where many users should have (controlled) access to the shared inventory.
|
||||
|
||||
Part-DB is also used by small companies and universities for managing their inventory.
|
||||
|
||||
|
|
@ -68,11 +68,11 @@ See [LICENSE](https://github.com/Part-DB/Part-DB-symfony/blob/master/LICENSE) fo
|
|||
## Donate for development
|
||||
|
||||
If you want to donate to the Part-DB developer, see the sponsor button in the top bar (next to the repo name).
|
||||
There you will find various methods to support development on a monthly or a one time base.
|
||||
There you will find various methods to support development on a monthly or a one-time basis.
|
||||
|
||||
## Built with
|
||||
|
||||
* [Symfony 5](https://symfony.com/): The main framework used for the serverside PHP
|
||||
* [Symfony 6](https://symfony.com/): The main framework used for the serverside PHP
|
||||
* [Bootstrap 5](https://getbootstrap.com/) and [Bootswatch](https://bootswatch.com/): Used as website theme
|
||||
* [Fontawesome](https://fontawesome.com/): Used as icon set
|
||||
* [Hotwire Stimulus](https://stimulus.hotwired.dev/) and [Hotwire Turbo](https://turbo.hotwired.dev/): Frontend
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ differences between them, which might be important for you. Therefore the pros a
|
|||
are listed here.
|
||||
|
||||
{: .important }
|
||||
You have to choose between the database types before you start using Part-DB and **you can not change it (easily) after
|
||||
you have started creating data**. So you should choose the database type for your use case (and possible future uses).
|
||||
While you can change the database platform later (see below), it is still experimental and not recommended.
|
||||
So you should choose the database type for your use case (and possible future uses).
|
||||
|
||||
## Comparison
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ you have started creating data**. So you should choose the database type for you
|
|||
|
||||
* **Performance**: SQLite is not as fast as MySQL or PostgreSQL, especially when using complex queries or many users.
|
||||
* **Emulated RegEx search**: SQLite does not support RegEx search natively. Part-DB can emulate it, however that is pretty slow.
|
||||
* **Emualted natural sorting**: SQLite does not support natural sorting natively. Part-DB can emulate it, but it is pretty slow.
|
||||
* **Emulated natural sorting**: SQLite does not support natural sorting natively. Part-DB can emulate it, but it is pretty slow.
|
||||
* **Limitations with Unicode**: SQLite has limitations in comparisons and sorting of Unicode characters, which might lead to
|
||||
unexpected behavior when using non-ASCII characters in your data. For example `µ` (micro sign) is not seen as equal to
|
||||
`μ` (greek minuscule mu), therefore searching for `µ` (micro sign) will not find parts containing `μ` (mu) and vice versa.
|
||||
|
|
@ -131,7 +131,7 @@ The host (here 127.0.0.1) and port should also be specified according to your My
|
|||
In the `serverVersion` parameter you can specify the version of the MySQL/MariaDB server you are using, in the way the server returns it
|
||||
(e.g. `8.0.37` for MySQL and `10.4.14-MariaDB`). If you do not know it, you can leave the default value.
|
||||
|
||||
If you want to use a unix socket for the connection instead of a TCP connnection, you can specify the socket path in the `unix_socket` parameter.
|
||||
If you want to use a unix socket for the connection instead of a TCP connection, you can specify the socket path in the `unix_socket` parameter.
|
||||
```shell
|
||||
DATABASE_URL="mysql://user:password@localhost/database?serverVersion=8.0.37&unix_socket=/var/run/mysqld/mysqld.sock"
|
||||
```
|
||||
|
|
@ -150,7 +150,7 @@ In the `serverVersion` parameter you can specify the version of the PostgreSQL s
|
|||
|
||||
The `charset` parameter specify the character set of the database. It should be set to `utf8` to ensure that all characters are stored correctly.
|
||||
|
||||
If you want to use a unix socket for the connection instead of a TCP connnection, you can specify the socket path in the `host` parameter.
|
||||
If you want to use a unix socket for the connection instead of a TCP connection, you can specify the socket path in the `host` parameter.
|
||||
```shell
|
||||
DATABASE_URL="postgresql://db_user@localhost/db_name?serverVersion=16.6&charset=utf8&host=/var/run/postgresql"
|
||||
```
|
||||
|
|
@ -177,6 +177,26 @@ In natural sorting, it would be sorted as:
|
|||
Part-DB can sort names in part tables and tree views naturally. PostgreSQL and MariaDB 10.7+ support natural sorting natively,
|
||||
and it is automatically used if available.
|
||||
|
||||
For SQLite and MySQL < 10.7 it has to be emulated if wanted, which is pretty slow. Therefore it has to be explicity enabled by setting the
|
||||
For SQLite and MySQL < 10.7 it has to be emulated if wanted, which is pretty slow. Therefore it has to be explicitly enabled by setting the
|
||||
`DATABASE_EMULATE_NATURAL_SORT` environment variable to `1`. If it is 0 the classical binary sorting is used, on these databases. The emulations
|
||||
might have some quirks and issues, so it is recommended to use a database which supports natural sorting natively, if you want to use it.
|
||||
|
||||
## Converting between database platforms
|
||||
|
||||
{: .important }
|
||||
The database conversion is still experimental. Therefore it is recommended to backup your database before performing a conversion, and check if everything works as expected afterwards.
|
||||
|
||||
If you want to change the database platform of your Part-DB installation (e.g. from SQLite to MySQL/MariaDB or PostgreSQL, or vice versa), there is the `partdb:migrations:convert-db-platform` console command, which can help you with that:
|
||||
|
||||
1. Make a backup of your current database to be safe if something goes wrong (see the backup documentation).
|
||||
2. Ensure that your database is at the latest schema by running the migrations: `php bin/console doctrine:migrations:migrate`
|
||||
3. Change the `DATABASE_URL` environment variable to the new database platform and connection information. Copy the old `DATABASE_URL` as you will need it later.
|
||||
4. Run `php bin/console doctrine:migrations:migrate` again to create the database schema in the new database. You will not need the admin password, that is shown when running the migrations.
|
||||
5. Run the conversion command, where you have to provide the old `DATABASE_URL` as parameter: `php bin/console partdb:migrations:convert-db-platform <OLD_DATABASE_URL>`
|
||||
Replace `<OLD_DATABASE_URL` with the actual old `DATABASE_URL` value (e.g. `sqlite:///%kernel.project_dir%/var/app.db`):
|
||||
```bash
|
||||
php bin/console partdb:migrations:convert-db-platform sqlite:///%kernel.project_dir%/var/app.db
|
||||
```
|
||||
6. The command will purge all data in the new database and copy all data from the old database to the new one. This might take some time and memory depending on the size of your database.
|
||||
7. Clear the cache: `php bin/console partdb:cache:clear`
|
||||
8. You can login with your existing user accounts in the new database now. Check if everything works as expected.
|
||||
|
|
|
|||
|
|
@ -15,13 +15,75 @@ To make emails work you have to properly configure a mail provider in Part-DB.
|
|||
## Configuration
|
||||
|
||||
Part-DB uses [Symfony Mailer](https://symfony.com/doc/current/mailer.html) to send emails, which supports multiple
|
||||
automatic mail providers (like MailChimp or SendGrid). If you want to use one of these providers, check the Symfony
|
||||
mail providers (like Mailgun, SendGrid, or Brevo). If you want to use one of these providers, check the Symfony
|
||||
Mailer documentation for more information.
|
||||
|
||||
We will only cover the configuration of an SMTP provider here, which is sufficient for most use-cases.
|
||||
You will need an email account, which you can use send emails from via password-bases SMTP authentication, this account
|
||||
You will need an email account, which you can use to send emails from via password-based SMTP authentication, this account
|
||||
should be dedicated to Part-DB.
|
||||
|
||||
### Using specialized mail providers (Mailgun, SendGrid, etc.)
|
||||
|
||||
If you want to use a specialized mail provider like Mailgun, SendGrid, Brevo (formerly Sendinblue), Amazon SES, or
|
||||
Postmark instead of SMTP, you need to install the corresponding Symfony mailer package first.
|
||||
|
||||
#### Docker installation
|
||||
|
||||
If you are using Part-DB in Docker, you can install additional mailer packages by setting the `COMPOSER_EXTRA_PACKAGES`
|
||||
environment variable in your `docker-compose.yaml`:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- COMPOSER_EXTRA_PACKAGES=symfony/mailgun-mailer
|
||||
- MAILER_DSN=mailgun+api://API_KEY:DOMAIN@default
|
||||
- EMAIL_SENDER_EMAIL=noreply@yourdomain.com
|
||||
- EMAIL_SENDER_NAME=Part-DB
|
||||
- ALLOW_EMAIL_PW_RESET=1
|
||||
```
|
||||
|
||||
You can install multiple packages by separating them with spaces:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- COMPOSER_EXTRA_PACKAGES=symfony/mailgun-mailer symfony/sendgrid-mailer
|
||||
```
|
||||
|
||||
The packages will be installed automatically when the container starts.
|
||||
|
||||
Common mailer packages:
|
||||
- `symfony/mailgun-mailer` - For [Mailgun](https://www.mailgun.com/)
|
||||
- `symfony/sendgrid-mailer` - For [SendGrid](https://sendgrid.com/)
|
||||
- `symfony/brevo-mailer` - For [Brevo](https://www.brevo.com/) (formerly Sendinblue)
|
||||
- `symfony/amazon-mailer` - For [Amazon SES](https://aws.amazon.com/ses/)
|
||||
- `symfony/postmark-mailer` - For [Postmark](https://postmarkapp.com/)
|
||||
|
||||
#### Direct installation (non-Docker)
|
||||
|
||||
If you have installed Part-DB directly on your server (not in Docker), you need to manually install the required
|
||||
mailer package using composer.
|
||||
|
||||
Navigate to your Part-DB installation directory and run:
|
||||
|
||||
```bash
|
||||
# Install the package as the web server user
|
||||
sudo -u www-data composer require symfony/mailgun-mailer
|
||||
|
||||
# Clear the cache
|
||||
sudo -u www-data php bin/console cache:clear
|
||||
```
|
||||
|
||||
Replace `symfony/mailgun-mailer` with the package you need. You can install multiple packages at once:
|
||||
|
||||
```bash
|
||||
sudo -u www-data composer require symfony/mailgun-mailer symfony/sendgrid-mailer
|
||||
```
|
||||
|
||||
After installing the package, configure the `MAILER_DSN` in your `.env.local` file according to the provider's
|
||||
documentation (see [Symfony Mailer documentation](https://symfony.com/doc/current/mailer.html) for DSN format for
|
||||
each provider).
|
||||
|
||||
## SMTP Configuration
|
||||
|
||||
To configure the SMTP provider, you have to set the following environment variables:
|
||||
|
||||
`MAILER_DSN`: You have to provide the SMTP server address and the credentials for the email account here. The format is
|
||||
|
|
|
|||
|
|
@ -8,4 +8,4 @@ has_children: true
|
|||
# Installation
|
||||
Below you can find some guides to install Part-DB.
|
||||
|
||||
For the hobbyists without much experience, we recommend the docker installation or direct installation on debian.
|
||||
For hobbyists without much experience, we recommend the Docker installation or direct installation on Debian.
|
||||
|
|
@ -80,7 +80,11 @@ services:
|
|||
#- BANNER=This is a test banner<br>with a line break
|
||||
|
||||
# If you use a reverse proxy in front of Part-DB, you must configure the trusted proxies IP addresses here (see reverse proxy documentation for more information):
|
||||
# - TRUSTED_PROXIES=127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||
# - TRUSTED_PROXIES=127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||
|
||||
# If you need to install additional composer packages (e.g., for specific mailer transports), you can specify them here:
|
||||
# The packages will be installed automatically when the container starts
|
||||
# - COMPOSER_EXTRA_PACKAGES=symfony/mailgun-mailer symfony/sendgrid-mailer
|
||||
```
|
||||
|
||||
4. Customize the settings by changing the environment variables (or adding new ones). See [Configuration]({% link
|
||||
|
|
@ -91,6 +95,11 @@ services:
|
|||
docker-compose up -d
|
||||
```
|
||||
|
||||
{: .warning }
|
||||
> If you run a root console inside the docker container, and wanna execute commands on the webserver behalf, be sure to use `sudo -E` command (with the `-E` flag) to preserve env variables from the current shell.
|
||||
> Otherwise Part-DB console might use the wrong configuration to execute commands.
|
||||
|
||||
|
||||
6. Create the initial database with
|
||||
|
||||
```bash
|
||||
|
|
@ -136,19 +145,22 @@ services:
|
|||
# In docker env logs will be redirected to stderr
|
||||
- APP_ENV=docker
|
||||
|
||||
# Uncomment this, if you want to use the automatic database migration feature. With this you have you do not have to
|
||||
# Uncomment this, if you want to use the automatic database migration feature. With this you do not have to
|
||||
# run the doctrine:migrations:migrate commands on installation or upgrade. A database backup is written to the uploads/
|
||||
# folder (under .automigration-backup), so you can restore it, if the migration fails.
|
||||
# This feature is currently experimental, so use it at your own risk!
|
||||
# - DB_AUTOMIGRATE=true
|
||||
|
||||
# You can configure Part-DB using the webUI or environment variables
|
||||
# However you can add add any other environment configuration you want here
|
||||
# However you can add any other environment configuration you want here
|
||||
# See .env file for all available options or https://docs.part-db.de/configuration.html
|
||||
|
||||
# Override value if you want to show to show a given text on homepage.
|
||||
# When this is outcommented the webUI can be used to configure the banner
|
||||
# Override value if you want to show a given text on homepage.
|
||||
# When this is commented out the webUI can be used to configure the banner
|
||||
#- BANNER=This is a test banner<br>with a line break
|
||||
|
||||
# If you need to install additional composer packages (e.g., for specific mailer transports), you can specify them here:
|
||||
# - COMPOSER_EXTRA_PACKAGES=symfony/mailgun-mailer symfony/sendgrid-mailer
|
||||
|
||||
database:
|
||||
container_name: partdb_database
|
||||
|
|
@ -169,6 +181,38 @@ services:
|
|||
|
||||
```
|
||||
|
||||
### Installing additional composer packages
|
||||
|
||||
If you need to use specific mailer transports or other functionality that requires additional composer packages, you can
|
||||
install them automatically at container startup using the `COMPOSER_EXTRA_PACKAGES` environment variable.
|
||||
|
||||
For example, if you want to use Mailgun as your email provider, you need to install the `symfony/mailgun-mailer` package.
|
||||
Add the following to your docker-compose.yaml environment section:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- COMPOSER_EXTRA_PACKAGES=symfony/mailgun-mailer
|
||||
- MAILER_DSN=mailgun+api://API_KEY:DOMAIN@default
|
||||
```
|
||||
|
||||
You can specify multiple packages by separating them with spaces:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- COMPOSER_EXTRA_PACKAGES=symfony/mailgun-mailer symfony/sendgrid-mailer
|
||||
```
|
||||
|
||||
{: .info }
|
||||
> The packages will be installed when the container starts. This may increase the container startup time on the first run.
|
||||
> The installed packages will persist in the container until it is recreated.
|
||||
|
||||
Common mailer packages you might need:
|
||||
- `symfony/mailgun-mailer` - For Mailgun email service
|
||||
- `symfony/sendgrid-mailer` - For SendGrid email service
|
||||
- `symfony/brevo-mailer` - For Brevo (formerly Sendinblue) email service
|
||||
- `symfony/amazon-mailer` - For Amazon SES email service
|
||||
- `symfony/postmark-mailer` - For Postmark email service
|
||||
|
||||
### Update Part-DB
|
||||
|
||||
You can update Part-DB by pulling the latest image and restarting the container.
|
||||
|
|
@ -180,6 +224,52 @@ docker-compose up -d
|
|||
docker exec --user=www-data partdb php bin/console doctrine:migrations:migrate
|
||||
```
|
||||
|
||||
### Automatic updates via Watchtower (Web UI)
|
||||
|
||||
Part-DB supports triggering Docker container updates directly from the web interface using [Watchtower](https://github.com/nicholas-fedor/watchtower).
|
||||
When configured, administrators can check for and apply updates from the **System > Update Manager** page.
|
||||
|
||||
{: .info }
|
||||
> The original `containrrr/watchtower` project is no longer maintained (last release November 2023). These docs use the actively maintained community fork at [`nicholas-fedor/watchtower`](https://github.com/nicholas-fedor/watchtower), which is drop-in compatible with the original HTTP API.
|
||||
|
||||
To enable this feature, add a Watchtower service to your `docker-compose.yaml` and configure the connection:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
partdb:
|
||||
container_name: partdb
|
||||
image: jbtronics/part-db1:latest
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.enable=true
|
||||
environment:
|
||||
# ... your existing environment variables ...
|
||||
|
||||
# Watchtower integration for web-based updates
|
||||
- WATCHTOWER_API_URL=http://watchtower:8080
|
||||
- WATCHTOWER_API_TOKEN=your-secret-token
|
||||
# ... your existing ports/volumes ...
|
||||
|
||||
watchtower:
|
||||
image: ghcr.io/nicholas-fedor/watchtower:latest
|
||||
container_name: watchtower
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- WATCHTOWER_HTTP_API_UPDATE=true
|
||||
- WATCHTOWER_HTTP_API_TOKEN=your-secret-token
|
||||
- WATCHTOWER_LABEL_ENABLE=true
|
||||
- WATCHTOWER_CLEANUP=true
|
||||
```
|
||||
|
||||
{: .important }
|
||||
> Replace `your-secret-token` with a strong, unique token. The same token must be set in both the Part-DB (`WATCHTOWER_API_TOKEN`) and Watchtower (`WATCHTOWER_HTTP_API_TOKEN`) environment variables.
|
||||
|
||||
{: .info }
|
||||
> `WATCHTOWER_LABEL_ENABLE=true` ensures Watchtower only manages containers with the `com.centurylinklabs.watchtower.enable=true` label, preventing it from updating other containers on the same host.
|
||||
|
||||
Once configured, the Update Manager page will show the Watchtower connection status and provide an **Update via Watchtower** button when a new version is available. Clicking it triggers Watchtower to pull the latest image and recreate the Part-DB container automatically.
|
||||
|
||||
## Direct use of docker image
|
||||
|
||||
You can use the `jbtronics/part-db1:master` image directly. You have to expose port 80 to a host port and configure
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ nav_order: 10
|
|||
|
||||
# Nginx
|
||||
|
||||
You can also use [nginx](https://www.nginx.com/) as webserver for Part-DB. Setup Part-DB with apache is a bit easier, so
|
||||
You can also use [nginx](https://www.nginx.com/) as webserver for Part-DB. Setting up Part-DB with Apache is a bit easier, so
|
||||
this is the method shown in the guides. This guide assumes that you already have a working nginx installation with PHP
|
||||
configured.
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ LDAP or Active Directory server.
|
|||
|
||||
{: .warning }
|
||||
> This feature is currently in beta. Please report any bugs you find.
|
||||
> So far it has only tested with Keycloak, but it should work with any SAML 2.0 compatible identity provider.
|
||||
> So far it has only been tested with Keycloak, but it should work with any SAML 2.0 compatible identity provider.
|
||||
|
||||
This guide will show you how to configure Part-DB with [Keycloak](https://www.keycloak.org/) as the SAML identity
|
||||
provider, but it should work with any SAML 2.0 compatible identity provider.
|
||||
|
|
@ -75,8 +75,8 @@ the [Keycloak Getting Started Guide](https://www.keycloak.org/docs/latest/gettin
|
|||
|
||||
### Configure Part-DB to use SAML
|
||||
|
||||
1. Open the `.env.local` file of Part-DB (or the docker-compose.yaml) for edit
|
||||
2. Set the `SAMLP_SP_PRIVATE_KEY` environment variable to the content of the private key file you downloaded in the
|
||||
1. Open the `.env.local` file of Part-DB (or the docker-compose.yaml) for editing
|
||||
2. Set the `SAML_SP_PRIVATE_KEY` environment variable to the content of the private key file you downloaded in the
|
||||
previous step. It should start with `MIEE` and end with `=`.
|
||||
3. Set the `SAML_SP_X509_CERT` environment variable to the content of the Certificate field shown in the `Keys` tab of
|
||||
the SAML client in Keycloak. It should start with `MIIC` and end with `=`.
|
||||
|
|
|
|||
BIN
docs/screenshots/update-manager-interface.png
Normal file
BIN
docs/screenshots/update-manager-interface.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
|
|
@ -9,7 +9,7 @@ Sometimes things go wrong and Part-DB shows an error message. This page should h
|
|||
|
||||
## Error messages
|
||||
|
||||
When a common, easy fixable error occurs (like a non-up-to-date database), Part-DB will show you some short instructions
|
||||
When a common, easily fixable error occurs (like a non-up-to-date database), Part-DB will show you some short instructions
|
||||
on how to fix the problem. If you have a problem that is not listed here, please open an issue on GitHub.
|
||||
|
||||
## General procedure
|
||||
|
|
@ -28,9 +28,9 @@ php bin/console cache:clear
|
|||
php bin/console doctrine:migrations:migrate
|
||||
```
|
||||
|
||||
If this does not help, please [open an issue on GitHub](https://github.com/Part-DB/Part-DB-symfony).
|
||||
If this does not help, please [open an issue on GitHub](https://github.com/Part-DB/Part-DB-server).
|
||||
|
||||
## Search for the user and reset the password:
|
||||
## Search for a user and reset the password
|
||||
|
||||
You can list all users with the following command: `php bin/console partdb:users:list`
|
||||
To reset the password of a user you can use the following
|
||||
|
|
@ -50,6 +50,21 @@ docker-compose logs -f
|
|||
|
||||
Please include the error logs in your issue on GitHub, if you open an issue.
|
||||
|
||||
## KiCad Integration Issues
|
||||
|
||||
### "API responded with error code: 0: Unknown"
|
||||
|
||||
If you get this error when trying to connect KiCad to Part-DB, it is most likely caused by KiCad not trusting your SSL/TLS certificate.
|
||||
|
||||
**Cause:** KiCad does not trust self-signed SSL/TLS certificates.
|
||||
|
||||
**Solutions:**
|
||||
- Use HTTP instead of HTTPS for the `root_url` in your KiCad library configuration (only recommended for local networks)
|
||||
- Use a certificate from a trusted Certificate Authority (CA) like [Let's Encrypt](https://letsencrypt.org/)
|
||||
- Add your self-signed certificate to the system's trusted certificate store on the computer running KiCad (the exact steps depend on your operating system)
|
||||
|
||||
For more information about KiCad integration, see the [EDA / KiCad integration](../usage/eda_integration.md) documentation.
|
||||
|
||||
## Report Issue
|
||||
|
||||
If an error occurs, or you found a bug, please [open an issue on GitHub](https://github.com/Part-DB/Part-DB-symfony).
|
||||
If an error occurs, or you found a bug, please [open an issue on GitHub](https://github.com/Part-DB/Part-DB-server).
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ fulfilled by the official Part-DB docker image.*
|
|||
|
||||
Part-DB 2.0 requires at least PHP 8.2 (newer versions are recommended). So if your existing Part-DB installation is still
|
||||
running PHP 8.1, you will have to upgrade your PHP version first.
|
||||
The minimum required version of node.js is now 20.0 or newer, so if you are using 18.0, you will have to upgrade it too.
|
||||
The minimum required version of node.js is now 22.0 or newer, so if you are using 18.0, you will have to upgrade it too.
|
||||
|
||||
Most distributions should have the possibility to get backports for PHP 8.4 and modern nodejs, so you should be able to
|
||||
easily upgrade your system to the new requirements. Otherwise, you can use the official Part-DB docker image, which
|
||||
|
|
@ -27,7 +27,7 @@ about the requirements at all.
|
|||
|
||||
## Changes
|
||||
* Configuration is now preferably done via a web settings interface. You can still use environment variables, these overwrite
|
||||
the settings in the web interface. Existing configuration will still work, but you should consider migriting them to the
|
||||
the settings in the web interface. Existing configuration will still work, but you should consider migrating them to the
|
||||
web interface as described below.
|
||||
* The `config/banner.md` file that could been used to customize the banner text, was removed. You can now set the banner text
|
||||
directly in the admin interface, or by setting the `BANNER` environment variable. If you want to keep your existing
|
||||
|
|
@ -43,7 +43,7 @@ The upgrade process works very similar to a normal (minor release) upgrade.
|
|||
|
||||
### Direct installation
|
||||
|
||||
**Be sure to execute the following steps as the user that owns the Part-DB files (e.g. `www-data`, or your webserver user). So prepend a `sudo -u wwww-data` where necessary.**
|
||||
**Be sure to execute the following steps as the user that owns the Part-DB files (e.g. `www-data`, or your webserver user). So prepend a `sudo -u www-data` where necessary.**
|
||||
|
||||
1. Make a backup of your existing Part-DB installation, including the database, data directories and the configuration files and `.env.local` file.
|
||||
The `php bin/console partdb:backup` command can help you with this.
|
||||
|
|
@ -51,7 +51,7 @@ The `php bin/console partdb:backup` command can help you with this.
|
|||
3. Remove the `var/cache/` directory inside the Part-DB installation to ensure that no old cache files remain.
|
||||
4. Run `composer install --no-dev -o` to update the dependencies.
|
||||
5. Run `yarn install` and `yarn build` to update the frontend assets.
|
||||
6. Rund `php bin/console doctrine:migrations:migrate` to update the database schema.
|
||||
6. Run `php bin/console doctrine:migrations:migrate` to update the database schema.
|
||||
7. Clear the cache with `php bin/console cache:clear`.
|
||||
8. Open your Part-DB instance in the browser and log in as an admin user.
|
||||
9. Go to the user or group permissions page, and give yourself (and other administrators) the right to change system settings (under "System" and "Configuration").
|
||||
|
|
@ -60,6 +60,8 @@ The `php bin/console partdb:backup` command can help you with this.
|
|||
If you want to change them, you must migrate them to the settings interface as described below.
|
||||
|
||||
### Docker installation
|
||||
**When running the console commands from inside a docker container's shell as root, be sure to use `sudo -E` to preserve the environment variables, so that they are correctly passed to the command.**
|
||||
|
||||
1. Make a backup of your existing Part-DB installation, including the database, data directories and the configuration files and the file where you configure the docker environment variables.
|
||||
2. Stop the existing Part-DB container with `docker compose down`
|
||||
3. Ensure that your docker compose file uses the new latest images (either `latest` or `2` tag).
|
||||
|
|
@ -79,7 +81,7 @@ To change it, you must migrate your environment variable configuration to the ne
|
|||
|
||||
For this there is the new console command `settings:migrate-env-to-settings`, which reads in all environment variables used to overwrite
|
||||
settings and write them to the database, so that you can safely delete them from your environment variable configuration afterwards, without
|
||||
loosing your configuration.
|
||||
losing your configuration.
|
||||
|
||||
To run the command, execute `php bin/console settings:migrate-env-to-settings --all` as webserver user (or run `docker exec --user=www-data -it partdb php bin/console settings:migrate-env-to-settings --all` for docker containers).
|
||||
It will list you all environment variables, it found and ask you for confirmation to migrate them. Answer with `yes` to migrate them and hit enter.
|
||||
|
|
|
|||
|
|
@ -5,5 +5,7 @@ nav_order: 7
|
|||
has_children: true
|
||||
---
|
||||
|
||||
# Upgrade
|
||||
|
||||
This section provides information on how to upgrade Part-DB to the latest version.
|
||||
This is intended for major release upgrades, where requirements or things changes significantly.
|
||||
This is intended for major release upgrades, where requirements or things change significantly.
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue